Skip to content

Commit

Permalink
tools: fast-usdc bootstrap script
Browse files Browse the repository at this point in the history
utility script to assist with local development. includes:
- start contract with optional --oracles
- fund agoric faucet with USDC from remote chain
- fund Liquidity Pool for USDC
- provision smart wallet (using --mnemonic)
  • Loading branch information
0xpatrickdev committed Jan 9, 2025
1 parent 5043b8d commit 378f571
Show file tree
Hide file tree
Showing 7 changed files with 297 additions and 18 deletions.
2 changes: 1 addition & 1 deletion multichain-testing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"lint-fix": "yarn lint:eslint --fix",
"test": "echo 'Run specific test suites:\nyarn test:main (needs `make start`)\nyarn test:fast-usdc (needs `make start FILE=config.fusdc.yaml`)'",
"test:main": "ava --config ava.main.config.js",
"test:fast-usdc": "FILE=config.fusdc.yaml ava --config ava.fusdc.config.js",
"test:fast-usdc": "ava --config ava.fusdc.config.js",
"starship:setup": "make setup-deps setup-kind",
"starship:install": "make install",
"starship:port-forward": "make port-forward",
Expand Down
283 changes: 283 additions & 0 deletions multichain-testing/scripts/fast-usdc-tool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
#!/usr/bin/env -S node --import ts-blank-space/register
/**
* @file tools for local integration testing for FastUSDC. See USAGE.
*/
import '@endo/init';
import { parseArgs } from 'node:util';
import type { ExecutionContext } from 'ava';
import { encodeAddressHook } from '@agoric/cosmic-proto/address-hooks.js';
import { AmountMath, type Brand } from '@agoric/ertp';
import type { USDCProposalShapes } from '@agoric/fast-usdc/src/pool-share-math.js';
import type { PoolMetrics } from '@agoric/fast-usdc/src/types.js';
import { divideBy } from '@agoric/zoe/src/contractSupport/ratio.js';
import { makeDenomTools } from '../tools/asset-info.js';
import { makeDoOffer } from '../tools/e2e-tools.js';
import { commonSetup } from '../test/support.js';
import {
makeFeedPolicyPartial,
oracleMnemonics,
} from '../test/fast-usdc/config.js';

const USAGE = `
Usage:
No arguments - start the contract and fund the liquidity pool
start - only start the contract
fund-pool - fund the FUSDC Liquidity Pool
provision-wallet - provision a smart wallet (requires --mnemonic)
fund-faucet - fund the faucet account with bridged USDC
register-forwarding - register forwarding account on noble (requires --eud)
--oracle - Comma-separated list of oracle addresses
Examples:
./fast-usdc-tool.ts
./fast-usdc-tool.ts --oracle oracle1:addr1,oracle2:addr2
./fast-usdc-tool.ts start
./fast-usdc-tool.ts start --oracle oracle1:addr1,oracle2:addr2
./fast-usdc-tool.ts fund-pool
./fast-usdc-tool.ts fund-faucet
./fast-usdc-tool.ts register-forwarding --eud osmo123
`;

const contractName = 'fastUsdc';
const contractBuilder =
'../packages/builders/scripts/fast-usdc/init-fast-usdc.js';

/** ava test context partial, to appease dependencies expecting this */
const runT = {
log: console.log,
is: (condition: unknown, expected: unknown, message: string) => {
if (condition !== expected) {
throw new Error(
`Condition: ${message} failed. Expected ${expected} got ${condition}.`,
);
}
},
} as ExecutionContext;

// from ../test/fast-usdc/fast-usdc.test.ts
type VStorageClient = Awaited<ReturnType<typeof commonSetup>>['vstorageClient'];
const agoricNamesQ = (vsc: VStorageClient) =>
harden({
brands: <K extends AssetKind>(_assetKind: K) =>
vsc
.queryData('published.agoricNames.brand')
.then(pairs => Object.fromEntries(pairs) as Record<string, Brand<K>>),
});

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 parseCommandLine = () => {
const { values, positionals } = parseArgs({
options: {
eud: {
type: 'string',
},
oracle: {
type: 'string',
},
mnemonic: {
type: 'string',
},
help: {
type: 'boolean',
},
},
allowPositionals: true,
});

if (values.help) {
console.log(USAGE);
return undefined;
}

const command = positionals[0];
const mnemonic = values.mnemonic;
const suppliedOracles = values.oracle?.split(',');
const oracles = suppliedOracles || [
'oracle1:agoric1yupasge4528pgkszg9v328x4faxtkldsnygwjl',
'oracle2:agoric1dh04lnl7epr7l4cpvqqprxvam7ewdswj7yv6ep',
'oracle3:agoric1ujmk0492mauq2f2vrcn7ylq3w3x55k0ap9mt2p',
];
const eud = values.eud;

return { command, eud, mnemonic, oracles, provisionOracles: true };
};

