diff --git a/a3p-integration/proposals/n:upgrade-next/performAction.js b/a3p-integration/proposals/n:upgrade-next/submitBid.js similarity index 100% rename from a3p-integration/proposals/n:upgrade-next/performAction.js rename to a3p-integration/proposals/n:upgrade-next/submitBid.js diff --git a/a3p-integration/proposals/n:upgrade-next/use.sh b/a3p-integration/proposals/n:upgrade-next/use.sh index fa6c4e42f65e..129c3b924449 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 6da02f85159f..b288cfbebcd5 100644 --- a/a3p-integration/proposals/z:acceptance/auction.test.js +++ b/a3p-integration/proposals/z:acceptance/auction.test.js @@ -1,41 +1,48 @@ /** * @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 - * - For book0, ATOM is collateral, set two types of bids; by price and by percentage, user1 is the bidder + * + * - 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 depositor 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').RetyrOptions} RetryOptions */ import { - addPreexistingOracles, agd, - agopsInter, agoric, - ATOM_DENOM, - CHAINID, - executeOffer, - getPriceQuote, getUser, GOV1ADDR, GOV3ADDR, - pushPrices, USER1ADDR, - VALIDATORADDR, } 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'; -import { AmountMath } from '@agoric/ertp'; - -export const scale6 = x => BigInt(Math.round(x * 1_000_000)); + calculateRetryUntilNextStartTime, + checkBidsOutcome, + checkDepositOutcome, + checkPrice, + depositCollateral, + fundAccts, + getCapturedPrice, + placeBids, + pushPricesForAuction, + scale6, +} from './test-lib/auction-lib.js'; const ambientAuthority = { query: agd.query, @@ -46,23 +53,21 @@ const ambientAuthority = { const fromBoard = makeFromBoard(); const marshaller = boardSlottingMarshaller(fromBoard.convertSlotToVal); -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 config = { depositor: { + name: 'gov1', + addr: GOV1ADDR, depositValue: '100000000', offerId: `gov1-deposit-${Date.now()}`, }, price: 50.0, - bidsSetup: [ - { + longLivingBidSetup: { + name: 'long-living-bidder', + // This bid is placed in an earlier proposal + give: '80IST', + }, + currentBidsSetup: { + user1: { bidder: USER1ADDR, bidderFund: { value: 90000000, @@ -72,7 +77,7 @@ const config = { give: '90IST', price: 46, }, - { + gov3: { bidder: GOV3ADDR, bidderFund: { value: 150000000, @@ -82,201 +87,27 @@ const config = { give: '150IST', discount: '13', }, - ], - bidsOutcome: [ - { + }, + bidsOutcome: { + longLivingBidder: { payouts: { Bid: 0, Collateral: 1.68421, }, }, - { + user1: { payouts: { Bid: 0, Collateral: 2.0, }, }, - { + gov3: { payouts: { Bid: 0, Collateral: 3.448275, }, }, - ], -}; - -const pushPricesForAuction = async t => { - const oraclesByBrand = new Map(); - await addPreexistingOracles('ATOM', oraclesByBrand); - - await pushPrices(config.price, 'ATOM', oraclesByBrand, t.context.roundId + 1); - - await retryUntilCondition( - () => getPriceQuote('ATOM'), - res => res === `+${scale6(config.price).toString()}`, - 'price not pushed yet', - { - log: t.log, - setTimeout: globalThis.setTimeout, - ...t.context.pushPriceRetryOpts, - }, - ); -}; - -const fundAccts = async (depositorAmt = '100000000', t) => { - const retryOpts = t.context.retryOpts.bankSendRetryOpts; - - await bankSend(VALIDATORADDR, GOV1ADDR, `${depositorAmt}${ATOM_DENOM}`), - await waitUntilAccountFunded( - GOV1ADDR, - ambientAuthority, - { denom: ATOM_DENOM, value: Number(depositorAmt) }, - { errorMessage: 'gov1 not funded yet', ...retryOpts }, - ); - - const user1Fund = config.bidsSetup[0].bidderFund; - await bankSend(GOV1ADDR, USER1ADDR, `${user1Fund.value}${user1Fund.denom}`); - await waitUntilAccountFunded(USER1ADDR, ambientAuthority, user1Fund, { - errorMessage: 'user1 not funded yet', - ...retryOpts, - }); - - const gov3Fund = config.bidsSetup[1].bidderFund; - await bankSend(GOV1ADDR, GOV3ADDR, `${gov3Fund.value}${gov3Fund.denom}`); - await waitUntilAccountFunded(GOV3ADDR, ambientAuthority, gov3Fund, { - errorMessage: 'gov3 not funded yet', - ...retryOpts, - }); -}; - -const bidByPrice = (price, give, offerId, bidder, t) => { - return agopsInter( - 'bid', - 'by-price', - `--price ${price}`, - `--give ${give}`, - '--from', - bidder, - '--keyring-backend test', - `--offer-id ${offerId}`, - ); -}; - -const bidByDiscount = (discount, give, offerId, bidder, t) => { - return agopsInter( - 'bid', - 'by-discount', - `--discount ${discount}`, - `--give ${give}`, - '--from', - bidder, - '--keyring-backend test', - `--offer-id ${offerId}`, - ); -}; - -const placeBids = t => { - return [...config.bidsSetup].map( - ({ bidder, offerId, price, give, discount }) => { - if (price) return bidByPrice(price, give, offerId, bidder, t); - return bidByDiscount(discount, give, offerId, bidder, t); - }, - ); -}; - -const depositCollateral = async t => { - 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: config.depositor.offerId, - 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, - config.depositor.offerId, - true, - ambientAuthority, - { - errorMessage: 'proceeds not distributed yet', - ...retryOptions, - }, - ); -}; - -const checkBidsOutcome = (settledBids, t, brands) => { - [...settledBids] - .map(bidResult => bidResult.status.payouts) - .forEach(({ Bid, Collateral }, i) => { - const { - payouts: { Bid: outcomeBidVal, Collateral: outcomeColVal }, - } = config.bidsOutcome[i]; - t.is( - AmountMath.isEqual( - Bid, - AmountMath.make(brands.IST, scale6(outcomeBidVal)), - ), - true, - ); - t.is( - AmountMath.isGTE( - Collateral, - AmountMath.make(brands.ATOM, scale6(outcomeColVal)), - ), - true, - ); - }); -}; - -const getCapturedPrice = async bookId => { - const result = await agoric.follow('-lF', `:published.auction.${bookId}`); - return result; -}; - -const checkPrice = (res, expected) => { - if (res.startPrice === null) return false; - else if (res.startPrice.numerator.value === expected) return true; - return false; -}; - -/** - * Calculates a set of retry options based on current auction params - */ -const calculateRetryUntilNextStartTime = async () => { - const schedule = await agoric.follow('-lF', ':published.auction.schedule'); - const nextStartTime = parseInt(schedule.nextStartTime.absValue); - - /** @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; + }, }; test.before(async t => { @@ -297,7 +128,6 @@ test.before(async t => { '-lF', ':published.priceFeed.ATOM-USD_price_feed.latestRound', ); - t.context = { roundId: parseInt(round.roundId), retryOpts: { @@ -307,9 +137,9 @@ test.before(async t => { }; }); -test.only('run auction', async t => { +test('run auction', async t => { // Push the price to a point where only our bids can settle - await pushPricesForAuction(t); + await pushPricesForAuction(t, config.price); // Wait until next round starts. Retry error message is useful for debugging const retryOptions = await calculateRetryUntilNextStartTime(); @@ -325,13 +155,13 @@ test.only('run auction', async t => { ); // Make sure depositor and bidders have enough balance - await fundAccts(config.depositor.depositValue, t); - const bidsP = placeBids(t); - const proceedsP = depositCollateral(t); + await fundAccts(t, config.depositor, config.currentBidsSetup); + const bidsP = placeBids(t, config.currentBidsSetup); + const proceedsP = depositCollateral(t, config.depositor); // Resolves when auction finalizes and depositor gets payouts const [longLivingBidderAddr] = await Promise.all([ - getUser('long-living-bidder'), + getUser(config.longLivingBidSetup.name), ...bidsP, proceedsP, ]); @@ -340,7 +170,12 @@ test.only('run auction', async t => { const [gov1Results, longLivingBidResults, user1Results, gov3Results, brands] = await Promise.all([ agoric - .follow('-lF', `:published.wallet.${GOV1ADDR}`, '-o', 'text') + .follow( + '-lF', + `:published.wallet.${config.depositor.addr}`, + '-o', + 'text', + ) .then(res => marshaller.fromCapData(JSON.parse(res))), agoric .follow( @@ -364,26 +199,17 @@ test.only('run auction', async t => { ]); // Assert depositor paid correctly - const { Bid: depositorBid, Collateral: depositorCol } = - gov1Results.status.payouts; - - t.is( - AmountMath.isEqual(depositorBid, AmountMath.make(brands.IST, 320_000_000n)), - true, - ); - - t.is( - AmountMath.isGTE( - AmountMath.make(brands.ATOM, 100_000_000n - 7_132_485n), - depositorCol, - ), - true, - ); + checkDepositOutcome(t, gov1Results.status.payouts, config, brands); // Assert bidders paid correctly checkBidsOutcome( - [longLivingBidResults, user1Results, gov3Results], 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 7fdbcef510ab..6aa5cf15577b 100755 --- a/a3p-integration/proposals/z:acceptance/scripts/test-vaults.mts +++ b/a3p-integration/proposals/z:acceptance/scripts/test-vaults.mts @@ -13,7 +13,7 @@ import { ISTunit, provisionWallet, setDebtLimit, -} from '../lib/vaults.mjs'; +} from '../test-lib/vaults.mjs'; const START_FREQUENCY = 600; // StartFrequency: 600s (auction runs every 10m) const CLOCK_STEP = 20; // ClockStep: 20s (ensures auction completes in time) 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 000000000000..0ff1d7a92fc8 --- /dev/null +++ b/a3p-integration/proposals/z:acceptance/test-lib/auction-lib.js @@ -0,0 +1,305 @@ +import { + addPreexistingOracles, + agd, + agopsInter, + agoric, + ATOM_DENOM, + CHAINID, + executeOffer, + getPriceQuote, + GOV1ADDR, + pushPrices, + VALIDATORADDR, +} from '@agoric/synthetic-chain'; +import { + retryUntilCondition, + waitUntilAccountFunded, + waitUntilOfferResult, +} from './sync-tools.js'; +import { boardSlottingMarshaller, makeFromBoard } from './rpc.js'; +import { AmountMath } from '@agoric/ertp'; + +/** + * Typo will be fixed with https://github.com/Agoric/agoric-sdk/pull/10171 + * @typedef {import('./sync-tools.js').RetyrOptions} RetryOptions + */ + +const ambientAuthority = { + query: agd.query, + follow: agoric.follow, + setTimeout: globalThis.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: globalThis.setTimeout, + ...t.context.pushPriceRetryOpts, + }, + ); +}; + +/** + * @param {any} t + * @param {{ + * name: string + * offerId: string, + * depositValue: string, + * }} depositor + * @param {Object} 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, t) => { + return agopsInter( + 'bid', + 'by-price', + `--price ${price}`, + `--give ${give}`, + '--from', + bidder, + '--keyring-backend test', + `--offer-id ${offerId}`, + ); +}; + +export const bidByDiscount = (discount, give, offerId, bidder, t) => { + 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, t); + return bidByDiscount(discount, give, offerId, bidder, t); + }, + ); +}; + +/** + * 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); + + /** @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)); + + 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 000000000000..a7c4cb902c1a --- /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 { checkBidsOutcome, checkDepositOutcome } from './auction-lib.js'; +import { GOV3ADDR, USER1ADDR } from '@agoric/synthetic-chain'; + +// 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', async 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