Skip to content

Commit bcecfdb

Browse files
turadgmergify[bot]
authored andcommitted
refactor: forwardFunds flow
1 parent 62a1c7b commit bcecfdb

File tree

7 files changed

+257
-94
lines changed

7 files changed

+257
-94
lines changed

packages/fast-usdc-contract/src/exos/settler.ts

Lines changed: 14 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -49,20 +49,14 @@ import type { WithdrawToSeat } from '@agoric/orchestration/src/utils/zoe-tools.j
4949
import { mustMatch, type MapStore } from '@agoric/store';
5050
import type { IBCChannelID, IBCPacket, VTransferIBCEvent } from '@agoric/vats';
5151
import type { TargetRegistration } from '@agoric/vats/src/bridge-target.js';
52-
import type { VowTools } from '@agoric/vow';
52+
import type { Vow, VowTools } from '@agoric/vow';
5353
import type { ZCF } from '@agoric/zoe/src/zoeService/zoe.js';
5454
import type { Zone } from '@agoric/zone';
5555
import { makeSupportsCctp } from '../utils/cctp.ts';
5656
import { asMultiset } from '../utils/store.ts';
5757
import type { LiquidityPoolKit } from './liquidity-pool.js';
5858
import type { StatusManager } from './status-manager.js';
5959

