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