@hyperlane-xyz/rebalancer 27.2.12 → 27.2.13
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/dist/core/InventoryRebalancer.d.ts +11 -19
- package/dist/core/InventoryRebalancer.d.ts.map +1 -1
- package/dist/core/InventoryRebalancer.js +336 -268
- package/dist/core/InventoryRebalancer.js.map +1 -1
- package/dist/core/InventoryRebalancer.test.js +397 -23
- package/dist/core/InventoryRebalancer.test.js.map +1 -1
- package/dist/core/Rebalancer.d.ts.map +1 -1
- package/dist/core/Rebalancer.js +12 -6
- package/dist/core/Rebalancer.js.map +1 -1
- package/dist/core/Rebalancer.test.js +51 -0
- package/dist/core/Rebalancer.test.js.map +1 -1
- package/dist/core/RebalancerOrchestrator.test.js +0 -1
- package/dist/core/RebalancerOrchestrator.test.js.map +1 -1
- package/dist/core/RebalancerService.d.ts +2 -3
- package/dist/core/RebalancerService.d.ts.map +1 -1
- package/dist/core/RebalancerService.js +3 -2
- package/dist/core/RebalancerService.js.map +1 -1
- package/dist/core/RebalancerService.test.js +24 -0
- package/dist/core/RebalancerService.test.js.map +1 -1
- package/dist/e2e/harness/TestHelpers.js +1 -2
- package/dist/e2e/harness/TestHelpers.js.map +1 -1
- package/dist/factories/RebalancerContextFactory.d.ts +4 -5
- package/dist/factories/RebalancerContextFactory.d.ts.map +1 -1
- package/dist/factories/RebalancerContextFactory.js +12 -7
- package/dist/factories/RebalancerContextFactory.js.map +1 -1
- package/dist/factories/RebalancerContextFactory.test.js +99 -2
- package/dist/factories/RebalancerContextFactory.test.js.map +1 -1
- package/dist/interfaces/IRebalancer.d.ts +4 -2
- package/dist/interfaces/IRebalancer.d.ts.map +1 -1
- package/dist/metrics/scripts/metrics.d.ts +1 -1
- package/dist/monitor/Monitor.d.ts.map +1 -1
- package/dist/monitor/Monitor.js +14 -6
- package/dist/monitor/Monitor.js.map +1 -1
- package/dist/strategy/BaseStrategy.d.ts.map +1 -1
- package/dist/strategy/BaseStrategy.js +13 -11
- package/dist/strategy/BaseStrategy.js.map +1 -1
- package/dist/strategy/CollateralDeficitStrategy.d.ts.map +1 -1
- package/dist/strategy/CollateralDeficitStrategy.js +2 -2
- package/dist/strategy/CollateralDeficitStrategy.js.map +1 -1
- package/dist/strategy/MinAmountStrategy.d.ts +1 -0
- package/dist/strategy/MinAmountStrategy.d.ts.map +1 -1
- package/dist/strategy/MinAmountStrategy.js +12 -8
- package/dist/strategy/MinAmountStrategy.js.map +1 -1
- package/dist/strategy/MinAmountStrategy.test.js +189 -2
- package/dist/strategy/MinAmountStrategy.test.js.map +1 -1
- package/dist/test/helpers.d.ts +11 -3
- package/dist/test/helpers.d.ts.map +1 -1
- package/dist/test/helpers.js +9 -11
- package/dist/test/helpers.js.map +1 -1
- package/dist/test/lifiMocks.d.ts.map +1 -1
- package/dist/test/lifiMocks.js +5 -2
- package/dist/test/lifiMocks.js.map +1 -1
- package/dist/tracking/ActionTracker.d.ts.map +1 -1
- package/dist/tracking/ActionTracker.js +2 -1
- package/dist/tracking/ActionTracker.js.map +1 -1
- package/dist/tracking/ActionTracker.test.js +39 -0
- package/dist/tracking/ActionTracker.test.js.map +1 -1
- package/dist/utils/balanceUtils.d.ts +7 -1
- package/dist/utils/balanceUtils.d.ts.map +1 -1
- package/dist/utils/balanceUtils.js +39 -1
- package/dist/utils/balanceUtils.js.map +1 -1
- package/dist/utils/balanceUtils.test.js +55 -1
- package/dist/utils/balanceUtils.test.js.map +1 -1
- package/dist/utils/blockTag.d.ts +3 -3
- package/dist/utils/blockTag.d.ts.map +1 -1
- package/dist/utils/blockTag.js +1 -1
- package/dist/utils/blockTag.js.map +1 -1
- package/package.json +7 -7
- package/src/core/InventoryRebalancer.test.ts +503 -38
- package/src/core/InventoryRebalancer.ts +483 -350
- package/src/core/Rebalancer.test.ts +84 -0
- package/src/core/Rebalancer.ts +22 -6
- package/src/core/RebalancerOrchestrator.test.ts +0 -1
- package/src/core/RebalancerService.test.ts +35 -0
- package/src/core/RebalancerService.ts +9 -5
- package/src/e2e/harness/TestHelpers.ts +3 -3
- package/src/factories/RebalancerContextFactory.test.ts +143 -6
- package/src/factories/RebalancerContextFactory.ts +29 -17
- package/src/interfaces/IRebalancer.ts +4 -1
- package/src/monitor/Monitor.ts +19 -6
- package/src/strategy/BaseStrategy.ts +18 -15
- package/src/strategy/CollateralDeficitStrategy.ts +4 -3
- package/src/strategy/MinAmountStrategy.test.ts +238 -2
- package/src/strategy/MinAmountStrategy.ts +29 -17
- package/src/test/helpers.ts +13 -12
- package/src/test/lifiMocks.ts +5 -2
- package/src/tracking/ActionTracker.test.ts +47 -0
- package/src/tracking/ActionTracker.ts +2 -1
- package/src/utils/balanceUtils.test.ts +87 -1
- package/src/utils/balanceUtils.ts +73 -2
- package/src/utils/blockTag.ts +9 -4
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { HyperlaneCore,
|
|
2
|
-
import { ProtocolType, assert, ensure0x,
|
|
1
|
+
import { HyperlaneCore, ProviderType, SealevelCoreAdapter, TOKEN_COLLATERALIZED_STANDARDS, WarpTxCategory, getSignerForChain, } from '@hyperlane-xyz/sdk';
|
|
2
|
+
import { ProtocolType, assert, ensure0x, fromWei } from '@hyperlane-xyz/utils';
|
|
3
3
|
import { MIN_VIABLE_COST_MULTIPLIER, calculateTransferCosts, } from '../utils/gasEstimation.js';
|
|
4
4
|
import { getExternalBridgeTokenAddress, isNativeTokenStandard, } from '../utils/tokenUtils.js';
|
|
5
5
|
import { parseSolanaPrivateKey } from '../utils/solanaKeyParser.js';
|
|
6
6
|
import { toProtocolTransaction } from '../utils/transactionUtils.js';
|
|
7
|
+
import { alignLocalToCanonical, denormalizeToLocal, normalizeToCanonical, } from '../utils/balanceUtils.js';
|
|
7
8
|
/**
|
|
8
9
|
* Buffer percentage to add when bridging inventory.
|
|
9
10
|
* Bridges (amount * (100 + BRIDGE_BUFFER_PERCENT)) / 100 to account for slippage.
|
|
@@ -21,6 +22,46 @@ const GAS_COST_MULTIPLIER = 20n;
|
|
|
21
22
|
* If gas exceeds this threshold, the bridge is not economically worthwhile.
|
|
22
23
|
*/
|
|
23
24
|
const MAX_GAS_PERCENT_THRESHOLD = 10n;
|
|
25
|
+
const RECOVERABLE_MAX_TRANSFER_ERROR_MESSAGES = [
|
|
26
|
+
'balance may be insufficient',
|
|
27
|
+
'transfer amount exceeds balance',
|
|
28
|
+
'insufficient balance',
|
|
29
|
+
];
|
|
30
|
+
function hasRecoverableMaxTransferErrorMessage(message) {
|
|
31
|
+
const normalized = message.toLowerCase();
|
|
32
|
+
return (normalized.includes('unpredictable_gas_limit') ||
|
|
33
|
+
RECOVERABLE_MAX_TRANSFER_ERROR_MESSAGES.some((pattern) => normalized.includes(pattern)));
|
|
34
|
+
}
|
|
35
|
+
function isRecoverableMaxTransferProbeError(error) {
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
const stack = [error];
|
|
38
|
+
while (stack.length > 0) {
|
|
39
|
+
const current = stack.pop();
|
|
40
|
+
if (current == null)
|
|
41
|
+
continue;
|
|
42
|
+
if (typeof current === 'string') {
|
|
43
|
+
if (hasRecoverableMaxTransferErrorMessage(current))
|
|
44
|
+
return true;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (typeof current !== 'object')
|
|
48
|
+
continue;
|
|
49
|
+
if (seen.has(current))
|
|
50
|
+
continue;
|
|
51
|
+
seen.add(current);
|
|
52
|
+
const candidate = current;
|
|
53
|
+
if (typeof candidate.code === 'string' &&
|
|
54
|
+
candidate.code.toUpperCase() === 'UNPREDICTABLE_GAS_LIMIT') {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
if (typeof candidate.message === 'string' &&
|
|
58
|
+
hasRecoverableMaxTransferErrorMessage(candidate.message)) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
stack.push(candidate.cause, candidate.error);
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
24
65
|
/**
|
|
25
66
|
* Executes inventory-based rebalances for chains that don't support MovableCollateralRouter.
|
|
26
67
|
*
|
|
@@ -169,6 +210,9 @@ export class InventoryRebalancer {
|
|
|
169
210
|
}
|
|
170
211
|
return total;
|
|
171
212
|
}
|
|
213
|
+
formatLocalAmount(amount, token) {
|
|
214
|
+
return fromWei(amount.toString(), token.decimals);
|
|
215
|
+
}
|
|
172
216
|
/**
|
|
173
217
|
* Get the effective available inventory for a chain, accounting for
|
|
174
218
|
* inventory already consumed during this execution cycle.
|
|
@@ -351,45 +395,96 @@ export class InventoryRebalancer {
|
|
|
351
395
|
this.logger.info({
|
|
352
396
|
strategyRoute: `${origin} (surplus) → ${destination} (deficit)`,
|
|
353
397
|
executionRoute: `transferRemote FROM ${destination} TO ${origin}`,
|
|
354
|
-
|
|
398
|
+
canonicalAmount: amount.toString(),
|
|
355
399
|
intentId: intent.id,
|
|
356
400
|
}, 'Executing inventory route');
|
|
401
|
+
const sourceToken = this.getTokenForChain(destination);
|
|
402
|
+
assert(sourceToken, `No token found for source chain: ${destination}`);
|
|
403
|
+
const requestedLocalAmount = denormalizeToLocal(amount, sourceToken);
|
|
404
|
+
const executionSender = this.getInventorySignerAddress(destination);
|
|
405
|
+
const executionRecipient = this.getInventorySignerAddress(origin);
|
|
357
406
|
// Check available inventory on the DESTINATION (deficit) chain
|
|
358
407
|
// We need inventory here because transferRemote is called FROM this chain
|
|
359
408
|
const availableInventory = this.getEffectiveAvailableInventory(destination);
|
|
360
409
|
this.logger.info({
|
|
361
410
|
checkingChain: destination,
|
|
362
411
|
availableInventory: availableInventory.toString(),
|
|
363
|
-
|
|
364
|
-
requiredAmount:
|
|
365
|
-
|
|
412
|
+
availableInventoryFormatted: this.formatLocalAmount(availableInventory, sourceToken),
|
|
413
|
+
requiredAmount: requestedLocalAmount.toString(),
|
|
414
|
+
requiredAmountFormatted: this.formatLocalAmount(requestedLocalAmount, sourceToken),
|
|
366
415
|
}, 'Checking effective inventory on destination (deficit) chain');
|
|
367
416
|
// Calculate transfer costs including max transferable and min viable amounts
|
|
368
417
|
// transferRemote is called FROM destination TO origin (swapped direction)
|
|
369
418
|
const costs = await calculateTransferCosts(destination, // FROM chain (where transferRemote is called)
|
|
370
419
|
origin, // TO chain (where Hyperlane message goes)
|
|
371
|
-
availableInventory,
|
|
372
|
-
const {
|
|
420
|
+
availableInventory, requestedLocalAmount, this.multiProvider, this.warpCore.multiProvider, this.getTokenForChain.bind(this), executionSender, isNativeTokenStandard, this.logger);
|
|
421
|
+
const { minViableTransfer } = costs;
|
|
422
|
+
let maxTransferable = costs.maxTransferable;
|
|
423
|
+
if (!isNativeTokenStandard(sourceToken.standard)) {
|
|
424
|
+
if (availableInventory === 0n) {
|
|
425
|
+
maxTransferable = 0n;
|
|
426
|
+
this.logger.debug({
|
|
427
|
+
fromChain: destination,
|
|
428
|
+
toChain: origin,
|
|
429
|
+
requestedAmount: requestedLocalAmount.toString(),
|
|
430
|
+
}, 'Skipping fee-aware max transferable probe because destination inventory is zero');
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
try {
|
|
434
|
+
const feeAwareMaxTransfer = await this.warpCore.getMaxTransferAmount({
|
|
435
|
+
balance: sourceToken.amount(availableInventory),
|
|
436
|
+
destination: origin,
|
|
437
|
+
sender: executionSender,
|
|
438
|
+
recipient: executionRecipient,
|
|
439
|
+
});
|
|
440
|
+
maxTransferable =
|
|
441
|
+
feeAwareMaxTransfer.amount < requestedLocalAmount
|
|
442
|
+
? feeAwareMaxTransfer.amount
|
|
443
|
+
: requestedLocalAmount;
|
|
444
|
+
this.logger.debug({
|
|
445
|
+
fromChain: destination,
|
|
446
|
+
toChain: origin,
|
|
447
|
+
availableInventory: availableInventory.toString(),
|
|
448
|
+
requestedAmount: requestedLocalAmount.toString(),
|
|
449
|
+
feeAwareMaxTransferable: maxTransferable.toString(),
|
|
450
|
+
}, 'Calculated fee-aware max transferable amount for non-native route');
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
if (!isRecoverableMaxTransferProbeError(error)) {
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
maxTransferable = 0n;
|
|
457
|
+
this.logger.warn({
|
|
458
|
+
fromChain: destination,
|
|
459
|
+
toChain: origin,
|
|
460
|
+
availableInventory: availableInventory.toString(),
|
|
461
|
+
requestedAmount: requestedLocalAmount.toString(),
|
|
462
|
+
error: error instanceof Error ? error.message : String(error),
|
|
463
|
+
intentId: intent.id,
|
|
464
|
+
}, 'Fee-aware max transferable probe failed due to insufficient balance, falling back to external bridge');
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
373
468
|
// Calculate total inventory across all chains
|
|
374
469
|
// Note: consumedInventory tracking is handled separately within this cycle
|
|
375
470
|
const totalInventory = this.getTotalInventory([]);
|
|
376
471
|
this.logger.info({
|
|
377
472
|
fromChain: destination,
|
|
378
473
|
toChain: origin,
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
canFullyFulfill: maxTransferable >= amount,
|
|
474
|
+
availableInventoryFormatted: this.formatLocalAmount(availableInventory, sourceToken),
|
|
475
|
+
requestedAmountFormatted: this.formatLocalAmount(requestedLocalAmount, sourceToken),
|
|
476
|
+
maxTransferableFormatted: this.formatLocalAmount(maxTransferable, sourceToken),
|
|
477
|
+
minViableTransferFormatted: this.formatLocalAmount(minViableTransfer, sourceToken),
|
|
478
|
+
canFullyFulfill: maxTransferable >= requestedLocalAmount,
|
|
385
479
|
canPartialFulfill: maxTransferable >= minViableTransfer,
|
|
480
|
+
totalInventory: totalInventory.toString(),
|
|
386
481
|
}, 'Calculated max transferable amount with cost-based threshold');
|
|
387
482
|
// Early exit: If remaining amount is below minViableTransfer, complete the intent
|
|
388
483
|
// This prevents infinite loops when the remaining amount is too small to economically bridge
|
|
389
|
-
if (
|
|
484
|
+
if (requestedLocalAmount < minViableTransfer) {
|
|
390
485
|
this.logger.info({
|
|
391
486
|
intentId: intent.id,
|
|
392
|
-
amount:
|
|
487
|
+
amount: requestedLocalAmount.toString(),
|
|
393
488
|
minViableTransfer: minViableTransfer.toString(),
|
|
394
489
|
}, 'Remaining amount below minViableTransfer, completing intent with acceptable loss');
|
|
395
490
|
await this.actionTracker.completeRebalanceIntent(intent.id);
|
|
@@ -405,156 +500,176 @@ export class InventoryRebalancer {
|
|
|
405
500
|
...route,
|
|
406
501
|
origin: destination, // transferRemote called FROM here
|
|
407
502
|
destination: origin, // Hyperlane message goes TO here
|
|
503
|
+
amount: requestedLocalAmount,
|
|
408
504
|
};
|
|
409
|
-
if (maxTransferable >=
|
|
505
|
+
if (maxTransferable >= requestedLocalAmount) {
|
|
410
506
|
// Sufficient inventory on destination - execute transferRemote directly
|
|
411
|
-
const
|
|
507
|
+
const fulfilledCanonicalAmount = normalizeToCanonical(requestedLocalAmount, sourceToken);
|
|
508
|
+
const result = await this.executeTransferRemote(swappedRoute, intent, fulfilledCanonicalAmount);
|
|
412
509
|
// Return original strategy route in result (not the swapped execution route)
|
|
413
510
|
return { ...result, route };
|
|
414
511
|
}
|
|
415
512
|
else if (maxTransferable > 0n && maxTransferable >= minViableTransfer) {
|
|
416
513
|
// Partial transfer: Transfer available inventory when economically viable
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
};
|
|
421
|
-
const result = await this.executeTransferRemote(partialSwappedRoute, intent, costs.gasQuote);
|
|
422
|
-
this.logger.info({
|
|
423
|
-
intentId: intent.id,
|
|
424
|
-
partialAmount: maxTransferable.toString(),
|
|
425
|
-
requestedAmount: amount.toString(),
|
|
426
|
-
remainingAmount: (amount - maxTransferable).toString(),
|
|
427
|
-
}, 'Executed partial inventory deposit, remaining will be handled in future cycles');
|
|
428
|
-
// Return original strategy route in result (not the swapped execution route)
|
|
429
|
-
return { ...result, route };
|
|
430
|
-
}
|
|
431
|
-
else {
|
|
432
|
-
// Inventory below cost-based threshold - trigger ExternalBridge movement TO destination chain
|
|
433
|
-
this.logger.info({
|
|
434
|
-
targetChain: destination,
|
|
435
|
-
maxTransferable: maxTransferable.toString(),
|
|
436
|
-
minViableTransfer: minViableTransfer.toString(),
|
|
437
|
-
costMultiplier: MIN_VIABLE_COST_MULTIPLIER.toString(),
|
|
438
|
-
intentId: intent.id,
|
|
439
|
-
}, 'Inventory below cost-based threshold on destination, triggering LiFi movement');
|
|
440
|
-
// Get all available source chains with raw inventory
|
|
441
|
-
const allSources = this.selectAllSourceChains(destination);
|
|
442
|
-
if (allSources.length === 0) {
|
|
443
|
-
this.logger.warn({
|
|
444
|
-
origin,
|
|
445
|
-
destination,
|
|
446
|
-
amount: amount.toString(),
|
|
514
|
+
const alignedExecution = alignLocalToCanonical(maxTransferable, sourceToken);
|
|
515
|
+
if (alignedExecution.messageAmount === 0n) {
|
|
516
|
+
this.logger.info({
|
|
447
517
|
intentId: intent.id,
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
route,
|
|
451
|
-
success: false,
|
|
452
|
-
error: 'No inventory available on any monitored chain',
|
|
453
|
-
};
|
|
518
|
+
maxTransferable: maxTransferable.toString(),
|
|
519
|
+
}, 'Skipping partial transferRemote because available local amount cannot produce canonical progress');
|
|
454
520
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
const maxViable = await this.calculateMaxViableBridgeAmount(source.chain, destination, source.availableAmount, route.externalBridge);
|
|
460
|
-
if (maxViable > 0n) {
|
|
461
|
-
viableSources.push({ chain: source.chain, maxViable });
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
// Sort by max viable descending (bridge from largest sources first)
|
|
465
|
-
viableSources.sort((a, b) => (a.maxViable > b.maxViable ? -1 : 1));
|
|
466
|
-
if (viableSources.length === 0) {
|
|
467
|
-
this.logger.warn({
|
|
468
|
-
targetChain: destination,
|
|
469
|
-
sourcesChecked: allSources.length,
|
|
470
|
-
intentId: intent.id,
|
|
471
|
-
}, 'No viable bridge sources - all chains have insufficient inventory or high gas costs');
|
|
472
|
-
return {
|
|
473
|
-
route,
|
|
474
|
-
success: false,
|
|
475
|
-
error: 'No viable bridge sources available',
|
|
521
|
+
else {
|
|
522
|
+
const partialSwappedRoute = {
|
|
523
|
+
...swappedRoute,
|
|
524
|
+
amount: alignedExecution.localAmount,
|
|
476
525
|
};
|
|
526
|
+
const result = await this.executeTransferRemote(partialSwappedRoute, intent, alignedExecution.messageAmount);
|
|
527
|
+
this.logger.info({
|
|
528
|
+
intentId: intent.id,
|
|
529
|
+
partialAmount: alignedExecution.localAmount.toString(),
|
|
530
|
+
partialAmountCanonical: alignedExecution.messageAmount.toString(),
|
|
531
|
+
requestedAmount: requestedLocalAmount.toString(),
|
|
532
|
+
requestedAmountCanonical: amount.toString(),
|
|
533
|
+
remainingAmountCanonical: (amount > alignedExecution.messageAmount
|
|
534
|
+
? amount - alignedExecution.messageAmount
|
|
535
|
+
: 0n).toString(),
|
|
536
|
+
}, 'Executed partial inventory deposit, remaining will be handled in future cycles');
|
|
537
|
+
// Return original strategy route in result (not the swapped execution route)
|
|
538
|
+
return { ...result, route };
|
|
477
539
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
540
|
+
}
|
|
541
|
+
// Inventory below cost-based threshold - trigger ExternalBridge movement TO destination chain
|
|
542
|
+
this.logger.info({
|
|
543
|
+
targetChain: destination,
|
|
544
|
+
maxTransferable: maxTransferable.toString(),
|
|
545
|
+
minViableTransfer: minViableTransfer.toString(),
|
|
546
|
+
costMultiplier: MIN_VIABLE_COST_MULTIPLIER.toString(),
|
|
547
|
+
intentId: intent.id,
|
|
548
|
+
}, 'Inventory below cost-based threshold on destination, triggering LiFi movement');
|
|
549
|
+
// Get all available source chains with raw inventory
|
|
550
|
+
const allSources = this.selectAllSourceChains(destination);
|
|
551
|
+
if (allSources.length === 0) {
|
|
552
|
+
this.logger.warn({
|
|
553
|
+
origin,
|
|
554
|
+
destination,
|
|
555
|
+
amount: requestedLocalAmount.toString(),
|
|
556
|
+
intentId: intent.id,
|
|
557
|
+
}, 'No inventory available on any monitored chain');
|
|
558
|
+
return {
|
|
559
|
+
route,
|
|
560
|
+
success: false,
|
|
561
|
+
error: 'No inventory available on any monitored chain',
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
// Calculate source capacities in destination-local units.
|
|
565
|
+
const viableSources = [];
|
|
566
|
+
for (const source of allSources) {
|
|
567
|
+
const capacity = await this.calculateBridgeCapacity(source.chain, destination, source.availableAmount, route.externalBridge);
|
|
568
|
+
if (capacity.maxTargetOutput > 0n) {
|
|
569
|
+
viableSources.push({ chain: source.chain, ...capacity });
|
|
492
570
|
}
|
|
493
|
-
|
|
571
|
+
}
|
|
572
|
+
// Sort by destination output descending.
|
|
573
|
+
viableSources.sort((a, b) => a.maxTargetOutput > b.maxTargetOutput ? -1 : 1);
|
|
574
|
+
if (viableSources.length === 0) {
|
|
575
|
+
this.logger.warn({
|
|
494
576
|
targetChain: destination,
|
|
495
|
-
|
|
496
|
-
chain: s.chain,
|
|
497
|
-
maxViable: s.maxViable.toString(),
|
|
498
|
-
maxViableEth: (Number(s.maxViable) / 1e18).toFixed(6),
|
|
499
|
-
})),
|
|
500
|
-
bridgePlans: bridgePlans.map((p) => ({
|
|
501
|
-
chain: p.chain,
|
|
502
|
-
amount: p.amount.toString(),
|
|
503
|
-
amountEth: (Number(p.amount) / 1e18).toFixed(6),
|
|
504
|
-
})),
|
|
505
|
-
totalPlanned: totalPlanned.toString(),
|
|
506
|
-
targetWithBuffer: targetWithBuffer.toString(),
|
|
577
|
+
sourcesChecked: allSources.length,
|
|
507
578
|
intentId: intent.id,
|
|
508
|
-
}, '
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
579
|
+
}, 'No viable bridge sources - all chains have insufficient inventory or high gas costs');
|
|
580
|
+
return {
|
|
581
|
+
route,
|
|
582
|
+
success: false,
|
|
583
|
+
error: 'No viable bridge sources available',
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
// Create bridge plans using destination-local output amounts.
|
|
587
|
+
const shortfall = requestedLocalAmount > availableInventory
|
|
588
|
+
? requestedLocalAmount - availableInventory
|
|
589
|
+
: 0n;
|
|
590
|
+
const targetWithBuffer = ((shortfall + costs.totalCost) * (100n + BRIDGE_BUFFER_PERCENT)) / 100n;
|
|
591
|
+
const bridgePlans = [];
|
|
592
|
+
let totalPlanned = 0n;
|
|
593
|
+
for (const source of viableSources) {
|
|
594
|
+
if (totalPlanned >= targetWithBuffer)
|
|
595
|
+
break;
|
|
596
|
+
const remaining = targetWithBuffer - totalPlanned;
|
|
597
|
+
const targetOutput = source.maxTargetOutput >= remaining
|
|
598
|
+
? remaining
|
|
599
|
+
: source.maxTargetOutput;
|
|
600
|
+
bridgePlans.push({
|
|
601
|
+
chain: source.chain,
|
|
602
|
+
maxSourceInput: source.maxSourceInput,
|
|
603
|
+
targetOutput,
|
|
604
|
+
});
|
|
605
|
+
totalPlanned += targetOutput;
|
|
606
|
+
}
|
|
607
|
+
this.logger.info({
|
|
608
|
+
targetChain: destination,
|
|
609
|
+
viableSources: viableSources.map((s) => ({
|
|
610
|
+
chain: s.chain,
|
|
611
|
+
maxSourceInput: s.maxSourceInput.toString(),
|
|
612
|
+
maxTargetOutput: s.maxTargetOutput.toString(),
|
|
613
|
+
})),
|
|
614
|
+
bridgePlans: bridgePlans.map((p) => ({
|
|
615
|
+
chain: p.chain,
|
|
616
|
+
maxSourceInput: p.maxSourceInput.toString(),
|
|
617
|
+
targetOutput: p.targetOutput.toString(),
|
|
618
|
+
})),
|
|
619
|
+
totalPlanned: totalPlanned.toString(),
|
|
620
|
+
shortfall: shortfall.toString(),
|
|
621
|
+
targetWithBuffer: targetWithBuffer.toString(),
|
|
622
|
+
intentId: intent.id,
|
|
623
|
+
}, 'Created bridge plans using gas-adjusted viable amounts');
|
|
624
|
+
// Execute all bridges in parallel
|
|
625
|
+
const bridgeResults = await Promise.allSettled(bridgePlans.map((plan) => this.executeInventoryMovement(plan.chain, destination, plan.targetOutput, plan.maxSourceInput, intent, route.externalBridge)));
|
|
626
|
+
// Process results
|
|
627
|
+
let successCount = 0;
|
|
628
|
+
let totalBridged = 0n;
|
|
629
|
+
const failedErrors = [];
|
|
630
|
+
for (let i = 0; i < bridgeResults.length; i++) {
|
|
631
|
+
const result = bridgeResults[i];
|
|
632
|
+
const plan = bridgePlans[i];
|
|
633
|
+
if (result.status === 'fulfilled' && result.value.success) {
|
|
634
|
+
successCount++;
|
|
635
|
+
totalBridged += plan.targetOutput;
|
|
636
|
+
this.logger.info({
|
|
637
|
+
sourceChain: plan.chain,
|
|
638
|
+
amount: plan.targetOutput.toString(),
|
|
639
|
+
txHash: result.value.txHash,
|
|
640
|
+
}, 'Inventory movement succeeded');
|
|
540
641
|
}
|
|
541
|
-
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
}
|
|
642
|
+
else {
|
|
643
|
+
const error = result.status === 'rejected'
|
|
644
|
+
? result.reason?.message
|
|
645
|
+
: result.value.error;
|
|
646
|
+
if (error) {
|
|
647
|
+
failedErrors.push(`${plan.chain}: ${error}`);
|
|
648
|
+
}
|
|
649
|
+
this.logger.warn({
|
|
650
|
+
sourceChain: plan.chain,
|
|
651
|
+
amount: plan.targetOutput.toString(),
|
|
652
|
+
error,
|
|
653
|
+
}, 'Inventory movement failed');
|
|
548
654
|
}
|
|
549
|
-
this.logger.info({
|
|
550
|
-
targetChain: destination,
|
|
551
|
-
successCount,
|
|
552
|
-
totalBridged: totalBridged.toString(),
|
|
553
|
-
targetAmount: amount.toString(),
|
|
554
|
-
intentId: intent.id,
|
|
555
|
-
}, 'Parallel inventory movements completed, transferRemote will execute after bridges complete');
|
|
556
|
-
return { route, success: true };
|
|
557
655
|
}
|
|
656
|
+
if (successCount === 0) {
|
|
657
|
+
const errorDetails = failedErrors.length > 0 ? ` (${failedErrors.join('; ')})` : '';
|
|
658
|
+
return {
|
|
659
|
+
route,
|
|
660
|
+
success: false,
|
|
661
|
+
error: `All inventory movements failed${errorDetails}`,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
this.logger.info({
|
|
665
|
+
targetChain: destination,
|
|
666
|
+
successCount,
|
|
667
|
+
totalBridged: totalBridged.toString(),
|
|
668
|
+
targetAmount: requestedLocalAmount.toString(),
|
|
669
|
+
targetAmountCanonical: amount.toString(),
|
|
670
|
+
intentId: intent.id,
|
|
671
|
+
}, 'Parallel inventory movements completed, transferRemote will execute after bridges complete');
|
|
672
|
+
return { route, success: true };
|
|
558
673
|
}
|
|
559
674
|
/**
|
|
560
675
|
* Execute a transferRemote to deposit collateral.
|
|
@@ -569,48 +684,21 @@ export class InventoryRebalancer {
|
|
|
569
684
|
*
|
|
570
685
|
* @param route - The transfer route (swapped direction)
|
|
571
686
|
* @param intent - The rebalance intent being executed
|
|
572
|
-
* @param gasQuote - Pre-calculated gas quote from calculateTransferCosts
|
|
573
687
|
*/
|
|
574
|
-
async executeTransferRemote(route, intent,
|
|
688
|
+
async executeTransferRemote(route, intent, fulfilledCanonicalAmount) {
|
|
575
689
|
const { origin, destination, amount } = route;
|
|
576
690
|
const originToken = this.getTokenForChain(origin);
|
|
577
691
|
if (!originToken) {
|
|
578
692
|
throw new Error(`No token found for origin chain: ${origin}`);
|
|
579
693
|
}
|
|
580
694
|
const destinationDomain = this.multiProvider.getDomainId(destination);
|
|
581
|
-
this.logger.debug({
|
|
582
|
-
origin,
|
|
583
|
-
destination,
|
|
584
|
-
amount: amount.toString(),
|
|
585
|
-
gasQuote: {
|
|
586
|
-
igpQuote: gasQuote.igpQuote.amount.toString(),
|
|
587
|
-
tokenFeeQuote: gasQuote.tokenFeeQuote?.amount?.toString() ?? 'none',
|
|
588
|
-
},
|
|
589
|
-
}, 'Using pre-calculated gas quote for transferRemote');
|
|
590
|
-
// Convert pre-calculated gas quote to TokenAmount for WarpCore
|
|
591
|
-
const originChainMetadata = this.multiProvider.getChainMetadata(origin);
|
|
592
|
-
const igpAddressOrDenom = gasQuote.igpQuote.addressOrDenom;
|
|
593
|
-
const igpToken = !igpAddressOrDenom || isZeroishAddress(igpAddressOrDenom)
|
|
594
|
-
? Token.FromChainMetadataNativeToken(originChainMetadata)
|
|
595
|
-
: this.warpCore.findToken(origin, igpAddressOrDenom);
|
|
596
|
-
assert(igpToken, `IGP fee token ${igpAddressOrDenom} is unknown`);
|
|
597
|
-
const interchainFee = new TokenAmount(gasQuote.igpQuote.amount, igpToken);
|
|
598
|
-
let tokenFeeQuote;
|
|
599
|
-
if (gasQuote.tokenFeeQuote?.amount) {
|
|
600
|
-
const feeAddress = gasQuote.tokenFeeQuote.addressOrDenom;
|
|
601
|
-
const feeToken = !feeAddress || isZeroishAddress(feeAddress)
|
|
602
|
-
? Token.FromChainMetadataNativeToken(originChainMetadata)
|
|
603
|
-
: originToken;
|
|
604
|
-
tokenFeeQuote = new TokenAmount(gasQuote.tokenFeeQuote.amount, feeToken);
|
|
605
|
-
}
|
|
695
|
+
this.logger.debug({ origin, destination, amount: amount.toString() }, 'Building transferRemote transactions for exact execution amount');
|
|
606
696
|
const originTokenAmount = originToken.amount(amount);
|
|
607
697
|
const transferTxs = await this.warpCore.getTransferRemoteTxs({
|
|
608
698
|
originTokenAmount,
|
|
609
699
|
destination,
|
|
610
700
|
sender: this.getInventorySignerAddress(origin),
|
|
611
701
|
recipient: this.getInventorySignerAddress(destination),
|
|
612
|
-
interchainFee,
|
|
613
|
-
tokenFeeQuote,
|
|
614
702
|
});
|
|
615
703
|
assert(transferTxs.length > 0, 'Expected at least one transaction from WarpCore');
|
|
616
704
|
this.logger.info({
|
|
@@ -651,7 +739,7 @@ export class InventoryRebalancer {
|
|
|
651
739
|
intentId: intent.id,
|
|
652
740
|
origin: this.multiProvider.getDomainId(origin),
|
|
653
741
|
destination: destinationDomain,
|
|
654
|
-
amount,
|
|
742
|
+
amount: fulfilledCanonicalAmount,
|
|
655
743
|
type: 'inventory_deposit',
|
|
656
744
|
txHash: transferTxHash,
|
|
657
745
|
messageId,
|
|
@@ -753,39 +841,26 @@ export class InventoryRebalancer {
|
|
|
753
841
|
return sources.sort((a, b) => a.availableAmount > b.availableAmount ? -1 : 1);
|
|
754
842
|
}
|
|
755
843
|
/**
|
|
756
|
-
* Calculate the
|
|
757
|
-
* Uses LiFi
|
|
758
|
-
*
|
|
759
|
-
*
|
|
760
|
-
* This is the key method for the gas-aware planning approach:
|
|
761
|
-
* - Gets a quote for the full raw inventory to determine actual gas costs
|
|
762
|
-
* - Applies conservative 20x buffer (LiFi underestimates by ~14x historically)
|
|
763
|
-
* - Returns 0 if gas > 10% of inventory (not worth bridging)
|
|
764
|
-
* - Returns inventory - estimatedGas if viable
|
|
844
|
+
* Calculate the bridge capacity from a source chain in destination-local units.
|
|
845
|
+
* Uses LiFi quotes to conservatively estimate the destination output available
|
|
846
|
+
* from the source chain's current local inventory.
|
|
765
847
|
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
* @param rawInventory - Raw available inventory on source chain
|
|
769
|
-
* @param externalBridgeType - External bridge type to use
|
|
770
|
-
* @returns Maximum viable bridge amount (0 if not viable)
|
|
848
|
+
* For native-token sources, gas is reserved from the source inventory and the
|
|
849
|
+
* output capacity is re-quoted from the remaining source input.
|
|
771
850
|
*/
|
|
772
|
-
async
|
|
851
|
+
async calculateBridgeCapacity(sourceChain, targetChain, rawInventory, externalBridgeType) {
|
|
773
852
|
const sourceToken = this.getTokenForChain(sourceChain);
|
|
774
853
|
const targetToken = this.getTokenForChain(targetChain);
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
// Only applies to native tokens (need gas from same balance)
|
|
778
|
-
if (!isNativeTokenStandard(sourceToken.standard)) {
|
|
779
|
-
return rawInventory; // ERC20s don't compete with gas
|
|
780
|
-
}
|
|
854
|
+
assert(sourceToken, `No token found for source chain: ${sourceChain}`);
|
|
855
|
+
assert(targetToken, `No token found for target chain: ${targetChain}`);
|
|
781
856
|
// Convert HypNative token addresses to the external bridge's native token representation
|
|
782
|
-
const fromTokenAddress = this.getNativeTokenAddress(
|
|
857
|
+
const fromTokenAddress = getExternalBridgeTokenAddress(sourceToken, externalBridgeType, this.getNativeTokenAddress.bind(this));
|
|
783
858
|
const toTokenAddress = getExternalBridgeTokenAddress(targetToken, externalBridgeType, this.getNativeTokenAddress.bind(this));
|
|
784
859
|
const sourceChainId = Number(this.multiProvider.getChainId(sourceChain));
|
|
785
860
|
const targetChainId = Number(this.multiProvider.getChainId(targetChain));
|
|
786
861
|
try {
|
|
787
862
|
const externalBridge = this.getExternalBridge(externalBridgeType);
|
|
788
|
-
const
|
|
863
|
+
const initialQuote = await externalBridge.quote({
|
|
789
864
|
fromChain: sourceChainId,
|
|
790
865
|
toChain: targetChainId,
|
|
791
866
|
fromToken: fromTokenAddress,
|
|
@@ -794,62 +869,72 @@ export class InventoryRebalancer {
|
|
|
794
869
|
fromAddress: this.getInventorySignerAddress(sourceChain),
|
|
795
870
|
toAddress: this.getInventorySignerAddress(targetChain),
|
|
796
871
|
});
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
}
|
|
813
|
-
|
|
872
|
+
let maxSourceInput = rawInventory;
|
|
873
|
+
let outputQuote = initialQuote;
|
|
874
|
+
if (isNativeTokenStandard(sourceToken.standard)) {
|
|
875
|
+
const estimatedGas = initialQuote.gasCosts * GAS_COST_MULTIPLIER;
|
|
876
|
+
const maxGasThreshold = rawInventory / MAX_GAS_PERCENT_THRESHOLD;
|
|
877
|
+
if (estimatedGas > maxGasThreshold) {
|
|
878
|
+
this.logger.info({
|
|
879
|
+
sourceChain,
|
|
880
|
+
targetChain,
|
|
881
|
+
rawInventory: rawInventory.toString(),
|
|
882
|
+
quotedGas: initialQuote.gasCosts.toString(),
|
|
883
|
+
estimatedGas: estimatedGas.toString(),
|
|
884
|
+
maxGasThreshold: maxGasThreshold.toString(),
|
|
885
|
+
}, 'Bridge not viable - gas cost exceeds 10% of inventory');
|
|
886
|
+
return { maxSourceInput: 0n, maxTargetOutput: 0n };
|
|
887
|
+
}
|
|
888
|
+
maxSourceInput = rawInventory - estimatedGas;
|
|
889
|
+
if (maxSourceInput <= 0n) {
|
|
890
|
+
return { maxSourceInput: 0n, maxTargetOutput: 0n };
|
|
891
|
+
}
|
|
892
|
+
outputQuote = await externalBridge.quote({
|
|
893
|
+
fromChain: sourceChainId,
|
|
894
|
+
toChain: targetChainId,
|
|
895
|
+
fromToken: fromTokenAddress,
|
|
896
|
+
toToken: toTokenAddress,
|
|
897
|
+
fromAmount: maxSourceInput,
|
|
898
|
+
fromAddress: this.getInventorySignerAddress(sourceChain),
|
|
899
|
+
toAddress: this.getInventorySignerAddress(targetChain),
|
|
900
|
+
});
|
|
814
901
|
}
|
|
815
|
-
// Max viable = inventory minus estimated gas
|
|
816
|
-
const maxViable = rawInventory - estimatedGas;
|
|
817
902
|
this.logger.info({
|
|
818
903
|
sourceChain,
|
|
819
904
|
targetChain,
|
|
820
905
|
rawInventory: rawInventory.toString(),
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
}
|
|
828
|
-
return maxViable;
|
|
906
|
+
maxSourceInput: maxSourceInput.toString(),
|
|
907
|
+
maxTargetOutput: outputQuote.toAmountMin.toString(),
|
|
908
|
+
}, 'Calculated bridge capacity');
|
|
909
|
+
return {
|
|
910
|
+
maxSourceInput,
|
|
911
|
+
maxTargetOutput: outputQuote.toAmountMin,
|
|
912
|
+
};
|
|
829
913
|
}
|
|
830
914
|
catch (error) {
|
|
831
915
|
this.logger.warn({
|
|
832
916
|
sourceChain,
|
|
833
917
|
targetChain,
|
|
834
918
|
error: error.message,
|
|
835
|
-
}, 'Failed to calculate
|
|
836
|
-
return 0n;
|
|
919
|
+
}, 'Failed to calculate bridge capacity, skipping chain');
|
|
920
|
+
return { maxSourceInput: 0n, maxTargetOutput: 0n };
|
|
837
921
|
}
|
|
838
922
|
}
|
|
839
923
|
/**
|
|
840
924
|
* Execute inventory movement from source chain to target chain via LiFi bridge.
|
|
841
925
|
*
|
|
842
|
-
*
|
|
843
|
-
*
|
|
926
|
+
* Uses reverse quotes (`toAmount`) so plans are expressed in target-chain local
|
|
927
|
+
* units and source-local spend is discovered by the bridge quote.
|
|
844
928
|
*
|
|
845
929
|
* @param sourceChain - Chain to move inventory from
|
|
846
930
|
* @param targetChain - Chain to move inventory to (origin chain for rebalancing)
|
|
847
|
-
* @param
|
|
931
|
+
* @param targetOutputAmount - Destination-local amount to receive
|
|
932
|
+
* @param maxSourceInput - Maximum source-local amount available for this plan
|
|
848
933
|
* @param intent - Rebalance intent for tracking
|
|
849
934
|
* @param externalBridgeType - External bridge type to use
|
|
850
935
|
* @returns Result with success status and optional txHash/error
|
|
851
936
|
*/
|
|
852
|
-
async executeInventoryMovement(sourceChain, targetChain,
|
|
937
|
+
async executeInventoryMovement(sourceChain, targetChain, targetOutputAmount, maxSourceInput, intent, externalBridgeType) {
|
|
853
938
|
const sourceToken = this.getTokenForChain(sourceChain);
|
|
854
939
|
if (!sourceToken) {
|
|
855
940
|
return {
|
|
@@ -878,30 +963,6 @@ export class InventoryRebalancer {
|
|
|
878
963
|
fromTokenAddress,
|
|
879
964
|
toTokenAddress,
|
|
880
965
|
}, 'Resolved token addresses for LiFi bridge');
|
|
881
|
-
// Calculate minViableTransfer for the target chain
|
|
882
|
-
// If bridging less than this, the received amount won't be enough to execute transferRemote
|
|
883
|
-
// So we over-bridge to ensure we can complete the intent in the next cycle
|
|
884
|
-
const costs = await calculateTransferCosts(targetChain, // FROM chain for transferRemote (the target of this bridge)
|
|
885
|
-
sourceChain, // TO chain for transferRemote (Hyperlane message destination)
|
|
886
|
-
amount, // availableInventory (not used for minViableTransfer calculation)
|
|
887
|
-
amount, // requestedAmount
|
|
888
|
-
this.multiProvider, this.warpCore.multiProvider, this.getTokenForChain.bind(this), this.getInventorySignerAddress(targetChain), isNativeTokenStandard, this.logger);
|
|
889
|
-
const { minViableTransfer } = costs;
|
|
890
|
-
// If the requested amount is below minViableTransfer, adjust it up
|
|
891
|
-
// This ensures we bridge enough to actually complete the final transferRemote
|
|
892
|
-
const effectiveAmount = amount < minViableTransfer ? minViableTransfer : amount;
|
|
893
|
-
if (effectiveAmount !== amount) {
|
|
894
|
-
this.logger.info({
|
|
895
|
-
originalAmount: amount.toString(),
|
|
896
|
-
effectiveAmount: effectiveAmount.toString(),
|
|
897
|
-
minViableTransfer: minViableTransfer.toString(),
|
|
898
|
-
originalAmountEth: (Number(amount) / 1e18).toFixed(6),
|
|
899
|
-
effectiveAmountEth: (Number(effectiveAmount) / 1e18).toFixed(6),
|
|
900
|
-
minViableTransferEth: (Number(minViableTransfer) / 1e18).toFixed(6),
|
|
901
|
-
adjustedUp: true,
|
|
902
|
-
intentId: intent.id,
|
|
903
|
-
}, 'Over-bridging to minViableTransfer to ensure final transferRemote can complete');
|
|
904
|
-
}
|
|
905
966
|
try {
|
|
906
967
|
const externalBridge = this.getExternalBridge(externalBridgeType);
|
|
907
968
|
const quote = await externalBridge.quote({
|
|
@@ -909,28 +970,33 @@ export class InventoryRebalancer {
|
|
|
909
970
|
toChain: targetChainId,
|
|
910
971
|
fromToken: fromTokenAddress,
|
|
911
972
|
toToken: toTokenAddress,
|
|
912
|
-
|
|
973
|
+
toAmount: targetOutputAmount,
|
|
913
974
|
fromAddress: this.getInventorySignerAddress(sourceChain),
|
|
914
975
|
toAddress: this.getInventorySignerAddress(targetChain),
|
|
915
976
|
});
|
|
916
977
|
const inputRequired = quote.fromAmount;
|
|
978
|
+
if (inputRequired > maxSourceInput) {
|
|
979
|
+
return {
|
|
980
|
+
success: false,
|
|
981
|
+
error: `Bridge input ${inputRequired} exceeded planned source capacity ${maxSourceInput}`,
|
|
982
|
+
};
|
|
983
|
+
}
|
|
917
984
|
this.logger.info({
|
|
918
985
|
sourceChain,
|
|
919
986
|
targetChain,
|
|
920
987
|
sourceChainId,
|
|
921
988
|
targetChainId,
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
effectiveAmount: effectiveAmount.toString(),
|
|
925
|
-
effectiveAmountEth: (Number(effectiveAmount) / 1e18).toFixed(6),
|
|
989
|
+
requestedTargetOutput: targetOutputAmount.toString(),
|
|
990
|
+
requestedTargetOutputFormatted: this.formatLocalAmount(targetOutputAmount, targetToken),
|
|
926
991
|
inputRequired: inputRequired.toString(),
|
|
992
|
+
inputRequiredFormatted: this.formatLocalAmount(inputRequired, sourceToken),
|
|
927
993
|
expectedOutput: quote.toAmount.toString(),
|
|
928
|
-
|
|
994
|
+
expectedOutputMin: quote.toAmountMin.toString(),
|
|
995
|
+
expectedOutputFormatted: this.formatLocalAmount(quote.toAmount, targetToken),
|
|
929
996
|
gasCosts: quote.gasCosts.toString(),
|
|
930
997
|
feeCosts: quote.feeCosts.toString(),
|
|
931
998
|
intentId: intent.id,
|
|
932
|
-
|
|
933
|
-
}, 'Executing inventory movement via LiFi with pre-validated amount');
|
|
999
|
+
}, 'Executing inventory movement via LiFi reverse quote');
|
|
934
1000
|
this.logger.debug({
|
|
935
1001
|
quoteId: quote.id,
|
|
936
1002
|
tool: quote.tool,
|
|
@@ -957,6 +1023,8 @@ export class InventoryRebalancer {
|
|
|
957
1023
|
txHash: result.txHash,
|
|
958
1024
|
intentId: intent.id,
|
|
959
1025
|
}, 'Inventory movement transaction executed');
|
|
1026
|
+
// Keep bridge consumption in source-local units; intent fulfillment only
|
|
1027
|
+
// advances from canonical inventory_deposit amounts after transferRemote.
|
|
960
1028
|
await this.actionTracker.createRebalanceAction({
|
|
961
1029
|
intentId: intent.id,
|
|
962
1030
|
origin: this.multiProvider.getDomainId(sourceChain),
|
|
@@ -980,7 +1048,7 @@ export class InventoryRebalancer {
|
|
|
980
1048
|
this.logger.error({
|
|
981
1049
|
sourceChain,
|
|
982
1050
|
targetChain,
|
|
983
|
-
amount:
|
|
1051
|
+
amount: targetOutputAmount.toString(),
|
|
984
1052
|
intentId: intent.id,
|
|
985
1053
|
error: error.message,
|
|
986
1054
|
}, 'Failed to execute inventory movement');
|