Skip to content

Commit

Permalink
test: get FastLP/ufastlp balance from x/bank query
Browse files Browse the repository at this point in the history
 - prune balancesFromPurses, which was not type-safe
 - factor out vstorage queries for type safety
 - add static check on deposit / withdraw proposals
   - refactor usdcToGive as give.USDC etc.

balancesFromPurses() seemed to return a Record<Brand, Amount>
but Brands can't be record keys. It "worked" by stringifying the brands.

```
> b = {[Symbol.toStringTag]:'B123'}
{ [Symbol(Symbol.toStringTag)]: 'B123' }
> b.toString()
'[object B123]'
> fromEntries([[b, 1]])
{ '[object B123]': 1 }
```
  • Loading branch information
dckc committed Dec 17, 2024
1 parent 843c208 commit b34dc50
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 64 deletions.
148 changes: 94 additions & 54 deletions multichain-testing/test/fast-usdc/fast-usdc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,20 @@ import { makeQueryClient } from '../../tools/query.js';
import { commonSetup, type SetupContextWithWallets } from '../support.js';
import { makeFeedPolicy, oracleMnemonics } from './config.js';
import { makeRandomDigits } from '../../tools/random.js';
import { balancesFromPurses } from '../../tools/purse.js';
import { makeTracer } from '@agoric/internal';
import type {
CctpTxEvidence,
EvmAddress,
PoolMetrics,
} from '@agoric/fast-usdc/src/types.js';
import type { CurrentWalletRecord } from '@agoric/smart-wallet/src/smartWallet.js';
import type { QueryBalanceResponseSDKType } from '@agoric/cosmic-proto/cosmos/bank/v1beta1/query.js';
import type { USDCProposalShapes } from '@agoric/fast-usdc/src/pool-share-math.js';

const log = makeTracer('MCFU');

const { keys, values, fromEntries } = Object;
const { isGTE, isEmpty, make } = AmountMath;
const { isGTE, isEmpty, make, subtract } = AmountMath;

const makeRandomNumber = () => Math.random();

Expand Down Expand Up @@ -107,23 +110,64 @@ test.after(async t => {
deleteTestKeys(accounts);
});

type VStorageClient = Awaited<ReturnType<typeof commonSetup>>['vstorageClient'];
const agoricNamesQ = (vsc: VStorageClient) =>
harden({
brands: <K extends AssetKind>(_assetKind: K) =>
vsc
.queryData('published.agoricNames.brand')
.then(pairs => fromEntries(pairs) as Record<string, Brand<K>>),
});
const walletQ = (vsc: VStorageClient) => {
const self = harden({
current: (addr: string) =>
vsc.queryData(
`published.wallet.${addr}.current`,
) as Promise<CurrentWalletRecord>,
findInvitationDetail: async (addr: string, description: string) => {
const { Invitation } = await agoricNamesQ(vsc).brands('set');
const current = await self.current(addr);
const { purses } = current;
const { value: details } = purses.find(p => p.brand === Invitation)!
.balance as Amount<'set', InvitationDetails>;
const detail = details.find(x => x.description === description);
return { current, detail };
},
});
return self;
};

const fastLPQ = (vsc: VStorageClient) =>
harden({
metrics: () =>
vsc.queryData(
`published.${contractName}.poolMetrics`,
) as Promise<PoolMetrics>,
info: () =>
vsc.queryData(`published.${contractName}`) as Promise<{
poolAccount: string;
settlementAccount: string;
}>,
});

const toOracleOfferId = (idx: number) => `oracle${idx + 1}-accept`;

