diff --git a/src/auction.test.ts b/src/auction.test.ts index d3fcbd8..d4bc200 100644 --- a/src/auction.test.ts +++ b/src/auction.test.ts @@ -1,12 +1,12 @@ -import { Registry, Account, createBid, INVALID_BID_ERROR } from './index'; -import { getConfig } from './testing/helper'; +import { Registry, Account, createBid, INVALID_BID_ERROR, RELEASE_FUNDS_ERROR, OWNER_MISMATCH_ERROR } from './index'; +import { checkAccountBalance, createTestAccounts, getConfig } from './testing/helper'; import { DENOM } from './constants'; jest.setTimeout(30 * 60 * 1000); const { chainId, rpcEndpoint, gqlEndpoint, privateKey, fee } = getConfig(); const duration = 60; -const txFees = 200000; +const txFees = Number(fee.amount[0].amount); const commitFee = '1000'; const revealFee = '1000'; @@ -28,21 +28,15 @@ const auctionTests = () => { registry = new Registry(gqlEndpoint, rpcEndpoint, { chainId }); // Create auction creator account - const mnenonic1 = Account.generateMnemonic(); - const auctionCreator = await Account.generateFromMnemonic(mnenonic1); - await auctionCreator.init(); - - await registry.sendCoins({ denom: DENOM, amount: creatorInitialBalance.toString(), destinationAddress: auctionCreator.address }, privateKey, fee); - auctionCreatorAccount = { address: auctionCreator.address, privateKey: auctionCreator.privateKey.toString('hex') }; + const auctionCreator = await createTestAccounts(1); + await registry.sendCoins({ denom: DENOM, amount: creatorInitialBalance.toString(), destinationAddress: auctionCreator[0].address }, privateKey, fee); + auctionCreatorAccount = { address: auctionCreator[0].address, privateKey: auctionCreator[0].privateKey.toString('hex') }; // Create bidder accounts + const accounts = await createTestAccounts(numBidders); for (let i = 0; i < numBidders; i++) { - const mnenonic = Account.generateMnemonic(); - const account = await Account.generateFromMnemonic(mnenonic); - await account.init(); - - await registry.sendCoins({ denom: DENOM, amount: bidderInitialBalance.toString(), destinationAddress: account.address }, privateKey, fee); - bidderAccounts.push({ address: account.address, privateKey: account.privateKey.toString('hex') }); + await registry.sendCoins({ denom: DENOM, amount: bidderInitialBalance.toString(), destinationAddress: accounts[i].address }, privateKey, fee); + bidderAccounts.push({ address: accounts[i].address, privateKey: accounts[i].privateKey.toString('hex') }); } }); @@ -111,12 +105,10 @@ const auctionTests = () => { // Check that the bid amounts are locked after reveal phase for (let i = 0; i < numBidders; i++) { - const [bidderrAccountObj] = await registry.getAccounts([bidderAccounts[i].address]); - expect(bidderrAccountObj).toBeDefined(); - - const [{ type, quantity }] = bidderrAccountObj.balance; + const { type, quantity } = await checkAccountBalance(registry, bidderAccounts[i].address); const actualBalance = parseInt(quantity); + // The bid amount, commit fee, reveal fee and tx fees (for commit and reveal txs) will be deducted from the bidder's acoount const expectedBalance = bidderInitialBalance - parseInt(bidAmounts[i]) - (2 * txFees) - parseInt(commitFee) - parseInt(revealFee); expect(actualBalance).toBe(expectedBalance); expect(type).toBe(DENOM); @@ -130,7 +122,7 @@ const auctionTests = () => { setTimeout(done, waitTime); }); - test('Check auction winner, status, and winner balance.', async () => { + test('Check auction status, winner address and balance.', async () => { const [auction] = await registry.getAuctionsByIds([auctionId]); expect(auction.status).toEqual('completed'); @@ -145,12 +137,10 @@ const auctionTests = () => { const winningPriceAmount = parseInt(auction.winnerPrice.quantity); - const [winnerAccountObj] = await registry.getAccounts([highestBidder.address]); - expect(winnerAccountObj).toBeDefined(); - - const [{ type, quantity }] = winnerAccountObj.balance; + const { type, quantity } = await checkAccountBalance(registry, highestBidder.address); const actualBalance = parseInt(quantity); + // The winning price will get deducted after auction completion expect(actualBalance).toBe(bidderInitialBalance - winningPriceAmount - (2 * txFees) - parseInt(commitFee)); expect(type).toBe(DENOM); }); @@ -164,7 +154,8 @@ const providerAuctionTestsWithBids = (bidAmounts: number[], numProviders: number let bidderAccounts: { address: string, privateKey: string, bid?: any }[] = []; let sortedBidders: { address: string, privateKey: string, bid?: any }[] = []; let filteredBidders: { address: string, privateKey: string, bid?: any }[] = []; - let invalidBidderAddresses: string[] = []; + let invalidBidderAddress: string; + let otherAccount: { address: string, privateKey: string }; const numBidders = bidAmounts.length; const maxPrice = 10 * lowestBidAmount; @@ -173,22 +164,23 @@ const providerAuctionTestsWithBids = (bidAmounts: number[], numProviders: number registry = new Registry(gqlEndpoint, rpcEndpoint, { chainId }); // Create auction creator account - const mnenonic1 = Account.generateMnemonic(); - const auctionCreator = await Account.generateFromMnemonic(mnenonic1); - await auctionCreator.init(); - - await registry.sendCoins({ denom: DENOM, amount: creatorInitialBalance.toString(), destinationAddress: auctionCreator.address }, privateKey, fee); - auctionCreatorAccount = { address: auctionCreator.address, privateKey: auctionCreator.privateKey.toString('hex') }; + const auctionCreator = await createTestAccounts(1); + await registry.sendCoins({ denom: DENOM, amount: creatorInitialBalance.toString(), destinationAddress: auctionCreator[0].address }, privateKey, fee); + auctionCreatorAccount = { address: auctionCreator[0].address, privateKey: auctionCreator[0].privateKey.toString('hex') }; // Create bidder accounts + const accounts = await createTestAccounts(numBidders); for (let i = 0; i < numBidders; i++) { - const mnenonic = Account.generateMnemonic(); - const account = await Account.generateFromMnemonic(mnenonic); - await account.init(); - - await registry.sendCoins({ denom: DENOM, amount: bidderInitialBalance.toString(), destinationAddress: account.address }, privateKey, fee); - bidderAccounts.push({ address: account.address, privateKey: account.privateKey.toString('hex') }); + await registry.sendCoins({ denom: DENOM, amount: bidderInitialBalance.toString(), destinationAddress: accounts[i].address }, privateKey, fee); + bidderAccounts.push({ address: accounts[i].address, privateKey: accounts[i].privateKey.toString('hex') }); } + + const mnenonic3 = Account.generateMnemonic(); + const otherAcc = await Account.generateFromMnemonic(mnenonic3); + await otherAcc.init(); + + await registry.sendCoins({ denom: DENOM, amount: creatorInitialBalance.toString(), destinationAddress: otherAcc.address }, privateKey, fee); + otherAccount = { address: otherAcc.address, privateKey: otherAcc.privateKey.toString('hex') }; }); test('Create a provider auction', async () => { @@ -210,12 +202,10 @@ const providerAuctionTestsWithBids = (bidAmounts: number[], numProviders: number expect(auction.auction?.status).toEqual('commit'); // Check that the total locked amount is deducted from the creator's account - const [creatorAccountObj] = await registry.getAccounts([auctionCreatorAccount.address]); - expect(creatorAccountObj).toBeDefined(); - - const [{ type, quantity }] = creatorAccountObj.balance; + const { type, quantity } = await checkAccountBalance(registry, auctionCreatorAccount.address); const actualBalance = parseInt(quantity); + // The locked amount and tx fees are deducted from the auction creator's account const expectedBalance = creatorInitialBalance - (maxPrice * numProviders) - txFees; expect(actualBalance).toBe(expectedBalance); expect(type).toBe(DENOM); @@ -233,7 +223,7 @@ const providerAuctionTestsWithBids = (bidAmounts: number[], numProviders: number for (const bidder of sortedBidders) { if (parseInt(bidder.bid.reveal.bidAmount) > maxPrice) { - invalidBidderAddresses.push(bidder.address); + invalidBidderAddress = bidder.address; } } @@ -264,7 +254,7 @@ const providerAuctionTestsWithBids = (bidAmounts: number[], numProviders: number fee ); } catch (error: any) { - if (invalidBidderAddresses.includes(bidderAccounts[i].address)) { + if (invalidBidderAddress === bidderAccounts[i].address) { expect(error.toString()).toContain(INVALID_BID_ERROR); } else { throw error; @@ -278,7 +268,7 @@ const providerAuctionTestsWithBids = (bidAmounts: number[], numProviders: number expect(auction.status).toEqual('reveal'); const expectedBids = sortedBidders.map((bidder: any) => { - if (invalidBidderAddresses.includes(bidder.address)) { + if (invalidBidderAddress === bidder.address) { return '0'; } return bidder.bid.reveal.bidAmount; @@ -311,17 +301,29 @@ const providerAuctionTestsWithBids = (bidAmounts: number[], numProviders: number const expectedWinningPrice = winningBidders[numWinners - 1].bid.reveal.bidAmount; expect(`${auction.winnerPrice.quantity}${auction.winnerPrice.type}`).toEqual(expectedWinningPrice); - const [creatorAccountObj] = await registry.getAccounts([auctionCreatorAccount.address]); - expect(creatorAccountObj).toBeDefined(); - - const [{ type, quantity }] = creatorAccountObj.balance; + const { type, quantity } = await checkAccountBalance(registry, auctionCreatorAccount.address); const actualBalance = parseInt(quantity); - // Check auction creator balance + // The total winning amount will get deducted and the leftover locked amount is returned const totalWinningAmount = parseInt(auction.winnerPrice.quantity) * winningBidders.length; const expectedCreatorBalance = creatorInitialBalance - totalWinningAmount - txFees; expect(actualBalance).toBe(expectedCreatorBalance); expect(type).toBe(DENOM); + + // Funds should not be given to winners on auction completion + for (let i = 0; i < winningBidders.length; i++) { + const bidWinner = winningBidders[i]; + + expect(auction.winnerAddresses[i]).toEqual(bidWinner.address); + expect(`${auction.winnerBids[i].quantity}${auction.winnerBids[i].type}`).toEqual(bidWinner.bid.reveal.bidAmount); + + const { type, quantity } = await checkAccountBalance(registry, bidWinner.address); + const actualBalance = parseInt(quantity); + + const expectedBidderBalance = bidderInitialBalance - (2 * txFees) - parseInt(commitFee); + expect(actualBalance).toBe(expectedBidderBalance); + expect(type).toBe(DENOM); + } }); test('Release funds and check provider balances.', async () => { @@ -333,20 +335,25 @@ const providerAuctionTestsWithBids = (bidAmounts: number[], numProviders: number const winningBidAmount = parseInt(auction.winnerPrice.quantity); - // Release funds - await registry.releaseFunds({ auctionId }, auctionCreatorAccount.privateKey, fee); + // Only owner can release funds + try { + await registry.releaseFunds({ auctionId }, otherAccount.privateKey, fee); + } catch (error: any) { + expect(error.toString()).toContain(OWNER_MISMATCH_ERROR); + } - // Check balances of auction winners + // Release funds + const auctionObj = await registry.releaseFunds({ auctionId }, auctionCreatorAccount.privateKey, fee); + expect(auctionObj.auction?.fundsReleased).toBe(true); + + // Auction winners get the winning amount after funds are released for (let i = 0; i < winningBidders.length; i++) { const bidWinner = winningBidders[i]; expect(auction.winnerAddresses[i]).toEqual(bidWinner.address); expect(`${auction.winnerBids[i].quantity}${auction.winnerBids[i].type}`).toEqual(bidWinner.bid.reveal.bidAmount); - const [winnerAccountObj] = await registry.getAccounts([bidWinner.address]); - expect(winnerAccountObj).toBeDefined(); - - const [{ type, quantity }] = winnerAccountObj.balance; + const { type, quantity } = await checkAccountBalance(registry, bidWinner.address); const actualBalance = parseInt(quantity); const expectedBidderBalance = bidderInitialBalance + winningBidAmount - (2 * txFees) - parseInt(commitFee); @@ -356,16 +363,11 @@ const providerAuctionTestsWithBids = (bidAmounts: number[], numProviders: number // Check balances of non-winners for (const bidder of losingBidders) { - const [bidderAccountObj] = await registry.getAccounts([bidder.address]); - expect(bidderAccountObj).toBeDefined(); - - const [{ type, quantity }] = bidderAccountObj.balance; + const { type, quantity } = await checkAccountBalance(registry, bidder.address); const actualBalance = parseInt(quantity); let expectedBidderBalance: number; - // Remove 'alnt' from bidAmount for comparison - const sanitizedBidAmount = parseInt(bidder.bid.reveal.bidAmount.toString().replace(/\D/g, ''), 10); - if (sanitizedBidAmount < maxPrice) { + if (invalidBidderAddress !== bidder.address) { expectedBidderBalance = bidderInitialBalance - (2 * txFees) - parseInt(commitFee); } else { // Bid is invalid, reveal fees are not returned @@ -375,19 +377,24 @@ const providerAuctionTestsWithBids = (bidAmounts: number[], numProviders: number expect(actualBalance).toBe(expectedBidderBalance); expect(type).toBe(DENOM); } + + // Funds cannot be released twice + try { + await registry.releaseFunds({ auctionId }, auctionCreatorAccount.privateKey, fee); + } catch (error: any) { + expect(error.toString()).toContain(RELEASE_FUNDS_ERROR); + } }); }; const providerAuctionTests = () => { - const bids1 = [10002000, 10009000, 10006000, 10004000]; + // With a bid amount greater than the max price + const bids1 = [10002000, 10009000, 10006000 * 10, 10004000]; + // Number of providers is less than number of bidders describe('Auction with numProviders < numBidders', () => providerAuctionTestsWithBids(bids1, 3)); // Number of providers is greater than number of bidders describe('Auction numProviders > numBidders', () => providerAuctionTestsWithBids(bids1, 5)); - - // With bid greater than max price - const bids2 = [10002000, 10009000, 100060000, 10004000]; - describe('Auction with a bid greater than the max price', () => providerAuctionTestsWithBids(bids2, 5)); }; describe('Vickrey Auction', () => auctionTests()); diff --git a/src/laconic-client.ts b/src/laconic-client.ts index c3a5685..5fcc940 100644 --- a/src/laconic-client.ts +++ b/src/laconic-client.ts @@ -14,7 +14,7 @@ import { MsgCancelBondEncodeObject, MsgCreateBondEncodeObject, MsgRefillBondEnco import { Coin } from './proto/cosmos/base/v1beta1/coin'; import { MsgAssociateBondEncodeObject, MsgDeleteNameEncodeObject, MsgDissociateBondEncodeObject, MsgDissociateRecordsEncodeObject, MsgReassociateRecordsEncodeObject, MsgReserveAuthorityEncodeObject, MsgSetAuthorityBondEncodeObject, MsgSetNameEncodeObject, MsgSetRecordEncodeObject, registryTypes, typeUrlMsgAssociateBond, typeUrlMsgDeleteName, typeUrlMsgDissociateBond, typeUrlMsgDissociateRecords, typeUrlMsgReassociateRecords, typeUrlMsgReserveAuthority, typeUrlMsgSetAuthorityBond, typeUrlMsgSetName, typeUrlMsgSetRecord, NAMESERVICE_ERRORS } from './types/cerc/registry/message'; import { MsgCommitBidEncodeObject, MsgCreateAuctionEncodeObject, MsgReleaseFundsEncodeObject, MsgRevealBidEncodeObject, auctionTypes, typeUrlMsgCommitBid, typeUrlMsgCreateAuction, typeUrlMsgReleaseFunds, typeUrlMsgRevealBid } from './types/cerc/auction/message'; -import { MsgOnboardParticipantEncodeObject, ONBOARDING_DISABLED_ERROR, onboardingTypes, typeUrlMsgOnboardParticipant } from './types/cerc/onboarding/message'; +import { INVALID_BID_ERROR, MsgOnboardParticipantEncodeObject, ONBOARDING_DISABLED_ERROR, onboardingTypes, OWNER_MISMATCH_ERROR, RELEASE_FUNDS_ERROR, typeUrlMsgOnboardParticipant } from './types/cerc/onboarding/message'; import { MsgAssociateBondResponse, MsgDeleteNameResponse, MsgDissociateBondResponse, MsgDissociateRecordsResponse, MsgReassociateRecordsResponse, MsgReserveAuthorityResponse, MsgSetAuthorityBondResponse, MsgSetNameResponse, MsgSetRecordResponse, Payload } from './proto/cerc/registry/v1/tx'; import { Record, Signature } from './proto/cerc/registry/v1/registry'; import { Account } from './account'; @@ -383,7 +383,7 @@ export class LaconicClient extends SigningStargateClient { } processWriteError (error: string) { - const errorMessage = [...NAMESERVICE_ERRORS, ONBOARDING_DISABLED_ERROR].find(message => error.includes(message)); + const errorMessage = [...NAMESERVICE_ERRORS, ONBOARDING_DISABLED_ERROR, INVALID_BID_ERROR, RELEASE_FUNDS_ERROR, OWNER_MISMATCH_ERROR].find(message => error.includes(message)); if (!errorMessage) { console.error(error); diff --git a/src/registry-client.ts b/src/registry-client.ts index e7d973f..4ab41ad 100644 --- a/src/registry-client.ts +++ b/src/registry-client.ts @@ -76,6 +76,7 @@ const auctionFields = ` quantity } numProviders + fundsReleased bids { bidderAddress status diff --git a/src/testing/helper.ts b/src/testing/helper.ts index 1efbb56..1f9be53 100644 --- a/src/testing/helper.ts +++ b/src/testing/helper.ts @@ -2,7 +2,7 @@ import assert from 'assert'; import yaml from 'node-yaml'; import semver from 'semver'; -import { Account } from '../index'; +import { Account, Registry } from '../index'; const DEFAULT_CHAIN_ID = 'laconic_9000-1'; @@ -48,3 +48,10 @@ export const createTestAccounts = async (numAccounts: number): Promise => { + const [winnerAccountObj] = await registry.getAccounts([address]); + const [{ type, quantity }] = winnerAccountObj.balance; + + return { type, quantity }; +}; diff --git a/src/types/cerc/onboarding/message.ts b/src/types/cerc/onboarding/message.ts index fa27d5b..26f32b8 100644 --- a/src/types/cerc/onboarding/message.ts +++ b/src/types/cerc/onboarding/message.ts @@ -17,6 +17,8 @@ export interface MsgOnboardParticipantEncodeObject extends EncodeObject { export const ONBOARDING_DISABLED_ERROR = 'Onboarding is disabled'; export const INVALID_BID_ERROR = 'Bid is higher than max price'; +export const RELEASE_FUNDS_ERROR = 'Auction funds already released'; +export const OWNER_MISMATCH_ERROR = 'Only auction owner can release funds'; interface ethPayload { address: string