@agoric/fast-usdc 0.1.1-other-dev-3eb1a1d.0 → 0.2.0-u18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,178 @@
1
+ /* eslint-env node */
2
+ /**
3
+ * @import {Command} from 'commander';
4
+ * @import {OfferSpec} from '@agoric/smart-wallet/src/offers.js';
5
+ * @import {ExecuteOfferAction} from '@agoric/smart-wallet/src/smartWallet.js';
6
+ * @import {USDCProposalShapes} from '../pool-share-math.js';
7
+ */
8
+
9
+ import {
10
+ fetchEnvNetworkConfig,
11
+ makeSmartWalletKit,
12
+ } from '@agoric/client-utils';
13
+ import { AmountMath } from '@agoric/ertp';
14
+ import {
15
+ assertParsableNumber,
16
+ ceilDivideBy,
17
+ multiplyBy,
18
+ parseRatio,
19
+ } from '@agoric/zoe/src/contractSupport/ratio.js';
20
+ import { InvalidArgumentError } from 'commander';
21
+ import { outputActionAndHint } from './bridge-action.js';
22
+
23
+ export const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
24
+
25
+ /** @param {string} arg */
26
+ const parseDecimal = arg => {
27
+ try {
28
+ assertParsableNumber(arg);
29
+ const n = Number(arg);
30
+ return n;
31
+ } catch {
32
+ throw new InvalidArgumentError('Not a number');
33
+ }
34
+ };
35
+
36
+ /**
37
+ * @param {string} amountString
38
+ * @param {Brand} usdc
39
+ */
40
+ const parseUSDCAmount = (amountString, usdc) => {
41
+ const USDC_DECIMALS = 6;
42
+ const unit = AmountMath.make(usdc, 10n ** BigInt(USDC_DECIMALS));
43
+ return multiplyBy(unit, parseRatio(amountString, usdc));
44
+ };
45
+
46
+ /**
47
+ * @param {Command} program
48
+ * @param {{
49
+ * fetch?: Window['fetch'];
50
+ * smartWalletKit?: import('@agoric/client-utils').SmartWalletKit;
51
+ * stdout: typeof process.stdout;
52
+ * stderr: typeof process.stderr;
53
+ * env: typeof process.env;
54
+ * now: typeof Date.now;
55
+ * }} io
56
+ */
57
+ export const addLPCommands = (
58
+ program,
59
+ { fetch, smartWalletKit, stderr, stdout, env, now },
60
+ ) => {
61
+ const loadSwk = async () => {
62
+ if (smartWalletKit) {
63
+ return smartWalletKit;
64
+ }
65
+ assert(fetch);
66
+ const networkConfig = await fetchEnvNetworkConfig({ env, fetch });
67
+ return makeSmartWalletKit({ delay, fetch }, networkConfig);
68
+ };
69
+ /** @type {undefined | ReturnType<typeof loadSwk>} */
70
+ let swkP;
71
+
72
+ program
73
+ .command('deposit')
74
+ .description('Deposit USDC into pool')
75
+ .addHelpText(
76
+ 'after',
77
+ '\nPipe the STDOUT to a file such as deposit.json, then use the Agoric CLI to broadcast it:\n agoric wallet send --offer deposit.json --from gov1 --keyring-backend="test"',
78
+ )
79
+ .requiredOption('--amount <number>', 'USDC amount', parseDecimal)
80
+ .option('--offerId <string>', 'Offer id', String, `lpDeposit-${now()}`)
81
+ .action(async opts => {
82
+ swkP ||= loadSwk();
83
+ const swk = await swkP;
84
+ /** @type {Brand<'nat'>} */
85
+ // @ts-expect-error it doesnt recognize usdc as a Brand type
86
+ const usdc = swk.agoricNames.brand.USDC;
87
+ assert(usdc, 'USDC brand not in agoricNames');
88
+
89
+ const usdcAmount = parseUSDCAmount(opts.amount, usdc);
90
+
91
+ /** @type {USDCProposalShapes['deposit']} */
92
+ const proposal = {
93
+ give: {
94
+ USDC: usdcAmount,
95
+ },
96
+ };
97
+
98
+ /** @type {OfferSpec} */
99
+ const offer = {
100
+ id: opts.offerId,
101
+ invitationSpec: {
102
+ source: 'agoricContract',
103
+ instancePath: ['fastUsdc'],
104
+ callPipe: [['makeDepositInvitation', []]],
105
+ },
106
+ proposal,
107
+ };
108
+
109
+ /** @type {ExecuteOfferAction} */
110
+ const bridgeAction = {
111
+ method: 'executeOffer',
112
+ offer,
113
+ };
114
+
115
+ outputActionAndHint(bridgeAction, { stderr, stdout }, swk.marshaller);
116
+ });
117
+
118
+ program
119
+ .command('withdraw')
120
+ .description("Withdraw USDC from the LP's pool share")
121
+ .addHelpText(
122
+ 'after',
123
+ '\nPipe the STDOUT to a file such as withdraw.json, then use the Agoric CLI to broadcast it:\n agoric wallet send --offer withdraw.json --from gov1 --keyring-backend="test"',
124
+ )
125
+ .requiredOption('--amount <number>', 'USDC amount', parseDecimal)
126
+ .option('--offerId <string>', 'Offer id', String, `lpWithdraw-${now()}`)
127
+ .action(async opts => {
128
+ swkP ||= loadSwk();
129
+ swkP ||= loadSwk();
130
+ const swk = await swkP;
131
+
132
+ /** @type {Brand<'nat'>} */
133
+ // @ts-expect-error it doesnt recognize FastLP as a Brand type
134
+ const poolShare = swk.agoricNames.brand.FastLP;
135
+ assert(poolShare, 'FastLP brand not in agoricNames');
136
+
137
+ /** @type {Brand<'nat'>} */
138
+ // @ts-expect-error it doesnt recognize usdc as a Brand type
139
+ const usdc = swk.agoricNames.brand.USDC;
140
+ assert(usdc, 'USDC brand not in agoricNames');
141
+
142
+ const usdcAmount = parseUSDCAmount(opts.amount, usdc);
143
+
144
+ /** @type {import('../types.js').PoolMetrics} */
145
+ // @ts-expect-error it treats this as "unknown"
146
+ const metrics = await swk.readPublished('fastUsdc.poolMetrics');
147
+ const fastLPAmount = ceilDivideBy(usdcAmount, metrics.shareWorth);
148
+
149
+ /** @type {USDCProposalShapes['withdraw']} */
150
+ const proposal = {
151
+ give: {
152
+ PoolShare: fastLPAmount,
153
+ },
154
+ want: {
155
+ USDC: usdcAmount,
156
+ },
157
+ };
158
+
159
+ /** @type {OfferSpec} */
160
+ const offer = {
161
+ id: opts.offerId,
162
+ invitationSpec: {
163
+ source: 'agoricContract',
164
+ instancePath: ['fastUsdc'],
165
+ callPipe: [['makeWithdrawInvitation', []]],
166
+ },
167
+ proposal,
168
+ };
169
+
170
+ outputActionAndHint(
171
+ { method: 'executeOffer', offer },
172
+ { stderr, stdout },
173
+ swk.marshaller,
174
+ );
175
+ });
176
+
177
+ return program;
178
+ };
@@ -0,0 +1,145 @@
1
+ /* eslint-env node */
2
+ /**
3
+ * @import {Command} from 'commander';
4
+ * @import {OfferSpec} from '@agoric/smart-wallet/src/offers.js';
5
+ * @import {ExecuteOfferAction} from '@agoric/smart-wallet/src/smartWallet.js';
6
+ * @import {OperatorKit} from '../exos/operator-kit.js';
7
+ */
8
+
9
+ import {
10
+ fetchEnvNetworkConfig,
11
+ makeSmartWalletKit,
12
+ } from '@agoric/client-utils';
13
+ import { mustMatch } from '@agoric/internal';
14
+ import { Nat } from '@endo/nat';
15
+ import { InvalidArgumentError } from 'commander';
16
+ import { INVITATION_MAKERS_DESC } from '../exos/transaction-feed.js';
17
+ import { CctpTxEvidenceShape } from '../type-guards.js';
18
+ import { outputActionAndHint } from './bridge-action.js';
19
+
20
+ export const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
21
+
22
+ /** @param {string} arg */
23
+ const parseNat = arg => {
24
+ const n = Nat(BigInt(arg));
25
+ return n;
26
+ };
27
+
28
+ /** @param {string} arg */
29
+ const parseHex = arg => {
30
+ if (!arg.startsWith('0x')) throw new InvalidArgumentError('not a hex string');
31
+ return arg;
32
+ };
33
+
34
+ /**
35
+ * @param {Command} program
36
+ * @param {{
37
+ * fetch: Window['fetch'];
38
+ * stdout: typeof process.stdout;
39
+ * stderr: typeof process.stderr;
40
+ * env: typeof process.env;
41
+ * now: typeof Date.now;
42
+ * }} io
43
+ */
44
+ export const addOperatorCommands = (
45
+ program,
46
+ { fetch, stderr, stdout, env, now },
47
+ ) => {
48
+ const operator = program
49
+ .command('operator')
50
+ .description('Oracle operator commands');
51
+
52
+ operator
53
+ .command('accept')
54
+ .description('Accept invitation to be an operator')
55
+ .addHelpText(
56
+ 'after',
57
+ '\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"',
58
+ )
59
+ .option('--offerId <string>', 'Offer id', String, `operatorAccept-${now()}`)
60
+ .action(async opts => {
61
+ const networkConfig = await fetchEnvNetworkConfig({ env, fetch });
62
+
63
+ const swk = await makeSmartWalletKit({ delay, fetch }, networkConfig);
64
+ const instance = swk.agoricNames.instance.fastUsdc;
65
+ assert(instance, 'fastUsdc instance not in agoricNames');
66
+
67
+ /** @type {OfferSpec} */
68
+ const offer = {
69
+ id: opts.offerId,
70
+ invitationSpec: {
71
+ source: 'purse',
72
+ instance,
73
+ description: INVITATION_MAKERS_DESC,
74
+ },
75
+ proposal: {},
76
+ };
77
+
78
+ /** @type {ExecuteOfferAction} */
79
+ const bridgeAction = {
80
+ method: 'executeOffer',
81
+ offer,
82
+ };
83
+
84
+ outputActionAndHint(bridgeAction, { stderr, stdout });
85
+ });
86
+
87
+ operator
88
+ .command('attest')
89
+ .description('Attest to an observed Fast USDC transfer')
90
+ .addHelpText(
91
+ 'after',
92
+ '\nPipe the STDOUT to a file such as attest.json, then use the Agoric CLI to broadcast it:\n agoric wallet send --offer attest.json --from gov1 --keyring-backend="test"',
93
+ )
94
+ .requiredOption('--previousOfferId <string>', 'Offer id', String)
95
+ .requiredOption('--forwardingChannel <string>', 'Channel id', String)
96
+ .requiredOption('--recipientAddress <string>', 'bech32 address', String)
97
+ .requiredOption('--blockHash <0xhex>', 'hex hash', parseHex)
98
+ .requiredOption('--blockNumber <number>', 'number', parseNat)
99
+ .requiredOption('--chainId <string>', 'chain id', Number)
100
+ .requiredOption('--amount <number>', 'number', parseNat)
101
+ .requiredOption('--forwardingAddress <string>', 'bech32 address', String)
102
+ .requiredOption('--sender <string>', 'Ethereum address initiating', String)
103
+ .requiredOption('--txHash <0xhexo>', 'hex hash', parseHex)
104
+ .option('--offerId <string>', 'Offer id', String, `operatorAttest-${now()}`)
105
+ .action(async opts => {
106
+ const {
107
+ offerId,
108
+ previousOfferId,
109
+ forwardingChannel,
110
+ recipientAddress,
111
+ amount,
112
+ forwardingAddress,
113
+ sender,
114
+ ...flat
115
+ } = opts;
116
+
117
+ const evidence = harden({
118
+ aux: { forwardingChannel, recipientAddress },
119
+ tx: { amount, forwardingAddress, sender },
120
+ ...flat,
121
+ });
122
+ mustMatch(evidence, CctpTxEvidenceShape);
123
+
124
+ /** @type {OfferSpec} */
125
+ const offer = {
126
+ id: offerId,
127
+ invitationSpec: {
128
+ source: 'continuing',
129
+ previousOffer: previousOfferId,
130
+ /** @type {string & keyof OperatorKit['invitationMakers'] } */
131
+ invitationMakerName: 'SubmitEvidence',
132
+ /** @type {Parameters<OperatorKit['invitationMakers']['SubmitEvidence']> } */
133
+ invitationArgs: [evidence],
134
+ },
135
+ proposal: {},
136
+ };
137
+
138
+ outputActionAndHint(
139
+ { method: 'executeOffer', offer },
140
+ { stderr, stdout },
141
+ );
142
+ });
143
+
144
+ return operator;
145
+ };
@@ -1,44 +1,54 @@
1
+ /* eslint-env node */
1
2
  /* global globalThis */
