Skip to content

Commit

Permalink
ID-1122 & ID-742 Add isRegisteredOffchain & registerOffchain methods,…
Browse files Browse the repository at this point in the history
… lazy wallet initialisation (#1015)

Co-authored-by: Hayden Fowler <[email protected]>
Co-authored-by: Jon <[email protected]>
Co-authored-by: Jon <[email protected]>
Co-authored-by: Hayden Fowler <[email protected]>
  • Loading branch information
5 people authored Nov 13, 2023
1 parent 75d0d51 commit b6b2848
Show file tree
Hide file tree
Showing 15 changed files with 688 additions and 336 deletions.
9 changes: 3 additions & 6 deletions packages/passport/sdk-sample-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,11 @@
## Running Locally

```bash
# install deps & build the sdk at project root
# Install deps
yarn
yarn build

# install deps & run the sample app
# cd packages/passport/sdk-sample-app
yarn
yarn dev
# Build the passport SDK and run the sample app
yarn workspace @imtbl/passport build && yarn workspace @imtbl/passport-sdk-sample-app dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
70 changes: 55 additions & 15 deletions packages/passport/sdk-sample-app/src/components/imx/ImxWorkflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,57 @@ function ImxWorkflow() {
const [showTransfer, setShowTransfer] = useState<boolean>(false);
const [showOrder, setShowOrder] = useState<boolean>(false);

const { addMessage, isLoading } = useStatusProvider();
const { addMessage, isLoading, setIsLoading } = useStatusProvider();
const { connectImx, imxProvider } = usePassportProvider();

const getAddress = useCallback(async () => {
const address = await imxProvider?.getAddress();
addMessage('Get Address', address);
}, [addMessage, imxProvider]);
const ensureUserIsRegistered = useCallback(async (callback: Function) => {
setIsLoading(true);
try {
if (await imxProvider?.isRegisteredOffchain()) {
await callback();
} else {
addMessage('Please call `registerOffchain` before calling this method');
}
} finally {
setIsLoading(false);
}
}, [addMessage, imxProvider, setIsLoading]);

const handleBulkTransfer = () => {
setShowBulkTransfer(true);
};
const getAddress = useCallback(async () => (
ensureUserIsRegistered(async () => {
const address = await imxProvider?.getAddress();
addMessage('Get Address', address);
})
), [addMessage, ensureUserIsRegistered, imxProvider]);

const handleTransfer = () => {
setShowTransfer(true);
const isRegisteredOffchain = async () => {
try {
setIsLoading(true);
const result = await imxProvider?.isRegisteredOffchain();
addMessage('Is Registered Offchain', result);
} catch (err) {
addMessage('Is Registered Offchain', err);
} finally {
setIsLoading(false);
}
};

const handleTrade = () => {
setShowTrade(true);
const registerUser = async () => {
try {
setIsLoading(true);
const result = await imxProvider?.registerOffchain();
addMessage('Register off chain', result);
} catch (err) {
addMessage('Register off chain', err);
} finally {
setIsLoading(false);
}
};

const handleOrder = useCallback(() => {
setShowOrder(true);
}, []);
const handleBulkTransfer = () => ensureUserIsRegistered(() => setShowBulkTransfer(true));
const handleTransfer = () => ensureUserIsRegistered(() => setShowTransfer(true));
const handleTrade = () => ensureUserIsRegistered(() => setShowTrade(true));
const handleOrder = () => ensureUserIsRegistered(() => setShowOrder(true));

return (
<CardStack title="Imx Workflow">
Expand Down Expand Up @@ -113,6 +141,18 @@ function ImxWorkflow() {
>
Get Address
</WorkflowButton>
<WorkflowButton
disabled={isLoading}
onClick={isRegisteredOffchain}
>
Is Registered Offchain
</WorkflowButton>
<WorkflowButton
disabled={isLoading}
onClick={registerUser}
>
Register User
</WorkflowButton>
</>
)}
</Stack>
Expand Down
12 changes: 7 additions & 5 deletions packages/passport/sdk-sample-app/src/context/StatusProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,19 @@ export function StatusProvider({

const addMessage = useCallback((operation: string, ...args: any[]) => {
let messageString: string;
if (args[0] instanceof PassportError) {
messageString = `${args[0].type}: ${args[0].message}`;
if (!args?.length) {
messageString = operation;
} else if (args[0] instanceof PassportError) {
messageString = `${operation}: ${args[0].type} - ${args[0].message}`;
} else {
messageString = args.map((arg) => {
messageString = `${operation}: ${args.map((arg) => {
if (arg instanceof Error) {
return arg.toString();
}
return JSON.stringify(arg, null, 2);
}).join(': ');
}).join(' - ')}`;
}
setMessages((prevMessages) => [...prevMessages, `${operation}: ${messageString}`]);
setMessages((prevMessages) => [...prevMessages, messageString]);
}, []);

const providerValues = useMemo(() => ({
Expand Down
18 changes: 9 additions & 9 deletions packages/passport/sdk/src/authManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { PassportErrorType, withPassportError } from './errors/passportError';
import {
PassportMetadata,
User,
DeviceCodeReponse,
DeviceCodeResponse,
DeviceConnectResponse,
DeviceTokenResponse,
DeviceErrorResponse,
Expand Down Expand Up @@ -106,9 +106,9 @@ export default class AuthManager {
};
if (passport?.imx_eth_address) {
user.imx = {
ethAddress: passport?.imx_eth_address,
starkAddress: passport?.imx_stark_address,
userAdminAddress: passport?.imx_user_admin_address,
ethAddress: passport.imx_eth_address,
starkAddress: passport.imx_stark_address,
userAdminAddress: passport.imx_user_admin_address,
};
}
if (passport?.zkevm_eth_address) {
Expand All @@ -132,11 +132,11 @@ export default class AuthManager {
nickname: idTokenPayload.nickname,
},
};
if (idTokenPayload?.passport?.imx_eth_address) {
if (idTokenPayload?.passport.imx_eth_address) {
user.imx = {
ethAddress: idTokenPayload?.passport?.imx_eth_address,
starkAddress: idTokenPayload?.passport?.imx_stark_address,
userAdminAddress: idTokenPayload?.passport?.imx_user_admin_address,
ethAddress: idTokenPayload.passport.imx_eth_address,
starkAddress: idTokenPayload.passport.imx_stark_address,
userAdminAddress: idTokenPayload.passport.imx_user_admin_address,
};
}
if (idTokenPayload?.passport?.zkevm_eth_address) {
Expand Down Expand Up @@ -168,7 +168,7 @@ export default class AuthManager {

public async loginWithDeviceFlow(): Promise<DeviceConnectResponse> {
return withPassportError<DeviceConnectResponse>(async () => {
const response = await axios.post<DeviceCodeReponse>(
const response = await axios.post<DeviceCodeResponse>(
`${this.config.authenticationDomain}/oauth/device/code`,
{
client_id: this.config.oidcConfiguration.clientId,
Expand Down
133 changes: 111 additions & 22 deletions packages/passport/sdk/src/starkEx/passportImxProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,31 @@ import {
UnsignedOrderRequest,
UnsignedTransferRequest,
} from '@imtbl/core-sdk';
import { mockUserImx, testConfig } from '../test/mocks';
import { Web3Provider } from '@ethersproject/providers';
import registerPassportStarkEx from './workflows/registration';
import { mockUserImx, testConfig, mockUser } from '../test/mocks';
import { PassportError, PassportErrorType } from '../errors/passportError';
import { PassportImxProvider } from './passportImxProvider';
import {
batchNftTransfer, cancelOrder, createOrder, createTrade, exchangeTransfer, transfer,
batchNftTransfer,
cancelOrder,
createOrder,
createTrade,
exchangeTransfer,
transfer,
} from './workflows';
import { ConfirmationScreen } from '../confirmation';
import { PassportConfiguration } from '../config';
import { PassportEventMap, PassportEvents } from '../types';
import TypedEventEmitter from '../utils/typedEventEmitter';
import AuthManager from '../authManager';
import MagicAdapter from '../magicAdapter';
import { getStarkSigner } from './getStarkSigner';

jest.mock('@ethersproject/providers');
jest.mock('./workflows');
jest.mock('./workflows/registration');
jest.mock('./getStarkSigner');

describe('PassportImxProvider', () => {
afterEach(jest.resetAllMocks);
Expand All @@ -52,22 +64,81 @@ describe('PassportImxProvider', () => {
getAddress: jest.fn(),
} as StarkSigner;

const mockEthSigner = {
signMessage: jest.fn(),
getAddress: jest.fn(),
};

const magicAdapterMock = {
login: jest.fn(),
};

const getSignerMock = jest.fn();

let passportEventEmitter: TypedEventEmitter<PassportEventMap>;

beforeEach(() => {
jest.restoreAllMocks();
getSignerMock.mockReturnValue(mockEthSigner);
(registerPassportStarkEx as jest.Mock).mockResolvedValue(null);
passportEventEmitter = new TypedEventEmitter<PassportEventMap>();
mockAuthManager.getUser.mockResolvedValue(mockUserImx);

// Signers
magicAdapterMock.login.mockResolvedValue({ getSigner: getSignerMock });
(Web3Provider as unknown as jest.Mock).mockReturnValue({ getSigner: getSignerMock });
(getStarkSigner as jest.Mock).mockResolvedValue(mockStarkSigner);

passportImxProvider = new PassportImxProvider({
authManager: mockAuthManager as unknown as AuthManager,
starkSigner: mockStarkSigner,
magicAdapter: magicAdapterMock as unknown as MagicAdapter,
confirmationScreen,
immutableXClient,
config: testConfig,
passportEventEmitter,
});
});

describe('async signer initialisation', () => {
it('initialises the eth and stark signers correctly', async () => {
// 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);
});

it('initialises the eth and stark signers only once', async () => {
await passportImxProvider.getAddress();
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 () => {
jest.resetAllMocks();
jest.restoreAllMocks();

mockAuthManager.getUser.mockResolvedValue(mockUserImx);
// Signers
magicAdapterMock.login.mockResolvedValue({});
(getStarkSigner as jest.Mock).mockRejectedValue(new Error('error'));

const pp = new PassportImxProvider({
authManager: mockAuthManager as unknown as AuthManager,
magicAdapter: magicAdapterMock as unknown as MagicAdapter,
confirmationScreen,
immutableXClient,
config: testConfig,
passportEventEmitter: new TypedEventEmitter<PassportEventMap>(),
});

await expect(pp.getAddress()).rejects.toThrow(new Error('error'));
});
});

describe('transfer', () => {
it('calls transfer workflow', async () => {
const returnValue = {} as CreateTransferResponseV1;
Expand All @@ -90,27 +161,25 @@ describe('PassportImxProvider', () => {
});
});

describe('registerOffchain', () => {
it('should throw error', async () => {
expect(passportImxProvider.registerOffchain)
.toThrow(
new PassportError(
'Operation not supported',
PassportErrorType.OPERATION_NOT_SUPPORTED_ERROR,
),
);
describe('isRegisteredOffchain', () => {
it('should return true when a user is registered', async () => {
const isRegistered = await passportImxProvider.isRegisteredOffchain();
expect(isRegistered).toEqual(true);
});
});

describe('isRegisteredOnchain', () => {
it('should throw error', async () => {
expect(passportImxProvider.isRegisteredOnchain)
.toThrow(
new PassportError(
'Operation not supported',
PassportErrorType.OPERATION_NOT_SUPPORTED_ERROR,
),
);
it('should return false when a user is not registered', async () => {
mockAuthManager.getUser.mockResolvedValue({});
const isRegistered = await passportImxProvider.isRegisteredOffchain();
expect(isRegistered).toEqual(false);
});

it('should bubble up the error if user is not logged in', async () => {
mockAuthManager.getUser.mockResolvedValue(undefined);

await expect(passportImxProvider.isRegisteredOffchain()).rejects.toThrow(new PassportError(
'User has been logged out',
PassportErrorType.NOT_LOGGED_IN_ERROR,
));
});
});

Expand Down Expand Up @@ -266,6 +335,26 @@ 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.loginSilent.mockResolvedValue({ ...mockUser, imx: { ethAddress: '', starkAddress: '', userAdminAddress: '' } });

await passportImxProvider.registerOffchain();

expect(registerPassportStarkEx).toHaveBeenCalledWith({
ethSigner: mockEthSigner,
starkSigner: mockStarkSigner,
usersApi: immutableXClient.usersApi,
}, mockUserImx.accessToken);
expect(mockAuthManager.loginSilent).toHaveBeenCalledTimes(1);
expect(mockAuthManager.loginSilent).toHaveBeenCalledWith({ forceRefresh: true });
});
});

describe.each([
['transfer' as const, {} as UnsignedTransferRequest],
['createOrder' as const, {} as UnsignedOrderRequest],
Expand Down
Loading

0 comments on commit b6b2848

Please sign in to comment.