Skip to content

Commit

Permalink
Merge pull request #2040 from aeternity/fix-aens
Browse files Browse the repository at this point in the history
AENS fixes and improvements
  • Loading branch information
davidyuk authored Feb 24, 2025
2 parents 11287f7 + c8dc8cc commit 5c23dea
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 53 deletions.
66 changes: 52 additions & 14 deletions src/aens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import { BigNumber } from 'bignumber.js';
import { genSalt, isAddressValid } from './utils/crypto.js';
import { commitmentHash, isAuctionName } from './tx/builder/helpers.js';
import { commitmentHash, isAuctionName, produceNameId } from './tx/builder/helpers.js';
import { Tag, AensName } from './tx/builder/constants.js';
import { Encoded, Encoding } from './utils/encoder.js';
import { LogicError } from './utils/errors.js';
Expand Down Expand Up @@ -47,9 +47,16 @@ interface NamePreclaimOptions
Optional<SendTransactionOptions, 'onAccount' | 'onNode'> {}

interface NameClaimOptions
extends BuildTxOptions<Tag.NameClaimTx, 'accountId' | 'nameSalt' | 'name'>,
extends BuildTxOptions<Tag.NameClaimTx, 'accountId' | 'name'>,
Optional<SendTransactionOptions, 'onAccount' | 'onNode'> {}

class NotAuctionNameError extends LogicError {
constructor(name: AensName, action: string) {
super(`Can't ${action} because ${name} is not an auction name`);
this.name = 'NotAuctionNameError';
}
}

/**
* @category AENS
* @example
Expand Down Expand Up @@ -80,6 +87,13 @@ export default class Name {
this.options = options;
}

/**
* Name ID encoded as nm_-prefixed string
*/
get id(): Encoded.Name {
return produceNameId(this.value);
}

/**
* Revoke a name
* @param options - Options
Expand Down Expand Up @@ -175,8 +189,7 @@ export default class Name {
}

/**
* Query the AENS name info from the node
* and return the object with info and predefined functions for manipulating name
* Query the AENS name info from the node and return the object with info
* @param options - Options
* @example
* ```js
Expand All @@ -190,7 +203,7 @@ export default class Name {
owner: Encoded.AccountAddress;
}
> {
const onNode = this.options.onNode ?? options.onNode;
const onNode = options.onNode ?? this.options.onNode;
const nameEntry = await onNode.getNameEntryByName(this.value);
return {
...nameEntry,
Expand All @@ -199,6 +212,31 @@ export default class Name {
};
}

/**
* Query the AENS auction info from the node and return the object with info
* @param options - Options
* @example
* ```js
* const auctionEntry = await name.getAuctionState()
* console.log(auctionEntry.highestBidder)
* ```
*/
async getAuctionState(options: { onNode?: Node } = {}): Promise<
Awaited<ReturnType<Node['getAuctionEntryByName']>> & {
id: Encoded.Name;
highestBidder: Encoded.AccountAddress;
}
> {
if (!isAuctionName(this.value)) throw new NotAuctionNameError(this.value, 'get auction state');
const onNode = options.onNode ?? this.options.onNode;
const nameEntry = await onNode.getAuctionEntryByName(this.value);
return {
...nameEntry,
id: nameEntry.id as Encoded.Name,
highestBidder: nameEntry.highestBidder as Encoded.AccountAddress,
};
}

