ReactNative+ExpoGo+SymbolSDKv3


- expo v53.0.6
- symbol-sdk v3.2.3

:::note warn
ニーモニックの生成やメッセージ暗号化部分については、まだ十分に確認できていません。  
NEM に関しても同様で、動作確認が必要です。  
もし試してみた方がいれば、フィードバックをいただけると助かります！
:::

## Expo 環境セットアップ

まずはプロジェクトを作成します。

```bash
npx create-expo-app@latest symbol-expo-sample --template blank-typescript
```

作成が完了したら、プロジェクトディレクトリに移動します。

```bash
cd symbol-expo-sample
```

Web 対応もしておきましょう。

```bash
npx expo install react-dom react-native-web @expo/metro-runtime
```

そのまま起動して動作確認をします。

```bash
npx expo start --tunnel
```

スマホやブラウザで表示できることを確認してください。

Android:

https://play.google.com/store/apps/details?id=host.exp.exponent&hl=ja

iOS:

https://apps.apple.com/jp/app/expo-go/id982107779

## Symbol SDK 向けのカスタマイズ

Symbol SDK とポリフィルをインストールします。

```bash
npm i symbol-sdk @noble/hashes @noble/ciphers buffer assert stream readable-stream text-encoding react-native-url-polyfill
npx expo install expo-crypto expo-random @expo/metro-config
```

### Symbol SDK の WASM 参照部分を削除

`node_modules/symbol-sdk/src/impl/ed25519.js` を編集します。
コードが存在するだけでエラーになるため、以下の部分を削除します。

```diff_javascript:node_modules/symbol-sdk/src/impl/ed25519.js
// this file contains implementation details and is not intended to be used directly

import ed25519_js from './ed25519_js.js';
- import ed25519_wasm from './ed25519_wasm.js';

let ed25519;
export default {
 get: () => {
  // 1. certain environments, like ReactNative, do not support WebAssembly
  //    in those cases, default to JS-implementation
  // 2. for testing, check environment variable to force JS-implementation
  if (!ed25519)
-   ed25519 = globalThis.WebAssembly && !process.env.SYMBOL_SDK_NO_WASM ? ed25519_wasm : ed25519_js;
+   ed25519 = ed25519_js;

  return ed25519;
 },
 unload: () => {
  ed25519 = undefined;
 }
};
```

### shim の追加

`shims` ディレクトリを作成し、以下の 2 ファイルを格納します。

```javascript:shims/shim.js
import { Readable } from "stream";
import { Buffer } from "buffer";

if (!global.Buffer) {
  global.Buffer = Buffer;
}

if (!global.assert) {
  global.assert = require("assert");
}

global.Readable = Readable;

if (!Uint8Array.prototype.copy) {
  Uint8Array.prototype.copy = function (
    target,
    targetStart = 0,
    sourceStart = 0,
    sourceEnd = this.length
  ) {
    target.set(this.subarray(sourceStart, sourceEnd), targetStart);
  };
}
```

