Skip to content

Commit ec38514

Browse files
committed
test: fast-usdc advance happy path
1 parent 59643f4 commit ec38514

File tree

4 files changed

+339
-8
lines changed

4 files changed

+339
-8
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,291 @@
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 { commonSetup, type SetupContextWithWallets } from '../support.js';
12+
import { makeFeedPolicy, oracleMnemonics } from './config.js';
13+
import { makeRandomDigits } from '../../tools/random.js';
14+
15+
const { keys, values, fromEntries } = Object;
16+
const { isGTE, isEmpty, make } = AmountMath;
17+
18+
const test = anyTest as TestFn<
19+
SetupContextWithWallets & {
20+
lpUser: WalletDriver;
21+
oracleWds: WalletDriver[];
22+
nobleAgoricChannelId: IBCChannelID;
23+
usdcOnOsmosis: Denom;
24+
}
25+
>;
26+
27+
const accounts = [...keys(oracleMnemonics), 'lp'];
28+
const contractName = 'fastUsdc';
29+
const contractBuilder =
30+
'../packages/builders/scripts/fast-usdc/init-fast-usdc.js';
31+
32+
test.before(async t => {
33+
const { setupTestKeys, ...common } = await commonSetup(t);
34+
const {
35+
chainInfo,
36+
commonBuilderOpts,
37+
deleteTestKeys,
38+
faucetTools,
39+
provisionSmartWallet,
40+
startContract,
41+
} = common;
42+
deleteTestKeys(accounts).catch();
43+
const wallets = await setupTestKeys(accounts, values(oracleMnemonics));
44+
45+
// provision oracle wallets first so invitation deposits don't fail
46+
const oracleWds = await Promise.all(
47+
keys(oracleMnemonics).map(n =>
48+
provisionSmartWallet(wallets[n], {
49+
BLD: 100n,
50+
}),
51+
),
52+
);
53+
54+
// calculate denomHash and channelId for privateArgs / builder opts
55+
const { getTransferChannelId, toDenomHash } = makeDenomTools(chainInfo);
56+
const usdcDenom = toDenomHash('uusdc', 'noblelocal', 'agoric');
57+
const usdcOnOsmosis = toDenomHash('uusdc', 'noblelocal', 'osmosis');
58+
const nobleAgoricChannelId = getTransferChannelId('agoriclocal', 'noble');
59+
if (!nobleAgoricChannelId) throw new Error('nobleAgoricChannelId not found');
60+
t.log('nobleAgoricChannelId', nobleAgoricChannelId);
61+
t.log('usdcDenom', usdcDenom);
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 = nobleTools.registerForwardingAcct(
194+
nobleAgoricChannelId,
195+
recipientAddress,
196+
);
197+
t.is(txRes?.code, 0, 'registered forwarding account');
198+
199+
const { address: userForwardingAddr } = nobleTools.queryForwardingAddress(
200+
nobleAgoricChannelId,
201+
recipientAddress,
202+
);
203+
t.log('got forwardingAddress', userForwardingAddr);
204+
205+
const mintAmount = 800_000n;
206+
207+
// TODO export CctpTxEvidence type
208+
const evidence = harden({
209+
blockHash:
210+
'0x90d7343e04f8160892e94f02d6a9b9f255663ed0ac34caca98544c8143fee665',
211+
blockNumber: 21037663n,
212+
txHash: `0xc81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff3875527617${makeRandomDigits(2n)}`,
213+
tx: {
214+
amount: mintAmount,
215+
forwardingAddress: userForwardingAddr,
216+
},
217+
aux: {
218+
forwardingChannel: nobleAgoricChannelId,
219+
recipientAddress,
220+
},
221+
chainId: 42161,
222+
});
223+
224+
console.log('User initiates evm mint', evidence.txHash);
225+
226+
// submit evidences
227+
await Promise.all(
228+
oracleWds.map(makeDoOffer).map((doOffer, i) =>
229+
doOffer({
230+
id: `${Date.now()}-evm-evidence`,
231+
invitationSpec: {
232+
source: 'continuing',
233+
previousOffer: toOracleOfferId(i),
234+
invitationMakerName: 'SubmitEvidence',
235+
invitationArgs: [evidence],
236+
},
237+
proposal: {},
238+
}),
239+
),
240+
);
241+
242+
const queryClient = makeQueryClient(
243+
await useChain('osmosis').getRestEndpoint(),
244+
);
245+
246+
await t.notThrowsAsync(() =>
247+
retryUntilCondition(
248+
() => queryClient.queryBalance(eudAddress, usdcOnOsmosis),
249+
({ balance }) => !!balance?.amount && BigInt(balance.amount) < mintAmount,
250+
`${eudAddress} advance available from fast-usdc`,
251+
{
252+
// this resolves quickly, so _decrease_ the interval so the timing is more apparent
253+
retryIntervalMs: 500,
254+
},
255+
),
256+
);
257+
258+
const queryTxStatus = async () =>
259+
vstorageClient.queryData(
260+
`published.${contractName}.status.${evidence.txHash}`,
261+
);
262+
263+
const assertTxStatus = async (status: string) =>
264+
t.notThrowsAsync(() =>
265+
retryUntilCondition(
266+
() => queryTxStatus(),
267+
txStatus => {
268+
console.log('tx status', txStatus);
269+
return txStatus === status;
270+
},
271+
`${evidence.txHash} is ${status}`,
272+
),
273+
);
274+
275+
await assertTxStatus('ADVANCED');
276+
console.log('Advance completed, waiting for mint...');
277+
278+
nobleTools.mockCctpMint(mintAmount, userForwardingAddr);
279+
await t.notThrowsAsync(() =>
280+
retryUntilCondition(
281+
() => vstorageClient.queryData(`published.${contractName}.poolMetrics`),
282+
({ encumberedBalance }) =>
283+
encumberedBalance && isEmpty(encumberedBalance),
284+
'encumberedBalance returns to 0',
285+
),
286+
);
287+
288+
await assertTxStatus('DISBURSED');
289+
});
290+
291+
test.todo('lp withdraws and earns fees');

