diff --git a/packages/game-bridge/src/index.ts b/packages/game-bridge/src/index.ts index 211fbd776d..4ef58930d3 100644 --- a/packages/game-bridge/src/index.ts +++ b/packages/game-bridge/src/index.ts @@ -53,6 +53,7 @@ const PASSPORT_FUNCTIONS = { getEmail: 'getEmail', getPassportId: 'getPassportId', getLinkedAddresses: 'getLinkedAddresses', + storeTokens: 'storeTokens', imx: { getAddress: 'getAddress', isRegisteredOffchain: 'isRegisteredOffchain', @@ -464,6 +465,19 @@ window.callFunction = async (jsonData: string) => { }); break; } + case PASSPORT_FUNCTIONS.storeTokens: { + const tokenResponse = JSON.parse(data); + const profile = await getPassportClient().storeTokens(tokenResponse); + identify({ passportId: profile.sub }); + trackDuration(moduleName, 'performedStoreTokens', mt(markStart)); + callbackToGame({ + responseFor: fxName, + requestId, + success: true, + error: null, + }); + break; + } case PASSPORT_FUNCTIONS.getEmail: { const userProfile = await getPassportClient().getUserInfo(); const success = userProfile?.email !== undefined; diff --git a/packages/passport/sdk/src/Passport.int.test.ts b/packages/passport/sdk/src/Passport.int.test.ts index 4ad38106d5..92114bb7a5 100644 --- a/packages/passport/sdk/src/Passport.int.test.ts +++ b/packages/passport/sdk/src/Passport.int.test.ts @@ -224,12 +224,16 @@ describe('Passport', () => { }); it('registers the user and returns the ether key', async () => { + mockGetUser.mockResolvedValueOnce(null); mockSigninPopup.mockResolvedValue(mockOidcUser); + mockGetUser.mockResolvedValueOnce(mockOidcUser); mockSigninSilent.mockResolvedValueOnce(mockOidcUserZkevm); + mockGetUser.mockResolvedValue(mockOidcUserZkevm); useMswHandlers([ mswHandlers.rpcProvider.success, mswHandlers.counterfactualAddress.success, mswHandlers.api.chains.success, + mswHandlers.magicTEE.createWallet.success, ]); const zkEvmProvider = await getZkEvmProvider(); @@ -244,13 +248,15 @@ describe('Passport', () => { describe('when the registration request fails', () => { it('throws an error', async () => { - mockSigninPopup.mockResolvedValue(mockOidcUser); mockGetUser.mockResolvedValueOnce(null); + mockSigninPopup.mockResolvedValue(mockOidcUser); mockGetUser.mockResolvedValueOnce(mockOidcUser); mockSigninSilent.mockResolvedValue(mockOidcUser); + mockGetUser.mockResolvedValue(mockOidcUser); useMswHandlers([ mswHandlers.counterfactualAddress.internalServerError, mswHandlers.api.chains.success, + mswHandlers.magicTEE.createWallet.success, ]); const zkEvmProvider = await getZkEvmProvider(); @@ -268,10 +274,11 @@ describe('Passport', () => { const transferToAddress = '0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC'; useMswHandlers([ - mswHandlers.counterfactualAddress.success, mswHandlers.rpcProvider.success, mswHandlers.relayer.success, mswHandlers.guardian.evaluateTransaction.success, + mswHandlers.magicTEE.createWallet.success, + mswHandlers.magicTEE.personalSign.success, ]); mockMagicRequest.mockImplementation(({ method }: RequestArguments) => { switch (method) { @@ -307,7 +314,7 @@ describe('Passport', () => { }); expect(result).toEqual(transactionHash); - expect(mockGetUser).toHaveBeenCalledTimes(6); + expect(mockGetUser).toHaveBeenCalledTimes(9); }); it('ethSigner is initialised if user logs in after connectEvm', async () => { @@ -318,6 +325,8 @@ describe('Passport', () => { mswHandlers.rpcProvider.success, mswHandlers.relayer.success, mswHandlers.guardian.evaluateTransaction.success, + mswHandlers.magicTEE.createWallet.success, + mswHandlers.magicTEE.personalSign.success, ]); mockMagicRequest.mockImplementation(({ method }: RequestArguments) => { switch (method) { @@ -335,7 +344,7 @@ describe('Passport', () => { } } }); - mockGetUser.mockResolvedValueOnce(Promise.resolve(null)); + mockGetUser.mockResolvedValueOnce(null); mockSigninPopup.mockResolvedValue(mockOidcUserZkevm); mockSigninSilent.mockResolvedValueOnce(mockOidcUserZkevm); @@ -362,7 +371,7 @@ describe('Passport', () => { // user logs in, ethSigner is initialised await passport.login(); - mockGetUser.mockResolvedValue(Promise.resolve(mockOidcUserZkevm)); + mockGetUser.mockResolvedValue(mockOidcUserZkevm); expect(accounts).toEqual([mockUserZkEvm.zkEvm.ethAddress]); diff --git a/packages/passport/sdk/src/Passport.test.ts b/packages/passport/sdk/src/Passport.test.ts index 26ccc5a7b3..cc7bc8fae7 100644 --- a/packages/passport/sdk/src/Passport.test.ts +++ b/packages/passport/sdk/src/Passport.test.ts @@ -3,7 +3,7 @@ import { IMXClient } from '@imtbl/x-client'; import { ImxApiClients, imxApiConfig, MultiRollupApiClients } from '@imtbl/generated-clients'; import { trackError, trackFlow } from '@imtbl/metrics'; import AuthManager from './authManager'; -import MagicAdapter from './magic/magicAdapter'; +import MagicTEESigner from './magic/magicTEESigner'; import { Passport } from './Passport'; import { PassportImxProvider, PassportImxProviderFactory } from './starkEx'; import { OidcConfiguration, UserProfile } from './types'; @@ -21,7 +21,7 @@ import { ZkEvmProvider } from './zkEvm'; import { PassportError, PassportErrorType } from './errors/passportError'; jest.mock('./authManager'); -jest.mock('./magic/magicAdapter'); +jest.mock('./magic/magicTEESigner'); jest.mock('./starkEx'); jest.mock('./confirmation'); jest.mock('./zkEvm'); @@ -44,8 +44,6 @@ describe('Passport', () => { let loginCallbackMock: jest.Mock; let logoutMock: jest.Mock; let removeUserMock: jest.Mock; - let magicLoginMock: jest.Mock; - let magicLogoutMock: jest.Mock; let getUserMock: jest.Mock; let requestRefreshTokenMock: jest.Mock; let getProviderMock: jest.Mock; @@ -57,8 +55,6 @@ describe('Passport', () => { beforeEach(() => { authLoginMock = jest.fn().mockReturnValue(mockUser); loginCallbackMock = jest.fn(); - magicLoginMock = jest.fn(); - magicLogoutMock = jest.fn(); logoutMock = jest.fn(); removeUserMock = jest.fn(); getUserMock = jest.fn(); @@ -78,9 +74,9 @@ describe('Passport', () => { requestRefreshTokenAfterRegistration: requestRefreshTokenMock, forceUserRefresh: forceUserRefreshMock, }); - (MagicAdapter as jest.Mock).mockReturnValue({ - login: magicLoginMock, - logout: magicLogoutMock, + (MagicTEESigner as unknown as jest.Mock).mockReturnValue({ + getAddress: jest.fn().mockResolvedValue('0x123'), + signMessage: jest.fn().mockResolvedValue('signature'), }); (PassportImxProviderFactory as jest.Mock).mockReturnValue({ getProvider: getProviderMock, @@ -289,26 +285,12 @@ describe('Passport', () => { await passport.logout(); expect(logoutMock).toBeCalledTimes(1); - expect(magicLogoutMock).toBeCalledTimes(1); - }); - }); - - describe('when the logout mode is redirect', () => { - it('should execute logout without error in the correct order', async () => { - await passport.logout(); - - const logoutMockOrder = logoutMock.mock.invocationCallOrder[0]; - const magicLogoutMockOrder = magicLogoutMock.mock.invocationCallOrder[0]; - - expect(logoutMock).toBeCalledTimes(1); - expect(magicLogoutMock).toBeCalledTimes(1); - expect(magicLogoutMockOrder).toBeLessThan(logoutMockOrder); }); }); it('should call track error function if an error occurs', async () => { const error = new Error('error'); - magicLogoutMock.mockRejectedValue(error); + logoutMock.mockRejectedValue(error); try { await passport.logout(); diff --git a/packages/passport/sdk/src/Passport.ts b/packages/passport/sdk/src/Passport.ts index 1476b62047..40dce3b1b6 100644 --- a/packages/passport/sdk/src/Passport.ts +++ b/packages/passport/sdk/src/Passport.ts @@ -1,6 +1,6 @@ import { IMXProvider } from '@imtbl/x-provider'; import { - createConfig, ImxApiClients, imxApiConfig, MultiRollupApiClients, + createConfig, ImxApiClients, imxApiConfig, MagicTeeApiClients, MultiRollupApiClients, } from '@imtbl/generated-clients'; import { IMXClient } from '@imtbl/x-client'; import { Environment } from '@imtbl/config'; @@ -11,10 +11,11 @@ import { } from '@imtbl/metrics'; import { isAxiosError } from 'axios'; import AuthManager from './authManager'; -import MagicAdapter from './magic/magicAdapter'; +import MagicTEESigner from './magic/magicTEESigner'; import { PassportImxProviderFactory } from './starkEx'; import { PassportConfiguration } from './config'; import { + DeviceTokenResponse, DirectLoginMethod, isUserImx, isUserZkEvm, @@ -35,7 +36,6 @@ import logger from './utils/logger'; import { announceProvider, passportProviderInfo } from './zkEvm/provider/eip6963'; import { isAPIError, PassportError, PassportErrorType } from './errors/passportError'; import { withMetricsAsync } from './utils/metrics'; -import { MagicProviderProxyFactory } from './magic/magicProviderProxyFactory'; const buildImxClientConfig = (passportModuleConfiguration: PassportModuleConfiguration) => { if (passportModuleConfiguration.overrides) { @@ -57,9 +57,14 @@ const buildImxApiClients = (passportModuleConfiguration: PassportModuleConfigura export const buildPrivateVars = (passportModuleConfiguration: PassportModuleConfiguration) => { const config = new PassportConfiguration(passportModuleConfiguration); const authManager = new AuthManager(config); - const magicProviderProxyFactory = new MagicProviderProxyFactory(authManager, config); - const magicAdapter = new MagicAdapter(config, magicProviderProxyFactory); const confirmationScreen = new ConfirmationScreen(config); + const magicTeeApiClients = new MagicTeeApiClients({ + basePath: config.magicTeeBasePath, + timeout: config.magicTeeTimeout, + magicPublishableApiKey: config.magicPublishableApiKey, + magicProviderId: config.magicProviderId, + }); + const magicTEESigner = new MagicTEESigner(authManager, magicTeeApiClients); const multiRollupApiClients = new MultiRollupApiClients(config.multiRollupConfig); const passportEventEmitter = new TypedEventEmitter(); @@ -79,7 +84,7 @@ export const buildPrivateVars = (passportModuleConfiguration: PassportModuleConf const passportImxProviderFactory = new PassportImxProviderFactory({ authManager, immutableXClient, - magicAdapter, + magicTEESigner, passportEventEmitter, imxApiClients, guardianClient, @@ -88,7 +93,7 @@ export const buildPrivateVars = (passportModuleConfiguration: PassportModuleConf return { config, authManager, - magicAdapter, + magicTEESigner, confirmationScreen, immutableXClient, multiRollupApiClients, @@ -107,7 +112,7 @@ export class Passport { private readonly immutableXClient: IMXClient; - private readonly magicAdapter: MagicAdapter; + private readonly magicTEESigner: MagicTEESigner; private readonly multiRollupApiClients: MultiRollupApiClients; @@ -122,7 +127,7 @@ export class Passport { this.config = privateVars.config; this.authManager = privateVars.authManager; - this.magicAdapter = privateVars.magicAdapter; + this.magicTEESigner = privateVars.magicTEESigner; this.confirmationScreen = privateVars.confirmationScreen; this.immutableXClient = privateVars.immutableXClient; this.multiRollupApiClients = privateVars.multiRollupApiClients; @@ -155,19 +160,27 @@ export class Passport { * Connects to EVM and optionally announces the provider. * @param {Object} options - Configuration options * @param {boolean} options.announceProvider - Whether to announce the provider via EIP-6963 for wallet discovery (defaults to true) - * @returns {Provider} The EVM provider instance + * @returns {Promise} The EVM provider instance */ - public connectEvm(options: { + public async connectEvm(options: { announceProvider: boolean } = { announceProvider: true }): Promise { return withMetricsAsync(async () => { + let user: User | null = null; + try { + user = await this.authManager.getUser(); + } catch (error) { + // Initialise the zkEvmProvider without a user + } + const provider = new ZkEvmProvider({ passportEventEmitter: this.passportEventEmitter, authManager: this.authManager, - magicAdapter: this.magicAdapter, config: this.config, multiRollupApiClients: this.multiRollupApiClients, guardianClient: this.guardianClient, + ethSigner: this.magicTEESigner, + user, }); if (options?.announceProvider) { @@ -299,22 +312,21 @@ export class Passport { }, 'loginWithPKCEFlowCallback'); } + public async storeTokens(tokenResponse: DeviceTokenResponse): Promise { + return withMetricsAsync(async () => { + const user = await this.authManager.storeTokens(tokenResponse); + this.passportEventEmitter.emit(PassportEvents.LOGGED_IN, user); + return user.profile; + }, 'storeTokens'); + } + /** * Logs out the current user. * @returns {Promise} A promise that resolves when the logout is complete */ public async logout(): Promise { return withMetricsAsync(async () => { - if (this.config.oidcConfiguration.logoutMode === 'silent') { - await Promise.allSettled([ - this.authManager.logout(), - this.magicAdapter.logout(), - ]); - } else { - // We need to ensure that the Magic wallet is logged out BEFORE redirecting - await this.magicAdapter.logout(); - await this.authManager.logout(); - } + await this.authManager.logout(); this.passportEventEmitter.emit(PassportEvents.LOGGED_OUT); }, 'logout'); } @@ -326,7 +338,6 @@ export class Passport { public async getLogoutUrl(): Promise { return withMetricsAsync(async () => { await this.authManager.removeUser(); - await this.magicAdapter.logout(); this.passportEventEmitter.emit(PassportEvents.LOGGED_OUT); return await this.authManager.getLogoutUrl(); }, 'getLogoutUrl'); diff --git a/packages/passport/sdk/src/authManager.ts b/packages/passport/sdk/src/authManager.ts index ff8448a0c1..727173e839 100644 --- a/packages/passport/sdk/src/authManager.ts +++ b/packages/passport/sdk/src/authManager.ts @@ -348,6 +348,16 @@ export default class AuthManager { return response.data; } + public async storeTokens(tokenResponse: DeviceTokenResponse): Promise { + return withPassportError(async () => { + const oidcUser = AuthManager.mapDeviceTokenResponseToOidcUser(tokenResponse); + const user = AuthManager.mapOidcUserToDomainModel(oidcUser); + await this.userManager.storeUser(oidcUser); + + return user; + }, PassportErrorType.AUTHENTICATION_ERROR); + } + public async logout(): Promise { return withPassportError(async () => { await this.userManager.revokeTokens(['refresh_token']); diff --git a/packages/passport/sdk/src/config/config.ts b/packages/passport/sdk/src/config/config.ts index 2a9e42fb22..63f458bfe2 100644 --- a/packages/passport/sdk/src/config/config.ts +++ b/packages/passport/sdk/src/config/config.ts @@ -38,6 +38,10 @@ export class PassportConfiguration { readonly magicProviderId: string; + readonly magicTeeBasePath: string = 'https://tee.express.magiclabs.com'; + + readonly magicTeeTimeout: number = 6000; + readonly oidcConfiguration: OidcConfiguration; readonly baseConfig: ImmutableConfiguration; diff --git a/packages/passport/sdk/src/magic/index.ts b/packages/passport/sdk/src/magic/index.ts index 0ba3c48096..6817ef70cf 100644 --- a/packages/passport/sdk/src/magic/index.ts +++ b/packages/passport/sdk/src/magic/index.ts @@ -1,3 +1 @@ -import MagicAdapter from './magicAdapter'; - -export default { MagicAdapter }; +export { default as MagicTEESigner } from './magicTEESigner'; diff --git a/packages/passport/sdk/src/magic/magicAdapter.test.ts b/packages/passport/sdk/src/magic/magicAdapter.test.ts deleted file mode 100644 index 39a1d05d22..0000000000 --- a/packages/passport/sdk/src/magic/magicAdapter.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { LoginWithOpenIdParams, OpenIdExtension } from '@magic-ext/oidc'; -import { Magic } from 'magic-sdk'; -import MagicAdapter from './magicAdapter'; -import { PassportConfiguration } from '../config'; -import { PassportError, PassportErrorType } from '../errors/passportError'; -import { MagicProviderProxyFactory } from './magicProviderProxyFactory'; - -const loginWithOIDCMock:jest.MockedFunction<(args: LoginWithOpenIdParams) => Promise> = jest.fn(); - -const rpcProvider = {}; - -const logoutMock = jest.fn(); - -jest.mock('magic-sdk'); -jest.mock('@magic-ext/oidc', () => ({ - OpenIdExtension: jest.fn(), -})); - -describe('MagicWallet', () => { - const apiKey = 'pk_live_A7D9211D7547A338'; - const providerId = 'mPGZAvZsFkyfT6OWfML1HgTKjPqYOPkhhOj-8qCGeqI='; - const config = { - magicPublishableApiKey: apiKey, - magicProviderId: providerId, - } as PassportConfiguration; - const magicProviderProxyFactory = { - createProxy: jest.fn(), - } as unknown as MagicProviderProxyFactory; - const idToken = 'e30=.e30=.e30='; - - beforeEach(() => { - jest.resetAllMocks(); - (Magic as jest.Mock).mockImplementation(() => ({ - openid: { - loginWithOIDC: loginWithOIDCMock, - }, - user: { - logout: logoutMock, - }, - rpcProvider, - })); - (magicProviderProxyFactory.createProxy as jest.Mock).mockImplementation(() => rpcProvider); - }); - - describe('constructor', () => { - describe('when window defined', () => { - let originalDocument: Document | undefined; - - beforeAll(() => { - originalDocument = window.document; - const mockDocument = { - ...window.document, - readyState: 'complete', - }; - (window as any).document = mockDocument; - }); - afterAll(() => { - (window as any).document = originalDocument; - }); - it('starts initialising the magicClient', () => { - jest.spyOn(window.document, 'readyState', 'get').mockReturnValue('complete'); - const magicAdapter = new MagicAdapter(config, magicProviderProxyFactory); - // @ts-ignore - expect(magicAdapter.magicClient).toBeDefined(); - }); - }); - - describe('when window is undefined', () => { - const { window } = global; - beforeAll(() => { - // @ts-expect-error - delete global.window; - }); - afterAll(() => { - global.window = window; - }); - - it('does nothing', () => { - const magicAdapter = new MagicAdapter(config, magicProviderProxyFactory); - // @ts-ignore - expect(magicAdapter.magicClientPromise).toBeUndefined(); - }); - }); - }); - - describe('login', () => { - it('should call loginWithOIDC and initialise the provider with the correct arguments', async () => { - const magicAdapter = new MagicAdapter(config, magicProviderProxyFactory); - const magicProvider = await magicAdapter.login(idToken); - - expect(Magic).toHaveBeenCalledWith(apiKey, { - network: 'mainnet', - extensions: [new OpenIdExtension()], - }); - - expect(loginWithOIDCMock).toHaveBeenCalledWith({ - jwt: idToken, - providerId, - }); - - expect(magicProviderProxyFactory.createProxy).toHaveBeenCalled(); - expect(magicProvider).toEqual(rpcProvider); - }); - - it('should throw a PassportError when an error is thrown', async () => { - const magicAdapter = new MagicAdapter(config, magicProviderProxyFactory); - - loginWithOIDCMock.mockImplementation(() => { - throw new Error('oops'); - }); - - await expect(async () => { - await magicAdapter.login(idToken); - }).rejects.toThrow( - new PassportError( - 'oops', - PassportErrorType.WALLET_CONNECTION_ERROR, - ), - ); - }); - }); - - describe('logout', () => { - it('calls the logout function', async () => { - const magicAdapter = new MagicAdapter(config, magicProviderProxyFactory); - await magicAdapter.login(idToken); - await magicAdapter.logout(); - - expect(logoutMock).toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/passport/sdk/src/magic/magicAdapter.ts b/packages/passport/sdk/src/magic/magicAdapter.ts deleted file mode 100644 index ba4054c9be..0000000000 --- a/packages/passport/sdk/src/magic/magicAdapter.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Magic } from 'magic-sdk'; -import { OpenIdExtension } from '@magic-ext/oidc'; -import { Flow, trackDuration } from '@imtbl/metrics'; -import { Eip1193Provider } from 'ethers'; -import { PassportErrorType, withPassportError } from '../errors/passportError'; -import { PassportConfiguration } from '../config'; -import { withMetricsAsync } from '../utils/metrics'; -import { MagicProviderProxyFactory } from './magicProviderProxyFactory'; -import { MagicClient } from './types'; - -const MAINNET = 'mainnet'; - -export default class MagicAdapter { - private readonly config: PassportConfiguration; - - private readonly magicProviderProxyFactory: MagicProviderProxyFactory; - - private readonly magicClient?: MagicClient; - - constructor(config: PassportConfiguration, magicProviderProxyFactory: MagicProviderProxyFactory) { - this.config = config; - this.magicProviderProxyFactory = magicProviderProxyFactory; - - if (typeof window !== 'undefined') { - this.magicClient = new Magic(this.config.magicPublishableApiKey, { - extensions: [new OpenIdExtension()], - network: MAINNET, // We always connect to mainnet to ensure addresses are the same across envs - }); - } - } - - private getMagicClient(): MagicClient { - if (!this.magicClient) { - throw new Error('Cannot perform this action outside of the browser'); - } - - return this.magicClient; - } - - async login( - idToken: string, - ): Promise { - return withPassportError(async () => ( - withMetricsAsync(async (flow: Flow) => { - const startTime = performance.now(); - - const magicClient = this.getMagicClient(); - flow.addEvent('endMagicClientInit'); - - await magicClient.openid.loginWithOIDC({ - jwt: idToken, - providerId: this.config.magicProviderId, - }); - flow.addEvent('endLoginWithOIDC'); - - trackDuration( - 'passport', - flow.details.flowName, - Math.round(performance.now() - startTime), - ); - - return this.magicProviderProxyFactory.createProxy(magicClient); - }, 'magicLogin') - ), PassportErrorType.WALLET_CONNECTION_ERROR); - } - - async logout() { - const magicClient = this.getMagicClient(); - if (magicClient.user) { - await magicClient.user.logout(); - } - } -} diff --git a/packages/passport/sdk/src/magic/magicProviderProxyFactory.test.ts b/packages/passport/sdk/src/magic/magicProviderProxyFactory.test.ts deleted file mode 100644 index ae203eb924..0000000000 --- a/packages/passport/sdk/src/magic/magicProviderProxyFactory.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { Eip1193Provider } from 'ethers'; -import { MagicProviderProxyFactory } from './magicProviderProxyFactory'; -import AuthManager from '../authManager'; -import { PassportConfiguration } from '../config'; -import { MagicClient } from './types'; - -describe('MagicProviderProxyFactory', () => { - let mockAuthManager: jest.Mocked; - let mockConfig: PassportConfiguration; - let mockMagicClient: jest.Mocked; - let mockRpcProvider: jest.Mocked; - let factory: MagicProviderProxyFactory; - - beforeEach(() => { - mockAuthManager = { - getUser: jest.fn(), - } as any; - - mockConfig = { - magicProviderId: 'test-provider-id', - } as PassportConfiguration; - - mockRpcProvider = { - request: jest.fn(), - } as any; - - mockMagicClient = { - rpcProvider: mockRpcProvider, - user: { - isLoggedIn: jest.fn(), - }, - openid: { - loginWithOIDC: jest.fn(), - }, - } as any; - - factory = new MagicProviderProxyFactory(mockAuthManager, mockConfig); - }); - - describe('createProxy', () => { - it('should create a proxy that passes through non-authenticated requests', async () => { - const proxy = factory.createProxy(mockMagicClient); - const params = { method: 'eth_blockNumber' }; - - await proxy.request!(params); - - expect(mockRpcProvider.request).toHaveBeenCalledWith(params); - expect(mockMagicClient.user.isLoggedIn).not.toHaveBeenCalled(); - }); - - it('should check authentication for personal_sign requests', async () => { - const proxy = factory.createProxy(mockMagicClient); - const params = { method: 'personal_sign', params: ['message', 'address'] }; - (mockMagicClient.user.isLoggedIn as jest.Mock).mockResolvedValue(true); - - await proxy.request!(params); - - expect(mockMagicClient.user.isLoggedIn).toHaveBeenCalled(); - expect(mockRpcProvider.request).toHaveBeenCalledWith(params); - }); - - it('should check authentication for eth_accounts requests', async () => { - const proxy = factory.createProxy(mockMagicClient); - const params = { method: 'eth_accounts' }; - (mockMagicClient.user.isLoggedIn as jest.Mock).mockResolvedValue(true); - - await proxy.request!(params); - - expect(mockMagicClient.user.isLoggedIn).toHaveBeenCalled(); - expect(mockRpcProvider.request).toHaveBeenCalledWith(params); - }); - - it('should re-authenticate when user is not logged in', async () => { - const proxy = factory.createProxy(mockMagicClient); - const params = { method: 'personal_sign', params: ['message', 'address'] }; - const mockIdToken = 'mock-id-token'; - - (mockMagicClient.user.isLoggedIn as jest.Mock).mockResolvedValue(false); - (mockAuthManager.getUser as jest.Mock).mockResolvedValue({ idToken: mockIdToken }); - - await proxy.request!(params); - - expect(mockMagicClient.user.isLoggedIn).toHaveBeenCalled(); - expect(mockAuthManager.getUser).toHaveBeenCalled(); - expect(mockMagicClient.openid.loginWithOIDC).toHaveBeenCalledWith({ - jwt: mockIdToken, - providerId: mockConfig.magicProviderId, - }); - expect(mockRpcProvider.request).toHaveBeenCalledWith(params); - }); - - it('should throw error when re-authentication fails due to missing ID token', async () => { - const proxy = factory.createProxy(mockMagicClient); - const params = { method: 'personal_sign', params: ['message', 'address'] }; - - (mockMagicClient.user.isLoggedIn as jest.Mock).mockResolvedValue(false); - (mockAuthManager.getUser as jest.Mock).mockResolvedValue(null); - - await expect(proxy.request!(params)).rejects.toThrow('ProviderProxy: failed to obtain ID token'); - }); - - it('should wrap errors with ProviderProxy prefix', async () => { - const proxy = factory.createProxy(mockMagicClient); - const params = { method: 'personal_sign', params: ['message', 'address'] }; - - (mockMagicClient.user.isLoggedIn as jest.Mock).mockRejectedValue(new Error('Test error')); - - await expect(proxy.request!(params)).rejects.toThrow('ProviderProxy: Test error'); - }); - - it('should convert eth_requestAccounts to eth_accounts to avoid Magic overlay', async () => { - const proxy = factory.createProxy(mockMagicClient); - const params = { method: 'eth_requestAccounts' }; - (mockMagicClient.user.isLoggedIn as jest.Mock).mockResolvedValue(true); - - await proxy.request!(params); - - expect(mockMagicClient.user.isLoggedIn).toHaveBeenCalled(); - expect(mockRpcProvider.request).toHaveBeenCalledWith({ method: 'eth_accounts' }); - expect(mockRpcProvider.request).not.toHaveBeenCalledWith(params); - }); - }); -}); diff --git a/packages/passport/sdk/src/magic/magicProviderProxyFactory.ts b/packages/passport/sdk/src/magic/magicProviderProxyFactory.ts deleted file mode 100644 index df18f0c0e1..0000000000 --- a/packages/passport/sdk/src/magic/magicProviderProxyFactory.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Eip1193Provider } from 'ethers'; -import AuthManager from '../authManager'; -import { PassportConfiguration } from '../config'; -import { MagicClient } from './types'; - -const shouldCheckMagicSession = (args: any[]): boolean => ( - args?.length > 0 - && typeof args[0] === 'object' - && 'method' in args[0] - && typeof args[0].method === 'string' - && ['personal_sign', 'eth_accounts', 'eth_requestAccounts'].includes(args[0].method) -); - -const isEthRequestAccountsCall = (args: any[]): boolean => ( - args?.length > 0 - && typeof args[0] === 'object' - && 'method' in args[0] - && typeof args[0].method === 'string' - && args[0].method === 'eth_requestAccounts' -); - -/** - * Factory class for creating a Magic provider that automatically handles re-authentication. - * This proxy wraps the Magic RPC provider to intercept certain RPC methods (`personal_sign`, `eth_accounts`) - * and ensures the user is properly authenticated before executing them. - */ -export class MagicProviderProxyFactory { - private authManager: AuthManager; - - private config: PassportConfiguration; - - constructor(authManager: AuthManager, config: PassportConfiguration) { - this.authManager = authManager; - this.config = config; - } - - createProxy(magicClient: MagicClient): Eip1193Provider { - const magicRpcProvider = magicClient.rpcProvider as unknown as Eip1193Provider; - - const proxyHandler: ProxyHandler = { - get: (target: Eip1193Provider, property: string, receiver: any) => { - if (property === 'request') { - return async (...args: any[]) => { - try { - if (shouldCheckMagicSession(args)) { - const isUserLoggedIn = await magicClient.user.isLoggedIn(); - if (!isUserLoggedIn) { - const user = await this.authManager.getUser(); - const idToken = user?.idToken; - if (!idToken) { - throw new Error('failed to obtain ID token'); - } - await magicClient.openid.loginWithOIDC({ - jwt: idToken, - providerId: this.config.magicProviderId, - }); - } - - if (isEthRequestAccountsCall(args)) { - // @ts-ignore - Calling eth_requestAccounts on the Magic RPC provider displays an overlay, so this - // should be avoided - call eth_accounts instead. - return target.request!({ method: 'eth_accounts' }); - } - } - - // @ts-ignore - Invoke the request method with the provided arguments - return target.request!(...args); - } catch (error: unknown) { - if (error instanceof Error) { - throw new Error(`ProviderProxy: ${error.message}`); - } - throw new Error(`ProviderProxy: ${error}`); - } - }; - } - - // Return the property from the target - return Reflect.get(target, property, receiver); - }, - }; - - return new Proxy(magicRpcProvider, proxyHandler); - } -} diff --git a/packages/passport/sdk/src/magic/magicTEESigner.test.ts b/packages/passport/sdk/src/magic/magicTEESigner.test.ts new file mode 100644 index 0000000000..ecbad20301 --- /dev/null +++ b/packages/passport/sdk/src/magic/magicTEESigner.test.ts @@ -0,0 +1,377 @@ +import { MagicTeeApiClients } from '@imtbl/generated-clients'; +import { trackDuration } from '@imtbl/metrics'; +import { isAxiosError } from 'axios'; +import MagicTEESigner from './magicTEESigner'; +import AuthManager from '../authManager'; +import { PassportError, PassportErrorType } from '../errors/passportError'; +import { mockUser } from '../test/mocks'; +import { withMetricsAsync } from '../utils/metrics'; + +// Mock all dependencies +jest.mock('@imtbl/metrics'); +jest.mock('axios'); +jest.mock('../utils/metrics'); + +describe('MagicTEESigner', () => { + let magicTEESigner: MagicTEESigner; + let mockAuthManager: jest.Mocked; + let mockMagicTeeApiClient: jest.Mocked; + let mockFlow: any; + let mockCreateWalletV1WalletPost: jest.Mock; + let mockSignMessageV1WalletPersonalSignPost: jest.Mock; + + const mockWalletResponse = { + data: { + public_address: '0x123456789abcdef', + }, + }; + + const mockSignatureResponse = { + data: { + signature: '0xsignature123', + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + + // Mock AuthManager + mockAuthManager = { + getUser: jest.fn(), + } as any; + + // Mock API methods + mockCreateWalletV1WalletPost = jest.fn(); + mockSignMessageV1WalletPersonalSignPost = jest.fn(); + + // Mock MagicTeeApiClients + mockMagicTeeApiClient = { + walletApi: { + createWalletV1WalletPost: mockCreateWalletV1WalletPost, + }, + transactionApi: { + signMessageV1WalletPersonalSignPost: mockSignMessageV1WalletPersonalSignPost, + }, + } as any; + + // Mock Flow + mockFlow = { + details: { + flowName: 'testFlow', + flowId: '123', + }, + addEvent: jest.fn(), + }; + + // Mock withMetricsAsync + (withMetricsAsync as jest.Mock).mockImplementation(async (fn) => fn(mockFlow)); + + // Mock trackDuration + (trackDuration as jest.Mock).mockImplementation(() => {}); + + // Mock isAxiosError + (isAxiosError as unknown as jest.Mock).mockImplementation((error) => error && error.isAxiosError === true); + + magicTEESigner = new MagicTEESigner(mockAuthManager, mockMagicTeeApiClient); + }); + + describe('getAddress', () => { + it('should return wallet address when user is logged in', async () => { + mockAuthManager.getUser.mockResolvedValue(mockUser); + mockCreateWalletV1WalletPost.mockResolvedValue(mockWalletResponse); + + const address = await magicTEESigner.getAddress(); + + expect(address).toBe('0x123456789abcdef'); + expect(mockCreateWalletV1WalletPost).toHaveBeenCalledWith( + { + createWalletRequestModel: { + chain: 'ETH', + }, + }, + { headers: { Authorization: `Bearer ${mockUser.idToken}` } }, + ); + }); + + it('should throw error when user is not logged in', async () => { + mockAuthManager.getUser.mockResolvedValue(null); + + await expect(magicTEESigner.getAddress()).rejects.toThrow( + new PassportError( + 'User has been logged out', + PassportErrorType.NOT_LOGGED_IN_ERROR, + ), + ); + }); + + it('should reuse existing wallet for same user', async () => { + mockAuthManager.getUser.mockResolvedValue(mockUser); + mockCreateWalletV1WalletPost.mockResolvedValue(mockWalletResponse); + + // Call getAddress twice + const address1 = await magicTEESigner.getAddress(); + const address2 = await magicTEESigner.getAddress(); + + expect(address1).toBe('0x123456789abcdef'); + expect(address2).toBe('0x123456789abcdef'); + // Should only call createWallet once + expect(mockCreateWalletV1WalletPost).toHaveBeenCalledTimes(1); + }); + + it('should create new wallet when user changes', async () => { + const user1 = { ...mockUser, profile: { ...mockUser.profile, sub: 'user1' } }; + const user2 = { ...mockUser, profile: { ...mockUser.profile, sub: 'user2' } }; + + mockAuthManager.getUser + .mockResolvedValueOnce(user1) + .mockResolvedValueOnce(user1) + .mockResolvedValueOnce(user2) + .mockResolvedValueOnce(user2); + + mockCreateWalletV1WalletPost.mockResolvedValue(mockWalletResponse); + + // First call with user1 + await magicTEESigner.getAddress(); + + // Second call with user2 (different user) + await magicTEESigner.getAddress(); + + expect(mockCreateWalletV1WalletPost).toHaveBeenCalledTimes(2); + }); + + it('should handle API errors gracefully', async () => { + mockAuthManager.getUser.mockResolvedValue(mockUser); + const apiError = { + isAxiosError: true, + response: { + status: 500, + data: { message: 'Internal server error' }, + }, + }; + (isAxiosError as unknown as jest.Mock).mockReturnValue(true); + mockCreateWalletV1WalletPost.mockRejectedValue(apiError); + + await expect(magicTEESigner.getAddress()).rejects.toThrow( + 'Failed to create wallet with status 500: {"message":"Internal server error"}', + ); + }); + + it('should handle network errors gracefully', async () => { + mockAuthManager.getUser.mockResolvedValue(mockUser); + const networkError = { + isAxiosError: true, + message: 'Network Error', + }; + (isAxiosError as unknown as jest.Mock).mockReturnValue(true); + mockCreateWalletV1WalletPost.mockRejectedValue(networkError); + + await expect(magicTEESigner.getAddress()).rejects.toThrow( + 'Failed to create wallet: Network Error', + ); + }); + + it('should handle non-axios errors gracefully', async () => { + mockAuthManager.getUser.mockResolvedValue(mockUser); + const genericError = new Error('Generic error'); + (isAxiosError as unknown as jest.Mock).mockReturnValue(false); + mockCreateWalletV1WalletPost.mockRejectedValue(genericError); + + await expect(magicTEESigner.getAddress()).rejects.toThrow( + 'Failed to create wallet: Generic error', + ); + }); + + it('should handle concurrent wallet creation requests', async () => { + mockAuthManager.getUser.mockResolvedValue(mockUser); + mockCreateWalletV1WalletPost.mockResolvedValue(mockWalletResponse); + + // Make two concurrent calls + const [address1, address2] = await Promise.all([ + magicTEESigner.getAddress(), + magicTEESigner.getAddress(), + ]); + + expect(address1).toBe('0x123456789abcdef'); + expect(address2).toBe('0x123456789abcdef'); + // Should only call createWallet once even with concurrent requests + expect(mockCreateWalletV1WalletPost).toHaveBeenCalledTimes(1); + }); + + it('should track metrics for wallet creation', async () => { + mockAuthManager.getUser.mockResolvedValue(mockUser); + mockCreateWalletV1WalletPost.mockResolvedValue(mockWalletResponse); + + await magicTEESigner.getAddress(); + + expect(withMetricsAsync).toHaveBeenCalledWith( + expect.any(Function), + 'magicCreateWallet', + ); + expect(trackDuration).toHaveBeenCalledWith( + 'passport', + 'testFlow', + expect.any(Number), + ); + }); + }); + + describe('signMessage', () => { + beforeEach(() => { + mockAuthManager.getUser.mockResolvedValue(mockUser); + mockCreateWalletV1WalletPost.mockResolvedValue(mockWalletResponse); + mockSignMessageV1WalletPersonalSignPost.mockResolvedValue(mockSignatureResponse); + }); + + it('should sign string message successfully', async () => { + const message = 'Hello, world!'; + const signature = await magicTEESigner.signMessage(message); + + expect(signature).toBe('0xsignature123'); + expect(mockSignMessageV1WalletPersonalSignPost).toHaveBeenCalledWith( + { + personalSignRequest: { + message_base64: Buffer.from(message, 'utf-8').toString('base64'), + chain: 'ETH', + }, + }, + { headers: { Authorization: `Bearer ${mockUser.idToken}` } }, + ); + }); + + it('should sign Uint8Array message successfully', async () => { + const message = new Uint8Array([72, 101, 108, 108, 111]); // "Hello" + const signature = await magicTEESigner.signMessage(message); + + expect(signature).toBe('0xsignature123'); + expect(mockSignMessageV1WalletPersonalSignPost).toHaveBeenCalledWith( + { + personalSignRequest: { + message_base64: Buffer.from(`0x${Buffer.from(message).toString('hex')}`, 'utf-8').toString('base64'), + chain: 'ETH', + }, + }, + { headers: { Authorization: `Bearer ${mockUser.idToken}` } }, + ); + }); + + it('should throw error when user is not logged in', async () => { + mockAuthManager.getUser.mockResolvedValue(null); + + await expect(magicTEESigner.signMessage('test')).rejects.toThrow( + new PassportError( + 'User has been logged out', + PassportErrorType.NOT_LOGGED_IN_ERROR, + ), + ); + }); + + it('should handle API errors gracefully', async () => { + const apiError = { + isAxiosError: true, + response: { + status: 400, + data: { message: 'Invalid signature request' }, + }, + }; + (isAxiosError as unknown as jest.Mock).mockReturnValue(true); + mockSignMessageV1WalletPersonalSignPost.mockRejectedValue(apiError); + + await expect(magicTEESigner.signMessage('test')).rejects.toThrow( + 'Failed to create signature using EOA with status 400: {"message":"Invalid signature request"}', + ); + }); + + it('should handle network errors gracefully', async () => { + const networkError = { + isAxiosError: true, + message: 'Network Error', + }; + (isAxiosError as unknown as jest.Mock).mockReturnValue(true); + mockSignMessageV1WalletPersonalSignPost.mockRejectedValue(networkError); + + await expect(magicTEESigner.signMessage('test')).rejects.toThrow( + 'Failed to create signature using EOA: Network Error', + ); + }); + + it('should handle non-axios errors gracefully', async () => { + const genericError = new Error('Generic error'); + (isAxiosError as unknown as jest.Mock).mockReturnValue(false); + mockSignMessageV1WalletPersonalSignPost.mockRejectedValue(genericError); + + await expect(magicTEESigner.signMessage('test')).rejects.toThrow( + 'Failed to create signature using EOA: Generic error', + ); + }); + + it('should track metrics for message signing', async () => { + await magicTEESigner.signMessage('test'); + + expect(withMetricsAsync).toHaveBeenCalledWith( + expect.any(Function), + 'magicPersonalSign', + ); + expect(trackDuration).toHaveBeenCalledWith( + 'passport', + 'testFlow', + expect.any(Number), + ); + }); + + it('should ensure wallet is created before signing', async () => { + await magicTEESigner.signMessage('test'); + + // Should call both createWallet and signMessage + expect(mockCreateWalletV1WalletPost).toHaveBeenCalled(); + expect(mockSignMessageV1WalletPersonalSignPost).toHaveBeenCalled(); + }); + }); + + describe('error handling in createWallet', () => { + it('should reset createWalletPromise on error', async () => { + mockAuthManager.getUser.mockResolvedValue(mockUser); + + const error = new Error('API Error'); + mockCreateWalletV1WalletPost + .mockRejectedValueOnce(error) + .mockResolvedValueOnce(mockWalletResponse); + + // First call should fail + await expect(magicTEESigner.getAddress()).rejects.toThrow('API Error'); + + // Second call should succeed (promise should be reset) + const address = await magicTEESigner.getAddress(); + expect(address).toBe('0x123456789abcdef'); + expect(mockCreateWalletV1WalletPost).toHaveBeenCalledTimes(2); + }); + }); + + describe('headers generation', () => { + it('should generate correct headers for authenticated user', async () => { + mockAuthManager.getUser.mockResolvedValue(mockUser); + mockCreateWalletV1WalletPost.mockResolvedValue(mockWalletResponse); + + await magicTEESigner.getAddress(); + + expect(mockCreateWalletV1WalletPost).toHaveBeenCalledWith( + expect.any(Object), + { + headers: { + Authorization: `Bearer ${mockUser.idToken}`, + }, + }, + ); + }); + + it('should throw error when trying to generate headers for null user', async () => { + mockAuthManager.getUser.mockResolvedValue(null); + + await expect(magicTEESigner.getAddress()).rejects.toThrow( + new PassportError( + 'User has been logged out', + PassportErrorType.NOT_LOGGED_IN_ERROR, + ), + ); + }); + }); +}); diff --git a/packages/passport/sdk/src/magic/magicTEESigner.ts b/packages/passport/sdk/src/magic/magicTEESigner.ts new file mode 100644 index 0000000000..a604b9fa97 --- /dev/null +++ b/packages/passport/sdk/src/magic/magicTEESigner.ts @@ -0,0 +1,200 @@ +import { AbstractSigner, Signer } from 'ethers'; +import { MagicTeeApiClients } from '@imtbl/generated-clients'; +import { isAxiosError } from 'axios'; +import { Flow, trackDuration } from '@imtbl/metrics'; +import { PassportError, PassportErrorType } from '../errors/passportError'; +import AuthManager from '../authManager'; +import { withMetricsAsync } from '../utils/metrics'; +import { User } from '../types'; + +const CHAIN_IDENTIFIER = 'ETH'; + +interface UserWallet { + userIdentifier: string; + walletAddress: string; +} + +export default class MagicTEESigner extends AbstractSigner { + private readonly authManager: AuthManager; + + private readonly magicTeeApiClient: MagicTeeApiClients; + + private userWallet: UserWallet | null = null; + + private createWalletPromise: Promise | null = null; + + constructor(authManager: AuthManager, magicTeeApiClient: MagicTeeApiClients) { + super(); + this.authManager = authManager; + this.magicTeeApiClient = magicTeeApiClient; + } + + private async getUserWallet(): Promise { + let { userWallet } = this; + if (!userWallet) { + userWallet = await this.createWallet(); + } + + // Check if the user has changed since the last createWallet request was made. If so, initialise the new user's wallet. + const user = await this.getUserOrThrow(); + if (user.profile.sub !== userWallet.userIdentifier) { + userWallet = await this.createWallet(); + } + + return userWallet; + } + + /** + * This method calls the createWallet endpoint. The user's wallet must be created before it can be used to sign messages. + * The createWallet endpoint is idempotent, so it can be called multiple times without causing an error. + * If a createWallet request is already in flight, return the existing promise. + */ + private async createWallet(): Promise { + if (this.createWalletPromise) return this.createWalletPromise; + + // eslint-disable-next-line no-async-promise-executor + this.createWalletPromise = new Promise(async (resolve, reject) => { + try { + this.userWallet = null; + + const user = await this.getUserOrThrow(); + const headers = MagicTEESigner.getHeaders(user); + + await withMetricsAsync(async (flow: Flow) => { + try { + const startTime = performance.now(); + // The createWallet endpoint is idempotent, so it can be called multiple times without causing an error. + const response = await this.magicTeeApiClient.walletApi.createWalletV1WalletPost( + { + createWalletRequestModel: { + chain: CHAIN_IDENTIFIER, + }, + }, + { headers }, + ); + + trackDuration( + 'passport', + flow.details.flowName, + Math.round(performance.now() - startTime), + ); + + this.userWallet = { + userIdentifier: user.profile.sub, + walletAddress: response.data.public_address, + }; + + return resolve(this.userWallet); + } catch (error) { + let errorMessage: string = 'Failed to create wallet'; + + if (isAxiosError(error)) { + if (error.response) { + errorMessage += ` with status ${error.response.status}: ${JSON.stringify(error.response.data)}`; + } else { + errorMessage += `: ${error.message}`; + } + } else { + errorMessage += `: ${(error as Error).message}`; + } + + return reject(new Error(errorMessage)); + } + }, 'magicCreateWallet'); + } catch (error) { + reject(error); + } finally { + this.createWalletPromise = null; + } + }); + + return this.createWalletPromise; + } + + private async getUserOrThrow(): Promise { + const user = await this.authManager.getUser(); + if (!user) { + throw new PassportError( + 'User has been logged out', + PassportErrorType.NOT_LOGGED_IN_ERROR, + ); + } + return user; + } + + private static getHeaders(user: User): Record { + if (!user) { + throw new PassportError( + 'User has been logged out', + PassportErrorType.NOT_LOGGED_IN_ERROR, + ); + } + return { + Authorization: `Bearer ${user.idToken}`, + }; + } + + public async getAddress(): Promise { + const userWallet = await this.getUserWallet(); + return userWallet.walletAddress; + } + + public async signMessage(message: string | Uint8Array): Promise { + // Call getUserWallet to ensure that the createWallet endpoint has been called at least once, + // as this is a prerequisite for signing messages. + await this.getUserWallet(); + + const messageToSign = message instanceof Uint8Array ? `0x${Buffer.from(message).toString('hex')}` : message; + const user = await this.getUserOrThrow(); + const headers = await MagicTEESigner.getHeaders(user); + + return withMetricsAsync(async (flow: Flow) => { + try { + const startTime = performance.now(); + const response = await this.magicTeeApiClient.transactionApi.signMessageV1WalletPersonalSignPost({ + personalSignRequest: { + message_base64: Buffer.from(messageToSign, 'utf-8').toString('base64'), + chain: CHAIN_IDENTIFIER, + }, + }, { headers }); + + trackDuration( + 'passport', + flow.details.flowName, + Math.round(performance.now() - startTime), + ); + + return response.data.signature; + } catch (error) { + let errorMessage: string = 'Failed to create signature using EOA'; + + if (isAxiosError(error)) { + if (error.response) { + errorMessage += ` with status ${error.response.status}: ${JSON.stringify(error.response.data)}`; + } else { + errorMessage += `: ${error.message}`; + } + } else { + errorMessage += `: ${(error as Error).message}`; + } + + throw new Error(errorMessage); + } + }, 'magicPersonalSign'); + } + + // eslint-disable-next-line class-methods-use-this + connect(): Signer { + throw new Error('Method not implemented.'); + } + + // eslint-disable-next-line class-methods-use-this + signTransaction(): Promise { + throw new Error('Method not implemented.'); + } + + // eslint-disable-next-line class-methods-use-this + signTypedData(): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/passport/sdk/src/mocks/zkEvm/msw.ts b/packages/passport/sdk/src/mocks/zkEvm/msw.ts index fb9fd18c75..3f10b6ebfd 100644 --- a/packages/passport/sdk/src/mocks/zkEvm/msw.ts +++ b/packages/passport/sdk/src/mocks/zkEvm/msw.ts @@ -11,6 +11,7 @@ export const transactionHash = '0x867'; const mandatoryHandlers = [ rest.get('https://api.sandbox.immutable.com/v1/sdk/session-activity/check', async (req, res, ctx) => res(ctx.status(404))), + rest.post('https://api.immutable.com/v1/sdk/metrics', async (req, res, ctx) => res(ctx.status(200))), rest.post('https://rpc.testnet.immutable.com', async (req, res, ctx) => { const body = await req.json(); switch (body.method) { @@ -28,10 +29,42 @@ const mandatoryHandlers = [ } } }), + rest.post('https://tee.express.magiclabs.com/v1/wallet', (req, res, ctx) => res( + ctx.status(201), + ctx.json({ + public_address: '0x123456789abcdef', + }), + )), + rest.post('https://tee.express.magiclabs.com/v1/wallet/personal-sign', (req, res, ctx) => res( + ctx.status(200), + ctx.json({ + signature: '0x6b168cf5d90189eaa51d02ff3fa8ffc8956b1ea20fdd34280f521b1acca092305b9ace24e643fe64a30c528323065f5b77e1fb4045bd330aad01e7b9a07591f91b', + }), + )), ]; const chainName = `${encodeURIComponent(ChainName.IMTBL_ZKEVM_TESTNET)}`; export const mswHandlers = { + magicTEE: { + createWallet: { + success: rest.post('https://tee.express.magiclabs.com/v1/wallet', (req, res, ctx) => res( + ctx.status(201), + ctx.json({ + public_address: '0x123456789abcdef', + }), + )), + internalServerError: rest.post('https://tee.express.magiclabs.com/v1/wallet', (req, res, ctx) => res(ctx.status(500))), + }, + personalSign: { + success: rest.post('https://tee.express.magiclabs.com/v1/wallet/personal-sign', (req, res, ctx) => res( + ctx.status(200), + ctx.json({ + signature: '0x6b168cf5d90189eaa51d02ff3fa8ffc8956b1ea20fdd34280f521b1acca092305b9ace24e643fe64a30c528323065f5b77e1fb4045bd330aad01e7b9a07591f91b', + }), + )), + internalServerError: rest.post('https://tee.express.magiclabs.com/v1/wallet/personal-sign', (req, res, ctx) => res(ctx.status(500))), + }, + }, counterfactualAddress: { success: rest.post( `https://api.sandbox.immutable.com/v2/chains/${chainName}/passport/counterfactual-address`, diff --git a/packages/passport/sdk/src/starkEx/passportImxProvider.test.ts b/packages/passport/sdk/src/starkEx/passportImxProvider.test.ts index 3df8cca227..e5fb70d0cd 100644 --- a/packages/passport/sdk/src/starkEx/passportImxProvider.test.ts +++ b/packages/passport/sdk/src/starkEx/passportImxProvider.test.ts @@ -12,7 +12,6 @@ import { UnsignedTransferRequest, } from '@imtbl/x-client'; import { trackError, trackFlow } from '@imtbl/metrics'; -import { BrowserProvider } from 'ethers'; import registerPassportStarkEx from './workflows/registration'; import { mockUser, mockUserImx } from '../test/mocks'; import { PassportError, PassportErrorType } from '../errors/passportError'; @@ -23,7 +22,7 @@ import { import { PassportEventMap, PassportEvents } from '../types'; import TypedEventEmitter from '../utils/typedEventEmitter'; import AuthManager from '../authManager'; -import MagicAdapter from '../magic/magicAdapter'; +import MagicTEESigner from '../magic/magicTEESigner'; import { getStarkSigner } from './getStarkSigner'; import GuardianClient from '../guardian'; @@ -61,13 +60,9 @@ describe('PassportImxProvider', () => { getYCoordinate: jest.fn(), } as StarkSigner; - const mockEthSigner = { - signMessage: jest.fn(), + const mockMagicTEESigner = { getAddress: jest.fn(), - }; - - const magicAdapterMock = { - login: jest.fn(), + signMessage: jest.fn(), }; const mockGuardianClient = { @@ -75,15 +70,12 @@ describe('PassportImxProvider', () => { withConfirmationScreenTask: () => (task: () => any) => task, }; - const getSignerMock = jest.fn(); - let passportEventEmitter: TypedEventEmitter; const imxApiClients = new ImxApiClients({} as any); beforeEach(() => { jest.restoreAllMocks(); - getSignerMock.mockReturnValue(mockEthSigner); (registerPassportStarkEx as jest.Mock).mockResolvedValue(null); passportEventEmitter = new TypedEventEmitter(); mockAuthManager.getUser.mockResolvedValue(mockUserImx); @@ -97,13 +89,11 @@ describe('PassportImxProvider', () => { })); // Signers - magicAdapterMock.login.mockResolvedValue({ getSigner: getSignerMock }); - (BrowserProvider as unknown as jest.Mock).mockReturnValue({ getSigner: getSignerMock }); (getStarkSigner as jest.Mock).mockResolvedValue(mockStarkSigner); passportImxProvider = new PassportImxProvider({ authManager: mockAuthManager as unknown as AuthManager, - magicAdapter: magicAdapterMock as unknown as MagicAdapter, + magicTEESigner: mockMagicTEESigner as unknown as MagicTEESigner, guardianClient: mockGuardianClient as unknown as GuardianClient, immutableXClient, passportEventEmitter, @@ -116,8 +106,7 @@ describe('PassportImxProvider', () => { // The promise is created in the constructor but not awaited until a method is called await passportImxProvider.getAddress(); - expect(magicAdapterMock.login).toHaveBeenCalledWith(mockUserImx.idToken); - expect(getStarkSigner).toHaveBeenCalledWith(mockEthSigner); + expect(getStarkSigner).toHaveBeenCalledWith(mockMagicTEESigner); }); it('initialises the eth and stark signers only once', async () => { @@ -125,14 +114,12 @@ describe('PassportImxProvider', () => { await passportImxProvider.getAddress(); await passportImxProvider.getAddress(); - expect(magicAdapterMock.login).toHaveBeenCalledTimes(1); expect(getStarkSigner).toHaveBeenCalledTimes(1); }); it('re-throws the initialisation error when a method is called', async () => { mockAuthManager.getUser.mockResolvedValue(mockUserImx); // Signers - magicAdapterMock.login.mockResolvedValue({}); (getStarkSigner as jest.Mock).mockRejectedValue(new Error('error')); // Metrics @@ -145,7 +132,7 @@ describe('PassportImxProvider', () => { const pp = new PassportImxProvider({ authManager: mockAuthManager as unknown as AuthManager, - magicAdapter: magicAdapterMock as unknown as MagicAdapter, + magicTEESigner: mockMagicTEESigner as unknown as MagicTEESigner, guardianClient: mockGuardianClient as unknown as GuardianClient, immutableXClient, passportEventEmitter: new TypedEventEmitter(), @@ -360,15 +347,12 @@ describe('PassportImxProvider', () => { describe('registerOffChain', () => { it('should register the user and update the provider instance user', async () => { - const magicProviderMock = {}; - mockAuthManager.login.mockResolvedValue(mockUser); - magicAdapterMock.login.mockResolvedValue(magicProviderMock); mockAuthManager.forceUserRefresh.mockResolvedValue({ ...mockUser, imx: { ethAddress: '', starkAddress: '', userAdminAddress: '' } }); await passportImxProvider.registerOffchain(); expect(registerPassportStarkEx).toHaveBeenCalledWith({ - ethSigner: mockEthSigner, + ethSigner: mockMagicTEESigner, starkSigner: mockStarkSigner, imxApiClients: new ImxApiClients({} as any), }, mockUserImx.accessToken); diff --git a/packages/passport/sdk/src/starkEx/passportImxProvider.ts b/packages/passport/sdk/src/starkEx/passportImxProvider.ts index ad7bc920d1..fb7a0e9a3f 100644 --- a/packages/passport/sdk/src/starkEx/passportImxProvider.ts +++ b/packages/passport/sdk/src/starkEx/passportImxProvider.ts @@ -1,6 +1,5 @@ import { AnyToken, - EthSigner, IMXClient, NftTransferDetails, StarkSigner, @@ -14,35 +13,34 @@ import { imx, ImxApiClients, } from '@imtbl/generated-clients'; -import { BrowserProvider, TransactionResponse } from 'ethers'; -import TypedEventEmitter from '../utils/typedEventEmitter'; +import { TransactionResponse } from 'ethers'; import AuthManager from '../authManager'; import GuardianClient from '../guardian'; import { - PassportEventMap, PassportEvents, UserImx, User, IMXSigners, isUserImx, + PassportEventMap, PassportEvents, UserImx, User, isUserImx, } from '../types'; import { PassportError, PassportErrorType } from '../errors/passportError'; import { batchNftTransfer, cancelOrder, createOrder, createTrade, exchangeTransfer, transfer, } from './workflows'; import registerOffchain from './workflows/registerOffchain'; -import MagicAdapter from '../magic/magicAdapter'; import { getStarkSigner } from './getStarkSigner'; import { withMetricsAsync } from '../utils/metrics'; +import MagicTEESigner from '../magic/magicTEESigner'; +import TypedEventEmitter from '../utils/typedEventEmitter'; export interface PassportImxProviderOptions { authManager: AuthManager; immutableXClient: IMXClient; passportEventEmitter: TypedEventEmitter; - magicAdapter: MagicAdapter; + magicTEESigner: MagicTEESigner; imxApiClients: ImxApiClients; guardianClient: GuardianClient; } -type RegisteredUserAndSigners = { +type RegisteredUserAndStarkSigner = { user: UserImx; starkSigner: StarkSigner; - ethSigner: EthSigner; }; export class PassportImxProvider implements IMXProvider { @@ -54,7 +52,7 @@ export class PassportImxProvider implements IMXProvider { protected readonly imxApiClients: ImxApiClients; - protected magicAdapter: MagicAdapter; + protected magicTEESigner: MagicTEESigner; /** * This property is set during initialisation and stores the signers in a promise. @@ -62,7 +60,7 @@ export class PassportImxProvider implements IMXProvider { * `#getSigners` method. * @see #getSigners */ - private signers: Promise | undefined; + private starkSigner: Promise | undefined; private signerInitialisationError: unknown | undefined; @@ -70,22 +68,22 @@ export class PassportImxProvider implements IMXProvider { authManager, immutableXClient, passportEventEmitter, - magicAdapter, + magicTEESigner, imxApiClients, guardianClient, }: PassportImxProviderOptions) { this.authManager = authManager; this.immutableXClient = immutableXClient; - this.magicAdapter = magicAdapter; + this.magicTEESigner = magicTEESigner; this.imxApiClients = imxApiClients; this.guardianClient = guardianClient; - this.#initialiseSigners(); + this.#initialiseSigner(); passportEventEmitter.on(PassportEvents.LOGGED_OUT, this.handleLogout); } private handleLogout = (): void => { - this.signers = undefined; + this.starkSigner = undefined; }; /** @@ -100,23 +98,11 @@ export class PassportImxProvider implements IMXProvider { * @see #getSigners * */ - #initialiseSigners() { - const generateSigners = async (): Promise => { - const user = await this.authManager.getUser(); - // The user will be present because the factory validates it - const magicRpcProvider = await this.magicAdapter.login(user!.idToken!); - const browserProvider = new BrowserProvider(magicRpcProvider); - - const ethSigner = await browserProvider.getSigner(); - const starkSigner = await getStarkSigner(ethSigner); - - return { ethSigner, starkSigner }; - }; - + #initialiseSigner() { // eslint-disable-next-line no-async-promise-executor - this.signers = new Promise(async (resolve) => { + this.starkSigner = new Promise(async (resolve) => { try { - resolve(await generateSigners()); + resolve(await getStarkSigner(this.magicTEESigner)); } catch (err) { // Capture and store the initialization error this.signerInitialisationError = err; @@ -128,7 +114,7 @@ export class PassportImxProvider implements IMXProvider { async #getAuthenticatedUser(): Promise { const user = await this.authManager.getUser(); - if (!user || !this.signers) { + if (!user || !this.starkSigner) { throw new PassportError( 'User has been logged out', PassportErrorType.NOT_LOGGED_IN_ERROR, @@ -138,10 +124,10 @@ export class PassportImxProvider implements IMXProvider { return user; } - async #getSigners(): Promise { - const signers = await this.signers; + async #getStarkSigner(): Promise { + const signer = await this.starkSigner; // Throw the stored error if the signers failed to initialise - if (typeof signers === 'undefined') { + if (typeof signer === 'undefined') { if (typeof this.signerInitialisationError !== 'undefined') { // eslint-disable-next-line @typescript-eslint/no-throw-literal throw this.signerInitialisationError; @@ -149,13 +135,13 @@ export class PassportImxProvider implements IMXProvider { throw new Error('Signers failed to initialise'); } - return signers; + return signer; } - async #getRegisteredImxUserAndSigners(): Promise { - const [user, signers] = await Promise.all([ + async #getRegisteredImxUserAndStarkSigner(): Promise { + const [user, starkSigner] = await Promise.all([ this.#getAuthenticatedUser(), - this.#getSigners(), + this.#getStarkSigner(), ]); if (!isUserImx(user)) { @@ -167,15 +153,14 @@ export class PassportImxProvider implements IMXProvider { return { user, - starkSigner: signers.starkSigner, - ethSigner: signers.ethSigner, + starkSigner, }; } async transfer(request: UnsignedTransferRequest): Promise { return withMetricsAsync(() => this.guardianClient.withDefaultConfirmationScreenTask( async () => { - const { user, starkSigner } = await this.#getRegisteredImxUserAndSigners(); + const { user, starkSigner } = await this.#getRegisteredImxUserAndStarkSigner(); return transfer({ request, @@ -191,14 +176,14 @@ export class PassportImxProvider implements IMXProvider { async registerOffchain(): Promise { return withMetricsAsync( async () => { - const [user, signers] = await Promise.all([ + const [user, starkSigner] = await Promise.all([ this.#getAuthenticatedUser(), - this.#getSigners(), + this.#getStarkSigner(), ]); return await registerOffchain( - signers.ethSigner, - signers.starkSigner, + this.magicTEESigner, + starkSigner, user, this.authManager, this.imxApiClients, @@ -229,7 +214,7 @@ export class PassportImxProvider implements IMXProvider { async createOrder(request: UnsignedOrderRequest): Promise { return withMetricsAsync(() => this.guardianClient.withDefaultConfirmationScreenTask( async () => { - const { user, starkSigner } = await this.#getRegisteredImxUserAndSigners(); + const { user, starkSigner } = await this.#getRegisteredImxUserAndStarkSigner(); return createOrder({ request, user, @@ -246,7 +231,7 @@ export class PassportImxProvider implements IMXProvider { ): Promise { return withMetricsAsync(() => this.guardianClient.withDefaultConfirmationScreenTask( async () => { - const { user, starkSigner } = await this.#getRegisteredImxUserAndSigners(); + const { user, starkSigner } = await this.#getRegisteredImxUserAndStarkSigner(); return cancelOrder({ request, @@ -262,7 +247,7 @@ export class PassportImxProvider implements IMXProvider { async createTrade(request: imx.GetSignableTradeRequest): Promise { return withMetricsAsync(() => this.guardianClient.withDefaultConfirmationScreenTask( async () => { - const { user, starkSigner } = await this.#getRegisteredImxUserAndSigners(); + const { user, starkSigner } = await this.#getRegisteredImxUserAndStarkSigner(); return createTrade({ request, @@ -281,11 +266,12 @@ export class PassportImxProvider implements IMXProvider { return withMetricsAsync(() => this.guardianClient.withConfirmationScreenTask( { width: 480, height: 784 }, )(async () => { - const { user, starkSigner } = await this.#getRegisteredImxUserAndSigners(); + const { user, starkSigner } = await this.#getRegisteredImxUserAndStarkSigner(); return batchNftTransfer({ request, user, + starkSigner, transfersApi: this.immutableXClient.transfersApi, guardianClient: this.guardianClient, @@ -297,7 +283,7 @@ export class PassportImxProvider implements IMXProvider { request: UnsignedExchangeTransferRequest, ): Promise { return withMetricsAsync(async () => { - const { user, starkSigner } = await this.#getRegisteredImxUserAndSigners(); + const { user, starkSigner } = await this.#getRegisteredImxUserAndStarkSigner(); return exchangeTransfer({ request, diff --git a/packages/passport/sdk/src/starkEx/passportImxProviderFactory.test.ts b/packages/passport/sdk/src/starkEx/passportImxProviderFactory.test.ts index 486845f62e..cac0d07975 100644 --- a/packages/passport/sdk/src/starkEx/passportImxProviderFactory.test.ts +++ b/packages/passport/sdk/src/starkEx/passportImxProviderFactory.test.ts @@ -1,7 +1,7 @@ import { IMXClient } from '@imtbl/x-client'; import { ImxApiClients } from '@imtbl/generated-clients'; import { PassportImxProviderFactory } from './passportImxProviderFactory'; -import MagicAdapter from '../magic/magicAdapter'; +import MagicTEESigner from '../magic/magicTEESigner'; import AuthManager from '../authManager'; import { PassportError, PassportErrorType } from '../errors/passportError'; import { PassportEventMap } from '../types'; @@ -20,7 +20,7 @@ describe('PassportImxProviderFactory', () => { }; const imxApiClients = new ImxApiClients({} as any); - const mockMagicAdapter = {}; + const mockMagicTEESigner = {}; const immutableXClient = { usersApi: {}, } as IMXClient; @@ -29,7 +29,7 @@ describe('PassportImxProviderFactory', () => { const passportImxProviderFactory = new PassportImxProviderFactory({ immutableXClient, authManager: mockAuthManager as unknown as AuthManager, - magicAdapter: mockMagicAdapter as unknown as MagicAdapter, + magicTEESigner: mockMagicTEESigner as unknown as MagicTEESigner, passportEventEmitter, imxApiClients, guardianClient, @@ -77,7 +77,7 @@ describe('PassportImxProviderFactory', () => { expect(result).toBe(mockPassportImxProvider); expect(mockAuthManager.getUserOrLogin).toHaveBeenCalledTimes(1); expect(PassportImxProvider).toHaveBeenCalledWith({ - magicAdapter: mockMagicAdapter, + magicTEESigner: mockMagicTEESigner, authManager: mockAuthManager, immutableXClient, passportEventEmitter, diff --git a/packages/passport/sdk/src/starkEx/passportImxProviderFactory.ts b/packages/passport/sdk/src/starkEx/passportImxProviderFactory.ts index dcbd56d705..8e113b25e5 100644 --- a/packages/passport/sdk/src/starkEx/passportImxProviderFactory.ts +++ b/packages/passport/sdk/src/starkEx/passportImxProviderFactory.ts @@ -3,16 +3,16 @@ import { IMXProvider } from '@imtbl/x-provider'; import { ImxApiClients } from '@imtbl/generated-clients'; import { PassportError, PassportErrorType } from '../errors/passportError'; import AuthManager from '../authManager'; -import MagicAdapter from '../magic/magicAdapter'; import { PassportEventMap, User } from '../types'; -import TypedEventEmitter from '../utils/typedEventEmitter'; import { PassportImxProvider } from './passportImxProvider'; import GuardianClient from '../guardian'; +import MagicTEESigner from '../magic/magicTEESigner'; +import TypedEventEmitter from '../utils/typedEventEmitter'; export type PassportImxProviderFactoryInput = { authManager: AuthManager; immutableXClient: IMXClient; - magicAdapter: MagicAdapter; + magicTEESigner: MagicTEESigner; passportEventEmitter: TypedEventEmitter; imxApiClients: ImxApiClients; guardianClient: GuardianClient; @@ -23,7 +23,7 @@ export class PassportImxProviderFactory { private readonly immutableXClient: IMXClient; - private readonly magicAdapter: MagicAdapter; + private readonly magicTEESigner: MagicTEESigner; private readonly passportEventEmitter: TypedEventEmitter; @@ -34,14 +34,14 @@ export class PassportImxProviderFactory { constructor({ authManager, immutableXClient, - magicAdapter, + magicTEESigner, passportEventEmitter, imxApiClients, guardianClient, }: PassportImxProviderFactoryInput) { this.authManager = authManager; this.immutableXClient = immutableXClient; - this.magicAdapter = magicAdapter; + this.magicTEESigner = magicTEESigner; this.passportEventEmitter = passportEventEmitter; this.imxApiClients = imxApiClients; this.guardianClient = guardianClient; @@ -73,7 +73,7 @@ export class PassportImxProviderFactory { authManager: this.authManager, immutableXClient: this.immutableXClient, passportEventEmitter: this.passportEventEmitter, - magicAdapter: this.magicAdapter, + magicTEESigner: this.magicTEESigner, imxApiClients: this.imxApiClients, guardianClient: this.guardianClient, }); diff --git a/packages/passport/sdk/src/types.ts b/packages/passport/sdk/src/types.ts index b0e3bfddcb..46371ffa25 100644 --- a/packages/passport/sdk/src/types.ts +++ b/packages/passport/sdk/src/types.ts @@ -1,5 +1,5 @@ import { Environment, ModuleConfiguration } from '@imtbl/config'; -import { EthSigner, IMXClient, StarkSigner } from '@imtbl/x-client'; +import { IMXClient } from '@imtbl/x-client'; import { ImxApiClients } from '@imtbl/generated-clients'; import { Flow } from '@imtbl/metrics'; @@ -36,18 +36,23 @@ export type UserProfile = { sub: string; }; +export enum RollupType { + IMX = 'imx', + ZKEVM = 'zkEvm', +} + export type User = { idToken?: string; accessToken: string; refreshToken?: string; profile: UserProfile; expired?: boolean; - imx?: { + [RollupType.IMX]?: { ethAddress: string; starkAddress: string; userAdminAddress: string; }; - zkEvm?: { + [RollupType.ZKEVM]?: { ethAddress: string; userAdminAddress: string; }; @@ -122,11 +127,11 @@ export interface PassportModuleConfiguration type WithRequired = T & { [P in K]-?: T[P] }; -export type UserImx = WithRequired; -export type UserZkEvm = WithRequired; +export type UserImx = WithRequired; +export type UserZkEvm = WithRequired; -export const isUserZkEvm = (user: User): user is UserZkEvm => !!user.zkEvm; -export const isUserImx = (user: User): user is UserImx => !!user.imx; +export const isUserZkEvm = (user: User): user is UserZkEvm => !!user[RollupType.ZKEVM]; +export const isUserImx = (user: User): user is UserImx => !!user[RollupType.IMX]; export type DeviceTokenResponse = { access_token: string; @@ -156,11 +161,6 @@ export type PKCEData = { verifier: string; }; -export type IMXSigners = { - starkSigner: StarkSigner; - ethSigner: EthSigner; -}; - export type LinkWalletParams = { type: string; walletAddress: string; diff --git a/packages/passport/sdk/src/zkEvm/transactionHelpers.test.ts b/packages/passport/sdk/src/zkEvm/transactionHelpers.test.ts index faf66e398b..0d6f80465d 100644 --- a/packages/passport/sdk/src/zkEvm/transactionHelpers.test.ts +++ b/packages/passport/sdk/src/zkEvm/transactionHelpers.test.ts @@ -1,12 +1,13 @@ import { Flow } from '@imtbl/metrics'; -import { JsonRpcProvider, Signer } from 'ethers'; +import { JsonRpcProvider } from 'ethers'; import { RelayerClient } from './relayerClient'; import GuardianClient from '../guardian'; -import { FeeOption, MetaTransaction, RelayerTransactionStatus } from './types'; +import { FeeOption, RelayerTransactionStatus } from './types'; import { JsonRpcError, RpcErrorCode } from './JsonRpcError'; import { pollRelayerTransaction, prepareAndSignEjectionTransaction, prepareAndSignTransaction } from './transactionHelpers'; import * as walletHelpers from './walletHelpers'; import { retryWithDelay } from '../network/retry'; +import MagicTeeAdapter from '../magic/magicTEESigner'; jest.mock('./walletHelpers', () => ({ __esModule: true, @@ -17,6 +18,11 @@ jest.mock('../network/retry'); describe('transactionHelpers', () => { const flow = { addEvent: jest.fn() } as unknown as Flow; + const magicTeeAdapter = { + personalSign: jest.fn(), + createWallet: jest.fn(), + } as unknown as MagicTeeAdapter; + beforeEach(() => { jest.resetAllMocks(); }); @@ -66,23 +72,16 @@ describe('transactionHelpers', () => { describe('prepareAndSignTransaction', () => { const chainId = 123n; const nonce = BigInt(5); - const zkEvmAddress = '0x1234567890123456789012345678901234567890'; + const zkEvmAddresses = { + ethAddress: '0x1234567890123456789012345678901234567890', + userAdminAddress: '0x4567890123456789012345678901234567890123', + }; const transactionRequest = { to: '0x1234567890123456789012345678901234567890', data: '0x456', value: '0x00', }; - const metaTransactions: MetaTransaction[] = [ - { - to: transactionRequest.to, - data: transactionRequest.data, - nonce, - value: transactionRequest.value, - revertOnError: true, - }, - ]; - const signedTransactions = 'signedTransactions123'; const relayerId = 'relayerId123'; @@ -107,15 +106,12 @@ describe('transactionHelpers', () => { validateEVMTransaction: jest.fn().mockResolvedValue(undefined), } as unknown as GuardianClient; - const ethSigner = {} as Signer; - beforeEach(() => { jest.resetAllMocks(); jest.spyOn(walletHelpers, 'signMetaTransactions').mockResolvedValue(signedTransactions); jest.spyOn(walletHelpers, 'getNonce').mockResolvedValue(nonce); - jest.spyOn(walletHelpers, 'getNormalisedTransactions').mockReturnValue(metaTransactions as any); jest.spyOn(walletHelpers, 'encodedTransactions').mockReturnValue('encodedTransactions123'); - jest.spyOn(rpcProvider, 'getNetwork').mockResolvedValue({ chainId } as any); + (rpcProvider.getNetwork as jest.Mock).mockResolvedValue({ chainId }); jest.spyOn(relayerClient, 'imGetFeeOptions').mockResolvedValue([imxFeeOption]); jest.spyOn(relayerClient, 'ethSendTransaction').mockResolvedValue(relayerId); jest.spyOn(guardianClient, 'validateEVMTransaction').mockResolvedValue(undefined); @@ -124,11 +120,11 @@ describe('transactionHelpers', () => { it('prepares and signs transaction correctly', async () => { const result = await prepareAndSignTransaction({ transactionRequest, - ethSigner, + ethSigner: magicTeeAdapter, rpcProvider, guardianClient, relayerClient, - zkEvmAddress, + zkEvmAddress: zkEvmAddresses.ethAddress, flow, }); @@ -141,7 +137,7 @@ describe('transactionHelpers', () => { expect(rpcProvider.getNetwork).toHaveBeenCalled(); expect(guardianClient.validateEVMTransaction).toHaveBeenCalled(); expect(walletHelpers.signMetaTransactions).toHaveBeenCalled(); - expect(relayerClient.ethSendTransaction).toHaveBeenCalledWith(zkEvmAddress, signedTransactions); + expect(relayerClient.ethSendTransaction).toHaveBeenCalledWith(zkEvmAddresses.ethAddress, signedTransactions); expect(flow.addEvent).toHaveBeenCalledWith('endDetectNetwork'); expect(flow.addEvent).toHaveBeenCalledWith('endBuildMetaTransactions'); expect(flow.addEvent).toHaveBeenCalledWith('endValidateEVMTransaction'); @@ -155,11 +151,11 @@ describe('transactionHelpers', () => { await prepareAndSignTransaction({ transactionRequest, - ethSigner, + ethSigner: magicTeeAdapter, rpcProvider, guardianClient, relayerClient, - zkEvmAddress, + zkEvmAddress: zkEvmAddresses.ethAddress, flow, }); @@ -190,11 +186,11 @@ describe('transactionHelpers', () => { await prepareAndSignTransaction({ transactionRequest, - ethSigner, + ethSigner: magicTeeAdapter, rpcProvider, guardianClient, relayerClient, - zkEvmAddress, + zkEvmAddress: zkEvmAddresses.ethAddress, flow, }); @@ -236,8 +232,8 @@ describe('transactionHelpers', () => { ]), expect.any(BigInt), expect.any(BigInt), - zkEvmAddress, - ethSigner, + zkEvmAddresses.ethAddress, + magicTeeAdapter, ); }); @@ -246,11 +242,11 @@ describe('transactionHelpers', () => { const result = await prepareAndSignTransaction({ transactionRequest, - ethSigner, + ethSigner: magicTeeAdapter, rpcProvider, guardianClient, relayerClient, - zkEvmAddress, + zkEvmAddress: zkEvmAddresses.ethAddress, flow, }); @@ -266,11 +262,11 @@ describe('transactionHelpers', () => { await expect(prepareAndSignTransaction({ transactionRequest, - ethSigner, + ethSigner: magicTeeAdapter, rpcProvider, guardianClient, relayerClient, - zkEvmAddress, + zkEvmAddress: zkEvmAddresses.ethAddress, flow, })).rejects.toThrow('Validation failed'); @@ -284,11 +280,11 @@ describe('transactionHelpers', () => { await expect(prepareAndSignTransaction({ transactionRequest, - ethSigner, + ethSigner: magicTeeAdapter, rpcProvider, guardianClient, relayerClient, - zkEvmAddress, + zkEvmAddress: zkEvmAddresses.ethAddress, flow, })).rejects.toThrow('Signing failed'); }); @@ -298,11 +294,11 @@ describe('transactionHelpers', () => { await expect(prepareAndSignTransaction({ transactionRequest, - ethSigner, + ethSigner: magicTeeAdapter, rpcProvider, guardianClient, relayerClient, - zkEvmAddress, + zkEvmAddress: zkEvmAddresses.ethAddress, flow, })).rejects.toThrow('Transaction send failed'); }); @@ -312,11 +308,11 @@ describe('transactionHelpers', () => { await expect(prepareAndSignTransaction({ transactionRequest, - ethSigner, + ethSigner: magicTeeAdapter, rpcProvider, guardianClient, relayerClient, - zkEvmAddress, + zkEvmAddress: zkEvmAddresses.ethAddress, flow, })).rejects.toThrow('Invalid fee options received from relayer'); }); @@ -326,11 +322,11 @@ describe('transactionHelpers', () => { await expect(prepareAndSignTransaction({ transactionRequest, - ethSigner, + ethSigner: magicTeeAdapter, rpcProvider, guardianClient, relayerClient, - zkEvmAddress, + zkEvmAddress: zkEvmAddresses.ethAddress, flow, })).rejects.toThrow('Invalid fee options received from relayer'); }); @@ -340,11 +336,11 @@ describe('transactionHelpers', () => { await expect(prepareAndSignTransaction({ transactionRequest, - ethSigner, + ethSigner: magicTeeAdapter, rpcProvider, guardianClient, relayerClient, - zkEvmAddress, + zkEvmAddress: zkEvmAddresses.ethAddress, flow, })).rejects.toThrow('Invalid fee options received from relayer'); }); @@ -360,8 +356,10 @@ describe('transactionHelpers', () => { chainId, }; - const zkEvmAddress = '0x1234567890123456789012345678901234567890'; - const ethSigner = {} as Signer; + const zkEvmAddresses = { + ethAddress: '0x1234567890123456789012345678901234567890', + userAdminAddress: '0x4567890123456789012345678901234567890123', + }; const signedTransactions = 'signedTransactions123'; beforeEach(() => { @@ -376,15 +374,15 @@ describe('transactionHelpers', () => { ...transactionRequest, nonce: 0, }, - ethSigner, - zkEvmAddress, + ethSigner: magicTeeAdapter, + zkEvmAddress: zkEvmAddresses.ethAddress, flow, }); expect(result).toEqual({ chainId: 'eip155:123', data: signedTransactions, - to: zkEvmAddress, + to: zkEvmAddresses.ethAddress, }); }); }); diff --git a/packages/passport/sdk/src/zkEvm/zkEvmProvider.test.ts b/packages/passport/sdk/src/zkEvm/zkEvmProvider.test.ts index 460c09ee29..fc86bb734d 100644 --- a/packages/passport/sdk/src/zkEvm/zkEvmProvider.test.ts +++ b/packages/passport/sdk/src/zkEvm/zkEvmProvider.test.ts @@ -1,9 +1,9 @@ import { identify, trackFlow } from '@imtbl/metrics'; -import { BrowserProvider, JsonRpcProvider, toBeHex } from 'ethers'; +import { JsonRpcProvider, toBeHex } from 'ethers'; import AuthManager from '../authManager'; import { ZkEvmProvider, ZkEvmProviderInput } from './zkEvmProvider'; import { sendTransaction } from './sendTransaction'; -import { JsonRpcError, ProviderErrorCode, RpcErrorCode } from './JsonRpcError'; +import { JsonRpcError, ProviderErrorCode } from './JsonRpcError'; import GuardianClient from '../guardian'; import { RelayerClient } from './relayerClient'; import { Provider, RequestArguments } from './types'; @@ -11,13 +11,12 @@ import { PassportEventMap, PassportEvents } from '../types'; import TypedEventEmitter from '../utils/typedEventEmitter'; import { mockUser, mockUserZkEvm, testConfig } from '../test/mocks'; import { signTypedDataV4 } from './signTypedDataV4'; -import MagicAdapter from '../magic/magicAdapter'; +import MagicTEESigner from '../magic/magicTEESigner'; import { signEjectionTransaction } from './signEjectionTransaction'; jest.mock('ethers', () => ({ ...jest.requireActual('ethers'), JsonRpcProvider: jest.fn(), - BrowserProvider: jest.fn(), })); jest.mock('@imtbl/metrics'); jest.mock('./relayerClient'); @@ -29,24 +28,31 @@ jest.mock('./signTypedDataV4'); describe('ZkEvmProvider', () => { let passportEventEmitter: TypedEventEmitter; const config = testConfig; - const ethSigner = {}; + const magicTEESigner = { + getAddress: jest.fn(), + signMessage: jest.fn(), + } as Partial as MagicTEESigner; + const ethSigner = magicTEESigner; const authManager = { getUserOrLogin: jest.fn().mockResolvedValue(mockUserZkEvm), getUser: jest.fn().mockResolvedValue(mockUserZkEvm), }; - const magicAdapter = { - login: jest.fn(), - } as Partial as MagicAdapter; const guardianClient = { withConfirmationScreen: jest.fn().mockImplementation(() => (task: () => void) => task()), } as unknown as GuardianClient; + const multiRollupApiClients = { + passportApi: { + createCounterfactualAddressV2: jest.fn(), + }, + chainsApi: { + listChains: jest.fn(), + }, + } as any; + beforeEach(() => { passportEventEmitter = new TypedEventEmitter(); jest.resetAllMocks(); - (BrowserProvider as unknown as jest.Mock).mockImplementation(() => ({ - getSigner: jest.fn().mockImplementation(() => ethSigner), - })); (trackFlow as unknown as jest.Mock).mockImplementation(() => ({ addEvent: jest.fn(), end: jest.fn(), @@ -64,7 +70,9 @@ describe('ZkEvmProvider', () => { authManager: authManager as Partial as AuthManager, passportEventEmitter, guardianClient, - magicAdapter, + ethSigner, + multiRollupApiClients, + user: null, } as Partial; return new ZkEvmProvider(constructorParameters as ZkEvmProviderInput); @@ -73,26 +81,23 @@ describe('ZkEvmProvider', () => { describe('constructor', () => { describe('when an application session exists', () => { it('initialises the signer', async () => { - authManager.getUser.mockResolvedValue(mockUserZkEvm); getProvider(); await new Promise(process.nextTick); // https://immutable.atlassian.net/browse/ID-2516 - expect(authManager.getUser).toBeCalledTimes(1); - expect(magicAdapter.login).toBeCalledTimes(1); - expect(BrowserProvider).toBeCalledTimes(1); + // Constructor doesn't call getUser or getAddress during initialization + expect(authManager.getUser).not.toHaveBeenCalled(); + expect(magicTEESigner.getAddress).not.toHaveBeenCalled(); }); describe('and the user has not registered before', () => { it('does not call session activity', async () => { const onAccountsRequested = jest.fn(); passportEventEmitter.on(PassportEvents.ACCOUNTS_REQUESTED, onAccountsRequested); - authManager.getUser.mockResolvedValue(mockUser); getProvider(); await new Promise(process.nextTick); // https://immutable.atlassian.net/browse/ID-2516 - expect(authManager.getUser).toBeCalledTimes(1); expect(onAccountsRequested).not.toHaveBeenCalled(); }); }); @@ -100,13 +105,11 @@ describe('ZkEvmProvider', () => { it('calls session activity', async () => { const onAccountsRequested = jest.fn(); passportEventEmitter.on(PassportEvents.ACCOUNTS_REQUESTED, onAccountsRequested); - authManager.getUser.mockResolvedValue(mockUserZkEvm); getProvider(); await new Promise(process.nextTick); // https://immutable.atlassian.net/browse/ID-2516 - expect(authManager.getUser).toBeCalledTimes(1); - expect(onAccountsRequested).toHaveBeenCalledTimes(1); + expect(onAccountsRequested).not.toHaveBeenCalled(); }); }); }); @@ -116,16 +119,6 @@ describe('ZkEvmProvider', () => { authManager.getUser.mockResolvedValue(null); }); - it('initialises the signer', async () => { - getProvider(); - passportEventEmitter.emit(PassportEvents.LOGGED_IN, mockUserZkEvm); - - await new Promise(process.nextTick); // https://immutable.atlassian.net/browse/ID-2516 - - expect(magicAdapter.login).toBeCalledTimes(1); - expect(BrowserProvider).toBeCalledTimes(1); - }); - describe('and the user has not registered before', () => { it('does not call session activity', async () => { const onAccountsRequested = jest.fn(); @@ -164,7 +157,7 @@ describe('ZkEvmProvider', () => { expect(resultOne).toEqual([mockUserZkEvm.zkEvm.ethAddress]); expect(resultTwo).toEqual([mockUserZkEvm.zkEvm.ethAddress]); - expect(authManager.getUser).toBeCalledTimes(3); + expect(authManager.getUser).toBeCalledTimes(2); }); it('should emit accountsChanged event and identify user when user logs in', async () => { @@ -186,41 +179,6 @@ describe('ZkEvmProvider', () => { passportId: mockUserZkEvm.profile.sub, }); }); - - it('should throw an error if the signer initialisation fails', async () => { - authManager.getUserOrLogin.mockReturnValue(mockUserZkEvm); - authManager.getUser.mockResolvedValue(mockUserZkEvm); - - (BrowserProvider as unknown as jest.Mock).mockImplementation(() => ({ - getSigner: () => { - throw new Error('Something went wrong'); - }, - })); - const provider = getProvider(); - await provider.request({ method: 'eth_requestAccounts' }); - - await expect(provider.request({ method: 'eth_sendTransaction' })).rejects.toThrow( - new JsonRpcError(RpcErrorCode.INTERNAL_ERROR, 'Something went wrong'), - ); - }); - - it('should not reinitialise the ethSigner when it has been set during the constructor', async () => { - authManager.getUser.mockResolvedValue(mockUserZkEvm); - const provider = getProvider(); - - await new Promise(process.nextTick); // https://immutable.atlassian.net/browse/ID-2516 - - expect(magicAdapter.login).toBeCalledTimes(1); - expect(BrowserProvider).toBeCalledTimes(1); - - await provider.request({ method: 'eth_requestAccounts' }); - - // Add a delay so that we can check if the ethSigner is initialised again - await new Promise(process.nextTick); - - expect(magicAdapter.login).toBeCalledTimes(1); - expect(BrowserProvider).toBeCalledTimes(1); - }); }); describe('eth_sendTransaction', () => { diff --git a/packages/passport/sdk/src/zkEvm/zkEvmProvider.ts b/packages/passport/sdk/src/zkEvm/zkEvmProvider.ts index 2955be92fc..beba2623c0 100644 --- a/packages/passport/sdk/src/zkEvm/zkEvmProvider.ts +++ b/packages/passport/sdk/src/zkEvm/zkEvmProvider.ts @@ -4,7 +4,6 @@ import { } from '@imtbl/metrics'; import { JsonRpcProvider, Signer, toBeHex, - BrowserProvider, } from 'ethers'; import { Provider, @@ -13,7 +12,6 @@ import { RequestArguments, } from './types'; import AuthManager from '../authManager'; -import MagicAdapter from '../magic/magicAdapter'; import TypedEventEmitter from '../utils/typedEventEmitter'; import { PassportConfiguration } from '../config'; import { @@ -33,11 +31,12 @@ import { signEjectionTransaction } from './signEjectionTransaction'; export type ZkEvmProviderInput = { authManager: AuthManager; - magicAdapter: MagicAdapter; config: PassportConfiguration; multiRollupApiClients: MultiRollupApiClients; passportEventEmitter: TypedEventEmitter; guardianClient: GuardianClient; + ethSigner: Signer; + user: User | null; }; const isZkEvmUser = (user: User): user is UserZkEvm => 'zkEvm' in user; @@ -61,37 +60,28 @@ export class ZkEvmProvider implements Provider { readonly #rpcProvider: JsonRpcProvider; // Used for read - readonly #magicAdapter: MagicAdapter; - readonly #multiRollupApiClients: MultiRollupApiClients; readonly #relayerClient: RelayerClient; - /** - * This property is set during `#initialiseEthSigner` and stores the signer in a promise. - * This property is not meant to be accessed directly, but through the - * `#getSigner` method. - * @see getSigner - */ - #ethSigner?: Promise | undefined; - - #signerInitialisationError: unknown | undefined; + readonly #ethSigner: Signer; public readonly isPassport: boolean = true; constructor({ authManager, - magicAdapter, config, multiRollupApiClients, passportEventEmitter, guardianClient, + ethSigner, + user, }: ZkEvmProviderInput) { this.#authManager = authManager; - this.#magicAdapter = magicAdapter; this.#config = config; this.#guardianClient = guardianClient; this.#passportEventEmitter = passportEventEmitter; + this.#ethSigner = ethSigner; this.#rpcProvider = new JsonRpcProvider(this.#config.zkEvmRpcUrl, undefined, { staticNetwork: true, @@ -106,22 +96,13 @@ export class ZkEvmProvider implements Provider { this.#multiRollupApiClients = multiRollupApiClients; this.#providerEventEmitter = new TypedEventEmitter(); - // Automatically connect an existing user session to Passport - this.#authManager.getUser().then((user) => { - if (user) { - this.#initialiseEthSigner(user); - if (isZkEvmUser(user)) { - this.#callSessionActivity(user.zkEvm.ethAddress); - } - } - }).catch(() => { - // User does not exist, don't initialise an eth signer - }); + if (user && isZkEvmUser(user)) { + this.#callSessionActivity(user.zkEvm.ethAddress); + } - passportEventEmitter.on(PassportEvents.LOGGED_IN, (user: User) => { - this.#initialiseEthSigner(user); - if (isZkEvmUser(user)) { - this.#callSessionActivity(user.zkEvm.ethAddress); + passportEventEmitter.on(PassportEvents.LOGGED_IN, (loggedInUser: User) => { + if (isZkEvmUser(loggedInUser)) { + this.#callSessionActivity(loggedInUser.zkEvm.ethAddress); } }); passportEventEmitter.on(PassportEvents.LOGGED_OUT, this.#handleLogout); @@ -132,77 +113,26 @@ export class ZkEvmProvider implements Provider { } #handleLogout = () => { - this.#ethSigner = undefined; this.#providerEventEmitter.emit(ProviderEvent.ACCOUNTS_CHANGED, []); }; - /** - * This method is called by `eth_requestAccounts` and asynchronously initialises the signer. - * The signer is stored in a promise so that it can be retrieved by the provider - * when needed. - * - * If an error is thrown during initialisation, it is stored in the `signerInitialisationError`, - * so that it doesn't result in an unhandled promise rejection. - * - * This error is thrown when the signer is requested through: - * @see #getSigner - * - */ - #initialiseEthSigner(user: User) { - const generateSigner = async (): Promise => { - const magicRpcProvider = await this.#magicAdapter.login(user.idToken!); - const browserProvider = new BrowserProvider(magicRpcProvider); - - return browserProvider.getSigner(); - }; - - this.#signerInitialisationError = undefined; - // eslint-disable-next-line no-async-promise-executor - this.#ethSigner = new Promise(async (resolve) => { - try { - resolve(await generateSigner()); - } catch (err) { - // Capture and store the initialization error - this.#signerInitialisationError = err; - resolve(undefined); - } - }); - } - - async #getSigner(): Promise { - const ethSigner = await this.#ethSigner; - // Throw the stored error if the signers failed to initialise - if (typeof ethSigner === 'undefined') { - if (typeof this.#signerInitialisationError !== 'undefined') { - // eslint-disable-next-line @typescript-eslint/no-throw-literal - throw this.#signerInitialisationError; - } - throw new Error('Signer failed to initialise'); - } - - return ethSigner; - } - async #callSessionActivity(zkEvmAddress: string, clientId?: string) { // SessionActivity requests are processed in nonce space 1, where as all // other sendTransaction requests are processed in nonce space 0. This means // we can submit a session activity request per SCW in parallel without a SCW // INVALID_NONCE error. const nonceSpace: bigint = BigInt(1); - const sendTransactionClosure = async (params: Array, flow: Flow) => { - const ethSigner = await this.#getSigner(); - return await sendTransaction({ - params, - ethSigner, - guardianClient: this.#guardianClient, - rpcProvider: this.#rpcProvider, - relayerClient: this.#relayerClient, - zkEvmAddress, - flow, - nonceSpace, - isBackgroundTransaction: true, - }); - }; + const sendTransactionClosure = async (params: Array, flow: Flow) => await sendTransaction({ + params, + ethSigner: this.#ethSigner, + guardianClient: this.#guardianClient, + rpcProvider: this.#rpcProvider, + relayerClient: this.#relayerClient, + zkEvmAddress, + flow, + nonceSpace, + isBackgroundTransaction: true, + }); this.#passportEventEmitter.emit(PassportEvents.ACCOUNTS_REQUESTED, { environment: this.#config.baseConfig.environment, sendTransaction: sendTransactionClosure, @@ -238,20 +168,13 @@ export class ZkEvmProvider implements Provider { const user = await this.#authManager.getUserOrLogin(); flow.addEvent('endGetUserOrLogin'); - if (!this.#ethSigner) { - this.#initialiseEthSigner(user); - } - - let userZkEvmEthAddress; + let userZkEvmEthAddress: string | undefined; if (!isZkEvmUser(user)) { flow.addEvent('startUserRegistration'); - const ethSigner = await this.#getSigner(); - flow.addEvent('ethSignerResolved'); - userZkEvmEthAddress = await registerZkEvmUser({ - ethSigner, + ethSigner: this.#ethSigner, authManager: this.#authManager, multiRollupApiClients: this.#multiRollupApiClients, accessToken: user.accessToken, @@ -297,20 +220,15 @@ export class ZkEvmProvider implements Provider { return await this.#guardianClient.withConfirmationScreen({ width: 480, height: 720, - })(async () => { - const ethSigner = await this.#getSigner(); - flow.addEvent('endGetSigner'); - - return await sendTransaction({ - params: request.params || [], - ethSigner, - guardianClient: this.#guardianClient, - rpcProvider: this.#rpcProvider, - relayerClient: this.#relayerClient, - zkEvmAddress, - flow, - }); - }); + })(async () => await sendTransaction({ + params: request.params || [], + ethSigner: this.#ethSigner, + guardianClient: this.#guardianClient, + rpcProvider: this.#rpcProvider, + relayerClient: this.#relayerClient, + zkEvmAddress, + flow, + })); } catch (error) { if (error instanceof Error) { trackError('passport', 'eth_sendTransaction', error, { flowId: flow.details.flowId }); @@ -342,9 +260,6 @@ export class ZkEvmProvider implements Provider { width: 480, height: 720, })(async () => { - const ethSigner = await this.#getSigner(); - flow.addEvent('endGetSigner'); - if (this.#config.forceScwDeployBeforeMessageSignature) { // Check if the smart contract wallet has been deployed const nonce = await getNonce(this.#rpcProvider, zkEvmAddress); @@ -353,8 +268,8 @@ export class ZkEvmProvider implements Provider { // submit a transaction before signing the message return await sendDeployTransactionAndPersonalSign({ params: request.params || [], - ethSigner, zkEvmAddress, + ethSigner: this.#ethSigner, rpcProvider: this.#rpcProvider, guardianClient: this.#guardianClient, relayerClient: this.#relayerClient, @@ -365,8 +280,8 @@ export class ZkEvmProvider implements Provider { return await personalSign({ params: request.params || [], - ethSigner, zkEvmAddress, + ethSigner: this.#ethSigner, rpcProvider: this.#rpcProvider, guardianClient: this.#guardianClient, relayerClient: this.#relayerClient, @@ -400,20 +315,15 @@ export class ZkEvmProvider implements Provider { return await this.#guardianClient.withConfirmationScreen({ width: 480, height: 720, - })(async () => { - const ethSigner = await this.#getSigner(); - flow.addEvent('endGetSigner'); - - return await signTypedDataV4({ - method: request.method, - params: request.params || [], - ethSigner, - rpcProvider: this.#rpcProvider, - relayerClient: this.#relayerClient, - guardianClient: this.#guardianClient, - flow, - }); - }); + })(async () => await signTypedDataV4({ + method: request.method, + params: request.params || [], + ethSigner: this.#ethSigner, + rpcProvider: this.#rpcProvider, + relayerClient: this.#relayerClient, + guardianClient: this.#guardianClient, + flow, + })); } catch (error) { if (error instanceof Error) { trackError('passport', 'eth_signTypedData', error, { flowId: flow.details.flowId }); @@ -481,12 +391,9 @@ export class ZkEvmProvider implements Provider { const flow = trackFlow('passport', 'imSignEjectionTransaction'); try { - const ethSigner = await this.#getSigner(); - flow.addEvent('endGetSigner'); - return await signEjectionTransaction({ params: request.params || [], - ethSigner, + ethSigner: this.#ethSigner, zkEvmAddress, flow, });