Skip to content

Commit db72909

Browse files
committed
test: fast-usdc advance happy path
1 parent a5e9d5b commit db72909

File tree

4 files changed

+324
-2
lines changed

4 files changed

+324
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { IBCChannelID } from '@agoric/vats';
2+
3+
export const oracleMnemonics = {
4+
oracle1:
5+
'cause eight cattle slot course mail more aware vapor slab hobby match',
6+
oracle2:
7+
'flower salute inspire label latin cattle believe sausage match total bless refuse',
8+
oracle3:
9+
'surge magnet typical drive cement artist stay latin chief obey word always',
10+
};
11+
harden(oracleMnemonics);
12+
13+
export const makeFeedPolicy = (nobleAgoricChannelId: IBCChannelID) => {
14+
return JSON.stringify({
15+
nobleAgoricChannelId,
16+
nobleDomainId: 4,
17+
chainPolicies: {
18+
Arbitrum: {
19+
cctpTokenMessengerAddress: '0x19330d10D9Cc8751218eaf51E8885D058642E08A',
20+
chainId: 42161,
21+
confirmations: 2,
22+
nobleContractAddress: '0x19330d10D9Cc8751218eaf51E8885D058642E08A',
23+
},
24+
},
25+
});
26+
};
27+
harden(makeFeedPolicy);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
import anyTest from '@endo/ses-ava/prepare-endo.js';
2+
import type { TestFn } from 'ava';
3+
import { AmountMath } from '@agoric/ertp';
4+
import type { Denom } from '@agoric/orchestration';
5+
import { divideBy } from '@agoric/zoe/src/contractSupport/ratio.js';
6+
import type { IBCChannelID } from '@agoric/vats';
7+
import { makeDoOffer, type WalletDriver } from '../../tools/e2e-tools.js';
8+
import { makeDenomTools } from '../../tools/asset-info.js';
9+
import { createWallet } from '../../tools/wallet.js';
10+
import { makeQueryClient } from '../../tools/query.js';
11+
import { makeFundAndTransfer } from '../../tools/ibc-transfer.js';
12+
import { commonSetup, type SetupContextWithWallets } from '../support.js';
13+
import { makeFeedPolicy, oracleMnemonics } from './config.js';
14+
import { makeRandomDigits } from '../../tools/random.js';
15+
16+
const { keys, values, fromEntries } = Object;
17+
const { isGTE, isEmpty, make } = AmountMath;
18+
19+
const test = anyTest as TestFn<
20+
SetupContextWithWallets & {
21+
lpUser: WalletDriver;
22+
oracleWds: WalletDriver[];
23+
nobleAgoricChannelId: IBCChannelID;
24+
usdcOnOsmosis: Denom;
25+
}
26+
>;
27+
28+
const accounts = [...keys(oracleMnemonics), 'lp'];
29+
const contractName = 'fastUsdc';
30+
const contractBuilder =
31+
'../packages/builders/scripts/fast-usdc/init-fast-usdc.js';
32+
33+
test.before(async t => {
34+
const { setupTestKeys, ...common } = await commonSetup(t);
35+
const {
36+
chainInfo,
37+
commonBuilderOpts,
38+
deleteTestKeys,
39+
faucetTools,
40+
provisionSmartWallet,
41+
startContract,
42+
} = common;
43+
deleteTestKeys(accounts).catch();
44+
const wallets = await setupTestKeys(accounts, values(oracleMnemonics));
45+
46+
// provision oracle wallets first so invitation deposits don't fail
47+
const oracleWds = await Promise.all(
48+
keys(oracleMnemonics).map(n =>
49+
provisionSmartWallet(wallets[n], {
50+
BLD: 100n,
51+
}),
52+
),
53+
);
54+
55+
// calculate denomHash and channelId for privateArgs / builder opts
56+
const { getTransferChannelId, toDenomHash } = makeDenomTools(chainInfo);
57+
const usdcDenom = toDenomHash('uusdc', 'noblelocal', 'agoric');
58+
const usdcOnOsmosis = toDenomHash('uusdc', 'noblelocal', 'osmosis');
59+
const nobleAgoricChannelId = getTransferChannelId('agoriclocal', 'noble');
60+
if (!nobleAgoricChannelId) throw new Error('nobleAgoricChannelId not found');
61+
t.log('nobleAgoricChannelId', nobleAgoricChannelId);
62+
63+
await startContract(contractName, contractBuilder, {
64+
oracle: keys(oracleMnemonics).map(n => `${n}:${wallets[n]}`),
65+
usdcDenom: usdcDenom,
66+
feedPolicy: makeFeedPolicy(nobleAgoricChannelId),
67+
...commonBuilderOpts,
68+
});
69+
70+
// provide faucet funds for LPs
71+
await faucetTools.fundFaucet([['noble', 'uusdc']]);
72+
73+
// save an LP in test context
74+
const lpUser = await provisionSmartWallet(wallets['lp'], {
75+
USDC: 100n,
76+
BLD: 100n,
77+
});
78+
79+
t.context = {
80+
...common,
81+
lpUser,
82+
oracleWds,
83+
nobleAgoricChannelId,
84+
usdcOnOsmosis,
85+
wallets,
86+
};
87+
});
88+
89+
test.after(async t => {
90+
const { deleteTestKeys } = t.context;
91+
deleteTestKeys(accounts);
92+
});
93+
94+
const toOracleOfferId = (idx: number) => `oracle${idx + 1}-accept`;
95+
96+
test.serial('oracles accept', async t => {
97+
const { oracleWds, retryUntilCondition, vstorageClient, wallets } = t.context;
98+
99+
const instances = await vstorageClient.queryData(
100+
'published.agoricNames.instance',
101+
);
102+
const instance = fromEntries(instances)[contractName];
103+
104+
// accept oracle operator invitations
105+
await Promise.all(
106+
oracleWds.map(makeDoOffer).map((doOffer, i) =>
107+
doOffer({
108+
id: toOracleOfferId(i),
109+
invitationSpec: {
110+
source: 'purse',
111+
instance,
112+
description: 'oracle operator invitation', // TODO export/import INVITATION_MAKERS_DESC
113+
},
114+
proposal: {},
115+
}),
116+
),
117+
);
118+
119+
for (const name of keys(oracleMnemonics)) {
120+
const addr = wallets[name];
121+
await t.notThrowsAsync(() =>
122+
retryUntilCondition(
123+
() => vstorageClient.queryData(`published.wallet.${addr}.current`),
124+
({ offerToUsedInvitation }) => {
125+
return offerToUsedInvitation[0][0] === `${name}-accept`;
126+
},
127+
`${name} invitation used`,
128+
),
129+
);
130+
}
131+
});
132+
133+
test.serial('lp deposits', async t => {
134+
const { lpUser, retryUntilCondition, vstorageClient } = t.context;
135+
136+
const lpDoOffer = makeDoOffer(lpUser);
137+
const brands = await vstorageClient.queryData('published.agoricNames.brand');
138+
const { USDC } = Object.fromEntries(brands);
139+
140+
const usdcGive = make(USDC, 10_000_000n);
141+
142+
const { shareWorth: currShareWorth } = await vstorageClient.queryData(
143+
`published.${contractName}.poolMetrics`,
144+
);
145+
146+
await lpDoOffer({
147+
id: `lp-deposit-${Date.now()}`,
148+
invitationSpec: {
149+
source: 'agoricContract',
150+
instancePath: [contractName],
151+
callPipe: [['makeDepositInvitation']],
152+
},
153+
proposal: {
154+
give: { USDC: usdcGive },
155+
want: { PoolShare: divideBy(usdcGive, currShareWorth) },
156+
},
157+
});
158+
159+
await t.notThrowsAsync(() =>
160+
retryUntilCondition(
161+
() => vstorageClient.queryData(`published.${contractName}.poolMetrics`),
162+
({ shareWorth }) =>
163+
!isGTE(currShareWorth.numerator, shareWorth.numerator),
164+
'share worth numerator increases from deposit',
165+
),
166+
);
167+
});
168+
169+
test.serial('advance and settlement', async t => {
170+
const {
171+
nobleTools,
172+
nobleAgoricChannelId,
173+
oracleWds,
174+
retryUntilCondition,
175+
useChain,
176+
usdcOnOsmosis,
177+
vstorageClient,
178+
} = t.context;
179+
180+
// EUD wallet on osmosis
181+
const eudWallet = await createWallet(useChain('osmosis').chain.bech32_prefix);
182+
const eudAddress = (await eudWallet.getAccounts())[0].address;
183+
184+
// parameterize agoric address
185+
const { settlementAccount } = await vstorageClient.queryData(
186+
`published.${contractName}`,
187+
);
188+
// TODO #10614 use bech32 encoding
189+
const recipientAddress = `${settlementAccount}?EUD=${eudAddress}`;
190+
t.log('recipientAddress', recipientAddress);
191+
192+
// register forwarding address on noble
193+
const txRes = await nobleTools.registerForwardingAcct(
194+
nobleAgoricChannelId,
195+
eudAddress,
196+
);
197+
t.is(txRes?.code, 0, 'registered forwarding account');
198+
199+
const { address: userForwardingAddr } =
200+
await nobleTools.queryForwardingAddress(nobleAgoricChannelId, eudAddress);
201+
t.log('got forwardingAddress', userForwardingAddr);
202+
203+
const mintAmount = 800_000n;
204+
205+
// TODO export CctpTxEvidence type
206+
const evidence = harden({
207+
blockHash:
208+
'0x90d7343e04f8160892e94f02d6a9b9f255663ed0ac34caca98544c8143fee665',
209+
blockNumber: 21037663n,
210+
txHash: `0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff3875527617${makeRandomDigits(2n)}`,
211+
tx: {
212+
amount: mintAmount,
213+
forwardingAddress: userForwardingAddr,
214+
},
215+
aux: {
216+
forwardingChannel: nobleAgoricChannelId,
217+
recipientAddress,
218+
},
219+
chainId: 42161,
220+
});
221+
222+
console.log('User initiates evm mint', evidence.txHash);
223+
224+
// submit evidences
225+
await Promise.all(
226+
oracleWds.map(makeDoOffer).map((doOffer, i) =>
227+
doOffer({
228+
id: `${Date.now()}-evm-evidence`,
229+
invitationSpec: {
230+
source: 'continuing',
231+
previousOffer: toOracleOfferId(i),
232+
invitationMakerName: 'SubmitEvidence',
233+
invitationArgs: [evidence],
234+
},
235+
proposal: {},
236+
}),
237+
),
238+
);
239+
240+
const queryClient = makeQueryClient(
241+
await useChain('osmosis').getRestEndpoint(),
242+
);
243+
244+
await t.notThrowsAsync(() =>
245+
retryUntilCondition(
246+
() => queryClient.queryBalance(eudAddress, usdcOnOsmosis),
247+
({ balance }) => !!balance?.amount && BigInt(balance.amount) < mintAmount,
248+
`${eudAddress} advance available from fast-usdc`,
249+
{
250+
// this resolves quickly, so _decrease_ the interval so the timing is more apparent
251+
retryIntervalMs: 500,
252+
},
253+
),
254+
);
255+
256+
console.log('Advance completed, waiting for mint...');
257+
258+
await nobleTools.mockCctpMint(mintAmount, userForwardingAddr);
259+
// TODO #10614: notThrowsAsync
260+
// Noble executes transfer but it's not recognized by vtransfer
261+
await t.throwsAsync(() =>
262+
retryUntilCondition(
263+
() => vstorageClient.queryData(`published.${contractName}.poolMetrics`),
264+
({ encumberedBalance }) =>
265+
encumberedBalance && isEmpty(encumberedBalance),
266+
'encumberedBalance returns to 0',
267+
),
268+
);
269+
});
270+
271+
// remove once settlement works, or move to failure path testing
272+
test.serial('tap settler', async t => {
273+
const { retryUntilCondition, vstorageClient, useChain } = t.context;
274+
const fundAndTransfer = makeFundAndTransfer(t, retryUntilCondition, useChain);
275+
276+
const { settlementAccount } = await vstorageClient.queryData(
277+
`published.${contractName}`,
278+
);
279+
await fundAndTransfer('noble', settlementAccount, 10_000_000n, 'uusdc');
280+
281+
// consider testing that no metrics change
282+
// for now, seeing "no EUD parameter" works to know an IBC transfer to the
283+
// forwarding address works / tap was registered.
284+
t.pass();
285+
});
286+
287+
test.todo('lp withdraws and earns fees');

multichain-testing/tools/noble-tools.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const makeNobleTools = ({
3838
const registerForwardingAcct = (
3939
channelId: IBCChannelID,
4040
address: ChainAddress['value'],
41-
) => {
41+
): { txhash: string; code: number; data: string; height: string } => {
4242
checkEnv();
4343
return JSON.parse(
4444
exec([
@@ -76,7 +76,7 @@ export const makeNobleTools = ({
7676
const queryForwardingAddress = (
7777
channelId: IBCChannelID,
7878
address: ChainAddress['value'],
79-
) => {
79+
): { address: string; exists: boolean } => {
8080
checkEnv();
8181
return JSON.parse(
8282
exec([

multichain-testing/tools/random.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Fail } from '@endo/errors';
2+
3+
export function makeRandomDigits(digits = 2n) {
4+
digits > 0n || Fail`digits must be positive`;
5+
const maxValue = Math.pow(10, Number(digits)) - 1;
6+
const num = Math.floor(Math.random() * (maxValue + 1));
7+
return num.toString().padStart(Number(digits), '0');
8+
}

0 commit comments

Comments
 (0)