2
3
 
3
- import { makeVStorage } from '@agoric/client-utils';
4
+ import {
5
+ fetchEnvNetworkConfig,
6
+ makeVStorage,
7
+ pickEndpoint,
8
+ } from '@agoric/client-utils';
9
+ import { encodeAddressHook } from '@agoric/cosmic-proto/address-hooks.js';
10
+ import { queryFastUSDCLocalChainAccount } from '../util/agoric.js';
4
11
  import { depositForBurn, makeProvider } from '../util/cctp.js';
5
12
  import {
6
13
  makeSigner,
7
14
  queryForwardingAccount,
8
15
  registerFwdAccount,
9
16
  } from '../util/noble.js';
10
- import { queryFastUSDCLocalChainAccount } from '../util/agoric.js';
17
+ import { queryUSDCBalance } from '../util/bank.js';
11
18
 
12
- /** @import { file } from '../util/file' */
19
+ /** @import { File } from '../util/file' */
13
20
  /** @import { VStorage } from '@agoric/client-utils' */
14
21
  /** @import { SigningStargateClient } from '@cosmjs/stargate' */
15
22
  /** @import { JsonRpcProvider as ethProvider } from 'ethers' */
16
23
 
17
24
  const transfer = async (
18
- /** @type {file} */ configFile,
25
+ /** @type {File} */ configFile,
19
26
  /** @type {string} */ amount,
20
- /** @type {string} */ destination,
27
+ /** @type {string} */ EUD,
21
28
  out = console,
22
29
  fetch = globalThis.fetch,
23
30
  /** @type {VStorage | undefined} */ vstorage,
24
31
  /** @type {{signer: SigningStargateClient, address: string} | undefined} */ nobleSigner,
25
32
  /** @type {ethProvider | undefined} */ ethProvider,
33
+ env = process.env,
34
+ setTimeout = globalThis.setTimeout,
26
35
  ) => {
27
36
  const execute = async (
28
37
  /** @type {import('./config').ConfigOpts} */ config,
29
38
  ) => {
39
+ const netConfig = await fetchEnvNetworkConfig({ env, fetch });
30
40
  vstorage ||= makeVStorage(
31
41
  { fetch },
32
- { chainName: 'agoric', rpcAddrs: [config.agoricRpc] },
42
+ { chainName: 'agoric', rpcAddrs: [pickEndpoint(netConfig)] },
33
43
  );
34
44
  const agoricAddr = await queryFastUSDCLocalChainAccount(vstorage, out);
35
- const appendedAddr = `${agoricAddr}?EUD=${destination}`;
36
- out.log(`forwarding destination ${appendedAddr}`);
45
+ const encodedAddr = encodeAddressHook(agoricAddr, { EUD });
46
+ out.log(`forwarding destination ${encodedAddr}`);
37
47
 
38
48
  const { exists, address } = await queryForwardingAccount(
39
49
  config.nobleApi,
40
50
  config.nobleToAgoricChannel,
41
- appendedAddr,
51
+ encodedAddr,
42
52
  out,
43
53
  fetch,
44
54
  );
@@ -51,18 +61,30 @@ const transfer = async (
51
61
  signer,
52
62
  signerAddress,
53
63
  config.nobleToAgoricChannel,
54
- appendedAddr,
64
+ encodedAddr,
55
65
  out,
56
66
  );
57
67
  out.log(res);
58
68
  } catch (e) {
59
69
  out.error(
60
- `Error registering noble forwarding account for ${appendedAddr} on channel ${config.nobleToAgoricChannel}`,
70
+ `Error registering noble forwarding account for ${encodedAddr} on channel ${config.nobleToAgoricChannel}`,
61
71
  );
62
72
  throw e;
63
73
  }
64
74
  }
