以下私の個人的なメモであり、詰まった変換処理等を追記していきます。なお、本記事では Javascript または Python SDK を使ったケースと使わなかったケースも掲載しています。歯抜けの所は該当箇所を該当パターンで書くことがありましたら更新致します。
基本的には SDK を使える時は SDK を利用する事を推奨致しますが、例えば Symbol 関連の処理をするべきコード量が極端に少ないケースや、SDK を動作させる事が困難なケースもありますため、そういったケースでは本記事に記載の SDK を使わなかったケースも参考にしてください。
hex address を base32 address へ変換する
Symbol で https://node-url:3001/transactions/confirmed
で検索した際に、recipientAddress
等のアドレスは以下のような hex 形式で出力されます。
9850A2A43114D0E1B4F772597E3BD92D78DF96CAE886B726
本来 Symbol Address は binary/uint8 array で表現され、 Node の MongoDB へ保存される際には hex 形式で格納されます。REST からのレスポンス時にはそのままこの hex 形式で返却されます。
こちらを Base32 へ変換すると次のように Wallet でよく見かける形式になります。
TBIKFJBRCTIODNHXOJMX4O6ZFV4N7FWK5CDLOJQ
以下に変換手順を記します。
Symbol Python SDK v3 の利用時
from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.sc import UnresolvedAddress
address = "9850A2A43114D0E1B4F772597E3BD92D78DF96CAE886B726"
facade = SymbolFacade("testnet")
plain_address = facade.Address(UnresolvedAddress(address).bytes)
print(plain_address)
Symbol Javascript SDK v3 利用時
hex -> base32
import symbolSdk from 'symbol-sdk';
const uint8 = symbolSdk.utils.hexToUint8(unresolvedAddress);
const plainAddress = new symbolSdk.facade.SymbolFacade.Address(uint8).toString();
console.log(plainAddress);
base32 -> hex
import symbolSdk from "symbol-sdk";
const address = new symbolSdk.symbol.Address("NC*********...");
console.log(symbolSdk.utils.uint8ToHex(address.bytes));
SDK を使わずに Python 標準モジュール利用時
from binascii import unhexlify
import base64
address = "9850A2A43114D0E1B4F772597E3BD92D78DF96CAE886B726"
raw_bytes: bytes = unhexlify(address)
plain_address = base64.b32encode(raw_bytes + bytes(0)).decode("utf8")[0:-1]
print(plain_address)
SDK を使わずに Javascript 実行時
/** @param {string} hex */
function hexToBase32(hex) {
// hex to bytes
const bytesArray = [];
for (var i = 0; i < unresolvedAddress.length; i += 2) {
bytesArray.push(parseInt(unresolvedAddress.substr(i, 2), 16));
}
const uint8Array = new Uint8Array(bytesArray);
// base32 encode
const base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
let bits = 0;
let value = 0;
let base32 = "";
for (var i = 0; i < uint8Array.length; i++) {
value = (value << 8) | uint8Array[i];
bits += 8;
while (bits >= 5) {
base32 += base32Chars[(value >>> (bits - 5)) & 0x1f];
bits -= 5;
}
}
if (bits > 0) {
base32 += base32Chars[(value << (5 - bits)) & 0x1f];
}
return base32;
}
const hexAddress = "9850A2A43114D0E1B4F772597E3BD92D78DF96CAE886B726";
console.log(hexToBase32(address));
アドレスの種類
Symbol では一言でアドレスといっても幾つかの表現があります。Node内ではアドレスは binary/uint8 array で取り扱われ、REST からの返却時はそれを hex として出力、クライアントサイドでは人が見やすいように Base32 で表示しています。
また、上記はデータ形式であり、アドレスの状態によって Unresolved Address または Resolved Address とも表現することがあります。 Symbol のアドレスや Mosaic ID は namespace として入力される可能性があるため、Catapult では resolver が受け取った文字列を一旦 namespace へ照会し、受け取った文字列が Address 形式であるか namespace であるか判別する必要があります。よって、この namespace 照会前の状態は Unresolved Address と表現され、照会後(解決後)は Resolved Address となります。
その他、アドレスの詳細な仕様はドキュメントを参照して下さい。
[暗号理論 - アドレス]
https://docs.symbol.dev/ja/concepts/cryptography.html#address
公開鍵より Base32 Address を生成する
以下のような公開鍵をアドレスに変換します。
公開鍵
1FB72997EDE77DB9595CA4CD7405A43D2FDB370AD3CDE192F01FA24C5EBC3242
アドレス
TDZYUHKKLQRFZWKQ4WPXF27DNP2ZPDBTYIEYJOI
Symbol Python SDK v3 利用時
from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.sc import PublicKey
public_key = "1FB72997EDE77DB9595CA4CD7405A43D2FDB370AD3CDE192F01FA24C5EBC3242"
facade = SymbolFacade("testnet")
address = facade.network.public_key_to_address(PublicKey(public_key))
print(address)
Symbol Javascript SDK v3 利用時
import symbolSdk from 'symbol-sdk';
const publicKey = "1FB72997EDE77DB9595CA4CD7405A43D2FDB370AD3CDE192F01FA24C5EBC3242"
const facade = new symbolSdk.facade.SymbolFacade("testnet");
const address = facade.network.publicKeyToAddress(new symbolSdk.PublicKey(publicKey));
console.log(address.toString());
SDK を使わずに Python で公開鍵からアドレスを取得する
こちらは blockchain node からの返り値より取得するのがお手軽です。ノードからの返り値のアドレス形式は UnresolvedAddress になっている為、更に変換が必要な点は注意して下さい。
import requests
public_key = "1FB72997EDE77DB9595CA4CD7405A43D2FDB370AD3CDE192F01FA24C5EBC3242"
url = f"https://symbolnode.blockchain-authn.app:3001/accounts/{public_key}"
headers = {"Content-Type": "application/json"}
response = requests.get(url, headers=headers)
res = response.json()
print(res['account']['address'])
node も SDK も使わない場合は次の通りです。
import hashlib
import base64
from binascii import unhexlify
from typing import Literal
from typing_extensions import Buffer
def ripemd160(data: Buffer) -> bytes:
"""Calculates RIPEMD-160 hash of data."""
builder: hashlib._Hash = hashlib.new("ripemd160")
builder.update(data)
return builder.digest()
NETWORK = {
"mainnet": 0x68,
"testnet": 0x98,
}
def public_key_to_address(public_key: str, network: Literal["mainnet", "testnet"]):
"""Converts a public key to an address."""
public_key_bytes: bytes = unhexlify(public_key)
if len(public_key_bytes) != 32:
raise ValueError("Public key is malformed.")
if network != "mainnet" and network != "testnet":
raise ValueError("Network is malformed.")
part_one_hash_builder: hashlib._Hash = hashlib.sha3_256()
part_one_hash_builder.update(public_key_bytes)
part_one_hash: bytes = part_one_hash_builder.digest()
part_two_hash: bytes = ripemd160(part_one_hash)
version: bytes = bytes([NETWORK[network]]) + part_two_hash
part_three_hash_builder = hashlib.sha3_256()
part_three_hash_builder.update(version)
bytes_address: bytes = version + part_three_hash_builder.digest()[0:3]
return base64.b32encode(bytes_address + bytes(0)).decode("utf8")[0:-1]
pubkey: str = "E959F11B571C3DC2EB4FA82107A4878989E528D3D82320E5C9DC890763F502E4"
address: str = public_key_to_address(public_key=pubkey, network="testnet")
print(address)
SDK を使わずに Javascript で公開鍵からアドレスを取得する
こちらは blockchain node からの返り値より取得するのがお手軽です。ノードからの返り値のアドレス形式は hex Address になっている為、更に変換が必要な点は注意して下さい。
const publicKey = "1FB72997EDE77DB9595CA4CD7405A43D2FDB370AD3CDE192F01FA24C5EBC3242";
const node = new URL(`accounts/${publicKey}`, "https://symbolnode.blockchain-authn.app:3001");
const res = await fetch(node, { method: "GET", headers: { "Content-Type": "application/json" } }).then((e) => e.json());
console.log(res.account.address);
Transaction Payload を Transaction へ変換
以下形式の Transaction Payload を Transaction 形式へ変換し、送り先のアドレス等を確認出来るようにします。Transaction 形式毎に適切な形式へ deserialize する必要があるため、本操作は SDK の利用を推奨します。
const payload =
"A000000000000000FCD44FFCEE0DA2A0865548DA1007C7A2D13E451C2A858E48A4FFF52040298156" +
"7CC6C0C51BB149231A46AA41DCA7FD613E46AFF336EB91C39DE64B84EE9C4A0987DA603E7BE5656C" +
"45692D5FC7F6D0EF8F24BB7A5C10ED5FDA8C5CFBC49FCBC8000000000198544140420F0000000000" +
"4F0047C709000000988E1191A25A88142C2FB3F69787576E3DC713EFC1CE4DE90000000000000000";
Symbol SDK Javascript v3 利用時
import { TransactionFactory } from "symbol-sdk/src/symbol/models.js";
import { Address } from 'symbol-sdk/src/symbol/Network.js';
import { hexToUint8 } from 'symbol-sdk/src/utils/converter.js';
const payload =
"A000000000000000FCD44FFCEE0DA2A0865548DA1007C7A2D13E451C2A858E48A4FFF52040298156" +
"7CC6C0C51BB149231A46AA41DCA7FD613E46AFF336EB91C39DE64B84EE9C4A0987DA603E7BE5656C" +
"45692D5FC7F6D0EF8F24BB7A5C10ED5FDA8C5CFBC49FCBC8000000000198544140420F0000000000" +
"4F0047C709000000988E1191A25A88142C2FB3F69787576E3DC713EFC1CE4DE90000000000000000";
console.log(new Address((TransactionFactory.deserialize(hexToUint8(payload)).recipientAddress.bytes)).toString());
Symbol SDK Javascript v3 を一部利用時
全ての処理を SDK v3 で記述したとき、 crypto モジュール等を求められる為、 webpack のバンドル方法をカスタムする必要があります。該当のカスタムが困難な場合は以下のように記述することで回避することができます。 webpack バンドルのカスタム方法は symbol-sdk v3 Javascript の README を参照して下さい。
import { TransactionFactory } from "symbol-sdk/src/symbol/models.js";
function uint8arrayToBase32(uint8Array) {
const base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
let bits = 0;
let value = 0;
let base32 = "";
for (var i = 0; i < uint8Array.length; i++) {
value = (value << 8) | uint8Array[i];
bits += 8;
while (bits >= 5) {
base32 += base32Chars[(value >>> (bits - 5)) & 0x1f];
bits -= 5;
}
}
if (bits > 0) {
base32 += base32Chars[(value << (5 - bits)) & 0x1f];
}
return base32;
}
const payload =
"A000000000000000FCD44FFCEE0DA2A0865548DA1007C7A2D13E451C2A858E48A4FFF52040298156" +
"7CC6C0C51BB149231A46AA41DCA7FD613E46AFF336EB91C39DE64B84EE9C4A0987DA603E7BE5656C" +
"45692D5FC7F6D0EF8F24BB7A5C10ED5FDA8C5CFBC49FCBC8000000000198544140420F0000000000" +
"4F0047C709000000988E1191A25A88142C2FB3F69787576E3DC713EFC1CE4DE90000000000000000";
const uint8Array = new Uint8Array(payload.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
const base32Address = uint8arrayUnresolvedAddressToEncodedAddress(
TransactionFactory.deserialize(uint8Array).recipientAddress.bytes
);
console.log(base32Address);
Mosaic 数量の相対値 ↔ 絶対値変換
相対値について知らない方は以下ドキュメントを参照して下さい
以下は メインネットの Symbol.xym
を変換する際の例となります。
SDK を使わずに Python で変換する
divisivility = 6
relativeValue = 44759657652 # 相対値
absoluteValue = relativeValue / 10 ** divisivility
print(absoluteValue) # 44759.657652
divisivility = 6
absoluteValue = 44759.657652
relativeValue = absoluteValue * 10 ** divisivility # 相対値
print(relativeValue) # 44759657652
SDK を使わずに Javascript で変換する
相対値→絶対値
const divisivility = 6;
const relativeValue = 44759657652; // 相対値
const absoluteValue = relativeValue / Math.pow(10,divisivility);
console.log(absoluteValue); // 44759.657652
絶対値→相対値
const divisivility = 6;
const absoluteValue = 44759.657652;
const relativeValue = absoluteValue * Math.pow(10,divisivility); // 相対値
console.log(relativeValue); // 44759657652
Deadline の調整
トランザクションを作成する際に指定する Deadline を任意に調整する方法を記載します。
Symbol SDK v3 Python を使ったとき
from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.symbol.Network import Network, NetworkTimestamp
from datetime import datetime, timezone, timedelta
facade: SymbolFacade = SymbolFacade("testnet")
deadline: NetworkTimestamp = facade.network.from_datetime(
datetime.now(tz=timezone.utc) + timedelta(hours=2)
)
print(deadline.timestamp)
Symbol SDK v3 Javascript を使ったとき
注意点として timestamp は BigInt です。 deadline.timestamp とすることで BitInt として取り出せます。以下は現在時刻の2時間後をDealineとするケースの例です。
import symbolSdk from "symbol-sdk";
const facade = new symbolSdk.facade.SymbolFacade("testnet");
/** @type {import("symbol-sdk/ts/src/symbol/Network").NetworkTimestamp} */
const deadline = facade.network.fromDatetime(new Date().getTime()).addHours(2);
console.log(deadline.timestamp)
SDK を使わなかったとき
deadline 算出は SDK を使わないケースはないと思うので割愛
新規アカウントの作成
以下、 SDK を用いて新規アカウントを作成します
Symbol SDK v3 Javascript を使ったとき
import symbolSdk from "symbol-sdk";
const facade = new symbolSdk.facade.SymbolFacade("testnet");
const privateKey = symbolSdk.PrivateKey.random();
// const privateKey = new symbolSdk.PrivateKey("${秘密鍵文字列}"); // 既存秘密鍵を取り込むとき
/** @type {import("symbol-sdk/ts/src/symbol/KeyPair").KeyPair} */
const keyPair = new symbolSdk.symbol.KeyPair(privateKey);
const publicKey = keyPair.publicKey;
/** @type {import("symbol-sdk/ts/src/symbol/Network").Address} */
const address = facade.network.publicKeyToAddress(publicKey);
console.log(address, publicKey);
転送トランザクションの作成
Symbol SDK v3 Javascript を使ったとき
以下、手数料を再計算しつつ、転送トランザクションをアナウンスする例となります。
import symbolSdk from "symbol-sdk";
const facade = new symbolSdk.facade.SymbolFacade("testnet");
const privateKey = symbolSdk.PrivateKey.random();
/** @type {import("symbol-sdk/ts/src/symbol/KeyPair").KeyPair} */
const keyPair = new symbolSdk.symbol.KeyPair(privateKey);
/** @type {import("symbol-sdk/ts/src/symbol/Network").Address} */
const address = facade.network.publicKeyToAddress(keyPair.publicKey);
/** @type {import("symbol-sdk/ts/src/symbol/Network").NetworkTimestamp} */
const deadline = facade.network.fromDatetime(new Date().getTime()).addHours(2);
const payload = {
type: "transfer_transaction_v1",
signerPublicKey: keyPair.publicKey,
deadline: deadline.timestamp,
recipientAddress: address.toString(),
fee: BigInt(0),
mosaics: [],
message: [String.fromCharCode(0), ...new TextEncoder("utf-8").encode("Hello")],
};
payload.fee = BigInt(facade.transactionFactory.create(payload).size * 1000 * 1);
const tx = facade.transactionFactory.create(payload);
const signedTx = facade.transactionFactory.constructor.attachSignature(tx, facade.signTransaction(keyPair, tx));
(async () => {
const node = new URL("transactions", "https://node-example-url.com:3001");
const res = await fetch(node, { method: "put", body: signedTx, headers: { "Content-Type": "application/json" } });
console.log(await res.json());
})();
Transaction から Transaction Hash の生成
Symbol SDK v3 Javascript を利用したとき
import symbolSdk from "symbol-sdk";
const facade = new symbolSdk.facade.SymbolFacade("testnet");
const transaction = facade.transactionFactory.create({
// -- 割愛 --
});
console.log(facade.hashTransaction(transaction).toString());
(暫定) Mosaic オブジェクトの作成
以下は Symbol SDK v2 に存在した new Mosaic 相当の処理を再現したものです。
import json
class Mosaic:
def __init__(self, mosaic_id: str, divisibility: int) -> None:
self.mosaic_id: str = mosaic_id
self.divisibility: int = divisibility
def to_with_amount(self, amount: float) -> dict[str, int]:
return {
"mosaic_id": int(f"0x{self.mosaic_id}", 16),
"amount": int(amount * (10**self.divisibility)),
}
def __str__(self) -> str:
return json.dumps(
{
"mosaic_id": self.mosaic_id,
"divisibility": self.divisibility,
}
)
print(Mosaic("72C0212E67A08BCE", 6).to_with_amount(float(1)))
Mnemonic から秘密/公開鍵
以下操作には追加のライブラリが必要になります。
npm install symbol-hd-wallets
Symbol SDK v3 Javascript を利用したとき
import symbolSdk from "symbol-sdk";
import { MnemonicPassPhrase, ExtendedKey, Wallet, Network } from "symbol-hd-wallets";
const mnemonicPhrase = MnemonicPassPhrase.createRandom();
// or const mnemonicPhrase = new MnemonicPassPhrase("**mnemonic**");
const seed = mnemonicPhrase.toSeed().toString("hex");
const mnemonic = mnemonicPhrase.plain;
const xkey = ExtendedKey.createFromSeed(seed, Network.SYMBOL);
const wallet = new Wallet(xkey);
const childAccount = wallet.getChildAccountPrivateKey("m/44'/4343'/0'/0'/0'");
const keyPair = new symbolSdk.symbol.KeyPair(new symbolSdk.PrivateKey(childAccount));
console.log("private key:", keyPair.privateKey.toString());
console.log("_public key:", keyPair.publicKey.toString());
Symbol Timestamp から現実時刻
Timestamp の計算は sdk 不要です。epochAdjustment は node から確認できます。 node から取得した時点では 1615853185s
と文字列となっている為、 s
を削除してから数値に変換してください。
https://${your-node-domain}:3001/network/properties
const epochAdjustment = 1615853185 * 1000;
const timestamp = 95587587284; // 対象トランザクションの timestamp
const date = new Date(epochAdjustment + timestamp).toLocaleString();
console.log(date);
import datetime
EPOCH_ADJUSTMENT = 1615853185 * 1000
TIMESTAMP = 95587587284
date = datetime.datetime.fromtimestamp(EPOCH_ADJUSTMENT + TIMESTAMP / 1000)
print(date.strftime("%Y/%m/%d %H:%M:%S"))
以下適宜自分が詰まった処理があれば更新していきます。