7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

セキュリティ上の理由から、実運用には適しません。
本記事の暗号化処理はJavaScriptによる実装であり、安全性が十分に担保されていないためです。

Expo 環境セットアップ

まずプロジェクトを作成しましょう。

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

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

cd symbol-expo-sample

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

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

そのまま起動して、動作確認を行いましょう。

npx expo start --tunnel

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

Android:

iOS:

Symbol SDK 向けのカスタマイズ

Symbol-SDKにパッチを適用するため、patch-packageをインストールします。

npm i -D patch-package

package.jsonscriptspostinstall を追加します。

package.json
    "scripts": {
      "start": "expo start",
      "android": "expo start --android",
      "ios": "expo start --ios",
-     "web": "expo start --web"
+     "web": "expo start --web",
+     "postinstall": "patch-package"
    },

patches ディレクトリを作成し、パッチファイル symbol-sdk+3.3.0.patch を配置します。

mkdir -p patches
cd patches
curl -OL https://github.com/nemnesia/symbol-sdk-non-node-patch/releases/download/v3.3.0/symbol-sdk+3.3.0.patch
cd -

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

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/metro-config

パッチが自動的に適用されなかった場合は、以下のコマンドを実行してください。

npm run postinstall

shim の追加

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

shims/shim.js
import { getRandomBytes } from "expo-crypto";
import { Buffer } from "buffer";

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

if (typeof global.process.browser === "undefined") {
  global.process.browser = true;
}

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

if (!global.window.crypto) {
  global.window.crypto = {
    getRandomValues: (arr) => {
      const bytes = getRandomBytes(arr.length);
      for (let i = 0; i < arr.length; i++) arr[i] = bytes[i];
      return arr;
    },
  };
}
shims/crypto.js
import { utf8ToBytes, bytesToHex } from "@noble/hashes/utils.js";
import { hmac as nobleHmac } from "@noble/hashes/hmac.js";
import { sha256, sha512 } from "@noble/hashes/sha2.js";
import { getRandomBytes } from "expo-crypto";
import { Buffer } from "buffer";

function concatMany(chunks) {
  if (!chunks.length) return new Uint8Array(0);
  let total = 0;
  for (const c of chunks) total += c.length;
  const out = new Uint8Array(total);
  let off = 0;
  for (const c of chunks) {
    out.set(c, off);
    off += c.length;
  }
  return out;
}

