@portal-hq/web 3.13.2 → 3.14.0-alpha.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/lib/commonjs/index.js +127 -9
- package/lib/commonjs/index.test.js +13 -0
- package/lib/commonjs/integrations/delegations/index.js +109 -2
- package/lib/commonjs/integrations/delegations/index.test.js +171 -0
- package/lib/commonjs/integrations/ramps/noah/index.test.js +18 -5
- package/lib/commonjs/integrations/trading/index.js +16 -5
- package/lib/commonjs/integrations/trading/lifi/index.js +297 -25
- package/lib/commonjs/integrations/trading/lifi/lifi.tradeAsset.test.js +360 -0
- package/lib/commonjs/integrations/trading/lifi/lifiStatusPoll.js +118 -0
- package/lib/commonjs/integrations/trading/lifi/lifiStatusPoll.test.js +66 -0
- package/lib/commonjs/integrations/trading/zero-x/index.js +129 -26
- package/lib/commonjs/integrations/trading/zero-x/index.test.js +163 -1
- package/lib/commonjs/integrations/yield/index.js +18 -4
- package/lib/commonjs/integrations/yield/yieldxyz.getValidators.test.js +71 -0
- package/lib/commonjs/integrations/yield/yieldxyz.highLevel.test.js +330 -0
- package/lib/commonjs/integrations/yield/yieldxyz.js +517 -1
- package/lib/commonjs/internal/pollLoop.js +64 -0
- package/lib/commonjs/internal/pollLoop.test.js +100 -0
- package/lib/commonjs/internal/stripStalePlanningNonce.js +65 -0
- package/lib/commonjs/internal/stripStalePlanningNonce.test.js +35 -0
- package/lib/commonjs/internal/waitForEvmOrUserOpConfirmation.js +155 -0
- package/lib/commonjs/internal/waitForEvmOrUserOpConfirmation.test.js +33 -0
- package/lib/commonjs/internal/waitForEvmTxConfirmation.js +104 -0
- package/lib/commonjs/internal/waitForSolanaTxConfirmation.js +106 -0
- package/lib/commonjs/internal/yieldEvmNetwork.js +60 -0
- package/lib/commonjs/mpc/index.js +116 -1
- package/lib/commonjs/provider/index.js +17 -0
- package/lib/commonjs/shared/trace/index.js +0 -1
- package/lib/esm/index.js +127 -9
- package/lib/esm/index.test.js +13 -0
- package/lib/esm/integrations/delegations/index.js +109 -2
- package/lib/esm/integrations/delegations/index.test.js +171 -0
- package/lib/esm/integrations/ramps/noah/index.test.js +18 -5
- package/lib/esm/integrations/trading/index.js +16 -5
- package/lib/esm/integrations/trading/lifi/index.js +292 -25
- package/lib/esm/integrations/trading/lifi/lifi.tradeAsset.test.js +332 -0
- package/lib/esm/integrations/trading/lifi/lifiStatusPoll.js +113 -0
- package/lib/esm/integrations/trading/lifi/lifiStatusPoll.test.js +64 -0
- package/lib/esm/integrations/trading/zero-x/index.js +129 -26
- package/lib/esm/integrations/trading/zero-x/index.test.js +141 -2
- package/lib/esm/integrations/yield/index.js +18 -4
- package/lib/esm/integrations/yield/yieldxyz.getValidators.test.js +66 -0
- package/lib/esm/integrations/yield/yieldxyz.highLevel.test.js +325 -0
- package/lib/esm/integrations/yield/yieldxyz.js +517 -1
- package/lib/esm/internal/pollLoop.js +59 -0
- package/lib/esm/internal/pollLoop.test.js +98 -0
- package/lib/esm/internal/stripStalePlanningNonce.js +61 -0
- package/lib/esm/internal/stripStalePlanningNonce.test.js +33 -0
- package/lib/esm/internal/waitForEvmOrUserOpConfirmation.js +151 -0
- package/lib/esm/internal/waitForEvmOrUserOpConfirmation.test.js +31 -0
- package/lib/esm/internal/waitForEvmTxConfirmation.js +100 -0
- package/lib/esm/internal/waitForSolanaTxConfirmation.js +102 -0
- package/lib/esm/internal/yieldEvmNetwork.js +55 -0
- package/lib/esm/mpc/index.js +116 -1
- package/lib/esm/provider/index.js +17 -0
- package/lib/esm/shared/trace/index.js +0 -1
- package/noah-types.d.ts +16 -2
- package/package.json +3 -2
- package/src/index.test.ts +15 -0
- package/src/index.ts +203 -14
- package/src/integrations/delegations/index.test.ts +251 -0
- package/src/integrations/delegations/index.ts +202 -4
- package/src/integrations/ramps/noah/index.test.ts +18 -5
- package/src/integrations/trading/index.ts +10 -7
- package/src/integrations/trading/lifi/index.ts +388 -28
- package/src/integrations/trading/lifi/lifi.tradeAsset.test.ts +436 -0
- package/src/integrations/trading/lifi/lifiStatusPoll.test.ts +74 -0
- package/src/integrations/trading/lifi/lifiStatusPoll.ts +158 -0
- package/src/integrations/trading/zero-x/index.test.ts +297 -1
- package/src/integrations/trading/zero-x/index.ts +181 -27
- package/src/integrations/yield/index.ts +24 -4
- package/src/integrations/yield/yieldxyz.getValidators.test.ts +70 -0
- package/src/integrations/yield/yieldxyz.highLevel.test.ts +403 -0
- package/src/integrations/yield/yieldxyz.ts +740 -8
- package/src/internal/pollLoop.test.ts +109 -0
- package/src/internal/pollLoop.ts +87 -0
- package/src/internal/stripStalePlanningNonce.test.ts +38 -0
- package/src/internal/stripStalePlanningNonce.ts +66 -0
- package/src/internal/waitForEvmOrUserOpConfirmation.test.ts +31 -0
- package/src/internal/waitForEvmOrUserOpConfirmation.ts +194 -0
- package/src/internal/waitForEvmTxConfirmation.ts +155 -0
- package/src/internal/waitForSolanaTxConfirmation.ts +135 -0
- package/src/internal/yieldEvmNetwork.ts +57 -0
- package/src/mpc/index.ts +142 -1
- package/src/provider/index.ts +25 -0
- package/src/shared/trace/index.ts +0 -1
- package/src/shared/types/README.md +6 -0
- package/src/shared/types/api.ts +12 -1
- package/src/shared/types/common.ts +332 -20
- package/src/shared/types/delegations.ts +10 -0
- package/src/shared/types/index.ts +1 -0
- package/src/shared/types/lifi.ts +82 -0
- package/src/shared/types/noah.ts +124 -33
- package/src/shared/types/yieldxyz.ts +186 -0
- package/src/shared/types/zero-x.ts +66 -0
- package/types.d.ts +6 -0
|
@@ -1,27 +1,93 @@
|
|
|
1
1
|
import Mpc from '../../mpc'
|
|
2
|
+
import { sdkLogger } from '../../logger'
|
|
3
|
+
import { waitForEvmTxConfirmation } from '../../internal/waitForEvmTxConfirmation'
|
|
4
|
+
import { stripStalePlanningNonceIfJsonObject } from '../../internal/stripStalePlanningNonce'
|
|
2
5
|
import {
|
|
6
|
+
isYieldEvmNetwork,
|
|
7
|
+
resolveYieldNetworkToCaip2,
|
|
8
|
+
} from '../../internal/yieldEvmNetwork'
|
|
9
|
+
import type {
|
|
10
|
+
YieldDepositParams,
|
|
11
|
+
YieldDepositResult,
|
|
12
|
+
YieldSubmitOptions,
|
|
13
|
+
YieldSubmitProgress,
|
|
14
|
+
YieldWithdrawParams,
|
|
15
|
+
YieldWithdrawResult,
|
|
16
|
+
YieldXyzActionTransaction,
|
|
17
|
+
YieldXyzEnterRequest,
|
|
18
|
+
YieldXyzEnterYieldResponse,
|
|
19
|
+
YieldXyzExitRequest,
|
|
20
|
+
YieldXyzExitResponse,
|
|
3
21
|
YieldXyzGetBalancesRequest,
|
|
4
22
|
YieldXyzGetBalancesResponse,
|
|
5
23
|
YieldXyzGetHistoricalActionsRequest,
|
|
6
24
|
YieldXyzGetHistoricalActionsResponse,
|
|
25
|
+
YieldXyzGetTransactionResponse,
|
|
26
|
+
YieldXyzGetYieldDefaultsRequest,
|
|
27
|
+
YieldXyzGetYieldDefaultsResponse,
|
|
28
|
+
YieldXyzGetYieldsRequest,
|
|
29
|
+
YieldXyzGetYieldsResponse,
|
|
7
30
|
YieldXyzManageYieldRequest,
|
|
8
31
|
YieldXyzManageYieldResponse,
|
|
9
32
|
YieldXyzTrackTransactionRequest,
|
|
10
33
|
YieldXyzTrackTransactionResponse,
|
|
11
|
-
|
|
12
|
-
YieldXyzGetYieldsRequest,
|
|
13
|
-
YieldXyzGetYieldsResponse,
|
|
14
|
-
YieldXyzEnterRequest,
|
|
15
|
-
YieldXyzEnterYieldResponse,
|
|
16
|
-
YieldXyzExitRequest,
|
|
17
|
-
YieldXyzExitResponse,
|
|
34
|
+
YieldXyzValidator,
|
|
18
35
|
} from '../../shared/types'
|
|
19
36
|
|
|
37
|
+
const LOG_PREFIX = '[YieldXyz]'
|
|
38
|
+
|
|
39
|
+
/** CAIP-2 chain ID format: namespace:reference (e.g. eip155:1, solana:5eykt...) */
|
|
40
|
+
const CAIP2_CHAIN_ID_REGEX = /^[a-z0-9-]+:[a-zA-Z0-9]+$/
|
|
41
|
+
|
|
20
42
|
export default class YieldXyz {
|
|
21
43
|
private mpc: Mpc
|
|
44
|
+
private signAndSendTransactionFn?: (
|
|
45
|
+
transaction: unknown,
|
|
46
|
+
network: string,
|
|
47
|
+
) => Promise<string>
|
|
48
|
+
private waitForConfirmationFn?: (
|
|
49
|
+
txHash: string,
|
|
50
|
+
network: string,
|
|
51
|
+
) => Promise<void | boolean>
|
|
52
|
+
private evmRequestFn?: (
|
|
53
|
+
method: string,
|
|
54
|
+
params: unknown[],
|
|
55
|
+
network: string,
|
|
56
|
+
) => Promise<unknown>
|
|
57
|
+
private evmPollerDefaults?: { pollIntervalMs?: number; timeoutMs?: number }
|
|
22
58
|
|
|
23
|
-
constructor({
|
|
59
|
+
constructor({
|
|
60
|
+
mpc,
|
|
61
|
+
waitForConfirmation,
|
|
62
|
+
evmRequestFn,
|
|
63
|
+
evmPollerOptions,
|
|
64
|
+
}: {
|
|
65
|
+
mpc: Mpc
|
|
66
|
+
waitForConfirmation?: (txHash: string, network: string) => Promise<void | boolean>
|
|
67
|
+
evmRequestFn?: (
|
|
68
|
+
method: string,
|
|
69
|
+
params: unknown[],
|
|
70
|
+
network: string,
|
|
71
|
+
) => Promise<unknown>
|
|
72
|
+
evmPollerOptions?: { pollIntervalMs?: number; timeoutMs?: number }
|
|
73
|
+
}) {
|
|
24
74
|
this.mpc = mpc
|
|
75
|
+
this.waitForConfirmationFn = waitForConfirmation
|
|
76
|
+
this.evmRequestFn = evmRequestFn
|
|
77
|
+
this.evmPollerDefaults = evmPollerOptions
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Configures the signer used by deposit() and withdraw() when no per-call
|
|
82
|
+
* `signAndSendTransaction` option is provided. Typically called once after construction
|
|
83
|
+
* (e.g. Portal wires in its MPC signer via this method instead of the constructor).
|
|
84
|
+
*
|
|
85
|
+
* Per-call `signAndSendTransaction` in {@link YieldSubmitOptions} always takes precedence.
|
|
86
|
+
*/
|
|
87
|
+
public setSignAndSendTransaction(
|
|
88
|
+
fn: (transaction: unknown, network: string) => Promise<string>,
|
|
89
|
+
): void {
|
|
90
|
+
this.signAndSendTransactionFn = fn
|
|
25
91
|
}
|
|
26
92
|
|
|
27
93
|
/**
|
|
@@ -119,4 +185,670 @@ export default class YieldXyz {
|
|
|
119
185
|
): Promise<YieldXyzGetTransactionResponse> {
|
|
120
186
|
return this.mpc?.getYieldXyzTransaction(transactionId)
|
|
121
187
|
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Resolve suggested defaults (amount, validators) from Portal yield defaults map.
|
|
191
|
+
* Calls the dedicated Yield.xyz defaults endpoint used in the deposit/withdraw flow.
|
|
192
|
+
*/
|
|
193
|
+
public async getYieldDefaults(
|
|
194
|
+
req?: YieldXyzGetYieldDefaultsRequest,
|
|
195
|
+
): Promise<YieldXyzGetYieldDefaultsResponse> {
|
|
196
|
+
return this.mpc?.getYieldXyzDefaults(req)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
public async getValidators(yieldId: string): Promise<YieldXyzValidator[]> {
|
|
200
|
+
const res = await this.mpc.getYieldXyzValidators(yieldId)
|
|
201
|
+
|
|
202
|
+
sdkLogger.debug(`${LOG_PREFIX} getValidators raw response`, res)
|
|
203
|
+
|
|
204
|
+
if (res.error) {
|
|
205
|
+
sdkLogger.error(`${LOG_PREFIX} getValidators error`, res.error)
|
|
206
|
+
throw new Error(`[YieldXyz] getValidators failed: ${res.error}`)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const d = res.data
|
|
210
|
+
const validators =
|
|
211
|
+
d?.validators ?? d?.rawResponse?.validators ?? d?.rawResponse?.items
|
|
212
|
+
|
|
213
|
+
sdkLogger.debug(`${LOG_PREFIX} getValidators processed`, { validators })
|
|
214
|
+
|
|
215
|
+
if (!validators || !Array.isArray(validators)) {
|
|
216
|
+
sdkLogger.error(
|
|
217
|
+
`${LOG_PREFIX} No validators found for yieldId="${yieldId}"`,
|
|
218
|
+
{ data: d },
|
|
219
|
+
)
|
|
220
|
+
throw new Error(
|
|
221
|
+
`[YieldXyz] No validators in response for yieldId="${yieldId}"`,
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return validators
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* @internal Validates and trims `chain` to a full CAIP-2 id (no aliases or numeric shortcuts).
|
|
230
|
+
*/
|
|
231
|
+
private requireFullCaip2Chain(chain: string): string {
|
|
232
|
+
const c = chain.trim()
|
|
233
|
+
if (!c) {
|
|
234
|
+
throw new Error('[YieldXyz] chain is required.')
|
|
235
|
+
}
|
|
236
|
+
if (!CAIP2_CHAIN_ID_REGEX.test(c)) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`[YieldXyz] chain must be a full CAIP-2 id (e.g. eip155:11155111). Received: "${chain}"`,
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
return c
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* @internal When `params` includes a non-empty `yieldId`, returns it. Otherwise requires
|
|
246
|
+
* `chain` + `token`, validates CAIP-2, and resolves `yieldId` from Portal yield defaults.
|
|
247
|
+
*/
|
|
248
|
+
private async resolveYieldIdForHighLevelAction(
|
|
249
|
+
params: YieldDepositParams,
|
|
250
|
+
): Promise<{ yieldId: string; chain?: string; token?: string }> {
|
|
251
|
+
const fromYieldId =
|
|
252
|
+
'yieldId' in params && params.yieldId != null
|
|
253
|
+
? String(params.yieldId).trim()
|
|
254
|
+
: ''
|
|
255
|
+
if (fromYieldId !== '') {
|
|
256
|
+
return { yieldId: fromYieldId }
|
|
257
|
+
}
|
|
258
|
+
if (!('chain' in params) || !('token' in params)) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
'[YieldXyz] Provide either yieldId, or both chain (full CAIP-2) and token.',
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
const chainCaip2 = this.requireFullCaip2Chain(params.chain)
|
|
264
|
+
const token = params.token.trim()
|
|
265
|
+
|
|
266
|
+
const yieldId = await this.resolveYieldIdFromPortalDefaults(
|
|
267
|
+
chainCaip2,
|
|
268
|
+
token,
|
|
269
|
+
)
|
|
270
|
+
return { yieldId, chain: chainCaip2, token }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* @internal Loads Portal yield defaults and reads `yieldId` for the exact map key `{caip2}:{token}`.
|
|
275
|
+
*/
|
|
276
|
+
private async resolveYieldIdFromPortalDefaults(
|
|
277
|
+
chainCaip2: string,
|
|
278
|
+
tokenSymbol: string,
|
|
279
|
+
): Promise<string> {
|
|
280
|
+
const token = tokenSymbol.trim()
|
|
281
|
+
if (!token) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
'[YieldXyz] token is required; use the exact symbol suffix from the yield defaults map.',
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
const key = `${chainCaip2}:${token}`
|
|
287
|
+
const res = await this.getYieldDefaults({
|
|
288
|
+
includeOpportunities: false,
|
|
289
|
+
})
|
|
290
|
+
if (res.error) {
|
|
291
|
+
throw new Error(`[YieldXyz] Failed to get yield defaults: ${res.error}`)
|
|
292
|
+
}
|
|
293
|
+
if (!res.data) {
|
|
294
|
+
throw new Error('[YieldXyz] No data returned from yield defaults endpoint')
|
|
295
|
+
}
|
|
296
|
+
const entry = res.data[key]
|
|
297
|
+
const yieldId = entry?.yieldId
|
|
298
|
+
if (yieldId == null || yieldId === '') {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`[YieldXyz] No default yield for key "${key}". Use chain and token exactly as in the Portal yield defaults response.`,
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
return yieldId
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* High-level deposit: build enter yield action, sign and submit each transaction, track hashes.
|
|
308
|
+
*
|
|
309
|
+
* The signer is resolved in priority order:
|
|
310
|
+
* 1. `options.signAndSendTransaction` — per-call override.
|
|
311
|
+
* 2. Instance setter — configured via {@link YieldXyz.setSignAndSendTransaction} (e.g. Portal).
|
|
312
|
+
* 3. Error — thrown if neither is available.
|
|
313
|
+
*
|
|
314
|
+
* Supply either **`yieldId`** + `amount` (optional `address` / `arguments`), or full **CAIP-2**
|
|
315
|
+
* **`chain`** + **`token`** + `amount` so the SDK can resolve `yieldId` from Portal yield defaults.
|
|
316
|
+
* Optional {@link YieldSubmitOptions.onProgress} fires the same steps regardless of input mode.
|
|
317
|
+
*/
|
|
318
|
+
public async deposit(
|
|
319
|
+
params: YieldDepositParams,
|
|
320
|
+
options?: YieldSubmitOptions,
|
|
321
|
+
): Promise<YieldDepositResult> {
|
|
322
|
+
const signAndSend =
|
|
323
|
+
options?.signAndSendTransaction ?? this.signAndSendTransactionFn
|
|
324
|
+
if (!signAndSend) {
|
|
325
|
+
throw new Error(
|
|
326
|
+
'[YieldXyz] No signer configured. Call setSignAndSendTransaction() on the instance or pass signAndSendTransaction in options.',
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const { yieldId, chain: resolvedChain, token: resolvedToken } =
|
|
331
|
+
await this.resolveYieldIdForHighLevelAction(params)
|
|
332
|
+
|
|
333
|
+
sdkLogger.info(`${LOG_PREFIX} deposit: entry`, {
|
|
334
|
+
amount: params.amount,
|
|
335
|
+
yieldId,
|
|
336
|
+
token: resolvedToken,
|
|
337
|
+
chain: resolvedChain,
|
|
338
|
+
usedExplicitYieldId: resolvedChain === undefined,
|
|
339
|
+
hasAddress: Boolean(params.address),
|
|
340
|
+
hasArguments: Boolean(params.arguments),
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
sdkLogger.debug(`${LOG_PREFIX} deposit: resolved yieldId`, { yieldId })
|
|
344
|
+
|
|
345
|
+
const request: YieldXyzEnterRequest = {
|
|
346
|
+
yieldId,
|
|
347
|
+
address: params.address,
|
|
348
|
+
arguments: {
|
|
349
|
+
...params.arguments,
|
|
350
|
+
amount: params.amount,
|
|
351
|
+
},
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const response = await this.enter(request)
|
|
356
|
+
const effectiveWaitForConfirmation =
|
|
357
|
+
this.resolveEffectiveWaitForConfirmation(options)
|
|
358
|
+
const base = await this.executeAndTrack(
|
|
359
|
+
response,
|
|
360
|
+
signAndSend,
|
|
361
|
+
options?.onProgress,
|
|
362
|
+
effectiveWaitForConfirmation,
|
|
363
|
+
'deposit',
|
|
364
|
+
)
|
|
365
|
+
const result: YieldDepositResult = {
|
|
366
|
+
...base,
|
|
367
|
+
...(resolvedChain !== undefined && resolvedToken !== undefined
|
|
368
|
+
? { chain: resolvedChain, token: resolvedToken }
|
|
369
|
+
: {}),
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
sdkLogger.info(`${LOG_PREFIX} deposit: success`, {
|
|
373
|
+
hashes: result.hashes,
|
|
374
|
+
yieldId: result.yieldId,
|
|
375
|
+
yieldOpportunityDetails: result.yieldOpportunityDetails,
|
|
376
|
+
})
|
|
377
|
+
return result
|
|
378
|
+
} catch (error) {
|
|
379
|
+
sdkLogger.debug(`${LOG_PREFIX} deposit: failed, re-throwing`, error)
|
|
380
|
+
throw error
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* High-level withdraw: same dual input modes, `yieldId` resolution, and signer fallback
|
|
386
|
+
* order as {@link YieldXyz.deposit}.
|
|
387
|
+
*/
|
|
388
|
+
public async withdraw(
|
|
389
|
+
params: YieldWithdrawParams,
|
|
390
|
+
options?: YieldSubmitOptions,
|
|
391
|
+
): Promise<YieldWithdrawResult> {
|
|
392
|
+
const signAndSend =
|
|
393
|
+
options?.signAndSendTransaction ?? this.signAndSendTransactionFn
|
|
394
|
+
if (!signAndSend) {
|
|
395
|
+
throw new Error(
|
|
396
|
+
'[YieldXyz] No signer configured. Call setSignAndSendTransaction() on the instance or pass signAndSendTransaction in options.',
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const { yieldId, chain: resolvedChain, token: resolvedToken } =
|
|
401
|
+
await this.resolveYieldIdForHighLevelAction(params)
|
|
402
|
+
|
|
403
|
+
sdkLogger.debug(`${LOG_PREFIX} withdraw: entry`, {
|
|
404
|
+
amount: params.amount,
|
|
405
|
+
yieldId,
|
|
406
|
+
token: resolvedToken,
|
|
407
|
+
chain: resolvedChain,
|
|
408
|
+
usedExplicitYieldId: resolvedChain === undefined,
|
|
409
|
+
hasAddress: Boolean(params.address),
|
|
410
|
+
hasArguments: Boolean(params.arguments),
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
sdkLogger.debug(`${LOG_PREFIX} withdraw: resolved yieldId`, { yieldId })
|
|
414
|
+
|
|
415
|
+
const request: YieldXyzExitRequest = {
|
|
416
|
+
yieldId,
|
|
417
|
+
address: params.address,
|
|
418
|
+
arguments: {
|
|
419
|
+
...params.arguments,
|
|
420
|
+
amount: params.amount,
|
|
421
|
+
},
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
const response = await this.exit(request)
|
|
426
|
+
const effectiveWaitForConfirmation =
|
|
427
|
+
this.resolveEffectiveWaitForConfirmation(options)
|
|
428
|
+
const base = await this.executeAndTrack(
|
|
429
|
+
response,
|
|
430
|
+
signAndSend,
|
|
431
|
+
options?.onProgress,
|
|
432
|
+
effectiveWaitForConfirmation,
|
|
433
|
+
'withdraw',
|
|
434
|
+
)
|
|
435
|
+
const result: YieldWithdrawResult = {
|
|
436
|
+
...base,
|
|
437
|
+
...(resolvedChain !== undefined && resolvedToken !== undefined
|
|
438
|
+
? { chain: resolvedChain, token: resolvedToken }
|
|
439
|
+
: {}),
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
sdkLogger.info(`${LOG_PREFIX} withdraw: success`, {
|
|
443
|
+
hashes: result.hashes,
|
|
444
|
+
yieldId: result.yieldId,
|
|
445
|
+
yieldOpportunityDetails: result.yieldOpportunityDetails,
|
|
446
|
+
})
|
|
447
|
+
return result
|
|
448
|
+
} catch (error) {
|
|
449
|
+
sdkLogger.debug(`${LOG_PREFIX} withdraw: failed, re-throwing`, error)
|
|
450
|
+
throw error
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private extractTransactions(
|
|
455
|
+
response: YieldXyzEnterYieldResponse | YieldXyzExitResponse,
|
|
456
|
+
): YieldXyzActionTransaction[] {
|
|
457
|
+
const transactions = response.data?.rawResponse?.transactions
|
|
458
|
+
if (
|
|
459
|
+
transactions &&
|
|
460
|
+
Array.isArray(transactions) &&
|
|
461
|
+
transactions.length > 0
|
|
462
|
+
) {
|
|
463
|
+
return transactions
|
|
464
|
+
}
|
|
465
|
+
return []
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private buildYieldOpportunityDetails(
|
|
469
|
+
response: YieldXyzEnterYieldResponse | YieldXyzExitResponse,
|
|
470
|
+
): YieldDepositResult['yieldOpportunityDetails'] {
|
|
471
|
+
const raw = response.data?.rawResponse
|
|
472
|
+
if (!raw) {
|
|
473
|
+
return { yieldId: '' }
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
yieldId: raw.yieldId,
|
|
477
|
+
intent: raw.intent,
|
|
478
|
+
type: raw.type,
|
|
479
|
+
executionPattern: raw.executionPattern,
|
|
480
|
+
status: raw.status,
|
|
481
|
+
amount: raw.amount,
|
|
482
|
+
amountUsd: raw.amountUsd,
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private parseUnsignedToObject(
|
|
487
|
+
unsigned: string | Record<string, unknown> | null,
|
|
488
|
+
): Record<string, unknown> {
|
|
489
|
+
if (unsigned == null) return {}
|
|
490
|
+
if (typeof unsigned === 'object') return { ...unsigned }
|
|
491
|
+
try {
|
|
492
|
+
const parsed = JSON.parse(unsigned)
|
|
493
|
+
// Validate parsed result is a plain object (not null, not array)
|
|
494
|
+
if (
|
|
495
|
+
parsed != null &&
|
|
496
|
+
typeof parsed === 'object' &&
|
|
497
|
+
!Array.isArray(parsed)
|
|
498
|
+
) {
|
|
499
|
+
return parsed as Record<string, unknown>
|
|
500
|
+
}
|
|
501
|
+
sdkLogger.warn(
|
|
502
|
+
`${LOG_PREFIX} JSON.parse returned non-object type: ${typeof parsed}`,
|
|
503
|
+
)
|
|
504
|
+
return {}
|
|
505
|
+
} catch {
|
|
506
|
+
return {}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private getTransactionPayloadSummary(
|
|
511
|
+
payload: unknown,
|
|
512
|
+
): Record<string, unknown> {
|
|
513
|
+
const obj = this.parseUnsignedToObject(
|
|
514
|
+
typeof payload === 'string'
|
|
515
|
+
? payload
|
|
516
|
+
: (payload as Record<string, unknown>),
|
|
517
|
+
)
|
|
518
|
+
const data = obj.data ?? obj.input
|
|
519
|
+
const dataStr = typeof data === 'string' ? data : JSON.stringify(data)
|
|
520
|
+
return {
|
|
521
|
+
from: obj.from,
|
|
522
|
+
to: obj.to,
|
|
523
|
+
dataLength: dataStr?.length ?? 0,
|
|
524
|
+
value: obj.value,
|
|
525
|
+
gasLimit: obj.gas ?? obj.gasLimit,
|
|
526
|
+
nonce: obj.nonce,
|
|
527
|
+
maxFeePerGas: obj.maxFeePerGas,
|
|
528
|
+
maxPriorityFeePerGas: obj.maxPriorityFeePerGas,
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private logError(context: string, error: unknown): void {
|
|
533
|
+
const err = error as Error & {
|
|
534
|
+
response?: unknown
|
|
535
|
+
status?: number
|
|
536
|
+
data?: unknown
|
|
537
|
+
}
|
|
538
|
+
sdkLogger.error(`${LOG_PREFIX} ${context}:`, error)
|
|
539
|
+
if (err.response !== undefined) {
|
|
540
|
+
sdkLogger.debug(`${LOG_PREFIX} ${context} HTTP response:`, err.response)
|
|
541
|
+
}
|
|
542
|
+
if (err.status !== undefined) {
|
|
543
|
+
sdkLogger.debug(`${LOG_PREFIX} ${context} HTTP status:`, err.status)
|
|
544
|
+
}
|
|
545
|
+
if (err.data !== undefined) {
|
|
546
|
+
sdkLogger.debug(`${LOG_PREFIX} ${context} HTTP response data:`, err.data)
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private resolveEffectiveWaitForConfirmation(
|
|
551
|
+
options?: YieldSubmitOptions,
|
|
552
|
+
):
|
|
553
|
+
| ((txHash: string, network: string) => Promise<void | boolean>)
|
|
554
|
+
| undefined {
|
|
555
|
+
if (options?.waitForConfirmation) {
|
|
556
|
+
return options.waitForConfirmation
|
|
557
|
+
}
|
|
558
|
+
if (this.waitForConfirmationFn) {
|
|
559
|
+
return this.waitForConfirmationFn
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const evmFn = options?.evmRequestFn ?? this.evmRequestFn
|
|
563
|
+
if (!evmFn) {
|
|
564
|
+
return undefined
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const pollerOpts = {
|
|
568
|
+
...this.evmPollerDefaults,
|
|
569
|
+
...options?.evmPollerOptions,
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return async (txHash: string, network: string) => {
|
|
573
|
+
if (!isYieldEvmNetwork(network)) {
|
|
574
|
+
sdkLogger.warn(
|
|
575
|
+
`${LOG_PREFIX} Cannot verify confirmation for non-EVM network ${network}. ` +
|
|
576
|
+
'Internal EVM poller fallback does not support non-EVM networks.',
|
|
577
|
+
)
|
|
578
|
+
return false
|
|
579
|
+
}
|
|
580
|
+
const caip2 = network.startsWith('eip155:')
|
|
581
|
+
? network
|
|
582
|
+
: resolveYieldNetworkToCaip2(network) ?? network
|
|
583
|
+
return waitForEvmTxConfirmation(txHash, caip2, evmFn, {
|
|
584
|
+
pollIntervalMs: pollerOpts.pollIntervalMs,
|
|
585
|
+
timeoutMs: pollerOpts.timeoutMs,
|
|
586
|
+
onTimeout: 'resolve_false',
|
|
587
|
+
})
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
private async executeAndTrack(
|
|
592
|
+
response: YieldXyzEnterYieldResponse | YieldXyzExitResponse,
|
|
593
|
+
signAndSend: (transaction: unknown, network: string) => Promise<string>,
|
|
594
|
+
onProgress?: (event: YieldSubmitProgress) => void,
|
|
595
|
+
waitForConfirmation?: (
|
|
596
|
+
txHash: string,
|
|
597
|
+
network: string,
|
|
598
|
+
) => Promise<void | boolean>,
|
|
599
|
+
method: 'deposit' | 'withdraw' = 'deposit',
|
|
600
|
+
): Promise<
|
|
601
|
+
Pick<
|
|
602
|
+
YieldDepositResult,
|
|
603
|
+
'hashes' | 'yieldId' | 'yieldOpportunityDetails'
|
|
604
|
+
>
|
|
605
|
+
> {
|
|
606
|
+
const transactions = this.extractTransactions(response)
|
|
607
|
+
|
|
608
|
+
sdkLogger.debug(`${LOG_PREFIX} executeAndTrack: transaction count`, {
|
|
609
|
+
method,
|
|
610
|
+
transactionCount: transactions.length,
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
if (transactions.length === 0) {
|
|
614
|
+
throw new Error('No transactions in yield action response.')
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const total = transactions.length
|
|
618
|
+
const hashes: string[] = []
|
|
619
|
+
let confirmationsReached = 0
|
|
620
|
+
|
|
621
|
+
for (let index = 0; index < transactions.length; index++) {
|
|
622
|
+
const tx = transactions[index]
|
|
623
|
+
|
|
624
|
+
// Validate transaction exists (defensive check for sparse arrays)
|
|
625
|
+
if (!tx) {
|
|
626
|
+
throw new Error(
|
|
627
|
+
`Transaction at index ${index} is undefined or null. This indicates a malformed API response.`,
|
|
628
|
+
)
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
try {
|
|
632
|
+
if (!tx.id) {
|
|
633
|
+
throw new Error(
|
|
634
|
+
`Transaction at index ${index} is missing required field "id".`,
|
|
635
|
+
)
|
|
636
|
+
}
|
|
637
|
+
if (tx.network == null || tx.network === '') {
|
|
638
|
+
throw new Error(
|
|
639
|
+
`Transaction ${tx.id} is missing required field "network".`,
|
|
640
|
+
)
|
|
641
|
+
}
|
|
642
|
+
if (tx.unsignedTransaction == null) {
|
|
643
|
+
throw new Error(
|
|
644
|
+
`Transaction ${tx.id} has no unsignedTransaction to sign.`,
|
|
645
|
+
)
|
|
646
|
+
}
|
|
647
|
+
const rawUnsigned = tx.unsignedTransaction
|
|
648
|
+
let payloadToSend: unknown = rawUnsigned
|
|
649
|
+
const isEvm =
|
|
650
|
+
typeof tx.network === 'string' && isYieldEvmNetwork(tx.network)
|
|
651
|
+
const resolvedNetwork = isEvm
|
|
652
|
+
? (resolveYieldNetworkToCaip2(tx.network) ?? tx.network)
|
|
653
|
+
: tx.network
|
|
654
|
+
if (isEvm) {
|
|
655
|
+
const canStripNonce =
|
|
656
|
+
typeof rawUnsigned === 'string' ||
|
|
657
|
+
(typeof rawUnsigned === 'object' &&
|
|
658
|
+
rawUnsigned !== null &&
|
|
659
|
+
!Array.isArray(rawUnsigned))
|
|
660
|
+
if (canStripNonce) {
|
|
661
|
+
payloadToSend = stripStalePlanningNonceIfJsonObject(
|
|
662
|
+
rawUnsigned as string | Record<string, unknown>,
|
|
663
|
+
)
|
|
664
|
+
}
|
|
665
|
+
sdkLogger.debug(
|
|
666
|
+
`${LOG_PREFIX} executeAndTrack: nonce stripped for EVM tx`,
|
|
667
|
+
{
|
|
668
|
+
index,
|
|
669
|
+
transactionId: tx.id,
|
|
670
|
+
network: tx.network,
|
|
671
|
+
payloadKind: typeof payloadToSend,
|
|
672
|
+
},
|
|
673
|
+
)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const payloadSummary = this.getTransactionPayloadSummary(payloadToSend)
|
|
677
|
+
sdkLogger.debug(
|
|
678
|
+
`${LOG_PREFIX} executeAndTrack: transaction preparation`,
|
|
679
|
+
{
|
|
680
|
+
method,
|
|
681
|
+
index,
|
|
682
|
+
total,
|
|
683
|
+
transactionId: tx.id,
|
|
684
|
+
network: tx.network,
|
|
685
|
+
type: tx.type,
|
|
686
|
+
payloadSummary,
|
|
687
|
+
},
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
const progressSigning: YieldSubmitProgress = {
|
|
691
|
+
step: 'signing',
|
|
692
|
+
index,
|
|
693
|
+
total,
|
|
694
|
+
}
|
|
695
|
+
sdkLogger.debug(`${LOG_PREFIX} executeAndTrack: signing start`, {
|
|
696
|
+
method,
|
|
697
|
+
index,
|
|
698
|
+
total,
|
|
699
|
+
transactionId: tx.id,
|
|
700
|
+
})
|
|
701
|
+
onProgress?.(progressSigning)
|
|
702
|
+
|
|
703
|
+
const hash = await signAndSend(payloadToSend, resolvedNetwork)
|
|
704
|
+
if (typeof hash !== 'string' || hash.trim() === '') {
|
|
705
|
+
throw new Error(
|
|
706
|
+
`Transaction ${tx.id} signing returned an empty or invalid hash.`,
|
|
707
|
+
)
|
|
708
|
+
}
|
|
709
|
+
hashes.push(hash)
|
|
710
|
+
|
|
711
|
+
sdkLogger.debug(`${LOG_PREFIX} executeAndTrack: signing complete`, {
|
|
712
|
+
method,
|
|
713
|
+
index,
|
|
714
|
+
total,
|
|
715
|
+
transactionId: tx.id,
|
|
716
|
+
hash,
|
|
717
|
+
})
|
|
718
|
+
sdkLogger.info(`${LOG_PREFIX} ${method}: submitted tx`, {
|
|
719
|
+
index: index + 1,
|
|
720
|
+
total,
|
|
721
|
+
hash,
|
|
722
|
+
})
|
|
723
|
+
|
|
724
|
+
const progressSubmitted: YieldSubmitProgress = {
|
|
725
|
+
step: 'submitted',
|
|
726
|
+
index,
|
|
727
|
+
total,
|
|
728
|
+
hash,
|
|
729
|
+
}
|
|
730
|
+
sdkLogger.debug(`${LOG_PREFIX} executeAndTrack: onProgress`, {
|
|
731
|
+
step: progressSubmitted.step,
|
|
732
|
+
index: progressSubmitted.index,
|
|
733
|
+
total: progressSubmitted.total,
|
|
734
|
+
hash: progressSubmitted.hash,
|
|
735
|
+
})
|
|
736
|
+
onProgress?.(progressSubmitted)
|
|
737
|
+
|
|
738
|
+
if (waitForConfirmation) {
|
|
739
|
+
const progressConfirming: YieldSubmitProgress = {
|
|
740
|
+
step: 'confirming',
|
|
741
|
+
index,
|
|
742
|
+
total,
|
|
743
|
+
hash,
|
|
744
|
+
}
|
|
745
|
+
sdkLogger.info(
|
|
746
|
+
`${LOG_PREFIX} executeAndTrack: waiting for confirmation`,
|
|
747
|
+
{
|
|
748
|
+
method,
|
|
749
|
+
index,
|
|
750
|
+
total,
|
|
751
|
+
hash,
|
|
752
|
+
network: tx.network,
|
|
753
|
+
strategy: 'waitForConfirmation',
|
|
754
|
+
},
|
|
755
|
+
)
|
|
756
|
+
onProgress?.(progressConfirming)
|
|
757
|
+
|
|
758
|
+
const waiterResult = await waitForConfirmation(hash, resolvedNetwork)
|
|
759
|
+
const isConfirmed = waiterResult === true
|
|
760
|
+
|
|
761
|
+
if (isConfirmed) {
|
|
762
|
+
confirmationsReached += 1
|
|
763
|
+
sdkLogger.debug(
|
|
764
|
+
`${LOG_PREFIX} executeAndTrack: confirmation received`,
|
|
765
|
+
{
|
|
766
|
+
method,
|
|
767
|
+
hash,
|
|
768
|
+
confirmationsReached,
|
|
769
|
+
total,
|
|
770
|
+
},
|
|
771
|
+
)
|
|
772
|
+
onProgress?.({ step: 'confirmed', index, total, hash })
|
|
773
|
+
} else {
|
|
774
|
+
sdkLogger.debug(
|
|
775
|
+
`${LOG_PREFIX} executeAndTrack: confirmation not reached (waitForConfirmation did not return true)`,
|
|
776
|
+
{
|
|
777
|
+
method,
|
|
778
|
+
hash,
|
|
779
|
+
confirmationsReached,
|
|
780
|
+
total,
|
|
781
|
+
waiterResult,
|
|
782
|
+
},
|
|
783
|
+
)
|
|
784
|
+
// Uncertain confirmation (timeout / false / no response): safe stop.
|
|
785
|
+
// Track the hash with backend before stopping, then return partial result.
|
|
786
|
+
sdkLogger.debug(
|
|
787
|
+
`${LOG_PREFIX} executeAndTrack: track (submitTransactionHash) call before break`,
|
|
788
|
+
{
|
|
789
|
+
method,
|
|
790
|
+
transactionId: tx.id,
|
|
791
|
+
hash,
|
|
792
|
+
},
|
|
793
|
+
)
|
|
794
|
+
await this.track({
|
|
795
|
+
transactionId: tx.id,
|
|
796
|
+
hash,
|
|
797
|
+
})
|
|
798
|
+
break
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
sdkLogger.debug(
|
|
803
|
+
`${LOG_PREFIX} executeAndTrack: track (submitTransactionHash) call`,
|
|
804
|
+
{
|
|
805
|
+
method,
|
|
806
|
+
transactionId: tx.id,
|
|
807
|
+
hash,
|
|
808
|
+
},
|
|
809
|
+
)
|
|
810
|
+
const trackResponse = await this.track({
|
|
811
|
+
transactionId: tx.id,
|
|
812
|
+
hash,
|
|
813
|
+
})
|
|
814
|
+
sdkLogger.debug(`${LOG_PREFIX} executeAndTrack: track response`, {
|
|
815
|
+
method,
|
|
816
|
+
transactionId: tx.id,
|
|
817
|
+
hash,
|
|
818
|
+
trackStatus: trackResponse?.data?.rawResponse?.status,
|
|
819
|
+
})
|
|
820
|
+
} catch (error) {
|
|
821
|
+
this.logError(
|
|
822
|
+
`executeAndTrack ${method} failed at index ${index}`,
|
|
823
|
+
error,
|
|
824
|
+
)
|
|
825
|
+
sdkLogger.debug(`${LOG_PREFIX} executeAndTrack: error context`, {
|
|
826
|
+
method,
|
|
827
|
+
index,
|
|
828
|
+
total,
|
|
829
|
+
transactionId: tx.id,
|
|
830
|
+
network: tx.network,
|
|
831
|
+
hashesSoFar: hashes.length,
|
|
832
|
+
})
|
|
833
|
+
throw error
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const rawResponse = response.data?.rawResponse
|
|
838
|
+
const yieldId = rawResponse?.yieldId ?? ''
|
|
839
|
+
const yieldOpportunityDetails = this.buildYieldOpportunityDetails(response)
|
|
840
|
+
|
|
841
|
+
sdkLogger.info(`${LOG_PREFIX} ${method}: executeAndTrack complete`, {
|
|
842
|
+
yieldId,
|
|
843
|
+
hashCount: hashes.length,
|
|
844
|
+
confirmationsReached:
|
|
845
|
+
confirmationsReached > 0 ? confirmationsReached : undefined,
|
|
846
|
+
})
|
|
847
|
+
|
|
848
|
+
return {
|
|
849
|
+
hashes,
|
|
850
|
+
yieldId,
|
|
851
|
+
yieldOpportunityDetails,
|
|
852
|
+
}
|
|
853
|
+
}
|
|
122
854
|
}
|