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

Add WalletConnect Provider #45

Merged
merged 5 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"@multiversx/sdk-native-auth-client": "^1.0.8",
"@multiversx/sdk-opera-provider": "1.0.0-alpha.1",
"@multiversx/sdk-wallet": "4.5.1",
"@multiversx/sdk-wallet-connect-provider": "4.1.2",
"@multiversx/sdk-wallet-connect-provider": "5.0.1",
"@multiversx/sdk-web-wallet-iframe-provider": "2.0.1",
"@multiversx/sdk-web-wallet-provider": "3.2.1",
"isomorphic-fetch": "3.0.0",
Expand Down
19 changes: 18 additions & 1 deletion src/core/providers/ProviderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { createExtensionProvider } from './helpers/extension/createExtensionProv
import { getConfig } from './helpers/getConfig';
import { createIframeProvider } from './helpers/iframe/createIframeProvider';
import { createLedgerProvider } from './helpers/ledger/createLedgerProvider';
import { createWalletConnectProvider } from './helpers/walletConnect/createWalletConnectProvider';
import {
ICustomProvider,
IProvider,
Expand All @@ -30,7 +31,7 @@ export class ProviderFactory {
}: IProviderFactory): Promise<DappProvider> {
let createdProvider: IProvider | null = null;
const config = await getConfig(userConfig);
const { account, UI } = config;
const { account, UI, walletConnect } = config;

switch (type) {
case ProviderTypeEnum.extension: {
Expand Down Expand Up @@ -109,6 +110,22 @@ export class ProviderFactory {

break;
}
case ProviderTypeEnum.walletConnect: {
const provider = await createWalletConnectProvider({
mount: UI.walletConnect.mount,
config: walletConnect
});

if (!provider) {
throw new Error('Unable to create wallet connect provider');
}

createdProvider = provider as unknown as IProvider;

createdProvider.getType = () => ProviderTypeEnum.walletConnect;

break;
}

default: {
for (const customProvider of this._customProviders) {
Expand Down
20 changes: 20 additions & 0 deletions src/core/providers/helpers/getConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { LedgerConnectModal } from '@multiversx/sdk-dapp-core-ui/dist/components/ledger-connect-modal';
import type { WalletConnectModal } from '@multiversx/sdk-dapp-core-ui/dist/components/wallet-connect-modal';

import { defineCustomElements } from '@multiversx/sdk-dapp-core-ui/loader';
import { safeWindow } from 'constants/index';
import {
Expand All @@ -12,6 +14,11 @@ const UI: IProviderConfigUI = {
mount: () => {
throw new Error('mount not implemented');
}
},
[ProviderTypeEnum.walletConnect]: {
mount: () => {
throw new Error('mount not implemented');
}
}
};

Expand All @@ -35,6 +42,19 @@ export const getConfig = async (config: IProviderConfig = defaultConfig) => {
const eventBus = await ledgerModalElement.getEventBus();
return eventBus;
}
},
[ProviderTypeEnum.walletConnect]: {
mount: async () => {
defineCustomElements(safeWindow);
const walletConnectModalElement = document.createElement(
'wallet-connect-modal'
) as WalletConnectModal;

document.body.appendChild(walletConnectModalElement);
await customElements.whenDefined('wallet-connect-modal');
const eventBus = await walletConnectModalElement.getEventBus();
return eventBus;
}
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { getIsLoggedIn } from 'core/methods/account/getIsLoggedIn';
import { logout } from 'core/providers/DappProvider/helpers/logout/logout';
import {
IEventBus,
IProvider,
IProviderConfig,
ProviderTypeEnum
} from 'core/providers/types/providerFactory.types';
import { getWalletConnectProvider } from './helpers/getWalletConnectProvider';
import { WalletConnectStateManager } from './helpers/WalletConnectStateManagement';
import {
WalletConnectEventsEnum,
WalletConnectV2Error
} from './walletConnect.types';

type WalletConnectProviderProps = {
mount: () => Promise<IEventBus>;
config: IProviderConfig['walletConnect'];
};

export async function createWalletConnectProvider({
mount,
config
}: WalletConnectProviderProps): Promise<IProvider | null> {
const shouldInitiateLogin = !getIsLoggedIn();

let eventBus: IEventBus | undefined;
if (shouldInitiateLogin) {
eventBus = await mount?.();
}

if (!eventBus) {
throw new Error('Event bus not provided for WalletConnect provider');
}

const manager = WalletConnectStateManager.getInstance(eventBus);

const { walletConnectProvider, dappMethods } =
await getWalletConnectProvider(config);

const { uri = '', approval } = await walletConnectProvider.connect({
methods: dappMethods
});

const createdProvider = { ...walletConnectProvider } as unknown as IProvider;
createdProvider.getType = () => ProviderTypeEnum.walletConnect;

manager.updateWcURI(uri);

const onClose = () => {
manager.closeAndReset();
};

eventBus.subscribe(WalletConnectEventsEnum.CLOSE, onClose);

const unsubscribeFromEvents = () => {
eventBus.unsubscribe(WalletConnectEventsEnum.CLOSE, onClose);
};

createdProvider.login = async (options?: {
callbackUrl?: string;
token?: string;
}): Promise<{
address: string;
signature: string;
}> => {
const isConnected = walletConnectProvider.isConnected();

if (isConnected) {
throw new Error(WalletConnectV2Error.connectError);
}

const reconnect = async (): Promise<{
address: string;
signature: string;
}> => {
try {
await walletConnectProvider.init();

const { uri = '', approval: wcApproval } =
await walletConnectProvider.connect({
methods: dappMethods
});

manager.updateWcURI(uri);

const providerInfo = await walletConnectProvider.login({
approval: wcApproval,
token: options?.token
});

const { address = '', signature = '' } = providerInfo ?? {};

manager.closeAndReset();
return { address, signature };
} catch {
console.log('Reconnecting....');
return await reconnect();
}
};

try {
const providerData = await walletConnectProvider.login({
approval,
token: options?.token
});

const { address = '', signature = '' } = providerData ?? {};

manager.closeAndReset();
return { address, signature };
} catch (err: any) {
console.error(WalletConnectV2Error.userRejected, err);
return await reconnect();
} finally {
unsubscribeFromEvents();
}
};

createdProvider.logout = async (): Promise<boolean> => {
try {
await logout({ provider: walletConnectProvider as unknown as IProvider });
return true;
} catch (error) {
console.error('Error logging out', error);
return false;
}
};

return createdProvider;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {
IWalletConnectModalData,
WalletConnectEventsEnum
} from '../walletConnect.types';

export interface IEventBus {
publish(event: string, data: any): void;
}

export class WalletConnectStateManager<T extends IEventBus = IEventBus> {
private static instance: WalletConnectStateManager<IEventBus> | null = null;
private eventBus: T;

private initialData: IWalletConnectModalData = {
wcURI: '',
shouldClose: false
};

private data: IWalletConnectModalData = { ...this.initialData };

private constructor(eventBus: T) {
this.eventBus = eventBus;
}

public static getInstance<U extends IEventBus>(
eventBus: U
): WalletConnectStateManager<U> {
if (!WalletConnectStateManager.instance) {
WalletConnectStateManager.instance = new WalletConnectStateManager(
eventBus
);
}
return WalletConnectStateManager.instance as WalletConnectStateManager<U>;
}

public closeAndReset(): void {
this.data.shouldClose = true;
this.notifyDataUpdate();
this.resetData();
}

private resetData(): void {
this.data = { ...this.initialData };
}

public updateWcURI(wcURI: string): void {
this.data.wcURI = wcURI;
this.notifyDataUpdate();
}

private notifyDataUpdate(): void {
this.eventBus.publish(WalletConnectEventsEnum.DATA_UPDATE, this.data);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { getIsLoggedIn } from 'core/methods/account/getIsLoggedIn';
import { IProviderConfig } from 'core/providers/types/providerFactory.types';
import { logoutAction } from 'store/actions';
import { nativeAuthConfigSelector } from 'store/selectors';
import { chainIdSelector } from 'store/selectors/networkSelectors';
import { getState } from 'store/store';
import {
WalletConnectV2Provider,
WalletConnectOptionalMethodsEnum,
type SessionEventTypes
} from 'utils/walletconnect/__sdkWalletconnectProvider';
mgavrila marked this conversation as resolved.
Show resolved Hide resolved
import { getAccountProvider } from '../../../accountProvider';
import { WalletConnectV2Error } from '../walletConnect.types';

const dappMethods: string[] = [
WalletConnectOptionalMethodsEnum.CANCEL_ACTION,
WalletConnectOptionalMethodsEnum.SIGN_LOGIN_TOKEN
];

export async function getWalletConnectProvider(
config: IProviderConfig['walletConnect']
) {
const isLoggedIn = getIsLoggedIn();
const chainId = chainIdSelector(getState());
const provider = getAccountProvider();
const nativeAuthConfig = nativeAuthConfigSelector(getState());

if (nativeAuthConfig) {
dappMethods.push(WalletConnectOptionalMethodsEnum.SIGN_NATIVE_AUTH_TOKEN);
}

if (!config?.walletConnectV2ProjectId) {
throw new Error(WalletConnectV2Error.invalidConfig);
}

const handleOnLogin = () => {
console.log('WalletConnect Login Event: Logged In');
mgavrila marked this conversation as resolved.
Show resolved Hide resolved
};

const handleOnLogout = async () => {
console.log('Logging out..');
if (config.onLogout) {
await config.onLogout();
}

logoutAction();
};

const handleOnEvent = (event: SessionEventTypes['event']) => {
console.log('WalletConnect Session Event: ', event);
};
const providerHandlers = {
onClientLogin: handleOnLogin,
onClientLogout: handleOnLogout,
onClientEvent: handleOnEvent
};

try {
const {
walletConnectV2ProjectId,
walletConnectV2Options = {},
walletConnectV2RelayAddress = ''
} = config;
const walletConnectProvider = new WalletConnectV2Provider(
providerHandlers,
chainId,
walletConnectV2RelayAddress,
walletConnectV2ProjectId,
walletConnectV2Options
);

await walletConnectProvider.init();

return { walletConnectProvider, dappMethods };
} catch (err) {
console.error('Could not initialize walletConnect', err);

if (isLoggedIn) {
await provider.logout();
}

throw err;
}
}
21 changes: 21 additions & 0 deletions src/core/providers/helpers/walletConnect/walletConnect.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export enum WalletConnectV2Error {
invalidAddress = 'Invalid address',
invalidConfig = 'Invalid WalletConnect setup',
invalidTopic = 'Expired connection',
sessionExpired = 'Unable to connect to existing session',
connectError = 'Unable to connect',
userRejected = 'User rejected connection proposal',
userRejectedExisting = 'User rejected existing connection proposal',
errorLogout = 'Unable to remove existing pairing',
invalidChainID = 'Invalid chainID'
}

export enum WalletConnectEventsEnum {
'CLOSE' = 'CLOSE',
'DATA_UPDATE' = 'DATA_UPDATE'
}

export interface IWalletConnectModalData {
wcURI: string;
shouldClose?: boolean;
}
Loading
Loading