diff --git a/a3p-integration/proposals/f:fast-usdc/.gitignore b/a3p-integration/proposals/f:fast-usdc/.gitignore new file mode 100644 index 00000000000..f12e6f870ca --- /dev/null +++ b/a3p-integration/proposals/f:fast-usdc/.gitignore @@ -0,0 +1 @@ +add-operators diff --git a/a3p-integration/proposals/f:fast-usdc/deploy.test.js b/a3p-integration/proposals/f:fast-usdc/deploy.test.js index 1bc0aa6b4d8..991766a29f5 100644 --- a/a3p-integration/proposals/f:fast-usdc/deploy.test.js +++ b/a3p-integration/proposals/f:fast-usdc/deploy.test.js @@ -2,19 +2,14 @@ /* eslint-env node */ import test from 'ava'; import '@endo/init/legacy.js'; // axios compat -import { makeSmartWalletKit } from '@agoric/client-utils'; +import { LOCAL_CONFIG, makeSmartWalletKit } from '@agoric/client-utils'; const io = { delay: ms => new Promise(resolve => setTimeout(() => resolve(undefined), ms)), fetch: global.fetch, }; -const networkConfig = { - rpcAddrs: ['http://0.0.0.0:26657'], - chainName: 'agoriclocal', -}; - test('fastUsdc is in agoricNames.instance', async t => { - const { agoricNames } = await makeSmartWalletKit(io, networkConfig); + const { agoricNames } = await makeSmartWalletKit(io, LOCAL_CONFIG); t.log('agoricNames.instance keys', Object.keys(agoricNames.instance)); t.truthy(agoricNames.instance.fastUsdc); diff --git a/a3p-integration/proposals/f:fast-usdc/operators.test.js b/a3p-integration/proposals/f:fast-usdc/operators.test.js new file mode 100644 index 00000000000..5b63acd77f2 --- /dev/null +++ b/a3p-integration/proposals/f:fast-usdc/operators.test.js @@ -0,0 +1,78 @@ +/* eslint-env node */ +import unknownTest from 'ava'; + +import '@endo/init/legacy.js'; // axios compat + +import { LOCAL_CONFIG, makeVstorageKit } from '@agoric/client-utils'; +import { evalBundles, waitForBlock } from '@agoric/synthetic-chain'; + +// XXX ~copied to not take a dependency on Zoe +/** + * @typedef {object} InvitationDetails + * @property {unknown} installation + * @property {unknown} instance + * @property {InvitationHandle} handle + * @property {string} description + * @property {Record} [customDetails] + */ + +/** + * @import {VstorageKit} from '@agoric/client-utils'; + */ + +const test = /** @type {import('ava').TestFn<{ vstorageKit: VstorageKit}>} */ ( + unknownTest +); + +const ADD_OPERATORS_DIR = 'add-operators'; + +/** + * @typedef {import('@agoric/ertp').NatAmount} NatAmount + * @typedef {{ + * allocations: { Fee: NatAmount, USD_LEMONS: NatAmount }, + * }} ReserveAllocations + */ + +test.before(async t => { + const vstorageKit = makeVstorageKit({ fetch }, LOCAL_CONFIG); + + t.context = { + vstorageKit, + }; +}); + +test.serial('add operators', async t => { + const { vstorageKit } = t.context; + + const walletPath = + // account mem3 in test of crabble-start proposal (64) + // This must match the oracleNew value in the add-operators builder argument + 'wallet.agoric1hmdue96vs0p6zj42aa26x6zrqlythpxnvgsgpr.current'; + + const readInvitationsPurseBalance = async () => { + const curr = await vstorageKit.readPublished(walletPath); + return /** @type {InvitationDetails[]} */ (curr.purses[0].balance.value); + }; + + let numInvitationsBefore = 0; + { + const invBalance = await readInvitationsPurseBalance(); + numInvitationsBefore = invBalance.length; + t.false( + invBalance.some(inv => inv.description === 'oracle operator invitation'), + ); + } + + await evalBundles(ADD_OPERATORS_DIR); + // give time for the invitations to be deposited + await waitForBlock(5); + + { + const invBalance = await readInvitationsPurseBalance(); + console.log('after', invBalance); + t.is(invBalance.length, numInvitationsBefore + 1); + t.true( + invBalance.some(inv => inv.description === 'oracle operator invitation'), + ); + } +}); diff --git a/a3p-integration/proposals/f:fast-usdc/package.json b/a3p-integration/proposals/f:fast-usdc/package.json index dfe43ef1501..a92e00c165b 100644 --- a/a3p-integration/proposals/f:fast-usdc/package.json +++ b/a3p-integration/proposals/f:fast-usdc/package.json @@ -2,7 +2,8 @@ "agoricProposal": { "source": "subdir", "sdk-generate": [ - "fast-usdc/start-fast-usdc.build.js submission --net A3P_INTEGRATION --noNoble" + "fast-usdc/start-fast-usdc.build.js submission --net A3P_INTEGRATION --noNoble", + "fast-usdc/add-operators.build.js add-operators --oracle oracleNew:agoric1hmdue96vs0p6zj42aa26x6zrqlythpxnvgsgpr" ], "type": "/agoric.swingset.CoreEvalProposal" }, diff --git a/packages/boot/test/fast-usdc/fast-usdc.test.ts b/packages/boot/test/fast-usdc/fast-usdc.test.ts index ea9a48f1c85..308b020e32d 100644 --- a/packages/boot/test/fast-usdc/fast-usdc.test.ts +++ b/packages/boot/test/fast-usdc/fast-usdc.test.ts @@ -469,6 +469,8 @@ test.serial('restart contract', async t => { test.serial('replace operators', async t => { const { agoricNamesRemotes, + buildProposal, + evalProposal, storage, runUtils: { EV }, walletFactoryDriver: wfd, @@ -521,6 +523,51 @@ test.serial('replace operators', async t => { } } - // TODO test adding new operators - // The naive approach is failing under XS. A new CoreEval may be necessary. + if (defaultManagerType === 'xs-worker') { + // XXX for some reason the code after this when run under XS fails with: + // message: 'unsettled value for "kp2526"', + return; + } + + // Add some new oracle operator + const { + // any one would do + oracles: { gov1: address }, + } = configurations.A3P_INTEGRATION; + const wallet = await wfd.provideSmartWallet(address); + + const addOperators = buildProposal( + '@agoric/builders/scripts/fast-usdc/add-operators.build.js', + ['--oracle', `gov1a3p:${address}`], + ); + await evalProposal(addOperators); + + await wallet.sendOffer({ + id: 'claim-oracle-invitation', + invitationSpec: { + source: 'purse', + instance: agoricNamesRemotes.instance.fastUsdc, + description: 'oracle operator invitation', + }, + proposal: {}, + }); + console.log('accepted invitation'); + + await wallet.sendOffer({ + id: 'submit', + invitationSpec: { + source: 'continuing', + previousOffer: 'claim-oracle-invitation', + invitationMakerName: 'SubmitEvidence', + invitationArgs: [evidence], + }, + proposal: {}, + }); + console.log('submitted price'); + t.like(wallet.getLatestUpdateRecord(), { + status: { + id: 'submit', + result: 'inert; nothing should be expected from this offer', + }, + }); }); diff --git a/packages/builders/scripts/fast-usdc/add-operators.build.js b/packages/builders/scripts/fast-usdc/add-operators.build.js new file mode 100644 index 00000000000..bc506d5cbe7 --- /dev/null +++ b/packages/builders/scripts/fast-usdc/add-operators.build.js @@ -0,0 +1,83 @@ +// @ts-check +import { makeHelpers } from '@agoric/deploy-script-support'; +import { getManifestForAddOperators } from '@agoric/fast-usdc/src/add-operators.core.js'; +import { toExternalConfig } from '@agoric/fast-usdc/src/utils/config-marshal.js'; +import { configurations } from '@agoric/fast-usdc/src/utils/deploy-config.js'; +import { Far } from '@endo/far'; +import { parseArgs } from 'node:util'; + +/** + * @import {CoreEvalBuilder, DeployScriptFunction} from '@agoric/deploy-script-support/src/externalTypes.js'; + * @import {ParseArgsConfig} from 'node:util'; + * @import {FastUSDCConfig, FeedPolicy} from '@agoric/fast-usdc/src/types.js'; + * @import {FastUSDCOpts} from './start-fast-usdc.build.js'; + */ + +const { keys } = Object; + +/** @type {ParseArgsConfig['options']} */ +const options = { + net: { type: 'string' }, + oracle: { type: 'string', multiple: true }, +}; +const oraclesUsage = 'use --oracle name:address ...'; + +const crossVatContext = /** @type {const} */ ({ + /** @type {Brand<'nat'>} */ + USDC: Far('USDC Brand'), +}); + +/** @type {CoreEvalBuilder} */ +export const defaultProposalBuilder = async ( + powers, + /** @type {FastUSDCConfig} */ config, +) => { + return harden({ + sourceSpec: '@agoric/fast-usdc/src/add-operators.core.js', + /** @type {[string, Parameters[1]]} */ + getManifestCall: [ + getManifestForAddOperators.name, + { + options: toExternalConfig(config, crossVatContext), + }, + ], + }); +}; + +/** @type {DeployScriptFunction} */ +export default async (homeP, endowments) => { + const { writeCoreEval } = await makeHelpers(homeP, endowments); + const { scriptArgs } = endowments; + + /** @type {{ values: FastUSDCOpts }} */ + // @ts-expect-error ensured by options + const { + values: { oracle: oracleArgs, net }, + } = parseArgs({ args: scriptArgs, options }); + + const parseOracleArgs = () => { + if (net) { + if (!(net in configurations)) { + throw Error(`${net} not in ${keys(configurations)}`); + } + return configurations[net].oracles; + } + if (!oracleArgs) throw Error(oraclesUsage); + return Object.fromEntries( + oracleArgs.map(arg => { + const result = arg.match(/(?[^:]+):(?
.+)/); + if (!(result && result.groups)) throw Error(oraclesUsage); + const { name, address } = result.groups; + return [name, address]; + }), + ); + }; + + const config = harden({ + oracles: parseOracleArgs(), + }); + + await writeCoreEval('add-operators', utils => + defaultProposalBuilder(utils, config), + ); +}; diff --git a/packages/fast-usdc/src/add-operators.core.js b/packages/fast-usdc/src/add-operators.core.js new file mode 100644 index 00000000000..a11faa2ebfc --- /dev/null +++ b/packages/fast-usdc/src/add-operators.core.js @@ -0,0 +1,63 @@ +import { makeTracer } from '@agoric/internal'; +import { inviteOracles } from './utils/core-eval.js'; + +/** + * @import {ManifestBundleRef} from '@agoric/deploy-script-support/src/externalTypes.js' + * @import {BootstrapManifest} from '@agoric/vats/src/core/lib-boot.js' + * @import {LegibleCapData} from './utils/config-marshal.js' + * @import {FastUSDCConfig} from './types.js' + * @import {FastUSDCCorePowers, FastUSDCKit} from './start-fast-usdc.core.js'; + */ + +const trace = makeTracer('FUSD-AddOperators', true); + +/** + * @throws if oracle smart wallets are not yet provisioned + * + * @param {BootstrapPowers & FastUSDCCorePowers } powers + * @param {{ options: LegibleCapData }} config + */ +export const addOperators = async ( + { consume: { namesByAddress, fastUsdcKit } }, + config, +) => { + trace(addOperators.name); + + const kit = await fastUsdcKit; + + const { creatorFacet } = kit; + + trace(config); + + // @ts-expect-error XXX LegibleCapData typedef + const { oracles } = config.options.structure; + + await inviteOracles({ creatorFacet, namesByAddress }, oracles); +}; +harden(addOperators); + +/** + * @param {{ + * restoreRef: (b: ERef) => Promise; + * }} utils + * @param {{ + * options: LegibleCapData; + * }} param1 + */ +export const getManifestForAddOperators = ({ restoreRef: _ }, { options }) => { + return { + /** @type {BootstrapManifest} */ + manifest: { + [addOperators.name]: { + consume: { + fastUsdcKit: true, + + // widely shared: name services + agoricNames: true, + namesByAddress: true, + }, + }, + }, + options, + }; +};