Skip to content

Instantly share code, notes, and snippets.

@YasunoriMATSUOKA
Last active May 30, 2024 10:30
Show Gist options
  • Save YasunoriMATSUOKA/cd268c9bb233f6ece5040d3aa755a175 to your computer and use it in GitHub Desktop.
Save YasunoriMATSUOKA/cd268c9bb233f6ece5040d3aa755a175 to your computer and use it in GitHub Desktop.
Create or Update Metadata on Symbol blockchain with symbol-sdk@2 (node_modules/symbol-sdk/dist/src/service/MetadataTransactionService.jsの中のcreateAccountMetadataTransactionメソッドを参考に、それを使ったパターンと使わないパターンそれぞれのコードを例示)
import {
Account,
AggregateTransaction,
Deadline,
KeyGenerator,
MetadataTransactionService,
NetworkType,
RepositoryFactoryHttp,
UInt64,
} from "symbol-sdk";
import { firstValueFrom } from "rxjs";
(async () => {
const nodeUrl = "https://sym-test-03.opening-line.jp:3001";
const repositoryFactory = new RepositoryFactoryHttp(nodeUrl);
const metadataRepository = repositoryFactory.createMetadataRepository();
const txRepository = repositoryFactory.createTransactionRepository();
const epochAdjustment = await firstValueFrom(
repositoryFactory.getEpochAdjustment(),
);
const generationHash = await firstValueFrom(
repositoryFactory.getGenerationHash(),
);
const aliceAccount = Account.createFromPrivateKey(
"_PUT_ALICE_PRIVATE_KEY_",
NetworkType.TEST_NET,
);
const bobAccount = Account.createFromPrivateKey(
"_PUT_BOB_PRIVATE_KEY_",
NetworkType.TEST_NET,
);
const feeMultiplier = 100;
const uint64Key = KeyGenerator.generateUInt64Key(
"_PUT_METADATA_KEY_",
);
const stringValue = "Hello, Bob!";
const metadataTransactionService = new MetadataTransactionService(metadataRepository);
// Note: MetadataTransactionServiceの中の関数名がcreateだが中身の実装を見るとupdateも扱える実装となっている。したがって新規設定時も更新時もどちらも同じ実装でOK。
const accountMetadataTx = await firstValueFrom(
metadataTransactionService.createAccountMetadataTransaction(
Deadline.create(epochAdjustment),
NetworkType.TEST_NET,
bobAccount.address,
uint64Key,
stringValue,
aliceAccount.address,
UInt64.fromUint(0), // Note: aggregateCompleteTxのsetMaxFeeForAggregateで指定するので本来不要だが、TSの型でUInt64が要求されているのでダミー値を入れておく必要がある。速習SymbolはTSではなくJS環境なので、ここに少し差が出る。
),
);
const aggregateCompletedTx = AggregateTransaction.createComplete(
Deadline.create(epochAdjustment),
[accountMetadataTx.toAggregate(aliceAccount.publicAccount)],
NetworkType.TEST_NET,
[],
).setMaxFeeForAggregate(feeMultiplier, 1);
const signedTx = aliceAccount.signTransactionWithCosignatories(
aggregateCompletedTx,
[bobAccount],
generationHash,
);
console.dir(
{
txHash: signedTx.hash,
txPayload: signedTx.payload,
},
{ depth: null },
);
const listener = repositoryFactory.createListener();
await listener.open();
listener.newBlock().subscribe((newBlock) => {
console.log(`newBlock: ${newBlock.height.toString()}`);
}); // Note: リスナーが意図せず切断されないよう毎ブロック30秒毎にメッセージが届くよう設定
listener
.status(aliceAccount.address)
.subscribe((txStatusError) => console.error(txStatusError)); //Note: エラー検知
listener.unconfirmedAdded(aliceAccount.address).subscribe((unconfirmedTx) => {
console.dir({ unconfirmedTx }, { depth: null });
}); // Note: エラー無く、ブロックチェーンに情報が一旦届いたが、まだブロックに組み込まれていない状態の検知
listener.confirmed(aliceAccount.address).subscribe((confirmedTx) => {
console.dir({ confirmedTx }, { depth: null });
listener.close();
}); // Note: ブロックに情報が組み込まれた状態。このタイミングでリスナーを切断してtxの状態遷移の監視を終了する
const txAnnounceResponse = await firstValueFrom(
txRepository.announce(signedTx),
);
console.dir({ txAnnounceResponse }, { depth: null });
})();
import {
Account,
AccountMetadataTransaction,
AggregateTransaction,
Convert,
Deadline,
InnerTransaction,
KeyGenerator,
MetadataType,
NetworkType,
RepositoryFactoryHttp,
} from "symbol-sdk";
import { firstValueFrom } from "rxjs";
(async () => {
const nodeUrl = "https://sym-test-03.opening-line.jp:3001";
const repositoryFactory = new RepositoryFactoryHttp(nodeUrl);
const metadataRepository = repositoryFactory.createMetadataRepository();
const txRepository = repositoryFactory.createTransactionRepository();
const epochAdjustment = await firstValueFrom(
repositoryFactory.getEpochAdjustment(),
);
const generationHash = await firstValueFrom(
repositoryFactory.getGenerationHash(),
);
const aliceAccount = Account.createFromPrivateKey(
"_PUT_ALICE_PRIVATE_KEY_",
NetworkType.TEST_NET,
);
const bobAccount = Account.createFromPrivateKey(
"_PUT_BOB_PRIVATE_KEY_",
NetworkType.TEST_NET,
);
const feeMultiplier = 100;
const uint64Key = KeyGenerator.generateUInt64Key(
"_PUT_METADATA_KEY_",
);
const stringValue = "Hello, Bob!";
//Note: MetadataTransactionServiceを使わない場合は、自分自身で現状のMetadataを取得し、Metadataが既にあるか否かで処理を分ける必要がある
//Note: 現状のMetadataの取得
const metadatas = await firstValueFrom(
metadataRepository.search({
targetAddress: bobAccount.address,
scopedMetadataKey: uint64Key.toHex(),
sourceAddress: aliceAccount.address,
metadataType: MetadataType.Account,
}),
);
let accountMetadataTx: InnerTransaction;
if (metadatas.data.length > 0) {
// Note: 対象のメタデータが既に存在する場合=更新の場合
const metadata = metadatas.data[0];
const currentValueBytes = Convert.utf8ToUint8(metadata.metadataEntry.value);
const newValueBytes = Convert.utf8ToUint8(stringValue);
const valueSizeDelta = newValueBytes.length - currentValueBytes.length;
const valueBytes = Convert.hexToUint8(
Convert.xor(currentValueBytes, newValueBytes),
);
accountMetadataTx = AccountMetadataTransaction.create(
Deadline.create(epochAdjustment),
bobAccount.address,
uint64Key,
valueSizeDelta,
valueBytes,
NetworkType.TEST_NET,
).toAggregate(aliceAccount.publicAccount);
} else {
// Note: 対象のメタデータが存在しない場合=新規設定の場合
const newValueBytes = Convert.utf8ToUint8(stringValue);
const valueSizeDelta = newValueBytes.length;
const valueBytes = newValueBytes;
accountMetadataTx = AccountMetadataTransaction.create(
Deadline.create(epochAdjustment),
bobAccount.address,
uint64Key,
valueSizeDelta,
valueBytes,
NetworkType.TEST_NET,
).toAggregate(aliceAccount.publicAccount);
}
const aggregateCompletedTx = AggregateTransaction.createComplete(
Deadline.create(epochAdjustment),
[accountMetadataTx],
NetworkType.TEST_NET,
[],
).setMaxFeeForAggregate(feeMultiplier, 1);
const signedTx = aliceAccount.signTransactionWithCosignatories(
aggregateCompletedTx,
[bobAccount],
generationHash,
);
console.dir(
{
txHash: signedTx.hash,
txPayload: signedTx.payload,
},
{ depth: null },
);
const txAnnounceResponse = await firstValueFrom(
txRepository.announce(signedTx),
);
console.dir({ txAnnounceResponse }, { depth: null });
})();
@YasunoriMATSUOKA
Copy link
Author