const main = async () => {
const job = parseCommandLine();
if (!job) return undefined;
const { command, eud, mnemonic, oracles, provisionOracles } = job;
const {
chainInfo,
commonBuilderOpts,
deleteTestKeys,
faucetTools,
nobleTools,
provisionSmartWallet,
setupTestKeys,
startContract,
vstorageClient,
} = await commonSetup(runT, { config: '../config.fusdc.yaml' });

const assertProvisioned = async address => {
try {
await vstorageClient.queryData(`published.wallet.${address}.current`);
} catch {
throw new Error(`${address} is not provisioned`);
}
};

const provisionWallet = async (mnemonic: string) => {
// provision-one must be called by the owner, so we need to add the key to the test keyring
const keyname = 'temp';
const address = (await setupTestKeys([keyname], [mnemonic]))[keyname];
try {
await provisionSmartWallet(address, {
BLD: 100n,
IST: 100n,
});
} finally {
await deleteTestKeys([keyname]);
}
};

const start = async () => {
if (!chainInfo.noble) {
console.debug('Chain Infos', Object.keys(chainInfo));
throw new Error(
'Noble chain not running. Try `make start FILE=config.fusdc.yaml`',
);
}
const { getTransferChannelId, toDenomHash } = makeDenomTools(chainInfo);
const usdcDenom = toDenomHash('uusdc', 'noblelocal', 'agoric');
const nobleAgoricChannelId = getTransferChannelId('agoriclocal', 'noble');
if (!nobleAgoricChannelId)
throw new Error('nobleAgoricChannelId not found');
console.log('nobleAgoricChannelId', nobleAgoricChannelId);
console.log('usdcDenom', usdcDenom);

for (const oracle of oracles) {
if (provisionOracles) {
await provisionWallet(oracleMnemonics[oracle.split(':')[0]]);
} else {
console.log(`Confirming ${oracle} smart wallet provisioned...`);
// oracles must be provisioned before the contract starts
await assertProvisioned(oracle.split(':')[1]);
}
}

await startContract(contractName, contractBuilder, {
oracle: oracles,
usdcDenom,
feedPolicy: JSON.stringify(makeFeedPolicyPartial(nobleAgoricChannelId)),
...commonBuilderOpts,
});
};

const fundFaucet = async () => faucetTools.fundFaucet([['noble', 'uusdc']]);

const fundLiquidityPool = async () => {
await fundFaucet();
const accounts = ['lp'];
await deleteTestKeys(accounts).catch();
const wallets = await setupTestKeys(accounts);
const lpUser = await provisionSmartWallet(wallets['lp'], {
USDC: 8_000n,
BLD: 100n,
});
const lpDoOffer = makeDoOffer(lpUser);
const { USDC } = await agoricNamesQ(vstorageClient).brands('nat');
const { shareWorth } = await fastLPQ(vstorageClient).metrics();

const LP_DEPOSIT_AMOUNT = 8_000n * 10n ** 6n;
const give = { USDC: AmountMath.make(USDC as Brand, LP_DEPOSIT_AMOUNT) };
const want = { PoolShare: divideBy(give.USDC, shareWorth) };
const proposal: USDCProposalShapes['deposit'] = harden({ give, want });

await lpDoOffer({
id: `lp-deposit-${Date.now()}`,
invitationSpec: {
source: 'agoricContract',
instancePath: [contractName],
callPipe: [['makeDepositInvitation']],
},
// @ts-expect-error 'NatAmount' vs 'AnyAmount'
proposal,
});
};

const registerForwardingAccount = async (EUD: string) => {
console.log('eud', EUD);
const { settlementAccount } = await vstorageClient.queryData(
`published.${contractName}`,
);
console.log('settlementAccount:', settlementAccount);

const recipientAddress = encodeAddressHook(settlementAccount, {
EUD,
});
console.log('recipientAddress:', recipientAddress);

const { getTransferChannelId } = makeDenomTools(chainInfo);
const nobleAgoricChannelId = getTransferChannelId('agoriclocal', 'noble');
if (!nobleAgoricChannelId)
throw new Error('nobleAgoricChannelId not found');

const txRes = nobleTools.registerForwardingAcct(
nobleAgoricChannelId,
recipientAddress,
);
runT.is(txRes?.code, 0, 'registered forwarding account');

const { address } = nobleTools.queryForwardingAddress(
nobleAgoricChannelId,
recipientAddress,
);
console.log('forwardingAddress:', address);
return address;
};

// Execute commands based on input
switch (command) {
case 'start':
await start();
break;
case 'fund-pool':
await fundLiquidityPool();
break;
case 'fund-faucet':
await fundFaucet();
break;
case 'provision-wallet':
if (!mnemonic) {
throw new Error('--mnemonic is required for provision-wallet command');
}
await provisionWallet(mnemonic);
break;
case 'register-forwarding':
if (!eud) {
throw new Error('--eud is required for register-forwarding command');
}
await registerForwardingAccount(eud);
break;
default:
// No command provided - run both start and fundLiquidityPool
await start();
await fundLiquidityPool();
}
};

