@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
@@ -5,7 +5,9 @@
5
5
  import ZeroX from '.'
6
6
  import Mpc from '../../../mpc'
7
7
  import portalMock from '../../../__mocks/portal/portal'
8
+ import * as waitEvm from '../../../internal/waitForEvmTxConfirmation'
8
9
  import {
10
+ mockEip155Address,
9
11
  mockHost,
10
12
  mockSourcesRes,
11
13
  mockZeroExQuoteV2Request,
@@ -82,7 +84,6 @@ describe('ZeroX', () => {
82
84
  zeroXApiKey: 'test-api-key',
83
85
  })
84
86
 
85
- console.log(`Result:`, result)
86
87
  expect(spy).toHaveBeenCalledTimes(1)
87
88
  expect(spy).toHaveBeenCalledWith(
88
89
  { chainId: 'eip155:1' },
@@ -151,4 +152,299 @@ describe('ZeroX', () => {
151
152
  )
152
153
  })
153
154
  })
155
+
156
+ describe('tradeAsset', () => {
157
+ it('throws when waitForConfirmation returns false and emits failed, not confirmed', async () => {
158
+ const onProgress = jest.fn()
159
+ jest
160
+ .spyOn(mpc, 'getSwapsQuoteV2')
161
+ .mockResolvedValue(mockZeroExQuoteV2Response)
162
+
163
+ await expect(
164
+ zeroX.tradeAsset(
165
+ {
166
+ ...mockZeroExQuoteV2Request,
167
+ fromAddress: mockEip155Address,
168
+ onProgress,
169
+ },
170
+ {
171
+ signAndSendTransaction: async () => '0xabc',
172
+ waitForConfirmation: async () => false,
173
+ },
174
+ ),
175
+ ).rejects.toThrow(/on-chain confirmation/)
176
+
177
+ const statuses = onProgress.mock.calls.map((c) => c[0])
178
+ expect(statuses).toContain('failed')
179
+ expect(statuses).not.toContain('confirmed')
180
+ })
181
+
182
+ it('emits confirmed after waitForConfirmation succeeds', async () => {
183
+ const onProgress = jest.fn()
184
+ jest
185
+ .spyOn(mpc, 'getSwapsQuoteV2')
186
+ .mockResolvedValue(mockZeroExQuoteV2Response)
187
+
188
+ await zeroX.tradeAsset(
189
+ {
190
+ ...mockZeroExQuoteV2Request,
191
+ fromAddress: mockEip155Address,
192
+ onProgress,
193
+ },
194
+ {
195
+ signAndSendTransaction: async () => '0xabc',
196
+ waitForConfirmation: async () => true,
197
+ },
198
+ )
199
+
200
+ expect(onProgress).toHaveBeenCalledWith('confirmed', { txHash: '0xabc' })
201
+ })
202
+
203
+ it('throws before signing when quote has error string', async () => {
204
+ jest.spyOn(mpc, 'getSwapsQuoteV2').mockResolvedValue({
205
+ error: 'insufficient liquidity',
206
+ } as never)
207
+
208
+ await expect(
209
+ zeroX.tradeAsset(
210
+ {
211
+ ...mockZeroExQuoteV2Request,
212
+ fromAddress: mockEip155Address,
213
+ },
214
+ {
215
+ signAndSendTransaction: async () => '0x',
216
+ waitForConfirmation: async () => true,
217
+ },
218
+ ),
219
+ ).rejects.toThrow(/Quote error: insufficient liquidity/)
220
+ })
221
+
222
+ it('throws before signing when neither waitForConfirmation nor evmRequestFn', async () => {
223
+ const quoteSpy = jest
224
+ .spyOn(mpc, 'getSwapsQuoteV2')
225
+ .mockResolvedValue(mockZeroExQuoteV2Response)
226
+
227
+ await expect(
228
+ zeroX.tradeAsset(
229
+ {
230
+ ...mockZeroExQuoteV2Request,
231
+ fromAddress: mockEip155Address,
232
+ },
233
+ {
234
+ signAndSendTransaction: async () => '0xshouldnotrun',
235
+ },
236
+ ),
237
+ ).rejects.toThrow(/requires waitForConfirmation.*or evmRequestFn/)
238
+
239
+ expect(quoteSpy).not.toHaveBeenCalled()
240
+ })
241
+
242
+ it('throws before signing when rawResponse is missing', async () => {
243
+ jest.spyOn(mpc, 'getSwapsQuoteV2').mockResolvedValue({
244
+ data: {},
245
+ } as never)
246
+
247
+ await expect(
248
+ zeroX.tradeAsset(
249
+ {
250
+ ...mockZeroExQuoteV2Request,
251
+ fromAddress: mockEip155Address,
252
+ },
253
+ {
254
+ signAndSendTransaction: async () => '0x',
255
+ waitForConfirmation: async () => true,
256
+ },
257
+ ),
258
+ ).rejects.toThrow(/missing data.rawResponse/)
259
+ })
260
+
261
+ it('throws before signing when transaction.to is invalid', async () => {
262
+ jest.spyOn(mpc, 'getSwapsQuoteV2').mockResolvedValue({
263
+ data: {
264
+ rawResponse: {
265
+ ...mockZeroExQuoteV2Response.data!.rawResponse,
266
+ transaction: { to: '' },
267
+ },
268
+ },
269
+ } as never)
270
+
271
+ const onProgress = jest.fn()
272
+ await expect(
273
+ zeroX.tradeAsset(
274
+ {
275
+ ...mockZeroExQuoteV2Request,
276
+ fromAddress: mockEip155Address,
277
+ onProgress,
278
+ },
279
+ {
280
+ signAndSendTransaction: async () => '0x',
281
+ waitForConfirmation: async () => true,
282
+ },
283
+ ),
284
+ ).rejects.toThrow(/missing valid transaction/)
285
+
286
+ expect(onProgress).toHaveBeenCalledWith(
287
+ 'failed',
288
+ expect.objectContaining({
289
+ errorMessage: expect.stringContaining('valid transaction'),
290
+ }),
291
+ )
292
+ })
293
+
294
+ it('signer throws: rethrows and emits failed', async () => {
295
+ const onProgress = jest.fn()
296
+ jest
297
+ .spyOn(mpc, 'getSwapsQuoteV2')
298
+ .mockResolvedValue(mockZeroExQuoteV2Response)
299
+
300
+ await expect(
301
+ zeroX.tradeAsset(
302
+ {
303
+ ...mockZeroExQuoteV2Request,
304
+ fromAddress: mockEip155Address,
305
+ onProgress,
306
+ },
307
+ {
308
+ signAndSendTransaction: async () => {
309
+ throw new Error('user rejected')
310
+ },
311
+ waitForConfirmation: async () => true,
312
+ },
313
+ ),
314
+ ).rejects.toThrow('user rejected')
315
+
316
+ expect(onProgress).toHaveBeenCalledWith(
317
+ 'failed',
318
+ expect.objectContaining({ errorMessage: 'user rejected' }),
319
+ )
320
+ })
321
+
322
+ it('empty hash after sign: throws and emits failed', async () => {
323
+ const onProgress = jest.fn()
324
+ jest
325
+ .spyOn(mpc, 'getSwapsQuoteV2')
326
+ .mockResolvedValue(mockZeroExQuoteV2Response)
327
+
328
+ await expect(
329
+ zeroX.tradeAsset(
330
+ {
331
+ ...mockZeroExQuoteV2Request,
332
+ fromAddress: mockEip155Address,
333
+ onProgress,
334
+ },
335
+ {
336
+ signAndSendTransaction: async () => ' ',
337
+ waitForConfirmation: async () => true,
338
+ },
339
+ ),
340
+ ).rejects.toThrow(/empty or invalid transaction hash/)
341
+
342
+ expect(onProgress).toHaveBeenCalledWith(
343
+ 'failed',
344
+ expect.objectContaining({ errorMessage: expect.any(String) }),
345
+ )
346
+ })
347
+
348
+ it('evmRequestFn path: waits for receipt with onTimeout throw', async () => {
349
+ const waitSpy = jest
350
+ .spyOn(waitEvm, 'waitForEvmTxConfirmation')
351
+ .mockResolvedValue(true)
352
+ jest
353
+ .spyOn(mpc, 'getSwapsQuoteV2')
354
+ .mockResolvedValue(mockZeroExQuoteV2Response)
355
+
356
+ await zeroX.tradeAsset(
357
+ {
358
+ ...mockZeroExQuoteV2Request,
359
+ fromAddress: mockEip155Address,
360
+ },
361
+ {
362
+ signAndSendTransaction: async () => '0xhash',
363
+ evmRequestFn: async () => ({}),
364
+ },
365
+ )
366
+
367
+ expect(waitSpy).toHaveBeenCalledWith(
368
+ '0xhash',
369
+ 'eip155:1',
370
+ expect.any(Function),
371
+ expect.objectContaining({ onTimeout: 'throw' }),
372
+ )
373
+ waitSpy.mockRestore()
374
+ })
375
+
376
+ it('instance zeroXApiKey wins over params.zeroXApiKey when both set', async () => {
377
+ const spy = jest
378
+ .spyOn(mpc, 'getSwapsQuoteV2')
379
+ .mockResolvedValue(mockZeroExQuoteV2Response)
380
+ const z = new ZeroX({
381
+ mpc,
382
+ zeroXApiKey: 'from-instance',
383
+ })
384
+
385
+ await z.tradeAsset(
386
+ {
387
+ ...mockZeroExQuoteV2Request,
388
+ fromAddress: mockEip155Address,
389
+ zeroXApiKey: 'from-params',
390
+ },
391
+ {
392
+ signAndSendTransaction: async () => '0xh',
393
+ waitForConfirmation: async () => true,
394
+ },
395
+ )
396
+
397
+ expect(spy).toHaveBeenCalledWith(
398
+ expect.any(Object),
399
+ expect.objectContaining({ zeroXApiKey: 'from-instance' }),
400
+ )
401
+ })
402
+
403
+ it('uses params.zeroXApiKey when instance has no key', async () => {
404
+ const spy = jest
405
+ .spyOn(mpc, 'getSwapsQuoteV2')
406
+ .mockResolvedValue(mockZeroExQuoteV2Response)
407
+
408
+ await zeroX.tradeAsset(
409
+ {
410
+ ...mockZeroExQuoteV2Request,
411
+ fromAddress: mockEip155Address,
412
+ zeroXApiKey: 'params-only',
413
+ },
414
+ {
415
+ signAndSendTransaction: async () => '0xh',
416
+ waitForConfirmation: async () => true,
417
+ },
418
+ )
419
+
420
+ expect(spy).toHaveBeenCalledWith(
421
+ expect.any(Object),
422
+ expect.objectContaining({ zeroXApiKey: 'params-only' }),
423
+ )
424
+ })
425
+
426
+ it('normalizes numeric chainId to eip155 for signer', async () => {
427
+ const sign = jest.fn().mockResolvedValue('0xh')
428
+ jest.spyOn(mpc, 'getSwapsQuoteV2').mockResolvedValue({
429
+ ...mockZeroExQuoteV2Response,
430
+ })
431
+
432
+ await zeroX.tradeAsset(
433
+ {
434
+ ...mockZeroExQuoteV2Request,
435
+ chainId: '1',
436
+ fromAddress: mockEip155Address,
437
+ },
438
+ {
439
+ signAndSendTransaction: sign,
440
+ waitForConfirmation: async () => true,
441
+ },
442
+ )
443
+
444
+ expect(sign).toHaveBeenCalledWith(
445
+ expect.anything(),
446
+ 'eip155:1',
447
+ )
448
+ })
449
+ })
154
450
  })
