25
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Blockchain Symbol での各種変換処理の定義方法

Last updated at Posted at 2023-11-19

以下私の個人的なメモであり、詰まった変換処理等を追記していきます。なお、本記事では 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"))

以下適宜自分が詰まった処理があれば更新していきます。

25
10
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
25
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?