1
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?

ReactNative+ExpoGo+SymbolSDKv3

Posted at
  • expo v53.0.6
  • symbol-sdk v3.2.3

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

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 とポリフィルをインストールします。

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 を編集します。
コードが存在するだけでエラーになるため、以下の部分を削除します。

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 ファイルを格納します。

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);
  };
}
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 を読み込むようにします。

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;

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

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

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",
  },
});
1
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
1
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?