Assigning metadata to an account

Add custom data to an account.

Use case

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.

../../_images/metadata-certificate.png

Prerequisites

Creating the account

  1. Create an account for Alice, using the CLI tool. Remember to use a suitable NODE_URL.

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

Method #01: Using the SDK

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());
});
  1. 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
  1. 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”.