@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.
@@ -10,8 +10,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  step((generator = generator.apply(thisArg, _arguments || [])).next());
11
11
  });
12
12
  };
13
- import Portal, { BackupMethods, GetTransactionsOrder } from '.';
14
- import { mockAddress, mockBackupConfig, mockBlockHashResponse, mockBuiltTronTransaction, mockCipherText, mockClientResponse, mockEip155Address, mockEjectResult, mockEjectPrivateKeysResult, mockEthRpcUrl, mockEthTransaction, mockMpcBackupResponse, mockOrgBackupShare, mockOrgBackupShares, mockQuoteArgs, mockRpcConfig, mockSharesOnDevice, mockSignedHash, mockSolanaAddress, mockSolRpcUrl, mockTronAddress, } from './__mocks/constants';
13
+ import Portal, { BackupMethods, GetTransactionsOrder, PortalCurve } from '.';
14
+ import { mockAddress, mockBackupConfig, mockBlockHashResponse, mockBuiltEip155Transaction, mockBuiltEip155TransactionNative, mockBuiltTronTransaction, mockBuildBatchedUserOpRequest, mockBuildBatchedUserOpResponse, mockBroadcastBatchedUserOpRequest, mockBroadcastBatchedUserOpResponse, mockCipherText, mockClientResponse, mockEip155Address, mockEjectResult, mockEjectPrivateKeysResult, mockEthRpcUrl, mockEthTransaction, mockMpcBackupResponse, mockOrgBackupShare, mockOrgBackupShares, mockQuoteArgs, mockRpcConfig, mockSendBatchUserOpRequest, mockSharesOnDevice, mockSignedHash, mockSolanaAddress, mockSolRpcUrl, mockTronAddress, } from './__mocks/constants';
15
15
  import mpcMock from './__mocks/portal/mpc';
16
16
  import providerMock from './__mocks/portal/provider';
