Skip to content

Commit

Permalink
move third party wallet connections to sdk (#473)
Browse files Browse the repository at this point in the history
* move third party wallet connections to sdk

* add coinbasewallet.sdk type

* update coinbasewallet.sdk type

* add test coverage

* update types

* update yarn.lock

* remove await

* remove walletconnect disconnect

* re-add wallet-connect disconnect

* pass in reloadOnDisconnect false

* fix return type
  • Loading branch information
hcote committed Mar 24, 2023
1 parent 4cbb97c commit 5e3070a
Show file tree
Hide file tree
Showing 42 changed files with 3,334 additions and 128 deletions.
4 changes: 2 additions & 2 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ const config: Config.InitialOptions = {
global: {
lines: 99,
statements: 99,
functions: 99,
functions: 98,
branches: 99,
}
},
},
setupFilesAfterEnv: ['./test/setup.ts'],
globals: {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"postinstall": "husky install && lerna link"
},
"devDependencies": {
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@ikscodes/browser-env": "~0.3.1",
"@ikscodes/eslint-config": "~7.0.2",
"@ikscodes/prettier-config": "~2.0.1",
Expand Down
2 changes: 2 additions & 0 deletions packages/@magic-sdk/provider/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@
"tslib": "^2.3.1"
},
"dependencies": {
"@coinbase/wallet-sdk": "3.6.3",
"@magic-sdk/types": "^11.6.2",
"@walletconnect/web3-provider": "^1.8.0",
"eventemitter3": "^4.0.4",
"web3-core": "1.5.2"
},
Expand Down
2 changes: 2 additions & 0 deletions packages/@magic-sdk/provider/src/core/sdk-environment.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ThirdPartyWalletOptions } from '@magic-sdk/types';
import type localForage from 'localforage';
import type { ViewController } from './view-controller';
import type { SDKBase } from './sdk';
Expand All @@ -17,6 +18,7 @@ export interface SDKEnvironment {
ViewController: ConstructorOf<ViewController>;
configureStorage: () => Promise<typeof localForage>;
bundleId?: string | null;
thirdPartyWalletOptions?: ThirdPartyWalletOptions;
}

export const SDKEnvironment: SDKEnvironment = {} as any;
Expand Down
7 changes: 5 additions & 2 deletions packages/@magic-sdk/provider/src/core/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable no-param-reassign */

import { EthNetworkConfiguration, QueryParameters, SupportedLocale } from '@magic-sdk/types';
import { EthNetworkConfiguration, QueryParameters, SupportedLocale, ThirdPartyWalletOptions } from '@magic-sdk/types';
import type { AbstractProvider } from 'web3-core';
import { coerce, satisfies } from '../util/semver';
import { encodeJSON } from '../util/base64-json';
Expand Down Expand Up @@ -95,6 +95,7 @@ export interface MagicSDKAdditionalConfiguration<
network?: EthNetworkConfiguration;
extensions?: TExt;
testMode?: boolean;
thirdPartyWalletOptions?: ThirdPartyWalletOptions | undefined;
}

