Updating metadata entries

Update any existent metadata entry.

Use case

Metadata transactions are stored on the blockchain. Once a transaction is included in a block—and the block receives enough confirmations—it is not possible to modify the record without invalidating the whole chain.

What we can do to update a metadata entry is to announce a second metadata transaction. This action will record a new transaction while keeping the history immutable. However, how can we retrieve the latest metadata value assigned to an asset without querying the whole chain? Symbol makes this possible by keeping a copy of the newest value assigned to a metadata entry as a state.

This guide shows you how to update a metadata entry attached to an account. However, you could follow a similar approach to update namespace and mosaic metadata entries.

Prerequisites

Method #01: Using the SDK

Bob—the notary from the assigning metadata entries to an account guide— is requested to remove Alice’s account CERT metadata entry because the certificate has expired.

../../_images/metadata-update.png

1. Define a new AccountMetadataTransaction setting Alice’s account as the metadata target. To indicate that the certificate has expired, Bob decides to add the new value 000000 to the metadata entry with key CERT. However, you need to pass an extra parameter that was not necessary when assigning a metadata entry for the first time.

By definition, blockchains can rollback up to a certain pre-established depth to resolve forks. In case that the state needs to be reverted, you need to indicate the difference of size between the previousValue assigned to the metadata entry and the newValue .

A) Retrieve the previous metadata value and calculate the difference of size with the newest value. Then, return the AccountMetadataTransaction object.

// replace with network type
const networkType = NetworkType.TEST_NET;
// replace with bob private key
const bobPrivateKey =
  '0000000000000000000000000000000000000000000000000000000000000000';
const bobAccount = Account.createFromPrivateKey(bobPrivateKey, networkType);
// replace with alice public key
const alicePublicKey =
  'D04AB232742BB4AB3A1368BD4615E4E6D0224AB71A016BAF8520A332C9778737';
const alicePublicAccount = PublicAccount.createFromPublicKey(
  alicePublicKey,
  networkType,
);
// replace with node endpoint
const nodeUrl = 'NODE_URL';
const repositoryFactory = new RepositoryFactoryHttp(nodeUrl);
const metadataHttp = repositoryFactory.createMetadataRepository();

// replace with key and new value
const key = KeyGenerator.generateUInt64Key('CERT');
const newValue = '000000';
const newValueBytes = Convert.utf8ToUint8(newValue);

const searchCriteria = {
  targetAddress: alicePublicAccount.address,
  scopedMetadataKey: key.toString(),
  sourceAddress: bobAccount.address,
};
const accountMetadataTransaction = metadataHttp.search(searchCriteria).pipe(
  mergeMap((metadata) => {
    const currentValueBytes = Convert.utf8ToUint8(
      metadata.data[0].metadataEntry.value,
    );
    return of(
      AccountMetadataTransaction.create(
        Deadline.create(epochAdjustment),
        alicePublicAccount.address,
        key,
        newValueBytes.length - currentValueBytes.length,
        Convert.decodeHex(Convert.xor(currentValueBytes, newValueBytes)),
        networkType,
      ),
    );
  }),
);
// replace with network type
const networkType = symbol_sdk_1.NetworkType.TEST_NET;
// replace with bob private key
const bobPrivateKey = '0000000000000000000000000000000000000000000000000000000000000000';
const bobAccount = symbol_sdk_1.Account.createFromPrivateKey(bobPrivateKey, networkType);
// replace with alice public key
const alicePublicKey = 'D04AB232742BB4AB3A1368BD4615E4E6D0224AB71A016BAF8520A332C9778737';
const alicePublicAccount = symbol_sdk_1.PublicAccount.createFromPublicKey(alicePublicKey, networkType);
// replace with node endpoint
const nodeUrl = 'NODE_URL';
const repositoryFactory = new symbol_sdk_1.RepositoryFactoryHttp(nodeUrl);
const metadataHttp = repositoryFactory.createMetadataRepository();
// replace with key and new value
const key = symbol_sdk_1.KeyGenerator.generateUInt64Key('CERT');
const newValue = '000000';
const newValueBytes = symbol_sdk_1.Convert.utf8ToUint8(newValue);
const searchCriteria = {
    targetAddress: alicePublicAccount.address,
    scopedMetadataKey: key.toString(),
    sourceAddress: bobAccount.address,
};
const accountMetadataTransaction = metadataHttp.search(searchCriteria).pipe((0, operators_1.mergeMap)((metadata) => {
    const currentValueBytes = symbol_sdk_1.Convert.utf8ToUint8(metadata.data[0].metadataEntry.value);
    return (0, rxjs_1.of)(symbol_sdk_1.AccountMetadataTransaction.create(symbol_sdk_1.Deadline.create(epochAdjustment), alicePublicAccount.address, key, newValueBytes.length - currentValueBytes.length, symbol_sdk_1.Convert.decodeHex(symbol_sdk_1.Convert.xor(currentValueBytes, newValueBytes)), networkType));
}));