17
17
  /**
@@ -719,6 +719,237 @@ describe('Portal', () => {
719
719
  expect(portal.mpc.getSources).toHaveBeenCalledWith('eip155:1', 'test');
720
720
  }));
721
721
  });
722
+ describe('buildBatchedUserOp', () => {
723
+ it('should correctly call mpc.accountAbstractionBuildBatchedUserOp', () => __awaiter(void 0, void 0, void 0, function* () {
724
+ yield portal.buildBatchedUserOp(mockBuildBatchedUserOpRequest);
725
+ expect(portal.mpc.accountAbstractionBuildBatchedUserOp).toHaveBeenCalledTimes(1);
726
+ expect(portal.mpc.accountAbstractionBuildBatchedUserOp).toHaveBeenCalledWith(mockBuildBatchedUserOpRequest);
727
+ }));
728
+ });
729
+ describe('broadcastBatchedUserOp', () => {
730
+ it('should correctly call mpc.accountAbstractionBroadcastBatchedUserOp', () => __awaiter(void 0, void 0, void 0, function* () {
731
+ yield portal.broadcastBatchedUserOp(mockBroadcastBatchedUserOpRequest);
732
+ expect(portal.mpc.accountAbstractionBroadcastBatchedUserOp).toHaveBeenCalledTimes(1);
733
+ expect(portal.mpc.accountAbstractionBroadcastBatchedUserOp).toHaveBeenCalledWith(mockBroadcastBatchedUserOpRequest);
734
+ }));
735
+ });
736
+ describe('sendBatchUserOp', () => {
737
+ it('should build, sign, and broadcast a batched UserOperation', () => __awaiter(void 0, void 0, void 0, function* () {
738
+ const result = yield portal.sendBatchUserOp(mockSendBatchUserOpRequest);
739
+ expect(result).toEqual(expect.objectContaining({
740
+ data: expect.objectContaining({ userOpHash: expect.any(String) }),
741
+ metadata: expect.objectContaining({ chainId: expect.any(String) }),
742
+ }));
743
+ // buildTransaction called once per transaction descriptor
744
+ expect(portal.mpc.buildTransaction).toHaveBeenCalledTimes(2);
745
+ expect(portal.mpc.buildTransaction).toHaveBeenCalledWith(mockSendBatchUserOpRequest.chain, mockSendBatchUserOpRequest.transactions[0].to, mockSendBatchUserOpRequest.transactions[0].token, mockSendBatchUserOpRequest.transactions[0].value, expect.any(String));
746
+ expect(portal.mpc.buildTransaction).toHaveBeenCalledWith(mockSendBatchUserOpRequest.chain, mockSendBatchUserOpRequest.transactions[1].to, mockSendBatchUserOpRequest.transactions[1].token, mockSendBatchUserOpRequest.transactions[1].value, expect.any(String));
747
+ // buildBatchedUserOp called with ERC-20 call shape (no value field) and the shared traceId
748
+ expect(portal.mpc.accountAbstractionBuildBatchedUserOp).toHaveBeenCalledTimes(1);
749
+ expect(portal.mpc.accountAbstractionBuildBatchedUserOp).toHaveBeenCalledWith({
750
+ chain: mockSendBatchUserOpRequest.chain,
751
+ calls: [
752
+ {
753
+ to: mockBuiltEip155Transaction.transaction.to,
754
+ data: mockBuiltEip155Transaction.transaction.data,
755
+ },
756
+ {
757
+ to: mockBuiltEip155Transaction.transaction.to,
758
+ data: mockBuiltEip155Transaction.transaction.data,
759
+ },
760
+ ],
761
+ }, expect.any(String));
762
+ // rawSign called with SECP256K1 and the userOpHash with 0x prefix stripped
763
+ expect(portal.mpc.rawSign).toHaveBeenCalledTimes(1);
764
+ expect(portal.mpc.rawSign).toHaveBeenCalledWith(PortalCurve.SECP256K1, mockBuildBatchedUserOpResponse.data.userOpHash.slice(2), { signatureApprovalMemo: undefined, traceId: expect.any(String) });
765
+ // broadcastBatchedUserOp called with the built userOperation and signature, and the shared traceId
766
+ expect(portal.mpc.accountAbstractionBroadcastBatchedUserOp).toHaveBeenCalledTimes(1);
767
+ expect(portal.mpc.accountAbstractionBroadcastBatchedUserOp).toHaveBeenCalledWith({
768
+ chain: mockSendBatchUserOpRequest.chain,
769
+ userOperation: mockBuildBatchedUserOpResponse.data.userOperation,
770
+ signature: mockSignedHash,
771
+ }, expect.any(String));
772
+ }));
773
+ it('should propagate the same traceId to buildBatchedUserOp and broadcastBatchedUserOp', () => __awaiter(void 0, void 0, void 0, function* () {
774
+ yield portal.sendBatchUserOp(mockSendBatchUserOpRequest);
775
+ const buildTraceId = portal.mpc.accountAbstractionBuildBatchedUserOp.mock.calls[0][1];
776
+ const broadcastTraceId = portal.mpc.accountAbstractionBroadcastBatchedUserOp.mock.calls[0][1];
777
+ expect(typeof buildTraceId).toBe('string');
778
+ expect(buildTraceId).toEqual(broadcastTraceId);
779
+ }));
780
+ it('should include value in the call for native ETH transfers', () => __awaiter(void 0, void 0, void 0, function* () {
781
+ ;
782
+ portal.mpc.buildTransaction.mockResolvedValueOnce(mockBuiltEip155TransactionNative);
783
+ yield portal.sendBatchUserOp({
784
+ chain: 'eip155:1',
785
+ transactions: [{ token: 'ETH', value: '0.1', to: '0x1111111111111111111111111111111111111111' }],
786
+ });
787
+ expect(portal.mpc.accountAbstractionBuildBatchedUserOp).toHaveBeenCalledWith({
788
+ chain: 'eip155:1',
789
+ calls: [
790
+ {
791
+ to: mockBuiltEip155TransactionNative.transaction.to,
792
+ data: '0x',
793
+ value: mockBuiltEip155TransactionNative.metadata.rawAmount,
794
+ },
795
+ ],
796
+ }, expect.any(String));
797
+ }));
798
+ it('should not include value in the call for ERC-20 transfers', () => __awaiter(void 0, void 0, void 0, function* () {
799
+ yield portal.sendBatchUserOp({
800
+ chain: 'eip155:1',
801
+ transactions: [{ token: 'USDC', value: '1.0', to: '0x1111111111111111111111111111111111111111' }],
802
+ });
803
+ const calls = portal.mpc.accountAbstractionBuildBatchedUserOp.mock.calls[0][0].calls;
804
+ expect(calls[0]).not.toHaveProperty('value');
805
+ }));
806
+ it('should forward signatureApprovalMemo to rawSign', () => __awaiter(void 0, void 0, void 0, function* () {
807
+ yield portal.sendBatchUserOp(Object.assign(Object.assign({}, mockSendBatchUserOpRequest), { signatureApprovalMemo: 'approve this batch' }));
808
+ expect(portal.mpc.rawSign).toHaveBeenCalledWith(PortalCurve.SECP256K1, expect.any(String), { signatureApprovalMemo: 'approve this batch', traceId: expect.any(String) });
809
+ }));
810
+ it('should throw if chain is not eip155', () => __awaiter(void 0, void 0, void 0, function* () {
811
+ yield expect(portal.sendBatchUserOp(Object.assign(Object.assign({}, mockSendBatchUserOpRequest), { chain: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' }))).rejects.toThrow('[Portal.sendBatchUserOp] UserOperations are only supported on EIP-155 (EVM) chains');
812
+ }));
813
+ it('should throw if transactions array is empty', () => __awaiter(void 0, void 0, void 0, function* () {
814
+ yield expect(portal.sendBatchUserOp(Object.assign(Object.assign({}, mockSendBatchUserOpRequest), { transactions: [] }))).rejects.toThrow('[Portal.sendBatchUserOp] transactions must contain at least one transaction');
815
+ }));
816
+ it('should throw with index context if buildTransaction fails for transaction 0', () => __awaiter(void 0, void 0, void 0, function* () {
817
+ ;
818
+ portal.mpc.buildTransaction.mockRejectedValueOnce(new Error('Network error'));
819
+ yield expect(portal.sendBatchUserOp({
820
+ chain: 'eip155:1',
821
+ transactions: [{ token: 'USDC', value: '1.0', to: '0x1111111111111111111111111111111111111111' }],
822
+ })).rejects.toThrow('[Portal.sendBatchUserOp] Failed to build call for transaction at index 0: Network error');
823
+ }));
824
+ it('should throw with index context if buildTransaction fails for transaction 1', () => __awaiter(void 0, void 0, void 0, function* () {
825
+ ;
826
+ portal.mpc.buildTransaction
827
+ .mockResolvedValueOnce(mockBuiltEip155Transaction)
828
+ .mockRejectedValueOnce(new Error('Network error'));
829
+ yield expect(portal.sendBatchUserOp(mockSendBatchUserOpRequest)).rejects.toThrow('[Portal.sendBatchUserOp] Failed to build call for transaction at index 1: Network error');
830
+ }));
831
+ it('should throw if buildBatchedUserOp fails', () => __awaiter(void 0, void 0, void 0, function* () {
832
+ ;
833
+ portal.mpc.accountAbstractionBuildBatchedUserOp.mockRejectedValueOnce(new Error('UserOp build failed'));
834
+ yield expect(portal.sendBatchUserOp(mockSendBatchUserOpRequest)).rejects.toThrow('[Portal.sendBatchUserOp] Failed to build UserOperation: UserOp build failed');
835
+ }));
836
+ it('should throw if rawSign fails', () => __awaiter(void 0, void 0, void 0, function* () {
837
+ ;
838
+ portal.mpc.rawSign.mockRejectedValueOnce(new Error('Signing failed'));
839
+ yield expect(portal.sendBatchUserOp(mockSendBatchUserOpRequest)).rejects.toThrow('[Portal.sendBatchUserOp] Failed to sign userOpHash: Signing failed');
840
+ }));
841
+ it('should throw if broadcastBatchedUserOp fails', () => __awaiter(void 0, void 0, void 0, function* () {
842
+ ;
843
+ portal.mpc.accountAbstractionBroadcastBatchedUserOp.mockRejectedValueOnce(new Error('Broadcast failed'));
844
+ yield expect(portal.sendBatchUserOp(mockSendBatchUserOpRequest)).rejects.toThrow('[Portal.sendBatchUserOp] Failed to broadcast UserOperation: Broadcast failed');
845
+ }));
846
+ });
847
+ describe('sendBatchedAssets', () => {
848
+ const feeRecipient = '0x2222222222222222222222222222222222222222';
849
+ const baseRequest = {
850
+ chain: 'eip155:1',
851
+ transactions: [
852
+ {
853
+ token: 'USDC',
854
+ value: '1.0',
855
+ to: '0x1111111111111111111111111111111111111111',
856
+ },
857
+ ],
858
+ };
859
+ it('should build twice, convert gas to a fee, sign, and broadcast', () => __awaiter(void 0, void 0, void 0, function* () {
860
+ const convertGasToFeeAmount = jest.fn().mockResolvedValue('0.5');
861
+ const result = yield portal.sendBatchedAssets(Object.assign(Object.assign({}, baseRequest), { gasReimbursement: { feeToken: 'USDC', feeRecipient, convertGasToFeeAmount } }));
862
+ expect(result).toEqual(mockBroadcastBatchedUserOpResponse);
863
+ // Estimation pass + final pass = 2 builds.
864
+ expect(portal.mpc.accountAbstractionBuildBatchedUserOp).toHaveBeenCalledTimes(2);
865
+ // buildTransaction: 1 user tx + 1 placeholder fee + 1 real fee = 3.
866
+ expect(portal.mpc.buildTransaction).toHaveBeenCalledTimes(3);
867
+ // Conversion called once with gasCostWei = totalGas(100000) * maxFeePerGas(1e9).
868
+ expect(convertGasToFeeAmount).toHaveBeenCalledTimes(1);
869
+ expect(convertGasToFeeAmount).toHaveBeenCalledWith(BigInt('100000000000000'));
870
+ // Placeholder fee call built with the default '0.01'; real fee call with '0.5'.
871
+ expect(portal.mpc.buildTransaction).toHaveBeenCalledWith('eip155:1', feeRecipient, 'USDC', '0.01', expect.any(String));
872
+ expect(portal.mpc.buildTransaction).toHaveBeenCalledWith('eip155:1', feeRecipient, 'USDC', '0.5', expect.any(String));
873
+ expect(portal.mpc.rawSign).toHaveBeenCalledTimes(1);
874
+ expect(portal.mpc.accountAbstractionBroadcastBatchedUserOp).toHaveBeenCalledTimes(1);
875
+ }));
876
+ it('should apply bufferBps to the gas cost before conversion', () => __awaiter(void 0, void 0, void 0, function* () {
877
+ const convertGasToFeeAmount = jest.fn().mockResolvedValue('0.6');
878
+ yield portal.sendBatchedAssets(Object.assign(Object.assign({}, baseRequest), { gasReimbursement: {
879
+ feeToken: 'USDC',
880
+ feeRecipient,
881
+ convertGasToFeeAmount,
882
+ bufferBps: 1000, // +10%
883
+ } }));
884
+ // 1e14 * 11000 / 10000 = 1.1e14
885
+ expect(convertGasToFeeAmount).toHaveBeenCalledWith(BigInt('110000000000000'));
886
+ }));
887
+ it('should throw if the build response is missing estimatedGasCostWei', () => __awaiter(void 0, void 0, void 0, function* () {
888
+ const noGasCost = Object.assign(Object.assign({}, mockBuildBatchedUserOpResponse), { metadata: { chainId: 'eip155:1' } });
889
+ portal.mpc.accountAbstractionBuildBatchedUserOp.mockResolvedValueOnce(noGasCost);
890
+ const convertGasToFeeAmount = jest.fn();
891
+ yield expect(portal.sendBatchedAssets(Object.assign(Object.assign({}, baseRequest), { gasReimbursement: { feeToken: 'USDC', feeRecipient, convertGasToFeeAmount } }))).rejects.toThrow('[Portal.sendBatchedAssets] build response is missing metadata.estimatedGasCostWei');
892
+ // Conversion should never run if we can't determine the gas cost.
893
+ expect(convertGasToFeeAmount).not.toHaveBeenCalled();
894
+ }));
895
+ it('should throw if the estimated gas cost is 0 (e.g. Ultra Relay zero-fee path)', () => __awaiter(void 0, void 0, void 0, function* () {
896
+ const zeroCost = Object.assign(Object.assign({}, mockBuildBatchedUserOpResponse), { metadata: {
897
+ chainId: 'eip155:1',
898
+ totalGas: '2000000',
899
+ maxFeePerGas: '0',
900
+ estimatedGasCostWei: '0',
901
+ } });
902
+ portal.mpc.accountAbstractionBuildBatchedUserOp.mockResolvedValueOnce(zeroCost);
903
+ const convertGasToFeeAmount = jest.fn();
904
+ yield expect(portal.sendBatchedAssets(Object.assign(Object.assign({}, baseRequest), { gasReimbursement: { feeToken: 'USDC', feeRecipient, convertGasToFeeAmount } }))).rejects.toThrow('[Portal.sendBatchedAssets] Estimated gas cost is 0');
905
+ expect(convertGasToFeeAmount).not.toHaveBeenCalled();
906
+ }));
907
+ it('should reuse the same traceId across both builds', () => __awaiter(void 0, void 0, void 0, function* () {
908
+ const convertGasToFeeAmount = jest.fn().mockResolvedValue('0.5');
909
+ yield portal.sendBatchedAssets(Object.assign(Object.assign({}, baseRequest), { gasReimbursement: { feeToken: 'USDC', feeRecipient, convertGasToFeeAmount } }));
910
+ const calls = portal.mpc.accountAbstractionBuildBatchedUserOp.mock.calls;
911
+ expect(typeof calls[0][1]).toBe('string');
912
+ expect(calls[0][1]).toEqual(calls[1][1]);
913
+ }));
914
+ it('should throw if chain is not eip155', () => __awaiter(void 0, void 0, void 0, function* () {
915
+ yield expect(portal.sendBatchedAssets({
916
+ chain: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
917
+ transactions: baseRequest.transactions,
918
+ gasReimbursement: {
919
+ feeToken: 'USDC',
920
+ feeRecipient,
921
+ convertGasToFeeAmount: () => '1',
922
+ },
923
+ })).rejects.toThrow('[Portal.sendBatchedAssets] UserOperations are only supported on EIP-155 (EVM) chains');
924
+ }));
925
+ it('should throw if transactions is empty', () => __awaiter(void 0, void 0, void 0, function* () {
926
+ yield expect(portal.sendBatchedAssets({
927
+ chain: 'eip155:1',
928
+ transactions: [],
929
+ gasReimbursement: {
930
+ feeToken: 'USDC',
931
+ feeRecipient,
932
+ convertGasToFeeAmount: () => '1',
933
+ },
934
+ })).rejects.toThrow('[Portal.sendBatchedAssets] transactions must contain at least one transaction');
935
+ }));
936
+ it('should throw if convertGasToFeeAmount is missing', () => __awaiter(void 0, void 0, void 0, function* () {
937
+ yield expect(portal.sendBatchedAssets(Object.assign(Object.assign({}, baseRequest), { gasReimbursement: { feeToken: 'USDC', feeRecipient } }))).rejects.toThrow('[Portal.sendBatchedAssets] gasReimbursement.convertGasToFeeAmount (a function) is required');
938
+ }));
939
+ it('should throw if feeRecipient is not a valid EVM address', () => __awaiter(void 0, void 0, void 0, function* () {
940
+ yield expect(portal.sendBatchedAssets(Object.assign(Object.assign({}, baseRequest), { gasReimbursement: {
941
+ feeToken: 'USDC',
942
+ feeRecipient: '0xnope',
943
+ convertGasToFeeAmount: () => '1',
944
+ } }))).rejects.toThrow('[Portal.sendBatchedAssets] Invalid gasReimbursement.feeRecipient');
945
+ }));
946
+ it('should surface an error thrown by the conversion callback', () => __awaiter(void 0, void 0, void 0, function* () {
947
+ const convertGasToFeeAmount = jest
948
+ .fn()
949
+ .mockRejectedValue(new Error('rate unavailable'));
950
+ yield expect(portal.sendBatchedAssets(Object.assign(Object.assign({}, baseRequest), { gasReimbursement: { feeToken: 'USDC', feeRecipient, convertGasToFeeAmount } }))).rejects.toThrow('[Portal.sendBatchedAssets] gasReimbursement.convertGasToFeeAmount threw: rate unavailable');
951
+ }));
952
+ });
722
953
  describe('storedClientBackupShare', () => {
723
954
  it('should correctly call mpc.storedClientBackupShare', () => __awaiter(void 0, void 0, void 0, function* () {
724
955
  yield portal.storedClientBackupShare(true, BackupMethods.password);
@@ -11,7 +11,7 @@ import { PortalMpcError } from './errors';
11
11
  import { sdkLogger } from '../logger';
12
12
  import { BackupMethods, } from '../index';
13
13
  import { generateTraceId } from '../shared/trace';
14
- const WEB_SDK_VERSION = '3.16.0';
14
+ const WEB_SDK_VERSION = '3.17.0-alpha.0';
15
15
  class Mpc {
16
16
  get ready() {
17
17
  return this._ready;
@@ -226,6 +226,7 @@ class Mpc {
226
226
  param }, ((options === null || options === void 0 ? void 0 : options.signatureApprovalMemo) !== undefined && {
227
227
  signatureApprovalMemo: options.signatureApprovalMemo,
228
228
  })),
229
+ traceId: options === null || options === void 0 ? void 0 : options.traceId,
229
230
  });
230
231
  });
231
232
  }
@@ -913,6 +914,31 @@ class Mpc {
913
914
  });
914
915
  });
915
916
  }
917
+ /*******************************
918
+ * Account Abstraction Methods
919
+ *******************************/
920
+ accountAbstractionBuildBatchedUserOp(data, traceId) {
921
+ return __awaiter(this, void 0, void 0, function* () {
922
+ return this.handleRequestToIframeAndPost({
923
+ methodMessage: 'portal:accountAbstraction:buildBatchedUserOp',
924
+ errorMessage: 'portal:accountAbstraction:buildBatchedUserOpError',
925
+ resultMessage: 'portal:accountAbstraction:buildBatchedUserOpResult',
926
+ data,
927
+ traceId,
928
+ });
929
+ });
930
+ }
931
+ accountAbstractionBroadcastBatchedUserOp(data, traceId) {
932
+ return __awaiter(this, void 0, void 0, function* () {
933
+ return this.handleRequestToIframeAndPost({
934
+ methodMessage: 'portal:accountAbstraction:broadcastBatchedUserOp',
935
+ errorMessage: 'portal:accountAbstraction:broadcastBatchedUserOpError',
936
+ resultMessage: 'portal:accountAbstraction:broadcastBatchedUserOpResult',
937
+ data,
938
+ traceId,
939
+ });
940
+ });
941
+ }
916
942
  /***************************
917
943
  * Private Methods
918
944
  ***************************/
