Skip to content

Commit b6b2848

Browse files
scastleman-immutablehaydenfowlerJon-AlonsoJon
authored
ID-1122 & ID-742 Add isRegisteredOffchain & registerOffchain methods, 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]>
1 parent 75d0d51 commit b6b2848

File tree

15 files changed

+688
-336
lines changed

15 files changed

+688
-336
lines changed

packages/passport/sdk-sample-app/README.md

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,11 @@
33
## Running Locally
44

55
```bash
6-
# install deps & build the sdk at project root
6+
# Install deps
77
yarn
8-
yarn build
98

10-
# install deps & run the sample app
11-
# cd packages/passport/sdk-sample-app
12-
yarn
13-
yarn dev
9+
# Build the passport SDK and run the sample app
10+
yarn workspace @imtbl/passport build && yarn workspace @imtbl/passport-sdk-sample-app dev
1411
```
1512

1613
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

packages/passport/sdk-sample-app/src/components/imx/ImxWorkflow.tsx

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,29 +18,57 @@ function ImxWorkflow() {
1818
const [showTransfer, setShowTransfer] = useState<boolean>(false);
1919
const [showOrder, setShowOrder] = useState<boolean>(false);
2020

21-
const { addMessage, isLoading } = useStatusProvider();
21+
const { addMessage, isLoading, setIsLoading } = useStatusProvider();
2222
const { connectImx, imxProvider } = usePassportProvider();
2323

24-
const getAddress = useCallback(async () => {
25-
const address = await imxProvider?.getAddress();
26-
addMessage('Get Address', address);
27-
}, [addMessage, imxProvider]);
24+
const ensureUserIsRegistered = useCallback(async (callback: Function) => {
25+
setIsLoading(true);
26+
try {
27+
if (await imxProvider?.isRegisteredOffchain()) {
28+
await callback();
29+
} else {
30+
addMessage('Please call `registerOffchain` before calling this method');
31+
}
32+
} finally {
33+
setIsLoading(false);
34+
}
35+
}, [addMessage, imxProvider, setIsLoading]);
2836

29-
const handleBulkTransfer = () => {
30-
setShowBulkTransfer(true);
31-
};
37+
const getAddress = useCallback(async () => (
38+
ensureUserIsRegistered(async () => {
39+
const address = await imxProvider?.getAddress();
40+
addMessage('Get Address', address);
41+
})
42+
), [addMessage, ensureUserIsRegistered, imxProvider]);
3243

33-
const handleTransfer = () => {
34-
setShowTransfer(true);
44+
const isRegisteredOffchain = async () => {
45+
try {
46+
setIsLoading(true);
47+
const result = await imxProvider?.isRegisteredOffchain();
48+
addMessage('Is Registered Offchain', result);
49+
} catch (err) {
50+
addMessage('Is Registered Offchain', err);
51+
} finally {
52+
setIsLoading(false);
53+
}
3554
};
3655

37-
const handleTrade = () => {
38-
setShowTrade(true);
56+
const registerUser = async () => {
57+
try {
58+
setIsLoading(true);
59+
const result = await imxProvider?.registerOffchain();
60+
addMessage('Register off chain', result);
61+
} catch (err) {
62+
addMessage('Register off chain', err);
63+
} finally {
64+
setIsLoading(false);
65+
}
3966
};
4067

41-
const handleOrder = useCallback(() => {
42-
setShowOrder(true);
43-
}, []);
68+
const handleBulkTransfer = () => ensureUserIsRegistered(() => setShowBulkTransfer(true));
69+
const handleTransfer = () => ensureUserIsRegistered(() => setShowTransfer(true));
70+
const handleTrade = () => ensureUserIsRegistered(() => setShowTrade(true));
71+
const handleOrder = () => ensureUserIsRegistered(() => setShowOrder(true));
4472

4573
return (
4674
<CardStack title="Imx Workflow">
@@ -113,6 +141,18 @@ function ImxWorkflow() {
113141
>
114142
Get Address
115143
</WorkflowButton>
144+
<WorkflowButton
145+
disabled={isLoading}
146+
onClick={isRegisteredOffchain}
147+
>
148+
Is Registered Offchain
149+
</WorkflowButton>
150+
<WorkflowButton
151+
disabled={isLoading}
152+
onClick={registerUser}
153+
>
154+
Register User
155+
</WorkflowButton>
116156
</>
117157
)}
118158
</Stack>

packages/passport/sdk-sample-app/src/context/StatusProvider.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,19 @@ export function StatusProvider({
2323

2424
const addMessage = useCallback((operation: string, ...args: any[]) => {
2525
let messageString: string;
26-
if (args[0] instanceof PassportError) {
27-
messageString = `${args[0].type}: ${args[0].message}`;
26+
if (!args?.length) {
27+
messageString = operation;
28+
} else if (args[0] instanceof PassportError) {
29+
messageString = `${operation}: ${args[0].type} - ${args[0].message}`;
2830
} else {
29-
messageString = args.map((arg) => {
31+
messageString = `${operation}: ${args.map((arg) => {
3032
if (arg instanceof Error) {
3133
return arg.toString();
3234
}
3335
return JSON.stringify(arg, null, 2);
34-
}).join(': ');
36+
}).join(' - ')}`;
3537
}
36-
setMessages((prevMessages) => [...prevMessages, `${operation}: ${messageString}`]);
38+
setMessages((prevMessages) => [...prevMessages, messageString]);
3739
}, []);
3840

3941
const providerValues = useMemo(() => ({

packages/passport/sdk/src/authManager.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { PassportErrorType, withPassportError } from './errors/passportError';
1414
import {
1515
PassportMetadata,
1616
User,
17-
DeviceCodeReponse,
17+
DeviceCodeResponse,
1818
DeviceConnectResponse,
1919
DeviceTokenResponse,
2020
DeviceErrorResponse,
@@ -106,9 +106,9 @@ export default class AuthManager {
106106
};
107107
if (passport?.imx_eth_address) {
108108
user.imx = {
109-
ethAddress: passport?.imx_eth_address,
110-
starkAddress: passport?.imx_stark_address,
111-
userAdminAddress: passport?.imx_user_admin_address,
109+
ethAddress: passport.imx_eth_address,
110+
starkAddress: passport.imx_stark_address,
111+
userAdminAddress: passport.imx_user_admin_address,
112112
};
113113
}
114114
if (passport?.zkevm_eth_address) {
@@ -132,11 +132,11 @@ export default class AuthManager {
132132
nickname: idTokenPayload.nickname,
133133
},
134134
};
135-
if (idTokenPayload?.passport?.imx_eth_address) {
135+
if (idTokenPayload?.passport.imx_eth_address) {
136136
user.imx = {
137-
ethAddress: idTokenPayload?.passport?.imx_eth_address,
138-
starkAddress: idTokenPayload?.passport?.imx_stark_address,
139-
userAdminAddress: idTokenPayload?.passport?.imx_user_admin_address,
137+
ethAddress: idTokenPayload.passport.imx_eth_address,
138+
starkAddress: idTokenPayload.passport.imx_stark_address,
139+
userAdminAddress: idTokenPayload.passport.imx_user_admin_address,
140140
};
141141
}
142142
if (idTokenPayload?.passport?.zkevm_eth_address) {
@@ -168,7 +168,7 @@ export default class AuthManager {
168168

169169
public async loginWithDeviceFlow(): Promise<DeviceConnectResponse> {
170170
return withPassportError<DeviceConnectResponse>(async () => {
171-
const response = await axios.post<DeviceCodeReponse>(
171+
const response = await axios.post<DeviceCodeResponse>(
172172
`${this.config.authenticationDomain}/oauth/device/code`,
173173
{
174174
client_id: this.config.oidcConfiguration.clientId,

packages/passport/sdk/src/starkEx/passportImxProvider.test.ts

Lines changed: 111 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,31 @@ import {
1414
UnsignedOrderRequest,
1515
UnsignedTransferRequest,
1616
} from '@imtbl/core-sdk';
17-
import { mockUserImx, testConfig } from '../test/mocks';
17+
import { Web3Provider } from '@ethersproject/providers';
18+
import registerPassportStarkEx from './workflows/registration';
19+
import { mockUserImx, testConfig, mockUser } from '../test/mocks';
1820
import { PassportError, PassportErrorType } from '../errors/passportError';
1921
import { PassportImxProvider } from './passportImxProvider';
2022
import {
21-
batchNftTransfer, cancelOrder, createOrder, createTrade, exchangeTransfer, transfer,
23+
batchNftTransfer,
24+
cancelOrder,
25+
createOrder,
26+
createTrade,
27+
exchangeTransfer,
28+
transfer,
2229
} from './workflows';
2330
import { ConfirmationScreen } from '../confirmation';
2431
import { PassportConfiguration } from '../config';
2532
import { PassportEventMap, PassportEvents } from '../types';
2633
import TypedEventEmitter from '../utils/typedEventEmitter';
2734
import AuthManager from '../authManager';
35+
import MagicAdapter from '../magicAdapter';
36+
import { getStarkSigner } from './getStarkSigner';
2837

38+
jest.mock('@ethersproject/providers');
2939
jest.mock('./workflows');
40+
jest.mock('./workflows/registration');
41+
jest.mock('./getStarkSigner');
3042

3143
describe('PassportImxProvider', () => {
3244
afterEach(jest.resetAllMocks);
@@ -52,22 +64,81 @@ describe('PassportImxProvider', () => {
5264
getAddress: jest.fn(),
5365
} as StarkSigner;
5466

67+
const mockEthSigner = {
68+
signMessage: jest.fn(),
69+
getAddress: jest.fn(),
70+
};
71+
72+
const magicAdapterMock = {
73+
login: jest.fn(),
74+
};
75+
76+
const getSignerMock = jest.fn();
77+
5578
let passportEventEmitter: TypedEventEmitter<PassportEventMap>;
5679

5780
beforeEach(() => {
81+
jest.restoreAllMocks();
82+
getSignerMock.mockReturnValue(mockEthSigner);
83+
(registerPassportStarkEx as jest.Mock).mockResolvedValue(null);
5884
passportEventEmitter = new TypedEventEmitter<PassportEventMap>();
5985
mockAuthManager.getUser.mockResolvedValue(mockUserImx);
6086

87+
// Signers
88+
magicAdapterMock.login.mockResolvedValue({ getSigner: getSignerMock });
89+
(Web3Provider as unknown as jest.Mock).mockReturnValue({ getSigner: getSignerMock });
90+
(getStarkSigner as jest.Mock).mockResolvedValue(mockStarkSigner);
91+
6192
passportImxProvider = new PassportImxProvider({
6293
authManager: mockAuthManager as unknown as AuthManager,
63-
starkSigner: mockStarkSigner,
94+
magicAdapter: magicAdapterMock as unknown as MagicAdapter,
6495
confirmationScreen,
6596
immutableXClient,
6697
config: testConfig,
6798
passportEventEmitter,
6899
});
69100
});
70101

102+
describe('async signer initialisation', () => {
103+
it('initialises the eth and stark signers correctly', async () => {
104+
// The promise is created in the constructor but not awaited until a method is called
105+
await passportImxProvider.getAddress();
106+
107+
expect(magicAdapterMock.login).toHaveBeenCalledWith(mockUserImx.idToken);
108+
expect(getStarkSigner).toHaveBeenCalledWith(mockEthSigner);
109+
});
110+
111+
it('initialises the eth and stark signers only once', async () => {
112+
await passportImxProvider.getAddress();
113+
await passportImxProvider.getAddress();
114+
await passportImxProvider.getAddress();
115+
116+
expect(magicAdapterMock.login).toHaveBeenCalledTimes(1);
117+
expect(getStarkSigner).toHaveBeenCalledTimes(1);
118+
});
119+
120+
it('re-throws the initialisation error when a method is called', async () => {
121+
jest.resetAllMocks();
122+
jest.restoreAllMocks();
123+
124+
mockAuthManager.getUser.mockResolvedValue(mockUserImx);
125+
// Signers
126+
magicAdapterMock.login.mockResolvedValue({});
127+
(getStarkSigner as jest.Mock).mockRejectedValue(new Error('error'));
128+
129+
const pp = new PassportImxProvider({
130+
authManager: mockAuthManager as unknown as AuthManager,
131+
magicAdapter: magicAdapterMock as unknown as MagicAdapter,
132+
confirmationScreen,
133+
immutableXClient,
134+
config: testConfig,
135+
passportEventEmitter: new TypedEventEmitter<PassportEventMap>(),
136+
});
137+
138+
await expect(pp.getAddress()).rejects.toThrow(new Error('error'));
139+
});
140+
});
141+
71142
describe('transfer', () => {
72143
it('calls transfer workflow', async () => {
73144
const returnValue = {} as CreateTransferResponseV1;
@@ -90,27 +161,25 @@ describe('PassportImxProvider', () => {
90161
});
91162
});
92163

93-
describe('registerOffchain', () => {
94-
it('should throw error', async () => {
95-
expect(passportImxProvider.registerOffchain)
96-
.toThrow(
97-
new PassportError(
98-
'Operation not supported',
99-
PassportErrorType.OPERATION_NOT_SUPPORTED_ERROR,
100-
),
101-
);
164+
describe('isRegisteredOffchain', () => {
165+
it('should return true when a user is registered', async () => {
166+
const isRegistered = await passportImxProvider.isRegisteredOffchain();
167+
expect(isRegistered).toEqual(true);
102168
});
103-
});
104169

105-
describe('isRegisteredOnchain', () => {
106-
it('should throw error', async () => {
107-
expect(passportImxProvider.isRegisteredOnchain)
108-
.toThrow(
109-
new PassportError(
110-
'Operation not supported',
111-
PassportErrorType.OPERATION_NOT_SUPPORTED_ERROR,
112-
),
113-
);
170+
it('should return false when a user is not registered', async () => {
171+
mockAuthManager.getUser.mockResolvedValue({});
172+
const isRegistered = await passportImxProvider.isRegisteredOffchain();
173+
expect(isRegistered).toEqual(false);
174+
});
175+
176+
it('should bubble up the error if user is not logged in', async () => {
177+
mockAuthManager.getUser.mockResolvedValue(undefined);
178+
179+
await expect(passportImxProvider.isRegisteredOffchain()).rejects.toThrow(new PassportError(
180+
'User has been logged out',
181+
PassportErrorType.NOT_LOGGED_IN_ERROR,
182+
));
114183
});
115184
});
116185

@@ -266,6 +335,26 @@ describe('PassportImxProvider', () => {
266335
});
267336
});
268337

338+
describe('registerOffChain', () => {
339+
it('should register the user and update the provider instance user', async () => {
340+
const magicProviderMock = {};
341+
342+
mockAuthManager.login.mockResolvedValue(mockUser);
343+
magicAdapterMock.login.mockResolvedValue(magicProviderMock);
344+
mockAuthManager.loginSilent.mockResolvedValue({ ...mockUser, imx: { ethAddress: '', starkAddress: '', userAdminAddress: '' } });
345+
346+
await passportImxProvider.registerOffchain();
347+
348+
expect(registerPassportStarkEx).toHaveBeenCalledWith({
349+
ethSigner: mockEthSigner,
350+
starkSigner: mockStarkSigner,
351+
usersApi: immutableXClient.usersApi,
352+
}, mockUserImx.accessToken);
353+
expect(mockAuthManager.loginSilent).toHaveBeenCalledTimes(1);
354+
expect(mockAuthManager.loginSilent).toHaveBeenCalledWith({ forceRefresh: true });
355+
});
356+
});
357+
269358
describe.each([
270359
['transfer' as const, {} as UnsignedTransferRequest],
271360
['createOrder' as const, {} as UnsignedOrderRequest],

0 commit comments

Comments
 (0)