diff --git a/src/aens.ts b/src/aens.ts index bb6fc583eb..9006bb7dd8 100644 --- a/src/aens.ts +++ b/src/aens.ts @@ -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'; @@ -47,9 +47,16 @@ interface NamePreclaimOptions Optional {} interface NameClaimOptions - extends BuildTxOptions, + extends BuildTxOptions, Optional {} +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 @@ -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 @@ -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 @@ -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, @@ -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> & { + 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) @@ -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); @@ -242,19 +280,21 @@ export default class Name { * await name.preclaim({ ttl, fee, nonce }) * ``` */ - async preclaim(options: NamePreclaimOptions = {}): ReturnType { + async preclaim( + options: NamePreclaimOptions = {}, + ): Promise> & { 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 }; } /** @@ -272,9 +312,7 @@ export default class Name { nameFee: number | string | BigNumber, options: Omit = {}, ): ReturnType { - 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, diff --git a/test/integration/aens.ts b/test/integration/aens.ts index 77472029f6..aed42d123e 100644 --- a/test/integration/aens.ts +++ b/test/integration/aens.ts @@ -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); @@ -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, }); @@ -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()); @@ -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, @@ -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 () => { @@ -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, @@ -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 () => { @@ -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, @@ -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', diff --git a/tooling/autorest/node.yaml b/tooling/autorest/node.yaml index 3f0ec6c131..12381fa5db 100644 --- a/tooling/autorest/node.yaml +++ b/tooling/autorest/node.yaml @@ -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: > diff --git a/tooling/autorest/postprocessing.js b/tooling/autorest/postprocessing.js index df9905391d..02a2ce9500 100644 --- a/tooling/autorest/postprocessing.js +++ b/tooling/autorest/postprocessing.js @@ -6,6 +6,7 @@ const nodeBigIntPropertyNames = [ 'fee', 'amount', 'nameFee', + 'highestBid', 'channelAmount', 'initiatorAmount', 'responderAmount',