From db2659a30ed9a076387d4efbddfa4d6efd85bee3 Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Tue, 12 Dec 2023 10:46:56 +0530 Subject: [PATCH] feat(aens): support update with raw pointers --- docs/guides/aens.md | 6 +-- src/aens.ts | 18 +++++-- src/tx/builder/field-types/index.ts | 1 + src/tx/builder/field-types/pointers2.ts | 62 +++++++++++++++++++++++++ src/tx/builder/schema.ts | 41 ++++++++++------ test/integration/aens.ts | 33 +++++++++---- test/integration/transaction.ts | 16 +++++++ 7 files changed, 149 insertions(+), 28 deletions(-) create mode 100644 src/tx/builder/field-types/pointers2.ts diff --git a/docs/guides/aens.md b/docs/guides/aens.md index 0c6069ba46..b2d1f7ca9f 100644 --- a/docs/guides/aens.md +++ b/docs/guides/aens.md @@ -170,19 +170,19 @@ Note: ## 2. Update a name Now that you own your AENS name you might want to update it in order to: -- Set pointers to `accounts`, `oracles`, `contracts` or `channels`. +- Set pointers to `accounts`, `oracles`, `contracts`, `channels`, or store binary data. - Extend the TTL before it expires. - By default a name will have a TTL of 180000 key blocks (~375 days). It cannot be extended longer than 180000 key blocks. ### Set pointers & update TTL ```js -import { getDefaultPointerKey } from '@aeternity/aepp-sdk' +import { getDefaultPointerKey, encode, Encoding } from '@aeternity/aepp-sdk' const name = 'testNameForTheGuide.chain' const oracle = 'ok_2519mBsgjJEVEFoRgno1ryDsn3BEaCZGRbXPEjThWYLX9MTpmk' const pointers = { account_pubkey: 'ak_2519mBsgjJEVEFoRgno1ryDsn3BEaCZGRbXPEjThWYLX9MTpmk', - customKey: 'ak_2519mBsgjJEVEFoRgno1ryDsn3BEaCZGRbXPEjThWYLX9MTpmk', + customKey: encode(Buffer.from('example data'), Encoding.Bytearray), [getDefaultPointerKey(oracle)]: oracle, // the same as `oracle_pubkey: oracle,` contract_pubkey: 'ct_2519mBsgjJEVEFoRgno1ryDsn3BEaCZGRbXPEjThWYLX9MTpmk', channel: 'ch_2519mBsgjJEVEFoRgno1ryDsn3BEaCZGRbXPEjThWYLX9MTpmk', diff --git a/src/aens.ts b/src/aens.ts index c736c530b1..0d0cbc68b2 100644 --- a/src/aens.ts +++ b/src/aens.ts @@ -7,10 +7,11 @@ */ import BigNumber from 'bignumber.js'; -import { genSalt } from './utils/crypto'; +import { genSalt, isAddressValid } from './utils/crypto'; import { commitmentHash, isAuctionName } from './tx/builder/helpers'; -import { Tag, AensName } from './tx/builder/constants'; -import { Encoded } from './utils/encoder'; +import { Tag, AensName, ConsensusProtocolVersion } from './tx/builder/constants'; +import { Encoded, Encoding } from './utils/encoder'; +import { UnsupportedProtocolError } from './utils/errors'; import { sendTransaction, SendTransactionOptions, getName } from './chain'; import { buildTxAsync, BuildTxOptions } from './tx/builder'; import { TransformNodeType } from './Node'; @@ -19,7 +20,7 @@ import AccountBase from './account/Base'; import { AddressEncodings } from './tx/builder/field-types/address'; interface KeyPointers { - [key: string]: Encoded.Generic; + [key: string]: Encoded.Generic; } /** @@ -101,9 +102,18 @@ export async function aensUpdate( ...pointers, }; + const hasRawPointers = Object.values(allPointers) + .some((v) => isAddressValid(v, Encoding.Bytearray)); + const isIris = (await options.onNode.getNodeInfo()) + .consensusProtocolVersion === ConsensusProtocolVersion.Iris; + if (hasRawPointers && isIris) { + throw new UnsupportedProtocolError('Raw pointers are available only in Ceres, the current protocol is Iris'); + } + const nameUpdateTx = await buildTxAsync({ ...options, tag: Tag.NameUpdateTx, + version: hasRawPointers ? 2 : 1, nameId: name, accountId: options.onAccount.address, pointers: Object.entries(allPointers) diff --git a/src/tx/builder/field-types/index.ts b/src/tx/builder/field-types/index.ts index 908d83d58b..39c1ad8a9e 100644 --- a/src/tx/builder/field-types/index.ts +++ b/src/tx/builder/field-types/index.ts @@ -18,6 +18,7 @@ export { default as nameFee } from './name-fee'; export { default as nameId } from './name-id'; export { default as nonce } from './nonce'; export { default as pointers } from './pointers'; +export { default as pointers2 } from './pointers2'; export { default as queryFee } from './query-fee'; export { default as raw } from './raw'; export { default as shortUInt } from './short-u-int'; diff --git a/src/tx/builder/field-types/pointers2.ts b/src/tx/builder/field-types/pointers2.ts new file mode 100644 index 0000000000..fe849ab3ef --- /dev/null +++ b/src/tx/builder/field-types/pointers2.ts @@ -0,0 +1,62 @@ +import { NamePointer as NamePointerString } from '../../../apis/node'; +import { toBytes } from '../../../utils/bytes'; +import { + Encoded, Encoding, decode, encode, +} from '../../../utils/encoder'; +import { isAddressValid } from '../../../utils/crypto'; +import { IllegalArgumentError, DecodeError, ArgumentError } from '../../../utils/errors'; +import address, { AddressEncodings, idTagToEncoding } from './address'; + +const ID_TAG = Buffer.from([1]); +const DATA_TAG = Buffer.from([2]); +const DATA_LENGTH_MAX = 1024; +const addressAny = address(...idTagToEncoding); + +// TODO: remove after fixing node types +type NamePointer = NamePointerString & { + id: Encoded.Generic; +}; + +export default { + /** + * Helper function to build pointers for name update TX + * @param pointers - Array of pointers + * `([ { key: 'account_pubkey', id: 'ak_32klj5j23k23j5423l434l2j3423'} ])` + * @returns Serialized pointers array + */ + serialize(pointers: NamePointer[]): Buffer[][] { + if (pointers.length > 32) { + throw new IllegalArgumentError(`Expected 32 pointers or less, got ${pointers.length} instead`); + } + return pointers.map(({ key, id }) => { + let payload; + if (isAddressValid(id, ...idTagToEncoding)) payload = [ID_TAG, addressAny.serialize(id)]; + if (isAddressValid(id, Encoding.Bytearray)) { + const data = decode(id); + if (data.length > DATA_LENGTH_MAX) { + throw new ArgumentError('Raw pointer', `shorter than ${DATA_LENGTH_MAX + 1} bytes`, `${data.length} bytes`); + } + payload = [DATA_TAG, data]; + } + if (payload == null) throw new DecodeError(`Unknown AENS pointer value: ${id}`); + return [toBytes(key), Buffer.concat(payload)]; + }); + }, + + /** + * Helper function to read pointers from name update TX + * @param pointers - Array of pointers + * @returns Deserialize pointer array + */ + deserialize(pointers: Array<[key: Buffer, id: Buffer]>): NamePointer[] { + return pointers.map(([bKey, bId]) => { + const tag = bId.subarray(0, 1); + const payload = bId.subarray(1); + let id; + if (tag.equals(ID_TAG)) id = addressAny.deserialize(payload); + if (tag.equals(DATA_TAG)) id = encode(payload, Encoding.Bytearray); + if (id == null) throw new DecodeError(`Unknown AENS pointer tag: ${tag}`); + return { key: bKey.toString(), id }; + }); + }, +}; diff --git a/src/tx/builder/schema.ts b/src/tx/builder/schema.ts index 4441ab37dd..7933622d92 100644 --- a/src/tx/builder/schema.ts +++ b/src/tx/builder/schema.ts @@ -7,8 +7,8 @@ import { Tag } from './constants'; import SchemaTypes from './SchemaTypes'; import { - uInt, shortUInt, coinAmount, name, nameId, nameFee, gasLimit, gasPrice, fee, - address, pointers, queryFee, entry, enumeration, mptree, shortUIntConst, string, encoded, raw, + uInt, shortUInt, coinAmount, name, nameId, nameFee, gasLimit, gasPrice, fee, address, pointers, + pointers2, queryFee, entry, enumeration, mptree, shortUIntConst, string, encoded, raw, array, boolean, ctVersion, abiVersion, ttl, nonce, map, withDefault, withFormatting, wrapped, } from './field-types'; import { Encoded, Encoding } from '../../utils/encoder'; @@ -129,6 +129,19 @@ interface MapOracles { const mapOracles = map(Encoding.OracleAddress, Tag.Oracle) as unknown as MapOracles; +// TODO: inline after dropping Iris compatibility +const clientTtl = withDefault(60 * 60, shortUInt); +// https://github.com/aeternity/protocol/blob/fd17982/AENS.md#update +const nameTtl = withFormatting( + (value) => { + const NAME_TTL = 180000; + value ??= NAME_TTL; + if (value >= 1 && value <= NAME_TTL) return value; + throw new ArgumentError('nameTtl', `a number between 1 and ${NAME_TTL} blocks`, value); + }, + shortUInt, +); + /** * @see {@link https://github.com/aeternity/protocol/blob/c007deeac4a01e401238412801ac7084ac72d60e/serializations.md#accounts-version-1-basic-accounts} */ @@ -193,18 +206,20 @@ export const txSchema = [{ accountId: address(Encoding.AccountAddress), nonce: nonce('accountId'), nameId, - // https://github.com/aeternity/protocol/blob/fd17982/AENS.md#update - nameTtl: withFormatting( - (nameTtl) => { - const NAME_TTL = 180000; - nameTtl ??= NAME_TTL; - if (nameTtl >= 1 && nameTtl <= NAME_TTL) return nameTtl; - throw new ArgumentError('nameTtl', `a number between 1 and ${NAME_TTL} blocks`, nameTtl); - }, - shortUInt, - ), + nameTtl, pointers, - clientTtl: withDefault(60 * 60, shortUInt), + clientTtl, + fee, + ttl, +}, { + tag: shortUIntConst(Tag.NameUpdateTx), + version: shortUIntConst(2), + accountId: address(Encoding.AccountAddress), + nonce: nonce('accountId'), + nameId, + nameTtl, + pointers: pointers2, + clientTtl, fee, ttl, }, { diff --git a/test/integration/aens.ts b/test/integration/aens.ts index c281bc40c7..3fa4009f48 100644 --- a/test/integration/aens.ts +++ b/test/integration/aens.ts @@ -145,14 +145,23 @@ describe('Aens', () => { }); const address = generateKeyPair().publicKey; - const pointers = { - myKey: address, - account_pubkey: address, - oracle_pubkey: encode(decode(address), Encoding.OracleAddress), - channel: encode(decode(address), Encoding.Channel), - contract_pubkey: buildContractId(address, 13), - }; - const pointersNode = Object.entries(pointers).map(([key, id]) => ({ key, id })); + let pointers: Parameters[1]; + let pointersNode: Array<{ key: string; id: typeof pointers[string] }>; + let isIris: boolean; + + before(async () => { + isIris = (await aeSdk.api.getNodeInfo()) + .consensusProtocolVersion === ConsensusProtocolVersion.Iris; + pointers = { + myKey: address, + ...!isIris && { 'my raw key': encode(Buffer.from('my raw value'), Encoding.Bytearray) }, + account_pubkey: address, + oracle_pubkey: encode(decode(address), Encoding.OracleAddress), + channel: encode(decode(address), Encoding.Channel), + contract_pubkey: buildContractId(address, 13), + }; + pointersNode = Object.entries(pointers).map(([key, id]) => ({ key, id })); + }); it('updates', async () => { const nameObject = await aeSdk.aensQuery(name); @@ -188,6 +197,14 @@ describe('Aens', () => { .to.be.rejectedWith('Expected 32 pointers or less, got 33 instead'); }); + it('throws error on setting too long raw pointer', async () => { + const nameObject = await aeSdk.aensQuery(name); + const pointersRaw = { raw: encode(Buffer.from('t'.repeat(1025)), Encoding.Bytearray) }; + await expect(nameObject.update(pointersRaw)).to.be.rejectedWith(isIris + ? 'Raw pointers are available only in Ceres, the current protocol is Iris' + : 'Raw pointer should be shorter than 1025 bytes, got 1025 bytes instead'); + }); + it('Extend name ttl', async () => { const nameObject = await aeSdk.aensQuery(name); const extendResult: Awaited> = await nameObject diff --git a/test/integration/transaction.ts b/test/integration/transaction.ts index 34f0e126d6..bac2bb47f5 100644 --- a/test/integration/transaction.ts +++ b/test/integration/transaction.ts @@ -97,6 +97,22 @@ describe('Transaction', () => { async () => aeSdk.buildTx({ tag: Tag.NameUpdateTx, accountId: senderId, nonce, nameId, nameTtl, pointers, clientTtl, }), + ], [ + 'name update v2', + 'tx_+JwiAqEB4TK48d23oE5jt/qWR5pUu8UlpTGn8bwM5JISGQMGf7ABoQL1zlEz+3+D5h4MF9POub3zp5zJ2fj6VUWGMNOhCyMYPAH4SfKOYWNjb3VudF9wdWJrZXmiAQHhMrjx3begTmO3+pZHmlS7xSWlMafxvAzkkhIZAwZ/sNWIdGVzdCBrZXmLAnRlc3QgdmFsdWUBhhCENFlgAABtaPdX', + async () => aeSdk.buildTx({ + tag: Tag.NameUpdateTx, + version: 2, + accountId: senderId, + nonce, + nameId, + nameTtl, + pointers: [ + ...pointers, + { key: 'test key', id: encode(Buffer.from('test value'), Encoding.Bytearray) }, + ], + clientTtl, + }), ], [ 'name revoke', 'tx_+E8jAaEB4TK48d23oE5jt/qWR5pUu8UlpTGn8bwM5JISGQMGf7ABoQL1zlEz+3+D5h4MF9POub3zp5zJ2fj6VUWGMNOhCyMYPIYPHaUyOAAA94BVgw==',