diff --git a/packages/passport/sdk/src/starkEx/passportImxProvider.ts b/packages/passport/sdk/src/starkEx/passportImxProvider.ts index 0a56dbf537..7f66593748 100644 --- a/packages/passport/sdk/src/starkEx/passportImxProvider.ts +++ b/packages/passport/sdk/src/starkEx/passportImxProvider.ts @@ -119,6 +119,15 @@ export class PassportImxProvider implements IMXProvider { ); } + // TODO: Remove once implemented + // eslint-disable-next-line class-methods-use-this + isRegisteredOffchain(): Promise { + throw new PassportError( + 'Operation not supported', + PassportErrorType.OPERATION_NOT_SUPPORTED_ERROR, + ); + } + // TODO: Remove once implemented // eslint-disable-next-line class-methods-use-this isRegisteredOnchain(): Promise { diff --git a/packages/provider/src/genericImxProvider.ts b/packages/provider/src/genericImxProvider.ts index fa312160c6..4caa0914b3 100644 --- a/packages/provider/src/genericImxProvider.ts +++ b/packages/provider/src/genericImxProvider.ts @@ -24,6 +24,7 @@ import { Signers } from './signable-actions/types'; import { batchTransfer, transfer } from './signable-actions/transfer'; import { cancelOrder, createOrder } from './signable-actions/orders'; import { + isRegisteredOffchain, isRegisteredOnChain, registerOffchain, } from './signable-actions/registration'; @@ -50,7 +51,15 @@ export class GenericIMXProvider implements IMXProvider { } async getAddress(): Promise { - return await this.signers.ethSigner.getAddress(); + return this.signers.ethSigner.getAddress(); + } + + async isRegisteredOffchain(): Promise { + const ethAddress = await this.getAddress(); + return isRegisteredOffchain( + ethAddress, + this.config, + ); } registerOffchain(): Promise { diff --git a/packages/provider/src/imxProvider.ts b/packages/provider/src/imxProvider.ts index edf30229ee..b2122bfa99 100644 --- a/packages/provider/src/imxProvider.ts +++ b/packages/provider/src/imxProvider.ts @@ -30,6 +30,12 @@ export interface IMXProvider { * @return {Promise} Returns a promise that resolves with the user registration response */ registerOffchain(): Promise; + /** + * Checks if a User is registered off-chain + * + * @return {Promise} Returns a promise that resolves with true if the User is registered with IMX, false otherwise + */ + isRegisteredOffchain(): Promise; /** * Checks if a User is registered on-chain * diff --git a/packages/provider/src/signable-actions/registration.test.ts b/packages/provider/src/signable-actions/registration.test.ts index 0f0b3925a2..b59dd9f6f2 100644 --- a/packages/provider/src/signable-actions/registration.test.ts +++ b/packages/provider/src/signable-actions/registration.test.ts @@ -1,7 +1,8 @@ import { Contracts, UsersApi } from '@imtbl/core-sdk'; import { signRaw } from '@imtbl/toolkit'; +import { AxiosError } from 'axios'; import { generateSigners, privateKey1, testConfig } from '../test/helpers'; -import { isRegisteredOnChain, registerOffchain } from './registration'; +import { isRegisteredOffchain, isRegisteredOnChain, registerOffchain } from './registration'; jest.mock('@imtbl/core-sdk'); jest.mock('@imtbl/toolkit'); @@ -39,6 +40,67 @@ describe('Registration', () => { }); }); + describe('isRegisteredOffchain', () => { + const getUsersMock = jest.fn(); + const ethAddress = '0x123'; + + beforeEach(() => { + jest.restoreAllMocks(); + + (UsersApi as jest.Mock).mockReturnValue({ + getUsers: getUsersMock, + }); + }); + + describe('when the user has registered with IMX', () => { + test('should return true', async () => { + getUsersMock.mockResolvedValue({ + data: { + accounts: [ethAddress], + }, + }); + + const result = await isRegisteredOffchain(ethAddress, testConfig); + + expect(result).toEqual(true); + expect(getUsersMock).toHaveBeenCalledTimes(1); + }); + }); + + describe('when the user has not registered with IMX', () => { + test('should return false', async () => { + const axiosError = new AxiosError(); + axiosError.response = { + config: axiosError.config!, + data: undefined, + headers: {}, + request: undefined, + status: 404, + statusText: '', + }; + getUsersMock.mockImplementation(() => Promise.reject(axiosError)); + + const result = await isRegisteredOffchain(ethAddress, testConfig); + + expect(result).toEqual(false); + expect(getUsersMock).toHaveBeenCalledTimes(1); + }); + }); + + describe('when getUsers throws an error that is not a 404', () => { + test('should throw the error', async () => { + const axiosResponse = new Error('oops'); + getUsersMock.mockImplementation(() => Promise.reject(axiosResponse)); + + await expect( + isRegisteredOffchain(ethAddress, testConfig), + ).rejects.toThrowError(axiosResponse); + + expect(getUsersMock).toHaveBeenCalledTimes(1); + }); + }); + }); + describe('registerOffchain', () => { let getSignableRegistrationOffchainMock: jest.Mock; let registerUserMock: jest.Mock; diff --git a/packages/provider/src/signable-actions/registration.ts b/packages/provider/src/signable-actions/registration.ts index dd13645e89..ed21df92e1 100644 --- a/packages/provider/src/signable-actions/registration.ts +++ b/packages/provider/src/signable-actions/registration.ts @@ -6,6 +6,7 @@ import { UsersApi, } from '@imtbl/core-sdk'; import { signRaw } from '@imtbl/toolkit'; +import { AxiosError } from 'axios'; import { Signers } from './types'; import { validateChain } from './helpers'; import { ProviderConfiguration } from '../config'; @@ -45,6 +46,23 @@ export async function registerOffchain( return registeredUser.data; } +export async function isRegisteredOffchain(ethAddress: string, config: ProviderConfiguration): Promise { + try { + const usersApi = new UsersApi(config.immutableXConfig.apiConfiguration); + const getUsersResult = await usersApi.getUsers({ + user: ethAddress, + }); + const { accounts } = getUsersResult.data; + + return accounts?.length > 0; + } catch (ex) { + if (ex instanceof AxiosError && ex.response?.status === 404) { + return false; + } + throw ex; + } +} + interface IsRegisteredCheckError { reason: string; }