diff --git a/docs/guides/custom-wallets.md b/docs/guides/custom-wallets.md index d4b827fab..ac459b068 100644 --- a/docs/guides/custom-wallets.md +++ b/docs/guides/custom-wallets.md @@ -8,11 +8,9 @@ The basic structure of a (browser) wallet should look like: ```ts import { - WalletModule, + WalletModuleFactory, WalletBehaviourFactory, BrowserWallet, - Action, - Transaction, } from "@near-wallet-selector/core"; export interface MyWalletParams { @@ -21,30 +19,19 @@ export interface MyWalletParams { const MyWallet: WalletBehaviourFactory = ({ options, - emitter, provider, }) => { + // Initialise wallet-sepecific client(s) here. + return { - async isAvailable() { - // Determine whether My Wallet is available. - // For example, some wallets aren't supported on mobile. - - return true; - }, - async connect() { // Connect to My Wallet for access to account(s). - const accounts = []; - emitter.emit("connected", { accounts }); - - return accounts; + return []; }, async disconnect() { // Disconnect from accounts and cleanup (e.g. listeners). - - emitter.emit("disconnected", null); }, async getAccounts() { @@ -79,23 +66,31 @@ const MyWallet: WalletBehaviourFactory = ({ export function setupMyWallet({ iconUrl = "./assets/my-wallet-icon.png", -}: MyWalletParams = {}): WalletModule { - return { - id: "my-wallet", - type: "browser", - name: "My Wallet", - description: null, - iconUrl, - wallet: MyWallet, +}: MyWalletParams = {}): WalletModuleFactory { + return async () => { + // Return null here when wallet is unavailable. + + return { + id: "my-wallet", + type: "browser", + metadata: { + name: "My Wallet", + description: null, + iconUrl, + }, + init: MyWallet, + }; }; } ``` -`WalletModule` is made up of two main parts: -- Behaviour: `wallet`. -- Metadata: `id`, `type`, `name`, `description` and `iconUrl`. +`WalletModule` (return type of `WalletModuleFactory`) is made up of four properties: +- `id`: Unique identifier for the wallet. +- `type`: Type of wallet to infer the behaviour and metadata. +- `metadata`: Metadata for displaying information to the user. +- `init`: The implementation (behaviour) of the wallet. -The metadata of a wallet is accessible as part of the selector's `wallets` state. It's important that `id` is unique to avoid conflicts with other wallets installed by a dApp. The `type` property is coupled to the parameter we pass to `WalletModule` and `WalletBehaviourFactory`. +A variation of `WalletModule` is added to state during setup under `modules` (`ModuleState`) and accessed by the UI to display the available wallets. It's important that `id` is unique to avoid conflicts with other wallets installed by a dApp. The `type` property is coupled to the parameter we pass to `WalletModuleFactory` and `WalletBehaviourFactory`. Although we've tried to implement a polymorphic approach to wallets, there are some differences between wallet types that means your implementation won't always mirror other wallets such as Sender vs. Ledger. There are currently four types of wallet: @@ -106,25 +101,15 @@ Although we've tried to implement a polymorphic approach to wallets, there are s ## Methods -### `isAvailable` - -This method is used to determine whether a wallet is available for connecting. For example, injected wallets such as Sender are unavailable on mobile where browser extensions are not supported. The UI will hide the wallet when `false` is returned. - -> Note: Injected wallets should be considered available if they aren't installed. The modal handles this case by displaying a download link (using `getDownloadUrl`) when attempting to connect. - ### `connect` -This method handles wallet setup (e.g. initialising wallet-specific libraries) and requesting access to accounts via `FunctionCall` access keys. It's important that `connected` is emitted only when we successfully gain access to at least one account. +This method handles access to accounts via `FunctionCall` access keys. It's important that at least one account is returned to be in a connected state. > Note: Hardware wallets are passed a `derivationPath` where other wallets types are called without any parameters. -> Note: The combination of setup and connecting is still under review. - ### `disconnect` -This method handles disconnecting from accounts and cleanup such as event listeners. It's called when either the user specifically disconnects or when switching to a different wallet. It's important that `disconnected` is emitted regardless of exceptions. - -> Note: The requirement to emit "disconnected" is still under review and may be removed in favour of accepting the Promise settling as a signal that this method has completed. +This method handles disconnecting from accounts and cleanup such as event listeners. It's called when either the user specifically disconnects or when switching to a different wallet. ### `getAccounts` @@ -143,9 +128,3 @@ Where you might have to construct NEAR Transactions and send them yourself, you This method is similar to `signAndSendTransaction` but instead sends a batch of Transactions. > Note: Exactly how this method should behave when transactions fail is still under review with no clear "right" way to do it. NEAR Wallet (website) seems to ignore any transactions that fail and continue executing the rest. Our approach attempts to execute the transactions in a series and bail if any fail (we will look to improve this in the future by implementing a retry feature). - -### `getDownloadUrl` - -This method returns the download link for users who haven't installed the wallet. This is usually the Chrome Web Store but ideally this should be browser aware to give the best experience for the user. - -> Note: This method is only applicable to injected wallets. diff --git a/examples/react/src/components/Content.tsx b/examples/react/src/components/Content.tsx index 44e115644..0d7be9564 100644 --- a/examples/react/src/components/Content.tsx +++ b/examples/react/src/components/Content.tsx @@ -77,14 +77,13 @@ const Content: React.FC = () => { selector.show(); }; - const handleSignOut = () => { - selector - .wallet() - .disconnect() - .catch((err) => { - console.log("Failed to sign out"); - console.error(err); - }); + const handleSignOut = async () => { + const wallet = await selector.wallet(); + + wallet.disconnect().catch((err) => { + console.log("Failed to sign out"); + console.error(err); + }); }; const handleSwitchProvider = () => { @@ -101,10 +100,11 @@ const Content: React.FC = () => { alert("Switched account to " + nextAccountId); }; - const handleSendMultipleTransactions = () => { + const handleSendMultipleTransactions = async () => { const { contractId } = selector.options; + const wallet = await selector.wallet(); - selector.wallet().signAndSendTransactions({ + await wallet.signAndSendTransactions({ transactions: [ { // Deploy your own version of https://github.com/near-examples/rust-counter using Gitpod to get a valid receiverId. @@ -140,7 +140,7 @@ const Content: React.FC = () => { }; const handleSubmit = useCallback( - (e: SubmitEvent) => { + async (e: SubmitEvent) => { e.preventDefault(); // TODO: Fix the typing so that target.elements exists.. @@ -153,8 +153,10 @@ const Content: React.FC = () => { // TODO: optimistically update page with new message, // update blockchain data in background // add uuid to each message, so we know which one is already known - selector - .wallet() + + const wallet = await selector.wallet(); + + wallet .signAndSendTransaction({ signerId: accountId!, actions: [ diff --git a/examples/react/src/contexts/WalletSelectorContext.tsx b/examples/react/src/contexts/WalletSelectorContext.tsx index 0db1b2de3..efdb02c1a 100644 --- a/examples/react/src/contexts/WalletSelectorContext.tsx +++ b/examples/react/src/contexts/WalletSelectorContext.tsx @@ -60,14 +60,15 @@ export const WalletSelectorContextProvider: React.FC = ({ children }) => { setupWalletSelector({ network: "testnet", contractId: "guest-book.testnet", - wallets: [ + debug: true, + modules: [ setupNearWallet(), setupSender(), setupMathWallet(), setupLedger(), setupWalletConnect({ projectId: "c4f79cc...", - appMetadata: { + metadata: { name: "NEAR Wallet Selector", description: "Example dApp used by NEAR Wallet Selector", url: "https://github.com/near/wallet-selector", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 713070b3c..c8172de99 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -10,21 +10,29 @@ export { Optional } from "./lib/utils.types"; export { WalletSelectorState, - WalletState, + ModuleState, AccountState, } from "./lib/store.types"; export { - Wallet, - WalletType, - WalletMetadata, - WalletBehaviour, + WalletModuleFactory, WalletModule, WalletBehaviourFactory, + WalletBehaviourOptions, + Wallet, + WalletType, + BrowserWalletMetadata, + BrowserWalletBehaviour, BrowserWallet, + InjectedWalletMetadata, + InjectedWalletBehaviour, InjectedWallet, - HardwareWallet, + HardwareWalletMetadata, HardwareWalletConnectParams, + HardwareWalletBehaviour, + HardwareWallet, + BridgeWalletMetadata, + BridgeWalletBehaviour, BridgeWallet, Transaction, Action, @@ -39,7 +47,5 @@ export { DeleteAccountAction, } from "./lib/wallet"; -export { errors } from "./lib/errors"; - export { transformActions } from "./lib/wallet"; export { waitFor } from "./lib/helpers"; diff --git a/packages/core/src/lib/constants.ts b/packages/core/src/lib/constants.ts index 07a040ed7..38ba1ee50 100644 --- a/packages/core/src/lib/constants.ts +++ b/packages/core/src/lib/constants.ts @@ -1,4 +1,5 @@ export const PACKAGE_NAME = "near-wallet-selector"; -export const LOCAL_STORAGE_SELECTED_WALLET_ID = `selectedWalletId`; +export const SELECTED_WALLET_ID = `selectedWalletId`; +export const PENDING_SELECTED_WALLET_ID = `selectedWalletId:pending`; export const DEFAULT_DERIVATION_PATH = "44'/397'/0'/0'/1'"; diff --git a/packages/core/src/lib/errors.ts b/packages/core/src/lib/errors.ts deleted file mode 100644 index 33dc98650..000000000 --- a/packages/core/src/lib/errors.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { WalletMetadata } from "./wallet"; - -enum ErrorCodes { - WalletNotInstalled = "WalletNotInstalled", -} - -class WalletSelectorError extends Error { - constructor(name: ErrorCodes, message: string) { - super(message); - - this.name = name; - - Object.setPrototypeOf(this, WalletSelectorError.prototype); - } -} - -const isError = (err: unknown, code?: ErrorCodes) => { - if (!(err instanceof WalletSelectorError)) { - return false; - } - - return code ? err.name === code : true; -}; - -export const errors = { - isWalletNotInstalledError: (err: unknown) => { - return isError(err, ErrorCodes.WalletNotInstalled); - }, - createWalletNotInstalledError: (metadata: WalletMetadata) => { - return new WalletSelectorError( - ErrorCodes.WalletNotInstalled, - `${metadata.name} not installed` - ); - }, -}; diff --git a/packages/core/src/lib/modal/components/LedgerDerivationPath.tsx b/packages/core/src/lib/modal/components/LedgerDerivationPath.tsx index 622810e3f..02a600396 100644 --- a/packages/core/src/lib/modal/components/LedgerDerivationPath.tsx +++ b/packages/core/src/lib/modal/components/LedgerDerivationPath.tsx @@ -25,10 +25,10 @@ export const LedgerDerivationPath: React.FC = ({ setLedgerDerivationPath(e.target.value); }; - const handleConnectClick = () => { + const handleConnectClick = async () => { setIsLoading(true); // TODO: Can't assume "ledger" once we implement more hardware wallets. - const wallet = selector.wallet("ledger"); + const wallet = await selector.wallet("ledger"); if (wallet.type !== "hardware") { return; @@ -36,16 +36,18 @@ export const LedgerDerivationPath: React.FC = ({ setIsLoading(true); - wallet + return wallet .connect({ derivationPath: ledgerDerivationPath }) .then(() => onConnected()) .catch((err) => setLedgerError(`Error: ${err.message}`)) .finally(() => setIsLoading(false)); }; - const handleEnterClick: KeyboardEventHandler = (e) => { + const handleEnterClick: KeyboardEventHandler = async ( + e + ) => { if (e.key === "Enter") { - handleConnectClick(); + await handleConnectClick(); } }; diff --git a/packages/core/src/lib/modal/components/Modal.tsx b/packages/core/src/lib/modal/components/Modal.tsx index 5daf7b5d8..c9d031dc3 100644 --- a/packages/core/src/lib/modal/components/Modal.tsx +++ b/packages/core/src/lib/modal/components/Modal.tsx @@ -1,11 +1,9 @@ import React, { MouseEvent, useCallback, useEffect, useState } from "react"; -import { Wallet } from "../../wallet"; import { WalletSelectorModal, ModalOptions, Theme } from "../modal.types"; import { WalletSelector } from "../../wallet-selector.types"; import { ModalRouteName } from "./Modal.types"; import { LedgerDerivationPath } from "./LedgerDerivationPath"; -import { WalletNotInstalled } from "./WalletNotInstalled"; import { WalletNetworkChanged } from "./WalletNetworkChanged"; import { WalletOptions } from "./WalletOptions"; import { AlertMessage } from "./AlertMessage"; @@ -38,9 +36,6 @@ export const Modal: React.FC = ({ hide, }) => { const [routeName, setRouteName] = useState("WalletOptions"); - const [notInstalledWallet, setNotInstalledWallet] = useState( - null - ); const [alertMessage, setAlertMessage] = useState(null); useEffect(() => { @@ -63,7 +58,6 @@ export const Modal: React.FC = ({ const handleDismissClick = useCallback(() => { setAlertMessage(null); - setNotInstalledWallet(null); setRouteName("WalletOptions"); hide(); }, [hide]); @@ -113,16 +107,12 @@ export const Modal: React.FC = ({ { - setNotInstalledWallet(wallet); - return setRouteName("WalletNotInstalled"); - }} onConnectHardwareWallet={() => { setRouteName("LedgerDerivationPath"); }} onConnected={handleDismissClick} - onError={(message) => { - setAlertMessage(message); + onError={(err) => { + setAlertMessage(err.message); setRouteName("AlertMessage"); }} /> @@ -134,15 +124,6 @@ export const Modal: React.FC = ({ onBack={() => setRouteName("WalletOptions")} /> )} - {routeName === "WalletNotInstalled" && notInstalledWallet && ( - { - setNotInstalledWallet(null); - setRouteName("WalletOptions"); - }} - /> - )} {routeName === "WalletNetworkChanged" && ( void; -} - -export const WalletNotInstalled: React.FC = ({ - notInstalledWallet, - onBack, -}) => { - return ( -
-
- {notInstalledWallet.name} -