60-
const FORWARD_TIMEOUT = {
61-
sec: 10n * 60n,
62-
p: '10m',
63-
} as const;
64-
harden(FORWARD_TIMEOUT);
65-
6660
const decodeEventPacket = (
6761
{ data }: IBCPacket,
6862
remoteDenom: string,
@@ -148,9 +142,9 @@ export const stateShape = harden({
148142
export const prepareSettler = (
149143
zone: Zone,
150144
{
151-
currentChainReference,
152145
chainHub,
153146
feeConfig,
147+
forwardFunds,
154148
getNobleICA,
155149
log = makeTracer('Settler', true),
156150
statusManager,
@@ -159,10 +153,14 @@ export const prepareSettler = (
159153
withdrawToSeat,
160154
zcf,
161155
}: {
162-
/** e.g., `agoric-3` */
163-
currentChainReference: string;
164156
chainHub: Pick<ChainHub, 'resolveAccountId'>;
165157
feeConfig: FeeConfig;
158+
forwardFunds: (tx: {
159+
txHash: EvmHash;
160+
amount: NatAmount;
161+
destination: AccountId;
162+
fundsInNobleIca?: boolean;
163+
}) => Vow<void>;
166164
getNobleICA: () => OrchestrationAccount<{ chainId: 'noble-1' }>;
167165
log?: LogFn;
168166
statusManager: StatusManager;
@@ -176,8 +174,6 @@ export const prepareSettler = (
176174

177175
const UsdcAmountShape = makeNatAmountShape(USDC);
178176

179-
const supportsCctp = makeSupportsCctp(chainHub);
180-
181177
return zone.exoClassKit(
182178
'Fast USDC Settler',
183179
{
@@ -203,6 +199,10 @@ export const prepareSettler = (
203199
),
204200
forward: M.call(EvmHashShape, UsdcAmountShape, M.string()).returns(),
205201
}),
202+
// XXX the following handlers are from before refactoring to an async flow for `forwardFunds`.
203+
// They must remain implemented for as long as any vow might settle and need their behavior.
204+
// Once all possible such vows are settled, the methods could be removed but the handler
205+
// facets must remain to satisfy the kind definition backward compatibility checker.
206206
transferHandler: M.interface('SettlerTransferI', {
207207
onFulfilled: M.call(M.undefined(), M.string()).returns(),
208208
onRejected: M.call(M.error(), M.string()).returns(),
@@ -427,7 +427,6 @@ export const prepareSettler = (
427427
fullValue: NatAmount,
428428
EUD: AccountId | Bech32Address,
429429
) {
430-
const { settlementAccount } = this.state;
431430
log('forwarding', fullValue.value, 'to', EUD, 'for', txHash);
432431

433432
const dest: AccountId | null = (() => {
@@ -446,44 +445,8 @@ export const prepareSettler = (
446445
})();
447446
if (!dest) return;
448447

449-
const { namespace, reference } = parseAccountId(dest);
450-
451-
const intermediateRecipient = getNobleICA().getAddress();
452-
453-
if (namespace === 'cosmos') {
454-
const transferOrSendV =
455-
reference === currentChainReference
456-
? E(settlementAccount).send(dest, fullValue)
457-
: E(settlementAccount).transfer(dest, fullValue, {
458-
timeoutRelativeSeconds: FORWARD_TIMEOUT.sec,
459-
forwardOpts: {
460-
intermediateRecipient,
461-
timeout: FORWARD_TIMEOUT.p,
462-
},
463-
});
464-
void vowTools.watch(
465-
transferOrSendV,
466-
this.facets.transferHandler,
467-
txHash,
468-
);
469-
} else if (supportsCctp(dest)) {
470-
// send to Noble then call `.depositForBurn(dest, fullValue)`
471-
void vowTools.watch(
472-
E(settlementAccount).transfer(intermediateRecipient, fullValue, {
473-
timeoutRelativeSeconds: FORWARD_TIMEOUT.sec,
474-
}),
475-
this.facets.intermediateTransferHandler,
476-
{ amt: fullValue, dest, txHash },
477-
);
478-
} else {
479-
log(
480-
'🚨 forward not attempted!',
481-
'unsupported destination',
482-
txHash,
483-
dest,
484-
);
485-
statusManager.forwardSkipped(txHash);
486-
}
448+
// This synchronous function returns a Vow that does its own error handling.
449+
void forwardFunds({ txHash, amount: fullValue, destination: dest });
487450
},
488451
},
489452
transferHandler: {

packages/fast-usdc-contract/src/fast-usdc.contract.ts

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
type OrchestrationPowers,
2121
type OrchestrationTools,
2222
} from '@agoric/orchestration';
23+
import type { HostForGuest } from '@agoric/orchestration/src/facade.js';
2324
import { makeZoeTools } from '@agoric/orchestration/src/utils/zoe-tools.js';
2425
import { provideSingleton } from '@agoric/zoe/src/contractSupport/durability.js';
2526
import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js';
@@ -48,6 +49,7 @@ import { prepareStatusManager } from './exos/status-manager.ts';
4849
import type { OperatorOfferResult } from './exos/transaction-feed.ts';
4950
import { prepareTransactionFeedKit } from './exos/transaction-feed.ts';
5051
import * as flows from './fast-usdc.flows.ts';
52+
import { makeSupportsCctp } from './utils/cctp.ts';
5153

5254
const trace = makeTracer('FastUsdc');
5355

@@ -133,17 +135,60 @@ export const contract = async (
133135
const getNobleICA = (): OrchestrationAccount<{ chainId: 'noble-1' }> =>
134136
baggage.get(NOBLE_ICA_BAGGAGE_KEY);
135137

138+
/** Chain, connection, and asset info can only be registered once */
139+
const firstIncarnationKey = 'firstIncarnationKey';
140+
// Perform this before makeLocalAccount() is called or it will cause
141+
// `agoric` chain info to be pulled from agoricNames.
142+
// UNTIL https://github.com/Agoric/agoric-sdk/issues/10602
143+
if (!baggage.has(firstIncarnationKey)) {
144+
baggage.init(firstIncarnationKey, true);
145+
registerChainsAndAssets(
146+
chainHub,
147+
terms.brands,
148+
privateArgs.chainInfo,
149+
privateArgs.assetInfo,
150+
);
151+
}
152+
153+
const supportsCctp = makeSupportsCctp(chainHub);
154+
155+
const { makeLocalAccount, makeNobleAccount } = orchestrateAll(
156+
{
157+
// TODO flow picker for orchestrateAll with different contexts or ordering
158+
makeLocalAccount: flows.makeLocalAccount,
159+
makeNobleAccount: flows.makeNobleAccount,
160+
},
161+
{},
162+
);
163+
164+
const poolAccountV = zone.makeOnce('PoolAccount', () => makeLocalAccount());
165+
const settleAccountV = zone.makeOnce('SettleAccount', () =>
166+
makeLocalAccount(),
167+
);
168+
const { forwardFunds } = orchestrateAll(
169+
// @ts-expect-error flow membrance type debt
170+
{ forwardFunds: flows.forwardFunds },
171+
{
172+
currentChainReference: privateArgs.chainInfo.agoric.chainId,
173+
getNobleICA,
174+
log: makeTracer('ForwardFunds'),
175+
settlementAccount: settleAccountV,
176+
supportsCctp,
177+
statusManager,
178+
},
179+
) as { forwardFunds: HostForGuest<typeof flows.forwardFunds> };
180+
136181
const makeSettler = prepareSettler(zone, {
137182
statusManager,
138183
USDC,
139184
withdrawToSeat,
140185
feeConfig,
186+
forwardFunds,
141187
getNobleICA,
142188
vowTools: tools.vowTools,
143189
zcf,
144190
// UNTIL we have an generic way to attenuate an Exo https://github.com/Agoric/agoric-sdk/issues/11309
145191
chainHub: { resolveAccountId: chainHub.resolveAccountId.bind(chainHub) },
146-
currentChainReference: privateArgs.chainInfo.agoric.chainId,
147192
});
148193

149194
const zoeTools = makeZoeTools(zcf, vowTools);
@@ -170,8 +215,6 @@ export const contract = async (
170215
{ makeRecorderKit },
171216
);
172217

173-
const { makeLocalAccount, makeNobleAccount } = orchestrateAll(flows, {});
174-
175218
const creatorFacet = zone.exo('Fast USDC Creator', undefined, {
176219
async makeOperatorInvitation(
177220
operatorId: string,
@@ -294,24 +337,8 @@ export const contract = async (
294337
makeLiquidityPoolKit(shareMint, privateArgs.poolMetricsNode),
295338
);
296339

297-
/** Chain, connection, and asset info can only be registered once */
298-
const firstIncarnationKey = 'firstIncarnationKey';
299-
if (!baggage.has(firstIncarnationKey)) {
300-
baggage.init(firstIncarnationKey, true);
301-
registerChainsAndAssets(
302-
chainHub,
303-
terms.brands,
304-
privateArgs.chainInfo,
305-
privateArgs.assetInfo,
306-
);
307-
}
308-
309340
const feedKit = zone.makeOnce('Feed Kit', () => makeFeedKit());
310341

311-
const poolAccountV = zone.makeOnce('PoolAccount', () => makeLocalAccount());
312-
const settleAccountV = zone.makeOnce('SettleAccount', () =>
313-
makeLocalAccount(),
314-
);
315342
// when() is OK here since this clearly resolves promptly.
316343
const [poolAccount, settlementAccount] = (await vowTools.when(
317344
vowTools.all([poolAccountV, settleAccountV]),

packages/fast-usdc-contract/src/fast-usdc.flows.ts

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,30 @@
1-
import type { OrchestrationFlow, Orchestrator } from '@agoric/orchestration';
1+
import type { NatAmount } from '@agoric/ertp';
2+
import type { EvmHash } from '@agoric/fast-usdc/src/types.ts';
3+
import type {
4+
AccountId,
5+
OrchestrationAccount,
6+
OrchestrationFlow,
7+
Orchestrator,
8+
} from '@agoric/orchestration';
9+
import { parseAccountId } from '@agoric/orchestration/src/utils/address.js';
10+
import { assertAllDefined } from '@agoric/internal';
11+
import type { StatusManager } from './exos/status-manager.ts';
12+
13+
const FORWARD_TIMEOUT = {
14+
sec: 10n * 60n,
15+
p: '10m',
16+
} as const;
17+
harden(FORWARD_TIMEOUT);
18+
19+
export interface Context {
20+
/** e.g., `agoric-3` */
21+
currentChainReference: string;
22+
supportsCctp: (destination: AccountId) => boolean;
23+
log: Console['log'];
24+
statusManager: StatusManager;
25+
getNobleICA: () => OrchestrationAccount<{ chainId: 'noble-1' }>;
26+
settlementAccount: Promise<OrchestrationAccount<{ chainId: 'agoric-any' }>>;
27+
}
228

329
export const makeLocalAccount = (async (orch: Orchestrator) => {
430
const agoricChain = await orch.getChain('agoric');
@@ -11,3 +37,98 @@ export const makeNobleAccount = (async (orch: Orchestrator) => {
1137
return nobleChain.makeAccount();
1238
}) satisfies OrchestrationFlow;
1339
harden(makeNobleAccount);
40+
41+
export const forwardFunds = async (
42+
orch: Orchestrator,
43+
{
44+
currentChainReference,
45+
supportsCctp,
46+
log,
47+
getNobleICA,
48+
settlementAccount,
49+
statusManager,
50+
}: Context,
51+
tx: {
52+
txHash: EvmHash;
53+
amount: NatAmount;
54+
destination: AccountId;
55+
},
56+
) => {
57+
await null;
58+
assertAllDefined({
59+
currentChainReference,
60+
supportsCctp,
61+
log,
62+
getNobleICA,
63+
settlementAccount,
64+
statusManager,
65+
tx,
66+
});
67+
const { amount, destination, txHash } = tx;
68+
log('trying forward for', amount, 'to', destination, 'for', txHash);
69+
70+
const { namespace, reference } = parseAccountId(destination);
71+
72+
const settlement = await settlementAccount;
73+
const intermediateRecipient = getNobleICA().getAddress();
74+
75+
if (namespace === 'cosmos') {
76+
const completion =
77+
reference === currentChainReference
78+
? settlement.send(destination, amount)
79+
: settlement.transfer(destination, amount, {
80+
timeoutRelativeSeconds: FORWARD_TIMEOUT.sec,
81+
forwardOpts: {
82+
intermediateRecipient,
83+
timeout: FORWARD_TIMEOUT.p,
84+
},
85+
});
86+
try {
87+
await completion;
88+
log('forward successful for', txHash);
89+
statusManager.forwarded(tx.txHash);
90+
} catch (reason) {
91+
// funds remain in `settlementAccount` and must be recovered via a
92+
// contract upgrade
93+
log('🚨 forward transfer rejected!', reason, txHash);
94+
// update status manager, flagging a terminal state that needs to be
95+
// manual intervention or a code update to remediate
96+
statusManager.forwardFailed(txHash);
97+
}
98+
} else if (supportsCctp(destination)) {
99+
try {
100+
await settlement.transfer(intermediateRecipient, amount, {
101+
timeoutRelativeSeconds: FORWARD_TIMEOUT.sec,
102+
});
103+
} catch (reason) {
104+
// funds remain in `settlementAccount` and must be recovered via a
105+
// contract upgrade
106+
log('🚨 forward intermediate transfer rejected!', reason, txHash);
107+
// update status manager, flagging a terminal state that needs manual
108+
// intervention or a code update to remediate
109+
statusManager.forwardFailed(txHash);
110+
}
111+
112+
try {
113+
await getNobleICA().depositForBurn(destination, amount);
114+
log('forward transfer and depositForBurn successful for', txHash);
115+
statusManager.forwarded(tx.txHash);
116+
} catch (reason) {
117+
// funds remain in `nobleAccount` and must be recovered via a
118+
// contract upgrade
119+
log('🚨 forward depositForBurn rejected!', reason, txHash);
120+
// update status manager, flagging a terminal state that needs manual
121+
// intervention or a code update to remediate
122+
statusManager.forwardFailed(txHash);
123+
}
124+
} else {
125+
log(
126+
'⚠️ forward not attempted',
127+
'unsupported destination',
128+
txHash,
129+
destination,
130+
);
131+
statusManager.forwardSkipped(txHash);
132+
}
133+
};
134+
harden(forwardFunds);

0 commit comments

Comments
 (0)