From dedf1f3e320e31ab73366fe7c07a47805887ed3c Mon Sep 17 00:00:00 2001 From: anilhelvaci Date: Wed, 2 Oct 2024 02:39:40 +0300 Subject: [PATCH 1/4] chore: run auction, WIP Refs: https://github.com/Agoric/BytePitchPartnerEng/issues/8 --- .../proposals/z:acceptance/auction.test.js | 221 ++++++++++++++++++ .../z:acceptance/scripts/test-vaults.mts | 6 +- 2 files changed, 224 insertions(+), 3 deletions(-) create mode 100644 a3p-integration/proposals/z:acceptance/auction.test.js diff --git a/a3p-integration/proposals/z:acceptance/auction.test.js b/a3p-integration/proposals/z:acceptance/auction.test.js new file mode 100644 index 00000000000..ea45cb580be --- /dev/null +++ b/a3p-integration/proposals/z:acceptance/auction.test.js @@ -0,0 +1,221 @@ +/** + * @file In this file we aim to test auctioneer in an isolated manner. Here's the scenario to test; + * - Send 100 ATOMs to gov1 from validator + * - Make sure auctioneer params like ClockStep, StartFrequency are reduced + * - For book0, ATOM is collateral, set two types of bids; by price and by percentage, user1 is the bidder + * - Deposit some collateral into book0, gov1 is the depositor + * - Wait until placed bids get their payouts + * - Make sure the depositer gets correct amounts + */ + +import { + addPreexistingOracles, + agd, + agopsInter, + agoric, + ATOM_DENOM, + bankSend, + createBid, + executeOffer, + getLiveOffers, + getPriceQuote, + GOV1ADDR, + pushPrices, + USER1ADDR, + waitForBlock, +} from '@agoric/synthetic-chain'; +import '@endo/init'; +import test from 'ava'; +import { boardSlottingMarshaller, makeFromBoard } from './test-lib/rpc.js'; +import { + retryUntilCondition, + waitUntilAccountFunded, + waitUntilOfferResult, +} from './test-lib/sync-tools.js'; + +const ambientAuthroity = { + query: agd.query, + follow: agoric.follow, + setTimeout: globalThis.setTimeout, +}; + +const config = { + price: 9.99, + bidsSetup: [ + { + give: '80IST', + discount: 0.1, + }, + { + give: '90IST', + price: 9.0, + }, + { + give: '150IST', + discount: 0.15, + }, + ], + bidsOutcome: [ + { + payouts: { + Bid: 0, + Collateral: 8.897786, + }, + }, + { + payouts: { + Bid: 0, + Collateral: 10.01001, + }, + }, + { + payouts: { + Bid: 10.46, + Collateral: 16.432903, + }, + }, + ], +}; + +const oraclesByBrand = new Map(); + +let roundId = 2; + +const setupOracles = async t => { + await addPreexistingOracles('ATOM', oraclesByBrand); + + await pushPrices(9.99, 'ATOM', oraclesByBrand, roundId); + roundId += 1; + await retryUntilCondition( + () => getPriceQuote('ATOM'), + res => res === '+9990000', + 'error', + { log: t.log, setTimeout: globalThis.setTimeout }, + ); +}; + +const BID_OFFER_ID = `bid-acceptance-${Date.now()}`; +const DEPOSIT_OFFER_ID = `gov1-deposit-${Date.now()}`; + +const createNewBid = async t => { + await createBid('20', USER1ADDR, BID_OFFER_ID); + const liveOffers = await getLiveOffers(USER1ADDR); + t.true(liveOffers[0].includes(BID_OFFER_ID)); +}; + +const getBalance = async (target, addr) => { + const { balances } = await agd.query('bank', 'balances', addr); + const { amount } = balances.find(({ denom }) => denom === target); + return Number(amount); +}; + +const fundAccts = async ( + depositorAmt = '100000000', + bidderAmt = '100000000', +) => { + await Promise.all([ + bankSend(GOV1ADDR, `${depositorAmt}${ATOM_DENOM}`), + bankSend(USER1ADDR, `${bidderAmt}${ATOM_DENOM}`), + ]); + + await Promise.all([ + waitUntilAccountFunded( + GOV1ADDR, + ambientAuthroity, + { denom: 'uist', value: Number(depositorAmt) }, + { errorMessage: 'gov1 not funded yet' }, + ), + waitUntilAccountFunded( + USER1ADDR, + ambientAuthroity, + { denom: ATOM_DENOM, value: Number(bidderAmt) }, + { errorMessage: 'user1 not funded yet' }, + ), + ]); +}; + +const bidByPrice = (price, give, offerId) => { + agopsInter( + 'bid', + 'by-price', + `--price ${price}`, + `--give ${give}`, + '--from', + USER1ADDR, + '--keyring-backend test', + `--offer-id ${offerId}`, + ); + + return waitUntilOfferResult(USER1ADDR, offerId, true, ambientAuthroity, { + errorMessage: 'bid not settled yet', + maxRetries: 10, + retryIntervalMs: 10000, + }); +}; + +const depositCollateral = async t => { + const fromBoard = makeFromBoard(); + const marshaller = boardSlottingMarshaller(fromBoard.convertSlotToVal); + + const brandsRaw = await agoric.follow( + '-lF', + ':published.agoricNames.brand', + '-o', + 'text', + ); + const brands = Object.fromEntries( + marshaller.fromCapData(JSON.parse(brandsRaw)), + ); + t.log(brands); + + const offerSpec = { + id: DEPOSIT_OFFER_ID, + invitationSpec: { + source: 'agoricContract', + instancePath: ['auctioneer'], + callPipe: [['makeDepositInvitation']], + }, + proposal: { + give: { + Collateral: { brand: brands.ATOM, value: 100_000_000n }, + }, + }, + }; + + const spendAction = { + method: 'executeOffer', + offer: offerSpec, + }; + + const offer = JSON.stringify(marshaller.toCapData(harden(spendAction))); + t.log('OFFER', offer); + + executeOffer(GOV1ADDR, offer); + return waitUntilOfferResult(GOV1ADDR, DEPOSIT_OFFER_ID, true, ambientAuthroity, { + errorMessage: 'proceeds not distributed yet', + maxRetries: 10, + retryIntervalMs: 10000, + }); +}; + +test.only('run auction', async t => { + await setupOracles(t); + await fundAccts(); + const settleBidP = bidByPrice( + config.bidsSetup[1].price, + config.bidsSetup[1].give, + BID_OFFER_ID, + ); + const proceedsP = depositCollateral(t); + + await Promise.all([settleBidP, proceedsP]); + + const [gov1Results, user1Results] = await Promise.all([ + agoric.follow('-lF', `:published.wallet.${GOV1ADDR}`), + agoric.follow('-lF', `:published.wallet.${USER1ADDR}`), + ]); + t.log('GOV1', gov1Results.status.payouts); + t.log('USER1', user1Results.status.payouts); + t.log('DONE!'); + t.pass(); +}); diff --git a/a3p-integration/proposals/z:acceptance/scripts/test-vaults.mts b/a3p-integration/proposals/z:acceptance/scripts/test-vaults.mts index 7fdbcef510a..7a1df695135 100755 --- a/a3p-integration/proposals/z:acceptance/scripts/test-vaults.mts +++ b/a3p-integration/proposals/z:acceptance/scripts/test-vaults.mts @@ -15,9 +15,9 @@ import { setDebtLimit, } from '../lib/vaults.mjs'; -const START_FREQUENCY = 600; // StartFrequency: 600s (auction runs every 10m) -const CLOCK_STEP = 20; // ClockStep: 20s (ensures auction completes in time) -const PRICE_LOCK_PERIOD = 300; +const START_FREQUENCY = 120; // StartFrequency: 600s (auction runs every 10m) +const CLOCK_STEP = 10; // ClockStep: 20s (ensures auction completes in time) +const PRICE_LOCK_PERIOD = 60; const oraclesAddresses = [GOV1ADDR, GOV2ADDR]; const oracles = [] as { address: string; id: string }[]; From 5bbdde88cd1a82de34dbf6697195bd96964de69b Mon Sep 17 00:00:00 2001 From: anilhelvaci Date: Fri, 4 Oct 2024 17:09:47 +0300 Subject: [PATCH 2/4] chore(acceptance): support long living bids, improve robustness against time related complexities fix: apply change requests from PR review, resolve rebase conflicts, style fixes * change `performAction.js` name to `submitBid.js` * remove `Math.round` from `scale6` * update indexing of bids and other constants in `config` object to improve readability (`auction.test.js`) * move helper functions in `auction.test.js` to `test-lib/auction-lib.js` * move `lib/vaults.mts` to `test-lib/vaults.mts` and remove empty `lib` directory * let it be known `sync-tools.js` is a stand-in code for #10171 Refs: https://github.com/Agoric/BytePitchPartnerEng/issues/8 fix: style fixes fix(acceptance-auction): lint fixes --- .../proposals/n:upgrade-next/submitBid.js | 40 +++ .../proposals/n:upgrade-next/tsconfig.json | 3 +- .../proposals/n:upgrade-next/use.sh | 1 + .../proposals/z:acceptance/auction.test.js | 337 +++++++++--------- .../z:acceptance/scripts/test-vaults.mts | 8 +- .../z:acceptance/test-lib/auction-lib.js | 306 ++++++++++++++++ .../z:acceptance/test-lib/auction-lib.test.js | 122 +++++++ .../z:acceptance/{lib => test-lib}/vaults.mts | 0 .../proposals/z:acceptance/test.sh | 5 +- 9 files changed, 645 insertions(+), 177 deletions(-) create mode 100644 a3p-integration/proposals/n:upgrade-next/submitBid.js create mode 100644 a3p-integration/proposals/z:acceptance/test-lib/auction-lib.js create mode 100644 a3p-integration/proposals/z:acceptance/test-lib/auction-lib.test.js rename a3p-integration/proposals/z:acceptance/{lib => test-lib}/vaults.mts (100%) diff --git a/a3p-integration/proposals/n:upgrade-next/submitBid.js b/a3p-integration/proposals/n:upgrade-next/submitBid.js new file mode 100644 index 00000000000..36b17c62a29 --- /dev/null +++ b/a3p-integration/proposals/n:upgrade-next/submitBid.js @@ -0,0 +1,40 @@ +#!/usr/bin/env node + +import { + GOV1ADDR, + CHAINID, + agd, + agopsInter, + addUser, + waitForBlock, + provisionSmartWallet, + ATOM_DENOM, +} from '@agoric/synthetic-chain'; + +export const bankSend = (from, addr, wanted) => { + const chain = ['--chain-id', CHAINID]; + const fromArg = ['--from', from]; + const testKeyring = ['--keyring-backend', 'test']; + const noise = [...fromArg, ...chain, ...testKeyring, '--yes']; + + return agd.tx('bank', 'send', from, addr, wanted, ...noise); +}; + +const bidder = await addUser('long-living-bidder'); +console.log('BIDDDER', bidder); +await bankSend(GOV1ADDR, bidder, `80000000uist`); +console.log('IST sent'); +await provisionSmartWallet(bidder, `20000000ubld,100000000${ATOM_DENOM}`); +console.log('Provision sent'); +await waitForBlock(3); +console.log('Wait For Block done. Sending bid offer'); +agopsInter( + 'bid', + 'by-price', + `--price 49.0`, + `--give 80IST`, + '--from', + bidder, + '--keyring-backend test', + `--offer-id long-living-bid-for-acceptance`, +); diff --git a/a3p-integration/proposals/n:upgrade-next/tsconfig.json b/a3p-integration/proposals/n:upgrade-next/tsconfig.json index 39de5a422e9..835db5a0316 100644 --- a/a3p-integration/proposals/n:upgrade-next/tsconfig.json +++ b/a3p-integration/proposals/n:upgrade-next/tsconfig.json @@ -1,7 +1,8 @@ { "compilerOptions": { "target": "esnext", - "module": "esnext", + "module": "NodeNext", + "moduleResolution": "NodeNext", "allowJs": true, "checkJs": true, "strict": false, diff --git a/a3p-integration/proposals/n:upgrade-next/use.sh b/a3p-integration/proposals/n:upgrade-next/use.sh index fa6c4e42f65..129c3b92444 100644 --- a/a3p-integration/proposals/n:upgrade-next/use.sh +++ b/a3p-integration/proposals/n:upgrade-next/use.sh @@ -8,3 +8,4 @@ node ./addGov4 ./verifyPushedPrice.js 'ATOM' 12.01 ./verifyPushedPrice.js 'stATOM' 12.01 +./submitBid.js diff --git a/a3p-integration/proposals/z:acceptance/auction.test.js b/a3p-integration/proposals/z:acceptance/auction.test.js index ea45cb580be..91c633a9ba2 100644 --- a/a3p-integration/proposals/z:acceptance/auction.test.js +++ b/a3p-integration/proposals/z:acceptance/auction.test.js @@ -1,221 +1,216 @@ +/* eslint-env node */ /** * @file In this file we aim to test auctioneer in an isolated manner. Here's the scenario to test; - * - Send 100 ATOMs to gov1 from validator - * - Make sure auctioneer params like ClockStep, StartFrequency are reduced - * - For book0, ATOM is collateral, set two types of bids; by price and by percentage, user1 is the bidder - * - Deposit some collateral into book0, gov1 is the depositor + * + * - Prerequisites: In one of the earlier proposal(n:upgrade-next), a user called "long-living-bidder" + * has placed a bid where { give: 80IST, price: 49.0 } + * - Push price so that 1 ATOM is 50 ISTs + * - Wait until the auctioneer captures the price we just pushed + * - Fund actors + * - gov1 gets 100 ATOMs + * - user1 gets 90 ISTs + * - gov3 gets 150 ISTs + * - Place bids for user1 and gov3 following the values in "config" + * - Deposit 100 ATOMs into book0, gov1 is the depositor * - Wait until placed bids get their payouts - * - Make sure the depositer gets correct amounts + * - Wait until proceeds are distributed to the depositor + * - Make sure all actors receive the correct payouts */ +// Typo will be fixed with https://github.com/Agoric/agoric-sdk/pull/10171 +/** @typedef {import('./test-lib/sync-tools.js').RetryOptions} RetryOptions */ + import { - addPreexistingOracles, agd, - agopsInter, agoric, - ATOM_DENOM, - bankSend, - createBid, - executeOffer, - getLiveOffers, - getPriceQuote, + getUser, GOV1ADDR, - pushPrices, + GOV3ADDR, USER1ADDR, - waitForBlock, } from '@agoric/synthetic-chain'; import '@endo/init'; import test from 'ava'; import { boardSlottingMarshaller, makeFromBoard } from './test-lib/rpc.js'; +import { retryUntilCondition } from './test-lib/sync-tools.js'; import { - retryUntilCondition, - waitUntilAccountFunded, - waitUntilOfferResult, -} from './test-lib/sync-tools.js'; - -const ambientAuthroity = { + calculateRetryUntilNextStartTime, + checkBidsOutcome, + checkDepositOutcome, + checkPrice, + depositCollateral, + fundAccts, + getCapturedPrice, + placeBids, + pushPricesForAuction, + scale6, +} from './test-lib/auction-lib.js'; + +const ambientAuthority = { query: agd.query, follow: agoric.follow, - setTimeout: globalThis.setTimeout, + setTimeout, }; +const fromBoard = makeFromBoard(); +const marshaller = boardSlottingMarshaller(fromBoard.convertSlotToVal); + const config = { - price: 9.99, - bidsSetup: [ - { - give: '80IST', - discount: 0.1, - }, - { + depositor: { + name: 'gov1', + addr: GOV1ADDR, + depositValue: '100000000', + offerId: `gov1-deposit-${Date.now()}`, + }, + price: 50.0, + longLivingBidSetup: { + name: 'long-living-bidder', + // This bid is placed in an earlier proposal + give: '80IST', + }, + currentBidsSetup: { + user1: { + bidder: USER1ADDR, + bidderFund: { + value: 90000000, + denom: 'uist', + }, + offerId: `user1-bid-${Date.now()}`, give: '90IST', - price: 9.0, + price: 46, }, - { + gov3: { + bidder: GOV3ADDR, + bidderFund: { + value: 150000000, + denom: 'uist', + }, + offerId: `gov3-bid-${Date.now()}`, give: '150IST', - discount: 0.15, + discount: '13', }, - ], - bidsOutcome: [ - { + }, + bidsOutcome: { + longLivingBidder: { payouts: { Bid: 0, - Collateral: 8.897786, + Collateral: 1.68421, }, }, - { + user1: { payouts: { Bid: 0, - Collateral: 10.01001, + Collateral: 2.0, }, }, - { + gov3: { payouts: { - Bid: 10.46, - Collateral: 16.432903, + Bid: 0, + Collateral: 3.448275, }, }, - ], + }, }; -const oraclesByBrand = new Map(); - -let roundId = 2; - -const setupOracles = async t => { - await addPreexistingOracles('ATOM', oraclesByBrand); - - await pushPrices(9.99, 'ATOM', oraclesByBrand, roundId); - roundId += 1; - await retryUntilCondition( - () => getPriceQuote('ATOM'), - res => res === '+9990000', - 'error', - { log: t.log, setTimeout: globalThis.setTimeout }, - ); -}; - -const BID_OFFER_ID = `bid-acceptance-${Date.now()}`; -const DEPOSIT_OFFER_ID = `gov1-deposit-${Date.now()}`; - -const createNewBid = async t => { - await createBid('20', USER1ADDR, BID_OFFER_ID); - const liveOffers = await getLiveOffers(USER1ADDR); - t.true(liveOffers[0].includes(BID_OFFER_ID)); -}; - -const getBalance = async (target, addr) => { - const { balances } = await agd.query('bank', 'balances', addr); - const { amount } = balances.find(({ denom }) => denom === target); - return Number(amount); -}; - -const fundAccts = async ( - depositorAmt = '100000000', - bidderAmt = '100000000', -) => { - await Promise.all([ - bankSend(GOV1ADDR, `${depositorAmt}${ATOM_DENOM}`), - bankSend(USER1ADDR, `${bidderAmt}${ATOM_DENOM}`), - ]); - - await Promise.all([ - waitUntilAccountFunded( - GOV1ADDR, - ambientAuthroity, - { denom: 'uist', value: Number(depositorAmt) }, - { errorMessage: 'gov1 not funded yet' }, - ), - waitUntilAccountFunded( - USER1ADDR, - ambientAuthroity, - { denom: ATOM_DENOM, value: Number(bidderAmt) }, - { errorMessage: 'user1 not funded yet' }, - ), - ]); -}; - -const bidByPrice = (price, give, offerId) => { - agopsInter( - 'bid', - 'by-price', - `--price ${price}`, - `--give ${give}`, - '--from', - USER1ADDR, - '--keyring-backend test', - `--offer-id ${offerId}`, - ); - - return waitUntilOfferResult(USER1ADDR, offerId, true, ambientAuthroity, { - errorMessage: 'bid not settled yet', - maxRetries: 10, - retryIntervalMs: 10000, - }); -}; +test.before(async t => { + /** @type {RetryOptions} */ + const pushPriceRetryOpts = { + maxRetries: 5, // arbitrary + retryIntervalMs: 5000, // in ms + }; -const depositCollateral = async t => { - const fromBoard = makeFromBoard(); - const marshaller = boardSlottingMarshaller(fromBoard.convertSlotToVal); + /** @type {RetryOptions} */ + const bankSendRetryOpts = { + maxRetries: 3, // arbitrary + retryIntervalMs: 3000, // in ms + }; - const brandsRaw = await agoric.follow( + // Get current round id + const round = await agoric.follow( '-lF', - ':published.agoricNames.brand', - '-o', - 'text', - ); - const brands = Object.fromEntries( - marshaller.fromCapData(JSON.parse(brandsRaw)), + ':published.priceFeed.ATOM-USD_price_feed.latestRound', ); - t.log(brands); - - const offerSpec = { - id: DEPOSIT_OFFER_ID, - invitationSpec: { - source: 'agoricContract', - instancePath: ['auctioneer'], - callPipe: [['makeDepositInvitation']], - }, - proposal: { - give: { - Collateral: { brand: brands.ATOM, value: 100_000_000n }, - }, + t.context = { + roundId: parseInt(round.roundId, 10), + retryOpts: { + bankSendRetryOpts, + pushPriceRetryOpts, }, }; +}); - const spendAction = { - method: 'executeOffer', - offer: offerSpec, - }; - - const offer = JSON.stringify(marshaller.toCapData(harden(spendAction))); - t.log('OFFER', offer); - - executeOffer(GOV1ADDR, offer); - return waitUntilOfferResult(GOV1ADDR, DEPOSIT_OFFER_ID, true, ambientAuthroity, { - errorMessage: 'proceeds not distributed yet', - maxRetries: 10, - retryIntervalMs: 10000, - }); -}; +test('run auction', async t => { + // Push the price to a point where only our bids can settle + await pushPricesForAuction(t, config.price); -test.only('run auction', async t => { - await setupOracles(t); - await fundAccts(); - const settleBidP = bidByPrice( - config.bidsSetup[1].price, - config.bidsSetup[1].give, - BID_OFFER_ID, + // Wait until next round starts. Retry error message is useful for debugging + const retryOptions = await calculateRetryUntilNextStartTime(); + await retryUntilCondition( + () => getCapturedPrice('book0'), + res => checkPrice(res, scale6(config.price).toString()), // scale price to uist + 'price not captured yet [AUCTION TEST]', + { + log: t.log, + ...ambientAuthority, + ...retryOptions, + }, ); - const proceedsP = depositCollateral(t); - await Promise.all([settleBidP, proceedsP]); + // Make sure depositor and bidders have enough balance + await fundAccts(t, config.depositor, config.currentBidsSetup); + const bidsP = placeBids(t, config.currentBidsSetup); + const proceedsP = depositCollateral(t, config.depositor); - const [gov1Results, user1Results] = await Promise.all([ - agoric.follow('-lF', `:published.wallet.${GOV1ADDR}`), - agoric.follow('-lF', `:published.wallet.${USER1ADDR}`), + // Resolves when auction finalizes and depositor gets payouts + const [longLivingBidderAddr] = await Promise.all([ + getUser(config.longLivingBidSetup.name), + ...bidsP, + proceedsP, ]); - t.log('GOV1', gov1Results.status.payouts); - t.log('USER1', user1Results.status.payouts); - t.log('DONE!'); - t.pass(); + + // Query wallets of the actors involved for assertions + const [gov1Results, longLivingBidResults, user1Results, gov3Results, brands] = + await Promise.all([ + agoric + .follow( + '-lF', + `:published.wallet.${config.depositor.addr}`, + '-o', + 'text', + ) + .then(res => marshaller.fromCapData(JSON.parse(res))), + agoric + .follow( + '-lF', + `:published.wallet.${longLivingBidderAddr}`, + '-o', + 'text', + ) + .then(res => marshaller.fromCapData(JSON.parse(res))), + agoric + .follow('-lF', `:published.wallet.${USER1ADDR}`, '-o', 'text') + .then(res => marshaller.fromCapData(JSON.parse(res))), + agoric + .follow('-lF', `:published.wallet.${GOV3ADDR}`, '-o', 'text') + .then(res => marshaller.fromCapData(JSON.parse(res))), + agoric + .follow('-lF', ':published.agoricNames.brand', '-o', 'text') + .then(res => + Object.fromEntries(marshaller.fromCapData(JSON.parse(res))), + ), + ]); + + // Assert depositor paid correctly + checkDepositOutcome(t, gov1Results.status.payouts, config, brands); + + // Assert bidders paid correctly + checkBidsOutcome( + t, + { + 'longLivingBidder.results': longLivingBidResults, + 'user1.results': user1Results, + 'gov3.results': gov3Results, + }, + config.bidsOutcome, + brands, + ); }); diff --git a/a3p-integration/proposals/z:acceptance/scripts/test-vaults.mts b/a3p-integration/proposals/z:acceptance/scripts/test-vaults.mts index 7a1df695135..6aa5cf15577 100755 --- a/a3p-integration/proposals/z:acceptance/scripts/test-vaults.mts +++ b/a3p-integration/proposals/z:acceptance/scripts/test-vaults.mts @@ -13,11 +13,11 @@ import { ISTunit, provisionWallet, setDebtLimit, -} from '../lib/vaults.mjs'; +} from '../test-lib/vaults.mjs'; -const START_FREQUENCY = 120; // StartFrequency: 600s (auction runs every 10m) -const CLOCK_STEP = 10; // ClockStep: 20s (ensures auction completes in time) -const PRICE_LOCK_PERIOD = 60; +const START_FREQUENCY = 600; // StartFrequency: 600s (auction runs every 10m) +const CLOCK_STEP = 20; // ClockStep: 20s (ensures auction completes in time) +const PRICE_LOCK_PERIOD = 300; const oraclesAddresses = [GOV1ADDR, GOV2ADDR]; const oracles = [] as { address: string; id: string }[]; diff --git a/a3p-integration/proposals/z:acceptance/test-lib/auction-lib.js b/a3p-integration/proposals/z:acceptance/test-lib/auction-lib.js new file mode 100644 index 00000000000..3db752700b9 --- /dev/null +++ b/a3p-integration/proposals/z:acceptance/test-lib/auction-lib.js @@ -0,0 +1,306 @@ +/* eslint-env node */ +import { + addPreexistingOracles, + agd, + agopsInter, + agoric, + ATOM_DENOM, + CHAINID, + executeOffer, + getPriceQuote, + GOV1ADDR, + pushPrices, + VALIDATORADDR, +} from '@agoric/synthetic-chain'; +import { AmountMath } from '@agoric/ertp'; +import { + retryUntilCondition, + waitUntilAccountFunded, + waitUntilOfferResult, +} from './sync-tools.js'; +import { boardSlottingMarshaller, makeFromBoard } from './rpc.js'; + +/** + * Typo will be fixed with https://github.com/Agoric/agoric-sdk/pull/10171 + * @typedef {import('./sync-tools.js').RetryOptions} RetryOptions + */ + +const ambientAuthority = { + query: agd.query, + follow: agoric.follow, + setTimeout, +}; + +export const scale6 = x => BigInt(x * 1_000_000); + +const fromBoard = makeFromBoard(); +const marshaller = boardSlottingMarshaller(fromBoard.convertSlotToVal); + +// Import from synthetic-chain once it is updated +export const bankSend = (from, addr, wanted) => { + const chain = ['--chain-id', CHAINID]; + const fromArg = ['--from', from]; + const testKeyring = ['--keyring-backend', 'test']; + const noise = [...fromArg, ...chain, ...testKeyring, '--yes']; + + return agd.tx('bank', 'send', from, addr, wanted, ...noise); +}; + +export const pushPricesForAuction = async (t, price) => { + const oraclesByBrand = new Map(); + await addPreexistingOracles('ATOM', oraclesByBrand); + + await pushPrices(price, 'ATOM', oraclesByBrand, t.context.roundId + 1); + + await retryUntilCondition( + () => getPriceQuote('ATOM'), + res => res === `+${scale6(price).toString()}`, + 'price not pushed yet', + { + log: t.log, + setTimeout, + ...t.context.pushPriceRetryOpts, + }, + ); +}; + +/** + * @param {any} t + * @param {{ + * name: string + * offerId: string, + * depositValue: string, + * }} depositor + * @param {Record} bidders + */ +export const fundAccts = async (t, depositor, bidders) => { + const retryOpts = t.context.retryOpts.bankSendRetryOpts; + + await bankSend( + VALIDATORADDR, + GOV1ADDR, + `${depositor.depositValue}${ATOM_DENOM}`, + ); + await waitUntilAccountFunded( + GOV1ADDR, + ambientAuthority, + { denom: ATOM_DENOM, value: Number(depositor.depositValue) }, + { errorMessage: `${depositor.name} not funded yet`, ...retryOpts }, + ); + + for await (const [key, value] of [...Object.entries(bidders)]) { + const fund = value.bidderFund; + await bankSend(GOV1ADDR, value.bidder, `${fund.value}${fund.denom}`); + await waitUntilAccountFunded(value.bidder, ambientAuthority, fund, { + errorMessage: `${key} not funded yet`, + ...retryOpts, + }); + } +}; + +export const bidByPrice = (price, give, offerId, bidder) => { + return agopsInter( + 'bid', + 'by-price', + `--price ${price}`, + `--give ${give}`, + '--from', + bidder, + '--keyring-backend test', + `--offer-id ${offerId}`, + ); +}; + +export const bidByDiscount = (discount, give, offerId, bidder) => { + return agopsInter( + 'bid', + 'by-discount', + `--discount ${discount}`, + `--give ${give}`, + '--from', + bidder, + '--keyring-backend test', + `--offer-id ${offerId}`, + ); +}; + +export const placeBids = (t, bidsSetup) => { + return [...Object.values(bidsSetup)].map( + ({ bidder, offerId, price, give, discount }) => { + if (price) return bidByPrice(price, give, offerId, bidder); + return bidByDiscount(discount, give, offerId, bidder); + }, + ); +}; + +/** + * Calculates retry options based on "nextStartTime" + */ +export const calculateRetryUntilNextStartTime = async () => { + const schedule = await agoric.follow('-lF', ':published.auction.schedule'); + const nextStartTime = parseInt(schedule.nextStartTime.absValue, 10); + + /** @type {RetryOptions} */ + const capturePriceRetryOpts = { + maxRetries: Math.round((nextStartTime * 1000 - Date.now()) / 10000) + 2, // wait until next schedule + retryIntervalMs: 10000, // 10 seconds in ms + }; + + return capturePriceRetryOpts; +}; + +/** + * + * @param {any} t + * @param {{ + * offerId: string, + * depositValue: string, + * addr: string + * }} depositor + */ +export const depositCollateral = async (t, depositor) => { + const [brandsRaw, retryOptions] = await Promise.all([ + agoric.follow('-lF', ':published.agoricNames.brand', '-o', 'text'), + calculateRetryUntilNextStartTime(), + ]); + const brands = Object.fromEntries( + marshaller.fromCapData(JSON.parse(brandsRaw)), + ); + + const offerSpec = { + id: depositor.offerId, + invitationSpec: { + source: 'agoricContract', + instancePath: ['auctioneer'], + callPipe: [['makeDepositInvitation']], + }, + proposal: { + give: { + Collateral: { + brand: brands.ATOM, + value: BigInt(depositor.depositValue), + }, + }, + }, + }; + + const spendAction = { + method: 'executeOffer', + offer: offerSpec, + }; + + const offer = JSON.stringify(marshaller.toCapData(harden(spendAction))); + t.log('OFFER', offer); + + executeOffer(depositor.addr, offer); + return waitUntilOfferResult( + depositor.addr, + depositor.offerId, + true, + ambientAuthority, + { + errorMessage: 'proceeds not distributed yet', + ...retryOptions, + }, + ); +}; + +export const checkBidsOutcome = (t, settledBids, bidsOutcome, brands) => { + [...Object.entries(settledBids)] + .map(([key, bidResult]) => [key.split('.')[0], bidResult.status.payouts]) + .forEach(([key, { Bid, Collateral }]) => { + t.log({ bidsOutcome }); + const { + payouts: { Bid: outcomeBidVal, Collateral: outcomeColVal }, + } = bidsOutcome[key]; + t.log({ outcomeBidVal, outcomeColVal }); + t.is( + AmountMath.isEqual( + Bid, + AmountMath.make(brands.IST, scale6(outcomeBidVal)), + ), + true, + ); + t.is( + AmountMath.isGTE( + Collateral, + AmountMath.make(brands.ATOM, scale6(outcomeColVal)), + ), + true, + ); + }); +}; + +export const checkDepositOutcome = (t, depositorPayouts, config, brands) => { + // Assert depositor paid correctly + const { Bid: depositorBid, Collateral: depositorCol } = depositorPayouts; + const { + depositor, + longLivingBidSetup: { give: longLivingBidGive }, + currentBidsSetup, + bidsOutcome, + } = config; + + const getNumberFromGive = give => + parseInt(give.substring(0, give.length - 3), 10); + + const calculateGiveTotal = () => { + let currentBidSum = getNumberFromGive(longLivingBidGive); + [...Object.values(currentBidsSetup)].forEach(({ give }) => { + currentBidSum += getNumberFromGive(give); + }); + + return scale6(currentBidSum); + }; + + const calculateOutcomeTotal = () => { + let total = 0n; + [...Object.values(bidsOutcome)] + .map(outcome => outcome.payouts.Collateral) + .forEach(element => { + t.log(element); + total += scale6(element); + }); + + return total; + }; + + t.is( + AmountMath.isEqual( + depositorBid, + AmountMath.make(brands.IST, calculateGiveTotal()), + ), + true, + ); + t.is( + AmountMath.isGTE( + AmountMath.make( + brands.ATOM, + BigInt(depositor.depositValue) - calculateOutcomeTotal(), + ), + depositorCol, + ), + true, + ); +}; + +export const getCapturedPrice = async bookId => { + const result = await agoric.follow('-lF', `:published.auction.${bookId}`); + return result; +}; + +export const checkPrice = (res, expected) => { + if (res.startPrice === null) return false; + else if (res.startPrice.numerator.value === expected) return true; + return false; +}; diff --git a/a3p-integration/proposals/z:acceptance/test-lib/auction-lib.test.js b/a3p-integration/proposals/z:acceptance/test-lib/auction-lib.test.js new file mode 100644 index 00000000000..1fa71951158 --- /dev/null +++ b/a3p-integration/proposals/z:acceptance/test-lib/auction-lib.test.js @@ -0,0 +1,122 @@ +import '@endo/init'; +import { AmountMath } from '@agoric/ertp'; +import { Far } from '@endo/far'; +import test from 'ava'; +import { GOV3ADDR, USER1ADDR } from '@agoric/synthetic-chain'; +import { checkBidsOutcome, checkDepositOutcome } from './auction-lib.js'; + +// From auction.test.js +const config = { + depositor: { + name: 'gov1', + depositValue: '100000000', + offerId: `gov1-deposit-${Date.now()}`, + }, + longLivingBidSetup: { + name: 'long-living-bidder', + // This bid is placed in an earlier proposal + give: '80IST', + }, + currentBidsSetup: { + user1: { + bidder: USER1ADDR, + bidderFund: { + value: 90000000, + denom: 'uist', + }, + offerId: `user1-bid-${Date.now()}`, + give: '90IST', + price: 46, + }, + gov3: { + bidder: GOV3ADDR, + bidderFund: { + value: 150000000, + denom: 'uist', + }, + offerId: `gov3-bid-${Date.now()}`, + give: '150IST', + discount: '13', + }, + }, + bidsOutcome: { + longLivingBidder: { + payouts: { + Bid: 0, + Collateral: 1.68421, + }, + }, + user1: { + payouts: { + Bid: 0, + Collateral: 2.0, + }, + }, + gov3: { + payouts: { + Bid: 0, + Collateral: 3.448275, + }, + }, + }, +}; + +test.before(t => { + const mockIST = Far('IST', {}); + const mockATOM = Far('ATOM', {}); + + t.context = { + brands: { + IST: mockIST, + ATOM: mockATOM, + }, + }; +}); + +test('make sure check* functions work properly', t => { + // @ts-expect-error + const { brands } = t.context; + const result = { + status: { + payouts: { + Bid: AmountMath.make(brands.IST, 0n), + Collateral: AmountMath.make(brands.ATOM, 1684210n), + }, + }, + }; + + checkBidsOutcome( + t, + { + 'longLivingBidder.results': result, + 'user1.results': { + status: { + payouts: { + Bid: AmountMath.make(brands.IST, 0n), + Collateral: AmountMath.make(brands.ATOM, 2000000n), + }, + }, + }, + 'gov3.results': { + status: { + payouts: { + Bid: AmountMath.make(brands.IST, 0n), + Collateral: AmountMath.make(brands.ATOM, 3448275n), + }, + }, + }, + }, + config.bidsOutcome, + brands, + ); + + checkDepositOutcome( + t, + harden({ + Bid: AmountMath.make(brands.IST, 320000000n), + Collateral: AmountMath.make(brands.ATOM, 100_000_000n - 7_132_485n), + }), + config, + brands, + ); +}); diff --git a/a3p-integration/proposals/z:acceptance/lib/vaults.mts b/a3p-integration/proposals/z:acceptance/test-lib/vaults.mts similarity index 100% rename from a3p-integration/proposals/z:acceptance/lib/vaults.mts rename to a3p-integration/proposals/z:acceptance/test-lib/vaults.mts diff --git a/a3p-integration/proposals/z:acceptance/test.sh b/a3p-integration/proposals/z:acceptance/test.sh index ee4e473496d..7f33803cfbd 100755 --- a/a3p-integration/proposals/z:acceptance/test.sh +++ b/a3p-integration/proposals/z:acceptance/test.sh @@ -5,7 +5,7 @@ set -ueo pipefail # The effects of this step are not persisted in further proposal layers. # test the state right after the previous proposals -yarn ava initial.test.js +# yarn ava initial.test.js # XXX some of these tests have path dependencies so no globs yarn ava core-eval.test.js @@ -29,6 +29,9 @@ yarn ava wallet.test.js echo ACCEPTANCE TESTING vaults yarn ava vaults.test.js +echo ACCEPTANCE TESTING auction +yarn ava auction.test.js + echo ACCEPTANCE TESTING psm yarn ava psm.test.js From bf054144ec43667195d237a0f72f3cb1760e4ff9 Mon Sep 17 00:00:00 2001 From: anilhelvaci Date: Wed, 30 Oct 2024 11:17:04 +0300 Subject: [PATCH 3/4] chore(auction-acceptance): workaround for auctioneer vats vstorage collision Refs: https://github.com/Agoric/BytePitchPartnerEng/issues/31 fix(auction-acceptance): formatting fixes fix(auction-acceptance): formatting fixes --- .../proposals/z:acceptance/auction.test.js | 18 +++--- .../z:acceptance/test-lib/auction-lib.js | 57 ++++++++++--------- 2 files changed, 42 insertions(+), 33 deletions(-) diff --git a/a3p-integration/proposals/z:acceptance/auction.test.js b/a3p-integration/proposals/z:acceptance/auction.test.js index 91c633a9ba2..02b6c8df86f 100644 --- a/a3p-integration/proposals/z:acceptance/auction.test.js +++ b/a3p-integration/proposals/z:acceptance/auction.test.js @@ -17,7 +17,6 @@ * - Make sure all actors receive the correct payouts */ -// Typo will be fixed with https://github.com/Agoric/agoric-sdk/pull/10171 /** @typedef {import('./test-lib/sync-tools.js').RetryOptions} RetryOptions */ import { @@ -36,10 +35,9 @@ import { calculateRetryUntilNextStartTime, checkBidsOutcome, checkDepositOutcome, - checkPrice, + checkPriceCaptured, depositCollateral, fundAccts, - getCapturedPrice, placeBids, pushPricesForAuction, scale6, @@ -143,11 +141,12 @@ test('run auction', async t => { await pushPricesForAuction(t, config.price); // Wait until next round starts. Retry error message is useful for debugging - const retryOptions = await calculateRetryUntilNextStartTime(); + const { retryOptions, nextStartTime } = + await calculateRetryUntilNextStartTime(); await retryUntilCondition( - () => getCapturedPrice('book0'), - res => checkPrice(res, scale6(config.price).toString()), // scale price to uist - 'price not captured yet [AUCTION TEST]', + () => Promise.resolve(Date.now()), + res => res >= nextStartTime * 1000, + 'next auction round not started yet [AUCTION TEST]', { log: t.log, ...ambientAuthority, @@ -160,6 +159,11 @@ test('run auction', async t => { const bidsP = placeBids(t, config.currentBidsSetup); const proceedsP = depositCollateral(t, config.depositor); + // Make sure price captured + // We have to check this after bids are placed and collateral deposited + // because of https://github.com/Agoric/BytePitchPartnerEng/issues/31 + await checkPriceCaptured('book0', scale6(config.price).toString()); + // Resolves when auction finalizes and depositor gets payouts const [longLivingBidderAddr] = await Promise.all([ getUser(config.longLivingBidSetup.name), diff --git a/a3p-integration/proposals/z:acceptance/test-lib/auction-lib.js b/a3p-integration/proposals/z:acceptance/test-lib/auction-lib.js index 3db752700b9..9ce1e3ddee1 100644 --- a/a3p-integration/proposals/z:acceptance/test-lib/auction-lib.js +++ b/a3p-integration/proposals/z:acceptance/test-lib/auction-lib.js @@ -1,12 +1,12 @@ /* eslint-env node */ import { - addPreexistingOracles, agd, agopsInter, agoric, ATOM_DENOM, CHAINID, executeOffer, + generateOracleMap, getPriceQuote, GOV1ADDR, pushPrices, @@ -36,6 +36,9 @@ export const scale6 = x => BigInt(x * 1_000_000); const fromBoard = makeFromBoard(); const marshaller = boardSlottingMarshaller(fromBoard.convertSlotToVal); +// From n-upgrade/verifyPushedPrice.js +const BASE_ID = 'n-upgrade'; + // Import from synthetic-chain once it is updated export const bankSend = (from, addr, wanted) => { const chain = ['--chain-id', CHAINID]; @@ -47,8 +50,7 @@ export const bankSend = (from, addr, wanted) => { }; export const pushPricesForAuction = async (t, price) => { - const oraclesByBrand = new Map(); - await addPreexistingOracles('ATOM', oraclesByBrand); + const oraclesByBrand = generateOracleMap(BASE_ID, ['ATOM']); await pushPrices(price, 'ATOM', oraclesByBrand, t.context.roundId + 1); @@ -67,20 +69,20 @@ export const pushPricesForAuction = async (t, price) => { /** * @param {any} t * @param {{ - * name: string - * offerId: string, - * depositValue: string, + * name: string + * offerId: string, + * depositValue: string, * }} depositor * @param {Record} bidders */ export const fundAccts = async (t, depositor, bidders) => { @@ -152,11 +154,11 @@ export const calculateRetryUntilNextStartTime = async () => { /** @type {RetryOptions} */ const capturePriceRetryOpts = { - maxRetries: Math.round((nextStartTime * 1000 - Date.now()) / 10000) + 2, // wait until next schedule + maxRetries: Math.ceil((nextStartTime * 1000 - Date.now()) / 10000) + 2, // wait until next schedule retryIntervalMs: 10000, // 10 seconds in ms }; - return capturePriceRetryOpts; + return { retryOptions: capturePriceRetryOpts, nextStartTime }; }; /** @@ -169,7 +171,7 @@ export const calculateRetryUntilNextStartTime = async () => { * }} depositor */ export const depositCollateral = async (t, depositor) => { - const [brandsRaw, retryOptions] = await Promise.all([ + const [brandsRaw, { retryOptions }] = await Promise.all([ agoric.follow('-lF', ':published.agoricNames.brand', '-o', 'text'), calculateRetryUntilNextStartTime(), ]); @@ -294,13 +296,16 @@ export const checkDepositOutcome = (t, depositorPayouts, config, brands) => { ); }; -export const getCapturedPrice = async bookId => { - const result = await agoric.follow('-lF', `:published.auction.${bookId}`); - return result; -}; - -export const checkPrice = (res, expected) => { - if (res.startPrice === null) return false; - else if (res.startPrice.numerator.value === expected) return true; - return false; +/** + * + * @param {string} bookId + * @param {string} expected + */ +export const checkPriceCaptured = async (bookId, expected) => { + const { + startPrice: { + numerator: { value }, + }, + } = await agoric.follow('-lF', `:published.auction.${bookId}`); + assert(value, expected); }; From 990126165e1c8ba2ac7cc8a52ad696cf7fc347a3 Mon Sep 17 00:00:00 2001 From: anilhelvaci Date: Thu, 31 Oct 2024 11:15:16 +0300 Subject: [PATCH 4/4] chore(auction-acceptance): address pr suggestions Refs: https://github.com/Agoric/BytePitchPartnerEng/issues/8 Refs: https://github.com/Agoric/agoric-sdk/issues/10111 --- .../proposals/n:upgrade-next/submitBid.js | 13 ++++++++++--- .../proposals/z:acceptance/auction.test.js | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/a3p-integration/proposals/n:upgrade-next/submitBid.js b/a3p-integration/proposals/n:upgrade-next/submitBid.js index 36b17c62a29..5ac4c1be879 100644 --- a/a3p-integration/proposals/n:upgrade-next/submitBid.js +++ b/a3p-integration/proposals/n:upgrade-next/submitBid.js @@ -9,9 +9,16 @@ import { waitForBlock, provisionSmartWallet, ATOM_DENOM, + VALIDATORADDR, } from '@agoric/synthetic-chain'; -export const bankSend = (from, addr, wanted) => { +/** + * + * @param {string} addr + * @param {string} wanted + * @param {string | undefined} from + */ +export const bankSend = (addr, wanted, from = VALIDATORADDR) => { const chain = ['--chain-id', CHAINID]; const fromArg = ['--from', from]; const testKeyring = ['--keyring-backend', 'test']; @@ -21,8 +28,8 @@ export const bankSend = (from, addr, wanted) => { }; const bidder = await addUser('long-living-bidder'); -console.log('BIDDDER', bidder); -await bankSend(GOV1ADDR, bidder, `80000000uist`); +console.log('BIDDER', bidder); +await bankSend(bidder, `80000000uist`, GOV1ADDR); console.log('IST sent'); await provisionSmartWallet(bidder, `20000000ubld,100000000${ATOM_DENOM}`); console.log('Provision sent'); diff --git a/a3p-integration/proposals/z:acceptance/auction.test.js b/a3p-integration/proposals/z:acceptance/auction.test.js index 02b6c8df86f..eda93ca260c 100644 --- a/a3p-integration/proposals/z:acceptance/auction.test.js +++ b/a3p-integration/proposals/z:acceptance/auction.test.js @@ -129,10 +129,10 @@ test.before(async t => { ); t.context = { roundId: parseInt(round.roundId, 10), - retryOpts: { + retryOpts: harden({ bankSendRetryOpts, pushPriceRetryOpts, - }, + }), }; });