Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: full deeplink protocol #992

Merged
merged 10 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { CommunicationLayerMessage } from '../../../types/CommunicationLayerMess
import { EventType } from '../../../types/EventType';
import { logger } from '../../../utils/logger';

export function handleWalletInitMessage(
export async function handleWalletInitMessage(
instance: RemoteCommunication,
message: CommunicationLayerMessage,
) {
Expand All @@ -19,32 +19,36 @@ export function handleWalletInitMessage(
'chainId' in data &&
'walletKey' in data
) {
// Persist channel config
const { channelConfig } = instance.state;
logger.RemoteCommunication(
`WALLET_INIT: channelConfig`,
JSON.stringify(channelConfig, null, 2),
);
try {
// Persist channel config
const { channelConfig } = instance.state;
logger.RemoteCommunication(
`WALLET_INIT: channelConfig`,
JSON.stringify(channelConfig, null, 2),
);

if (channelConfig) {
const accounts = data.accounts as string[];
const chainId = data.chainId as string;
const walletKey = data.walletKey as string;
if (channelConfig) {
const accounts = data.accounts as string[];
const chainId = data.chainId as string;
const walletKey = data.walletKey as string;

instance.state.storageManager?.persistChannelConfig({
...channelConfig,
otherKey: walletKey,
relayPersistence: true,
});
await instance.state.storageManager?.persistChannelConfig({
...channelConfig,
otherKey: walletKey,
relayPersistence: true,
});

instance.state.storageManager?.persistAccounts(accounts);
instance.state.storageManager?.persistChainId(chainId);
}
await instance.state.storageManager?.persistAccounts(accounts);
await instance.state.storageManager?.persistChainId(chainId);
}

instance.emit(EventType.WALLET_INIT, {
accounts: data.accounts,
chainId: data.chainId,
});
instance.emit(EventType.WALLET_INIT, {
accounts: data.accounts,
chainId: data.chainId,
});
} catch (error) {
console.error('RemoteCommunication::on "wallet_init" -- error', error);
}
} else {
console.error(
'RemoteCommunication::on "wallet_init" -- invalid data format',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { SocketService } from '../../../SocketService';
import { EventType } from '../../../types/EventType';
import { MessageType } from '../../../types/MessageType';
import { logger } from '../../../utils/logger';
import { resume } from './resume';

Expand Down Expand Up @@ -36,7 +35,7 @@ describe('resume', () => {
start: mockStart,
},
},
remote: { state: {} },
remote: { state: {}, hasRelayPersistence: jest.fn() },
sendMessage: mockSendMessage,
} as unknown as SocketService;
});
Expand Down Expand Up @@ -76,25 +75,7 @@ describe('resume', () => {

resume(instance);

expect(mockSendMessage).toHaveBeenCalledWith({ type: MessageType.READY });
});

it('should not send READY message if an originator, but initiate key exchange', () => {
instance.state.isOriginator = true;

mockAreKeysExchanged.mockReturnValue(true);

resume(instance);

expect(mockSendMessage).not.toHaveBeenCalled();
});

it('should start key exchange if keys are not exchanged and not an originator', () => {
mockAreKeysExchanged.mockReturnValue(false);

resume(instance);

expect(mockStart).toHaveBeenCalledWith({ isOriginator: false });
expect(mockEmit).toHaveBeenCalled();
});

it('should update manualDisconnect and resumed state after resuming', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,24 @@ import { handleJoinChannelResults } from './handleJoinChannelResult';
*
* @param instance The current instance of the SocketService.
*/
export function resume(instance: SocketService) {
export function resume(instance: SocketService): void {
const { state, remote } = instance;
const { socket, channelId, context, keyExchange, isOriginator } = state;
const { isOriginator: remoteIsOriginator } = remote.state;

logger.SocketService(
`[SocketService: resume()] context=${context} connected=${
`[SocketService: resume()] channelId=${channelId} context=${context} connected=${
socket?.connected
} manualDisconnect=${state.manualDisconnect} resumed=${
state.resumed
} keysExchanged=${keyExchange?.areKeysExchanged()}`,
);

if (!channelId) {
logger.SocketService(`[SocketService: resume()] channelId is not defined`);
throw new Error('ChannelId is not defined');
}

if (socket?.connected) {
logger.SocketService(`[SocketService: resume()] already connected.`);
socket.emit(MessageType.PING, {
Expand All @@ -35,6 +40,15 @@ export function resume(instance: SocketService) {
context: 'on_channel_config',
message: '',
});

if (!remote.hasRelayPersistence() && !keyExchange?.areKeysExchanged()) {
// Always try to recover key exchange from both side (wallet / dapp)
if (isOriginator) {
instance.sendMessage({ type: MessageType.READY });
} else {
keyExchange?.start({ isOriginator: false });
}
}
} else {
socket?.connect();

Expand All @@ -51,7 +65,11 @@ export function resume(instance: SocketService) {
},
async (
error: string | null,
result?: { ready: boolean; persistence?: boolean; walletKey?: string },
result?: {
ready: boolean;
persistence?: boolean;
walletKey?: string;
},
) => {
try {
await handleJoinChannelResults(instance, error, result);
Expand All @@ -62,17 +80,6 @@ export function resume(instance: SocketService) {
);
}

// Always try to recover key exchange from both side (wallet / dapp)
if (keyExchange?.areKeysExchanged()) {
if (!isOriginator) {
instance.sendMessage({ type: MessageType.READY });
}
} else if (!isOriginator) {
keyExchange?.start({
isOriginator: isOriginator ?? false,
});
}

state.manualDisconnect = false;
state.resumed = true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export function handleSendMessage(
message: CommunicationLayerMessage,
) {
if (!instance.state.channelId) {
logger.SocketService(
`handleSendMessage: no channelId - Create a channel first`,
);
throw new Error('Create a channel first');
}

Expand Down
13 changes: 9 additions & 4 deletions packages/sdk-socket-server-next/src/protocol/handleMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,16 @@ export const handleMessage = async ({
);

// broadcast that the channel supports relayPersistence
socket.broadcast
.to(channelId)
.emit(`config-${channelId}`, { persistence: true });
socket.broadcast.to(channelId).emit(`config-${channelId}`, {
persistence: true,
walletKey: channelConfig.walletKey,
});

// also inform current client
socket.emit(`config-${channelId}`, { persistence: true });
socket.emit(`config-${channelId}`, {
persistence: true,
walletKey: channelConfig.walletKey,
});
}
return;
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@ export function onMessage(
}

if (!message?.name) {
logger(
`[RCPMS: onMessage()] ignore message without name message=${message}`,
);
logger(`[RCPMS: onMessage()] ignore message without name`, message);
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ describe('write function', () => {
const mockGetKeyInfo = jest.fn();
const mockIsSecure = jest.fn();
const mockOpenDeeplink = jest.fn();
const mockIsMetaMaskInstalled = jest.fn();
const isProviderConnected = jest.fn();

let mockRemoteCommunicationPostMessageStream = {
Expand All @@ -44,6 +45,7 @@ describe('write function', () => {
isSecure: mockIsSecure,
isMobileWeb: mockIsMobileWeb,
openDeeplink: mockOpenDeeplink,
isMetaMaskInstalled: mockIsMetaMaskInstalled,
},
debug: false,
},
Expand All @@ -57,13 +59,15 @@ describe('write function', () => {
callback = jest.fn();

mockIsMobileWeb.mockReturnValue(false);
mockIsMetaMaskInstalled.mockReturnValue(true);

mockEthereum.mockReturnValue({
isConnected: isProviderConnected,
});

mockExtractMethod.mockReturnValue({
method: 'metamask_getProviderState',
data: { data: {} },
});

mockRemoteCommunicationPostMessageStream = {
Expand All @@ -81,6 +85,7 @@ describe('write function', () => {
isSecure: mockIsSecure,
openDeeplink: mockOpenDeeplink,
isMobileWeb: mockIsMobileWeb,
isMetaMaskInstalled: mockIsMetaMaskInstalled,
},
debug: false,
},
Expand Down Expand Up @@ -125,21 +130,6 @@ describe('write function', () => {
mockIsMobileWeb.mockReturnValue(false);
});

it('should call the callback and not send a message if neither socketConnected nor ready', async () => {
mockIsConnected.mockReturnValue(false);
mockIsReady.mockReturnValue(false);

await write(
mockRemoteCommunicationPostMessageStream,
{ jsonrpc: '2.0', method: 'some_method' },
'utf8',
callback,
);

expect(callback).toHaveBeenCalledWith(new Error('disconnected'));
expect(mockSendMessage).not.toHaveBeenCalled();
});

it('should warn if ready is true but socketConnected is false', async () => {
mockIsReady.mockReturnValue(true);
mockIsConnected.mockReturnValue(false);
Expand All @@ -154,13 +144,14 @@ describe('write function', () => {
);

expect(consoleWarnSpy).toHaveBeenCalledWith(
`[RCPMS: _write()] invalid socket status -- shouldn't happen`,
'[RCPMS: write()] activeDeeplinkProtocol=undefined',
);
});

it('should debug log if both ready and socketConnected are true', async () => {
mockIsReady.mockReturnValue(true);
mockIsConnected.mockReturnValue(true);
mockGetChannelId.mockReturnValue('some_channel_id');

await write(
mockRemoteCommunicationPostMessageStream,
Expand All @@ -170,7 +161,10 @@ describe('write function', () => {
);

expect(spyLogger).toHaveBeenCalledWith(
`[RCPMS: _write()] method metamask_getProviderState doesn't need redirect.`,
expect.stringContaining(
"[RCPMS: write()] method='metamask_getProviderState' isRemoteReady=true",
),
expect.anything(),
);
});
});
Expand All @@ -182,10 +176,10 @@ describe('write function', () => {
mockIsAuthorized.mockReturnValue(true);
mockIsMobileWeb.mockReturnValue(false);
mockIsSecure.mockReturnValue(true);
mockGetChannelId.mockReturnValue('some_channel_id');
});

it('should redirect if method exists in METHODS_TO_REDIRECT', async () => {
mockGetChannelId.mockReturnValue('some_channel_id');
mockExtractMethod.mockReturnValue({
method: Object.keys(METHODS_TO_REDIRECT)[0],
});
Expand All @@ -198,15 +192,16 @@ describe('write function', () => {
);

expect(mockOpenDeeplink).toHaveBeenCalledWith(
'https://metamask.app.link/connect?channelId=some_channel_id&pubkey=&comm=socket&t=d&v=2',
'metamask://connect?channelId=some_channel_id&pubkey=&comm=socket&t=d&v=2',
expect.stringContaining(
'https://metamask.app.link/connect?channelId=some_channel_id',
),
expect.stringContaining('metamask://connect?channelId=some_channel_id'),
'_self',
);
});

it('should create a deeplink if remote is paused', async () => {
mockIsPaused.mockReturnValue(true);
mockGetChannelId.mockReturnValue('some_channel_id');

await write(
mockRemoteCommunicationPostMessageStream,
Expand All @@ -216,8 +211,12 @@ describe('write function', () => {
);

expect(mockOpenDeeplink).toHaveBeenCalledWith(
'https://metamask.app.link/connect?redirect=true&channelId=some_channel_id&pubkey=&comm=socket&t=d&v=2',
'metamask://connect?redirect=true&channelId=some_channel_id&pubkey=&comm=socket&t=d&v=2',
expect.stringContaining(
'https://metamask.app.link/connect?redirect=true&channelId=some_channel_id',
),
expect.stringContaining(
'metamask://connect?redirect=true&channelId=some_channel_id',
),
'_self',
);
});
Expand All @@ -228,6 +227,7 @@ describe('write function', () => {
mockIsReady.mockReturnValue(true);
mockIsConnected.mockReturnValue(true);
mockIsMobileWeb.mockReturnValue(false);
mockGetChannelId.mockReturnValue('some_channel_id');

await write(
mockRemoteCommunicationPostMessageStream,
Expand Down
Loading
Loading