@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,61 +1,421 @@
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 {
3
- LifiQuoteRequest,
4
- LifiQuoteResponse,
6
+ pollLifiStatusUntilTerminal,
7
+ } from './lifiStatusPoll'
8
+ import type {
9
+ LifiPollStatusOptions,
5
10
  LifiRoutesRequest,
6
11
  LifiRoutesResponse,
12
+ LifiQuoteRequest,
13
+ LifiQuoteResponse,
7
14
  LifiStatusRequest,
8
15
  LifiStatusResponse,
16
+ LifiStatusRawResponse,
17
+ LifiStep,
9
18
  LifiStepTransactionRequest,
10
19
  LifiStepTransactionResponse,
20
+ LifiTradeAssetOptions,
21
+ LifiTradeAssetParams,
22
+ LifiTradeAssetResult,
11
23
  } from '../../../shared/types'
12
24
 
13
- export default class LiFi {
25
+ const LOG_PREFIX = '[LiFi]'
26
+
27
+ /**
28
+ * Internal sentinel error used to avoid emitting duplicate `failed` progress events.
29
+ */
30
+ class LifiReportedError extends Error {
31
+ constructor(message: string) {
32
+ super(message)
33
+ this.name = 'LifiReportedError'
34
+ }
35
+ }
36
+
37
+ export interface LifiOptions extends LifiTradeAssetOptions {}
38
+
39
+ export interface ILiFi {
40
+ getRoutes(data: LifiRoutesRequest): Promise<LifiRoutesResponse>
41
+ getQuote(data: LifiQuoteRequest): Promise<LifiQuoteResponse>
42
+ getStatus(data: LifiStatusRequest): Promise<LifiStatusResponse>
43
+ getRouteStep(
44
+ data: LifiStepTransactionRequest,
45
+ ): Promise<LifiStepTransactionResponse>
46
+ tradeAsset(
47
+ params: LifiTradeAssetParams,
48
+ options?: LifiTradeAssetOptions,
49
+ ): Promise<LifiTradeAssetResult>
50
+ pollStatus(
51
+ request: Pick<LifiStatusRequest, 'txHash' | 'fromChain' | 'toChain' | 'bridge'>,
52
+ options?: LifiPollStatusOptions & {
53
+ onUpdate?: (raw: LifiStatusRawResponse) => boolean | void
54
+ },
55
+ ): Promise<LifiStatusRawResponse>
56
+ }
57
+
58
+ /** Normalize LiFi chain key / id to CAIP-2 `eip155:*` for the Portal provider. */
59
+ export function normalizeChainToNetwork(chain: string): string {
60
+ const c = chain.trim()
61
+ if (c.startsWith('eip155:')) {
62
+ return c
63
+ }
64
+ if (/^0x[0-9a-fA-F]+$/.test(c)) {
65
+ return `eip155:${parseInt(c, 16)}`
66
+ }
67
+ if (/^\d+$/.test(c)) {
68
+ return `eip155:${c}`
69
+ }
70
+ return c
71
+ }
72
+
73
+ export function resolveLifiStepNetworkCaip2(step: LifiStep): string {
74
+ const fromTx = step.transactionRequest?.chainId
75
+ if (fromTx) {
76
+ return normalizeChainToNetwork(fromTx)
77
+ }
78
+ return normalizeChainToNetwork(step.action.fromChainId)
79
+ }
80
+
81
+ export function parseLifiTransactionRequestFromStep(
82
+ step: LifiStep,
83
+ ): Record<string, unknown> {
84
+ const tr = step.transactionRequest
85
+ if (!tr) {
86
+ throw new Error(
87
+ `${LOG_PREFIX} Step has no transactionRequest; call getRouteStep first.`,
88
+ )
89
+ }
90
+ const base: Record<string, unknown> = {
91
+ data: tr.data,
92
+ to: tr.to,
93
+ from: tr.from,
94
+ value: tr.value ?? '0x0',
95
+ gas: tr.gasLimit,
96
+ gasPrice: tr.gasPrice,
97
+ }
98
+ if (tr.maxFeePerGas != null) {
99
+ base.maxFeePerGas = tr.maxFeePerGas
100
+ }
101
+ if (tr.maxPriorityFeePerGas != null) {
102
+ base.maxPriorityFeePerGas = tr.maxPriorityFeePerGas
103
+ }
104
+ return base
105
+ }
106
+
107
+ export function statusBridgeFromStepTool(
108
+ tool: string | undefined,
109
+ ): LifiStatusRequest['bridge'] {
110
+ if (!tool) {
111
+ return undefined
112
+ }
113
+ return tool.toLowerCase() as LifiStatusRequest['bridge']
114
+ }
115
+
116
+ function mergeLifiOptions(
117
+ instance: LifiTradeAssetOptions | undefined,
118
+ perCall: LifiTradeAssetOptions | undefined,
119
+ ): LifiTradeAssetOptions {
120
+ return {
121
+ ...instance,
122
+ ...perCall,
123
+ }
124
+ }
125
+
126
+ export default class LiFi implements ILiFi {
14
127
  private mpc: Mpc
128
+ private readonly defaultTradeOptions: LifiTradeAssetOptions
15
129
 
16
- constructor({ mpc }: { mpc: Mpc }) {
130
+ constructor({ mpc, ...tradeDefaults }: { mpc: Mpc } & LifiOptions) {
17
131
  this.mpc = mpc
132
+ this.defaultTradeOptions = tradeDefaults
18
133
  }
19
134
 
20
- /**
21
- * Retrieves routes from the Li.Fi integration.
22
- * @param data - The parameters for the routes request.
23
- * @returns A `LifiRoutesResponse` promise.
24
- * @throws An error if the operation fails.
25
- */
26
135
  public async getRoutes(data: LifiRoutesRequest): Promise<LifiRoutesResponse> {
27
136
  return this.mpc?.getLifiRoutes(data)
28
137
  }
29
138
 
30
- /**
31
- * Retrieves a quote from the Li.Fi integration.
32
- * @param data - The parameters for the quote request.
33
- * @returns A `LifiQuoteResponse` promise.
34
- * @throws An error if the operation fails.
35
- */
36
139
  public async getQuote(data: LifiQuoteRequest): Promise<LifiQuoteResponse> {
37
140
  return this.mpc?.getLifiQuote(data)
38
141
  }
39
142
 
40
- /**
41
- * Retrieves the status of a transaction from the Li.Fi integration.
42
- * @param data - The parameters for the status request.
43
- * @returns A `LifiStatusResponse` promise.
44
- * @throws An error if the operation fails.
45
- */
46
143
  public async getStatus(data: LifiStatusRequest): Promise<LifiStatusResponse> {
47
144
  return this.mpc?.getLifiStatus(data)
48
145
  }
49
146
 
50
- /**
51
- * Retrieves an unsigned transaction from the Li.Fi integration that has yet to be signed/submitted.
52
- * @param data - The step transaction request containing the step details.
53
- * @returns A `LifiStepTransactionResponse` promise.
54
- * @throws An error if the operation fails.
55
- */
56
147
  public async getRouteStep(
57
148
  data: LifiStepTransactionRequest,
58
149
  ): Promise<LifiStepTransactionResponse> {
59
150
  return this.mpc?.getLifiRouteStep(data)
60
151
  }
152
+
153
+ public async pollStatus(
154
+ request: Pick<
155
+ LifiStatusRequest,
156
+ 'txHash' | 'fromChain' | 'toChain' | 'bridge'
157
+ >,
158
+ options?: LifiPollStatusOptions & {
159
+ onUpdate?: (raw: LifiStatusRawResponse) => boolean | void
160
+ },
161
+ ): Promise<LifiStatusRawResponse> {
162
+ const {
163
+ everyMs = 10_000,
164
+ initialDelayMs = 10_000,
165
+ timeoutMs = 600_000,
166
+ maxConsecutiveErrors = 10,
167
+ backoff = { factor: 1.5, maxIntervalMs: 15_000 },
168
+ onUpdate,
169
+ } = options ?? {}
170
+
171
+ return pollLifiStatusUntilTerminal({
172
+ getStatus: (r) => this.mpc.getLifiStatus(r),
173
+ request,
174
+ onUpdate,
175
+ everyMs,
176
+ initialDelayMs,
177
+ timeoutMs,
178
+ maxConsecutiveErrors,
179
+ backoff,
180
+ })
181
+ }
182
+
183
+ public async tradeAsset(
184
+ params: LifiTradeAssetParams,
185
+ options?: LifiTradeAssetOptions,
186
+ ): Promise<LifiTradeAssetResult> {
187
+ try {
188
+ const o = mergeLifiOptions(this.defaultTradeOptions, options)
189
+ const signAndSend = o.signAndSendTransaction
190
+ if (!signAndSend) {
191
+ throw new Error(`${LOG_PREFIX} tradeAsset requires signAndSendTransaction`)
192
+ }
193
+
194
+ const effectiveWait = async (
195
+ txHash: string,
196
+ network: string,
197
+ ): Promise<void | boolean> => {
198
+ if (o.waitForConfirmation) {
199
+ return o.waitForConfirmation(txHash, network)
200
+ }
201
+ if (o.evmRequestFn) {
202
+ if (!network.startsWith('eip155:')) {
203
+ sdkLogger.warn(
204
+ `${LOG_PREFIX} Cannot verify confirmation for non-EVM network ${network}. ` +
205
+ 'Provide waitForConfirmation or use Portal.waitForConfirmation.',
206
+ )
207
+ return Promise.resolve(false)
208
+ }
209
+ return waitForEvmTxConfirmation(txHash, network, o.evmRequestFn, {
210
+ pollIntervalMs: o.evmPollerOptions?.pollIntervalMs,
211
+ timeoutMs: o.evmPollerOptions?.timeoutMs,
212
+ // Intentionally differs from Yield's optimistic `resolve_false` timeout
213
+ // handling: LiFi must abort here so we do not continue into bridge-status
214
+ // polling after source-chain confirmation has already timed out.
215
+ onTimeout: 'throw',
216
+ })
217
+ }
218
+ throw new Error(
219
+ `${LOG_PREFIX} tradeAsset requires waitForConfirmation or evmRequestFn fallback.`,
220
+ )
221
+ }
222
+
223
+ params.onProgress?.('fetching_routes')
224
+
225
+ const routesRes = await this.getRoutes({
226
+ fromChainId: normalizeChainToNetwork(params.fromChain),
227
+ toChainId: normalizeChainToNetwork(params.toChain),
228
+ fromTokenAddress: params.fromToken,
229
+ toTokenAddress: params.toToken,
230
+ fromAmount: params.amount,
231
+ fromAddress: params.fromAddress,
232
+ toAddress: params.toAddress,
233
+ options: params.routeOptions,
234
+ })
235
+
236
+ if (routesRes.error) {
237
+ throw new Error(`${LOG_PREFIX} getRoutes: ${routesRes.error}`)
238
+ }
239
+
240
+ const routes = routesRes.data?.rawResponse.routes ?? []
241
+ if (routes.length === 0) {
242
+ throw new Error(`${LOG_PREFIX} No routes returned from LiFi`)
243
+ }
244
+
245
+ const idx = params.routeIndex ?? 0
246
+ const route = routes[idx]
247
+ if (!route) {
248
+ throw new Error(`${LOG_PREFIX} no route available at index ${idx}`)
249
+ }
250
+
251
+ params.onProgress?.('route_selected', { route, routeIndex: idx })
252
+
253
+ const hashes: string[] = []
254
+ const stepsOut: LifiStep[] = []
255
+ const totalSteps = route.steps.length
256
+
257
+ for (let stepIndex = 0; stepIndex < route.steps.length; stepIndex++) {
258
+ const step = route.steps[stepIndex]
259
+
260
+ // Validate step exists (defensive check for sparse arrays)
261
+ if (!step) {
262
+ throw new Error(
263
+ `${LOG_PREFIX} Step at index ${stepIndex} is undefined or null. This indicates a malformed route response.`,
264
+ )
265
+ }
266
+
267
+ params.onProgress?.('preparing_step', {
268
+ route,
269
+ routeIndex: idx,
270
+ step,
271
+ stepIndex,
272
+ totalSteps,
273
+ })
274
+
275
+ const stepRes = await this.getRouteStep(step)
276
+ if (stepRes.error) {
277
+ throw new Error(`${LOG_PREFIX} getRouteStep: ${stepRes.error}`)
278
+ }
279
+
280
+ const populated = stepRes.data?.rawResponse
281
+ if (!populated) {
282
+ throw new Error(`${LOG_PREFIX} getRouteStep returned no step data`)
283
+ }
284
+
285
+ const network = resolveLifiStepNetworkCaip2(populated)
286
+ let tx = parseLifiTransactionRequestFromStep(populated)
287
+ if (network.startsWith('eip155:')) {
288
+ const stripped = stripStalePlanningNonceIfJsonObject(tx)
289
+ // Validate stripped result is still an object
290
+ if (
291
+ typeof stripped !== 'object' ||
292
+ stripped === null ||
293
+ Array.isArray(stripped)
294
+ ) {
295
+ throw new Error(
296
+ `${LOG_PREFIX} stripStalePlanningNonce returned invalid type: ${typeof stripped}`,
297
+ )
298
+ }
299
+ tx = stripped as Record<string, unknown>
300
+ }
301
+
302
+ params.onProgress?.('signing', {
303
+ route,
304
+ routeIndex: idx,
305
+ step: populated,
306
+ stepIndex,
307
+ totalSteps,
308
+ transaction: tx,
309
+ })
310
+
311
+ const txHash = await signAndSend(tx, network)
312
+ if (typeof txHash !== 'string' || txHash.trim().length === 0) {
313
+ const msg = `Invalid transaction hash returned from signAndSendTransaction at step ${stepIndex} for network ${network}.`
314
+ params.onProgress?.('failed', {
315
+ errorMessage: msg,
316
+ txHash,
317
+ routeIndex: idx,
318
+ stepIndex,
319
+ totalSteps,
320
+ step: populated,
321
+ })
322
+ throw new LifiReportedError(`${LOG_PREFIX} ${msg}`)
323
+ }
324
+ hashes.push(txHash)
325
+
326
+ params.onProgress?.('submitted', {
327
+ route,
328
+ routeIndex: idx,
329
+ step: populated,
330
+ stepIndex,
331
+ totalSteps,
332
+ txHash,
333
+ })
334
+
335
+ params.onProgress?.('confirming', {
336
+ route,
337
+ routeIndex: idx,
338
+ step: populated,
339
+ stepIndex,
340
+ totalSteps,
341
+ txHash,
342
+ })
343
+
344
+ const waiterResult = (await effectiveWait(txHash, network)) as
345
+ | boolean
346
+ | void
347
+ const ok = waiterResult === true
348
+ if (!ok) {
349
+ const msg = `${LOG_PREFIX} on-chain confirmation did not complete (waitForConfirmation did not return true) for ${txHash} on ${network}`
350
+ params.onProgress?.('failed', {
351
+ errorMessage: msg,
352
+ txHash,
353
+ routeIndex: idx,
354
+ stepIndex,
355
+ totalSteps,
356
+ step: populated,
357
+ })
358
+ throw new LifiReportedError(msg)
359
+ }
360
+
361
+ const bridgeTool = statusBridgeFromStepTool(populated.tool)
362
+ const crossLike =
363
+ populated.type === 'cross' ||
364
+ populated.action.fromChainId !== populated.action.toChainId
365
+
366
+ if (crossLike && bridgeTool) {
367
+ params.onProgress?.('lifi_pending', {
368
+ route,
369
+ routeIndex: idx,
370
+ step: populated,
371
+ stepIndex,
372
+ totalSteps,
373
+ txHash,
374
+ })
375
+
376
+ const pollOpts = params.statusPoll ?? {}
377
+ const terminal = await this.pollStatus(
378
+ {
379
+ txHash,
380
+ fromChain: params.fromChain,
381
+ toChain: params.toChain,
382
+ bridge: bridgeTool,
383
+ },
384
+ pollOpts,
385
+ )
386
+
387
+ params.onProgress?.('step_done', {
388
+ route,
389
+ routeIndex: idx,
390
+ step: populated,
391
+ stepIndex,
392
+ totalSteps,
393
+ txHash,
394
+ lifiStatus: terminal,
395
+ })
396
+ } else {
397
+ params.onProgress?.('step_done', {
398
+ route,
399
+ routeIndex: idx,
400
+ step: populated,
401
+ stepIndex,
402
+ totalSteps,
403
+ txHash,
404
+ })
405
+ }
406
+
407
+ stepsOut.push(populated)
408
+ }
409
+
410
+ params.onProgress?.('complete', { route, routeIndex: idx })
411
+ return { hashes, steps: stepsOut, route }
412
+ } catch (error: unknown) {
413
+ if (!(error instanceof LifiReportedError)) {
414
+ const errorMessage =
415
+ error instanceof Error ? error.message : String(error)
416
+ params.onProgress?.('failed', { errorMessage })
417
+ }
418
+ throw error
419
+ }
420
+ }
61
421
  }