@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.
- package/lib/commonjs/index.js +127 -9
- package/lib/commonjs/index.test.js +13 -0
- package/lib/commonjs/integrations/delegations/index.js +109 -2
- package/lib/commonjs/integrations/delegations/index.test.js +171 -0
- package/lib/commonjs/integrations/ramps/noah/index.test.js +18 -5
- package/lib/commonjs/integrations/trading/index.js +16 -5
- package/lib/commonjs/integrations/trading/lifi/index.js +297 -25
- package/lib/commonjs/integrations/trading/lifi/lifi.tradeAsset.test.js +360 -0
- package/lib/commonjs/integrations/trading/lifi/lifiStatusPoll.js +118 -0
- package/lib/commonjs/integrations/trading/lifi/lifiStatusPoll.test.js +66 -0
- package/lib/commonjs/integrations/trading/zero-x/index.js +129 -26
- package/lib/commonjs/integrations/trading/zero-x/index.test.js +163 -1
- package/lib/commonjs/integrations/yield/index.js +18 -4
- package/lib/commonjs/integrations/yield/yieldxyz.getValidators.test.js +71 -0
- package/lib/commonjs/integrations/yield/yieldxyz.highLevel.test.js +330 -0
- package/lib/commonjs/integrations/yield/yieldxyz.js +517 -1
- package/lib/commonjs/internal/pollLoop.js +64 -0
- package/lib/commonjs/internal/pollLoop.test.js +100 -0
- package/lib/commonjs/internal/stripStalePlanningNonce.js +65 -0
- package/lib/commonjs/internal/stripStalePlanningNonce.test.js +35 -0
- package/lib/commonjs/internal/waitForEvmOrUserOpConfirmation.js +155 -0
- package/lib/commonjs/internal/waitForEvmOrUserOpConfirmation.test.js +33 -0
- package/lib/commonjs/internal/waitForEvmTxConfirmation.js +104 -0
- package/lib/commonjs/internal/waitForSolanaTxConfirmation.js +106 -0
- package/lib/commonjs/internal/yieldEvmNetwork.js +60 -0
- package/lib/commonjs/mpc/index.js +116 -1
- package/lib/commonjs/provider/index.js +17 -0
- package/lib/commonjs/shared/trace/index.js +0 -1
- package/lib/esm/index.js +127 -9
- package/lib/esm/index.test.js +13 -0
- package/lib/esm/integrations/delegations/index.js +109 -2
- package/lib/esm/integrations/delegations/index.test.js +171 -0
- package/lib/esm/integrations/ramps/noah/index.test.js +18 -5
- package/lib/esm/integrations/trading/index.js +16 -5
- package/lib/esm/integrations/trading/lifi/index.js +292 -25
- package/lib/esm/integrations/trading/lifi/lifi.tradeAsset.test.js +332 -0
- package/lib/esm/integrations/trading/lifi/lifiStatusPoll.js +113 -0
- package/lib/esm/integrations/trading/lifi/lifiStatusPoll.test.js +64 -0
- package/lib/esm/integrations/trading/zero-x/index.js +129 -26
- package/lib/esm/integrations/trading/zero-x/index.test.js +141 -2
- package/lib/esm/integrations/yield/index.js +18 -4
- package/lib/esm/integrations/yield/yieldxyz.getValidators.test.js +66 -0
- package/lib/esm/integrations/yield/yieldxyz.highLevel.test.js +325 -0
- package/lib/esm/integrations/yield/yieldxyz.js +517 -1
- package/lib/esm/internal/pollLoop.js +59 -0
- package/lib/esm/internal/pollLoop.test.js +98 -0
- package/lib/esm/internal/stripStalePlanningNonce.js +61 -0
- package/lib/esm/internal/stripStalePlanningNonce.test.js +33 -0
- package/lib/esm/internal/waitForEvmOrUserOpConfirmation.js +151 -0
- package/lib/esm/internal/waitForEvmOrUserOpConfirmation.test.js +31 -0
- package/lib/esm/internal/waitForEvmTxConfirmation.js +100 -0
- package/lib/esm/internal/waitForSolanaTxConfirmation.js +102 -0
- package/lib/esm/internal/yieldEvmNetwork.js +55 -0
- package/lib/esm/mpc/index.js +116 -1
- package/lib/esm/provider/index.js +17 -0
- package/lib/esm/shared/trace/index.js +0 -1
- package/noah-types.d.ts +16 -2
- package/package.json +3 -2
- package/src/index.test.ts +15 -0
- package/src/index.ts +203 -14
- package/src/integrations/delegations/index.test.ts +251 -0
- package/src/integrations/delegations/index.ts +202 -4
- package/src/integrations/ramps/noah/index.test.ts +18 -5
- package/src/integrations/trading/index.ts +10 -7
- package/src/integrations/trading/lifi/index.ts +388 -28
- package/src/integrations/trading/lifi/lifi.tradeAsset.test.ts +436 -0
- package/src/integrations/trading/lifi/lifiStatusPoll.test.ts +74 -0
- package/src/integrations/trading/lifi/lifiStatusPoll.ts +158 -0
- package/src/integrations/trading/zero-x/index.test.ts +297 -1
- package/src/integrations/trading/zero-x/index.ts +181 -27
- package/src/integrations/yield/index.ts +24 -4
- package/src/integrations/yield/yieldxyz.getValidators.test.ts +70 -0
- package/src/integrations/yield/yieldxyz.highLevel.test.ts +403 -0
- package/src/integrations/yield/yieldxyz.ts +740 -8
- package/src/internal/pollLoop.test.ts +109 -0
- package/src/internal/pollLoop.ts +87 -0
- package/src/internal/stripStalePlanningNonce.test.ts +38 -0
- package/src/internal/stripStalePlanningNonce.ts +66 -0
- package/src/internal/waitForEvmOrUserOpConfirmation.test.ts +31 -0
- package/src/internal/waitForEvmOrUserOpConfirmation.ts +194 -0
- package/src/internal/waitForEvmTxConfirmation.ts +155 -0
- package/src/internal/waitForSolanaTxConfirmation.ts +135 -0
- package/src/internal/yieldEvmNetwork.ts +57 -0
- package/src/mpc/index.ts +142 -1
- package/src/provider/index.ts +25 -0
- package/src/shared/trace/index.ts +0 -1
- package/src/shared/types/README.md +6 -0
- package/src/shared/types/api.ts +12 -1
- package/src/shared/types/common.ts +332 -20
- package/src/shared/types/delegations.ts +10 -0
- package/src/shared/types/index.ts +1 -0
- package/src/shared/types/lifi.ts +82 -0
- package/src/shared/types/noah.ts +124 -33
- package/src/shared/types/yieldxyz.ts +186 -0
- package/src/shared/types/zero-x.ts +66 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
+
})
|