export class SDKBase {
Expand All @@ -103,6 +104,7 @@ export class SDKBase {
protected readonly endpoint: string;
protected readonly parameters: string;
public readonly testMode: boolean;
public readonly thirdPartyWalletOptions: ThirdPartyWalletOptions | undefined;

/**
* Contains methods for starting a Magic SDK authentication flow.
Expand Down Expand Up @@ -142,9 +144,10 @@ export class SDKBase {
createReactNativeEndpointConfigurationWarning().log();
}

const { defaultEndpoint, version } = SDKEnvironment;
const { defaultEndpoint, version, thirdPartyWalletOptions } = SDKEnvironment;
this.testMode = !!options?.testMode;
this.endpoint = createURL(options?.endpoint ?? defaultEndpoint).origin;
this.thirdPartyWalletOptions = thirdPartyWalletOptions || options?.thirdPartyWalletOptions;

// Prepare built-in modules
this.auth = new AuthModule(this);
Expand Down
260 changes: 253 additions & 7 deletions packages/@magic-sdk/provider/src/modules/wallet.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,76 @@
import { MagicPayloadMethod, RequestUserInfoScope, UserInfo, WalletInfo } from '@magic-sdk/types';
import WalletConnectProvider from '@walletconnect/web3-provider';
import { CoinbaseWalletProvider, CoinbaseWalletSDK } from '@coinbase/wallet-sdk';
import {
Errors,
Events,
MagicPayloadMethod,
RequestUserInfoScope,
UserEnv,
UserInfo,
WalletInfo,
Wallets,
} from '@magic-sdk/types';

import { BaseModule } from './base-module';
import { createJsonRpcRequestPayload } from '../core/json-rpc';
import { clearKeys } from '../util/web-crypto';
import { setItem, getItem, removeItem } from '../util/storage';

export class WalletModule extends BaseModule {
/* Prompt Magic's Login Form */
public connectWithUI() {
const requestPayload = createJsonRpcRequestPayload(MagicPayloadMethod.RequestAccounts);
return this.request<string[]>(requestPayload);
public async connectWithUI() {
// If within metamask wallet browser, auto-connect without any UI (if dapp has metamask enabled)
if (this.isMetaMaskBrowser()) {
try {
const isMetaMaskEnabled = await this.isWalletEnabled(Wallets.MetaMask);
if (isMetaMaskEnabled) {
return this.autoConnectIfWalletBrowser(Wallets.MetaMask);
}
// If not enabled, continue with normal flow
} catch (error) {
console.error(error);
}
}
// If within coinbase wallet browser, auto-connect without any UI (if dapp has coinbase enabled)
if (this.isCoinbaseWalletBrowser()) {
try {
const isCoinbaseWalletEnabled = await this.isWalletEnabled(Wallets.CoinbaseWallet);
if (isCoinbaseWalletEnabled) {
return this.autoConnectIfWalletBrowser(Wallets.CoinbaseWallet);
}
// If not enabled, continue with normal flow
} catch (error) {
console.error(error);
}
}
const userEnv = this.getUserEnv();
const loginRequestPayload = createJsonRpcRequestPayload(MagicPayloadMethod.Login, [userEnv]);
const loginRequest = this.request<string[]>(loginRequestPayload);
loginRequest.on(Events.WalletSelected as any, (params) =>
this.handleWalletSelected({ ...params, showModal: !!params.showModal, payloadId: loginRequestPayload.id }),
);
return loginRequest;
}

/* Returns the provider for the connected wallet */
public async getProvider(): Promise<any> {
const activeWallet = await getItem(this.localForageKey);
switch (activeWallet) {
case Wallets.MetaMask:
return this.getMetaMaskProvider();
case Wallets.WalletConnect:
if (!this.sdk.thirdPartyWalletOptions?.walletConnect) {
throw new Error(Errors.WalletConnectError);
}
return this.getWalletConnectProvider(false);
case Wallets.CoinbaseWallet:
if (!this.sdk.thirdPartyWalletOptions?.coinbaseWallet) {
throw new Error(Errors.CoinbaseWalletError);
}
return this.getCoinbaseProvider().provider;
default:
return this.sdk.rpcProvider;
}
}

/* Prompt Magic's Wallet UI (not available for users logged in with third party wallets) */
Expand All @@ -17,8 +80,9 @@ export class WalletModule extends BaseModule {
}

/* Get user info such as the wallet type they are logged in with */
public getInfo() {
const requestPayload = createJsonRpcRequestPayload(MagicPayloadMethod.GetInfo);
public async getInfo() {
const activeWallet = await getItem(this.localForageKey);
const requestPayload = createJsonRpcRequestPayload(MagicPayloadMethod.GetInfo, [{ walletType: activeWallet }]);
return this.request<WalletInfo>(requestPayload);
}

Expand All @@ -29,9 +93,191 @@ export class WalletModule extends BaseModule {
}

/* Logout user */
public disconnect() {
public async disconnect() {
clearKeys();
const activeWallet = await getItem(this.localForageKey);
if (activeWallet === Wallets.WalletConnect) {
try {
const provider = await this.getWalletConnectProvider(false);
await provider.disconnect();
} catch (error) {
console.error(error);
}
}
if (activeWallet === Wallets.CoinbaseWallet) {
const coinbase = this.getCoinbaseProvider();
coinbase.provider.disconnect();
}
removeItem(this.localForageKey);
const requestPayload = createJsonRpcRequestPayload(MagicPayloadMethod.Disconnect);
return this.request<boolean>(requestPayload);
}

/* Private methods */

private localForageKey = 'mc_active_wallet';

/* MetaMask */
private isMetaMaskInstalled(): boolean {
return (
(window as any).ethereum?.isMetaMask ||
!!(window as any).ethereum?.providers?.find((provider: any) => provider?.isMetaMask)
);
}

private isMetaMaskBrowser(): boolean {
return this.isMobile() && this.isMetaMaskInstalled();
}

private getMetaMaskProvider(): any {
return (window as any).ethereum?.providers?.find((p: any) => p?.isMetaMask) || (window as any).ethereum;
}

private connectToMetaMask(): Promise<string[]> {
// Redirect to MetaMask app if user selects MetaMask on mobile
if (this.isMobile() && !this.isMetaMaskInstalled()) {
const metaMaskDeepLink = `https://metamask.app.link/dapp/${window.location.href.replace(/(^\w+:|^)\/\//, '')}`;
window.location.href = metaMaskDeepLink;
}
return this.getMetaMaskProvider().request({ method: 'eth_requestAccounts' });
}

/* Wallet Connect */
private async getWalletConnectProvider(showModal: boolean): Promise<WalletConnectProvider> {
const providerConfig = this.sdk.thirdPartyWalletOptions?.walletConnect;
if (!providerConfig) {
throw new Error(Errors.WalletConnectError);
}
const provider = new WalletConnectProvider({
...(providerConfig as any),
qrcode: showModal,
});
const activeWallet = await getItem(this.localForageKey);
const isConnected = localStorage.getItem('walletconnect');
// Only enable Wallet Connect provider if wallet is still connected
if (activeWallet && isConnected) {
await provider.enable();
}
return provider;
}

private async connectToWalletConnect(payloadId: any, showModal?: boolean): Promise<string[]> {
if (!this.sdk.thirdPartyWalletOptions?.walletConnect) {
throw new Error(Errors.WalletConnectError);
}
const provider = await this.getWalletConnectProvider(!!showModal);
provider.connector.on(Events.DisplayUri, (err, payload: any) => {
if (!showModal) {
const uri = payload.params[0];
this.createIntermediaryEvent(Events.Uri as any, payloadId)(uri);
}
});
return provider.enable();
}

/* Coinbase Wallet */
private isCoinbaseWalletInstalled(): boolean {
return (
(window as any).ethereum?.isCoinbaseWallet ||
!!(window as any).ethereum?.providers?.find((provider: any) => provider?.isCoinbaseWallet)
);
}

private isCoinbaseWalletBrowser(): boolean {
return !!(window as any).ethereum?.isCoinbaseBrowser;
}

private getCoinbaseProvider(): { provider: CoinbaseWalletProvider; qrCodeUrl: string | null } {
const providerConfig = this.sdk.thirdPartyWalletOptions?.coinbaseWallet?.provider;
const sdkConfig = this.sdk.thirdPartyWalletOptions?.coinbaseWallet?.sdk;
if (!providerConfig || !sdkConfig) {
throw new Error(Errors.CoinbaseWalletError);
}
const coinbaseWallet = new CoinbaseWalletSDK({
...(sdkConfig as any),
overrideIsMetaMask: false,
headlessMode: true,
reloadOnDisconnect: false,
});
const qrCodeUrl = coinbaseWallet.getQrUrl();
const provider = coinbaseWallet.makeWeb3Provider(providerConfig.jsonRpcUrl, providerConfig.chainId);
return { provider, qrCodeUrl };
}

private connectToCoinbaseWallet(payloadId: any): Promise<string[]> {
// Redirect to Coinbase Wallet app if user selects Coinbase Wallet on mobile
if (this.isMobile() && !this.isCoinbaseWalletBrowser()) {
const coinbaseWalletDeepLink = `https://go.cb-w.com/dapp?cb_url=${encodeURIComponent(window.location.href)}`;
window.location.href = coinbaseWalletDeepLink;
}
if (!this.sdk.thirdPartyWalletOptions?.coinbaseWallet) {
throw new Error(Errors.CoinbaseWalletError);
}
const coinbase = this.getCoinbaseProvider();
if (coinbase.qrCodeUrl) {
this.createIntermediaryEvent(Events.Uri as any, payloadId)(coinbase.qrCodeUrl);
}
return coinbase.provider.request({ method: 'eth_requestAccounts' });
}

/* Helpers */
private isMobile(): boolean {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|BB|PlayBook|IEMobile|Windows Phone|Silk|Opera Mini/i.test(
navigator.userAgent,
);
}

private getUserEnv(): UserEnv {
return {
env: {
isMetaMaskInstalled: this.isMetaMaskInstalled(),
isCoinbaseWalletInstalled: this.isCoinbaseWalletInstalled(),
},
};
}

private connectToThirdPartyWallet(provider: Wallets, payloadId: any, showModal?: boolean): Promise<any> {
switch (provider) {
case Wallets.MetaMask:
return this.connectToMetaMask();
case Wallets.WalletConnect:
return this.connectToWalletConnect(payloadId, showModal);
case Wallets.CoinbaseWallet:
return this.connectToCoinbaseWallet(payloadId);
default:
throw new Error(
`Invalid provider: ${provider}. Must be one of "metamask", "coinbase_wallet", or "wallet_connect".`,
);
}
}

private isWalletEnabled(wallet: Wallets): Promise<boolean> {
const isWalletEnabled = createJsonRpcRequestPayload('mc_is_wallet_enabled', [{ wallet }]);
return this.request<boolean>(isWalletEnabled);
}

/* Triggers connection to wallet, emits success/reject event back to iframe */
private async handleWalletSelected(params: { wallet: Wallets; showModal: boolean; payloadId: number }) {
try {
const address = await this.connectToThirdPartyWallet(params.wallet, params.payloadId, params.showModal);
await setItem(this.localForageKey, params.wallet);
this.createIntermediaryEvent(Events.WalletConnected as any, params.payloadId as any)(address);
} catch (error) {
this.createIntermediaryEvent(Events.WalletRejected as any, params.payloadId as any)();
}
}

private async autoConnectIfWalletBrowser(wallet: Wallets): Promise<string[]> {
let address;
if (wallet === Wallets.MetaMask) {
address = await this.getMetaMaskProvider().request({ method: 'eth_requestAccounts' });
}
if (wallet === Wallets.CoinbaseWallet) {
address = await this.getCoinbaseProvider().provider.request({ method: 'eth_requestAccounts' });
}
await setItem(this.localForageKey, wallet);
const autoConnectPayload = createJsonRpcRequestPayload(MagicPayloadMethod.AutoConnect, [{ wallet, address }]);
const autoConnectRequest = this.request<string[]>(autoConnectPayload);
return autoConnectRequest;
}
}

0 comments on commit 5e3070a

Please sign in to comment.