Skip to content

Commit

Permalink
feat(fast-usdc): operator attest cli command (#10610)
Browse files Browse the repository at this point in the history
closes: #10567

## Description

Fill in the operator `attest` action and test it.

### Security Considerations

none; it writes to stdout

### Scaling / Upgrade Considerations

none

### Documentation Considerations

```
$ yarn fast-usdc operator attest --help
Usage: fast-usdc operator attest [options]

Attest to an observed Fast USDC transfer

Options:
  --previousOfferId <string>    Offer id
  --forwardingChannel <string>  Channel id
  --recipientAddress <string>   bech32 address
  --blockHash <0xhex>           hex hash
  --blockNumber <number>        number
  --blockTimestamp <number>     number
  --chainId <string>            chain id
  --amount <number>             number
  --forwardingAddress <string>  bech32 address
  --txHash <0xhexo>             hex hash
  --offerId <string>            Offer id (default: "operatorAttest-1733212600270")
  -h, --help                    display help for command
```

It's quite minimalistic. It requires a `--previousOfferId` without providing a `find-continuing-id` sub-command.

It doesn't handle sign/broadcast nor reporting the outcome of the offer. I suppose `agops perf satisfaction` is available for that; I just hope don't end up relying on `agops` in production use.

### Testing Considerations

One happy-path test, using injected io, to check that the bridgeAction that it writes is as expected.

```
  ✔ fast-usdc operator attest sub-command
    ℹ node fast-usdc operator attest --previousOfferId 123 --forwardingChannel channel-21 --recipientAddress agoric16kv2g7snfc4q24vg3pjdlnnqgngtjpwtetd2h689nz09lcklvh5s8u37ek?EUD=dydx183dejcnmkka5dzcu9xw6mywq0p2m5peks28men --amount 300000000 --forwardingAddress noble1x0ydg69dh6fqvr27xjvp6maqmrldam6yfelktz --blockHash 0x80d7343e04f8160892e94f02d6a9b9f255663ed0ac34caca98544c8143fee699 --blockNumber 21037669 --blockTimestamp 1730762099 --txHash 0xd81bc6105b60a234c7c50ac17816ebcd5561d366df8bf3be59ff387552761799 --chainId 1
```
  • Loading branch information
mergify[bot] authored Dec 4, 2024
2 parents cf1d435 + 918eabe commit d6a7ffb
Show file tree
Hide file tree
Showing 11 changed files with 280 additions and 34 deletions.
4 changes: 3 additions & 1 deletion packages/fast-usdc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"type": "module",
"files": [
"contract",
"src"
"src",
"tools"
],
"bin": {
"fast-usdc": "./src/cli/bin.js"
Expand Down Expand Up @@ -49,6 +50,7 @@
"@endo/far": "^1.1.9",
"@endo/init": "^1.1.7",
"@endo/marshal": "^1.6.2",
"@endo/nat": "^5.0.13",
"@endo/pass-style": "^1.4.7",
"@endo/patterns": "^1.4.7",
"@endo/promise-kit": "^1.1.8",
Expand Down
28 changes: 27 additions & 1 deletion packages/fast-usdc/src/cli/cli.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-env node */
/* global globalThis */
import { assertParsableNumber } from '@agoric/zoe/src/contractSupport/ratio.js';
import {
Command,
Expand Down Expand Up @@ -34,6 +36,11 @@ export const initProgram = (
writeFile = writeAsync,
mkdir = mkdirSync,
exists = existsSync,
fetch = globalThis.fetch,
stdout = process.stdout,
stderr = process.stderr,
env = process.env,
now = () => Date.now(),
) => {
const program = new Command();

Expand All @@ -55,8 +62,27 @@ export const initProgram = (
return makeFile(getConfigPath(), readFile, writeFile, mkdir, exists);
};

program.addHelpText(
'afterAll',
`
Agoric test networks provide configuration info at, for example,
https://devnet.agoric.net/network-config
To use RPC endpoints from such a configuration, use:
export AGORIC_NET=devnet
Use AGORIC_NET=local or leave it unset to use localhost and chain id agoriclocal.
`,
);
addConfigCommands(program, configHelpers, makeConfigFile);
addOperatorCommands(program);
addOperatorCommands(program, {
fetch,
stdout,
stderr,
env,
now,
});

/** @param {string} value */
const parseDecimal = value => {
Expand Down
1 change: 0 additions & 1 deletion packages/fast-usdc/src/cli/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { stdin as input, stdout as output } from 'node:process';
nobleSeed: string,
ethSeed: string,
nobleToAgoricChannel: string,
agoricRpc: string,
nobleApi: string,
nobleRpc: string,
ethRpc: string,
Expand Down
95 changes: 75 additions & 20 deletions packages/fast-usdc/src/cli/operator-commands.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,44 @@
/* eslint-env node */
/**
* @import {Command} from 'commander';
* @import {OfferSpec} from '@agoric/smart-wallet/src/offers.js';
* @import {ExecuteOfferAction} from '@agoric/smart-wallet/src/smartWallet.js';
* @import {OperatorKit} from '../exos/operator-kit.js';
*/

import { fetchEnvNetworkConfig, makeVstorageKit } from '@agoric/client-utils';
import { mustMatch } from '@agoric/internal';
import { Nat } from '@endo/nat';
import { InvalidArgumentError } from 'commander';
import { INVITATION_MAKERS_DESC } from '../exos/transaction-feed.js';
import { CctpTxEvidenceShape } from '../type-guards.js';
import { outputActionAndHint } from './bridge-action.js';

/** @param {string} arg */
const parseNat = arg => {
const n = Nat(BigInt(arg));
return n;
};

/** @param {string} arg */
const parseHex = arg => {
if (!arg.startsWith('0x')) throw new InvalidArgumentError('not a hex string');
return arg;
};

/**
* @param {Command} program
* @param {{
* fetch: Window['fetch'];
* stdout: typeof process.stdout;
* stderr: typeof process.stderr;
* env: typeof process.env;
* now: typeof Date.now;
* }} io
*/
export const addOperatorCommands = program => {
export const addOperatorCommands = (
program,
{ fetch, stderr, stdout, env, now },
) => {
const operator = program
.command('operator')
.description('Oracle operator commands');
Expand All @@ -24,17 +50,9 @@ export const addOperatorCommands = program => {
'after',
'\nPipe the STDOUT to a file such as accept.json, then use the Agoric CLI to broadcast it:\n agoric wallet send --offer accept.json --from gov1 --keyring-backend="test"',
)
.option(
'--offerId <string>',
'Offer id',
String,
`operatorAccept-${Date.now()}`,
)
.option('--offerId <string>', 'Offer id', String, `operatorAccept-${now()}`)
.action(async opts => {
const networkConfig = await fetchEnvNetworkConfig({
env: process.env,
fetch,
});
const networkConfig = await fetchEnvNetworkConfig({ env, fetch });
const vsk = await makeVstorageKit({ fetch }, networkConfig);
const instance = vsk.agoricNames.instance.fastUsdc;
assert(instance, 'fastUsdc instance not in agoricNames');
Expand All @@ -56,21 +74,58 @@ export const addOperatorCommands = program => {
offer,
};

outputActionAndHint(bridgeAction, {
stderr: process.stderr,
stdout: process.stdout,
});
outputActionAndHint(bridgeAction, { stderr, stdout });
});

operator
.command('attest')
.description('Attest to an observed Fast USDC transfer')
.requiredOption('--previousOfferId <string>', 'Offer id', String)
.action(async options => {
const { previousOfferId } = options;
console.error(
'TODO: Implement attest logic for request:',
.requiredOption('--forwardingChannel <string>', 'Channel id', String)
.requiredOption('--recipientAddress <string>', 'bech32 address', String)
.requiredOption('--blockHash <0xhex>', 'hex hash', parseHex)
.requiredOption('--blockNumber <number>', 'number', parseNat)
.requiredOption('--blockTimestamp <number>', 'number', parseNat)
.requiredOption('--chainId <string>', 'chain id', Number)
.requiredOption('--amount <number>', 'number', parseNat)
.requiredOption('--forwardingAddress <string>', 'bech32 address', String)
.requiredOption('--txHash <0xhexo>', 'hex hash', parseHex)
.option('--offerId <string>', 'Offer id', String, `operatorAttest-${now()}`)
.action(async opts => {
const {
offerId,
previousOfferId,
forwardingChannel,
recipientAddress,
amount,
forwardingAddress,
...flat
} = opts;

const evidence = harden({
aux: { forwardingChannel, recipientAddress },
tx: { amount, forwardingAddress },
...flat,
});
mustMatch(evidence, CctpTxEvidenceShape);

/** @type {OfferSpec} */
const offer = {
id: offerId,
invitationSpec: {
source: 'continuing',
previousOffer: previousOfferId,
/** @type {string & keyof OperatorKit['invitationMakers'] } */
invitationMakerName: 'SubmitEvidence',
/** @type {Parameters<OperatorKit['invitationMakers']['SubmitEvidence']> } */
invitationArgs: [evidence],
},
proposal: {},
};

outputActionAndHint(
{ method: 'executeOffer', offer },
{ stderr, stdout },
);
});

Expand Down
13 changes: 10 additions & 3 deletions packages/fast-usdc/src/cli/transfer.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
/* eslint-env node */
/* global globalThis */

import { makeVStorage } from '@agoric/client-utils';
import {
fetchEnvNetworkConfig,
makeVStorage,
pickEndpoint,
} from '@agoric/client-utils';
import { queryFastUSDCLocalChainAccount } from '../util/agoric.js';
import { depositForBurn, makeProvider } from '../util/cctp.js';
import {
makeSigner,
queryForwardingAccount,
registerFwdAccount,
} from '../util/noble.js';
import { queryFastUSDCLocalChainAccount } from '../util/agoric.js';

/** @import { File } from '../util/file' */
/** @import { VStorage } from '@agoric/client-utils' */
Expand All @@ -23,13 +28,15 @@ const transfer = async (
/** @type {VStorage | undefined} */ vstorage,
/** @type {{signer: SigningStargateClient, address: string} | undefined} */ nobleSigner,
/** @type {ethProvider | undefined} */ ethProvider,
env = process.env,
) => {
const execute = async (
/** @type {import('./config').ConfigOpts} */ config,
) => {
const netConfig = await fetchEnvNetworkConfig({ env, fetch });
vstorage ||= makeVStorage(
{ fetch },
{ chainName: 'agoric', rpcAddrs: [config.agoricRpc] },
{ chainName: 'agoric', rpcAddrs: [pickEndpoint(netConfig)] },
);
const agoricAddr = await queryFastUSDCLocalChainAccount(vstorage, out);
const appendedAddr = `${agoricAddr}?EUD=${destination}`;
Expand Down
53 changes: 53 additions & 0 deletions packages/fast-usdc/test/cli/operator-commands.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { makeMarshal } from '@endo/marshal';
import test from 'ava';
import { Command } from 'commander';
import { addOperatorCommands } from '../../src/cli/operator-commands.js';
import { flags } from '../../tools/cli-tools.js';
import { mockStream } from '../../tools/mock-io.js';
import { MockCctpTxEvidences } from '../fixtures.js';

const marshalData = makeMarshal(_v => assert.fail('data only'));

test('fast-usdc operator attest sub-command', async t => {
const evidence = harden(MockCctpTxEvidences.AGORIC_PLUS_DYDX());
const { aux, tx, ...flat } = evidence;
const argv = [
...`node fast-usdc operator attest`.split(' '),
...flags({ previousOfferId: 123, ...aux, ...tx, ...flat }),
];
t.log(...argv);
const program = new Command();
program.exitOverride();
const out = [] as string[];
const err = [] as string[];

addOperatorCommands(program, {
fetch: null as unknown as Window['fetch'],
stdout: mockStream<typeof process.stdout>(out),
stderr: mockStream<typeof process.stderr>(err),
env: {},
now: () => 1234,
});

await program.parseAsync(argv);

const action = marshalData.fromCapData(JSON.parse(out.join('')));
t.deepEqual(action, {
method: 'executeOffer',
offer: {
id: 'operatorAttest-1234',
invitationSpec: {
invitationArgs: [evidence],
invitationMakerName: 'SubmitEvidence',
previousOffer: '123',
source: 'continuing',
},
proposal: {},
},
});

t.is(
err.join(''),
'Now use `agoric wallet send ...` to sign and broadcast the offer.\n',
);
});
Loading

0 comments on commit d6a7ffb

Please sign in to comment.