B) You can achieve the same result with less effort using the MetadataService. Behind the scenes, the Symbol SDK handles the complexity of updating metadata entries.

// replace with network type
const networkType = NetworkType.TEST_NET;
// replace with bob private key
const bobPrivateKey =
  '0000000000000000000000000000000000000000000000000000000000000000';
const bobAccount = Account.createFromPrivateKey(bobPrivateKey, networkType);
// replace with alice public key
const alicePublicKey =
  'D04AB232742BB4AB3A1368BD4615E4E6D0224AB71A016BAF8520A332C9778737';
const alicePublicAccount = PublicAccount.createFromPublicKey(
  alicePublicKey,
  networkType,
);
// replace with node endpoint
const nodeUrl = 'NODE_URL';
const metadataHttp = new MetadataHttp(nodeUrl);
const metadataService = new MetadataTransactionService(metadataHttp);

// replace with key and new value
const key = KeyGenerator.generateUInt64Key('CERT');
const newValue = '000000';

const accountMetadataTransaction = metadataService.createAccountMetadataTransaction(
  Deadline.create(epochAdjustment),
  networkType,
  alicePublicAccount.address,
  key,
  newValue,
  bobAccount.publicAccount.address,
  UInt64.fromUint(0),
);

2. To avoid spamming the account with invalid metadata, 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 to be updated. Wrap the AccountMetadataTransaction inside an AggregateBondedTransaction and sign the transaction using Bob’s account.

// replace with meta.networkGenerationHash (nodeUrl + '/node/info')
const networkGenerationHash =
  '1DFB2FAA9E7F054168B0C5FCB84F4DEB62CC2B4D317D861F3168D161F54EA78B';
const signedAggregateTransaction = accountMetadataTransaction.pipe(
  mergeMap((transaction) => {
    const aggregateTransaction = AggregateTransaction.createBonded(
      Deadline.create(epochAdjustment),
      [transaction.toAggregate(bobAccount.publicAccount)],
      networkType,
      [],
      UInt64.fromUint(2000000),
    );
    const signedTransaction = bobAccount.sign(
      aggregateTransaction,
      networkGenerationHash,
    );
    return of(signedTransaction);
  }),
);

3. 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, locking the amount of symbol.xym required to announce the aggregate transaction.

interface SignedAggregateHashLock {
  readonly aggregate: SignedTransaction;
  readonly hashLock: SignedTransaction;
}

// replace with symbol.xym id
const networkCurrencyMosaicId = new MosaicId('5E62990DCAC5BE8A');
// replace with network currency divisibility
const networkCurrencyDivisibility = 6;

const signedAggregateHashLock = signedAggregateTransaction.pipe(
  mergeMap((signedAggregateTransaction) => {
    const hashLockTransaction = HashLockTransaction.create(
      Deadline.create(epochAdjustment),
      new Mosaic(
        networkCurrencyMosaicId,
        UInt64.fromUint(10 * Math.pow(10, networkCurrencyDivisibility)),
      ),
      UInt64.fromUint(480),
      signedAggregateTransaction,
      networkType,
      UInt64.fromUint(2000000),
    );
    const signedTransaction = bobAccount.sign(
      hashLockTransaction,
      networkGenerationHash,
    );
    const signedAggregateHashLock: SignedAggregateHashLock = {
      aggregate: signedAggregateTransaction,
      hashLock: signedTransaction,
    };
    console.log(
      'Aggregate Transaction Hash:',
      signedAggregateTransaction.hash + '\n',
    );
    console.log('HashLock Transaction Hash:', signedTransaction.hash + '\n');
    return of(signedAggregateHashLock);
  }),
);

Note

Bob will receive the locked funds back if Alice cosigns the aggregate during the next 480 blocks.

4. Announce the HashLockTransaction. Monitor the network until the transaction gets confirmed, and then announce the AggregateTransaction containing the AccountMetadataTransaction.

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(() => {
  signedAggregateHashLock
    .pipe(
      mergeMap((signedAggregateHashLock) =>
        transactionService.announceHashLockAggregateBonded(
          signedAggregateHashLock.hashLock,
          signedAggregateHashLock.aggregate,
          listener,
        ),
      ),
    )
    .subscribe(
      () => console.log('Transaction confirmed'),
      (err) => console.log(err),
      () => listener.close(),
    );
});
  1. Once the transaction gets confirmed, cosign the hash obtained in the third step using Alice’s profile.

symbol-cli transaction cosign --hash <transaction-hash> --profile alice
  1. Retrieve the metadata entries assigned to Alice’s account following the next guide.