@circle-fin/bridge-kit 1.0.0 → 1.1.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/index.cjs.js CHANGED
@@ -25,6 +25,47 @@ require('bs58');
25
25
  var units = require('@ethersproject/units');
26
26
  var providerCctpV2 = require('@circle-fin/provider-cctp-v2');
27
27
 
28
+ /**
29
+ * Detect the runtime environment and return a shortened identifier.
30
+ *
31
+ * @returns Runtime string, e.g., "node/18", "browser/Chrome", or "unknown"
32
+ */
33
+ /**
34
+ * Set an application-level identifier prefix for all HTTP requests.
35
+ *
36
+ * This allows applications to identify themselves in the user agent string,
37
+ * which is useful for tracking and analytics at the application level.
38
+ *
39
+ * @param prefix - Application identifier with version, e.g., "my-app/1.0.0"
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * import { setExternalPrefix } from '\@circle-fin/bridge-kit'
44
+ *
45
+ * setExternalPrefix('my-dapp/2.1.0')
46
+ * // All subsequent HTTP requests will include this prefix
47
+ * ```
48
+ */
49
+ const setExternalPrefix = (prefix) => {
50
+ if (typeof globalThis !== 'undefined') {
51
+ globalThis.__STABLECOIN_KITS_EXTERNAL_PREFIX__ = prefix;
52
+ }
53
+ };
54
+ /**
55
+ * Register a kit identifier globally (internal use only).
56
+ *
57
+ * This is called automatically when a kit module loads, using build-time
58
+ * injected constants. For now, only one kit can be registered at a time.
59
+ *
60
+ * @param kitIdentifier - Kit identifier, e.g., "\@circle-fin/bridge-kit/1.2.3"
61
+ * @internal
62
+ */
63
+ const registerKit = (kitIdentifier) => {
64
+ if (typeof globalThis !== 'undefined') {
65
+ globalThis.__STABLECOIN_KITS_CURRENT_KIT__ = kitIdentifier;
66
+ }
67
+ };
68
+
28
69
  /**
29
70
  * Custom error class for validation errors.
30
71
  * Provides structured error information while hiding implementation details.
@@ -328,6 +369,12 @@ const parseUnits = (value, decimals) => {
328
369
  return units.parseUnits(value, decimals).toBigInt();
329
370
  };
330
371
 
372
+ var name = "@circle-fin/bridge-kit";
373
+ var version = "1.1.0";
374
+ var pkg = {
375
+ name: name,
376
+ version: version};
377
+
331
378
  /**
332
379
  * Valid recoverability values for error handling strategies.
333
380
  *
@@ -525,6 +572,16 @@ class KitError extends Error {
525
572
  }
526
573
  }
527
574
 
575
+ /**
576
+ * Minimum error code for INPUT type errors.
577
+ * INPUT errors represent validation failures and invalid parameters.
578
+ */
579
+ const INPUT_ERROR_CODE_MIN = 1000;
580
+ /**
581
+ * Maximum error code for INPUT type errors (exclusive).
582
+ * INPUT error codes range from 1000 to 1099 inclusive.
583
+ */
584
+ const INPUT_ERROR_CODE_MAX = 1100;
528
585
  /**
529
586
  * Standardized error definitions for INPUT type errors.
530
587
  *
@@ -742,6 +799,185 @@ function createInvalidChainError(chain, reason) {
742
799
  return new KitError(errorDetails);
743
800
  }
744
801
 
802
+ /**
803
+ * Type guard to check if an error is a KitError instance.
804
+ *
805
+ * This guard enables TypeScript to narrow the type from `unknown` to
806
+ * `KitError`, providing access to structured error properties like
807
+ * code, name, and recoverability.
808
+ *
809
+ * @param error - Unknown error to check
810
+ * @returns True if error is KitError with proper type narrowing
811
+ *
812
+ * @example
813
+ * ```typescript
814
+ * import { isKitError } from '@core/errors'
815
+ *
816
+ * try {
817
+ * await kit.bridge(params)
818
+ * } catch (error) {
819
+ * if (isKitError(error)) {
820
+ * // TypeScript knows this is KitError
821
+ * console.log(`Structured error: ${error.name} (${error.code})`)
822
+ * } else {
823
+ * console.log('Regular error:', error)
824
+ * }
825
+ * }
826
+ * ```
827
+ */
828
+ function isKitError(error) {
829
+ return error instanceof KitError;
830
+ }
831
+ /**
832
+ * Checks if an error is a KitError with FATAL recoverability.
833
+ *
834
+ * FATAL errors indicate issues that cannot be resolved through retries,
835
+ * such as invalid inputs, configuration problems, or business rule
836
+ * violations. These errors require user intervention to fix.
837
+ *
838
+ * @param error - Unknown error to check
839
+ * @returns True if error is a KitError with FATAL recoverability
840
+ *
841
+ * @example
842
+ * ```typescript
843
+ * import { isFatalError } from '@core/errors'
844
+ *
845
+ * try {
846
+ * await kit.bridge(params)
847
+ * } catch (error) {
848
+ * if (isFatalError(error)) {
849
+ * // Show user-friendly error message - don't retry
850
+ * showUserError(error.message)
851
+ * }
852
+ * }
853
+ * ```
854
+ */
855
+ function isFatalError(error) {
856
+ return isKitError(error) && error.recoverability === 'FATAL';
857
+ }
858
+ /**
859
+ * Checks if an error is a KitError with RETRYABLE recoverability.
860
+ *
861
+ * RETRYABLE errors indicate transient failures that may succeed on
862
+ * subsequent attempts, such as network timeouts or temporary service
863
+ * unavailability. These errors are safe to retry after a delay.
864
+ *
865
+ * @param error - Unknown error to check
866
+ * @returns True if error is a KitError with RETRYABLE recoverability
867
+ *
868
+ * @example
869
+ * ```typescript
870
+ * import { isRetryableError } from '@core/errors'
871
+ *
872
+ * try {
873
+ * await kit.bridge(params)
874
+ * } catch (error) {
875
+ * if (isRetryableError(error)) {
876
+ * // Implement retry logic with exponential backoff
877
+ * setTimeout(() => retryOperation(), 5000)
878
+ * }
879
+ * }
880
+ * ```
881
+ */
882
+ function isRetryableError(error) {
883
+ return isKitError(error) && error.recoverability === 'RETRYABLE';
884
+ }
885
+ /**
886
+ * Combined type guard to check if error is KitError with INPUT type.
887
+ *
888
+ * INPUT errors have error codes in the 1000-1099 range and represent
889
+ * validation failures, invalid parameters, or user input problems.
890
+ * These errors are always FATAL and require the user to correct their
891
+ * input before retrying.
892
+ *
893
+ * @param error - Unknown error to check
894
+ * @returns True if error is KitError with INPUT error code range
895
+ *
896
+ * @example
897
+ * ```typescript
898
+ * import { isInputError } from '@core/errors'
899
+ * import { createBridgeKit } from '@core/bridge'
900
+ *
901
+ * async function handleBridge() {
902
+ * const kit = createBridgeKit(config)
903
+ * const params = { amount: '100', from: 'ethereum', to: 'polygon' }
904
+ *
905
+ * try {
906
+ * await kit.bridge(params)
907
+ * } catch (error) {
908
+ * if (isInputError(error)) {
909
+ * console.log(`Input error: ${error.message} (code: ${error.code})`)
910
+ * }
911
+ * }
912
+ * }
913
+ * ```
914
+ */
915
+ function isInputError(error) {
916
+ return (isKitError(error) &&
917
+ error.code >= INPUT_ERROR_CODE_MIN &&
918
+ error.code < INPUT_ERROR_CODE_MAX);
919
+ }
920
+ /**
921
+ * Safely extracts error message from any error type.
922
+ *
923
+ * This utility handles different error types gracefully, extracting
924
+ * meaningful messages from Error instances, string errors, or providing
925
+ * a fallback for unknown error types. Never throws.
926
+ *
927
+ * @param error - Unknown error to extract message from
928
+ * @returns Error message string, or fallback message
929
+ *
930
+ * @example
931
+ * ```typescript
932
+ * import { getErrorMessage } from '@core/errors'
933
+ *
934
+ * try {
935
+ * await riskyOperation()
936
+ * } catch (error) {
937
+ * const message = getErrorMessage(error)
938
+ * console.log('Error occurred:', message)
939
+ * // Works with Error, KitError, string, or any other type
940
+ * }
941
+ * ```
942
+ */
943
+ function getErrorMessage(error) {
944
+ if (error instanceof Error) {
945
+ return error.message;
946
+ }
947
+ if (typeof error === 'string') {
948
+ return error;
949
+ }
950
+ return 'An unknown error occurred';
951
+ }
952
+ /**
953
+ * Gets the error code from a KitError, or null if not applicable.
954
+ *
955
+ * This utility safely extracts the numeric error code from KitError
956
+ * instances, returning null for non-KitError types. Useful for
957
+ * programmatic error handling based on specific error codes.
958
+ *
959
+ * @param error - Unknown error to extract code from
960
+ * @returns Error code number, or null if not a KitError
961
+ *
962
+ * @example
963
+ * ```typescript
964
+ * import { getErrorCode, InputError } from '@core/errors'
965
+ *
966
+ * try {
967
+ * await kit.bridge(params)
968
+ * } catch (error) {
969
+ * const code = getErrorCode(error)
970
+ * if (code === InputError.NETWORK_MISMATCH.code) {
971
+ * // Handle network mismatch specifically
972
+ * showNetworkMismatchHelp()
973
+ * }
974
+ * }
975
+ * ```
976
+ */
977
+ function getErrorCode(error) {
978
+ return isKitError(error) ? error.code : null;
979
+ }
980
+
745
981
  /**
746
982
  * Extracts chain information including name, display name, and expected address format.
747
983
  *
@@ -1401,6 +1637,7 @@ exports.Blockchain = void 0;
1401
1637
  Blockchain["Algorand_Testnet"] = "Algorand_Testnet";
1402
1638
  Blockchain["Aptos"] = "Aptos";
1403
1639
  Blockchain["Aptos_Testnet"] = "Aptos_Testnet";
1640
+ Blockchain["Arc_Testnet"] = "Arc_Testnet";
1404
1641
  Blockchain["Arbitrum"] = "Arbitrum";
1405
1642
  Blockchain["Arbitrum_Sepolia"] = "Arbitrum_Sepolia";
1406
1643
  Blockchain["Avalanche"] = "Avalanche";
@@ -1621,6 +1858,48 @@ const BRIDGE_CONTRACT_EVM_TESTNET = '0xC5567a5E3370d4DBfB0540025078e283e36A363d'
1621
1858
  */