@@ -1,4 +1,6 @@
1
1
  import Mpc from '../../../mpc'
2
+ import { waitForEvmTxConfirmation } from '../../../internal/waitForEvmTxConfirmation'
3
+ import { normalizeChainToNetwork } from '../lifi'
2
4
  import {
3
5
  ZeroExOptions,
4
6
  ZeroExPriceRequest,
@@ -6,55 +8,207 @@ import {
6
8
  ZeroExQuoteRequest,
7
9
  ZeroExQuoteResponse,
8
10
  ZeroExSourcesResponse,
11
+ ZeroXTradeAssetOptions,
12
+ ZeroXTradeAssetParams,
13
+ ZeroXTradeAssetResult,
9
14
  } from '../../../shared/types/zero-x'
10
15
 
11
- export default class ZeroX {
16
+ const LOG_PREFIX = '[ZeroX]'
17
+
18
+ export interface ZeroXOptions extends ZeroXTradeAssetOptions {
19
+ zeroXApiKey?: string
20
+ }
21
+
22
+ export interface IZeroX {
23
+ getQuote(
24
+ args: ZeroExQuoteRequest,
25
+ options?: ZeroExOptions,
26
+ ): Promise<ZeroExQuoteResponse>
27
+ getSources(
28
+ chainId: string,
29
+ options?: ZeroExOptions,
30
+ ): Promise<ZeroExSourcesResponse>
31
+ getPrice(
32
+ args: ZeroExPriceRequest,
33
+ options?: ZeroExOptions,
34
+ ): Promise<ZeroExPriceResponse>
35
+ tradeAsset(
36
+ params: ZeroXTradeAssetParams,
37
+ options?: ZeroXTradeAssetOptions & ZeroExOptions,
38
+ ): Promise<ZeroXTradeAssetResult>
39
+ }
40
+
41
+ function mergeZeroXOptions(
42
+ instance: (ZeroXTradeAssetOptions & ZeroExOptions) | undefined,
43
+ perCall: (ZeroXTradeAssetOptions & ZeroExOptions) | undefined,
44
+ ): ZeroXTradeAssetOptions & ZeroExOptions {
45
+ return { ...instance, ...perCall }
46
+ }
47
+
48
+ export default class ZeroX implements IZeroX {
12
49
  private mpc: Mpc
50
+ private readonly defaults: ZeroXTradeAssetOptions & ZeroExOptions
13
51
 
14
- constructor({ mpc }: { mpc: Mpc }) {
52
+ constructor({ mpc, ...defaults }: { mpc: Mpc } & ZeroXOptions) {
15
53
  this.mpc = mpc
54
+ this.defaults = defaults
16
55
  }
17
56
 
18
- /**
19
- * Get a quote from the Swaps API.
20
- *
21
- * @param apiKey - The API key for the Swaps API.
22
- * @param args - The arguments for the quote.
23
- * @param chainId - The chain ID for the quote.
24
- * @returns The quote response.
25
- */
26
57
  public async getQuote(
27
58
  args: ZeroExQuoteRequest,
28
59
  options?: ZeroExOptions,
29
60
  ): Promise<ZeroExQuoteResponse> {
30
- return this.mpc?.getSwapsQuoteV2(args, options)
61
+ const merged = { ...this.defaults, ...options }
62
+ const key = merged.zeroXApiKey
63
+ return this.mpc?.getSwapsQuoteV2(
64
+ args,
65
+ key != null && key !== '' ? { zeroXApiKey: key } : undefined,
66
+ )
31
67
  }
32
68
 
33
- /**
34
- * Get the valid, swappable token sources that can be used with your Portal MPC Wallet.
35
- *
36
- * @param apiKey - The API key for the Swaps API.
37
- * @param chainId - The chain ID for the sources.
38
- * @returns The sources response.
39
- */
40
69
  public async getSources(
41
70
  chainId: string,
42
71
  options?: ZeroExOptions,
43
72
  ): Promise<ZeroExSourcesResponse> {
44
- return this.mpc?.getSwapsSourcesV2({ chainId }, options)
73
+ const merged = { ...this.defaults, ...options }
74
+ const key = merged.zeroXApiKey
75
+ return this.mpc?.getSwapsSourcesV2(
76
+ { chainId },
77
+ key != null && key !== '' ? { zeroXApiKey: key } : undefined,
78
+ )
45
79
  }
46
80
 
47
- /**
48
- * Get the price of a token from the Swaps API.
49
- *
50
- * @param args - The arguments for the price.
51
- * @param options - The options for the price.
52
- * @returns The price response.
53
- */
54
81
  public async getPrice(
55
82
  args: ZeroExPriceRequest,
56
83
  options?: ZeroExOptions,
57
84
  ): Promise<ZeroExPriceResponse> {
58
- return this.mpc?.getSwapsPrice(args, options)
85
+ const merged = { ...this.defaults, ...options }
86
+ const key = merged.zeroXApiKey
87
+ return this.mpc?.getSwapsPrice(
88
+ args,
89
+ key != null && key !== '' ? { zeroXApiKey: key } : undefined,
90
+ )
91
+ }
92
+
93
+ public async tradeAsset(
94
+ params: ZeroXTradeAssetParams,
95
+ options?: ZeroXTradeAssetOptions & ZeroExOptions,
96
+ ): Promise<ZeroXTradeAssetResult> {
97
+ const o = mergeZeroXOptions(this.defaults, options)
98
+ const signAndSend = o.signAndSendTransaction
99
+ if (!signAndSend) {
100
+ throw new Error(`${LOG_PREFIX} tradeAsset requires signAndSendTransaction`)
101
+ }
102
+
103
+ if (!o.waitForConfirmation && !o.evmRequestFn) {
104
+ throw new Error(
105
+ `${LOG_PREFIX} tradeAsset requires waitForConfirmation (instance default or per-call option), or evmRequestFn fallback.`,
106
+ )
107
+ }
108
+
109
+ const apiOpts = { zeroXApiKey: o.zeroXApiKey ?? params.zeroXApiKey }
110
+
111
+ params.onProgress?.('fetching_quote')
112
+
113
+ const quoteReq: ZeroExQuoteRequest = {
114
+ chainId: params.chainId,
115
+ buyToken: params.buyToken,
116
+ sellToken: params.sellToken,
117
+ sellAmount: params.sellAmount,
118
+ txOrigin: params.fromAddress,
119
+ swapFeeRecipient: params.swapFeeRecipient,
120
+ swapFeeBps: params.swapFeeBps,
121
+ swapFeeToken: params.swapFeeToken,
122
+ tradeSurplusRecipient: params.tradeSurplusRecipient,
123
+ gasPrice: params.gasPrice,
124
+ slippageBps: params.slippageBps,
125
+ excludedSources: params.excludedSources,
126
+ sellEntireBalance: params.sellEntireBalance,
127
+ }
128
+
129
+ const quote = await this.mpc.getSwapsQuoteV2(quoteReq, apiOpts)
130
+
131
+ const err = quote.error?.trim()
132
+ if (err) {
133
+ throw new Error(`${LOG_PREFIX} Quote error: ${err}`)
134
+ }
135
+
136
+ const raw = quote.data?.rawResponse
137
+ if (!raw) {
138
+ const msg = 'Quote response missing data.rawResponse'
139
+ params.onProgress?.('failed', { errorMessage: msg })
140
+ throw new Error(`${LOG_PREFIX} ${msg}`)
141
+ }
142
+
143
+ const transaction = raw.transaction
144
+ const to =
145
+ transaction !== null &&
146
+ transaction !== undefined &&
147
+ typeof transaction === 'object' &&
148
+ 'to' in transaction
149
+ ? (transaction as { to?: unknown }).to
150
+ : undefined
151
+
152
+ if (
153
+ typeof to !== 'string' ||
154
+ to.trim() === '' ||
155
+ transaction === null ||
156
+ transaction === undefined ||
157
+ typeof transaction !== 'object'
158
+ ) {
159
+ const msg =
160
+ 'Quote response missing valid transaction (expected object with non-empty string "to")'
161
+ params.onProgress?.('failed', { errorMessage: msg })
162
+ throw new Error(`${LOG_PREFIX} ${msg}`)
163
+ }
164
+
165
+ const tx = transaction
166
+
167
+ const network = normalizeChainToNetwork(params.chainId)
168
+
169
+ params.onProgress?.('signing', {
170
+ buyAmount: quote.data?.rawResponse?.buyAmount,
171
+ sellAmount: quote.data?.rawResponse?.sellAmount,
172
+ transaction: tx,
173
+ })
174
+
175
+ let txHash: string
176
+ try {
177
+ txHash = await signAndSend(tx, network)
178
+ } catch (e) {
179
+ const msg = e instanceof Error ? e.message : String(e)
180
+ params.onProgress?.('failed', { errorMessage: msg })
181
+ throw e
182
+ }
183
+
184
+ if (typeof txHash !== 'string' || txHash.trim() === '') {
185
+ const msg = 'signAndSendTransaction returned empty or invalid transaction hash'
186
+ params.onProgress?.('failed', { errorMessage: msg })
187
+ throw new Error(`${LOG_PREFIX} ${msg}`)
188
+ }
189
+
190
+ params.onProgress?.('submitted', { txHash })
191
+
192
+ if (o.waitForConfirmation) {
193
+ params.onProgress?.('confirming', { txHash })
194
+ const waiterResult = await o.waitForConfirmation(txHash, network)
195
+ const ok = waiterResult === true
196
+ if (!ok) {
197
+ const msg = `${LOG_PREFIX} on-chain confirmation did not complete (waitForConfirmation did not return true) for ${txHash} on ${network}`
198
+ params.onProgress?.('failed', { errorMessage: msg, txHash })
199
+ throw new Error(msg)
200
+ }
201
+ params.onProgress?.('confirmed', { txHash })
202
+ } else if (o.evmRequestFn) {
203
+ params.onProgress?.('confirming', { txHash })
204
+ await waitForEvmTxConfirmation(txHash, network, o.evmRequestFn, {
205
+ pollIntervalMs: o.evmPollerOptions?.pollIntervalMs,
206
+ timeoutMs: o.evmPollerOptions?.timeoutMs,
207
+ onTimeout: 'throw',
208
+ })
209
+ params.onProgress?.('confirmed', { txHash })
210
+ }
211
+
212
+ return { hashes: [txHash] }
59
213
  }
60
214
  }
@@ -1,14 +1,34 @@
1
1
  import Mpc from '../../mpc'
2
+ import type { YieldXyzValidator } from '../../shared/types'
2
3
  import YieldXyz from './yieldxyz'
3
4
 
5
+ export interface YieldOptions {
6
+ waitForConfirmation?: (
7
+ txHash: string,
8
+ network: string,
9
+ ) => Promise<void | boolean>
10
+ evmRequestFn?: (
11
+ method: string,
12
+ params: unknown[],
13
+ network: string,
14
+ ) => Promise<unknown>
15
+ evmPollerOptions?: {
16
+ pollIntervalMs?: number
17
+ timeoutMs?: number
18
+ }
19
+ }
20
+
4
21
  /**
5
- * This class is a container for the YieldXyz class.
6
- * In the future, Yield domain logic should be here.
22
+ * Yield integrations (Yield.xyz, etc.).
7
23
  */
8
24
  export default class Yield {
9
25
  public yieldXyz: YieldXyz
10
26
 
11
- constructor({ mpc }: { mpc: Mpc }) {
12
- this.yieldXyz = new YieldXyz({ mpc })
27
+ constructor({ mpc, ...rest }: { mpc: Mpc } & YieldOptions) {
28
+ this.yieldXyz = new YieldXyz({ mpc, ...rest })
29
+ }
30
+
31
+ public getValidators(yieldId: string): Promise<YieldXyzValidator[]> {
32
+ return this.yieldXyz.getValidators(yieldId)
13
33
  }
14
34
  }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ import YieldXyz from './yieldxyz'
6
+ import Mpc from '../../mpc'
7
+ import portalMock from '../../__mocks/portal/portal'
8
+ import { mockHost } from '../../__mocks/constants'
9
+
10
+ describe('YieldXyz getValidators', () => {
11
+ let mpc: Mpc
12
+ let yieldXyz: YieldXyz
13
+
14
+ beforeEach(() => {
15
+ jest.clearAllMocks()
16
+ portalMock.host = mockHost
17
+ mpc = new Mpc({ portal: portalMock })
18
+ yieldXyz = new YieldXyz({ mpc })
19
+ })
20
+
21
+ it('returns validators from data.validators', async () => {
22
+ const v = [{ address: '0x1', name: 'v1' }]
23
+ jest.spyOn(mpc, 'getYieldXyzValidators').mockResolvedValue({
24
+ data: { validators: v },
25
+ })
26
+ await expect(yieldXyz.getValidators('y1')).resolves.toEqual(v)
27
+ })
28
+
29
+ it('falls back to data.rawResponse.validators', async () => {
30
+ const v = [{ address: '0x2' }]
31
+ jest.spyOn(mpc, 'getYieldXyzValidators').mockResolvedValue({
32
+ data: { rawResponse: { validators: v } },
33
+ })
34
+ await expect(yieldXyz.getValidators('y1')).resolves.toEqual(v)
35
+ })
36
+
37
+ it('falls back to data.rawResponse.items', async () => {
38
+ const v = [{ address: '0x3' }]
39
+ jest.spyOn(mpc, 'getYieldXyzValidators').mockResolvedValue({
40
+ data: { rawResponse: { items: v, total: 1 } },
41
+ })
42
+ await expect(yieldXyz.getValidators('y1')).resolves.toEqual(v)
43
+ })
44
+
45
+ it('returns empty array when validators is empty (valid)', async () => {
46
+ jest.spyOn(mpc, 'getYieldXyzValidators').mockResolvedValue({
47
+ data: { validators: [] },
48
+ })
49
+ await expect(yieldXyz.getValidators('y1')).resolves.toEqual([])
50
+ })
51
+
52
+ it('throws on res.error', async () => {
53
+ jest.spyOn(mpc, 'getYieldXyzValidators').mockResolvedValue({
54
+ error: 'not found',
55
+ data: { validators: [{ address: 'x' }] },
56
+ })
57
+ await expect(yieldXyz.getValidators('y1')).rejects.toThrow(
58
+ '[YieldXyz] getValidators failed: not found',
59
+ )
60
+ })
61
+
62
+ it('throws when no validators array in response', async () => {
63
+ jest.spyOn(mpc, 'getYieldXyzValidators').mockResolvedValue({
64
+ data: { rawResponse: {} },
65
+ })
66
+ await expect(yieldXyz.getValidators('y1')).rejects.toThrow(
67
+ 'No validators in response',
68
+ )
69
+ })
70
+ })