@portal-hq/web 3.16.0 → 3.17.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 +294 -0
- package/lib/commonjs/index.test.js +231 -0
- package/lib/commonjs/mpc/index.js +27 -1
- package/lib/commonjs/mpc/index.test.js +134 -0
- package/lib/commonjs/shared/types/accountAbstraction.js +2 -0
- package/lib/commonjs/shared/types/index.js +2 -0
- package/lib/esm/index.js +294 -0
- package/lib/esm/index.test.js +233 -2
- package/lib/esm/mpc/index.js +27 -1
- package/lib/esm/mpc/index.test.js +135 -1
- package/lib/esm/shared/types/accountAbstraction.js +1 -0
- package/lib/esm/shared/types/index.js +2 -0
- package/package.json +3 -2
- package/src/__mocks/constants.ts +93 -0
- package/src/__mocks/portal/mpc.ts +11 -0
- package/src/index.test.ts +473 -1
- package/src/index.ts +454 -0
- package/src/mpc/index.test.ts +164 -0
- package/src/mpc/index.ts +36 -1
- package/src/shared/types/accountAbstraction.ts +122 -0
- package/src/shared/types/common.ts +3 -0
- package/src/shared/types/index.ts +3 -0
package/src/index.test.ts
CHANGED
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
* @jest-environment jsdom
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import Portal, { BackupMethods, GetTransactionsOrder } from '.'
|
|
5
|
+
import Portal, { BackupMethods, GetTransactionsOrder, PortalCurve } from '.'
|
|
6
6
|
import {
|
|
7
7
|
mockAddress,
|
|
8
8
|
mockBackupConfig,
|
|
9
9
|
mockBlockHashResponse,
|
|
10
|
+
mockBuiltEip155Transaction,
|
|
11
|
+
mockBuiltEip155TransactionNative,
|
|
10
12
|
mockBuiltTronTransaction,
|
|
13
|
+
mockBuildBatchedUserOpRequest,
|
|
14
|
+
mockBuildBatchedUserOpResponse,
|
|
15
|
+
mockBroadcastBatchedUserOpRequest,
|
|
16
|
+
mockBroadcastBatchedUserOpResponse,
|
|
11
17
|
mockCipherText,
|
|
12
18
|
mockClientResponse,
|
|
13
19
|
mockEip155Address,
|
|
@@ -20,6 +26,7 @@ import {
|
|
|
20
26
|
mockOrgBackupShares,
|
|
21
27
|
mockQuoteArgs,
|
|
22
28
|
mockRpcConfig,
|
|
29
|
+
mockSendBatchUserOpRequest,
|
|
23
30
|
mockSharesOnDevice,
|
|
24
31
|
mockSignedHash,
|
|
25
32
|
mockSolanaAddress,
|
|
@@ -1167,6 +1174,471 @@ describe('Portal', () => {
|
|
|
1167
1174
|
})
|
|
1168
1175
|
})
|
|
1169
1176
|
|
|
1177
|
+
describe('buildBatchedUserOp', () => {
|
|
1178
|
+
it('should correctly call mpc.accountAbstractionBuildBatchedUserOp', async () => {
|
|
1179
|
+
await portal.buildBatchedUserOp(mockBuildBatchedUserOpRequest)
|
|
1180
|
+
|
|
1181
|
+
expect(
|
|
1182
|
+
portal.mpc.accountAbstractionBuildBatchedUserOp,
|
|
1183
|
+
).toHaveBeenCalledTimes(1)
|
|
1184
|
+
expect(
|
|
1185
|
+
portal.mpc.accountAbstractionBuildBatchedUserOp,
|
|
1186
|
+
).toHaveBeenCalledWith(mockBuildBatchedUserOpRequest)
|
|
1187
|
+
})
|
|
1188
|
+
})
|
|
1189
|
+
|
|
1190
|
+
describe('broadcastBatchedUserOp', () => {
|
|
1191
|
+
it('should correctly call mpc.accountAbstractionBroadcastBatchedUserOp', async () => {
|
|
1192
|
+
await portal.broadcastBatchedUserOp(mockBroadcastBatchedUserOpRequest)
|
|
1193
|
+
|
|
1194
|
+
expect(
|
|
1195
|
+
portal.mpc.accountAbstractionBroadcastBatchedUserOp,
|
|
1196
|
+
).toHaveBeenCalledTimes(1)
|
|
1197
|
+
expect(
|
|
1198
|
+
portal.mpc.accountAbstractionBroadcastBatchedUserOp,
|
|
1199
|
+
).toHaveBeenCalledWith(mockBroadcastBatchedUserOpRequest)
|
|
1200
|
+
})
|
|
1201
|
+
})
|
|
1202
|
+
|
|
1203
|
+
describe('sendBatchUserOp', () => {
|
|
1204
|
+
it('should build, sign, and broadcast a batched UserOperation', async () => {
|
|
1205
|
+
const result = await portal.sendBatchUserOp(mockSendBatchUserOpRequest)
|
|
1206
|
+
|
|
1207
|
+
expect(result).toEqual(
|
|
1208
|
+
expect.objectContaining({
|
|
1209
|
+
data: expect.objectContaining({ userOpHash: expect.any(String) }),
|
|
1210
|
+
metadata: expect.objectContaining({ chainId: expect.any(String) }),
|
|
1211
|
+
}),
|
|
1212
|
+
)
|
|
1213
|
+
|
|
1214
|
+
// buildTransaction called once per transaction descriptor
|
|
1215
|
+
expect(portal.mpc.buildTransaction).toHaveBeenCalledTimes(2)
|
|
1216
|
+
expect(portal.mpc.buildTransaction).toHaveBeenCalledWith(
|
|
1217
|
+
mockSendBatchUserOpRequest.chain,
|
|
1218
|
+
mockSendBatchUserOpRequest.transactions[0].to,
|
|
1219
|
+
mockSendBatchUserOpRequest.transactions[0].token,
|
|
1220
|
+
mockSendBatchUserOpRequest.transactions[0].value,
|
|
1221
|
+
expect.any(String),
|
|
1222
|
+
)
|
|
1223
|
+
expect(portal.mpc.buildTransaction).toHaveBeenCalledWith(
|
|
1224
|
+
mockSendBatchUserOpRequest.chain,
|
|
1225
|
+
mockSendBatchUserOpRequest.transactions[1].to,
|
|
1226
|
+
mockSendBatchUserOpRequest.transactions[1].token,
|
|
1227
|
+
mockSendBatchUserOpRequest.transactions[1].value,
|
|
1228
|
+
expect.any(String),
|
|
1229
|
+
)
|
|
1230
|
+
|
|
1231
|
+
// buildBatchedUserOp called with ERC-20 call shape (no value field) and the shared traceId
|
|
1232
|
+
expect(
|
|
1233
|
+
portal.mpc.accountAbstractionBuildBatchedUserOp,
|
|
1234
|
+
).toHaveBeenCalledTimes(1)
|
|
1235
|
+
expect(
|
|
1236
|
+
portal.mpc.accountAbstractionBuildBatchedUserOp,
|
|
1237
|
+
).toHaveBeenCalledWith(
|
|
1238
|
+
{
|
|
1239
|
+
chain: mockSendBatchUserOpRequest.chain,
|
|
1240
|
+
calls: [
|
|
1241
|
+
{
|
|
1242
|
+
to: mockBuiltEip155Transaction.transaction.to,
|
|
1243
|
+
data: mockBuiltEip155Transaction.transaction.data,
|
|
1244
|
+
},
|
|
1245
|
+
{
|
|
1246
|
+
to: mockBuiltEip155Transaction.transaction.to,
|
|
1247
|
+
data: mockBuiltEip155Transaction.transaction.data,
|
|
1248
|
+
},
|
|
1249
|
+
],
|
|
1250
|
+
},
|
|
1251
|
+
expect.any(String),
|
|
1252
|
+
)
|
|
1253
|
+
|
|
1254
|
+
// rawSign called with SECP256K1 and the userOpHash with 0x prefix stripped
|
|
1255
|
+
expect(portal.mpc.rawSign).toHaveBeenCalledTimes(1)
|
|
1256
|
+
expect(portal.mpc.rawSign).toHaveBeenCalledWith(
|
|
1257
|
+
PortalCurve.SECP256K1,
|
|
1258
|
+
mockBuildBatchedUserOpResponse.data.userOpHash.slice(2),
|
|
1259
|
+
{ signatureApprovalMemo: undefined, traceId: expect.any(String) },
|
|
1260
|
+
)
|
|
1261
|
+
|
|
1262
|
+
// broadcastBatchedUserOp called with the built userOperation and signature, and the shared traceId
|
|
1263
|
+
expect(
|
|
1264
|
+
portal.mpc.accountAbstractionBroadcastBatchedUserOp,
|
|
1265
|
+
).toHaveBeenCalledTimes(1)
|
|
1266
|
+
expect(
|
|
1267
|
+
portal.mpc.accountAbstractionBroadcastBatchedUserOp,
|
|
1268
|
+
).toHaveBeenCalledWith(
|
|
1269
|
+
{
|
|
1270
|
+
chain: mockSendBatchUserOpRequest.chain,
|
|
1271
|
+
userOperation: mockBuildBatchedUserOpResponse.data.userOperation,
|
|
1272
|
+
signature: mockSignedHash,
|
|
1273
|
+
},
|
|
1274
|
+
expect.any(String),
|
|
1275
|
+
)
|
|
1276
|
+
})
|
|
1277
|
+
|
|
1278
|
+
it('should propagate the same traceId to buildBatchedUserOp and broadcastBatchedUserOp', async () => {
|
|
1279
|
+
await portal.sendBatchUserOp(mockSendBatchUserOpRequest)
|
|
1280
|
+
|
|
1281
|
+
const buildTraceId = (
|
|
1282
|
+
portal.mpc.accountAbstractionBuildBatchedUserOp as jest.Mock
|
|
1283
|
+
).mock.calls[0][1]
|
|
1284
|
+
const broadcastTraceId = (
|
|
1285
|
+
portal.mpc.accountAbstractionBroadcastBatchedUserOp as jest.Mock
|
|
1286
|
+
).mock.calls[0][1]
|
|
1287
|
+
|
|
1288
|
+
expect(typeof buildTraceId).toBe('string')
|
|
1289
|
+
expect(buildTraceId).toEqual(broadcastTraceId)
|
|
1290
|
+
})
|
|
1291
|
+
|
|
1292
|
+
it('should include value in the call for native ETH transfers', async () => {
|
|
1293
|
+
;(portal.mpc.buildTransaction as jest.Mock).mockResolvedValueOnce(
|
|
1294
|
+
mockBuiltEip155TransactionNative,
|
|
1295
|
+
)
|
|
1296
|
+
|
|
1297
|
+
await portal.sendBatchUserOp({
|
|
1298
|
+
chain: 'eip155:1',
|
|
1299
|
+
transactions: [{ token: 'ETH', value: '0.1', to: '0x1111111111111111111111111111111111111111' }],
|
|
1300
|
+
})
|
|
1301
|
+
|
|
1302
|
+
expect(
|
|
1303
|
+
portal.mpc.accountAbstractionBuildBatchedUserOp,
|
|
1304
|
+
).toHaveBeenCalledWith(
|
|
1305
|
+
{
|
|
1306
|
+
chain: 'eip155:1',
|
|
1307
|
+
calls: [
|
|
1308
|
+
{
|
|
1309
|
+
to: mockBuiltEip155TransactionNative.transaction.to,
|
|
1310
|
+
data: '0x',
|
|
1311
|
+
value: mockBuiltEip155TransactionNative.metadata.rawAmount,
|
|
1312
|
+
},
|
|
1313
|
+
],
|
|
1314
|
+
},
|
|
1315
|
+
expect.any(String),
|
|
1316
|
+
)
|
|
1317
|
+
})
|
|
1318
|
+
|
|
1319
|
+
it('should not include value in the call for ERC-20 transfers', async () => {
|
|
1320
|
+
await portal.sendBatchUserOp({
|
|
1321
|
+
chain: 'eip155:1',
|
|
1322
|
+
transactions: [{ token: 'USDC', value: '1.0', to: '0x1111111111111111111111111111111111111111' }],
|
|
1323
|
+
})
|
|
1324
|
+
|
|
1325
|
+
const calls = (
|
|
1326
|
+
portal.mpc.accountAbstractionBuildBatchedUserOp as jest.Mock
|
|
1327
|
+
).mock.calls[0][0].calls
|
|
1328
|
+
|
|
1329
|
+
expect(calls[0]).not.toHaveProperty('value')
|
|
1330
|
+
})
|
|
1331
|
+
|
|
1332
|
+
it('should forward signatureApprovalMemo to rawSign', async () => {
|
|
1333
|
+
await portal.sendBatchUserOp({
|
|
1334
|
+
...mockSendBatchUserOpRequest,
|
|
1335
|
+
signatureApprovalMemo: 'approve this batch',
|
|
1336
|
+
})
|
|
1337
|
+
|
|
1338
|
+
expect(portal.mpc.rawSign).toHaveBeenCalledWith(
|
|
1339
|
+
PortalCurve.SECP256K1,
|
|
1340
|
+
expect.any(String),
|
|
1341
|
+
{ signatureApprovalMemo: 'approve this batch', traceId: expect.any(String) },
|
|
1342
|
+
)
|
|
1343
|
+
})
|
|
1344
|
+
|
|
1345
|
+
it('should throw if chain is not eip155', async () => {
|
|
1346
|
+
await expect(
|
|
1347
|
+
portal.sendBatchUserOp({
|
|
1348
|
+
...mockSendBatchUserOpRequest,
|
|
1349
|
+
chain: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
|
|
1350
|
+
}),
|
|
1351
|
+
).rejects.toThrow(
|
|
1352
|
+
'[Portal.sendBatchUserOp] UserOperations are only supported on EIP-155 (EVM) chains',
|
|
1353
|
+
)
|
|
1354
|
+
})
|
|
1355
|
+
|
|
1356
|
+
it('should throw if transactions array is empty', async () => {
|
|
1357
|
+
await expect(
|
|
1358
|
+
portal.sendBatchUserOp({
|
|
1359
|
+
...mockSendBatchUserOpRequest,
|
|
1360
|
+
transactions: [],
|
|
1361
|
+
}),
|
|
1362
|
+
).rejects.toThrow(
|
|
1363
|
+
'[Portal.sendBatchUserOp] transactions must contain at least one transaction',
|
|
1364
|
+
)
|
|
1365
|
+
})
|
|
1366
|
+
|
|
1367
|
+
it('should throw with index context if buildTransaction fails for transaction 0', async () => {
|
|
1368
|
+
;(portal.mpc.buildTransaction as jest.Mock).mockRejectedValueOnce(
|
|
1369
|
+
new Error('Network error'),
|
|
1370
|
+
)
|
|
1371
|
+
|
|
1372
|
+
await expect(
|
|
1373
|
+
portal.sendBatchUserOp({
|
|
1374
|
+
chain: 'eip155:1',
|
|
1375
|
+
transactions: [{ token: 'USDC', value: '1.0', to: '0x1111111111111111111111111111111111111111' }],
|
|
1376
|
+
}),
|
|
1377
|
+
).rejects.toThrow(
|
|
1378
|
+
'[Portal.sendBatchUserOp] Failed to build call for transaction at index 0: Network error',
|
|
1379
|
+
)
|
|
1380
|
+
})
|
|
1381
|
+
|
|
1382
|
+
it('should throw with index context if buildTransaction fails for transaction 1', async () => {
|
|
1383
|
+
;(portal.mpc.buildTransaction as jest.Mock)
|
|
1384
|
+
.mockResolvedValueOnce(mockBuiltEip155Transaction)
|
|
1385
|
+
.mockRejectedValueOnce(new Error('Network error'))
|
|
1386
|
+
|
|
1387
|
+
await expect(
|
|
1388
|
+
portal.sendBatchUserOp(mockSendBatchUserOpRequest),
|
|
1389
|
+
).rejects.toThrow(
|
|
1390
|
+
'[Portal.sendBatchUserOp] Failed to build call for transaction at index 1: Network error',
|
|
1391
|
+
)
|
|
1392
|
+
})
|
|
1393
|
+
|
|
1394
|
+
it('should throw if buildBatchedUserOp fails', async () => {
|
|
1395
|
+
;(
|
|
1396
|
+
portal.mpc.accountAbstractionBuildBatchedUserOp as jest.Mock
|
|
1397
|
+
).mockRejectedValueOnce(new Error('UserOp build failed'))
|
|
1398
|
+
|
|
1399
|
+
await expect(
|
|
1400
|
+
portal.sendBatchUserOp(mockSendBatchUserOpRequest),
|
|
1401
|
+
).rejects.toThrow(
|
|
1402
|
+
'[Portal.sendBatchUserOp] Failed to build UserOperation: UserOp build failed',
|
|
1403
|
+
)
|
|
1404
|
+
})
|
|
1405
|
+
|
|
1406
|
+
it('should throw if rawSign fails', async () => {
|
|
1407
|
+
;(portal.mpc.rawSign as jest.Mock).mockRejectedValueOnce(
|
|
1408
|
+
new Error('Signing failed'),
|
|
1409
|
+
)
|
|
1410
|
+
|
|
1411
|
+
await expect(
|
|
1412
|
+
portal.sendBatchUserOp(mockSendBatchUserOpRequest),
|
|
1413
|
+
).rejects.toThrow(
|
|
1414
|
+
'[Portal.sendBatchUserOp] Failed to sign userOpHash: Signing failed',
|
|
1415
|
+
)
|
|
1416
|
+
})
|
|
1417
|
+
|
|
1418
|
+
it('should throw if broadcastBatchedUserOp fails', async () => {
|
|
1419
|
+
;(
|
|
1420
|
+
portal.mpc.accountAbstractionBroadcastBatchedUserOp as jest.Mock
|
|
1421
|
+
).mockRejectedValueOnce(new Error('Broadcast failed'))
|
|
1422
|
+
|
|
1423
|
+
await expect(
|
|
1424
|
+
portal.sendBatchUserOp(mockSendBatchUserOpRequest),
|
|
1425
|
+
).rejects.toThrow(
|
|
1426
|
+
'[Portal.sendBatchUserOp] Failed to broadcast UserOperation: Broadcast failed',
|
|
1427
|
+
)
|
|
1428
|
+
})
|
|
1429
|
+
})
|
|
1430
|
+
|
|
1431
|
+
describe('sendBatchedAssets', () => {
|
|
1432
|
+
const feeRecipient = '0x2222222222222222222222222222222222222222'
|
|
1433
|
+
const baseRequest = {
|
|
1434
|
+
chain: 'eip155:1',
|
|
1435
|
+
transactions: [
|
|
1436
|
+
{
|
|
1437
|
+
token: 'USDC',
|
|
1438
|
+
value: '1.0',
|
|
1439
|
+
to: '0x1111111111111111111111111111111111111111',
|
|
1440
|
+
},
|
|
1441
|
+
],
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
it('should build twice, convert gas to a fee, sign, and broadcast', async () => {
|
|
1445
|
+
const convertGasToFeeAmount = jest.fn().mockResolvedValue('0.5')
|
|
1446
|
+
|
|
1447
|
+
const result = await portal.sendBatchedAssets({
|
|
1448
|
+
...baseRequest,
|
|
1449
|
+
gasReimbursement: { feeToken: 'USDC', feeRecipient, convertGasToFeeAmount },
|
|
1450
|
+
})
|
|
1451
|
+
|
|
1452
|
+
expect(result).toEqual(mockBroadcastBatchedUserOpResponse)
|
|
1453
|
+
|
|
1454
|
+
// Estimation pass + final pass = 2 builds.
|
|
1455
|
+
expect(
|
|
1456
|
+
portal.mpc.accountAbstractionBuildBatchedUserOp,
|
|
1457
|
+
).toHaveBeenCalledTimes(2)
|
|
1458
|
+
|
|
1459
|
+
// buildTransaction: 1 user tx + 1 placeholder fee + 1 real fee = 3.
|
|
1460
|
+
expect(portal.mpc.buildTransaction).toHaveBeenCalledTimes(3)
|
|
1461
|
+
|
|
1462
|
+
// Conversion called once with gasCostWei = totalGas(100000) * maxFeePerGas(1e9).
|
|
1463
|
+
expect(convertGasToFeeAmount).toHaveBeenCalledTimes(1)
|
|
1464
|
+
expect(convertGasToFeeAmount).toHaveBeenCalledWith(BigInt('100000000000000'))
|
|
1465
|
+
|
|
1466
|
+
// Placeholder fee call built with the default '0.01'; real fee call with '0.5'.
|
|
1467
|
+
expect(portal.mpc.buildTransaction).toHaveBeenCalledWith(
|
|
1468
|
+
'eip155:1',
|
|
1469
|
+
feeRecipient,
|
|
1470
|
+
'USDC',
|
|
1471
|
+
'0.01',
|
|
1472
|
+
expect.any(String),
|
|
1473
|
+
)
|
|
1474
|
+
expect(portal.mpc.buildTransaction).toHaveBeenCalledWith(
|
|
1475
|
+
'eip155:1',
|
|
1476
|
+
feeRecipient,
|
|
1477
|
+
'USDC',
|
|
1478
|
+
'0.5',
|
|
1479
|
+
expect.any(String),
|
|
1480
|
+
)
|
|
1481
|
+
|
|
1482
|
+
expect(portal.mpc.rawSign).toHaveBeenCalledTimes(1)
|
|
1483
|
+
expect(
|
|
1484
|
+
portal.mpc.accountAbstractionBroadcastBatchedUserOp,
|
|
1485
|
+
).toHaveBeenCalledTimes(1)
|
|
1486
|
+
})
|
|
1487
|
+
|
|
1488
|
+
it('should apply bufferBps to the gas cost before conversion', async () => {
|
|
1489
|
+
const convertGasToFeeAmount = jest.fn().mockResolvedValue('0.6')
|
|
1490
|
+
|
|
1491
|
+
await portal.sendBatchedAssets({
|
|
1492
|
+
...baseRequest,
|
|
1493
|
+
gasReimbursement: {
|
|
1494
|
+
feeToken: 'USDC',
|
|
1495
|
+
feeRecipient,
|
|
1496
|
+
convertGasToFeeAmount,
|
|
1497
|
+
bufferBps: 1000, // +10%
|
|
1498
|
+
},
|
|
1499
|
+
})
|
|
1500
|
+
|
|
1501
|
+
// 1e14 * 11000 / 10000 = 1.1e14
|
|
1502
|
+
expect(convertGasToFeeAmount).toHaveBeenCalledWith(BigInt('110000000000000'))
|
|
1503
|
+
})
|
|
1504
|
+
|
|
1505
|
+
it('should throw if the build response is missing estimatedGasCostWei', async () => {
|
|
1506
|
+
const noGasCost = {
|
|
1507
|
+
...mockBuildBatchedUserOpResponse,
|
|
1508
|
+
metadata: { chainId: 'eip155:1' },
|
|
1509
|
+
}
|
|
1510
|
+
;(
|
|
1511
|
+
portal.mpc.accountAbstractionBuildBatchedUserOp as jest.Mock
|
|
1512
|
+
).mockResolvedValueOnce(noGasCost)
|
|
1513
|
+
const convertGasToFeeAmount = jest.fn()
|
|
1514
|
+
|
|
1515
|
+
await expect(
|
|
1516
|
+
portal.sendBatchedAssets({
|
|
1517
|
+
...baseRequest,
|
|
1518
|
+
gasReimbursement: { feeToken: 'USDC', feeRecipient, convertGasToFeeAmount },
|
|
1519
|
+
}),
|
|
1520
|
+
).rejects.toThrow(
|
|
1521
|
+
'[Portal.sendBatchedAssets] build response is missing metadata.estimatedGasCostWei',
|
|
1522
|
+
)
|
|
1523
|
+
// Conversion should never run if we can't determine the gas cost.
|
|
1524
|
+
expect(convertGasToFeeAmount).not.toHaveBeenCalled()
|
|
1525
|
+
})
|
|
1526
|
+
|
|
1527
|
+
it('should throw if the estimated gas cost is 0 (e.g. Ultra Relay zero-fee path)', async () => {
|
|
1528
|
+
const zeroCost = {
|
|
1529
|
+
...mockBuildBatchedUserOpResponse,
|
|
1530
|
+
metadata: {
|
|
1531
|
+
chainId: 'eip155:1',
|
|
1532
|
+
totalGas: '2000000',
|
|
1533
|
+
maxFeePerGas: '0',
|
|
1534
|
+
estimatedGasCostWei: '0',
|
|
1535
|
+
},
|
|
1536
|
+
}
|
|
1537
|
+
;(
|
|
1538
|
+
portal.mpc.accountAbstractionBuildBatchedUserOp as jest.Mock
|
|
1539
|
+
).mockResolvedValueOnce(zeroCost)
|
|
1540
|
+
const convertGasToFeeAmount = jest.fn()
|
|
1541
|
+
|
|
1542
|
+
await expect(
|
|
1543
|
+
portal.sendBatchedAssets({
|
|
1544
|
+
...baseRequest,
|
|
1545
|
+
gasReimbursement: { feeToken: 'USDC', feeRecipient, convertGasToFeeAmount },
|
|
1546
|
+
}),
|
|
1547
|
+
).rejects.toThrow(
|
|
1548
|
+
'[Portal.sendBatchedAssets] Estimated gas cost is 0',
|
|
1549
|
+
)
|
|
1550
|
+
expect(convertGasToFeeAmount).not.toHaveBeenCalled()
|
|
1551
|
+
})
|
|
1552
|
+
|
|
1553
|
+
it('should reuse the same traceId across both builds', async () => {
|
|
1554
|
+
const convertGasToFeeAmount = jest.fn().mockResolvedValue('0.5')
|
|
1555
|
+
|
|
1556
|
+
await portal.sendBatchedAssets({
|
|
1557
|
+
...baseRequest,
|
|
1558
|
+
gasReimbursement: { feeToken: 'USDC', feeRecipient, convertGasToFeeAmount },
|
|
1559
|
+
})
|
|
1560
|
+
|
|
1561
|
+
const calls = (
|
|
1562
|
+
portal.mpc.accountAbstractionBuildBatchedUserOp as jest.Mock
|
|
1563
|
+
).mock.calls
|
|
1564
|
+
expect(typeof calls[0][1]).toBe('string')
|
|
1565
|
+
expect(calls[0][1]).toEqual(calls[1][1])
|
|
1566
|
+
})
|
|
1567
|
+
|
|
1568
|
+
it('should throw if chain is not eip155', async () => {
|
|
1569
|
+
await expect(
|
|
1570
|
+
portal.sendBatchedAssets({
|
|
1571
|
+
chain: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
|
|
1572
|
+
transactions: baseRequest.transactions,
|
|
1573
|
+
gasReimbursement: {
|
|
1574
|
+
feeToken: 'USDC',
|
|
1575
|
+
feeRecipient,
|
|
1576
|
+
convertGasToFeeAmount: () => '1',
|
|
1577
|
+
},
|
|
1578
|
+
}),
|
|
1579
|
+
).rejects.toThrow(
|
|
1580
|
+
'[Portal.sendBatchedAssets] UserOperations are only supported on EIP-155 (EVM) chains',
|
|
1581
|
+
)
|
|
1582
|
+
})
|
|
1583
|
+
|
|
1584
|
+
it('should throw if transactions is empty', async () => {
|
|
1585
|
+
await expect(
|
|
1586
|
+
portal.sendBatchedAssets({
|
|
1587
|
+
chain: 'eip155:1',
|
|
1588
|
+
transactions: [],
|
|
1589
|
+
gasReimbursement: {
|
|
1590
|
+
feeToken: 'USDC',
|
|
1591
|
+
feeRecipient,
|
|
1592
|
+
convertGasToFeeAmount: () => '1',
|
|
1593
|
+
},
|
|
1594
|
+
}),
|
|
1595
|
+
).rejects.toThrow(
|
|
1596
|
+
'[Portal.sendBatchedAssets] transactions must contain at least one transaction',
|
|
1597
|
+
)
|
|
1598
|
+
})
|
|
1599
|
+
|
|
1600
|
+
it('should throw if convertGasToFeeAmount is missing', async () => {
|
|
1601
|
+
await expect(
|
|
1602
|
+
portal.sendBatchedAssets({
|
|
1603
|
+
...baseRequest,
|
|
1604
|
+
gasReimbursement: { feeToken: 'USDC', feeRecipient } as never,
|
|
1605
|
+
}),
|
|
1606
|
+
).rejects.toThrow(
|
|
1607
|
+
'[Portal.sendBatchedAssets] gasReimbursement.convertGasToFeeAmount (a function) is required',
|
|
1608
|
+
)
|
|
1609
|
+
})
|
|
1610
|
+
|
|
1611
|
+
it('should throw if feeRecipient is not a valid EVM address', async () => {
|
|
1612
|
+
await expect(
|
|
1613
|
+
portal.sendBatchedAssets({
|
|
1614
|
+
...baseRequest,
|
|
1615
|
+
gasReimbursement: {
|
|
1616
|
+
feeToken: 'USDC',
|
|
1617
|
+
feeRecipient: '0xnope',
|
|
1618
|
+
convertGasToFeeAmount: () => '1',
|
|
1619
|
+
},
|
|
1620
|
+
}),
|
|
1621
|
+
).rejects.toThrow(
|
|
1622
|
+
'[Portal.sendBatchedAssets] Invalid gasReimbursement.feeRecipient',
|
|
1623
|
+
)
|
|
1624
|
+
})
|
|
1625
|
+
|
|
1626
|
+
it('should surface an error thrown by the conversion callback', async () => {
|
|
1627
|
+
const convertGasToFeeAmount = jest
|
|
1628
|
+
.fn()
|
|
1629
|
+
.mockRejectedValue(new Error('rate unavailable'))
|
|
1630
|
+
|
|
1631
|
+
await expect(
|
|
1632
|
+
portal.sendBatchedAssets({
|
|
1633
|
+
...baseRequest,
|
|
1634
|
+
gasReimbursement: { feeToken: 'USDC', feeRecipient, convertGasToFeeAmount },
|
|
1635
|
+
}),
|
|
1636
|
+
).rejects.toThrow(
|
|
1637
|
+
'[Portal.sendBatchedAssets] gasReimbursement.convertGasToFeeAmount threw: rate unavailable',
|
|
1638
|
+
)
|
|
1639
|
+
})
|
|
1640
|
+
})
|
|
1641
|
+
|
|
1170
1642
|
describe('storedClientBackupShare', () => {
|
|
1171
1643
|
it('should correctly call mpc.storedClientBackupShare', async () => {
|
|
1172
1644
|
await portal.storedClientBackupShare(true, BackupMethods.password)
|