@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
|
@@ -119,4 +119,255 @@ describe('Delegations', () => {
|
|
|
119
119
|
).rejects.toThrow('Test error')
|
|
120
120
|
})
|
|
121
121
|
})
|
|
122
|
+
|
|
123
|
+
describe('approveAndSubmit, revokeAndSubmit, transferAndSubmit', () => {
|
|
124
|
+
const mockSignAndSend = jest.fn()
|
|
125
|
+
|
|
126
|
+
beforeEach(() => {
|
|
127
|
+
mockSignAndSend.mockResolvedValue('0xtxhash')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('throws when signAndSendTransaction is not configured', async () => {
|
|
131
|
+
const d = new Delegations({ mpc })
|
|
132
|
+
|
|
133
|
+
await expect(d.approveAndSubmit(mockEVMApproveRequest)).rejects.toThrow(
|
|
134
|
+
'[Delegations] No signer configured. Call setSignAndSendTransaction() on the instance or pass signAndSendTransaction in options.',
|
|
135
|
+
)
|
|
136
|
+
await expect(d.revokeAndSubmit(mockEVMRevokeRequest)).rejects.toThrow(
|
|
137
|
+
'[Delegations] No signer configured. Call setSignAndSendTransaction() on the instance or pass signAndSendTransaction in options.',
|
|
138
|
+
)
|
|
139
|
+
await expect(d.transferAndSubmit(mockTransferFromRequest)).rejects.toThrow(
|
|
140
|
+
'[Delegations] No signer configured. Call setSignAndSendTransaction() on the instance or pass signAndSendTransaction in options.',
|
|
141
|
+
)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('approveAndSubmit calls MPC then signAndSend per transaction', async () => {
|
|
145
|
+
jest.spyOn(mpc, 'delegationsApprove').mockResolvedValue(mockEVMApproveResponse)
|
|
146
|
+
const d = new Delegations({
|
|
147
|
+
mpc,
|
|
148
|
+
signAndSendTransaction: mockSignAndSend,
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
const result = await d.approveAndSubmit(mockEVMApproveRequest)
|
|
152
|
+
|
|
153
|
+
expect(mpc.delegationsApprove).toHaveBeenCalledWith(mockEVMApproveRequest)
|
|
154
|
+
expect(mockSignAndSend).toHaveBeenCalledTimes(1)
|
|
155
|
+
expect(mockSignAndSend).toHaveBeenCalledWith(
|
|
156
|
+
mockEVMApproveResponse.transactions![0],
|
|
157
|
+
mockEVMApproveRequest.chain,
|
|
158
|
+
)
|
|
159
|
+
expect(result).toEqual({ hashes: ['0xtxhash'] })
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('revokeAndSubmit calls delegationsRevoke then signAndSend per transaction', async () => {
|
|
163
|
+
jest.spyOn(mpc, 'delegationsRevoke').mockResolvedValue(mockEVMRevokeResponse)
|
|
164
|
+
const d = new Delegations({
|
|
165
|
+
mpc,
|
|
166
|
+
signAndSendTransaction: mockSignAndSend,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const result = await d.revokeAndSubmit(mockEVMRevokeRequest)
|
|
170
|
+
|
|
171
|
+
expect(mpc.delegationsRevoke).toHaveBeenCalledWith(mockEVMRevokeRequest)
|
|
172
|
+
expect(mockSignAndSend).toHaveBeenCalledTimes(1)
|
|
173
|
+
expect(mockSignAndSend).toHaveBeenCalledWith(
|
|
174
|
+
mockEVMRevokeResponse.transactions![0],
|
|
175
|
+
mockEVMRevokeRequest.chain,
|
|
176
|
+
)
|
|
177
|
+
expect(result).toEqual({ hashes: ['0xtxhash'] })
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('transferAndSubmit calls delegationsTransferFrom then signAndSend per transaction', async () => {
|
|
181
|
+
jest
|
|
182
|
+
.spyOn(mpc, 'delegationsTransferFrom')
|
|
183
|
+
.mockResolvedValue(mockTransferFromResponse)
|
|
184
|
+
const d = new Delegations({
|
|
185
|
+
mpc,
|
|
186
|
+
signAndSendTransaction: mockSignAndSend,
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const result = await d.transferAndSubmit(mockTransferFromRequest)
|
|
190
|
+
|
|
191
|
+
expect(mpc.delegationsTransferFrom).toHaveBeenCalledWith(
|
|
192
|
+
mockTransferFromRequest,
|
|
193
|
+
)
|
|
194
|
+
expect(mockSignAndSend).toHaveBeenCalledTimes(1)
|
|
195
|
+
expect(mockSignAndSend).toHaveBeenCalledWith(
|
|
196
|
+
mockTransferFromResponse.transactions![0],
|
|
197
|
+
mockTransferFromRequest.chain,
|
|
198
|
+
)
|
|
199
|
+
expect(result).toEqual({ hashes: ['0xtxhash'] })
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('uses encodedTransactions when transactions is empty', async () => {
|
|
203
|
+
const response = {
|
|
204
|
+
transactions: [],
|
|
205
|
+
encodedTransactions: ['solTxPayload'],
|
|
206
|
+
metadata: mockEVMApproveResponse.metadata,
|
|
207
|
+
}
|
|
208
|
+
jest.spyOn(mpc, 'delegationsApprove').mockResolvedValue(response)
|
|
209
|
+
const d = new Delegations({
|
|
210
|
+
mpc,
|
|
211
|
+
signAndSendTransaction: mockSignAndSend,
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
await d.approveAndSubmit(mockEVMApproveRequest)
|
|
215
|
+
|
|
216
|
+
expect(mockSignAndSend).toHaveBeenCalledWith(
|
|
217
|
+
'solTxPayload',
|
|
218
|
+
mockEVMApproveRequest.chain,
|
|
219
|
+
)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('invokes onProgress for each transaction', async () => {
|
|
223
|
+
jest.spyOn(mpc, 'delegationsApprove').mockResolvedValue(mockEVMApproveResponse)
|
|
224
|
+
const onProgress = jest.fn()
|
|
225
|
+
const d = new Delegations({
|
|
226
|
+
mpc,
|
|
227
|
+
signAndSendTransaction: mockSignAndSend,
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
await d.approveAndSubmit(mockEVMApproveRequest, { onProgress })
|
|
231
|
+
|
|
232
|
+
expect(onProgress).toHaveBeenCalledTimes(2)
|
|
233
|
+
expect(onProgress).toHaveBeenNthCalledWith(1, {
|
|
234
|
+
step: 'signing',
|
|
235
|
+
index: 0,
|
|
236
|
+
total: 1,
|
|
237
|
+
})
|
|
238
|
+
expect(onProgress).toHaveBeenNthCalledWith(2, {
|
|
239
|
+
step: 'submitted',
|
|
240
|
+
index: 0,
|
|
241
|
+
total: 1,
|
|
242
|
+
hash: '0xtxhash',
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('throws when response has no transactions', async () => {
|
|
247
|
+
jest.spyOn(mpc, 'delegationsApprove').mockResolvedValue({
|
|
248
|
+
metadata: mockEVMApproveResponse.metadata,
|
|
249
|
+
})
|
|
250
|
+
const d = new Delegations({
|
|
251
|
+
mpc,
|
|
252
|
+
signAndSendTransaction: mockSignAndSend,
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
await expect(d.approveAndSubmit(mockEVMApproveRequest)).rejects.toThrow(
|
|
256
|
+
'No transactions in delegation response.',
|
|
257
|
+
)
|
|
258
|
+
expect(mockSignAndSend).not.toHaveBeenCalled()
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('submits multiple EVM transactions in order', async () => {
|
|
262
|
+
const txA = { ...(
|
|
263
|
+
mockEVMApproveResponse.transactions as NonNullable<
|
|
264
|
+
typeof mockEVMApproveResponse.transactions
|
|
265
|
+
>
|
|
266
|
+
)[0]! }
|
|
267
|
+
const txB = { ...txA, data: '0xbbbb' }
|
|
268
|
+
jest.spyOn(mpc, 'delegationsApprove').mockResolvedValue({
|
|
269
|
+
...mockEVMApproveResponse,
|
|
270
|
+
transactions: [txA, txB],
|
|
271
|
+
})
|
|
272
|
+
const d = new Delegations({
|
|
273
|
+
mpc,
|
|
274
|
+
signAndSendTransaction: mockSignAndSend,
|
|
275
|
+
})
|
|
276
|
+
mockSignAndSend.mockResolvedValueOnce('0xh1').mockResolvedValueOnce('0xh2')
|
|
277
|
+
|
|
278
|
+
const result = await d.approveAndSubmit(mockEVMApproveRequest)
|
|
279
|
+
|
|
280
|
+
expect(mockSignAndSend).toHaveBeenCalledTimes(2)
|
|
281
|
+
expect(mockSignAndSend).toHaveBeenNthCalledWith(1, txA, mockEVMApproveRequest.chain)
|
|
282
|
+
expect(mockSignAndSend).toHaveBeenNthCalledWith(2, txB, mockEVMApproveRequest.chain)
|
|
283
|
+
expect(result.hashes).toEqual(['0xh1', '0xh2'])
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('propagates error after first tx succeeds (partial submit)', async () => {
|
|
287
|
+
const txA = { ...(
|
|
288
|
+
mockEVMApproveResponse.transactions as NonNullable<
|
|
289
|
+
typeof mockEVMApproveResponse.transactions
|
|
290
|
+
>
|
|
291
|
+
)[0]! }
|
|
292
|
+
const txB = { ...txA, data: '0xbbbb' }
|
|
293
|
+
jest.spyOn(mpc, 'delegationsApprove').mockResolvedValue({
|
|
294
|
+
...mockEVMApproveResponse,
|
|
295
|
+
transactions: [txA, txB],
|
|
296
|
+
})
|
|
297
|
+
const d = new Delegations({
|
|
298
|
+
mpc,
|
|
299
|
+
signAndSendTransaction: mockSignAndSend,
|
|
300
|
+
})
|
|
301
|
+
mockSignAndSend
|
|
302
|
+
.mockResolvedValueOnce('0xh1')
|
|
303
|
+
.mockRejectedValueOnce(new Error('second tx failed'))
|
|
304
|
+
|
|
305
|
+
await expect(d.approveAndSubmit(mockEVMApproveRequest)).rejects.toThrow(
|
|
306
|
+
'second tx failed',
|
|
307
|
+
)
|
|
308
|
+
expect(mockSignAndSend).toHaveBeenCalledTimes(2)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('per-call signAndSendTransaction overrides constructor signer', async () => {
|
|
312
|
+
jest.spyOn(mpc, 'delegationsApprove').mockResolvedValue(mockEVMApproveResponse)
|
|
313
|
+
const instanceSigner = jest.fn().mockResolvedValue('0xinstance')
|
|
314
|
+
const perCallSigner = jest.fn().mockResolvedValue('0xpercall')
|
|
315
|
+
const d = new Delegations({
|
|
316
|
+
mpc,
|
|
317
|
+
signAndSendTransaction: instanceSigner,
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
await d.approveAndSubmit(mockEVMApproveRequest, {
|
|
321
|
+
signAndSendTransaction: perCallSigner,
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
expect(instanceSigner).not.toHaveBeenCalled()
|
|
325
|
+
expect(perCallSigner).toHaveBeenCalledTimes(1)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('throws on whitespace-only hash from signer', async () => {
|
|
329
|
+
jest.spyOn(mpc, 'delegationsApprove').mockResolvedValue(mockEVMApproveResponse)
|
|
330
|
+
mockSignAndSend.mockResolvedValue(' ')
|
|
331
|
+
const d = new Delegations({
|
|
332
|
+
mpc,
|
|
333
|
+
signAndSendTransaction: mockSignAndSend,
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
await expect(d.approveAndSubmit(mockEVMApproveRequest)).rejects.toThrow(
|
|
337
|
+
'Invalid transaction hash',
|
|
338
|
+
)
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it('prefers transactions over encodedTransactions when both non-empty', async () => {
|
|
342
|
+
const evmTx = mockEVMApproveResponse.transactions![0]!
|
|
343
|
+
jest.spyOn(mpc, 'delegationsApprove').mockResolvedValue({
|
|
344
|
+
transactions: [evmTx],
|
|
345
|
+
encodedTransactions: ['should-not-use'],
|
|
346
|
+
metadata: mockEVMApproveResponse.metadata,
|
|
347
|
+
})
|
|
348
|
+
const d = new Delegations({
|
|
349
|
+
mpc,
|
|
350
|
+
signAndSendTransaction: mockSignAndSend,
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
await d.approveAndSubmit(mockEVMApproveRequest)
|
|
354
|
+
|
|
355
|
+
expect(mockSignAndSend).toHaveBeenCalledWith(
|
|
356
|
+
evmTx,
|
|
357
|
+
mockEVMApproveRequest.chain,
|
|
358
|
+
)
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
it('setSignAndSendTransaction enables submit after construction', async () => {
|
|
362
|
+
jest.spyOn(mpc, 'delegationsApprove').mockResolvedValue(mockEVMApproveResponse)
|
|
363
|
+
const d = new Delegations({ mpc })
|
|
364
|
+
const late = jest.fn().mockResolvedValue('0xlate')
|
|
365
|
+
d.setSignAndSendTransaction(late)
|
|
366
|
+
|
|
367
|
+
const result = await d.approveAndSubmit(mockEVMApproveRequest)
|
|
368
|
+
|
|
369
|
+
expect(late).toHaveBeenCalledTimes(1)
|
|
370
|
+
expect(result).toEqual({ hashes: ['0xlate'] })
|
|
371
|
+
})
|
|
372
|
+
})
|
|
122
373
|
})
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
DelegationStatusResponse,
|
|
9
9
|
TransferFromRequest,
|
|
10
10
|
TransferFromResponse,
|
|
11
|
+
DelegationSubmitProgress,
|
|
11
12
|
} from '../../shared/types'
|
|
12
13
|
|
|
13
14
|
export interface IPortalDelegationsApi {
|
|
@@ -19,13 +20,68 @@ export interface IPortalDelegationsApi {
|
|
|
19
20
|
transferFrom(params: TransferFromRequest): Promise<TransferFromResponse>
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
const DELEGATIONS_SUBMIT_CONFIG_ERROR =
|
|
24
|
+
'[Delegations] No signer configured. Call setSignAndSendTransaction() on the instance or pass signAndSendTransaction in options.'
|
|
25
|
+
|
|
26
|
+
/** Optional callback for sign-and-submit flows. Invoked once per transaction. */
|
|
27
|
+
export interface DelegationSubmitOptions {
|
|
28
|
+
signAndSendTransaction?: (
|
|
29
|
+
transaction: unknown,
|
|
30
|
+
chainId: string,
|
|
31
|
+
) => Promise<string>
|
|
32
|
+
onProgress?: (event: DelegationSubmitProgress) => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Full delegations surface exposed on {@link Portal.delegations}, including sign-and-submit helpers.
|
|
37
|
+
*/
|
|
38
|
+
export interface IDelegations extends IPortalDelegationsApi {
|
|
39
|
+
/** Sets the default signer used by approveAndSubmit, revokeAndSubmit, and transferAndSubmit when options omit signAndSendTransaction. */
|
|
40
|
+
setSignAndSendTransaction(
|
|
41
|
+
fn: (transaction: unknown, chainId: string) => Promise<string>,
|
|
42
|
+
): void
|
|
43
|
+
approveAndSubmit(
|
|
44
|
+
params: ApproveDelegationRequest,
|
|
45
|
+
options?: DelegationSubmitOptions,
|
|
46
|
+
): Promise<{ hashes: string[] }>
|
|
47
|
+
revokeAndSubmit(
|
|
48
|
+
params: RevokeDelegationRequest,
|
|
49
|
+
options?: DelegationSubmitOptions,
|
|
50
|
+
): Promise<{ hashes: string[] }>
|
|
51
|
+
transferAndSubmit(
|
|
52
|
+
params: TransferFromRequest,
|
|
53
|
+
options?: DelegationSubmitOptions,
|
|
54
|
+
): Promise<{ hashes: string[] }>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface DelegationsOptions {
|
|
58
|
+
mpc: Mpc
|
|
59
|
+
/**
|
|
60
|
+
* When set, enables {@link Delegations.approveAndSubmit}, {@link Delegations.revokeAndSubmit},
|
|
61
|
+
* and {@link Delegations.transferAndSubmit}. `Portal` passes this by default using the iframe provider.
|
|
62
|
+
*/
|
|
63
|
+
signAndSendTransaction?: (
|
|
64
|
+
transaction: unknown,
|
|
65
|
+
chainId: string,
|
|
66
|
+
) => Promise<string>
|
|
67
|
+
}
|
|
23
68
|
|
|
24
69
|
export default class Delegations implements IDelegations {
|
|
25
|
-
private mpc: Mpc
|
|
70
|
+
private readonly mpc: Mpc
|
|
71
|
+
private signAndSendTransaction?: (
|
|
72
|
+
transaction: unknown,
|
|
73
|
+
chainId: string,
|
|
74
|
+
) => Promise<string>
|
|
26
75
|
|
|
27
|
-
constructor(
|
|
28
|
-
this.mpc = mpc
|
|
76
|
+
constructor(options: DelegationsOptions) {
|
|
77
|
+
this.mpc = options.mpc
|
|
78
|
+
this.signAndSendTransaction = options.signAndSendTransaction
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public setSignAndSendTransaction(
|
|
82
|
+
fn: (transaction: unknown, chainId: string) => Promise<string>,
|
|
83
|
+
): void {
|
|
84
|
+
this.signAndSendTransaction = fn
|
|
29
85
|
}
|
|
30
86
|
|
|
31
87
|
/**
|
|
@@ -75,4 +131,146 @@ export default class Delegations implements IDelegations {
|
|
|
75
131
|
): Promise<TransferFromResponse> {
|
|
76
132
|
return this.mpc?.delegationsTransferFrom(params)
|
|
77
133
|
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Approves a delegation, then signs and broadcasts each returned transaction via
|
|
137
|
+
* {@link DelegationsOptions.signAndSendTransaction}.
|
|
138
|
+
*
|
|
139
|
+
* Requires `signAndSendTransaction` (provided by default on `Portal.delegations`).
|
|
140
|
+
* Confirming transactions on-chain remains the application's responsibility.
|
|
141
|
+
*
|
|
142
|
+
* @param params - Same as {@link Delegations.approve}
|
|
143
|
+
* @param options - Optional progress callbacks
|
|
144
|
+
* @returns Transaction hashes in submission order
|
|
145
|
+
*/
|
|
146
|
+
public async approveAndSubmit(
|
|
147
|
+
params: Parameters<IPortalDelegationsApi['approve']>[0],
|
|
148
|
+
options?: DelegationSubmitOptions,
|
|
149
|
+
): Promise<{ hashes: string[] }> {
|
|
150
|
+
const signAndSend =
|
|
151
|
+
options?.signAndSendTransaction ?? this.signAndSendTransaction
|
|
152
|
+
if (!signAndSend) {
|
|
153
|
+
throw new Error(DELEGATIONS_SUBMIT_CONFIG_ERROR)
|
|
154
|
+
}
|
|
155
|
+
const response = await this.approve(params)
|
|
156
|
+
return this.executeAndTrack(
|
|
157
|
+
response,
|
|
158
|
+
params.chain,
|
|
159
|
+
signAndSend,
|
|
160
|
+
options?.onProgress,
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Revokes a delegation, then signs and broadcasts each returned transaction.
|
|
166
|
+
*
|
|
167
|
+
* Requires `signAndSendTransaction` (provided by default on `Portal.delegations`).
|
|
168
|
+
*
|
|
169
|
+
* @param params - Same as {@link Delegations.revoke}
|
|
170
|
+
* @param options - Optional progress callbacks
|
|
171
|
+
* @returns Transaction hashes in submission order
|
|
172
|
+
*/
|
|
173
|
+
public async revokeAndSubmit(
|
|
174
|
+
params: Parameters<IPortalDelegationsApi['revoke']>[0],
|
|
175
|
+
options?: DelegationSubmitOptions,
|
|
176
|
+
): Promise<{ hashes: string[] }> {
|
|
177
|
+
const signAndSend =
|
|
178
|
+
options?.signAndSendTransaction ?? this.signAndSendTransaction
|
|
179
|
+
if (!signAndSend) {
|
|
180
|
+
throw new Error(DELEGATIONS_SUBMIT_CONFIG_ERROR)
|
|
181
|
+
}
|
|
182
|
+
const response = await this.revoke(params)
|
|
183
|
+
return this.executeAndTrack(
|
|
184
|
+
response,
|
|
185
|
+
params.chain,
|
|
186
|
+
signAndSend,
|
|
187
|
+
options?.onProgress,
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Executes a delegated transfer, then signs and broadcasts each returned transaction.
|
|
193
|
+
*
|
|
194
|
+
* Requires `signAndSendTransaction` (provided by default on `Portal.delegations`).
|
|
195
|
+
*
|
|
196
|
+
* @param params - Same as {@link Delegations.transferFrom}
|
|
197
|
+
* @param options - Optional progress callbacks
|
|
198
|
+
* @returns Transaction hashes in submission order
|
|
199
|
+
*/
|
|
200
|
+
public async transferAndSubmit(
|
|
201
|
+
params: Parameters<IPortalDelegationsApi['transferFrom']>[0],
|
|
202
|
+
options?: DelegationSubmitOptions,
|
|
203
|
+
): Promise<{ hashes: string[] }> {
|
|
204
|
+
const signAndSend =
|
|
205
|
+
options?.signAndSendTransaction ?? this.signAndSendTransaction
|
|
206
|
+
if (!signAndSend) {
|
|
207
|
+
throw new Error(DELEGATIONS_SUBMIT_CONFIG_ERROR)
|
|
208
|
+
}
|
|
209
|
+
const response = await this.transferFrom(params)
|
|
210
|
+
return this.executeAndTrack(
|
|
211
|
+
response,
|
|
212
|
+
params.chain,
|
|
213
|
+
signAndSend,
|
|
214
|
+
options?.onProgress,
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private normalizeToTransactionList(
|
|
219
|
+
response:
|
|
220
|
+
| ApproveDelegationResponse
|
|
221
|
+
| RevokeDelegationResponse
|
|
222
|
+
| TransferFromResponse,
|
|
223
|
+
): unknown[] {
|
|
224
|
+
const transactions = Array.isArray(response.transactions)
|
|
225
|
+
? response.transactions
|
|
226
|
+
: []
|
|
227
|
+
const encodedTransactions = Array.isArray(response.encodedTransactions)
|
|
228
|
+
? response.encodedTransactions
|
|
229
|
+
: []
|
|
230
|
+
if (transactions.length > 0) {
|
|
231
|
+
return transactions
|
|
232
|
+
}
|
|
233
|
+
if (encodedTransactions.length > 0) {
|
|
234
|
+
return encodedTransactions
|
|
235
|
+
}
|
|
236
|
+
return []
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private async executeAndTrack(
|
|
240
|
+
response:
|
|
241
|
+
| ApproveDelegationResponse
|
|
242
|
+
| RevokeDelegationResponse
|
|
243
|
+
| TransferFromResponse,
|
|
244
|
+
chainId: string,
|
|
245
|
+
signAndSend: (transaction: unknown, chainId: string) => Promise<string>,
|
|
246
|
+
onProgress?: (event: DelegationSubmitProgress) => void,
|
|
247
|
+
): Promise<{ hashes: string[] }> {
|
|
248
|
+
const transactions = this.normalizeToTransactionList(response)
|
|
249
|
+
if (transactions.length === 0) {
|
|
250
|
+
throw new Error('No transactions in delegation response.')
|
|
251
|
+
}
|
|
252
|
+
const total = transactions.length
|
|
253
|
+
const hashes: string[] = []
|
|
254
|
+
for (let index = 0; index < transactions.length; index++) {
|
|
255
|
+
const tx = transactions[index]
|
|
256
|
+
|
|
257
|
+
// Validate transaction exists (defensive check for sparse arrays)
|
|
258
|
+
if (!tx) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`Transaction at index ${index} is undefined or null. This indicates a malformed API response.`,
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
onProgress?.({ step: 'signing', index, total })
|
|
265
|
+
const hash = await signAndSend(tx, chainId)
|
|
266
|
+
if (typeof hash !== 'string' || hash.trim().length === 0) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
`Invalid transaction hash returned from signAndSendTransaction at index ${index} for chain ${chainId}.`,
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
hashes.push(hash)
|
|
272
|
+
onProgress?.({ step: 'submitted', index, total, hash })
|
|
273
|
+
}
|
|
274
|
+
return { hashes }
|
|
275
|
+
}
|
|
78
276
|
}
|
|
@@ -63,7 +63,15 @@ describe('Noah', () => {
|
|
|
63
63
|
destinationAddress: 'SoLAddr1111111111111111111111111111111111111',
|
|
64
64
|
}
|
|
65
65
|
const spy = jest.spyOn(mpc, 'initiatePayin').mockResolvedValue({
|
|
66
|
-
data: {
|
|
66
|
+
data: {
|
|
67
|
+
payinId: 'p1',
|
|
68
|
+
bankDetails: {
|
|
69
|
+
paymentMethodId: 'pm-123',
|
|
70
|
+
paymentMethodType: 'bank_account',
|
|
71
|
+
accountNumber: '1234567890',
|
|
72
|
+
network: 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
67
75
|
})
|
|
68
76
|
await noah.initiatePayin(req)
|
|
69
77
|
expect(spy).toHaveBeenCalledWith(req)
|
|
@@ -76,7 +84,7 @@ describe('Noah', () => {
|
|
|
76
84
|
fiatCurrency: 'USD',
|
|
77
85
|
}
|
|
78
86
|
const spy = jest.spyOn(mpc, 'simulatePayin').mockResolvedValue({
|
|
79
|
-
data: {},
|
|
87
|
+
data: { fiatDepositId: 'deposit-123' },
|
|
80
88
|
})
|
|
81
89
|
await noah.simulatePayin(sim)
|
|
82
90
|
expect(spy).toHaveBeenCalledWith(sim)
|
|
@@ -84,7 +92,7 @@ describe('Noah', () => {
|
|
|
84
92
|
|
|
85
93
|
it('should call mpc.getPayoutCountries', async () => {
|
|
86
94
|
const spy = jest.spyOn(mpc, 'getPayoutCountries').mockResolvedValue({
|
|
87
|
-
data: { countries: [] },
|
|
95
|
+
data: { countries: { US: ['USD'], MX: ['MXN', 'USD'] } },
|
|
88
96
|
})
|
|
89
97
|
await noah.getPayoutCountries()
|
|
90
98
|
expect(spy).toHaveBeenCalled()
|
|
@@ -98,7 +106,7 @@ describe('Noah', () => {
|
|
|
98
106
|
fiatAmount: '10',
|
|
99
107
|
}
|
|
100
108
|
const spy = jest.spyOn(mpc, 'getPayoutChannels').mockResolvedValue({
|
|
101
|
-
data: [],
|
|
109
|
+
data: { items: [] },
|
|
102
110
|
} as never)
|
|
103
111
|
await noah.getPayoutChannels(req)
|
|
104
112
|
expect(spy).toHaveBeenCalledWith(req)
|
|
@@ -107,7 +115,12 @@ describe('Noah', () => {
|
|
|
107
115
|
it('should call mpc.getPayoutChannelForm', async () => {
|
|
108
116
|
const spy = jest
|
|
109
117
|
.spyOn(mpc, 'getPayoutChannelForm')
|
|
110
|
-
.mockResolvedValue({
|
|
118
|
+
.mockResolvedValue({
|
|
119
|
+
data: {
|
|
120
|
+
formSchema: { type: 'object', properties: {} },
|
|
121
|
+
formMetadata: { contentHash: 'abc123' },
|
|
122
|
+
},
|
|
123
|
+
} as never)
|
|
111
124
|
await noah.getPayoutChannelForm('ch-1')
|
|
112
125
|
expect(spy).toHaveBeenCalledWith('ch-1')
|
|
113
126
|
})
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import Mpc from '../../mpc'
|
|
2
|
-
import ZeroX from './zero-x'
|
|
3
|
-
import LiFi from './lifi'
|
|
2
|
+
import ZeroX, { type IZeroX, type ZeroXOptions } from './zero-x'
|
|
3
|
+
import LiFi, { type ILiFi, type LifiOptions } from './lifi'
|
|
4
|
+
|
|
5
|
+
export type TradingOptions = LifiOptions & ZeroXOptions
|
|
6
|
+
|
|
7
|
+
export type { LifiOptions, ILiFi, ZeroXOptions, IZeroX }
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
|
-
*
|
|
7
|
-
* In the future, Trading domain logic should be here.
|
|
10
|
+
* Trading integrations (LiFi, 0x).
|
|
8
11
|
*/
|
|
9
12
|
export default class Trading {
|
|
10
13
|
public lifi: LiFi
|
|
11
14
|
public zeroX: ZeroX
|
|
12
15
|
|
|
13
|
-
constructor({ mpc }: { mpc: Mpc }) {
|
|
14
|
-
this.lifi = new LiFi({ mpc })
|
|
15
|
-
this.zeroX = new ZeroX({ mpc })
|
|
16
|
+
constructor({ mpc, ...defaults }: { mpc: Mpc } & TradingOptions) {
|
|
17
|
+
this.lifi = new LiFi({ mpc, ...defaults })
|
|
18
|
+
this.zeroX = new ZeroX({ mpc, ...defaults })
|
|
16
19
|
}
|
|
17
20
|
}
|