|
| 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'); |
0 commit comments