registry-sdk/src/auction.test.ts
2024-09-23 16:39:59 +05:30

383 lines
15 KiB
TypeScript

import { Registry, Account, createBid, INVALID_BID_ERROR } from './index';
import { 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 commitFee = '1000';
const revealFee = '1000';
const creatorInitialBalance = 1000000000000;
const bidderInitialBalance = 20000000;
const lowestBidAmount = 10000000;
const auctionTests = () => {
let registry: Registry;
let auctionId: string;
let auctionCreatorAccount: { address: string, privateKey: string };
let bidderAccounts: { address: string, privateKey: string, bid?: any }[] = [];
const numBidders = 3;
let bidAmounts: string[] = [];
beforeAll(async () => {
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') };
// Create bidder accounts
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') });
}
});
test('Create a vickrey auction', async () => {
const minimumBid = '1000000';
const auction = await registry.createAuction(
{
commitsDuration: duration.toString(),
revealsDuration: duration.toString(),
denom: DENOM,
commitFee,
revealFee,
minimumBid
},
auctionCreatorAccount.privateKey,
fee
);
expect(auction.auction?.id).toBeDefined();
auctionId = auction.auction?.id || '';
expect(auction.auction?.status).toEqual('commit');
});
test('Commit bids.', async () => {
for (let i = 0; i < numBidders; i++) {
bidAmounts.push((lowestBidAmount + (i * 500)).toString());
bidderAccounts[i].bid = await createBid(chainId, auctionId, bidderAccounts[i].address, `${bidAmounts[i]}${DENOM}`);
await registry.commitBid({ auctionId, commitHash: bidderAccounts[i].bid.commitHash }, bidderAccounts[i].privateKey, fee);
}
const [auction] = await registry.getAuctionsByIds([auctionId]);
expect(auction.status).toEqual('commit');
expect(auction.bids.length).toEqual(3);
auction.bids.forEach((bid: any) => {
expect(bid.status).toEqual('commit');
});
});
test('Wait for reveal phase.', (done) => {
const commitTime = duration * 1000;
const waitTime = commitTime + (6 * 1000);
setTimeout(done, waitTime);
});
test('Reveal bids.', async () => {
const [auction] = await registry.getAuctionsByIds([auctionId]);
expect(auction.status).toEqual('reveal');
for (let i = 0; i < numBidders; i++) {
await registry.revealBid({ auctionId, reveal: bidderAccounts[i].bid.revealString }, bidderAccounts[i].privateKey, fee);
}
});
test('Check bids are revealed', async () => {
const [auction] = await registry.getAuctionsByIds([auctionId]);
expect(auction.status).toEqual('reveal');
auction.bids.forEach((bid: any) => {
expect(bid.status).toEqual('reveal');
});
const expectedBidAmounts = bidAmounts.map(bidAmount => { return { quantity: bidAmount.toString(), type: DENOM }; });
const actualBidAmounts = auction.bids.map((bid: any) => bid.bidAmount);
expect(actualBidAmounts).toEqual(expect.arrayContaining(expectedBidAmounts));
// 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 actualBalance = parseInt(quantity);
const expectedBalance = bidderInitialBalance - parseInt(bidAmounts[i]) - (2 * txFees) - parseInt(commitFee) - parseInt(revealFee);
expect(actualBalance).toBe(expectedBalance);
expect(type).toBe(DENOM);
}
});
test('Wait for auction completion.', (done) => {
const revealTime = duration * 1000;
const waitTime = revealTime + (6 * 1000);
setTimeout(done, waitTime);
});
test('Check auction winner, status, and winner balance.', async () => {
const [auction] = await registry.getAuctionsByIds([auctionId]);
expect(auction.status).toEqual('completed');
const highestBidder = bidderAccounts[bidderAccounts.length - 1];
const secondHighestBidder = bidderAccounts[bidderAccounts.length - 2];
expect(auction.winnerAddresses).toHaveLength(1);
expect(auction.winnerAddresses[0]).toEqual(highestBidder.address);
expect(highestBidder.bid.reveal.bidAmount).toEqual(`${auction.winnerBids[0].quantity}${auction.winnerBids[0].type}`);
expect(secondHighestBidder.bid.reveal.bidAmount).toEqual(`${auction.winnerPrice.quantity}${auction.winnerPrice.type}`);
const winningPriceAmount = parseInt(auction.winnerPrice.quantity);
const [winnerAccountObj] = await registry.getAccounts([highestBidder.address]);
expect(winnerAccountObj).toBeDefined();
const [{ type, quantity }] = winnerAccountObj.balance;
const actualBalance = parseInt(quantity);
expect(actualBalance).toBe(bidderInitialBalance - winningPriceAmount - (2 * txFees) - parseInt(commitFee));
expect(type).toBe(DENOM);
});
};
const providerAuctionTestsWithBids = (bidAmounts: number[], numProviders: number) => {
let registry: Registry;
let auctionId: string;
let auctionCreatorAccount: { address: string, privateKey: string };
let bidderAccounts: { address: string, privateKey: string, bid?: any }[] = [];
let sortedBidders: { address: string, privateKey: string, bid?: any }[] = [];
let invalidBidderAddresses: string[] = [];
const numBidders = bidAmounts.length;
const maxPrice = 10 * lowestBidAmount;
beforeAll(async () => {
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') };
// Create bidder accounts
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') });
}
});
test('Create a provider auction', async () => {
const auction = await registry.createProviderAuction(
{
commitsDuration: duration.toString(),
revealsDuration: duration.toString(),
denom: DENOM,
commitFee,
revealFee,
maxPrice: maxPrice.toString(),
numProviders
},
auctionCreatorAccount.privateKey, fee
);
expect(auction.auction?.id).toBeDefined();
auctionId = auction.auction?.id || '';
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 actualBalance = parseInt(quantity);
const expectedBalance = creatorInitialBalance - (maxPrice * numProviders) - txFees;
expect(actualBalance).toBe(expectedBalance);
expect(type).toBe(DENOM);
});
test('Commit bids.', async () => {
for (let i = 0; i < numBidders; i++) {
bidderAccounts[i].bid = await createBid(chainId, auctionId, bidderAccounts[i].address, `${bidAmounts[i]}${DENOM}`);
await registry.commitBid({ auctionId, commitHash: bidderAccounts[i].bid.commitHash }, bidderAccounts[i].privateKey, fee);
}
sortedBidders = bidderAccounts.slice().sort((a, b) => {
return parseInt(a.bid.reveal.bidAmount) - parseInt(b.bid.reveal.bidAmount);
});
for (const bidder of sortedBidders) {
if (parseInt(bidder.bid.reveal.bidAmount) > maxPrice) {
invalidBidderAddresses.push(bidder.address);
}
}
const [auction] = await registry.getAuctionsByIds([auctionId]);
expect(auction.status).toEqual('commit');
expect(auction.bids.length).toEqual(numBidders);
auction.bids.forEach((bid: any) => {
expect(bid.status).toEqual('commit');
});
});
test('Wait for reveal phase.', (done) => {
const commitTime = duration * 1000;
const waitTime = commitTime + (6 * 1000);
setTimeout(done, waitTime);
});
test('Reveal bids.', async () => {
const [auction] = await registry.getAuctionsByIds([auctionId]);
expect(auction.status).toEqual('reveal');
for (let i = 0; i < numBidders; i++) {
try {
await registry.revealBid(
{ auctionId, reveal: bidderAccounts[i].bid.revealString },
bidderAccounts[i].privateKey,
fee
);
} catch (error: any) {
if (invalidBidderAddresses.includes(bidderAccounts[i].address)) {
expect(error.toString()).toContain(INVALID_BID_ERROR);
} else {
throw error;
}
}
}
});
test('Check bids are revealed', async () => {
const [auction] = await registry.getAuctionsByIds([auctionId]);
expect(auction.status).toEqual('reveal');
const expectedBids = sortedBidders.map((bidder: any) => {
if (invalidBidderAddresses.includes(bidder.address)) {
return '0';
}
return bidder.bid.reveal.bidAmount;
});
const actualBidAmounts = auction.bids.map((bid: any) => `${bid.bidAmount.quantity}${bid.bidAmount.type}`);
expect(actualBidAmounts).toEqual(expect.arrayContaining(expectedBids));
});
test('Wait for auction completion.', (done) => {
const revealTime = duration * 1000;
const waitTime = revealTime + (6 * 1000);
setTimeout(done, waitTime);
});
test('Check auction winner, status, and all bidder balances.', async () => {
const [auction] = await registry.getAuctionsByIds([auctionId]);
expect(auction.status).toEqual('completed');
// Filter bidders to exclude bids exceeding maxPrice
const filteredBidders = sortedBidders.filter(bidder => {
return parseInt(bidder.bid.reveal.bidAmount) <= parseInt(`${maxPrice}${DENOM}`);
});
const numWinners = Math.min(filteredBidders.length, numProviders);
const winningBidders = filteredBidders.slice(0, numWinners);
const losingBidders = bidderAccounts.filter(bidder => !winningBidders.includes(bidder));
// Check winner price is equal to highest winning bid
const winningBidAmount = parseInt(auction.winnerPrice.quantity);
const expectedWinningPrice = winningBidders[numWinners - 1].bid.reveal.bidAmount;
expect(`${auction.winnerPrice.quantity}${auction.winnerPrice.type}`).toEqual(expectedWinningPrice);
// Check balances of auction winners
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 actualBalance = parseInt(quantity);
const expectedBidderBalance = bidderInitialBalance + winningBidAmount - (2 * txFees) - parseInt(commitFee);
expect(actualBalance).toBe(expectedBidderBalance);
expect(type).toBe(DENOM);
}
// 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 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) {
expectedBidderBalance = bidderInitialBalance - (2 * txFees) - parseInt(commitFee);
} else {
// Bid is invalid, reveal fees are not returned
expectedBidderBalance = bidderInitialBalance - (2 * txFees) - parseInt(commitFee) - parseInt(revealFee);
}
expect(actualBalance).toBe(expectedBidderBalance);
expect(type).toBe(DENOM);
}
const [creatorAccountObj] = await registry.getAccounts([auctionCreatorAccount.address]);
expect(creatorAccountObj).toBeDefined();
const [{ type, quantity }] = creatorAccountObj.balance;
const actualBalance = parseInt(quantity);
// Check auction creator balance
const totalWinningAmount = parseInt(auction.winnerPrice.quantity) * winningBidders.length;
const expectedCreatorBalance = creatorInitialBalance - totalWinningAmount - txFees;
expect(actualBalance).toBe(expectedCreatorBalance);
expect(type).toBe(DENOM);
});
};
const providerAuctionTests = () => {
const bids1 = [10002000, 10009000, 10006000, 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());
describe('Provider Auction', () => providerAuctionTests());