@circle-fin/provider-cctp-v2 1.1.0 → 1.2.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 +11 -0
- package/index.cjs +336 -26
- package/index.d.ts +143 -4
- package/index.mjs +334 -27
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @circle-fin/provider-cctp-v2
|
|
2
2
|
|
|
3
|
+
## 1.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add attestation expiry detection utilities for CCTP v2 fast transfers:
|
|
8
|
+
- `isAttestationExpired(attestation, currentBlockNumber)` - Check if an attestation has expired based on the destination chain's current block/slot
|
|
9
|
+
- `getBlocksUntilExpiry(attestation, currentBlockNumber)` - Get the number of blocks remaining until expiry (returns `null` for attestations that never expire)
|
|
10
|
+
- `isMintFailureRelatedToAttestation(error)` - Detect if a mint failure was caused by attestation expiry to know when to call `reAttest()`
|
|
11
|
+
|
|
12
|
+
- Add native balance validation for transaction gas fees before bridge tx.
|
|
13
|
+
|
|
3
14
|
## 1.1.0
|
|
4
15
|
|
|
5
16
|
### Minor Changes
|
package/index.cjs
CHANGED
|
@@ -385,8 +385,11 @@ const ArcTestnet = defineChain({
|
|
|
385
385
|
name: 'Arc Testnet',
|
|
386
386
|
title: 'ArcTestnet',
|
|
387
387
|
nativeCurrency: {
|
|
388
|
-
name: '
|
|
389
|
-
symbol: '
|
|
388
|
+
name: 'USDC',
|
|
389
|
+
symbol: 'USDC',
|
|
390
|
+
// Arc uses native USDC with 18 decimals for gas payments (EVM standard).
|
|
391
|
+
// Note: The ERC-20 USDC contract at usdcAddress uses 6 decimals.
|
|
392
|
+
// See: https://docs.arc.network/arc/references/contract-addresses
|
|
390
393
|
decimals: 18,
|
|
391
394
|
},
|
|
392
395
|
chainId: 5042002,
|
|
@@ -3644,6 +3647,12 @@ const BalanceError = {
|
|
|
3644
3647
|
code: 9001,
|
|
3645
3648
|
name: 'BALANCE_INSUFFICIENT_TOKEN',
|
|
3646
3649
|
type: 'BALANCE',
|
|
3650
|
+
},
|
|
3651
|
+
/** Insufficient native token (ETH/SOL/etc) for gas fees */
|
|
3652
|
+
INSUFFICIENT_GAS: {
|
|
3653
|
+
code: 9002,
|
|
3654
|
+
name: 'BALANCE_INSUFFICIENT_GAS',
|
|
3655
|
+
type: 'BALANCE',
|
|
3647
3656
|
}};
|
|
3648
3657
|
|
|
3649
3658
|
/**
|
|
@@ -3866,7 +3875,7 @@ function createValidationErrorFromZod(zodError, context) {
|
|
|
3866
3875
|
*
|
|
3867
3876
|
* @param chain - The blockchain network where the balance check failed
|
|
3868
3877
|
* @param token - The token symbol (e.g., 'USDC', 'ETH')
|
|
3869
|
-
* @param
|
|
3878
|
+
* @param trace - Optional trace context to include in error (can include rawError and additional debugging data)
|
|
3870
3879
|
* @returns KitError with insufficient token balance details
|
|
3871
3880
|
*
|
|
3872
3881
|
* @example
|
|
@@ -3879,24 +3888,71 @@ function createValidationErrorFromZod(zodError, context) {
|
|
|
3879
3888
|
*
|
|
3880
3889
|
* @example
|
|
3881
3890
|
* ```typescript
|
|
3882
|
-
* // With
|
|
3891
|
+
* // With trace context for debugging
|
|
3883
3892
|
* try {
|
|
3884
3893
|
* await transfer(...)
|
|
3885
3894
|
* } catch (error) {
|
|
3886
|
-
* throw createInsufficientTokenBalanceError('Base', 'USDC',
|
|
3895
|
+
* throw createInsufficientTokenBalanceError('Base', 'USDC', {
|
|
3896
|
+
* rawError: error,
|
|
3897
|
+
* balance: '1000000',
|
|
3898
|
+
* amount: '5000000',
|
|
3899
|
+
* })
|
|
3887
3900
|
* }
|
|
3888
3901
|
* ```
|
|
3889
3902
|
*/
|
|
3890
|
-
function createInsufficientTokenBalanceError(chain, token,
|
|
3903
|
+
function createInsufficientTokenBalanceError(chain, token, trace) {
|
|
3891
3904
|
return new KitError({
|
|
3892
3905
|
...BalanceError.INSUFFICIENT_TOKEN,
|
|
3893
3906
|
recoverability: 'FATAL',
|
|
3894
3907
|
message: `Insufficient ${token} balance on ${chain}`,
|
|
3895
3908
|
cause: {
|
|
3896
3909
|
trace: {
|
|
3910
|
+
...trace,
|
|
3897
3911
|
chain,
|
|
3898
3912
|
token,
|
|
3899
|
-
|
|
3913
|
+
},
|
|
3914
|
+
},
|
|
3915
|
+
});
|
|
3916
|
+
}
|
|
3917
|
+
/**
|
|
3918
|
+
* Creates error for insufficient gas funds.
|
|
3919
|
+
*
|
|
3920
|
+
* This error is thrown when a wallet does not have enough native tokens
|
|
3921
|
+
* (ETH, SOL, etc.) to pay for transaction gas fees. The error is FATAL
|
|
3922
|
+
* as it requires user intervention to add gas funds.
|
|
3923
|
+
*
|
|
3924
|
+
* @param chain - The blockchain network where the gas check failed
|
|
3925
|
+
* @param trace - Optional trace context to include in error (can include rawError and additional debugging data)
|
|
3926
|
+
* @returns KitError with insufficient gas details
|
|
3927
|
+
*
|
|
3928
|
+
* @example
|
|
3929
|
+
* ```typescript
|
|
3930
|
+
* import { createInsufficientGasError } from '@core/errors'
|
|
3931
|
+
*
|
|
3932
|
+
* throw createInsufficientGasError('Ethereum')
|
|
3933
|
+
* // Message: "Insufficient gas funds on Ethereum"
|
|
3934
|
+
* ```
|
|
3935
|
+
*
|
|
3936
|
+
* @example
|
|
3937
|
+
* ```typescript
|
|
3938
|
+
* // With trace context for debugging
|
|
3939
|
+
* throw createInsufficientGasError('Ethereum', {
|
|
3940
|
+
* rawError: error,
|
|
3941
|
+
* gasRequired: '21000',
|
|
3942
|
+
* gasAvailable: '10000',
|
|
3943
|
+
* walletAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
|
|
3944
|
+
* })
|
|
3945
|
+
* ```
|
|
3946
|
+
*/
|
|
3947
|
+
function createInsufficientGasError(chain, trace) {
|
|
3948
|
+
return new KitError({
|
|
3949
|
+
...BalanceError.INSUFFICIENT_GAS,
|
|
3950
|
+
recoverability: 'FATAL',
|
|
3951
|
+
message: `Insufficient gas funds on ${chain}`,
|
|
3952
|
+
cause: {
|
|
3953
|
+
trace: {
|
|
3954
|
+
...trace,
|
|
3955
|
+
chain,
|
|
3900
3956
|
},
|
|
3901
3957
|
},
|
|
3902
3958
|
});
|
|
@@ -3958,6 +4014,38 @@ function isKitError(error) {
|
|
|
3958
4014
|
function isFatalError(error) {
|
|
3959
4015
|
return isKitError(error) && error.recoverability === 'FATAL';
|
|
3960
4016
|
}
|
|
4017
|
+
/**
|
|
4018
|
+
* Safely extracts error message from any error type.
|
|
4019
|
+
*
|
|
4020
|
+
* This utility handles different error types gracefully, extracting
|
|
4021
|
+
* meaningful messages from Error instances, string errors, or providing
|
|
4022
|
+
* a fallback for unknown error types. Never throws.
|
|
4023
|
+
*
|
|
4024
|
+
* @param error - Unknown error to extract message from
|
|
4025
|
+
* @returns Error message string, or fallback message
|
|
4026
|
+
*
|
|
4027
|
+
* @example
|
|
4028
|
+
* ```typescript
|
|
4029
|
+
* import { getErrorMessage } from '@core/errors'
|
|
4030
|
+
*
|
|
4031
|
+
* try {
|
|
4032
|
+
* await riskyOperation()
|
|
4033
|
+
* } catch (error) {
|
|
4034
|
+
* const message = getErrorMessage(error)
|
|
4035
|
+
* console.log('Error occurred:', message)
|
|
4036
|
+
* // Works with Error, KitError, string, or any other type
|
|
4037
|
+
* }
|
|
4038
|
+
* ```
|
|
4039
|
+
*/
|
|
4040
|
+
function getErrorMessage(error) {
|
|
4041
|
+
if (error instanceof Error) {
|
|
4042
|
+
return error.message;
|
|
4043
|
+
}
|
|
4044
|
+
if (typeof error === 'string') {
|
|
4045
|
+
return error;
|
|
4046
|
+
}
|
|
4047
|
+
return 'An unknown error occurred';
|
|
4048
|
+
}
|
|
3961
4049
|
|
|
3962
4050
|
/**
|
|
3963
4051
|
* Validates data against a Zod schema with enhanced error reporting.
|
|
@@ -5389,6 +5477,170 @@ mintAddress) => {
|
|
|
5389
5477
|
}
|
|
5390
5478
|
};
|
|
5391
5479
|
|
|
5480
|
+
/**
|
|
5481
|
+
* Converts a block number to bigint with validation.
|
|
5482
|
+
*
|
|
5483
|
+
* @param value - The block number value to convert (bigint, number, or string)
|
|
5484
|
+
* @returns The validated block number as a bigint
|
|
5485
|
+
* @throws KitError If the value is invalid (empty string, non-integer, negative, etc.)
|
|
5486
|
+
* @internal
|
|
5487
|
+
*/
|
|
5488
|
+
const toBlockNumber = (value) => {
|
|
5489
|
+
if (value === null || value === undefined) {
|
|
5490
|
+
throw createValidationFailedError('blockNumber', value, 'cannot be null or undefined');
|
|
5491
|
+
}
|
|
5492
|
+
// Empty string edge case - BigInt('') === 0n which is misleading
|
|
5493
|
+
if (value === '') {
|
|
5494
|
+
throw createValidationFailedError('blockNumber', value, 'cannot be empty string');
|
|
5495
|
+
}
|
|
5496
|
+
// For numbers, validate before BigInt conversion
|
|
5497
|
+
if (typeof value === 'number') {
|
|
5498
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) {
|
|
5499
|
+
throw createValidationFailedError('blockNumber', value, 'must be a finite integer');
|
|
5500
|
+
}
|
|
5501
|
+
}
|
|
5502
|
+
let result;
|
|
5503
|
+
try {
|
|
5504
|
+
result = BigInt(value);
|
|
5505
|
+
}
|
|
5506
|
+
catch {
|
|
5507
|
+
throw createValidationFailedError('blockNumber', value, 'cannot be converted to BigInt');
|
|
5508
|
+
}
|
|
5509
|
+
if (result < 0n) {
|
|
5510
|
+
throw createValidationFailedError('blockNumber', result.toString(), 'must be non-negative');
|
|
5511
|
+
}
|
|
5512
|
+
return result;
|
|
5513
|
+
};
|
|
5514
|
+
/**
|
|
5515
|
+
* Determines whether an attestation has expired based on the current block number.
|
|
5516
|
+
*
|
|
5517
|
+
* An attestation expires when the destination chain's current block number is greater
|
|
5518
|
+
* than or equal to the expiration block specified in the attestation message.
|
|
5519
|
+
* Slow transfers and re-attested messages have `expirationBlock: '0'` and never expire.
|
|
5520
|
+
*
|
|
5521
|
+
* @param attestation - The attestation message containing expiration block information
|
|
5522
|
+
* @param currentBlockNumber - The current block number on the destination chain (bigint, number, or string)
|
|
5523
|
+
* @returns `true` if the attestation has expired, `false` if still valid or never expires
|
|
5524
|
+
* @throws KitError If currentBlockNumber or expirationBlock is invalid
|
|
5525
|
+
*
|
|
5526
|
+
* @example
|
|
5527
|
+
* ```typescript
|
|
5528
|
+
* import { isAttestationExpired } from '@circle-fin/cctp-v2-provider'
|
|
5529
|
+
*
|
|
5530
|
+
* // Check if attestation is expired on EVM chain
|
|
5531
|
+
* const publicClient = await adapter.getPublicClient(destinationChain)
|
|
5532
|
+
* const currentBlock = await publicClient.getBlockNumber()
|
|
5533
|
+
* const expired = isAttestationExpired(attestation, currentBlock)
|
|
5534
|
+
*
|
|
5535
|
+
* if (expired) {
|
|
5536
|
+
* const freshAttestation = await provider.reAttest(source, burnTxHash)
|
|
5537
|
+
* }
|
|
5538
|
+
* ```
|
|
5539
|
+
*
|
|
5540
|
+
* @example
|
|
5541
|
+
* ```typescript
|
|
5542
|
+
* // Check on Solana
|
|
5543
|
+
* const slot = await adapter.getConnection(destinationChain).getSlot()
|
|
5544
|
+
* const expired = isAttestationExpired(attestation, slot)
|
|
5545
|
+
* ```
|
|
5546
|
+
*/
|
|
5547
|
+
const isAttestationExpired = (attestation, currentBlockNumber) => {
|
|
5548
|
+
const currentBlock = toBlockNumber(currentBlockNumber);
|
|
5549
|
+
const expiration = toBlockNumber(attestation.decodedMessage.decodedMessageBody.expirationBlock);
|
|
5550
|
+
// 0n means it never expires (re-attested messages and slow transfers)
|
|
5551
|
+
if (expiration === 0n) {
|
|
5552
|
+
return false;
|
|
5553
|
+
}
|
|
5554
|
+
// Attestation is expired if current block >= expiration block
|
|
5555
|
+
return currentBlock >= expiration;
|
|
5556
|
+
};
|
|
5557
|
+
/**
|
|
5558
|
+
* Calculates the number of blocks remaining until an attestation expires.
|
|
5559
|
+
*
|
|
5560
|
+
* Returns the difference between the expiration block and the current block number.
|
|
5561
|
+
* Returns `null` if the attestation has `expirationBlock: '0'` (never expires).
|
|
5562
|
+
* Returns `0n` or a negative bigint if the attestation has already expired.
|
|
5563
|
+
*
|
|
5564
|
+
* @param attestation - The attestation message containing expiration block information
|
|
5565
|
+
* @param currentBlockNumber - The current block number on the destination chain (bigint, number, or string)
|
|
5566
|
+
* @returns The number of blocks until expiry as a bigint, or `null` if the attestation never expires
|
|
5567
|
+
* @throws KitError If currentBlockNumber or expirationBlock is invalid
|
|
5568
|
+
*
|
|
5569
|
+
* @example
|
|
5570
|
+
* ```typescript
|
|
5571
|
+
* import { getBlocksUntilExpiry } from '@circle-fin/cctp-v2-provider'
|
|
5572
|
+
*
|
|
5573
|
+
* const publicClient = await adapter.getPublicClient(destinationChain)
|
|
5574
|
+
* const currentBlock = await publicClient.getBlockNumber()
|
|
5575
|
+
* const blocksRemaining = getBlocksUntilExpiry(attestation, currentBlock)
|
|
5576
|
+
*
|
|
5577
|
+
* if (blocksRemaining === null) {
|
|
5578
|
+
* console.log('Attestation never expires')
|
|
5579
|
+
* } else if (blocksRemaining <= 0n) {
|
|
5580
|
+
* console.log('Attestation has expired')
|
|
5581
|
+
* } else {
|
|
5582
|
+
* console.log(`${blocksRemaining} blocks until expiry`)
|
|
5583
|
+
* }
|
|
5584
|
+
* ```
|
|
5585
|
+
*/
|
|
5586
|
+
const getBlocksUntilExpiry = (attestation, currentBlockNumber) => {
|
|
5587
|
+
const currentBlock = toBlockNumber(currentBlockNumber);
|
|
5588
|
+
const expiration = toBlockNumber(attestation.decodedMessage.decodedMessageBody.expirationBlock);
|
|
5589
|
+
// 0n means it never expires (re-attested messages and slow transfers)
|
|
5590
|
+
if (expiration === 0n) {
|
|
5591
|
+
return null;
|
|
5592
|
+
}
|
|
5593
|
+
// Return the difference (can be negative if expired)
|
|
5594
|
+
return expiration - currentBlock;
|
|
5595
|
+
};
|
|
5596
|
+
/**
|
|
5597
|
+
* Determines whether a mint failure was caused by an expired attestation.
|
|
5598
|
+
*
|
|
5599
|
+
* This function inspects the error thrown during a mint operation to detect
|
|
5600
|
+
* if the failure is due to the attestation's expiration block being exceeded.
|
|
5601
|
+
* When this returns `true`, the caller should use `reAttest()` to obtain a
|
|
5602
|
+
* fresh attestation before retrying the mint.
|
|
5603
|
+
*
|
|
5604
|
+
* @param error - The error thrown during the mint operation
|
|
5605
|
+
* @returns `true` if the error indicates the attestation has expired, `false` otherwise
|
|
5606
|
+
*
|
|
5607
|
+
* @example
|
|
5608
|
+
* ```typescript
|
|
5609
|
+
* import { isMintFailureRelatedToAttestation } from '@circle-fin/cctp-v2-provider'
|
|
5610
|
+
*
|
|
5611
|
+
* try {
|
|
5612
|
+
* await mintRequest.execute()
|
|
5613
|
+
* } catch (error) {
|
|
5614
|
+
* if (isMintFailureRelatedToAttestation(error)) {
|
|
5615
|
+
* // Attestation expired - get a fresh one
|
|
5616
|
+
* const freshAttestation = await provider.reAttest(source, burnTxHash)
|
|
5617
|
+
* const newMintRequest = await provider.mint(source, destination, freshAttestation)
|
|
5618
|
+
* await newMintRequest.execute()
|
|
5619
|
+
* } else {
|
|
5620
|
+
* throw error
|
|
5621
|
+
* }
|
|
5622
|
+
* }
|
|
5623
|
+
* ```
|
|
5624
|
+
*/
|
|
5625
|
+
const isMintFailureRelatedToAttestation = (error) => {
|
|
5626
|
+
if (error === null || error === undefined) {
|
|
5627
|
+
return false;
|
|
5628
|
+
}
|
|
5629
|
+
const errorString = getErrorMessage(error).toLowerCase();
|
|
5630
|
+
// Check for Solana "MessageExpired" error pattern
|
|
5631
|
+
// Full error: "AnchorError thrown in ...handle_receive_finalized_message.rs:169.
|
|
5632
|
+
// Error Code: MessageExpired. Error Number: 6016. Error Message: Message has expired."
|
|
5633
|
+
if (errorString.includes('messageexpired')) {
|
|
5634
|
+
return true;
|
|
5635
|
+
}
|
|
5636
|
+
// Check for EVM attestation expiry errors
|
|
5637
|
+
// Contract reverts with: "Message expired and must be re-signed"
|
|
5638
|
+
if (errorString.includes('message') && errorString.includes('expired')) {
|
|
5639
|
+
return true;
|
|
5640
|
+
}
|
|
5641
|
+
return false;
|
|
5642
|
+
};
|
|
5643
|
+
|
|
5392
5644
|
/**
|
|
5393
5645
|
* Checks if a decoded attestation field matches the corresponding transfer parameter.
|
|
5394
5646
|
* If the values do not match, appends a descriptive error message to the errors array.
|
|
@@ -6232,21 +6484,64 @@ const validateBalanceForTransaction = async (params) => {
|
|
|
6232
6484
|
// Extract chain name from operationContext
|
|
6233
6485
|
const chainName = extractChainInfo(operationContext.chain).name;
|
|
6234
6486
|
// Create KitError with rich context in trace
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6487
|
+
throw createInsufficientTokenBalanceError(chainName, token, {
|
|
6488
|
+
balance: balance.toString(),
|
|
6489
|
+
amount,
|
|
6490
|
+
tokenAddress,
|
|
6491
|
+
walletAddress: operationContext.address,
|
|
6492
|
+
});
|
|
6493
|
+
}
|
|
6494
|
+
};
|
|
6495
|
+
|
|
6496
|
+
/**
|
|
6497
|
+
* Validate that the adapter has sufficient native token balance for transaction fees.
|
|
6498
|
+
*
|
|
6499
|
+
* This function checks if the adapter's current native token balance (ETH, SOL, etc.)
|
|
6500
|
+
* is greater than zero. It throws a KitError with code 9002 (BALANCE_INSUFFICIENT_GAS)
|
|
6501
|
+
* if the balance is zero, indicating the wallet cannot pay for transaction fees.
|
|
6502
|
+
*
|
|
6503
|
+
* @param params - The validation parameters containing adapter and operation context.
|
|
6504
|
+
* @returns A promise that resolves to void if validation passes.
|
|
6505
|
+
* @throws {KitError} When the adapter's native balance is zero (code: 9002).
|
|
6506
|
+
*
|
|
6507
|
+
* @example
|
|
6508
|
+
* ```typescript
|
|
6509
|
+
* import { validateNativeBalanceForTransaction } from '@core/adapter'
|
|
6510
|
+
* import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
|
|
6511
|
+
* import { isKitError, ERROR_TYPES } from '@core/errors'
|
|
6512
|
+
*
|
|
6513
|
+
* const adapter = createViemAdapterFromPrivateKey({
|
|
6514
|
+
* privateKey: '0x...',
|
|
6515
|
+
* chain: 'Ethereum',
|
|
6516
|
+
* })
|
|
6517
|
+
*
|
|
6518
|
+
* try {
|
|
6519
|
+
* await validateNativeBalanceForTransaction({
|
|
6520
|
+
* adapter,
|
|
6521
|
+
* operationContext: { chain: 'Ethereum' },
|
|
6522
|
+
* })
|
|
6523
|
+
* console.log('Native balance validation passed')
|
|
6524
|
+
* } catch (error) {
|
|
6525
|
+
* if (isKitError(error) && error.type === ERROR_TYPES.BALANCE) {
|
|
6526
|
+
* console.error('Insufficient gas funds:', error.message)
|
|
6527
|
+
* }
|
|
6528
|
+
* }
|
|
6529
|
+
* ```
|
|
6530
|
+
*/
|
|
6531
|
+
const validateNativeBalanceForTransaction = async (params) => {
|
|
6532
|
+
const { adapter, operationContext } = params;
|
|
6533
|
+
const balancePrepared = await adapter.prepareAction('native.balanceOf', {
|
|
6534
|
+
walletAddress: operationContext.address,
|
|
6535
|
+
}, operationContext);
|
|
6536
|
+
const balance = await balancePrepared.execute();
|
|
6537
|
+
if (BigInt(balance) === 0n) {
|
|
6538
|
+
// Extract chain name from operationContext
|
|
6539
|
+
const chainName = extractChainInfo(operationContext.chain).name;
|
|
6540
|
+
// Create KitError with rich context in trace
|
|
6541
|
+
throw createInsufficientGasError(chainName, {
|
|
6542
|
+
balance: '0',
|
|
6543
|
+
walletAddress: operationContext.address,
|
|
6544
|
+
});
|
|
6250
6545
|
}
|
|
6251
6546
|
};
|
|
6252
6547
|
|
|
@@ -7123,16 +7418,28 @@ class CCTPV2BridgingProvider extends BridgingProvider {
|
|
|
7123
7418
|
async bridge(params) {
|
|
7124
7419
|
// CCTP-specific bridge params validation (includes base validation)
|
|
7125
7420
|
assertCCTPv2BridgeParams(params);
|
|
7126
|
-
const { source, amount, token } = params;
|
|
7421
|
+
const { source, destination, amount, token } = params;
|
|
7127
7422
|
// Extract operation context from source wallet context for balance validation
|
|
7128
|
-
const
|
|
7129
|
-
// Validate balance for transaction
|
|
7423
|
+
const sourceOperationContext = this.extractOperationContext(source);
|
|
7424
|
+
// Validate USDC balance for transaction on source chain
|
|
7130
7425
|
await validateBalanceForTransaction({
|
|
7131
7426
|
adapter: source.adapter,
|
|
7132
7427
|
amount,
|
|
7133
7428
|
token,
|
|
7134
7429
|
tokenAddress: source.chain.usdcAddress,
|
|
7135
|
-
operationContext,
|
|
7430
|
+
operationContext: sourceOperationContext,
|
|
7431
|
+
});
|
|
7432
|
+
// Validate native balance > 0 for gas fees on source chain
|
|
7433
|
+
await validateNativeBalanceForTransaction({
|
|
7434
|
+
adapter: source.adapter,
|
|
7435
|
+
operationContext: sourceOperationContext,
|
|
7436
|
+
});
|
|
7437
|
+
// Extract operation context from destination wallet context
|
|
7438
|
+
const destinationOperationContext = this.extractOperationContext(destination);
|
|
7439
|
+
// Validate native balance > 0 for gas fees on destination chain
|
|
7440
|
+
await validateNativeBalanceForTransaction({
|
|
7441
|
+
adapter: destination.adapter,
|
|
7442
|
+
operationContext: destinationOperationContext,
|
|
7136
7443
|
});
|
|
7137
7444
|
return bridge(params, this);
|
|
7138
7445
|
}
|
|
@@ -7792,5 +8099,8 @@ class CCTPV2BridgingProvider extends BridgingProvider {
|
|
|
7792
8099
|
}
|
|
7793
8100
|
|
|
7794
8101
|
exports.CCTPV2BridgingProvider = CCTPV2BridgingProvider;
|
|
8102
|
+
exports.getBlocksUntilExpiry = getBlocksUntilExpiry;
|
|
7795
8103
|
exports.getMintRecipientAccount = getMintRecipientAccount;
|
|
8104
|
+
exports.isAttestationExpired = isAttestationExpired;
|
|
8105
|
+
exports.isMintFailureRelatedToAttestation = isMintFailureRelatedToAttestation;
|
|
7796
8106
|
//# sourceMappingURL=index.cjs.map
|
package/index.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { Abi } from 'abitype';
|
|
20
|
-
import { TransactionInstruction, Signer } from '@solana/web3.js';
|
|
20
|
+
import { TransactionInstruction, Signer, AddressLookupTableAccount } from '@solana/web3.js';
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* @packageDocumentation
|
|
@@ -644,14 +644,26 @@ interface SolanaPreparedChainRequestParams {
|
|
|
644
644
|
*/
|
|
645
645
|
instructions: TransactionInstruction[];
|
|
646
646
|
/**
|
|
647
|
-
* Additional signers besides the Adapter
|
|
647
|
+
* Additional signers besides the Adapter's wallet (e.g. program-derived authorities).
|
|
648
648
|
*/
|
|
649
649
|
signers?: Signer[];
|
|
650
650
|
/**
|
|
651
651
|
* Optional override for how many compute units this transaction may consume.
|
|
652
|
-
* If omitted, the network
|
|
652
|
+
* If omitted, the network's default compute budget applies.
|
|
653
653
|
*/
|
|
654
654
|
computeUnitLimit?: number;
|
|
655
|
+
/**
|
|
656
|
+
* Optional Address Lookup Table accounts for transaction compression.
|
|
657
|
+
* Used to reduce transaction size by compressing frequently-used addresses.
|
|
658
|
+
* This is used by @solana/web3.js adapters that have already fetched the ALT data.
|
|
659
|
+
*/
|
|
660
|
+
addressLookupTableAccounts?: AddressLookupTableAccount[];
|
|
661
|
+
/**
|
|
662
|
+
* Optional Address Lookup Table addresses for transaction compression.
|
|
663
|
+
* Used by adapters that need to fetch ALT data themselves (e.g., @solana/kit adapters).
|
|
664
|
+
* These are base58-encoded addresses of ALT accounts to use for compression.
|
|
665
|
+
*/
|
|
666
|
+
addressLookupTableAddresses?: string[];
|
|
655
667
|
}
|
|
656
668
|
/**
|
|
657
669
|
* Solana-specific configuration for transaction estimation.
|
|
@@ -1333,6 +1345,34 @@ interface CCTPActionMap {
|
|
|
1333
1345
|
readonly v2: CCTPv2ActionMap;
|
|
1334
1346
|
}
|
|
1335
1347
|
|
|
1348
|
+
/**
|
|
1349
|
+
* Action map for native token operations (ETH, SOL, MATIC, etc.).
|
|
1350
|
+
*
|
|
1351
|
+
* Native tokens are the primary currency of each blockchain network,
|
|
1352
|
+
* used for paying transaction fees and as a store of value.
|
|
1353
|
+
* These actions operate on the native token without requiring
|
|
1354
|
+
* a separate token contract address.
|
|
1355
|
+
*
|
|
1356
|
+
* @remarks
|
|
1357
|
+
* Native token operations differ from ERC-20/SPL token operations
|
|
1358
|
+
* in that they don't require contract interactions for basic transfers
|
|
1359
|
+
* and balance checks.
|
|
1360
|
+
*
|
|
1361
|
+
* @see {@link ActionMap} for the complete action structure
|
|
1362
|
+
*/
|
|
1363
|
+
interface NativeActionMap {
|
|
1364
|
+
/**
|
|
1365
|
+
* Get the native token balance (SOL, ETH, etc.) for a wallet address.
|
|
1366
|
+
*/
|
|
1367
|
+
balanceOf: ActionParameters & {
|
|
1368
|
+
/**
|
|
1369
|
+
* The address to check the native balance for. If not provided, it will be
|
|
1370
|
+
* automatically derived from the adapter context.
|
|
1371
|
+
*/
|
|
1372
|
+
walletAddress?: string | undefined;
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1336
1376
|
interface TokenActionMap {
|
|
1337
1377
|
/**
|
|
1338
1378
|
* Set an allowance for a delegate to spend tokens on behalf of the wallet.
|
|
@@ -1564,6 +1604,8 @@ interface USDCActionMap {
|
|
|
1564
1604
|
interface ActionMap {
|
|
1565
1605
|
/** CCTP-specific operations with automatic address resolution. */
|
|
1566
1606
|
readonly cctp: CCTPActionMap;
|
|
1607
|
+
/** Native token operations (ETH, SOL, MATIC, etc.). */
|
|
1608
|
+
readonly native: NativeActionMap;
|
|
1567
1609
|
/** General token operations requiring explicit token addresses. */
|
|
1568
1610
|
readonly token: TokenActionMap;
|
|
1569
1611
|
/** USDC-specific operations with automatic address resolution. */
|
|
@@ -3917,5 +3959,102 @@ rawAddress: string,
|
|
|
3917
3959
|
/** The USDC mint address (ignored for EVM chains, required base58 address for Solana) */
|
|
3918
3960
|
mintAddress: string) => Promise<string>;
|
|
3919
3961
|
|
|
3920
|
-
|
|
3962
|
+
/** A block number value that can be provided as bigint, number, or string. */
|
|
3963
|
+
type BlockNumberInput = bigint | number | string | undefined;
|
|
3964
|
+
/**
|
|
3965
|
+
* Determines whether an attestation has expired based on the current block number.
|
|
3966
|
+
*
|
|
3967
|
+
* An attestation expires when the destination chain's current block number is greater
|
|
3968
|
+
* than or equal to the expiration block specified in the attestation message.
|
|
3969
|
+
* Slow transfers and re-attested messages have `expirationBlock: '0'` and never expire.
|
|
3970
|
+
*
|
|
3971
|
+
* @param attestation - The attestation message containing expiration block information
|
|
3972
|
+
* @param currentBlockNumber - The current block number on the destination chain (bigint, number, or string)
|
|
3973
|
+
* @returns `true` if the attestation has expired, `false` if still valid or never expires
|
|
3974
|
+
* @throws KitError If currentBlockNumber or expirationBlock is invalid
|
|
3975
|
+
*
|
|
3976
|
+
* @example
|
|
3977
|
+
* ```typescript
|
|
3978
|
+
* import { isAttestationExpired } from '@circle-fin/cctp-v2-provider'
|
|
3979
|
+
*
|
|
3980
|
+
* // Check if attestation is expired on EVM chain
|
|
3981
|
+
* const publicClient = await adapter.getPublicClient(destinationChain)
|
|
3982
|
+
* const currentBlock = await publicClient.getBlockNumber()
|
|
3983
|
+
* const expired = isAttestationExpired(attestation, currentBlock)
|
|
3984
|
+
*
|
|
3985
|
+
* if (expired) {
|
|
3986
|
+
* const freshAttestation = await provider.reAttest(source, burnTxHash)
|
|
3987
|
+
* }
|
|
3988
|
+
* ```
|
|
3989
|
+
*
|
|
3990
|
+
* @example
|
|
3991
|
+
* ```typescript
|
|
3992
|
+
* // Check on Solana
|
|
3993
|
+
* const slot = await adapter.getConnection(destinationChain).getSlot()
|
|
3994
|
+
* const expired = isAttestationExpired(attestation, slot)
|
|
3995
|
+
* ```
|
|
3996
|
+
*/
|
|
3997
|
+
declare const isAttestationExpired: (attestation: AttestationMessage, currentBlockNumber: BlockNumberInput) => boolean;
|
|
3998
|
+
/**
|
|
3999
|
+
* Calculates the number of blocks remaining until an attestation expires.
|
|
4000
|
+
*
|
|
4001
|
+
* Returns the difference between the expiration block and the current block number.
|
|
4002
|
+
* Returns `null` if the attestation has `expirationBlock: '0'` (never expires).
|
|
4003
|
+
* Returns `0n` or a negative bigint if the attestation has already expired.
|
|
4004
|
+
*
|
|
4005
|
+
* @param attestation - The attestation message containing expiration block information
|
|
4006
|
+
* @param currentBlockNumber - The current block number on the destination chain (bigint, number, or string)
|
|
4007
|
+
* @returns The number of blocks until expiry as a bigint, or `null` if the attestation never expires
|
|
4008
|
+
* @throws KitError If currentBlockNumber or expirationBlock is invalid
|
|
4009
|
+
*
|
|
4010
|
+
* @example
|
|
4011
|
+
* ```typescript
|
|
4012
|
+
* import { getBlocksUntilExpiry } from '@circle-fin/cctp-v2-provider'
|
|
4013
|
+
*
|
|
4014
|
+
* const publicClient = await adapter.getPublicClient(destinationChain)
|
|
4015
|
+
* const currentBlock = await publicClient.getBlockNumber()
|
|
4016
|
+
* const blocksRemaining = getBlocksUntilExpiry(attestation, currentBlock)
|
|
4017
|
+
*
|
|
4018
|
+
* if (blocksRemaining === null) {
|
|
4019
|
+
* console.log('Attestation never expires')
|
|
4020
|
+
* } else if (blocksRemaining <= 0n) {
|
|
4021
|
+
* console.log('Attestation has expired')
|
|
4022
|
+
* } else {
|
|
4023
|
+
* console.log(`${blocksRemaining} blocks until expiry`)
|
|
4024
|
+
* }
|
|
4025
|
+
* ```
|
|
4026
|
+
*/
|
|
4027
|
+
declare const getBlocksUntilExpiry: (attestation: AttestationMessage, currentBlockNumber: BlockNumberInput) => bigint | null;
|
|
4028
|
+
/**
|
|
4029
|
+
* Determines whether a mint failure was caused by an expired attestation.
|
|
4030
|
+
*
|
|
4031
|
+
* This function inspects the error thrown during a mint operation to detect
|
|
4032
|
+
* if the failure is due to the attestation's expiration block being exceeded.
|
|
4033
|
+
* When this returns `true`, the caller should use `reAttest()` to obtain a
|
|
4034
|
+
* fresh attestation before retrying the mint.
|
|
4035
|
+
*
|
|
4036
|
+
* @param error - The error thrown during the mint operation
|
|
4037
|
+
* @returns `true` if the error indicates the attestation has expired, `false` otherwise
|
|
4038
|
+
*
|
|
4039
|
+
* @example
|
|
4040
|
+
* ```typescript
|
|
4041
|
+
* import { isMintFailureRelatedToAttestation } from '@circle-fin/cctp-v2-provider'
|
|
4042
|
+
*
|
|
4043
|
+
* try {
|
|
4044
|
+
* await mintRequest.execute()
|
|
4045
|
+
* } catch (error) {
|
|
4046
|
+
* if (isMintFailureRelatedToAttestation(error)) {
|
|
4047
|
+
* // Attestation expired - get a fresh one
|
|
4048
|
+
* const freshAttestation = await provider.reAttest(source, burnTxHash)
|
|
4049
|
+
* const newMintRequest = await provider.mint(source, destination, freshAttestation)
|
|
4050
|
+
* await newMintRequest.execute()
|
|
4051
|
+
* } else {
|
|
4052
|
+
* throw error
|
|
4053
|
+
* }
|
|
4054
|
+
* }
|
|
4055
|
+
* ```
|
|
4056
|
+
*/
|
|
4057
|
+
declare const isMintFailureRelatedToAttestation: (error: unknown) => boolean;
|
|
4058
|
+
|
|
4059
|
+
export { CCTPV2BridgingProvider, getBlocksUntilExpiry, getMintRecipientAccount, isAttestationExpired, isMintFailureRelatedToAttestation };
|
|
3921
4060
|
export type { CCTPV2Config };
|
package/index.mjs
CHANGED
|
@@ -379,8 +379,11 @@ const ArcTestnet = defineChain({
|
|
|
379
379
|
name: 'Arc Testnet',
|
|
380
380
|
title: 'ArcTestnet',
|
|
381
381
|
nativeCurrency: {
|
|
382
|
-
name: '
|
|
383
|
-
symbol: '
|
|
382
|
+
name: 'USDC',
|
|
383
|
+
symbol: 'USDC',
|
|
384
|
+
// Arc uses native USDC with 18 decimals for gas payments (EVM standard).
|
|
385
|
+
// Note: The ERC-20 USDC contract at usdcAddress uses 6 decimals.
|
|
386
|
+
// See: https://docs.arc.network/arc/references/contract-addresses
|
|
384
387
|
decimals: 18,
|
|
385
388
|
},
|
|
386
389
|
chainId: 5042002,
|
|
@@ -3638,6 +3641,12 @@ const BalanceError = {
|
|
|
3638
3641
|
code: 9001,
|
|
3639
3642
|
name: 'BALANCE_INSUFFICIENT_TOKEN',
|
|
3640
3643
|
type: 'BALANCE',
|
|
3644
|
+
},
|
|
3645
|
+
/** Insufficient native token (ETH/SOL/etc) for gas fees */
|
|
3646
|
+
INSUFFICIENT_GAS: {
|
|
3647
|
+
code: 9002,
|
|
3648
|
+
name: 'BALANCE_INSUFFICIENT_GAS',
|
|
3649
|
+
type: 'BALANCE',
|
|
3641
3650
|
}};
|
|
3642
3651
|
|
|
3643
3652
|
/**
|
|
@@ -3860,7 +3869,7 @@ function createValidationErrorFromZod(zodError, context) {
|
|
|
3860
3869
|
*
|
|
3861
3870
|
* @param chain - The blockchain network where the balance check failed
|
|
3862
3871
|
* @param token - The token symbol (e.g., 'USDC', 'ETH')
|
|
3863
|
-
* @param
|
|
3872
|
+
* @param trace - Optional trace context to include in error (can include rawError and additional debugging data)
|
|
3864
3873
|
* @returns KitError with insufficient token balance details
|
|
3865
3874
|
*
|
|
3866
3875
|
* @example
|
|
@@ -3873,24 +3882,71 @@ function createValidationErrorFromZod(zodError, context) {
|
|
|
3873
3882
|
*
|
|
3874
3883
|
* @example
|
|
3875
3884
|
* ```typescript
|
|
3876
|
-
* // With
|
|
3885
|
+
* // With trace context for debugging
|
|
3877
3886
|
* try {
|
|
3878
3887
|
* await transfer(...)
|
|
3879
3888
|
* } catch (error) {
|
|
3880
|
-
* throw createInsufficientTokenBalanceError('Base', 'USDC',
|
|
3889
|
+
* throw createInsufficientTokenBalanceError('Base', 'USDC', {
|
|
3890
|
+
* rawError: error,
|
|
3891
|
+
* balance: '1000000',
|
|
3892
|
+
* amount: '5000000',
|
|
3893
|
+
* })
|
|
3881
3894
|
* }
|
|
3882
3895
|
* ```
|
|
3883
3896
|
*/
|
|
3884
|
-
function createInsufficientTokenBalanceError(chain, token,
|
|
3897
|
+
function createInsufficientTokenBalanceError(chain, token, trace) {
|
|
3885
3898
|
return new KitError({
|
|
3886
3899
|
...BalanceError.INSUFFICIENT_TOKEN,
|
|
3887
3900
|
recoverability: 'FATAL',
|
|
3888
3901
|
message: `Insufficient ${token} balance on ${chain}`,
|
|
3889
3902
|
cause: {
|
|
3890
3903
|
trace: {
|
|
3904
|
+
...trace,
|
|
3891
3905
|
chain,
|
|
3892
3906
|
token,
|
|
3893
|
-
|
|
3907
|
+
},
|
|
3908
|
+
},
|
|
3909
|
+
});
|
|
3910
|
+
}
|
|
3911
|
+
/**
|
|
3912
|
+
* Creates error for insufficient gas funds.
|
|
3913
|
+
*
|
|
3914
|
+
* This error is thrown when a wallet does not have enough native tokens
|
|
3915
|
+
* (ETH, SOL, etc.) to pay for transaction gas fees. The error is FATAL
|
|
3916
|
+
* as it requires user intervention to add gas funds.
|
|
3917
|
+
*
|
|
3918
|
+
* @param chain - The blockchain network where the gas check failed
|
|
3919
|
+
* @param trace - Optional trace context to include in error (can include rawError and additional debugging data)
|
|
3920
|
+
* @returns KitError with insufficient gas details
|
|
3921
|
+
*
|
|
3922
|
+
* @example
|
|
3923
|
+
* ```typescript
|
|
3924
|
+
* import { createInsufficientGasError } from '@core/errors'
|
|
3925
|
+
*
|
|
3926
|
+
* throw createInsufficientGasError('Ethereum')
|
|
3927
|
+
* // Message: "Insufficient gas funds on Ethereum"
|
|
3928
|
+
* ```
|
|
3929
|
+
*
|
|
3930
|
+
* @example
|
|
3931
|
+
* ```typescript
|
|
3932
|
+
* // With trace context for debugging
|
|
3933
|
+
* throw createInsufficientGasError('Ethereum', {
|
|
3934
|
+
* rawError: error,
|
|
3935
|
+
* gasRequired: '21000',
|
|
3936
|
+
* gasAvailable: '10000',
|
|
3937
|
+
* walletAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
|
|
3938
|
+
* })
|
|
3939
|
+
* ```
|
|
3940
|
+
*/
|
|
3941
|
+
function createInsufficientGasError(chain, trace) {
|
|
3942
|
+
return new KitError({
|
|
3943
|
+
...BalanceError.INSUFFICIENT_GAS,
|
|
3944
|
+
recoverability: 'FATAL',
|
|
3945
|
+
message: `Insufficient gas funds on ${chain}`,
|
|
3946
|
+
cause: {
|
|
3947
|
+
trace: {
|
|
3948
|
+
...trace,
|
|
3949
|
+
chain,
|
|
3894
3950
|
},
|
|
3895
3951
|
},
|
|
3896
3952
|
});
|
|
@@ -3952,6 +4008,38 @@ function isKitError(error) {
|
|
|
3952
4008
|
function isFatalError(error) {
|
|
3953
4009
|
return isKitError(error) && error.recoverability === 'FATAL';
|
|
3954
4010
|
}
|
|
4011
|
+
/**
|
|
4012
|
+
* Safely extracts error message from any error type.
|
|
4013
|
+
*
|
|
4014
|
+
* This utility handles different error types gracefully, extracting
|
|
4015
|
+
* meaningful messages from Error instances, string errors, or providing
|
|
4016
|
+
* a fallback for unknown error types. Never throws.
|
|
4017
|
+
*
|
|
4018
|
+
* @param error - Unknown error to extract message from
|
|
4019
|
+
* @returns Error message string, or fallback message
|
|
4020
|
+
*
|
|
4021
|
+
* @example
|
|
4022
|
+
* ```typescript
|
|
4023
|
+
* import { getErrorMessage } from '@core/errors'
|
|
4024
|
+
*
|
|
4025
|
+
* try {
|
|
4026
|
+
* await riskyOperation()
|
|
4027
|
+
* } catch (error) {
|
|
4028
|
+
* const message = getErrorMessage(error)
|
|
4029
|
+
* console.log('Error occurred:', message)
|
|
4030
|
+
* // Works with Error, KitError, string, or any other type
|
|
4031
|
+
* }
|
|
4032
|
+
* ```
|
|
4033
|
+
*/
|
|
4034
|
+
function getErrorMessage(error) {
|
|
4035
|
+
if (error instanceof Error) {
|
|
4036
|
+
return error.message;
|
|
4037
|
+
}
|
|
4038
|
+
if (typeof error === 'string') {
|
|
4039
|
+
return error;
|
|
4040
|
+
}
|
|
4041
|
+
return 'An unknown error occurred';
|
|
4042
|
+
}
|
|
3955
4043
|
|
|
3956
4044
|
/**
|
|
3957
4045
|
* Validates data against a Zod schema with enhanced error reporting.
|
|
@@ -5383,6 +5471,170 @@ mintAddress) => {
|
|
|
5383
5471
|
}
|
|
5384
5472
|
};
|
|
5385
5473
|
|
|
5474
|
+
/**
|
|
5475
|
+
* Converts a block number to bigint with validation.
|
|
5476
|
+
*
|
|
5477
|
+
* @param value - The block number value to convert (bigint, number, or string)
|
|
5478
|
+
* @returns The validated block number as a bigint
|
|
5479
|
+
* @throws KitError If the value is invalid (empty string, non-integer, negative, etc.)
|
|
5480
|
+
* @internal
|
|
5481
|
+
*/
|
|
5482
|
+
const toBlockNumber = (value) => {
|
|
5483
|
+
if (value === null || value === undefined) {
|
|
5484
|
+
throw createValidationFailedError('blockNumber', value, 'cannot be null or undefined');
|
|
5485
|
+
}
|
|
5486
|
+
// Empty string edge case - BigInt('') === 0n which is misleading
|
|
5487
|
+
if (value === '') {
|
|
5488
|
+
throw createValidationFailedError('blockNumber', value, 'cannot be empty string');
|
|
5489
|
+
}
|
|
5490
|
+
// For numbers, validate before BigInt conversion
|
|
5491
|
+
if (typeof value === 'number') {
|
|
5492
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) {
|
|
5493
|
+
throw createValidationFailedError('blockNumber', value, 'must be a finite integer');
|
|
5494
|
+
}
|
|
5495
|
+
}
|
|
5496
|
+
let result;
|
|
5497
|
+
try {
|
|
5498
|
+
result = BigInt(value);
|
|
5499
|
+
}
|
|
5500
|
+
catch {
|
|
5501
|
+
throw createValidationFailedError('blockNumber', value, 'cannot be converted to BigInt');
|
|
5502
|
+
}
|
|
5503
|
+
if (result < 0n) {
|
|
5504
|
+
throw createValidationFailedError('blockNumber', result.toString(), 'must be non-negative');
|
|
5505
|
+
}
|
|
5506
|
+
return result;
|
|
5507
|
+
};
|
|
5508
|
+
/**
|
|
5509
|
+
* Determines whether an attestation has expired based on the current block number.
|
|
5510
|
+
*
|
|
5511
|
+
* An attestation expires when the destination chain's current block number is greater
|
|
5512
|
+
* than or equal to the expiration block specified in the attestation message.
|
|
5513
|
+
* Slow transfers and re-attested messages have `expirationBlock: '0'` and never expire.
|
|
5514
|
+
*
|
|
5515
|
+
* @param attestation - The attestation message containing expiration block information
|
|
5516
|
+
* @param currentBlockNumber - The current block number on the destination chain (bigint, number, or string)
|
|
5517
|
+
* @returns `true` if the attestation has expired, `false` if still valid or never expires
|
|
5518
|
+
* @throws KitError If currentBlockNumber or expirationBlock is invalid
|
|
5519
|
+
*
|
|
5520
|
+
* @example
|
|
5521
|
+
* ```typescript
|
|
5522
|
+
* import { isAttestationExpired } from '@circle-fin/cctp-v2-provider'
|
|
5523
|
+
*
|
|
5524
|
+
* // Check if attestation is expired on EVM chain
|
|
5525
|
+
* const publicClient = await adapter.getPublicClient(destinationChain)
|
|
5526
|
+
* const currentBlock = await publicClient.getBlockNumber()
|
|
5527
|
+
* const expired = isAttestationExpired(attestation, currentBlock)
|
|
5528
|
+
*
|
|
5529
|
+
* if (expired) {
|
|
5530
|
+
* const freshAttestation = await provider.reAttest(source, burnTxHash)
|
|
5531
|
+
* }
|
|
5532
|
+
* ```
|
|
5533
|
+
*
|
|
5534
|
+
* @example
|
|
5535
|
+
* ```typescript
|
|
5536
|
+
* // Check on Solana
|
|
5537
|
+
* const slot = await adapter.getConnection(destinationChain).getSlot()
|
|
5538
|
+
* const expired = isAttestationExpired(attestation, slot)
|
|
5539
|
+
* ```
|
|
5540
|
+
*/
|
|
5541
|
+
const isAttestationExpired = (attestation, currentBlockNumber) => {
|
|
5542
|
+
const currentBlock = toBlockNumber(currentBlockNumber);
|
|
5543
|
+
const expiration = toBlockNumber(attestation.decodedMessage.decodedMessageBody.expirationBlock);
|
|
5544
|
+
// 0n means it never expires (re-attested messages and slow transfers)
|
|
5545
|
+
if (expiration === 0n) {
|
|
5546
|
+
return false;
|
|
5547
|
+
}
|
|
5548
|
+
// Attestation is expired if current block >= expiration block
|
|
5549
|
+
return currentBlock >= expiration;
|
|
5550
|
+
};
|
|
5551
|
+
/**
|
|
5552
|
+
* Calculates the number of blocks remaining until an attestation expires.
|
|
5553
|
+
*
|
|
5554
|
+
* Returns the difference between the expiration block and the current block number.
|
|
5555
|
+
* Returns `null` if the attestation has `expirationBlock: '0'` (never expires).
|
|
5556
|
+
* Returns `0n` or a negative bigint if the attestation has already expired.
|
|
5557
|
+
*
|
|
5558
|
+
* @param attestation - The attestation message containing expiration block information
|
|
5559
|
+
* @param currentBlockNumber - The current block number on the destination chain (bigint, number, or string)
|
|
5560
|
+
* @returns The number of blocks until expiry as a bigint, or `null` if the attestation never expires
|
|
5561
|
+
* @throws KitError If currentBlockNumber or expirationBlock is invalid
|
|
5562
|
+
*
|
|
5563
|
+
* @example
|
|
5564
|
+
* ```typescript
|
|
5565
|
+
* import { getBlocksUntilExpiry } from '@circle-fin/cctp-v2-provider'
|
|
5566
|
+
*
|
|
5567
|
+
* const publicClient = await adapter.getPublicClient(destinationChain)
|
|
5568
|
+
* const currentBlock = await publicClient.getBlockNumber()
|
|
5569
|
+
* const blocksRemaining = getBlocksUntilExpiry(attestation, currentBlock)
|
|
5570
|
+
*
|
|
5571
|
+
* if (blocksRemaining === null) {
|
|
5572
|
+
* console.log('Attestation never expires')
|
|
5573
|
+
* } else if (blocksRemaining <= 0n) {
|
|
5574
|
+
* console.log('Attestation has expired')
|
|
5575
|
+
* } else {
|
|
5576
|
+
* console.log(`${blocksRemaining} blocks until expiry`)
|
|
5577
|
+
* }
|
|
5578
|
+
* ```
|
|
5579
|
+
*/
|
|
5580
|
+
const getBlocksUntilExpiry = (attestation, currentBlockNumber) => {
|
|
5581
|
+
const currentBlock = toBlockNumber(currentBlockNumber);
|
|
5582
|
+
const expiration = toBlockNumber(attestation.decodedMessage.decodedMessageBody.expirationBlock);
|
|
5583
|
+
// 0n means it never expires (re-attested messages and slow transfers)
|
|
5584
|
+
if (expiration === 0n) {
|
|
5585
|
+
return null;
|
|
5586
|
+
}
|
|
5587
|
+
// Return the difference (can be negative if expired)
|
|
5588
|
+
return expiration - currentBlock;
|
|
5589
|
+
};
|
|
5590
|
+
/**
|
|
5591
|
+
* Determines whether a mint failure was caused by an expired attestation.
|
|
5592
|
+
*
|
|
5593
|
+
* This function inspects the error thrown during a mint operation to detect
|
|
5594
|
+
* if the failure is due to the attestation's expiration block being exceeded.
|
|
5595
|
+
* When this returns `true`, the caller should use `reAttest()` to obtain a
|
|
5596
|
+
* fresh attestation before retrying the mint.
|
|
5597
|
+
*
|
|
5598
|
+
* @param error - The error thrown during the mint operation
|
|
5599
|
+
* @returns `true` if the error indicates the attestation has expired, `false` otherwise
|
|
5600
|
+
*
|
|
5601
|
+
* @example
|
|
5602
|
+
* ```typescript
|
|
5603
|
+
* import { isMintFailureRelatedToAttestation } from '@circle-fin/cctp-v2-provider'
|
|
5604
|
+
*
|
|
5605
|
+
* try {
|
|
5606
|
+
* await mintRequest.execute()
|
|
5607
|
+
* } catch (error) {
|
|
5608
|
+
* if (isMintFailureRelatedToAttestation(error)) {
|
|
5609
|
+
* // Attestation expired - get a fresh one
|
|
5610
|
+
* const freshAttestation = await provider.reAttest(source, burnTxHash)
|
|
5611
|
+
* const newMintRequest = await provider.mint(source, destination, freshAttestation)
|
|
5612
|
+
* await newMintRequest.execute()
|
|
5613
|
+
* } else {
|
|
5614
|
+
* throw error
|
|
5615
|
+
* }
|
|
5616
|
+
* }
|
|
5617
|
+
* ```
|
|
5618
|
+
*/
|
|
5619
|
+
const isMintFailureRelatedToAttestation = (error) => {
|
|
5620
|
+
if (error === null || error === undefined) {
|
|
5621
|
+
return false;
|
|
5622
|
+
}
|
|
5623
|
+
const errorString = getErrorMessage(error).toLowerCase();
|
|
5624
|
+
// Check for Solana "MessageExpired" error pattern
|
|
5625
|
+
// Full error: "AnchorError thrown in ...handle_receive_finalized_message.rs:169.
|
|
5626
|
+
// Error Code: MessageExpired. Error Number: 6016. Error Message: Message has expired."
|
|
5627
|
+
if (errorString.includes('messageexpired')) {
|
|
5628
|
+
return true;
|
|
5629
|
+
}
|
|
5630
|
+
// Check for EVM attestation expiry errors
|
|
5631
|
+
// Contract reverts with: "Message expired and must be re-signed"
|
|
5632
|
+
if (errorString.includes('message') && errorString.includes('expired')) {
|
|
5633
|
+
return true;
|
|
5634
|
+
}
|
|
5635
|
+
return false;
|
|
5636
|
+
};
|
|
5637
|
+
|
|
5386
5638
|
/**
|
|
5387
5639
|
* Checks if a decoded attestation field matches the corresponding transfer parameter.
|
|
5388
5640
|
* If the values do not match, appends a descriptive error message to the errors array.
|
|
@@ -6226,21 +6478,64 @@ const validateBalanceForTransaction = async (params) => {
|
|
|
6226
6478
|
// Extract chain name from operationContext
|
|
6227
6479
|
const chainName = extractChainInfo(operationContext.chain).name;
|
|
6228
6480
|
// Create KitError with rich context in trace
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6481
|
+
throw createInsufficientTokenBalanceError(chainName, token, {
|
|
6482
|
+
balance: balance.toString(),
|
|
6483
|
+
amount,
|
|
6484
|
+
tokenAddress,
|
|
6485
|
+
walletAddress: operationContext.address,
|
|
6486
|
+
});
|
|
6487
|
+
}
|
|
6488
|
+
};
|
|
6489
|
+
|
|
6490
|
+
/**
|
|
6491
|
+
* Validate that the adapter has sufficient native token balance for transaction fees.
|
|
6492
|
+
*
|
|
6493
|
+
* This function checks if the adapter's current native token balance (ETH, SOL, etc.)
|
|
6494
|
+
* is greater than zero. It throws a KitError with code 9002 (BALANCE_INSUFFICIENT_GAS)
|
|
6495
|
+
* if the balance is zero, indicating the wallet cannot pay for transaction fees.
|
|
6496
|
+
*
|
|
6497
|
+
* @param params - The validation parameters containing adapter and operation context.
|
|
6498
|
+
* @returns A promise that resolves to void if validation passes.
|
|
6499
|
+
* @throws {KitError} When the adapter's native balance is zero (code: 9002).
|
|
6500
|
+
*
|
|
6501
|
+
* @example
|
|
6502
|
+
* ```typescript
|
|
6503
|
+
* import { validateNativeBalanceForTransaction } from '@core/adapter'
|
|
6504
|
+
* import { createViemAdapterFromPrivateKey } from '@circle-fin/adapter-viem-v2'
|
|
6505
|
+
* import { isKitError, ERROR_TYPES } from '@core/errors'
|
|
6506
|
+
*
|
|
6507
|
+
* const adapter = createViemAdapterFromPrivateKey({
|
|
6508
|
+
* privateKey: '0x...',
|
|
6509
|
+
* chain: 'Ethereum',
|
|
6510
|
+
* })
|
|
6511
|
+
*
|
|
6512
|
+
* try {
|
|
6513
|
+
* await validateNativeBalanceForTransaction({
|
|
6514
|
+
* adapter,
|
|
6515
|
+
* operationContext: { chain: 'Ethereum' },
|
|
6516
|
+
* })
|
|
6517
|
+
* console.log('Native balance validation passed')
|
|
6518
|
+
* } catch (error) {
|
|
6519
|
+
* if (isKitError(error) && error.type === ERROR_TYPES.BALANCE) {
|
|
6520
|
+
* console.error('Insufficient gas funds:', error.message)
|
|
6521
|
+
* }
|
|
6522
|
+
* }
|
|
6523
|
+
* ```
|
|
6524
|
+
*/
|
|
6525
|
+
const validateNativeBalanceForTransaction = async (params) => {
|
|
6526
|
+
const { adapter, operationContext } = params;
|
|
6527
|
+
const balancePrepared = await adapter.prepareAction('native.balanceOf', {
|
|
6528
|
+
walletAddress: operationContext.address,
|
|
6529
|
+
}, operationContext);
|
|
6530
|
+
const balance = await balancePrepared.execute();
|
|
6531
|
+
if (BigInt(balance) === 0n) {
|
|
6532
|
+
// Extract chain name from operationContext
|
|
6533
|
+
const chainName = extractChainInfo(operationContext.chain).name;
|
|
6534
|
+
// Create KitError with rich context in trace
|
|
6535
|
+
throw createInsufficientGasError(chainName, {
|
|
6536
|
+
balance: '0',
|
|
6537
|
+
walletAddress: operationContext.address,
|
|
6538
|
+
});
|
|
6244
6539
|
}
|
|
6245
6540
|
};
|
|
6246
6541
|
|
|
@@ -7117,16 +7412,28 @@ class CCTPV2BridgingProvider extends BridgingProvider {
|
|
|
7117
7412
|
async bridge(params) {
|
|
7118
7413
|
// CCTP-specific bridge params validation (includes base validation)
|
|
7119
7414
|
assertCCTPv2BridgeParams(params);
|
|
7120
|
-
const { source, amount, token } = params;
|
|
7415
|
+
const { source, destination, amount, token } = params;
|
|
7121
7416
|
// Extract operation context from source wallet context for balance validation
|
|
7122
|
-
const
|
|
7123
|
-
// Validate balance for transaction
|
|
7417
|
+
const sourceOperationContext = this.extractOperationContext(source);
|
|
7418
|
+
// Validate USDC balance for transaction on source chain
|
|
7124
7419
|
await validateBalanceForTransaction({
|
|
7125
7420
|
adapter: source.adapter,
|
|
7126
7421
|
amount,
|
|
7127
7422
|
token,
|
|
7128
7423
|
tokenAddress: source.chain.usdcAddress,
|
|
7129
|
-
operationContext,
|
|
7424
|
+
operationContext: sourceOperationContext,
|
|
7425
|
+
});
|
|
7426
|
+
// Validate native balance > 0 for gas fees on source chain
|
|
7427
|
+
await validateNativeBalanceForTransaction({
|
|
7428
|
+
adapter: source.adapter,
|
|
7429
|
+
operationContext: sourceOperationContext,
|
|
7430
|
+
});
|
|
7431
|
+
// Extract operation context from destination wallet context
|
|
7432
|
+
const destinationOperationContext = this.extractOperationContext(destination);
|
|
7433
|
+
// Validate native balance > 0 for gas fees on destination chain
|
|
7434
|
+
await validateNativeBalanceForTransaction({
|
|
7435
|
+
adapter: destination.adapter,
|
|
7436
|
+
operationContext: destinationOperationContext,
|
|
7130
7437
|
});
|
|
7131
7438
|
return bridge(params, this);
|
|
7132
7439
|
}
|
|
@@ -7785,5 +8092,5 @@ class CCTPV2BridgingProvider extends BridgingProvider {
|
|
|
7785
8092
|
}
|
|
7786
8093
|
}
|
|
7787
8094
|
|
|
7788
|
-
export { CCTPV2BridgingProvider, getMintRecipientAccount };
|
|
8095
|
+
export { CCTPV2BridgingProvider, getBlocksUntilExpiry, getMintRecipientAccount, isAttestationExpired, isMintFailureRelatedToAttestation };
|
|
7789
8096
|
//# sourceMappingURL=index.mjs.map
|