multichain-testing/tools/noble-tools.ts

+15-8
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@ const makeKubeArgs = () => {
1919
];
2020
};
2121

22-
export const makeNobleTools = ({
23-
execFileSync,
24-
}: {
25-
execFileSync: ExecSync;
26-
}) => {
22+
export const makeNobleTools = (
23+
{
24+
execFileSync,
25+
}: {
26+
execFileSync: ExecSync;
27+
},
28+
log: (...args: unknown[]) => void = console.log,
29+
) => {
2730
const exec = (
2831
args: string[],
2932
opts = { encoding: 'utf-8' as const, stdio: ['ignore', 'pipe', 'ignore'] },
@@ -38,8 +41,9 @@ export const makeNobleTools = ({
3841
const registerForwardingAcct = (
3942
channelId: IBCChannelID,
4043
address: ChainAddress['value'],
41-
) => {
44+
): { txhash: string; code: number; data: string; height: string } => {
4245
checkEnv();
46+
log('creating forwarding address', address, channelId);
4347
return JSON.parse(
4448
exec([
4549
'tx',
@@ -57,14 +61,16 @@ export const makeNobleTools = ({
5761

5862
const mockCctpMint = (amount: bigint, destination: ChainAddress['value']) => {
5963
checkEnv();
64+
const denomAmount = `${Number(amount)}uusdc`;
65+
log('mock cctp mint', destination, denomAmount);
6066
return JSON.parse(
6167
exec([
6268
'tx',
6369
'bank',
6470
'send',
6571
'faucet',
6672
destination,
67-
`${Number(amount)}uusdc`,
73+
denomAmount,
6874
'--from=faucet',
6975
'-y',
7076
'-b',
@@ -76,8 +82,9 @@ export const makeNobleTools = ({
7682
const queryForwardingAddress = (
7783
channelId: IBCChannelID,
7884
address: ChainAddress['value'],
79-
) => {
85+
): { address: string; exists: boolean } => {
8086
checkEnv();
87+
log('querying forwarding address', address, channelId);
8188
return JSON.parse(
8289
exec([
8390
'query',

multichain-testing/tools/random.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export function makeRandomDigits(digits = 2n) {
2+
if (digits < 1n) throw new Error('digits must be positive');
3+
const maxValue = Math.pow(10, Number(digits)) - 1;
4+
const num = Math.floor(Math.random() * (maxValue + 1));
5+
return num.toString().padStart(Number(digits), '0');
6+
}

0 commit comments

Comments
 (0)