test.serial('oracles accept', async t => {
const { oracleWds, retryUntilCondition, vstorageClient, wallets } = t.context;
const brands = await vstorageClient.queryData('published.agoricNames.brand');
const { Invitation } = Object.fromEntries(brands);

const description = 'oracle operator invitation';

// ensure we have an unused (or used) oracle invitation in each purse
let hasAccepted = false;
for (const name of keys(oracleMnemonics)) {
const { offerToUsedInvitation, purses } = await vstorageClient.queryData(
`published.wallet.${wallets[name]}.current`,
const {
current: { offerToUsedInvitation },
detail,
} = await walletQ(vstorageClient).findInvitationDetail(
wallets[name],
description,
);
const { value: invitations } = balancesFromPurses(purses)[Invitation];
const hasInvitation = invitations.some(x => x.description === description);
const hasInvitation = !!detail;
const usedInvitation = offerToUsedInvitation?.[0]?.[0] === `${name}-accept`;
t.log({ name, hasInvitation, usedInvitation });
t.true(hasInvitation || usedInvitation, 'has or accepted invitation');
Expand Down Expand Up @@ -167,51 +211,53 @@ test.serial('oracles accept', async t => {
}
});

const toAmt = (
brand: Brand<'nat'>,
balance: QueryBalanceResponseSDKType['balance'],
) => make(brand, BigInt(balance?.amount || 0));

test.serial('lp deposits', async t => {
const { lpUser, retryUntilCondition, vstorageClient, wallets } = t.context;

const lpDoOffer = makeDoOffer(lpUser);
const brands = await vstorageClient.queryData('published.agoricNames.brand');
const { USDC, FastLP } = Object.fromEntries(brands);

const usdcToGive = make(USDC, LP_DEPOSIT_AMOUNT);
const { USDC, FastLP } = await agoricNamesQ(vstorageClient).brands('nat');

const { shareWorth: currShareWorth } = await vstorageClient.queryData(
`published.${contractName}.poolMetrics`,
);
const poolSharesWanted = divideBy(usdcToGive, currShareWorth);
const give = { USDC: make(USDC, LP_DEPOSIT_AMOUNT) };

const metricsPre = await fastLPQ(vstorageClient).metrics();
const want = { PoolShare: divideBy(give.USDC, metricsPre.shareWorth) };

const proposal: USDCProposalShapes['deposit'] = harden({ give, want });
await lpDoOffer({
id: `lp-deposit-${Date.now()}`,
invitationSpec: {
source: 'agoricContract',
instancePath: [contractName],
callPipe: [['makeDepositInvitation']],
},
proposal: {
give: { USDC: usdcToGive },
want: { PoolShare: poolSharesWanted },
},
proposal,
});

await t.notThrowsAsync(() =>
retryUntilCondition(
() => vstorageClient.queryData(`published.${contractName}.poolMetrics`),
() => fastLPQ(vstorageClient).metrics(),
({ shareWorth }) =>
!isGTE(currShareWorth.numerator, shareWorth.numerator),
!isGTE(metricsPre.shareWorth.numerator, shareWorth.numerator),
'share worth numerator increases from deposit',
{ log },
),
);

const { useChain } = t.context;
const queryClient = makeQueryClient(
await useChain('agoric').getRestEndpoint(),
);

await t.notThrowsAsync(() =>
retryUntilCondition(
() =>
vstorageClient.queryData(`published.wallet.${wallets['lp']}.current`),
({ purses }) => {
const currentPoolShares = balancesFromPurses(purses)[FastLP];
return currentPoolShares && isGTE(currentPoolShares, poolSharesWanted);
},
() => queryClient.queryBalance(wallets['lp'], 'ufastlp'),
({ balance }) => isGTE(toAmt(FastLP, balance), want.PoolShare),
'lp has pool shares',
{ log },
),
Expand Down Expand Up @@ -344,7 +390,7 @@ const advanceAndSettleScenario = test.macro({
nobleTools.mockCctpMint(mintAmt, userForwardingAddr);
await t.notThrowsAsync(() =>
retryUntilCondition(
() => vstorageClient.queryData(`published.${contractName}.poolMetrics`),
() => fastLPQ(vstorageClient).metrics(),
({ encumberedBalance }) =>
encumberedBalance && isEmpty(encumberedBalance),
'encumberedBalance returns to 0',
Expand Down Expand Up @@ -372,60 +418,54 @@ test.serial('lp withdraws', async t => {
await useChain('agoric').getRestEndpoint(),
);
const lpDoOffer = makeDoOffer(lpUser);
const brands = await vstorageClient.queryData('published.agoricNames.brand');
const { FastLP } = Object.fromEntries(brands);
const { FastLP } = await agoricNamesQ(vstorageClient).brands('nat');
t.log('FastLP brand', FastLP);

const { shareWorth: currShareWorth } = await vstorageClient.queryData(
`published.${contractName}.poolMetrics`,
);
const { purses } = await vstorageClient.queryData(
`published.wallet.${wallets['lp']}.current`,
const metricsPre = await fastLPQ(vstorageClient).metrics();

const { balance: lpCoins } = await queryClient.queryBalance(
wallets['lp'],
'ufastlp',
);
const currentPoolShares = balancesFromPurses(purses)[FastLP];
t.log('currentPoolShares', currentPoolShares);
const usdcWanted = multiplyBy(currentPoolShares, currShareWorth);
t.log('usdcWanted', usdcWanted);
const give = { PoolShare: toAmt(FastLP, lpCoins) };
t.log('give', give, lpCoins);

const { balance: currentUSDCBalance } = await queryClient.queryBalance(
const { balance: usdcCoins } = await queryClient.queryBalance(
wallets['lp'],
usdcDenom,
);
t.log(`current ${usdcDenom} balance`, currentUSDCBalance);
const want = { USDC: multiplyBy(give.PoolShare, metricsPre.shareWorth) };
t.log('want', want, usdcCoins);

const proposal: USDCProposalShapes['withdraw'] = harden({ give, want });
await lpDoOffer({
id: `lp-withdraw-${Date.now()}`,
invitationSpec: {
source: 'agoricContract',
instancePath: [contractName],
callPipe: [['makeWithdrawInvitation']],
},
proposal: {
give: { PoolShare: currentPoolShares },
want: { USDC: usdcWanted },
},
proposal,
});

await t.notThrowsAsync(() =>
retryUntilCondition(
() =>
vstorageClient.queryData(`published.wallet.${wallets['lp']}.current`),
({ purses }) => {
const currentPoolShares = balancesFromPurses(purses)[FastLP];
return !currentPoolShares || isEmpty(currentPoolShares);
},
() => queryClient.queryBalance(wallets['lp'], 'ufastlp'),
({ balance }) => isEmpty(toAmt(FastLP, balance)),
'lp no longer has pool shares',
{ log },
),
);

const USDC = want.USDC.brand;
await t.notThrowsAsync(() =>
retryUntilCondition(
() => queryClient.queryBalance(wallets['lp'], usdcDenom),
({ balance }) =>
!!balance?.amount &&
BigInt(balance.amount) - BigInt(currentUSDCBalance!.amount!) >
LP_DEPOSIT_AMOUNT,
!isGTE(
make(USDC, LP_DEPOSIT_AMOUNT),
subtract(toAmt(USDC, balance), want.USDC),
),
"lp's USDC balance increases",
{ log },
),
Expand Down
10 changes: 0 additions & 10 deletions multichain-testing/tools/purse.ts

This file was deleted.

0 comments on commit b34dc50

Please sign in to comment.