/**
*
* @param nameTtl - represents in number of blocks (max and default is 180000)
Expand All @@ -225,10 +263,10 @@ export default class Name {
const opt = { ...this.options, ...options };
const tx = await buildTxAsync({
_isInternalBuild: true,
nameSalt: this.#salt,
...opt,
tag: Tag.NameClaimTx,
accountId: opt.onAccount.address,
nameSalt: this.#salt,
name: this.value,
});
return sendTransaction(tx, opt);
Expand All @@ -242,19 +280,21 @@ export default class Name {
* await name.preclaim({ ttl, fee, nonce })
* ```
*/
async preclaim(options: NamePreclaimOptions = {}): ReturnType<typeof sendTransaction> {
async preclaim(
options: NamePreclaimOptions = {},
): Promise<Awaited<ReturnType<typeof sendTransaction>> & { nameSalt: number }> {
const opt = { ...this.options, ...options };
const salt = genSalt();
const nameSalt = genSalt();
const tx = await buildTxAsync({
_isInternalBuild: true,
...opt,
tag: Tag.NamePreclaimTx,
accountId: opt.onAccount.address,
commitmentId: commitmentHash(this.value, salt),
commitmentId: commitmentHash(this.value, nameSalt),
});
const result = await sendTransaction(tx, opt);
this.#salt = salt;
return result;
this.#salt = nameSalt;
return { ...result, nameSalt };
}

/**
Expand All @@ -272,9 +312,7 @@ export default class Name {
nameFee: number | string | BigNumber,
options: Omit<NameClaimOptions, 'nameFee'> = {},
): ReturnType<typeof sendTransaction> {
if (!isAuctionName(this.value)) {
throw new LogicError('This is not auction name, so cant make a bid!');
}
if (!isAuctionName(this.value)) throw new NotAuctionNameError(this.value, 'make a bid');
const opt = { ...this.options, ...options };
const tx = await buildTxAsync({
_isInternalBuild: true,
Expand Down
124 changes: 85 additions & 39 deletions test/integration/aens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ describe('Aens', () => {
name = new Name(randomName(30), aeSdk.getContext());
});

it('gives name id', () => {
expect(name.id).to.satisfies((id: string) => id.startsWith(Encoding.Name));
expect(name.id).to.equal(produceNameId(name.value));
});

it('claims a name', async () => {
const preclaimRes = await name.preclaim();
assertNotNull(preclaimRes.tx);
Expand All @@ -54,6 +59,7 @@ describe('Aens', () => {
blockHash: preclaimRes.blockHash,
encodedTx: preclaimRes.encodedTx,
hash: preclaimRes.hash,
nameSalt: preclaimRes.nameSalt,
signatures: [preclaimRes.signatures[0]],
rawTx: preclaimRes.rawTx,
});
Expand Down Expand Up @@ -86,13 +92,24 @@ describe('Aens', () => {

assertNotNull(claimRes.blockHeight);
expect(await aeSdk.api.getNameEntryByName(name.value)).to.eql({
id: produceNameId(name.value),
id: name.id,
owner: aeSdk.address,
pointers: [],
ttl: claimRes.blockHeight + 180000,
});
}).timeout(timeoutBlock);

it('claims a name using different Name instances', async () => {
const string = randomName(30);

const name1 = new Name(string, aeSdk.getContext());
const { nameSalt } = await name1.preclaim();
expect(nameSalt).to.be.a('number');

const name2 = new Name(string, aeSdk.getContext());
await name2.claim({ nameSalt });
}).timeout(timeoutBlock);

it('claims a long name without preclaim', async () => {
const nameString = randomName(30);
const n = new Name(nameString, aeSdk.getContext());
Expand Down Expand Up @@ -143,7 +160,7 @@ describe('Aens', () => {

assertNotNull(claimRes.blockHeight);
expect(await aeSdk.api.getNameEntryByName(n.value)).to.eql({
id: produceNameId(n.value),
id: n.id,
owner: aeSdk.address,
pointers: [],
ttl: claimRes.blockHeight + 180000,
Expand All @@ -158,46 +175,75 @@ describe('Aens', () => {
expect(preclaimRes.tx.accountId).to.equal(onAccount.address);
});

(isLimitedCoins ? it.skip : it)('starts a unicode name auction and makes a bid', async () => {
const nameShortString = ${randomString(4)}.chain`;
ensureName(nameShortString);
const n = new Name(nameShortString, aeSdk.getContext());
await n.preclaim();
await n.claim();
(isLimitedCoins ? describe.skip : describe)('Auction', () => {
let auction: Name;

const bidFee = computeBidFee(n.value);
const onAccount = Object.values(aeSdk.accounts)[1];
const bidRes = await n.bid(bidFee, { onAccount });
assertNotNull(bidRes.tx);
assertNotNull(bidRes.signatures);
expect(bidRes).to.eql({
tx: {
fee: bidRes.tx.fee,
nonce: bidRes.tx.nonce,
accountId: onAccount.address,
name: nameShortString,
nameSalt: 0,
nameFee: 3008985000000000000n,
version: 2,
ttl: bidRes.tx.ttl,
type: 'NameClaimTx',
},
blockHeight: bidRes.blockHeight,
blockHash: bidRes.blockHash,
encodedTx: bidRes.encodedTx,
hash: bidRes.hash,
signatures: [bidRes.signatures[0]],
rawTx: bidRes.rawTx,
before(() => {
const nameShortString = ${randomString(4)}.chain` as const;
auction = new Name(nameShortString, aeSdk.getContext());
});

it('starts a unicode name auction', async () => {
await auction.preclaim();
await auction.claim();
});

it('fails to query name state of auction', async () => {
await expect(auction.getState()).to.be.rejectedWith(
RestError,
`v3/names/%C3%A6${auction.value.slice(1)} error: Name not found`,
);
});

it('queries auction state from the node', async () => {
const state = await auction.getAuctionState();
expect(state).to.eql(await aeSdk.api.getAuctionEntryByName(auction.value));
expect(state).to.eql({
id: auction.id,
startedAt: state.startedAt,
endsAt: 480 + state.startedAt,
highestBidder: aeSdk.address,
highestBid: 2865700000000000000n,
});
});

it('makes a bid', async () => {
const bidFee = computeBidFee(auction.value);
const onAccount = Object.values(aeSdk.accounts)[1];
const bidRes = await auction.bid(bidFee, { onAccount });
assertNotNull(bidRes.tx);
assertNotNull(bidRes.signatures);
expect(bidRes).to.eql({
tx: {
fee: bidRes.tx.fee,
nonce: bidRes.tx.nonce,
accountId: onAccount.address,
name: auction.value,
nameSalt: 0,
nameFee: 3008985000000000000n,
version: 2,
ttl: bidRes.tx.ttl,
type: 'NameClaimTx',
},
blockHeight: bidRes.blockHeight,
blockHash: bidRes.blockHash,
encodedTx: bidRes.encodedTx,
hash: bidRes.hash,
signatures: [bidRes.signatures[0]],
rawTx: bidRes.rawTx,
});
});
await expect(n.getState()).to.be.rejectedWith(
RestError,
`v3/names/%C3%A6${n.value.slice(1)} error: Name not found`,
);
});

it('queries state from the node', async () => {
const state = await name.getState();
expect(state).to.eql(await aeSdk.api.getNameEntryByName(name.value));
expect(state).to.eql({
id: name.id,
owner: aeSdk.address,
ttl: state.ttl,
pointers: [],
});
});

it('throws error on querying non-existent name', async () => {
Expand Down Expand Up @@ -271,7 +317,7 @@ describe('Aens', () => {
fee: updateRes.tx.fee,
nonce: updateRes.tx.nonce,
accountId: aeSdk.address,
nameId: produceNameId(name.value),
nameId: name.id,
nameTtl: 180000,
pointers: pointersNode,
clientTtl: 3600,
Expand Down Expand Up @@ -353,7 +399,7 @@ describe('Aens', () => {
const onAccount = Object.values(aeSdk.accounts)[1];
const spendRes = await aeSdk.spend(100, name.value, { onAccount });
assertNotNull(spendRes.tx);
expect(spendRes.tx.recipientId).to.equal(produceNameId(name.value));
expect(spendRes.tx.recipientId).to.equal(name.id);
});

it('transfers name', async () => {
Expand All @@ -368,7 +414,7 @@ describe('Aens', () => {
fee: transferRes.tx.fee,
nonce: transferRes.tx.nonce,
accountId: aeSdk.address,
nameId: produceNameId(name.value),
nameId: name.id,
recipientId: recipient,
version: 1,
ttl: transferRes.tx.ttl,
Expand Down Expand Up @@ -396,7 +442,7 @@ describe('Aens', () => {
fee: revokeRes.tx.fee,
nonce: revokeRes.tx.nonce,
accountId: onAccount.address,
nameId: produceNameId(name.value),
nameId: name.id,
version: 1,
ttl: revokeRes.tx.ttl,
type: 'NameRevokeTx',
Expand Down
8 changes: 8 additions & 0 deletions tooling/autorest/node.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@ directive:
Convert time as milliseconds to dates
https://github.com/aeternity/aeternity/issues/4386
- from: openapi-document
where: $.components.schemas.AuctionEntry
transform: >
$.required.push('started_at');
reason: >
marks additional fields as required
remove after fixing https://github.com/aeternity/aeternity/pull/4537
- from: openapi-document
where: $.components.schemas
transform: >
Expand Down
1 change: 1 addition & 0 deletions tooling/autorest/postprocessing.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const nodeBigIntPropertyNames = [
'fee',
'amount',
'nameFee',
'highestBid',
'channelAmount',
'initiatorAmount',
'responderAmount',
Expand Down

0 comments on commit 5c23dea

Please sign in to comment.