module.exports = {
  randomBytes: (size) => {
    if (!Number.isInteger(size) || size < 1) {
      throw new TypeError("randomBytes: size must be a positive integer");
    }

    if (size > 67108864) {
      throw new RangeError("randomBytes: size too large (max 64MB)");
    }

    let bytes;
    let retryCount = 0;
    const maxRetries = 3;

    while (retryCount < maxRetries) {
      try {
        bytes = getRandomBytes(size);

        if (bytes.every((byte) => byte === 0)) {
          throw new Error("PRNG failure: all zeros detected");
        }

        const uniqueBytes = new Set(bytes);
        if (uniqueBytes.size < Math.min(size, 4)) {
          throw new Error("PRNG failure: insufficient entropy detected");
        }

        break;
      } catch (e) {
        retryCount++;
        if (retryCount >= maxRetries) {
          throw new Error(
            `randomBytes: failed to generate secure random bytes after ${maxRetries} attempts. ${e.message}`
          );
        }
        continue;
      }
    }

    if (!bytes || bytes.length !== size) {
      throw new Error("randomBytes: generated bytes size mismatch");
    }

    const buffer = Buffer.from(bytes);

    try {
      bytes.fill(0);
      if (bytes.buffer) {
        new Uint8Array(bytes.buffer).fill(0);
      }
    } catch (e) {
      // ignore cleanup errors
    }

    return buffer;
  },

  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 _dataChunks = [];

    const hmacInstance = {
      update: (data) => {
        const bytes = typeof data === "string" ? encoder.encode(data) : data;
        _dataChunks.push(bytes);
        return hmacInstance;
      },
      digest: (encoding) => {
        const allData = concatMany(_dataChunks);
        const mac = nobleHmac(algo, _key, allData);
        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 = concatMany(data);
        const hashed = hashFn(all);

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

    return hashInstance;
  },
};

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

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 を新規作成します。

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;

動作確認

以下の動作ができるか確認しましょう。

  • ニーモニック生成
  • アカウント生成
  • トランザクション生成
App.tsx
import { StatusBar } from "expo-status-bar";
import { useEffect, useState } from "react";
import { StyleSheet, Text, View } from "react-native";
import { Bip32 } from "symbol-sdk";
import {
  descriptors,
  generateMosaicAliasId,
  models,
  Network,
  SymbolAccount,
  SymbolFacade,
  SymbolTransactionFactory,
} from "symbol-sdk/symbol";

export default function App() {
  const [address, setAddress] = useState("");
  const [publicKey, setPublicKey] = useState("");
  const [txPayload, setTxPayload] = useState("");

  useEffect(() => {
    // facade生成
    const facade = new SymbolFacade(Network.TESTNET);

    const genAccount = (id: number): SymbolAccount => {
      const bip32Path = facade.bip32Path(id); // Bip32Path生成
      const childBip32Node = bip32Node.derivePath(bip32Path); // Bip32Pathから子Bip32Path生成
      const keyPair = SymbolFacade.bip32NodeToKeyPair(childBip32Node); // 子Bip32Pathからキーペア生成
      const account = new SymbolAccount(facade, keyPair); // キーペアからアカウント生成
      console.log(`Account PrivateKey: ${account.keyPair.privateKey.toString()}`);
      console.log(`Account PublicKey : ${account.publicKey.toString()}`);
      console.log(`Account Address   : ${account.address.toString()}`);
      return account;
    };

    // ニーモニック生成
    const bip32 = new Bip32();
    const mnemonic = bip32.random();
    console.log("Mnemonic:", mnemonic);
    // Bip32Node生成
    const passwd = "";
    const bip32Node = bip32.fromMnemonic(mnemonic, passwd);
    // アカウントを生成する
    const account = genAccount(0);
    setAddress(account.address.toString());
    setPublicKey(account.publicKey.toString());

    // _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
    // _/ トランザクション生成
    // _/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/

    // 受信者アカウント生成
    const recipientAccount = genAccount(1);

    // 転送モザイク設定(ネームスペースからモザイクIDを解決)
    const mosaicId = generateMosaicAliasId("symbol.xym");
    const mosaics = [
      new descriptors.UnresolvedMosaicDescriptor(
        new models.UnresolvedMosaicId(mosaicId),
        new models.Amount(10_000000n)
      ),
    ];
    // 平文メッセージ
    const message = new TextEncoder().encode("\0Hello, Symbol!!");
    // 転送トランザクションディスクリプタ生成
    const transferTxDescriptor =
      new descriptors.TransferTransactionV1Descriptor(
        recipientAccount.address, // 受信者アドレス
        mosaics, // 転送モザイク
        message // メッセージ
      );
    // 転送トランザクション生成
    const transferTx = facade.createTransactionFromTypedDescriptor(
      transferTxDescriptor, // トランザクションディスクリプタ
      account.publicKey, // 送信者公開鍵
      100, // 手数料係数
      60 * 60 * 2 // 有効期限(秒)
    );
    // 署名
    const sig = account.signTransaction(transferTx);
    const payloadJsonString = SymbolTransactionFactory.attachSignature(
      transferTx,
      sig
    );
    setTxPayload(JSON.parse(payloadJsonString).payload);
    console.log("TxPayload:", JSON.stringify(transferTx.toJson(), null, 2));
  }, []);

  return (
    <View style={styles.container}>
      <Text style={styles.label}>Address:</Text>
      <Text style={styles.value}>{address}</Text>
      <Text style={styles.label}>PublicKey:</Text>
      <Text style={styles.value}>{publicKey}</Text>
      <Text style={styles.label}>TxPayload:</Text>
      <Text style={styles.value}>{txPayload}</Text>
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "flex-start",
    justifyContent: "flex-start",
    padding: 16,
  },
  label: {
    fontWeight: "bold",
    marginTop: 8,
  },
  value: {
    width: "100%",
    flexWrap: "wrap",
    color: "#333",
    marginBottom: 4,
  },
});
npx expo start --tunnel

アカウントアドレスとトランザクションペイロードが表示されていれば成功です。

7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?