@portal-hq/web 3.13.1 → 3.13.2-0

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