Symbol SDK v3を使う機会が出てきそうなので自分用のメモとして逆引きリファレンス形式で使い方をまとめていきます。
今回はアドレスまわりのコードをまとめています。
この記事の概要
- ある程度TS/ESMに慣れた人向けで前提条件などは省いています。
-
v3.3.0に準拠しています。特にv3.1.x系とv3.2.0には破壊的変更が多く、互換性がない場合があります。 - 9月のアップデートにより、
v3.2.0では一部のトランザクションが非互換となっています。
⚠️ 注意事項
この記事で使用している秘密鍵・ニーモニックは学習・テスト目的のサンプルです。
- 絶対にメインネットで使用しないでください
- これらの鍵を使用したアドレスに送金すると、誰でもアクセス可能な状態となり資産を失う可能性があります
- 実際の運用では必ず新規生成した秘密鍵を使用し、厳重に管理してください
- 本記事のコードを使用して発生したいかなる損害についても、筆者は一切の責任を負いません
アカウント
前提条件
このセクションのすべてのコード例では、以下のセットアップを前提としています。
import { Bip32, PrivateKey, PublicKey } from 'symbol-sdk';
import { KeyPair } from 'symbol-sdk/symbol';
import { Address } from 'symbol-sdk/symbol';
import { SymbolFacade } from 'symbol-sdk/symbol';
import { models } from 'symbol-sdk/symbol';
// "testnet" / "mainnet" の文字列でFacadeを生成できる
const facade = new SymbolFacade('testnet');
// テスト用の各種値
const PRIVATE_KEY_HEX = 'EDB671EB741BD676969D8A035271D1EE5E75DF33278083D877F23615EB839FEC';
const PUBLIC_KEY_HEX = '87DA603E7BE5656C45692D5FC7F6D0EF8F24BB7A5C10ED5FDA8C5CFBC49FCBC8';
const TESTNET_ADDRESS = 'TD4WXUXYAPPB5Y42VT6FHISG6T32I2IBUXIKKPQ';
const BIP39_MNEMONIC_24_WORDS = Array(23).fill('abandon').concat(['art']).join(' ');
秘密鍵を含むアカウント
秘密鍵を含むアカウント周りのコード
秘密鍵を生成する(hex 64文字 / 0埋めではない)
const privateKey = PrivateKey.random();
// 32 bytes = 256-bit
console.log('privateKey.bytes.length', privateKey.bytes.length);
// => privateKey.bytes.length 32
const privateKeyHex = privateKey.toString();
console.log('privateKeyHex', privateKeyHex);
// => privateKeyHex A1B2C3D4E5F6... (64文字のランダムな16進数文字列)
console.log('is not all zeros', privateKeyHex !== '0'.repeat(64));
// => is not all zeros true
秘密鍵からアカウントを生成する(公開鍵・アドレスを得る)
const privateKey = PrivateKey.random();
const account = facade.createAccount(privateKey);
console.log('publicKey', account.publicKey.toString());
// => publicKey 1234567890ABCDEF... (64文字の公開鍵)
const addressRaw = account.address.toString();
console.log('address', addressRaw);
// => address TABCDEFGHIJKLMNOPQRSTUVWXYZ234567890
console.log('address.length', addressRaw.length);
// => address.length 39
console.log('starts with T', addressRaw.startsWith('T'));
// => starts with T true
秘密鍵から公開鍵を導出する
const privateKey = new PrivateKey(PRIVATE_KEY_HEX);
const keyPair = new KeyPair(privateKey);
const publicKeyHex = keyPair.publicKey.toString();
console.log('privateKey', privateKey.toString());
// => privateKey EDB671EB741BD676969D8A035271D1EE5E75DF33278083D877F23615EB839FEC
console.log('publicKey', publicKeyHex);
// => publicKey 87DA603E7BE5656C45692D5FC7F6D0EF8F24BB7A5C10ED5FDA8C5CFBC49FCBC8
秘密鍵からアドレスを導出する
const privateKey = new PrivateKey(PRIVATE_KEY_HEX);
const account = facade.createAccount(privateKey);
const addressRaw = account.address.toString();
console.log('privateKey', privateKey.toString());
// => privateKey EDB671EB741BD676969D8A035271D1EE5E75DF33278083D877F23615EB839FEC
console.log('address', addressRaw);
// => address TD4WXUXYAPPB5Y42VT6FHISG6T32I2IBUXIKKPQ
秘密鍵を含まないアカウント
秘密鍵を含まないアカウント関連のコード
公開鍵からアドレスを生成する
const publicKey = new PublicKey(PUBLIC_KEY_HEX);
const address = facade.network.publicKeyToAddress(publicKey);
const addressRaw = address.toString();
console.log('publicKey', publicKey.toString());
// => publicKey 87DA603E7BE5656C45692D5FC7F6D0EF8F24BB7A5C10ED5FDA8C5CFBC49FCBC8
console.log('address', addressRaw);
// => address TD4WXUXYAPPB5Y42VT6FHISG6T32I2IBUXIKKPQ
console.log('isValidAddress', facade.network.isValidAddress(address));
// => isValidAddress true
アドレス(文字列)からAddressクラスを生成する
const address = new Address(TESTNET_ADDRESS);
console.log('address', address.toString());
// => address TD4WXUXYAPPB5Y42VT6FHISG6T32I2IBUXIKKPQ
console.log('address.bytes.length', address.bytes.length);
// => address.bytes.length 24
console.log('isValidAddressString', facade.network.isValidAddressString(address.toString()));
// => isValidAddressString true
アドレスを plain / pretty 形式で出力する
const toPrettyAddress = (plain: string) => plain.match(/.{1,6}/g)!.join('-');
const address = new Address(TESTNET_ADDRESS);
const plain = address.toString();
const pretty = toPrettyAddress(plain);
console.log('address(plain)', plain);
// => address(plain) TD4WXUXYAPPB5Y42VT6FHISG6T32I2IBUXIKKPQ
console.log('address(pretty)', pretty);
// => address(pretty) TD4WXU-XYAPPB-5Y42VT-6FHISG-6T32I2-IBUXIK-KPQ
アドレスの妥当性を検証する(改ざん / ネットワーク違い)
// 正常系: testnetのアドレスはtestnetで有効
console.log('valid address', facade.network.isValidAddressString(TESTNET_ADDRESS));
// => valid address true
応用
// 改ざん: base32としては成立するがチェックサムが壊れるケース
const tampered = `${TESTNET_ADDRESS.slice(0, -1)}A`;
console.log('address(valid)', TESTNET_ADDRESS);
// => address(valid) TD4WXUXYAPPB5Y42VT6FHISG6T32I2IBUXIKKPQ
console.log('address(tampered)', tampered);
// => address(tampered) TD4WXUXYAPPB5Y42VT6FHISG6T32I2IBUXIKKA
console.log('tampered.length', tampered.length);
// => tampered.length 39
console.log('tampered is invalid', facade.network.isValidAddressString(tampered));
// => tampered is invalid false
// ネットワーク違い: mainnet facade では testnet のアドレスは無効
const mainnetFacade = new SymbolFacade('mainnet');
console.log('testnet address is invalid on mainnet', mainnetFacade.network.isValidAddressString(TESTNET_ADDRESS));
// => testnet address is invalid on mainnet false
// 参考: 同一公開鍵から mainnet/testnet で別アドレスが生成される
const publicKey = new PublicKey(PUBLIC_KEY_HEX);
const mainnetAddress = mainnetFacade.network.publicKeyToAddress(publicKey).toString();
const testnetAddress = facade.network.publicKeyToAddress(publicKey).toString();
console.log('address(mainnet)', mainnetAddress);
// => address(mainnet) ND4WXUXYAPPB5Y42VT6FHISG6T32I2IBUXJLMPA
console.log('address(testnet)', testnetAddress);
// => address(testnet) TD4WXUXYAPPB5Y42VT6FHISG6T32I2IBUXIKKPQ
console.log('addresses are different', mainnetAddress !== testnetAddress);
// => addresses are different true
decoded address hex(REST形式)をAddressへ変換する
// RESTのdecodedAddressは、base32(39文字)ではなく「生bytesのhex」になっていることが多い
const address = new Address(TESTNET_ADDRESS);
const decodedHex = Buffer.from(address.bytes).toString('hex').toUpperCase();
console.log('address(plain)', address.toString());
// => address(plain) TD4WXUXYAPPB5Y42VT6FHISG6T32I2IBUXIKKPQ
console.log('address(decodedHex)', decodedHex);
// => address(decodedHex) 983C63C5C00FA0A0EB7C2B8F07EA4EFEB85B4B23B2E22E2A
const reconstructed = Address.fromDecodedAddressHexString(decodedHex);
console.log('reconstructed address', reconstructed.toString());
// => reconstructed address TD4WXUXYAPPB5Y42VT6FHISG6T32I2IBUXIKKPQ
console.log('reconstructed decodedHex', Buffer.from(reconstructed.bytes).toString('hex').toUpperCase());
// => reconstructed decodedHex 983C63C5C00FA0A0EB7C2B8F07EA4EFEB85B4B23B2E22E2A
ネームスペースエイリアス(アドレス)がエイリアスか判定する
const normalAddress = new Address(TESTNET_ADDRESS);
console.log('normal address toNamespaceId', normalAddress.toNamespaceId());
// => normal address toNamespaceId undefined
const namespaceId = new models.NamespaceId(0x84b3552d375ffa4bn);
const aliasAddress = Address.fromNamespaceId(namespaceId, facade.network.identifier);
console.log('address(normal)', normalAddress.toString());
// => address(normal) TD4WXUXYAPPB5Y42VT6FHISG6T32I2IBUXIKKPQ
console.log('address(alias)', aliasAddress.toString());
// => address(alias) T99UFEBX7DWLJBSJV2AAMQ5AAD3TBAQ
const extracted = aliasAddress.toNamespaceId();
console.log('extracted is defined', extracted !== undefined);
// => extracted is defined true
console.log('extracted value matches', extracted!.value === namespaceId.value);
// => extracted value matches true
ニーモニック周り
BIP39ニーモニック(一般に24語/12語など)を扱うコード
ゼロから新しいBIP39ニーモニックを生成する
const bip32 = new Bip32(SymbolFacade.BIP32_CURVE_NAME, 'english');
const mnemonic = bip32.random(); // デフォルト seedLength=32 => 24 words
console.log('mnemonic(random)', mnemonic);
// => mnemonic(random) abandon abandon abandon ... art (24語のランダムなニーモニック)
const words = mnemonic.trim().split(/\s+/);
console.log('words.length', words.length);
// => words.length 24
// 生成したニーモニックが実際に使えること(fromMnemonicできること)を確認
const root = bip32.fromMnemonic(mnemonic, '');
const childPrivateKey0 = root.derivePath(facade.bip32Path(0)).privateKey.toString();
console.log('childPrivateKey0', childPrivateKey0);
// => childPrivateKey0 A1B2C3D4E5F6... (64文字の秘密鍵)
console.log('is not all zeros', childPrivateKey0 !== '0'.repeat(64));
// => is not all zeros true
BIP39ニーモニック(24語)から子秘密鍵を導出する
// symbol-sdkのBip32は curveName と language を指定して使う
const bip32 = new Bip32(SymbolFacade.BIP32_CURVE_NAME, 'english');
const root = bip32.fromMnemonic(BIP39_MNEMONIC_24_WORDS, '');
const childPrivateKey0 = root.derivePath(facade.bip32Path(0)).privateKey.toString();
console.log('mnemonic', BIP39_MNEMONIC_24_WORDS);
// => mnemonic abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art
console.log('childPrivateKey(0)', childPrivateKey0);
// => childPrivateKey(0) 99DA0B339E5C3E3DDDD59678B52A7C7E5F9E02BD07AF4E220CD69228766BCDDB
HDウォレットで次の子秘密鍵を導出する(height 0/1)
const bip32 = new Bip32(SymbolFacade.BIP32_CURVE_NAME, 'english');
const root = bip32.fromMnemonic(BIP39_MNEMONIC_24_WORDS, '');
const childPrivateKey0 = root.derivePath(facade.bip32Path(0)).privateKey.toString();
const childPrivateKey1 = root.derivePath(facade.bip32Path(1)).privateKey.toString();
console.log('mnemonic', BIP39_MNEMONIC_24_WORDS);
// => mnemonic abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art
console.log('childPrivateKey(0)', childPrivateKey0);
// => childPrivateKey(0) 99DA0B339E5C3E3DDDD59678B52A7C7E5F9E02BD07AF4E220CD69228766BCDDB
console.log('childPrivateKey(1)', childPrivateKey1);
// => childPrivateKey(1) 52F4459104AC7D749049AB48FD3A820E5C865D052223040875B955B427D11A51
// "次" であること(重複しないこと)も最低限確認
console.log('keys are different', childPrivateKey0 !== childPrivateKey1);
// => keys are different true
まとめ
アカウント周りで実用的なコードは以上かなと思います。
他に思いついたら追記していきます。
明日はもう一歩踏み込んでトランザクションなどをやっていきたいと思います。 書けたら。