main().catch(error => {
console.error('An error occurred:', error);
process.exit(1);
});
4 changes: 2 additions & 2 deletions multichain-testing/test/fast-usdc/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ export const oracleMnemonics = {
};
harden(oracleMnemonics);

export const makeFeedPolicy = (
export const makeFeedPolicyPartial = (
nobleAgoricChannelId: IBCChannelID,
): Omit<FeedPolicy, 'chainPolicies'> => {
return {
nobleAgoricChannelId,
nobleDomainId: 4,
};
};
harden(makeFeedPolicy);
harden(makeFeedPolicyPartial);
8 changes: 5 additions & 3 deletions multichain-testing/test/fast-usdc/fast-usdc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { makeDenomTools } from '../../tools/asset-info.js';
import { createWallet } from '../../tools/wallet.js';
import { makeQueryClient } from '../../tools/query.js';
import { commonSetup, type SetupContextWithWallets } from '../support.js';
import { makeFeedPolicy, oracleMnemonics } from './config.js';
import { makeFeedPolicyPartial, oracleMnemonics } from './config.js';
import { makeRandomDigits } from '../../tools/random.js';
import { makeTracer } from '@agoric/internal';
import type {
Expand Down Expand Up @@ -48,7 +48,9 @@ const contractBuilder =
const LP_DEPOSIT_AMOUNT = 8_000n * 10n ** 6n;

test.before(async t => {
const { setupTestKeys, ...common } = await commonSetup(t);
const { setupTestKeys, ...common } = await commonSetup(t, {
config: '../config.fusdc.yaml',
});
const {
chainInfo,
commonBuilderOpts,
Expand Down Expand Up @@ -81,7 +83,7 @@ test.before(async t => {
await startContract(contractName, contractBuilder, {
oracle: keys(oracleMnemonics).map(n => `${n}:${wallets[n]}`),
usdcDenom,
feedPolicy: JSON.stringify(makeFeedPolicy(nobleAgoricChannelId)),
feedPolicy: JSON.stringify(makeFeedPolicyPartial(nobleAgoricChannelId)),
...commonBuilderOpts,
});

Expand Down
2 changes: 1 addition & 1 deletion multichain-testing/test/fast-usdc/noble-forwarding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const test = anyTest as TestFn<SetupContext>;

test('noble forwarding', async t => {
const { nobleTools, retryUntilCondition, useChain, vstorageClient } =
await commonSetup(t);
await commonSetup(t, { config: '../config.fusdc.yaml' });

const agoricWallet = await createWallet('agoric');
const agoricAddr = (await agoricWallet.getAccounts())[0].address;
Expand Down
7 changes: 5 additions & 2 deletions multichain-testing/test/support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,14 @@ const makeKeyring = async (
return { setupTestKeys, deleteTestKeys };
};

export const commonSetup = async (t: ExecutionContext) => {
export const commonSetup = async (
t: ExecutionContext,
{ config = '../config.yaml' } = {},
) => {
let useChain: MultichainRegistry['useChain'];
try {
const registry = await setupRegistry({
config: `../${process.env.FILE || 'config.yaml'}`,
config,
});
useChain = registry.useChain;
} catch (e) {
Expand Down
9 changes: 0 additions & 9 deletions multichain-testing/tools/noble-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,10 @@ export const makeNobleTools = (
opts = { encoding: 'utf-8' as const, stdio: ['ignore', 'pipe', 'ignore'] },
) => execFileSync(kubectlBinary, [...makeKubeArgs(), ...args], opts);

const checkEnv = () => {
if (process.env.FILE !== 'config.fusdc.yaml') {
console.error('Warning: Noble chain must be running for this to work');
}
};

const registerForwardingAcct = (
channelId: IBCChannelID,
address: ChainAddress['value'],
): { txhash: string; code: number; data: string; height: string } => {
checkEnv();
log('creating forwarding address', address, channelId);
return JSON.parse(
exec([
Expand All @@ -61,7 +54,6 @@ export const makeNobleTools = (
};

const mockCctpMint = (amount: bigint, destination: ChainAddress['value']) => {
checkEnv();
const denomAmount = `${Number(amount)}uusdc`;
log('mock cctp mint', destination, denomAmount);
return JSON.parse(
Expand All @@ -84,7 +76,6 @@ export const makeNobleTools = (
channelId: IBCChannelID,
address: ChainAddress['value'],
): { address: NobleAddress; exists: boolean } => {
checkEnv();
log('querying forwarding address', address, channelId);
return JSON.parse(
exec([
Expand Down

0 comments on commit 378f571

Please sign in to comment.