65
75
 
76
+ const destChain = config.destinationChains?.find(chain =>
77
+ EUD.startsWith(chain.bech32Prefix),
78
+ );
79
+ if (!destChain) {
80
+ out.error(
81
+ `No destination chain found in config with matching bech32 prefix for ${EUD}, cannot query destination address`,
82
+ );
83
+ throw new Error();
84
+ }
85
+ const { api, USDCDenom } = destChain;
86
+ const startingBalance = await queryUSDCBalance(EUD, api, USDCDenom, fetch);
87
+
66
88
  ethProvider ||= makeProvider(config.ethRpc);
67
89
  await depositForBurn(
68
90
  ethProvider,
@@ -73,6 +95,34 @@ const transfer = async (
73
95
  amount,
74
96
  out,
75
97
  );
98
+
99
+ const refreshDelayMS = 1200;
100
+ const completeP = /** @type {Promise<void>} */ (
101
+ new Promise((res, rej) => {
102
+ const refreshUSDCBalance = async () => {
103
+ out.log('polling usdc balance');
104
+ const currentBalance = await queryUSDCBalance(
105
+ EUD,
106
+ api,
107
+ USDCDenom,
108
+ fetch,
109
+ );
110
+ if (currentBalance !== startingBalance) {
111
+ res();
112
+ } else {
113
+ setTimeout(() => refreshUSDCBalance().catch(rej), refreshDelayMS);
114
+ }
115
+ };
116
+ refreshUSDCBalance().catch(rej);
117
+ })
118
+ ).catch(e => {
119
+ out.error(
120
+ 'Error checking destination address balance, could not detect completion of transfer.',
121
+ );
122
+ out.error(e.message);
123
+ });
124
+
125
+ await completeP;
76
126
  };
