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

test(multichain): fusdc "advance and settle" as a macro #10692

Merged
merged 3 commits into from
Dec 17, 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
5 changes: 4 additions & 1 deletion a3p-integration/proposals/z:acceptance/governance.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ const governanceAddresses = [GOV4ADDR, GOV2ADDR, GOV1ADDR];
const delay = ms =>
new Promise(resolve => setTimeout(() => resolve(undefined), ms));

test.serial(
const testSkipXXX = test.skip; // same lenth as test.serial to avoid reformatting all lines

// z:acceptance governance fails/flakes: No quorum #10708
testSkipXXX(
'economic committee can make governance proposal and vote on it',
async t => {
const { waitUntil } = makeTimerUtils({ setTimeout });
Expand Down
259 changes: 145 additions & 114 deletions multichain-testing/test/fast-usdc/fast-usdc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const accounts = [...keys(oracleMnemonics), 'lp'];
const contractName = 'fastUsdc';
const contractBuilder =
'../packages/builders/scripts/fast-usdc/init-fast-usdc.js';
const LP_DEPOSIT_AMOUNT = 10_000_000n;
const LP_DEPOSIT_AMOUNT = 8_000n * 10n ** 6n;
Copy link
Member

Choose a reason for hiding this comment

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

we have the 10^6 as a constant somewhere but this is clear enough. better than the literal


test.before(async t => {
const { setupTestKeys, ...common } = await commonSetup(t);
Expand Down Expand Up @@ -79,7 +79,7 @@ test.before(async t => {

// save an LP in test context
const lpUser = await provisionSmartWallet(wallets['lp'], {
USDC: 100n,
USDC: 8_000n,
BLD: 100n,
});

Expand All @@ -103,21 +103,41 @@ 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 instances = await vstorageClient.queryData(
'published.agoricNames.instance',
);
const instance = fromEntries(instances)[contractName];
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)) {
Copy link
Member

Choose a reason for hiding this comment

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

There's a lot going on in this loop.

If we reify the oracle operators as objects somewhat like makeOracleOperator or makeAgoricWatcher, we can make this read much more straightforwardly:

Suggested change
for (const name of keys(oracleMnemonics)) {
for (const op of oracleOperators) {
const status = await oracle.invitationCheck(); // returns the invitation or `used` if used;
t.log({ name: op.name, status });
t.truthy(status);
}

const { offerToUsedInvitation, purses } = await vstorageClient.queryData(
`published.wallet.${wallets[name]}.current`,
);
const { value: invitations } = balancesFromPurses(purses)[Invitation];
Copy link
Member

Choose a reason for hiding this comment

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

Does that work?

balancesFromPurses returns a map, right? and Invitation is a Brand

const hasInvitation = invitations.some(x => x.description === description);
const usedInvitation = offerToUsedInvitation?.[0]?.[0] === `${name}-accept`;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const usedInvitation = offerToUsedInvitation?.[0]?.[0] === `${name}-accept`;
const usedInvitation = offerToUsedInvitation.some(([id, _v]) => id === `${name}-accept`);

t.log({ name, hasInvitation, usedInvitation });
t.true(hasInvitation || usedInvitation, 'has or accepted invitation');
if (usedInvitation) hasAccepted = true;
}
// if the oracles have already accepted, skip the rest of the test this is
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// if the oracles have already accepted, skip the rest of the test this is
// if any of the oracles have already accepted, skip the rest of the test this is

right?

// primarily to facilitate active development but could support testing on
// images where operator invs are already accepted
if (hasAccepted) return t.pass();

// accept oracle operator invitations
const instance = fromEntries(
await vstorageClient.queryData('published.agoricNames.instance'),
)[contractName];
await Promise.all(
oracleWds.map(makeDoOffer).map((doOffer, i) =>
doOffer({
id: toOracleOfferId(i),
invitationSpec: {
source: 'purse',
instance,
description: 'oracle operator invitation', // TODO export/import INVITATION_MAKERS_DESC
description,
},
proposal: {},
}),
Expand Down Expand Up @@ -187,129 +207,136 @@ test.serial('lp deposits', async t => {
);
});

test.serial('advance and settlement', async t => {
const {
nobleTools,
nobleAgoricChannelId,
oracleWds,
retryUntilCondition,
useChain,
usdcOnOsmosis,
vstorageClient,
} = t.context;

// EUD wallet on osmosis
const eudWallet = await createWallet(useChain('osmosis').chain.bech32_prefix);
const EUD = (await eudWallet.getAccounts())[0].address;

// parameterize agoric address
const { settlementAccount } = await vstorageClient.queryData(
`published.${contractName}`,
);
t.log('settlementAccount address', settlementAccount);
const advanceAndSettleScenario = test.macro({
title: (_, mintAmt: bigint, eudChain: string) =>
Copy link
Member

Choose a reason for hiding this comment

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

eudChain is kinda... tortured

Suggested change
title: (_, mintAmt: bigint, eudChain: string) =>
title: (_, mintAmt: bigint, destChain: string) =>

`advance ${mintAmt} uusdc to ${eudChain} and settle`,
exec: async (t, mintAmt: bigint, eudChain: string) => {
const {
nobleTools,
nobleAgoricChannelId,
oracleWds,
retryUntilCondition,
useChain,
usdcOnOsmosis,
vstorageClient,
} = t.context;

// EUD wallet on the specified chain
const eudWallet = await createWallet(
Copy link
Member

Choose a reason for hiding this comment

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

likewise...

Suggested change
const eudWallet = await createWallet(
const destWallet = await createWallet(

Copy link
Member

Choose a reason for hiding this comment

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

I'd rather avoid using the ambient random source inside createWallet. I don't suppose we have a widget where you pass crypto.randomBytes(plenty) and it makes a mnemonic, do we? Or can we pass in a private key?

I can imagine this isn't worthwhile just now, so... adding it to the list:

useChain(eudChain).chain.bech32_prefix,
);
const EUD = (await eudWallet.getAccounts())[0].address;
t.log(`EUD wallet created: ${EUD}`);

const recipientAddress = encodeAddressHook(settlementAccount, { EUD });
t.log('recipientAddress', recipientAddress);
// parameterize agoric address
const { settlementAccount } = await vstorageClient.queryData(
`published.${contractName}`,
);
t.log('settlementAccount address', settlementAccount);

// register forwarding address on noble
const txRes = nobleTools.registerForwardingAcct(
nobleAgoricChannelId,
recipientAddress,
);
t.is(txRes?.code, 0, 'registered forwarding account');
const recipientAddress = encodeAddressHook(settlementAccount, { EUD });
t.log('recipientAddress', recipientAddress);

const { address: userForwardingAddr } = nobleTools.queryForwardingAddress(
nobleAgoricChannelId,
recipientAddress,
);
t.log('got forwardingAddress', userForwardingAddr);

const mintAmount = 800_000n;

// TODO export CctpTxEvidence type
const evidence = harden({
blockHash:
'0x90d7343e04f8160892e94f02d6a9b9f255663ed0ac34caca98544c8143fee665',
blockNumber: 21037663n,
txHash: `0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff3875527617${makeRandomDigits(makeRandomNumber(), 2n)}`,
tx: {
amount: mintAmount,
forwardingAddress: userForwardingAddr,
},
aux: {
forwardingChannel: nobleAgoricChannelId,
// register forwarding address on noble
const txRes = nobleTools.registerForwardingAcct(
Copy link
Member

Choose a reason for hiding this comment

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

again, lots going on; my tiny brain wants the roles (user, noble chain, ...) reified as objects.

nobleAgoricChannelId,
recipientAddress,
},
chainId: 42161,
});

console.log('User initiates evm mint', evidence.txHash);

// submit evidences
await Promise.all(
oracleWds.map(makeDoOffer).map((doOffer, i) =>
doOffer({
id: `${Date.now()}-evm-evidence`,
invitationSpec: {
source: 'continuing',
previousOffer: toOracleOfferId(i),
invitationMakerName: 'SubmitEvidence',
invitationArgs: [evidence],
},
proposal: {},
}),
),
);

const queryClient = makeQueryClient(
await useChain('osmosis').getRestEndpoint(),
);
);
t.is(txRes?.code, 0, 'registered forwarding account');

await t.notThrowsAsync(() =>
retryUntilCondition(
() => queryClient.queryBalance(EUD, usdcOnOsmosis),
({ balance }) => !!balance?.amount && BigInt(balance.amount) < mintAmount,
`${EUD} advance available from fast-usdc`,
{
// this resolves quickly, so _decrease_ the interval so the timing is more apparent
retryIntervalMs: 500,
const { address: userForwardingAddr } = nobleTools.queryForwardingAddress(
nobleAgoricChannelId,
recipientAddress,
);
t.log('got forwardingAddress', userForwardingAddr);

// TODO export CctpTxEvidence type
const evidence = harden({
blockHash:
'0x90d7343e04f8160892e94f02d6a9b9f255663ed0ac34caca98544c8143fee665',
blockNumber: 21037663n,
txHash: `0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff3875527617${makeRandomDigits(makeRandomNumber(), 2n)}`,
tx: {
amount: mintAmt,
forwardingAddress: userForwardingAddr,
},
),
);
aux: {
forwardingChannel: nobleAgoricChannelId,
recipientAddress,
},
chainId: 42161,
});

console.log('User initiates evm mint:', evidence.txHash);

// submit evidences
await Promise.all(
oracleWds.map(makeDoOffer).map((doOffer, i) =>
Copy link
Member

Choose a reason for hiding this comment

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

would love to see...

Suggested change
oracleWds.map(makeDoOffer).map((doOffer, i) =>
oracleOperators.map(op => op.submit(evidence));

doOffer({
id: `${Date.now()}-evm-evidence`,
invitationSpec: {
source: 'continuing',
previousOffer: toOracleOfferId(i),
invitationMakerName: 'SubmitEvidence',
invitationArgs: [evidence],
},
proposal: {},
}),
),
);

const queryTxStatus = async () =>
vstorageClient.queryData(
`published.${contractName}.status.${evidence.txHash}`,
const queryClient = makeQueryClient(
await useChain(eudChain).getRestEndpoint(),
);

const assertTxStatus = async (status: string) =>
t.notThrowsAsync(() =>
await t.notThrowsAsync(() =>
retryUntilCondition(
() => queryTxStatus(),
txStatus => {
console.log('tx status', txStatus);
return txStatus === status;
},
`${evidence.txHash} is ${status}`,
() => queryClient.queryBalance(EUD, usdcOnOsmosis),
({ balance }) => !!balance?.amount && BigInt(balance.amount) < mintAmt,
`${EUD} advance available from fast-usdc`,
// this resolves quickly, so _decrease_ the interval so the timing is more apparent
{ retryIntervalMs: 500 },
),
);

await assertTxStatus('ADVANCED');
console.log('Advance completed, waiting for mint...');

nobleTools.mockCctpMint(mintAmount, userForwardingAddr);
await t.notThrowsAsync(() =>
retryUntilCondition(
() => vstorageClient.queryData(`published.${contractName}.poolMetrics`),
({ encumberedBalance }) =>
encumberedBalance && isEmpty(encumberedBalance),
'encumberedBalance returns to 0',
),
);
const queryTxStatus = async () =>
vstorageClient.queryData(
`published.${contractName}.status.${evidence.txHash}`,
);

const assertTxStatus = async (status: string) =>
t.notThrowsAsync(() =>
retryUntilCondition(
() => queryTxStatus(),
txStatus => {
console.log('tx status', txStatus);
return txStatus === status;
},
`${evidence.txHash} is ${status}`,
),
);

await assertTxStatus('ADVANCED');
console.log('Advance completed, waiting for mint...');

nobleTools.mockCctpMint(mintAmt, userForwardingAddr);
await t.notThrowsAsync(() =>
retryUntilCondition(
() => vstorageClient.queryData(`published.${contractName}.poolMetrics`),
({ encumberedBalance }) =>
encumberedBalance && isEmpty(encumberedBalance),
'encumberedBalance returns to 0',
),
);

await assertTxStatus('DISBURSED');
await assertTxStatus('DISBURSED');
},
});

test.serial(advanceAndSettleScenario, LP_DEPOSIT_AMOUNT / 4n, 'osmosis');
test.serial(advanceAndSettleScenario, LP_DEPOSIT_AMOUNT / 8n, 'noble');
test.serial(advanceAndSettleScenario, LP_DEPOSIT_AMOUNT / 5n, 'agoric');

test.serial('lp withdraws', async t => {
const {
lpUser,
Expand Down Expand Up @@ -380,3 +407,7 @@ test.serial('lp withdraws', async t => {
),
);
});

test.todo('insufficient LP funds; forward path');
test.todo('mint while Advancing; still Disbursed');
test.todo('transfer failed (e.g. to cosmos, not in env)');
Loading