{notInstalledWallet.name}

-
-

- {`You'll need to install ${notInstalledWallet.name} to continue. After installing`} - window.location.reload()}> -  refresh the page. - -

-
- - -
-
- ); -}; diff --git a/packages/core/src/lib/modal/components/WalletOptions.tsx b/packages/core/src/lib/modal/components/WalletOptions.tsx index a5acfa7e8..c0cfe1df8 100644 --- a/packages/core/src/lib/modal/components/WalletOptions.tsx +++ b/packages/core/src/lib/modal/components/WalletOptions.tsx @@ -1,82 +1,62 @@ import React, { Fragment, useEffect, useState } from "react"; -import { WalletState } from "../../store.types"; -import { Wallet } from "../../wallet"; +import { ModuleState } from "../../store.types"; import { WalletSelector } from "../../wallet-selector.types"; import { ModalOptions, WalletSelectorModal } from "../modal.types"; import { logger } from "../../services"; -import { errors } from "../../errors"; interface WalletOptionsProps { // TODO: Remove omit once modal is a separate package. selector: Omit; options?: ModalOptions; - onWalletNotInstalled: (wallet: Wallet) => void; onConnectHardwareWallet: () => void; onConnected: () => void; - onError: (message: string) => void; + onError: (error: Error) => void; } export const WalletOptions: React.FC = ({ selector, options, - onWalletNotInstalled, onError, onConnectHardwareWallet, onConnected, }) => { const [connecting, setConnecting] = useState(false); const [walletInfoVisible, setWalletInfoVisible] = useState(false); - const [availableWallets, setAvailableWallets] = useState>( - [] - ); - - const getAvailableWallets = async (wallets: Array) => { - const result: Array = []; - - for (let i = 0; i < wallets.length; i += 1) { - const wallet = selector.wallet(wallets[i].id); - - if (await wallet.isAvailable()) { - result.push(wallets[i]); - } - } - - return result; - }; + const [modules, setModules] = useState>([]); useEffect(() => { const subscription = selector.store.observable.subscribe((state) => { - getAvailableWallets(state.wallets).then(setAvailableWallets); + setModules(state.modules); }); return () => subscription.unsubscribe(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const handleWalletClick = (wallet: Wallet) => () => { + const handleWalletClick = (module: ModuleState) => async () => { if (connecting) { return; } + setConnecting(true); + + const wallet = await module.wallet(); + if (wallet.type === "hardware") { return onConnectHardwareWallet(); } - setConnecting(true); - wallet .connect() .then(() => onConnected()) .catch((err) => { - if (errors.isWalletNotInstalledError(err)) { - return onWalletNotInstalled(wallet); - } + const { name } = wallet.metadata; - logger.log(`Failed to select ${wallet.name}`); + logger.log(`Failed to select ${name}`); logger.error(err); - onError(`Failed to connect with ${wallet.name}: ${err.message}`); + onError(new Error(`Failed to connect with ${name}: ${err.message}`)); }) .finally(() => setConnecting(false)); }; @@ -93,37 +73,34 @@ export const WalletOptions: React.FC = ({ "Modal-option-list " + (connecting ? "selection-process" : "") } > - {availableWallets.reduce>( - (result, { id, selected }) => { - const wallet = selector.wallet(id); - - const { name, description, iconUrl } = wallet; - - result.push( -
  • -
    - {name} -
    - {name} -
    - {selected && ( -
    - selected -
    - )} + {modules.reduce>((result, module) => { + const { selectedWalletId } = selector.store.getState(); + const { name, description, iconUrl } = module.metadata; + const selected = module.id === selectedWalletId; + + result.push( +
  • +
    + {name} +
    + {name}
    -
  • - ); + {selected && ( +
    + selected +
    + )} + + + ); - return result; - }, - [] - )} + return result; + }, [])}
    diff --git a/packages/core/src/lib/modules/wallet-instance.ts b/packages/core/src/lib/modules/wallet-instance.ts new file mode 100644 index 000000000..8e664a451 --- /dev/null +++ b/packages/core/src/lib/modules/wallet-instance.ts @@ -0,0 +1,127 @@ +import { EventEmitter, logger, Provider, storage } from "../services"; +import { Wallet, WalletEvents, WalletModule } from "../wallet"; +import { ModuleState, Store } from "../store.types"; +import { WalletSelectorEvents } from "../wallet-selector.types"; +import { PENDING_SELECTED_WALLET_ID } from "../constants"; +import { Options } from "../options.types"; + +interface WalletInstanceParams { + modules: Array; + module: WalletModule; + store: Store; + options: Options; + emitter: EventEmitter; +} + +export const setupWalletInstance = async ({ + modules, + module, + store, + options, + emitter, +}: WalletInstanceParams) => { + const walletEmitter = new EventEmitter(); + + const handleDisconnected = (walletId: string) => { + store.dispatch({ + type: "WALLET_DISCONNECTED", + payload: { walletId }, + }); + }; + + const disconnect = async (walletId: string) => { + const walletModule = modules.find((x) => x.id === walletId)!; + const wallet = await walletModule.wallet(); + + await wallet.disconnect().catch((err) => { + logger.log(`Failed to disconnect ${walletId}`); + logger.error(err); + + // At least clean up state on our side. + handleDisconnected(walletId); + }); + }; + + const handleConnected = async ( + walletId: string, + { accounts = [] }: WalletEvents["connected"] + ) => { + const { selectedWalletId } = store.getState(); + + if (!accounts.length) { + // We can't guarantee the user will actually sign in with browser wallets. + // Best we can do is set in storage and validate on init. + if (module.type === "browser") { + storage.setItem(PENDING_SELECTED_WALLET_ID, walletId); + } + + return; + } + + if (selectedWalletId && selectedWalletId !== walletId) { + await disconnect(selectedWalletId); + } + + store.dispatch({ + type: "WALLET_CONNECTED", + payload: { walletId, accounts }, + }); + }; + + walletEmitter.on("disconnected", () => { + handleDisconnected(module.id); + }); + + walletEmitter.on("connected", (event) => { + handleConnected(module.id, event); + }); + + walletEmitter.on("accountsChanged", async ({ accounts }) => { + if (!accounts.length) { + return disconnect(module.id); + } + + store.dispatch({ + type: "ACCOUNTS_CHANGED", + payload: { walletId: module.id, accounts }, + }); + }); + + walletEmitter.on("networkChanged", ({ networkId }) => { + emitter.emit("networkChanged", { walletId: module.id, networkId }); + }); + + const wallet = { + id: module.id, + type: module.type, + metadata: module.metadata, + ...(await module.init({ + id: module.id, + type: module.type, + metadata: module.metadata, + options, + provider: new Provider(options.network.nodeUrl), + emitter: walletEmitter, + logger, + storage, + })), + } as Wallet; + + const _connect = wallet.connect; + const _disconnect = wallet.disconnect; + + wallet.connect = async (params: never) => { + const accounts = await _connect(params); + + await handleConnected(wallet.id, { accounts }); + return accounts; + }; + + wallet.disconnect = async () => { + await _disconnect(); + + handleDisconnected(wallet.id); + }; + + return wallet; +}; diff --git a/packages/core/src/lib/modules/wallet-modules.ts b/packages/core/src/lib/modules/wallet-modules.ts new file mode 100644 index 000000000..bb3f41ae3 --- /dev/null +++ b/packages/core/src/lib/modules/wallet-modules.ts @@ -0,0 +1,140 @@ +import { Options } from "../options.types"; +import { AccountState, ModuleState, Store } from "../store.types"; +import { logger, EventEmitter, storage } from "../services"; +import { WalletSelectorEvents } from "../wallet-selector.types"; +import { WalletModuleFactory, Wallet } from "../wallet"; +import { setupWalletInstance } from "./wallet-instance"; +import { PENDING_SELECTED_WALLET_ID } from "../constants"; + +interface WalletModulesParams { + factories: Array; + options: Options; + store: Store; + emitter: EventEmitter; +} + +export const setupWalletModules = async ({ + factories, + options, + store, + emitter, +}: WalletModulesParams) => { + const modules: Array = []; + const instances: Record = {}; + + const getWallet = async ( + id: string | null + ) => { + const module = modules.find((x) => x.id === id); + + if (!module) { + return null; + } + + return (await module.wallet()) as Variation; + }; + + const validateWallet = async (id: string | null) => { + let accounts: Array = []; + const wallet = await getWallet(id); + + if (wallet) { + // Ensure our persistent state aligns with the selected wallet. + // For example a wallet is selected, but it returns no accounts (not connected). + accounts = await wallet.getAccounts().catch((err) => { + logger.log(`Failed to validate ${wallet.id} during setup`); + logger.error(err); + + return []; + }); + } + + return accounts; + }; + + const getSelectedWallet = async () => { + const pendingSelectedWalletId = storage.getItem( + PENDING_SELECTED_WALLET_ID + ); + + if (pendingSelectedWalletId) { + const accounts = await validateWallet(pendingSelectedWalletId); + + storage.removeItem(PENDING_SELECTED_WALLET_ID); + + if (accounts.length) { + const { selectedWalletId } = store.getState(); + const selectedWallet = await getWallet(selectedWalletId); + + if (selectedWallet) { + await selectedWallet.disconnect().catch((err) => { + logger.log("Failed to disconnect existing wallet"); + logger.error(err); + }); + } + + return { + accounts, + selectedWalletId: pendingSelectedWalletId, + }; + } + } + + const { selectedWalletId } = store.getState(); + const accounts = await validateWallet(selectedWalletId); + + return { + accounts, + selectedWalletId: accounts.length ? selectedWalletId : null, + }; + }; + + for (let i = 0; i < factories.length; i += 1) { + const module = await factories[i](); + + // Filter out wallets that aren't available. + if (!module) { + continue; + } + + modules.push({ + id: module.id, + type: module.type, + metadata: module.metadata, + wallet: async () => { + let instance = instances[module.id]; + + if (instance) { + return instance; + } + + instance = await setupWalletInstance({ + modules, + module, + store, + options, + emitter, + }); + + instances[module.id] = instance; + + return instance; + }, + }); + } + + const { accounts, selectedWalletId } = await getSelectedWallet(); + + store.dispatch({ + type: "SETUP_WALLET_MODULES", + payload: { + modules, + accounts, + selectedWalletId, + }, + }); + + return { + getWallet, + }; +}; diff --git a/packages/core/src/lib/store.ts b/packages/core/src/lib/store.ts index a775a389c..1b597df2a 100644 --- a/packages/core/src/lib/store.ts +++ b/packages/core/src/lib/store.ts @@ -1,11 +1,12 @@ import { BehaviorSubject } from "rxjs"; -import { logger } from "./services"; +import { logger, storage } from "./services"; import { Store, WalletSelectorState, WalletSelectorAction, } from "./store.types"; +import { SELECTED_WALLET_ID } from "./constants"; const reducer = ( state: WalletSelectorState, @@ -15,64 +16,47 @@ const reducer = ( switch (action.type) { case "SETUP_WALLET_MODULES": { - const { wallets, selectedWalletId, accounts } = action.payload; + const { modules, selectedWalletId, accounts } = action.payload; return { ...state, - wallets: wallets.map((wallet) => ({ - id: wallet.id, - name: wallet.name, - description: wallet.description, - iconUrl: wallet.iconUrl, - type: wallet.type, - selected: wallet.id === selectedWalletId, - })), + modules, accounts, + selectedWalletId, }; } case "WALLET_CONNECTED": { - const { walletId, pending, accounts } = action.payload; + const { walletId, accounts } = action.payload; + + if (!accounts.length) { + return state; + } return { ...state, - wallets: state.wallets.map((wallet) => { - if (wallet.id === walletId) { - return { - ...wallet, - selected: !pending && !!accounts.length, - }; - } - - if (wallet.selected) { - return { - ...wallet, - selected: false, - }; - } - - return wallet; - }), accounts, + selectedWalletId: walletId, }; } case "WALLET_DISCONNECTED": { + const { walletId } = action.payload; + + if (walletId !== state.selectedWalletId) { + return state; + } + return { ...state, - wallets: state.wallets.map((wallet) => { - if (!wallet.selected) { - return wallet; - } - - return { - ...wallet, - selected: false, - }; - }), accounts: [], + selectedWalletId: null, }; } case "ACCOUNTS_CHANGED": { - const { accounts } = action.payload; + const { walletId, accounts } = action.payload; + + if (walletId !== state.selectedWalletId) { + return state; + } return { ...state, @@ -84,10 +68,33 @@ const reducer = ( } }; +const syncStorage = ( + prevState: WalletSelectorState, + state: WalletSelectorState +) => { + if (state.selectedWalletId === prevState.selectedWalletId) { + return; + } + + if (state.selectedWalletId) { + storage.setItem(SELECTED_WALLET_ID, state.selectedWalletId); + return; + } + + storage.removeItem(SELECTED_WALLET_ID); +}; + export const createStore = (): Store => { const subject = new BehaviorSubject({ + modules: [], accounts: [], - wallets: [], + selectedWalletId: storage.getItem(SELECTED_WALLET_ID), + }); + + let prevState = subject.getValue(); + subject.subscribe((state) => { + syncStorage(prevState, state); + prevState = state; }); return { diff --git a/packages/core/src/lib/store.types.ts b/packages/core/src/lib/store.types.ts index 50fa7aa57..db8a9fb7d 100644 --- a/packages/core/src/lib/store.types.ts +++ b/packages/core/src/lib/store.types.ts @@ -1,34 +1,37 @@ import { BehaviorSubject } from "rxjs"; -import { Wallet, WalletMetadata } from "./wallet"; +import { Wallet } from "./wallet"; + +export type ModuleState = { + id: Variation["id"]; + type: Variation["type"]; + metadata: Variation["metadata"]; + wallet(): Promise; +}; export interface AccountState { accountId: string; } -export type WalletState = WalletMetadata & { - selected: boolean; -}; - export interface WalletSelectorState { - wallets: Array; + modules: Array; accounts: Array; + selectedWalletId: string | null; } export type WalletSelectorAction = | { type: "SETUP_WALLET_MODULES"; payload: { - wallets: Array; - selectedWalletId: string | null; + modules: Array; accounts: Array; + selectedWalletId: string | null; }; } | { type: "WALLET_CONNECTED"; payload: { walletId: string; - pending: boolean; accounts: Array; }; } @@ -41,6 +44,7 @@ export type WalletSelectorAction = | { type: "ACCOUNTS_CHANGED"; payload: { + walletId: string; accounts: Array; }; }; diff --git a/packages/core/src/lib/testUtils.ts b/packages/core/src/lib/testUtils.ts index 4a1faa101..a40226fe9 100644 --- a/packages/core/src/lib/testUtils.ts +++ b/packages/core/src/lib/testUtils.ts @@ -1,6 +1,6 @@ import { mock } from "jest-mock-extended"; -import { Wallet, WalletEvents, WalletModule } from "./wallet"; +import { WalletModuleFactory, Wallet, WalletEvents } from "./wallet"; import { Options } from "./options.types"; import { ProviderService, @@ -18,8 +18,8 @@ export interface MockWalletDependencies { storage?: StorageService; } -export const mockWallet = ( - { wallet, ...metadata }: WalletModule, +export const mockWallet = async ( + factory: WalletModuleFactory, deps: MockWalletDependencies = {} ) => { const options = deps.options || { @@ -28,12 +28,22 @@ export const mockWallet = ( debug: false, }; - return wallet({ + const module = await factory(); + + if (!module) { + return null; + } + + const wallet = await module.init({ + id: module.id, + type: module.type, + metadata: module.metadata, options, - metadata, provider: deps.provider || mock(), emitter: deps.emitter || mock>(), logger: deps.logger || mock(), storage: deps.storage || mock(), - }) as WalletVariation; + }); + + return wallet as Variation; }; diff --git a/packages/core/src/lib/utils.types.ts b/packages/core/src/lib/utils.types.ts index e0bfdbf25..93cb8bda2 100644 --- a/packages/core/src/lib/utils.types.ts +++ b/packages/core/src/lib/utils.types.ts @@ -1,10 +1 @@ -export type UnpackedPromise = T extends Promise ? U : T; -export type GenericFunction, R> = (...args: TS) => R; - -export type Promisify = { - [K in keyof T]: T[K] extends GenericFunction - ? (...args: TS) => Promise> - : T[K]; -}; - export type Optional = Omit & Partial>; diff --git a/packages/core/src/lib/wallet-controller.ts b/packages/core/src/lib/wallet-controller.ts deleted file mode 100644 index aadef466c..000000000 --- a/packages/core/src/lib/wallet-controller.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { logger, storage, Provider, EventEmitter } from "./services"; -import { Wallet, WalletEvents, WalletModule } from "./wallet"; -import { LOCAL_STORAGE_SELECTED_WALLET_ID } from "./constants"; -import { AccountState, Store } from "./store.types"; -import { Options } from "./options.types"; -import { WalletSelectorEvents } from "./wallet-selector.types"; - -class WalletController { - private options: Options; - private modules: Array; - private wallets: Array; - private store: Store; - private emitter: EventEmitter; - - constructor( - options: Options, - modules: Array, - store: Store, - emitter: EventEmitter - ) { - this.options = options; - this.modules = modules; - this.store = store; - this.emitter = emitter; - this.wallets = []; - } - - private getSelectedWalletId() { - return storage.getItem(LOCAL_STORAGE_SELECTED_WALLET_ID); - } - - private setSelectedWalletId(walletId: string) { - storage.setItem(LOCAL_STORAGE_SELECTED_WALLET_ID, walletId); - } - - private removeSelectedWalletId() { - return storage.removeItem(LOCAL_STORAGE_SELECTED_WALLET_ID); - } - - private setupWalletModule({ wallet, ...metadata }: WalletModule) { - const emitter = new EventEmitter(); - const provider = new Provider(this.options.network.nodeUrl); - - emitter.on("connected", this.handleConnected(metadata.id)); - emitter.on("disconnected", this.handleDisconnected(metadata.id)); - emitter.on("accountsChanged", this.handleAccountsChanged(metadata.id)); - emitter.on("networkChanged", this.handleNetworkChanged(metadata.id)); - - return { - ...metadata, - ...wallet({ - options: this.options, - metadata, - provider, - emitter, - // TODO: Make a scoped logger. - logger, - // TODO: Make a scoped storage. - storage, - }), - } as Wallet; - } - - private async setupWalletModules() { - let selectedWalletId = this.getSelectedWalletId(); - let accounts: Array = []; - - const wallets = this.modules.map((module) => { - return this.setupWalletModule(module); - }); - - const selectedWallet = wallets.find((x) => x.id === selectedWalletId); - - if (selectedWallet) { - // Ensure our persistent state aligns with the selected wallet. - // For example a wallet is selected, but it returns no accounts (not connected). - accounts = await selectedWallet.connect().catch((err) => { - logger.log(`Failed to connect to ${selectedWallet.id} during setup`); - logger.error(err); - - return []; - }); - } - - if (!accounts.length) { - this.removeSelectedWalletId(); - selectedWalletId = null; - } - - this.wallets = wallets; - - this.store.dispatch({ - type: "SETUP_WALLET_MODULES", - payload: { wallets, selectedWalletId, accounts }, - }); - } - - private handleConnected = - (walletId: string) => - async ({ pending = false, accounts = [] }: WalletEvents["connected"]) => { - const existingWallet = this.getWallet(); - - if (existingWallet && existingWallet.id !== walletId) { - await existingWallet.disconnect().catch((err) => { - logger.log("Failed to disconnect existing wallet"); - logger.error(err); - - // At least clean up state on our side. - this.handleDisconnected(existingWallet.id)(); - }); - } - - if (pending || accounts.length) { - this.setSelectedWalletId(walletId); - } - - this.store.dispatch({ - type: "WALLET_CONNECTED", - payload: { walletId, pending, accounts }, - }); - }; - - private handleDisconnected = (walletId: string) => () => { - this.removeSelectedWalletId(); - - this.store.dispatch({ - type: "WALLET_DISCONNECTED", - payload: { walletId }, - }); - }; - - private handleAccountsChanged = - (walletId: string) => - ({ accounts }: WalletEvents["accountsChanged"]) => { - const { wallets } = this.store.getState(); - const selected = wallets.some((wallet) => { - return wallet.id === walletId && wallet.selected; - }); - - if (!selected) { - return; - } - - this.store.dispatch({ - type: "ACCOUNTS_CHANGED", - payload: { accounts }, - }); - }; - - private handleNetworkChanged = - (walletId: string) => - ({ networkId }: WalletEvents["networkChanged"]) => { - this.emitter.emit("networkChanged", { walletId, networkId }); - }; - - async init() { - await this.setupWalletModules(); - } - - getWallet(walletId?: string) { - const lookupWalletId = walletId || this.getSelectedWalletId(); - const wallet = this.wallets.find((x) => x.id === lookupWalletId) || null; - - return wallet as WalletVariation | null; - } -} - -export default WalletController; diff --git a/packages/core/src/lib/wallet-selector.ts b/packages/core/src/lib/wallet-selector.ts index ec53fd86f..5f1454323 100644 --- a/packages/core/src/lib/wallet-selector.ts +++ b/packages/core/src/lib/wallet-selector.ts @@ -1,4 +1,3 @@ -import WalletController from "./wallet-controller"; import { resolveOptions } from "./options"; import { createStore } from "./store"; import { @@ -7,26 +6,26 @@ import { WalletSelectorParams, } from "./wallet-selector.types"; import { WalletSelectorModal } from "./modal/modal.types"; -import { setupModal } from "./modal/modal"; -import { Wallet } from "./wallet"; import { EventEmitter, Logger } from "./services"; +import { Wallet } from "./wallet"; +import { setupWalletModules } from "./modules/wallet-modules"; +import { setupModal } from "./modal/modal"; export const setupWalletSelector = async ( params: WalletSelectorParams ): Promise => { const options = resolveOptions(params); + Logger.debug = options.debug; + const emitter = new EventEmitter(); const store = createStore(); - const controller = new WalletController( + + const walletModules = await setupWalletModules({ + factories: params.modules, options, - params.wallets, store, - emitter - ); - - Logger.debug = options.debug; - - await controller.init(); + emitter, + }); // TODO: Remove omit once modal is a separate package. const selector: Omit = { @@ -40,11 +39,14 @@ export const setupWalletSelector = async ( return Boolean(accounts.length); }, options, - wallet: (walletId?: string) => { - const wallet = controller.getWallet(walletId); + wallet: async (id?: string) => { + const { selectedWalletId } = store.getState(); + const wallet = await walletModules.getWallet( + id || selectedWalletId + ); if (!wallet) { - if (walletId) { + if (id) { throw new Error("Invalid wallet id"); } diff --git a/packages/core/src/lib/wallet-selector.types.ts b/packages/core/src/lib/wallet-selector.types.ts index de0531fe9..18db9c5aa 100644 --- a/packages/core/src/lib/wallet-selector.types.ts +++ b/packages/core/src/lib/wallet-selector.types.ts @@ -1,6 +1,6 @@ import { Observable } from "rxjs"; -import { WalletModule, Wallet } from "./wallet"; +import { Wallet, WalletModuleFactory } from "./wallet/wallet.types"; import { WalletSelectorState } from "./store.types"; import { Network, NetworkId, Options } from "./options.types"; import { ModalOptions, WalletSelectorModal } from "./modal/modal.types"; @@ -10,7 +10,7 @@ export interface WalletSelectorParams { network: NetworkId | Network; contractId: string; methodNames?: Array; - wallets: Array; + modules: Array; ui?: ModalOptions; debug?: boolean; } @@ -30,9 +30,9 @@ export interface WalletSelector extends WalletSelectorModal { options: Options; connected: boolean; - wallet( + wallet( walletId?: string - ): WalletVariation; + ): Promise; on( eventName: EventName, diff --git a/packages/core/src/lib/wallet/actions.ts b/packages/core/src/lib/wallet/actions.ts deleted file mode 100644 index 2f6a16d40..000000000 --- a/packages/core/src/lib/wallet/actions.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { transactions, utils } from "near-api-js"; -import { BN } from "bn.js"; - -export interface CreateAccountAction { - type: "CreateAccount"; -} - -export interface DeployContractAction { - type: "DeployContract"; - params: { - code: Uint8Array; - }; -} - -export interface FunctionCallAction { - type: "FunctionCall"; - params: { - methodName: string; - args: object; - gas: string; - deposit: string; - }; -} - -export interface TransferAction { - type: "Transfer"; - params: { - deposit: string; - }; -} - -export interface StakeAction { - type: "Stake"; - params: { - stake: string; - publicKey: string; - }; -} - -export type AddKeyPermission = - | "FullAccess" - | { - receiverId: string; - allowance?: string; - methodNames?: Array; - }; - -export interface AddKeyAction { - type: "AddKey"; - params: { - publicKey: string; - accessKey: { - nonce?: number; - permission: AddKeyPermission; - }; - }; -} - -export interface DeleteKeyAction { - type: "DeleteKey"; - params: { - publicKey: string; - }; -} - -export interface DeleteAccountAction { - type: "DeleteAccount"; - params: { - beneficiaryId: string; - }; -} - -export type Action = - | CreateAccountAction - | DeployContractAction - | FunctionCallAction - | TransferAction - | StakeAction - | AddKeyAction - | DeleteKeyAction - | DeleteAccountAction; - -export type ActionType = Action["type"]; - -const getAccessKey = (permission: AddKeyPermission) => { - if (permission === "FullAccess") { - return transactions.fullAccessKey(); - } - - const { receiverId, methodNames = [] } = permission; - const allowance = permission.allowance - ? new BN(permission.allowance) - : undefined; - - return transactions.functionCallAccessKey(receiverId, methodNames, allowance); -}; - -export const transformActions = (actions: Array) => { - return actions.map((action) => { - switch (action.type) { - case "CreateAccount": - return transactions.createAccount(); - case "DeployContract": { - const { code } = action.params; - - return transactions.deployContract(code); - } - case "FunctionCall": { - const { methodName, args, gas, deposit } = action.params; - - return transactions.functionCall( - methodName, - args, - new BN(gas), - new BN(deposit) - ); - } - case "Transfer": { - const { deposit } = action.params; - - return transactions.transfer(new BN(deposit)); - } - case "Stake": { - const { stake, publicKey } = action.params; - - return transactions.stake( - new BN(stake), - utils.PublicKey.from(publicKey) - ); - } - case "AddKey": { - const { publicKey, accessKey } = action.params; - - return transactions.addKey( - utils.PublicKey.from(publicKey), - // TODO: Use accessKey.nonce? near-api-js seems to think 0 is fine? - getAccessKey(accessKey.permission) - ); - } - case "DeleteKey": { - const { publicKey } = action.params; - - return transactions.deleteKey(utils.PublicKey.from(publicKey)); - } - case "DeleteAccount": { - const { beneficiaryId } = action.params; - - return transactions.deleteAccount(beneficiaryId); - } - default: - throw new Error("Invalid action type"); - } - }); -}; diff --git a/packages/core/src/lib/wallet/index.ts b/packages/core/src/lib/wallet/index.ts index fb3a78b91..0e296eda9 100644 --- a/packages/core/src/lib/wallet/index.ts +++ b/packages/core/src/lib/wallet/index.ts @@ -1,3 +1,3 @@ -export * from "./wallet"; +export * from "./wallet.types"; +export * from "./transactions.types"; export * from "./transactions"; -export * from "./actions"; diff --git a/packages/core/src/lib/wallet/actions.spec.ts b/packages/core/src/lib/wallet/transactions.spec.ts similarity index 97% rename from packages/core/src/lib/wallet/actions.spec.ts rename to packages/core/src/lib/wallet/transactions.spec.ts index 54722d2aa..abe9f4808 100644 --- a/packages/core/src/lib/wallet/actions.spec.ts +++ b/packages/core/src/lib/wallet/transactions.spec.ts @@ -1,8 +1,8 @@ -import { transformActions } from "./actions"; +import { transformActions } from "./transactions"; import { transactions, utils } from "near-api-js"; import { BN } from "bn.js"; -describe("actions", () => { +describe("transformActions", () => { it("correctly transforms 'CreateAccount' action", () => { const actions = transformActions([{ type: "CreateAccount" }]); diff --git a/packages/core/src/lib/wallet/transactions.ts b/packages/core/src/lib/wallet/transactions.ts index 759faafaa..9af5625f1 100644 --- a/packages/core/src/lib/wallet/transactions.ts +++ b/packages/core/src/lib/wallet/transactions.ts @@ -1,7 +1,75 @@ -import { Action } from "./actions"; +import { transactions, utils } from "near-api-js"; +import { BN } from "bn.js"; -export interface Transaction { - signerId: string; - receiverId: string; - actions: Array; -} +import { Action, AddKeyPermission } from "./transactions.types"; + +const getAccessKey = (permission: AddKeyPermission) => { + if (permission === "FullAccess") { + return transactions.fullAccessKey(); + } + + const { receiverId, methodNames = [] } = permission; + const allowance = permission.allowance + ? new BN(permission.allowance) + : undefined; + + return transactions.functionCallAccessKey(receiverId, methodNames, allowance); +}; + +export const transformActions = (actions: Array) => { + return actions.map((action) => { + switch (action.type) { + case "CreateAccount": + return transactions.createAccount(); + case "DeployContract": { + const { code } = action.params; + + return transactions.deployContract(code); + } + case "FunctionCall": { + const { methodName, args, gas, deposit } = action.params; + + return transactions.functionCall( + methodName, + args, + new BN(gas), + new BN(deposit) + ); + } + case "Transfer": { + const { deposit } = action.params; + + return transactions.transfer(new BN(deposit)); + } + case "Stake": { + const { stake, publicKey } = action.params; + + return transactions.stake( + new BN(stake), + utils.PublicKey.from(publicKey) + ); + } + case "AddKey": { + const { publicKey, accessKey } = action.params; + + return transactions.addKey( + utils.PublicKey.from(publicKey), + // TODO: Use accessKey.nonce? near-api-js seems to think 0 is fine? + getAccessKey(accessKey.permission) + ); + } + case "DeleteKey": { + const { publicKey } = action.params; + + return transactions.deleteKey(utils.PublicKey.from(publicKey)); + } + case "DeleteAccount": { + const { beneficiaryId } = action.params; + + return transactions.deleteAccount(beneficiaryId); + } + default: + throw new Error("Invalid action type"); + } + }); +}; diff --git a/packages/core/src/lib/wallet/transactions.types.ts b/packages/core/src/lib/wallet/transactions.types.ts new file mode 100644 index 000000000..c62d651e3 --- /dev/null +++ b/packages/core/src/lib/wallet/transactions.types.ts @@ -0,0 +1,86 @@ +export interface CreateAccountAction { + type: "CreateAccount"; +} + +export interface DeployContractAction { + type: "DeployContract"; + params: { + code: Uint8Array; + }; +} + +export interface FunctionCallAction { + type: "FunctionCall"; + params: { + methodName: string; + args: object; + gas: string; + deposit: string; + }; +} + +export interface TransferAction { + type: "Transfer"; + params: { + deposit: string; + }; +} + +export interface StakeAction { + type: "Stake"; + params: { + stake: string; + publicKey: string; + }; +} + +export type AddKeyPermission = + | "FullAccess" + | { + receiverId: string; + allowance?: string; + methodNames?: Array; + }; + +export interface AddKeyAction { + type: "AddKey"; + params: { + publicKey: string; + accessKey: { + nonce?: number; + permission: AddKeyPermission; + }; + }; +} + +export interface DeleteKeyAction { + type: "DeleteKey"; + params: { + publicKey: string; + }; +} + +export interface DeleteAccountAction { + type: "DeleteAccount"; + params: { + beneficiaryId: string; + }; +} + +export type Action = + | CreateAccountAction + | DeployContractAction + | FunctionCallAction + | TransferAction + | StakeAction + | AddKeyAction + | DeleteKeyAction + | DeleteAccountAction; + +export type ActionType = Action["type"]; + +export interface Transaction { + signerId: string; + receiverId: string; + actions: Array; +} diff --git a/packages/core/src/lib/wallet/wallet.ts b/packages/core/src/lib/wallet/wallet.ts deleted file mode 100644 index cedf12ded..000000000 --- a/packages/core/src/lib/wallet/wallet.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { providers } from "near-api-js"; - -import { - EventEmitterService, - ProviderService, - LoggerService, - StorageService, -} from "../services"; -import { Transaction } from "./transactions"; -import { Action } from "./actions"; -import { Options } from "../options.types"; -import { Optional } from "../utils.types"; -import { AccountState } from "../store.types"; - -export interface HardwareWalletConnectParams { - derivationPath: string; -} - -export interface SignAndSendTransactionParams { - signerId?: string; - receiverId?: string; - actions: Array; -} - -export interface SignAndSendTransactionsParams { - transactions: Array>; -} - -export type WalletEvents = { - connected: { pending?: boolean; accounts?: Array }; - disconnected: null; - accountsChanged: { accounts: Array }; - networkChanged: { networkId: string }; -}; - -export interface WalletMetadata { - id: string; - name: string; - description: string | null; - iconUrl: string; - type: Type; -} - -interface BaseWallet< - Type extends string, - ExecutionOutcome = providers.FinalExecutionOutcome -> extends WalletMetadata { - // Determines if the wallet is available for selection. - // TODO: Make this async to support checking if an injected wallet is installed. - isAvailable(): Promise; - - // Requests sign in for the given wallet. - // Note: Hardware wallets should defer HID connection until user input is required (e.g. public key or signing). - connect(params?: object): Promise>; - - // Removes connection to the wallet and triggers a cleanup of subscriptions etc. - disconnect(): Promise; - - // Retrieves all active accounts. - getAccounts(): Promise>; - - // Signs a list of actions before sending them via an RPC endpoint. - signAndSendTransaction( - params: SignAndSendTransactionParams - ): Promise; - - // Sings a list of transactions before sending them via an RPC endpoint. - signAndSendTransactions( - params: SignAndSendTransactionsParams - ): Promise>; -} - -export type BrowserWallet = BaseWallet<"browser", void>; - -export interface InjectedWallet extends BaseWallet<"injected"> { - getDownloadUrl(): string; -} - -export interface HardwareWallet extends BaseWallet<"hardware"> { - connect(params?: HardwareWalletConnectParams): Promise>; -} - -export type BridgeWallet = BaseWallet<"bridge", void>; - -export type Wallet = - | BrowserWallet - | InjectedWallet - | HardwareWallet - | BridgeWallet; - -export type WalletType = Wallet["type"]; - -export interface WalletOptions { - options: Options; - metadata: WalletMetadata; - provider: ProviderService; - emitter: EventEmitterService; - logger: LoggerService; - storage: StorageService; -} - -export type WalletBehaviour = Omit< - WalletVariation, - keyof WalletMetadata ->; - -export type WalletModule = - WalletMetadata & { - wallet( - options: WalletOptions - ): WalletBehaviour; - }; - -export type WalletBehaviourFactory< - WalletVariation extends Wallet, - ExtraWalletOptions extends object = object -> = ( - options: WalletOptions & ExtraWalletOptions -) => WalletBehaviour; diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts new file mode 100644 index 000000000..dca77f7a9 --- /dev/null +++ b/packages/core/src/lib/wallet/wallet.types.ts @@ -0,0 +1,182 @@ +import { providers } from "near-api-js"; +import { AccountState } from "../store.types"; +import { Options } from "../options.types"; +import { + EventEmitterService, + LoggerService, + ProviderService, + StorageService, +} from "../services"; +import { Action } from "./transactions.types"; +import { Optional } from "../utils.types"; +import { Transaction } from "./transactions.types"; + +interface BaseWalletMetadata { + name: string; + description: string | null; + iconUrl: string; +} + +type BaseWallet< + Type extends string, + Metadata extends BaseWalletMetadata, + Behaviour +> = { + id: string; + type: Type; + metadata: Metadata; +} & Behaviour; + +export interface SignAndSendTransactionParams { + signerId?: string; + receiverId?: string; + actions: Array; +} + +export interface SignAndSendTransactionsParams { + transactions: Array>; +} + +export type WalletEvents = { + connected: { accounts: Array }; + disconnected: null; + accountsChanged: { accounts: Array }; + networkChanged: { networkId: string }; +}; + +// ----- Browser Wallet ----- // + +export type BrowserWalletMetadata = BaseWalletMetadata; + +export interface BrowserWalletBehaviour { + connect(): Promise>; + disconnect(): Promise; + getAccounts(): Promise>; + signAndSendTransaction(params: SignAndSendTransactionParams): Promise; + signAndSendTransactions(params: SignAndSendTransactionsParams): Promise; +} + +export type BrowserWallet = BaseWallet< + "browser", + BrowserWalletMetadata, + BrowserWalletBehaviour +>; + +// ----- Injected Wallet ----- // + +export type InjectedWalletMetadata = BaseWalletMetadata & { + downloadUrl: string; +}; + +export interface InjectedWalletBehaviour { + connect(): Promise>; + disconnect(): Promise; + getAccounts(): Promise>; + signAndSendTransaction( + params: SignAndSendTransactionParams + ): Promise; + signAndSendTransactions( + params: SignAndSendTransactionsParams + ): Promise>; +} + +export type InjectedWallet = BaseWallet< + "injected", + InjectedWalletMetadata, + InjectedWalletBehaviour +>; + +// ----- Hardware Wallet ----- // + +export type HardwareWalletMetadata = BaseWalletMetadata; + +export interface HardwareWalletConnectParams { + derivationPath: string; +} + +export interface HardwareWalletBehaviour { + connect(params: HardwareWalletConnectParams): Promise>; + disconnect(): Promise; + getAccounts(): Promise>; + signAndSendTransaction( + params: SignAndSendTransactionParams + ): Promise; + signAndSendTransactions( + params: SignAndSendTransactionsParams + ): Promise>; +} + +export type HardwareWallet = BaseWallet< + "hardware", + HardwareWalletMetadata, + HardwareWalletBehaviour +>; + +// ----- Bridge Wallet ----- // + +export type BridgeWalletMetadata = BaseWalletMetadata; + +export interface BridgeWalletBehaviour { + connect(): Promise>; + disconnect(): Promise; + getAccounts(): Promise>; + signAndSendTransaction( + params: SignAndSendTransactionParams + ): Promise; + signAndSendTransactions( + params: SignAndSendTransactionsParams + ): Promise>; +} + +export type BridgeWallet = BaseWallet< + "bridge", + BridgeWalletMetadata, + BridgeWalletBehaviour +>; + +// ----- Misc ----- // + +export type WalletMetadata = + | BrowserWalletMetadata + | InjectedWalletMetadata + | HardwareWalletMetadata + | BridgeWalletMetadata; + +export type Wallet = + | BrowserWallet + | InjectedWallet + | HardwareWallet + | BridgeWallet; + +export type WalletType = Wallet["type"]; + +export interface WalletBehaviourOptions { + id: Variation["id"]; + type: Variation["type"]; + metadata: Variation["metadata"]; + options: Options; + provider: ProviderService; + emitter: EventEmitterService; + logger: LoggerService; + storage: StorageService; +} + +// Note: TypeScript doesn't seem to like reusing this in WalletModule. +export type WalletBehaviourFactory< + Variation extends Wallet, + ExtraOptions extends object = object +> = ( + options: WalletBehaviourOptions & ExtraOptions +) => Promise>; + +export type WalletModule = { + id: Variation["id"]; + type: Variation["type"]; + metadata: Variation["metadata"]; + init( + options: WalletBehaviourOptions + ): Promise>; +}; + +export type WalletModuleFactory = + () => Promise | null>; diff --git a/packages/ledger/src/lib/ledger-client.spec.ts b/packages/ledger/src/lib/ledger-client.spec.ts index 7ef8c7cb5..d3fe68656 100644 --- a/packages/ledger/src/lib/ledger-client.spec.ts +++ b/packages/ledger/src/lib/ledger-client.spec.ts @@ -101,6 +101,7 @@ describe("getVersion", () => { }); await client.connect(); const result = await client.getVersion(); + expect(transport.send).toHaveBeenCalledWith( constants.CLA, constants.INS_GET_APP_VERSION, @@ -134,7 +135,6 @@ describe("getPublicKey", () => { constants.networkId, parseDerivationPath(derivationPath) ); - expect(result).toEqual("GF7tLvSzcxX4EtrMFtGvGTb2yUj2DhL8hWzc97BwUkyC"); }); }); @@ -155,6 +155,7 @@ describe("sign", () => { data, derivationPath: "44'/397'/0'/0'/1'", }); + expect(transport.send).toHaveBeenCalledWith( constants.CLA, constants.INS_GET_APP_VERSION, @@ -189,8 +190,8 @@ describe("on", () => { await client.connect(); await client.on(event, listener); + expect(transport.on).toHaveBeenCalledWith(event, listener); - expect(transport.on).toHaveBeenCalledTimes(1); }); }); @@ -203,8 +204,8 @@ describe("off", () => { await client.connect(); await client.off(event, listener); + expect(transport.off).toHaveBeenCalledWith(event, listener); - expect(transport.off).toHaveBeenCalledTimes(1); }); }); @@ -220,7 +221,7 @@ describe("setScrambleKey", () => { await client.connect(); await client.setScrambleKey(scrambleKey); + expect(transport.setScrambleKey).toHaveBeenCalledWith(scrambleKey); - expect(transport.setScrambleKey).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/ledger/src/lib/ledger-client.ts b/packages/ledger/src/lib/ledger-client.ts index 31ef3f749..eba521c34 100644 --- a/packages/ledger/src/lib/ledger-client.ts +++ b/packages/ledger/src/lib/ledger-client.ts @@ -1,6 +1,5 @@ import TransportWebHID from "@ledgerhq/hw-transport-webhid"; import Transport from "@ledgerhq/hw-transport"; -import { listen, Log } from "@ledgerhq/logs"; import { utils } from "near-api-js"; // Further reading regarding APDU Ledger API: @@ -65,7 +64,7 @@ export const isLedgerSupported = () => { }; export class LedgerClient { - private transport: Transport; + private transport: Transport | null = null; isConnected = () => { return Boolean(this.transport); @@ -73,21 +72,29 @@ export class LedgerClient { connect = async () => { this.transport = await TransportWebHID.create(); - }; - disconnect = () => { - return this.transport.close(); + const handleDisconnect = () => { + this.transport?.off("disconnect", handleDisconnect); + this.transport = null; + }; + + this.transport.on("disconnect", handleDisconnect); }; - listen = (callback: (data: Log) => void) => { - const unsubscribe = listen(callback); + disconnect = async () => { + if (!this.transport) { + throw new Error("Device not connected"); + } - return { - remove: () => unsubscribe(), - }; + await this.transport.close(); + this.transport = null; }; setScrambleKey = (key: string) => { + if (!this.transport) { + throw new Error("Device not connected"); + } + this.transport.setScrambleKey(key); }; @@ -95,18 +102,30 @@ export class LedgerClient { event: Event, callback: (data: EventMap[Event]) => void ): Subscription => { + if (!this.transport) { + throw new Error("Device not connected"); + } + this.transport.on(event, callback); return { - remove: () => this.transport.off(event, callback), + remove: () => this.transport?.off(event, callback), }; }; off = (event: keyof EventMap, callback: () => void) => { + if (!this.transport) { + throw new Error("Device not connected"); + } + this.transport.off(event, callback); }; getVersion = async () => { + if (!this.transport) { + throw new Error("Device not connected"); + } + const res = await this.transport.send( CLA, INS_GET_APP_VERSION, @@ -120,6 +139,10 @@ export class LedgerClient { }; getPublicKey = async ({ derivationPath }: GetPublicKeyParams) => { + if (!this.transport) { + throw new Error("Device not connected"); + } + const res = await this.transport.send( CLA, INS_GET_PUBLIC_KEY, @@ -132,6 +155,10 @@ export class LedgerClient { }; sign = async ({ data, derivationPath }: SignParams) => { + if (!this.transport) { + throw new Error("Device not connected"); + } + // NOTE: getVersion call resets state to avoid starting from partially filled buffer await this.getVersion(); diff --git a/packages/ledger/src/lib/ledger.spec.ts b/packages/ledger/src/lib/ledger.spec.ts index 33b4b83a3..c1d05ce5d 100644 --- a/packages/ledger/src/lib/ledger.spec.ts +++ b/packages/ledger/src/lib/ledger.spec.ts @@ -12,7 +12,7 @@ import { } from "../../../core/src/lib/services"; import { LedgerClient } from "./ledger-client"; -const createLedgerWallet = (deps: MockWalletDependencies = {}) => { +const createLedgerWallet = async (deps: MockWalletDependencies = {}) => { const storageState: Record = {}; const publicKey = "GF7tLvSzcxX4EtrMFtGvGTb2yUj2DhL8hWzc97BwUkyC"; @@ -27,7 +27,7 @@ const createLedgerWallet = (deps: MockWalletDependencies = {}) => { storageState[key] = value; }), }); - const ledgerWallet = mockWallet(setupLedger(), { + const ledgerWallet = await mockWallet(setupLedger(), { storage, provider, ...deps, @@ -66,7 +66,7 @@ const createLedgerWallet = (deps: MockWalletDependencies = {}) => { return { nearApiJs: require("near-api-js"), - wallet: ledgerWallet, + wallet: ledgerWallet!, storage: deps.storage || storage, ledgerClient, publicKey, @@ -77,19 +77,13 @@ afterEach(() => { jest.resetModules(); }); -describe("isAvailable", () => { - it("returns true", async () => { - const { wallet } = createLedgerWallet(); - expect(await wallet.isAvailable()).toEqual(true); - }); -}); - describe("connect", () => { // TODO: Need to mock fetching for account id. it.skip("signs in", async () => { const accountId = "accountId"; const derivationPath = "derivationPath"; - const { wallet, ledgerClient, storage, publicKey } = createLedgerWallet(); + const { wallet, ledgerClient, storage, publicKey } = + await createLedgerWallet(); await wallet.connect({ derivationPath }); expect(storage.setItem).toHaveBeenCalledWith("ledger:authData", { accountId, @@ -106,7 +100,7 @@ describe("getAccounts", () => { // TODO: Need to mock fetching for account id. it.skip("returns account objects", async () => { const accountId = "accountId"; - const { wallet } = createLedgerWallet(); + const { wallet } = await createLedgerWallet(); await wallet.connect({ derivationPath: "derivationPath", }); @@ -117,7 +111,7 @@ describe("getAccounts", () => { // describe("signAndSendTransaction", () => { // it("signs and sends transaction", async () => { -// const { wallet, authData } = createLedgerWallet(); +// const { wallet, authData } = await createLedgerWallet(); // await wallet.connect({ // accountId: authData.accountId, // derivationPath: authData.derivationPath, diff --git a/packages/ledger/src/lib/ledger.ts b/packages/ledger/src/lib/ledger.ts index e07ac9f6f..eb846b494 100644 --- a/packages/ledger/src/lib/ledger.ts +++ b/packages/ledger/src/lib/ledger.ts @@ -2,8 +2,9 @@ import { transactions as nearTransactions, utils } from "near-api-js"; import { TypedError } from "near-api-js/lib/utils/errors"; import isMobile from "is-mobile"; import { - WalletModule, + WalletModuleFactory, WalletBehaviourFactory, + WalletBehaviourOptions, AccountState, HardwareWallet, transformActions, @@ -29,7 +30,9 @@ interface GetAccountIdFromPublicKeyParams { } interface LedgerState { + client: LedgerClient; authData: AuthData | null; + subscriptions: Array; } export interface LedgerParams { @@ -38,19 +41,24 @@ export interface LedgerParams { export const LOCAL_STORAGE_AUTH_DATA = `ledger:authData`; -const Ledger: WalletBehaviourFactory = ({ +const setupLedgerState = ( + storage: WalletBehaviourOptions["storage"] +): LedgerState => { + return { + client: new LedgerClient(), + subscriptions: [], + authData: storage.getItem(LOCAL_STORAGE_AUTH_DATA), + }; +}; + +const Ledger: WalletBehaviourFactory = async ({ options, metadata, provider, - emitter, logger, storage, }) => { - let _wallet: LedgerClient | null; - let _subscriptions: Record = {}; - const _state: LedgerState = { authData: null }; - - const debugMode = false; + const _state = setupLedgerState(storage); const getAccounts = ( authData: AuthData | null = _state.authData @@ -65,66 +73,31 @@ const Ledger: WalletBehaviourFactory = ({ }; const cleanup = () => { - for (const key in _subscriptions) { - _subscriptions[key].remove(); - } + _state.subscriptions.forEach((subscription) => subscription.remove()); - _subscriptions = {}; + _state.subscriptions = []; _state.authData = null; + storage.removeItem(LOCAL_STORAGE_AUTH_DATA); - _wallet = null; }; const disconnect = async () => { - const connected = Boolean(_state.authData); - - if (_wallet && _wallet.isConnected()) { - await _wallet.disconnect().catch((err) => { + if (_state.client.isConnected()) { + await _state.client.disconnect().catch((err) => { logger.log("Failed to disconnect"); logger.error(err); }); } cleanup(); - - if (connected) { - emitter.emit("disconnected", null); - } - }; - - const setupWallet = async (): Promise => { - if (_wallet) { - return _wallet; - } - - const ledgerClient = new LedgerClient(); - - await ledgerClient.connect(); - ledgerClient.setScrambleKey("NEAR"); - - _subscriptions["disconnect"] = ledgerClient.on("disconnect", (err) => { - logger.error(err); - - disconnect(); - }); - - if (debugMode) { - _subscriptions["logs"] = ledgerClient.listen((data) => { - logger.log("Ledger:init:logs", data); - }); - } - - _wallet = ledgerClient; - - return ledgerClient; }; - const getWallet = (): Promise => { - if (!_state.authData) { - throw new Error(`${metadata.name} not connected`); + const connectLedgerDevice = async () => { + if (_state.client.isConnected()) { + return; } - return setupWallet(); + await _state.client.connect(); }; const validateAccessKey = ({ @@ -133,9 +106,8 @@ const Ledger: WalletBehaviourFactory = ({ }: ValidateAccessKeyParams) => { logger.log("Ledger:validateAccessKey", { accountId, publicKey }); - return provider - .viewAccessKey({ accountId, publicKey }) - .then((accessKey) => { + return provider.viewAccessKey({ accountId, publicKey }).then( + (accessKey) => { logger.log("Ledger:validateAccessKey:accessKey", { accessKey }); if (accessKey.permission !== "FullAccess") { @@ -143,14 +115,15 @@ const Ledger: WalletBehaviourFactory = ({ } return accessKey; - }) - .catch((err) => { + }, + (err) => { if (err instanceof TypedError && err.type === "AccessKeyDoesNotExist") { return null; } throw err; - }); + } + ); }; const getAccountIdFromPublicKey = async ({ @@ -173,29 +146,6 @@ const Ledger: WalletBehaviourFactory = ({ return accountIds[0]; }; - const signTransaction = async ( - transaction: nearTransactions.Transaction, - derivationPath: string - ) => { - const wallet = await getWallet(); - const serializedTx = utils.serialize.serialize( - nearTransactions.SCHEMA, - transaction - ); - const signature = await wallet.sign({ - data: serializedTx, - derivationPath, - }); - - return new nearTransactions.SignedTransaction({ - transaction, - signature: new nearTransactions.Signature({ - keyType: transaction.publicKey.keyType, - data: signature, - }), - }); - }; - const signTransactions = async ( transactions: Array> ) => { @@ -224,45 +174,46 @@ const Ledger: WalletBehaviourFactory = ({ utils.serialize.base_decode(block.header.hash) ); - const signedTx = await signTransaction(transaction, derivationPath); + const serializedTx = utils.serialize.serialize( + nearTransactions.SCHEMA, + transaction + ); + + const signature = await _state.client.sign({ + data: serializedTx, + derivationPath, + }); + + const signedTx = new nearTransactions.SignedTransaction({ + transaction, + signature: new nearTransactions.Signature({ + keyType: transaction.publicKey.keyType, + data: signature, + }), + }); + signedTransactions.push(signedTx); } + return signedTransactions; }; return { - async isAvailable() { - return !isMobile() && isLedgerSupported(); - }, - - async connect(params) { - if (!_state.authData) { - // Only load previous state to avoid prompting connection via USB. - // Connection must be triggered by user interaction. - const authData = storage.getItem(LOCAL_STORAGE_AUTH_DATA); - const existingAccounts = getAccounts(authData); - - _state.authData = authData; - - if (!params) { - return existingAccounts; - } - } - + async connect({ derivationPath }) { const existingAccounts = getAccounts(); if (existingAccounts.length) { return existingAccounts; } - const { derivationPath } = params || {}; - if (!derivationPath) { throw new Error("Invalid derivation path"); } - const wallet = await setupWallet(); - const publicKey = await wallet.getPublicKey({ derivationPath }); + // Note: Connection must be triggered by user interaction. + await connectLedgerDevice(); + + const publicKey = await _state.client.getPublicKey({ derivationPath }); const accountId = await getAccountIdFromPublicKey({ publicKey }); return validateAccessKey({ accountId, publicKey }) @@ -282,10 +233,7 @@ const Ledger: WalletBehaviourFactory = ({ storage.setItem(LOCAL_STORAGE_AUTH_DATA, authData); _state.authData = authData; - const newAccounts = getAccounts(); - emitter.emit("connected", { accounts: newAccounts }); - - return newAccounts; + return getAccounts(); }) .catch(async (err) => { await disconnect(); @@ -312,34 +260,32 @@ const Ledger: WalletBehaviourFactory = ({ }); if (!_state.authData) { - throw new Error(`${metadata.name} not connected`); + throw new Error("Wallet not connected"); } - const { accountId, derivationPath, publicKey } = _state.authData; + // Note: Connection must be triggered by user interaction. + await connectLedgerDevice(); - const [block, accessKey] = await Promise.all([ - provider.block({ finality: "final" }), - provider.viewAccessKey({ accountId, publicKey }), + const [signedTx] = await signTransactions([ + { + receiverId, + actions, + }, ]); - logger.log("Ledger:signAndSendTransaction:block", block); - logger.log("Ledger:signAndSendTransaction:accessKey", accessKey); - - const transaction = nearTransactions.createTransaction( - accountId, - utils.PublicKey.from(publicKey), - receiverId, - accessKey.nonce + 1, - transformActions(actions), - utils.serialize.base_decode(block.header.hash) - ); - - const signedTx = await signTransaction(transaction, derivationPath); - return provider.sendTransaction(signedTx); }, async signAndSendTransactions({ transactions }) { + logger.log("Ledger:signAndSendTransactions", { transactions }); + + if (!_state.authData) { + throw new Error("Wallet not connected"); + } + + // Note: Connection must be triggered by user interaction. + await connectLedgerDevice(); + const signedTransactions = await signTransactions(transactions); return Promise.all( @@ -351,13 +297,24 @@ const Ledger: WalletBehaviourFactory = ({ export function setupLedger({ iconUrl = "./assets/ledger-icon.png", -}: LedgerParams = {}): WalletModule { - return { - id: "ledger", - type: "hardware", - name: "Ledger", - description: null, - iconUrl, - wallet: Ledger, +}: LedgerParams = {}): WalletModuleFactory { + return async () => { + const mobile = isMobile(); + const supported = isLedgerSupported(); + + if (mobile || !supported) { + return null; + } + + return { + id: "ledger", + type: "hardware", + metadata: { + name: "Ledger", + description: null, + iconUrl, + }, + init: Ledger, + }; }; } diff --git a/packages/math-wallet/src/lib/math-wallet.ts b/packages/math-wallet/src/lib/math-wallet.ts index 8d029a7f5..60682aae1 100644 --- a/packages/math-wallet/src/lib/math-wallet.ts +++ b/packages/math-wallet/src/lib/math-wallet.ts @@ -1,13 +1,12 @@ import { transactions as nearTransactions, utils } from "near-api-js"; import isMobile from "is-mobile"; import { - WalletModule, + WalletModuleFactory, WalletBehaviourFactory, - AccountState, InjectedWallet, + AccountState, transformActions, waitFor, - errors, } from "@near-wallet-selector/core"; import { InjectedMathWallet } from "./injected-math-wallet"; @@ -22,128 +21,80 @@ export interface MathWalletParams { iconUrl?: string; } -const MathWallet: WalletBehaviourFactory = ({ - options, - metadata, - provider, - emitter, - logger, -}) => { - let _wallet: InjectedMathWallet | null = null; +interface MathWalletState { + wallet: InjectedMathWallet; +} - const isInstalled = async () => { - try { - return await waitFor(() => !!window.nearWalletApi); - } catch (e) { - logger.log("MathWallet:isInstalled:error", e); +const isInstalled = async () => { + try { + return waitFor(() => !!window.nearWalletApi); + } catch (err) { + return false; + } +}; - return false; - } - }; +const setupMathWalletState = async ( + contractId: string +): Promise => { + const wallet = window.nearWalletApi!; + + // This wallet currently has weird behaviour regarding signer.account. + // - When you initially sign in, you get a SignedInAccount interface. + // - When the extension loads after this, you get a PreviouslySignedInAccount interface. + // This method normalises the behaviour to only return the SignedInAccount interface. + if (wallet.signer.account && "address" in wallet.signer.account) { + await wallet.login({ contractId }); + } - const cleanup = () => { - _wallet = null; + return { + wallet, }; +}; - const getAccounts = (): Array => { - if (!_wallet?.signer.account) { - return []; - } - - const accountId = - "accountId" in _wallet.signer.account - ? _wallet.signer.account.accountId - : _wallet.signer.account.name; - - return [{ accountId }]; - }; +const MathWallet: WalletBehaviourFactory = async ({ + options, + provider, + logger, +}) => { + const _state = await setupMathWalletState(options.contractId); const getSignedInAccount = () => { - if (_wallet?.signer.account && "accountId" in _wallet.signer.account) { - return _wallet.signer.account; + if ( + _state.wallet.signer.account && + "accountId" in _state.wallet.signer.account + ) { + return _state.wallet.signer.account; } return null; }; - const setupWallet = async (): Promise => { - if (_wallet) { - return _wallet; - } - - const installed = await isInstalled(); - - if (!installed) { - throw errors.createWalletNotInstalledError(metadata); - } - - const wallet = window.nearWalletApi!; - - // This wallet currently has weird behaviour regarding signer.account. - // - When you initially sign in, you get a SignedInAccount interface. - // - When the extension loads after this, you get a PreviouslySignedInAccount interface. - // This method normalises the behaviour to only return the SignedInAccount interface. - if (wallet.signer.account && "address" in wallet.signer.account) { - await wallet.login({ contractId: options.contractId }); - } - - _wallet = wallet; - - return wallet; - }; + const getAccounts = (): Array => { + const account = getSignedInAccount(); - const getWallet = (): InjectedMathWallet => { - if (!_wallet) { - throw new Error(`${metadata.name} not connected`); + if (!account) { + return []; } - return _wallet; + return [{ accountId: account.accountId }]; }; return { - getDownloadUrl() { - return "https://chrome.google.com/webstore/detail/math-wallet/afbcbjpbpfadlkmhmclhkeeodmamcflc"; - }, - - async isAvailable() { - return !isMobile(); - }, - async connect() { - const wallet = await setupWallet(); const existingAccounts = getAccounts(); if (existingAccounts.length) { return existingAccounts; } - await wallet.login({ contractId: options.contractId }).catch((err) => { - this.disconnect(); - - throw err; - }); - - const newAccounts = getAccounts(); - emitter.emit("connected", { accounts: newAccounts }); + await _state.wallet.login({ contractId: options.contractId }); - return newAccounts; + return getAccounts(); }, - // Must only trigger "disconnected" if we were connected. async disconnect() { - if (!_wallet) { - return; - } - - if (!_wallet.signer.account) { - return cleanup(); - } - // Ignore if unsuccessful (returns false). - await _wallet.logout(); - cleanup(); - - emitter.emit("disconnected", null); + await _state.wallet.logout(); }, async getAccounts() { @@ -161,8 +112,13 @@ const MathWallet: WalletBehaviourFactory = ({ actions, }); - const wallet = getWallet(); - const { accountId, publicKey } = getSignedInAccount()!; + const account = getSignedInAccount(); + + if (!account) { + throw new Error("Wallet not connected"); + } + + const { accountId, publicKey } = account; const [block, accessKey] = await Promise.all([ provider.block({ finality: "final" }), provider.viewAccessKey({ accountId, publicKey }), @@ -182,7 +138,7 @@ const MathWallet: WalletBehaviourFactory = ({ const [hash, signedTx] = await nearTransactions.signTransaction( transaction, - wallet.signer, + _state.wallet.signer, accountId ); @@ -194,8 +150,13 @@ const MathWallet: WalletBehaviourFactory = ({ async signAndSendTransactions({ transactions }) { logger.log("MathWallet:signAndSendTransactions", { transactions }); - const wallet = getWallet(); - const { accountId, publicKey } = getSignedInAccount()!; + const account = getSignedInAccount(); + + if (!account) { + throw new Error("Wallet not connected"); + } + + const { accountId, publicKey } = account; const [block, accessKey] = await Promise.all([ provider.block({ finality: "final" }), provider.viewAccessKey({ accountId, publicKey }), @@ -219,7 +180,7 @@ const MathWallet: WalletBehaviourFactory = ({ const [hash, signedTx] = await nearTransactions.signTransaction( transaction, - wallet.signer, + _state.wallet.signer, accountId ); @@ -240,15 +201,28 @@ const MathWallet: WalletBehaviourFactory = ({ }; }; -export function setupMathWallet({ +export const setupMathWallet = ({ iconUrl = "./assets/math-wallet-icon.png", -}: MathWalletParams = {}): WalletModule { - return { - id: "math-wallet", - type: "injected", - name: "Math Wallet", - description: null, - iconUrl, - wallet: MathWallet, +}: MathWalletParams = {}): WalletModuleFactory => { + return async () => { + const mobile = isMobile(); + const installed = await isInstalled(); + + if (mobile || !installed) { + return null; + } + + return { + id: "math-wallet", + type: "injected", + metadata: { + name: "Math Wallet", + description: null, + iconUrl, + downloadUrl: + "https://chrome.google.com/webstore/detail/math-wallet/afbcbjpbpfadlkmhmclhkeeodmamcflc", + }, + init: MathWallet, + }; }; -} +}; diff --git a/packages/near-wallet/src/lib/near-wallet.spec.ts b/packages/near-wallet/src/lib/near-wallet.spec.ts index 8be0c872d..8562bfad1 100644 --- a/packages/near-wallet/src/lib/near-wallet.spec.ts +++ b/packages/near-wallet/src/lib/near-wallet.spec.ts @@ -9,7 +9,7 @@ import { } from "../../../core/src/lib/testUtils"; import { BrowserWallet } from "../../../core/src/lib/wallet"; -const createNearWallet = (deps: MockWalletDependencies = {}) => { +const createNearWallet = async (deps: MockWalletDependencies = {}) => { const walletConnection = mock(); const account = mock(); @@ -39,11 +39,11 @@ const createNearWallet = (deps: MockWalletDependencies = {}) => { // eslint-disable-next-line @typescript-eslint/no-var-requires const { setupNearWallet } = require("./near-wallet"); - const nearWallet = mockWallet(setupNearWallet(), deps); + const nearWallet = await mockWallet(setupNearWallet(), deps); return { nearApiJs: require("near-api-js"), - wallet: nearWallet, + wallet: nearWallet!, walletConnection, account, }; @@ -53,17 +53,9 @@ afterEach(() => { jest.resetModules(); }); -describe("isAvailable", () => { - it("returns true", async () => { - const { wallet } = createNearWallet(); - - expect(await wallet.isAvailable()).toEqual(true); - }); -}); - describe("connect", () => { it("sign into near wallet", async () => { - const { wallet, nearApiJs } = createNearWallet(); + const { wallet, nearApiJs } = await createNearWallet(); await wallet.connect(); @@ -73,7 +65,7 @@ describe("connect", () => { describe("disconnect", () => { it("sign out of near wallet", async () => { - const { wallet, walletConnection } = createNearWallet(); + const { wallet, walletConnection } = await createNearWallet(); await wallet.connect(); await wallet.disconnect(); @@ -84,7 +76,7 @@ describe("disconnect", () => { describe("getAccounts", () => { it("returns array of accounts", async () => { - const { wallet, walletConnection } = createNearWallet(); + const { wallet, walletConnection } = await createNearWallet(); await wallet.connect(); const result = await wallet.getAccounts(); @@ -97,7 +89,7 @@ describe("getAccounts", () => { describe("signAndSendTransaction", () => { // TODO: Figure out why imports to core are returning undefined. it.skip("signs and sends transaction", async () => { - const { wallet, walletConnection, account } = createNearWallet(); + const { wallet, walletConnection, account } = await createNearWallet(); await wallet.connect(); const result = await wallet.signAndSendTransaction({ diff --git a/packages/near-wallet/src/lib/near-wallet.ts b/packages/near-wallet/src/lib/near-wallet.ts index 1b634478b..394fee979 100644 --- a/packages/near-wallet/src/lib/near-wallet.ts +++ b/packages/near-wallet/src/lib/near-wallet.ts @@ -1,12 +1,13 @@ import { WalletConnection, connect, keyStores, utils } from "near-api-js"; import * as nearApi from "near-api-js"; import { - WalletModule, + WalletModuleFactory, WalletBehaviourFactory, BrowserWallet, Transaction, Optional, transformActions, + Network, } from "@near-wallet-selector/core"; export interface NearWalletParams { @@ -14,47 +15,68 @@ export interface NearWalletParams { iconUrl?: string; } -export const LOCAL_STORAGE_PENDING = `near-wallet:pending`; +interface NearWalletState { + wallet: WalletConnection; + keyStore: keyStores.BrowserLocalStorageKeyStore; +} -const NearWallet: WalletBehaviourFactory< - BrowserWallet, - Pick -> = ({ options, metadata, walletUrl, emitter, logger, storage }) => { - let _keyStore: keyStores.BrowserLocalStorageKeyStore | null = null; - let _wallet: WalletConnection | null = null; - - const getWalletUrl = () => { - if (walletUrl) { - return walletUrl; - } +type NearWalletExtraOptions = Pick; + +const getWalletUrl = (network: Network, walletUrl?: string) => { + if (walletUrl) { + return walletUrl; + } + + switch (network.networkId) { + case "mainnet": + return "https://wallet.near.org"; + case "testnet": + return "https://wallet.testnet.near.org"; + case "betanet": + return "https://wallet.betanet.near.org"; + default: + throw new Error("Invalid wallet url"); + } +}; - switch (options.network.networkId) { - case "mainnet": - return "https://wallet.near.org"; - case "testnet": - return "https://wallet.testnet.near.org"; - case "betanet": - return "https://wallet.betanet.near.org"; - default: - throw new Error("Invalid wallet URL"); - } +const setupWalletState = async ( + params: NearWalletExtraOptions, + network: Network +): Promise => { + const keyStore = new keyStores.BrowserLocalStorageKeyStore(); + + const near = await connect({ + keyStore, + walletUrl: getWalletUrl(network, params.walletUrl), + ...network, + headers: {}, + }); + + const wallet = new WalletConnection(near, "near_app"); + + // Cleanup up any pending keys (cancelled logins). + if (!wallet.isSignedIn()) { + await keyStore.clear(); + } + + return { + wallet, + keyStore, }; +}; - const cleanup = async () => { - if (_keyStore) { - await _keyStore.clear(); - _keyStore = null; - } +const NearWallet: WalletBehaviourFactory< + BrowserWallet, + { params: NearWalletExtraOptions } +> = async ({ options, params, logger }) => { + const _state = await setupWalletState(params, options.network); - _wallet = null; + const cleanup = () => { + _state.keyStore.clear(); }; const getAccounts = () => { - if (!_wallet) { - return []; - } - - const accountId: string | null = _wallet.getAccountId(); + const accountId: string | null = _state.wallet.getAccountId(); if (!accountId) { return []; @@ -63,44 +85,10 @@ const NearWallet: WalletBehaviourFactory< return [{ accountId }]; }; - const setupWallet = async (): Promise => { - if (_wallet) { - return _wallet; - } - - const localStorageKeyStore = new keyStores.BrowserLocalStorageKeyStore(); - - const near = await connect({ - keyStore: localStorageKeyStore, - walletUrl: getWalletUrl(), - ...options.network, - headers: {}, - }); - - _wallet = new WalletConnection(near, "near_app"); - _keyStore = localStorageKeyStore; - - // Cleanup up any pending keys (cancelled logins). - if (!_wallet.isSignedIn()) { - await localStorageKeyStore.clear(); - } - - return _wallet; - }; - - const getWallet = (): WalletConnection => { - if (!_wallet) { - throw new Error(`${metadata.name} not connected`); - } - - return _wallet; - }; - const transformTransactions = async ( transactions: Array> ) => { - const wallet = getWallet(); - const account = wallet.account(); + const account = _state.wallet.account(); const { networkId, signer, provider } = account.connection; const localKey = await signer.getPublicKey(account.accountId, networkId); @@ -135,50 +123,27 @@ const NearWallet: WalletBehaviourFactory< }; return { - async isAvailable() { - return true; - }, - async connect() { - const wallet = await setupWallet(); - const pending = storage.getItem(LOCAL_STORAGE_PENDING); const existingAccounts = getAccounts(); - if (pending) { - storage.removeItem(LOCAL_STORAGE_PENDING); - } - - if (pending || existingAccounts.length) { + if (existingAccounts.length) { return existingAccounts; } - await wallet.requestSignIn({ + await _state.wallet.requestSignIn({ contractId: options.contractId, methodNames: options.methodNames, }); - // We use the pending flag because we can't guarantee the user will - // actually sign in. Best we can do is set in storage and validate on init. - const newAccounts = getAccounts(); - storage.setItem(LOCAL_STORAGE_PENDING, true); - emitter.emit("connected", { pending: true, accounts: newAccounts }); - - return newAccounts; + return getAccounts(); }, async disconnect() { - if (!_wallet || !_keyStore) { - return; - } - - if (!_wallet.isSignedIn()) { - return cleanup(); + if (_state.wallet.isSignedIn()) { + _state.wallet.signOut(); } - _wallet.signOut(); - await cleanup(); - - emitter.emit("disconnected", null); + cleanup(); }, async getAccounts() { @@ -196,8 +161,11 @@ const NearWallet: WalletBehaviourFactory< actions, }); - const wallet = getWallet(); - const account = wallet.account(); + if (!_state.wallet.isSignedIn()) { + throw new Error("Wallet not connected"); + } + + const account = _state.wallet.account(); return account["signAndSendTransaction"]({ receiverId, @@ -211,9 +179,11 @@ const NearWallet: WalletBehaviourFactory< async signAndSendTransactions({ transactions }) { logger.log("NearWallet:signAndSendTransactions", { transactions }); - const wallet = getWallet(); + if (!_state.wallet.isSignedIn()) { + throw new Error("Wallet not connected"); + } - return wallet.requestSignTransactions({ + return _state.wallet.requestSignTransactions({ transactions: await transformTransactions(transactions), }); }, @@ -223,13 +193,24 @@ const NearWallet: WalletBehaviourFactory< export function setupNearWallet({ walletUrl, iconUrl = "./assets/near-wallet-icon.png", -}: NearWalletParams = {}): WalletModule { - return { - id: "near-wallet", - type: "browser", - name: "NEAR Wallet", - description: null, - iconUrl, - wallet: (options) => NearWallet({ ...options, walletUrl }), +}: NearWalletParams = {}): WalletModuleFactory { + return async () => { + return { + id: "near-wallet", + type: "browser", + metadata: { + name: "NEAR Wallet", + description: null, + iconUrl, + }, + init: (options) => { + return NearWallet({ + ...options, + params: { + walletUrl, + }, + }); + }, + }; }; } diff --git a/packages/sender/src/lib/injected-sender.ts b/packages/sender/src/lib/injected-sender.ts index a0df90dac..aa07d44e0 100644 --- a/packages/sender/src/lib/injected-sender.ts +++ b/packages/sender/src/lib/injected-sender.ts @@ -106,6 +106,7 @@ export interface SenderEvents { export interface InjectedSender { isSender: boolean; + callbacks: Record; getAccountId: () => string | null; getRpc: () => Promise; requestSignIn: ( @@ -113,6 +114,7 @@ export interface InjectedSender { ) => Promise; signOut: () => Promise; isSignedIn: () => boolean; + remove: (event: string) => void; on: ( event: Event, callback: SenderEvents[Event] diff --git a/packages/sender/src/lib/sender.ts b/packages/sender/src/lib/sender.ts index b8836526a..95de3f24c 100644 --- a/packages/sender/src/lib/sender.ts +++ b/packages/sender/src/lib/sender.ts @@ -1,6 +1,6 @@ import isMobile from "is-mobile"; import { - WalletModule, + WalletModuleFactory, WalletBehaviourFactory, InjectedWallet, Action, @@ -8,7 +8,6 @@ import { FunctionCallAction, Optional, waitFor, - errors, } from "@near-wallet-selector/core"; import { InjectedSender } from "./injected-sender"; @@ -24,41 +23,45 @@ export interface SenderParams { } interface SenderState { - wallet: InjectedSender | null; + wallet: InjectedSender; } -const Sender: WalletBehaviourFactory = ({ +const isInstalled = async () => { + try { + return waitFor(() => !!window.near?.isSender); + } catch (err) { + return false; + } +}; + +const setupSenderState = (): SenderState => { + const wallet = window.near!; + + return { + wallet, + }; +}; + +const Sender: WalletBehaviourFactory = async ({ options, metadata, emitter, logger, }) => { - const _state: SenderState = { wallet: null }; - - const isInstalled = async () => { - try { - return await waitFor(() => !!window.near?.isSender); - } catch (e) { - logger.log("Sender:isInstalled:error", e); - - return false; - } - }; + const _state = setupSenderState(); const cleanup = () => { - _state.wallet = null; + for (const key in _state.wallet.callbacks) { + _state.wallet.remove(key); + } }; - // TODO: Remove event listeners. - // Must only trigger "disconnected" if we were connected. const disconnect = async () => { - if (!_state.wallet) { + if (!_state.wallet.isSignedIn()) { return; } - if (!_state.wallet.isSignedIn()) { - return cleanup(); - } + cleanup(); const res = await _state.wallet.signOut(); @@ -72,72 +75,34 @@ const Sender: WalletBehaviourFactory = ({ "Failed to disconnect" ); } - - cleanup(); - - emitter.emit("disconnected", null); - }; - - const getAccounts = () => { - if (!_state.wallet) { - return []; - } - - const accountId = _state.wallet.getAccountId(); - - if (!accountId) { - return []; - } - - return [{ accountId }]; }; - const setupWallet = async (): Promise => { - if (_state.wallet) { - return _state.wallet; - } - - const installed = await isInstalled(); - - if (!installed) { - throw errors.createWalletNotInstalledError(metadata); - } - - _state.wallet = window.near!; - - try { - // Add extra wait to ensure Sender's sign in status is read from the - // browser extension background env. - await waitFor(() => !!_state.wallet?.isSignedIn(), { timeout: 300 }); - } catch (e) { - logger.log("Sender:setupWallet: Not signed in yet"); - } - + const setupEvents = () => { _state.wallet.on("accountChanged", async (newAccountId) => { logger.log("Sender:onAccountChange", newAccountId); - - cleanup(); - emitter.emit("disconnected", null); }); _state.wallet.on("rpcChanged", async ({ rpc }) => { + logger.log("Sender:onNetworkChange", rpc); + if (options.network.networkId !== rpc.networkId) { await disconnect(); + emitter.emit("disconnected", null); emitter.emit("networkChanged", { networkId: rpc.networkId }); } }); - - return _state.wallet; }; - const getWallet = (): InjectedSender => { - if (!_state.wallet) { - throw new Error(`${metadata.name} not connected`); + const getAccounts = () => { + const accountId = _state.wallet.getAccountId(); + + if (!accountId) { + return []; } - return _state.wallet; + return [{ accountId }]; }; const isValidActions = ( @@ -169,25 +134,19 @@ const Sender: WalletBehaviourFactory = ({ }); }; - return { - getDownloadUrl() { - return "https://chrome.google.com/webstore/detail/sender-wallet/epapihdplajcdnnkdeiahlgigofloibg"; - }, - - async isAvailable() { - return !isMobile(); - }, + if (_state.wallet.isSignedIn()) { + setupEvents(); + } + return { async connect() { - const wallet = await setupWallet(); const existingAccounts = getAccounts(); if (existingAccounts.length) { - emitter.emit("connected", { accounts: existingAccounts }); return existingAccounts; } - const { accessKey, error } = await wallet.requestSignIn({ + const { accessKey, error } = await _state.wallet.requestSignIn({ contractId: options.contractId, methodNames: options.methodNames, }); @@ -201,10 +160,9 @@ const Sender: WalletBehaviourFactory = ({ ); } - const newAccounts = getAccounts(); - emitter.emit("connected", { accounts: newAccounts }); + setupEvents(); - return newAccounts; + return getAccounts(); }, disconnect, @@ -224,9 +182,11 @@ const Sender: WalletBehaviourFactory = ({ actions, }); - const wallet = getWallet(); + if (!_state.wallet.isSignedIn()) { + throw new Error("Wallet not connected"); + } - return wallet + return _state.wallet .signAndSendTransaction({ receiverId, actions: transformActions(actions), @@ -248,9 +208,11 @@ const Sender: WalletBehaviourFactory = ({ async signAndSendTransactions({ transactions }) { logger.log("Sender:signAndSendTransactions", { transactions }); - const wallet = getWallet(); + if (!_state.wallet.isSignedIn()) { + throw new Error("Wallet not connected"); + } - return wallet + return _state.wallet .requestSignTransactions({ transactions: transformTransactions(transactions), }) @@ -272,13 +234,32 @@ const Sender: WalletBehaviourFactory = ({ export function setupSender({ iconUrl = "./assets/sender-icon.png", -}: SenderParams = {}): WalletModule { - return { - id: "sender", - type: "injected", - name: "Sender", - description: null, - iconUrl, - wallet: Sender, +}: SenderParams = {}): WalletModuleFactory { + return async () => { + const mobile = isMobile(); + const installed = await isInstalled(); + + if (mobile || !installed) { + return null; + } + + // Add extra wait to ensure Sender's sign in status is read from the + // browser extension background env. + await waitFor(() => !!window.near?.isSignedIn(), { timeout: 300 }).catch( + () => false + ); + + return { + id: "sender", + type: "injected", + metadata: { + name: "Sender", + description: null, + iconUrl, + downloadUrl: + "https://chrome.google.com/webstore/detail/sender-wallet/epapihdplajcdnnkdeiahlgigofloibg", + }, + init: Sender, + }; }; } diff --git a/packages/wallet-connect/src/lib/wallet-connect.ts b/packages/wallet-connect/src/lib/wallet-connect.ts index 759d10591..22e0a1e08 100644 --- a/packages/wallet-connect/src/lib/wallet-connect.ts +++ b/packages/wallet-connect/src/lib/wallet-connect.ts @@ -1,6 +1,6 @@ import { AppMetadata, SessionTypes } from "@walletconnect/types"; import { - WalletModule, + WalletModuleFactory, WalletBehaviourFactory, BridgeWallet, Subscription, @@ -10,143 +10,126 @@ import WalletConnectClient from "./wallet-connect-client"; export interface WalletConnectParams { projectId: string; - appMetadata: AppMetadata; + metadata: AppMetadata; relayUrl?: string; iconUrl?: string; + chainId?: string; } +type WalletConnectExtraOptions = Pick & + Required>; + +interface WalletConnectState { + client: WalletConnectClient; + session: SessionTypes.Settled | null; + subscriptions: Array; +} + +const setupWalletConnectState = async ( + params: WalletConnectExtraOptions +): Promise => { + const client = new WalletConnectClient(); + let session: SessionTypes.Settled | null = null; + + await client.init(params); + + if (client.session.topics.length) { + session = await client.session.get(client.session.topics[0]); + } + + return { + client, + session, + subscriptions: [], + }; +}; + const WalletConnect: WalletBehaviourFactory< BridgeWallet, - Pick -> = ({ - options, - metadata, - projectId, - appMetadata, - relayUrl, - emitter, - logger, -}) => { - let _wallet: WalletConnectClient | null = null; - let _subscriptions: Array = []; - let _session: SessionTypes.Settled | null = null; + { params: WalletConnectExtraOptions } +> = async ({ options, params, emitter, logger }) => { + const _state = await setupWalletConnectState(params); const getChainId = () => { + if (params.chainId) { + return params.chainId; + } + const { networkId } = options.network; if (["mainnet", "testnet", "betanet"].includes(networkId)) { return `near:${networkId}`; } - return "near:testnet"; + throw new Error("Invalid chain id"); }; const getAccounts = () => { - if (!_session) { + if (!_state.session) { return []; } - return _session.state.accounts.map((wcAccountId) => ({ + return _state.session.state.accounts.map((wcAccountId) => ({ accountId: wcAccountId.split(":")[2], })); }; const cleanup = () => { - _subscriptions.forEach((subscription) => subscription.remove()); + _state.subscriptions.forEach((subscription) => subscription.remove()); - _wallet = null; - _subscriptions = []; - _session = null; + _state.subscriptions = []; + _state.session = null; }; const disconnect = async () => { - if (!_wallet) { - return; - } - - if (!_session) { - return cleanup(); + if (_state.session) { + await _state.client.disconnect({ + topic: _state.session.topic, + reason: { + code: 5900, + message: "User disconnected", + }, + }); } - await _wallet.disconnect({ - topic: _session.topic, - reason: { - code: 5900, - message: "User disconnected", - }, - }); cleanup(); - - emitter.emit("disconnected", null); }; - const setupWallet = async (): Promise => { - if (_wallet) { - return _wallet; - } - - const client = new WalletConnectClient(); - - await client.init({ - projectId, - relayUrl, - metadata: appMetadata, - }); - - _subscriptions.push( - client.on("pairing_created", (pairing) => { + const setupEvents = () => { + _state.subscriptions.push( + _state.client.on("pairing_created", (pairing) => { logger.log("Pairing Created", pairing); }) ); - _subscriptions.push( - client.on("session_updated", (updatedSession) => { + _state.subscriptions.push( + _state.client.on("session_updated", (updatedSession) => { logger.log("Session Updated", updatedSession); - if (updatedSession.topic === _session?.topic) { - _session = updatedSession; + if (updatedSession.topic === _state.session?.topic) { + _state.session = updatedSession; emitter.emit("accountsChanged", { accounts: getAccounts() }); } }) ); - _subscriptions.push( - client.on("session_deleted", (deletedSession) => { + _state.subscriptions.push( + _state.client.on("session_deleted", async (deletedSession) => { logger.log("Session Deleted", deletedSession); - if (deletedSession.topic === _session?.topic) { - disconnect(); + if (deletedSession.topic === _state.session?.topic) { + await disconnect(); } }) ); - - if (client.session.topics.length) { - _session = await client.session.get(client.session.topics[0]); - } - - _wallet = client; - - return client; }; - const getWallet = () => { - if (!_wallet || !_session) { - throw new Error(`${metadata.name} not connected`); - } - - return { - wallet: _wallet, - session: _session, - }; - }; + if (_state.session) { + setupEvents(); + } return { - async isAvailable() { - return true; - }, - async connect() { - const wallet = await setupWallet(); const existingAccounts = getAccounts(); if (existingAccounts.length) { @@ -154,8 +137,8 @@ const WalletConnect: WalletBehaviourFactory< } try { - _session = await wallet.connect({ - metadata: appMetadata, + _state.session = await _state.client.connect({ + metadata: params.metadata, timeout: 30 * 1000, permissions: { blockchain: { @@ -170,12 +153,11 @@ const WalletConnect: WalletBehaviourFactory< }, }); - const newAccounts = getAccounts(); - emitter.emit("connected", { accounts: newAccounts }); + setupEvents(); - return newAccounts; + return getAccounts(); } catch (err) { - await this.disconnect(); + await disconnect(); throw err; } @@ -198,11 +180,13 @@ const WalletConnect: WalletBehaviourFactory< actions, }); - const { wallet, session } = getWallet(); + if (!_state.session) { + throw new Error("Wallet not connected"); + } - return wallet.request({ + return _state.client.request({ timeout: 30 * 1000, - topic: session.topic, + topic: _state.session.topic, chainId: getChainId(), request: { method: "near_signAndSendTransaction", @@ -218,11 +202,13 @@ const WalletConnect: WalletBehaviourFactory< async signAndSendTransactions({ transactions }) { logger.log("WalletConnect:signAndSendTransactions", { transactions }); - const { wallet, session } = getWallet(); + if (!_state.session) { + throw new Error("Wallet not connected"); + } - return wallet.request({ + return _state.client.request({ timeout: 30 * 1000, - topic: session.topic, + topic: _state.session.topic, chainId: getChainId(), request: { method: "near_signAndSendTransactions", @@ -235,23 +221,31 @@ const WalletConnect: WalletBehaviourFactory< export function setupWalletConnect({ projectId, - appMetadata, + metadata, + chainId, relayUrl = "wss://relay.walletconnect.com", iconUrl = "./assets/wallet-connect-icon.png", -}: WalletConnectParams): WalletModule { - return { - id: "wallet-connect", - type: "bridge", - name: "WalletConnect", - description: null, - iconUrl, - wallet: (options) => { - return WalletConnect({ - ...options, - projectId, - appMetadata, - relayUrl, - }); - }, +}: WalletConnectParams): WalletModuleFactory { + return async () => { + return { + id: "wallet-connect", + type: "bridge", + metadata: { + name: "WalletConnect", + description: null, + iconUrl, + }, + init: (options) => { + return WalletConnect({ + ...options, + params: { + projectId, + metadata, + relayUrl, + chainId, + }, + }); + }, + }; }; }