@@ -12,7 +12,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
12
12
  };
13
13
  import { BackupMethods, MpcStatuses, PortalCurve } from '../index';
14
14
  import Mpc from '.';
15
- import { mockAddress, mockApikey, mockAssets, mockBackupConfig, mockBackupIds, mockBalances, mockBuiltTransaction, mockCipherText, mockClientResponse, mockEjectResult, mockEjectPrivateKeysResult, mockEvaluationResult, mockHost, mockNFTs, mockOrgBackupShares, mockQuoteArgs, mockRpcUrl, mockSharesOnDevice, mockSimulationResult, mockTransactionToEvaluate, mockTransactionToSimulate, mockYieldXyzGetYieldsRequest, mockYieldXyzGetYieldsResponse, mockYieldXyzEnterRequest, mockYieldXyzEnterResponse, mockYieldXyzExitRequest, mockYieldXyzExitResponse, mockYieldXyzGetBalancesRequest, mockYieldXyzGetBalancesResponse, mockYieldXyzGetHistoricalActionsRequest, mockYieldXyzGetHistoricalActionsResponse, mockYieldXyzManageYieldRequest, mockYieldXyzManageYieldResponse, mockYieldXyzTrackTransactionRequest, mockYieldXyzTrackTransactionResponse, mockYieldXyzGetTransactionResponse, mockLifiGetRoutesRequest, mockLifiGetRoutesResponse, mockLifiGetQuoteRequest, mockLifiGetQuoteResponse, mockLifiGetStatusRequest, mockLifiGetStatusResponse, mockLifiGetRouteStepRequest, mockLifiGetRouteStepResponse, mockZeroExQuoteV2Request, mockZeroExQuoteV2Response, mockZeroExSourcesV2Request, mockZeroExSourcesV2Response, mockZeroExPriceRequest, mockZeroExPriceResponse, mockZeroExOptions, mockScanAddressesRequest, mockScanAddressesResponse, mockScanEVMTxRequest, mockScanEVMTxResponse, mockScanEip712TxRequest, mockScanEip712TxResponse, mockScanSolanaTxRequest, mockScanSolanaTxResponse, mockScanNftRequest, mockScanNftResponse, mockScanTokenRequest, mockScanTokenResponse, mockScanUrlRequest, mockScanUrlResponse, mockBlockaidScanEvmTxRequest, mockBlockaidScanEvmTxResponse, mockBlockaidScanSolanaTxRequest, mockBlockaidScanSolanaTxResponse, mockBlockaidScanAddressRequest, mockBlockaidScanAddressResponse, mockBlockaidScanTokensRequest, mockBlockaidScanTokensResponse, mockBlockaidScanUrlRequest, mockBlockaidScanUrlResponse, } from '../__mocks/constants';
15
+ import { mockAddress, mockApikey, mockAssets, mockBackupConfig, mockBackupIds, mockBalances, mockBuiltTransaction, mockCipherText, mockClientResponse, mockEjectResult, mockEjectPrivateKeysResult, mockEvaluationResult, mockHost, mockNFTs, mockOrgBackupShares, mockQuoteArgs, mockRpcUrl, mockSharesOnDevice, mockSimulationResult, mockTransactionToEvaluate, mockTransactionToSimulate, mockYieldXyzGetYieldsRequest, mockYieldXyzGetYieldsResponse, mockYieldXyzEnterRequest, mockYieldXyzEnterResponse, mockYieldXyzExitRequest, mockYieldXyzExitResponse, mockYieldXyzGetBalancesRequest, mockYieldXyzGetBalancesResponse, mockYieldXyzGetHistoricalActionsRequest, mockYieldXyzGetHistoricalActionsResponse, mockYieldXyzManageYieldRequest, mockYieldXyzManageYieldResponse, mockYieldXyzTrackTransactionRequest, mockYieldXyzTrackTransactionResponse, mockYieldXyzGetTransactionResponse, mockLifiGetRoutesRequest, mockLifiGetRoutesResponse, mockLifiGetQuoteRequest, mockLifiGetQuoteResponse, mockLifiGetStatusRequest, mockLifiGetStatusResponse, mockLifiGetRouteStepRequest, mockLifiGetRouteStepResponse, mockZeroExQuoteV2Request, mockZeroExQuoteV2Response, mockZeroExSourcesV2Request, mockZeroExSourcesV2Response, mockZeroExPriceRequest, mockZeroExPriceResponse, mockZeroExOptions, mockScanAddressesRequest, mockScanAddressesResponse, mockScanEVMTxRequest, mockScanEVMTxResponse, mockScanEip712TxRequest, mockScanEip712TxResponse, mockScanSolanaTxRequest, mockScanSolanaTxResponse, mockScanNftRequest, mockScanNftResponse, mockScanTokenRequest, mockScanTokenResponse, mockScanUrlRequest, mockScanUrlResponse, mockBlockaidScanEvmTxRequest, mockBlockaidScanEvmTxResponse, mockBlockaidScanSolanaTxRequest, mockBlockaidScanSolanaTxResponse, mockBlockaidScanAddressRequest, mockBlockaidScanAddressResponse, mockBlockaidScanTokensRequest, mockBlockaidScanTokensResponse, mockBlockaidScanUrlRequest, mockBlockaidScanUrlResponse, mockBuildBatchedUserOpRequest, mockBuildBatchedUserOpResponse, mockBroadcastBatchedUserOpRequest, mockBroadcastBatchedUserOpResponse, } from '../__mocks/constants';
16
16
  import portalMock from '../__mocks/portal/portal';
