@agoric/fast-usdc 0.1.1-other-dev-3eb1a1d.0 → 0.1.1-other-dev-fbe72e7.0.fbe72e7
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.
- package/README.md +162 -36
- package/package.json +39 -38
- package/src/cli/bridge-action.js +40 -0
- package/src/cli/cli.js +47 -154
- package/src/cli/config-commands.js +108 -0
- package/src/cli/config.js +15 -9
- package/src/cli/lp-commands.js +160 -0
- package/src/cli/operator-commands.js +143 -0
- package/src/cli/transfer.js +81 -21
- package/src/cli/util/agoric.js +11 -0
- package/src/cli/util/bank.js +12 -0
- package/src/{util → cli/util}/cctp.js +1 -1
- package/src/{util → cli/util}/file.js +1 -1
- package/src/clientSupport.js +98 -0
- package/src/constants.js +29 -6
- package/src/main.js +1 -0
- package/src/operator-kit-interface.js +29 -0
- package/src/pool-share-math.js +72 -34
- package/src/type-guards.js +122 -34
- package/src/types.ts +136 -15
- package/src/utils/fees.js +105 -20
- package/tools/cli-tools.ts +9 -0
- package/tools/mock-evidence.ts +205 -0
- package/tools/mock-io.ts +14 -0
- package/src/exos/README.md +0 -26
- package/src/exos/advancer.js +0 -255
- package/src/exos/liquidity-pool.js +0 -365
- package/src/exos/operator-kit.js +0 -120
- package/src/exos/settler.js +0 -97
- package/src/exos/status-manager.js +0 -176
- package/src/exos/transaction-feed.js +0 -180
- package/src/fast-usdc.contract.js +0 -235
- package/src/fast-usdc.flows.js +0 -13
- package/src/fast-usdc.start.js +0 -284
- package/src/util/agoric.js +0 -12
- package/src/utils/address.js +0 -71
- package/src/utils/config-marshal.js +0 -130
- package/src/utils/zoe.js +0 -28
- /package/src/{util → cli/util}/noble.js +0 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import {Command} from 'commander';
|
|
3
|
+
* @import {File} from './util/file.js';
|
|
4
|
+
* @import * as ConfigHelpers from './config.js';
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
*
|
|
8
|
+
* @param {Command} program
|
|
9
|
+
* @param {ConfigHelpers} configHelpers
|
|
10
|
+
* @param {() => File} makeConfigFile
|
|
11
|
+
*/
|
|
12
|
+
export const addConfigCommands = (program, configHelpers, makeConfigFile) => {
|
|
13
|
+
const config = program.command('config').description('Manage config');
|
|
14
|
+
|
|
15
|
+
config
|
|
16
|
+
.command('show')
|
|
17
|
+
.description('Show current config')
|
|
18
|
+
.action(async () => {
|
|
19
|
+
await configHelpers.show(makeConfigFile());
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
config
|
|
23
|
+
.command('init')
|
|
24
|
+
.description('Set initial config values')
|
|
25
|
+
.requiredOption(
|
|
26
|
+
'--noble-seed <seed>',
|
|
27
|
+
'Seed phrase for Noble account. CAUTION: Stored unencrypted in file system',
|
|
28
|
+
)
|
|
29
|
+
.requiredOption(
|
|
30
|
+
'--eth-seed <seed>',
|
|
31
|
+
'Seed phrase for Ethereum account. CAUTION: Stored unencrypted in file system',
|
|
32
|
+
)
|
|
33
|
+
.requiredOption(
|
|
34
|
+
'--agoric-seed <seed>',
|
|
35
|
+
'Seed phrase for Agoric LP account. CAUTION: Stored unencrypted in file system',
|
|
36
|
+
)
|
|
37
|
+
.option(
|
|
38
|
+
'--agoric-rpc [url]',
|
|
39
|
+
'Agoric RPC endpoint',
|
|
40
|
+
'http://127.0.0.1:26656',
|
|
41
|
+
)
|
|
42
|
+
.option(
|
|
43
|
+
'--agoric-api [url]',
|
|
44
|
+
'Agoric RPC endpoint',
|
|
45
|
+
'http://127.0.0.1:1317',
|
|
46
|
+
)
|
|
47
|
+
.option('--noble-rpc [url]', 'Noble RPC endpoint', 'http://127.0.0.1:26657')
|
|
48
|
+
.option('--noble-api [url]', 'Noble API endpoint', 'http://127.0.0.1:1318')
|
|
49
|
+
.option('--eth-rpc [url]', 'Ethereum RPC Endpoint', 'http://127.0.0.1:8545')
|
|
50
|
+
.option(
|
|
51
|
+
'--noble-to-agoric-channel [channel]',
|
|
52
|
+
'Channel ID on Noble for Agoric',
|
|
53
|
+
'channel-21',
|
|
54
|
+
)
|
|
55
|
+
.option(
|
|
56
|
+
'--token-messenger-address [address]',
|
|
57
|
+
'Address of TokenMessenger contract',
|
|
58
|
+
// Default to ETH mainnet contract address. For ETH sepolia, use 0x9f3B8679c73C2Fef8b59B4f3444d4e156fb70AA5
|
|
59
|
+
'0xbd3fa81b58ba92a82136038b25adec7066af3155',
|
|
60
|
+
)
|
|
61
|
+
.option(
|
|
62
|
+
'--token-contract-address [address]',
|
|
63
|
+
'Address of USDC token contract',
|
|
64
|
+
// Detault to ETH mainnet token address. For ETH sepolia, use 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238
|
|
65
|
+
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
|
66
|
+
)
|
|
67
|
+
.action(async options => {
|
|
68
|
+
await configHelpers.init(makeConfigFile(), options);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
config
|
|
72
|
+
.command('update')
|
|
73
|
+
.description('Update config values')
|
|
74
|
+
.option(
|
|
75
|
+
'--noble-seed [string]',
|
|
76
|
+
'Seed phrase for Noble account. CAUTION: Stored unencrypted in file system',
|
|
77
|
+
)
|
|
78
|
+
.option(
|
|
79
|
+
'--eth-seed [string]',
|
|
80
|
+
'Seed phrase for Ethereum account. CAUTION: Stored unencrypted in file system',
|
|
81
|
+
)
|
|
82
|
+
.option(
|
|
83
|
+
'--agoric-seed <seed>',
|
|
84
|
+
'Seed phrase for Agoric LP account. CAUTION: Stored unencrypted in file system',
|
|
85
|
+
)
|
|
86
|
+
.option('--agoric-rpc [url]', 'Agoric RPC endpoint')
|
|
87
|
+
.option('--agoric-api [url]', 'Agoric API endpoint')
|
|
88
|
+
.option('--noble-rpc [url]', 'Noble RPC endpoint')
|
|
89
|
+
.option('--noble-api [url]', 'Noble API endpoint')
|
|
90
|
+
.option('--eth-rpc [url]', 'Ethereum RPC Endpoint')
|
|
91
|
+
.option(
|
|
92
|
+
'--noble-to-agoric-channel [channel]',
|
|
93
|
+
'Channel ID on Noble for Agoric',
|
|
94
|
+
)
|
|
95
|
+
.option(
|
|
96
|
+
'--token-messenger-address [address]',
|
|
97
|
+
'Address of TokenMessenger contract',
|
|
98
|
+
)
|
|
99
|
+
.option(
|
|
100
|
+
'--token-contract-address [address]',
|
|
101
|
+
'Address of USDC token contract',
|
|
102
|
+
)
|
|
103
|
+
.action(async options => {
|
|
104
|
+
await configHelpers.update(makeConfigFile(), options);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return config;
|
|
108
|
+
};
|
package/src/cli/config.js
CHANGED
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
import * as readline from 'node:readline/promises';
|
|
2
2
|
import { stdin as input, stdout as output } from 'node:process';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
@typedef {{
|
|
6
|
+
bech32Prefix: string,
|
|
7
|
+
api: string,
|
|
8
|
+
USDCDenom: string
|
|
9
|
+
}} DestinationChain
|
|
10
|
+
*/
|
|
11
|
+
|
|
4
12
|
/**
|
|
5
13
|
@typedef {{
|
|
6
14
|
nobleSeed: string,
|
|
7
15
|
ethSeed: string,
|
|
8
16
|
nobleToAgoricChannel: string,
|
|
9
|
-
agoricRpc: string,
|
|
10
17
|
nobleApi: string,
|
|
11
18
|
nobleRpc: string,
|
|
12
19
|
ethRpc: string,
|
|
13
20
|
tokenMessengerAddress: string,
|
|
14
21
|
tokenAddress: string
|
|
22
|
+
destinationChains?: DestinationChain[]
|
|
15
23
|
}} ConfigOpts
|
|
16
24
|
*/
|
|
17
25
|
|
|
18
|
-
/** @import {
|
|
26
|
+
/** @import { File } from './util/file.js' */
|
|
19
27
|
|
|
20
|
-
const init = async (
|
|
21
|
-
/** @type {
|
|
28
|
+
export const init = async (
|
|
29
|
+
/** @type {File} */ configFile,
|
|
22
30
|
/** @type {ConfigOpts} */ options,
|
|
23
31
|
out = console,
|
|
24
32
|
rl = readline.createInterface({ input, output }),
|
|
@@ -52,8 +60,8 @@ const init = async (
|
|
|
52
60
|
await writeConfig();
|
|
53
61
|
};
|
|
54
62
|
|
|
55
|
-
const update = async (
|
|
56
|
-
/** @type {
|
|
63
|
+
export const update = async (
|
|
64
|
+
/** @type {File} */ configFile,
|
|
57
65
|
/** @type {Partial<ConfigOpts>} */ options,
|
|
58
66
|
out = console,
|
|
59
67
|
) => {
|
|
@@ -83,7 +91,7 @@ const update = async (
|
|
|
83
91
|
await updateConfig({ ...JSON.parse(file), ...options });
|
|
84
92
|
};
|
|
85
93
|
|
|
86
|
-
const show = async (/** @type {
|
|
94
|
+
export const show = async (/** @type {File} */ configFile, out = console) => {
|
|
87
95
|
let contents;
|
|
88
96
|
await null;
|
|
89
97
|
try {
|
|
@@ -97,5 +105,3 @@ const show = async (/** @type {file} */ configFile, out = console) => {
|
|
|
97
105
|
out.log(`Config found at ${configFile.path}:`);
|
|
98
106
|
out.log(contents);
|
|
99
107
|
};
|
|
100
|
-
|
|
101
|
-
export default { init, update, show };
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/* eslint-env node */
|
|
2
|
+
/**
|
|
3
|
+
* @import {Command} from 'commander';
|
|
4
|
+
* @import {Amount, Brand} from '@agoric/ertp';
|
|
5
|
+
* @import {OfferSpec} from '@agoric/smart-wallet/src/offers.js';
|
|
6
|
+
* @import {ExecuteOfferAction} from '@agoric/smart-wallet/src/smartWallet.js';
|
|
7
|
+
* @import {USDCProposalShapes} from '../pool-share-math.js';
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
fetchEnvNetworkConfig,
|
|
12
|
+
makeSmartWalletKit,
|
|
13
|
+
} from '@agoric/client-utils';
|
|
14
|
+
import { AmountMath } from '@agoric/ertp';
|
|
15
|
+
import {
|
|
16
|
+
assertParsableNumber,
|
|
17
|
+
ceilDivideBy,
|
|
18
|
+
floorDivideBy,
|
|
19
|
+
multiplyBy,
|
|
20
|
+
parseRatio,
|
|
21
|
+
} from '@agoric/ertp/src/ratio.js';
|
|
22
|
+
import { InvalidArgumentError } from 'commander';
|
|
23
|
+
import { outputActionAndHint } from './bridge-action.js';
|
|
24
|
+
import { Offers } from '../clientSupport.js';
|
|
25
|
+
|
|
26
|
+
export const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
27
|
+
|
|
28
|
+
/** @param {string} arg */
|
|
29
|
+
const parseDecimal = arg => {
|
|
30
|
+
try {
|
|
31
|
+
assertParsableNumber(arg);
|
|
32
|
+
const n = Number(arg);
|
|
33
|
+
return n;
|
|
34
|
+
} catch {
|
|
35
|
+
throw new InvalidArgumentError('Not a number');
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {string} amountString
|
|
41
|
+
* @param {Brand<'nat'>} usdc
|
|
42
|
+
*/
|
|
43
|
+
const parseUSDCAmount = (amountString, usdc) => {
|
|
44
|
+
const USDC_DECIMALS = 6;
|
|
45
|
+
const unit = AmountMath.make(usdc, 10n ** BigInt(USDC_DECIMALS));
|
|
46
|
+
return multiplyBy(unit, parseRatio(amountString, usdc));
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {Command} program
|
|
51
|
+
* @param {{
|
|
52
|
+
* fetch?: Window['fetch'];
|
|
53
|
+
* smartWalletKit?: import('@agoric/client-utils').SmartWalletKit;
|
|
54
|
+
* stdout: typeof process.stdout;
|
|
55
|
+
* stderr: typeof process.stderr;
|
|
56
|
+
* env: typeof process.env;
|
|
57
|
+
* now: typeof Date.now;
|
|
58
|
+
* }} io
|
|
59
|
+
*/
|
|
60
|
+
export const addLPCommands = (
|
|
61
|
+
program,
|
|
62
|
+
{ fetch, smartWalletKit, stderr, stdout, env, now },
|
|
63
|
+
) => {
|
|
64
|
+
const loadSwk = async () => {
|
|
65
|
+
if (smartWalletKit) {
|
|
66
|
+
return smartWalletKit;
|
|
67
|
+
}
|
|
68
|
+
assert(fetch);
|
|
69
|
+
const networkConfig = await fetchEnvNetworkConfig({ env, fetch });
|
|
70
|
+
return makeSmartWalletKit({ delay, fetch }, networkConfig);
|
|
71
|
+
};
|
|
72
|
+
/** @type {undefined | ReturnType<typeof loadSwk>} */
|
|
73
|
+
let swkP;
|
|
74
|
+
|
|
75
|
+
program
|
|
76
|
+
.command('deposit')
|
|
77
|
+
.description('Deposit USDC into pool')
|
|
78
|
+
.addHelpText(
|
|
79
|
+
'after',
|
|
80
|
+
'\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"',
|
|
81
|
+
)
|
|
82
|
+
.requiredOption('--amount <number>', 'USDC amount', parseDecimal)
|
|
83
|
+
.option('--offerId <string>', 'Offer id', String, `lpDeposit-${now()}`)
|
|
84
|
+
.action(async opts => {
|
|
85
|
+
swkP ||= loadSwk();
|
|
86
|
+
const swk = await swkP;
|
|
87
|
+
|
|
88
|
+
/** @type {Brand<'nat'>} */
|
|
89
|
+
// @ts-expect-error it doesnt recognize usdc as a Brand type
|
|
90
|
+
const usdc = swk.agoricNames.brand.USDC;
|
|
91
|
+
assert(usdc, 'USDC brand not in agoricNames');
|
|
92
|
+
|
|
93
|
+
/** @type {Brand<'nat'>} */
|
|
94
|
+
// @ts-expect-error it doesnt recognize FastLP as a Brand type
|
|
95
|
+
const poolShare = swk.agoricNames.brand.FastLP;
|
|
96
|
+
assert(poolShare, 'FastLP brand not in agoricNames');
|
|
97
|
+
|
|
98
|
+
const usdcAmount = parseUSDCAmount(opts.amount, usdc);
|
|
99
|
+
|
|
100
|
+
const metrics = await swk.readPublished('fastUsdc.poolMetrics');
|
|
101
|
+
const fastLPAmount = floorDivideBy(usdcAmount, metrics.shareWorth);
|
|
102
|
+
|
|
103
|
+
const offer = Offers.fastUsdc.Deposit(swk.agoricNames, {
|
|
104
|
+
offerId: opts.offerId,
|
|
105
|
+
fastLPAmount: fastLPAmount.value,
|
|
106
|
+
usdcAmount: usdcAmount.value,
|
|
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
|
+
const swk = await swkP;
|
|
130
|
+
|
|
131
|
+
/** @type {Brand<'nat'>} */
|
|
132
|
+
// @ts-expect-error it doesnt recognize FastLP as a Brand type
|
|
133
|
+
const poolShare = swk.agoricNames.brand.FastLP;
|
|
134
|
+
assert(poolShare, 'FastLP brand not in agoricNames');
|
|
135
|
+
|
|
136
|
+
/** @type {Brand<'nat'>} */
|
|
137
|
+
// @ts-expect-error it doesnt recognize usdc as a Brand type
|
|
138
|
+
const usdc = swk.agoricNames.brand.USDC;
|
|
139
|
+
assert(usdc, 'USDC brand not in agoricNames');
|
|
140
|
+
|
|
141
|
+
const usdcAmount = parseUSDCAmount(opts.amount, usdc);
|
|
142
|
+
|
|
143
|
+
const metrics = await swk.readPublished('fastUsdc.poolMetrics');
|
|
144
|
+
const fastLPAmount = ceilDivideBy(usdcAmount, metrics.shareWorth);
|
|
145
|
+
|
|
146
|
+
const offer = Offers.fastUsdc.Withdraw(swk.agoricNames, {
|
|
147
|
+
offerId: opts.offerId,
|
|
148
|
+
fastLPAmount: fastLPAmount.value,
|
|
149
|
+
usdcAmount: usdcAmount.value,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
outputActionAndHint(
|
|
153
|
+
{ method: 'executeOffer', offer },
|
|
154
|
+
{ stderr, stdout },
|
|
155
|
+
swk.marshaller,
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return program;
|
|
160
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
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
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
fetchEnvNetworkConfig,
|
|
10
|
+
makeSmartWalletKit,
|
|
11
|
+
} from '@agoric/client-utils';
|
|
12
|
+
import { mustMatch } from '@agoric/internal';
|
|
13
|
+
import { Nat } from '@endo/nat';
|
|
14
|
+
import { InvalidArgumentError } from 'commander';
|
|
15
|
+
import { INVITATION_MAKERS_DESC } from '../operator-kit-interface.js';
|
|
16
|
+
import { CctpTxEvidenceShape } from '../type-guards.js';
|
|
17
|
+
import { outputActionAndHint } from './bridge-action.js';
|
|
18
|
+
|
|
19
|
+
export const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
20
|
+
|
|
21
|
+
/** @param {string} arg */
|
|
22
|
+
const parseNat = arg => {
|
|
23
|
+
const n = Nat(BigInt(arg));
|
|
24
|
+
return n;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** @param {string} arg */
|
|
28
|
+
const parseHex = arg => {
|
|
29
|
+
if (!arg.startsWith('0x')) throw new InvalidArgumentError('not a hex string');
|
|
30
|
+
return arg;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {Command} program
|
|
35
|
+
* @param {{
|
|
36
|
+
* fetch: Window['fetch'];
|
|
37
|
+
* stdout: typeof process.stdout;
|
|
38
|
+
* stderr: typeof process.stderr;
|
|
39
|
+
* env: typeof process.env;
|
|
40
|
+
* now: typeof Date.now;
|
|
41
|
+
* }} io
|
|
42
|
+
*/
|
|
43
|
+
export const addOperatorCommands = (
|
|
44
|
+
program,
|
|
45
|
+
{ fetch, stderr, stdout, env, now },
|
|
46
|
+
) => {
|
|
47
|
+
const operator = program
|
|
48
|
+
.command('operator')
|
|
49
|
+
.description('Oracle operator commands');
|
|
50
|
+
|
|
51
|
+
operator
|
|
52
|
+
.command('accept')
|
|
53
|
+
.description('Accept invitation to be an operator')
|
|
54
|
+
.addHelpText(
|
|
55
|
+
'after',
|
|
56
|
+
'\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"',
|
|
57
|
+
)
|
|
58
|
+
.option('--offerId <string>', 'Offer id', String, `operatorAccept-${now()}`)
|
|
59
|
+
.action(async opts => {
|
|
60
|
+
const networkConfig = await fetchEnvNetworkConfig({ env, fetch });
|
|
61
|
+
|
|
62
|
+
const swk = await makeSmartWalletKit({ delay, fetch }, networkConfig);
|
|
63
|
+
const instance = swk.agoricNames.instance.fastUsdc;
|
|
64
|
+
assert(instance, 'fastUsdc instance not in agoricNames');
|
|
65
|
+
|
|
66
|
+
/** @type {OfferSpec} */
|
|
67
|
+
const offer = {
|
|
68
|
+
id: opts.offerId,
|
|
69
|
+
invitationSpec: {
|
|
70
|
+
source: 'purse',
|
|
71
|
+
instance,
|
|
72
|
+
description: INVITATION_MAKERS_DESC,
|
|
73
|
+
},
|
|
74
|
+
proposal: {},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** @type {ExecuteOfferAction} */
|
|
78
|
+
const bridgeAction = {
|
|
79
|
+
method: 'executeOffer',
|
|
80
|
+
offer,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
outputActionAndHint(bridgeAction, { stderr, stdout });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
operator
|
|
87
|
+
.command('attest')
|
|
88
|
+
.description('Attest to an observed Fast USDC transfer')
|
|
89
|
+
.addHelpText(
|
|
90
|
+
'after',
|
|
91
|
+
'\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"',
|
|
92
|
+
)
|
|
93
|
+
.requiredOption('--previousOfferId <string>', 'Offer id', String)
|
|
94
|
+
.requiredOption('--forwardingChannel <string>', 'Channel id', String)
|
|
95
|
+
.requiredOption('--recipientAddress <string>', 'bech32 address', String)
|
|
96
|
+
.requiredOption('--blockHash <0xhex>', 'hex hash', parseHex)
|
|
97
|
+
.requiredOption('--blockNumber <number>', 'number', parseNat)
|
|
98
|
+
.requiredOption('--blockTimestamp <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
|
+
invitationMakerName: 'SubmitEvidence',
|
|
131
|
+
invitationArgs: [evidence],
|
|
132
|
+
},
|
|
133
|
+
proposal: {},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
outputActionAndHint(
|
|
137
|
+
{ method: 'executeOffer', offer },
|
|
138
|
+
{ stderr, stdout },
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return operator;
|
|
143
|
+
};
|
package/src/cli/transfer.js
CHANGED
|
@@ -1,44 +1,66 @@
|
|
|
1
|
+
/* eslint-env node */
|
|
1
2
|
/* global globalThis */
|
|
2
3
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
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';
|
|
11
|
+
import { depositForBurn, makeProvider } from './util/cctp.js';
|
|
5
12
|
import {
|
|
6
13
|
makeSigner,
|
|
7
14
|
queryForwardingAccount,
|
|
8
15
|
registerFwdAccount,
|
|
9
|
-
} from '
|
|
10
|
-
import {
|
|
16
|
+
} from './util/noble.js';
|
|
17
|
+
import { queryUSDCBalance } from './util/bank.js';
|
|
11
18
|
|
|
12
|
-
/** @import {
|
|
19
|
+
/** @import { File } from './util/file.js' */
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
/**
|
|
25
|
+
* @param {File} configFile
|
|
26
|
+
* @param {string} amount
|
|
27
|
+
* @param {string} EUD
|
|
28
|
+
* @param {typeof globalThis.console} [out]
|
|
29
|
+
* @param {typeof globalThis.fetch} [fetch]
|
|
30
|
+
* @param {VStorage} [vstorage]
|
|
31
|
+
* @param {{signer: SigningStargateClient, address: string}} [nobleSigner]
|
|
32
|
+
* @param {ethProvider} [ethProvider]
|
|
33
|
+
* @param {typeof process.env} [env]
|
|
34
|
+
* @param {typeof globalThis.setTimeout} [setTimeout]
|
|
35
|
+
*/
|
|
36
|
+
export const transfer = async (
|
|
37
|
+
configFile,
|
|
38
|
+
amount,
|
|
39
|
+
EUD,
|
|
21
40
|
out = console,
|
|
22
41
|
fetch = globalThis.fetch,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
42
|
+
vstorage,
|
|
43
|
+
nobleSigner,
|
|
44
|
+
ethProvider,
|
|
45
|
+
env = process.env,
|
|
46
|
+
setTimeout = globalThis.setTimeout,
|
|
26
47
|
) => {
|
|
27
48
|
const execute = async (
|
|
28
|
-
/** @type {import('./config').ConfigOpts} */ config,
|
|
49
|
+
/** @type {import('./config.js').ConfigOpts} */ config,
|
|
29
50
|
) => {
|
|
51
|
+
const netConfig = await fetchEnvNetworkConfig({ env, fetch });
|
|
30
52
|
vstorage ||= makeVStorage(
|
|
31
53
|
{ fetch },
|
|
32
|
-
{ chainName: 'agoric', rpcAddrs: [
|
|
54
|
+
{ chainName: 'agoric', rpcAddrs: [pickEndpoint(netConfig)] },
|
|
33
55
|
);
|
|
34
56
|
const agoricAddr = await queryFastUSDCLocalChainAccount(vstorage, out);
|
|
35
|
-
const
|
|
36
|
-
out.log(`forwarding destination ${
|
|
57
|
+
const encodedAddr = encodeAddressHook(agoricAddr, { EUD });
|
|
58
|
+
out.log(`forwarding destination ${encodedAddr}`);
|
|
37
59
|
|
|
38
60
|
const { exists, address } = await queryForwardingAccount(
|
|
39
61
|
config.nobleApi,
|
|
40
62
|
config.nobleToAgoricChannel,
|
|
41
|
-
|
|
63
|
+
encodedAddr,
|
|
42
64
|
out,
|
|
43
65
|
fetch,
|
|
44
66
|
);
|
|
@@ -51,18 +73,30 @@ const transfer = async (
|
|
|
51
73
|
signer,
|
|
52
74
|
signerAddress,
|
|
53
75
|
config.nobleToAgoricChannel,
|
|
54
|
-
|
|
76
|
+
encodedAddr,
|
|
55
77
|
out,
|
|
56
78
|
);
|
|
57
79
|
out.log(res);
|
|
58
80
|
} catch (e) {
|
|
59
81
|
out.error(
|
|
60
|
-
`Error registering noble forwarding account for ${
|
|
82
|
+
`Error registering noble forwarding account for ${encodedAddr} on channel ${config.nobleToAgoricChannel}`,
|
|
61
83
|
);
|
|
62
84
|
throw e;
|
|
63
85
|
}
|
|
64
86
|
}
|
|
65
87
|
|
|
88
|
+
const destChain = config.destinationChains?.find(chain =>
|
|
89
|
+
EUD.startsWith(chain.bech32Prefix),
|
|
90
|
+
);
|
|
91
|
+
if (!destChain) {
|
|
92
|
+
out.error(
|
|
93
|
+
`No destination chain found in config with matching bech32 prefix for ${EUD}, cannot query destination address`,
|
|
94
|
+
);
|
|
95
|
+
throw new Error();
|
|
96
|
+
}
|
|
97
|
+
const { api, USDCDenom } = destChain;
|
|
98
|
+
const startingBalance = await queryUSDCBalance(EUD, api, USDCDenom, fetch);
|
|
99
|
+
|
|
66
100
|
ethProvider ||= makeProvider(config.ethRpc);
|
|
67
101
|
await depositForBurn(
|
|
68
102
|
ethProvider,
|
|
@@ -73,6 +107,34 @@ const transfer = async (
|
|
|
73
107
|
amount,
|
|
74
108
|
out,
|
|
75
109
|
);
|
|
110
|
+
|
|
111
|
+
const refreshDelayMS = 1200;
|
|
112
|
+
const completeP = /** @type {Promise<void>} */ (
|
|
113
|
+
new Promise((res, rej) => {
|
|
114
|
+
const refreshUSDCBalance = async () => {
|
|
115
|
+
out.log('polling usdc balance');
|
|
116
|
+
const currentBalance = await queryUSDCBalance(
|
|
117
|
+
EUD,
|
|
118
|
+
api,
|
|
119
|
+
USDCDenom,
|
|
120
|
+
fetch,
|
|
121
|
+
);
|
|
122
|
+
if (currentBalance !== startingBalance) {
|
|
123
|
+
res();
|
|
124
|
+
} else {
|
|
125
|
+
setTimeout(() => refreshUSDCBalance().catch(rej), refreshDelayMS);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
refreshUSDCBalance().catch(rej);
|
|
129
|
+
})
|
|
130
|
+
).catch(e => {
|
|
131
|
+
out.error(
|
|
132
|
+
'Error checking destination address balance, could not detect completion of transfer.',
|
|
133
|
+
);
|
|
134
|
+
out.error(e.message);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await completeP;
|
|
76
138
|
};
|
|
77
139
|
|
|
78
140
|
let config;
|
|
@@ -87,5 +149,3 @@ const transfer = async (
|
|
|
87
149
|
}
|
|
88
150
|
await execute(config);
|
|
89
151
|
};
|
|
90
|
-
|
|
91
|
-
export default { transfer };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** @import { VStorage } from '@agoric/client-utils' */
|
|
2
|
+
|
|
3
|
+
export const queryFastUSDCLocalChainAccount = async (
|
|
4
|
+
/** @type {VStorage} */ vstorage,
|
|
5
|
+
out = console,
|
|
6
|
+
) => {
|
|
7
|
+
const { value } = await vstorage.readLatest('published.fastUsdc');
|
|
8
|
+
const { settlementAccount } = JSON.parse(JSON.parse(value).values[0]);
|
|
9
|
+
out.log(`settlementAccount: ${settlementAccount}`);
|
|
10
|
+
return settlementAccount;
|
|
11
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const queryUSDCBalance = async (
|
|
2
|
+
/** @type {string} */ address,
|
|
3
|
+
/** @type {string} */ api,
|
|
4
|
+
/** @type {string} */ denom,
|
|
5
|
+
/** @type {typeof globalThis.fetch} */ fetch,
|
|
6
|
+
) => {
|
|
7
|
+
const query = `${api}/cosmos/bank/v1beta1/balances/${address}`;
|
|
8
|
+
const json = await fetch(query).then(res => res.json());
|
|
9
|
+
const amount = json.balances?.find(b => b.denom === denom)?.amount ?? '0';
|
|
10
|
+
|
|
11
|
+
return BigInt(amount);
|
|
12
|
+
};
|
|
@@ -67,5 +67,5 @@ export const depositForBurn = async (
|
|
|
67
67
|
|
|
68
68
|
out.log('Transaction confirmed in block', receipt.blockNumber);
|
|
69
69
|
out.log('Transaction hash:', receipt.hash);
|
|
70
|
-
out.log('USDC transfer initiated successfully
|
|
70
|
+
out.log('USDC transfer initiated successfully');
|
|
71
71
|
};
|