Add custom data to an account.
Bob works as a digital notary that stamp accounts on Symbol’s public blockchain. When a customer comes to Bob to notarize a document, he checks the authentication of the customer’s documents then tags the customer’s account with the digitized document as metadata.
Alice is a recent graduate who wants her educational certificate accredited to her Symbol account to avoid the hassle of repeatedly providing verification of her degree. So she goes to Bob and provides him with proof of her degree. Once Alice pays a fee, Bob verifies the authenticity and stamps Alice’s account with metadata that signifies her degree.
In this tutorial, you are going to implement a program to allow Bob tag accounts issuing metadata transactions.
Complete the getting started section.
Create accounts for Alice and Bob.
Load Bob’s account with enough symbol.xym
to pay for transaction fees.
symbol-cli account generate --save
Enter network type (MAIN_NET, TEST_NET): TEST_NET
Do you want to save it? [y/n]: y
Enter a Symbol Node URL. (Example: http://localhost:3000): <NODE_URL>
Insert profile name: alice
1. Bob has to pick a key to store Alice’s certificate.
Imagine that CERT
is a common key to store university degrees.
Define this key as a new variable.
// replace with key
const key = KeyGenerator.generateUInt64Key('CERT');
// replace with key
const key = symbol_sdk_1.KeyGenerator.generateUInt64Key('CERT');
2. Alice’s degree brings the identifier 123456
for her certificate.
Help Bob to assign this value to the key defined in the previous step.
To achieve so, define an AccountMetadataTransaction linking Alice account, the key (CERT), and the associated value (123456).
// replace with network type
const networkType = NetworkType.TEST_NET;
// replace with public key
const alicePublicKey =
'D04AB232742BB4AB3A1368BD4615E4E6D0224AB71A016BAF8520A332C9778737';
const alicePublicAccount = PublicAccount.createFromPublicKey(
alicePublicKey,
networkType,
);
// replace with value
const value = '123456';
const accountMetadataTransaction = AccountMetadataTransaction.create(
Deadline.create(epochAdjustment),
alicePublicAccount.address,
key,
value.length,
value,
networkType,
);
// replace with network type
const networkType = symbol_sdk_1.NetworkType.TEST_NET;
// replace with public key
const alicePublicKey = 'D04AB232742BB4AB3A1368BD4615E4E6D0224AB71A016BAF8520A332C9778737';
const alicePublicAccount = symbol_sdk_1.PublicAccount.createFromPublicKey(alicePublicKey, networkType);
// replace with value
const value = '123456';
const accountMetadataTransaction = symbol_sdk_1.AccountMetadataTransaction.create(symbol_sdk_1.Deadline.create(epochAdjustment), alicePublicAccount.address, key, value.length, value, networkType);
3. To avoid spamming the account with invalid metadata keys, all metadata is attached only with the consent of the account owner through Aggregate Transactions. Thus, Alice will have to opt-in if she wants the metadata entry assigned to its account. Wrap the AccountMetadataTransaction inside an AggregateBondedTransaction and sign the transaction using Bob’s account.
// replace with bob private key
const bobPrivateKey =
'0000000000000000000000000000000000000000000000000000000000000000';
const bobAccount = Account.createFromPrivateKey(bobPrivateKey, networkType);
const aggregateTransaction = AggregateTransaction.createBonded(
Deadline.create(epochAdjustment),
[accountMetadataTransaction.toAggregate(bobAccount.publicAccount)],
networkType,
[],
UInt64.fromUint(2000000),
);
// replace with meta.networkGenerationHash (nodeUrl + '/node/info')
const networkGenerationHash =
'1DFB2FAA9E7F054168B0C5FCB84F4DEB62CC2B4D317D861F3168D161F54EA78B';
const signedTransaction = bobAccount.sign(
aggregateTransaction,
networkGenerationHash,
);
console.log(signedTransaction.hash);
// replace with bob private key
const bobPrivateKey = '0000000000000000000000000000000000000000000000000000000000000000';
const bobAccount = symbol_sdk_1.Account.createFromPrivateKey(bobPrivateKey, networkType);
const aggregateTransaction = symbol_sdk_1.AggregateTransaction.createBonded(symbol_sdk_1.Deadline.create(epochAdjustment), [accountMetadataTransaction.toAggregate(bobAccount.publicAccount)], networkType, [], symbol_sdk_1.UInt64.fromUint(2000000));
// replace with meta.networkGenerationHash (nodeUrl + '/node/info')
const networkGenerationHash = '1DFB2FAA9E7F054168B0C5FCB84F4DEB62CC2B4D317D861F3168D161F54EA78B';
const signedTransaction = bobAccount.sign(aggregateTransaction, networkGenerationHash);
console.log(signedTransaction.hash);
4. Before sending an aggregate transaction to the network, Bob has to lock 10
symbol.xym
.
Define a new HashLockTransaction and sign it with Bob’s account.
// replace with symbol.xym id
const networkCurrencyMosaicId = new MosaicId('5E62990DCAC5BE8A');
// replace with network currency divisibility
const networkCurrencyDivisibility = 6;
const hashLockTransaction = HashLockTransaction.create(
Deadline.create(epochAdjustment),
new Mosaic(
networkCurrencyMosaicId,
UInt64.fromUint(10 * Math.pow(10, networkCurrencyDivisibility)),
),
UInt64.fromUint(480),
signedTransaction,
networkType,
UInt64.fromUint(2000000),
);
const signedHashLockTransaction = bobAccount.sign(
hashLockTransaction,
networkGenerationHash,
);
// replace with symbol.xym id
const networkCurrencyMosaicId = new symbol_sdk_1.MosaicId('5E62990DCAC5BE8A');
// replace with network currency divisibility
const networkCurrencyDivisibility = 6;
const hashLockTransaction = symbol_sdk_1.HashLockTransaction.create(symbol_sdk_1.Deadline.create(epochAdjustment), new symbol_sdk_1.Mosaic(networkCurrencyMosaicId, symbol_sdk_1.UInt64.fromUint(10 * Math.pow(10, networkCurrencyDivisibility))), symbol_sdk_1.UInt64.fromUint(480), signedTransaction, networkType, symbol_sdk_1.UInt64.fromUint(2000000));
const signedHashLockTransaction = bobAccount.sign(hashLockTransaction, networkGenerationHash);
Note
Bob will receive the locked funds back if Alice cosigns the aggregate during the next 480
blocks.
5. Announce the HashLockTransaction. Monitor the network until the transaction gets confirmed, and then announce the AggregateTransaction containing the AccountMetadataTransaction.
// replace with node endpoint
const nodeUrl = 'NODE_URL';
const repositoryFactory = new RepositoryFactoryHttp(nodeUrl);
const listener = repositoryFactory.createListener();
const receiptHttp = repositoryFactory.createReceiptRepository();
const transactionHttp = repositoryFactory.createTransactionRepository();
const transactionService = new TransactionService(transactionHttp, receiptHttp);
listener.open().then(() => {
transactionService
.announceHashLockAggregateBonded(
signedHashLockTransaction,
signedTransaction,
listener,
)
.subscribe(
(x) => console.log(x),
(err) => console.log(err),
() => listener.close(),
);
});
// replace with node endpoint
const nodeUrl = 'NODE_URL';
const repositoryFactory = new symbol_sdk_1.RepositoryFactoryHttp(nodeUrl);
const listener = repositoryFactory.createListener();
const receiptHttp = repositoryFactory.createReceiptRepository();
const transactionHttp = repositoryFactory.createTransactionRepository();
const transactionService = new symbol_sdk_1.TransactionService(transactionHttp, receiptHttp);
listener.open().then(() => {
transactionService
.announceHashLockAggregateBonded(signedHashLockTransaction, signedTransaction, listener)
.subscribe((x) => console.log(x), (err) => console.log(err), () => listener.close());
});
Once the transaction gets confirmed, cosign the hash obtained in the fourth step using Alice’s profile.
symbol-cli transaction cosign --hash <transaction-hash> --profile alice
If everything goes well, now Alice has assigned the metadata value {bobPublicKey, CERT, 123456}
, which can be read as “Alice account has the certificate number 123456 and it was verified by Bob”.