77
127
 
78
128
  let config;
package/src/constants.js CHANGED
@@ -7,12 +7,29 @@ export const TxStatus = /** @type {const} */ ({
7
7
  /** tx was observed but not advanced */
8
8
  Observed: 'OBSERVED',
9
9
  /** IBC transfer is initiated */
10
+ Advancing: 'ADVANCING',
11
+ /** IBC transfer is complete */
10
12
  Advanced: 'ADVANCED',
11
- /** settlement for matching advance received and funds dispersed */
12
- Settled: 'SETTLED',
13
+ /** IBC transfer failed (timed out) */
14
+ AdvanceFailed: 'ADVANCE_FAILED',
15
+ /** Advance skipped and waiting for forward */
16
+ AdvanceSkipped: 'ADVANCE_SKIPPED',
17
+ /** settlement for matching advance received and funds disbursed */
18
+ Disbursed: 'DISBURSED',
19
+ /** fallback: do not collect fees */
20
+ Forwarded: 'FORWARDED',
21
+ /** failed to forward to EUD */
22
+ ForwardFailed: 'FORWARD_FAILED',
13
23
  });
14
24
  harden(TxStatus);
15
25
 
26
+ // According to the state diagram
27
+ export const TerminalTxStatus = {
28
+ [TxStatus.Forwarded]: true,
29
+ [TxStatus.ForwardFailed]: true,
30
+ [TxStatus.Disbursed]: true,
31
+ };
32
+
16
33
  /**
17
34
  * Status values for the StatusManager.
18
35
  *
@@ -22,6 +39,12 @@ export const PendingTxStatus = /** @type {const} */ ({
22
39
  /** tx was observed but not advanced */
23
40
  Observed: 'OBSERVED',
24
41
  /** IBC transfer is initiated */
42
+ Advancing: 'ADVANCING',
43
+ /** IBC transfer failed (timed out) */
44
+ AdvanceFailed: 'ADVANCE_FAILED',
45
+ /** IBC transfer is complete */
25
46
  Advanced: 'ADVANCED',
47
+ /** Advance skipped and waiting for forward */
48
+ AdvanceSkipped: 'ADVANCE_SKIPPED',
26
49
  });
27
50
  harden(PendingTxStatus);