diff --git a/src/auction.test.ts b/src/auction.test.ts index 999a911..c859c45 100644 --- a/src/auction.test.ts +++ b/src/auction.test.ts @@ -1,6 +1,6 @@ import assert from 'assert'; -import { Registry, Account } from './index'; +import { Registry, Account, createBid } from './index'; import { getConfig } from './testing/helper'; jest.setTimeout(30 * 60 * 1000); @@ -10,7 +10,7 @@ const { chainId, restEndpoint, gqlEndpoint, privateKey, accountAddress, fee } = const auctionTests = (numBidders = 3) => { let registry: Registry; - const accounts: { address: string, privateKey: string }[] = []; + const accounts: { address: string, privateKey: string, bid?: any }[] = []; let auctionId: string; let authorityName: string; @@ -28,11 +28,11 @@ const auctionTests = (numBidders = 3) => { await account.init(); const bidderAddress = account.formattedCosmosAddress; assert(bidderAddress) - await registry.sendCoins({ denom: 'uwire', amount: '1000000000', destinationAddress: bidderAddress }, accountAddress, privateKey, fee); + await registry.sendCoins({ denom: 'aphoton', amount: '1000000000', destinationAddress: bidderAddress }, accountAddress, privateKey, fee); accounts.push({ address: bidderAddress, privateKey: account.privateKey.toString('hex') }); } - accounts.unshift({ address: accountAddress, privateKey }); + accounts[0] = { address: accountAddress, privateKey }; }); test('Reserve authority.', async () => { @@ -51,6 +51,68 @@ const auctionTests = (numBidders = 3) => { auctionId = record.auction.id; }); + + test('Commit bids.', async () => { + for (let i = 0; i < numBidders; i++) { + accounts[i].bid = await createBid(chainId, auctionId, accounts[i].address, `${10000000 + (i * 500)}aphoton`); + await registry.commitBid({ auctionId, commitHash: accounts[i].bid.commitHash }, accounts[i].address, accounts[i].privateKey, fee); + } + }); + + test('Check bids are committed', async () => { + const [record] = await registry.lookupAuthorities([authorityName], true); + expect(record.auction.id).toBeDefined(); + expect(record.auction.status).toEqual('commit'); + expect(record.auction.bids).toHaveLength(accounts.length); + + record.auction.bids.forEach((bid: any) => { + expect(bid.status).toEqual('commit'); + }); + }); + + test('Wait for reveal phase.', (done) => { + setTimeout(done, 60 * 1000); + }); + + test('Reveal bids.', async () => { + const [auction] = await registry.getAuctionsByIds([auctionId]); + expect(auction.status).toEqual('reveal'); + + for (let i = 0; i < numBidders; i++) { + // eslint-disable-next-line no-await-in-loop + await registry.revealBid({ auctionId, reveal: accounts[i].bid.revealString }, accounts[i].address, accounts[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'); + }); + }); + + test('Wait for auction completion.', (done) => { + setTimeout(done, 60 * 1000); + }); + + test('Check auction winner, authority owner and status.', async () => { + const [auction] = await registry.getAuctionsByIds([auctionId]); + expect(auction.status).toEqual('completed'); + + const highestBidder = accounts[accounts.length - 1]; + const secondHighestBidder = (accounts.length > 1 ? accounts[accounts.length - 2] : highestBidder); + + expect(auction.winnerAddress).toEqual(highestBidder.address); + expect(highestBidder.bid.reveal.bidAmount).toEqual(`${auction.winnerBid.quantity}${auction.winnerBid.type}`); + expect(secondHighestBidder.bid.reveal.bidAmount).toEqual(`${auction.winnerPrice.quantity}${auction.winnerPrice.type}`); + + const [record] = await registry.lookupAuthorities([authorityName], true); + expect(record.ownerAddress).toEqual(highestBidder.address); + expect(record.height).toBeDefined(); + expect(record.status).toEqual('active'); + }); }; const withNumBidders = (numBidders: number) => () => auctionTests(numBidders); @@ -70,6 +132,6 @@ if (!process.env.AUCTIONS_ENABLED) { yarn test:auctions */ describe('Auction (1 bidder)', withNumBidders(1)); - describe('Auction (2 bidders)', withNumBidders(2)); - describe('Auction (4 bidders)', withNumBidders(4)); + xdescribe('Auction (2 bidders)', withNumBidders(2)); + xdescribe('Auction (4 bidders)', withNumBidders(4)); } diff --git a/src/index.ts b/src/index.ts index 454399b..885fda1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,12 +9,40 @@ import { MessageSendParams } from '@tharsis/transactions' -import { createTxMsgCancelBond, createTxMsgCreateBond, createTxMsgRefillBond, createTxMsgWithdrawBond, MessageMsgCancelBond, MessageMsgCreateBond, MessageMsgRefillBond, MessageMsgWithdrawBond } from "./messages/bond"; import { RegistryClient } from "./registry-client"; import { Account } from "./account"; import { createTransaction } from "./txbuilder"; -import { createTxMsgDeleteName, createTxMsgReserveAuthority, createTxMsgSetAuthorityBond, createTxMsgSetName, createTxMsgSetRecord, MessageMsgDeleteName, MessageMsgReserveAuthority, MessageMsgSetAuthorityBond, MessageMsgSetName, MessageMsgSetRecord, NAMESERVICE_ERRORS } from './messages/nameservice'; import { Payload, Record } from './types'; +import { Util } from './util'; +import { + createTxMsgCancelBond, + createTxMsgCreateBond, + createTxMsgRefillBond, + createTxMsgWithdrawBond, + MessageMsgCancelBond, + MessageMsgCreateBond, + MessageMsgRefillBond, + MessageMsgWithdrawBond +} from "./messages/bond"; +import { + createTxMsgDeleteName, + createTxMsgReserveAuthority, + createTxMsgSetAuthorityBond, + createTxMsgSetName, + createTxMsgSetRecord, + MessageMsgDeleteName, + MessageMsgReserveAuthority, + MessageMsgSetAuthorityBond, + MessageMsgSetName, + MessageMsgSetRecord, + NAMESERVICE_ERRORS +} from './messages/nameservice'; +import { + createTxMsgCommitBid, + createTxMsgRevealBid, + MessageMsgCommitBid, + MessageMsgRevealBid +} from './messages/auction'; const DEFAULT_WRITE_ERROR = 'Unable to write to chiba-clonk.'; @@ -35,6 +63,32 @@ export const parseTxResponse = (result: any) => { return { hash, height, ...txResponse }; }; +/** + * Create an auction bid. + */ + export const createBid = async (chainId: string, auctionId: string, bidderAddress: string, bidAmount: string, noise?: string) => { + if (!noise) { + noise = Account.generateMnemonic(); + } + + const reveal = { + chainId, + auctionId, + bidderAddress, + bidAmount, + noise + }; + + const commitHash = await Util.getContentId(reveal); + const revealString = Buffer.from(JSON.stringify(reveal)).toString('hex'); + + return { + commitHash, + reveal, + revealString + }; +}; + export const isKeyValid = (key: string) => key && key.match(/^[0-9a-fA-F]{64}$/); export class Registry { @@ -71,7 +125,7 @@ export class Registry { /** * Get account by addresses. */ - async getAccount(address: string) { + async getAccount(address: string) { return this._client.getAccount(address); } @@ -108,7 +162,7 @@ export class Registry { /** * Send coins. */ - async sendCoins(params: MessageSendParams, senderAddress: string, privateKey: string, fee: Fee) { + async sendCoins(params: MessageSendParams, senderAddress: string, privateKey: string, fee: Fee) { let result; const { account: { base_account: accountInfo } } = await this.getAccount(senderAddress); @@ -128,7 +182,7 @@ export class Registry { /** * Computes the next bondId for the given account private key. */ - async getNextBondId(address: string) { + async getNextBondId(address: string) { let result; const { account } = await this.getAccount(address); const accountObj = account.base_account; @@ -142,21 +196,21 @@ export class Registry { /** * Get bonds by ids. */ - async getBondsByIds(ids: string[]) { + async getBondsByIds(ids: string[]) { return this._client.getBondsByIds(ids); } /** * Query bonds by attributes. */ - async queryBonds(attributes = {}) { + async queryBonds(attributes = {}) { return this._client.queryBonds(attributes); } /** * Create bond. */ - async createBond(params: MessageMsgCreateBond, senderAddress: string, privateKey: string, fee: Fee) { + async createBond(params: MessageMsgCreateBond, senderAddress: string, privateKey: string, fee: Fee) { let result; const { account: { base_account: accountInfo } } = await this.getAccount(senderAddress); @@ -176,7 +230,7 @@ export class Registry { /** * Refill bond. */ - async refillBond(params: MessageMsgRefillBond, senderAddress: string, privateKey: string, fee: Fee) { + async refillBond(params: MessageMsgRefillBond, senderAddress: string, privateKey: string, fee: Fee) { let result; const { account: { base_account: accountInfo } } = await this.getAccount(senderAddress); @@ -196,7 +250,7 @@ export class Registry { /** * Withdraw (from) bond. */ - async withdrawBond(params: MessageMsgWithdrawBond, senderAddress: string, privateKey: string, fee: Fee) { + async withdrawBond(params: MessageMsgWithdrawBond, senderAddress: string, privateKey: string, fee: Fee) { let result; const { account: { base_account: accountInfo } } = await this.getAccount(senderAddress); @@ -277,6 +331,53 @@ export class Registry { return parseTxResponse(result); } + /** + * Commit auction bid. + */ + async commitBid(params: MessageMsgCommitBid, senderAddress: string, privateKey: string, fee: Fee) { + let result; + const { account: { base_account: accountInfo } } = await this.getAccount(senderAddress); + + const sender = { + accountAddress: accountInfo.address, + sequence: accountInfo.sequence, + accountNumber: accountInfo.account_number, + pubkey: accountInfo.pub_key.key, + } + + const msg = createTxMsgCommitBid(this._chain, sender, fee, '', params) + result = await this._submitTx(msg, privateKey, sender); + + return parseTxResponse(result); + } + + /** + * Reveal auction bid. + */ + async revealBid(params: MessageMsgRevealBid, senderAddress: string, privateKey: string, fee: Fee) { + let result; + const { account: { base_account: accountInfo } } = await this.getAccount(senderAddress); + + const sender = { + accountAddress: accountInfo.address, + sequence: accountInfo.sequence, + accountNumber: accountInfo.account_number, + pubkey: accountInfo.pub_key.key, + } + + const msg = createTxMsgRevealBid(this._chain, sender, fee, '', params) + result = await this._submitTx(msg, privateKey, sender); + + return parseTxResponse(result); + } + + /** + * Get records by ids. + */ + async getAuctionsByIds(ids: string[]) { + return this._client.getAuctionsByIds(ids); + } + /** * Lookup authorities by names. */ diff --git a/src/messages/auction.ts b/src/messages/auction.ts new file mode 100644 index 0000000..ddb6ad0 --- /dev/null +++ b/src/messages/auction.ts @@ -0,0 +1,149 @@ +import { + generateTypes, +} from '@tharsis/eip712' +import { + Chain, + Sender, + Fee, +} from '@tharsis/transactions' + +import * as auctionTx from '../proto/vulcanize/auction/v1beta1/tx' +import { createTx } from './util' + +const MSG_COMMIT_BID_TYPES = { + MsgValue: [ + { name: 'auction_id', type: 'string' }, + { name: 'commit_hash', type: 'string' }, + { name: 'signer', type: 'string' }, + ] +} + +export interface MessageMsgCommitBid { + auctionId: string, + commitHash: string, +} + +const MSG_REVEAL_BID_TYPES = { + MsgValue: [ + { name: 'auction_id', type: 'string' }, + { name: 'reveal', type: 'string' }, + { name: 'signer', type: 'string' }, + ] +} + +export interface MessageMsgRevealBid { + auctionId: string, + reveal: string, +} + +export function createTxMsgCommitBid( + chain: Chain, + sender: Sender, + fee: Fee, + memo: string, + params: MessageMsgCommitBid, +) { + const types = generateTypes(MSG_COMMIT_BID_TYPES) + + const msg = createMsgCommitBid( + params.auctionId, + params.commitHash, + sender.accountAddress, + ) + + const msgCosmos = protoCreateMsgCommitBid( + params.auctionId, + params.commitHash, + sender.accountAddress, + ) + + return createTx(chain, sender, fee, memo, types, msg, msgCosmos) +} + +export function createTxMsgRevealBid( + chain: Chain, + sender: Sender, + fee: Fee, + memo: string, + params: MessageMsgRevealBid, +) { + const types = generateTypes(MSG_REVEAL_BID_TYPES) + + const msg = createMsgRevealBid( + params.auctionId, + params.reveal, + sender.accountAddress, + ) + + const msgCosmos = protoCreateMsgRevealBid( + params.auctionId, + params.reveal, + sender.accountAddress, + ) + + return createTx(chain, sender, fee, memo, types, msg, msgCosmos) +} + +function createMsgCommitBid( + auctionId: string, + commitHash: string, + signer: string +) { + return { + type: 'auction/MsgCommitBid', + value: { + auction_id: auctionId, + commit_hash: commitHash, + signer, + }, + } +} + +const protoCreateMsgCommitBid = ( + auctionId: string, + commitHash: string, + signer: string +) => { + const commitBidMessage = new auctionTx.vulcanize.auction.v1beta1.MsgCommitBid({ + auction_id: auctionId, + commit_hash: commitHash, + signer, + }) + + return { + message: commitBidMessage, + path: 'vulcanize.auction.v1beta1.MsgCommitBid', + } +} + +function createMsgRevealBid( + auctionId: string, + reveal: string, + signer: string +) { + return { + type: 'auction/MsgRevealBid', + value: { + auction_id: auctionId, + reveal, + signer, + }, + } +} + +const protoCreateMsgRevealBid = ( + auctionId: string, + reveal: string, + signer: string +) => { + const revealBidMessage = new auctionTx.vulcanize.auction.v1beta1.MsgRevealBid({ + auction_id: auctionId, + reveal, + signer, + }) + + return { + message: revealBidMessage, + path: 'vulcanize.auction.v1beta1.MsgRevealBid', + } +} diff --git a/src/registry-client.ts b/src/registry-client.ts index 5fa4e23..706ecef 100644 --- a/src/registry-client.ts +++ b/src/registry-client.ts @@ -136,7 +136,7 @@ export class RegistryClient { /** * Fetch Account. */ - async getAccount(address: string) { + async getAccount(address: string) { assert(address); let { data } = await axios.get(`${this._restEndpoint}${generateEndpointAccount(address)}`) @@ -147,7 +147,7 @@ export class RegistryClient { /** * Get records by attributes. */ - async queryRecords(attributes: {[key: string]: any}, all = false, refs = false) { + async queryRecords(attributes: {[key: string]: any}, all = false, refs = false) { if (!attributes) { attributes = {}; } @@ -179,7 +179,7 @@ export class RegistryClient { /** * Lookup authorities by names. */ - async lookupAuthorities(names: string[], auction = false) { + async lookupAuthorities(names: string[], auction = false) { assert(names.length); const query = `query ($names: [String!]) { @@ -203,6 +203,26 @@ export class RegistryClient { return result['lookupAuthorities']; } + /** + * Get auctions by ids. + */ + async getAuctionsByIds(ids: string[]) { + assert(ids); + assert(ids.length); + + const query = `query ($ids: [String!]) { + getAuctionsByIds(ids: $ids) { + ${auctionFields} + } + }`; + + const variables = { + ids + }; + + return RegistryClient.getResult(this._graph(query)(variables), 'getAuctionsByIds'); + } + /** * Lookup names. */