@circle-fin/app-kit 1.6.1 → 1.7.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 +10 -0
- package/earn.cjs +970 -172
- package/earn.mjs +970 -172
- package/index.cjs +739 -272
- package/index.d.ts +339 -5
- package/index.mjs +739 -272
- package/package.json +6 -6
package/earn.cjs
CHANGED
|
@@ -990,6 +990,151 @@ function createTransactionRevertedError(chain, reason, trace, txHash, explorerUr
|
|
|
990
990
|
});
|
|
991
991
|
}
|
|
992
992
|
|
|
993
|
+
/**
|
|
994
|
+
* Type guard to check if an error is a KitError instance.
|
|
995
|
+
*
|
|
996
|
+
* This guard enables TypeScript to narrow the type from `unknown` to
|
|
997
|
+
* `KitError`, providing access to structured error properties like
|
|
998
|
+
* code, name, and recoverability.
|
|
999
|
+
*
|
|
1000
|
+
* @param error - Unknown error to check
|
|
1001
|
+
* @returns True if error is KitError with proper type narrowing
|
|
1002
|
+
*
|
|
1003
|
+
* @example
|
|
1004
|
+
* ```typescript
|
|
1005
|
+
* import { isKitError } from '@core/errors'
|
|
1006
|
+
*
|
|
1007
|
+
* try {
|
|
1008
|
+
* await kit.bridge(params)
|
|
1009
|
+
* } catch (error) {
|
|
1010
|
+
* if (isKitError(error)) {
|
|
1011
|
+
* // TypeScript knows this is KitError
|
|
1012
|
+
* console.log(`Structured error: ${error.name} (${error.code})`)
|
|
1013
|
+
* } else {
|
|
1014
|
+
* console.log('Regular error:', error)
|
|
1015
|
+
* }
|
|
1016
|
+
* }
|
|
1017
|
+
* ```
|
|
1018
|
+
*/
|
|
1019
|
+
function isKitError(error) {
|
|
1020
|
+
return error instanceof KitError;
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Error codes that are considered retryable by default.
|
|
1024
|
+
*
|
|
1025
|
+
* @remarks
|
|
1026
|
+
* These are typically transient errors that may succeed on retry:
|
|
1027
|
+
* - Network connectivity issues (3001, 3002)
|
|
1028
|
+
* - Provider unavailability (4001, 4002)
|
|
1029
|
+
* - RPC nonce errors (4003)
|
|
1030
|
+
*/
|
|
1031
|
+
const DEFAULT_RETRYABLE_ERROR_CODES = [
|
|
1032
|
+
// Network errors
|
|
1033
|
+
3001, // NETWORK_CONNECTION_FAILED
|
|
1034
|
+
3002, // NETWORK_TIMEOUT
|
|
1035
|
+
// Provider errors
|
|
1036
|
+
4001, // PROVIDER_UNAVAILABLE
|
|
1037
|
+
4002, // PROVIDER_TIMEOUT
|
|
1038
|
+
4003, // RPC_NONCE_ERROR
|
|
1039
|
+
];
|
|
1040
|
+
/**
|
|
1041
|
+
* Checks if an error is retryable.
|
|
1042
|
+
*
|
|
1043
|
+
* @remarks
|
|
1044
|
+
* Check order for KitError instances:
|
|
1045
|
+
* 1. If `recoverability === 'RETRYABLE'` or `recoverability === 'RESUMABLE'`,
|
|
1046
|
+
* return `true` immediately (priority check).
|
|
1047
|
+
* 2. Otherwise, check if `error.code` is in `DEFAULT_RETRYABLE_ERROR_CODES` (fallback check).
|
|
1048
|
+
* 3. Non-KitError instances always return `false`.
|
|
1049
|
+
*
|
|
1050
|
+
* This two-tier approach allows both explicit recoverability control and
|
|
1051
|
+
* backward-compatible code-based retry logic.
|
|
1052
|
+
*
|
|
1053
|
+
* RETRYABLE errors indicate transient failures that may succeed on
|
|
1054
|
+
* subsequent attempts, such as network timeouts or temporary service
|
|
1055
|
+
* unavailability. These errors are safe to retry after a delay.
|
|
1056
|
+
*
|
|
1057
|
+
* RESUMABLE errors indicate a multi-phase operation that completed some phases
|
|
1058
|
+
* before failing (for example, a token approval landed but the execution
|
|
1059
|
+
* transaction failed). They are also retryable — re-running the operation is
|
|
1060
|
+
* safe — but callers that have a kit-level `retry()` should prefer it so that
|
|
1061
|
+
* already-completed phases are skipped.
|
|
1062
|
+
*
|
|
1063
|
+
* @param error - Unknown error to check
|
|
1064
|
+
* @returns True if error is retryable
|
|
1065
|
+
*
|
|
1066
|
+
* @example
|
|
1067
|
+
* ```typescript
|
|
1068
|
+
* import { isRetryableError } from '@core/errors'
|
|
1069
|
+
*
|
|
1070
|
+
* try {
|
|
1071
|
+
* await kit.bridge(params)
|
|
1072
|
+
* } catch (error) {
|
|
1073
|
+
* if (isRetryableError(error)) {
|
|
1074
|
+
* // Implement retry logic with exponential backoff
|
|
1075
|
+
* setTimeout(() => retryOperation(), 5000)
|
|
1076
|
+
* }
|
|
1077
|
+
* }
|
|
1078
|
+
* ```
|
|
1079
|
+
*
|
|
1080
|
+
* @example
|
|
1081
|
+
* ```typescript
|
|
1082
|
+
* import { isRetryableError, createNetworkConnectionError, KitError } from '@core/errors'
|
|
1083
|
+
*
|
|
1084
|
+
* // KitError with RETRYABLE recoverability (priority check)
|
|
1085
|
+
* const error1 = createNetworkConnectionError('Ethereum')
|
|
1086
|
+
* isRetryableError(error1) // true
|
|
1087
|
+
*
|
|
1088
|
+
* // KitError with default retryable code (fallback check)
|
|
1089
|
+
* const error2 = new KitError({
|
|
1090
|
+
* code: 3002, // NETWORK_TIMEOUT - in DEFAULT_RETRYABLE_ERROR_CODES
|
|
1091
|
+
* name: 'NETWORK_TIMEOUT',
|
|
1092
|
+
* type: 'NETWORK',
|
|
1093
|
+
* recoverability: 'FATAL', // Not RETRYABLE
|
|
1094
|
+
* message: 'Timeout',
|
|
1095
|
+
* })
|
|
1096
|
+
* isRetryableError(error2) // true (code 3002 is in default list)
|
|
1097
|
+
*
|
|
1098
|
+
* // KitError with non-retryable code and FATAL recoverability
|
|
1099
|
+
* const error3 = new KitError({
|
|
1100
|
+
* code: 1001,
|
|
1101
|
+
* name: 'INPUT_NETWORK_MISMATCH',
|
|
1102
|
+
* type: 'INPUT',
|
|
1103
|
+
* recoverability: 'FATAL',
|
|
1104
|
+
* message: 'Invalid input',
|
|
1105
|
+
* })
|
|
1106
|
+
* isRetryableError(error3) // false
|
|
1107
|
+
*
|
|
1108
|
+
* // KitError with RESUMABLE recoverability (partially-completed operation)
|
|
1109
|
+
* const error4 = new KitError({
|
|
1110
|
+
* code: 8101,
|
|
1111
|
+
* name: 'EARN_EXECUTION_FAILED',
|
|
1112
|
+
* type: 'SERVICE',
|
|
1113
|
+
* recoverability: 'RESUMABLE',
|
|
1114
|
+
* message: 'Execution failed after approval',
|
|
1115
|
+
* })
|
|
1116
|
+
* isRetryableError(error4) // true
|
|
1117
|
+
*
|
|
1118
|
+
* // Non-KitError
|
|
1119
|
+
* const error5 = new Error('Standard error')
|
|
1120
|
+
* isRetryableError(error5) // false
|
|
1121
|
+
* ```
|
|
1122
|
+
*/
|
|
1123
|
+
function isRetryableError$1(error) {
|
|
1124
|
+
// Use proper type guard to check if it's a KitError
|
|
1125
|
+
if (isKitError(error)) {
|
|
1126
|
+
// Priority check: explicit recoverability. RESUMABLE errors are a subset of
|
|
1127
|
+
// retryable errors — re-running the operation is safe, but a kit-level
|
|
1128
|
+
// retry() can resume from the failed phase instead.
|
|
1129
|
+
if (error.recoverability === 'RETRYABLE' ||
|
|
1130
|
+
error.recoverability === 'RESUMABLE') {
|
|
1131
|
+
return true;
|
|
1132
|
+
}
|
|
1133
|
+
// Fallback check: error code against default retryable codes
|
|
1134
|
+
return DEFAULT_RETRYABLE_ERROR_CODES.includes(error.code);
|
|
1135
|
+
}
|
|
1136
|
+
return false;
|
|
1137
|
+
}
|
|
993
1138
|
/**
|
|
994
1139
|
* Safely extracts error message from any error type.
|
|
995
1140
|
*
|
|
@@ -6512,6 +6657,120 @@ function validateOrThrow(value, schema, message) {
|
|
|
6512
6657
|
}
|
|
6513
6658
|
}
|
|
6514
6659
|
|
|
6660
|
+
/**
|
|
6661
|
+
* A type-safe event emitter for managing action-based event subscriptions.
|
|
6662
|
+
*
|
|
6663
|
+
* Actionable provides a strongly-typed publish/subscribe pattern for events,
|
|
6664
|
+
* where each event (action) has its own specific payload type. Handlers can
|
|
6665
|
+
* subscribe to specific events or use a wildcard to receive all events.
|
|
6666
|
+
*
|
|
6667
|
+
* @typeParam AllActions - A record mapping action names to their payload types.
|
|
6668
|
+
*
|
|
6669
|
+
* @example
|
|
6670
|
+
* ```typescript
|
|
6671
|
+
* import { Actionable } from '@circle-fin/bridge-kit/utils';
|
|
6672
|
+
*
|
|
6673
|
+
* // Define your action types
|
|
6674
|
+
* type TransferActions = {
|
|
6675
|
+
* started: { txHash: string; amount: string };
|
|
6676
|
+
* completed: { txHash: string; destinationTxHash: string };
|
|
6677
|
+
* failed: { error: Error };
|
|
6678
|
+
* };
|
|
6679
|
+
*
|
|
6680
|
+
* // Create an actionable instance
|
|
6681
|
+
* const transferEvents = new Actionable<TransferActions>();
|
|
6682
|
+
*
|
|
6683
|
+
* // Subscribe to a specific event
|
|
6684
|
+
* transferEvents.on('completed', (payload) => {
|
|
6685
|
+
* console.log(`Transfer completed with hash: ${payload.destinationTxHash}`);
|
|
6686
|
+
* });
|
|
6687
|
+
*
|
|
6688
|
+
* // Subscribe to all events
|
|
6689
|
+
* transferEvents.on('*', (payload) => {
|
|
6690
|
+
* console.log('Event received:', payload);
|
|
6691
|
+
* });
|
|
6692
|
+
*
|
|
6693
|
+
* // Dispatch an event
|
|
6694
|
+
* transferEvents.dispatch('completed', {
|
|
6695
|
+
* txHash: '0x123',
|
|
6696
|
+
* destinationTxHash: '0xabc'
|
|
6697
|
+
* });
|
|
6698
|
+
* ```
|
|
6699
|
+
*/
|
|
6700
|
+
class Actionable {
|
|
6701
|
+
// Store event handlers by action key
|
|
6702
|
+
handlers = {};
|
|
6703
|
+
// Store wildcard handlers that receive all events
|
|
6704
|
+
wildcard = [];
|
|
6705
|
+
// Implementation that handles both overloads
|
|
6706
|
+
on(action, handler) {
|
|
6707
|
+
if (action === '*') {
|
|
6708
|
+
// Add to wildcard handlers array
|
|
6709
|
+
this.wildcard.push(handler);
|
|
6710
|
+
}
|
|
6711
|
+
else {
|
|
6712
|
+
// Initialize the action's handler array if it doesn't exist
|
|
6713
|
+
if (!this.handlers[action]) {
|
|
6714
|
+
this.handlers[action] = [];
|
|
6715
|
+
}
|
|
6716
|
+
// Add the handler to the specific action's array
|
|
6717
|
+
this.handlers[action].push(handler);
|
|
6718
|
+
}
|
|
6719
|
+
}
|
|
6720
|
+
// Implementation that handles both overloads
|
|
6721
|
+
off(action, handler) {
|
|
6722
|
+
if (action === '*') {
|
|
6723
|
+
// Find and remove the handler from wildcard array
|
|
6724
|
+
const index = this.wildcard.indexOf(handler);
|
|
6725
|
+
if (index !== -1) {
|
|
6726
|
+
this.wildcard.splice(index, 1);
|
|
6727
|
+
}
|
|
6728
|
+
}
|
|
6729
|
+
else if (this.handlers[action]) {
|
|
6730
|
+
// Check if there are handlers for this action
|
|
6731
|
+
// Find and remove the specific handler
|
|
6732
|
+
const index = this.handlers[action].indexOf(handler);
|
|
6733
|
+
if (index !== -1) {
|
|
6734
|
+
this.handlers[action].splice(index, 1);
|
|
6735
|
+
}
|
|
6736
|
+
}
|
|
6737
|
+
}
|
|
6738
|
+
/**
|
|
6739
|
+
* Dispatch an action with its payload to all registered handlers.
|
|
6740
|
+
*
|
|
6741
|
+
* This method notifies both:
|
|
6742
|
+
* - Handlers registered specifically for this action
|
|
6743
|
+
* - Wildcard handlers registered for all actions
|
|
6744
|
+
*
|
|
6745
|
+
* @param action - The action key identifying the event type.
|
|
6746
|
+
* @param payload - The data associated with the action.
|
|
6747
|
+
*
|
|
6748
|
+
* @example
|
|
6749
|
+
* ```typescript
|
|
6750
|
+
* type Actions = {
|
|
6751
|
+
* transferStarted: { amount: string; destination: string };
|
|
6752
|
+
* transferComplete: { txHash: string };
|
|
6753
|
+
* };
|
|
6754
|
+
*
|
|
6755
|
+
* const events = new Actionable<Actions>();
|
|
6756
|
+
*
|
|
6757
|
+
* // Dispatch an event
|
|
6758
|
+
* events.dispatch('transferStarted', {
|
|
6759
|
+
* amount: '100',
|
|
6760
|
+
* destination: '0xABC123'
|
|
6761
|
+
* });
|
|
6762
|
+
* ```
|
|
6763
|
+
*/
|
|
6764
|
+
dispatch(action, payload) {
|
|
6765
|
+
// Execute all handlers registered for this specific action
|
|
6766
|
+
for (const h of this.handlers[action] ?? [])
|
|
6767
|
+
h(payload);
|
|
6768
|
+
// Execute all wildcard handlers
|
|
6769
|
+
for (const h of this.wildcard)
|
|
6770
|
+
h(payload);
|
|
6771
|
+
}
|
|
6772
|
+
}
|
|
6773
|
+
|
|
6515
6774
|
/**
|
|
6516
6775
|
* Convert a value from its smallest unit representation to a human-readable decimal string.
|
|
6517
6776
|
*
|
|
@@ -7382,7 +7641,7 @@ function resolveKitSdkName(pkgName) {
|
|
|
7382
7641
|
}
|
|
7383
7642
|
|
|
7384
7643
|
var name$2 = "@circle-fin/bridge-kit";
|
|
7385
|
-
var version$2 = "1.10.
|
|
7644
|
+
var version$2 = "1.10.2";
|
|
7386
7645
|
var pkg$2 = {
|
|
7387
7646
|
name: name$2,
|
|
7388
7647
|
version: version$2};
|
|
@@ -8295,7 +8554,7 @@ resolveKitSdkName(pkg$2.name);
|
|
|
8295
8554
|
registerKit(`${pkg$2.name}/${pkg$2.version}`);
|
|
8296
8555
|
|
|
8297
8556
|
var name$1 = "@circle-fin/swap-kit";
|
|
8298
|
-
var version$1 = "1.2.
|
|
8557
|
+
var version$1 = "1.2.3";
|
|
8299
8558
|
var pkg$1 = {
|
|
8300
8559
|
name: name$1,
|
|
8301
8560
|
version: version$1};
|
|
@@ -11772,7 +12031,7 @@ resolveKitSdkName(pkg$1.name);
|
|
|
11772
12031
|
registerKit(`${pkg$1.name}/${pkg$1.version}`);
|
|
11773
12032
|
|
|
11774
12033
|
var name = "@circle-fin/earn-kit";
|
|
11775
|
-
var version = "1.0
|
|
12034
|
+
var version = "1.1.0";
|
|
11776
12035
|
var pkg = {
|
|
11777
12036
|
name: name,
|
|
11778
12037
|
version: version};
|
|
@@ -12182,6 +12441,253 @@ function validateExecutionDeadline(executionParams) {
|
|
|
12182
12441
|
}
|
|
12183
12442
|
}
|
|
12184
12443
|
|
|
12444
|
+
/** Base discriminators shared by every earn action payload. */
|
|
12445
|
+
const EARN_ACTION_BASE = {
|
|
12446
|
+
protocol: 'earn',
|
|
12447
|
+
service: 'earn-service',
|
|
12448
|
+
};
|
|
12449
|
+
/**
|
|
12450
|
+
* Dispatch a step event for an earn operation through the kit's action
|
|
12451
|
+
* dispatcher.
|
|
12452
|
+
*
|
|
12453
|
+
* Builds the action payload for the given action/operation/step combination
|
|
12454
|
+
* and forwards it to any registered listeners (including wildcard `'*'`
|
|
12455
|
+
* listeners). The call is a no-op when no dispatcher has been registered, so
|
|
12456
|
+
* callers can invoke it unconditionally.
|
|
12457
|
+
*
|
|
12458
|
+
* The `approve` action only fires for `deposit` and `withdraw` operations and
|
|
12459
|
+
* only carries `approve` steps; the `deposit`, `withdraw`, and `claimRewards`
|
|
12460
|
+
* actions carry only `fetchParams` and `execute` steps. Mismatched
|
|
12461
|
+
* combinations are ignored defensively.
|
|
12462
|
+
*
|
|
12463
|
+
* @param dispatcher - The action dispatcher, or `undefined` if none registered.
|
|
12464
|
+
* @param action - The action name to dispatch under.
|
|
12465
|
+
* @param operation - The earn operation the step belongs to.
|
|
12466
|
+
* @param step - The step to deliver as the event payload.
|
|
12467
|
+
*
|
|
12468
|
+
* @example
|
|
12469
|
+
* ```typescript
|
|
12470
|
+
* dispatchEarnEvent(dispatcher, 'deposit', 'deposit', {
|
|
12471
|
+
* name: 'execute',
|
|
12472
|
+
* state: 'success',
|
|
12473
|
+
* txHash: '0xabc...',
|
|
12474
|
+
* })
|
|
12475
|
+
* ```
|
|
12476
|
+
*/
|
|
12477
|
+
function dispatchEarnEvent(dispatcher, action, operation, step) {
|
|
12478
|
+
if (dispatcher === undefined) {
|
|
12479
|
+
return;
|
|
12480
|
+
}
|
|
12481
|
+
// Listener errors must never abort the financial operation flow. A throwing
|
|
12482
|
+
// listener on a 'success' event after a transaction has landed on-chain
|
|
12483
|
+
// would otherwise propagate to the caller as a RESUMABLE error and trigger
|
|
12484
|
+
// a retry that re-submits the same transaction (double-spend risk).
|
|
12485
|
+
try {
|
|
12486
|
+
if (action === 'approve') {
|
|
12487
|
+
// The approve action only applies to deposit/withdraw approval steps.
|
|
12488
|
+
if (step.name !== 'approve' || operation === 'claimRewards') {
|
|
12489
|
+
return;
|
|
12490
|
+
}
|
|
12491
|
+
dispatcher.dispatch('approve', {
|
|
12492
|
+
...EARN_ACTION_BASE,
|
|
12493
|
+
operation,
|
|
12494
|
+
method: 'approve',
|
|
12495
|
+
values: step,
|
|
12496
|
+
});
|
|
12497
|
+
return;
|
|
12498
|
+
}
|
|
12499
|
+
// deposit / withdraw / claimRewards actions carry fetchParams + execute steps.
|
|
12500
|
+
if (step.name === 'approve') {
|
|
12501
|
+
return;
|
|
12502
|
+
}
|
|
12503
|
+
const payload = { ...EARN_ACTION_BASE, method: step.name, values: step };
|
|
12504
|
+
if (action === 'deposit') {
|
|
12505
|
+
dispatcher.dispatch('deposit', { ...payload, operation: 'deposit' });
|
|
12506
|
+
return;
|
|
12507
|
+
}
|
|
12508
|
+
if (action === 'withdraw') {
|
|
12509
|
+
dispatcher.dispatch('withdraw', { ...payload, operation: 'withdraw' });
|
|
12510
|
+
return;
|
|
12511
|
+
}
|
|
12512
|
+
if (action === 'claimRewards') {
|
|
12513
|
+
dispatcher.dispatch('claimRewards', {
|
|
12514
|
+
...payload,
|
|
12515
|
+
operation: 'claimRewards',
|
|
12516
|
+
});
|
|
12517
|
+
return;
|
|
12518
|
+
}
|
|
12519
|
+
}
|
|
12520
|
+
catch {
|
|
12521
|
+
// Swallow listener errors. Observability of the failure is the listener's
|
|
12522
|
+
// responsibility; the operation flow continues unaffected.
|
|
12523
|
+
return;
|
|
12524
|
+
}
|
|
12525
|
+
// Reached only when `action` is not a known earn action. Thrown outside the
|
|
12526
|
+
// try/catch so a missing case surfaces as a developer error instead of being
|
|
12527
|
+
// silently swallowed alongside listener failures.
|
|
12528
|
+
const exhaustive = action;
|
|
12529
|
+
throw new Error(`Unhandled earn action: ${String(exhaustive)}`);
|
|
12530
|
+
}
|
|
12531
|
+
|
|
12532
|
+
const EARN_OPERATIONS = new Set([
|
|
12533
|
+
'deposit',
|
|
12534
|
+
'withdraw',
|
|
12535
|
+
'claimRewards',
|
|
12536
|
+
]);
|
|
12537
|
+
/**
|
|
12538
|
+
* Operations whose service params carry a top-level `amount` input field
|
|
12539
|
+
* (`deposit` and `withdraw`); `claimRewards` params have no `amount`. This
|
|
12540
|
+
* refers to the request input — distinct from the reward `amount` that appears
|
|
12541
|
+
* inside a claimRewards *result*.
|
|
12542
|
+
*/
|
|
12543
|
+
const OPERATIONS_WITH_AMOUNT = new Set([
|
|
12544
|
+
'deposit',
|
|
12545
|
+
'withdraw',
|
|
12546
|
+
]);
|
|
12547
|
+
/**
|
|
12548
|
+
* Validate that `params` carries the minimum shape `retry()` needs to resume
|
|
12549
|
+
* the given operation: an adapter context (`from`) and a `vaultAddress`, plus
|
|
12550
|
+
* an `amount` for `deposit`/`withdraw`. This is a structural sanity check, not
|
|
12551
|
+
* a full schema validation — operations re-validate their inputs when re-run —
|
|
12552
|
+
* but it lets a mismatched or forged trace fail the {@link isEarnErrorTrace}
|
|
12553
|
+
* guard cleanly instead of crashing deep inside the resume path.
|
|
12554
|
+
*/
|
|
12555
|
+
function hasEarnServiceParamsShape(operation, params) {
|
|
12556
|
+
if (params['from'] === null || typeof params['from'] !== 'object') {
|
|
12557
|
+
return false;
|
|
12558
|
+
}
|
|
12559
|
+
if (typeof params['vaultAddress'] !== 'string') {
|
|
12560
|
+
return false;
|
|
12561
|
+
}
|
|
12562
|
+
if (OPERATIONS_WITH_AMOUNT.has(operation) &&
|
|
12563
|
+
typeof params['amount'] !== 'string') {
|
|
12564
|
+
return false;
|
|
12565
|
+
}
|
|
12566
|
+
return true;
|
|
12567
|
+
}
|
|
12568
|
+
/**
|
|
12569
|
+
* Wrap an error thrown during a multi-phase earn operation with step-tracking
|
|
12570
|
+
* and resume context.
|
|
12571
|
+
*
|
|
12572
|
+
* Produces a new {@link KitError} that preserves the original error's code,
|
|
12573
|
+
* name, type, and message, merges any existing `cause.trace`, and adds an
|
|
12574
|
+
* {@link EarnErrorTrace} (`provider`, `operation`, `steps`, `params`). When a
|
|
12575
|
+
* prior phase had already committed work (a successful step exists) and the
|
|
12576
|
+
* underlying error is retryable, the recoverability is upgraded to
|
|
12577
|
+
* `RESUMABLE` so callers know `retry()` can resume from the failed phase. A
|
|
12578
|
+
* fatal underlying error stays fatal — it cannot be resumed.
|
|
12579
|
+
*
|
|
12580
|
+
* Non-{@link KitError} inputs (which should not occur on provider code paths)
|
|
12581
|
+
* are normalized to a retryable {@link ServiceError.INTERNAL_ERROR}.
|
|
12582
|
+
*
|
|
12583
|
+
* @param error - The error caught from a phase of the operation.
|
|
12584
|
+
* @param context - The operation context used to build the trace.
|
|
12585
|
+
* @returns A new {@link KitError} carrying the {@link EarnErrorTrace}.
|
|
12586
|
+
*
|
|
12587
|
+
* @example
|
|
12588
|
+
* ```typescript
|
|
12589
|
+
* try {
|
|
12590
|
+
* await executeEarnAction(...)
|
|
12591
|
+
* } catch (error) {
|
|
12592
|
+
* throw augmentEarnError(error, {
|
|
12593
|
+
* providerName: 'EarnService',
|
|
12594
|
+
* operation: 'deposit',
|
|
12595
|
+
* steps,
|
|
12596
|
+
* params,
|
|
12597
|
+
* })
|
|
12598
|
+
* }
|
|
12599
|
+
* ```
|
|
12600
|
+
*/
|
|
12601
|
+
function augmentEarnError(error, context) {
|
|
12602
|
+
const base = isKitError(error)
|
|
12603
|
+
? error
|
|
12604
|
+
: new KitError({
|
|
12605
|
+
...ServiceError.INTERNAL_ERROR,
|
|
12606
|
+
recoverability: 'RETRYABLE',
|
|
12607
|
+
message: getErrorMessage(error),
|
|
12608
|
+
});
|
|
12609
|
+
// Only upgrade to RESUMABLE when a prior *on-chain* phase succeeded. The
|
|
12610
|
+
// RESUMABLE label promises retry() can skip already-committed work — true
|
|
12611
|
+
// for a landed `approve` or `execute`, but a successful `fetchParams` saves
|
|
12612
|
+
// nothing (retry() re-fetches it anyway, since execution params/deadlines
|
|
12613
|
+
// expire), so it stays a plain RETRYABLE.
|
|
12614
|
+
const priorOnChainPhaseSucceeded = context.steps.some((step) => step.state === 'success' &&
|
|
12615
|
+
(step.name === 'approve' || step.name === 'execute'));
|
|
12616
|
+
const recoverability = priorOnChainPhaseSucceeded && base.recoverability === 'RETRYABLE'
|
|
12617
|
+
? 'RESUMABLE'
|
|
12618
|
+
: base.recoverability;
|
|
12619
|
+
const existingTrace = base.cause?.trace !== undefined &&
|
|
12620
|
+
base.cause.trace !== null &&
|
|
12621
|
+
typeof base.cause.trace === 'object'
|
|
12622
|
+
? base.cause.trace
|
|
12623
|
+
: {};
|
|
12624
|
+
// Snapshot the steps so the trace `retry()` reads can't be mutated by a
|
|
12625
|
+
// future code path that retains the live `EarnRunContext.steps` array.
|
|
12626
|
+
const trace = {
|
|
12627
|
+
...existingTrace,
|
|
12628
|
+
provider: context.providerName,
|
|
12629
|
+
operation: context.operation,
|
|
12630
|
+
steps: [...context.steps],
|
|
12631
|
+
};
|
|
12632
|
+
// Store the original service params non-enumerably. `retry()` reads them by
|
|
12633
|
+
// direct property access (`trace.params`), but JSON.stringify, console.log,
|
|
12634
|
+
// and telemetry serializers skip non-enumerable properties — so a
|
|
12635
|
+
// permissioned call's `config.kitKey` (a `KIT_KEY:<id>:<secret>` value) and
|
|
12636
|
+
// the in-memory adapter never leak through a logged error. defineProperty
|
|
12637
|
+
// also redefines any enumerable `params` an upstream trace may have carried.
|
|
12638
|
+
Object.defineProperty(trace, 'params', {
|
|
12639
|
+
value: context.params,
|
|
12640
|
+
enumerable: false,
|
|
12641
|
+
writable: true,
|
|
12642
|
+
configurable: true,
|
|
12643
|
+
});
|
|
12644
|
+
return new KitError({
|
|
12645
|
+
code: base.code,
|
|
12646
|
+
name: base.name,
|
|
12647
|
+
type: base.type,
|
|
12648
|
+
recoverability,
|
|
12649
|
+
message: base.message,
|
|
12650
|
+
cause: { trace },
|
|
12651
|
+
});
|
|
12652
|
+
}
|
|
12653
|
+
/**
|
|
12654
|
+
* Runtime type guard for {@link EarnErrorTrace}.
|
|
12655
|
+
*
|
|
12656
|
+
* Verifies the value carries the fields `retry()` needs to resume an earn
|
|
12657
|
+
* operation: a provider name, a known operation, a steps array, and a `params`
|
|
12658
|
+
* object whose shape matches that operation (an adapter context and vault
|
|
12659
|
+
* address, plus an amount for `deposit`/`withdraw`). Used to validate
|
|
12660
|
+
* `KitError.cause.trace` before resuming so a mismatched or forged trace is
|
|
12661
|
+
* rejected cleanly rather than crashing inside the resume path.
|
|
12662
|
+
*
|
|
12663
|
+
* @param value - The candidate value (typically `error.cause?.trace`).
|
|
12664
|
+
* @returns `true` if `value` is a usable {@link EarnErrorTrace}.
|
|
12665
|
+
*
|
|
12666
|
+
* @example
|
|
12667
|
+
* ```typescript
|
|
12668
|
+
* const trace = error.cause?.trace
|
|
12669
|
+
* if (isEarnErrorTrace(trace)) {
|
|
12670
|
+
* console.log(trace.operation, trace.steps.length)
|
|
12671
|
+
* }
|
|
12672
|
+
* ```
|
|
12673
|
+
*/
|
|
12674
|
+
function isEarnErrorTrace(value) {
|
|
12675
|
+
if (value === null || typeof value !== 'object') {
|
|
12676
|
+
return false;
|
|
12677
|
+
}
|
|
12678
|
+
const candidate = value;
|
|
12679
|
+
const operation = candidate['operation'];
|
|
12680
|
+
if (typeof candidate['provider'] !== 'string' ||
|
|
12681
|
+
typeof operation !== 'string' ||
|
|
12682
|
+
!EARN_OPERATIONS.has(operation) ||
|
|
12683
|
+
!Array.isArray(candidate['steps']) ||
|
|
12684
|
+
candidate['params'] === null ||
|
|
12685
|
+
typeof candidate['params'] !== 'object') {
|
|
12686
|
+
return false;
|
|
12687
|
+
}
|
|
12688
|
+
return hasEarnServiceParamsShape(operation, candidate['params']);
|
|
12689
|
+
}
|
|
12690
|
+
|
|
12185
12691
|
// ---------------------------------------------------------------------------
|
|
12186
12692
|
// Shared primitives
|
|
12187
12693
|
// ---------------------------------------------------------------------------
|
|
@@ -13169,12 +13675,63 @@ async function fetchClaimRewardsQuote(params) {
|
|
|
13169
13675
|
}
|
|
13170
13676
|
}
|
|
13171
13677
|
|
|
13678
|
+
/**
|
|
13679
|
+
* Build the `pending` {@link EarnStep} for a phase about to start,
|
|
13680
|
+
* discriminating on `stepName` so the result is a typed union member rather
|
|
13681
|
+
* than an unsafe `as EarnStep` cast.
|
|
13682
|
+
*
|
|
13683
|
+
* @param stepName - The name of the phase about to start.
|
|
13684
|
+
* @returns The typed pending step for the phase.
|
|
13685
|
+
*/
|
|
13686
|
+
function buildPendingStep(stepName) {
|
|
13687
|
+
if (stepName === 'fetchParams') {
|
|
13688
|
+
return { name: 'fetchParams', state: 'pending' };
|
|
13689
|
+
}
|
|
13690
|
+
return { name: stepName, state: 'pending' };
|
|
13691
|
+
}
|
|
13692
|
+
/**
|
|
13693
|
+
* Build the `success` {@link EarnStep} for a completed phase, discriminating
|
|
13694
|
+
* on `stepName` so the `fetchParams` variant (which has no `txHash` in its
|
|
13695
|
+
* type) cannot accidentally carry a transaction hash.
|
|
13696
|
+
*
|
|
13697
|
+
* @param stepName - The name of the completed phase.
|
|
13698
|
+
* @param txHash - The on-chain transaction hash, if any.
|
|
13699
|
+
* @returns The typed success step for the phase.
|
|
13700
|
+
*/
|
|
13701
|
+
function buildSuccessStep(stepName, txHash) {
|
|
13702
|
+
if (stepName === 'fetchParams') {
|
|
13703
|
+
return { name: 'fetchParams', state: 'success' };
|
|
13704
|
+
}
|
|
13705
|
+
if (txHash === undefined) {
|
|
13706
|
+
return { name: stepName, state: 'success' };
|
|
13707
|
+
}
|
|
13708
|
+
return { name: stepName, state: 'success', txHash };
|
|
13709
|
+
}
|
|
13710
|
+
/**
|
|
13711
|
+
* Build the `error` {@link EarnStep} for a failed phase, discriminating on
|
|
13712
|
+
* `stepName` so the result is a typed union member rather than an unsafe
|
|
13713
|
+
* `as EarnStep` cast.
|
|
13714
|
+
*
|
|
13715
|
+
* @param stepName - The name of the failed phase.
|
|
13716
|
+
* @param errorMessage - The human-readable failure message.
|
|
13717
|
+
* @param error - The raw error that caused the phase to fail.
|
|
13718
|
+
* @returns The typed error step for the phase.
|
|
13719
|
+
*/
|
|
13720
|
+
function buildErrorStep(stepName, errorMessage, error) {
|
|
13721
|
+
if (stepName === 'fetchParams') {
|
|
13722
|
+
return { name: 'fetchParams', state: 'error', errorMessage, error };
|
|
13723
|
+
}
|
|
13724
|
+
return { name: stepName, state: 'error', errorMessage, error };
|
|
13725
|
+
}
|
|
13172
13726
|
/**
|
|
13173
13727
|
* Earn Service provider for the Circle earn service API.
|
|
13174
13728
|
*
|
|
13175
13729
|
* Implement the {@link EarningProvider} interface for yield-bearing vault
|
|
13176
13730
|
* operations including vault discovery, position queries, deposits,
|
|
13177
|
-
* withdrawals, and reward claiming.
|
|
13731
|
+
* withdrawals, and reward claiming. Multi-phase operations (deposit,
|
|
13732
|
+
* withdraw, claimRewards) emit step-level events through the kit's action
|
|
13733
|
+
* dispatcher and attach step progress to thrown errors so callers can resume
|
|
13734
|
+
* via {@link EarnServiceProvider.retry}.
|
|
13178
13735
|
*
|
|
13179
13736
|
* @example
|
|
13180
13737
|
* ```typescript
|
|
@@ -13205,6 +13762,8 @@ class EarnServiceProvider {
|
|
|
13205
13762
|
name = 'EarnService';
|
|
13206
13763
|
/** {@inheritdoc} */
|
|
13207
13764
|
supportedChains = Object.keys(CHAIN_TO_API).map((chain) => resolveChainIdentifier(chain));
|
|
13765
|
+
/** {@inheritdoc} */
|
|
13766
|
+
actionDispatcher = undefined;
|
|
13208
13767
|
defaultConfig;
|
|
13209
13768
|
/**
|
|
13210
13769
|
* Create a new EarnServiceProvider.
|
|
@@ -13215,12 +13774,49 @@ class EarnServiceProvider {
|
|
|
13215
13774
|
constructor(config) {
|
|
13216
13775
|
this.defaultConfig = config;
|
|
13217
13776
|
}
|
|
13777
|
+
/** {@inheritdoc} */
|
|
13778
|
+
registerDispatcher(dispatcher) {
|
|
13779
|
+
this.actionDispatcher = dispatcher;
|
|
13780
|
+
}
|
|
13218
13781
|
resolveConfig(config) {
|
|
13219
13782
|
const resolvedConfig = this.defaultConfig === undefined
|
|
13220
13783
|
? config
|
|
13221
13784
|
: { ...this.defaultConfig, ...config };
|
|
13222
13785
|
return resolvedConfig;
|
|
13223
13786
|
}
|
|
13787
|
+
/**
|
|
13788
|
+
* Run one phase of a multi-phase earn operation: dispatch a `pending` event,
|
|
13789
|
+
* execute it, then dispatch a `success` event (recording the resulting step)
|
|
13790
|
+
* or — on failure — dispatch an `error` event, record the failed step, and
|
|
13791
|
+
* re-throw the original error for the caller to wrap with full step context.
|
|
13792
|
+
*
|
|
13793
|
+
* @typeParam R - The phase's result type.
|
|
13794
|
+
* @param ctx - The operation context (dispatcher, operation, steps, params).
|
|
13795
|
+
* @param action - The action name to dispatch the events under.
|
|
13796
|
+
* @param stepName - The phase name.
|
|
13797
|
+
* @param run - The phase work to execute.
|
|
13798
|
+
* @param txHashOf - Extracts the on-chain transaction hash from the result, if any.
|
|
13799
|
+
* @returns The phase result.
|
|
13800
|
+
*/
|
|
13801
|
+
async runPhase(ctx, action, stepName, run, txHashOf) {
|
|
13802
|
+
dispatchEarnEvent(ctx.dispatcher, action, ctx.operation, buildPendingStep(stepName));
|
|
13803
|
+
let result;
|
|
13804
|
+
try {
|
|
13805
|
+
result = await run();
|
|
13806
|
+
}
|
|
13807
|
+
catch (error) {
|
|
13808
|
+
const failedStep = buildErrorStep(stepName, getErrorMessage(error), error);
|
|
13809
|
+
ctx.steps.push(failedStep);
|
|
13810
|
+
dispatchEarnEvent(ctx.dispatcher, action, ctx.operation, failedStep);
|
|
13811
|
+
throw error;
|
|
13812
|
+
}
|
|
13813
|
+
// Discriminate on stepName so the `fetchParams` variant (which has no
|
|
13814
|
+
// `txHash` field in its type) cannot accidentally pick up a tx hash.
|
|
13815
|
+
const successStep = buildSuccessStep(stepName, txHashOf(result));
|
|
13816
|
+
ctx.steps.push(successStep);
|
|
13817
|
+
dispatchEarnEvent(ctx.dispatcher, action, ctx.operation, successStep);
|
|
13818
|
+
return result;
|
|
13819
|
+
}
|
|
13224
13820
|
/** {@inheritdoc} */
|
|
13225
13821
|
async getVaults(params) {
|
|
13226
13822
|
const config = this.resolveConfig(params.config);
|
|
@@ -13242,146 +13838,246 @@ class EarnServiceProvider {
|
|
|
13242
13838
|
}
|
|
13243
13839
|
/** {@inheritdoc} */
|
|
13244
13840
|
async deposit(params) {
|
|
13245
|
-
|
|
13246
|
-
|
|
13247
|
-
|
|
13248
|
-
const
|
|
13249
|
-
|
|
13250
|
-
|
|
13251
|
-
|
|
13252
|
-
|
|
13253
|
-
|
|
13254
|
-
|
|
13255
|
-
|
|
13256
|
-
address,
|
|
13257
|
-
chain
|
|
13258
|
-
|
|
13259
|
-
|
|
13260
|
-
|
|
13261
|
-
|
|
13262
|
-
|
|
13263
|
-
|
|
13264
|
-
|
|
13265
|
-
|
|
13841
|
+
return this.runDepositFlow(params, { skipApprove: false });
|
|
13842
|
+
}
|
|
13843
|
+
async runDepositFlow(params, options) {
|
|
13844
|
+
const ctx = {
|
|
13845
|
+
dispatcher: this.actionDispatcher,
|
|
13846
|
+
operation: 'deposit',
|
|
13847
|
+
steps: [],
|
|
13848
|
+
params,
|
|
13849
|
+
};
|
|
13850
|
+
try {
|
|
13851
|
+
const config = this.resolveConfig(params.config);
|
|
13852
|
+
const { address, chain: apiChain, chainDefinition: chain, } = await resolveAdapterContext(params.from);
|
|
13853
|
+
const adapterContractAddress = requireAdapterContract(chain);
|
|
13854
|
+
const rawUsdcAddress = chain.usdcAddress;
|
|
13855
|
+
if (rawUsdcAddress === null) {
|
|
13856
|
+
throw createUnsupportedTokenError('USDC', chain.name);
|
|
13857
|
+
}
|
|
13858
|
+
const usdcAddress = assertHexAddress('chain.usdcAddress', rawUsdcAddress, `USDC address for chain ${chain.name} must be a 0x-prefixed 20-byte hex address.`);
|
|
13859
|
+
const { adapter } = params.from;
|
|
13860
|
+
const { executionParams, signature } = await this.runPhase(ctx, 'deposit', 'fetchParams', async () => fetchDeposit({
|
|
13861
|
+
vaultAddress: params.vaultAddress,
|
|
13862
|
+
amount: params.amount,
|
|
13863
|
+
address,
|
|
13864
|
+
chain: apiChain,
|
|
13865
|
+
config,
|
|
13866
|
+
}), () => undefined);
|
|
13867
|
+
validateExecutionDeadline(executionParams);
|
|
13868
|
+
const tokenInputs = buildEarnTokenInputs(executionParams, usdcAddress);
|
|
13869
|
+
const approvalToken = tokenInputs[0]?.token;
|
|
13870
|
+
if (!options.skipApprove && approvalToken !== undefined) {
|
|
13871
|
+
await this.runPhase(ctx, 'approve', 'approve', async () => approveMaxIfNeeded({
|
|
13872
|
+
adapter,
|
|
13873
|
+
chain,
|
|
13874
|
+
tokenAddress: approvalToken,
|
|
13875
|
+
delegate: adapterContractAddress,
|
|
13876
|
+
address,
|
|
13877
|
+
revertMessage: 'USDC approval reverted on-chain',
|
|
13878
|
+
}), (txHash) => txHash);
|
|
13879
|
+
}
|
|
13880
|
+
const { txHash, explorerUrl } = await this.runPhase(ctx, 'deposit', 'execute', async () => executeEarnAction({
|
|
13266
13881
|
adapter,
|
|
13267
13882
|
chain,
|
|
13268
|
-
tokenAddress: approvalToken,
|
|
13269
|
-
delegate: adapterContractAddress,
|
|
13270
13883
|
address,
|
|
13271
|
-
|
|
13884
|
+
actionKey: 'earn.deposit',
|
|
13885
|
+
actionParams: {
|
|
13886
|
+
executeParams: executionParams,
|
|
13887
|
+
tokenInputs,
|
|
13888
|
+
signature,
|
|
13889
|
+
},
|
|
13890
|
+
revertMessage: 'Earn deposit reverted on-chain',
|
|
13891
|
+
}), ({ txHash }) => txHash);
|
|
13892
|
+
return {
|
|
13893
|
+
txHash,
|
|
13894
|
+
explorerUrl,
|
|
13895
|
+
vaultAddress: params.vaultAddress,
|
|
13896
|
+
amount: params.amount,
|
|
13897
|
+
};
|
|
13898
|
+
}
|
|
13899
|
+
catch (error) {
|
|
13900
|
+
throw augmentEarnError(error, {
|
|
13901
|
+
providerName: this.name,
|
|
13902
|
+
operation: ctx.operation,
|
|
13903
|
+
steps: ctx.steps,
|
|
13904
|
+
params: ctx.params,
|
|
13272
13905
|
});
|
|
13273
13906
|
}
|
|
13274
|
-
const { txHash, explorerUrl } = await executeEarnAction({
|
|
13275
|
-
adapter,
|
|
13276
|
-
chain,
|
|
13277
|
-
address,
|
|
13278
|
-
actionKey: 'earn.deposit',
|
|
13279
|
-
actionParams: {
|
|
13280
|
-
executeParams: executionParams,
|
|
13281
|
-
tokenInputs,
|
|
13282
|
-
signature,
|
|
13283
|
-
},
|
|
13284
|
-
revertMessage: 'Earn deposit reverted on-chain',
|
|
13285
|
-
});
|
|
13286
|
-
return {
|
|
13287
|
-
txHash,
|
|
13288
|
-
explorerUrl,
|
|
13289
|
-
vaultAddress: params.vaultAddress,
|
|
13290
|
-
amount: params.amount,
|
|
13291
|
-
};
|
|
13292
13907
|
}
|
|
13293
13908
|
/** {@inheritdoc} */
|
|
13294
13909
|
async withdraw(params) {
|
|
13295
|
-
|
|
13296
|
-
|
|
13297
|
-
|
|
13298
|
-
const
|
|
13299
|
-
|
|
13300
|
-
|
|
13301
|
-
|
|
13302
|
-
|
|
13303
|
-
|
|
13304
|
-
|
|
13305
|
-
|
|
13306
|
-
|
|
13307
|
-
|
|
13308
|
-
|
|
13309
|
-
|
|
13310
|
-
|
|
13311
|
-
|
|
13910
|
+
return this.runWithdrawFlow(params, { skipApprove: false });
|
|
13911
|
+
}
|
|
13912
|
+
async runWithdrawFlow(params, options) {
|
|
13913
|
+
const ctx = {
|
|
13914
|
+
dispatcher: this.actionDispatcher,
|
|
13915
|
+
operation: 'withdraw',
|
|
13916
|
+
steps: [],
|
|
13917
|
+
params,
|
|
13918
|
+
};
|
|
13919
|
+
try {
|
|
13920
|
+
const config = this.resolveConfig(params.config);
|
|
13921
|
+
const { address, chain: apiChain, chainDefinition: chain, } = await resolveAdapterContext(params.from);
|
|
13922
|
+
const adapterContractAddress = requireAdapterContract(chain);
|
|
13923
|
+
const vaultAddress = assertHexAddress('vaultAddress', params.vaultAddress, 'Vault address must be a 0x-prefixed 20-byte hex address.');
|
|
13924
|
+
const { adapter } = params.from;
|
|
13925
|
+
const { executionParams, signature } = await this.runPhase(ctx, 'withdraw', 'fetchParams', async () => fetchWithdraw({
|
|
13926
|
+
vaultAddress,
|
|
13927
|
+
amount: params.amount,
|
|
13928
|
+
address,
|
|
13929
|
+
chain: apiChain,
|
|
13930
|
+
config,
|
|
13931
|
+
}), () => undefined);
|
|
13932
|
+
validateExecutionDeadline(executionParams);
|
|
13933
|
+
const tokenInputs = buildEarnTokenInputs(executionParams, vaultAddress);
|
|
13934
|
+
const approvalToken = tokenInputs[0]?.token;
|
|
13935
|
+
if (!options.skipApprove && approvalToken !== undefined) {
|
|
13936
|
+
await this.runPhase(ctx, 'approve', 'approve', async () => approveMaxIfNeeded({
|
|
13937
|
+
adapter,
|
|
13938
|
+
chain,
|
|
13939
|
+
tokenAddress: approvalToken,
|
|
13940
|
+
delegate: adapterContractAddress,
|
|
13941
|
+
address,
|
|
13942
|
+
revertMessage: 'Vault share token approval reverted on-chain',
|
|
13943
|
+
}), (txHash) => txHash);
|
|
13944
|
+
}
|
|
13945
|
+
const { txHash, explorerUrl } = await this.runPhase(ctx, 'withdraw', 'execute', async () => executeEarnAction({
|
|
13312
13946
|
adapter,
|
|
13313
13947
|
chain,
|
|
13314
|
-
tokenAddress: approvalToken,
|
|
13315
|
-
delegate: adapterContractAddress,
|
|
13316
13948
|
address,
|
|
13317
|
-
|
|
13949
|
+
actionKey: 'earn.withdraw',
|
|
13950
|
+
actionParams: {
|
|
13951
|
+
executeParams: executionParams,
|
|
13952
|
+
tokenInputs,
|
|
13953
|
+
signature,
|
|
13954
|
+
},
|
|
13955
|
+
revertMessage: 'Earn withdraw reverted on-chain',
|
|
13956
|
+
}), ({ txHash }) => txHash);
|
|
13957
|
+
return {
|
|
13958
|
+
txHash,
|
|
13959
|
+
explorerUrl,
|
|
13960
|
+
vaultAddress,
|
|
13961
|
+
amount: params.amount,
|
|
13962
|
+
};
|
|
13963
|
+
}
|
|
13964
|
+
catch (error) {
|
|
13965
|
+
throw augmentEarnError(error, {
|
|
13966
|
+
providerName: this.name,
|
|
13967
|
+
operation: ctx.operation,
|
|
13968
|
+
steps: ctx.steps,
|
|
13969
|
+
params: ctx.params,
|
|
13318
13970
|
});
|
|
13319
13971
|
}
|
|
13320
|
-
const { txHash, explorerUrl } = await executeEarnAction({
|
|
13321
|
-
adapter,
|
|
13322
|
-
chain,
|
|
13323
|
-
address,
|
|
13324
|
-
actionKey: 'earn.withdraw',
|
|
13325
|
-
actionParams: {
|
|
13326
|
-
executeParams: executionParams,
|
|
13327
|
-
tokenInputs,
|
|
13328
|
-
signature,
|
|
13329
|
-
},
|
|
13330
|
-
revertMessage: 'Earn withdraw reverted on-chain',
|
|
13331
|
-
});
|
|
13332
|
-
return {
|
|
13333
|
-
txHash,
|
|
13334
|
-
explorerUrl,
|
|
13335
|
-
vaultAddress,
|
|
13336
|
-
amount: params.amount,
|
|
13337
|
-
};
|
|
13338
13972
|
}
|
|
13339
13973
|
/** {@inheritdoc} */
|
|
13340
13974
|
async claimRewards(params) {
|
|
13341
|
-
|
|
13342
|
-
|
|
13343
|
-
|
|
13344
|
-
|
|
13345
|
-
|
|
13346
|
-
|
|
13347
|
-
|
|
13348
|
-
|
|
13349
|
-
|
|
13350
|
-
|
|
13351
|
-
|
|
13352
|
-
|
|
13353
|
-
|
|
13354
|
-
|
|
13355
|
-
|
|
13356
|
-
|
|
13357
|
-
|
|
13358
|
-
|
|
13359
|
-
|
|
13360
|
-
|
|
13361
|
-
|
|
13362
|
-
|
|
13363
|
-
|
|
13364
|
-
|
|
13365
|
-
|
|
13366
|
-
|
|
13975
|
+
return this.runClaimRewardsFlow(params);
|
|
13976
|
+
}
|
|
13977
|
+
async runClaimRewardsFlow(params) {
|
|
13978
|
+
const ctx = {
|
|
13979
|
+
dispatcher: this.actionDispatcher,
|
|
13980
|
+
operation: 'claimRewards',
|
|
13981
|
+
steps: [],
|
|
13982
|
+
params,
|
|
13983
|
+
};
|
|
13984
|
+
try {
|
|
13985
|
+
const config = this.resolveConfig(params.config);
|
|
13986
|
+
const { address, chain: apiChain, chainDefinition: chain, } = await resolveAdapterContext(params.from);
|
|
13987
|
+
// Claim rewards has no approval step, but still requires adapter support.
|
|
13988
|
+
requireAdapterContract(chain);
|
|
13989
|
+
const { rewards, executionParams, signature } = await this.runPhase(ctx, 'claimRewards', 'fetchParams', async () => fetchClaimRewards({
|
|
13990
|
+
address,
|
|
13991
|
+
chain: apiChain,
|
|
13992
|
+
vaultAddress: params.vaultAddress,
|
|
13993
|
+
config,
|
|
13994
|
+
}), () => undefined);
|
|
13995
|
+
const nothingToClaim = rewards.length === 0;
|
|
13996
|
+
if (nothingToClaim) {
|
|
13997
|
+
return { status: 'no_rewards', rewards: [] };
|
|
13998
|
+
}
|
|
13999
|
+
const missingExecutionParams = executionParams === undefined;
|
|
14000
|
+
const missingSignature = signature === undefined;
|
|
14001
|
+
if (missingExecutionParams || missingSignature) {
|
|
14002
|
+
throw new KitError({
|
|
14003
|
+
...EarnError.INTERNAL_ERROR,
|
|
14004
|
+
recoverability: 'RETRYABLE',
|
|
14005
|
+
message: 'Claim rewards response must include executionParams and signature when rewards are claimable',
|
|
14006
|
+
cause: {
|
|
14007
|
+
trace: {
|
|
14008
|
+
rewardsCount: rewards.length,
|
|
14009
|
+
missingExecutionParams,
|
|
14010
|
+
missingSignature,
|
|
14011
|
+
},
|
|
13367
14012
|
},
|
|
14013
|
+
});
|
|
14014
|
+
}
|
|
14015
|
+
validateExecutionDeadline(executionParams);
|
|
14016
|
+
const { txHash, explorerUrl } = await this.runPhase(ctx, 'claimRewards', 'execute', async () => executeEarnAction({
|
|
14017
|
+
adapter: params.from.adapter,
|
|
14018
|
+
chain,
|
|
14019
|
+
address,
|
|
14020
|
+
actionKey: 'earn.claimRewards',
|
|
14021
|
+
actionParams: {
|
|
14022
|
+
executeParams: executionParams,
|
|
14023
|
+
tokenInputs: [],
|
|
14024
|
+
signature,
|
|
13368
14025
|
},
|
|
14026
|
+
revertMessage: 'Earn claim rewards reverted on-chain',
|
|
14027
|
+
}), ({ txHash }) => txHash);
|
|
14028
|
+
return { status: 'claimed', rewards, txHash, explorerUrl };
|
|
14029
|
+
}
|
|
14030
|
+
catch (error) {
|
|
14031
|
+
throw augmentEarnError(error, {
|
|
14032
|
+
providerName: this.name,
|
|
14033
|
+
operation: ctx.operation,
|
|
14034
|
+
steps: ctx.steps,
|
|
14035
|
+
params: ctx.params,
|
|
13369
14036
|
});
|
|
13370
14037
|
}
|
|
13371
|
-
|
|
13372
|
-
|
|
13373
|
-
|
|
13374
|
-
|
|
13375
|
-
|
|
13376
|
-
|
|
13377
|
-
|
|
13378
|
-
|
|
13379
|
-
|
|
13380
|
-
|
|
13381
|
-
|
|
13382
|
-
|
|
13383
|
-
|
|
13384
|
-
|
|
14038
|
+
}
|
|
14039
|
+
/** {@inheritdoc} */
|
|
14040
|
+
supportsRetry(error) {
|
|
14041
|
+
return (isKitError(error) &&
|
|
14042
|
+
isRetryableError$1(error) &&
|
|
14043
|
+
isEarnErrorTrace(error.cause?.trace) &&
|
|
14044
|
+
error.cause.trace.provider === this.name);
|
|
14045
|
+
}
|
|
14046
|
+
/** {@inheritdoc} */
|
|
14047
|
+
async retry(error) {
|
|
14048
|
+
// Validation order mirrors EarnKit.retry().
|
|
14049
|
+
if (!isKitError(error)) {
|
|
14050
|
+
throw createValidationFailedError('error', error, 'retry() requires a KitError thrown by a previous earn operation');
|
|
14051
|
+
}
|
|
14052
|
+
if (!isRetryableError$1(error)) {
|
|
14053
|
+
throw createValidationFailedError('error.recoverability', error.recoverability, 'retry() requires a retryable or resumable error — check isRetryableError(error) first');
|
|
14054
|
+
}
|
|
14055
|
+
const trace = error.cause?.trace;
|
|
14056
|
+
if (!isEarnErrorTrace(trace)) {
|
|
14057
|
+
throw createValidationFailedError('error.cause.trace', trace, 'retry() requires a KitError carrying earn retry context (operation, steps, provider, params)');
|
|
14058
|
+
}
|
|
14059
|
+
if (trace.provider !== this.name) {
|
|
14060
|
+
throw createValidationFailedError('error.cause.trace.provider', trace.provider, `Cannot retry: error was produced by provider "${trace.provider}", not "${this.name}"`);
|
|
14061
|
+
}
|
|
14062
|
+
const approveCompleted = trace.steps.some((step) => step.name === 'approve' && step.state === 'success');
|
|
14063
|
+
// `trace` is a discriminated union on `operation`, so each branch narrows
|
|
14064
|
+
// `trace.params` to the matching service-params type — no cast needed.
|
|
14065
|
+
switch (trace.operation) {
|
|
14066
|
+
case 'deposit':
|
|
14067
|
+
return this.runDepositFlow(trace.params, {
|
|
14068
|
+
skipApprove: approveCompleted,
|
|
14069
|
+
});
|
|
14070
|
+
case 'withdraw':
|
|
14071
|
+
return this.runWithdrawFlow(trace.params, {
|
|
14072
|
+
skipApprove: approveCompleted,
|
|
14073
|
+
});
|
|
14074
|
+
case 'claimRewards':
|
|
14075
|
+
return this.runClaimRewardsFlow(trace.params);
|
|
14076
|
+
default: {
|
|
14077
|
+
const exhaustive = trace;
|
|
14078
|
+
throw createValidationFailedError('error.cause.trace.operation', exhaustive, 'retry() does not support this earn operation');
|
|
14079
|
+
}
|
|
14080
|
+
}
|
|
13385
14081
|
}
|
|
13386
14082
|
/** {@inheritdoc} */
|
|
13387
14083
|
async getDepositQuote(params) {
|
|
@@ -13465,54 +14161,6 @@ function createEarnKitContext(config = {}) {
|
|
|
13465
14161
|
return context;
|
|
13466
14162
|
}
|
|
13467
14163
|
|
|
13468
|
-
/**
|
|
13469
|
-
* Symbol used to track that assertEarnParams has validated an object.
|
|
13470
|
-
* @internal
|
|
13471
|
-
*/
|
|
13472
|
-
const ASSERT_EARN_PARAMS_SYMBOL = Symbol('assertEarnParams');
|
|
13473
|
-
/**
|
|
13474
|
-
* Assert that the provided value conforms to the given earn params schema.
|
|
13475
|
-
*
|
|
13476
|
-
* Validate earn parameters using the provided Zod schema and track
|
|
13477
|
-
* validation state to avoid duplicate checks. Throw a structured
|
|
13478
|
-
* error with detailed validation messages if any parameter is invalid.
|
|
13479
|
-
*
|
|
13480
|
-
* @typeParam T - The expected type after validation
|
|
13481
|
-
* @param params - The earn parameters to validate
|
|
13482
|
-
* @param schema - The Zod schema to validate against
|
|
13483
|
-
* @throws {@link KitError} If the parameters fail validation
|
|
13484
|
-
*
|
|
13485
|
-
* @example
|
|
13486
|
-
* ```typescript
|
|
13487
|
-
* import { assertEarnParams, depositParamsSchema } from '@circle-fin/earn-kit'
|
|
13488
|
-
*
|
|
13489
|
-
* assertEarnParams(params, depositParamsSchema)
|
|
13490
|
-
* ```
|
|
13491
|
-
*/
|
|
13492
|
-
function assertEarnParams(params, schema) {
|
|
13493
|
-
validateWithStateTracking(params, schema, 'earn parameters', ASSERT_EARN_PARAMS_SYMBOL);
|
|
13494
|
-
}
|
|
13495
|
-
|
|
13496
|
-
function findProvider(context, chain, operation = 'earn') {
|
|
13497
|
-
let fallback;
|
|
13498
|
-
for (const provider of context.providers) {
|
|
13499
|
-
fallback ??= provider;
|
|
13500
|
-
if (provider.supportedChains.length === 0)
|
|
13501
|
-
continue;
|
|
13502
|
-
if (chain === undefined ||
|
|
13503
|
-
provider.supportedChains.some((c) => c.chain === chain.chain)) {
|
|
13504
|
-
return provider;
|
|
13505
|
-
}
|
|
13506
|
-
}
|
|
13507
|
-
if (fallback === undefined) {
|
|
13508
|
-
throw createValidationFailedError('context.providers', [], 'No earn providers configured');
|
|
13509
|
-
}
|
|
13510
|
-
if (chain !== undefined) {
|
|
13511
|
-
throw createUnsupportedEarnRouteError(operation, chain.name);
|
|
13512
|
-
}
|
|
13513
|
-
return fallback;
|
|
13514
|
-
}
|
|
13515
|
-
|
|
13516
14164
|
/**
|
|
13517
14165
|
* Format a provider amount object as a human-readable decimal string.
|
|
13518
14166
|
*
|
|
@@ -13655,6 +14303,54 @@ function formatClaimRewardsResult(result) {
|
|
|
13655
14303
|
};
|
|
13656
14304
|
}
|
|
13657
14305
|
|
|
14306
|
+
/**
|
|
14307
|
+
* Symbol used to track that assertEarnParams has validated an object.
|
|
14308
|
+
* @internal
|
|
14309
|
+
*/
|
|
14310
|
+
const ASSERT_EARN_PARAMS_SYMBOL = Symbol('assertEarnParams');
|
|
14311
|
+
/**
|
|
14312
|
+
* Assert that the provided value conforms to the given earn params schema.
|
|
14313
|
+
*
|
|
14314
|
+
* Validate earn parameters using the provided Zod schema and track
|
|
14315
|
+
* validation state to avoid duplicate checks. Throw a structured
|
|
14316
|
+
* error with detailed validation messages if any parameter is invalid.
|
|
14317
|
+
*
|
|
14318
|
+
* @typeParam T - The expected type after validation
|
|
14319
|
+
* @param params - The earn parameters to validate
|
|
14320
|
+
* @param schema - The Zod schema to validate against
|
|
14321
|
+
* @throws {@link KitError} If the parameters fail validation
|
|
14322
|
+
*
|
|
14323
|
+
* @example
|
|
14324
|
+
* ```typescript
|
|
14325
|
+
* import { assertEarnParams, depositParamsSchema } from '@circle-fin/earn-kit'
|
|
14326
|
+
*
|
|
14327
|
+
* assertEarnParams(params, depositParamsSchema)
|
|
14328
|
+
* ```
|
|
14329
|
+
*/
|
|
14330
|
+
function assertEarnParams(params, schema) {
|
|
14331
|
+
validateWithStateTracking(params, schema, 'earn parameters', ASSERT_EARN_PARAMS_SYMBOL);
|
|
14332
|
+
}
|
|
14333
|
+
|
|
14334
|
+
function findProvider(context, chain, operation = 'earn') {
|
|
14335
|
+
let fallback;
|
|
14336
|
+
for (const provider of context.providers) {
|
|
14337
|
+
fallback ??= provider;
|
|
14338
|
+
if (provider.supportedChains.length === 0)
|
|
14339
|
+
continue;
|
|
14340
|
+
if (chain === undefined ||
|
|
14341
|
+
provider.supportedChains.some((c) => c.chain === chain.chain)) {
|
|
14342
|
+
return provider;
|
|
14343
|
+
}
|
|
14344
|
+
}
|
|
14345
|
+
if (fallback === undefined) {
|
|
14346
|
+
throw createValidationFailedError('context.providers', [], 'No earn providers configured');
|
|
14347
|
+
}
|
|
14348
|
+
if (chain !== undefined) {
|
|
14349
|
+
throw createUnsupportedEarnRouteError(operation, chain.name);
|
|
14350
|
+
}
|
|
14351
|
+
return fallback;
|
|
14352
|
+
}
|
|
14353
|
+
|
|
13658
14354
|
/**
|
|
13659
14355
|
* Schema for the adapter context within earn operations.
|
|
13660
14356
|
*
|
|
@@ -14304,6 +15000,19 @@ async function getClaimRewardsQuote$1(context, params) {
|
|
|
14304
15000
|
return formatClaimRewardsQuoteInfo(result);
|
|
14305
15001
|
}
|
|
14306
15002
|
|
|
15003
|
+
function formatRetryResult(operation, result) {
|
|
15004
|
+
switch (operation) {
|
|
15005
|
+
case 'deposit':
|
|
15006
|
+
case 'withdraw':
|
|
15007
|
+
return result;
|
|
15008
|
+
case 'claimRewards':
|
|
15009
|
+
return formatClaimRewardsResult(result);
|
|
15010
|
+
default: {
|
|
15011
|
+
const exhaustive = operation;
|
|
15012
|
+
throw createValidationFailedError('error.cause.trace.operation', exhaustive, 'EarnKit.retry() does not support this earn operation');
|
|
15013
|
+
}
|
|
15014
|
+
}
|
|
15015
|
+
}
|
|
14307
15016
|
/**
|
|
14308
15017
|
* A high-level class-based interface for DeFi lending vault operations.
|
|
14309
15018
|
*
|
|
@@ -14361,6 +15070,12 @@ async function getClaimRewardsQuote$1(context, params) {
|
|
|
14361
15070
|
*/
|
|
14362
15071
|
class EarnKit {
|
|
14363
15072
|
context;
|
|
15073
|
+
/**
|
|
15074
|
+
* Event dispatcher for step-level events emitted during multi-phase earn
|
|
15075
|
+
* operations. Prefer {@link EarnKit.on} / {@link EarnKit.off} over using
|
|
15076
|
+
* this directly.
|
|
15077
|
+
*/
|
|
15078
|
+
actionDispatcher;
|
|
14364
15079
|
/**
|
|
14365
15080
|
* Create a new EarnKit instance.
|
|
14366
15081
|
*
|
|
@@ -14380,6 +15095,89 @@ class EarnKit {
|
|
|
14380
15095
|
*/
|
|
14381
15096
|
constructor(config = {}) {
|
|
14382
15097
|
this.context = createEarnKitContext(config);
|
|
15098
|
+
this.actionDispatcher = new Actionable();
|
|
15099
|
+
for (const provider of this.context.providers) {
|
|
15100
|
+
provider.registerDispatcher(this.actionDispatcher);
|
|
15101
|
+
}
|
|
15102
|
+
}
|
|
15103
|
+
on(action, handler) {
|
|
15104
|
+
if (action === '*') {
|
|
15105
|
+
this.actionDispatcher.on('*', handler);
|
|
15106
|
+
}
|
|
15107
|
+
else {
|
|
15108
|
+
this.actionDispatcher.on(action, handler);
|
|
15109
|
+
}
|
|
15110
|
+
}
|
|
15111
|
+
off(action, handler) {
|
|
15112
|
+
if (action === '*') {
|
|
15113
|
+
this.actionDispatcher.off('*', handler);
|
|
15114
|
+
}
|
|
15115
|
+
else {
|
|
15116
|
+
this.actionDispatcher.off(action, handler);
|
|
15117
|
+
}
|
|
15118
|
+
}
|
|
15119
|
+
/**
|
|
15120
|
+
* Resume a multi-phase earn operation that previously failed.
|
|
15121
|
+
*
|
|
15122
|
+
* Pass the {@link KitError} caught from a `deposit`, `withdraw`, or
|
|
15123
|
+
* `claimRewards` call. The error carries the original operation, inputs,
|
|
15124
|
+
* and step progress, so the operation can be re-run while skipping phases
|
|
15125
|
+
* that already completed (for example a successful token approval). Use
|
|
15126
|
+
* `isRetryableError(error)` to check whether retrying is worthwhile before
|
|
15127
|
+
* calling this.
|
|
15128
|
+
*
|
|
15129
|
+
* @remarks
|
|
15130
|
+
* Resuming re-fetches execution params and re-submits the `execute`
|
|
15131
|
+
* transaction; the earn service deduplicates execution server-side, which
|
|
15132
|
+
* makes this safe in the common case. But if a prior attempt broadcast the
|
|
15133
|
+
* `execute` transaction and then failed before its receipt was observed,
|
|
15134
|
+
* that transaction may still be in flight when `retry()` re-broadcasts.
|
|
15135
|
+
* Treat `retry()` as best-effort recovery, not an atomic operation.
|
|
15136
|
+
*
|
|
15137
|
+
* @param error - The error caught from a previous multi-phase earn operation.
|
|
15138
|
+
* @returns A promise resolving to the result of the resumed operation.
|
|
15139
|
+
* @throws {@link KitError} If `error` is not a retryable {@link KitError}
|
|
15140
|
+
* carrying earn retry context, names an unknown provider, or the resumed
|
|
15141
|
+
* operation itself fails.
|
|
15142
|
+
*
|
|
15143
|
+
* @example
|
|
15144
|
+
* ```typescript
|
|
15145
|
+
* import { isRetryableError } from '@circle-fin/earn-kit'
|
|
15146
|
+
*
|
|
15147
|
+
* try {
|
|
15148
|
+
* await kit.deposit(params)
|
|
15149
|
+
* } catch (error) {
|
|
15150
|
+
* if (isRetryableError(error)) {
|
|
15151
|
+
* const result = await kit.retry(error)
|
|
15152
|
+
* console.log('recovered, tx:', 'txHash' in result ? result.txHash : undefined)
|
|
15153
|
+
* }
|
|
15154
|
+
* }
|
|
15155
|
+
* ```
|
|
15156
|
+
*/
|
|
15157
|
+
async retry(error) {
|
|
15158
|
+
if (!isKitError(error)) {
|
|
15159
|
+
throw createValidationFailedError('error', error, 'EarnKit.retry() requires a KitError thrown by a previous earn operation');
|
|
15160
|
+
}
|
|
15161
|
+
if (!isRetryableError$1(error)) {
|
|
15162
|
+
throw createValidationFailedError('error.recoverability', error.recoverability, 'EarnKit.retry() requires a retryable or resumable error — check isRetryableError(error) first');
|
|
15163
|
+
}
|
|
15164
|
+
const trace = error.cause?.trace;
|
|
15165
|
+
if (!isEarnErrorTrace(trace)) {
|
|
15166
|
+
throw createValidationFailedError('error.cause.trace', trace, 'EarnKit.retry() requires a KitError carrying earn retry context (operation, steps, provider, params)');
|
|
15167
|
+
}
|
|
15168
|
+
const provider = this.context.providers.find((candidate) => candidate.name === trace.provider);
|
|
15169
|
+
if (provider === undefined) {
|
|
15170
|
+
throw createValidationFailedError('error.cause.trace.provider', trace.provider, `No earn provider named "${trace.provider}" is registered with this kit`);
|
|
15171
|
+
}
|
|
15172
|
+
const result = await provider.retry(error);
|
|
15173
|
+
// `provider.retry` returns a flat result union with no compile-time link to
|
|
15174
|
+
// `trace.operation`, so narrow the operation here to select the matching
|
|
15175
|
+
// overload. The result cast in each branch is sound: the provider always
|
|
15176
|
+
// returns the result type corresponding to the resumed operation.
|
|
15177
|
+
if (trace.operation === 'claimRewards') {
|
|
15178
|
+
return formatRetryResult(trace.operation, result);
|
|
15179
|
+
}
|
|
15180
|
+
return formatRetryResult(trace.operation, result);
|
|
14383
15181
|
}
|
|
14384
15182
|
/**
|
|
14385
15183
|
* Return the chains supported by configured earn providers.
|