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

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

```python
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

```javascript
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

```javascript
import symbolSdk from "symbol-sdk";

const address = new symbolSdk.symbol.Address("NC*********...");
console.log(symbolSdk.utils.uint8ToHex(address.bytes));

```


**SDK を使わずに Python 標準モジュール利用時**

```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 実行時**

```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));
```

:::note info
**アドレスの種類**
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 利用時**

```python
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 利用時**

```javascript 
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 になっている為、更に変換が必要な点は注意して下さい。

```python
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 も使わない場合は次の通りです。

```python 
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 になっている為、更に変換が必要な点は注意して下さい。

```javascript
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 利用時**

```javascript
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 を参照して下さい。

https://www.npmjs.com/package/symbol-sdk

```javascript
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 数量の相対値 ↔ 絶対値変換

相対値について知らない方は以下ドキュメントを参照して下さい

https://docs.symbol.dev/concepts/mosaic.html#divisibility

以下は メインネットの `Symbol.xym` を変換する際の例となります。

**SDK を使わずに Python で変換する**

```python
divisivility = 6
relativeValue = 44759657652  # 相対値
absoluteValue = relativeValue / 10 ** divisivility
print(absoluteValue)  # 44759.657652
```

```python 
divisivility = 6
absoluteValue = 44759.657652
relativeValue = absoluteValue * 10 ** divisivility  # 相対値
print(relativeValue)  # 44759657652
```

**SDK を使わずに Javascript で変換する**

相対値→絶対値
```javascript
const divisivility = 6;
const relativeValue = 44759657652; // 相対値
const absoluteValue = relativeValue / Math.pow(10,divisivility);
console.log(absoluteValue); // 44759.657652
```

絶対値→相対値
```javascript
const divisivility = 6;
const absoluteValue = 44759.657652;
const relativeValue = absoluteValue * Math.pow(10,divisivility); // 相対値
console.log(relativeValue); // 44759657652
```

## Deadline の調整

トランザクションを作成する際に指定する Deadline を任意に調整する方法を記載します。

**Symbol SDK v3 Python を使ったとき**

```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とするケースの例です。

```javascript 
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 を使ったとき**

```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 を使ったとき**

以下、手数料を再計算しつつ、転送トランザクションをアナウンスする例となります。

```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 を利用したとき**

```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 相当の処理を再現したものです。

```python 
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 から秘密/公開鍵

以下操作には追加のライブラリが必要になります。

```shell
npm install symbol-hd-wallets
```

**Symbol SDK v3 Javascript を利用したとき**

```typescript
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`

```javascript
const epochAdjustment = 1615853185 * 1000;
const timestamp = 95587587284; // 対象トランザクションの timestamp
const date = new Date(epochAdjustment + timestamp).toLocaleString();
console.log(date);
```

```python 
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"))
```

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