17
17
  import { PortalMpcError } from './errors';
18
18
  describe('Mpc', () => {
@@ -3759,4 +3759,138 @@ describe('Mpc', () => {
3759
3759
  });
3760
3760
  });
3761
3761
  });
3762
+ describe('accountAbstractionBuildBatchedUserOp', () => {
3763
+ const args = mockBuildBatchedUserOpRequest;
3764
+ const res = mockBuildBatchedUserOpResponse;
3765
+ it('should successfully build a user operation', (done) => {
3766
+ var _a;
3767
+ jest
3768
+ .spyOn((_a = mpc.iframe) === null || _a === void 0 ? void 0 : _a.contentWindow, 'postMessage')
3769
+ .mockImplementation((message, origin) => {
3770
+ const { type, data } = message;
3771
+ expect(type).toEqual('portal:accountAbstraction:buildBatchedUserOp');
3772
+ expect(data).toMatchObject(args);
3773
+ expect(typeof message.traceId).toBe('string');
3774
+ expect(origin).toEqual(mockHostOrigin);
3775
+ window.dispatchEvent(new MessageEvent('message', {
3776
+ origin: mockHostOrigin,
3777
+ data: {
3778
+ type: 'portal:accountAbstraction:buildBatchedUserOpResult',
3779
+ data: res,
3780
+ },
3781
+ }));
3782
+ });
3783
+ mpc
3784
+ .accountAbstractionBuildBatchedUserOp(args)
3785
+ .then((data) => {
3786
+ expect(data).toEqual(res);
3787
+ done();
3788
+ })
3789
+ .catch((_) => {
3790
+ expect(0).toEqual(1);
3791
+ done();
3792
+ });
3793
+ });
3794
+ it('should error out if the iframe sends an error message', (done) => {
3795
+ var _a;
3796
+ jest
3797
+ .spyOn((_a = mpc.iframe) === null || _a === void 0 ? void 0 : _a.contentWindow, 'postMessage')
3798
+ .mockImplementationOnce((message, origin) => {
3799
+ const { type, data } = message;
3800
+ expect(type).toEqual('portal:accountAbstraction:buildBatchedUserOp');
3801
+ expect(data).toMatchObject(args);
3802
+ expect(typeof message.traceId).toBe('string');
3803
+ expect(origin).toEqual(mockHostOrigin);
3804
+ window.dispatchEvent(new MessageEvent('message', {
3805
+ origin: mockHostOrigin,
3806
+ data: {
3807
+ type: 'portal:accountAbstraction:buildBatchedUserOpError',
3808
+ data: {
3809
+ code: 1,
3810
+ message: 'test',
3811
+ },
3812
+ },
3813
+ }));
3814
+ });
3815
+ mpc
3816
+ .accountAbstractionBuildBatchedUserOp(args)
3817
+ .then(() => {
3818
+ expect(0).toEqual(1);
3819
+ done();
3820
+ })
3821
+ .catch((e) => {
3822
+ expect(e).toBeInstanceOf(PortalMpcError);
3823
+ expect(e.message).toEqual('test');
3824
+ expect(e.code).toEqual(1);
3825
+ done();
3826
+ });
3827
+ });
3828
+ });
3829
+ describe('accountAbstractionBroadcastBatchedUserOp', () => {
3830
+ const args = mockBroadcastBatchedUserOpRequest;
3831
+ const res = mockBroadcastBatchedUserOpResponse;
3832
+ it('should successfully broadcast a user operation', (done) => {
3833
+ var _a;
3834
+ jest
3835
+ .spyOn((_a = mpc.iframe) === null || _a === void 0 ? void 0 : _a.contentWindow, 'postMessage')
3836
+ .mockImplementation((message, origin) => {
3837
+ const { type, data } = message;
3838
+ expect(type).toEqual('portal:accountAbstraction:broadcastBatchedUserOp');
3839
+ expect(data).toMatchObject(args);
3840
+ expect(typeof message.traceId).toBe('string');
3841
+ expect(origin).toEqual(mockHostOrigin);
3842
+ window.dispatchEvent(new MessageEvent('message', {
3843
+ origin: mockHostOrigin,
3844
+ data: {
3845
+ type: 'portal:accountAbstraction:broadcastBatchedUserOpResult',
3846
+ data: res,
3847
+ },
3848
+ }));
3849
+ });
3850
+ mpc
3851
+ .accountAbstractionBroadcastBatchedUserOp(args)
3852
+ .then((data) => {
3853
+ expect(data).toEqual(res);
3854
+ done();
3855
+ })
3856
+ .catch((_) => {
3857
+ expect(0).toEqual(1);
3858
+ done();
3859
+ });
3860
+ });
3861
+ it('should error out if the iframe sends an error message', (done) => {
3862
+ var _a;
3863
+ jest
3864
+ .spyOn((_a = mpc.iframe) === null || _a === void 0 ? void 0 : _a.contentWindow, 'postMessage')
3865
+ .mockImplementationOnce((message, origin) => {
3866
+ const { type, data } = message;
3867
+ expect(type).toEqual('portal:accountAbstraction:broadcastBatchedUserOp');
3868
+ expect(data).toMatchObject(args);
3869
+ expect(typeof message.traceId).toBe('string');
3870
+ expect(origin).toEqual(mockHostOrigin);
3871
+ window.dispatchEvent(new MessageEvent('message', {
3872
+ origin: mockHostOrigin,
3873
+ data: {
3874
+ type: 'portal:accountAbstraction:broadcastBatchedUserOpError',
3875
+ data: {
3876
+ code: 1,
3877
+ message: 'test',
3878
+ },
3879
+ },
3880
+ }));
3881
+ });
3882
+ mpc
3883
+ .accountAbstractionBroadcastBatchedUserOp(args)
3884
+ .then(() => {
3885
+ expect(0).toEqual(1);
3886
+ done();
3887
+ })
3888
+ .catch((e) => {
3889
+ expect(e).toBeInstanceOf(PortalMpcError);
3890
+ expect(e.message).toEqual('test');
3891
+ expect(e.code).toEqual(1);
3892
+ done();
3893
+ });
3894
+ });
3895
+ });
3762
3896
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -16,3 +16,5 @@ export * from './lifi';
16
16
  export * from './noah';