YasunoriMATSUOKA commented May 30, 2024

MetadataTransactionServiceの中のcreateAccountMetadataTransactionメソッドを使用してもよいと思います。(npmパッケージ内の実装を見るとcreateだけでなくupdateも扱える実装になっています。)

ハマりどころとしては、以下のような点がありうるでしょうか。

  1. メタデータのsource, targetが同一アカウントだとしてもAggregateCompleteTxとしてwrapして実行する必要があったり、
  2. AggregateCompleteTxとしてwrapする前に、InnerTxに変換する.toAggregate(alice.publicAccount)を付け忘れたり、 (←すみません、最初このエラーが出るコードになってました。)
  3. MetadataTransactionServiceの中のcreateAccountMetadataTransactionメソッドのmaxFeeはfeeMultiplier(=手数料係数100)と異なる最大手数料そのものの数値となるのですが、最終的な手数料はwrapしたAggregateCompleteTxにsetMaxFeeすると楽に設定できるので、createAccountMetadataTransactionメソッドの中のmaxFeeは本来は未設定でOKなのですが、TSでは未設定にすると型エラーになるため、仮でUInt64.fromUint(0))などを指定しておくと楽といったワークアラウンドがある点に注意が必要だったりすると思います。(←速習SymbolはJSなので、その変数が未設定でも成り立つが、TSだと未設定やundefinedだと型で怒られるのでダミー値を一旦入れる必要があります。)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment