@@ -87,6 +87,25 @@ export interface SolvedEdgeFlow {
87
87
/** Model shape for javascript-lp-solver */
88
88
export type LpModel = IModel < string , string > ;
89
89
90
+ /**
91
+ * Link to Factory and Wallet contracts:
92
+ * https://github.com/agoric-labs/agoric-to-axelar-local/blob/cd6087fa44de3b019b2cdac6962bb49b6a2bc1ca/packages/axelar-local-dev-cosmos/src/__tests__/contracts/Factory.sol
93
+ *
94
+ * Gas estimation interface -
95
+ * @see {@link ../../../services/ymax-planner/src/gas-estimation.ts }
96
+ * - getFactoryContractEstimate: Estimate gas fees for executing the factory
97
+ * contract to create a wallet on the specified chain
98
+ * - getReturnFeeEstimate: Estimate return fees for sending a transaction back
99
+ * from the factory contract to Agoric
100
+ * - getWalletEstimate: Estimate gas fees for remote wallet operations on the
101
+ * specified chain
102
+ */
103
+ type GasEstimator = {
104
+ getWalletEstimate : ( chainName : AxelarChain ) => Promise < bigint > ;
105
+ getFactoryContractEstimate : ( chainName : AxelarChain ) => Promise < bigint > ;
106
+ getReturnFeeEstimate : ( chainName : AxelarChain ) => Promise < bigint > ;
107
+ } ;
108
+
90
109
// --- keep existing type declarations above ---
91
110
92
111
/**
@@ -150,26 +169,35 @@ export const buildBaseGraph = (
150
169
const hub = `@${ chainName } ` as AssetPlaceRef ;
151
170
if ( node === hub ) continue ;
152
171
153
- const feeMode = Object . keys ( AxelarChain ) . includes ( chainName )
154
- ? { feeMode : 'gmpCall' as FeeMode }
155
- : { } ;
172
+ const chainIsEvm = Object . keys ( AxelarChain ) . includes ( chainName ) ;
156
173
const base : Omit < FlowEdge , 'src' | 'dest' | 'id' > = {
157
174
capacity : ( Number . MAX_SAFE_INTEGER + 1 ) / 4 ,
158
175
variableFee : vf ,
159
176
fixedFee : 0 ,
160
177
timeFixed : tf ,
161
178
via : 'local' ,
162
- ...feeMode ,
163
179
} ;
164
180
165
- // eslint-disable-next-line no-plusplus
166
- edges . push ( { id : `e${ eid ++ } ` , src : node , dest : hub , ...base } ) ;
181
+ edges . push ( {
182
+ // eslint-disable-next-line no-plusplus
183
+ id : `e${ eid ++ } ` ,
184
+ src : node ,
185
+ dest : hub ,
186
+ ...base ,
187
+ ...( chainIsEvm ? { feeMode : 'poolToEvm' } : { } ) ,
188
+ } ) ;
167
189
168
190
// Skip @agoric → +agoric edge
169
191
if ( node === '+agoric' ) continue ;
170
192
171
- // eslint-disable-next-line no-plusplus
172
- edges . push ( { id : `e${ eid ++ } ` , src : hub , dest : node , ...base } ) ;
193
+ edges . push ( {
194
+ // eslint-disable-next-line no-plusplus
195
+ id : `e${ eid ++ } ` ,
196
+ src : hub ,
197
+ dest : node ,
198
+ ...base ,
199
+ ...( chainIsEvm ? { feeMode : 'evmToPool' } : { } ) ,
200
+ } ) ;
173
201
}
174
202
175
203
// Return mutable graph (do NOT harden so we can add inter-chain links later)
@@ -373,10 +401,11 @@ export const solveRebalance = async (
373
401
return flows ;
374
402
} ;
375
403
376
- export const rebalanceMinCostFlowSteps = (
404
+ export const rebalanceMinCostFlowSteps = async (
377
405
flows : SolvedEdgeFlow [ ] ,
378
406
graph : RebalanceGraph ,
379
- ) : MovementDesc [ ] => {
407
+ gasEstimator : GasEstimator ,
408
+ ) : Promise < MovementDesc [ ] > => {
380
409
const supplies = new Map (
381
410
typedEntries ( graph . supplies ) . filter ( ( [ _place , amount ] ) => amount > 0 ) ,
382
411
) ;
@@ -425,37 +454,67 @@ export const rebalanceMinCostFlowSteps = (
425
454
pendingFlows . delete ( chosen . edge . id ) ;
426
455
lastChain = chosen . srcChain ;
427
456
}
428
-
429
- const steps : MovementDesc [ ] = prioritized . map ( ( { edge, flow } ) => {
430
- Number . isSafeInteger ( flow ) ||
431
- Fail `flow ${ flow } for edge ${ edge } is not a safe integer` ;
432
- const amount = AmountMath . make ( graph . brand , BigInt ( flow ) ) ;
433
-
434
- let details = { } ;
435
- switch ( edge . feeMode ) {
436
- case 'gmpTransfer' :
437
- // TODO: Rather than hard-code, derive from Axelar `estimateGasFee`.
438
- // https://docs.axelar.dev/dev/axelarjs-sdk/axelar-query-api#estimategasfee
439
- details = { fee : AmountMath . make ( graph . feeBrand , 30_000_000n ) } ;
440
- break ;
441
- case 'gmpCall' :
442
- // TODO: Rather than hard-code, derive from Axelar `estimateGasFee`.
443
- // https://docs.axelar.dev/dev/axelarjs-sdk/axelar-query-api#estimategasfee
444
- details = { fee : AmountMath . make ( graph . feeBrand , 30_000_000n ) } ;
445
- break ;
446
- case 'toUSDN' : {
447
- // NOTE USDN transfer incurs a fee on output amount in basis points
448
- const usdnOut =
449
- ( BigInt ( flow ) * ( 10000n - BigInt ( edge . variableFee ) ) ) / 10000n ;
450
- details = { detail : { usdnOut } } ;
451
- break ;
457
+ /**
458
+ * Pad each fee estimate in case the landscape changes between estimation and
459
+ * execution.
460
+ */
461
+ const padFeeEstimate = ( estimate : bigint ) : bigint => estimate * 3n ;
462
+
463
+ const steps : MovementDesc [ ] = await Promise . all (
464
+ prioritized . map ( async ( { edge, flow } ) => {
465
+ Number . isSafeInteger ( flow ) ||
466
+ Fail `flow ${ flow } for edge ${ edge } is not a safe integer` ;
467
+ const amount = AmountMath . make ( graph . brand , BigInt ( flow ) ) ;
468
+
469
+ await null ;
470
+ let details = { } ;
471
+ switch ( edge . feeMode ) {
472
+ case 'makeEvmAccount' : {
473
+ const feeValue = await gasEstimator . getFactoryContractEstimate (
474
+ chainOf ( edge . dest ) as AxelarChain ,
475
+ ) ;
476
+ details = {
477
+ // XXX: not using getReturnFeeEstimate until we can verify axelar
478
+ // API is accurate for this
479
+ detail : { evmGas : 200_000_000_000_000n } ,
480
+ fee : AmountMath . make ( graph . feeBrand , padFeeEstimate ( feeValue ) ) ,
481
+ } ;
482
+ break ;
483
+ }
484
+ // XXX: revisit https://github.com/Agoric/agoric-sdk/pull/11953#discussion_r2383034184
485
+ case 'poolToEvm' :
486
+ case 'evmToPool' : {
487
+ const feeValue = await gasEstimator . getWalletEstimate (
488
+ chainOf ( edge . dest ) as AxelarChain ,
489
+ ) ;
490
+ details = {
491
+ fee : AmountMath . make ( graph . feeBrand , padFeeEstimate ( feeValue ) ) ,
492
+ } ;
493
+ break ;
494
+ }
495
+ case 'evmToNoble' : {
496
+ const feeValue = await gasEstimator . getWalletEstimate (
497
+ chainOf ( edge . src ) as AxelarChain ,
498
+ ) ;
499
+ details = {
500
+ fee : AmountMath . make ( graph . feeBrand , padFeeEstimate ( feeValue ) ) ,
501
+ } ;
502
+ break ;
503
+ }
504
+ case 'toUSDN' : {
505
+ // NOTE USDN transfer incurs a fee on output amount in basis points
506
+ const usdnOut =
507
+ ( BigInt ( flow ) * ( 10000n - BigInt ( edge . variableFee ) ) ) / 10000n ;
508
+ details = { detail : { usdnOut } } ;
509
+ break ;
510
+ }
511
+ default :
512
+ break ;
452
513
}
453
- default :
454
- break ;
455
- }
456
514
457
- return { src : edge . src , dest : edge . dest , amount, ...details } ;
458
- } ) ;
515
+ return { src : edge . src , dest : edge . dest , amount, ...details } ;
516
+ } ) ,
517
+ ) ;
459
518
460
519
return harden ( steps ) ;
461
520
} ;
@@ -476,8 +535,17 @@ export const planRebalanceFlow = async (opts: {
476
535
brand : Amount [ 'brand' ] ;
477
536
feeBrand : Amount [ 'brand' ] ;
478
537
mode ?: RebalanceMode ;
538
+ gasEstimator : GasEstimator ;
479
539
} ) => {
480
- const { network, current, target, brand, feeBrand, mode = 'fastest' } = opts ;
540
+ const {
541
+ network,
542
+ current,
543
+ target,
544
+ brand,
545
+ feeBrand,
546
+ mode = 'fastest' ,
547
+ gasEstimator,
548
+ } = opts ;
481
549
// TODO remove "automatic" values that should be static
482
550
const graph = makeGraphFromDefinition (
483
551
network ,
@@ -497,7 +565,7 @@ export const planRebalanceFlow = async (opts: {
497
565
preflightValidateNetworkPlan ( network as any , current as any , target as any ) ;
498
566
throw err ;
499
567
}
500
- const steps = rebalanceMinCostFlowSteps ( flows , graph ) ;
568
+ const steps = await rebalanceMinCostFlowSteps ( flows , graph , gasEstimator ) ;
501
569
return harden ( { graph, model, flows, steps } ) ;
502
570
} ;
503
571
0 commit comments