1622
1859
  const BRIDGE_CONTRACT_EVM_MAINNET = '0xB3FA262d0fB521cc93bE83d87b322b8A23DAf3F0';
1623
1860
 
1861
+ /**
1862
+ * Arc Testnet chain definition
1863
+ * @remarks
1864
+ * This represents the test network for the Arc blockchain,
1865
+ * Circle's EVM-compatible Layer-1 designed for stablecoin finance
1866
+ * and asset tokenization. Arc uses USDC as the native gas token and
1867
+ * features the Malachite Byzantine Fault Tolerant (BFT) consensus
1868
+ * engine for sub-second finality.
1869
+ */
1870
+ const ArcTestnet = defineChain({
1871
+ type: 'evm',
1872
+ chain: exports.Blockchain.Arc_Testnet,
1873
+ name: 'Arc Testnet',
1874
+ title: 'ArcTestnet',
1875
+ nativeCurrency: {
1876
+ name: 'Arc',
1877
+ symbol: 'Arc',
1878
+ decimals: 18,
1879
+ },
1880
+ chainId: 5042002,
1881
+ isTestnet: true,
1882
+ explorerUrl: 'https://testnet.arcscan.app/tx/{hash}',
1883
+ rpcEndpoints: ['https://rpc.testnet.arc.network/'],
1884
+ eurcAddress: '0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a',
1885
+ usdcAddress: '0x3600000000000000000000000000000000000000',
1886
+ cctp: {
1887
+ domain: 26,
1888
+ contracts: {
1889
+ v2: {
1890
+ type: 'split',
1891
+ tokenMessenger: '0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA',
1892
+ messageTransmitter: '0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275',
1893
+ confirmations: 1,
1894
+ fastConfirmations: 1,
1895
+ },
1896
+ },
1897
+ },
1898
+ kitContracts: {
1899
+ bridge: BRIDGE_CONTRACT_EVM_TESTNET,
1900
+ },
1901
+ });
1902
+
1624
1903
  /**
1625
1904
  * Arbitrum Mainnet chain definition
1626
1905
  * @remarks
@@ -2912,26 +3191,26 @@ const Sonic = defineChain({
2912
3191
  });
2913
3192
 
2914
3193
  /**
2915
- * Sonic Blaze Testnet chain definition
3194
+ * Sonic Testnet chain definition
2916
3195
  * @remarks
2917
3196
  * This represents the official test network for the Sonic blockchain.
2918
3197
  */
