@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/src/index.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  import {
9
9
  GetAssetsResponse,
10
10
  BuiltTransaction,
11
+ BuiltEip155Transaction,
11
12
  BuiltTronTransaction,
12
13
  EvaluateTransactionOperationType,
13
14
  EvaluateTransactionParam,
@@ -35,6 +36,14 @@ import {
35
36
  type SimulatedTransaction,
36
37
  type Transaction,
37
38
  type TypedData,
39
+ type BuildBatchedUserOpRequest,
40
+ type BuildBatchedUserOpResponse,
41
+ type BroadcastBatchedUserOpRequest,
42
+ type BroadcastBatchedUserOpResponse,
43
+ type UserOperationCall,
44
+ type SendBatchUserOpRequest,
45
+ type SendBatchUserOpTransaction,
46
+ type SendBatchedAssetsRequest,
38
47
  } from './shared/types'
39
48
 
40
49
  import {
@@ -1285,6 +1294,442 @@ class Portal {
1285
1294
  return this.mpc?.buildTransaction(chainId, to, token, amount, traceId)
1286
1295
  }
1287
1296
 
1297
+ /*******************************
1298
+ * Account Abstraction Methods
1299
+ *******************************/
1300
+
1301
+ public async buildBatchedUserOp(
1302
+ data: BuildBatchedUserOpRequest,
1303
+ ): Promise<BuildBatchedUserOpResponse> {
1304
+ return this.mpc?.accountAbstractionBuildBatchedUserOp(data)
1305
+ }
1306
+
1307
+ public async broadcastBatchedUserOp(
1308
+ data: BroadcastBatchedUserOpRequest,
1309
+ ): Promise<BroadcastBatchedUserOpResponse> {
1310
+ return this.mpc?.accountAbstractionBroadcastBatchedUserOp(data)
1311
+ }
1312
+
1313
+ /**
1314
+ * Build, sign, and broadcast a batch of EIP-155 UserOperations (ERC-4337).
1315
+ *
1316
+ * Returns the broadcast response containing `userOpHash`. Broadcasting does not
1317
+ * guarantee on-chain inclusion — UserOps are processed by a bundler that may
1318
+ * delay or drop them. To wait for confirmation call:
1319
+ * `await portal.waitForConfirmation(result.data.userOpHash, data.chain)`
1320
+ *
1321
+ * @param data.chain - CAIP-2 chain ID; must start with 'eip155:'
1322
+ * @param data.transactions - Ordered list of transfers to batch
1323
+ * @param data.signatureApprovalMemo - Optional memo for the signing approval prompt
1324
+ * @param data.traceId - Optional trace ID for request correlation
1325
+ */
1326
+ public async sendBatchUserOp(
1327
+ data: SendBatchUserOpRequest,
1328
+ ): Promise<BroadcastBatchedUserOpResponse> {
1329
+ if (!data.chain.startsWith('eip155:')) {
1330
+ throw new Error(
1331
+ '[Portal.sendBatchUserOp] UserOperations are only supported on EIP-155 (EVM) chains',
1332
+ )
1333
+ }
1334
+
1335
+ if (!data.transactions || data.transactions.length === 0) {
1336
+ throw new Error(
1337
+ '[Portal.sendBatchUserOp] transactions must contain at least one transaction',
1338
+ )
1339
+ }
1340
+
1341
+ // Validate every destination address before starting any async work.
1342
+ for (const tx of data.transactions) {
1343
+ if (!tx.to || !/^0x[0-9a-fA-F]{40}$/i.test(tx.to)) {
1344
+ throw new Error(
1345
+ `[Portal.sendBatchUserOp] Invalid 'to' address "${String(tx.to)}": must be a 42-character EVM address (0x + 40 hex chars)`,
1346
+ )
1347
+ }
1348
+ }
1349
+
1350
+ const traceId = data.traceId ?? generateTraceId()
1351
+
1352
+
1353
+ // Build each transaction sequentially. The iframe serializes all postMessages
1354
+ // via _portalMessageQueue, so Promise.all would only create the illusion of
1355
+ // parallelism while hiding failures from all-but-the-first transaction.
1356
+ const calls: UserOperationCall[] = []
1357
+ for (let index = 0; index < data.transactions.length; index++) {
1358
+ calls.push(
1359
+ await this.buildUserOpCall(
1360
+ data.chain,
1361
+ data.transactions[index],
1362
+ traceId,
1363
+ index,
1364
+ 'Portal.sendBatchUserOp',
1365
+ ),
1366
+ )
1367
+ }
1368
+
1369
+ // Build the batched UserOperation.
1370
+ let buildResponse: BuildBatchedUserOpResponse
1371
+ try {
1372
+ buildResponse = await this.mpc?.accountAbstractionBuildBatchedUserOp(
1373
+ { chain: data.chain, calls },
1374
+ traceId,
1375
+ )
1376
+ } catch (error) {
1377
+ throw new Error(
1378
+ `[Portal.sendBatchUserOp] Failed to build UserOperation: ${error instanceof Error ? error.message : String(error)}`,
1379
+ )
1380
+ }
1381
+
1382
+ // Validate, sign, and broadcast the built UserOperation, reusing the same
1383
+ // traceId for end-to-end correlation.
1384
+ return this.signAndBroadcastBuiltUserOp(
1385
+ buildResponse,
1386
+ data.chain,
1387
+ data.signatureApprovalMemo,
1388
+ traceId,
1389
+ 'Portal.sendBatchUserOp',
1390
+ )
1391
+ }
1392
+
1393
+ /**
1394
+ * Build, sign, and broadcast a gas-subsidized batch where the end user
1395
+ * reimburses the platform — in a fee token (e.g. USDC) — for the gas Portal's
1396
+ * paymaster sponsored.
1397
+ *
1398
+ * The flow is two-pass because the reimbursement amount depends on the gas of
1399
+ * the batch it's part of (a chicken-and-egg the helper resolves for you):
1400
+ * 1. build `[...userCalls, feeCallPlaceholder]` to read the estimated gas cost
1401
+ * (`metadata.estimatedGasCostWei`; the placeholder ensures the estimate
1402
+ * reflects the FINAL batch shape)
1403
+ * 2. convert that native gas cost → fee-token amount via your `convertGasToFeeAmount`
1404
+ * 3. build `[...userCalls, feeCall]` with the real amount, sign, broadcast
1405
+ *
1406
+ * Important characteristics:
1407
+ * - You are charging the build-time gas estimate (an upper bound), not the
1408
+ * post-execution actual — actual gas is unknowable before the op runs, and an
1409
+ * atomic batch must fix the reimbursement amount at sign time. Use `bufferBps`
1410
+ * to absorb gas-price drift between the two builds.
1411
+ * - Conversion (native → fee token) is entirely yours; Portal does no FX.
1412
+ * - Throws if the estimated gas cost is 0. Some chains/providers carry no
1413
+ * on-chain fee on the user operation (e.g. Ultra Relay bundler-level
1414
+ * sponsorship on Monad mainnet), so there's nothing to reimburse from; those
1415
+ * chains need a gas price sourced another way.
1416
+ *
1417
+ * @param data.chain - CAIP-2 chain ID; must start with 'eip155:'
1418
+ * @param data.transactions - The end user's actual transfers to execute
1419
+ * @param data.gasReimbursement - Fee token, recipient, and conversion callback
1420
+ * @param data.signatureApprovalMemo - Optional memo for the signing approval prompt
1421
+ * @param data.traceId - Optional trace ID for request correlation
1422
+ */
1423
+ public async sendBatchedAssets(
1424
+ data: SendBatchedAssetsRequest,
1425
+ ): Promise<BroadcastBatchedUserOpResponse> {
1426
+ const ctx = 'Portal.sendBatchedAssets'
1427
+
1428
+ if (!data.chain.startsWith('eip155:')) {
1429
+ throw new Error(
1430
+ `[${ctx}] UserOperations are only supported on EIP-155 (EVM) chains`,
1431
+ )
1432
+ }
1433
+ if (!data.transactions || data.transactions.length === 0) {
1434
+ throw new Error(
1435
+ `[${ctx}] transactions must contain at least one transaction`,
1436
+ )
1437
+ }
1438
+
1439
+ const gr = data.gasReimbursement
1440
+ if (!gr || typeof gr.convertGasToFeeAmount !== 'function') {
1441
+ throw new Error(
1442
+ `[${ctx}] gasReimbursement.convertGasToFeeAmount (a function) is required`,
1443
+ )
1444
+ }
1445
+ if (!gr.feeToken) {
1446
+ throw new Error(`[${ctx}] gasReimbursement.feeToken is required`)
1447
+ }
1448
+
1449
+ // Validate every destination address before starting any async work.
1450
+ const evmAddress = /^0x[0-9a-fA-F]{40}$/i
1451
+ for (const tx of data.transactions) {
1452
+ if (!tx.to || !evmAddress.test(tx.to)) {
1453
+ throw new Error(
1454
+ `[${ctx}] Invalid 'to' address "${String(tx.to)}": must be a 42-character EVM address (0x + 40 hex chars)`,
1455
+ )
1456
+ }
1457
+ }
1458
+ if (!gr.feeRecipient || !evmAddress.test(gr.feeRecipient)) {
1459
+ throw new Error(
1460
+ `[${ctx}] Invalid gasReimbursement.feeRecipient "${String(gr.feeRecipient)}": must be a 42-character EVM address (0x + 40 hex chars)`,
1461
+ )
1462
+ }
1463
+
1464
+ const traceId = data.traceId ?? generateTraceId()
1465
+
1466
+ // 1. Build the user's actual calls once — they don't change between passes.
1467
+ const userCalls: UserOperationCall[] = []
1468
+ for (let index = 0; index < data.transactions.length; index++) {
1469
+ userCalls.push(
1470
+ await this.buildUserOpCall(
1471
+ data.chain,
1472
+ data.transactions[index],
1473
+ traceId,
1474
+ index,
1475
+ ctx,
1476
+ ),
1477
+ )
1478
+ }
1479
+
1480
+ // 2. Build a placeholder fee call so the estimation pass reflects the final
1481
+ // batch shape (an N+1-call executeBatch). The amount does not affect the
1482
+ // gas estimate; only the call's presence and shape do.
1483
+ const feeIndex = data.transactions.length
1484
+ const placeholderAmount = gr.placeholderAmount ?? '0.01'
1485
+ let placeholderFeeCall: UserOperationCall
1486
+ try {
1487
+ placeholderFeeCall = await this.buildUserOpCall(
1488
+ data.chain,
1489
+ { token: gr.feeToken, value: placeholderAmount, to: gr.feeRecipient },
1490
+ traceId,
1491
+ feeIndex,
1492
+ ctx,
1493
+ )
1494
+ } catch (error) {
1495
+ throw new Error(
1496
+ `[${ctx}] Failed to build placeholder fee call: ${error instanceof Error ? error.message : String(error)}`,
1497
+ )
1498
+ }
1499
+
1500
+ // 3. Estimation pass — build the full batch to read the gas it's bounded by.
1501
+ let estimateResponse: BuildBatchedUserOpResponse
1502
+ try {
1503
+ estimateResponse = await this.mpc?.accountAbstractionBuildBatchedUserOp(
1504
+ { chain: data.chain, calls: [...userCalls, placeholderFeeCall] },
1505
+ traceId,
1506
+ )
1507
+ } catch (error) {
1508
+ throw new Error(
1509
+ `[${ctx}] Failed to estimate UserOperation gas: ${error instanceof Error ? error.message : String(error)}`,
1510
+ )
1511
+ }
1512
+
1513
+ let gasCostWei = this.resolveUserOpGasCostWei(estimateResponse, ctx)
1514
+
1515
+ // Guard against a zero gas cost. Some chains/providers return no on-chain fee
1516
+ // on the user operation (e.g. Ultra Relay bundler-level sponsorship on Monad
1517
+ // mainnet), so the build-time cost is 0 — there's nothing to derive a
1518
+ // reimbursement from. Fail loudly rather than silently charging the user 0.
1519
+ if (gasCostWei === BigInt(0)) {
1520
+ throw new Error(
1521
+ `[${ctx}] Estimated gas cost is 0 — this chain/provider carries no on-chain fee on the user operation (e.g. Ultra Relay bundler-level sponsorship). Cannot derive a reimbursement amount; supply the gas price another way for this chain.`,
1522
+ )
1523
+ }
1524
+
1525
+ // Apply optional buffer (basis points) to absorb gas-price drift before FX.
1526
+ if (gr.bufferBps && gr.bufferBps > 0) {
1527
+ gasCostWei =
1528
+ (gasCostWei * BigInt(10000 + Math.floor(gr.bufferBps))) / BigInt(10000)
1529
+ }
1530
+
1531
+ // 4. Platform-owned conversion: native gas cost (wei) → fee-token amount.
1532
+ let feeAmount: string
1533
+ try {
1534
+ feeAmount = await gr.convertGasToFeeAmount(gasCostWei)
1535
+ } catch (error) {
1536
+ throw new Error(
1537
+ `[${ctx}] gasReimbursement.convertGasToFeeAmount threw: ${error instanceof Error ? error.message : String(error)}`,
1538
+ )
1539
+ }
1540
+ if (typeof feeAmount !== 'string' || feeAmount.length === 0) {
1541
+ throw new Error(
1542
+ `[${ctx}] convertGasToFeeAmount must return a non-empty amount string, got "${String(feeAmount)}"`,
1543
+ )
1544
+ }
1545
+
1546
+ // 5. Build the real fee call with the converted amount.
1547
+ let feeCall: UserOperationCall
1548
+ try {
1549
+ feeCall = await this.buildUserOpCall(
1550
+ data.chain,
1551
+ { token: gr.feeToken, value: feeAmount, to: gr.feeRecipient },
1552
+ traceId,
1553
+ feeIndex,
1554
+ ctx,
1555
+ )
1556
+ } catch (error) {
1557
+ throw new Error(
1558
+ `[${ctx}] Failed to build fee reimbursement call: ${error instanceof Error ? error.message : String(error)}`,
1559
+ )
1560
+ }
1561
+
1562
+ // 6. Final pass — build the batch we'll actually sign and broadcast.
1563
+ let buildResponse: BuildBatchedUserOpResponse
1564
+ try {
1565
+ buildResponse = await this.mpc?.accountAbstractionBuildBatchedUserOp(
1566
+ { chain: data.chain, calls: [...userCalls, feeCall] },
1567
+ traceId,
1568
+ )
1569
+ } catch (error) {
1570
+ throw new Error(
1571
+ `[${ctx}] Failed to build UserOperation: ${error instanceof Error ? error.message : String(error)}`,
1572
+ )
1573
+ }
1574
+
1575
+ return this.signAndBroadcastBuiltUserOp(
1576
+ buildResponse,
1577
+ data.chain,
1578
+ data.signatureApprovalMemo,
1579
+ traceId,
1580
+ ctx,
1581
+ )
1582
+ }
1583
+
1584
+ /**
1585
+ * Build a single ERC-4337 call from a high-level transfer descriptor by routing
1586
+ * it through `buildTransaction` (which resolves token contract + calldata and
1587
+ * normalizes the amount). Native transfers carry `value` (the base-unit amount);
1588
+ * ERC-20 transfers carry `data` and omit `value`.
1589
+ */
1590
+ private async buildUserOpCall(
1591
+ chain: string,
1592
+ tx: SendBatchUserOpTransaction,
1593
+ traceId: string,
1594
+ index: number,
1595
+ ctx: string,
1596
+ ): Promise<UserOperationCall> {
1597
+ let builtTx: BuiltTransaction
1598
+ try {
1599
+ builtTx = await this.buildTransaction(
1600
+ chain,
1601
+ tx.to,
1602
+ tx.token,
1603
+ tx.value,
1604
+ traceId,
1605
+ )
1606
+ } catch (error) {
1607
+ throw new Error(
1608
+ `[${ctx}] Failed to build call for transaction at index ${index}: ${error instanceof Error ? error.message : String(error)}`,
1609
+ )
1610
+ }
1611
+
1612
+ // Validate the built transaction shape before casting. buildTransaction
1613
+ // returns BuiltTransaction (a union type), so the cast is TypeScript-only and
1614
+ // provides no runtime safety against a changed or unexpected backend response.
1615
+ const rawTx = builtTx as {
1616
+ transaction?: Record<string, unknown>
1617
+ metadata?: Record<string, unknown>
1618
+ }
1619
+ if (typeof rawTx?.transaction?.to !== 'string' || !rawTx.transaction.to) {
1620
+ throw new Error(
1621
+ `[${ctx}] buildTransaction returned unexpected shape for transaction at index ${index}`,
1622
+ )
1623
+ }
1624
+ const eip155Tx = builtTx as BuiltEip155Transaction
1625
+
1626
+ // Normalize the data field — guard against undefined/null/empty string from
1627
+ // the backend. Empty data means no calldata (native transfer). rawAmount is
1628
+ // passed as-is (decimal string e.g. '1000000000000000000'); the AA backend's
1629
+ // build-user-operation endpoint accepts decimal or hex.
1630
+ const txData = eip155Tx.transaction.data || '0x'
1631
+ const isNativeTransfer = txData === '0x'
1632
+ return {
1633
+ to: eip155Tx.transaction.to,
1634
+ data: txData,
1635
+ ...(isNativeTransfer ? { value: eip155Tx.metadata.rawAmount } : {}),
1636
+ }
1637
+ }
1638
+
1639
+ /**
1640
+ * Resolve the native gas cost (in wei) a built UserOperation is bounded by,
1641
+ * from the backend-computed `metadata.estimatedGasCostWei` (`totalGas *
1642
+ * maxFeePerGas`, an upper bound). Throws if the field is absent — the
1643
+ * connect-api build-user-operation gas-cost change must be deployed.
1644
+ */
1645
+ private resolveUserOpGasCostWei(
1646
+ buildResponse: BuildBatchedUserOpResponse,
1647
+ ctx: string,
1648
+ ): bigint {
1649
+ const wei = buildResponse?.metadata?.estimatedGasCostWei
1650
+ if (wei == null || `${wei}`.length === 0) {
1651
+ throw new Error(
1652
+ `[${ctx}] build response is missing metadata.estimatedGasCostWei; the connect-api build-user-operation gas-cost change must be deployed for the target environment`,
1653
+ )
1654
+ }
1655
+ return this.toBigIntOrThrow(wei, 'metadata.estimatedGasCostWei', ctx)
1656
+ }
1657
+
1658
+ private toBigIntOrThrow(value: unknown, field: string, ctx: string): bigint {
1659
+ try {
1660
+ // BigInt() accepts both decimal ('1000000000') and hex ('0x3b9aca00') strings.
1661
+ return BigInt(value as string | number)
1662
+ } catch {
1663
+ throw new Error(
1664
+ `[${ctx}] Could not parse ${field} as an integer: "${String(value)}"`,
1665
+ )
1666
+ }
1667
+ }
1668
+
1669
+ /**
1670
+ * Validate a built UserOperation, sign its hash with the SECP256K1 raw signer,
1671
+ * and broadcast it. Shared by the batched-UserOp send paths.
1672
+ */
1673
+ private async signAndBroadcastBuiltUserOp(
1674
+ buildResponse: BuildBatchedUserOpResponse,
1675
+ chain: string,
1676
+ signatureApprovalMemo: string | undefined,
1677
+ traceId: string,
1678
+ ctx: string,
1679
+ ): Promise<BroadcastBatchedUserOpResponse> {
1680
+ const { userOperation, userOpHash } = buildResponse.data
1681
+
1682
+ // Validate userOperation is parseable JSON before signing or broadcasting.
1683
+ // A malformed string from the backend would produce an opaque bundler rejection.
1684
+ try {
1685
+ const parsed: unknown = JSON.parse(userOperation)
1686
+ if (!parsed || typeof parsed !== 'object') {
1687
+ throw new Error('parsed value is not a JSON object')
1688
+ }
1689
+ } catch (e) {
1690
+ throw new Error(
1691
+ `[${ctx}] buildBatchedUserOp returned an invalid userOperation: ${e instanceof Error ? e.message : String(e)}`,
1692
+ )
1693
+ }
1694
+
1695
+ // Validate userOpHash is a valid 32-byte hex string before signing.
1696
+ // Signing an empty or malformed hash wastes the signing operation and produces
1697
+ // a signature that no bundler will accept.
1698
+ if (!userOpHash || !/^(0x)?[0-9a-fA-F]{64}$/.test(userOpHash)) {
1699
+ throw new Error(
1700
+ `[${ctx}] Invalid userOpHash received from buildBatchedUserOp: "${String(userOpHash)}"`,
1701
+ )
1702
+ }
1703
+
1704
+ // Sign the userOpHash — strip the 0x prefix before passing to rawSign.
1705
+ let signature: string
1706
+ try {
1707
+ const hashToSign = userOpHash.startsWith('0x')
1708
+ ? userOpHash.slice(2)
1709
+ : userOpHash
1710
+ signature = await this.rawSign(PortalCurve.SECP256K1, hashToSign, {
1711
+ signatureApprovalMemo,
1712
+ traceId,
1713
+ })
1714
+ } catch (error) {
1715
+ throw new Error(
1716
+ `[${ctx}] Failed to sign userOpHash: ${error instanceof Error ? error.message : String(error)}`,
1717
+ )
1718
+ }
1719
+
1720
+ // Broadcast the signed UserOperation, reusing the same traceId for end-to-end correlation.
1721
+ try {
1722
+ return await this.mpc?.accountAbstractionBroadcastBatchedUserOp(
1723
+ { chain, userOperation, signature },
1724
+ traceId,
1725
+ )
1726
+ } catch (error) {
1727
+ throw new Error(
1728
+ `[${ctx}] Failed to broadcast UserOperation: ${error instanceof Error ? error.message : String(error)}`,
1729
+ )
1730
+ }
1731
+ }
1732
+
1288
1733
  /*******************************
1289
1734
  * Swaps Methods
1290
1735
  *******************************/
@@ -1551,6 +1996,15 @@ export {
1551
1996
  type TransactionHistoryItem,
1552
1997
  type SolanaTransactionDetails,
1553
1998
  type Transaction,
1999
+ type UserOperationCall,
2000
+ type BuildBatchedUserOpRequest,
2001
+ type BuildBatchedUserOpResponse,
2002
+ type BroadcastBatchedUserOpRequest,
2003
+ type BroadcastBatchedUserOpResponse,
2004
+ type SendBatchUserOpTransaction,
2005
+ type SendBatchUserOpRequest,
2006
+ type GasReimbursement,
2007
+ type SendBatchedAssetsRequest,
1554
2008
  } from './shared/types'
1555
2009
 
1556
2010
  export type {
@@ -80,6 +80,10 @@ import {
80
80
  mockBlockaidScanTokensResponse,
81
81
  mockBlockaidScanUrlRequest,
82
82
  mockBlockaidScanUrlResponse,
83
+ mockBuildBatchedUserOpRequest,
84
+ mockBuildBatchedUserOpResponse,
85
+ mockBroadcastBatchedUserOpRequest,
86
+ mockBroadcastBatchedUserOpResponse,
83
87
  } from '../__mocks/constants'
84
88
  import portalMock from '../__mocks/portal/portal'
85
89
  import { PortalMpcError } from './errors'
@@ -4447,4 +4451,164 @@ describe('Mpc', () => {
4447
4451
  })
4448
4452
  })
4449
4453
  })
