Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add FastLP to vbank #10709

Merged
merged 5 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,62 @@ test.after(async t => {
deleteTestKeys(accounts);
});

type VStorageClient = Awaited<ReturnType<typeof commonSetup>>['vstorageClient'];
const agoricNamesQ = (vsc: VStorageClient) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a blocker but I find these one-off abstractions a little hard to work with. I hope we can design good general abstractions in @agoric/client-utils to invest in. #10713 attempts to replace test calls to VStorageClient with SmartWalletKit, getting typed data for query paths.

If the client-utils abstractions aren't right, let's work on to improve them, so all consumers benefit.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for #10713; I looked it over and made some comments.

The cycle time in multi-chain testing is extremely high, so I'm not eager to go beyond this 1 test file for this feature.

In general, my habit is to experiment until I see a pattern that works well in at least 2 or 3 cases and then DRY it out.

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.fastUsdc.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 +209,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 +388,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 +416,56 @@ 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: usdcCoinsPre } = await queryClient.queryBalance(
wallets['lp'],
usdcDenom,
);
t.log(`current ${usdcDenom} balance`, currentUSDCBalance);
t.log('usdc coins pre', usdcCoinsPre);

const want = { USDC: multiplyBy(give.PoolShare, metricsPre.shareWorth) };
t.log('want', want);

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), toAmt(USDC, usdcCoinsPre)),
),
"lp's USDC balance increases",
{ log },
),
Expand Down
10 changes: 0 additions & 10 deletions multichain-testing/tools/purse.ts

This file was deleted.

32 changes: 30 additions & 2 deletions packages/boot/test/fast-usdc/fast-usdc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,22 @@ test.serial(
refreshAgoricNamesRemotes();
t.truthy(agoricNamesRemotes.instance.fastUsdc);
t.truthy(agoricNamesRemotes.brand.FastLP);
const lpAsset = agoricNamesRemotes.vbankAsset.FastLP;
t.like(lpAsset, {
issuerName: 'FastLP',
denom: 'ufastlp',
displayInfo: { assetKind: 'nat', decimalPlaces: 6 },
});
const lpId = lpAsset.brand.getBoardId() || assert.fail('impossible');
t.is(agoricNamesRemotes.brand.FastLP.getBoardId(), lpId);

const { EV } = t.context.runUtils;
const agoricNames = await EV.vat('bootstrap').consumeItem('agoricNames');
const board = await EV.vat('bootstrap').consumeItem('board');
const getBoardAux = async name => {
const brand = await EV(agoricNames).lookup('brand', name);
const id = await EV(board).getId(brand);
t.is(id, lpId);
t.truthy(storage.data.get(`published.boardAux.${id}`));
return unmarshalFromVstorage(
storage.data,
Expand All @@ -121,7 +130,7 @@ test.serial(

const current = watcherWallet.getCurrentWalletRecord();

// XXX We should be able to compare objects by identity like this:
// XXX #10491 We should be able to compare objects by identity like this:
//
// const invitationPurse = current.purses.find(
// p => p.brand === agoricNamesRemotes.brand.Invitation,
Expand Down Expand Up @@ -216,9 +225,11 @@ test.serial('makes usdc advance', async t => {
),
);

const lp = oracles[0]; // somewhat arbitrary

// @ts-expect-error it doesnt recognize usdc as a Brand type
const usdc = agoricNamesRemotes.vbankAsset.USDC.brand as Brand<'nat'>;
await oracles[0].sendOffer({
await lp.sendOffer({
id: 'deposit-lp-0',
invitationSpec: {
source: 'agoricContract',
Expand All @@ -233,6 +244,23 @@ test.serial('makes usdc advance', async t => {
});
await eventLoopIteration();

const { getOutboundMessages } = t.context.bridgeUtils;
const lpBankDeposit = getOutboundMessages(BridgeId.BANK).find(
obj =>
obj.type === 'VBANK_GIVE' &&
obj.denom === 'ufastlp' &&
obj.recipient === lp.getAddress(),
);
t.log('LP vbank deposit', lpBankDeposit);
t.true(BigInt(lpBankDeposit.amount) > 1_000_000n, 'vbank GIVEs shares to LP');

const { purses } = lp.getCurrentWalletRecord();
// XXX #10491 should not need to resort to string match on brand
t.falsy(
purses.find(p => `${p.brand}`.match(/FastLP/)),
'FastLP balance not in wallet record',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to assert that it is in the wallet's bank in this test context? I might suggest at least saying 'FastLP balance should be in bank, not in wallet record', or maybe put a want in the proposal. Otherwise, looking at this assertion on its own might make it seem like we just don't expect fastlp tokens at all. Maybe it's not necessary at all?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea... I think I can do that...

Copy link
Member

@0xpatrickdev 0xpatrickdev Dec 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like this should help with that:

Wrote this before reading the code changes, seems you already have this.

const queryClient = makeQueryClient(
  await useChain('agoric').getRestEndpoint(),
);
const { balance } = await queryClient.queryBalance('$ADDR', 'ufastlp')

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah; there's no rest endpoint in the bootstrap world

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah getOutboundMessages didn't know about this, neat.

);

const evidence = MockCctpTxEvidences.AGORIC_PLUS_OSMO();

harness?.useRunPolicy(true);
Expand Down
1 change: 1 addition & 0 deletions packages/boot/tools/drivers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export const makeWalletFactoryDriver = async (
isNew: boolean,
) => ({
isNew,
getAddress: () => walletAddress,

executeOffer(offer: OfferSpec): Promise<void> {
const offerCapData = marshaller.toCapData(
Expand Down
Loading
Loading