17
17
  export * from './hypernative';
18
18
  export * from './blockaid';
19
+ // Account Abstraction types
20
+ export * from './accountAbstraction';
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Portal MPC Support for Web",
4
4
  "author": "Portal Labs, Inc.",
5
5
  "homepage": "https://portalhq.io/",
6
- "version": "3.16.0",
6
+ "version": "3.17.0-alpha.0",
7
7
  "license": "MIT",
8
8
  "main": "lib/commonjs/index",
9
9
  "module": "lib/esm/index",
@@ -58,5 +58,6 @@
58
58
  },
59
59
  "dependencies": {
60
60
  "@solana/web3.js": "^1.91.8"
61
- }
61
+ },
62
+ "stableVersion": "3.16.0-alpha.1"
62
63
  }
@@ -13,9 +13,15 @@ import {
13
13
  Build7702UpgradeTxResponse,
14
14
  BuildAuthorizationListRequest,
15
15
  BuildAuthorizationListResponse,
16
+ BuildBatchedUserOpRequest,
17
+ BuildBatchedUserOpResponse,
18
+ BroadcastBatchedUserOpRequest,
19
+ BroadcastBatchedUserOpResponse,
20
+ BuiltEip155Transaction,
16
21
  EvmAccountTypeGetStatusRequest,
17
22
  EvmAccountTypeGetStatusResponse,
18
23
  GetAddressesResponse,
24
+ SendBatchUserOpRequest,
19
25
  UpgradeTo7702Request,
20
26
  UpgradeTo7702Response,
21
27
  } from 'src/shared/types'
