@circle-fin/app-kit 1.9.0 → 1.10.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.
- package/CHANGELOG.md +26 -0
- package/bridge.cjs +622 -26
- package/bridge.d.cts +147 -10
- package/bridge.d.mts +147 -10
- package/bridge.d.ts +147 -10
- package/bridge.mjs +622 -26
- package/chains.cjs +8 -2
- package/chains.d.cts +1 -0
- package/chains.d.mts +1 -0
- package/chains.d.ts +1 -0
- package/chains.mjs +8 -2
- package/context.cjs +1 -0
- package/context.d.cts +152 -12
- package/context.d.mts +152 -12
- package/context.d.ts +152 -12
- package/context.mjs +1 -0
- package/earn.cjs +859 -420
- package/earn.d.cts +521 -94
- package/earn.d.mts +521 -94
- package/earn.d.ts +521 -94
- package/earn.mjs +859 -421
- package/estimateBridge.cjs +622 -26
- package/estimateBridge.d.cts +147 -10
- package/estimateBridge.d.mts +147 -10
- package/estimateBridge.d.ts +147 -10
- package/estimateBridge.mjs +622 -26
- package/estimateSwap.cjs +810 -92
- package/estimateSwap.d.cts +147 -10
- package/estimateSwap.d.mts +147 -10
- package/estimateSwap.d.ts +147 -10
- package/estimateSwap.mjs +810 -92
- package/index.cjs +2450 -645
- package/index.d.cts +1003 -126
- package/index.d.mts +1003 -126
- package/index.d.ts +1003 -126
- package/index.mjs +2450 -645
- package/package.json +6 -6
- package/swap.cjs +810 -92
- package/swap.d.cts +147 -10
- package/swap.d.mts +147 -10
- package/swap.d.ts +147 -10
- package/swap.mjs +810 -92
- package/unifiedBalance.cjs +722 -115
- package/unifiedBalance.d.cts +222 -4
- package/unifiedBalance.d.mts +222 -4
- package/unifiedBalance.d.ts +222 -4
- package/unifiedBalance.mjs +722 -115
package/index.mjs
CHANGED
|
@@ -20,9 +20,9 @@ import { z } from 'zod';
|
|
|
20
20
|
import pino from 'pino';
|
|
21
21
|
import { parseUnits as parseUnits$1, formatUnits as formatUnits$1 } from '@ethersproject/units';
|
|
22
22
|
import { hexlify, hexZeroPad } from '@ethersproject/bytes';
|
|
23
|
+
import '@ethersproject/abi';
|
|
23
24
|
import { getAddress } from '@ethersproject/address';
|
|
24
25
|
import bs58 from 'bs58';
|
|
25
|
-
import '@ethersproject/abi';
|
|
26
26
|
import { PublicKey } from '@solana/web3.js';
|
|
27
27
|
import 'bn.js';
|
|
28
28
|
import '@coral-xyz/anchor';
|
|
@@ -61,6 +61,7 @@ import { keccak256 } from '@ethersproject/keccak256';
|
|
|
61
61
|
...params,
|
|
62
62
|
actions: {
|
|
63
63
|
bridge: {},
|
|
64
|
+
earn: {},
|
|
64
65
|
...params.actions
|
|
65
66
|
}
|
|
66
67
|
};
|
|
@@ -3544,14 +3545,14 @@ class KitError extends Error {
|
|
|
3544
3545
|
}
|
|
3545
3546
|
|
|
3546
3547
|
/**
|
|
3547
|
-
* Standardized error definitions for Earn
|
|
3548
|
+
* Standardized error definitions for Earn operations.
|
|
3548
3549
|
*
|
|
3549
3550
|
* These error codes provide fine-grained categorization of failures
|
|
3550
|
-
* from the
|
|
3551
|
+
* from the Earn service, enabling SDK consumers to distinguish
|
|
3551
3552
|
* between input errors (fix your request) and service errors (retry later).
|
|
3552
3553
|
*
|
|
3553
3554
|
* Error code ranges:
|
|
3554
|
-
* - 1100-
|
|
3555
|
+
* - 1100-1106: INPUT errors — invalid, unsupported, or stale request state
|
|
3555
3556
|
* - 8100-8105: SERVICE errors — retryable backend/provider failures
|
|
3556
3557
|
*
|
|
3557
3558
|
* @example
|
|
@@ -3604,6 +3605,14 @@ class KitError extends Error {
|
|
|
3604
3605
|
name: 'EARN_UNSUPPORTED_BRIDGE_ROUTE',
|
|
3605
3606
|
type: 'INPUT'
|
|
3606
3607
|
},
|
|
3608
|
+
/**
|
|
3609
|
+
* The bridge quote expired. This is an INPUT error because the prepared
|
|
3610
|
+
* request is stale and must be replaced instead of retried.
|
|
3611
|
+
*/ BRIDGE_QUOTE_EXPIRED: {
|
|
3612
|
+
code: 1106,
|
|
3613
|
+
name: 'EARN_BRIDGE_QUOTE_EXPIRED',
|
|
3614
|
+
type: 'INPUT'
|
|
3615
|
+
},
|
|
3607
3616
|
/** The proxy signing call failed — retryable. */ SIGNING_FAILED: {
|
|
3608
3617
|
code: 8100,
|
|
3609
3618
|
name: 'EARN_SIGNING_FAILED',
|
|
@@ -3663,6 +3672,9 @@ function getOptionalString(value) {
|
|
|
3663
3672
|
* internal-error, vault-refresh-busy, off-chain-paused, position-PnL-pending,
|
|
3664
3673
|
* bridge failures/status lookup failures
|
|
3665
3674
|
*
|
|
3675
|
+
* Quote expiry is INPUT/FATAL because callers must start a fresh bridge prepare
|
|
3676
|
+
* flow rather than retry the stale prepared bundle.
|
|
3677
|
+
*
|
|
3666
3678
|
* Unrecognized codes fall through to `parseApiError` for HTTP-status-based
|
|
3667
3679
|
* handling.
|
|
3668
3680
|
*
|
|
@@ -3896,6 +3908,13 @@ function getOptionalString(value) {
|
|
|
3896
3908
|
errorDef: EarnError.PROVIDER_ERROR,
|
|
3897
3909
|
recoverability: 'FATAL'
|
|
3898
3910
|
}
|
|
3911
|
+
],
|
|
3912
|
+
[
|
|
3913
|
+
380506,
|
|
3914
|
+
{
|
|
3915
|
+
errorDef: EarnError.BRIDGE_QUOTE_EXPIRED,
|
|
3916
|
+
recoverability: 'FATAL'
|
|
3917
|
+
}
|
|
3899
3918
|
]
|
|
3900
3919
|
]);
|
|
3901
3920
|
/**
|
|
@@ -4640,7 +4659,10 @@ var EarnChain;
|
|
|
4640
4659
|
contracts: {
|
|
4641
4660
|
v1: {
|
|
4642
4661
|
wallet: GATEWAY_WALLET_EVM_TESTNET,
|
|
4643
|
-
minter: GATEWAY_MINTER_EVM_TESTNET
|
|
4662
|
+
minter: GATEWAY_MINTER_EVM_TESTNET,
|
|
4663
|
+
// DepositForHandler the GenericExecutor calls to run a fast cross-chain
|
|
4664
|
+
// deposit into the GatewayWallet above.
|
|
4665
|
+
depositForHandler: '0xD05E7D2E7d30b92c5F17d7d0fC575fce231F1A48'
|
|
4644
4666
|
}
|
|
4645
4667
|
},
|
|
4646
4668
|
forwarderSupported: {
|
|
@@ -7795,7 +7817,10 @@ var Chains = /*#__PURE__*/Object.freeze({
|
|
|
7795
7817
|
minter: z.string({
|
|
7796
7818
|
required_error: 'Gateway minter address is required. Please provide a valid contract address.',
|
|
7797
7819
|
invalid_type_error: 'Gateway minter address must be a string.'
|
|
7798
|
-
}).min(1, 'Gateway minter address cannot be empty.')
|
|
7820
|
+
}).min(1, 'Gateway minter address cannot be empty.'),
|
|
7821
|
+
depositForHandler: z.string({
|
|
7822
|
+
invalid_type_error: 'Gateway depositForHandler address must be a string.'
|
|
7823
|
+
}).min(1, 'Gateway depositForHandler address cannot be empty.').optional()
|
|
7799
7824
|
}).strict() // Reject any additional properties not defined in the schema
|
|
7800
7825
|
;
|
|
7801
7826
|
/**
|
|
@@ -8335,21 +8360,31 @@ const swapTokenEnumSchema = z.enum([
|
|
|
8335
8360
|
* returning the appropriate address based on the requested contract type.
|
|
8336
8361
|
*
|
|
8337
8362
|
* @param chain - The chain definition to resolve the contract address for
|
|
8338
|
-
* @param contractType - The type of contract address to resolve ('tokenMessenger' or '
|
|
8363
|
+
* @param contractType - The type of contract address to resolve ('tokenMessenger', 'messageTransmitter', or 'tokenMessengerWithFees')
|
|
8339
8364
|
* @returns The contract address for the specified contract type
|
|
8340
8365
|
* @throws Error when chain does not support CCTP v2 or has unsupported contract configuration
|
|
8366
|
+
* @throws Error when 'tokenMessengerWithFees' is requested but not configured on the chain
|
|
8341
8367
|
*/ const resolveCCTPV2ContractAddress = (chain, contractType)=>{
|
|
8342
8368
|
// Handle custom bridge contract for tokenMessenger (burn transaction)
|
|
8343
|
-
if (hasCustomContractSupport(chain, 'bridge') && chain.kitContracts?.bridge !== undefined) {
|
|
8369
|
+
if (contractType === 'tokenMessenger' && hasCustomContractSupport(chain, 'bridge') && chain.kitContracts?.bridge !== undefined) {
|
|
8344
8370
|
return chain.kitContracts.bridge;
|
|
8345
8371
|
}
|
|
8346
8372
|
// At this point we know CCTP v2 is supported, so contracts exist
|
|
8347
8373
|
const cctpConfig = chain.cctp;
|
|
8348
8374
|
const contracts = cctpConfig.contracts.v2;
|
|
8375
|
+
// The `TokenMessengerWithFees` wrapper (prepaid FORWARD path) is an optional
|
|
8376
|
+
// deployment carried alongside both split and merged configurations.
|
|
8377
|
+
if (contractType === 'tokenMessengerWithFees') {
|
|
8378
|
+
const wrapper = contracts.tokenMessengerWithFees;
|
|
8379
|
+
if (wrapper === undefined || wrapper === '') {
|
|
8380
|
+
throw new Error(`TokenMessengerWithFees is not configured on chain ${chain.name}. The prepaid FORWARD path is unavailable on this chain.`);
|
|
8381
|
+
}
|
|
8382
|
+
return wrapper;
|
|
8383
|
+
}
|
|
8349
8384
|
// Handle different contract types with explicit type checking
|
|
8350
8385
|
switch(contracts.type){
|
|
8351
8386
|
case 'split':
|
|
8352
|
-
return contracts.tokenMessenger ;
|
|
8387
|
+
return contractType === 'tokenMessenger' ? contracts.tokenMessenger : contracts.messageTransmitter;
|
|
8353
8388
|
case 'merged':
|
|
8354
8389
|
return contracts.contract;
|
|
8355
8390
|
default:
|
|
@@ -11623,7 +11658,7 @@ function resolveOptions(options) {
|
|
|
11623
11658
|
}
|
|
11624
11659
|
|
|
11625
11660
|
var name$4 = "@circle-fin/bridge-kit";
|
|
11626
|
-
var version$5 = "1.12.
|
|
11661
|
+
var version$5 = "1.12.1";
|
|
11627
11662
|
var pkg$5 = {
|
|
11628
11663
|
name: name$4,
|
|
11629
11664
|
version: version$5};
|
|
@@ -14166,6 +14201,144 @@ const FAST_TIER_FINALITY_THRESHOLD = 1000;
|
|
|
14166
14201
|
return false;
|
|
14167
14202
|
};
|
|
14168
14203
|
|
|
14204
|
+
/**
|
|
14205
|
+
* The zero address, denoting a native-currency fee in a signed quote.
|
|
14206
|
+
*/ const ZERO_ADDRESS$1 = '0x0000000000000000000000000000000000000000';
|
|
14207
|
+
/**
|
|
14208
|
+
* Resolve the fee payment channel for a prepaid-FORWARD burn via `TokenMessengerWithFees`.
|
|
14209
|
+
*
|
|
14210
|
+
* Determines the native `msg.value` and the ERC-20 approvals required, honouring
|
|
14211
|
+
* the quote's `feeToken`:
|
|
14212
|
+
* - Native fee (`feeToken` is the zero address): attach exactly `feeTotalAmount`
|
|
14213
|
+
* as `msg.value`; approve only the burn amount.
|
|
14214
|
+
* - ERC-20 fee equal to the burn token (both USDC — the `isBurnTokenFee` case):
|
|
14215
|
+
* approve a single combined `amount + feeTotalAmount` and skip the redundant
|
|
14216
|
+
* second approval.
|
|
14217
|
+
* - ERC-20 fee different from the burn token: approve the burn amount and the fee
|
|
14218
|
+
* amount separately.
|
|
14219
|
+
*
|
|
14220
|
+
* This encodes only balance/allowance intent; it does not fetch balances. The
|
|
14221
|
+
* caller is responsible for a balance preflight against the fresh quote.
|
|
14222
|
+
*
|
|
14223
|
+
* @param params - The fee token, burn token, burn amount, and total fee amount.
|
|
14224
|
+
* @returns The resolved fee payment plan.
|
|
14225
|
+
* @throws KitError if `amount` or `feeTotalAmount` is negative.
|
|
14226
|
+
*
|
|
14227
|
+
* @example
|
|
14228
|
+
* ```typescript
|
|
14229
|
+
* // Native fee
|
|
14230
|
+
* resolveFeePayment({
|
|
14231
|
+
* feeToken: '0x0000000000000000000000000000000000000000',
|
|
14232
|
+
* burnToken: '0xUSDC...',
|
|
14233
|
+
* amount: 1_000_000n,
|
|
14234
|
+
* feeTotalAmount: 3_500_000n,
|
|
14235
|
+
* })
|
|
14236
|
+
* // → { isNativeFee: true, isBurnTokenFee: false, nativeValue: 3_500_000n,
|
|
14237
|
+
* // approvals: [{ token: '0xUSDC...', amount: 1_000_000n }] }
|
|
14238
|
+
* ```
|
|
14239
|
+
*/ const resolveFeePayment = (params)=>{
|
|
14240
|
+
const { feeToken, burnToken, amount, feeTotalAmount } = params;
|
|
14241
|
+
if (typeof amount !== 'bigint' || amount < 0n) {
|
|
14242
|
+
throw createValidationFailedError$1('amount', amount, 'Must be a non-negative bigint');
|
|
14243
|
+
}
|
|
14244
|
+
if (typeof feeTotalAmount !== 'bigint' || feeTotalAmount < 0n) {
|
|
14245
|
+
throw createValidationFailedError$1('feeTotalAmount', feeTotalAmount, 'Must be a non-negative bigint');
|
|
14246
|
+
}
|
|
14247
|
+
const isNativeFee = feeToken.toLowerCase() === ZERO_ADDRESS$1;
|
|
14248
|
+
const isBurnTokenFee = !isNativeFee && feeToken.toLowerCase() === burnToken.toLowerCase();
|
|
14249
|
+
if (isNativeFee) {
|
|
14250
|
+
return {
|
|
14251
|
+
isNativeFee: true,
|
|
14252
|
+
isBurnTokenFee: false,
|
|
14253
|
+
nativeValue: feeTotalAmount,
|
|
14254
|
+
approvals: [
|
|
14255
|
+
{
|
|
14256
|
+
token: burnToken,
|
|
14257
|
+
amount
|
|
14258
|
+
}
|
|
14259
|
+
]
|
|
14260
|
+
};
|
|
14261
|
+
}
|
|
14262
|
+
if (isBurnTokenFee) {
|
|
14263
|
+
// Fee and burn draw on the same token — a single combined approval covers
|
|
14264
|
+
// both; the redundant second approval is skipped.
|
|
14265
|
+
return {
|
|
14266
|
+
isNativeFee: false,
|
|
14267
|
+
isBurnTokenFee: true,
|
|
14268
|
+
nativeValue: 0n,
|
|
14269
|
+
approvals: [
|
|
14270
|
+
{
|
|
14271
|
+
token: burnToken,
|
|
14272
|
+
amount: amount + feeTotalAmount
|
|
14273
|
+
}
|
|
14274
|
+
]
|
|
14275
|
+
};
|
|
14276
|
+
}
|
|
14277
|
+
return {
|
|
14278
|
+
isNativeFee: false,
|
|
14279
|
+
isBurnTokenFee: false,
|
|
14280
|
+
nativeValue: 0n,
|
|
14281
|
+
approvals: [
|
|
14282
|
+
{
|
|
14283
|
+
token: burnToken,
|
|
14284
|
+
amount
|
|
14285
|
+
},
|
|
14286
|
+
{
|
|
14287
|
+
token: feeToken,
|
|
14288
|
+
amount: feeTotalAmount
|
|
14289
|
+
}
|
|
14290
|
+
]
|
|
14291
|
+
};
|
|
14292
|
+
};
|
|
14293
|
+
|
|
14294
|
+
/**
|
|
14295
|
+
* The ASCII "cctp-forward" magic, hex-encoded (no `0x`), that a forward-friendly
|
|
14296
|
+
* hookData must start with.
|
|
14297
|
+
*/ const CCTP_FORWARD_MAGIC_HEX = Buffer.from(CCTP_FORWARD_MAGIC_PREFIX, 'ascii').toString('hex');
|
|
14298
|
+
/**
|
|
14299
|
+
* Determine whether a hookData blob begins with the `cctp-forward` envelope.
|
|
14300
|
+
*
|
|
14301
|
+
* The prepaid FORWARD path requires the GenericExecutor blob to be wrapped in a
|
|
14302
|
+
* `cctp-forward` frame; without it the wrapper reverts `ForwardFeeWithoutHook`.
|
|
14303
|
+
*
|
|
14304
|
+
* @param hookData - The 0x-prefixed hookData hex string.
|
|
14305
|
+
* @returns True when the hookData starts with the `cctp-forward` magic.
|
|
14306
|
+
*
|
|
14307
|
+
* @example
|
|
14308
|
+
* ```typescript
|
|
14309
|
+
* hasForwardHook('0x636374702d666f7277617264...') // true
|
|
14310
|
+
* hasForwardHook('0xdeadbeef') // false
|
|
14311
|
+
* ```
|
|
14312
|
+
*/ const hasForwardHook = (hookData)=>{
|
|
14313
|
+
if (typeof hookData !== 'string') {
|
|
14314
|
+
return false;
|
|
14315
|
+
}
|
|
14316
|
+
const normalized = (hookData.startsWith('0x') ? hookData.slice(2) : hookData).toLowerCase();
|
|
14317
|
+
return normalized.startsWith(CCTP_FORWARD_MAGIC_HEX);
|
|
14318
|
+
};
|
|
14319
|
+
/**
|
|
14320
|
+
* Assert that a hookData blob is forward-friendly for the prepaid FORWARD path.
|
|
14321
|
+
*
|
|
14322
|
+
* The prepaid FORWARD path always requests a FORWARD fee item, so the wrapper
|
|
14323
|
+
* requires the hookData to start with a `cctp-forward` frame. Validating this
|
|
14324
|
+
* before the burn surfaces the guaranteed `ForwardFeeWithoutHook` revert as a
|
|
14325
|
+
* typed input error instead of an on-chain failure.
|
|
14326
|
+
*
|
|
14327
|
+
* @param hookData - The 0x-prefixed hookData hex string.
|
|
14328
|
+
* @throws KitError (`INPUT_VALIDATION_FAILED`) if the hookData is missing or lacks
|
|
14329
|
+
* the `cctp-forward` frame.
|
|
14330
|
+
*
|
|
14331
|
+
* @example
|
|
14332
|
+
* ```typescript
|
|
14333
|
+
* assertForwardHookData(geForwardHookData) // ok
|
|
14334
|
+
* assertForwardHookData('0xdeadbeef') // throws — would revert ForwardFeeWithoutHook
|
|
14335
|
+
* ```
|
|
14336
|
+
*/ const assertForwardHookData = (hookData)=>{
|
|
14337
|
+
if (!hasForwardHook(hookData)) {
|
|
14338
|
+
throw createValidationFailedError$1('hookData', hookData, 'Prepaid FORWARD burns require a cctp-forward-wrapped hookData; without it the TokenMessengerWithFees wrapper reverts ForwardFeeWithoutHook');
|
|
14339
|
+
}
|
|
14340
|
+
};
|
|
14341
|
+
|
|
14169
14342
|
/**
|
|
14170
14343
|
* Type guard to validate the forwardFee object structure.
|
|
14171
14344
|
*
|
|
@@ -15097,6 +15270,109 @@ const assertCCTPv2BridgeParamsSymbol = Symbol('assertCCTPv2BridgeParams');
|
|
|
15097
15270
|
}
|
|
15098
15271
|
}
|
|
15099
15272
|
|
|
15273
|
+
/**
|
|
15274
|
+
* Assert that `params` is a well-formed {@link BurnWithFeesParams} object.
|
|
15275
|
+
*
|
|
15276
|
+
* Validates the full public-boundary input before any field destructuring,
|
|
15277
|
+
* `BigInt()` coercion, or adapter preparation runs, so malformed JS-caller
|
|
15278
|
+
* inputs always produce typed `KitError` validation failures.
|
|
15279
|
+
*
|
|
15280
|
+
* Checks performed (in order):
|
|
15281
|
+
* - `params` must be a non-null plain object
|
|
15282
|
+
* - `source` — valid CCTP v2 wallet context (via `assertCCTPv2WalletContext`)
|
|
15283
|
+
* - `destinationChain` — present and supports CCTP v2
|
|
15284
|
+
* - source and destination chains must both be testnet or both mainnet
|
|
15285
|
+
* - source and destination chains must differ
|
|
15286
|
+
* - `executor` — non-empty string
|
|
15287
|
+
* - `amount` — bigint or non-empty string coercible to bigint
|
|
15288
|
+
* - `feeTotalAmount` — bigint or non-empty string coercible to bigint
|
|
15289
|
+
* - `feeToken` — valid EVM address (`0x` + 40 hex chars)
|
|
15290
|
+
* - `claim.signedQuote` — valid `0x`-prefixed hex string
|
|
15291
|
+
* - `claim.refundAddress` — valid EVM address
|
|
15292
|
+
* - `hookData` — valid `0x`-prefixed hex string when present
|
|
15293
|
+
*
|
|
15294
|
+
* @param params - The value to validate.
|
|
15295
|
+
* @throws {KitError} If any field is missing or invalid.
|
|
15296
|
+
*
|
|
15297
|
+
* @example
|
|
15298
|
+
* ```typescript
|
|
15299
|
+
* assertBurnWithFeesParams(params)
|
|
15300
|
+
* // params is now typed as BurnWithFeesParams and safe to use
|
|
15301
|
+
* const { source, destinationChain, amount } = params
|
|
15302
|
+
* ```
|
|
15303
|
+
*/ function assertBurnWithFeesParams(params) {
|
|
15304
|
+
if (params === null || typeof params !== 'object' || Array.isArray(params)) {
|
|
15305
|
+
throw createValidationFailedError$1('params', params, 'Must be a non-null plain object');
|
|
15306
|
+
}
|
|
15307
|
+
const p = params;
|
|
15308
|
+
// Source wallet context
|
|
15309
|
+
assertCCTPv2WalletContext(p['source']);
|
|
15310
|
+
const source = p['source'];
|
|
15311
|
+
// destinationChain
|
|
15312
|
+
const destinationChain = p['destinationChain'];
|
|
15313
|
+
if (destinationChain === null || destinationChain === undefined || typeof destinationChain !== 'object') {
|
|
15314
|
+
throw createValidationFailedError$1('destinationChain', destinationChain, 'Must be a chain definition object');
|
|
15315
|
+
}
|
|
15316
|
+
if (!isCCTPV2Supported(destinationChain)) {
|
|
15317
|
+
throw createValidationFailedError$1('destinationChain', destinationChain, 'destinationChain must support CCTP v2');
|
|
15318
|
+
}
|
|
15319
|
+
const dest = destinationChain;
|
|
15320
|
+
// Testnet / mainnet mismatch
|
|
15321
|
+
if (source.chain.isTestnet !== dest.isTestnet) {
|
|
15322
|
+
throw createNetworkMismatchError(source.chain, dest);
|
|
15323
|
+
}
|
|
15324
|
+
// Same-chain guard
|
|
15325
|
+
if (source.chain.name === dest.name) {
|
|
15326
|
+
throw createUnsupportedRouteError(source.chain.name, dest.name);
|
|
15327
|
+
}
|
|
15328
|
+
// executor
|
|
15329
|
+
const executor = p['executor'];
|
|
15330
|
+
if (typeof executor !== 'string' || executor === '') {
|
|
15331
|
+
throw createValidationFailedError$1('executor', executor, 'A GenericExecutor address is required (used for both mintRecipient and destinationCaller)');
|
|
15332
|
+
}
|
|
15333
|
+
// amount
|
|
15334
|
+
const rawAmount = p['amount'];
|
|
15335
|
+
if (typeof rawAmount !== 'bigint' && typeof rawAmount !== 'string' || rawAmount === '') {
|
|
15336
|
+
throw createValidationFailedError$1('amount', rawAmount, 'Must be a bigint or a numeric string');
|
|
15337
|
+
}
|
|
15338
|
+
try {
|
|
15339
|
+
BigInt(rawAmount);
|
|
15340
|
+
} catch {
|
|
15341
|
+
throw createValidationFailedError$1('amount', rawAmount, 'Must be a valid integer value coercible to bigint');
|
|
15342
|
+
}
|
|
15343
|
+
// feeTotalAmount
|
|
15344
|
+
const rawFee = p['feeTotalAmount'];
|
|
15345
|
+
if (typeof rawFee !== 'bigint' && typeof rawFee !== 'string' || rawFee === '') {
|
|
15346
|
+
throw createValidationFailedError$1('feeTotalAmount', rawFee, 'Must be a bigint or a numeric string');
|
|
15347
|
+
}
|
|
15348
|
+
try {
|
|
15349
|
+
BigInt(rawFee);
|
|
15350
|
+
} catch {
|
|
15351
|
+
throw createValidationFailedError$1('feeTotalAmount', rawFee, 'Must be a valid integer value coercible to bigint');
|
|
15352
|
+
}
|
|
15353
|
+
// feeToken
|
|
15354
|
+
if (!evmAddressSchema.safeParse(p['feeToken']).success) {
|
|
15355
|
+
throw createValidationFailedError$1('feeToken', p['feeToken'], 'Must be a valid EVM address (use the zero address for a native fee)');
|
|
15356
|
+
}
|
|
15357
|
+
// claim
|
|
15358
|
+
const rawClaim = p['claim'];
|
|
15359
|
+
if (rawClaim === null || typeof rawClaim !== 'object' || Array.isArray(rawClaim)) {
|
|
15360
|
+
throw createValidationFailedError$1('claim', rawClaim, 'Must be an object with signedQuote and refundAddress');
|
|
15361
|
+
}
|
|
15362
|
+
const claim = rawClaim;
|
|
15363
|
+
if (!hexStringSchema.safeParse(claim['signedQuote']).success) {
|
|
15364
|
+
throw createValidationFailedError$1('claim.signedQuote', claim['signedQuote'], 'Must be a valid hex string starting with 0x');
|
|
15365
|
+
}
|
|
15366
|
+
if (!evmAddressSchema.safeParse(claim['refundAddress']).success) {
|
|
15367
|
+
throw createValidationFailedError$1('claim.refundAddress', claim['refundAddress'], 'Must be a valid EVM address');
|
|
15368
|
+
}
|
|
15369
|
+
// hookData (optional)
|
|
15370
|
+
const hookData = p['hookData'];
|
|
15371
|
+
if (hookData !== undefined && hookData !== '' && !hexStringSchema.safeParse(hookData).success) {
|
|
15372
|
+
throw createValidationFailedError$1('hookData', hookData, 'Must be a valid hex string starting with 0x');
|
|
15373
|
+
}
|
|
15374
|
+
}
|
|
15375
|
+
|
|
15100
15376
|
/**
|
|
15101
15377
|
* CCTP bridge step names that can occur in the bridging flow.
|
|
15102
15378
|
*
|
|
@@ -16305,10 +16581,15 @@ const mockAttestationMessage = {
|
|
|
16305
16581
|
const burnCallData = burnRequest.getCallData();
|
|
16306
16582
|
// batchExecute may throw before submission (wallet declined) but never
|
|
16307
16583
|
// after — post-submission errors are returned as empty receipts.
|
|
16584
|
+
// The sender is threaded for adapters whose execution is routed through a
|
|
16585
|
+
// signing strategy (which has no wallet account to read it from); the
|
|
16586
|
+
// wallet-client path ignores it.
|
|
16308
16587
|
const batchResult = await adapter.batchExecute([
|
|
16309
16588
|
approveCallData,
|
|
16310
16589
|
burnCallData
|
|
16311
|
-
], chain
|
|
16590
|
+
], chain, {
|
|
16591
|
+
fromAddress: params.source.address
|
|
16592
|
+
});
|
|
16312
16593
|
const approveReceipt = batchResult.receipts[0];
|
|
16313
16594
|
const burnReceipt = batchResult.receipts[1];
|
|
16314
16595
|
const approveStep = await buildBatchedStep('approve', approveReceipt, batchResult.batchId, adapter, chain, batchResult.statusCode, batchResult.error);
|
|
@@ -16451,7 +16732,7 @@ const mockAttestationMessage = {
|
|
|
16451
16732
|
return step;
|
|
16452
16733
|
}
|
|
16453
16734
|
|
|
16454
|
-
var version$4 = "1.
|
|
16735
|
+
var version$4 = "1.10.0";
|
|
16455
16736
|
var pkg$4 = {
|
|
16456
16737
|
version: version$4};
|
|
16457
16738
|
|
|
@@ -17044,7 +17325,7 @@ var pkg$4 = {
|
|
|
17044
17325
|
* }
|
|
17045
17326
|
* )
|
|
17046
17327
|
* ```
|
|
17047
|
-
*/ async function retry(result, context, provider, invocationMeta) {
|
|
17328
|
+
*/ async function retry$1(result, context, provider, invocationMeta) {
|
|
17048
17329
|
const analysis = analyzeSteps(result);
|
|
17049
17330
|
// Resolve invocation context for retry operation
|
|
17050
17331
|
const resolvedInvocation = resolveRetryInvocation(invocationMeta);
|
|
@@ -17145,7 +17426,7 @@ var pkg$4 = {
|
|
|
17145
17426
|
// Continue with remaining steps.
|
|
17146
17427
|
// Recursive call handles subsequent pending states (e.g., next step may also
|
|
17147
17428
|
// be pending), allowing the retry logic to loop through all actionable steps.
|
|
17148
|
-
return await retry(result, context, provider);
|
|
17429
|
+
return await retry$1(result, context, provider);
|
|
17149
17430
|
} catch (error) {
|
|
17150
17431
|
// Re-throw FATAL validation errors - these indicate invalid input
|
|
17151
17432
|
if (isFatalError(error)) {
|
|
@@ -17359,7 +17640,7 @@ function assertCCTPV2Config(config) {
|
|
|
17359
17640
|
* const retryResult = await provider.retry(failedResult, retryContext)
|
|
17360
17641
|
* ```
|
|
17361
17642
|
*/ async retry(result, context, invocationMeta) {
|
|
17362
|
-
return retry(result, context, this, invocationMeta);
|
|
17643
|
+
return retry$1(result, context, this, invocationMeta);
|
|
17363
17644
|
}
|
|
17364
17645
|
/**
|
|
17365
17646
|
* Estimate the cost and fees for a CCTP v2 cross-chain bridge operation.
|
|
@@ -17606,7 +17887,7 @@ function assertCCTPV2Config(config) {
|
|
|
17606
17887
|
throw new Error(`Failed to resolve operation context: ${error instanceof Error ? error.message : String(error)}`);
|
|
17607
17888
|
}
|
|
17608
17889
|
// Resolve spender address with proper error handling
|
|
17609
|
-
const spenderAddress = resolveCCTPV2ContractAddress(chain);
|
|
17890
|
+
const spenderAddress = resolveCCTPV2ContractAddress(chain, 'tokenMessenger');
|
|
17610
17891
|
// Prepare action parameters
|
|
17611
17892
|
const actionParams = {
|
|
17612
17893
|
amount: BigInt(amount),
|
|
@@ -18086,6 +18367,106 @@ function assertCCTPV2Config(config) {
|
|
|
18086
18367
|
return await source.adapter.prepareAction('cctp.v2.depositForBurn', actionParams, resolvedContext);
|
|
18087
18368
|
}
|
|
18088
18369
|
/**
|
|
18370
|
+
* Prepare a prepaid-FORWARD burn through the `TokenMessengerWithFees` wrapper.
|
|
18371
|
+
*
|
|
18372
|
+
* Builds the source-chain `depositForBurnWithHookAndFees` call for the
|
|
18373
|
+
* GenericExecutor FORWARD path: fees are collected up front on the source chain
|
|
18374
|
+
* against a signed quote, `mintRecipient` and `destinationCaller` are both set to
|
|
18375
|
+
* the GenericExecutor, and the GE `hookData` is passed through unchanged.
|
|
18376
|
+
*
|
|
18377
|
+
* This is the low-level on-chain primitive behind the UBK `fastCrossChainDeposit`
|
|
18378
|
+
* and Bridge Kit `bridge({ deposit })` flows. The `hookData` and signed-quote
|
|
18379
|
+
* `claim` are produced elsewhere and passed in here:
|
|
18380
|
+
* - `hookData`: `buildForwardingHookDataWithPayload(version,
|
|
18381
|
+
* buildDepositForGenericExecutorPayload(...).hookData)` from `@core/utils`.
|
|
18382
|
+
* - `claim.signedQuote` / `feeToken` / `feeTotalAmount`: from `fetchFeeQuote`
|
|
18383
|
+
* (`@circle-fin/provider-fee-v1`), whose FORWARD item must be bound to the
|
|
18384
|
+
* SAME `hookData` and executor `destinationCaller` used here.
|
|
18385
|
+
*
|
|
18386
|
+
* The returned approvals and burn are NOT executed — the caller executes the
|
|
18387
|
+
* approvals first (in order) and then the burn. The fee payment channel matches
|
|
18388
|
+
* the quote's `feeToken`:
|
|
18389
|
+
* - Native fee: exactly `feeTotalAmount` is attached as the burn's `msg.value`;
|
|
18390
|
+
* only the burn amount is approved.
|
|
18391
|
+
* - USDC fee (same token as the burn): a single combined `amount + feeTotalAmount`
|
|
18392
|
+
* approval covers both; the redundant second approval is skipped.
|
|
18393
|
+
*
|
|
18394
|
+
* @typeParam TFromAdapterCapabilities - The source adapter's capabilities.
|
|
18395
|
+
* @param params - The burn amount, executor, hookData, signed-quote claim, and fee.
|
|
18396
|
+
* @returns The prepared approvals, the prepared burn, and the resolved fee plan.
|
|
18397
|
+
* @throws {KitError} If the wallet context is invalid, `destinationChain` does not
|
|
18398
|
+
* support CCTP v2, the executor is missing, `amount` or `feeTotalAmount` is not
|
|
18399
|
+
* a bigint or a numeric string coercible to bigint, the hookData lacks a
|
|
18400
|
+
* `cctp-forward` frame (guaranteed `ForwardFeeWithoutHook`), or the operation
|
|
18401
|
+
* context cannot be resolved.
|
|
18402
|
+
*
|
|
18403
|
+
* @example
|
|
18404
|
+
* ```typescript
|
|
18405
|
+
* const { approvals, burn } = await provider.burnWithFees({
|
|
18406
|
+
* source,
|
|
18407
|
+
* destinationChain: Arc,
|
|
18408
|
+
* amount: 1_000_000n,
|
|
18409
|
+
* executor: genericExecutorAddress,
|
|
18410
|
+
* hookData: geForwardHookData,
|
|
18411
|
+
* claim: { signedQuote: '0x01...', refundAddress: userAddress },
|
|
18412
|
+
* feeToken: '0x0000000000000000000000000000000000000000', // native
|
|
18413
|
+
* feeTotalAmount: 3_500_000n,
|
|
18414
|
+
* })
|
|
18415
|
+
* for (const approval of approvals) await approval.execute()
|
|
18416
|
+
* const txHash = await burn.execute()
|
|
18417
|
+
* ```
|
|
18418
|
+
*/ async burnWithFees(params) {
|
|
18419
|
+
assertBurnWithFeesParams(params);
|
|
18420
|
+
const { source, destinationChain, executor, hookData, claim, feeToken } = params;
|
|
18421
|
+
const amount = BigInt(params.amount);
|
|
18422
|
+
const feeTotalAmount = BigInt(params.feeTotalAmount);
|
|
18423
|
+
// Coupling guard: the prepaid FORWARD path always requests a FORWARD fee item,
|
|
18424
|
+
// so the hookData must carry a cctp-forward frame; otherwise the wrapper
|
|
18425
|
+
// reverts ForwardFeeWithoutHook. Surface it as a typed input error up front.
|
|
18426
|
+
assertForwardHookData(hookData);
|
|
18427
|
+
const burnToken = source.chain.usdcAddress;
|
|
18428
|
+
const feePayment = resolveFeePayment({
|
|
18429
|
+
feeToken,
|
|
18430
|
+
burnToken,
|
|
18431
|
+
amount,
|
|
18432
|
+
feeTotalAmount
|
|
18433
|
+
});
|
|
18434
|
+
// Resolve operation context from the source wallet context.
|
|
18435
|
+
const operationContext = this.extractOperationContext(source);
|
|
18436
|
+
let resolvedContext;
|
|
18437
|
+
try {
|
|
18438
|
+
resolvedContext = await resolveOperationContext(source.adapter, operationContext);
|
|
18439
|
+
} catch (error) {
|
|
18440
|
+
throw createValidationFailedError$1('source.adapter', undefined, `Failed to resolve operation context: ${error instanceof Error ? error.message : String(error)}`);
|
|
18441
|
+
}
|
|
18442
|
+
const context = resolvedContext;
|
|
18443
|
+
const wrapperAddress = resolveCCTPV2ContractAddress(source.chain, 'tokenMessengerWithFees');
|
|
18444
|
+
// Build the ERC-20 approvals to the wrapper (burn token, plus a distinct fee
|
|
18445
|
+
// token only when the fee is not paid in the burn token).
|
|
18446
|
+
const approvals = await Promise.all(feePayment.approvals.map(async (approval)=>source.adapter.prepareAction('token.approve', {
|
|
18447
|
+
tokenAddress: approval.token,
|
|
18448
|
+
delegate: wrapperAddress,
|
|
18449
|
+
amount: approval.amount
|
|
18450
|
+
}, context)));
|
|
18451
|
+
// Build the burn: mintRecipient AND destinationCaller are both the executor.
|
|
18452
|
+
const burn = await source.adapter.prepareAction('cctp.v2.depositForBurnWithFees', {
|
|
18453
|
+
fromChain: source.chain,
|
|
18454
|
+
toChain: destinationChain,
|
|
18455
|
+
amount,
|
|
18456
|
+
mintRecipient: executor,
|
|
18457
|
+
destinationCaller: executor,
|
|
18458
|
+
hookData,
|
|
18459
|
+
claim,
|
|
18460
|
+
feeToken,
|
|
18461
|
+
feeTotalAmount
|
|
18462
|
+
}, context);
|
|
18463
|
+
return {
|
|
18464
|
+
approvals,
|
|
18465
|
+
burn,
|
|
18466
|
+
feePayment
|
|
18467
|
+
};
|
|
18468
|
+
}
|
|
18469
|
+
/**
|
|
18089
18470
|
* Waits for a transaction to be mined and confirmed on the blockchain.
|
|
18090
18471
|
*
|
|
18091
18472
|
* This method should block until the transaction is confirmed on the blockchain.
|
|
@@ -18965,7 +19346,7 @@ registerKit(`${pkg$5.name}/${pkg$5.version}`);
|
|
|
18965
19346
|
};
|
|
18966
19347
|
|
|
18967
19348
|
var name$3 = "@circle-fin/swap-kit";
|
|
18968
|
-
var version$3 = "1.
|
|
19349
|
+
var version$3 = "1.4.0";
|
|
18969
19350
|
var pkg$3 = {
|
|
18970
19351
|
name: name$3,
|
|
18971
19352
|
version: version$3};
|
|
@@ -19030,7 +19411,10 @@ const optionalChainIdentifierField = chainIdentifierField.optional();
|
|
|
19030
19411
|
}).min(1, 'kitKey must be a non-empty string').optional(),
|
|
19031
19412
|
provider: z.string({
|
|
19032
19413
|
invalid_type_error: 'provider must be a string'
|
|
19033
|
-
}).min(1, 'provider must be a non-empty string').optional()
|
|
19414
|
+
}).min(1, 'provider must be a non-empty string').optional(),
|
|
19415
|
+
batchTransactions: z.boolean({
|
|
19416
|
+
invalid_type_error: 'batchTransactions must be a boolean'
|
|
19417
|
+
}).optional()
|
|
19034
19418
|
});
|
|
19035
19419
|
/**
|
|
19036
19420
|
* Zod schema for adapter context.
|
|
@@ -19561,7 +19945,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
|
|
|
19561
19945
|
/**
|
|
19562
19946
|
* Circle Stablecoin Service API Key.
|
|
19563
19947
|
* Must be a valid API key format.
|
|
19564
|
-
*/ apiKey: apiKeySchema
|
|
19948
|
+
*/ apiKey: apiKeySchema.optional()
|
|
19565
19949
|
}).superRefine(requireCrossChainQuoteToAddress);
|
|
19566
19950
|
/**
|
|
19567
19951
|
* Zod schema for validating CreateSwapRequest parameters.
|
|
@@ -19619,7 +20003,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
|
|
|
19619
20003
|
/**
|
|
19620
20004
|
* Circle Stablecoin Service API Key.
|
|
19621
20005
|
* Must be a valid API key format.
|
|
19622
|
-
*/ apiKey: apiKeySchema
|
|
20006
|
+
*/ apiKey: apiKeySchema.optional()
|
|
19623
20007
|
});
|
|
19624
20008
|
/**
|
|
19625
20009
|
* Zod schema for validating GetSwapStatusResponse data.
|
|
@@ -19655,7 +20039,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
|
|
|
19655
20039
|
toChain: z.string({
|
|
19656
20040
|
invalid_type_error: 'toChain must be a string'
|
|
19657
20041
|
}).min(1, 'toChain must be a non-empty string if provided').optional(),
|
|
19658
|
-
apiKey: apiKeySchema
|
|
20042
|
+
apiKey: apiKeySchema.optional()
|
|
19659
20043
|
});
|
|
19660
20044
|
/**
|
|
19661
20045
|
* Zod schema for validating CreateSwapResponse payloads.
|
|
@@ -19664,13 +20048,15 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
|
|
|
19664
20048
|
required_error: 'fee token is required',
|
|
19665
20049
|
invalid_type_error: 'fee token must be a string'
|
|
19666
20050
|
}).min(1, 'fee token must be a non-empty string'),
|
|
19667
|
-
amount: feeAmountSchema
|
|
20051
|
+
amount: feeAmountSchema,
|
|
20052
|
+
decimals: z.number().int('fee token decimals must be an integer').nonnegative('fee token decimals must be a non-negative integer').optional(),
|
|
20053
|
+
symbol: z.string({
|
|
20054
|
+
invalid_type_error: 'fee token symbol must be a string'
|
|
20055
|
+
}).min(1, 'fee token symbol must be a non-empty string').optional()
|
|
19668
20056
|
});
|
|
19669
20057
|
/**
|
|
19670
20058
|
* Developer fee item schema with basis field.
|
|
19671
|
-
*/ const createSwapDeveloperFeeItemSchema =
|
|
19672
|
-
token: z.string().min(1, 'fee token must be a non-empty string'),
|
|
19673
|
-
amount: feeAmountSchema,
|
|
20059
|
+
*/ const createSwapDeveloperFeeItemSchema = createSwapFeeItemSchema.extend({
|
|
19674
20060
|
basis: z.enum([
|
|
19675
20061
|
'inputAmount',
|
|
19676
20062
|
'estimatedAmount'
|
|
@@ -19762,7 +20148,7 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
|
|
|
19762
20148
|
addresses: z.array(z.string({
|
|
19763
20149
|
invalid_type_error: 'addresses entries must be strings'
|
|
19764
20150
|
}).min(1, 'addresses entries must be non-empty strings')).min(1, 'addresses must contain at least one entry when provided').max(MAX_RATE_ADDRESSES_PER_REQUEST, `addresses supports at most ${String(MAX_RATE_ADDRESSES_PER_REQUEST)} values per request`).optional(),
|
|
19765
|
-
apiKey: apiKeySchema
|
|
20151
|
+
apiKey: apiKeySchema.optional()
|
|
19766
20152
|
});
|
|
19767
20153
|
/**
|
|
19768
20154
|
* Zod schema for validating GetTokenRatesResponse payloads.
|
|
@@ -19979,7 +20365,10 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
|
|
|
19979
20365
|
...DEFAULT_CONFIG$1,
|
|
19980
20366
|
headers: {
|
|
19981
20367
|
...DEFAULT_CONFIG$1.headers,
|
|
19982
|
-
|
|
20368
|
+
// Permissionless mode: no Authorization header when the kit key is absent.
|
|
20369
|
+
...apiKey !== undefined && {
|
|
20370
|
+
Authorization: `Bearer ${apiKey}`
|
|
20371
|
+
}
|
|
19983
20372
|
}
|
|
19984
20373
|
};
|
|
19985
20374
|
try {
|
|
@@ -20133,7 +20522,10 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
|
|
|
20133
20522
|
...DEFAULT_CONFIG$1,
|
|
20134
20523
|
headers: {
|
|
20135
20524
|
...DEFAULT_CONFIG$1.headers,
|
|
20136
|
-
|
|
20525
|
+
// Permissionless mode: no Authorization header when the kit key is absent.
|
|
20526
|
+
...validatedParams.apiKey !== undefined && {
|
|
20527
|
+
Authorization: `Bearer ${validatedParams.apiKey}`
|
|
20528
|
+
}
|
|
20137
20529
|
}
|
|
20138
20530
|
};
|
|
20139
20531
|
return pollApiGet(url, isGetQuoteResponse, effectiveConfig);
|
|
@@ -20188,7 +20580,9 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
|
|
|
20188
20580
|
const validatedParams = {
|
|
20189
20581
|
txHash: result.data.txHash,
|
|
20190
20582
|
chain: result.data.chain,
|
|
20191
|
-
|
|
20583
|
+
...result.data.apiKey !== undefined && {
|
|
20584
|
+
apiKey: result.data.apiKey
|
|
20585
|
+
},
|
|
20192
20586
|
...result.data.toChain !== undefined && {
|
|
20193
20587
|
toChain: result.data.toChain
|
|
20194
20588
|
}
|
|
@@ -20198,7 +20592,10 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
|
|
|
20198
20592
|
...DEFAULT_CONFIG$1,
|
|
20199
20593
|
headers: {
|
|
20200
20594
|
...DEFAULT_CONFIG$1.headers,
|
|
20201
|
-
|
|
20595
|
+
// Permissionless mode: no Authorization header when the kit key is absent.
|
|
20596
|
+
...validatedParams.apiKey !== undefined && {
|
|
20597
|
+
Authorization: `Bearer ${validatedParams.apiKey}`
|
|
20598
|
+
}
|
|
20202
20599
|
}
|
|
20203
20600
|
};
|
|
20204
20601
|
return pollApiGet(url, isGetSwapStatusResponse, effectiveConfig);
|
|
@@ -20287,7 +20684,9 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
|
|
|
20287
20684
|
}
|
|
20288
20685
|
const validatedParams = {
|
|
20289
20686
|
chain: result.data.chain,
|
|
20290
|
-
|
|
20687
|
+
...result.data.apiKey !== undefined && {
|
|
20688
|
+
apiKey: result.data.apiKey
|
|
20689
|
+
},
|
|
20291
20690
|
...result.data.addresses !== undefined && {
|
|
20292
20691
|
addresses: result.data.addresses
|
|
20293
20692
|
}
|
|
@@ -20297,7 +20696,10 @@ getQuoteRequestBaseSchema.superRefine(requireCrossChainQuoteToAddress);
|
|
|
20297
20696
|
...DEFAULT_CONFIG$1,
|
|
20298
20697
|
headers: {
|
|
20299
20698
|
...DEFAULT_CONFIG$1.headers,
|
|
20300
|
-
|
|
20699
|
+
// Permissionless mode: no Authorization header when the kit key is absent.
|
|
20700
|
+
...validatedParams.apiKey !== undefined && {
|
|
20701
|
+
Authorization: `Bearer ${validatedParams.apiKey}`
|
|
20702
|
+
}
|
|
20301
20703
|
}
|
|
20302
20704
|
};
|
|
20303
20705
|
return pollApiGet(url, isGetTokenRatesResponse, effectiveConfig);
|
|
@@ -22342,6 +22744,47 @@ const S_HEX_LENGTH = 32 * HEX_CHARS_PER_BYTE$1 // 32 bytes for 's'
|
|
|
22342
22744
|
*/ function hasSignTypedData(adapter) {
|
|
22343
22745
|
return typeof adapter === 'object' && adapter !== null && 'signTypedData' in adapter && typeof adapter.signTypedData === 'function';
|
|
22344
22746
|
}
|
|
22747
|
+
/**
|
|
22748
|
+
* Type guard to check if an adapter can actually produce an EIP-712
|
|
22749
|
+
* typed-data signature.
|
|
22750
|
+
*
|
|
22751
|
+
* @remarks
|
|
22752
|
+
* Strengthens {@link hasSignTypedData}: having a `signTypedData` method
|
|
22753
|
+
* does not guarantee it can succeed. Adapters whose signer is delegated
|
|
22754
|
+
* (e.g. through a signing strategy backed by a smart contract account)
|
|
22755
|
+
* expose the method but reject typed-data payloads at runtime. Such
|
|
22756
|
+
* adapters report their real capability through an optional
|
|
22757
|
+
* `supportsSignTypedData()` method, which this guard consults when
|
|
22758
|
+
* present. Adapters without the capability method are assumed able to
|
|
22759
|
+
* sign, preserving the previous duck-typing behavior.
|
|
22760
|
+
*
|
|
22761
|
+
* @param adapter - The adapter to check
|
|
22762
|
+
* @returns True if calling `signTypedData` can be expected to succeed
|
|
22763
|
+
*
|
|
22764
|
+
* @example
|
|
22765
|
+
* ```typescript
|
|
22766
|
+
* import { canSignTypedData } from '@core/adapter-evm'
|
|
22767
|
+
*
|
|
22768
|
+
* if (canSignTypedData(adapter)) {
|
|
22769
|
+
* const signature = await adapter.signTypedData(typedData, context)
|
|
22770
|
+
* } else {
|
|
22771
|
+
* // take an on-chain approval path instead of a permit signature
|
|
22772
|
+
* }
|
|
22773
|
+
* ```
|
|
22774
|
+
*/ function canSignTypedData(adapter) {
|
|
22775
|
+
if (!hasSignTypedData(adapter)) {
|
|
22776
|
+
return false;
|
|
22777
|
+
}
|
|
22778
|
+
if (typeof adapter.supportsSignTypedData === 'function') {
|
|
22779
|
+
// The value is `boolean` per the interface, but a plain-JS adapter may
|
|
22780
|
+
// return anything; treat it as untrusted and coerce to a strict
|
|
22781
|
+
// boolean. Comparing an `unknown` (not a `boolean`) also keeps the
|
|
22782
|
+
// lint autofix from stripping this as a redundant `=== true`.
|
|
22783
|
+
const supported = adapter.supportsSignTypedData();
|
|
22784
|
+
return supported === true;
|
|
22785
|
+
}
|
|
22786
|
+
return true;
|
|
22787
|
+
}
|
|
22345
22788
|
|
|
22346
22789
|
/**
|
|
22347
22790
|
* Build EIP-2612 typed data for permit signing.
|
|
@@ -23874,10 +24317,13 @@ function writeBytes32(buffer, hex, offset) {
|
|
|
23874
24317
|
* at usage time rather than construction time.
|
|
23875
24318
|
*
|
|
23876
24319
|
* Validates:
|
|
23877
|
-
* - Kit key
|
|
24320
|
+
* - Kit key matches the required format (KIT_KEY:id:secret) when provided.
|
|
24321
|
+
* An absent or empty kit key is permitted (permissionless mode) — the swap
|
|
24322
|
+
* service now treats the key as optional.
|
|
23878
24323
|
*
|
|
23879
|
-
* @param kitKey - The inline kit key from the swap operation config
|
|
23880
|
-
* @throws KitError with VALIDATION_FAILED if kit key is
|
|
24324
|
+
* @param kitKey - The inline kit key from the swap operation config (optional)
|
|
24325
|
+
* @throws KitError with VALIDATION_FAILED if a kit key is provided but does not
|
|
24326
|
+
* match the KIT_KEY:<keyId>:<keySecret> format
|
|
23881
24327
|
*
|
|
23882
24328
|
* @example
|
|
23883
24329
|
* ```typescript
|
|
@@ -23889,9 +24335,11 @@ function writeBytes32(buffer, hex, offset) {
|
|
|
23889
24335
|
* assertKitKey(kitKey)
|
|
23890
24336
|
* ```
|
|
23891
24337
|
*/ function assertKitKey(kitKey) {
|
|
23892
|
-
//
|
|
24338
|
+
// Permissionless mode: the swap service treats the kit key as optional, so an
|
|
24339
|
+
// absent (or empty) key is valid. Only validate the format when a key is
|
|
24340
|
+
// actually provided.
|
|
23893
24341
|
if (!kitKey) {
|
|
23894
|
-
|
|
24342
|
+
return;
|
|
23895
24343
|
}
|
|
23896
24344
|
const apiKeyResult = apiKeySchema.safeParse(kitKey);
|
|
23897
24345
|
if (!apiKeyResult.success) {
|
|
@@ -24188,8 +24636,8 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
|
|
|
24188
24636
|
validateResolvedAddress(resolvedTokenInAddress, chain);
|
|
24189
24637
|
validateResolvedAddress(resolvedTokenOutAddress, destinationChain);
|
|
24190
24638
|
validateResolvedAddress(to, destinationChain);
|
|
24191
|
-
const kitKey = config?.kitKey
|
|
24192
|
-
//
|
|
24639
|
+
const kitKey = config?.kitKey;
|
|
24640
|
+
// Validate the kit key format when one is provided (permissionless otherwise).
|
|
24193
24641
|
assertKitKey(kitKey);
|
|
24194
24642
|
// Validate custom fee configuration if present
|
|
24195
24643
|
const customFee = config?.customFee;
|
|
@@ -24234,7 +24682,10 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
|
|
|
24234
24682
|
}
|
|
24235
24683
|
}
|
|
24236
24684
|
},
|
|
24237
|
-
apiKey
|
|
24685
|
+
// Map kitKey → apiKey for the service client; omitted in permissionless mode.
|
|
24686
|
+
...kitKey ? {
|
|
24687
|
+
apiKey: kitKey
|
|
24688
|
+
} : {}
|
|
24238
24689
|
};
|
|
24239
24690
|
}
|
|
24240
24691
|
|
|
@@ -24888,6 +25339,37 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
|
|
|
24888
25339
|
}
|
|
24889
25340
|
}
|
|
24890
25341
|
|
|
25342
|
+
/**
|
|
25343
|
+
* Determine whether an adapter can produce an EIP-2612 permit signature.
|
|
25344
|
+
*
|
|
25345
|
+
* @remarks
|
|
25346
|
+
* A gasless permit needs two adapter capabilities: fetching the token's
|
|
25347
|
+
* EIP-2612 nonce and producing an EIP-712 typed-data signature. The
|
|
25348
|
+
* typed-data check uses {@link canSignTypedData} rather than a bare
|
|
25349
|
+
* `hasSignTypedData` guard so that an adapter routed through a signing
|
|
25350
|
+
* strategy that cannot produce typed-data signatures — one whose manifest
|
|
25351
|
+
* omits `evm-typed-data`, surfaced through an optional `supportsSignTypedData()`
|
|
25352
|
+
* — is correctly excluded. Such an adapter falls back to an on-chain approval
|
|
25353
|
+
* (batched into a single submission when it supports atomic execution) instead
|
|
25354
|
+
* of attempting a permit its strategy would reject.
|
|
25355
|
+
*
|
|
25356
|
+
* @param adapter - The source adapter to inspect.
|
|
25357
|
+
* @returns `true` when the adapter can both fetch a nonce and sign typed data.
|
|
25358
|
+
*
|
|
25359
|
+
* @example
|
|
25360
|
+
* ```typescript
|
|
25361
|
+
* import { adapterSupportsPermit } from './utils'
|
|
25362
|
+
*
|
|
25363
|
+
* if (adapterSupportsPermit(adapter)) {
|
|
25364
|
+
* // gasless permit path — fold the approval into the swap transaction
|
|
25365
|
+
* } else {
|
|
25366
|
+
* // on-chain approval path (batched when supportsAtomicBatch is true)
|
|
25367
|
+
* }
|
|
25368
|
+
* ```
|
|
25369
|
+
*/ function adapterSupportsPermit(adapter) {
|
|
25370
|
+
return hasEIP2612NonceFetching(adapter) && canSignTypedData(adapter);
|
|
25371
|
+
}
|
|
25372
|
+
|
|
24891
25373
|
/**
|
|
24892
25374
|
* Generate EIP-2612 permit signature for token approval.
|
|
24893
25375
|
*
|
|
@@ -25023,8 +25505,7 @@ function resolveFeePayoutChain(tokenIn, tokenOut, sourceChain, destinationChain)
|
|
|
25023
25505
|
}
|
|
25024
25506
|
// Skip permit generation if the adapter lacks the required capabilities.
|
|
25025
25507
|
// handleEvmTokenApproval will have already sent an on-chain approval in this case.
|
|
25026
|
-
|
|
25027
|
-
if (!adapterSupportsPermit) {
|
|
25508
|
+
if (!adapterSupportsPermit(adapter)) {
|
|
25028
25509
|
return [
|
|
25029
25510
|
createFallbackTokenInput(tokenInAddress, inputAmount)
|
|
25030
25511
|
];
|
|
@@ -25583,6 +26064,65 @@ const TOKEN_REGISTRY$3 = createTokenRegistry();
|
|
|
25583
26064
|
return `Insufficient ${displaySymbol} balance for swap operation.\n\n` + `Wallet: ${walletAddress}\n` + `Current balance: ${currentDisplay}\n` + `Required: ${requiredDisplay}\n` + `Shortfall: ${shortfallDisplay}\n\n` + `This swap requires ${requiredSummary} to complete the transaction.\n\n` + `Action: Add at least ${actionAmount} to your wallet to complete this swap.`;
|
|
25584
26065
|
}
|
|
25585
26066
|
|
|
26067
|
+
/**
|
|
26068
|
+
* Determine which chain a fee token should be resolved and formatted against.
|
|
26069
|
+
*
|
|
26070
|
+
* @remarks
|
|
26071
|
+
* Fees returned by the service may be denominated in either the input token
|
|
26072
|
+
* (on the source chain) or the output token (on the destination chain). A
|
|
26073
|
+
* contract address only resolves on the chain it belongs to, so formatting a
|
|
26074
|
+
* destination-denominated fee against the source chain causes
|
|
26075
|
+
* {@link resolveTokenSymbol} to miss and the amount to be returned as raw base
|
|
26076
|
+
* units (e.g. a cross-chain swap charging a fee in the destination output
|
|
26077
|
+
* token — an EURC-on-Base address shows `'13202'` instead of `'0.013202'` when
|
|
26078
|
+
* resolved against the source chain). This is the fallback for fee items that
|
|
26079
|
+
* are not self-described with their own `decimals`/`chain`.
|
|
26080
|
+
*
|
|
26081
|
+
* Prefer the source chain (covers same-chain swaps and input-denominated
|
|
26082
|
+
* fees), then fall back to the destination chain when the token only resolves
|
|
26083
|
+
* there. When neither chain recognises the token, default to the source chain
|
|
26084
|
+
* so existing on-chain decimal lookups via the source adapter still apply.
|
|
26085
|
+
*
|
|
26086
|
+
* Symbol tokens (`'USDC'`, `'NATIVE'`) resolve on either chain, so the
|
|
26087
|
+
* source-first preference keeps them on the source chain. That is correct for
|
|
26088
|
+
* registry stablecoins, and for `'NATIVE'` only when both chains share native
|
|
26089
|
+
* decimals (EVM↔EVM, 18). It does NOT honor per-chain native decimals: a
|
|
26090
|
+
* `'NATIVE'`-denominated fee on a Solana↔EVM swap (9 vs 18) would be
|
|
26091
|
+
* mis-scaled. This is latent — providers emit the address form, and
|
|
26092
|
+
* self-describing fee items carry their own `decimals` and never reach this
|
|
26093
|
+
* helper — so the gap only opens for a future `'NATIVE'` fee that arrives
|
|
26094
|
+
* without `decimals` on a cross-native-decimal route.
|
|
26095
|
+
*
|
|
26096
|
+
* @param token - The fee token identifier — a symbol (`'USDC'`) or contract address.
|
|
26097
|
+
* @param sourceChain - The chain the swap originates from.
|
|
26098
|
+
* @param destinationChain - The chain the swap settles on (equals `sourceChain` for same-chain swaps).
|
|
26099
|
+
* @returns The chain definition the fee token should be resolved against.
|
|
26100
|
+
*
|
|
26101
|
+
* @example
|
|
26102
|
+
* ```typescript
|
|
26103
|
+
* import { resolveFeeChain } from './resolveFeeChain'
|
|
26104
|
+
* import { Ethereum, Base } from '@core/chains'
|
|
26105
|
+
*
|
|
26106
|
+
* // Cross-chain swap fee charged in the destination (output) token
|
|
26107
|
+
* resolveFeeChain('0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42', Ethereum, Base)
|
|
26108
|
+
* // => Base (EURC resolves on Base, not Ethereum)
|
|
26109
|
+
*
|
|
26110
|
+
* // Symbol or source-token fees stay on the source chain
|
|
26111
|
+
* resolveFeeChain('USDC', Ethereum, Base) // => Ethereum
|
|
26112
|
+
* ```
|
|
26113
|
+
*/ function resolveFeeChain(token, sourceChain, destinationChain) {
|
|
26114
|
+
if (sourceChain.chain === destinationChain.chain) {
|
|
26115
|
+
return sourceChain;
|
|
26116
|
+
}
|
|
26117
|
+
if (resolveTokenSymbol(token, sourceChain) !== null) {
|
|
26118
|
+
return sourceChain;
|
|
26119
|
+
}
|
|
26120
|
+
if (resolveTokenSymbol(token, destinationChain) !== null) {
|
|
26121
|
+
return destinationChain;
|
|
26122
|
+
}
|
|
26123
|
+
return sourceChain;
|
|
26124
|
+
}
|
|
26125
|
+
|
|
25586
26126
|
const TOKEN_REGISTRY$2 = createTokenRegistry();
|
|
25587
26127
|
/**
|
|
25588
26128
|
* Format a raw base-unit amount into a human-readable decimal string.
|
|
@@ -25673,6 +26213,186 @@ const TOKEN_REGISTRY$2 = createTokenRegistry();
|
|
|
25673
26213
|
}
|
|
25674
26214
|
}
|
|
25675
26215
|
|
|
26216
|
+
/**
|
|
26217
|
+
* Runtime guard for {@link BatchCapableSwapAdapter}.
|
|
26218
|
+
*
|
|
26219
|
+
* @param adapter - The adapter to inspect.
|
|
26220
|
+
* @returns `true` when the adapter exposes both batch methods.
|
|
26221
|
+
*
|
|
26222
|
+
* @example
|
|
26223
|
+
* ```typescript
|
|
26224
|
+
* if (isBatchCapableSwapAdapter(adapter)) {
|
|
26225
|
+
* // adapter.supportsAtomicBatch / adapter.batchExecute are available
|
|
26226
|
+
* }
|
|
26227
|
+
* ```
|
|
26228
|
+
*/ function isBatchCapableSwapAdapter(adapter) {
|
|
26229
|
+
return typeof adapter === 'object' && adapter !== null && typeof adapter.supportsAtomicBatch === 'function' && typeof adapter.batchExecute === 'function';
|
|
26230
|
+
}
|
|
26231
|
+
/**
|
|
26232
|
+
* Decide whether the EVM swap should take the batched approve-and-swap path.
|
|
26233
|
+
*
|
|
26234
|
+
* @remarks
|
|
26235
|
+
* Batching only helps when an on-chain approval would otherwise be required, so
|
|
26236
|
+
* it is skipped for native tokens (no approval) and for the gasless permit path
|
|
26237
|
+
* (already a single transaction). USDT is skipped because its reset-to-zero
|
|
26238
|
+
* allowance flow cannot be expressed as a fixed approve+swap pair. When those
|
|
26239
|
+
* gates pass, the adapter's actual atomic-batch capability is queried; any
|
|
26240
|
+
* failure resolves to `false` so the swap falls back to the sequential path.
|
|
26241
|
+
*
|
|
26242
|
+
* @param args - The decision inputs.
|
|
26243
|
+
* @param args.adapter - The source adapter.
|
|
26244
|
+
* @param args.chain - The source chain definition.
|
|
26245
|
+
* @param args.tokenInAddress - The resolved input-token address.
|
|
26246
|
+
* @param args.allowanceStrategy - Optional allowance strategy override.
|
|
26247
|
+
* @param args.batchTransactions - Optional explicit opt-out (`false` disables).
|
|
26248
|
+
* @returns `true` when the batched approve-and-swap path should be used.
|
|
26249
|
+
*
|
|
26250
|
+
* @example
|
|
26251
|
+
* ```typescript
|
|
26252
|
+
* const useBatched = await shouldUseBatchedSwap({
|
|
26253
|
+
* adapter,
|
|
26254
|
+
* chain,
|
|
26255
|
+
* tokenInAddress: '0xA0b8...',
|
|
26256
|
+
* allowanceStrategy: config?.allowanceStrategy,
|
|
26257
|
+
* batchTransactions: config?.batchTransactions,
|
|
26258
|
+
* })
|
|
26259
|
+
* ```
|
|
26260
|
+
*/ async function shouldUseBatchedSwap({ adapter, chain, tokenInAddress, allowanceStrategy, batchTransactions }) {
|
|
26261
|
+
// Explicit opt-out.
|
|
26262
|
+
if (batchTransactions === false) {
|
|
26263
|
+
return false;
|
|
26264
|
+
}
|
|
26265
|
+
// Batching is an EVM capability (EIP-5792 or a signing strategy).
|
|
26266
|
+
if (chain.type !== 'evm') {
|
|
26267
|
+
return false;
|
|
26268
|
+
}
|
|
26269
|
+
// Native tokens need no approval — the swap is already a single transaction.
|
|
26270
|
+
if (isNativeEvmAddress(tokenInAddress)) {
|
|
26271
|
+
return false;
|
|
26272
|
+
}
|
|
26273
|
+
// A gasless permit folds the approval into the swap transaction, so there is
|
|
26274
|
+
// nothing to batch. Mirrors the permit gate in handleEvmTokenApproval.
|
|
26275
|
+
const canUsePermit = allowanceStrategy !== 'approve' && supportsEIP2612(tokenInAddress, chain) && adapterSupportsPermit(adapter);
|
|
26276
|
+
if (canUsePermit) {
|
|
26277
|
+
return false;
|
|
26278
|
+
}
|
|
26279
|
+
// USDT's reset-to-zero allowance dance cannot be expressed as a fixed
|
|
26280
|
+
// approve+swap pair; leave it on the sequential path.
|
|
26281
|
+
const usdt = chain.usdtAddress?.toLowerCase();
|
|
26282
|
+
if (usdt !== undefined && tokenInAddress.toLowerCase() === usdt) {
|
|
26283
|
+
return false;
|
|
26284
|
+
}
|
|
26285
|
+
if (!isBatchCapableSwapAdapter(adapter)) {
|
|
26286
|
+
return false;
|
|
26287
|
+
}
|
|
26288
|
+
try {
|
|
26289
|
+
return await adapter.supportsAtomicBatch(chain);
|
|
26290
|
+
} catch {
|
|
26291
|
+
return false;
|
|
26292
|
+
}
|
|
26293
|
+
}
|
|
26294
|
+
/**
|
|
26295
|
+
* Execute the approval and swap as a single atomic batch.
|
|
26296
|
+
*
|
|
26297
|
+
* @remarks
|
|
26298
|
+
* Extracts the raw call data from both prepared requests, submits them as one
|
|
26299
|
+
* batch via `adapter.batchExecute`, and maps the swap receipt back to a
|
|
26300
|
+
* transaction hash. The `fromAddress` is threaded for adapters routed through a
|
|
26301
|
+
* signing strategy (which have no wallet account to read the sender from); the
|
|
26302
|
+
* wallet-client path ignores it.
|
|
26303
|
+
*
|
|
26304
|
+
* Following the batch contract, `batchExecute` never throws once the batch is
|
|
26305
|
+
* submitted — a missing or failed swap receipt is surfaced here as a thrown
|
|
26306
|
+
* {@link KitError} (FATAL) so the caller does not resubmit an already-broadcast
|
|
26307
|
+
* batch and double-swap.
|
|
26308
|
+
*
|
|
26309
|
+
* @param args - The execution inputs.
|
|
26310
|
+
* @param args.adapter - The batch-capable source adapter.
|
|
26311
|
+
* @param args.chain - The EVM chain to execute on.
|
|
26312
|
+
* @param args.approveRequest - The prepared ERC-20 approval request.
|
|
26313
|
+
* @param args.swapRequest - The prepared swap request (pre-approval / NONE permit).
|
|
26314
|
+
* @param args.fromAddress - The address authorizing the batch.
|
|
26315
|
+
* @returns The swap transaction hash and the executed approval + swap records.
|
|
26316
|
+
* @throws {@link KitError} when the prepared requests cannot yield call data.
|
|
26317
|
+
* @throws {@link KitError} when the batch does not confirm or the swap reverts.
|
|
26318
|
+
*
|
|
26319
|
+
* @example
|
|
26320
|
+
* ```typescript
|
|
26321
|
+
* const { swapTxHash, executedTransactions } = await executeBatchedApproveAndSwap({
|
|
26322
|
+
* adapter,
|
|
26323
|
+
* chain,
|
|
26324
|
+
* approveRequest,
|
|
26325
|
+
* swapRequest,
|
|
26326
|
+
* fromAddress: '0x742d...',
|
|
26327
|
+
* })
|
|
26328
|
+
* ```
|
|
26329
|
+
*/ async function executeBatchedApproveAndSwap({ adapter, chain, approveRequest, swapRequest, fromAddress }) {
|
|
26330
|
+
if (approveRequest.type !== 'evm' || swapRequest.type !== 'evm' || !approveRequest.getCallData || !swapRequest.getCallData) {
|
|
26331
|
+
throw new KitError({
|
|
26332
|
+
...InputError.UNSUPPORTED_ACTION,
|
|
26333
|
+
recoverability: 'FATAL',
|
|
26334
|
+
message: 'Batched swap requires EVM prepared requests with getCallData() support.'
|
|
26335
|
+
});
|
|
26336
|
+
}
|
|
26337
|
+
const approveCallData = approveRequest.getCallData();
|
|
26338
|
+
const swapCallData = swapRequest.getCallData();
|
|
26339
|
+
const batchResult = await adapter.batchExecute([
|
|
26340
|
+
approveCallData,
|
|
26341
|
+
swapCallData
|
|
26342
|
+
], chain, {
|
|
26343
|
+
fromAddress
|
|
26344
|
+
});
|
|
26345
|
+
const swapReceipt = batchResult.receipts[1];
|
|
26346
|
+
// A missing swap receipt means the batch never confirmed (polling timed out
|
|
26347
|
+
// or the wallet returned fewer receipts than calls). Re-throw the underlying
|
|
26348
|
+
// error when present (already FATAL); otherwise surface a FATAL timeout so the
|
|
26349
|
+
// caller checks the batch status rather than resubmitting.
|
|
26350
|
+
if (swapReceipt === undefined || swapReceipt.txHash === '') {
|
|
26351
|
+
if (isKitError(batchResult.error)) {
|
|
26352
|
+
throw batchResult.error;
|
|
26353
|
+
}
|
|
26354
|
+
throw new KitError({
|
|
26355
|
+
...NetworkError.TIMEOUT,
|
|
26356
|
+
recoverability: 'FATAL',
|
|
26357
|
+
message: `Batched swap did not confirm on-chain (batchId: ${batchResult.batchId}). ` + 'The batch was already submitted — check its status before retrying.',
|
|
26358
|
+
// Preserve the underlying confirmation failure when it isn't a KitError —
|
|
26359
|
+
// the signing-strategy path returns a raw viem error (e.g. a dropped or
|
|
26360
|
+
// replaced tx) — so the root cause survives behind the generic timeout.
|
|
26361
|
+
cause: {
|
|
26362
|
+
trace: {
|
|
26363
|
+
batchId: batchResult.batchId,
|
|
26364
|
+
...batchResult.error != null && {
|
|
26365
|
+
error: batchResult.error
|
|
26366
|
+
}
|
|
26367
|
+
}
|
|
26368
|
+
}
|
|
26369
|
+
});
|
|
26370
|
+
}
|
|
26371
|
+
if (swapReceipt.status !== 'success') {
|
|
26372
|
+
throw createTransactionRevertedError(chain.name, 'Batched swap transaction reverted on-chain', undefined, swapReceipt.txHash, buildExplorerUrl(chain, swapReceipt.txHash));
|
|
26373
|
+
}
|
|
26374
|
+
const executedTransactions = [];
|
|
26375
|
+
const approveReceipt = batchResult.receipts[0];
|
|
26376
|
+
// An atomic batch is a single on-chain transaction, so the approve and swap
|
|
26377
|
+
// receipts share one hash. Only surface a distinct approval record when it is
|
|
26378
|
+
// genuinely a separate transaction; otherwise the lone swap record represents
|
|
26379
|
+
// the batch, avoiding a phantom duplicate tx in executedTransactions.
|
|
26380
|
+
if (approveReceipt !== undefined && approveReceipt.txHash !== '' && approveReceipt.txHash !== swapReceipt.txHash) {
|
|
26381
|
+
executedTransactions.push({
|
|
26382
|
+
type: 'approval',
|
|
26383
|
+
txHash: approveReceipt.txHash
|
|
26384
|
+
});
|
|
26385
|
+
}
|
|
26386
|
+
executedTransactions.push({
|
|
26387
|
+
type: 'swap',
|
|
26388
|
+
txHash: swapReceipt.txHash
|
|
26389
|
+
});
|
|
26390
|
+
return {
|
|
26391
|
+
swapTxHash: swapReceipt.txHash,
|
|
26392
|
+
executedTransactions
|
|
26393
|
+
};
|
|
26394
|
+
}
|
|
26395
|
+
|
|
25676
26396
|
/**
|
|
25677
26397
|
* Safety multiplier applied to locally estimated gas for EVM swap execution.
|
|
25678
26398
|
* Derived from refund cap (max 1/5 of total gas used) plus an extra 0.1 margin,
|
|
@@ -25799,7 +26519,9 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
|
|
|
25799
26519
|
const statusResult = await getSwapStatus$2({
|
|
25800
26520
|
txHash,
|
|
25801
26521
|
chain: chain.chain,
|
|
25802
|
-
apiKey
|
|
26522
|
+
...apiKey !== undefined && {
|
|
26523
|
+
apiKey
|
|
26524
|
+
}
|
|
25803
26525
|
});
|
|
25804
26526
|
if (statusResult.status === 'DONE' && statusResult.amountOut !== undefined) {
|
|
25805
26527
|
return {
|
|
@@ -26390,8 +27112,7 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
|
|
|
26390
27112
|
// Note: tokenInAddress from executionCtx is already resolved (handles NATIVE alias, ETH, etc.)
|
|
26391
27113
|
const isNativeToken = isNativeEvmAddress(executionCtx.tokenInAddress);
|
|
26392
27114
|
const tokenSupportsPermit = supportsEIP2612(executionCtx.tokenInAddress, chain);
|
|
26393
|
-
const
|
|
26394
|
-
const canUsePermitFlow = tokenSupportsPermit && adapterSupportsPermit && allowanceStrategy !== 'approve';
|
|
27115
|
+
const canUsePermitFlow = tokenSupportsPermit && adapterSupportsPermit(adapter) && allowanceStrategy !== 'approve';
|
|
26395
27116
|
const needsApproval = !isNativeToken && !canUsePermitFlow;
|
|
26396
27117
|
if (!needsApproval) {
|
|
26397
27118
|
return;
|
|
@@ -26670,34 +27391,55 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
|
|
|
26670
27391
|
const serviceResponse = await createSwap(serviceParams);
|
|
26671
27392
|
// Track executed transactions
|
|
26672
27393
|
const executedTransactions = [];
|
|
26673
|
-
// Prepare swap action based on chain type
|
|
26674
|
-
|
|
26675
|
-
|
|
26676
|
-
|
|
26677
|
-
|
|
26678
|
-
|
|
26679
|
-
|
|
26680
|
-
|
|
26681
|
-
|
|
26682
|
-
|
|
26683
|
-
|
|
26684
|
-
|
|
26685
|
-
}
|
|
27394
|
+
// Prepare swap action(s) based on chain type and batch capability.
|
|
27395
|
+
// Returns either a single prepared action (Solana / sequential EVM) or a
|
|
27396
|
+
// batched approve+swap plan (EVM atomic-batch path).
|
|
27397
|
+
const { preparedAction, batchedSwapPlan } = await this.prepareSwapRequests({
|
|
27398
|
+
adapter,
|
|
27399
|
+
chain,
|
|
27400
|
+
serviceResponse,
|
|
27401
|
+
resolvedContext,
|
|
27402
|
+
executionCtx,
|
|
27403
|
+
config,
|
|
27404
|
+
executedTransactions
|
|
27405
|
+
});
|
|
26686
27406
|
// Execute swap transaction via adapter
|
|
26687
27407
|
// For EVM chains, use gas limit from proxy service API
|
|
26688
27408
|
let txHash;
|
|
26689
27409
|
const evmGasLimit = 'gasLimit' in serviceResponse.transaction ? serviceResponse.transaction.gasLimit : undefined;
|
|
26690
27410
|
try {
|
|
26691
|
-
|
|
26692
|
-
|
|
26693
|
-
|
|
26694
|
-
|
|
26695
|
-
|
|
26696
|
-
|
|
26697
|
-
|
|
26698
|
-
|
|
26699
|
-
|
|
26700
|
-
|
|
27411
|
+
if (batchedSwapPlan) {
|
|
27412
|
+
// Approve + swap submitted as one atomic batch. batchExecute confirms
|
|
27413
|
+
// the swap internally, so no separate waitForTransaction is needed.
|
|
27414
|
+
const batched = await executeBatchedApproveAndSwap({
|
|
27415
|
+
adapter: adapter,
|
|
27416
|
+
chain: chain,
|
|
27417
|
+
approveRequest: batchedSwapPlan.approveRequest,
|
|
27418
|
+
swapRequest: batchedSwapPlan.swapRequest,
|
|
27419
|
+
fromAddress: executionCtx.fromAddress
|
|
27420
|
+
});
|
|
27421
|
+
txHash = batched.swapTxHash;
|
|
27422
|
+
executedTransactions.push(...batched.executedTransactions);
|
|
27423
|
+
} else if (preparedAction) {
|
|
27424
|
+
txHash = await this.executeSwapTransaction(preparedAction, evmGasLimit);
|
|
27425
|
+
executedTransactions.push({
|
|
27426
|
+
type: 'swap',
|
|
27427
|
+
txHash
|
|
27428
|
+
});
|
|
27429
|
+
// Wait for transaction confirmation and verify success
|
|
27430
|
+
const txReceipt = await adapter.waitForTransaction(txHash, undefined, chain);
|
|
27431
|
+
if (txReceipt.status === 'reverted') {
|
|
27432
|
+
const explorerUrl = buildExplorerUrl(chain, txHash);
|
|
27433
|
+
throw createTransactionRevertedError(chain.name, 'Swap transaction reverted on-chain', undefined, txHash, explorerUrl);
|
|
27434
|
+
}
|
|
27435
|
+
} else {
|
|
27436
|
+
// Unreachable: the preparation step always yields either a batched plan
|
|
27437
|
+
// or a prepared action.
|
|
27438
|
+
throw new KitError({
|
|
27439
|
+
...InputError.UNSUPPORTED_ACTION,
|
|
27440
|
+
recoverability: 'FATAL',
|
|
27441
|
+
message: 'No swap execution path was prepared.'
|
|
27442
|
+
});
|
|
26701
27443
|
}
|
|
26702
27444
|
} catch (err) {
|
|
26703
27445
|
handleSwapExecutionError(err, txHash, chain);
|
|
@@ -26716,7 +27458,9 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
|
|
|
26716
27458
|
isCrossChainSwap,
|
|
26717
27459
|
txHash,
|
|
26718
27460
|
chain,
|
|
26719
|
-
|
|
27461
|
+
...serviceParams.apiKey !== undefined && {
|
|
27462
|
+
apiKey: serviceParams.apiKey
|
|
27463
|
+
}
|
|
26720
27464
|
});
|
|
26721
27465
|
// Build and return SwapResult
|
|
26722
27466
|
return {
|
|
@@ -26741,6 +27485,79 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
|
|
|
26741
27485
|
};
|
|
26742
27486
|
}
|
|
26743
27487
|
/**
|
|
27488
|
+
* Prepare the swap execution request(s) for the source wallet's chain.
|
|
27489
|
+
*
|
|
27490
|
+
* Produces either a single {@link PreparedChainRequest} (Solana, or the
|
|
27491
|
+
* sequential EVM approve-then-swap path) or a `batchedSwapPlan` (the EVM
|
|
27492
|
+
* atomic approve+swap path chosen when the adapter supports EIP-5792 atomic
|
|
27493
|
+
* batching). The caller executes whichever field is populated. Any on-chain
|
|
27494
|
+
* approval sent on the sequential path is appended to `executedTransactions`.
|
|
27495
|
+
*
|
|
27496
|
+
* @typeParam TFromAdapterCapabilities - Source-adapter capability set.
|
|
27497
|
+
* @param args - Inputs derived from the validated swap request.
|
|
27498
|
+
* @param args.adapter - Source-chain wallet adapter.
|
|
27499
|
+
* @param args.chain - Source chain definition.
|
|
27500
|
+
* @param args.serviceResponse - Validated createSwap response.
|
|
27501
|
+
* @param args.resolvedContext - Resolved operation context.
|
|
27502
|
+
* @param args.executionCtx - Minimal on-chain execution context.
|
|
27503
|
+
* @param args.config - Optional swap configuration (allowance/batch flags).
|
|
27504
|
+
* @param args.executedTransactions - Array appended with any sent approval.
|
|
27505
|
+
* @returns The prepared action or the batched approve+swap plan.
|
|
27506
|
+
* @throws KitError when the EVM atomic-batch path is selected but the chain
|
|
27507
|
+
* has no configured adapter contract.
|
|
27508
|
+
*/ async prepareSwapRequests(args) {
|
|
27509
|
+
const { adapter, chain, serviceResponse, resolvedContext, executionCtx, config, executedTransactions } = args;
|
|
27510
|
+
if (chain.type === 'solana') {
|
|
27511
|
+
// Solana: No approval needed, directly prepare swap action
|
|
27512
|
+
return {
|
|
27513
|
+
preparedAction: await prepareSolanaSwapAction(adapter, serviceResponse, resolvedContext)
|
|
27514
|
+
};
|
|
27515
|
+
}
|
|
27516
|
+
const useBatch = await shouldUseBatchedSwap({
|
|
27517
|
+
adapter,
|
|
27518
|
+
chain,
|
|
27519
|
+
tokenInAddress: executionCtx.tokenInAddress,
|
|
27520
|
+
allowanceStrategy: config?.allowanceStrategy,
|
|
27521
|
+
batchTransactions: config?.batchTransactions
|
|
27522
|
+
});
|
|
27523
|
+
if (useBatch) {
|
|
27524
|
+
// EVM chains: fuse the ERC-20 approval and the swap into a single atomic
|
|
27525
|
+
// batch (one signing challenge for smart-contract wallets). Force the
|
|
27526
|
+
// swap onto the pre-approval (PermitType.NONE) path since the approval
|
|
27527
|
+
// rides in the same batch.
|
|
27528
|
+
const adapterContractAddress = chain.kitContracts?.adapter;
|
|
27529
|
+
if (!adapterContractAddress) {
|
|
27530
|
+
throw new KitError({
|
|
27531
|
+
...InputError.VALIDATION_FAILED,
|
|
27532
|
+
recoverability: 'FATAL',
|
|
27533
|
+
message: `Adapter contract not configured for chain ${chain.name}. Swap operations require an adapter contract.`,
|
|
27534
|
+
cause: {
|
|
27535
|
+
trace: {
|
|
27536
|
+
chain: chain.name
|
|
27537
|
+
}
|
|
27538
|
+
}
|
|
27539
|
+
});
|
|
27540
|
+
}
|
|
27541
|
+
const [approveRequest, swapRequest] = await Promise.all([
|
|
27542
|
+
this.approve(adapter, executionCtx.amount, executionCtx.tokenInAddress, adapterContractAddress, resolvedContext),
|
|
27543
|
+
prepareEvmSwapAction(adapter, serviceResponse, resolvedContext, 'approve')
|
|
27544
|
+
]);
|
|
27545
|
+
return {
|
|
27546
|
+
batchedSwapPlan: {
|
|
27547
|
+
approveRequest,
|
|
27548
|
+
swapRequest
|
|
27549
|
+
}
|
|
27550
|
+
};
|
|
27551
|
+
}
|
|
27552
|
+
// EVM chains: Handle token approval if needed, then prepare the swap.
|
|
27553
|
+
// prepareEvmSwapAction handles EIP-2612 permit generation; the adapter
|
|
27554
|
+
// contract address is read from chain.kitContracts.adapter.
|
|
27555
|
+
await this.handleEvmTokenApproval(adapter, chain, executionCtx, resolvedContext, executedTransactions, config?.allowanceStrategy);
|
|
27556
|
+
return {
|
|
27557
|
+
preparedAction: await prepareEvmSwapAction(adapter, serviceResponse, resolvedContext, config?.allowanceStrategy)
|
|
27558
|
+
};
|
|
27559
|
+
}
|
|
27560
|
+
/**
|
|
26744
27561
|
* Executes a swap transaction with the appropriate gas limit for the chain type.
|
|
26745
27562
|
*
|
|
26746
27563
|
* For EVM chains, performs a local eth_estimateGas call, applies a 1.3x safety
|
|
@@ -26789,8 +27606,8 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
|
|
|
26789
27606
|
*/ async buildFormattedFees(fees, chain, destinationChain, adapter, recipientAddress) {
|
|
26790
27607
|
if (!fees) return [];
|
|
26791
27608
|
const [providerFees, swapFees, developerFees] = await Promise.all([
|
|
26792
|
-
this.formatServiceFees(fees.provider, chain, 'provider', adapter),
|
|
26793
|
-
this.formatServiceFees(fees.swap, chain, 'swap', adapter),
|
|
27609
|
+
this.formatServiceFees(fees.provider, chain, destinationChain, 'provider', adapter),
|
|
27610
|
+
this.formatServiceFees(fees.swap, chain, destinationChain, 'swap', adapter),
|
|
26794
27611
|
recipientAddress ? this.formatDeveloperFees(fees.developer, chain, destinationChain, recipientAddress, adapter) : Promise.resolve([])
|
|
26795
27612
|
]);
|
|
26796
27613
|
return [
|
|
@@ -26800,6 +27617,45 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
|
|
|
26800
27617
|
];
|
|
26801
27618
|
}
|
|
26802
27619
|
/**
|
|
27620
|
+
* Resolve a single fee item to its display token and human-readable amount.
|
|
27621
|
+
*
|
|
27622
|
+
* @remarks
|
|
27623
|
+
* Prefer the self-describing metadata the service attaches to each fee:
|
|
27624
|
+
* `decimals` (and `symbol`) come straight from the provider quote, so they
|
|
27625
|
+
* are authoritative even for a token absent from the SDK registry on both
|
|
27626
|
+
* chains. That is the case {@link resolveFeeChain} cannot recover — a
|
|
27627
|
+
* destination-denominated fee token resolves on neither the source registry
|
|
27628
|
+
* nor the source-bound adapter, leaving the amount as raw base units. When
|
|
27629
|
+
* the service omits `decimals` (optional during rollout), fall back to
|
|
27630
|
+
* inferring the fee token's chain and resolving via the registry/adapter.
|
|
27631
|
+
*
|
|
27632
|
+
* Like {@link formatTokenValue}, this never throws: fee display is cosmetic
|
|
27633
|
+
* and must not fail an estimate/swap. A malformed self-describing `decimals`
|
|
27634
|
+
* (e.g. a non-numeric `amount` or invalid decimal count that makes
|
|
27635
|
+
* {@link formatUnits} throw) falls through to chain-based resolution rather
|
|
27636
|
+
* than propagating out of {@link buildFormattedFees}.
|
|
27637
|
+
*
|
|
27638
|
+
* @param fee - The fee item from the service response.
|
|
27639
|
+
* @param chain - The source chain definition.
|
|
27640
|
+
* @param destinationChain - The destination chain definition.
|
|
27641
|
+
* @param adapter - The adapter for on-chain decimals lookup of unregistered tokens.
|
|
27642
|
+
* @returns Promise resolving to the formatted amount and display token.
|
|
27643
|
+
*/ async formatFeeValue(fee, chain, destinationChain, adapter) {
|
|
27644
|
+
if (fee.decimals != null) {
|
|
27645
|
+
try {
|
|
27646
|
+
return {
|
|
27647
|
+
amount: formatUnits(fee.amount, fee.decimals),
|
|
27648
|
+
token: fee.symbol ?? fee.token
|
|
27649
|
+
};
|
|
27650
|
+
} catch {
|
|
27651
|
+
// Malformed service metadata — fall through to chain-based resolution,
|
|
27652
|
+
// which never throws (worst case: raw passthrough).
|
|
27653
|
+
}
|
|
27654
|
+
}
|
|
27655
|
+
const feeChain = resolveFeeChain(fee.token, chain, destinationChain);
|
|
27656
|
+
return formatTokenValue(fee.amount, fee.token, feeChain, adapter);
|
|
27657
|
+
}
|
|
27658
|
+
/**
|
|
26803
27659
|
* Format service fee items into the SDK's ServiceSwapFee structure.
|
|
26804
27660
|
*
|
|
26805
27661
|
* @remarks
|
|
@@ -26810,14 +27666,15 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
|
|
|
26810
27666
|
* - Raw passthrough only when both registry and adapter fail
|
|
26811
27667
|
*
|
|
26812
27668
|
* @param feeItems - Array of fee items from the service response.
|
|
26813
|
-
* @param chain - The chain definition for token resolution and formatting.
|
|
27669
|
+
* @param chain - The source chain definition for token resolution and formatting.
|
|
27670
|
+
* @param destinationChain - The destination chain, used to resolve fees denominated in the output token.
|
|
26814
27671
|
* @param type - The fee type to assign ('provider' or 'swap').
|
|
26815
27672
|
* @param adapter - The adapter for on-chain decimals lookup of unregistered tokens.
|
|
26816
27673
|
* @returns Promise resolving to formatted ServiceSwapFee array.
|
|
26817
|
-
*/ async formatServiceFees(feeItems, chain, type, adapter) {
|
|
27674
|
+
*/ async formatServiceFees(feeItems, chain, destinationChain, type, adapter) {
|
|
26818
27675
|
if (!feeItems) return [];
|
|
26819
27676
|
return Promise.all(feeItems.map(async (fee)=>{
|
|
26820
|
-
const formatted = await
|
|
27677
|
+
const formatted = await this.formatFeeValue(fee, chain, destinationChain, adapter);
|
|
26821
27678
|
return {
|
|
26822
27679
|
token: formatted.token,
|
|
26823
27680
|
amount: formatted.amount,
|
|
@@ -26829,16 +27686,15 @@ async function fetchSameChainStatusSnapshot({ isCrossChainSwap, txHash, chain, a
|
|
|
26829
27686
|
* Format developer fee items into the SDK's ServiceSwapFee structure.
|
|
26830
27687
|
*
|
|
26831
27688
|
* @param feeItems - Array of developer fee items from the service response.
|
|
26832
|
-
* @param chain - The chain definition for token resolution and formatting.
|
|
27689
|
+
* @param chain - The source chain definition for token resolution and formatting.
|
|
27690
|
+
* @param destinationChain - The destination chain, used to resolve fees denominated in the output token.
|
|
26833
27691
|
* @param recipientAddress - The developer's fee recipient address from config.
|
|
26834
27692
|
* @param adapter - The adapter for on-chain decimals lookup of unregistered tokens.
|
|
26835
27693
|
* @returns Promise resolving to formatted ServiceSwapFee array with developer entries.
|
|
26836
27694
|
*/ async formatDeveloperFees(feeItems, chain, destinationChain, recipientAddress, adapter) {
|
|
26837
27695
|
if (!feeItems) return [];
|
|
26838
|
-
const isCrossChainSwap = destinationChain.chain !== chain.chain;
|
|
26839
27696
|
return Promise.all(feeItems.map(async (fee)=>{
|
|
26840
|
-
const
|
|
26841
|
-
const formatted = await formatTokenValue(fee.amount, fee.token, feeChain, adapter);
|
|
27697
|
+
const formatted = await this.formatFeeValue(fee, chain, destinationChain, adapter);
|
|
26842
27698
|
return {
|
|
26843
27699
|
token: formatted.token,
|
|
26844
27700
|
amount: formatted.amount,
|
|
@@ -28878,7 +29734,7 @@ const isCrossChain = (fromChain, toChain)=>toChain !== undefined && toChain.chai
|
|
|
28878
29734
|
/**
|
|
28879
29735
|
* Resolve fee recipient address from context or policy.
|
|
28880
29736
|
* @internal
|
|
28881
|
-
*/ async function resolveFeeRecipient(context, params, existingRecipient) {
|
|
29737
|
+
*/ async function resolveFeeRecipient$1(context, params, existingRecipient) {
|
|
28882
29738
|
if (existingRecipient) {
|
|
28883
29739
|
return existingRecipient;
|
|
28884
29740
|
}
|
|
@@ -28906,7 +29762,7 @@ const isCrossChain = (fromChain, toChain)=>toChain !== undefined && toChain.chai
|
|
|
28906
29762
|
* @internal
|
|
28907
29763
|
*/ async function applyExistingFeeAmount(context, params, existingAmount, existingFeeRecipient) {
|
|
28908
29764
|
await validateFeeAmount(context, params, existingAmount);
|
|
28909
|
-
const feeRecipient = await resolveFeeRecipient(context, params, existingFeeRecipient);
|
|
29765
|
+
const feeRecipient = await resolveFeeRecipient$1(context, params, existingFeeRecipient);
|
|
28910
29766
|
if (feeRecipient) {
|
|
28911
29767
|
params.config = {
|
|
28912
29768
|
...params.config,
|
|
@@ -28940,7 +29796,7 @@ const isCrossChain = (fromChain, toChain)=>toChain !== undefined && toChain.chai
|
|
|
28940
29796
|
...params,
|
|
28941
29797
|
type: 'input'
|
|
28942
29798
|
};
|
|
28943
|
-
const feeRecipient = await resolveFeeRecipient(context, params, existingFeeRecipient);
|
|
29799
|
+
const feeRecipient = await resolveFeeRecipient$1(context, params, existingFeeRecipient);
|
|
28944
29800
|
const amount = await context.customFeePolicy?.computeFee(feeContext);
|
|
28945
29801
|
if (amount !== undefined) {
|
|
28946
29802
|
await validateInputFee(context, params, BigInt(amount), amount);
|
|
@@ -29233,22 +30089,9 @@ const transformAmount = (value, direction, decimals)=>formatUnits(value, decimal
|
|
|
29233
30089
|
try {
|
|
29234
30090
|
// Step 1: Build quote params directly (no need for buildServiceParams)
|
|
29235
30091
|
// Use chain.chain (Blockchain enum value like "World_Chain") not chain.name
|
|
30092
|
+
// The kit key is optional (permissionless mode); when absent the quote is
|
|
30093
|
+
// fetched without an Authorization header.
|
|
29236
30094
|
const kitKey = params.config?.kitKey;
|
|
29237
|
-
if (!kitKey) {
|
|
29238
|
-
throw new KitError({
|
|
29239
|
-
code: 1098,
|
|
29240
|
-
name: 'INPUT_VALIDATION_FAILED',
|
|
29241
|
-
type: 'INPUT',
|
|
29242
|
-
recoverability: 'FATAL',
|
|
29243
|
-
message: 'kitKey is required in config for callback-based fees',
|
|
29244
|
-
cause: {
|
|
29245
|
-
trace: {
|
|
29246
|
-
operation: 'handleOutputFeeCallback',
|
|
29247
|
-
params
|
|
29248
|
-
}
|
|
29249
|
-
}
|
|
29250
|
-
});
|
|
29251
|
-
}
|
|
29252
30095
|
// Resolve token aliases to addresses for the quote API
|
|
29253
30096
|
// The quote endpoint requires resolved addresses, not aliases like 'USDC'
|
|
29254
30097
|
const chain = params.from.chain;
|
|
@@ -29273,7 +30116,9 @@ const transformAmount = (value, direction, decimals)=>formatUnits(value, decimal
|
|
|
29273
30116
|
...params.config?.slippageBps !== undefined && {
|
|
29274
30117
|
slippageBps: params.config.slippageBps
|
|
29275
30118
|
},
|
|
29276
|
-
|
|
30119
|
+
...kitKey ? {
|
|
30120
|
+
apiKey: kitKey
|
|
30121
|
+
} : {}
|
|
29277
30122
|
};
|
|
29278
30123
|
// Step 2: Get quote from service
|
|
29279
30124
|
const quoteResponse = await getQuote(quoteParams);
|
|
@@ -29715,7 +30560,9 @@ const sleep$2 = async (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
|
|
|
29715
30560
|
...isCrossChain && {
|
|
29716
30561
|
toChain: chainOut
|
|
29717
30562
|
},
|
|
29718
|
-
|
|
30563
|
+
...params.kitKey ? {
|
|
30564
|
+
apiKey: params.kitKey
|
|
30565
|
+
} : {}
|
|
29719
30566
|
};
|
|
29720
30567
|
let raw = await getSwapStatus$2(request);
|
|
29721
30568
|
// When the service hasn't finished indexing a just-submitted swap it
|
|
@@ -29855,7 +30702,9 @@ const isResultShape = (params)=>'result' in params;
|
|
|
29855
30702
|
...chainOut !== undefined && {
|
|
29856
30703
|
chainOut
|
|
29857
30704
|
},
|
|
29858
|
-
|
|
30705
|
+
...params.kitKey ? {
|
|
30706
|
+
kitKey: params.kitKey
|
|
30707
|
+
} : {}
|
|
29859
30708
|
};
|
|
29860
30709
|
const deadline = Date.now() + timeoutMs;
|
|
29861
30710
|
let pollIndex = 0;
|
|
@@ -30016,7 +30865,9 @@ function resolveTokenEntry(entry, index, chain, chainDef, context) {
|
|
|
30016
30865
|
const resolvedAddresses = params.tokens?.map((entry, index)=>resolveTokenEntry(entry, index, chain, chainDef, context));
|
|
30017
30866
|
return getTokenRates$2({
|
|
30018
30867
|
chain,
|
|
30019
|
-
|
|
30868
|
+
...params.kitKey ? {
|
|
30869
|
+
apiKey: params.kitKey
|
|
30870
|
+
} : {},
|
|
30020
30871
|
...resolvedAddresses !== undefined && {
|
|
30021
30872
|
addresses: resolvedAddresses
|
|
30022
30873
|
}
|
|
@@ -31013,7 +31864,7 @@ registerKit(`${pkg$3.name}/${pkg$3.version}`);
|
|
|
31013
31864
|
};
|
|
31014
31865
|
|
|
31015
31866
|
var name$2 = "@circle-fin/earn-kit";
|
|
31016
|
-
var version$2 = "1.
|
|
31867
|
+
var version$2 = "1.3.0";
|
|
31017
31868
|
var pkg$2 = {
|
|
31018
31869
|
name: name$2,
|
|
31019
31870
|
version: version$2};
|
|
@@ -31480,7 +32331,7 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
|
|
|
31480
32331
|
*
|
|
31481
32332
|
* @param params - Adapter, chain, token/delegate/wallet addresses, the required
|
|
31482
32333
|
* allowance for the signed payload, and a revert message for on-chain failure.
|
|
31483
|
-
* @returns The approval transaction
|
|
32334
|
+
* @returns The approval transaction result when an approval was submitted, or
|
|
31484
32335
|
* `undefined` when the existing allowance already covers `requiredAllowance`
|
|
31485
32336
|
* (or `requiredAllowance` is zero).
|
|
31486
32337
|
* @throws {@link KitError} If the `token.allowance` response is malformed.
|
|
@@ -31488,7 +32339,7 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
|
|
|
31488
32339
|
*
|
|
31489
32340
|
* @example
|
|
31490
32341
|
* ```typescript
|
|
31491
|
-
* const
|
|
32342
|
+
* const approval = await approveAllowanceIfNeeded({
|
|
31492
32343
|
* adapter,
|
|
31493
32344
|
* chain,
|
|
31494
32345
|
* tokenAddress: usdcAddress,
|
|
@@ -31550,7 +32401,15 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
|
|
|
31550
32401
|
maxAttempts: params.allowancePropagation?.maxAttempts ?? DEFAULT_PROPAGATION_ATTEMPTS,
|
|
31551
32402
|
delayMs: params.allowancePropagation?.delayMs ?? DEFAULT_PROPAGATION_DELAY_MS
|
|
31552
32403
|
});
|
|
31553
|
-
return
|
|
32404
|
+
return {
|
|
32405
|
+
txHash: approvalTxHash,
|
|
32406
|
+
...approvalReceipt.gasUsed !== undefined && {
|
|
32407
|
+
gasUsed: approvalReceipt.gasUsed
|
|
32408
|
+
},
|
|
32409
|
+
...approvalReceipt.effectiveGasPrice !== undefined && {
|
|
32410
|
+
effectiveGasPrice: approvalReceipt.effectiveGasPrice
|
|
32411
|
+
}
|
|
32412
|
+
};
|
|
31554
32413
|
}
|
|
31555
32414
|
|
|
31556
32415
|
/** @internal */ function isSameAddress(actual, expected) {
|
|
@@ -31698,7 +32557,13 @@ const GAS_SAFETY_MULTIPLIER_DENOMINATOR = 10n;
|
|
|
31698
32557
|
}
|
|
31699
32558
|
return {
|
|
31700
32559
|
txHash,
|
|
31701
|
-
explorerUrl
|
|
32560
|
+
explorerUrl,
|
|
32561
|
+
...receipt.gasUsed !== undefined && {
|
|
32562
|
+
gasUsed: receipt.gasUsed
|
|
32563
|
+
},
|
|
32564
|
+
...receipt.effectiveGasPrice !== undefined && {
|
|
32565
|
+
effectiveGasPrice: receipt.effectiveGasPrice
|
|
32566
|
+
}
|
|
31702
32567
|
};
|
|
31703
32568
|
}
|
|
31704
32569
|
|
|
@@ -32010,112 +32875,6 @@ const EARN_OPERATIONS = new Set([
|
|
|
32010
32875
|
return hasEarnServiceParamsShape(operation, candidate['params']);
|
|
32011
32876
|
}
|
|
32012
32877
|
|
|
32013
|
-
function buildGasFeeBase(name, chain) {
|
|
32014
|
-
return {
|
|
32015
|
-
name,
|
|
32016
|
-
token: chain.nativeCurrency.symbol,
|
|
32017
|
-
blockchain: chain.chain
|
|
32018
|
-
};
|
|
32019
|
-
}
|
|
32020
|
-
function buildGasFeeSuccess(name, chain, fees) {
|
|
32021
|
-
return {
|
|
32022
|
-
...buildGasFeeBase(name, chain),
|
|
32023
|
-
fees
|
|
32024
|
-
};
|
|
32025
|
-
}
|
|
32026
|
-
function buildGasFeeFailure(name, chain, error) {
|
|
32027
|
-
return {
|
|
32028
|
-
...buildGasFeeBase(name, chain),
|
|
32029
|
-
fees: null,
|
|
32030
|
-
error: getErrorMessage(error)
|
|
32031
|
-
};
|
|
32032
|
-
}
|
|
32033
|
-
async function estimatePreparedGasFee(name, chain, prepared) {
|
|
32034
|
-
try {
|
|
32035
|
-
const estimate = bufferEstimatedGas(await prepared.estimate());
|
|
32036
|
-
if (estimate.gas <= 0n) {
|
|
32037
|
-
throw createValidationFailedError$1('estimate.gas', estimate.gas.toString(), 'gas estimate must be greater than zero');
|
|
32038
|
-
}
|
|
32039
|
-
return buildGasFeeSuccess(name, chain, estimate);
|
|
32040
|
-
} catch (error) {
|
|
32041
|
-
return buildGasFeeFailure(name, chain, error);
|
|
32042
|
-
}
|
|
32043
|
-
}
|
|
32044
|
-
async function estimateApprovalGasFeeIfNeeded(params) {
|
|
32045
|
-
const { adapter, chain, address, tokenAddress, delegate, requiredAllowance } = params;
|
|
32046
|
-
if (requiredAllowance <= 0n) {
|
|
32047
|
-
return undefined;
|
|
32048
|
-
}
|
|
32049
|
-
try {
|
|
32050
|
-
const allowancePrepared = await adapter.prepareAction('token.allowance', {
|
|
32051
|
-
tokenAddress,
|
|
32052
|
-
delegate
|
|
32053
|
-
}, {
|
|
32054
|
-
chain,
|
|
32055
|
-
address
|
|
32056
|
-
});
|
|
32057
|
-
const allowanceRaw = await allowancePrepared.execute();
|
|
32058
|
-
const currentAllowance = parseAllowanceResponse(allowanceRaw);
|
|
32059
|
-
if (currentAllowance >= requiredAllowance) {
|
|
32060
|
-
return undefined;
|
|
32061
|
-
}
|
|
32062
|
-
// Reuse the execute path's approval builder so the estimate simulates the
|
|
32063
|
-
// exact approval (action, amount, and WARM_SLOT_RESIDUAL) that
|
|
32064
|
-
// approveAllowanceIfNeeded later submits.
|
|
32065
|
-
const approvalPrepared = await prepareApprovalAction({
|
|
32066
|
-
adapter,
|
|
32067
|
-
chain,
|
|
32068
|
-
address,
|
|
32069
|
-
tokenAddress,
|
|
32070
|
-
delegate,
|
|
32071
|
-
currentAllowance,
|
|
32072
|
-
requiredAllowance
|
|
32073
|
-
});
|
|
32074
|
-
return await estimatePreparedGasFee('Approve', chain, approvalPrepared);
|
|
32075
|
-
} catch (error) {
|
|
32076
|
-
return buildGasFeeFailure('Approve', chain, error);
|
|
32077
|
-
}
|
|
32078
|
-
}
|
|
32079
|
-
/**
|
|
32080
|
-
* Estimate gas fee entries for an earn quote without submitting transactions.
|
|
32081
|
-
*
|
|
32082
|
-
* Each entry is produced by simulating the prepared transaction against
|
|
32083
|
-
* current chain state. When an approval is required (allowance below the
|
|
32084
|
-
* signed payload's required amount), the subsequent action simulation runs
|
|
32085
|
-
* without that approval in place and is expected to revert — the action entry
|
|
32086
|
-
* then carries `fees: null` with the revert message while the approval entry
|
|
32087
|
-
* still estimates normally. Quote consumers must treat that as "estimate
|
|
32088
|
-
* pending approval", not a hard failure.
|
|
32089
|
-
*
|
|
32090
|
-
* @internal
|
|
32091
|
-
*/ async function estimateEarnQuoteGasFees(params) {
|
|
32092
|
-
const { adapter, chain, address, actionName, actionKey, actionParams, approval } = params;
|
|
32093
|
-
const gasFees = [];
|
|
32094
|
-
if (approval !== undefined) {
|
|
32095
|
-
const approvalEstimate = await estimateApprovalGasFeeIfNeeded({
|
|
32096
|
-
adapter,
|
|
32097
|
-
chain,
|
|
32098
|
-
address,
|
|
32099
|
-
tokenAddress: approval.token,
|
|
32100
|
-
delegate: approval.delegate,
|
|
32101
|
-
requiredAllowance: approval.requiredAllowance
|
|
32102
|
-
});
|
|
32103
|
-
if (approvalEstimate !== undefined) {
|
|
32104
|
-
gasFees.push(approvalEstimate);
|
|
32105
|
-
}
|
|
32106
|
-
}
|
|
32107
|
-
try {
|
|
32108
|
-
const actionPrepared = await adapter.prepareAction(actionKey, actionParams, {
|
|
32109
|
-
chain,
|
|
32110
|
-
address
|
|
32111
|
-
});
|
|
32112
|
-
gasFees.push(await estimatePreparedGasFee(actionName, chain, actionPrepared));
|
|
32113
|
-
} catch (error) {
|
|
32114
|
-
gasFees.push(buildGasFeeFailure(actionName, chain, error));
|
|
32115
|
-
}
|
|
32116
|
-
return gasFees;
|
|
32117
|
-
}
|
|
32118
|
-
|
|
32119
32878
|
// ---------------------------------------------------------------------------
|
|
32120
32879
|
// Shared primitives
|
|
32121
32880
|
// ---------------------------------------------------------------------------
|
|
@@ -32193,7 +32952,11 @@ const bridgeFeeTokenSchema = hexAddressSchema;
|
|
|
32193
32952
|
asset: z.string(),
|
|
32194
32953
|
assetAddress: z.string(),
|
|
32195
32954
|
lltv: z.number(),
|
|
32196
|
-
supplyUsd: z.number()
|
|
32955
|
+
supplyUsd: z.number(),
|
|
32956
|
+
// Optional during the expand/contract window (a backend that predates the
|
|
32957
|
+
// field omits the key), mirroring the `.optional()` facets on the base
|
|
32958
|
+
// schema; `null` when the product exposes no per-market allocation (V2).
|
|
32959
|
+
allocationPct: z.number().nullable().optional()
|
|
32197
32960
|
});
|
|
32198
32961
|
/**
|
|
32199
32962
|
* Zod schema for a Morpho vault warning in the API response.
|
|
@@ -32207,7 +32970,74 @@ const bridgeFeeTokenSchema = hexAddressSchema;
|
|
|
32207
32970
|
])
|
|
32208
32971
|
});
|
|
32209
32972
|
/**
|
|
32210
|
-
* Zod schema for
|
|
32973
|
+
* Zod schema for the manager (curator) facet in the API response.
|
|
32974
|
+
*
|
|
32975
|
+
* @internal
|
|
32976
|
+
*/ const managerSchema = z.object({
|
|
32977
|
+
name: z.string(),
|
|
32978
|
+
address: z.string().optional(),
|
|
32979
|
+
// Only 'curator' is emitted today (Morpho V1/V2). Additional manager roles
|
|
32980
|
+
// are added here as the providers that emit them land, rather than shipped
|
|
32981
|
+
// speculatively.
|
|
32982
|
+
type: z.enum([
|
|
32983
|
+
'curator'
|
|
32984
|
+
])
|
|
32985
|
+
});
|
|
32986
|
+
/**
|
|
32987
|
+
* Zod schema for the APY profile facet in the API response.
|
|
32988
|
+
*
|
|
32989
|
+
* @internal
|
|
32990
|
+
*/ const apyProfileSchema = z.object({
|
|
32991
|
+
current: z.number(),
|
|
32992
|
+
native: z.number().nullable(),
|
|
32993
|
+
d7: z.number().nullable(),
|
|
32994
|
+
d30: z.number().nullable(),
|
|
32995
|
+
d90: z.number().nullable(),
|
|
32996
|
+
rewardShare: z.number().nullable(),
|
|
32997
|
+
source: z.string().optional(),
|
|
32998
|
+
asOf: z.string().optional()
|
|
32999
|
+
});
|
|
33000
|
+
/**
|
|
33001
|
+
* Zod schema for the fee split facet in the API response.
|
|
33002
|
+
*
|
|
33003
|
+
* @internal
|
|
33004
|
+
*/ const feeInfoSchema = z.object({
|
|
33005
|
+
performance: z.number().nullable(),
|
|
33006
|
+
management: z.number().nullable()
|
|
33007
|
+
});
|
|
33008
|
+
/**
|
|
33009
|
+
* Zod schema for the liquidity profile facet in the API response.
|
|
33010
|
+
*
|
|
33011
|
+
* `totalSupply` is the outstanding vault share tokens (ERC4626 totalSupply);
|
|
33012
|
+
* it is validated as a raw JSON amount, like `totalDeposits`/`available`.
|
|
33013
|
+
*
|
|
33014
|
+
* @internal
|
|
33015
|
+
*/ const liquidityProfileSchema = z.object({
|
|
33016
|
+
totalDeposits: amountJsonSchema,
|
|
33017
|
+
available: amountJsonSchema,
|
|
33018
|
+
totalSupply: amountJsonSchema,
|
|
33019
|
+
status: z.enum([
|
|
33020
|
+
'active',
|
|
33021
|
+
'low_liquidity'
|
|
33022
|
+
])
|
|
33023
|
+
});
|
|
33024
|
+
/**
|
|
33025
|
+
* Zod schema for the risk signals facet in the API response.
|
|
33026
|
+
*
|
|
33027
|
+
* @internal
|
|
33028
|
+
*/ const riskSignalsSchema = z.object({
|
|
33029
|
+
circleSentinel: z.boolean(),
|
|
33030
|
+
warnings: z.array(vaultWarningSchema).optional(),
|
|
33031
|
+
earnKitWarnings: z.array(z.string()).optional()
|
|
33032
|
+
});
|
|
33033
|
+
/**
|
|
33034
|
+
* Zod schema for the universal earn-opportunity base in the API response.
|
|
33035
|
+
*
|
|
33036
|
+
* Retains every existing deprecated flat field (kept validated through the
|
|
33037
|
+
* expand/contract window so default-strip does not drop them) and adds the
|
|
33038
|
+
* new nested facets. The nested facets are `.optional()` during the
|
|
33039
|
+
* transition so the SDK still validates against a not-yet-fully-deployed
|
|
33040
|
+
* backend; they become required after Expand ships.
|
|
32211
33041
|
*
|
|
32212
33042
|
* @internal
|
|
32213
33043
|
*/ const vaultInfoResponseSchema = z.object({
|
|
@@ -32232,6 +33062,96 @@ const bridgeFeeTokenSchema = hexAddressSchema;
|
|
|
32232
33062
|
warnings: z.array(vaultWarningSchema).optional(),
|
|
32233
33063
|
earnKitWarnings: z.array(z.string()).optional()
|
|
32234
33064
|
});
|
|
33065
|
+
/**
|
|
33066
|
+
* Shared base schema: existing flat fields (kept) plus the new nested
|
|
33067
|
+
* facets and neutral identity. Facets are `.optional()` during the
|
|
33068
|
+
* transition; flip to required once the backend is confirmed emitting.
|
|
33069
|
+
*
|
|
33070
|
+
* @internal
|
|
33071
|
+
*/ const earnBaseSchema = vaultInfoResponseSchema.extend({
|
|
33072
|
+
address: z.string().optional(),
|
|
33073
|
+
asOf: z.string().optional(),
|
|
33074
|
+
manager: managerSchema.nullable().optional(),
|
|
33075
|
+
apyProfile: apyProfileSchema.optional(),
|
|
33076
|
+
fee: feeInfoSchema.optional(),
|
|
33077
|
+
liquidityProfile: liquidityProfileSchema.optional(),
|
|
33078
|
+
riskSignals: riskSignalsSchema.optional()
|
|
33079
|
+
});
|
|
33080
|
+
/**
|
|
33081
|
+
* Zod schema for the `vault` opportunity variant.
|
|
33082
|
+
*
|
|
33083
|
+
* @internal
|
|
33084
|
+
*/ const vaultOpportunitySchema = earnBaseSchema.extend({
|
|
33085
|
+
productType: z.literal('vault'),
|
|
33086
|
+
collateral: z.array(collateralSchema)
|
|
33087
|
+
});
|
|
33088
|
+
/**
|
|
33089
|
+
* Discriminated union over `productType`. Add union members here as new
|
|
33090
|
+
* product types (e.g. `lending_market`, `rwa_token`) land.
|
|
33091
|
+
*
|
|
33092
|
+
* @internal
|
|
33093
|
+
*/ const earnOpportunityVariants = [
|
|
33094
|
+
vaultOpportunitySchema
|
|
33095
|
+
];
|
|
33096
|
+
/** @internal */ const earnOpportunitySchema = z.discriminatedUnion('productType', earnOpportunityVariants);
|
|
33097
|
+
/** Product types this SDK version knows how to parse. */ const knownProductTypes = new Set(earnOpportunityVariants.map((variant)=>variant.shape.productType.value));
|
|
33098
|
+
/**
|
|
33099
|
+
* Tolerant list parser for earn opportunities.
|
|
33100
|
+
*
|
|
33101
|
+
* `z.discriminatedUnion` throws on an unrecognized discriminant and
|
|
33102
|
+
* `z.array` fails the whole array if any element fails. Two migration-window
|
|
33103
|
+
* cases are smoothed over here so neither breaks an already-shipped SDK:
|
|
33104
|
+
*
|
|
33105
|
+
* - A backend that predates `productType` omits it entirely. `'vault'` was the
|
|
33106
|
+
* only opportunity type then, so default a missing discriminant to `'vault'`
|
|
33107
|
+
* rather than dropping every vault the backend returns.
|
|
33108
|
+
* - A future backend adds a *second* `productType` this SDK version does not
|
|
33109
|
+
* know. Drop those elements (a present-but-unrecognized discriminant) instead
|
|
33110
|
+
* of rejecting the whole list.
|
|
33111
|
+
*
|
|
33112
|
+
* Only the drop above is a *tolerant* case. Anything that is not a plain object
|
|
33113
|
+
* with a present-but-unknown string `productType` — `null`, `undefined`,
|
|
33114
|
+
* primitives, or an object whose `productType` is malformed — is passed through
|
|
33115
|
+
* untouched so `z.array(earnOpportunitySchema)` reports it as a normal
|
|
33116
|
+
* validation failure. It is deliberately not silently dropped (which would hide
|
|
33117
|
+
* malformed backend data) and never throws here (an unguarded property read on
|
|
33118
|
+
* a non-object would escape `safeParse` as a raw `TypeError` instead of a
|
|
33119
|
+
* `ZodError`).
|
|
33120
|
+
*
|
|
33121
|
+
* @internal
|
|
33122
|
+
*/ const earnOpportunityListSchema = z.preprocess((raw)=>{
|
|
33123
|
+
if (!Array.isArray(raw)) {
|
|
33124
|
+
return raw;
|
|
33125
|
+
}
|
|
33126
|
+
// Array.isArray narrows `raw` to `any[]`; view it as `unknown[]` so the
|
|
33127
|
+
// map/filter chain stays type-safe and no `any` leaks into the return.
|
|
33128
|
+
const entries = raw;
|
|
33129
|
+
return entries.map((entry)=>{
|
|
33130
|
+
// Only touch plain objects; non-objects fall through to fail validation.
|
|
33131
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
33132
|
+
return entry;
|
|
33133
|
+
}
|
|
33134
|
+
const record = entry;
|
|
33135
|
+
// Older backend predating productType: default to the only type then.
|
|
33136
|
+
return record.productType === undefined ? {
|
|
33137
|
+
...record,
|
|
33138
|
+
productType: 'vault'
|
|
33139
|
+
} : record;
|
|
33140
|
+
}).filter((entry)=>{
|
|
33141
|
+
// Drop ONLY a present-but-unknown string discriminant (a future
|
|
33142
|
+
// productType this SDK version doesn't know). Everything else —
|
|
33143
|
+
// non-objects, a non-string productType — flows through to
|
|
33144
|
+
// z.array(earnOpportunitySchema) and fails/passes validation normally.
|
|
33145
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
33146
|
+
return true;
|
|
33147
|
+
}
|
|
33148
|
+
const productType = entry.productType;
|
|
33149
|
+
if (typeof productType !== 'string') {
|
|
33150
|
+
return true;
|
|
33151
|
+
}
|
|
33152
|
+
return knownProductTypes.has(productType);
|
|
33153
|
+
});
|
|
33154
|
+
}, z.array(earnOpportunitySchema));
|
|
32235
33155
|
// ---------------------------------------------------------------------------
|
|
32236
33156
|
// Position response schema
|
|
32237
33157
|
// ---------------------------------------------------------------------------
|
|
@@ -32361,6 +33281,7 @@ const positionPnlSchema = z.discriminatedUnion('status', [
|
|
|
32361
33281
|
*
|
|
32362
33282
|
* @internal
|
|
32363
33283
|
*/ const depositPayloadSchema = z.object({
|
|
33284
|
+
execId: bridgeDepositExecIdSchema,
|
|
32364
33285
|
executionParams: depositExecutionParamsSchema,
|
|
32365
33286
|
signature: hexSignatureSchema
|
|
32366
33287
|
});
|
|
@@ -32452,6 +33373,21 @@ const bridgeDepositPrepareReviewSchema = z.object({
|
|
|
32452
33373
|
amount: amountJsonSchema,
|
|
32453
33374
|
vaultAddress: hexAddressSchema
|
|
32454
33375
|
}).passthrough();
|
|
33376
|
+
/** @internal */ const bridgeQuoteExpirySchema = z.discriminatedUnion('mode', [
|
|
33377
|
+
z.object({
|
|
33378
|
+
mode: z.literal('TIMESTAMP'),
|
|
33379
|
+
expiresAt: z.string().datetime({
|
|
33380
|
+
offset: true
|
|
33381
|
+
})
|
|
33382
|
+
}),
|
|
33383
|
+
z.object({
|
|
33384
|
+
mode: z.literal('BLOCK_NUMBER'),
|
|
33385
|
+
expiresAtBlock: z.number().int(),
|
|
33386
|
+
blockEstimatedAt: z.string().datetime({
|
|
33387
|
+
offset: true
|
|
33388
|
+
}).optional()
|
|
33389
|
+
})
|
|
33390
|
+
]).optional().catch(undefined);
|
|
32455
33391
|
/**
|
|
32456
33392
|
* Zod schema for the bridge deposit prepare payload.
|
|
32457
33393
|
*
|
|
@@ -32463,6 +33399,10 @@ const bridgeDepositPrepareReviewSchema = z.object({
|
|
|
32463
33399
|
execId: bridgeDepositExecIdSchema,
|
|
32464
33400
|
erc3009TypedData: bridgeDepositPreparedBundleSchema,
|
|
32465
33401
|
expiresAt: z.string().datetime(),
|
|
33402
|
+
quoteIssuedAt: z.string().datetime({
|
|
33403
|
+
offset: true
|
|
33404
|
+
}).optional().catch(undefined),
|
|
33405
|
+
quoteExpiry: bridgeQuoteExpirySchema,
|
|
32466
33406
|
review: bridgeDepositPrepareReviewSchema
|
|
32467
33407
|
});
|
|
32468
33408
|
/**
|
|
@@ -32528,6 +33468,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
|
|
|
32528
33468
|
*
|
|
32529
33469
|
* @internal
|
|
32530
33470
|
*/ const withdrawPayloadSchema = z.object({
|
|
33471
|
+
execId: bridgeDepositExecIdSchema,
|
|
32531
33472
|
executionParams: withdrawExecutionParamsSchema,
|
|
32532
33473
|
signature: hexSignatureSchema
|
|
32533
33474
|
});
|
|
@@ -32541,6 +33482,27 @@ const bridgeDepositPrepareReviewSchema = z.object({
|
|
|
32541
33482
|
data: withdrawPayloadSchema
|
|
32542
33483
|
});
|
|
32543
33484
|
// ---------------------------------------------------------------------------
|
|
33485
|
+
// Transaction report response schema
|
|
33486
|
+
// ---------------------------------------------------------------------------
|
|
33487
|
+
/**
|
|
33488
|
+
* Zod schema for the transaction report payload inside the API `data` envelope.
|
|
33489
|
+
*
|
|
33490
|
+
* The Earn Service returns an empty payload (`{"data":{}}`) on success, so the
|
|
33491
|
+
* schema accepts any object shape and does not require specific fields.
|
|
33492
|
+
*
|
|
33493
|
+
* @internal
|
|
33494
|
+
*/ const transactionReportPayloadSchema = z.object({}).passthrough();
|
|
33495
|
+
/**
|
|
33496
|
+
* Zod schema for the `POST /v1/earnKit/transactions/report` API response.
|
|
33497
|
+
*
|
|
33498
|
+
* The Earn Service API wraps the transaction report payload in a `data`
|
|
33499
|
+
* envelope.
|
|
33500
|
+
*
|
|
33501
|
+
* @internal
|
|
33502
|
+
*/ const transactionReportResponseSchema = z.object({
|
|
33503
|
+
data: transactionReportPayloadSchema
|
|
33504
|
+
});
|
|
33505
|
+
// ---------------------------------------------------------------------------
|
|
32544
33506
|
// Claim rewards response schema
|
|
32545
33507
|
// ---------------------------------------------------------------------------
|
|
32546
33508
|
/**
|
|
@@ -32601,6 +33563,30 @@ const bridgeDepositPrepareReviewSchema = z.object({
|
|
|
32601
33563
|
token: z.string(),
|
|
32602
33564
|
amount: amountJsonSchema
|
|
32603
33565
|
});
|
|
33566
|
+
/**
|
|
33567
|
+
* Zod schema for a native gas-fee entry in an EarnKit quote response.
|
|
33568
|
+
*
|
|
33569
|
+
* The Earn Service backend estimates gas server-side and returns one entry per
|
|
33570
|
+
* action (`Approve`, `Deposit`, `Withdraw`). A successful estimate carries
|
|
33571
|
+
* `fees` in the SDK `EstimatedGas` shape (`{ gas, gasPrice, fee }`), each a raw
|
|
33572
|
+
* integer string in the chain's native base units. When the backend cannot
|
|
33573
|
+
* estimate an action it returns `fees: null` with an `error` message instead.
|
|
33574
|
+
*
|
|
33575
|
+
* The schema deliberately validates almost nothing beyond the envelope: `name`
|
|
33576
|
+
* is optional and `fees` is entirely unvalidated (`unknown`). ALL validation
|
|
33577
|
+
* of `fees` — that it is an object at all, and that `gas`, `gasPrice`, and
|
|
33578
|
+
* `fee` are parseable integer strings — is deferred to {@link toQuoteGasFees},
|
|
33579
|
+
* which degrades a malformed entry to a `fees: null` soft failure. This is
|
|
33580
|
+
* intentional: gas is best-effort, so a single unparseable gas entry (a wrong
|
|
33581
|
+
* type such as `fees: 123` or `fees: 'bad'`, a missing field, or a non-numeric
|
|
33582
|
+
* `fee`) must never fail Zod validation and reject the entire quote.
|
|
33583
|
+
*
|
|
33584
|
+
* @internal
|
|
33585
|
+
*/ const quoteGasFeeSchema = z.object({
|
|
33586
|
+
name: z.string().optional(),
|
|
33587
|
+
fees: z.unknown(),
|
|
33588
|
+
error: z.string().optional()
|
|
33589
|
+
}).passthrough();
|
|
32604
33590
|
/**
|
|
32605
33591
|
* Zod schema for the inner deposit quote payload.
|
|
32606
33592
|
*
|
|
@@ -32616,7 +33602,8 @@ const bridgeDepositPrepareReviewSchema = z.object({
|
|
|
32616
33602
|
expectedShares: amountJsonSchema,
|
|
32617
33603
|
sharePrice: z.string(),
|
|
32618
33604
|
currentApy: z.number(),
|
|
32619
|
-
fees: z.array(feeSchema).optional()
|
|
33605
|
+
fees: z.array(feeSchema).optional(),
|
|
33606
|
+
gasFees: z.array(quoteGasFeeSchema).optional()
|
|
32620
33607
|
});
|
|
32621
33608
|
/**
|
|
32622
33609
|
* Zod schema for the `POST /v1/earnKit/deposit/quote` API response.
|
|
@@ -32643,6 +33630,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
|
|
|
32643
33630
|
sharePrice: z.string(),
|
|
32644
33631
|
maxWithdrawable: amountJsonSchema,
|
|
32645
33632
|
fees: z.array(feeSchema),
|
|
33633
|
+
gasFees: z.array(quoteGasFeeSchema).optional(),
|
|
32646
33634
|
warnings: z.array(z.string()).optional()
|
|
32647
33635
|
});
|
|
32648
33636
|
/**
|
|
@@ -32700,7 +33688,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
|
|
|
32700
33688
|
*
|
|
32701
33689
|
* @internal
|
|
32702
33690
|
*/ const getVaultsPayloadSchema = z.object({
|
|
32703
|
-
vaults:
|
|
33691
|
+
vaults: earnOpportunityListSchema,
|
|
32704
33692
|
errors: z.array(vaultErrorSchema)
|
|
32705
33693
|
});
|
|
32706
33694
|
/**
|
|
@@ -32730,7 +33718,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
|
|
|
32730
33718
|
*
|
|
32731
33719
|
* @internal
|
|
32732
33720
|
*/ const exploreVaultsPayloadSchema = z.object({
|
|
32733
|
-
vaults:
|
|
33721
|
+
vaults: earnOpportunityListSchema,
|
|
32734
33722
|
pagination: explorePaginationSchema
|
|
32735
33723
|
});
|
|
32736
33724
|
/**
|
|
@@ -32823,6 +33811,16 @@ const bridgeDepositPrepareReviewSchema = z.object({
|
|
|
32823
33811
|
*/ function isWithdrawResponse(value) {
|
|
32824
33812
|
return withdrawResponseSchema.safeParse(value).success;
|
|
32825
33813
|
}
|
|
33814
|
+
/**
|
|
33815
|
+
* Type guard for the transaction report API response.
|
|
33816
|
+
*
|
|
33817
|
+
* @param value - Unknown response value to validate
|
|
33818
|
+
* @returns True when the value matches the transaction report response shape
|
|
33819
|
+
*
|
|
33820
|
+
* @internal
|
|
33821
|
+
*/ function isTransactionReportResponse(value) {
|
|
33822
|
+
return transactionReportResponseSchema.safeParse(value).success;
|
|
33823
|
+
}
|
|
32826
33824
|
/**
|
|
32827
33825
|
* Type guard for the claim rewards API response.
|
|
32828
33826
|
*
|
|
@@ -32865,7 +33863,7 @@ const bridgeDepositPrepareReviewSchema = z.object({
|
|
|
32865
33863
|
}
|
|
32866
33864
|
|
|
32867
33865
|
var name$1 = "@circle-fin/provider-earn-service";
|
|
32868
|
-
var version$1 = "1.
|
|
33866
|
+
var version$1 = "1.3.0";
|
|
32869
33867
|
var pkg$1 = {
|
|
32870
33868
|
name: name$1,
|
|
32871
33869
|
version: version$1};
|
|
@@ -32985,7 +33983,7 @@ var pkg$1 = {
|
|
|
32985
33983
|
}
|
|
32986
33984
|
|
|
32987
33985
|
/**
|
|
32988
|
-
* Convert an API vault info object into the SDK {@link
|
|
33986
|
+
* Convert an API vault info object into the SDK {@link EarnOpportunity} shape.
|
|
32989
33987
|
*
|
|
32990
33988
|
* Map the API chain code back to the SDK chain identifier and hydrate the
|
|
32991
33989
|
* amount payloads into {@link Amount} instances.
|
|
@@ -32996,16 +33994,29 @@ var pkg$1 = {
|
|
|
32996
33994
|
*
|
|
32997
33995
|
* @internal
|
|
32998
33996
|
*/ function toVaultInfo(data) {
|
|
32999
|
-
const { totalDeposits, liquidity, ...vault } = data;
|
|
33997
|
+
const { totalDeposits, liquidity, liquidityProfile, ...vault } = data;
|
|
33000
33998
|
const chain = toSdkChain(vault.chain);
|
|
33001
33999
|
if (chain === undefined) {
|
|
33002
34000
|
throw createInvalidChainError(vault.chain, 'Chain returned by the Earn Service is not supported by the SDK');
|
|
33003
34001
|
}
|
|
34002
|
+
// The nested facets are `.optional()` in the schema (a backend that predates
|
|
34003
|
+
// them omits them) and are typed optional on `EarnOpportunity` to match.
|
|
34004
|
+
// Convert the nested liquidity amounts when present and pass the remaining
|
|
34005
|
+
// facets straight through; each absent facet stays absent rather than being
|
|
34006
|
+
// asserted present by a cast.
|
|
33004
34007
|
return {
|
|
33005
34008
|
...vault,
|
|
33006
34009
|
chain,
|
|
33007
34010
|
totalDeposits: Amount.fromJSON(totalDeposits),
|
|
33008
|
-
liquidity: Amount.fromJSON(liquidity)
|
|
34011
|
+
liquidity: Amount.fromJSON(liquidity),
|
|
34012
|
+
...liquidityProfile !== undefined && {
|
|
34013
|
+
liquidityProfile: {
|
|
34014
|
+
...liquidityProfile,
|
|
34015
|
+
totalDeposits: Amount.fromJSON(liquidityProfile.totalDeposits),
|
|
34016
|
+
available: Amount.fromJSON(liquidityProfile.available),
|
|
34017
|
+
totalSupply: Amount.fromJSON(liquidityProfile.totalSupply)
|
|
34018
|
+
}
|
|
34019
|
+
}
|
|
33009
34020
|
};
|
|
33010
34021
|
}
|
|
33011
34022
|
|
|
@@ -33040,8 +34051,11 @@ function toVaultError(error) {
|
|
|
33040
34051
|
}
|
|
33041
34052
|
try {
|
|
33042
34053
|
const response = await pollApiGet(url.toString(), isGetVaultsResponse, pollingConfig);
|
|
34054
|
+
// `pollApiGet` validates via a boolean guard and returns the raw JSON — it
|
|
34055
|
+
// does not run the schema's preprocess. Parse explicitly so unknown
|
|
34056
|
+
// `productType` values are dropped before `toVaultInfo`.
|
|
33043
34057
|
return {
|
|
33044
|
-
vaults: response.data.vaults.map(toVaultInfo),
|
|
34058
|
+
vaults: earnOpportunityListSchema.parse(response.data.vaults).map(toVaultInfo),
|
|
33045
34059
|
errors: response.data.errors.map(toVaultError)
|
|
33046
34060
|
};
|
|
33047
34061
|
} catch (error) {
|
|
@@ -33090,8 +34104,11 @@ function toVaultError(error) {
|
|
|
33090
34104
|
}
|
|
33091
34105
|
try {
|
|
33092
34106
|
const response = await pollApiGet(url.toString(), isExploreVaultsResponse, pollingConfig);
|
|
34107
|
+
// `pollApiGet` validates via a boolean guard and returns the raw JSON — it
|
|
34108
|
+
// does not run the schema's preprocess. Parse explicitly so unknown
|
|
34109
|
+
// `productType` values are dropped before `toVaultInfo`.
|
|
33093
34110
|
return {
|
|
33094
|
-
vaults: response.data.vaults.map(toVaultInfo),
|
|
34111
|
+
vaults: earnOpportunityListSchema.parse(response.data.vaults).map(toVaultInfo),
|
|
33095
34112
|
pagination: response.data.pagination
|
|
33096
34113
|
};
|
|
33097
34114
|
} catch (error) {
|
|
@@ -33255,6 +34272,12 @@ function toPositionInfo(data) {
|
|
|
33255
34272
|
execId: response.data.execId,
|
|
33256
34273
|
preparedBundle,
|
|
33257
34274
|
expiresAt: response.data.expiresAt,
|
|
34275
|
+
...response.data.quoteIssuedAt !== undefined && {
|
|
34276
|
+
quoteIssuedAt: response.data.quoteIssuedAt
|
|
34277
|
+
},
|
|
34278
|
+
...response.data.quoteExpiry !== undefined && {
|
|
34279
|
+
quoteExpiry: response.data.quoteExpiry
|
|
34280
|
+
},
|
|
33258
34281
|
review: response.data.review
|
|
33259
34282
|
};
|
|
33260
34283
|
} catch (error) {
|
|
@@ -33596,7 +34619,110 @@ function toClaimedAmount(reward) {
|
|
|
33596
34619
|
}
|
|
33597
34620
|
}
|
|
33598
34621
|
|
|
33599
|
-
|
|
34622
|
+
/**
|
|
34623
|
+
* Map the Earn Service's server-side quote gas estimates into the SDK
|
|
34624
|
+
* {@link EarnGasFeeEstimate} shape.
|
|
34625
|
+
*
|
|
34626
|
+
* The Earn Service estimates gas for each action (`Approve`, `Deposit`,
|
|
34627
|
+
* `Withdraw`) and returns `{ name, fees: { gas, gasPrice, fee } }` with raw
|
|
34628
|
+
* integer strings.
|
|
34629
|
+
* The SDK type additionally carries `token` (the chain's native currency
|
|
34630
|
+
* symbol) and `blockchain`, which are filled in here from the chain
|
|
34631
|
+
* definition.
|
|
34632
|
+
*
|
|
34633
|
+
* Gas reporting is best-effort: a malformed entry (e.g. a non-integer string
|
|
34634
|
+
* that fails `BigInt` parsing) degrades to a `{ fees: null, error }` estimate
|
|
34635
|
+
* rather than throwing, so one bad entry never fails the whole quote.
|
|
34636
|
+
*
|
|
34637
|
+
* @param gasFees - Backend gas-fee entries from the quote response, if any.
|
|
34638
|
+
* @param chain - Chain definition, used for the native token symbol and
|
|
34639
|
+
* blockchain identifier.
|
|
34640
|
+
* @returns One {@link EarnGasFeeEstimate} per backend entry (empty when the
|
|
34641
|
+
* backend returned none).
|
|
34642
|
+
*
|
|
34643
|
+
* @example
|
|
34644
|
+
* ```typescript
|
|
34645
|
+
* toQuoteGasFees(
|
|
34646
|
+
* [{ name: 'Deposit', fees: { gas: '364142', gasPrice: '21000000000', fee: '7646982000000000' } }],
|
|
34647
|
+
* arcTestnet,
|
|
34648
|
+
* )
|
|
34649
|
+
* // [{ name: 'Deposit', token: 'USDC', blockchain: 'Arc_Testnet',
|
|
34650
|
+
* // fees: { gas: 364142n, gasPrice: 21000000000n, fee: '7646982000000000' } }]
|
|
34651
|
+
* ```
|
|
34652
|
+
*
|
|
34653
|
+
* @internal
|
|
34654
|
+
*/ function toQuoteGasFees(gasFees, chain) {
|
|
34655
|
+
if (gasFees === undefined) {
|
|
34656
|
+
return [];
|
|
34657
|
+
}
|
|
34658
|
+
return gasFees.map((entry)=>{
|
|
34659
|
+
const base = {
|
|
34660
|
+
// `name` is optional on the wire; label an unnamed entry rather than
|
|
34661
|
+
// emitting `name: undefined`.
|
|
34662
|
+
name: entry.name ?? 'Unknown',
|
|
34663
|
+
token: chain.nativeCurrency.symbol,
|
|
34664
|
+
blockchain: chain.chain
|
|
34665
|
+
};
|
|
34666
|
+
// The Earn Service itself reports a failed estimate as `fees: null` with
|
|
34667
|
+
// an error; propagate that soft failure verbatim.
|
|
34668
|
+
if (entry.fees === null || entry.fees === undefined) {
|
|
34669
|
+
return {
|
|
34670
|
+
...base,
|
|
34671
|
+
fees: null,
|
|
34672
|
+
error: entry.error ?? 'gas estimate unavailable'
|
|
34673
|
+
};
|
|
34674
|
+
}
|
|
34675
|
+
// `fees` is `unknown` at the schema layer, so ALL validation happens here:
|
|
34676
|
+
// that it is an object at all, and that `gas`, `gasPrice`, and `fee` are
|
|
34677
|
+
// each parseable integer strings (including `fee`, which the SDK contract
|
|
34678
|
+
// requires be a numeric base-unit string). Any failure — a wrong type
|
|
34679
|
+
// (`fees: 123`), a missing field, or a non-numeric value — degrades the
|
|
34680
|
+
// whole entry to a `fees: null` soft failure rather than surfacing a
|
|
34681
|
+
// malformed "successful" estimate or rejecting the quote.
|
|
34682
|
+
try {
|
|
34683
|
+
if (typeof entry.fees !== 'object') {
|
|
34684
|
+
throw new TypeError(`gas fees must be an object (got ${typeof entry.fees})`);
|
|
34685
|
+
}
|
|
34686
|
+
const { gas, gasPrice, fee } = entry.fees;
|
|
34687
|
+
return {
|
|
34688
|
+
...base,
|
|
34689
|
+
fees: {
|
|
34690
|
+
gas: toBigInt('gas', gas),
|
|
34691
|
+
gasPrice: toBigInt('gasPrice', gasPrice),
|
|
34692
|
+
fee: toBigInt('fee', fee).toString()
|
|
34693
|
+
}
|
|
34694
|
+
};
|
|
34695
|
+
} catch (error) {
|
|
34696
|
+
return {
|
|
34697
|
+
...base,
|
|
34698
|
+
fees: null,
|
|
34699
|
+
error: getErrorMessage(error)
|
|
34700
|
+
};
|
|
34701
|
+
}
|
|
34702
|
+
});
|
|
34703
|
+
}
|
|
34704
|
+
/**
|
|
34705
|
+
* Parse an unknown value into a `bigint`, rejecting anything that is not a
|
|
34706
|
+
* non-empty integer string. `BigInt` alone is too permissive for this path —
|
|
34707
|
+
* it accepts numbers, booleans, and empty strings — so guard the type first.
|
|
34708
|
+
*
|
|
34709
|
+
* @param field - Field name, used in the thrown error message.
|
|
34710
|
+
* @param value - Raw value from the backend gas entry.
|
|
34711
|
+
* @returns The parsed `bigint`.
|
|
34712
|
+
* @throws {TypeError} When `value` is not a non-empty integer string.
|
|
34713
|
+
*/ function toBigInt(field, value) {
|
|
34714
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
34715
|
+
throw new TypeError(`gas fee field "${field}" must be an integer string`);
|
|
34716
|
+
}
|
|
34717
|
+
try {
|
|
34718
|
+
// BigInt throws on non-integer strings (e.g. "1.5", "not-a-number").
|
|
34719
|
+
return BigInt(value);
|
|
34720
|
+
} catch {
|
|
34721
|
+
throw new Error(`gas fee field "${field}" is not a valid integer string: ${value}`);
|
|
34722
|
+
}
|
|
34723
|
+
}
|
|
34724
|
+
|
|
34725
|
+
function toDepositQuoteInfo(data, chain) {
|
|
33600
34726
|
const fees = (data.fees ?? []).map(({ token: feeTokenSymbol, ...fee })=>{
|
|
33601
34727
|
// Earn Service returns fee.token as a display symbol, for example "USDC".
|
|
33602
34728
|
return {
|
|
@@ -33625,7 +34751,10 @@ function toDepositQuoteInfo(data) {
|
|
|
33625
34751
|
sharePrice: data.sharePrice,
|
|
33626
34752
|
currentApy: data.currentApy,
|
|
33627
34753
|
fees,
|
|
33628
|
-
|
|
34754
|
+
// The Earn Service estimates gas server-side; the chain fills token/blockchain.
|
|
34755
|
+
// Cross-chain quotes resolve no local chain definition, so gasFees stays
|
|
34756
|
+
// empty there (unchanged behavior).
|
|
34757
|
+
gasFees: chain === undefined ? [] : toQuoteGasFees(data.gasFees, chain)
|
|
33629
34758
|
};
|
|
33630
34759
|
}
|
|
33631
34760
|
/**
|
|
@@ -33660,7 +34789,7 @@ function toDepositQuoteInfo(data) {
|
|
|
33660
34789
|
};
|
|
33661
34790
|
try {
|
|
33662
34791
|
const response = await pollApiPost(url.toString(), requestBody, isDepositQuoteResponse, pollingConfig);
|
|
33663
|
-
return toDepositQuoteInfo(response.data);
|
|
34792
|
+
return toDepositQuoteInfo(response.data, params.chainDefinition);
|
|
33664
34793
|
} catch (error) {
|
|
33665
34794
|
throw parseEarnApiError(error, {
|
|
33666
34795
|
operation: 'getDepositQuote'
|
|
@@ -33668,7 +34797,7 @@ function toDepositQuoteInfo(data) {
|
|
|
33668
34797
|
}
|
|
33669
34798
|
}
|
|
33670
34799
|
|
|
33671
|
-
function toWithdrawalQuoteInfo(data) {
|
|
34800
|
+
function toWithdrawalQuoteInfo(data, chain) {
|
|
33672
34801
|
return {
|
|
33673
34802
|
vaultAddress: data.vaultAddress,
|
|
33674
34803
|
vaultName: data.vaultName,
|
|
@@ -33696,7 +34825,7 @@ function toWithdrawalQuoteInfo(data) {
|
|
|
33696
34825
|
status
|
|
33697
34826
|
}
|
|
33698
34827
|
})),
|
|
33699
|
-
gasFees: [],
|
|
34828
|
+
gasFees: chain === undefined ? [] : toQuoteGasFees(data.gasFees, chain),
|
|
33700
34829
|
// Wire format uses `warnings`, but the SDK surface uses
|
|
33701
34830
|
// `earnKitWarnings` to match the precedent set by `VaultInfo` —
|
|
33702
34831
|
// `warnings` is reserved for the structured `VaultWarning` shape.
|
|
@@ -33728,7 +34857,7 @@ function toWithdrawalQuoteInfo(data) {
|
|
|
33728
34857
|
};
|
|
33729
34858
|
try {
|
|
33730
34859
|
const response = await pollApiPost(url.toString(), requestBody, isWithdrawalQuoteResponse, pollingConfig);
|
|
33731
|
-
return toWithdrawalQuoteInfo(response.data);
|
|
34860
|
+
return toWithdrawalQuoteInfo(response.data, params.chainDefinition);
|
|
33732
34861
|
} catch (error) {
|
|
33733
34862
|
throw parseEarnApiError(error, {
|
|
33734
34863
|
operation: 'getWithdrawalQuote'
|
|
@@ -33764,6 +34893,8 @@ function toWithdrawalQuoteInfo(data) {
|
|
|
33764
34893
|
amount: Amount.fromJSON(r.amount),
|
|
33765
34894
|
address: r.token
|
|
33766
34895
|
})),
|
|
34896
|
+
// The claimRewards/quote response does not carry a gas estimate (unlike
|
|
34897
|
+
// deposit/withdrawal quotes), so there is nothing to surface here.
|
|
33767
34898
|
gasFees: []
|
|
33768
34899
|
};
|
|
33769
34900
|
} catch (error) {
|
|
@@ -33773,6 +34904,83 @@ function toWithdrawalQuoteInfo(data) {
|
|
|
33773
34904
|
}
|
|
33774
34905
|
}
|
|
33775
34906
|
|
|
34907
|
+
/**
|
|
34908
|
+
* Build the native gas triple the backend expects for `gasUsed`.
|
|
34909
|
+
*
|
|
34910
|
+
* Returns `undefined` unless both receipt components are present, so the
|
|
34911
|
+
* caller can omit the field entirely — the Earn Service treats a missing
|
|
34912
|
+
* triple as "skip the gas cache write, still return 200".
|
|
34913
|
+
*
|
|
34914
|
+
* @param gasUsed - Receipt gas units used.
|
|
34915
|
+
* @param effectiveGasPrice - Receipt effective gas price.
|
|
34916
|
+
* @returns The `{ gas, gasPrice, fee }` triple, or `undefined` when either
|
|
34917
|
+
* component is missing.
|
|
34918
|
+
*
|
|
34919
|
+
* @example
|
|
34920
|
+
* ```typescript
|
|
34921
|
+
* buildReportedGasUsed(362454n, 29466364605n)
|
|
34922
|
+
* // { gas: '362454', gasPrice: '29466364605', fee: '10680201716540670' }
|
|
34923
|
+
* ```
|
|
34924
|
+
*
|
|
34925
|
+
* @internal
|
|
34926
|
+
*/ function buildReportedGasUsed(gasUsed, effectiveGasPrice) {
|
|
34927
|
+
if (gasUsed === undefined || effectiveGasPrice === undefined) {
|
|
34928
|
+
return undefined;
|
|
34929
|
+
}
|
|
34930
|
+
return {
|
|
34931
|
+
gas: gasUsed.toString(),
|
|
34932
|
+
gasPrice: effectiveGasPrice.toString(),
|
|
34933
|
+
fee: (gasUsed * effectiveGasPrice).toString()
|
|
34934
|
+
};
|
|
34935
|
+
}
|
|
34936
|
+
/**
|
|
34937
|
+
* Report the outcome of an SDK-submitted same-chain Earn transaction.
|
|
34938
|
+
*
|
|
34939
|
+
* @param params - Transaction report parameters.
|
|
34940
|
+
* @throws {@link KitError} When the API call fails.
|
|
34941
|
+
*
|
|
34942
|
+
* @internal
|
|
34943
|
+
*/ async function reportEarnTransaction(params) {
|
|
34944
|
+
const { pollingConfig, baseUrl } = buildConfig(params.config);
|
|
34945
|
+
const url = new URL(`${EARN_KIT_API_PREFIX}/transactions/report`, baseUrl);
|
|
34946
|
+
// The report endpoint is not idempotent: success reports refresh the gas
|
|
34947
|
+
// cache and failure reports increment counts. If the first request succeeds
|
|
34948
|
+
// server-side but the client times out or sees a transient 5xx, retrying
|
|
34949
|
+
// would duplicate the report (double-writing an outcome or inflating failure
|
|
34950
|
+
// counts). Reporting is best-effort (see the fire-and-forget caller), so
|
|
34951
|
+
// make exactly one attempt and never retry — a single dropped report is
|
|
34952
|
+
// preferable to a duplicated one. `maxRetries` here is the total attempt
|
|
34953
|
+
// count in pollApiWithValidation (loop runs `attempt <= maxRetries`), so 1
|
|
34954
|
+
// means one request with no retry; 0 would skip the request entirely.
|
|
34955
|
+
const reportConfig = {
|
|
34956
|
+
...pollingConfig,
|
|
34957
|
+
maxRetries: 1
|
|
34958
|
+
};
|
|
34959
|
+
const gasUsed = buildReportedGasUsed(params.gasUsed, params.effectiveGasPrice);
|
|
34960
|
+
const requestBody = {
|
|
34961
|
+
execId: params.execId,
|
|
34962
|
+
chain: params.chain,
|
|
34963
|
+
status: params.status,
|
|
34964
|
+
action: params.action,
|
|
34965
|
+
...params.txHash !== undefined && {
|
|
34966
|
+
txHash: params.txHash
|
|
34967
|
+
},
|
|
34968
|
+
...gasUsed !== undefined && {
|
|
34969
|
+
gasUsed
|
|
34970
|
+
},
|
|
34971
|
+
...params.errorCode !== undefined && {
|
|
34972
|
+
errorCode: params.errorCode
|
|
34973
|
+
}
|
|
34974
|
+
};
|
|
34975
|
+
try {
|
|
34976
|
+
await pollApiPost(url.toString(), requestBody, isTransactionReportResponse, reportConfig);
|
|
34977
|
+
} catch (error) {
|
|
34978
|
+
throw parseEarnApiError(error, {
|
|
34979
|
+
operation: 'transactionReport'
|
|
34980
|
+
});
|
|
34981
|
+
}
|
|
34982
|
+
}
|
|
34983
|
+
|
|
33776
34984
|
/**
|
|
33777
34985
|
* Sum the amounts across every token input to size the allowance approval.
|
|
33778
34986
|
*
|
|
@@ -33883,6 +35091,59 @@ function toWithdrawalQuoteInfo(data) {
|
|
|
33883
35091
|
// Intentionally built-ins-only: Earn bridge support is limited to SDK-known
|
|
33884
35092
|
// token contracts plus the explicit ERC-3009 domain allowlist below.
|
|
33885
35093
|
const TOKEN_REGISTRY = createTokenRegistry();
|
|
35094
|
+
function submitTransactionReport(reportContext, action, status, details) {
|
|
35095
|
+
void reportEarnTransaction({
|
|
35096
|
+
execId: reportContext.execId,
|
|
35097
|
+
chain: reportContext.chain,
|
|
35098
|
+
config: reportContext.config,
|
|
35099
|
+
action,
|
|
35100
|
+
status,
|
|
35101
|
+
...details
|
|
35102
|
+
}).catch(()=>undefined);
|
|
35103
|
+
}
|
|
35104
|
+
function reportTransactionSuccess(reportContext, action, result) {
|
|
35105
|
+
if (result === undefined) {
|
|
35106
|
+
return;
|
|
35107
|
+
}
|
|
35108
|
+
submitTransactionReport(reportContext, action, 'success', {
|
|
35109
|
+
txHash: result.txHash,
|
|
35110
|
+
gasUsed: result.gasUsed,
|
|
35111
|
+
effectiveGasPrice: result.effectiveGasPrice
|
|
35112
|
+
});
|
|
35113
|
+
}
|
|
35114
|
+
function reportTransactionFailure(reportContext, action, error) {
|
|
35115
|
+
submitTransactionReport(reportContext, action, 'failure', {
|
|
35116
|
+
txHash: transactionReportTxHash(error),
|
|
35117
|
+
errorCode: transactionReportErrorCode(error)
|
|
35118
|
+
});
|
|
35119
|
+
}
|
|
35120
|
+
function transactionReportErrorCode(error) {
|
|
35121
|
+
if (isKitError(error)) {
|
|
35122
|
+
return error.name;
|
|
35123
|
+
}
|
|
35124
|
+
const message = getErrorMessage(error);
|
|
35125
|
+
if (/user (rejected|denied)|rejected by user/i.test(message)) {
|
|
35126
|
+
return 'USER_REJECTED';
|
|
35127
|
+
}
|
|
35128
|
+
if (/insufficient funds/i.test(message)) {
|
|
35129
|
+
return 'INSUFFICIENT_FUNDS';
|
|
35130
|
+
}
|
|
35131
|
+
if (/timeout|timed out/i.test(message)) {
|
|
35132
|
+
return 'TIMEOUT';
|
|
35133
|
+
}
|
|
35134
|
+
return 'UNKNOWN_ERROR';
|
|
35135
|
+
}
|
|
35136
|
+
function transactionReportTxHash(error) {
|
|
35137
|
+
if (!isKitError(error)) {
|
|
35138
|
+
return undefined;
|
|
35139
|
+
}
|
|
35140
|
+
const trace = error.cause?.trace;
|
|
35141
|
+
if (typeof trace !== 'object' || trace === null) {
|
|
35142
|
+
return undefined;
|
|
35143
|
+
}
|
|
35144
|
+
const txHash = trace['txHash'];
|
|
35145
|
+
return typeof txHash === 'string' && txHash !== '' ? txHash : undefined;
|
|
35146
|
+
}
|
|
33886
35147
|
/**
|
|
33887
35148
|
* Build the typed error raised when a cross-chain wait is cancelled via its
|
|
33888
35149
|
* `AbortSignal`. Mirrors `@core/adapter-base`'s `createAbortError` (same
|
|
@@ -34214,7 +35475,7 @@ function finishElapsedWait(lastStatus, lastError) {
|
|
|
34214
35475
|
const adapterContractAddress = requireAdapterContract(chain);
|
|
34215
35476
|
const { adapter } = params.from;
|
|
34216
35477
|
const vaultAddress = assertHexAddress('vaultAddress', params.vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
|
|
34217
|
-
const { executionParams, signature } = await this.runPhase(ctx, 'deposit', 'fetchParams', async ()=>fetchDeposit({
|
|
35478
|
+
const { execId, executionParams, signature } = await this.runPhase(ctx, 'deposit', 'fetchParams', async ()=>fetchDeposit({
|
|
34218
35479
|
vaultAddress,
|
|
34219
35480
|
amount: params.amount,
|
|
34220
35481
|
address,
|
|
@@ -34222,32 +35483,55 @@ function finishElapsedWait(lastStatus, lastError) {
|
|
|
34222
35483
|
config
|
|
34223
35484
|
}), ()=>undefined);
|
|
34224
35485
|
validateExecutionDeadline(executionParams);
|
|
35486
|
+
const transactionReportContext = {
|
|
35487
|
+
execId,
|
|
35488
|
+
chain: apiChain,
|
|
35489
|
+
config
|
|
35490
|
+
};
|
|
34225
35491
|
const approvalToken = resolveEarnApprovalToken(executionParams);
|
|
34226
35492
|
const tokenInputs = approvalToken === undefined ? [] : buildEarnTokenInputs(executionParams, approvalToken);
|
|
34227
35493
|
const requiredAllowance = sumTokenInputAmounts(tokenInputs);
|
|
34228
35494
|
if (!options.skipApprove && approvalToken !== undefined && requiredAllowance > 0n) {
|
|
34229
|
-
await this.runPhase(ctx, 'approve', 'approve', async ()=>
|
|
35495
|
+
await this.runPhase(ctx, 'approve', 'approve', async ()=>{
|
|
35496
|
+
try {
|
|
35497
|
+
const approval = await approveAllowanceIfNeeded({
|
|
35498
|
+
adapter,
|
|
35499
|
+
chain,
|
|
35500
|
+
tokenAddress: approvalToken,
|
|
35501
|
+
delegate: adapterContractAddress,
|
|
35502
|
+
address,
|
|
35503
|
+
requiredAllowance,
|
|
35504
|
+
revertMessage: 'Earn deposit token approval reverted on-chain'
|
|
35505
|
+
});
|
|
35506
|
+
reportTransactionSuccess(transactionReportContext, 'Approve', approval);
|
|
35507
|
+
return approval;
|
|
35508
|
+
} catch (error) {
|
|
35509
|
+
reportTransactionFailure(transactionReportContext, 'Approve', error);
|
|
35510
|
+
throw error;
|
|
35511
|
+
}
|
|
35512
|
+
}, (approval)=>approval?.txHash);
|
|
35513
|
+
}
|
|
35514
|
+
const { txHash, explorerUrl } = await this.runPhase(ctx, 'deposit', 'execute', async ()=>{
|
|
35515
|
+
try {
|
|
35516
|
+
const result = await executeEarnAction({
|
|
34230
35517
|
adapter,
|
|
34231
35518
|
chain,
|
|
34232
|
-
tokenAddress: approvalToken,
|
|
34233
|
-
delegate: adapterContractAddress,
|
|
34234
35519
|
address,
|
|
34235
|
-
|
|
34236
|
-
|
|
34237
|
-
|
|
34238
|
-
|
|
34239
|
-
|
|
34240
|
-
|
|
34241
|
-
|
|
34242
|
-
|
|
34243
|
-
|
|
34244
|
-
|
|
34245
|
-
|
|
34246
|
-
|
|
34247
|
-
|
|
34248
|
-
|
|
34249
|
-
|
|
34250
|
-
}), ({ txHash })=>txHash);
|
|
35520
|
+
actionKey: 'earn.deposit',
|
|
35521
|
+
actionParams: {
|
|
35522
|
+
executeParams: executionParams,
|
|
35523
|
+
tokenInputs,
|
|
35524
|
+
signature
|
|
35525
|
+
},
|
|
35526
|
+
revertMessage: 'Earn deposit reverted on-chain'
|
|
35527
|
+
});
|
|
35528
|
+
reportTransactionSuccess(transactionReportContext, 'Deposit', result);
|
|
35529
|
+
return result;
|
|
35530
|
+
} catch (error) {
|
|
35531
|
+
reportTransactionFailure(transactionReportContext, 'Deposit', error);
|
|
35532
|
+
throw error;
|
|
35533
|
+
}
|
|
35534
|
+
}, ({ txHash })=>txHash);
|
|
34251
35535
|
return {
|
|
34252
35536
|
kind: 'same-chain',
|
|
34253
35537
|
txHash,
|
|
@@ -34327,7 +35611,13 @@ function finishElapsedWait(lastStatus, lastError) {
|
|
|
34327
35611
|
amount: params.amount,
|
|
34328
35612
|
sourceChain: sourceChain.chain,
|
|
34329
35613
|
destinationChain: destinationChain.chain,
|
|
34330
|
-
expiresAt: prepared.expiresAt
|
|
35614
|
+
expiresAt: prepared.expiresAt,
|
|
35615
|
+
...prepared.quoteIssuedAt !== undefined && {
|
|
35616
|
+
quoteIssuedAt: prepared.quoteIssuedAt
|
|
35617
|
+
},
|
|
35618
|
+
...prepared.quoteExpiry !== undefined && {
|
|
35619
|
+
quoteExpiry: prepared.quoteExpiry
|
|
35620
|
+
}
|
|
34331
35621
|
};
|
|
34332
35622
|
}
|
|
34333
35623
|
/** {@inheritdoc} */ async withdraw(params) {
|
|
@@ -34348,7 +35638,7 @@ function finishElapsedWait(lastStatus, lastError) {
|
|
|
34348
35638
|
const adapterContractAddress = requireAdapterContract(chain);
|
|
34349
35639
|
const vaultAddress = assertHexAddress('vaultAddress', params.vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
|
|
34350
35640
|
const { adapter } = params.from;
|
|
34351
|
-
const { executionParams, signature } = await this.runPhase(ctx, 'withdraw', 'fetchParams', async ()=>fetchWithdraw({
|
|
35641
|
+
const { execId, executionParams, signature } = await this.runPhase(ctx, 'withdraw', 'fetchParams', async ()=>fetchWithdraw({
|
|
34352
35642
|
vaultAddress,
|
|
34353
35643
|
amount: params.amount,
|
|
34354
35644
|
address,
|
|
@@ -34356,32 +35646,55 @@ function finishElapsedWait(lastStatus, lastError) {
|
|
|
34356
35646
|
config
|
|
34357
35647
|
}), ()=>undefined);
|
|
34358
35648
|
validateExecutionDeadline(executionParams);
|
|
35649
|
+
const transactionReportContext = {
|
|
35650
|
+
execId,
|
|
35651
|
+
chain: apiChain,
|
|
35652
|
+
config
|
|
35653
|
+
};
|
|
34359
35654
|
const tokenInputs = buildEarnTokenInputs(executionParams, vaultAddress);
|
|
34360
35655
|
const approvalToken = tokenInputs[0]?.token;
|
|
34361
35656
|
const requiredAllowance = sumTokenInputAmounts(tokenInputs);
|
|
34362
35657
|
if (!options.skipApprove && approvalToken !== undefined) {
|
|
34363
|
-
await this.runPhase(ctx, 'approve', 'approve', async ()=>
|
|
35658
|
+
await this.runPhase(ctx, 'approve', 'approve', async ()=>{
|
|
35659
|
+
try {
|
|
35660
|
+
const approval = await approveAllowanceIfNeeded({
|
|
35661
|
+
adapter,
|
|
35662
|
+
chain,
|
|
35663
|
+
tokenAddress: approvalToken,
|
|
35664
|
+
delegate: adapterContractAddress,
|
|
35665
|
+
address,
|
|
35666
|
+
requiredAllowance,
|
|
35667
|
+
revertMessage: 'Vault share token approval reverted on-chain'
|
|
35668
|
+
});
|
|
35669
|
+
reportTransactionSuccess(transactionReportContext, 'Approve', approval);
|
|
35670
|
+
return approval;
|
|
35671
|
+
} catch (error) {
|
|
35672
|
+
reportTransactionFailure(transactionReportContext, 'Approve', error);
|
|
35673
|
+
throw error;
|
|
35674
|
+
}
|
|
35675
|
+
}, (approval)=>approval?.txHash);
|
|
35676
|
+
}
|
|
35677
|
+
const { txHash, explorerUrl } = await this.runPhase(ctx, 'withdraw', 'execute', async ()=>{
|
|
35678
|
+
try {
|
|
35679
|
+
const result = await executeEarnAction({
|
|
34364
35680
|
adapter,
|
|
34365
35681
|
chain,
|
|
34366
|
-
tokenAddress: approvalToken,
|
|
34367
|
-
delegate: adapterContractAddress,
|
|
34368
35682
|
address,
|
|
34369
|
-
|
|
34370
|
-
|
|
34371
|
-
|
|
34372
|
-
|
|
34373
|
-
|
|
34374
|
-
|
|
34375
|
-
|
|
34376
|
-
|
|
34377
|
-
|
|
34378
|
-
|
|
34379
|
-
|
|
34380
|
-
|
|
34381
|
-
|
|
34382
|
-
|
|
34383
|
-
|
|
34384
|
-
}), ({ txHash })=>txHash);
|
|
35683
|
+
actionKey: 'earn.withdraw',
|
|
35684
|
+
actionParams: {
|
|
35685
|
+
executeParams: executionParams,
|
|
35686
|
+
tokenInputs,
|
|
35687
|
+
signature
|
|
35688
|
+
},
|
|
35689
|
+
revertMessage: 'Earn withdraw reverted on-chain'
|
|
35690
|
+
});
|
|
35691
|
+
reportTransactionSuccess(transactionReportContext, 'Withdraw', result);
|
|
35692
|
+
return result;
|
|
35693
|
+
} catch (error) {
|
|
35694
|
+
reportTransactionFailure(transactionReportContext, 'Withdraw', error);
|
|
35695
|
+
throw error;
|
|
35696
|
+
}
|
|
35697
|
+
}, ({ txHash })=>txHash);
|
|
34385
35698
|
return {
|
|
34386
35699
|
txHash,
|
|
34387
35700
|
explorerUrl,
|
|
@@ -34515,141 +35828,6 @@ function finishElapsedWait(lastStatus, lastError) {
|
|
|
34515
35828
|
}
|
|
34516
35829
|
}
|
|
34517
35830
|
}
|
|
34518
|
-
gasEstimateFailure(name, chain, error) {
|
|
34519
|
-
return {
|
|
34520
|
-
name,
|
|
34521
|
-
token: chain.nativeCurrency.symbol,
|
|
34522
|
-
blockchain: chain.chain,
|
|
34523
|
-
fees: null,
|
|
34524
|
-
error: getErrorMessage(error)
|
|
34525
|
-
};
|
|
34526
|
-
}
|
|
34527
|
-
async estimateDepositQuoteGasFees(params) {
|
|
34528
|
-
const { adapter, chain, apiChain, address, vaultAddress, amount, config } = params;
|
|
34529
|
-
try {
|
|
34530
|
-
const adapterContractAddress = requireAdapterContract(chain);
|
|
34531
|
-
const normalizedVaultAddress = assertHexAddress('vaultAddress', vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
|
|
34532
|
-
const { executionParams, signature } = await fetchDeposit({
|
|
34533
|
-
vaultAddress: normalizedVaultAddress,
|
|
34534
|
-
amount,
|
|
34535
|
-
address,
|
|
34536
|
-
chain: apiChain,
|
|
34537
|
-
config
|
|
34538
|
-
});
|
|
34539
|
-
validateExecutionDeadline(executionParams);
|
|
34540
|
-
const approvalToken = resolveEarnApprovalToken(executionParams);
|
|
34541
|
-
const tokenInputs = approvalToken === undefined ? [] : buildEarnTokenInputs(executionParams, approvalToken);
|
|
34542
|
-
const requiredAllowance = sumTokenInputAmounts(tokenInputs);
|
|
34543
|
-
return await estimateEarnQuoteGasFees({
|
|
34544
|
-
adapter,
|
|
34545
|
-
chain,
|
|
34546
|
-
address,
|
|
34547
|
-
actionName: 'Deposit',
|
|
34548
|
-
actionKey: 'earn.deposit',
|
|
34549
|
-
actionParams: {
|
|
34550
|
-
executeParams: executionParams,
|
|
34551
|
-
tokenInputs,
|
|
34552
|
-
signature
|
|
34553
|
-
},
|
|
34554
|
-
approval: approvalToken !== undefined && requiredAllowance > 0n ? {
|
|
34555
|
-
token: approvalToken,
|
|
34556
|
-
delegate: adapterContractAddress,
|
|
34557
|
-
requiredAllowance
|
|
34558
|
-
} : undefined
|
|
34559
|
-
});
|
|
34560
|
-
} catch (error) {
|
|
34561
|
-
return [
|
|
34562
|
-
this.gasEstimateFailure('Deposit', chain, error)
|
|
34563
|
-
];
|
|
34564
|
-
}
|
|
34565
|
-
}
|
|
34566
|
-
async estimateWithdrawalQuoteGasFees(params) {
|
|
34567
|
-
const { adapter, chain, apiChain, address, vaultAddress, amount, config } = params;
|
|
34568
|
-
try {
|
|
34569
|
-
const adapterContractAddress = requireAdapterContract(chain);
|
|
34570
|
-
const normalizedVaultAddress = assertHexAddress('vaultAddress', vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
|
|
34571
|
-
const { executionParams, signature } = await fetchWithdraw({
|
|
34572
|
-
vaultAddress: normalizedVaultAddress,
|
|
34573
|
-
amount,
|
|
34574
|
-
address,
|
|
34575
|
-
chain: apiChain,
|
|
34576
|
-
config
|
|
34577
|
-
});
|
|
34578
|
-
validateExecutionDeadline(executionParams);
|
|
34579
|
-
const tokenInputs = buildEarnTokenInputs(executionParams, normalizedVaultAddress);
|
|
34580
|
-
const approvalToken = tokenInputs[0]?.token;
|
|
34581
|
-
const requiredAllowance = sumTokenInputAmounts(tokenInputs);
|
|
34582
|
-
return await estimateEarnQuoteGasFees({
|
|
34583
|
-
adapter,
|
|
34584
|
-
chain,
|
|
34585
|
-
address,
|
|
34586
|
-
actionName: 'Withdraw',
|
|
34587
|
-
actionKey: 'earn.withdraw',
|
|
34588
|
-
actionParams: {
|
|
34589
|
-
executeParams: executionParams,
|
|
34590
|
-
tokenInputs,
|
|
34591
|
-
signature
|
|
34592
|
-
},
|
|
34593
|
-
approval: approvalToken !== undefined ? {
|
|
34594
|
-
token: approvalToken,
|
|
34595
|
-
delegate: adapterContractAddress,
|
|
34596
|
-
requiredAllowance
|
|
34597
|
-
} : undefined
|
|
34598
|
-
});
|
|
34599
|
-
} catch (error) {
|
|
34600
|
-
return [
|
|
34601
|
-
this.gasEstimateFailure('Withdraw', chain, error)
|
|
34602
|
-
];
|
|
34603
|
-
}
|
|
34604
|
-
}
|
|
34605
|
-
async estimateClaimRewardsQuoteGasFees(params) {
|
|
34606
|
-
const { adapter, chain, apiChain, address, vaultAddress, config } = params;
|
|
34607
|
-
try {
|
|
34608
|
-
requireAdapterContract(chain);
|
|
34609
|
-
const { rewards, executionParams, signature } = await fetchClaimRewards({
|
|
34610
|
-
address,
|
|
34611
|
-
chain: apiChain,
|
|
34612
|
-
vaultAddress,
|
|
34613
|
-
config
|
|
34614
|
-
});
|
|
34615
|
-
if (rewards.length === 0) {
|
|
34616
|
-
return [];
|
|
34617
|
-
}
|
|
34618
|
-
const missingExecutionParams = executionParams === undefined;
|
|
34619
|
-
const missingSignature = signature === undefined;
|
|
34620
|
-
if (missingExecutionParams || missingSignature) {
|
|
34621
|
-
throw new KitError({
|
|
34622
|
-
...EarnError.INTERNAL_ERROR,
|
|
34623
|
-
recoverability: 'RETRYABLE',
|
|
34624
|
-
message: 'Claim rewards response must include executionParams and signature when rewards are claimable',
|
|
34625
|
-
cause: {
|
|
34626
|
-
trace: {
|
|
34627
|
-
rewardsCount: rewards.length,
|
|
34628
|
-
missingExecutionParams,
|
|
34629
|
-
missingSignature
|
|
34630
|
-
}
|
|
34631
|
-
}
|
|
34632
|
-
});
|
|
34633
|
-
}
|
|
34634
|
-
validateExecutionDeadline(executionParams);
|
|
34635
|
-
return await estimateEarnQuoteGasFees({
|
|
34636
|
-
adapter,
|
|
34637
|
-
chain,
|
|
34638
|
-
address,
|
|
34639
|
-
actionName: 'Claim Rewards',
|
|
34640
|
-
actionKey: 'earn.claimRewards',
|
|
34641
|
-
actionParams: {
|
|
34642
|
-
executeParams: executionParams,
|
|
34643
|
-
tokenInputs: [],
|
|
34644
|
-
signature
|
|
34645
|
-
}
|
|
34646
|
-
});
|
|
34647
|
-
} catch (error) {
|
|
34648
|
-
return [
|
|
34649
|
-
this.gasEstimateFailure('Claim Rewards', chain, error)
|
|
34650
|
-
];
|
|
34651
|
-
}
|
|
34652
|
-
}
|
|
34653
35831
|
/** {@inheritdoc} */ async getDepositQuote(params) {
|
|
34654
35832
|
const config = this.resolveConfig(params.config);
|
|
34655
35833
|
if (hasQuoteDestinationChain(params)) {
|
|
@@ -34672,96 +35850,43 @@ function finishElapsedWait(lastStatus, lastError) {
|
|
|
34672
35850
|
throw createValidationFailedError$1('chain', destinationChain.chain, 'chain is only supported for cross-chain Earn deposit quotes; omit chain/address when quoting on the source chain');
|
|
34673
35851
|
}
|
|
34674
35852
|
const { address, chain, chainDefinition } = await resolveAdapterContext(params.from);
|
|
34675
|
-
//
|
|
34676
|
-
//
|
|
34677
|
-
//
|
|
34678
|
-
|
|
34679
|
-
|
|
34680
|
-
|
|
34681
|
-
|
|
34682
|
-
|
|
34683
|
-
|
|
34684
|
-
|
|
34685
|
-
|
|
34686
|
-
this.estimateDepositQuoteGasFees({
|
|
34687
|
-
adapter: params.from.adapter,
|
|
34688
|
-
chain: chainDefinition,
|
|
34689
|
-
apiChain: chain,
|
|
34690
|
-
address,
|
|
34691
|
-
vaultAddress: params.vaultAddress,
|
|
34692
|
-
amount: params.amount,
|
|
34693
|
-
config
|
|
34694
|
-
})
|
|
34695
|
-
]);
|
|
34696
|
-
return {
|
|
34697
|
-
...quote,
|
|
34698
|
-
gasFees
|
|
34699
|
-
};
|
|
35853
|
+
// Gas is estimated server-side by the Earn Service and returned on the quote, so the
|
|
35854
|
+
// SDK no longer simulates it locally. `chainDefinition` lets the fetch fill
|
|
35855
|
+
// the native token symbol / blockchain on each gas entry.
|
|
35856
|
+
return fetchDepositQuote({
|
|
35857
|
+
vaultAddress: params.vaultAddress,
|
|
35858
|
+
amount: params.amount,
|
|
35859
|
+
address,
|
|
35860
|
+
chain,
|
|
35861
|
+
config,
|
|
35862
|
+
chainDefinition
|
|
35863
|
+
});
|
|
34700
35864
|
}
|
|
34701
35865
|
/** {@inheritdoc} */ async getWithdrawalQuote(params) {
|
|
34702
35866
|
const config = this.resolveConfig(params.config);
|
|
34703
35867
|
const { address, chain, chainDefinition } = await resolveAdapterContext(params.from);
|
|
34704
|
-
//
|
|
34705
|
-
|
|
34706
|
-
|
|
34707
|
-
|
|
34708
|
-
|
|
34709
|
-
|
|
34710
|
-
|
|
34711
|
-
|
|
34712
|
-
|
|
34713
|
-
config
|
|
34714
|
-
}),
|
|
34715
|
-
this.estimateWithdrawalQuoteGasFees({
|
|
34716
|
-
adapter: params.from.adapter,
|
|
34717
|
-
chain: chainDefinition,
|
|
34718
|
-
apiChain: chain,
|
|
34719
|
-
address,
|
|
34720
|
-
vaultAddress: params.vaultAddress,
|
|
34721
|
-
amount: params.amount,
|
|
34722
|
-
config
|
|
34723
|
-
})
|
|
34724
|
-
]);
|
|
34725
|
-
return {
|
|
34726
|
-
...quote,
|
|
34727
|
-
gasFees
|
|
34728
|
-
};
|
|
35868
|
+
// Gas is estimated server-side by the Earn Service and returned on the quote.
|
|
35869
|
+
return fetchWithdrawalQuote({
|
|
35870
|
+
vaultAddress: params.vaultAddress,
|
|
35871
|
+
amount: params.amount,
|
|
35872
|
+
address,
|
|
35873
|
+
chain,
|
|
35874
|
+
config,
|
|
35875
|
+
chainDefinition
|
|
35876
|
+
});
|
|
34729
35877
|
}
|
|
34730
35878
|
/** {@inheritdoc} */ async getClaimRewardsQuote(params) {
|
|
34731
35879
|
const config = this.resolveConfig(params.config);
|
|
34732
|
-
const { address, chain
|
|
34733
|
-
|
|
35880
|
+
const { address, chain } = await resolveAdapterContext(params.from);
|
|
35881
|
+
// The claimRewards/quote response carries no gas estimate (unlike
|
|
35882
|
+
// deposit/withdrawal quotes), and the SDK no longer estimates gas locally,
|
|
35883
|
+
// so gasFees is always empty for claim rewards.
|
|
35884
|
+
return fetchClaimRewardsQuote({
|
|
34734
35885
|
vaultAddress: params.vaultAddress,
|
|
34735
35886
|
address,
|
|
34736
35887
|
chain,
|
|
34737
35888
|
config
|
|
34738
35889
|
});
|
|
34739
|
-
// No claimable rewards means there is nothing to execute, so there is no
|
|
34740
|
-
// gas to estimate. Short-circuit on the already-fetched quote rather than
|
|
34741
|
-
// calling the (heavier) claim execution endpoint again — this also keeps
|
|
34742
|
-
// `gasFees` empty as documented, instead of risking a `{ fees: null }`
|
|
34743
|
-
// estimation-error entry when the adapter/RPC is unavailable. This
|
|
34744
|
-
// short-circuit is why the claim path stays sequential instead of using
|
|
34745
|
-
// the Promise.all pattern of the deposit/withdrawal quotes: estimating in
|
|
34746
|
-
// parallel would hit the signing endpoint even when nothing is claimable.
|
|
34747
|
-
if (quote.rewards.length === 0) {
|
|
34748
|
-
return {
|
|
34749
|
-
...quote,
|
|
34750
|
-
gasFees: []
|
|
34751
|
-
};
|
|
34752
|
-
}
|
|
34753
|
-
const gasFees = await this.estimateClaimRewardsQuoteGasFees({
|
|
34754
|
-
adapter: params.from.adapter,
|
|
34755
|
-
chain: chainDefinition,
|
|
34756
|
-
apiChain: chain,
|
|
34757
|
-
address,
|
|
34758
|
-
vaultAddress: params.vaultAddress,
|
|
34759
|
-
config
|
|
34760
|
-
});
|
|
34761
|
-
return {
|
|
34762
|
-
...quote,
|
|
34763
|
-
gasFees
|
|
34764
|
-
};
|
|
34765
35890
|
}
|
|
34766
35891
|
}
|
|
34767
35892
|
function hasDepositDestination(params) {
|
|
@@ -34999,11 +36124,27 @@ function formatPositionPnL(pnl) {
|
|
|
34999
36124
|
* @param vault - Provider vault info with raw amount objects
|
|
35000
36125
|
* @returns Vault info with total deposits and liquidity formatted as strings
|
|
35001
36126
|
*/ function formatVaultInfo(vault) {
|
|
35002
|
-
|
|
36127
|
+
// The flat `totalDeposits`/`liquidity` are deprecated aliases that are
|
|
36128
|
+
// intentionally dual-read through the migration window so existing
|
|
36129
|
+
// consumers keep receiving them until Contract.
|
|
36130
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
36131
|
+
const { totalDeposits, liquidity, liquidityProfile, ...rest } = vault;
|
|
36132
|
+
// `liquidityProfile` is `.optional()` in the response schema during the
|
|
36133
|
+
// expand/contract window (an old backend that predates the nested facets
|
|
36134
|
+
// omits it), so only format and re-attach it when present — matching the
|
|
36135
|
+
// provider-side `toVaultInfo` mapper.
|
|
35003
36136
|
return {
|
|
35004
36137
|
...rest,
|
|
35005
36138
|
totalDeposits: formatAmount$1(totalDeposits),
|
|
35006
|
-
liquidity: formatAmount$1(liquidity)
|
|
36139
|
+
liquidity: formatAmount$1(liquidity),
|
|
36140
|
+
...liquidityProfile !== undefined && {
|
|
36141
|
+
liquidityProfile: {
|
|
36142
|
+
...liquidityProfile,
|
|
36143
|
+
totalDeposits: formatAmount$1(liquidityProfile.totalDeposits),
|
|
36144
|
+
available: formatAmount$1(liquidityProfile.available),
|
|
36145
|
+
totalSupply: formatAmount$1(liquidityProfile.totalSupply)
|
|
36146
|
+
}
|
|
36147
|
+
}
|
|
35007
36148
|
};
|
|
35008
36149
|
}
|
|
35009
36150
|
/**
|
|
@@ -36915,25 +38056,6 @@ function formatRetryResult(operation, result) {
|
|
|
36915
38056
|
// Auto-register this kit for user agent tracking
|
|
36916
38057
|
registerKit(`${pkg$2.name}/${pkg$2.version}`);
|
|
36917
38058
|
|
|
36918
|
-
/**
|
|
36919
|
-
* Create an EarnKit instance for AppKit earn operations.
|
|
36920
|
-
*
|
|
36921
|
-
* @remarks The context parameter is reserved for future EarnKit wiring.
|
|
36922
|
-
* EarnKit does not currently support AppKit developer fee hooks, so the
|
|
36923
|
-
* factory does not read fee callbacks from the context. Earn custom fees remain
|
|
36924
|
-
* reserved until EarnKit fee support ships.
|
|
36925
|
-
*
|
|
36926
|
-
* When EarnKit supports developer fee hooks, this factory can wire AppKit context through.
|
|
36927
|
-
*
|
|
36928
|
-
* @param context - AppKit context reserved for future EarnKit wiring
|
|
36929
|
-
* @returns A new EarnKit instance
|
|
36930
|
-
*
|
|
36931
|
-
* @example
|
|
36932
|
-
* ```typescript
|
|
36933
|
-
* const earnKit = createEarnKit(context)
|
|
36934
|
-
* ```
|
|
36935
|
-
*/ const createEarnKit = ()=>new EarnKit();
|
|
36936
|
-
|
|
36937
38059
|
/**
|
|
36938
38060
|
* Register event handlers from a context actions map to a kit instance.
|
|
36939
38061
|
*
|
|
@@ -36998,6 +38120,29 @@ registerKit(`${pkg$2.name}/${pkg$2.version}`);
|
|
|
36998
38120
|
}
|
|
36999
38121
|
};
|
|
37000
38122
|
|
|
38123
|
+
/**
|
|
38124
|
+
* Create an EarnKit instance for AppKit earn operations.
|
|
38125
|
+
*
|
|
38126
|
+
* Attaches any earn event handlers previously registered on the AppKit
|
|
38127
|
+
* context (via `kit.on('earn.*', …)` or `kit.on('*', …)`) so step events
|
|
38128
|
+
* fire during the returned kit's earn operations.
|
|
38129
|
+
*
|
|
38130
|
+
* @remarks Developer fee hooks from the AppKit context are not applied.
|
|
38131
|
+
* EarnKit does not yet support custom fee policies.
|
|
38132
|
+
*
|
|
38133
|
+
* @param context - AppKit context with earn event handlers and kit options
|
|
38134
|
+
* @returns An EarnKit instance ready for AppKit earn operations
|
|
38135
|
+
*
|
|
38136
|
+
* @example
|
|
38137
|
+
* ```typescript
|
|
38138
|
+
* const earnKit = createEarnKit(context)
|
|
38139
|
+
* ```
|
|
38140
|
+
*/ const createEarnKit = (context)=>{
|
|
38141
|
+
const kit = new EarnKit();
|
|
38142
|
+
registerActionHandlers(kit, context.actions.earn, 'earn');
|
|
38143
|
+
return kit;
|
|
38144
|
+
};
|
|
38145
|
+
|
|
37001
38146
|
/**
|
|
37002
38147
|
* List of all supported token aliases for App Kit send operations.
|
|
37003
38148
|
*
|
|
@@ -37788,7 +38933,8 @@ const tokens = createTokenRegistry();
|
|
|
37788
38933
|
* polling loop yourself.
|
|
37789
38934
|
*
|
|
37790
38935
|
* @param context - AppKit context.
|
|
37791
|
-
* @param params - `txHash
|
|
38936
|
+
* @param params - `txHash` and `chainIn`, plus optional `chainOut` and
|
|
38937
|
+
* `kitKey`.
|
|
37792
38938
|
* @returns A snapshot of the swap's status at the time of the call.
|
|
37793
38939
|
*
|
|
37794
38940
|
* @example
|
|
@@ -37877,7 +39023,7 @@ const tokens = createTokenRegistry();
|
|
|
37877
39023
|
* addresses before indexing into `result.rates[chain]`.
|
|
37878
39024
|
*
|
|
37879
39025
|
* @param context - AppKit context.
|
|
37880
|
-
* @param params - `chain`, optional `tokens
|
|
39026
|
+
* @param params - `chain`, plus optional `tokens` and `kitKey`.
|
|
37881
39027
|
* @returns A nested map of `[chain][address] → { priceUSD, fetchedAt }`.
|
|
37882
39028
|
*
|
|
37883
39029
|
* @example
|
|
@@ -37972,7 +39118,7 @@ const tokens = createTokenRegistry();
|
|
|
37972
39118
|
}
|
|
37973
39119
|
// Return earn-specific chains
|
|
37974
39120
|
if (operationType === 'earn') {
|
|
37975
|
-
const earnKit = createEarnKit();
|
|
39121
|
+
const earnKit = createEarnKit(context);
|
|
37976
39122
|
return earnKit.getSupportedChains();
|
|
37977
39123
|
}
|
|
37978
39124
|
// Return unified balance chains
|
|
@@ -37983,7 +39129,7 @@ const tokens = createTokenRegistry();
|
|
|
37983
39129
|
// Reuse kit instances here if provider constructors become expensive or stateful.
|
|
37984
39130
|
const bridgeKit = createBridgeKit(context);
|
|
37985
39131
|
const swapKit = createSwapKit(context);
|
|
37986
|
-
const earnKit = createEarnKit();
|
|
39132
|
+
const earnKit = createEarnKit(context);
|
|
37987
39133
|
const bridgeChains = bridgeKit.getSupportedChains();
|
|
37988
39134
|
const swapChains = swapKit.getSupportedChains();
|
|
37989
39135
|
const earnChains = earnKit.getSupportedChains();
|
|
@@ -38005,7 +39151,7 @@ const tokens = createTokenRegistry();
|
|
|
38005
39151
|
};
|
|
38006
39152
|
|
|
38007
39153
|
async function deposit$2(context, params) {
|
|
38008
|
-
return createEarnKit().deposit(params);
|
|
39154
|
+
return createEarnKit(context).deposit(params);
|
|
38009
39155
|
}
|
|
38010
39156
|
/**
|
|
38011
39157
|
* Execute an earn withdrawal operation.
|
|
@@ -38029,7 +39175,7 @@ async function deposit$2(context, params) {
|
|
|
38029
39175
|
* })
|
|
38030
39176
|
* ```
|
|
38031
39177
|
*/ async function withdraw(context, params) {
|
|
38032
|
-
return createEarnKit().withdraw(params);
|
|
39178
|
+
return createEarnKit(context).withdraw(params);
|
|
38033
39179
|
}
|
|
38034
39180
|
/**
|
|
38035
39181
|
* Claim earn rewards.
|
|
@@ -38052,7 +39198,7 @@ async function deposit$2(context, params) {
|
|
|
38052
39198
|
* })
|
|
38053
39199
|
* ```
|
|
38054
39200
|
*/ async function claimRewards(context, params) {
|
|
38055
|
-
return createEarnKit().claimRewards(params);
|
|
39201
|
+
return createEarnKit(context).claimRewards(params);
|
|
38056
39202
|
}
|
|
38057
39203
|
/**
|
|
38058
39204
|
* Fetch vault information.
|
|
@@ -38074,7 +39220,7 @@ async function deposit$2(context, params) {
|
|
|
38074
39220
|
* })
|
|
38075
39221
|
* ```
|
|
38076
39222
|
*/ async function getVaults(context, params) {
|
|
38077
|
-
return createEarnKit().getVaults(params);
|
|
39223
|
+
return createEarnKit(context).getVaults(params);
|
|
38078
39224
|
}
|
|
38079
39225
|
/**
|
|
38080
39226
|
* Discover vaults available on a chain.
|
|
@@ -38098,7 +39244,7 @@ async function deposit$2(context, params) {
|
|
|
38098
39244
|
* })
|
|
38099
39245
|
* ```
|
|
38100
39246
|
*/ async function exploreVaults(context, params) {
|
|
38101
|
-
return createEarnKit().exploreVaults(params);
|
|
39247
|
+
return createEarnKit(context).exploreVaults(params);
|
|
38102
39248
|
}
|
|
38103
39249
|
/**
|
|
38104
39250
|
* Lazily iterate every vault available on a chain.
|
|
@@ -38122,7 +39268,7 @@ async function deposit$2(context, params) {
|
|
|
38122
39268
|
* }
|
|
38123
39269
|
* ```
|
|
38124
39270
|
*/ function exploreVaultsIterator(context, params) {
|
|
38125
|
-
return createEarnKit().exploreVaultsIterator(params);
|
|
39271
|
+
return createEarnKit(context).exploreVaultsIterator(params);
|
|
38126
39272
|
}
|
|
38127
39273
|
/**
|
|
38128
39274
|
* Fetch a wallet position in a vault.
|
|
@@ -38145,7 +39291,7 @@ async function deposit$2(context, params) {
|
|
|
38145
39291
|
* })
|
|
38146
39292
|
* ```
|
|
38147
39293
|
*/ async function getPosition(context, params) {
|
|
38148
|
-
return createEarnKit().getPosition(params);
|
|
39294
|
+
return createEarnKit(context).getPosition(params);
|
|
38149
39295
|
}
|
|
38150
39296
|
/**
|
|
38151
39297
|
* Fetch the current status of a cross-chain Earn deposit.
|
|
@@ -38167,7 +39313,7 @@ async function deposit$2(context, params) {
|
|
|
38167
39313
|
* console.log(status.status)
|
|
38168
39314
|
* ```
|
|
38169
39315
|
*/ async function getCrossChainDepositStatus(context, params) {
|
|
38170
|
-
return createEarnKit().getCrossChainDepositStatus(params);
|
|
39316
|
+
return createEarnKit(context).getCrossChainDepositStatus(params);
|
|
38171
39317
|
}
|
|
38172
39318
|
/**
|
|
38173
39319
|
* Poll a cross-chain Earn deposit until it reaches a terminal bridge state.
|
|
@@ -38190,7 +39336,7 @@ async function deposit$2(context, params) {
|
|
|
38190
39336
|
* console.log(result.outcome)
|
|
38191
39337
|
* ```
|
|
38192
39338
|
*/ async function waitForCrossChainDeposit(context, params) {
|
|
38193
|
-
return createEarnKit().waitForCrossChainDeposit(params);
|
|
39339
|
+
return createEarnKit(context).waitForCrossChainDeposit(params);
|
|
38194
39340
|
}
|
|
38195
39341
|
/**
|
|
38196
39342
|
* Fetch a deposit quote.
|
|
@@ -38214,7 +39360,7 @@ async function deposit$2(context, params) {
|
|
|
38214
39360
|
* })
|
|
38215
39361
|
* ```
|
|
38216
39362
|
*/ async function getDepositQuote(context, params) {
|
|
38217
|
-
return createEarnKit().getDepositQuote(params);
|
|
39363
|
+
return createEarnKit(context).getDepositQuote(params);
|
|
38218
39364
|
}
|
|
38219
39365
|
/**
|
|
38220
39366
|
* Fetch a withdrawal quote.
|
|
@@ -38238,7 +39384,7 @@ async function deposit$2(context, params) {
|
|
|
38238
39384
|
* })
|
|
38239
39385
|
* ```
|
|
38240
39386
|
*/ async function getWithdrawalQuote(context, params) {
|
|
38241
|
-
return createEarnKit().getWithdrawalQuote(params);
|
|
39387
|
+
return createEarnKit(context).getWithdrawalQuote(params);
|
|
38242
39388
|
}
|
|
38243
39389
|
/**
|
|
38244
39390
|
* Fetch a claim rewards quote.
|
|
@@ -38261,11 +39407,47 @@ async function deposit$2(context, params) {
|
|
|
38261
39407
|
* })
|
|
38262
39408
|
* ```
|
|
38263
39409
|
*/ async function getClaimRewardsQuote(context, params) {
|
|
38264
|
-
return createEarnKit().getClaimRewardsQuote(params);
|
|
39410
|
+
return createEarnKit(context).getClaimRewardsQuote(params);
|
|
39411
|
+
}
|
|
39412
|
+
/**
|
|
39413
|
+
* Resume a multi-phase earn operation that previously failed.
|
|
39414
|
+
*
|
|
39415
|
+
* Pass the {@link KitError} caught from `deposit`, `withdraw`, or
|
|
39416
|
+
* `claimRewards`. Completed phases can be skipped when the error carries
|
|
39417
|
+
* earn retry context. Call `isRetryableError(error)` first.
|
|
39418
|
+
*
|
|
39419
|
+
* @remarks
|
|
39420
|
+
* Retry re-fetches execution params and may re-submit the execute
|
|
39421
|
+
* transaction. Treat this as best-effort recovery if a prior execute
|
|
39422
|
+
* broadcast may still be in flight.
|
|
39423
|
+
*
|
|
39424
|
+
* @param context - AppKit context
|
|
39425
|
+
* @param error - The error caught from a previous multi-phase earn operation
|
|
39426
|
+
* @returns Promise resolving to the result of the resumed operation
|
|
39427
|
+
* @throws If the error is not retryable or lacks earn retry context
|
|
39428
|
+
*
|
|
39429
|
+
* @example
|
|
39430
|
+
* ```typescript
|
|
39431
|
+
* import { isRetryableError } from '@circle-fin/app-kit'
|
|
39432
|
+
* import { createContext } from '@circle-fin/app-kit/context'
|
|
39433
|
+
* import { retry } from '@circle-fin/app-kit/earn'
|
|
39434
|
+
*
|
|
39435
|
+
* const context = createContext()
|
|
39436
|
+
*
|
|
39437
|
+
* try {
|
|
39438
|
+
* await deposit(context, params)
|
|
39439
|
+
* } catch (error) {
|
|
39440
|
+
* if (isRetryableError(error)) {
|
|
39441
|
+
* const result = await retry(context, error)
|
|
39442
|
+
* }
|
|
39443
|
+
* }
|
|
39444
|
+
* ```
|
|
39445
|
+
*/ async function retry(context, error) {
|
|
39446
|
+
return createEarnKit(context).retry(error);
|
|
38265
39447
|
}
|
|
38266
39448
|
|
|
38267
39449
|
var name = "@circle-fin/unified-balance-kit";
|
|
38268
|
-
var version = "1.
|
|
39450
|
+
var version = "1.3.0";
|
|
38269
39451
|
var pkg = {
|
|
38270
39452
|
name: name,
|
|
38271
39453
|
version: version};
|
|
@@ -40733,29 +41915,33 @@ const CIRCLE_BPS_DIVISOR = 10_000n;
|
|
|
40733
41915
|
const DEFAULT_GAS_FEE = parseUnits('0.1', USDC_DECIMALS);
|
|
40734
41916
|
/**
|
|
40735
41917
|
* Return the estimated Gateway gas fee for a chain in USDC atomic units.
|
|
40736
|
-
*
|
|
41918
|
+
* Prefers an entry in `overrides` (the real per-chain fee derived from a
|
|
41919
|
+
* prior estimate), then the static {@link GAS_FEE_BY_CHAIN} constant, and
|
|
41920
|
+
* finally a conservative 0.1 USDC fallback for unlisted chains.
|
|
40737
41921
|
*
|
|
40738
41922
|
* @param chain - The source blockchain.
|
|
41923
|
+
* @param overrides - Optional real per-chain gas fees keyed by chain.
|
|
40739
41924
|
* @returns Gas fee in USDC atomic units.
|
|
40740
|
-
*/ function getGasFee(chain) {
|
|
40741
|
-
return GAS_FEE_BY_CHAIN.get(chain) ?? DEFAULT_GAS_FEE;
|
|
41925
|
+
*/ function getGasFee(chain, overrides) {
|
|
41926
|
+
return overrides?.get(chain) ?? GAS_FEE_BY_CHAIN.get(chain) ?? DEFAULT_GAS_FEE;
|
|
40742
41927
|
}
|
|
40743
41928
|
/**
|
|
40744
41929
|
* Return the estimated forwarder fee for the destination chain
|
|
40745
41930
|
* (service fee + destination gas fee).
|
|
40746
41931
|
*
|
|
40747
41932
|
* @param destinationChain - The mint destination chain.
|
|
41933
|
+
* @param overrides - Optional real per-chain gas fees keyed by chain.
|
|
40748
41934
|
* @returns Forwarder fee in USDC atomic units.
|
|
40749
|
-
*/ function getForwarderFee(destinationChain) {
|
|
40750
|
-
const destGas = getGasFee(destinationChain);
|
|
41935
|
+
*/ function getForwarderFee(destinationChain, overrides) {
|
|
41936
|
+
const destGas = getGasFee(destinationChain, overrides);
|
|
40751
41937
|
return FORWARDER_SERVICE_FEE + destGas;
|
|
40752
41938
|
}
|
|
40753
41939
|
/**
|
|
40754
41940
|
* Estimate the fixed fees (gas + forwarder) and compute the maximum
|
|
40755
41941
|
* amount that can be drawn from this chain for a single intent,
|
|
40756
41942
|
* accounting for the 0.5 bps transfer fee if cross-chain.
|
|
40757
|
-
*/ function computeMaxDrawable(slot, forwarderFeeRemaining) {
|
|
40758
|
-
const gasFee = getGasFee(slot.chain);
|
|
41943
|
+
*/ function computeMaxDrawable(slot, forwarderFeeRemaining, overrides) {
|
|
41944
|
+
const gasFee = getGasFee(slot.chain, overrides);
|
|
40759
41945
|
let fixedFees = gasFee;
|
|
40760
41946
|
let forwarderFeeUsed = 0n;
|
|
40761
41947
|
if (forwarderFeeRemaining > 0n) {
|
|
@@ -40787,14 +41973,14 @@ const DEFAULT_GAS_FEE = parseUnits('0.1', USDC_DECIMALS);
|
|
|
40787
41973
|
* buffer), and returns the allocations for this pass.
|
|
40788
41974
|
*
|
|
40789
41975
|
* Mutates `slot.remaining` so the next pass sees reduced balances.
|
|
40790
|
-
*/ function greedyAllocate(slots, amount, destinationChain, useForwarder) {
|
|
41976
|
+
*/ function greedyAllocate(slots, amount, destinationChain, useForwarder, overrides) {
|
|
40791
41977
|
const result = [];
|
|
40792
41978
|
let remaining = amount;
|
|
40793
|
-
let forwarderFeeRemaining = useForwarder ? getForwarderFee(destinationChain) : 0n;
|
|
41979
|
+
let forwarderFeeRemaining = useForwarder ? getForwarderFee(destinationChain, overrides) : 0n;
|
|
40794
41980
|
if (remaining <= 0n) return result;
|
|
40795
41981
|
for (const slot of slots){
|
|
40796
41982
|
if (remaining <= 0n) break;
|
|
40797
|
-
const { drawable, gasFee, forwarderFeeUsed } = computeMaxDrawable(slot, forwarderFeeRemaining);
|
|
41983
|
+
const { drawable, gasFee, forwarderFeeUsed } = computeMaxDrawable(slot, forwarderFeeRemaining, overrides);
|
|
40798
41984
|
if (drawable <= 0n) continue;
|
|
40799
41985
|
// Greedy: take as much as we can from this chain
|
|
40800
41986
|
const take = remaining < drawable ? remaining : drawable // NOSONAR: This is a false positive — Math.min() only accepts number, not bigint, so the ternary is the correct pattern here.
|
|
@@ -40893,7 +42079,7 @@ const DEFAULT_GAS_FEE = parseUnits('0.1', USDC_DECIMALS);
|
|
|
40893
42079
|
// After this pass, slot.remaining reflects consumed capacity.
|
|
40894
42080
|
// -----------------------------------------------------------------------
|
|
40895
42081
|
const transferAmount = parseUnits(ctx.amountIn, USDC_DECIMALS);
|
|
40896
|
-
const allocations = greedyAllocate(slots, transferAmount, ctx.destinationChain, ctx.useForwarder);
|
|
42082
|
+
const allocations = greedyAllocate(slots, transferAmount, ctx.destinationChain, ctx.useForwarder, ctx.gasFeeOverrides);
|
|
40897
42083
|
assertFullyAllocated(allocations, transferAmount, ctx.amountIn);
|
|
40898
42084
|
// -----------------------------------------------------------------------
|
|
40899
42085
|
// 4. Phase 2 — Allocate developer fee
|
|
@@ -40901,7 +42087,7 @@ const DEFAULT_GAS_FEE = parseUnits('0.1', USDC_DECIMALS);
|
|
|
40901
42087
|
// Same-chain first here too — if the destination chain still has
|
|
40902
42088
|
// capacity, use it (same-chain fee intent = cheapest gas).
|
|
40903
42089
|
// -----------------------------------------------------------------------
|
|
40904
|
-
const developerFeeAllocations = greedyAllocate(slots, devFeeAmount, ctx.destinationChain, false);
|
|
42090
|
+
const developerFeeAllocations = greedyAllocate(slots, devFeeAmount, ctx.destinationChain, false, ctx.gasFeeOverrides);
|
|
40905
42091
|
if (devFeeAmount > 0n) {
|
|
40906
42092
|
assertFullyAllocated(developerFeeAllocations, devFeeAmount, formatUnits(devFeeAmount.toString(), USDC_DECIMALS));
|
|
40907
42093
|
}
|
|
@@ -40910,7 +42096,7 @@ const DEFAULT_GAS_FEE = parseUnits('0.1', USDC_DECIMALS);
|
|
|
40910
42096
|
// Again same ordering, same shared reduced balances.
|
|
40911
42097
|
// Same-chain first for the same reason.
|
|
40912
42098
|
// -----------------------------------------------------------------------
|
|
40913
|
-
const circleFeeAllocations = greedyAllocate(slots, circleFeeAmount, ctx.destinationChain, false);
|
|
42099
|
+
const circleFeeAllocations = greedyAllocate(slots, circleFeeAmount, ctx.destinationChain, false, ctx.gasFeeOverrides);
|
|
40914
42100
|
if (circleFeeAmount > 0n) {
|
|
40915
42101
|
assertFullyAllocated(circleFeeAllocations, circleFeeAmount, formatUnits(circleFeeAmount.toString(), USDC_DECIMALS));
|
|
40916
42102
|
}
|
|
@@ -41009,10 +42195,12 @@ const BPS_DIVISOR = 100_000n;
|
|
|
41009
42195
|
*
|
|
41010
42196
|
* Unlike `findChainNameByDomain` (which returns the display `name`),
|
|
41011
42197
|
* this returns `chain.chain` — the enum identifier expected by
|
|
41012
|
-
* {@link FeeAllocation}.
|
|
42198
|
+
* {@link FeeAllocation}. Returns `undefined` when no allocation covers the
|
|
42199
|
+
* domain so callers skip the intent rather than bucketing it under a
|
|
42200
|
+
* fabricated sentinel.
|
|
41013
42201
|
*/ function findBlockchainByDomain(domain, allocations) {
|
|
41014
42202
|
const alloc = allocations.find((a)=>a.chain.gateway.domain === domain);
|
|
41015
|
-
return alloc?.chain.chain
|
|
42203
|
+
return alloc?.chain.chain;
|
|
41016
42204
|
}
|
|
41017
42205
|
/**
|
|
41018
42206
|
* Normalize any address/salt format to lowercase bytes32 hex.
|
|
@@ -41136,6 +42324,33 @@ const BPS_DIVISOR = 100_000n;
|
|
|
41136
42324
|
};
|
|
41137
42325
|
});
|
|
41138
42326
|
}
|
|
42327
|
+
/**
|
|
42328
|
+
* Read an intent's transfer value as a BigInt, tolerating the string form
|
|
42329
|
+
* that can appear on estimate-response specs.
|
|
42330
|
+
*/ function intentValue(intent) {
|
|
42331
|
+
const { value } = intent.spec;
|
|
42332
|
+
return typeof value === 'bigint' ? value : safeBigInt(String(value), 'spec.value');
|
|
42333
|
+
}
|
|
42334
|
+
/**
|
|
42335
|
+
* Split a single intent's `maxFee` into its transfer-fee and gas-fee
|
|
42336
|
+
* components.
|
|
42337
|
+
*
|
|
42338
|
+
* `transferFee = value * GATEWAY_TRANSFER_FEE_SCALED_BPS / BPS_DIVISOR`
|
|
42339
|
+
* `gasFee = maxFee - transferFee`
|
|
42340
|
+
*
|
|
42341
|
+
* Same-chain transfers (withdrawals) do not incur a transfer fee, so the
|
|
42342
|
+
* whole `maxFee` is gas. See {@link aggregateFeesByIntent} for the caveats
|
|
42343
|
+
* on re-deriving the split locally.
|
|
42344
|
+
*/ function splitIntentFee(intent) {
|
|
42345
|
+
const { maxFee, spec } = intent;
|
|
42346
|
+
const isSameChain = spec.sourceDomain === spec.destinationDomain;
|
|
42347
|
+
const transferFee = isSameChain ? 0n : intentValue(intent) * GATEWAY_TRANSFER_FEE_SCALED_BPS / BPS_DIVISOR;
|
|
42348
|
+
const gasFee = maxFee > transferFee ? maxFee - transferFee : 0n;
|
|
42349
|
+
return {
|
|
42350
|
+
transferFee,
|
|
42351
|
+
gasFee
|
|
42352
|
+
};
|
|
42353
|
+
}
|
|
41139
42354
|
/**
|
|
41140
42355
|
* Decompose each intent's `maxFee` into a transfer fee and a gas fee,
|
|
41141
42356
|
* then aggregate both by source chain.
|
|
@@ -41160,7 +42375,6 @@ const BPS_DIVISOR = 100_000n;
|
|
|
41160
42375
|
* names from source domains.
|
|
41161
42376
|
* @returns Per-chain and total transfer/gas fee breakdowns.
|
|
41162
42377
|
*/ function aggregateFeesByIntent(estimatedIntents, allocations) {
|
|
41163
|
-
const transferFeeBps = GATEWAY_TRANSFER_FEE_SCALED_BPS;
|
|
41164
42378
|
const transferFeeByChain = new Map();
|
|
41165
42379
|
const gasFeeByChain = new Map();
|
|
41166
42380
|
let totalTransferFee = 0n;
|
|
@@ -41168,18 +42382,18 @@ const BPS_DIVISOR = 100_000n;
|
|
|
41168
42382
|
for (const intent of estimatedIntents){
|
|
41169
42383
|
const { maxFee, spec } = intent;
|
|
41170
42384
|
if (maxFee === 0n) continue;
|
|
41171
|
-
const
|
|
41172
|
-
|
|
41173
|
-
|
|
41174
|
-
|
|
41175
|
-
|
|
42385
|
+
const { transferFee, gasFee } = splitIntentFee(intent);
|
|
42386
|
+
totalTransferFee += transferFee;
|
|
42387
|
+
totalGasFee += gasFee;
|
|
42388
|
+
// Totals stay complete even if a domain can't be resolved; only the
|
|
42389
|
+
// per-chain breakdown skips it rather than inventing a placeholder chain.
|
|
42390
|
+
const chain = findBlockchainByDomain(spec.sourceDomain, allocations);
|
|
42391
|
+
if (chain === undefined) continue;
|
|
41176
42392
|
if (transferFee > 0n) {
|
|
41177
|
-
|
|
41178
|
-
transferFeeByChain.set(chainName, (transferFeeByChain.get(chainName) ?? 0n) + transferFee);
|
|
42393
|
+
transferFeeByChain.set(chain, (transferFeeByChain.get(chain) ?? 0n) + transferFee);
|
|
41179
42394
|
}
|
|
41180
42395
|
if (gasFee > 0n) {
|
|
41181
|
-
|
|
41182
|
-
gasFeeByChain.set(chainName, (gasFeeByChain.get(chainName) ?? 0n) + gasFee);
|
|
42396
|
+
gasFeeByChain.set(chain, (gasFeeByChain.get(chain) ?? 0n) + gasFee);
|
|
41183
42397
|
}
|
|
41184
42398
|
}
|
|
41185
42399
|
return {
|
|
@@ -41243,6 +42457,91 @@ const BPS_DIVISOR = 100_000n;
|
|
|
41243
42457
|
}
|
|
41244
42458
|
return fees;
|
|
41245
42459
|
}
|
|
42460
|
+
/**
|
|
42461
|
+
* Derive the real per-chain Gateway gas fee from estimated intents, keyed by
|
|
42462
|
+
* source {@link Blockchain}.
|
|
42463
|
+
*
|
|
42464
|
+
* The value is the maximum single-intent gas fee observed on each chain
|
|
42465
|
+
* (`maxFee − transferFee`) — the amount `computeAutoAllocation` must reserve
|
|
42466
|
+
* per burn intent on that chain. Gas is (near) amount-independent, so every
|
|
42467
|
+
* intent on a chain pays roughly the same; taking the max is a conservative
|
|
42468
|
+
* choice for the multi-intent-per-chain case.
|
|
42469
|
+
*
|
|
42470
|
+
* Intended for `AutoAllocationContext.gasFeeOverrides` so the corrective
|
|
42471
|
+
* re-allocation pass reserves the API's real fee instead of the static
|
|
42472
|
+
* {@link GAS_FEE_BY_CHAIN} constant.
|
|
42473
|
+
*
|
|
42474
|
+
* @param estimatedIntents - Intents with `maxFee` from {@link parseEstimateResponse}.
|
|
42475
|
+
* @param allocations - Normalised allocations used to resolve chain from source domain.
|
|
42476
|
+
* @returns Per-chain real gas fees in USDC atomic units.
|
|
42477
|
+
*
|
|
42478
|
+
* @example
|
|
42479
|
+
* ```typescript
|
|
42480
|
+
* import type { BurnIntent } from '../createIntent/types'
|
|
42481
|
+
* import type { NormalizedAllocation } from '../allocations'
|
|
42482
|
+
*
|
|
42483
|
+
* declare const estimatedIntents: BurnIntent[]
|
|
42484
|
+
* declare const allocations: NormalizedAllocation[]
|
|
42485
|
+
*
|
|
42486
|
+
* // Real per-chain gas, ready to pass as AutoAllocationContext.gasFeeOverrides
|
|
42487
|
+
* // to re-run computeAutoAllocation with the corrected reserve.
|
|
42488
|
+
* const overrides = deriveGasFeeOverrides(estimatedIntents, allocations)
|
|
42489
|
+
* ```
|
|
42490
|
+
*/ function deriveGasFeeOverrides(estimatedIntents, allocations) {
|
|
42491
|
+
const overrides = new Map();
|
|
42492
|
+
for (const intent of estimatedIntents){
|
|
42493
|
+
if (intent.maxFee === 0n) continue;
|
|
42494
|
+
const chain = findBlockchainByDomain(intent.spec.sourceDomain, allocations);
|
|
42495
|
+
if (chain === undefined) continue;
|
|
42496
|
+
const { gasFee } = splitIntentFee(intent);
|
|
42497
|
+
const prev = overrides.get(chain) ?? 0n;
|
|
42498
|
+
if (gasFee > prev) overrides.set(chain, gasFee);
|
|
42499
|
+
}
|
|
42500
|
+
return overrides;
|
|
42501
|
+
}
|
|
42502
|
+
/**
|
|
42503
|
+
* Sum the total balance each source chain must cover, keyed by source
|
|
42504
|
+
* {@link Blockchain}.
|
|
42505
|
+
*
|
|
42506
|
+
* Approximates the Gateway API's balance validation, which rejects a transfer
|
|
42507
|
+
* (`BALANCE_INSUFFICIENT_TOKEN`) when a depositor's confirmed balance on a
|
|
42508
|
+
* source chain is below `sum(intent.value + intent.maxFee)` for that
|
|
42509
|
+
* depositor's intents. This aggregates by chain across all sources, so it is
|
|
42510
|
+
* exact for the common single-depositor-per-chain wallet. When several
|
|
42511
|
+
* depositors hold USDC on the same chain, the chain-level sum can mask a
|
|
42512
|
+
* per-depositor shortfall (or a surplus on one depositor can hide it); the
|
|
42513
|
+
* API's own per-depositor `9001` remains the backstop for that case. Scope the
|
|
42514
|
+
* comparison per (depositor, chain) if that multi-depositor case must be caught
|
|
42515
|
+
* pre-submit.
|
|
42516
|
+
*
|
|
42517
|
+
* @param estimatedIntents - Intents with `maxFee` from {@link parseEstimateResponse}.
|
|
42518
|
+
* @param allocations - Normalised allocations used to resolve chain from source domain.
|
|
42519
|
+
* @returns Per-chain required amount (transfer value + fees) in USDC atomic units.
|
|
42520
|
+
*
|
|
42521
|
+
* @example
|
|
42522
|
+
* ```typescript
|
|
42523
|
+
* import { Blockchain } from '@core/chains'
|
|
42524
|
+
* import type { BurnIntent } from '../createIntent/types'
|
|
42525
|
+
* import type { NormalizedAllocation } from '../allocations'
|
|
42526
|
+
*
|
|
42527
|
+
* declare const estimatedIntents: BurnIntent[]
|
|
42528
|
+
* declare const allocations: NormalizedAllocation[]
|
|
42529
|
+
* declare const confirmedBalanceAtomic: bigint
|
|
42530
|
+
*
|
|
42531
|
+
* const required = sumRequiredPerChain(estimatedIntents, allocations)
|
|
42532
|
+
* const overDrawn =
|
|
42533
|
+
* (required.get(Blockchain.Ethereum) ?? 0n) > confirmedBalanceAtomic
|
|
42534
|
+
* ```
|
|
42535
|
+
*/ function sumRequiredPerChain(estimatedIntents, allocations) {
|
|
42536
|
+
const required = new Map();
|
|
42537
|
+
for (const intent of estimatedIntents){
|
|
42538
|
+
const chain = findBlockchainByDomain(intent.spec.sourceDomain, allocations);
|
|
42539
|
+
if (chain === undefined) continue;
|
|
42540
|
+
const amount = intentValue(intent) + intent.maxFee;
|
|
42541
|
+
required.set(chain, (required.get(chain) ?? 0n) + amount);
|
|
42542
|
+
}
|
|
42543
|
+
return required;
|
|
42544
|
+
}
|
|
41246
42545
|
|
|
41247
42546
|
/**
|
|
41248
42547
|
* Sign each adapter group: Solana one intent per signature, EVM batch per adapter.
|
|
@@ -41497,69 +42796,127 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
|
|
|
41497
42796
|
* non-forwarder transfer response is missing attestation or signature.
|
|
41498
42797
|
* @throws KitError Propagated from adapter signing if the user rejects
|
|
41499
42798
|
* or the signer is unavailable.
|
|
41500
|
-
*/
|
|
41501
|
-
|
|
41502
|
-
|
|
41503
|
-
|
|
41504
|
-
|
|
41505
|
-
|
|
41506
|
-
|
|
41507
|
-
|
|
41508
|
-
|
|
41509
|
-
|
|
41510
|
-
|
|
41511
|
-
|
|
41512
|
-
|
|
41513
|
-
|
|
41514
|
-
|
|
41515
|
-
|
|
41516
|
-
|
|
41517
|
-
|
|
41518
|
-
|
|
41519
|
-
|
|
41520
|
-
|
|
41521
|
-
|
|
41522
|
-
|
|
41523
|
-
|
|
41524
|
-
|
|
41525
|
-
|
|
41526
|
-
|
|
41527
|
-
|
|
41528
|
-
|
|
41529
|
-
|
|
41530
|
-
|
|
41531
|
-
|
|
41532
|
-
|
|
41533
|
-
|
|
41534
|
-
const breakdowns = balanceResults[i]?.breakdown[0]?.breakdown ?? [];
|
|
41535
|
-
for (const b of breakdowns){
|
|
41536
|
-
chainBalances.push({
|
|
41537
|
-
chain: b.chain,
|
|
41538
|
-
confirmedBalance: b.confirmedBalance,
|
|
41539
|
-
sourceIndex: i
|
|
41540
|
-
});
|
|
42799
|
+
*/ /**
|
|
42800
|
+
* Fetch confirmed per-chain USDC balances for every auto-allocation source.
|
|
42801
|
+
*
|
|
42802
|
+
* Used only on the `amountIn` (auto-allocation) path. Returns one
|
|
42803
|
+
* {@link ChainBalance} per (source, chain) pair so the greedy allocator — and
|
|
42804
|
+
* the corrective re-allocation pass — can reason about draw limits without a
|
|
42805
|
+
* second balance round-trip.
|
|
42806
|
+
*
|
|
42807
|
+
* @param params - Spend parameters (source(s) and token).
|
|
42808
|
+
* @param destChain - Resolved destination chain (used for network type).
|
|
42809
|
+
* @returns Confirmed balances tagged with their originating source index.
|
|
42810
|
+
*/ async function fetchChainBalances(params, destChain) {
|
|
42811
|
+
const rawSources = Array.isArray(params.from) ? params.from : [
|
|
42812
|
+
params.from
|
|
42813
|
+
];
|
|
42814
|
+
const sourcesArray = rawSources.filter((s)=>s != null);
|
|
42815
|
+
const networkType = destChain.isTestnet ? 'testnet' : 'mainnet';
|
|
42816
|
+
const balanceResults = await Promise.all(sourcesArray.map(async (source)=>{
|
|
42817
|
+
// When sourceAccount is set (delegate flow), scope the balance
|
|
42818
|
+
// query to the Gateway depositor — not the signer. Using the
|
|
42819
|
+
// address-only path bypasses adapter address resolution, which
|
|
42820
|
+
// would otherwise return the signer's balance (developer-
|
|
42821
|
+
// controlled) or reject an explicit address (user-controlled).
|
|
42822
|
+
let querySource;
|
|
42823
|
+
if (source.sourceAccount) {
|
|
42824
|
+
querySource = {
|
|
42825
|
+
address: source.sourceAccount
|
|
42826
|
+
};
|
|
42827
|
+
} else {
|
|
42828
|
+
querySource = {
|
|
42829
|
+
adapter: source.adapter
|
|
42830
|
+
};
|
|
42831
|
+
if ('address' in source && source.address) {
|
|
42832
|
+
querySource['address'] = source.address;
|
|
41541
42833
|
}
|
|
41542
42834
|
}
|
|
41543
|
-
|
|
41544
|
-
|
|
41545
|
-
|
|
41546
|
-
|
|
41547
|
-
chainBalances,
|
|
41548
|
-
useForwarder,
|
|
41549
|
-
...customFeeConfig ? {
|
|
41550
|
-
customFee: customFeeConfig
|
|
41551
|
-
} : {}
|
|
42835
|
+
return getBalances$1({
|
|
42836
|
+
token: params.token,
|
|
42837
|
+
sources: querySource,
|
|
42838
|
+
networkType
|
|
41552
42839
|
});
|
|
41553
|
-
|
|
41554
|
-
|
|
41555
|
-
|
|
41556
|
-
|
|
41557
|
-
|
|
41558
|
-
|
|
41559
|
-
|
|
42840
|
+
}));
|
|
42841
|
+
const chainBalances = [];
|
|
42842
|
+
for(let i = 0; i < balanceResults.length; i++){
|
|
42843
|
+
const breakdowns = balanceResults[i]?.breakdown[0]?.breakdown ?? [];
|
|
42844
|
+
for (const b of breakdowns){
|
|
42845
|
+
chainBalances.push({
|
|
42846
|
+
chain: b.chain,
|
|
42847
|
+
confirmedBalance: b.confirmedBalance,
|
|
42848
|
+
sourceIndex: i
|
|
42849
|
+
});
|
|
42850
|
+
}
|
|
42851
|
+
}
|
|
42852
|
+
return chainBalances;
|
|
42853
|
+
}
|
|
42854
|
+
/**
|
|
42855
|
+
* Build auto-allocated normalised allocations and burn intents from
|
|
42856
|
+
* pre-fetched balances.
|
|
42857
|
+
*
|
|
42858
|
+
* Performs no balance API call, so it can be re-invoked with
|
|
42859
|
+
* `gasFeeOverrides` (the real per-chain gas from a prior estimate) to correct
|
|
42860
|
+
* an over-draw without re-querying balances.
|
|
42861
|
+
*
|
|
42862
|
+
* @param params - Spend parameters (source(s), token, optional custom fee).
|
|
42863
|
+
* @param destChain - Resolved destination chain with Gateway v1 config.
|
|
42864
|
+
* @param recipientAddress - Resolved recipient address on the destination chain.
|
|
42865
|
+
* @param useForwarder - Whether the Forwarding Service path is active.
|
|
42866
|
+
* @param amountIn - Human-readable USDC amount to allocate.
|
|
42867
|
+
* @param chainBalances - Confirmed balances from {@link fetchChainBalances}.
|
|
42868
|
+
* @param gasFeeOverrides - Optional real per-chain gas fees to reserve.
|
|
42869
|
+
* @returns Normalised allocations and burn intents for the estimate/transfer API.
|
|
42870
|
+
*/ async function buildAutoAllocatedFromBalances(params, destChain, recipientAddress, useForwarder, amountIn, chainBalances, gasFeeOverrides) {
|
|
42871
|
+
const rawSources = Array.isArray(params.from) ? params.from : [
|
|
42872
|
+
params.from
|
|
42873
|
+
];
|
|
42874
|
+
const sourcesArray = rawSources.filter((s)=>s != null);
|
|
42875
|
+
const customFeeConfig = params.config?.customFee;
|
|
42876
|
+
const autoAllocResult = computeAutoAllocation({
|
|
42877
|
+
amountIn,
|
|
42878
|
+
destinationChain: destChain.chain,
|
|
42879
|
+
chainBalances,
|
|
42880
|
+
useForwarder,
|
|
42881
|
+
...customFeeConfig ? {
|
|
42882
|
+
customFee: customFeeConfig
|
|
42883
|
+
} : {},
|
|
42884
|
+
...gasFeeOverrides ? {
|
|
42885
|
+
gasFeeOverrides
|
|
42886
|
+
} : {}
|
|
42887
|
+
});
|
|
42888
|
+
const normalizedAutoAllocations = await normalizeAutoAllocations(autoAllocResult, sourcesArray);
|
|
42889
|
+
const intents = buildAutoAllocatedBurnIntents(normalizedAutoAllocations, destChain, recipientAddress, params.token, params.config?.customFee);
|
|
42890
|
+
const allocations = [
|
|
42891
|
+
...normalizedAutoAllocations.user,
|
|
42892
|
+
...normalizedAutoAllocations.devFee,
|
|
42893
|
+
...normalizedAutoAllocations.circleFee
|
|
42894
|
+
];
|
|
42895
|
+
return {
|
|
42896
|
+
allocations,
|
|
42897
|
+
intents
|
|
42898
|
+
};
|
|
42899
|
+
}
|
|
42900
|
+
/**
|
|
42901
|
+
* Resolve allocations and burn intents for the spend.
|
|
42902
|
+
*
|
|
42903
|
+
* Auto-allocation (`amountIn`) fetches balances once and returns them so the
|
|
42904
|
+
* caller can detect and correct over-draw without re-querying. Explicit
|
|
42905
|
+
* allocations return no balances (they are user-authoritative).
|
|
42906
|
+
*
|
|
42907
|
+
* @param params - Spend parameters.
|
|
42908
|
+
* @param destChain - Resolved destination chain with Gateway v1 config.
|
|
42909
|
+
* @param recipientAddress - Resolved recipient address.
|
|
42910
|
+
* @param useForwarder - Whether the Forwarding Service path is active.
|
|
42911
|
+
* @returns Allocations, intents, and (auto-allocation only) confirmed balances.
|
|
42912
|
+
*/ async function resolveAllocationsAndIntents(params, destChain, recipientAddress, useForwarder) {
|
|
42913
|
+
if (params.amountIn) {
|
|
42914
|
+
const chainBalances = await fetchChainBalances(params, destChain);
|
|
42915
|
+
const { allocations, intents } = await buildAutoAllocatedFromBalances(params, destChain, recipientAddress, useForwarder, params.amountIn, chainBalances);
|
|
41560
42916
|
return {
|
|
41561
42917
|
allocations,
|
|
41562
|
-
intents
|
|
42918
|
+
intents,
|
|
42919
|
+
chainBalances
|
|
41563
42920
|
};
|
|
41564
42921
|
}
|
|
41565
42922
|
const allocations = await normalizeAllocations(params);
|
|
@@ -41601,6 +42958,148 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
|
|
|
41601
42958
|
forwardingFee: undefined
|
|
41602
42959
|
};
|
|
41603
42960
|
}
|
|
42961
|
+
/** Sum confirmed balances (atomic USDC) per source chain. */ function computeAvailablePerChain(chainBalances) {
|
|
42962
|
+
const available = new Map();
|
|
42963
|
+
for (const b of chainBalances){
|
|
42964
|
+
const atomic = parseUnits(b.confirmedBalance, USDC_DECIMALS$1);
|
|
42965
|
+
available.set(b.chain, (available.get(b.chain) ?? 0n) + atomic);
|
|
42966
|
+
}
|
|
42967
|
+
return available;
|
|
42968
|
+
}
|
|
42969
|
+
/**
|
|
42970
|
+
* Detect source chains whose required draw (value + maxFee across their
|
|
42971
|
+
* intents) exceeds the confirmed balance — the condition the Gateway API
|
|
42972
|
+
* rejects with `BALANCE_INSUFFICIENT_TOKEN` at `/v1/transfer`.
|
|
42973
|
+
*
|
|
42974
|
+
* Both sides are summed per chain (see {@link sumRequiredPerChain} and
|
|
42975
|
+
* {@link computeAvailablePerChain}), so detection is exact for the common
|
|
42976
|
+
* single-depositor-per-chain wallet. When multiple depositors hold USDC on the
|
|
42977
|
+
* same chain, a chain-level surplus can mask a per-depositor shortfall; the
|
|
42978
|
+
* API's own per-depositor `9001` remains the backstop in that case.
|
|
42979
|
+
*/ function findOverdrawnChains(estimatedIntents, allocations, chainBalances) {
|
|
42980
|
+
const required = sumRequiredPerChain(estimatedIntents, allocations);
|
|
42981
|
+
const available = computeAvailablePerChain(chainBalances);
|
|
42982
|
+
const overdrawn = [];
|
|
42983
|
+
for (const [chain, req] of required){
|
|
42984
|
+
const avail = available.get(chain) ?? 0n;
|
|
42985
|
+
if (req > avail) {
|
|
42986
|
+
overdrawn.push({
|
|
42987
|
+
chain,
|
|
42988
|
+
required: req,
|
|
42989
|
+
available: avail
|
|
42990
|
+
});
|
|
42991
|
+
}
|
|
42992
|
+
}
|
|
42993
|
+
return overdrawn;
|
|
42994
|
+
}
|
|
42995
|
+
/**
|
|
42996
|
+
* Build a descriptive KitError for an auto-allocation gas shortfall that
|
|
42997
|
+
* survives the corrective re-allocation, naming the per-chain gap so the
|
|
42998
|
+
* caller sees the real cause instead of the opaque API 9001 rejection.
|
|
42999
|
+
*/ function createAutoAllocationGasError(overdrawn, cause) {
|
|
43000
|
+
const detail = overdrawn.map((o)=>`${String(o.chain)} needs ${formatUnits(o.required.toString(), USDC_DECIMALS$1)} USDC ` + `(transfer + gas) but only ${formatUnits(o.available.toString(), USDC_DECIMALS$1)} USDC is available`).join('; ');
|
|
43001
|
+
return new KitError({
|
|
43002
|
+
...BalanceError.INSUFFICIENT_GAS,
|
|
43003
|
+
recoverability: 'FATAL',
|
|
43004
|
+
message: `Insufficient USDC to cover the transfer amount plus Gateway gas fees: ${detail}. ` + `Reduce the amount or add USDC on the affected chain(s).`,
|
|
43005
|
+
...cause === undefined ? {} : {
|
|
43006
|
+
cause: {
|
|
43007
|
+
trace: {
|
|
43008
|
+
cause
|
|
43009
|
+
}
|
|
43010
|
+
}
|
|
43011
|
+
}
|
|
43012
|
+
});
|
|
43013
|
+
}
|
|
43014
|
+
/**
|
|
43015
|
+
* Validate allocations against the network/forwarder rules, call the estimate
|
|
43016
|
+
* API, and return the estimated intents with any forwarding fee.
|
|
43017
|
+
*
|
|
43018
|
+
* @param allocations - Normalised allocations for the estimate.
|
|
43019
|
+
* @param intents - Burn intents to estimate.
|
|
43020
|
+
* @param destChain - Resolved destination chain with Gateway v1 config.
|
|
43021
|
+
* @param useForwarder - Whether the Forwarding Service path is active.
|
|
43022
|
+
* @returns Estimated intents (with real maxFee) and optional forwarding fee.
|
|
43023
|
+
*/ async function validateAndEstimate(allocations, intents, destChain, useForwarder) {
|
|
43024
|
+
assertNetworkCompatibility(allocations, destChain);
|
|
43025
|
+
if (useForwarder) {
|
|
43026
|
+
assertForwarderRouteSupport(destChain, allocations);
|
|
43027
|
+
}
|
|
43028
|
+
const apiBaseUrl = getGatewayApiBaseUrl(destChain.isTestnet);
|
|
43029
|
+
const estimateBody = buildEstimateRequestBody(intents);
|
|
43030
|
+
const { entries, forwardingFee } = await fetchEstimate(apiBaseUrl, estimateBody, useForwarder, allocations);
|
|
43031
|
+
const estimatedIntents = parseEstimateResponse(entries, intents);
|
|
43032
|
+
return {
|
|
43033
|
+
estimatedIntents,
|
|
43034
|
+
forwardingFee
|
|
43035
|
+
};
|
|
43036
|
+
}
|
|
43037
|
+
/**
|
|
43038
|
+
* Fold newly-observed per-chain gas into the accumulated overrides, keeping the
|
|
43039
|
+
* higher fee per chain so a chain a later pass reveals is never under-reserved.
|
|
43040
|
+
*/ function mergeGasFeeOverrides(base, next) {
|
|
43041
|
+
const merged = new Map(base);
|
|
43042
|
+
for (const [chain, fee] of next){
|
|
43043
|
+
const prev = merged.get(chain);
|
|
43044
|
+
if (prev === undefined || fee > prev) {
|
|
43045
|
+
merged.set(chain, fee);
|
|
43046
|
+
}
|
|
43047
|
+
}
|
|
43048
|
+
return merged;
|
|
43049
|
+
}
|
|
43050
|
+
/**
|
|
43051
|
+
* Maximum corrective re-allocation passes before failing fast. One pass fixes
|
|
43052
|
+
* the common case; a second/third covers a chain that a spill only introduces
|
|
43053
|
+
* after gas is reserved. Bounds the worst case at this many extra estimate
|
|
43054
|
+
* round-trips (only ever reached when the balance genuinely falls short).
|
|
43055
|
+
*/ const MAX_CORRECTION_PASSES = 3;
|
|
43056
|
+
/**
|
|
43057
|
+
* Correct an auto-allocation over-draw: reserve the estimate's real per-chain
|
|
43058
|
+
* gas, re-allocate from the same balances, and re-estimate — repeating up to
|
|
43059
|
+
* {@link MAX_CORRECTION_PASSES} times, accumulating the real gas each pass
|
|
43060
|
+
* reveals.
|
|
43061
|
+
*
|
|
43062
|
+
* One pass fixes the common case, where the over-drawn chain was already in the
|
|
43063
|
+
* first estimate. A further pass covers a chain that a spill only introduces
|
|
43064
|
+
* once gas is reserved on the destination: that chain isn't in the first
|
|
43065
|
+
* estimate, so its real gas is unknown until it appears, and its first
|
|
43066
|
+
* re-allocation falls back to the static reserve. Each pass folds the newly
|
|
43067
|
+
* revealed gas into the overrides (see {@link mergeGasFeeOverrides}) so the
|
|
43068
|
+
* next pass reserves it too. Per-chain gas is ~amount-independent, so once
|
|
43069
|
+
* every drawn chain's real gas is known the allocation converges.
|
|
43070
|
+
*
|
|
43071
|
+
* When the shortfall is genuine — the re-allocation can't cover the amount, or
|
|
43072
|
+
* the passes are exhausted while still over-drawn — throws a gas-specific
|
|
43073
|
+
* {@link KitError} instead of submitting a doomed transfer.
|
|
43074
|
+
*/ async function correctOverdraw(opts) {
|
|
43075
|
+
let overrides = deriveGasFeeOverrides(opts.estimatedIntents, opts.allocations);
|
|
43076
|
+
let overdrawn = opts.overdrawn;
|
|
43077
|
+
for(let pass = 0; pass < MAX_CORRECTION_PASSES; pass++){
|
|
43078
|
+
let corrected;
|
|
43079
|
+
try {
|
|
43080
|
+
corrected = await buildAutoAllocatedFromBalances(opts.params, opts.destChain, opts.recipientAddress, opts.useForwarder, opts.amountIn, opts.chainBalances, overrides);
|
|
43081
|
+
} catch (err) {
|
|
43082
|
+
// Re-allocating with the real gas reserved can't cover the amount →
|
|
43083
|
+
// surface a gas-specific error instead of the opaque API rejection.
|
|
43084
|
+
if (err instanceof KitError && err.code === BalanceError.INSUFFICIENT_TOKEN.code) {
|
|
43085
|
+
throw createAutoAllocationGasError(overdrawn, err);
|
|
43086
|
+
}
|
|
43087
|
+
throw err;
|
|
43088
|
+
}
|
|
43089
|
+
const { estimatedIntents, forwardingFee } = await validateAndEstimate(corrected.allocations, corrected.intents, opts.destChain, opts.useForwarder);
|
|
43090
|
+
const stillOverdrawn = findOverdrawnChains(estimatedIntents, corrected.allocations, opts.chainBalances);
|
|
43091
|
+
if (stillOverdrawn.length === 0) {
|
|
43092
|
+
return {
|
|
43093
|
+
allocations: corrected.allocations,
|
|
43094
|
+
estimatedIntents,
|
|
43095
|
+
forwardingFee
|
|
43096
|
+
};
|
|
43097
|
+
}
|
|
43098
|
+
overdrawn = stillOverdrawn;
|
|
43099
|
+
overrides = mergeGasFeeOverrides(overrides, deriveGasFeeOverrides(estimatedIntents, corrected.allocations));
|
|
43100
|
+
}
|
|
43101
|
+
throw createAutoAllocationGasError(overdrawn);
|
|
43102
|
+
}
|
|
41604
43103
|
/**
|
|
41605
43104
|
* Validate allocations, call the estimate API, and return estimated intents.
|
|
41606
43105
|
*
|
|
@@ -41608,6 +43107,14 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
|
|
|
41608
43107
|
* path. Handles forwarder route validation, network compatibility, and the
|
|
41609
43108
|
* estimate API call.
|
|
41610
43109
|
*
|
|
43110
|
+
* For auto-allocation (`amountIn`), the greedy allocator reserves a static
|
|
43111
|
+
* per-chain gas fee that can undershoot the API's real fee, draining a source
|
|
43112
|
+
* (typically the destination chain) below `value + maxFee` and triggering a
|
|
43113
|
+
* `BALANCE_INSUFFICIENT_TOKEN` rejection. When the first estimate reveals such
|
|
43114
|
+
* an over-draw, a bounded corrective re-allocation reserves the real gas and
|
|
43115
|
+
* re-estimates until it converges or fails fast (see {@link correctOverdraw}).
|
|
43116
|
+
* Explicit allocations are user-authoritative and never re-allocated.
|
|
43117
|
+
*
|
|
41611
43118
|
* @param params - Spend parameters.
|
|
41612
43119
|
* @param destChain - Resolved destination chain with Gateway v1 config.
|
|
41613
43120
|
* @param recipientAddress - Resolved recipient address.
|
|
@@ -41617,15 +43124,24 @@ const FORWARDER_POLL_TIMEOUT_MS = 300_000;
|
|
|
41617
43124
|
if (useForwarder) {
|
|
41618
43125
|
assertForwarderRouteSupport(destChain);
|
|
41619
43126
|
}
|
|
41620
|
-
const { allocations, intents } = await resolveAllocationsAndIntents(params, destChain, recipientAddress, useForwarder);
|
|
41621
|
-
|
|
41622
|
-
if (
|
|
41623
|
-
|
|
43127
|
+
const { allocations, intents, chainBalances } = await resolveAllocationsAndIntents(params, destChain, recipientAddress, useForwarder);
|
|
43128
|
+
const { estimatedIntents, forwardingFee } = await validateAndEstimate(allocations, intents, destChain, useForwarder);
|
|
43129
|
+
if (params.amountIn && chainBalances) {
|
|
43130
|
+
const overdrawn = findOverdrawnChains(estimatedIntents, allocations, chainBalances);
|
|
43131
|
+
if (overdrawn.length > 0) {
|
|
43132
|
+
return correctOverdraw({
|
|
43133
|
+
params,
|
|
43134
|
+
destChain,
|
|
43135
|
+
recipientAddress,
|
|
43136
|
+
useForwarder,
|
|
43137
|
+
amountIn: params.amountIn,
|
|
43138
|
+
chainBalances,
|
|
43139
|
+
estimatedIntents,
|
|
43140
|
+
allocations,
|
|
43141
|
+
overdrawn
|
|
43142
|
+
});
|
|
43143
|
+
}
|
|
41624
43144
|
}
|
|
41625
|
-
const apiBaseUrl = getGatewayApiBaseUrl(destChain.isTestnet);
|
|
41626
|
-
const estimateBody = buildEstimateRequestBody(intents);
|
|
41627
|
-
const { entries, forwardingFee } = await fetchEstimate(apiBaseUrl, estimateBody, useForwarder, allocations);
|
|
41628
|
-
const estimatedIntents = parseEstimateResponse(entries, intents);
|
|
41629
43145
|
return {
|
|
41630
43146
|
allocations,
|
|
41631
43147
|
estimatedIntents,
|
|
@@ -42533,8 +44049,10 @@ const assertCustomFeePolicySymbol = Symbol('assertCustomFeePolicy');
|
|
|
42533
44049
|
*
|
|
42534
44050
|
* - `computeFee` — required function that receives resolved spend params
|
|
42535
44051
|
* and returns a fee as a string (or `Promise<string>`).
|
|
42536
|
-
* - `resolveFeeRecipientAddress` —
|
|
42537
|
-
* recipient address as a string (or `Promise<string>`).
|
|
44052
|
+
* - `resolveFeeRecipientAddress` — optional function that returns a
|
|
44053
|
+
* recipient address as a string (or `Promise<string>`). Omit it when
|
|
44054
|
+
* using `setFeeRecipients()`'s declarative map instead — a policy
|
|
44055
|
+
* with neither throws at spend time.
|
|
42538
44056
|
*
|
|
42539
44057
|
* @example
|
|
42540
44058
|
* ```ts
|
|
@@ -42547,7 +44065,7 @@ const assertCustomFeePolicySymbol = Symbol('assertCustomFeePolicy');
|
|
|
42547
44065
|
* ```
|
|
42548
44066
|
*/ const customFeePolicySchema = z.object({
|
|
42549
44067
|
computeFee: z.function().returns(z.string().or(z.promise(z.string()))),
|
|
42550
|
-
resolveFeeRecipientAddress: z.function().returns(z.string().or(z.promise(z.string())))
|
|
44068
|
+
resolveFeeRecipientAddress: z.function().returns(z.string().or(z.promise(z.string()))).optional()
|
|
42551
44069
|
}).strict();
|
|
42552
44070
|
/**
|
|
42553
44071
|
* Assert that the provided value conforms to {@link CustomFeePolicy}.
|
|
@@ -42569,6 +44087,71 @@ const assertCustomFeePolicySymbol = Symbol('assertCustomFeePolicy');
|
|
|
42569
44087
|
validateWithStateTracking(config, customFeePolicySchema, 'UnifiedBalanceKit custom fee policy', assertCustomFeePolicySymbol);
|
|
42570
44088
|
}
|
|
42571
44089
|
|
|
44090
|
+
const assertFeeRecipientsConfigSymbol = Symbol('assertFeeRecipientsConfig');
|
|
44091
|
+
/**
|
|
44092
|
+
* Schema for validating {@link FeeRecipientsConfig}.
|
|
44093
|
+
*
|
|
44094
|
+
* Requires at least one of `evm`/`solana`, non-empty string values for
|
|
44095
|
+
* whichever keys are present, and — mirroring the `depositAccount`
|
|
44096
|
+
* validation in `deposit/validate/assertions` — an address format that
|
|
44097
|
+
* matches the given chain type (EVM hex vs Solana base58).
|
|
44098
|
+
*
|
|
44099
|
+
* @example
|
|
44100
|
+
* ```ts
|
|
44101
|
+
* const config = {
|
|
44102
|
+
* evm: '0x1234567890123456789012345678901234567890',
|
|
44103
|
+
* solana: '9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM',
|
|
44104
|
+
* }
|
|
44105
|
+
* const result = feeRecipientsConfigSchema.safeParse(config)
|
|
44106
|
+
* // result.success === true
|
|
44107
|
+
* ```
|
|
44108
|
+
*/ const feeRecipientsConfigSchema = z.object({
|
|
44109
|
+
evm: z.string().min(1, 'Fee recipient address is required.').optional(),
|
|
44110
|
+
solana: z.string().min(1, 'Fee recipient address is required.').optional()
|
|
44111
|
+
}).strict().refine((config)=>Object.keys(config).length > 0, {
|
|
44112
|
+
message: 'At least one fee recipient (evm or solana) is required.'
|
|
44113
|
+
}).superRefine((config, ctx)=>{
|
|
44114
|
+
for (const type of Object.keys(config)){
|
|
44115
|
+
const address = config[type];
|
|
44116
|
+
if (address == null) continue;
|
|
44117
|
+
// `{ name: type, type }` is a placeholder chain identifier — only
|
|
44118
|
+
// `.type` is checked by these two helpers today, `.name` is unused.
|
|
44119
|
+
// No real ChainDefinition exists here, since validation runs before
|
|
44120
|
+
// a destination chain is resolved.
|
|
44121
|
+
if (!isValidAddressForChain(address, {
|
|
44122
|
+
name: type,
|
|
44123
|
+
type
|
|
44124
|
+
})) {
|
|
44125
|
+
const { expectedAddressFormat } = extractChainInfo({
|
|
44126
|
+
name: type,
|
|
44127
|
+
type
|
|
44128
|
+
});
|
|
44129
|
+
ctx.addIssue({
|
|
44130
|
+
code: z.ZodIssueCode.custom,
|
|
44131
|
+
path: [
|
|
44132
|
+
type
|
|
44133
|
+
],
|
|
44134
|
+
message: `Invalid ${type} address "${address}". Expected ${expectedAddressFormat}.`
|
|
44135
|
+
});
|
|
44136
|
+
}
|
|
44137
|
+
}
|
|
44138
|
+
});
|
|
44139
|
+
/**
|
|
44140
|
+
* Assert that the provided value conforms to {@link FeeRecipientsConfig}.
|
|
44141
|
+
*
|
|
44142
|
+
* Throws a validation error with annotated paths if the configuration is
|
|
44143
|
+
* malformed.
|
|
44144
|
+
*
|
|
44145
|
+
* @param config - The fee recipients map to validate.
|
|
44146
|
+
*
|
|
44147
|
+
* @example
|
|
44148
|
+
* ```ts
|
|
44149
|
+
* assertFeeRecipientsConfig({ evm: '0x1234567890123456789012345678901234567890' })
|
|
44150
|
+
* ```
|
|
44151
|
+
*/ function assertFeeRecipientsConfig(config) {
|
|
44152
|
+
validateWithStateTracking(config, feeRecipientsConfigSchema, 'UnifiedBalanceKit fee recipients config', assertFeeRecipientsConfigSymbol);
|
|
44153
|
+
}
|
|
44154
|
+
|
|
42572
44155
|
function sameChain(a, b) {
|
|
42573
44156
|
return a.chain !== undefined && a.chain === b.chain;
|
|
42574
44157
|
}
|
|
@@ -43549,6 +45132,105 @@ function assertSourceAccountAddresses(from) {
|
|
|
43549
45132
|
config: params.config
|
|
43550
45133
|
};
|
|
43551
45134
|
}
|
|
45135
|
+
/**
|
|
45136
|
+
* Tracks, per {@link CustomFeePolicy} instance, which chain types have
|
|
45137
|
+
* already triggered the "falling back to resolveFeeRecipientAddress"
|
|
45138
|
+
* warning, so repeated `spend()`/`estimateSpend()` calls (e.g. live
|
|
45139
|
+
* quoting) warn once per (policy, chain type) pair rather than on every
|
|
45140
|
+
* call.
|
|
45141
|
+
*/ const warnedFeeRecipientFallbacks = new WeakMap();
|
|
45142
|
+
/**
|
|
45143
|
+
* Invoke `resolveFeeRecipientAddress` and validate its return value has a
|
|
45144
|
+
* plausible address format for `destChain`, the same check
|
|
45145
|
+
* `setFeeRecipients()` already applies at config time. Unlike the map,
|
|
45146
|
+
* the callback's return value can't be validated ahead of time, so it's
|
|
45147
|
+
* checked here instead — a malformed value throws immediately rather
|
|
45148
|
+
* than silently becoming the fee recipient.
|
|
45149
|
+
*
|
|
45150
|
+
* @internal
|
|
45151
|
+
*/ async function resolveFeeRecipientFromCallback(callback, destChain, params) {
|
|
45152
|
+
const address = await callback(destChain, params);
|
|
45153
|
+
if (!isValidAddressForChain(address, destChain)) {
|
|
45154
|
+
throw new KitError({
|
|
45155
|
+
...InputError.VALIDATION_FAILED,
|
|
45156
|
+
recoverability: 'FATAL',
|
|
45157
|
+
message: `resolveFeeRecipientAddress returned an invalid address ` + `"${address}" for chain type "${destChain.type}" ` + `(resolved destination: ${destChain.name}).`
|
|
45158
|
+
});
|
|
45159
|
+
}
|
|
45160
|
+
return address;
|
|
45161
|
+
}
|
|
45162
|
+
/**
|
|
45163
|
+
* Resolve the single fee recipient address for a spend.
|
|
45164
|
+
*
|
|
45165
|
+
* Every fee burn intent in a spend mints to the same destination
|
|
45166
|
+
* chain regardless of which source chain(s) funded it, so exactly one
|
|
45167
|
+
* recipient address — valid on `destChain` — is ever needed.
|
|
45168
|
+
*
|
|
45169
|
+
* `feeRecipients` (set via `setFeeRecipients`) takes priority over the
|
|
45170
|
+
* policy's `resolveFeeRecipientAddress` callback for any chain type it
|
|
45171
|
+
* has an entry for, since it's a direct lookup and doesn't require
|
|
45172
|
+
* invoking developer code. For a chain type `feeRecipients` doesn't
|
|
45173
|
+
* cover, it falls back to `resolveFeeRecipientAddress` if one is
|
|
45174
|
+
* configured — a warning is logged once per (policy, chain type) pair
|
|
45175
|
+
* so the fallback isn't a silent surprise, without spamming repeated
|
|
45176
|
+
* `estimateSpend()` calls used for live quoting. Throws if neither
|
|
45177
|
+
* resolves `destChain`'s type, or if `resolveFeeRecipientAddress`
|
|
45178
|
+
* resolves it to a malformed address (see
|
|
45179
|
+
* {@link resolveFeeRecipientFromCallback}).
|
|
45180
|
+
*
|
|
45181
|
+
* @internal
|
|
45182
|
+
*/ async function resolveFeeRecipient(destChain, policy, feeRecipients, params) {
|
|
45183
|
+
if (feeRecipients) {
|
|
45184
|
+
// `destChain.type` is `@core/chains`' broader `ChainType` union;
|
|
45185
|
+
// `FeeRecipientChainType` is the narrower subset this map supports
|
|
45186
|
+
// today. A type not present as a key simply has no configured
|
|
45187
|
+
// recipient, which is handled below.
|
|
45188
|
+
const type = destChain.type;
|
|
45189
|
+
const recipientAddress = feeRecipients[type];
|
|
45190
|
+
if (recipientAddress) {
|
|
45191
|
+
return recipientAddress;
|
|
45192
|
+
}
|
|
45193
|
+
if (policy.resolveFeeRecipientAddress) {
|
|
45194
|
+
const warnedTypes = warnedFeeRecipientFallbacks.get(policy);
|
|
45195
|
+
if (!warnedTypes?.has(type)) {
|
|
45196
|
+
warnedFeeRecipientFallbacks.set(policy, (warnedTypes ?? new Set()).add(type));
|
|
45197
|
+
console.warn(`setFeeRecipients() is configured but has no entry for chain ` + `type "${type}" — falling back to customFeePolicy.` + `resolveFeeRecipientAddress for this chain type. Add a ` + `"${type}" entry to setFeeRecipients() to avoid this fallback.`);
|
|
45198
|
+
}
|
|
45199
|
+
return resolveFeeRecipientFromCallback(policy.resolveFeeRecipientAddress, destChain, params);
|
|
45200
|
+
}
|
|
45201
|
+
throw new KitError({
|
|
45202
|
+
...InputError.VALIDATION_FAILED,
|
|
45203
|
+
recoverability: 'FATAL',
|
|
45204
|
+
message: `No fee recipient configured for chain type "${type}" ` + `(resolved destination: ${destChain.name}). Call setFeeRecipients() ` + `with an entry for "${type}", or provide resolveFeeRecipientAddress ` + `on the custom fee policy.`
|
|
45205
|
+
});
|
|
45206
|
+
}
|
|
45207
|
+
if (!policy.resolveFeeRecipientAddress) {
|
|
45208
|
+
throw new KitError({
|
|
45209
|
+
...InputError.VALIDATION_FAILED,
|
|
45210
|
+
recoverability: 'FATAL',
|
|
45211
|
+
message: 'No fee recipient configured — call setFeeRecipients() or provide ' + 'resolveFeeRecipientAddress on the custom fee policy.'
|
|
45212
|
+
});
|
|
45213
|
+
}
|
|
45214
|
+
return resolveFeeRecipientFromCallback(policy.resolveFeeRecipientAddress, destChain, params);
|
|
45215
|
+
}
|
|
45216
|
+
/**
|
|
45217
|
+
* Guard against a common misconfiguration: a developer sets the
|
|
45218
|
+
* declarative `feeRecipients` map expecting it alone to drive fee
|
|
45219
|
+
* collection, but no fee is ever charged without a `computeFee` from
|
|
45220
|
+
* `customFeePolicy` to determine the amount. Without this check that
|
|
45221
|
+
* misconfiguration fails silently — no fee is charged and no error is
|
|
45222
|
+
* raised.
|
|
45223
|
+
*
|
|
45224
|
+
* @internal
|
|
45225
|
+
*/ function assertFeeRecipientsHasPolicy(feeRecipients) {
|
|
45226
|
+
if (feeRecipients) {
|
|
45227
|
+
throw new KitError({
|
|
45228
|
+
...InputError.VALIDATION_FAILED,
|
|
45229
|
+
recoverability: 'FATAL',
|
|
45230
|
+
message: 'setFeeRecipients() is configured but no developer fee will be ' + 'charged: setCustomFeePolicy() must also be set to provide ' + 'computeFee, which determines the fee amount. Call ' + 'setCustomFeePolicy(), or remove setFeeRecipients() if no ' + 'developer fee is intended.'
|
|
45231
|
+
});
|
|
45232
|
+
}
|
|
45233
|
+
}
|
|
43552
45234
|
/**
|
|
43553
45235
|
* Apply a {@link CustomFeePolicy} to an adapter-only spend.
|
|
43554
45236
|
*
|
|
@@ -43557,15 +45239,20 @@ function assertSourceAccountAddresses(from) {
|
|
|
43557
45239
|
* `config.customFee` so the provider sees it.
|
|
43558
45240
|
*
|
|
43559
45241
|
* @internal
|
|
43560
|
-
*/ async function mergeCustomFeePolicyForAdapterOnly(params, policy) {
|
|
43561
|
-
if (params.config?.customFee
|
|
45242
|
+
*/ async function mergeCustomFeePolicyForAdapterOnly(params, policy, feeRecipients) {
|
|
45243
|
+
if (params.config?.customFee) {
|
|
45244
|
+
return params;
|
|
45245
|
+
}
|
|
45246
|
+
if (!policy) {
|
|
45247
|
+
assertFeeRecipientsHasPolicy(feeRecipients);
|
|
43562
45248
|
return params;
|
|
43563
45249
|
}
|
|
43564
45250
|
const destChain = resolveChainIdentifier(params.to.chain);
|
|
43565
|
-
|
|
43566
|
-
|
|
43567
|
-
|
|
43568
|
-
|
|
45251
|
+
// Resolve the recipient before computing the fee: a KitError here
|
|
45252
|
+
// (missing/unresolvable recipient) shouldn't be preceded by an
|
|
45253
|
+
// otherwise-wasted computeFee call, which may be a network request.
|
|
45254
|
+
const recipientAddress = await resolveFeeRecipient(destChain, policy, feeRecipients, params);
|
|
45255
|
+
const feeValue = await policy.computeFee(params);
|
|
43569
45256
|
return {
|
|
43570
45257
|
...params,
|
|
43571
45258
|
config: {
|
|
@@ -43595,18 +45282,35 @@ function assertSourceAccountAddresses(from) {
|
|
|
43595
45282
|
});
|
|
43596
45283
|
}
|
|
43597
45284
|
}
|
|
43598
|
-
async function mergeCustomFeeConfig(resolved, policy) {
|
|
43599
|
-
if (resolved.config?.customFee
|
|
45285
|
+
async function mergeCustomFeeConfig(resolved, policy, feeRecipients) {
|
|
45286
|
+
if (resolved.config?.customFee) {
|
|
43600
45287
|
return resolved;
|
|
43601
45288
|
}
|
|
43602
|
-
|
|
43603
|
-
|
|
45289
|
+
if (!policy) {
|
|
45290
|
+
assertFeeRecipientsHasPolicy(feeRecipients);
|
|
43604
45291
|
return resolved;
|
|
43605
45292
|
}
|
|
43606
|
-
|
|
43607
|
-
|
|
43608
|
-
|
|
43609
|
-
|
|
45293
|
+
// Skip fee resolution when there's no source chain to spend from at
|
|
45294
|
+
// all. This state can't arise from validated input today — the
|
|
45295
|
+
// caller re-checks and throws "No source chain found" right after
|
|
45296
|
+
// this returns — but skipping here isn't dead code: verified that
|
|
45297
|
+
// removing it lets a degenerate zero-allocation resolved value reach
|
|
45298
|
+
// computeFee/assertDeveloperFeeWithinBounds first, which throws a
|
|
45299
|
+
// misleading "Developer fee must be less than the total spend
|
|
45300
|
+
// amount" (0 >= 0 total allocation) instead of the correct "No
|
|
45301
|
+
// source chain found" error — or, for a real developer computeFee
|
|
45302
|
+
// that assumes a non-empty allocation, an uncaught raw exception
|
|
45303
|
+
// instead of any KitError at all. This guard exists to guarantee the
|
|
45304
|
+
// caller's clear error is what actually surfaces, not for
|
|
45305
|
+
// correctness.
|
|
45306
|
+
if (collectSourceChains(resolved).length === 0) {
|
|
45307
|
+
return resolved;
|
|
45308
|
+
}
|
|
45309
|
+
// Resolve the recipient before computing the fee: a KitError here
|
|
45310
|
+
// (missing/unresolvable recipient) shouldn't be preceded by an
|
|
45311
|
+
// otherwise-wasted computeFee call, which may be a network request.
|
|
45312
|
+
const recipientAddress = await resolveFeeRecipient(resolved.to.chain, policy, feeRecipients, resolved);
|
|
45313
|
+
const feeValue = await policy.computeFee(resolved);
|
|
43610
45314
|
return {
|
|
43611
45315
|
...resolved,
|
|
43612
45316
|
config: {
|
|
@@ -43662,14 +45366,14 @@ async function mergeCustomFeeConfig(resolved, policy) {
|
|
|
43662
45366
|
}
|
|
43663
45367
|
const destChain = resolveChainIdentifier(params.to.chain);
|
|
43664
45368
|
if (!hasExplicitAllocations(params.from)) {
|
|
43665
|
-
const merged = await mergeCustomFeePolicyForAdapterOnly(params, context.customFeePolicy);
|
|
45369
|
+
const merged = await mergeCustomFeePolicyForAdapterOnly(params, context.customFeePolicy, context.feeRecipients);
|
|
43666
45370
|
assertDeveloperFeeWithinAmount(merged);
|
|
43667
45371
|
const provider = findProviderForChain(context, normalizeToken(merged.token), destChain);
|
|
43668
45372
|
return callSpend(provider, toProviderAdapterOnlyParams(merged));
|
|
43669
45373
|
}
|
|
43670
45374
|
const resolved = await resolveSpendParams(params);
|
|
43671
45375
|
assertSpendNetworkCompatibility(resolved);
|
|
43672
|
-
const withFee = await mergeCustomFeeConfig(resolved, context.customFeePolicy);
|
|
45376
|
+
const withFee = await mergeCustomFeeConfig(resolved, context.customFeePolicy, context.feeRecipients);
|
|
43673
45377
|
assertDeveloperFeeWithinBounds(withFee);
|
|
43674
45378
|
const sourceChains = collectSourceChains(withFee);
|
|
43675
45379
|
if (sourceChains.length === 0) {
|
|
@@ -43709,14 +45413,14 @@ async function mergeCustomFeeConfig(resolved, policy) {
|
|
|
43709
45413
|
assertSpendParams(params);
|
|
43710
45414
|
const destChain = resolveChainIdentifier(params.to.chain);
|
|
43711
45415
|
if (!hasExplicitAllocations(params.from)) {
|
|
43712
|
-
const merged = await mergeCustomFeePolicyForAdapterOnly(params, context.customFeePolicy);
|
|
45416
|
+
const merged = await mergeCustomFeePolicyForAdapterOnly(params, context.customFeePolicy, context.feeRecipients);
|
|
43713
45417
|
assertDeveloperFeeWithinAmount(merged);
|
|
43714
45418
|
const provider = findProviderForChain(context, normalizeToken(merged.token), destChain);
|
|
43715
45419
|
return provider.estimateSpend(toProviderAdapterOnlyParams(merged));
|
|
43716
45420
|
}
|
|
43717
45421
|
const resolved = await resolveSpendParams(params);
|
|
43718
45422
|
assertSpendNetworkCompatibility(resolved);
|
|
43719
|
-
const withFee = await mergeCustomFeeConfig(resolved, context.customFeePolicy);
|
|
45423
|
+
const withFee = await mergeCustomFeeConfig(resolved, context.customFeePolicy, context.feeRecipients);
|
|
43720
45424
|
assertDeveloperFeeWithinBounds(withFee);
|
|
43721
45425
|
const sourceChains = collectSourceChains(withFee);
|
|
43722
45426
|
if (sourceChains.length === 0) {
|
|
@@ -44691,6 +46395,46 @@ const removeFundParamsSchema = z.object({
|
|
|
44691
46395
|
*/ removeCustomFeePolicy() {
|
|
44692
46396
|
delete this.context.customFeePolicy;
|
|
44693
46397
|
}
|
|
46398
|
+
/**
|
|
46399
|
+
* Set a declarative fee recipient map, keyed by chain type. Once set,
|
|
46400
|
+
* `spend()`/`estimateSpend()` resolve the fee recipient by looking up
|
|
46401
|
+
* the spend's destination chain type in this map — taking priority
|
|
46402
|
+
* over `customFeePolicy`'s `resolveFeeRecipientAddress` callback.
|
|
46403
|
+
*
|
|
46404
|
+
* @remarks
|
|
46405
|
+
* This only controls which address a fee is sent to — it does not by
|
|
46406
|
+
* itself cause any fee to be charged. You still need
|
|
46407
|
+
* {@link UnifiedBalanceKit.setCustomFeePolicy}'s `computeFee` to
|
|
46408
|
+
* determine the fee amount; calling `setFeeRecipients` without ever
|
|
46409
|
+
* calling `setCustomFeePolicy` throws at spend time (there is no
|
|
46410
|
+
* `computeFee` to determine an amount).
|
|
46411
|
+
*
|
|
46412
|
+
* @param config - Fee recipient addresses keyed by chain type (e.g.
|
|
46413
|
+
* `{ evm: '0x...', solana: 'Sol...' }`). Provide entries for every
|
|
46414
|
+
* chain type you expect to spend to; spending to a chain type with
|
|
46415
|
+
* no matching entry throws before any fee collection is attempted.
|
|
46416
|
+
*
|
|
46417
|
+
* @example
|
|
46418
|
+
* ```typescript
|
|
46419
|
+
* kit.setFeeRecipients({
|
|
46420
|
+
* evm: '0x1234567890123456789012345678901234567890',
|
|
46421
|
+
* solana: '9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM',
|
|
46422
|
+
* })
|
|
46423
|
+
* ```
|
|
46424
|
+
*/ setFeeRecipients(config) {
|
|
46425
|
+
assertFeeRecipientsConfig(config);
|
|
46426
|
+
this.context.feeRecipients = config;
|
|
46427
|
+
}
|
|
46428
|
+
/**
|
|
46429
|
+
* Remove the declarative fee recipient map for the kit.
|
|
46430
|
+
*
|
|
46431
|
+
* @example
|
|
46432
|
+
* ```typescript
|
|
46433
|
+
* kit.removeFeeRecipients()
|
|
46434
|
+
* ```
|
|
46435
|
+
*/ removeFeeRecipients() {
|
|
46436
|
+
delete this.context.feeRecipients;
|
|
46437
|
+
}
|
|
44694
46438
|
}
|
|
44695
46439
|
|
|
44696
46440
|
// Auto-register this kit for user agent tracking
|
|
@@ -45017,6 +46761,45 @@ registerKit(`${pkg.name}/${pkg.version}`);
|
|
|
45017
46761
|
*/ removeCustomFeePolicy() {
|
|
45018
46762
|
this.kit.removeCustomFeePolicy();
|
|
45019
46763
|
}
|
|
46764
|
+
/**
|
|
46765
|
+
* Set a declarative fee recipient map, keyed by chain type.
|
|
46766
|
+
*
|
|
46767
|
+
* Once set, `spend()`/`estimateSpend()` resolve the fee recipient by
|
|
46768
|
+
* looking up the spend's destination chain type in this map — taking
|
|
46769
|
+
* priority over `customFeePolicy`'s `resolveFeeRecipientAddress`
|
|
46770
|
+
* callback.
|
|
46771
|
+
*
|
|
46772
|
+
* @remarks
|
|
46773
|
+
* This only controls which address a fee is sent to — it does not by
|
|
46774
|
+
* itself cause any fee to be charged. You still need
|
|
46775
|
+
* `setCustomFeePolicy`'s `computeFee` to determine the fee amount;
|
|
46776
|
+
* calling `setFeeRecipients` without ever calling `setCustomFeePolicy`
|
|
46777
|
+
* throws at spend time (there is no `computeFee` to determine an
|
|
46778
|
+
* amount).
|
|
46779
|
+
*
|
|
46780
|
+
* @param config - Fee recipient addresses keyed by chain type (e.g.
|
|
46781
|
+
* `{ evm: '0x...', solana: 'Sol...' }`).
|
|
46782
|
+
*
|
|
46783
|
+
* @example
|
|
46784
|
+
* ```typescript
|
|
46785
|
+
* kit.unifiedBalance.setFeeRecipients({
|
|
46786
|
+
* evm: '0x1234567890123456789012345678901234567890',
|
|
46787
|
+
* solana: '9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM',
|
|
46788
|
+
* })
|
|
46789
|
+
* ```
|
|
46790
|
+
*/ setFeeRecipients(config) {
|
|
46791
|
+
this.kit.setFeeRecipients(config);
|
|
46792
|
+
}
|
|
46793
|
+
/**
|
|
46794
|
+
* Remove the declarative fee recipient map.
|
|
46795
|
+
*
|
|
46796
|
+
* @example
|
|
46797
|
+
* ```typescript
|
|
46798
|
+
* kit.unifiedBalance.removeFeeRecipients()
|
|
46799
|
+
* ```
|
|
46800
|
+
*/ removeFeeRecipients() {
|
|
46801
|
+
this.kit.removeFeeRecipients();
|
|
46802
|
+
}
|
|
45020
46803
|
}
|
|
45021
46804
|
|
|
45022
46805
|
/**
|
|
@@ -45168,7 +46951,8 @@ registerKit(`${pkg.name}/${pkg.version}`);
|
|
|
45168
46951
|
waitForCrossChainDeposit: async (params)=>waitForCrossChainDeposit(this.context, params),
|
|
45169
46952
|
getDepositQuote: async (params)=>getDepositQuote(this.context, params),
|
|
45170
46953
|
getWithdrawalQuote: async (params)=>getWithdrawalQuote(this.context, params),
|
|
45171
|
-
getClaimRewardsQuote: async (params)=>getClaimRewardsQuote(this.context, params)
|
|
46954
|
+
getClaimRewardsQuote: async (params)=>getClaimRewardsQuote(this.context, params),
|
|
46955
|
+
retry: async (error)=>retry(this.context, error)
|
|
45172
46956
|
};
|
|
45173
46957
|
}
|
|
45174
46958
|
/**
|
|
@@ -45427,7 +47211,8 @@ registerKit(`${pkg.name}/${pkg.version}`);
|
|
|
45427
47211
|
* or `'NOT_FOUND'`). Use {@link AppKit.waitForSwap} if you'd rather
|
|
45428
47212
|
* not write the polling loop yourself.
|
|
45429
47213
|
*
|
|
45430
|
-
* @param params - `txHash
|
|
47214
|
+
* @param params - `txHash` and `chainIn`, plus optional `chainOut` and
|
|
47215
|
+
* `kitKey`.
|
|
45431
47216
|
* @returns A snapshot of the swap's status at the time of the call.
|
|
45432
47217
|
* @throws \{KitError\} If `chainIn` or `chainOut` is malformed.
|
|
45433
47218
|
*
|
|
@@ -45541,7 +47326,7 @@ registerKit(`${pkg.name}/${pkg.version}`);
|
|
|
45541
47326
|
* translates to the chain's native sentinel address — `0xEee…` for EVM,
|
|
45542
47327
|
* `1111…` for Solana — before querying the service.
|
|
45543
47328
|
*
|
|
45544
|
-
* @param params - `chain`, optional `tokens
|
|
47329
|
+
* @param params - `chain`, plus optional `tokens` and `kitKey`.
|
|
45545
47330
|
* @returns A nested map of `[chain][address] → { priceUSD, fetchedAt }`.
|
|
45546
47331
|
* @throws \{KitError\} If `chain` is malformed, `tokens` exceeds 100
|
|
45547
47332
|
* entries, or any entry is neither a registered symbol nor a
|
|
@@ -45605,6 +47390,10 @@ registerKit(`${pkg.name}/${pkg.version}`);
|
|
|
45605
47390
|
this.context.actions.bridge[action] ??= [];
|
|
45606
47391
|
this.context.actions.bridge[action].push(typedHandler);
|
|
45607
47392
|
}
|
|
47393
|
+
if (action === '*' || action.startsWith('earn.')) {
|
|
47394
|
+
this.context.actions.earn[action] ??= [];
|
|
47395
|
+
this.context.actions.earn[action].push(typedHandler);
|
|
47396
|
+
}
|
|
45608
47397
|
}
|
|
45609
47398
|
off(actionOrWildCard, handler) {
|
|
45610
47399
|
const action = actionOrWildCard;
|
|
@@ -45614,16 +47403,32 @@ registerKit(`${pkg.name}/${pkg.version}`);
|
|
|
45614
47403
|
this.unifiedBalance.off(ubAction, typedHandler);
|
|
45615
47404
|
}
|
|
45616
47405
|
if (action === '*' || action.startsWith('bridge.')) {
|
|
45617
|
-
|
|
45618
|
-
|
|
45619
|
-
|
|
45620
|
-
|
|
45621
|
-
|
|
45622
|
-
|
|
45623
|
-
|
|
45624
|
-
|
|
45625
|
-
|
|
45626
|
-
|
|
47406
|
+
this.removeStoredActionHandler(this.context.actions.bridge, action, typedHandler);
|
|
47407
|
+
}
|
|
47408
|
+
if (action === '*' || action.startsWith('earn.')) {
|
|
47409
|
+
this.removeStoredActionHandler(this.context.actions.earn, action, typedHandler);
|
|
47410
|
+
}
|
|
47411
|
+
}
|
|
47412
|
+
/**
|
|
47413
|
+
* Remove one handler from a deferred AppKit action bucket.
|
|
47414
|
+
*
|
|
47415
|
+
* Deletes the action key when its handler list becomes empty.
|
|
47416
|
+
*
|
|
47417
|
+
* @param handlers - Action bucket to update (`bridge` or `earn`)
|
|
47418
|
+
* @param action - Stored action key, including namespace or `*`
|
|
47419
|
+
* @param handler - Handler reference previously passed to {@link on}
|
|
47420
|
+
*/ removeStoredActionHandler(handlers, action, handler) {
|
|
47421
|
+
const actionHandlers = handlers[action];
|
|
47422
|
+
if (!actionHandlers) {
|
|
47423
|
+
return;
|
|
47424
|
+
}
|
|
47425
|
+
const index = actionHandlers.indexOf(handler);
|
|
47426
|
+
if (index === -1) {
|
|
47427
|
+
return;
|
|
47428
|
+
}
|
|
47429
|
+
actionHandlers.splice(index, 1);
|
|
47430
|
+
if (actionHandlers.length === 0) {
|
|
47431
|
+
Reflect.deleteProperty(handlers, action);
|
|
45627
47432
|
}
|
|
45628
47433
|
}
|
|
45629
47434
|
}
|