@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
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import LiFi, { normalizeChainToNetwork } from './index'
|
|
6
|
+
import Mpc from '../../../mpc'
|
|
7
|
+
import portalMock from '../../../__mocks/portal/portal'
|
|
8
|
+
import {
|
|
9
|
+
mockEip155Address,
|
|
10
|
+
mockHost,
|
|
11
|
+
mockLifiAction,
|
|
12
|
+
mockLifiGetRouteStepResponse,
|
|
13
|
+
mockLifiGetRoutesResponse,
|
|
14
|
+
mockLifiGetStatusResponse,
|
|
15
|
+
mockLifiRoute,
|
|
16
|
+
mockLifiStep,
|
|
17
|
+
} from '../../../__mocks/constants'
|
|
18
|
+
|
|
19
|
+
describe('normalizeChainToNetwork', () => {
|
|
20
|
+
it('maps hex chain ids to eip155', () => {
|
|
21
|
+
expect(normalizeChainToNetwork('0x1')).toBe('eip155:1')
|
|
22
|
+
expect(normalizeChainToNetwork('0xa')).toBe('eip155:10')
|
|
23
|
+
expect(normalizeChainToNetwork('0x89')).toBe('eip155:137')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('preserves existing eip155 and numeric decimal strings', () => {
|
|
27
|
+
expect(normalizeChainToNetwork('eip155:42161')).toBe('eip155:42161')
|
|
28
|
+
expect(normalizeChainToNetwork('42161')).toBe('eip155:42161')
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('LiFi tradeAsset', () => {
|
|
33
|
+
let mpc: Mpc
|
|
34
|
+
let lifi: LiFi
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
jest.clearAllMocks()
|
|
38
|
+
portalMock.host = mockHost
|
|
39
|
+
mpc = new Mpc({ portal: portalMock })
|
|
40
|
+
lifi = new LiFi({
|
|
41
|
+
mpc,
|
|
42
|
+
signAndSendTransaction: async () => '0xsent',
|
|
43
|
+
waitForConfirmation: async () => false,
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('throws when waitForConfirmation returns false and does not poll bridge status', async () => {
|
|
48
|
+
jest
|
|
49
|
+
.spyOn(mpc, 'getLifiRoutes')
|
|
50
|
+
.mockResolvedValue(mockLifiGetRoutesResponse as never)
|
|
51
|
+
jest
|
|
52
|
+
.spyOn(mpc, 'getLifiRouteStep')
|
|
53
|
+
.mockResolvedValue(mockLifiGetRouteStepResponse as never)
|
|
54
|
+
const pollSpy = jest
|
|
55
|
+
.spyOn(lifi, 'pollStatus')
|
|
56
|
+
.mockRejectedValue(new Error('pollStatus should not run'))
|
|
57
|
+
|
|
58
|
+
await expect(
|
|
59
|
+
lifi.tradeAsset({
|
|
60
|
+
fromChain: 'eip155:8453',
|
|
61
|
+
toChain: 'eip155:42161',
|
|
62
|
+
fromToken: 'ETH',
|
|
63
|
+
toToken: 'USDC',
|
|
64
|
+
amount: '1000000000000000',
|
|
65
|
+
fromAddress: mockEip155Address,
|
|
66
|
+
}),
|
|
67
|
+
).rejects.toThrow(/on-chain confirmation did not complete/)
|
|
68
|
+
|
|
69
|
+
expect(pollSpy).not.toHaveBeenCalled()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('strips nonce from EVM transaction request before sign', async () => {
|
|
73
|
+
const swapStep = {
|
|
74
|
+
...mockLifiStep,
|
|
75
|
+
type: 'swap' as const,
|
|
76
|
+
action: {
|
|
77
|
+
...mockLifiAction,
|
|
78
|
+
fromChainId: 'eip155:1',
|
|
79
|
+
toChainId: 'eip155:1',
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
const route = {
|
|
83
|
+
...mockLifiRoute,
|
|
84
|
+
fromChainId: 'eip155:1',
|
|
85
|
+
toChainId: 'eip155:1',
|
|
86
|
+
steps: [swapStep],
|
|
87
|
+
}
|
|
88
|
+
const routesRes = {
|
|
89
|
+
data: {
|
|
90
|
+
rawResponse: {
|
|
91
|
+
routes: [route],
|
|
92
|
+
unavailableRoutes: { filteredOut: [], failed: [] },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
const stepRes = {
|
|
97
|
+
data: {
|
|
98
|
+
rawResponse: {
|
|
99
|
+
...swapStep,
|
|
100
|
+
transactionRequest: {
|
|
101
|
+
...swapStep.transactionRequest,
|
|
102
|
+
nonce: '0xdeadbeef',
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
jest.spyOn(mpc, 'getLifiRoutes').mockResolvedValue(routesRes as never)
|
|
109
|
+
jest.spyOn(mpc, 'getLifiRouteStep').mockResolvedValue(stepRes as never)
|
|
110
|
+
|
|
111
|
+
const signer = jest.fn().mockResolvedValue('0xsigned')
|
|
112
|
+
const waiter = jest.fn().mockResolvedValue(true)
|
|
113
|
+
const lifiOk = new LiFi({
|
|
114
|
+
mpc,
|
|
115
|
+
signAndSendTransaction: signer,
|
|
116
|
+
waitForConfirmation: waiter,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
await lifiOk.tradeAsset({
|
|
120
|
+
fromChain: 'eip155:1',
|
|
121
|
+
toChain: 'eip155:1',
|
|
122
|
+
fromToken: 'ETH',
|
|
123
|
+
toToken: 'USDC',
|
|
124
|
+
amount: '1000000000000000',
|
|
125
|
+
fromAddress: mockEip155Address,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
expect(signer).toHaveBeenCalledTimes(1)
|
|
129
|
+
const sent = signer.mock.calls[0]![0] as Record<string, unknown>
|
|
130
|
+
expect(sent.nonce).toBeUndefined()
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('emits onProgress failed on unexpected errors (outer catch)', async () => {
|
|
134
|
+
const onProgress = jest.fn()
|
|
135
|
+
jest.spyOn(mpc, 'getLifiRoutes').mockRejectedValue(new Error('network down'))
|
|
136
|
+
|
|
137
|
+
await expect(
|
|
138
|
+
lifi.tradeAsset(
|
|
139
|
+
{
|
|
140
|
+
fromChain: 'eip155:8453',
|
|
141
|
+
toChain: 'eip155:42161',
|
|
142
|
+
fromToken: 'ETH',
|
|
143
|
+
toToken: 'USDC',
|
|
144
|
+
amount: '1000000000000000',
|
|
145
|
+
fromAddress: mockEip155Address,
|
|
146
|
+
onProgress,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
signAndSendTransaction: async () => '0x',
|
|
150
|
+
waitForConfirmation: async () => true,
|
|
151
|
+
},
|
|
152
|
+
),
|
|
153
|
+
).rejects.toThrow('network down')
|
|
154
|
+
|
|
155
|
+
expect(onProgress).toHaveBeenCalledWith(
|
|
156
|
+
'failed',
|
|
157
|
+
expect.objectContaining({ errorMessage: 'network down' }),
|
|
158
|
+
)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('throws when routeIndex is out of bounds', async () => {
|
|
162
|
+
jest
|
|
163
|
+
.spyOn(mpc, 'getLifiRoutes')
|
|
164
|
+
.mockResolvedValue(mockLifiGetRoutesResponse as never)
|
|
165
|
+
const lifiOk = new LiFi({
|
|
166
|
+
mpc,
|
|
167
|
+
signAndSendTransaction: async () => '0x',
|
|
168
|
+
waitForConfirmation: async () => true,
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
await expect(
|
|
172
|
+
lifiOk.tradeAsset({
|
|
173
|
+
fromChain: 'eip155:8453',
|
|
174
|
+
toChain: 'eip155:42161',
|
|
175
|
+
fromToken: 'ETH',
|
|
176
|
+
toToken: 'USDC',
|
|
177
|
+
amount: '1000000000000000',
|
|
178
|
+
fromAddress: mockEip155Address,
|
|
179
|
+
routeIndex: 99,
|
|
180
|
+
}),
|
|
181
|
+
).rejects.toThrow(/no route available at index 99/)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('throws when getRouteStep returns error', async () => {
|
|
185
|
+
jest
|
|
186
|
+
.spyOn(mpc, 'getLifiRoutes')
|
|
187
|
+
.mockResolvedValue(mockLifiGetRoutesResponse as never)
|
|
188
|
+
jest.spyOn(mpc, 'getLifiRouteStep').mockResolvedValue({
|
|
189
|
+
error: 'step build failed',
|
|
190
|
+
} as never)
|
|
191
|
+
const lifiOk = new LiFi({
|
|
192
|
+
mpc,
|
|
193
|
+
signAndSendTransaction: async () => '0x',
|
|
194
|
+
waitForConfirmation: async () => true,
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
await expect(
|
|
198
|
+
lifiOk.tradeAsset({
|
|
199
|
+
fromChain: 'eip155:8453',
|
|
200
|
+
toChain: 'eip155:42161',
|
|
201
|
+
fromToken: 'ETH',
|
|
202
|
+
toToken: 'USDC',
|
|
203
|
+
amount: '1000000000000000',
|
|
204
|
+
fromAddress: mockEip155Address,
|
|
205
|
+
}),
|
|
206
|
+
).rejects.toThrow('getRouteStep: step build failed')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('cross-chain step calls pollStatus with fast statusPoll options', async () => {
|
|
210
|
+
jest
|
|
211
|
+
.spyOn(mpc, 'getLifiRoutes')
|
|
212
|
+
.mockResolvedValue(mockLifiGetRoutesResponse as never)
|
|
213
|
+
jest
|
|
214
|
+
.spyOn(mpc, 'getLifiRouteStep')
|
|
215
|
+
.mockResolvedValue(mockLifiGetRouteStepResponse as never)
|
|
216
|
+
jest
|
|
217
|
+
.spyOn(mpc, 'getLifiStatus')
|
|
218
|
+
.mockResolvedValue(mockLifiGetStatusResponse as never)
|
|
219
|
+
|
|
220
|
+
const pollSpy = jest.spyOn(LiFi.prototype, 'pollStatus')
|
|
221
|
+
const signer = jest.fn().mockResolvedValue('0xstep1')
|
|
222
|
+
const lifiOk = new LiFi({
|
|
223
|
+
mpc,
|
|
224
|
+
signAndSendTransaction: signer,
|
|
225
|
+
waitForConfirmation: async () => true,
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
await lifiOk.tradeAsset({
|
|
229
|
+
fromChain: 'eip155:8453',
|
|
230
|
+
toChain: 'eip155:42161',
|
|
231
|
+
fromToken: 'ETH',
|
|
232
|
+
toToken: 'USDC',
|
|
233
|
+
amount: '1000000000000000',
|
|
234
|
+
fromAddress: mockEip155Address,
|
|
235
|
+
statusPoll: {
|
|
236
|
+
initialDelayMs: 0,
|
|
237
|
+
everyMs: 5,
|
|
238
|
+
timeoutMs: 20000,
|
|
239
|
+
},
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
expect(signer).toHaveBeenCalled()
|
|
243
|
+
expect(pollSpy).toHaveBeenCalled()
|
|
244
|
+
pollSpy.mockRestore()
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('same-chain swap does not call pollStatus', async () => {
|
|
248
|
+
const swapStep = {
|
|
249
|
+
...mockLifiStep,
|
|
250
|
+
type: 'swap' as const,
|
|
251
|
+
action: {
|
|
252
|
+
...mockLifiAction,
|
|
253
|
+
fromChainId: 'eip155:1',
|
|
254
|
+
toChainId: 'eip155:1',
|
|
255
|
+
},
|
|
256
|
+
}
|
|
257
|
+
const route = {
|
|
258
|
+
...mockLifiRoute,
|
|
259
|
+
fromChainId: 'eip155:1',
|
|
260
|
+
toChainId: 'eip155:1',
|
|
261
|
+
steps: [swapStep],
|
|
262
|
+
}
|
|
263
|
+
const routesRes = {
|
|
264
|
+
data: {
|
|
265
|
+
rawResponse: {
|
|
266
|
+
routes: [route],
|
|
267
|
+
unavailableRoutes: { filteredOut: [], failed: [] },
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
}
|
|
271
|
+
const stepRes = {
|
|
272
|
+
data: {
|
|
273
|
+
rawResponse: {
|
|
274
|
+
...swapStep,
|
|
275
|
+
transactionRequest: { ...swapStep.transactionRequest, chainId: 'eip155:1' },
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
jest.spyOn(mpc, 'getLifiRoutes').mockResolvedValue(routesRes as never)
|
|
280
|
+
jest.spyOn(mpc, 'getLifiRouteStep').mockResolvedValue(stepRes as never)
|
|
281
|
+
|
|
282
|
+
const pollSpy = jest.spyOn(LiFi.prototype, 'pollStatus')
|
|
283
|
+
const lifiOk = new LiFi({
|
|
284
|
+
mpc,
|
|
285
|
+
signAndSendTransaction: async () => '0xswap',
|
|
286
|
+
waitForConfirmation: async () => true,
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
await lifiOk.tradeAsset({
|
|
290
|
+
fromChain: 'eip155:1',
|
|
291
|
+
toChain: 'eip155:1',
|
|
292
|
+
fromToken: 'ETH',
|
|
293
|
+
toToken: 'USDC',
|
|
294
|
+
amount: '1000000000000000',
|
|
295
|
+
fromAddress: mockEip155Address,
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
expect(pollSpy).not.toHaveBeenCalled()
|
|
299
|
+
pollSpy.mockRestore()
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('throws when signer returns empty hash', async () => {
|
|
303
|
+
jest
|
|
304
|
+
.spyOn(mpc, 'getLifiRoutes')
|
|
305
|
+
.mockResolvedValue(mockLifiGetRoutesResponse as never)
|
|
306
|
+
jest
|
|
307
|
+
.spyOn(mpc, 'getLifiRouteStep')
|
|
308
|
+
.mockResolvedValue(mockLifiGetRouteStepResponse as never)
|
|
309
|
+
const onProgress = jest.fn()
|
|
310
|
+
const lifiOk = new LiFi({
|
|
311
|
+
mpc,
|
|
312
|
+
signAndSendTransaction: async () => ' ',
|
|
313
|
+
waitForConfirmation: async () => true,
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
await expect(
|
|
317
|
+
lifiOk.tradeAsset({
|
|
318
|
+
fromChain: 'eip155:8453',
|
|
319
|
+
toChain: 'eip155:42161',
|
|
320
|
+
fromToken: 'ETH',
|
|
321
|
+
toToken: 'USDC',
|
|
322
|
+
amount: '1000000000000000',
|
|
323
|
+
fromAddress: mockEip155Address,
|
|
324
|
+
onProgress,
|
|
325
|
+
}),
|
|
326
|
+
).rejects.toThrow(/Invalid transaction hash/)
|
|
327
|
+
|
|
328
|
+
expect(onProgress).toHaveBeenCalledWith(
|
|
329
|
+
'failed',
|
|
330
|
+
expect.objectContaining({ errorMessage: expect.stringContaining('Invalid') }),
|
|
331
|
+
)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('emits failed only ONCE when signer returns empty hash (no duplicate)', async () => {
|
|
335
|
+
jest
|
|
336
|
+
.spyOn(mpc, 'getLifiRoutes')
|
|
337
|
+
.mockResolvedValue(mockLifiGetRoutesResponse as never)
|
|
338
|
+
jest
|
|
339
|
+
.spyOn(mpc, 'getLifiRouteStep')
|
|
340
|
+
.mockResolvedValue(mockLifiGetRouteStepResponse as never)
|
|
341
|
+
const onProgress = jest.fn()
|
|
342
|
+
const lifiOk = new LiFi({
|
|
343
|
+
mpc,
|
|
344
|
+
signAndSendTransaction: async () => '',
|
|
345
|
+
waitForConfirmation: async () => true,
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
await expect(
|
|
349
|
+
lifiOk.tradeAsset({
|
|
350
|
+
fromChain: 'eip155:8453',
|
|
351
|
+
toChain: 'eip155:42161',
|
|
352
|
+
fromToken: 'ETH',
|
|
353
|
+
toToken: 'USDC',
|
|
354
|
+
amount: '1000000000000000',
|
|
355
|
+
fromAddress: mockEip155Address,
|
|
356
|
+
onProgress,
|
|
357
|
+
}),
|
|
358
|
+
).rejects.toThrow(/Invalid transaction hash/)
|
|
359
|
+
|
|
360
|
+
const failedCalls = onProgress.mock.calls.filter(
|
|
361
|
+
(call) => call[0] === 'failed',
|
|
362
|
+
)
|
|
363
|
+
expect(failedCalls).toHaveLength(1)
|
|
364
|
+
expect(failedCalls[0]![1]).toMatchObject({
|
|
365
|
+
errorMessage: expect.stringContaining('Invalid transaction hash'),
|
|
366
|
+
stepIndex: 0,
|
|
367
|
+
totalSteps: 1,
|
|
368
|
+
})
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
it('emits failed only ONCE when waitForConfirmation returns false (no duplicate)', async () => {
|
|
372
|
+
jest
|
|
373
|
+
.spyOn(mpc, 'getLifiRoutes')
|
|
374
|
+
.mockResolvedValue(mockLifiGetRoutesResponse as never)
|
|
375
|
+
jest
|
|
376
|
+
.spyOn(mpc, 'getLifiRouteStep')
|
|
377
|
+
.mockResolvedValue(mockLifiGetRouteStepResponse as never)
|
|
378
|
+
const onProgress = jest.fn()
|
|
379
|
+
const lifiOk = new LiFi({
|
|
380
|
+
mpc,
|
|
381
|
+
signAndSendTransaction: async () => '0xhash',
|
|
382
|
+
waitForConfirmation: async () => false,
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
await expect(
|
|
386
|
+
lifiOk.tradeAsset({
|
|
387
|
+
fromChain: 'eip155:8453',
|
|
388
|
+
toChain: 'eip155:42161',
|
|
389
|
+
fromToken: 'ETH',
|
|
390
|
+
toToken: 'USDC',
|
|
391
|
+
amount: '1000000000000000',
|
|
392
|
+
fromAddress: mockEip155Address,
|
|
393
|
+
onProgress,
|
|
394
|
+
}),
|
|
395
|
+
).rejects.toThrow(/on-chain confirmation did not complete/)
|
|
396
|
+
|
|
397
|
+
const failedCalls = onProgress.mock.calls.filter(
|
|
398
|
+
(call) => call[0] === 'failed',
|
|
399
|
+
)
|
|
400
|
+
expect(failedCalls).toHaveLength(1)
|
|
401
|
+
expect(failedCalls[0]![1]).toMatchObject({
|
|
402
|
+
errorMessage: expect.stringContaining('on-chain confirmation'),
|
|
403
|
+
txHash: '0xhash',
|
|
404
|
+
stepIndex: 0,
|
|
405
|
+
totalSteps: 1,
|
|
406
|
+
})
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it('throws when waitForConfirmation throws', async () => {
|
|
410
|
+
jest
|
|
411
|
+
.spyOn(mpc, 'getLifiRoutes')
|
|
412
|
+
.mockResolvedValue(mockLifiGetRoutesResponse as never)
|
|
413
|
+
jest
|
|
414
|
+
.spyOn(mpc, 'getLifiRouteStep')
|
|
415
|
+
.mockResolvedValue(mockLifiGetRouteStepResponse as never)
|
|
416
|
+
|
|
417
|
+
const lifiOk = new LiFi({
|
|
418
|
+
mpc,
|
|
419
|
+
signAndSendTransaction: async () => '0xabc',
|
|
420
|
+
waitForConfirmation: async () => {
|
|
421
|
+
throw new Error('waiter boom')
|
|
422
|
+
},
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
await expect(
|
|
426
|
+
lifiOk.tradeAsset({
|
|
427
|
+
fromChain: 'eip155:8453',
|
|
428
|
+
toChain: 'eip155:42161',
|
|
429
|
+
fromToken: 'ETH',
|
|
430
|
+
toToken: 'USDC',
|
|
431
|
+
amount: '1000000000000000',
|
|
432
|
+
fromAddress: mockEip155Address,
|
|
433
|
+
}),
|
|
434
|
+
).rejects.toThrow('waiter boom')
|
|
435
|
+
})
|
|
436
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
pollLifiStatusUntilTerminal,
|
|
7
|
+
isLikelyLifiIndexerNotReadyMessage,
|
|
8
|
+
} from './lifiStatusPoll'
|
|
9
|
+
import type { LifiStatusResponse } from '../../../shared/types'
|
|
10
|
+
|
|
11
|
+
describe('lifiStatusPoll', () => {
|
|
12
|
+
describe('isLikelyLifiIndexerNotReadyMessage', () => {
|
|
13
|
+
it('returns true for known indexer lag phrases', () => {
|
|
14
|
+
expect(
|
|
15
|
+
isLikelyLifiIndexerNotReadyMessage('No transfer information found'),
|
|
16
|
+
).toBe(true)
|
|
17
|
+
expect(isLikelyLifiIndexerNotReadyMessage('transfer not found')).toBe(
|
|
18
|
+
true,
|
|
19
|
+
)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('returns false for unrelated errors', () => {
|
|
23
|
+
expect(isLikelyLifiIndexerNotReadyMessage('Network error 400')).toBe(
|
|
24
|
+
false,
|
|
25
|
+
)
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('pollLifiStatusUntilTerminal', () => {
|
|
30
|
+
it('treats response error field as hard error without resetting counter incorrectly', async () => {
|
|
31
|
+
const getStatus = jest
|
|
32
|
+
.fn()
|
|
33
|
+
.mockResolvedValueOnce({
|
|
34
|
+
error: 'temporary',
|
|
35
|
+
data: undefined,
|
|
36
|
+
} as LifiStatusResponse)
|
|
37
|
+
.mockResolvedValueOnce({
|
|
38
|
+
data: { rawResponse: { status: 'DONE' as const } },
|
|
39
|
+
} as LifiStatusResponse)
|
|
40
|
+
|
|
41
|
+
const result = await pollLifiStatusUntilTerminal({
|
|
42
|
+
getStatus,
|
|
43
|
+
request: { txHash: '0x1' },
|
|
44
|
+
everyMs: 5,
|
|
45
|
+
initialDelayMs: 0,
|
|
46
|
+
timeoutMs: 5000,
|
|
47
|
+
maxConsecutiveErrors: 10,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
expect(result.status).toBe('DONE')
|
|
51
|
+
expect(getStatus).toHaveBeenCalledTimes(2)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('retries on indexer lag message without counting as hard error', async () => {
|
|
55
|
+
const getStatus = jest
|
|
56
|
+
.fn()
|
|
57
|
+
.mockRejectedValueOnce(new Error('transfer not found'))
|
|
58
|
+
.mockResolvedValue({
|
|
59
|
+
data: { rawResponse: { status: 'DONE' as const } },
|
|
60
|
+
} as LifiStatusResponse)
|
|
61
|
+
|
|
62
|
+
const result = await pollLifiStatusUntilTerminal({
|
|
63
|
+
getStatus,
|
|
64
|
+
request: { txHash: '0x1' },
|
|
65
|
+
everyMs: 5,
|
|
66
|
+
initialDelayMs: 0,
|
|
67
|
+
timeoutMs: 5000,
|
|
68
|
+
maxConsecutiveErrors: 2,
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
expect(result.status).toBe('DONE')
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
})
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LiFi status polling: domain tick (DONE / FAILED / indexer lag) over shared {@link pollLoop}.
|
|
3
|
+
*/
|
|
4
|
+
import { sdkLogger } from '../../../logger'
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
LifiStatusRawResponse,
|
|
8
|
+
LifiStatusRequest,
|
|
9
|
+
LifiStatusResponse,
|
|
10
|
+
} from '../../../shared/types'
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
pollLoop,
|
|
14
|
+
PollLoopTimeoutError,
|
|
15
|
+
type PollTickResult,
|
|
16
|
+
} from '../../../internal/pollLoop'
|
|
17
|
+
|
|
18
|
+
const LOG_PREFIX = '[LiFi.pollStatus]'
|
|
19
|
+
|
|
20
|
+
const INDEXER_LAG_MESSAGE_SNIPPETS = [
|
|
21
|
+
'not an evm transaction',
|
|
22
|
+
'no transfer information found',
|
|
23
|
+
'transfer not found',
|
|
24
|
+
] as const
|
|
25
|
+
|
|
26
|
+
export function isLikelyLifiIndexerNotReadyMessage(message: string): boolean {
|
|
27
|
+
const m = message.toLowerCase()
|
|
28
|
+
return INDEXER_LAG_MESSAGE_SNIPPETS.some((s) => m.includes(s))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type LifiGetStatusFn = (
|
|
32
|
+
request: LifiStatusRequest,
|
|
33
|
+
) => Promise<LifiStatusResponse>
|
|
34
|
+
|
|
35
|
+
export interface PollLifiUntilTerminalParams {
|
|
36
|
+
getStatus: LifiGetStatusFn
|
|
37
|
+
request: Pick<LifiStatusRequest, 'txHash' | 'fromChain' | 'toChain' | 'bridge'>
|
|
38
|
+
onUpdate?: (raw: LifiStatusRawResponse) => boolean | void
|
|
39
|
+
everyMs: number
|
|
40
|
+
initialDelayMs: number
|
|
41
|
+
timeoutMs: number
|
|
42
|
+
maxConsecutiveErrors: number
|
|
43
|
+
backoff?: { factor: number; maxIntervalMs: number }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function pollLifiStatusUntilTerminal(
|
|
47
|
+
params: PollLifiUntilTerminalParams,
|
|
48
|
+
): Promise<LifiStatusRawResponse> {
|
|
49
|
+
let consecutiveHardErrors = 0
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
return await pollLoop<LifiStatusRawResponse>({
|
|
53
|
+
tick: async (): Promise<PollTickResult<LifiStatusRawResponse>> => {
|
|
54
|
+
return lifiStatusPollTick(params, {
|
|
55
|
+
get: () => consecutiveHardErrors,
|
|
56
|
+
set: (n: number) => {
|
|
57
|
+
consecutiveHardErrors = n
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
},
|
|
61
|
+
intervalMs: params.everyMs,
|
|
62
|
+
initialDelayMs: params.initialDelayMs,
|
|
63
|
+
backoff: params.backoff,
|
|
64
|
+
timeoutMs: params.timeoutMs,
|
|
65
|
+
})
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error instanceof PollLoopTimeoutError) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`${LOG_PREFIX} timed out after ${params.timeoutMs}ms waiting for LiFi terminal status`,
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
throw error
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function lifiStatusPollTick(
|
|
77
|
+
params: PollLifiUntilTerminalParams,
|
|
78
|
+
counter: { get: () => number; set: (n: number) => void },
|
|
79
|
+
): Promise<PollTickResult<LifiStatusRawResponse>> {
|
|
80
|
+
try {
|
|
81
|
+
const res: LifiStatusResponse = await params.getStatus(
|
|
82
|
+
params.request as LifiStatusRequest,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if (res.error) {
|
|
86
|
+
sdkLogger.warn(`${LOG_PREFIX} response error field`, res.error)
|
|
87
|
+
return bumpHardError(counter, params.maxConsecutiveErrors)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
counter.set(0)
|
|
91
|
+
|
|
92
|
+
const raw = res.data?.rawResponse
|
|
93
|
+
if (!raw) {
|
|
94
|
+
return { kind: 'continue' }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const { status } = raw
|
|
98
|
+
|
|
99
|
+
if (status === 'INVALID') {
|
|
100
|
+
return {
|
|
101
|
+
kind: 'throw',
|
|
102
|
+
error: new Error(
|
|
103
|
+
`${LOG_PREFIX} LiFi status INVALID for ${params.request.txHash}`,
|
|
104
|
+
),
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (status === 'DONE') {
|
|
109
|
+
return { kind: 'stop', value: raw }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (status === 'FAILED') {
|
|
113
|
+
const detail =
|
|
114
|
+
raw.substatusMessage ?? raw.substatus ?? 'LiFi transfer FAILED'
|
|
115
|
+
return {
|
|
116
|
+
kind: 'throw',
|
|
117
|
+
error: new Error(`${LOG_PREFIX} ${detail}`),
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (params.onUpdate) {
|
|
122
|
+
const carryOn = params.onUpdate(raw)
|
|
123
|
+
if (carryOn === false) {
|
|
124
|
+
return { kind: 'stop', value: raw }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { kind: 'continue' }
|
|
129
|
+
} catch (error) {
|
|
130
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
131
|
+
|
|
132
|
+
if (isLikelyLifiIndexerNotReadyMessage(message)) {
|
|
133
|
+
sdkLogger.debug(`${LOG_PREFIX} indexer not ready, will retry`, message)
|
|
134
|
+
counter.set(0)
|
|
135
|
+
return { kind: 'continue' }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
sdkLogger.warn(`${LOG_PREFIX} getStatus error`, error)
|
|
139
|
+
return bumpHardError(counter, params.maxConsecutiveErrors)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function bumpHardError(
|
|
144
|
+
counter: { get: () => number; set: (n: number) => void },
|
|
145
|
+
max: number,
|
|
146
|
+
): PollTickResult<LifiStatusRawResponse> {
|
|
147
|
+
const next = counter.get() + 1
|
|
148
|
+
counter.set(next)
|
|
149
|
+
if (next >= max) {
|
|
150
|
+
return {
|
|
151
|
+
kind: 'throw',
|
|
152
|
+
error: new Error(
|
|
153
|
+
`${LOG_PREFIX} too many consecutive getStatus failures (${max})`,
|
|
154
|
+
),
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { kind: 'continue' }
|
|
158
|
+
}
|