4454
+
4455
+ describe('accountAbstractionBuildBatchedUserOp', () => {
4456
+ const args = mockBuildBatchedUserOpRequest
4457
+ const res = mockBuildBatchedUserOpResponse
4458
+
4459
+ it('should successfully build a user operation', (done) => {
4460
+ jest
4461
+ .spyOn(mpc.iframe?.contentWindow!, 'postMessage')
4462
+ .mockImplementation((message: any, origin?) => {
4463
+ const { type, data } = message
4464
+
4465
+ expect(type).toEqual('portal:accountAbstraction:buildBatchedUserOp')
4466
+ expect(data).toMatchObject(args)
4467
+ expect(typeof message.traceId).toBe('string')
4468
+ expect(origin).toEqual(mockHostOrigin)
4469
+
4470
+ window.dispatchEvent(
4471
+ new MessageEvent('message', {
4472
+ origin: mockHostOrigin,
4473
+ data: {
4474
+ type: 'portal:accountAbstraction:buildBatchedUserOpResult',
4475
+ data: res,
4476
+ },
4477
+ }),
4478
+ )
4479
+ })
4480
+
4481
+ mpc
4482
+ .accountAbstractionBuildBatchedUserOp(args)
4483
+ .then((data) => {
4484
+ expect(data).toEqual(res)
4485
+ done()
4486
+ })
4487
+ .catch((_) => {
4488
+ expect(0).toEqual(1)
4489
+ done()
4490
+ })
4491
+ })
4492
+
4493
+ it('should error out if the iframe sends an error message', (done) => {
4494
+ jest
4495
+ .spyOn(mpc.iframe?.contentWindow!, 'postMessage')
4496
+ .mockImplementationOnce((message: any, origin?) => {
4497
+ const { type, data } = message
4498
+
4499
+ expect(type).toEqual('portal:accountAbstraction:buildBatchedUserOp')
4500
+ expect(data).toMatchObject(args)
4501
+ expect(typeof message.traceId).toBe('string')
4502
+ expect(origin).toEqual(mockHostOrigin)
4503
+
4504
+ window.dispatchEvent(
4505
+ new MessageEvent('message', {
4506
+ origin: mockHostOrigin,
4507
+ data: {
4508
+ type: 'portal:accountAbstraction:buildBatchedUserOpError',
4509
+ data: {
4510
+ code: 1,
4511
+ message: 'test',
4512
+ },
4513
+ },
4514
+ }),
4515
+ )
4516
+ })
4517
+
4518
+ mpc
4519
+ .accountAbstractionBuildBatchedUserOp(args)
4520
+ .then(() => {
4521
+ expect(0).toEqual(1)
4522
+ done()
4523
+ })
4524
+ .catch((e) => {
4525
+ expect(e).toBeInstanceOf(PortalMpcError)
4526
+ expect(e.message).toEqual('test')
4527
+ expect(e.code).toEqual(1)
4528
+ done()
4529
+ })
4530
+ })
4531
+ })
4532
+
4533
+ describe('accountAbstractionBroadcastBatchedUserOp', () => {
4534
+ const args = mockBroadcastBatchedUserOpRequest
4535
+ const res = mockBroadcastBatchedUserOpResponse
4536
+
4537
+ it('should successfully broadcast a user operation', (done) => {
4538
+ jest
4539
+ .spyOn(mpc.iframe?.contentWindow!, 'postMessage')
4540
+ .mockImplementation((message: any, origin?) => {
4541
+ const { type, data } = message
4542
+
4543
+ expect(type).toEqual(
4544
+ 'portal:accountAbstraction:broadcastBatchedUserOp',
4545
+ )
4546
+ expect(data).toMatchObject(args)
4547
+ expect(typeof message.traceId).toBe('string')
4548
+ expect(origin).toEqual(mockHostOrigin)
4549
+
4550
+ window.dispatchEvent(
4551
+ new MessageEvent('message', {
4552
+ origin: mockHostOrigin,
4553
+ data: {
4554
+ type: 'portal:accountAbstraction:broadcastBatchedUserOpResult',
4555
+ data: res,
4556
+ },
4557
+ }),
4558
+ )
4559
+ })
4560
+
4561
+ mpc
4562
+ .accountAbstractionBroadcastBatchedUserOp(args)
4563
+ .then((data) => {
4564
+ expect(data).toEqual(res)
4565
+ done()
4566
+ })
4567
+ .catch((_) => {
4568
+ expect(0).toEqual(1)
4569
+ done()
4570
+ })
4571
+ })
4572
+
4573
+ it('should error out if the iframe sends an error message', (done) => {
4574
+ jest
4575
+ .spyOn(mpc.iframe?.contentWindow!, 'postMessage')
4576
+ .mockImplementationOnce((message: any, origin?) => {
4577
+ const { type, data } = message
4578
+
4579
+ expect(type).toEqual(
4580
+ 'portal:accountAbstraction:broadcastBatchedUserOp',
4581
+ )
4582
+ expect(data).toMatchObject(args)
4583
+ expect(typeof message.traceId).toBe('string')
4584
+ expect(origin).toEqual(mockHostOrigin)
4585
+
4586
+ window.dispatchEvent(
4587
+ new MessageEvent('message', {
4588
+ origin: mockHostOrigin,
4589
+ data: {
4590
+ type: 'portal:accountAbstraction:broadcastBatchedUserOpError',
4591
+ data: {
4592
+ code: 1,
4593
+ message: 'test',
4594
+ },
4595
+ },
4596
+ }),
4597
+ )
4598
+ })
4599
+
4600
+ mpc
4601
+ .accountAbstractionBroadcastBatchedUserOp(args)
4602
+ .then(() => {
4603
+ expect(0).toEqual(1)
4604
+ done()
4605
+ })
4606
+ .catch((e) => {
4607
+ expect(e).toBeInstanceOf(PortalMpcError)
4608
+ expect(e.message).toEqual('test')
4609
+ expect(e.code).toEqual(1)
4610
+ done()
4611
+ })
4612
+ })
4613
+ })
4450
4614
  })