@@ -2085,3 +2091,90 @@ export const mockUpgradeTo7702Request = {
2085
2091
  export const mockUpgradeTo7702Response = {
2086
2092
  txHash: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
2087
2093
  } as UpgradeTo7702Response
2094
+
2095
+ export const mockBuildBatchedUserOpRequest = {
2096
+ chain: 'eip155:1',
2097
+ calls: [
2098
+ {
2099
+ to: '0xcae0d97d201ad54275b6e8a6b547c7611ad47963',
2100
+ value: '0',
2101
+ data: '0x',
2102
+ },
2103
+ ],
2104
+ } as BuildBatchedUserOpRequest
2105
+
2106
+ export const mockBuildBatchedUserOpResponse = {
2107
+ data: {
2108
+ userOperation:
2109
+ '{"sender":"0xabc","nonce":"0x0","initCode":"0x","callData":"0x","callGasLimit":"0x5208","verificationGasLimit":"0x0186A0","preVerificationGas":"0xC350","maxFeePerGas":"0x3B9ACA00","maxPriorityFeePerGas":"0x3B9ACA00","paymasterAndData":"0x","signature":"0x"}',
2110
+ userOpHash:
2111
+ '0xdeadbeef1234567890abcdef1234567890abcdef1234567890abcdef12345678',
2112
+ },
2113
+ metadata: {
2114
+ chainId: 'eip155:1',
2115
+ // Gas info as decimal strings (matching connect-api): totalGas (units),
2116
+ // maxFeePerGas (wei/gas), estimatedGasCostWei = totalGas * maxFeePerGas.
2117
+ totalGas: '100000',
2118
+ maxFeePerGas: '1000000000',
2119
+ estimatedGasCostWei: '100000000000000',
2120
+ },
2121
+ } as BuildBatchedUserOpResponse
2122
+
2123
+ export const mockBroadcastBatchedUserOpRequest = {
2124
+ chain: 'eip155:1',
2125
+ userOperation:
2126
+ '{"sender":"0xabc","nonce":"0x0","initCode":"0x","callData":"0x","callGasLimit":"0x5208","verificationGasLimit":"0x0186A0","preVerificationGas":"0xC350","maxFeePerGas":"0x3B9ACA00","maxPriorityFeePerGas":"0x3B9ACA00","paymasterAndData":"0x","signature":"0x1234"}',
2127
+ signature: '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab',
2128
+ } as BroadcastBatchedUserOpRequest
2129
+
2130
+ export const mockBroadcastBatchedUserOpResponse = {
2131
+ data: {
2132
+ userOpHash:
2133
+ '0xfeedface1234567890abcdef1234567890abcdef1234567890abcdef12345678',
2134
+ },
2135
+ metadata: {
2136
+ chainId: 'eip155:1',
2137
+ },
2138
+ } as BroadcastBatchedUserOpResponse
2139
+
2140
+ // ERC-20 token transfer (data contains calldata, no native ETH value)
2141
+ export const mockBuiltEip155Transaction = {
2142
+ transaction: {
2143
+ from: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
2144
+ to: '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
2145
+ data: '0xa9059cbb000000000000000000000000111111111111111111111111111111111111111100000000000000000000000000000000000000000000000000000000000f4240',
2146
+ },
2147
+ metadata: {
2148
+ amount: '1.0',
2149
+ fromAddress: '0xsender',
2150
+ toAddress: '0xrecipient',
2151
+ tokenAddress: '0xusdc-contract',
2152
+ tokenDecimals: 6,
2153
+ rawAmount: '1000000',
2154
+ },
2155
+ } as BuiltEip155Transaction
2156
+
2157
+ // Native ETH transfer (data is "0x", value comes from rawAmount)
2158
+ export const mockBuiltEip155TransactionNative = {
2159
+ transaction: {
2160
+ from: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
2161
+ to: '0x1111111111111111111111111111111111111111',
2162
+ data: '0x',
2163
+ },
2164
+ metadata: {
2165
+ amount: '0.1',
2166
+ fromAddress: '0xsender',
2167
+ toAddress: '0xrecipient',
2168
+ tokenAddress: '0x0000000000000000000000000000000000000000',
2169
+ tokenDecimals: 18,
2170
+ rawAmount: '100000000000000000',
2171
+ },
2172
+ } as BuiltEip155Transaction
2173
+
2174
+ export const mockSendBatchUserOpRequest = {
2175
+ chain: 'eip155:1',
2176
+ transactions: [
2177
+ { token: 'USDC', value: '1.0', to: '0x1111111111111111111111111111111111111111' },
2178
+ { token: 'USDC', value: '2.0', to: '0x2222222222222222222222222222222222222222' },
2179
+ ],
2180
+ } as SendBatchUserOpRequest
@@ -1,5 +1,8 @@
1
1
  import {
2
2
  mockAddress,
3
+ mockBuiltEip155Transaction,
4
+ mockBuildBatchedUserOpResponse,
5
+ mockBroadcastBatchedUserOpResponse,
3
6
  mockClientResponse,
4
7
  mockEjectResult,
5
8
  mockEjectPrivateKeysResult,
@@ -28,5 +31,13 @@ mpcMock.ejectPrivateKeys = jest
28
31
  .mockResolvedValue(mockEjectPrivateKeysResult)
29
32
  mpcMock.checkSharesOnDevice = jest.fn().mockResolvedValue(mockSharesOnDevice)
30
33
  mpcMock.setBackupStatus = jest.fn().mockResolvedValue(true)
34
+ mpcMock.rawSign = jest.fn().mockResolvedValue(mockSignedHash)
35
+ mpcMock.buildTransaction = jest.fn().mockResolvedValue(mockBuiltEip155Transaction)
36
+ mpcMock.accountAbstractionBuildBatchedUserOp = jest
37
+ .fn()
38
+ .mockResolvedValue(mockBuildBatchedUserOpResponse)
39
+ mpcMock.accountAbstractionBroadcastBatchedUserOp = jest
40
+ .fn()
41
+ .mockResolvedValue(mockBroadcastBatchedUserOpResponse)
31
42
 
32
43
  export default mpcMock as jest.Mocked<Mpc>