@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
@@ -0,0 +1,403 @@
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 { mockAddress, mockHost } from '../../__mocks/constants'
9
+ import type { YieldXyzEnterYieldResponse, YieldXyzExitResponse } from '../../shared/types'
10
+
11
+ describe('YieldXyz high-level (deposit, withdraw, defaults)', () => {
12
+ let mpc: Mpc
13
+ let yieldXyz: YieldXyz
14
+
15
+ beforeEach(() => {
16
+ jest.clearAllMocks()
17
+ portalMock.host = mockHost
18
+ mpc = new Mpc({ portal: portalMock })
19
+ yieldXyz = new YieldXyz({ mpc })
20
+ yieldXyz.setSignAndSendTransaction(jest.fn().mockResolvedValue('0xabc'))
21
+ })
22
+
23
+ function enterResponseWithTxs(
24
+ txs: Array<{
25
+ id: string
26
+ network: string
27
+ unsignedTransaction: unknown
28
+ }>,
29
+ ): YieldXyzEnterYieldResponse {
30
+ return {
31
+ data: {
32
+ rawResponse: {
33
+ id: 'action-1',
34
+ intent: 'enter',
35
+ type: 'STAKE',
36
+ yieldId: 'resolved-yield',
37
+ address: mockAddress,
38
+ createdAt: '2024-01-01T00:00:00Z',
39
+ status: 'CREATED',
40
+ executionPattern: 'synchronous',
41
+ transactions: txs,
42
+ },
43
+ },
44
+ } as YieldXyzEnterYieldResponse
45
+ }
46
+
47
+ function exitResponseWithTxs(
48
+ txs: Array<{
49
+ id: string
50
+ network: string
51
+ unsignedTransaction: unknown
52
+ }>,
53
+ ): YieldXyzExitResponse {
54
+ return {
55
+ data: {
56
+ rawResponse: {
57
+ id: 'action-1',
58
+ intent: 'exit',
59
+ type: 'UNSTAKE',
60
+ yieldId: 'resolved-yield',
61
+ address: mockAddress,
62
+ createdAt: '2024-01-01T00:00:00Z',
63
+ status: 'CREATED',
64
+ executionPattern: 'synchronous',
65
+ transactions: txs,
66
+ },
67
+ },
68
+ } as YieldXyzExitResponse
69
+ }
70
+
71
+ it('deposit throws when no signer is configured', async () => {
72
+ const y = new YieldXyz({ mpc })
73
+ await expect(
74
+ y.deposit({ yieldId: 'y1', amount: '1', address: mockAddress }),
75
+ ).rejects.toThrow(
76
+ '[YieldXyz] No signer configured. Call setSignAndSendTransaction()',
77
+ )
78
+ })
79
+
80
+ it('deposit throws when chain is not full CAIP-2', async () => {
81
+ await expect(
82
+ yieldXyz.deposit({
83
+ chain: '1',
84
+ token: 'ETH',
85
+ amount: '1',
86
+ address: mockAddress,
87
+ }),
88
+ ).rejects.toThrow('full CAIP-2')
89
+ })
90
+
91
+ it('deposit throws when neither yieldId nor chain+token', async () => {
92
+ await expect(
93
+ yieldXyz.deposit({ amount: '1', address: mockAddress } as never),
94
+ ).rejects.toThrow('Provide either yieldId')
95
+ })
96
+
97
+ it('resolveYieldIdFromPortalDefaults: throws when defaults endpoint returns error', async () => {
98
+ jest.spyOn(mpc, 'getYieldXyzDefaults').mockResolvedValue({
99
+ error: 'boom',
100
+ })
101
+
102
+ await expect(
103
+ yieldXyz.deposit({
104
+ chain: 'eip155:1',
105
+ token: 'ETH',
106
+ amount: '1',
107
+ address: mockAddress,
108
+ }),
109
+ ).rejects.toThrow('Failed to get yield defaults')
110
+ })
111
+
112
+ it('resolveYieldIdFromPortalDefaults: throws when no data', async () => {
113
+ jest.spyOn(mpc, 'getYieldXyzDefaults').mockResolvedValue({})
114
+
115
+ await expect(
116
+ yieldXyz.deposit({
117
+ chain: 'eip155:1',
118
+ token: 'ETH',
119
+ amount: '1',
120
+ address: mockAddress,
121
+ }),
122
+ ).rejects.toThrow('No data returned from yield defaults endpoint')
123
+ })
124
+
125
+ it('resolveYieldIdFromPortalDefaults: throws when key missing', async () => {
126
+ jest.spyOn(mpc, 'getYieldXyzDefaults').mockResolvedValue({
127
+ data: { 'eip155:1:WETH': { yieldId: 'y', opportunity: null } },
128
+ })
129
+
130
+ await expect(
131
+ yieldXyz.deposit({
132
+ chain: 'eip155:1',
133
+ token: 'ETH',
134
+ amount: '1',
135
+ address: mockAddress,
136
+ }),
137
+ ).rejects.toThrow('No default yield for key "eip155:1:ETH"')
138
+ })
139
+
140
+ it('resolveYieldIdFromPortalDefaults: trims token for map key', async () => {
141
+ jest.spyOn(mpc, 'getYieldXyzDefaults').mockResolvedValue({
142
+ data: {
143
+ 'eip155:1:ETH': { yieldId: 'yield-from-defaults', opportunity: null },
144
+ },
145
+ })
146
+ const enterSpy = jest
147
+ .spyOn(mpc, 'enterYieldXyzYield')
148
+ .mockResolvedValue(
149
+ enterResponseWithTxs([
150
+ {
151
+ id: 'tx1',
152
+ network: 'ethereum',
153
+ unsignedTransaction: { to: '0x1', data: '0x' },
154
+ },
155
+ ]),
156
+ )
157
+ jest.spyOn(mpc, 'trackYieldXyzTransaction').mockResolvedValue({
158
+ data: { rawResponse: { status: 'BROADCASTED' } },
159
+ } as never)
160
+
161
+ await yieldXyz.deposit({
162
+ chain: 'eip155:1',
163
+ token: ' ETH ',
164
+ amount: '1',
165
+ address: mockAddress,
166
+ })
167
+
168
+ expect(mpc.getYieldXyzDefaults).toHaveBeenCalledWith({
169
+ includeOpportunities: false,
170
+ })
171
+ expect(enterSpy).toHaveBeenCalledWith(
172
+ expect.objectContaining({ yieldId: 'yield-from-defaults' }),
173
+ )
174
+ })
175
+
176
+ it('deposit merges arguments so top-level amount wins', async () => {
177
+ const enterSpy = jest
178
+ .spyOn(mpc, 'enterYieldXyzYield')
179
+ .mockResolvedValue(
180
+ enterResponseWithTxs([
181
+ {
182
+ id: 'tx1',
183
+ network: 'eip155:1',
184
+ unsignedTransaction: { to: '0x2', data: '0x' },
185
+ },
186
+ ]),
187
+ )
188
+ jest.spyOn(mpc, 'trackYieldXyzTransaction').mockResolvedValue({})
189
+
190
+ await yieldXyz.deposit({
191
+ yieldId: 'y1',
192
+ amount: '10',
193
+ address: mockAddress,
194
+ arguments: { amount: '99', validatorAddress: '0xv' },
195
+ })
196
+
197
+ expect(enterSpy).toHaveBeenCalledWith({
198
+ yieldId: 'y1',
199
+ address: mockAddress,
200
+ arguments: {
201
+ amount: '10',
202
+ validatorAddress: '0xv',
203
+ },
204
+ })
205
+ })
206
+
207
+ it('withdraw merges arguments so top-level amount wins (consistent with deposit)', async () => {
208
+ const exitSpy = jest.spyOn(mpc, 'exitYieldXyzYield').mockResolvedValue(
209
+ exitResponseWithTxs([
210
+ {
211
+ id: 'tx1',
212
+ network: 'eip155:1',
213
+ unsignedTransaction: { to: '0x2', data: '0x' },
214
+ },
215
+ ]),
216
+ )
217
+ jest.spyOn(mpc, 'trackYieldXyzTransaction').mockResolvedValue({})
218
+
219
+ await yieldXyz.withdraw({
220
+ yieldId: 'y1',
221
+ amount: '10',
222
+ address: mockAddress,
223
+ arguments: { amount: '77' },
224
+ })
225
+
226
+ expect(exitSpy).toHaveBeenCalledWith({
227
+ yieldId: 'y1',
228
+ address: mockAddress,
229
+ arguments: { amount: '10' },
230
+ })
231
+ })
232
+
233
+ it('executeAndTrack: throws when no transactions', async () => {
234
+ jest.spyOn(mpc, 'enterYieldXyzYield').mockResolvedValue({
235
+ data: {
236
+ rawResponse: {
237
+ id: 'a',
238
+ intent: 'enter',
239
+ type: 'STAKE',
240
+ yieldId: 'y',
241
+ address: mockAddress,
242
+ createdAt: '2024-01-01T00:00:00Z',
243
+ status: 'CREATED',
244
+ executionPattern: 'synchronous',
245
+ transactions: [],
246
+ },
247
+ },
248
+ } as YieldXyzEnterYieldResponse)
249
+
250
+ await expect(
251
+ yieldXyz.deposit({ yieldId: 'y', amount: '1', address: mockAddress }),
252
+ ).rejects.toThrow('No transactions in yield action response.')
253
+ })
254
+
255
+ it('executeAndTrack: single tx signs, tracks, returns hashes', async () => {
256
+ const sign = jest.fn().mockResolvedValue('0xsig')
257
+ yieldXyz.setSignAndSendTransaction(sign)
258
+ jest.spyOn(mpc, 'enterYieldXyzYield').mockResolvedValue(
259
+ enterResponseWithTxs([
260
+ {
261
+ id: 'tx1',
262
+ network: 'ethereum',
263
+ unsignedTransaction: JSON.stringify({
264
+ to: '0xabc',
265
+ nonce: '0x1',
266
+ data: '0x',
267
+ }),
268
+ },
269
+ ]),
270
+ )
271
+ const trackSpy = jest
272
+ .spyOn(mpc, 'trackYieldXyzTransaction')
273
+ .mockResolvedValue({})
274
+
275
+ const result = await yieldXyz.deposit({ yieldId: 'rid', amount: '1', address: mockAddress })
276
+
277
+ expect(sign).toHaveBeenCalledTimes(1)
278
+ expect(trackSpy).toHaveBeenCalledWith({
279
+ transactionId: 'tx1',
280
+ hash: '0xsig',
281
+ })
282
+ expect(result.hashes).toEqual(['0xsig'])
283
+ expect(result.yieldId).toBe('resolved-yield')
284
+ })
285
+
286
+ it('executeAndTrack: multi-tx sequential', async () => {
287
+ const sign = jest
288
+ .fn()
289
+ .mockResolvedValueOnce('0x1')
290
+ .mockResolvedValueOnce('0x2')
291
+ yieldXyz.setSignAndSendTransaction(sign)
292
+ jest.spyOn(mpc, 'enterYieldXyzYield').mockResolvedValue(
293
+ enterResponseWithTxs([
294
+ {
295
+ id: 'a',
296
+ network: 'eip155:1',
297
+ unsignedTransaction: { to: '0x1' },
298
+ },
299
+ {
300
+ id: 'b',
301
+ network: 'eip155:1',
302
+ unsignedTransaction: { to: '0x2' },
303
+ },
304
+ ]),
305
+ )
306
+ jest.spyOn(mpc, 'trackYieldXyzTransaction').mockResolvedValue({})
307
+
308
+ const result = await yieldXyz.deposit({ yieldId: 'r', amount: '1', address: mockAddress })
309
+
310
+ expect(sign).toHaveBeenCalledTimes(2)
311
+ expect(result.hashes).toEqual(['0x1', '0x2'])
312
+ })
313
+
314
+ it('waitForConfirmation false: still tracks but no confirmed progress', async () => {
315
+ const progress: string[] = []
316
+ const waiter = jest.fn().mockResolvedValue(false)
317
+ const y = new YieldXyz({
318
+ mpc,
319
+ waitForConfirmation: waiter,
320
+ })
321
+ y.setSignAndSendTransaction(jest.fn().mockResolvedValue('0xh'))
322
+ jest.spyOn(mpc, 'enterYieldXyzYield').mockResolvedValue(
323
+ enterResponseWithTxs([
324
+ {
325
+ id: 'tx1',
326
+ network: 'eip155:1',
327
+ unsignedTransaction: { to: '0x1' },
328
+ },
329
+ ]),
330
+ )
331
+ const trackSpy = jest
332
+ .spyOn(mpc, 'trackYieldXyzTransaction')
333
+ .mockResolvedValue({})
334
+
335
+ await y.deposit({ yieldId: 'y', amount: '1', address: mockAddress }, {
336
+ onProgress: (e) => progress.push(e.step),
337
+ })
338
+
339
+ expect(progress).toEqual([
340
+ 'signing',
341
+ 'submitted',
342
+ 'confirming',
343
+ ])
344
+ expect(progress.includes('confirmed')).toBe(false)
345
+ expect(trackSpy).toHaveBeenCalledWith({
346
+ transactionId: 'tx1',
347
+ hash: '0xh',
348
+ })
349
+ })
350
+
351
+ it('waitForConfirmation true: emits confirmed', async () => {
352
+ const progress: string[] = []
353
+ const y = new YieldXyz({
354
+ mpc,
355
+ waitForConfirmation: jest.fn().mockResolvedValue(true),
356
+ })
357
+ y.setSignAndSendTransaction(jest.fn().mockResolvedValue('0xh'))
358
+ jest.spyOn(mpc, 'enterYieldXyzYield').mockResolvedValue(
359
+ enterResponseWithTxs([
360
+ {
361
+ id: 'tx1',
362
+ network: 'eip155:1',
363
+ unsignedTransaction: { to: '0x1' },
364
+ },
365
+ ]),
366
+ )
367
+ jest.spyOn(mpc, 'trackYieldXyzTransaction').mockResolvedValue({})
368
+
369
+ await y.deposit({ yieldId: 'y', amount: '1', address: mockAddress }, {
370
+ onProgress: (e) => progress.push(e.step),
371
+ })
372
+
373
+ expect(progress).toContain('confirmed')
374
+ })
375
+
376
+ it('echoes chain and token on result when resolved from defaults', async () => {
377
+ jest.spyOn(mpc, 'getYieldXyzDefaults').mockResolvedValue({
378
+ data: {
379
+ 'eip155:1:ETH': { yieldId: 'ydef', opportunity: null },
380
+ },
381
+ })
382
+ jest.spyOn(mpc, 'enterYieldXyzYield').mockResolvedValue(
383
+ enterResponseWithTxs([
384
+ {
385
+ id: 'tx1',
386
+ network: 'eip155:1',
387
+ unsignedTransaction: { to: '0x1' },
388
+ },
389
+ ]),
390
+ )
391
+ jest.spyOn(mpc, 'trackYieldXyzTransaction').mockResolvedValue({})
392
+
393
+ const r = await yieldXyz.deposit({
394
+ chain: 'eip155:1',
395
+ token: 'ETH',
396
+ amount: '1',
397
+ address: mockAddress,
398
+ })
399
+
400
+ expect(r.chain).toBe('eip155:1')
401
+ expect(r.token).toBe('ETH')
402
+ })
403
+ })