環境
- symbol-sdk: 3.2.2
登場人物
- Alice …Carol のファン
- Bob …Carol のマネージャ
- Carol …アイドル
Carol のファンである Alice は、Carol にプレゼントを贈りたい。
マネージャの Bob はプレゼントに偽装した危険物を Carol が受け取ってしまうとのを防ぎたい。
ファンへの返事は Carol 本人からして欲しい。
アカウント
Alice
- 一般的な普通のアカウント
Bob
- Carol アカウントの親シグ
Carol
- Bob アカウントの子シグ
- Bob 以外から受信できない
- 送信は誰にでもできる
Carol への制限追加
以下をアグリゲートでまとめて送信します。
- Bob 以外からの受信不可
- マルチシグ化(連署者は Bob)
ソース
import {
descriptors,
KeyPair,
models,
Network,
SymbolAccount,
SymbolFacade,
SymbolTransactionFactory,
} from "symbol-sdk/symbol";
import { Env } from "./Env.js";
import { PrivateKey, utils } from "symbol-sdk";
const facade = new SymbolFacade(Network.TESTNET);
const bobPrikey = new PrivateKey(Env.BOB_PRIVATE_KEY);
const bob = new SymbolAccount(facade, new KeyPair(bobPrikey));
const carolPrikey = new PrivateKey(Env.CAROL_PRIVATE_KEY);
const carol = new SymbolAccount(facade, new KeyPair(carolPrikey));
// ボブ以外から受信不可
const f = models.AccountRestrictionFlags.ADDRESS.value; // アドレス制限
const flags = new models.AccountRestrictionFlags(f); // 指定アドレスからのみ受信許可
const accountRestrictionDescriptor =
new descriptors.AccountAddressRestrictionTransactionV1Descriptor(
flags,
[bob.address],
[]
);
const accountRestrictionEmbeddedTx =
facade.createEmbeddedTransactionFromTypedDescriptor(
accountRestrictionDescriptor,
carol.publicKey
);
// マルチシグ
const multisigDescriptor =
new descriptors.MultisigAccountModificationTransactionV1Descriptor(
1,
1,
[bob.address],
[]
);
const multisigEmbeddedTx = facade.createEmbeddedTransactionFromTypedDescriptor(
multisigDescriptor,
carol.publicKey
);
// アグリゲートTx作成
const embeddedTransactions = [accountRestrictionEmbeddedTx, multisigEmbeddedTx];
const aggregateDescriptor =
new descriptors.AggregateCompleteTransactionV2Descriptor(
facade.static.hashEmbeddedTransactions(embeddedTransactions),
embeddedTransactions
);
const aggregateTx = facade.createTransactionFromTypedDescriptor(
aggregateDescriptor,
bob.publicKey,
100,
60 * 60 * 2,
1
) as models.AggregateCompleteTransactionV2;
// 署名
const payloadJson = SymbolTransactionFactory.attachSignature(
aggregateTx,
bob.signTransaction(aggregateTx)
);
// 連署
aggregateTx.cosignatures.push(carol.cosignTransaction(aggregateTx, false));
// アナウンス
const response = await fetch(new URL("/transactions", Env.REST_GATEWAY_URL), {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
payload: utils.uint8ToHex(aggregateTx.serialize()),
}),
});
const responseJson = await response.json();
console.log(responseJson.message);
Alice から Carol へ転送
Carol は Bob からのみ受信できるので、失敗します。
ソース
import {
KeyPair,
Network,
SymbolAccount,
SymbolFacade,
SymbolTransactionFactory,
descriptors,
models,
} from "symbol-sdk/symbol";
import { PrivateKey } from "symbol-sdk";
import { Env } from "./Env.js";
const facade = new SymbolFacade(Network.TESTNET);
const alicePrikey = new PrivateKey(Env.ALICE_PRIVATE_KEY);
const alice = new SymbolAccount(facade, new KeyPair(alicePrikey));
const carolPrikey = new PrivateKey(Env.CAROL_PRIVATE_KEY);
const carol = new SymbolAccount(facade, new KeyPair(carolPrikey));
// 転送モザイク設定
const sendMosaics = [
new descriptors.UnresolvedMosaicDescriptor(
new models.UnresolvedMosaicId(Env.CURRENCY_MOSAIC_ID),
new models.Amount(100_000000n)
),
];
// 平文メッセージ
const messageData = "\0Hello, Carol!!";
// 転送トランザクション生成
const transferDescriptor = new descriptors.TransferTransactionV1Descriptor(
carol.address,
sendMosaics,
messageData
);
const transferTx = facade.createTransactionFromTypedDescriptor(
transferDescriptor,
alice.publicKey,
100,
60 * 60 * 2
);
// 署名
const payloadJson = SymbolTransactionFactory.attachSignature(
transferTx,
alice.signTransaction(transferTx)
);
// アナウンス
const transactionsResponse = await fetch(
new URL("/transactions", Env.REST_GATEWAY_URL),
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: payloadJson,
}
);
const responseJson = await transactionsResponse.json();
console.log(responseJson.message);
Alice から Carol へアグリゲート転送
Bob への転送に紛れてアグリゲートで Carol に転送しようとしてみても、失敗します。
ソース
import { PrivateKey } from "symbol-sdk";
import {
SymbolFacade,
Network,
SymbolAccount,
KeyPair,
descriptors,
models,
SymbolTransactionFactory,
} from "symbol-sdk/symbol";
import { Env } from "./Env.js";
const facade = new SymbolFacade(Network.TESTNET);
const alicePrikey = new PrivateKey(Env.ALICE_PRIVATE_KEY);
const alice = new SymbolAccount(facade, new KeyPair(alicePrikey));
const bobPrikey = new PrivateKey(Env.BOB_PRIVATE_KEY);
const bob = new SymbolAccount(facade, new KeyPair(bobPrikey));
const carolPrikey = new PrivateKey(Env.CAROL_PRIVATE_KEY);
const carol = new SymbolAccount(facade, new KeyPair(carolPrikey));
// Bobへの転送
const transferDescriptor1 = new descriptors.TransferTransactionV1Descriptor(
bob.address,
[
new descriptors.UnresolvedMosaicDescriptor(
new models.UnresolvedMosaicId(Env.CURRENCY_MOSAIC_ID),
new models.Amount(100_000000n)
),
],
"\0Hello, Bob!!"
);
const transferTx1 = facade.createEmbeddedTransactionFromTypedDescriptor(
transferDescriptor1,
alice.publicKey
);
// Carolへの転送
const transferDescriptor2 = new descriptors.TransferTransactionV1Descriptor(
carol.address,
[
new descriptors.UnresolvedMosaicDescriptor(
new models.UnresolvedMosaicId(Env.CURRENCY_MOSAIC_ID),
new models.Amount(100_000000n)
),
],
"\0Hello, Carol!!"
);
const transferTx2 = facade.createEmbeddedTransactionFromTypedDescriptor(
transferDescriptor2,
alice.publicKey
);
// アグリゲートTx作成
const embeddedTransactions = [transferTx1, transferTx2];
const aggregateDescriptor =
new descriptors.AggregateCompleteTransactionV2Descriptor(
facade.static.hashEmbeddedTransactions(embeddedTransactions),
embeddedTransactions
);
const aggregateTx = facade.createTransactionFromTypedDescriptor(
aggregateDescriptor,
alice.publicKey,
100,
60 * 60 * 2
) as models.AggregateCompleteTransactionV2;
// 署名
const payloadJson = SymbolTransactionFactory.attachSignature(
aggregateTx,
alice.signTransaction(aggregateTx)
);
// アナウンス
const response = await fetch(new URL("/transactions", Env.REST_GATEWAY_URL), {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: payloadJson,
});
const responseJson = await response.json();
console.log(responseJson.message);
Alice から Bob へ、Bob から Carol へ、そして Carol から Alice へ
Alice から Bob への転送は許可されているので、Bob が受け取り Carol へ渡します。受け取るかチェックを行いたいので、Alice からはシークレットロックで送信してもらいます。
シークレットロックで送ってもらうことで、Bob が受け取りを拒否した場合、期限が切れるとともに自動的に Alice に返還されます。
- Alice から Bob へシークレットロックを送信
- Bob はシークレットロックを確認
- シークレットロックの内容に問題がない場合プルーフを送信
- Bob は Carol にシークレットロック分のモザイクを転送
- Bob は Carol に Alice への返事を依頼
3~5 をアグリゲート処理します。
Bob はシークレットのモザイクをそのまま Carol へ渡し、こう返事を出せと命令します。
もはや、Carol は Bob の言いなり…
シークレットロック使用済みや期限切れとかのチェックは省略。
ソース
import { sha3_256 } from "@noble/hashes/sha3";
import { getRandomValues } from "crypto";
import { Hash256, PrivateKey, utils } from "symbol-sdk";
import {
descriptors,
KeyPair,
models,
Network,
SymbolAccount,
SymbolFacade,
SymbolTransactionFactory,
} from "symbol-sdk/symbol";
import { Env } from "./Env.js";
const facade = new SymbolFacade(Network.TESTNET);
const alicePrikey = new PrivateKey(Env.ALICE_PRIVATE_KEY);
const alice = new SymbolAccount(facade, new KeyPair(alicePrikey));
const bobPrikey = new PrivateKey(Env.BOB_PRIVATE_KEY);
const bob = new SymbolAccount(facade, new KeyPair(bobPrikey));
const carolPrikey = new PrivateKey(Env.CAROL_PRIVATE_KEY);
const carol = new SymbolAccount(facade, new KeyPair(carolPrikey));
/** Alice->BobシークレットTx */
// シークレットとプルーフ作成
const proof = getRandomValues(new Uint8Array(20));
const secretDigest = sha3_256.create().update(proof).digest();
const secret = new Hash256(secretDigest);
console.log(`secret: ${utils.uint8ToHex(secretDigest)}`);
console.log(`proof : ${utils.uint8ToHex(proof)}`);
// シークレットロックトランザクション作成
const secretLockDescriptor = new descriptors.SecretLockTransactionV1Descriptor(
bob.address,
secret,
new descriptors.UnresolvedMosaicDescriptor(
new models.UnresolvedMosaicId(Env.CURRENCY_MOSAIC_ID),
new models.Amount(132_000000n)
),
new models.BlockDuration(5n),
models.LockHashAlgorithm.SHA3_256
);
const secretLockTx = facade.createTransactionFromTypedDescriptor(
secretLockDescriptor,
alice.publicKey,
100,
60 * 60 * 2
);
// Alice署名
const payloadJson = SymbolTransactionFactory.attachSignature(
secretLockTx,
alice.signTransaction(secretLockTx)
);
const secretLockTxHash = facade.hashTransaction(secretLockTx);
// アナウンス
const secretLockResponse = await fetch(
new URL("/transactions", Env.REST_GATEWAY_URL),
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: payloadJson,
}
);
console.log((await secretLockResponse.json()).message);
/** 承認待機 */
// シークレットTx承認待ち
for (let i = 0; i < 100; i++) {
await new Promise((resolve) => setTimeout(resolve, 1000));
const secretTxStatus = await fetch(
new URL(`/transactionStatus/${secretLockTxHash}`, Env.REST_GATEWAY_URL),
{
method: "GET",
headers: { "Content-Type": "application/json" },
}
);
if ((await secretTxStatus.json()).group === "confirmed") {
break;
}
}
/** シークレットロック確認 */
// シークレットロック読み
const secretInfoJson = await fetch(
new URL(
`/lock/secret?secret=${utils.uint8ToHex(secretDigest)}`,
Env.REST_GATEWAY_URL
),
{
method: "GET",
headers: { "Content-Type": "application/json" },
}
);
const secretInfo = await secretInfoJson.json();
const secretMosaicId = new models.UnresolvedMosaicId(
BigInt("0x" + secretInfo.data[0].lock.mosaicId)
);
const secretAmount = new models.Amount(BigInt(secretInfo.data[0].lock.amount));
/** プルーフと転送Tx送信 */
// シークレットプルーフTx
const embeddedSecretProofDescriptor =
new descriptors.SecretProofTransactionV1Descriptor(
bob.address,
secret,
models.LockHashAlgorithm.SHA3_256,
proof
);
const embeddedSecretProofTx =
facade.createEmbeddedTransactionFromTypedDescriptor(
embeddedSecretProofDescriptor,
bob.publicKey
);
// Bob->Carol転送Tx
const embeddedTransferDescriptor1 =
new descriptors.TransferTransactionV1Descriptor(
carol.address,
[new descriptors.UnresolvedMosaicDescriptor(secretMosaicId, secretAmount)],
""
);
const embeddedTransferDescriptorTx1 =
facade.createEmbeddedTransactionFromTypedDescriptor(
embeddedTransferDescriptor1,
bob.publicKey
);
// Carol->Alice転送Tx
const embeddedTransferDescriptor2 =
new descriptors.TransferTransactionV1Descriptor(
alice.address,
[],
`\0Thank You!! (${secretAmount.value})`
);
const embeddedTransferDescriptorTx2 =
facade.createEmbeddedTransactionFromTypedDescriptor(
embeddedTransferDescriptor2,
carol.publicKey
);
// アグリゲートTx作成
const embeddedTransactions = [
embeddedSecretProofTx,
embeddedTransferDescriptorTx1,
embeddedTransferDescriptorTx2,
];
const aggregateDescriptor =
new descriptors.AggregateCompleteTransactionV2Descriptor(
facade.static.hashEmbeddedTransactions(embeddedTransactions),
embeddedTransactions
);
const aggregateTx = facade.createTransactionFromTypedDescriptor(
aggregateDescriptor,
bob.publicKey,
100,
60 * 60 * 2,
1
) as models.AggregateCompleteTransactionV2;
// 署名
const aggregateTxSignature = facade.signTransaction(bob.keyPair, aggregateTx);
const aggregateTxPayloadJson = SymbolTransactionFactory.attachSignature(
aggregateTx,
aggregateTxSignature
);
// アグリゲートTxアナウンス
const aggregateResponse = await fetch(
new URL("/transactions", Env.REST_GATEWAY_URL),
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: aggregateTxPayloadJson,
}
);
console.log((await aggregateResponse.json()).message);
トランザクション