2919
3198
  const SonicTestnet = defineChain({
2920
3199
  type: 'evm',
2921
3200
  chain: exports.Blockchain.Sonic_Testnet,
2922
- name: 'Sonic Blaze Testnet',
2923
- title: 'Sonic Blaze Testnet',
3201
+ name: 'Sonic Testnet',
3202
+ title: 'Sonic Testnet',
2924
3203
  nativeCurrency: {
2925
3204
  name: 'Sonic',
2926
3205
  symbol: 'S',
2927
3206
  decimals: 18,
2928
3207
  },
2929
- chainId: 57054,
3208
+ chainId: 14601,
2930
3209
  isTestnet: true,
2931
3210
  explorerUrl: 'https://testnet.sonicscan.org/tx/{hash}',
2932
- rpcEndpoints: ['https://rpc.blaze.soniclabs.com'],
3211
+ rpcEndpoints: ['https://rpc.testnet.soniclabs.com'],
2933
3212
  eurcAddress: null,
2934
- usdcAddress: '0xA4879Fed32Ecbef99399e5cbC247E533421C4eC6',
3213
+ usdcAddress: '0x0BA304580ee7c9a980CF72e55f5Ed2E9fd30Bc51',
2935
3214
  cctp: {
2936
3215
  domain: 13,
2937
3216
  contracts: {
@@ -3448,6 +3727,7 @@ var Blockchains = {
3448
3727
  AptosTestnet: AptosTestnet,
3449
3728
  Arbitrum: Arbitrum,
3450
3729
  ArbitrumSepolia: ArbitrumSepolia,
3730
+ ArcTestnet: ArcTestnet,
3451
3731
  Avalanche: Avalanche,
3452
3732
  AvalancheFuji: AvalancheFuji,
3453
3733
  Base: Base,
@@ -4081,9 +4361,9 @@ const customFeeSchema = zod.z
4081
4361
  * token: 'USDC',
4082
4362
  * config: {
4083
4363
  * transferSpeed: 'FAST',
4084
- * maxFee: '1000000'
4364
+ * maxFee: '1.5', // Decimal format
4085
4365
  * customFee: {
4086
- * value: '1000000',
4366
+ * value: '0.5', // Decimal format
4087
4367
  * recipientAddress: '0x1234567890123456789012345678901234567890'
4088
4368
  * }
4089
4369
  * }
@@ -4110,7 +4390,12 @@ zod.z.object({
4110
4390
  transferSpeed: zod.z.nativeEnum(TransferSpeed).optional(),
4111
4391
  maxFee: zod.z
4112
4392
  .string()
4113
- .regex(/^\d+$/, 'Max fee must be a numeric string')
4393
+ .pipe(createDecimalStringValidator({
4394
+ allowZero: true,
4395
+ regexMessage: 'maxFee must be a numeric string with optional decimal places (e.g., "1", "0.5", "1.5")',
4396
+ attributeName: 'maxFee',
4397
+ maxDecimals: 6,
4398
+ })(zod.z.string()))
4114
4399
  .optional(),
4115
4400
  customFee: customFeeSchema.optional(),
4116
4401
  }),
@@ -4119,10 +4404,12 @@ zod.z.object({
4119
4404
  /**
4120
4405
  * Schema for validating AdapterContext.
4121
4406
  * Must always contain both adapter and chain explicitly.
4407
+ * Optionally includes address for developer-controlled adapters.
4122
4408
  */
4123
4409
  const adapterContextSchema = zod.z.object({
4124
4410
  adapter: adapterSchema,
4125
4411
  chain: chainIdentifierSchema,
4412
+ address: zod.z.string().optional(),
4126
4413
  });
4127
4414
  /**
4128
4415
  * Schema for validating BridgeDestinationWithAddress objects.
@@ -4200,7 +4487,7 @@ const bridgeDestinationSchema = zod.z.union([
4200
4487
  * ```
4201
4488
  */
4202
4489
  const bridgeParamsWithChainIdentifierSchema = zod.z.object({
4203
- from: adapterContextSchema,
4490
+ from: adapterContextSchema.strict(),
4204
4491
  to: bridgeDestinationSchema,
4205
4492
  amount: zod.z
4206
4493
  .string()
@@ -4217,7 +4504,13 @@ const bridgeParamsWithChainIdentifierSchema = zod.z.object({
4217
4504
  transferSpeed: zod.z.nativeEnum(TransferSpeed).optional(),
4218
4505
  maxFee: zod.z
4219
4506
  .string()
4220
- .regex(/^\d+$/, 'Max fee must be a numeric string')
4507
+ .min(1, 'Required')
4508
+ .pipe(createDecimalStringValidator({
4509
+ allowZero: true,
4510
+ regexMessage: 'Max fee must be a numeric string with optional decimal places (e.g., 1, 0.5, 1.5)',
4511
+ attributeName: 'maxFee',
4512
+ maxDecimals: 6,
4513
+ })(zod.z.string()))
4221
4514
  .optional(),
4222
4515
  customFee: customFeeSchema.optional(),
4223
4516
  })
@@ -4286,10 +4579,25 @@ async function resolveAddress(ctx) {
4286
4579
  if ('recipientAddress' in ctx) {
4287
4580
  return ctx.recipientAddress;
4288
4581
  }
4289
- // Handle AdapterContext (derive from adapter)
4290
- // Pass the chain to support OperationContext pattern
4291
- const chain = resolveChainDefinition(ctx);
4292
- return await ctx.adapter.getAddress(chain);
4582
+ // Handle based on adapter's addressContext
4583
+ if (ctx.adapter.capabilities?.addressContext === 'developer-controlled') {
4584
+ // Developer-controlled: address must be provided explicitly
4585
+ if ('address' in ctx && ctx.address) {
4586
+ return ctx.address;
4587
+ }
4588
+ throw new Error('Address is required in context for developer-controlled adapters. ' +
4589
+ 'Please provide: { adapter, chain, address: "0x..." }');
4590
+ }
4591
+ else {
4592
+ // User-controlled: address should not be provided (auto-resolved from adapter)
4593
+ if ('address' in ctx && ctx.address) {
4594
+ throw new Error('Address should not be provided for user-controlled adapters. ' +
4595
+ 'The address is automatically resolved from the connected wallet.');
4596
+ }
4597
+ // Derive address from adapter
4598
+ const chain = resolveChainDefinition(ctx);
4599
+ return await ctx.adapter.getAddress(chain);
4600
+ }
4293
4601
  }
4294
4602
  /**
4295
4603
  * Resolves the amount of a transfer by formatting it according to the token's decimal places.
@@ -4326,7 +4634,8 @@ function resolveAmount(params) {
4326
4634
  * This function takes the optional configuration from bridge parameters and returns
4327
4635
  * a normalized BridgeConfig with:
4328
4636
  * - Default transfer speed set to FAST if not provided
4329
- * - Custom fee values formatted with proper decimal places (6 decimals for USDC)
4637
+ * - Max fee values converted from human-readable to smallest units (6 decimals for USDC)
4638
+ * - Custom fee values converted from human-readable to smallest units (6 decimals for USDC)
4330
4639
  *
4331
4640
  * @param params - The bridge parameters containing optional configuration
4332
4641
  * @returns A normalized BridgeConfig with defaults applied and values formatted
@@ -4341,35 +4650,45 @@ function resolveAmount(params) {
4341
4650
  * from: { adapter: mockAdapter, chain: Ethereum },
4342
4651
  * to: { adapter: mockAdapter, chain: Base },
4343
4652
  * config: {
4653
+ * maxFee: '1',
4344
4654
  * customFee: { value: '0.5' }
4345
4655
  * }
4346
4656
  * }
4347
4657
  * const config = resolveConfig(params)
4348
4658
  * // Returns: {
4349
4659
  * // transferSpeed: TransferSpeed.FAST,
4660
+ * // maxFee: '1000000',
4350
4661
  * // customFee: { value: '500000' }
4351
4662
  * // }
4352
4663
  * ```
4353
4664
  */
4354
4665
  function resolveConfig(params) {
4666
+ // Convert maxFee from human-readable to minor units if provided
4667
+ const maxFee = params.config?.maxFee
4668
+ ? parseUnits(params.config.maxFee, 6).toString()
4669
+ : undefined;
4670
+ // Convert customFee.value from human-readable to minor units if provided
4671
+ const rawCustomFee = params.config?.customFee;
4672
+ const customFee = rawCustomFee
4673
+ ? {
4674
+ ...rawCustomFee,
4675
+ value: rawCustomFee.value
4676
+ ? parseUnits(rawCustomFee.value, 6).toString()
4677
+ : undefined,
4678
+ }
4679
+ : undefined;
4355
4680
  return {
4356
4681
  ...params.config,
4357
4682
  transferSpeed: params.config?.transferSpeed ?? TransferSpeed.FAST,
4358
- customFee: params.config?.customFee
4359
- ? {
4360
- ...params.config?.customFee,
4361
- value: params.config?.customFee?.value
4362
- ? parseUnits(params.config?.customFee?.value, 6).toString()
4363
- : undefined,
4364
- }
4365
- : undefined,
4683
+ ...(maxFee !== undefined && { maxFee }),
4684
+ ...(customFee !== undefined && { customFee }),
4366
4685
  };
4367
4686
  }
4368
4687
  /**
4369
4688
  * Resolves and normalizes bridge parameters for the BridgeKit.
4370
4689
  *
4371
4690
  * This function takes bridge parameters with explicit adapter contexts and normalizes them
4372
- * into a consistent format that can be used by the bridging providers.
4691
+ * into the format expected by bridging providers.
4373
4692
  *
4374
4693
  * The function performs parallel resolution of:
4375
4694
  * - Source and destination addresses
@@ -4856,7 +5175,24 @@ class BridgeKit {
4856
5175
  }
4857
5176
  }
4858
5177
 
5178
+ /**
5179
+ *
5180
+ * Bridge Kit
5181
+ *
5182
+ * A strongly-typed SDK for moving USDC between heterogeneous blockchain networks
5183
+ * @packageDocumentation
5184
+ */
5185
+ // Auto-register this kit for user agent tracking
5186
+ registerKit(`${pkg.name}/${pkg.version}`);
5187
+
4859
5188
  exports.BridgeKit = BridgeKit;
4860
5189
  exports.KitError = KitError;
4861
5190
  exports.bridgeParamsWithChainIdentifierSchema = bridgeParamsWithChainIdentifierSchema;
5191
+ exports.getErrorCode = getErrorCode;
5192
+ exports.getErrorMessage = getErrorMessage;
5193
+ exports.isFatalError = isFatalError;
5194
+ exports.isInputError = isInputError;
5195
+ exports.isKitError = isKitError;
5196
+ exports.isRetryableError = isRetryableError;
5197
+ exports.setExternalPrefix = setExternalPrefix;
4862
5198
  //# sourceMappingURL=index.cjs.js.map