@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.
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 +330 -0
  16. package/lib/commonjs/integrations/yield/yieldxyz.js +517 -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 +325 -0
  44. package/lib/esm/integrations/yield/yieldxyz.js +517 -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 +403 -0
  74. package/src/integrations/yield/yieldxyz.ts +740 -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 +186 -0
  95. package/src/shared/types/zero-x.ts +66 -0
  96. 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
- YieldXyzGetTransactionResponse,
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({ mpc }: { mpc: Mpc }) {
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
  }