Skip to content

Commit

Permalink
feat(aens): support update with raw pointers
Browse files Browse the repository at this point in the history
  • Loading branch information
davidyuk committed Jan 25, 2024
1 parent 4ced3fb commit db2659a
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 28 deletions.
6 changes: 3 additions & 3 deletions docs/guides/aens.md
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
18 changes: 14 additions & 4 deletions src/aens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -19,7 +20,7 @@ import AccountBase from './account/Base';
import { AddressEncodings } from './tx/builder/field-types/address';

interface KeyPointers {
[key: string]: Encoded.Generic<AddressEncodings>;
[key: string]: Encoded.Generic<AddressEncodings | Encoding.Bytearray>;
}

/**
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions src/tx/builder/field-types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
62 changes: 62 additions & 0 deletions src/tx/builder/field-types/pointers2.ts
Original file line number Diff line number Diff line change
@@ -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<AddressEncodings | Encoding.Bytearray>;
};

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)];

This comment has been minimized.

Copy link
@janmichek

janmichek May 9, 2024

Collaborator

@davidyuk how to decode the pointer key from response correctly? eg here: https://testnet.aeternity.io/mdw/v2/names/testaensrawpointers.chain

This comment has been minimized.

Copy link
@davidyuk

davidyuk May 14, 2024

Author Member

The best is to use Buffer

> Buffer.from('UG9pbnRlcjE=', 'base64').toString()
'Pointer1'
> Buffer.from('UG9pbnRlcjI=', 'base64').toString()
'Pointer2'
> Buffer.from('UG9pbnRlcjM=', 'base64').toString()
'Pointer3'
> Buffer.from('UG9pbnRlcjQ=', 'base64').toString()
'Pointer4'
});
},

/**
* 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 };
});
},
};
41 changes: 28 additions & 13 deletions src/tx/builder/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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}
*/
Expand Down Expand Up @@ -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,
}, {
Expand Down
33 changes: 25 additions & 8 deletions test/integration/aens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AeSdk['aensUpdate']>[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);
Expand Down Expand Up @@ -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<ReturnType<typeof aeSdk.aensUpdate>> = await nameObject
Expand Down
16 changes: 16 additions & 0 deletions test/integration/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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==',
Expand Down

0 comments on commit db2659a

Please sign in to comment.