```javascript:shims/crypto.js
import { utf8ToBytes, bytesToHex } from "@noble/hashes/utils";
import { hmac } from "@noble/hashes/hmac";
import { sha256, sha512 } from "@noble/hashes/sha2";
import { TextEncoder } from "text-encoding";
import { gcm } from "@noble/ciphers/aes";
import { getRandomBytes } from "expo-crypto";

function concatArrays(a, b) {
  const result = new Uint8Array(a.length + b.length);
  result.set(a, 0);
  result.set(b, a.length);
  return result;
}

module.exports = {
  randomBytes: (size) => getRandomBytes(size),

  createHmac: (algorithm, key) => {
    let algo;
    if (algorithm === "sha256") {
      algo = sha256;
    } else if (algorithm === "sha512") {
      algo = sha512;
    } else {
      throw new Error("Only sha256 and sha512 are supported in shim.");
    }

    const encoder = new TextEncoder();
    const _key = typeof key === "string" ? encoder.encode(key) : key;
    let _data = new Uint8Array();

    const hmacInstance = {
      update: (data) => {
        _data = typeof data === "string" ? encoder.encode(data) : data;
        return hmacInstance;
      },
      digest: (encoding) => {
        const mac = hmac(algo, _key, _data);
        if (encoding === "hex") {
          return bytesToHex(mac);
        }
        return mac;
      },
    };

    return hmacInstance;
  },

  createHash: (algo) => {
    const hashFn =
      algo === "sha256" ? sha256 : algo === "sha512" ? sha512 : null;
    if (!hashFn) throw new Error(`Unsupported hash algorithm: ${algo}`);

    let data = [];

    const hashInstance = {
      update: (chunk) => {
        data.push(typeof chunk === "string" ? utf8ToBytes(chunk) : chunk);
        return hashInstance;
      },
      digest: (encoding = "hex") => {
        const all = Uint8Array.from(data.flatMap((d) => Array.from(d)));
        const hashed = hashFn(all);

        if (encoding === "hex") {
          return bytesToHex(hashed);
        }
        return hashed;
      },
    };

    return hashInstance;
  },

  createCipheriv: (algo, key, iv) => {
    if (algo !== "aes-256-gcm") {
      throw new Error("Only aes-256-gcm is supported.");
    }
    const aes = gcm(key, iv);

    let _authTag = undefined;
    let _cipherText = undefined;

    const cipherInstance = {
      update: (cipherText) => {
        const bytes =
          typeof cipherText === "string" ? utf8ToBytes(cipherText) : cipherText;
        const encrypted = aes.encrypt(bytes);

        _authTag = encrypted.slice(-16);
        _cipherText = encrypted.slice(0, -16);

        return _cipherText;
      },
      getAuthTag: () => {
        if (!_authTag) {
          throw new Error("AuthTag not available. Call update first.");
        }
        return _authTag;
      },
      final: () => Buffer.alloc(0),
    };

    return cipherInstance;
  },

  createDecipheriv: (algo, key, iv) => {
    if (algo !== "aes-256-gcm") {
      throw new Error("Only aes-256-gcm is supported.");
    }
    const aes = gcm(key, iv);

    let _authTag = undefined;
    let _cipherText = undefined;

    return {
      update: (cipherText) => {
        _cipherText = cipherText;
        if (!_cipherText || !_authTag) {
          throw new Error("Ciphertext or AuthTag missing");
        }
        const fullEncrypted = concatArrays(_cipherText, _authTag);
        return aes.decrypt(fullEncrypted);
      },
      setAuthTag: (authTag) => {
        _authTag = authTag;
      },
      final: () => {
        return null;
      },
    };
  },
};
```

アプリ起動時に `./shims/shim` を読み込むようにします。

```typescript:index.ts
import "./shims/shim";
import { registerRootComponent } from "expo";

import App from "./App";

// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(App);
```

### Metro コンフィグの設定

プロジェクトのルートに `metro.config.js` を作成します。

```javascript:metro.config.js
const { getDefaultConfig } = require("@expo/metro-config");

const config = getDefaultConfig(__dirname);

config.resolver.extraNodeModules = {
  ...config.resolver.extraNodeModules,
  crypto: require.resolve("./shims/crypto.js"),
  url: require.resolve("react-native-url-polyfill"),
};

module.exports = config;
```

## Symbol SDK を使ったトランザクションのアナウンス

セットアップが完了したので、簡単なトランザクションを発行してみましょう。
以下は、ボタンを押すと Alice から Bob に 1xym を送信する例です。

```typescript:App.tsx
import { StatusBar } from "expo-status-bar";
import { Button, StyleSheet, Text, View } from "react-native";
import { PrivateKey, PublicKey } from "symbol-sdk";
import { descriptors, models, Network, SymbolFacade } from "symbol-sdk/symbol";

const NODE = "https://t.sakia.harvestasya.com:3001";
const ALICE_PRIVATEKEY =
  "****************************************************************";
const BOB_PUBLICKEY =
  "****************************************************************";

export default function App() {
  const handlePress = async () => {
    const facade = new SymbolFacade(Network.TESTNET);
    const alice = facade.createAccount(new PrivateKey(ALICE_PRIVATEKEY));
    const bob = facade.createPublicAccount(new PublicKey(BOB_PUBLICKEY));
    const messageData = "\0Hello, Symbol!";
    const transferTxDescriptor =
      new descriptors.TransferTransactionV1Descriptor(
        bob.address,
        [
          new descriptors.UnresolvedMosaicDescriptor(
            new models.UnresolvedMosaicId(0x72c0212e67a08bcen),
            new models.Amount(1_000000n)
          ),
        ],
        messageData
      );
    const transferTx = facade.createTransactionFromTypedDescriptor(
      transferTxDescriptor,
      alice.publicKey,
      100,
      60 * 60 * 2
    );
    const sig = alice.signTransaction(transferTx);
    const jsonPayload = facade.transactionFactory.static.attachSignature(
      transferTx,
      sig
    );
    const res = await fetch(new URL("/transactions", NODE), {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: jsonPayload,
    })
      .then((res) => res.json())
      .then((json) => {
        return json;
      });
    console.log(res);
  };

  return (
    <View style={styles.container}>
      <Button title="送信" onPress={handlePress} />
      <Text>Open up App.tsx to start working on your app!</Text>
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
});
```