From 45a82a6bdb2566eaef1a82fff820faff4eda7f6d Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Fri, 29 Apr 2022 08:08:49 +0100 Subject: [PATCH 01/64] Simplified state to store modules instead of wallets. --- packages/core/src/lib/store.ts | 44 +++++----------------------- packages/core/src/lib/store.types.ts | 16 +++++----- 2 files changed, 16 insertions(+), 44 deletions(-) diff --git a/packages/core/src/lib/store.ts b/packages/core/src/lib/store.ts index a775a389c..f54852920 100644 --- a/packages/core/src/lib/store.ts +++ b/packages/core/src/lib/store.ts @@ -15,19 +15,13 @@ 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": { @@ -35,40 +29,15 @@ const reducer = ( 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: !pending && accounts.length ? walletId : null, }; } case "WALLET_DISCONNECTED": { return { ...state, - wallets: state.wallets.map((wallet) => { - if (!wallet.selected) { - return wallet; - } - - return { - ...wallet, - selected: false, - }; - }), accounts: [], + selectedWalletId: null, }; } case "ACCOUNTS_CHANGED": { @@ -86,8 +55,9 @@ const reducer = ( export const createStore = (): Store => { const subject = new BehaviorSubject({ + modules: [], accounts: [], - wallets: [], + selectedWalletId: null, }); return { diff --git a/packages/core/src/lib/store.types.ts b/packages/core/src/lib/store.types.ts index 50fa7aa57..9d0c44dff 100644 --- a/packages/core/src/lib/store.types.ts +++ b/packages/core/src/lib/store.types.ts @@ -1,27 +1,29 @@ import { BehaviorSubject } from "rxjs"; -import { Wallet, WalletMetadata } from "./wallet"; +import { Wallet, WalletBehaviour, WalletMetadata } from "./wallet"; export interface AccountState { accountId: string; } -export type WalletState = WalletMetadata & { - selected: boolean; -}; +export type WalletModuleState = + WalletMetadata & { + wallet(): WalletBehaviour; + }; 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; }; } | { From bd606c498992ca0d6f79c7b294fff992751a40fc Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Fri, 29 Apr 2022 08:11:41 +0100 Subject: [PATCH 02/64] Removed wallet function and renamed wallets to modules. --- packages/core/src/lib/wallet-selector.ts | 16 +--------------- packages/core/src/lib/wallet-selector.types.ts | 8 ++------ 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/packages/core/src/lib/wallet-selector.ts b/packages/core/src/lib/wallet-selector.ts index ec53fd86f..3881bac6b 100644 --- a/packages/core/src/lib/wallet-selector.ts +++ b/packages/core/src/lib/wallet-selector.ts @@ -8,7 +8,6 @@ import { } from "./wallet-selector.types"; import { WalletSelectorModal } from "./modal/modal.types"; import { setupModal } from "./modal/modal"; -import { Wallet } from "./wallet"; import { EventEmitter, Logger } from "./services"; export const setupWalletSelector = async ( @@ -19,7 +18,7 @@ export const setupWalletSelector = async ( const store = createStore(); const controller = new WalletController( options, - params.wallets, + params.modules, store, emitter ); @@ -40,19 +39,6 @@ export const setupWalletSelector = async ( return Boolean(accounts.length); }, options, - wallet: (walletId?: string) => { - const wallet = controller.getWallet(walletId); - - if (!wallet) { - if (walletId) { - throw new Error("Invalid wallet id"); - } - - throw new Error("No wallet selected"); - } - - return wallet; - }, on: (eventName, callback) => { return emitter.on(eventName, callback); }, diff --git a/packages/core/src/lib/wallet-selector.types.ts b/packages/core/src/lib/wallet-selector.types.ts index de0531fe9..1033f419e 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 { WalletModule } from "./wallet"; 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,10 +30,6 @@ export interface WalletSelector extends WalletSelectorModal { options: Options; connected: boolean; - wallet( - walletId?: string - ): WalletVariation; - on( eventName: EventName, callback: (event: WalletSelectorEvents[EventName]) => void From 735e5dd72a068263a3ac034d32e5a176dfdb9487 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Fri, 29 Apr 2022 08:21:45 +0100 Subject: [PATCH 03/64] Updated setup logic. --- packages/core/src/lib/wallet-controller.ts | 88 ++++++++++++---------- 1 file changed, 48 insertions(+), 40 deletions(-) diff --git a/packages/core/src/lib/wallet-controller.ts b/packages/core/src/lib/wallet-controller.ts index aadef466c..82757581d 100644 --- a/packages/core/src/lib/wallet-controller.ts +++ b/packages/core/src/lib/wallet-controller.ts @@ -1,14 +1,13 @@ 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 { AccountState, Store, WalletModuleState } 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; @@ -22,7 +21,6 @@ class WalletController { this.modules = modules; this.store = store; this.emitter = emitter; - this.wallets = []; } private getSelectedWalletId() { @@ -37,45 +35,57 @@ class WalletController { return storage.removeItem(LOCAL_STORAGE_SELECTED_WALLET_ID); } - private setupWalletModule({ wallet, ...metadata }: WalletModule) { + private setupWalletModule(module: WalletModule): WalletModuleState { 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)); + emitter.on("connected", this.handleConnected(module.id)); + emitter.on("disconnected", this.handleDisconnected(module.id)); + emitter.on("accountsChanged", this.handleAccountsChanged(module.id)); + emitter.on("networkChanged", this.handleNetworkChanged(module.id)); return { - ...metadata, - ...wallet({ - options: this.options, - metadata, - provider, - emitter, - // TODO: Make a scoped logger. - logger, - // TODO: Make a scoped storage. - storage, - }), - } as Wallet; + ...module, + wallet: () => { + return module.wallet({ + options: this.options, + metadata: { + id: module.id, + name: module.name, + description: module.description, + iconUrl: module.iconUrl, + type: module.type, + }, + provider, + emitter, + // TODO: Make a scoped logger. + logger, + // TODO: Make a scoped storage. + storage, + }); + }, + }; } private async setupWalletModules() { let selectedWalletId = this.getSelectedWalletId(); let accounts: Array = []; - const wallets = this.modules.map((module) => { + const modules = this.modules.map((module) => { return this.setupWalletModule(module); }); - const selectedWallet = wallets.find((x) => x.id === selectedWalletId); + const selectedModule = modules.find((x) => x.id === selectedWalletId); - if (selectedWallet) { + if (selectedModule) { // 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`); + const wallet = await selectedModule.wallet(); + + accounts = await wallet.getAccounts().catch((err) => { + logger.log( + `Failed to get accounts for ${selectedModule.id} during setup` + ); logger.error(err); return []; @@ -87,26 +97,26 @@ class WalletController { selectedWalletId = null; } - this.wallets = wallets; - this.store.dispatch({ type: "SETUP_WALLET_MODULES", - payload: { wallets, selectedWalletId, accounts }, + payload: { modules, accounts, selectedWalletId }, }); } private handleConnected = (walletId: string) => async ({ pending = false, accounts = [] }: WalletEvents["connected"]) => { - const existingWallet = this.getWallet(); + const existingModule = this.getModule(); + + if (existingModule && existingModule.id !== walletId) { + const wallet = await existingModule.wallet(); - if (existingWallet && existingWallet.id !== walletId) { - await existingWallet.disconnect().catch((err) => { + await wallet.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)(); + this.handleDisconnected(existingModule.id)(); }); } @@ -132,12 +142,10 @@ class WalletController { private handleAccountsChanged = (walletId: string) => ({ accounts }: WalletEvents["accountsChanged"]) => { - const { wallets } = this.store.getState(); - const selected = wallets.some((wallet) => { - return wallet.id === walletId && wallet.selected; - }); + const { selectedWalletId } = this.store.getState(); - if (!selected) { + // TODO: Move this check into the store. + if (walletId !== selectedWalletId) { return; } @@ -157,11 +165,11 @@ class WalletController { await this.setupWalletModules(); } - getWallet(walletId?: string) { + getModule(walletId?: string) { const lookupWalletId = walletId || this.getSelectedWalletId(); - const wallet = this.wallets.find((x) => x.id === lookupWalletId) || null; + const module = this.modules.find((x) => x.id === lookupWalletId) || null; - return wallet as WalletVariation | null; + return module as WalletModuleState | null; } } From c55697daf581373109fa8813c4b2d5f8dc475421 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Fri, 29 Apr 2022 09:02:52 +0100 Subject: [PATCH 04/64] Brought back wallet function. --- .../src/contexts/WalletSelectorContext.tsx | 35 +++---- .../lib/modal/components/WalletOptions.tsx | 92 ++++++++----------- packages/core/src/lib/wallet-controller.ts | 2 + packages/core/src/lib/wallet-selector.ts | 45 +++++++++ .../core/src/lib/wallet-selector.types.ts | 6 +- 5 files changed, 107 insertions(+), 73 deletions(-) diff --git a/examples/react/src/contexts/WalletSelectorContext.tsx b/examples/react/src/contexts/WalletSelectorContext.tsx index 0db1b2de3..e8c19cce3 100644 --- a/examples/react/src/contexts/WalletSelectorContext.tsx +++ b/examples/react/src/contexts/WalletSelectorContext.tsx @@ -5,11 +5,11 @@ import { WalletSelector, AccountState, } from "@near-wallet-selector/core"; -import { setupNearWallet } from "@near-wallet-selector/near-wallet"; -import { setupSender } from "@near-wallet-selector/sender"; +// import { setupNearWallet } from "@near-wallet-selector/near-wallet"; +// import { setupSender } from "@near-wallet-selector/sender"; import { setupMathWallet } from "@near-wallet-selector/math-wallet"; -import { setupLedger } from "@near-wallet-selector/ledger"; -import { setupWalletConnect } from "@near-wallet-selector/wallet-connect"; +// import { setupLedger } from "@near-wallet-selector/ledger"; +// import { setupWalletConnect } from "@near-wallet-selector/wallet-connect"; declare global { interface Window { @@ -60,20 +60,21 @@ export const WalletSelectorContextProvider: React.FC = ({ children }) => { setupWalletSelector({ network: "testnet", contractId: "guest-book.testnet", - wallets: [ - setupNearWallet(), - setupSender(), + debug: true, + modules: [ + // setupNearWallet(), + // setupSender(), setupMathWallet(), - setupLedger(), - setupWalletConnect({ - projectId: "c4f79cc...", - appMetadata: { - name: "NEAR Wallet Selector", - description: "Example dApp used by NEAR Wallet Selector", - url: "https://github.com/near/wallet-selector", - icons: ["https://avatars.githubusercontent.com/u/37784886"], - }, - }), + // setupLedger(), + // setupWalletConnect({ + // projectId: "c4f79cc...", + // appMetadata: { + // name: "NEAR Wallet Selector", + // description: "Example dApp used by NEAR Wallet Selector", + // url: "https://github.com/near/wallet-selector", + // icons: ["https://avatars.githubusercontent.com/u/37784886"], + // }, + // }), ], }) .then((instance) => { diff --git a/packages/core/src/lib/modal/components/WalletOptions.tsx b/packages/core/src/lib/modal/components/WalletOptions.tsx index a5acfa7e8..9850ee9da 100644 --- a/packages/core/src/lib/modal/components/WalletOptions.tsx +++ b/packages/core/src/lib/modal/components/WalletOptions.tsx @@ -1,7 +1,6 @@ import React, { Fragment, useEffect, useState } from "react"; -import { WalletState } from "../../store.types"; -import { Wallet } from "../../wallet"; +import { WalletModuleState } from "../../store.types"; import { WalletSelector } from "../../wallet-selector.types"; import { ModalOptions, WalletSelectorModal } from "../modal.types"; import { logger } from "../../services"; @@ -11,7 +10,7 @@ interface WalletOptionsProps { // TODO: Remove omit once modal is a separate package. selector: Omit; options?: ModalOptions; - onWalletNotInstalled: (wallet: Wallet) => void; + onWalletNotInstalled: (module: WalletModuleState) => void; onConnectHardwareWallet: () => void; onConnected: () => void; onError: (message: string) => void; @@ -27,56 +26,42 @@ export const WalletOptions: React.FC = ({ }) => { 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: WalletModuleState) => () => { if (connecting) { return; } - if (wallet.type === "hardware") { + if (module.type === "hardware") { return onConnectHardwareWallet(); } setConnecting(true); + const wallet = module.wallet(); + wallet .connect() .then(() => onConnected()) .catch((err) => { if (errors.isWalletNotInstalledError(err)) { - return onWalletNotInstalled(wallet); + return onWalletNotInstalled(module); } - logger.log(`Failed to select ${wallet.name}`); + logger.log(`Failed to select ${module.name}`); logger.error(err); - onError(`Failed to connect with ${wallet.name}: ${err.message}`); + onError(`Failed to connect with ${module.name}: ${err.message}`); }) .finally(() => setConnecting(false)); }; @@ -93,37 +78,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 { id, name, description, iconUrl } = module; + const selected = module.id === selectedWalletId; + + result.push( +
  • +
    + {name} +
    + {name}
    -
  • - ); + {selected && ( +
    + selected +
    + )} + + + ); - return result; - }, - [] - )} + return result; + }, [])}
    diff --git a/packages/core/src/lib/wallet-controller.ts b/packages/core/src/lib/wallet-controller.ts index 82757581d..4a6751e46 100644 --- a/packages/core/src/lib/wallet-controller.ts +++ b/packages/core/src/lib/wallet-controller.ts @@ -35,6 +35,7 @@ class WalletController { return storage.removeItem(LOCAL_STORAGE_SELECTED_WALLET_ID); } + // TODO: Add "cache" for "wallet" call. private setupWalletModule(module: WalletModule): WalletModuleState { const emitter = new EventEmitter(); const provider = new Provider(this.options.network.nodeUrl); @@ -67,6 +68,7 @@ class WalletController { }; } + // TODO: Move isAvailable to module level and filter out here. private async setupWalletModules() { let selectedWalletId = this.getSelectedWalletId(); let accounts: Array = []; diff --git a/packages/core/src/lib/wallet-selector.ts b/packages/core/src/lib/wallet-selector.ts index 3881bac6b..14f1c8caf 100644 --- a/packages/core/src/lib/wallet-selector.ts +++ b/packages/core/src/lib/wallet-selector.ts @@ -9,6 +9,7 @@ import { import { WalletSelectorModal } from "./modal/modal.types"; import { setupModal } from "./modal/modal"; import { EventEmitter, Logger } from "./services"; +import { Wallet, WalletBehaviour } from "./wallet"; export const setupWalletSelector = async ( params: WalletSelectorParams @@ -39,6 +40,50 @@ export const setupWalletSelector = async ( return Boolean(accounts.length); }, options, + wallet: (walletId?: string) => { + const module = controller.getModule(walletId); + + if (!module) { + if (walletId) { + throw new Error("Invalid wallet id"); + } + + throw new Error("No wallet selected"); + } + + return { + isAvailable: async () => { + const wallet = await module.wallet(); + + return wallet.isAvailable(); + }, + connect: async (args: never) => { + const wallet = await module.wallet(); + + return wallet.connect(args); + }, + disconnect: async () => { + const wallet = await module.wallet(); + + return wallet.disconnect(); + }, + getAccounts: async () => { + const wallet = await module.wallet(); + + return wallet.getAccounts(); + }, + signAndSendTransaction: async (args: never) => { + const wallet = await module.wallet(); + + return wallet.signAndSendTransaction(args); + }, + signAndSendTransactions: async (args: never) => { + const wallet = await module.wallet(); + + return wallet.signAndSendTransactions(args); + }, + } as WalletBehaviour; + }, on: (eventName, callback) => { return emitter.on(eventName, callback); }, diff --git a/packages/core/src/lib/wallet-selector.types.ts b/packages/core/src/lib/wallet-selector.types.ts index 1033f419e..5af023478 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 } from "./wallet"; +import { WalletModule, Wallet, WalletBehaviour } from "./wallet"; import { WalletSelectorState } from "./store.types"; import { Network, NetworkId, Options } from "./options.types"; import { ModalOptions, WalletSelectorModal } from "./modal/modal.types"; @@ -30,6 +30,10 @@ export interface WalletSelector extends WalletSelectorModal { options: Options; connected: boolean; + wallet( + walletId?: string + ): WalletBehaviour; + on( eventName: EventName, callback: (event: WalletSelectorEvents[EventName]) => void From 359d23949bf2a3497779b68e9ca84f82fa916eb0 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Fri, 29 Apr 2022 09:18:10 +0100 Subject: [PATCH 05/64] Made wallet behaviour async. --- packages/core/src/lib/modal/components/WalletOptions.tsx | 5 ++--- packages/core/src/lib/store.types.ts | 3 ++- packages/core/src/lib/wallet/wallet.ts | 4 ++-- packages/math-wallet/src/lib/math-wallet.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/core/src/lib/modal/components/WalletOptions.tsx b/packages/core/src/lib/modal/components/WalletOptions.tsx index 9850ee9da..511cf8f2a 100644 --- a/packages/core/src/lib/modal/components/WalletOptions.tsx +++ b/packages/core/src/lib/modal/components/WalletOptions.tsx @@ -48,9 +48,8 @@ export const WalletOptions: React.FC = ({ setConnecting(true); - const wallet = module.wallet(); - - wallet + selector + .wallet(module.id) .connect() .then(() => onConnected()) .catch((err) => { diff --git a/packages/core/src/lib/store.types.ts b/packages/core/src/lib/store.types.ts index 9d0c44dff..b9d76237b 100644 --- a/packages/core/src/lib/store.types.ts +++ b/packages/core/src/lib/store.types.ts @@ -6,9 +6,10 @@ export interface AccountState { accountId: string; } +// TODO: Make this WalletModule without "wallet" and "isAvailable"? export type WalletModuleState = WalletMetadata & { - wallet(): WalletBehaviour; + wallet(): Promise>; }; export interface WalletSelectorState { diff --git a/packages/core/src/lib/wallet/wallet.ts b/packages/core/src/lib/wallet/wallet.ts index cedf12ded..95477851e 100644 --- a/packages/core/src/lib/wallet/wallet.ts +++ b/packages/core/src/lib/wallet/wallet.ts @@ -108,7 +108,7 @@ export type WalletModule = WalletMetadata & { wallet( options: WalletOptions - ): WalletBehaviour; + ): Promise>; }; export type WalletBehaviourFactory< @@ -116,4 +116,4 @@ export type WalletBehaviourFactory< ExtraWalletOptions extends object = object > = ( options: WalletOptions & ExtraWalletOptions -) => WalletBehaviour; +) => Promise>; diff --git a/packages/math-wallet/src/lib/math-wallet.ts b/packages/math-wallet/src/lib/math-wallet.ts index 8d029a7f5..23aa30169 100644 --- a/packages/math-wallet/src/lib/math-wallet.ts +++ b/packages/math-wallet/src/lib/math-wallet.ts @@ -22,7 +22,7 @@ export interface MathWalletParams { iconUrl?: string; } -const MathWallet: WalletBehaviourFactory = ({ +const MathWallet: WalletBehaviourFactory = async ({ options, metadata, provider, @@ -118,7 +118,7 @@ const MathWallet: WalletBehaviourFactory = ({ } await wallet.login({ contractId: options.contractId }).catch((err) => { - this.disconnect(); + cleanup(); throw err; }); From 35413c92f9674bb6815cddc693b8062eb3231b3c Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Fri, 29 Apr 2022 09:48:29 +0100 Subject: [PATCH 06/64] Initial attempt at improved types. --- packages/core/src/lib/wallet/wallet.types.ts | 81 ++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 packages/core/src/lib/wallet/wallet.types.ts 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..fe84202a0 --- /dev/null +++ b/packages/core/src/lib/wallet/wallet.types.ts @@ -0,0 +1,81 @@ +// - WalletModules -> Wallet +// - Shouldn't initialise the wallet until we want to connect or already connected. +// - We need the type alongside the methods to help with type checking. +// - We need getDownloadUrl and isAvailable outside the initialisation of a wallet. + +import { providers } from "near-api-js"; +import { AccountState } from "../store.types"; +import { + SignAndSendTransactionParams, + SignAndSendTransactionsParams, +} from "./wallet"; + +export interface BrowserWalletMetadata { + id: string; + type: "browser"; + name: string; + description: string | null; + iconUrl: string; +} + +export interface BrowserWalletBehaviour { + connect(): Promise>; + disconnect(): Promise; + getAccounts(): Promise>; + signAndSendTransaction(params: SignAndSendTransactionParams): Promise; + signAndSendTransactions(params: SignAndSendTransactionsParams): Promise; +} + +export type BrowserWallet = BrowserWalletMetadata & BrowserWalletBehaviour; + +export interface BrowserWalletModule< + Wallet extends BrowserWallet | BrowserWalletBehaviour = BrowserWallet +> { + id: string; + type: "browser"; + name: string; + description: string | null; + iconUrl: string; + + isAvailable(): Promise; + init(): Promise; +} + +export interface InjectedWalletMetadata { + id: string; + type: "injected"; + name: string; + description: string | null; + iconUrl: string; +} + +export interface InjectedWalletBehaviour { + connect(): Promise>; + disconnect(): Promise; + getAccounts(): Promise>; + signAndSendTransaction( + params: SignAndSendTransactionParams + ): Promise; + signAndSendTransactions( + params: SignAndSendTransactionsParams + ): Promise>; +} + +type InjectedWallet = InjectedWalletMetadata & InjectedWalletBehaviour; + +export interface InjectedWalletModule< + Wallet extends InjectedWallet | InjectedWalletBehaviour = InjectedWallet +> { + id: string; + type: "injected"; + name: string; + description: string | null; + iconUrl: string; + + isAvailable(): Promise; + getDownloadUrl(): string; + init(): Promise; +} + +type WalletModule = BrowserWalletModule | InjectedWalletModule; +type Wallet = BrowserWallet | InjectedWallet; From 1163a5f0c997737c16b4f88c59234ff370440620 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Fri, 29 Apr 2022 09:52:44 +0100 Subject: [PATCH 07/64] Added hardware and bridge wallets. --- packages/core/src/lib/wallet/wallet.types.ts | 88 +++++++++++++++++++- 1 file changed, 86 insertions(+), 2 deletions(-) diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts index fe84202a0..cc725311a 100644 --- a/packages/core/src/lib/wallet/wallet.types.ts +++ b/packages/core/src/lib/wallet/wallet.types.ts @@ -6,10 +6,13 @@ import { providers } from "near-api-js"; import { AccountState } from "../store.types"; import { + HardwareWalletConnectParams, SignAndSendTransactionParams, SignAndSendTransactionsParams, } from "./wallet"; +// ----- Browser Wallet ----- // + export interface BrowserWalletMetadata { id: string; type: "browser"; @@ -41,6 +44,8 @@ export interface BrowserWalletModule< init(): Promise; } +// ----- Injected Wallet ----- // + export interface InjectedWalletMetadata { id: string; type: "injected"; @@ -77,5 +82,84 @@ export interface InjectedWalletModule< init(): Promise; } -type WalletModule = BrowserWalletModule | InjectedWalletModule; -type Wallet = BrowserWallet | InjectedWallet; +// ----- Hardware Wallet ----- // + +export interface HardwareWalletMetadata { + id: string; + type: "hardware"; + name: string; + description: string | null; + iconUrl: string; +} + +export interface HardwareWalletBehaviour { + connect(params: HardwareWalletConnectParams): Promise>; + disconnect(): Promise; + getAccounts(): Promise>; + signAndSendTransaction( + params: SignAndSendTransactionParams + ): Promise; + signAndSendTransactions( + params: SignAndSendTransactionsParams + ): Promise>; +} + +type HardwareWallet = HardwareWalletMetadata & HardwareWalletBehaviour; + +export interface HardwareWalletModule< + Wallet extends HardwareWallet | HardwareWalletBehaviour = HardwareWallet +> { + id: string; + type: "hardware"; + name: string; + description: string | null; + iconUrl: string; + + isAvailable(): Promise; + init(): Promise; +} + +// ----- Bridge Wallet ----- // + +export interface BridgeWalletMetadata { + id: string; + type: "bridge"; + name: string; + description: string | null; + iconUrl: string; +} + +export interface BridgeWalletBehaviour { + connect(): Promise>; + disconnect(): Promise; + getAccounts(): Promise>; + signAndSendTransaction( + params: SignAndSendTransactionParams + ): Promise; + signAndSendTransactions( + params: SignAndSendTransactionsParams + ): Promise>; +} + +type BridgeWallet = BridgeWalletMetadata & BridgeWalletBehaviour; + +export interface BridgeWalletModule< + Wallet extends BridgeWallet | BridgeWalletBehaviour = BridgeWallet +> { + id: string; + type: "bridge"; + name: string; + description: string | null; + iconUrl: string; + + isAvailable(): Promise; + init(): Promise; +} + +type WalletModule = + | BrowserWalletModule + | InjectedWalletModule + | HardwareWalletModule + | BridgeWalletModule; + +type Wallet = BrowserWallet | InjectedWallet | HardwareWallet | BridgeWallet; From 3e110c552936098666377af659cde31221e996e6 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Fri, 29 Apr 2022 12:12:34 +0100 Subject: [PATCH 08/64] Removed the need for isAvailable. --- packages/core/src/lib/wallet/wallet.types.ts | 196 ++++++++++++------- 1 file changed, 120 insertions(+), 76 deletions(-) diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts index cc725311a..59353addd 100644 --- a/packages/core/src/lib/wallet/wallet.types.ts +++ b/packages/core/src/lib/wallet/wallet.types.ts @@ -9,18 +9,22 @@ import { HardwareWalletConnectParams, SignAndSendTransactionParams, SignAndSendTransactionsParams, + WalletOptions, } from "./wallet"; +import isMobile from "is-mobile"; -// ----- Browser Wallet ----- // - -export interface BrowserWalletMetadata { +interface BaseWalletMetadata { id: string; - type: "browser"; + type: Type; name: string; description: string | null; iconUrl: string; } +// ----- Browser Wallet ----- // + +type BrowserWalletMetadata = BaseWalletMetadata<"browser">; + export interface BrowserWalletBehaviour { connect(): Promise>; disconnect(): Promise; @@ -31,28 +35,25 @@ export interface BrowserWalletBehaviour { export type BrowserWallet = BrowserWalletMetadata & BrowserWalletBehaviour; -export interface BrowserWalletModule< - Wallet extends BrowserWallet | BrowserWalletBehaviour = BrowserWallet -> { - id: string; - type: "browser"; - name: string; - description: string | null; - iconUrl: string; - - isAvailable(): Promise; - init(): Promise; -} +export type BrowserWalletModule< + Variation extends + | BrowserWallet + | BrowserWalletBehaviour = BrowserWalletBehaviour, + Init = Variation extends BrowserWallet + ? () => Promise + : (options: WalletOptions) => Promise +> = () => Promise< + | (BrowserWalletMetadata & { + init: Init; + }) + | null +>; // ----- Injected Wallet ----- // -export interface InjectedWalletMetadata { - id: string; - type: "injected"; - name: string; - description: string | null; - iconUrl: string; -} +type InjectedWalletMetadata = BaseWalletMetadata<"injected"> & { + downloadUrl: string; +}; export interface InjectedWalletBehaviour { connect(): Promise>; @@ -66,31 +67,25 @@ export interface InjectedWalletBehaviour { ): Promise>; } -type InjectedWallet = InjectedWalletMetadata & InjectedWalletBehaviour; - -export interface InjectedWalletModule< - Wallet extends InjectedWallet | InjectedWalletBehaviour = InjectedWallet -> { - id: string; - type: "injected"; - name: string; - description: string | null; - iconUrl: string; - - isAvailable(): Promise; - getDownloadUrl(): string; - init(): Promise; -} +export type InjectedWallet = InjectedWalletMetadata & InjectedWalletBehaviour; + +export type InjectedWalletModule< + Variation extends + | InjectedWallet + | InjectedWalletBehaviour = InjectedWalletBehaviour, + Init = Variation extends InjectedWallet + ? () => Promise + : (options: WalletOptions) => Promise +> = () => Promise< + | (InjectedWalletMetadata & { + init: Init; + }) + | null +>; // ----- Hardware Wallet ----- // -export interface HardwareWalletMetadata { - id: string; - type: "hardware"; - name: string; - description: string | null; - iconUrl: string; -} +type HardwareWalletMetadata = BaseWalletMetadata<"hardware">; export interface HardwareWalletBehaviour { connect(params: HardwareWalletConnectParams): Promise>; @@ -106,28 +101,23 @@ export interface HardwareWalletBehaviour { type HardwareWallet = HardwareWalletMetadata & HardwareWalletBehaviour; -export interface HardwareWalletModule< - Wallet extends HardwareWallet | HardwareWalletBehaviour = HardwareWallet -> { - id: string; - type: "hardware"; - name: string; - description: string | null; - iconUrl: string; - - isAvailable(): Promise; - init(): Promise; -} +export type HardwareWalletModule< + Variation extends + | HardwareWallet + | HardwareWalletBehaviour = HardwareWalletBehaviour, + Init = Variation extends HardwareWallet + ? () => Promise + : (options: WalletOptions) => Promise +> = () => Promise< + | (HardwareWalletMetadata & { + init: Init; + }) + | null +>; // ----- Bridge Wallet ----- // -export interface BridgeWalletMetadata { - id: string; - type: "bridge"; - name: string; - description: string | null; - iconUrl: string; -} +type BridgeWalletMetadata = BaseWalletMetadata<"bridge">; export interface BridgeWalletBehaviour { connect(): Promise>; @@ -143,18 +133,19 @@ export interface BridgeWalletBehaviour { type BridgeWallet = BridgeWalletMetadata & BridgeWalletBehaviour; -export interface BridgeWalletModule< - Wallet extends BridgeWallet | BridgeWalletBehaviour = BridgeWallet -> { - id: string; - type: "bridge"; - name: string; - description: string | null; - iconUrl: string; - - isAvailable(): Promise; - init(): Promise; -} +export type BridgeWalletModule< + Variation extends + | BridgeWallet + | BridgeWalletBehaviour = BridgeWalletBehaviour, + Init = Variation extends BridgeWallet + ? () => Promise + : (options: WalletOptions) => Promise +> = () => Promise< + | (BridgeWalletMetadata & { + init: Init; + }) + | null +>; type WalletModule = | BrowserWalletModule @@ -163,3 +154,56 @@ type WalletModule = | BridgeWalletModule; type Wallet = BrowserWallet | InjectedWallet | HardwareWallet | BridgeWallet; + +type WalletModuleState = + | BrowserWalletModule + | InjectedWalletModule + | HardwareWalletModule + | BridgeWalletModule; + +const MathWallet = async (): Promise => { + return { + connect: async () => [], + disconnect: async () => {}, + getAccounts: async () => [], + signAndSendTransaction: async (): any => {}, + signAndSendTransactions: async (): any => {}, + }; +}; + +interface MathWalletParams { + iconUrl: string; +} + +const setupMathWallet = (params: MathWalletParams): InjectedWalletModule => { + return async () => { + if (isMobile()) { + return null; + } + + return { + id: "math-wallet", + type: "injected", + name: "Math Wallet", + description: null, + iconUrl: "", + downloadUrl: "https://example.com", + + init: MathWallet, + }; + }; +}; + +const setupWalletModules = async (modules: Array) => { + const results: Array = []; + + for (let i = 0; i < modules.length; i += 1) { + const module = await modules[i](); + + if (!module) { + continue; + } + } + + return results; +}; From 555b42ab2b72ec4cabc81519e473da6f64c4d46c Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Fri, 29 Apr 2022 12:23:45 +0100 Subject: [PATCH 09/64] Resolved setup type issues. --- packages/core/src/lib/wallet/wallet.types.ts | 41 ++++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts index 59353addd..8f1f49d2d 100644 --- a/packages/core/src/lib/wallet/wallet.types.ts +++ b/packages/core/src/lib/wallet/wallet.types.ts @@ -49,6 +49,10 @@ export type BrowserWalletModule< | null >; +export type BrowserWalletModuleState = BrowserWalletMetadata & { + init: () => Promise; +}; + // ----- Injected Wallet ----- // type InjectedWalletMetadata = BaseWalletMetadata<"injected"> & { @@ -83,6 +87,10 @@ export type InjectedWalletModule< | null >; +export type InjectedWalletModuleState = InjectedWalletMetadata & { + init: () => Promise; +}; + // ----- Hardware Wallet ----- // type HardwareWalletMetadata = BaseWalletMetadata<"hardware">; @@ -115,6 +123,10 @@ export type HardwareWalletModule< | null >; +export type HardwareWalletModuleState = HardwareWalletMetadata & { + init: () => Promise; +}; + // ----- Bridge Wallet ----- // type BridgeWalletMetadata = BaseWalletMetadata<"bridge">; @@ -147,6 +159,10 @@ export type BridgeWalletModule< | null >; +export type BridgeWalletModuleState = BridgeWalletMetadata & { + init: () => Promise; +}; + type WalletModule = | BrowserWalletModule | InjectedWalletModule @@ -156,10 +172,10 @@ type WalletModule = type Wallet = BrowserWallet | InjectedWallet | HardwareWallet | BridgeWallet; type WalletModuleState = - | BrowserWalletModule - | InjectedWalletModule - | HardwareWalletModule - | BridgeWalletModule; + | BrowserWalletModuleState + | InjectedWalletModuleState + | HardwareWalletModuleState + | BridgeWalletModuleState; const MathWallet = async (): Promise => { return { @@ -203,7 +219,24 @@ const setupWalletModules = async (modules: Array) => { if (!module) { continue; } + + results.push({ + ...module, + init: () => { + return module.init({} as any); + }, + } as WalletModuleState); } return results; }; + +(async () => { + const modules: Array = []; + const walletModules = await setupWalletModules(modules); + const walletModule = walletModules[0]; + + if (walletModule.type === "browser") { + const wallet = await walletModule.init(); + } +})(); From 0ffa78daae4bda5813010f6885c17d52ca325e47 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Fri, 29 Apr 2022 12:42:20 +0100 Subject: [PATCH 10/64] Refactored types. --- packages/core/src/lib/wallet/wallet.types.ts | 119 ++++++++++--------- 1 file changed, 61 insertions(+), 58 deletions(-) diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts index 8f1f49d2d..4dec9e810 100644 --- a/packages/core/src/lib/wallet/wallet.types.ts +++ b/packages/core/src/lib/wallet/wallet.types.ts @@ -9,9 +9,16 @@ import { HardwareWalletConnectParams, SignAndSendTransactionParams, SignAndSendTransactionsParams, - WalletOptions, + WalletEvents, } from "./wallet"; import isMobile from "is-mobile"; +import { Options } from "../options.types"; +import { + EventEmitterService, + LoggerService, + ProviderService, + StorageService, +} from "../services"; interface BaseWalletMetadata { id: string; @@ -21,9 +28,28 @@ interface BaseWalletMetadata { iconUrl: string; } +export interface WalletOptions> { + options: Options; + metadata: Metadata; + provider: ProviderService; + emitter: EventEmitterService; + logger: LoggerService; + storage: StorageService; +} + +type BaseWalletModule< + Metadata extends BaseWalletMetadata, + Behaviour +> = () => Promise< + | (Metadata & { + init: (options: WalletOptions) => Promise; + }) + | null +>; + // ----- Browser Wallet ----- // -type BrowserWalletMetadata = BaseWalletMetadata<"browser">; +export type BrowserWalletMetadata = BaseWalletMetadata<"browser">; export interface BrowserWalletBehaviour { connect(): Promise>; @@ -35,27 +61,19 @@ export interface BrowserWalletBehaviour { export type BrowserWallet = BrowserWalletMetadata & BrowserWalletBehaviour; -export type BrowserWalletModule< - Variation extends - | BrowserWallet - | BrowserWalletBehaviour = BrowserWalletBehaviour, - Init = Variation extends BrowserWallet - ? () => Promise - : (options: WalletOptions) => Promise -> = () => Promise< - | (BrowserWalletMetadata & { - init: Init; - }) - | null +export type BrowserWalletModule = BaseWalletModule< + BrowserWalletMetadata, + BrowserWalletBehaviour >; +// TODO: Move to store types. export type BrowserWalletModuleState = BrowserWalletMetadata & { init: () => Promise; }; // ----- Injected Wallet ----- // -type InjectedWalletMetadata = BaseWalletMetadata<"injected"> & { +export type InjectedWalletMetadata = BaseWalletMetadata<"injected"> & { downloadUrl: string; }; @@ -73,27 +91,19 @@ export interface InjectedWalletBehaviour { export type InjectedWallet = InjectedWalletMetadata & InjectedWalletBehaviour; -export type InjectedWalletModule< - Variation extends - | InjectedWallet - | InjectedWalletBehaviour = InjectedWalletBehaviour, - Init = Variation extends InjectedWallet - ? () => Promise - : (options: WalletOptions) => Promise -> = () => Promise< - | (InjectedWalletMetadata & { - init: Init; - }) - | null +export type InjectedWalletModule = BaseWalletModule< + InjectedWalletMetadata, + InjectedWalletBehaviour >; +// TODO: Move to store types. export type InjectedWalletModuleState = InjectedWalletMetadata & { init: () => Promise; }; // ----- Hardware Wallet ----- // -type HardwareWalletMetadata = BaseWalletMetadata<"hardware">; +export type HardwareWalletMetadata = BaseWalletMetadata<"hardware">; export interface HardwareWalletBehaviour { connect(params: HardwareWalletConnectParams): Promise>; @@ -107,29 +117,21 @@ export interface HardwareWalletBehaviour { ): Promise>; } -type HardwareWallet = HardwareWalletMetadata & HardwareWalletBehaviour; +export type HardwareWallet = HardwareWalletMetadata & HardwareWalletBehaviour; -export type HardwareWalletModule< - Variation extends - | HardwareWallet - | HardwareWalletBehaviour = HardwareWalletBehaviour, - Init = Variation extends HardwareWallet - ? () => Promise - : (options: WalletOptions) => Promise -> = () => Promise< - | (HardwareWalletMetadata & { - init: Init; - }) - | null +export type HardwareWalletModule = BaseWalletModule< + HardwareWalletMetadata, + HardwareWalletBehaviour >; +// TODO: Move to store types. export type HardwareWalletModuleState = HardwareWalletMetadata & { init: () => Promise; }; // ----- Bridge Wallet ----- // -type BridgeWalletMetadata = BaseWalletMetadata<"bridge">; +export type BridgeWalletMetadata = BaseWalletMetadata<"bridge">; export interface BridgeWalletBehaviour { connect(): Promise>; @@ -143,40 +145,41 @@ export interface BridgeWalletBehaviour { ): Promise>; } -type BridgeWallet = BridgeWalletMetadata & BridgeWalletBehaviour; +export type BridgeWallet = BridgeWalletMetadata & BridgeWalletBehaviour; -export type BridgeWalletModule< - Variation extends - | BridgeWallet - | BridgeWalletBehaviour = BridgeWalletBehaviour, - Init = Variation extends BridgeWallet - ? () => Promise - : (options: WalletOptions) => Promise -> = () => Promise< - | (BridgeWalletMetadata & { - init: Init; - }) - | null +export type BridgeWalletModule = BaseWalletModule< + BridgeWalletMetadata, + BridgeWalletBehaviour >; +// TODO: Move to store types. export type BridgeWalletModuleState = BridgeWalletMetadata & { init: () => Promise; }; -type WalletModule = +// ----- Misc ----- // + +export type WalletModule = | BrowserWalletModule | InjectedWalletModule | HardwareWalletModule | BridgeWalletModule; -type Wallet = BrowserWallet | InjectedWallet | HardwareWallet | BridgeWallet; +export type Wallet = + | BrowserWallet + | InjectedWallet + | HardwareWallet + | BridgeWallet; -type WalletModuleState = +// TODO: Move to store types. +export type WalletModuleState = | BrowserWalletModuleState | InjectedWalletModuleState | HardwareWalletModuleState | BridgeWalletModuleState; +// ----- Implementation Tests ----- // + const MathWallet = async (): Promise => { return { connect: async () => [], From 9aea44820ee9203879609ae0339c3b59059fada8 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Fri, 29 Apr 2022 12:44:06 +0100 Subject: [PATCH 11/64] Added comment. --- packages/core/src/lib/wallet/wallet.types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts index 4dec9e810..60aabf26d 100644 --- a/packages/core/src/lib/wallet/wallet.types.ts +++ b/packages/core/src/lib/wallet/wallet.types.ts @@ -219,6 +219,7 @@ const setupWalletModules = async (modules: Array) => { for (let i = 0; i < modules.length; i += 1) { const module = await modules[i](); + // Filter out wallets that aren't available. if (!module) { continue; } From a2ac6cee78da58b191287a2a30f78defc31df30e Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Fri, 29 Apr 2022 13:07:05 +0100 Subject: [PATCH 12/64] Added comment. --- packages/core/src/lib/wallet/wallet.types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts index 60aabf26d..8c663bfb9 100644 --- a/packages/core/src/lib/wallet/wallet.types.ts +++ b/packages/core/src/lib/wallet/wallet.types.ts @@ -2,6 +2,7 @@ // - Shouldn't initialise the wallet until we want to connect or already connected. // - We need the type alongside the methods to help with type checking. // - We need getDownloadUrl and isAvailable outside the initialisation of a wallet. +// - selector.wallet can remain sync and handle rejecting signing for unselected wallets. import { providers } from "near-api-js"; import { AccountState } from "../store.types"; From 5375053adeb5d4d39b22ba9bd8a6dee6f00b7297 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Fri, 29 Apr 2022 13:58:57 +0100 Subject: [PATCH 13/64] More refactoring. --- packages/core/src/index.ts | 44 ++++++++++++++++---- packages/core/src/lib/wallet/wallet.types.ts | 35 +++++++++++++--- packages/math-wallet/src/lib/math-wallet.ts | 43 ++++++++++--------- 3 files changed, 88 insertions(+), 34 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 713070b3c..958e1f5bb 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -10,22 +10,52 @@ export { Optional } from "./lib/utils.types"; export { WalletSelectorState, - WalletState, + // WalletModuleState, AccountState, } from "./lib/store.types"; export { - Wallet, - WalletType, - WalletMetadata, - WalletBehaviour, WalletModule, - WalletBehaviourFactory, + Wallet, + WalletModuleState, + BrowserWalletMetadata, + BrowserWalletBehaviour, + BrowserWalletBehaviourFactory, + BrowserWalletModule, BrowserWallet, + BrowserWalletModuleState, + InjectedWalletMetadata, + InjectedWalletBehaviour, + InjectedWalletBehaviourFactory, + InjectedWalletModule, InjectedWallet, + InjectedWalletModuleState, + HardwareWalletMetadata, + HardwareWalletBehaviour, + HardwareWalletBehaviourFactory, + HardwareWalletModule, HardwareWallet, - HardwareWalletConnectParams, + HardwareWalletModuleState, + BridgeWalletMetadata, + BridgeWalletBehaviour, + BridgeWalletBehaviourFactory, + BridgeWalletModule, BridgeWallet, + BridgeWalletModuleState, +} from "./lib/wallet/wallet.types"; + +export { + // Wallet, + WalletType, + // WalletMetadata, + // WalletBehaviour, + // WalletModule, + // WalletBehaviourFactory, + // BrowserWallet, + // InjectedWallet, + // HardwareWallet, + HardwareWalletConnectParams, + // BridgeWallet, Transaction, Action, ActionType, diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts index 8c663bfb9..5deeb9b2d 100644 --- a/packages/core/src/lib/wallet/wallet.types.ts +++ b/packages/core/src/lib/wallet/wallet.types.ts @@ -38,12 +38,17 @@ export interface WalletOptions> { storage: StorageService; } +type BaseWalletBehaviourFactory< + Metadata extends BaseWalletMetadata, + Behaviour +> = (options: WalletOptions) => Promise; + type BaseWalletModule< Metadata extends BaseWalletMetadata, Behaviour > = () => Promise< | (Metadata & { - init: (options: WalletOptions) => Promise; + init: BaseWalletBehaviourFactory; }) | null >; @@ -60,13 +65,18 @@ export interface BrowserWalletBehaviour { signAndSendTransactions(params: SignAndSendTransactionsParams): Promise; } -export type BrowserWallet = BrowserWalletMetadata & BrowserWalletBehaviour; +export type BrowserWalletBehaviourFactory = BaseWalletBehaviourFactory< + BrowserWalletMetadata, + BrowserWalletBehaviour +>; export type BrowserWalletModule = BaseWalletModule< BrowserWalletMetadata, BrowserWalletBehaviour >; +export type BrowserWallet = BrowserWalletMetadata & BrowserWalletBehaviour; + // TODO: Move to store types. export type BrowserWalletModuleState = BrowserWalletMetadata & { init: () => Promise; @@ -90,13 +100,18 @@ export interface InjectedWalletBehaviour { ): Promise>; } -export type InjectedWallet = InjectedWalletMetadata & InjectedWalletBehaviour; +export type InjectedWalletBehaviourFactory = BaseWalletBehaviourFactory< + InjectedWalletMetadata, + InjectedWalletBehaviour +>; export type InjectedWalletModule = BaseWalletModule< InjectedWalletMetadata, InjectedWalletBehaviour >; +export type InjectedWallet = InjectedWalletMetadata & InjectedWalletBehaviour; + // TODO: Move to store types. export type InjectedWalletModuleState = InjectedWalletMetadata & { init: () => Promise; @@ -118,13 +133,18 @@ export interface HardwareWalletBehaviour { ): Promise>; } -export type HardwareWallet = HardwareWalletMetadata & HardwareWalletBehaviour; +export type HardwareWalletBehaviourFactory = BaseWalletBehaviourFactory< + HardwareWalletMetadata, + HardwareWalletBehaviour +>; export type HardwareWalletModule = BaseWalletModule< HardwareWalletMetadata, HardwareWalletBehaviour >; +export type HardwareWallet = HardwareWalletMetadata & HardwareWalletBehaviour; + // TODO: Move to store types. export type HardwareWalletModuleState = HardwareWalletMetadata & { init: () => Promise; @@ -146,13 +166,18 @@ export interface BridgeWalletBehaviour { ): Promise>; } -export type BridgeWallet = BridgeWalletMetadata & BridgeWalletBehaviour; +export type BridgeWalletBehaviourFactory = BaseWalletBehaviourFactory< + BridgeWalletMetadata, + BridgeWalletBehaviour +>; export type BridgeWalletModule = BaseWalletModule< BridgeWalletMetadata, BridgeWalletBehaviour >; +export type BridgeWallet = BridgeWalletMetadata & BridgeWalletBehaviour; + // TODO: Move to store types. export type BridgeWalletModuleState = BridgeWalletMetadata & { init: () => Promise; diff --git a/packages/math-wallet/src/lib/math-wallet.ts b/packages/math-wallet/src/lib/math-wallet.ts index 23aa30169..3b17dc68c 100644 --- a/packages/math-wallet/src/lib/math-wallet.ts +++ b/packages/math-wallet/src/lib/math-wallet.ts @@ -1,10 +1,9 @@ import { transactions as nearTransactions, utils } from "near-api-js"; import isMobile from "is-mobile"; import { - WalletModule, - WalletBehaviourFactory, + InjectedWalletModule, + InjectedWalletBehaviourFactory, AccountState, - InjectedWallet, transformActions, waitFor, errors, @@ -22,7 +21,7 @@ export interface MathWalletParams { iconUrl?: string; } -const MathWallet: WalletBehaviourFactory = async ({ +const MathWallet: InjectedWalletBehaviourFactory = async ({ options, metadata, provider, @@ -101,14 +100,6 @@ const MathWallet: WalletBehaviourFactory = async ({ }; 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(); @@ -240,15 +231,23 @@ const MathWallet: WalletBehaviourFactory = async ({ }; }; -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 = {}): InjectedWalletModule => { + return async () => { + if (!isMobile()) { + return null; + } + + return { + id: "math-wallet", + type: "injected", + name: "Math Wallet", + description: null, + iconUrl, + downloadUrl: + "https://chrome.google.com/webstore/detail/math-wallet/afbcbjpbpfadlkmhmclhkeeodmamcflc", + wallet: MathWallet, + }; }; -} +}; From 8f725927c9c34d4f8924522f4f6d9566d4ddaafa Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Fri, 29 Apr 2022 14:16:07 +0100 Subject: [PATCH 14/64] More experiementing with setup and types. --- packages/core/src/lib/store.types.ts | 12 ++--- packages/core/src/lib/utils.ts | 10 +++++ packages/core/src/lib/wallet/wallet.types.ts | 47 +++++--------------- 3 files changed, 24 insertions(+), 45 deletions(-) create mode 100644 packages/core/src/lib/utils.ts diff --git a/packages/core/src/lib/store.types.ts b/packages/core/src/lib/store.types.ts index b9d76237b..38cfea0c9 100644 --- a/packages/core/src/lib/store.types.ts +++ b/packages/core/src/lib/store.types.ts @@ -1,19 +1,13 @@ import { BehaviorSubject } from "rxjs"; -import { Wallet, WalletBehaviour, WalletMetadata } from "./wallet"; +import { WalletMetadata } from "./wallet/wallet.types"; export interface AccountState { accountId: string; } -// TODO: Make this WalletModule without "wallet" and "isAvailable"? -export type WalletModuleState = - WalletMetadata & { - wallet(): Promise>; - }; - export interface WalletSelectorState { - modules: Array; + modules: Array; accounts: Array; selectedWalletId: string | null; } @@ -22,7 +16,7 @@ export type WalletSelectorAction = | { type: "SETUP_WALLET_MODULES"; payload: { - modules: Array; + modules: Array; accounts: Array; selectedWalletId: string | null; }; diff --git a/packages/core/src/lib/utils.ts b/packages/core/src/lib/utils.ts new file mode 100644 index 000000000..ccd5c3941 --- /dev/null +++ b/packages/core/src/lib/utils.ts @@ -0,0 +1,10 @@ +export const omit = ( + obj: Value, + keys: Array +): Omit => { + const result = { ...obj }; + + keys.forEach((key) => delete result[key]); + + return result; +}; diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts index 5deeb9b2d..42e84b76c 100644 --- a/packages/core/src/lib/wallet/wallet.types.ts +++ b/packages/core/src/lib/wallet/wallet.types.ts @@ -20,6 +20,7 @@ import { ProviderService, StorageService, } from "../services"; +import { omit } from "../utils"; interface BaseWalletMetadata { id: string; @@ -77,11 +78,6 @@ export type BrowserWalletModule = BaseWalletModule< export type BrowserWallet = BrowserWalletMetadata & BrowserWalletBehaviour; -// TODO: Move to store types. -export type BrowserWalletModuleState = BrowserWalletMetadata & { - init: () => Promise; -}; - // ----- Injected Wallet ----- // export type InjectedWalletMetadata = BaseWalletMetadata<"injected"> & { @@ -112,11 +108,6 @@ export type InjectedWalletModule = BaseWalletModule< export type InjectedWallet = InjectedWalletMetadata & InjectedWalletBehaviour; -// TODO: Move to store types. -export type InjectedWalletModuleState = InjectedWalletMetadata & { - init: () => Promise; -}; - // ----- Hardware Wallet ----- // export type HardwareWalletMetadata = BaseWalletMetadata<"hardware">; @@ -145,11 +136,6 @@ export type HardwareWalletModule = BaseWalletModule< export type HardwareWallet = HardwareWalletMetadata & HardwareWalletBehaviour; -// TODO: Move to store types. -export type HardwareWalletModuleState = HardwareWalletMetadata & { - init: () => Promise; -}; - // ----- Bridge Wallet ----- // export type BridgeWalletMetadata = BaseWalletMetadata<"bridge">; @@ -178,13 +164,14 @@ export type BridgeWalletModule = BaseWalletModule< export type BridgeWallet = BridgeWalletMetadata & BridgeWalletBehaviour; -// TODO: Move to store types. -export type BridgeWalletModuleState = BridgeWalletMetadata & { - init: () => Promise; -}; - // ----- Misc ----- // +export type WalletMetadata = + | BrowserWalletMetadata + | InjectedWalletMetadata + | HardwareWalletMetadata + | BridgeWalletMetadata; + export type WalletModule = | BrowserWalletModule | InjectedWalletModule @@ -197,13 +184,6 @@ export type Wallet = | HardwareWallet | BridgeWallet; -// TODO: Move to store types. -export type WalletModuleState = - | BrowserWalletModuleState - | InjectedWalletModuleState - | HardwareWalletModuleState - | BridgeWalletModuleState; - // ----- Implementation Tests ----- // const MathWallet = async (): Promise => { @@ -240,7 +220,7 @@ const setupMathWallet = (params: MathWalletParams): InjectedWalletModule => { }; const setupWalletModules = async (modules: Array) => { - const results: Array = []; + const results: Array = []; for (let i = 0; i < modules.length; i += 1) { const module = await modules[i](); @@ -250,12 +230,7 @@ const setupWalletModules = async (modules: Array) => { continue; } - results.push({ - ...module, - init: () => { - return module.init({} as any); - }, - } as WalletModuleState); + results.push(omit(module, ["init"]) as WalletMetadata); } return results; @@ -266,7 +241,7 @@ const setupWalletModules = async (modules: Array) => { const walletModules = await setupWalletModules(modules); const walletModule = walletModules[0]; - if (walletModule.type === "browser") { - const wallet = await walletModule.init(); + if (walletModule.type === "injected") { + console.log(walletModule.downloadUrl); } })(); From 0087147843272674e2b62c5e062c74c8ba337e8c Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Fri, 29 Apr 2022 14:23:54 +0100 Subject: [PATCH 15/64] Fixed Modal types. --- packages/core/src/index.ts | 8 ++------ packages/core/src/lib/modal/components/Modal.tsx | 7 +++---- .../core/src/lib/modal/components/WalletNotInstalled.tsx | 6 +++--- packages/core/src/lib/modal/components/WalletOptions.tsx | 8 ++++---- packages/core/src/lib/wallet/wallet.types.ts | 2 ++ 5 files changed, 14 insertions(+), 17 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 958e1f5bb..a8223c5ba 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -17,36 +17,32 @@ export { export { WalletModule, Wallet, - WalletModuleState, + WalletType, BrowserWalletMetadata, BrowserWalletBehaviour, BrowserWalletBehaviourFactory, BrowserWalletModule, BrowserWallet, - BrowserWalletModuleState, InjectedWalletMetadata, InjectedWalletBehaviour, InjectedWalletBehaviourFactory, InjectedWalletModule, InjectedWallet, - InjectedWalletModuleState, HardwareWalletMetadata, HardwareWalletBehaviour, HardwareWalletBehaviourFactory, HardwareWalletModule, HardwareWallet, - HardwareWalletModuleState, BridgeWalletMetadata, BridgeWalletBehaviour, BridgeWalletBehaviourFactory, BridgeWalletModule, BridgeWallet, - BridgeWalletModuleState, } from "./lib/wallet/wallet.types"; export { // Wallet, - WalletType, + // WalletType, // WalletMetadata, // WalletBehaviour, // WalletModule, diff --git a/packages/core/src/lib/modal/components/Modal.tsx b/packages/core/src/lib/modal/components/Modal.tsx index 5daf7b5d8..e60fd07ac 100644 --- a/packages/core/src/lib/modal/components/Modal.tsx +++ b/packages/core/src/lib/modal/components/Modal.tsx @@ -1,6 +1,6 @@ import React, { MouseEvent, useCallback, useEffect, useState } from "react"; -import { Wallet } from "../../wallet"; +import { WalletMetadata } from "../../wallet/wallet.types"; import { WalletSelectorModal, ModalOptions, Theme } from "../modal.types"; import { WalletSelector } from "../../wallet-selector.types"; import { ModalRouteName } from "./Modal.types"; @@ -38,9 +38,8 @@ export const Modal: React.FC = ({ hide, }) => { const [routeName, setRouteName] = useState("WalletOptions"); - const [notInstalledWallet, setNotInstalledWallet] = useState( - null - ); + const [notInstalledWallet, setNotInstalledWallet] = + useState(null); const [alertMessage, setAlertMessage] = useState(null); useEffect(() => { diff --git a/packages/core/src/lib/modal/components/WalletNotInstalled.tsx b/packages/core/src/lib/modal/components/WalletNotInstalled.tsx index 316fa99d3..924a07213 100644 --- a/packages/core/src/lib/modal/components/WalletNotInstalled.tsx +++ b/packages/core/src/lib/modal/components/WalletNotInstalled.tsx @@ -1,9 +1,9 @@ import React from "react"; -import { Wallet } from "../../wallet"; +import { WalletMetadata } from "../../wallet/wallet.types"; interface WalletNotInstalledProps { - notInstalledWallet: Wallet; + notInstalledWallet: WalletMetadata; onBack: () => void; } @@ -34,7 +34,7 @@ export const WalletNotInstalled: React.FC = ({ return; } - window.open(notInstalledWallet.getDownloadUrl(), "_blank"); + window.open(notInstalledWallet.downloadUrl, "_blank"); }} > {`Open ${notInstalledWallet.name}`} diff --git a/packages/core/src/lib/modal/components/WalletOptions.tsx b/packages/core/src/lib/modal/components/WalletOptions.tsx index 511cf8f2a..8a1a5e770 100644 --- a/packages/core/src/lib/modal/components/WalletOptions.tsx +++ b/packages/core/src/lib/modal/components/WalletOptions.tsx @@ -1,6 +1,6 @@ import React, { Fragment, useEffect, useState } from "react"; -import { WalletModuleState } from "../../store.types"; +import { WalletMetadata } from "../../wallet/wallet.types"; import { WalletSelector } from "../../wallet-selector.types"; import { ModalOptions, WalletSelectorModal } from "../modal.types"; import { logger } from "../../services"; @@ -10,7 +10,7 @@ interface WalletOptionsProps { // TODO: Remove omit once modal is a separate package. selector: Omit; options?: ModalOptions; - onWalletNotInstalled: (module: WalletModuleState) => void; + onWalletNotInstalled: (module: WalletMetadata) => void; onConnectHardwareWallet: () => void; onConnected: () => void; onError: (message: string) => void; @@ -26,7 +26,7 @@ export const WalletOptions: React.FC = ({ }) => { const [connecting, setConnecting] = useState(false); const [walletInfoVisible, setWalletInfoVisible] = useState(false); - const [modules, setModules] = useState>([]); + const [modules, setModules] = useState>([]); useEffect(() => { const subscription = selector.store.observable.subscribe((state) => { @@ -37,7 +37,7 @@ export const WalletOptions: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const handleWalletClick = (module: WalletModuleState) => () => { + const handleWalletClick = (module: WalletMetadata) => () => { if (connecting) { return; } diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts index 42e84b76c..dc5543461 100644 --- a/packages/core/src/lib/wallet/wallet.types.ts +++ b/packages/core/src/lib/wallet/wallet.types.ts @@ -184,6 +184,8 @@ export type Wallet = | HardwareWallet | BridgeWallet; +export type WalletType = Wallet["type"]; + // ----- Implementation Tests ----- // const MathWallet = async (): Promise => { From 812eeac52ce9d59fc633ea39c600c6af05c91505 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Fri, 29 Apr 2022 14:55:18 +0100 Subject: [PATCH 16/64] Initial attempt at refactoring the controller. --- .../src/contexts/WalletSelectorContext.tsx | 2 +- packages/core/src/lib/store.ts | 6 +- packages/core/src/lib/store.types.ts | 4 +- packages/core/src/lib/wallet-modules.ts | 41 ++++++++ packages/core/src/lib/wallet-selector.ts | 99 +++++++++---------- .../core/src/lib/wallet-selector.types.ts | 11 ++- packages/core/src/lib/wallet/wallet.types.ts | 33 +++++-- 7 files changed, 128 insertions(+), 68 deletions(-) create mode 100644 packages/core/src/lib/wallet-modules.ts diff --git a/examples/react/src/contexts/WalletSelectorContext.tsx b/examples/react/src/contexts/WalletSelectorContext.tsx index e8c19cce3..cc67374e1 100644 --- a/examples/react/src/contexts/WalletSelectorContext.tsx +++ b/examples/react/src/contexts/WalletSelectorContext.tsx @@ -61,7 +61,7 @@ export const WalletSelectorContextProvider: React.FC = ({ children }) => { network: "testnet", contractId: "guest-book.testnet", debug: true, - modules: [ + wallets: [ // setupNearWallet(), // setupSender(), setupMathWallet(), diff --git a/packages/core/src/lib/store.ts b/packages/core/src/lib/store.ts index f54852920..335b35f1d 100644 --- a/packages/core/src/lib/store.ts +++ b/packages/core/src/lib/store.ts @@ -6,6 +6,8 @@ import { WalletSelectorState, WalletSelectorAction, } from "./store.types"; +import { omit } from "./utils"; +import { WalletMetadata } from "./wallet/wallet.types"; const reducer = ( state: WalletSelectorState, @@ -19,7 +21,9 @@ const reducer = ( return { ...state, - modules, + modules: modules.map((module) => { + return omit(module, ["init"]) as WalletMetadata; + }), accounts, selectedWalletId, }; diff --git a/packages/core/src/lib/store.types.ts b/packages/core/src/lib/store.types.ts index 38cfea0c9..fd9abc8af 100644 --- a/packages/core/src/lib/store.types.ts +++ b/packages/core/src/lib/store.types.ts @@ -1,6 +1,6 @@ import { BehaviorSubject } from "rxjs"; -import { WalletMetadata } from "./wallet/wallet.types"; +import { WalletModule, WalletMetadata } from "./wallet/wallet.types"; export interface AccountState { accountId: string; @@ -16,7 +16,7 @@ export type WalletSelectorAction = | { type: "SETUP_WALLET_MODULES"; payload: { - modules: Array; + modules: Array; accounts: Array; selectedWalletId: string | null; }; diff --git a/packages/core/src/lib/wallet-modules.ts b/packages/core/src/lib/wallet-modules.ts new file mode 100644 index 000000000..d0de002d9 --- /dev/null +++ b/packages/core/src/lib/wallet-modules.ts @@ -0,0 +1,41 @@ +import { Options } from "./options.types"; +import { Store } from "./store.types"; +import { EventEmitter } from "./services"; +import { WalletSelectorEvents } from "./wallet-selector.types"; +import { WalletModule, WalletModuleFactory } from "./wallet/wallet.types"; + +interface WalletModulesParams { + factories: Array; + options: Options; + store: Store; + emitter: EventEmitter; +} + +export const setupWalletModules = async ({ + factories, + options, + store, + emitter, +}: WalletModulesParams) => { + const modules: Array = []; + + 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(module); + } + + store.dispatch({ + type: "SETUP_WALLET_MODULES", + payload: { + modules, + accounts: [], + selectedWalletId: null, + }, + }); +}; diff --git a/packages/core/src/lib/wallet-selector.ts b/packages/core/src/lib/wallet-selector.ts index 14f1c8caf..107e1d0b4 100644 --- a/packages/core/src/lib/wallet-selector.ts +++ b/packages/core/src/lib/wallet-selector.ts @@ -10,23 +10,23 @@ import { WalletSelectorModal } from "./modal/modal.types"; import { setupModal } from "./modal/modal"; import { EventEmitter, Logger } from "./services"; import { Wallet, WalletBehaviour } from "./wallet"; +import { setupWalletModules } from "./wallet-modules"; 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( + + await setupWalletModules({ + factories: params.wallets, options, - params.modules, store, - emitter - ); - - Logger.debug = options.debug; - - await controller.init(); + emitter, + }); // TODO: Remove omit once modal is a separate package. const selector: Omit = { @@ -40,50 +40,45 @@ export const setupWalletSelector = async ( return Boolean(accounts.length); }, options, - wallet: (walletId?: string) => { - const module = controller.getModule(walletId); - - if (!module) { - if (walletId) { - throw new Error("Invalid wallet id"); - } - - throw new Error("No wallet selected"); - } - - return { - isAvailable: async () => { - const wallet = await module.wallet(); - - return wallet.isAvailable(); - }, - connect: async (args: never) => { - const wallet = await module.wallet(); - - return wallet.connect(args); - }, - disconnect: async () => { - const wallet = await module.wallet(); - - return wallet.disconnect(); - }, - getAccounts: async () => { - const wallet = await module.wallet(); - - return wallet.getAccounts(); - }, - signAndSendTransaction: async (args: never) => { - const wallet = await module.wallet(); - - return wallet.signAndSendTransaction(args); - }, - signAndSendTransactions: async (args: never) => { - const wallet = await module.wallet(); - - return wallet.signAndSendTransactions(args); - }, - } as WalletBehaviour; - }, + // wallet: (walletId?: string) => { + // const module = null; // controller.getModule(walletId); + // + // if (!module) { + // if (walletId) { + // throw new Error("Invalid wallet id"); + // } + // + // throw new Error("No wallet selected"); + // } + // + // return { + // connect: async (args: never) => { + // const wallet = await module.wallet(); + // + // return wallet.connect(args); + // }, + // disconnect: async () => { + // const wallet = await module.wallet(); + // + // return wallet.disconnect(); + // }, + // getAccounts: async () => { + // const wallet = await module.wallet(); + // + // return wallet.getAccounts(); + // }, + // signAndSendTransaction: async (args: never) => { + // const wallet = await module.wallet(); + // + // return wallet.signAndSendTransaction(args); + // }, + // signAndSendTransactions: async (args: never) => { + // const wallet = await module.wallet(); + // + // return wallet.signAndSendTransactions(args); + // }, + // } as WalletBehaviour; + // }, on: (eventName, callback) => { return emitter.on(eventName, callback); }, diff --git a/packages/core/src/lib/wallet-selector.types.ts b/packages/core/src/lib/wallet-selector.types.ts index 5af023478..2b8c42782 100644 --- a/packages/core/src/lib/wallet-selector.types.ts +++ b/packages/core/src/lib/wallet-selector.types.ts @@ -1,6 +1,7 @@ import { Observable } from "rxjs"; -import { WalletModule, Wallet, WalletBehaviour } from "./wallet"; +// import { Wallet, WalletBehaviour } from "./wallet"; +import { 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 +11,7 @@ export interface WalletSelectorParams { network: NetworkId | Network; contractId: string; methodNames?: Array; - modules: Array; + wallets: Array; ui?: ModalOptions; debug?: boolean; } @@ -30,9 +31,9 @@ export interface WalletSelector extends WalletSelectorModal { options: Options; connected: boolean; - wallet( - walletId?: string - ): WalletBehaviour; + // wallet( + // walletId?: string + // ): WalletBehaviour; on( eventName: EventName, diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts index dc5543461..db1c4bc60 100644 --- a/packages/core/src/lib/wallet/wallet.types.ts +++ b/packages/core/src/lib/wallet/wallet.types.ts @@ -44,15 +44,14 @@ type BaseWalletBehaviourFactory< Behaviour > = (options: WalletOptions) => Promise; +type BaseWalletModuleFactory = () => Promise; + type BaseWalletModule< Metadata extends BaseWalletMetadata, Behaviour -> = () => Promise< - | (Metadata & { - init: BaseWalletBehaviourFactory; - }) - | null ->; +> = Metadata & { + init: BaseWalletBehaviourFactory; +}; // ----- Browser Wallet ----- // @@ -76,6 +75,9 @@ export type BrowserWalletModule = BaseWalletModule< BrowserWalletBehaviour >; +export type BrowserWalletModuleFactory = + BaseWalletModuleFactory; + export type BrowserWallet = BrowserWalletMetadata & BrowserWalletBehaviour; // ----- Injected Wallet ----- // @@ -106,6 +108,9 @@ export type InjectedWalletModule = BaseWalletModule< InjectedWalletBehaviour >; +export type InjectedWalletModuleFactory = + BaseWalletModuleFactory; + export type InjectedWallet = InjectedWalletMetadata & InjectedWalletBehaviour; // ----- Hardware Wallet ----- // @@ -134,6 +139,9 @@ export type HardwareWalletModule = BaseWalletModule< HardwareWalletBehaviour >; +export type HardwareWalletModuleFactory = + BaseWalletModuleFactory; + export type HardwareWallet = HardwareWalletMetadata & HardwareWalletBehaviour; // ----- Bridge Wallet ----- // @@ -162,6 +170,9 @@ export type BridgeWalletModule = BaseWalletModule< BridgeWalletBehaviour >; +export type BridgetWalletModuleFactory = + BaseWalletModuleFactory; + export type BridgeWallet = BridgeWalletMetadata & BridgeWalletBehaviour; // ----- Misc ----- // @@ -178,6 +189,12 @@ export type WalletModule = | HardwareWalletModule | BridgeWalletModule; +export type WalletModuleFactory = + | BrowserWalletModuleFactory + | InjectedWalletModuleFactory + | HardwareWalletModuleFactory + | BridgetWalletModuleFactory; + export type Wallet = | BrowserWallet | InjectedWallet @@ -202,7 +219,9 @@ interface MathWalletParams { iconUrl: string; } -const setupMathWallet = (params: MathWalletParams): InjectedWalletModule => { +const setupMathWallet = ( + params: MathWalletParams +): InjectedWalletModuleFactory => { return async () => { if (isMobile()) { return null; From 0acfaa8cdb95a1ad16ece154f57a00bc3d0448d1 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Fri, 29 Apr 2022 15:10:05 +0100 Subject: [PATCH 17/64] More refactoring. --- examples/react/src/contexts/WalletSelectorContext.tsx | 2 +- packages/core/src/index.ts | 4 ++++ packages/core/src/lib/wallet-modules.ts | 2 ++ packages/core/src/lib/wallet-selector.ts | 5 ++--- packages/core/src/lib/wallet-selector.types.ts | 2 +- packages/math-wallet/src/lib/math-wallet.ts | 8 ++++---- 6 files changed, 14 insertions(+), 9 deletions(-) diff --git a/examples/react/src/contexts/WalletSelectorContext.tsx b/examples/react/src/contexts/WalletSelectorContext.tsx index cc67374e1..e8c19cce3 100644 --- a/examples/react/src/contexts/WalletSelectorContext.tsx +++ b/examples/react/src/contexts/WalletSelectorContext.tsx @@ -61,7 +61,7 @@ export const WalletSelectorContextProvider: React.FC = ({ children }) => { network: "testnet", contractId: "guest-book.testnet", debug: true, - wallets: [ + modules: [ // setupNearWallet(), // setupSender(), setupMathWallet(), diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a8223c5ba..218f0fbbc 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -22,21 +22,25 @@ export { BrowserWalletBehaviour, BrowserWalletBehaviourFactory, BrowserWalletModule, + BrowserWalletModuleFactory, BrowserWallet, InjectedWalletMetadata, InjectedWalletBehaviour, InjectedWalletBehaviourFactory, InjectedWalletModule, + InjectedWalletModuleFactory, InjectedWallet, HardwareWalletMetadata, HardwareWalletBehaviour, HardwareWalletBehaviourFactory, HardwareWalletModule, + HardwareWalletModuleFactory, HardwareWallet, BridgeWalletMetadata, BridgeWalletBehaviour, BridgeWalletBehaviourFactory, BridgeWalletModule, + BridgetWalletModuleFactory, BridgeWallet, } from "./lib/wallet/wallet.types"; diff --git a/packages/core/src/lib/wallet-modules.ts b/packages/core/src/lib/wallet-modules.ts index d0de002d9..f448b236d 100644 --- a/packages/core/src/lib/wallet-modules.ts +++ b/packages/core/src/lib/wallet-modules.ts @@ -38,4 +38,6 @@ export const setupWalletModules = async ({ selectedWalletId: null, }, }); + + return modules; }; diff --git a/packages/core/src/lib/wallet-selector.ts b/packages/core/src/lib/wallet-selector.ts index 107e1d0b4..afbeeb233 100644 --- a/packages/core/src/lib/wallet-selector.ts +++ b/packages/core/src/lib/wallet-selector.ts @@ -20,9 +20,8 @@ export const setupWalletSelector = async ( const emitter = new EventEmitter(); const store = createStore(); - - await setupWalletModules({ - factories: params.wallets, + const modules = await setupWalletModules({ + factories: params.modules, options, store, emitter, diff --git a/packages/core/src/lib/wallet-selector.types.ts b/packages/core/src/lib/wallet-selector.types.ts index 2b8c42782..3cab045e7 100644 --- a/packages/core/src/lib/wallet-selector.types.ts +++ b/packages/core/src/lib/wallet-selector.types.ts @@ -11,7 +11,7 @@ export interface WalletSelectorParams { network: NetworkId | Network; contractId: string; methodNames?: Array; - wallets: Array; + modules: Array; ui?: ModalOptions; debug?: boolean; } diff --git a/packages/math-wallet/src/lib/math-wallet.ts b/packages/math-wallet/src/lib/math-wallet.ts index 3b17dc68c..f1f7e2a0c 100644 --- a/packages/math-wallet/src/lib/math-wallet.ts +++ b/packages/math-wallet/src/lib/math-wallet.ts @@ -1,7 +1,7 @@ import { transactions as nearTransactions, utils } from "near-api-js"; import isMobile from "is-mobile"; import { - InjectedWalletModule, + InjectedWalletModuleFactory, InjectedWalletBehaviourFactory, AccountState, transformActions, @@ -233,9 +233,9 @@ const MathWallet: InjectedWalletBehaviourFactory = async ({ export const setupMathWallet = ({ iconUrl = "./assets/math-wallet-icon.png", -}: MathWalletParams = {}): InjectedWalletModule => { +}: MathWalletParams = {}): InjectedWalletModuleFactory => { return async () => { - if (!isMobile()) { + if (isMobile()) { return null; } @@ -247,7 +247,7 @@ export const setupMathWallet = ({ iconUrl, downloadUrl: "https://chrome.google.com/webstore/detail/math-wallet/afbcbjpbpfadlkmhmclhkeeodmamcflc", - wallet: MathWallet, + init: MathWallet, }; }; }; From 8dcc1f63a5b95ccfb192beb8086582a9fbc8ed36 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Fri, 29 Apr 2022 15:47:05 +0100 Subject: [PATCH 18/64] Initial approach to caching init method. --- packages/core/src/lib/store.ts | 6 +-- packages/core/src/lib/store.types.ts | 4 +- packages/core/src/lib/wallet-modules.ts | 52 +++++++++++++++++++++--- packages/core/src/lib/wallet-selector.ts | 5 +-- 4 files changed, 52 insertions(+), 15 deletions(-) diff --git a/packages/core/src/lib/store.ts b/packages/core/src/lib/store.ts index 335b35f1d..f54852920 100644 --- a/packages/core/src/lib/store.ts +++ b/packages/core/src/lib/store.ts @@ -6,8 +6,6 @@ import { WalletSelectorState, WalletSelectorAction, } from "./store.types"; -import { omit } from "./utils"; -import { WalletMetadata } from "./wallet/wallet.types"; const reducer = ( state: WalletSelectorState, @@ -21,9 +19,7 @@ const reducer = ( return { ...state, - modules: modules.map((module) => { - return omit(module, ["init"]) as WalletMetadata; - }), + modules, accounts, selectedWalletId, }; diff --git a/packages/core/src/lib/store.types.ts b/packages/core/src/lib/store.types.ts index fd9abc8af..38cfea0c9 100644 --- a/packages/core/src/lib/store.types.ts +++ b/packages/core/src/lib/store.types.ts @@ -1,6 +1,6 @@ import { BehaviorSubject } from "rxjs"; -import { WalletModule, WalletMetadata } from "./wallet/wallet.types"; +import { WalletMetadata } from "./wallet/wallet.types"; export interface AccountState { accountId: string; @@ -16,7 +16,7 @@ export type WalletSelectorAction = | { type: "SETUP_WALLET_MODULES"; payload: { - modules: Array; + modules: Array; accounts: Array; selectedWalletId: string | null; }; diff --git a/packages/core/src/lib/wallet-modules.ts b/packages/core/src/lib/wallet-modules.ts index f448b236d..6ff4147bd 100644 --- a/packages/core/src/lib/wallet-modules.ts +++ b/packages/core/src/lib/wallet-modules.ts @@ -1,8 +1,15 @@ import { Options } from "./options.types"; import { Store } from "./store.types"; -import { EventEmitter } from "./services"; +import { logger, storage, Provider, EventEmitter } from "./services"; import { WalletSelectorEvents } from "./wallet-selector.types"; -import { WalletModule, WalletModuleFactory } from "./wallet/wallet.types"; +import { + Wallet, + WalletMetadata, + WalletModule, + WalletModuleFactory, +} from "./wallet/wallet.types"; +import { omit } from "./utils"; +import { WalletEvents } from "./wallet"; interface WalletModulesParams { factories: Array; @@ -18,6 +25,7 @@ export const setupWalletModules = async ({ emitter, }: WalletModulesParams) => { const modules: Array = []; + const instances: Record = {}; for (let i = 0; i < factories.length; i += 1) { const module = await factories[i](); @@ -30,14 +38,48 @@ export const setupWalletModules = async ({ modules.push(module); } + const getWallet = async (id: string) => { + let instance = instances[id]; + + if (instance) { + return instances[id]; + } + + const module = modules.find((x) => x.id === id); + + if (!module) { + return null; + } + + const metadata = omit(module, ["init"]) as WalletMetadata; + const walletEmitter = new EventEmitter(); + const provider = new Provider(options.network.nodeUrl); + + instance = { + ...metadata, + ...(await module.init({ + options, + metadata: metadata as never, + provider, + emitter: walletEmitter, + logger, + storage, + })), + } as Wallet; + + instances[id] = instance; + + return instance; + }; + store.dispatch({ type: "SETUP_WALLET_MODULES", payload: { - modules, + modules: modules.map((module) => { + return omit(module, ["init"]) as WalletMetadata; + }), accounts: [], selectedWalletId: null, }, }); - - return modules; }; diff --git a/packages/core/src/lib/wallet-selector.ts b/packages/core/src/lib/wallet-selector.ts index afbeeb233..96fae1e24 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 { @@ -9,7 +8,6 @@ import { import { WalletSelectorModal } from "./modal/modal.types"; import { setupModal } from "./modal/modal"; import { EventEmitter, Logger } from "./services"; -import { Wallet, WalletBehaviour } from "./wallet"; import { setupWalletModules } from "./wallet-modules"; export const setupWalletSelector = async ( @@ -20,7 +18,8 @@ export const setupWalletSelector = async ( const emitter = new EventEmitter(); const store = createStore(); - const modules = await setupWalletModules({ + + await setupWalletModules({ factories: params.modules, options, store, From 37d82d16bf21fdb0f2069388fc4a1490fd70450d Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Fri, 29 Apr 2022 18:39:07 +0100 Subject: [PATCH 19/64] More changes to wallet types. --- packages/core/src/lib/store.types.ts | 6 +- packages/core/src/lib/utils.types.ts | 2 + packages/core/src/lib/wallet-modules.ts | 43 ++-- packages/core/src/lib/wallet/wallet.types.ts | 203 ++++++++----------- packages/math-wallet/src/lib/math-wallet.ts | 12 +- 5 files changed, 116 insertions(+), 150 deletions(-) diff --git a/packages/core/src/lib/store.types.ts b/packages/core/src/lib/store.types.ts index 38cfea0c9..4a9e793c6 100644 --- a/packages/core/src/lib/store.types.ts +++ b/packages/core/src/lib/store.types.ts @@ -1,13 +1,13 @@ import { BehaviorSubject } from "rxjs"; -import { WalletMetadata } from "./wallet/wallet.types"; +import { WalletModule } from "./wallet/wallet.types"; export interface AccountState { accountId: string; } export interface WalletSelectorState { - modules: Array; + modules: Array; accounts: Array; selectedWalletId: string | null; } @@ -16,7 +16,7 @@ export type WalletSelectorAction = | { type: "SETUP_WALLET_MODULES"; payload: { - modules: Array; + modules: Array; accounts: Array; selectedWalletId: string | null; }; diff --git a/packages/core/src/lib/utils.types.ts b/packages/core/src/lib/utils.types.ts index e0bfdbf25..c0759f501 100644 --- a/packages/core/src/lib/utils.types.ts +++ b/packages/core/src/lib/utils.types.ts @@ -8,3 +8,5 @@ export type Promisify = { }; export type Optional = Omit & Partial>; + +export type Modify = Omit & R; diff --git a/packages/core/src/lib/wallet-modules.ts b/packages/core/src/lib/wallet-modules.ts index 6ff4147bd..73d9d244d 100644 --- a/packages/core/src/lib/wallet-modules.ts +++ b/packages/core/src/lib/wallet-modules.ts @@ -4,11 +4,9 @@ import { logger, storage, Provider, EventEmitter } from "./services"; import { WalletSelectorEvents } from "./wallet-selector.types"; import { Wallet, - WalletMetadata, WalletModule, WalletModuleFactory, } from "./wallet/wallet.types"; -import { omit } from "./utils"; import { WalletEvents } from "./wallet"; interface WalletModulesParams { @@ -35,7 +33,26 @@ export const setupWalletModules = async ({ continue; } - modules.push(module); + modules.push({ + id: module.id, + type: module.type, + metadata: module.metadata, + // @ts-ignore: TypeScript is struggling with the module.init type. + init: async () => { + return { + id: module.id, + type: module.type, + metadata: module.metadata, + ...(await module.init({ + options, + provider: new Provider(options.network.nodeUrl), + emitter: new EventEmitter(), + logger, + storage, + })), + }; + }, + }); } const getWallet = async (id: string) => { @@ -51,21 +68,7 @@ export const setupWalletModules = async ({ return null; } - const metadata = omit(module, ["init"]) as WalletMetadata; - const walletEmitter = new EventEmitter(); - const provider = new Provider(options.network.nodeUrl); - - instance = { - ...metadata, - ...(await module.init({ - options, - metadata: metadata as never, - provider, - emitter: walletEmitter, - logger, - storage, - })), - } as Wallet; + instance = await module.init(); instances[id] = instance; @@ -75,9 +78,7 @@ export const setupWalletModules = async ({ store.dispatch({ type: "SETUP_WALLET_MODULES", payload: { - modules: modules.map((module) => { - return omit(module, ["init"]) as WalletMetadata; - }), + modules, accounts: [], selectedWalletId: null, }, diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts index db1c4bc60..8ff066985 100644 --- a/packages/core/src/lib/wallet/wallet.types.ts +++ b/packages/core/src/lib/wallet/wallet.types.ts @@ -1,8 +1,9 @@ -// - WalletModules -> Wallet +// - WalletModules -> Wallets // - Shouldn't initialise the wallet until we want to connect or already connected. // - We need the type alongside the methods to help with type checking. // - We need getDownloadUrl and isAvailable outside the initialisation of a wallet. // - selector.wallet can remain sync and handle rejecting signing for unselected wallets. +// - WalletModule import { providers } from "near-api-js"; import { AccountState } from "../store.types"; @@ -12,7 +13,6 @@ import { SignAndSendTransactionsParams, WalletEvents, } from "./wallet"; -import isMobile from "is-mobile"; import { Options } from "../options.types"; import { EventEmitterService, @@ -20,42 +20,60 @@ import { ProviderService, StorageService, } from "../services"; -import { omit } from "../utils"; -interface BaseWalletMetadata { - id: string; - type: Type; +interface BaseWalletMetadata { name: string; description: string | null; iconUrl: string; } -export interface WalletOptions> { +export interface WalletOptions { options: Options; - metadata: Metadata; provider: ProviderService; emitter: EventEmitterService; logger: LoggerService; storage: StorageService; } -type BaseWalletBehaviourFactory< - Metadata extends BaseWalletMetadata, +type BaseWallet< + Type extends string, + Metadata extends BaseWalletMetadata, Behaviour -> = (options: WalletOptions) => Promise; +> = { + id: string; + type: Type; + metadata: Metadata; +} & Behaviour; -type BaseWalletModuleFactory = () => Promise; +// Type to handle definition of wallet behaviour +type BaseWalletBehaviourFactory< + Wallet extends BaseWallet +> = ( + options: WalletOptions +) => Promise>; + +// Type to handle definition of wallet module. +type BaseWalletModuleFactory< + Wallet extends BaseWallet +> = () => Promise<{ + id: Wallet["id"]; + type: Wallet["type"]; + metadata: Wallet["metadata"]; + init: BaseWalletBehaviourFactory; +} | null>; type BaseWalletModule< - Metadata extends BaseWalletMetadata, - Behaviour -> = Metadata & { - init: BaseWalletBehaviourFactory; + Wallet extends BaseWallet +> = { + id: Wallet["id"]; + type: Wallet["type"]; + metadata: Wallet["metadata"]; + init: () => Promise; }; // ----- Browser Wallet ----- // -export type BrowserWalletMetadata = BaseWalletMetadata<"browser">; +export type BrowserWalletMetadata = BaseWalletMetadata; export interface BrowserWalletBehaviour { connect(): Promise>; @@ -65,24 +83,20 @@ export interface BrowserWalletBehaviour { signAndSendTransactions(params: SignAndSendTransactionsParams): Promise; } -export type BrowserWalletBehaviourFactory = BaseWalletBehaviourFactory< - BrowserWalletMetadata, - BrowserWalletBehaviour ->; - -export type BrowserWalletModule = BaseWalletModule< +export type BrowserWallet = BaseWallet< + "browser", BrowserWalletMetadata, BrowserWalletBehaviour >; -export type BrowserWalletModuleFactory = - BaseWalletModuleFactory; - -export type BrowserWallet = BrowserWalletMetadata & BrowserWalletBehaviour; +export type BrowserWalletModule = BaseWalletModule; +export type BrowserWalletModuleFactory = BaseWalletModuleFactory; +export type BrowserWalletBehaviourFactory = + BaseWalletBehaviourFactory; // ----- Injected Wallet ----- // -export type InjectedWalletMetadata = BaseWalletMetadata<"injected"> & { +export type InjectedWalletMetadata = BaseWalletMetadata & { downloadUrl: string; }; @@ -98,24 +112,21 @@ export interface InjectedWalletBehaviour { ): Promise>; } -export type InjectedWalletBehaviourFactory = BaseWalletBehaviourFactory< - InjectedWalletMetadata, - InjectedWalletBehaviour ->; - -export type InjectedWalletModule = BaseWalletModule< +export type InjectedWallet = BaseWallet< + "injected", InjectedWalletMetadata, InjectedWalletBehaviour >; +export type InjectedWalletModule = BaseWalletModule; export type InjectedWalletModuleFactory = - BaseWalletModuleFactory; - -export type InjectedWallet = InjectedWalletMetadata & InjectedWalletBehaviour; + BaseWalletModuleFactory; +export type InjectedWalletBehaviourFactory = + BaseWalletBehaviourFactory; // ----- Hardware Wallet ----- // -export type HardwareWalletMetadata = BaseWalletMetadata<"hardware">; +export type HardwareWalletMetadata = BaseWalletMetadata; export interface HardwareWalletBehaviour { connect(params: HardwareWalletConnectParams): Promise>; @@ -129,24 +140,21 @@ export interface HardwareWalletBehaviour { ): Promise>; } -export type HardwareWalletBehaviourFactory = BaseWalletBehaviourFactory< - HardwareWalletMetadata, - HardwareWalletBehaviour ->; - -export type HardwareWalletModule = BaseWalletModule< +export type HardwareWallet = BaseWallet< + "hardware", HardwareWalletMetadata, HardwareWalletBehaviour >; +export type HardwareWalletModule = BaseWalletModule; export type HardwareWalletModuleFactory = - BaseWalletModuleFactory; - -export type HardwareWallet = HardwareWalletMetadata & HardwareWalletBehaviour; + BaseWalletModuleFactory; +export type HardwareWalletBehaviourFactory = + BaseWalletBehaviourFactory; // ----- Bridge Wallet ----- // -export type BridgeWalletMetadata = BaseWalletMetadata<"bridge">; +export type BridgeWalletMetadata = BaseWalletMetadata; export interface BridgeWalletBehaviour { connect(): Promise>; @@ -160,20 +168,16 @@ export interface BridgeWalletBehaviour { ): Promise>; } -export type BridgeWalletBehaviourFactory = BaseWalletBehaviourFactory< - BridgeWalletMetadata, - BridgeWalletBehaviour ->; - -export type BridgeWalletModule = BaseWalletModule< +export type BridgeWallet = BaseWallet< + "bridge", BridgeWalletMetadata, BridgeWalletBehaviour >; -export type BridgetWalletModuleFactory = - BaseWalletModuleFactory; - -export type BridgeWallet = BridgeWalletMetadata & BridgeWalletBehaviour; +export type BridgeWalletModule = BaseWalletModule; +export type BridgetWalletModuleFactory = BaseWalletModuleFactory; +export type BridgeWalletBehaviourFactory = + BaseWalletBehaviourFactory; // ----- Misc ----- // @@ -203,66 +207,23 @@ export type Wallet = export type WalletType = Wallet["type"]; -// ----- Implementation Tests ----- // - -const MathWallet = async (): Promise => { - return { - connect: async () => [], - disconnect: async () => {}, - getAccounts: async () => [], - signAndSendTransaction: async (): any => {}, - signAndSendTransactions: async (): any => {}, - }; -}; - -interface MathWalletParams { - iconUrl: string; -} - -const setupMathWallet = ( - params: MathWalletParams -): InjectedWalletModuleFactory => { - return async () => { - if (isMobile()) { - return null; - } - - return { - id: "math-wallet", - type: "injected", - name: "Math Wallet", - description: null, - iconUrl: "", - downloadUrl: "https://example.com", - - init: MathWallet, - }; - }; -}; - -const setupWalletModules = async (modules: Array) => { - const results: Array = []; - - for (let i = 0; i < modules.length; i += 1) { - const module = await modules[i](); - - // Filter out wallets that aren't available. - if (!module) { - continue; - } - - results.push(omit(module, ["init"]) as WalletMetadata); - } - - return results; -}; - -(async () => { - const modules: Array = []; - const walletModules = await setupWalletModules(modules); - const walletModule = walletModules[0]; - - if (walletModule.type === "injected") { - console.log(walletModule.downloadUrl); - } -})(); +// export const setupMathWallet = (): InjectedWalletModuleFactory => { +// return async () => { +// if (isMobile()) { +// 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: (): any => {}, +// }; +// }; +// }; diff --git a/packages/math-wallet/src/lib/math-wallet.ts b/packages/math-wallet/src/lib/math-wallet.ts index f1f7e2a0c..87c7955b3 100644 --- a/packages/math-wallet/src/lib/math-wallet.ts +++ b/packages/math-wallet/src/lib/math-wallet.ts @@ -242,11 +242,13 @@ export const setupMathWallet = ({ return { id: "math-wallet", type: "injected", - name: "Math Wallet", - description: null, - iconUrl, - downloadUrl: - "https://chrome.google.com/webstore/detail/math-wallet/afbcbjpbpfadlkmhmclhkeeodmamcflc", + metadata: { + name: "Math Wallet", + description: null, + iconUrl, + downloadUrl: + "https://chrome.google.com/webstore/detail/math-wallet/afbcbjpbpfadlkmhmclhkeeodmamcflc", + }, init: MathWallet, }; }; From 82652772a64036b11994b2e10928ea0753b54aa4 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Fri, 29 Apr 2022 18:58:41 +0100 Subject: [PATCH 20/64] Added id and metadata. --- packages/core/src/lib/errors.ts | 2 +- packages/core/src/lib/wallet-modules.ts | 4 +++- packages/core/src/lib/wallet/wallet.types.ts | 6 ++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/core/src/lib/errors.ts b/packages/core/src/lib/errors.ts index 33dc98650..035feb2de 100644 --- a/packages/core/src/lib/errors.ts +++ b/packages/core/src/lib/errors.ts @@ -1,4 +1,4 @@ -import { WalletMetadata } from "./wallet"; +import { WalletMetadata } from "./wallet/wallet.types"; enum ErrorCodes { WalletNotInstalled = "WalletNotInstalled", diff --git a/packages/core/src/lib/wallet-modules.ts b/packages/core/src/lib/wallet-modules.ts index 73d9d244d..20eff24d7 100644 --- a/packages/core/src/lib/wallet-modules.ts +++ b/packages/core/src/lib/wallet-modules.ts @@ -42,8 +42,10 @@ export const setupWalletModules = async ({ return { id: module.id, type: module.type, - metadata: module.metadata, + metadata: module.metadata as never, ...(await module.init({ + id: module.id, + metadata: module.metadata as never, options, provider: new Provider(options.network.nodeUrl), emitter: new EventEmitter(), diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts index 8ff066985..9c4f88877 100644 --- a/packages/core/src/lib/wallet/wallet.types.ts +++ b/packages/core/src/lib/wallet/wallet.types.ts @@ -27,7 +27,9 @@ interface BaseWalletMetadata { iconUrl: string; } -export interface WalletOptions { +export interface WalletOptions { + id: string; + metadata: Metadata; options: Options; provider: ProviderService; emitter: EventEmitterService; @@ -49,7 +51,7 @@ type BaseWallet< type BaseWalletBehaviourFactory< Wallet extends BaseWallet > = ( - options: WalletOptions + options: WalletOptions ) => Promise>; // Type to handle definition of wallet module. From a9a563861038a60bb8a6517c412a5720eb021211 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Fri, 29 Apr 2022 19:00:41 +0100 Subject: [PATCH 21/64] Added type. --- packages/core/src/lib/wallet-modules.ts | 1 + packages/core/src/lib/wallet/wallet.types.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/core/src/lib/wallet-modules.ts b/packages/core/src/lib/wallet-modules.ts index 20eff24d7..9d3a7a4c6 100644 --- a/packages/core/src/lib/wallet-modules.ts +++ b/packages/core/src/lib/wallet-modules.ts @@ -45,6 +45,7 @@ export const setupWalletModules = async ({ metadata: module.metadata as never, ...(await module.init({ id: module.id, + type: module.type, metadata: module.metadata as never, options, provider: new Provider(options.network.nodeUrl), diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts index 9c4f88877..1b52027d1 100644 --- a/packages/core/src/lib/wallet/wallet.types.ts +++ b/packages/core/src/lib/wallet/wallet.types.ts @@ -29,6 +29,7 @@ interface BaseWalletMetadata { export interface WalletOptions { id: string; + type: WalletType; metadata: Metadata; options: Options; provider: ProviderService; From fc106217a58591e3ef64ed09725b93ef86872a2e Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Sat, 30 Apr 2022 06:53:04 +0100 Subject: [PATCH 22/64] Fixed wallet types. --- packages/core/src/index.ts | 14 +-- packages/core/src/lib/wallet-modules.ts | 7 +- packages/core/src/lib/wallet/wallet.types.ts | 120 +++++-------------- packages/math-wallet/src/lib/math-wallet.ts | 9 +- 4 files changed, 39 insertions(+), 111 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 218f0fbbc..46e610e4c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -16,31 +16,21 @@ export { export { WalletModule, + WalletModuleFactory, + WalletBehaviourFactory, Wallet, WalletType, BrowserWalletMetadata, BrowserWalletBehaviour, - BrowserWalletBehaviourFactory, - BrowserWalletModule, - BrowserWalletModuleFactory, BrowserWallet, InjectedWalletMetadata, InjectedWalletBehaviour, - InjectedWalletBehaviourFactory, - InjectedWalletModule, - InjectedWalletModuleFactory, InjectedWallet, HardwareWalletMetadata, HardwareWalletBehaviour, - HardwareWalletBehaviourFactory, - HardwareWalletModule, - HardwareWalletModuleFactory, HardwareWallet, BridgeWalletMetadata, BridgeWalletBehaviour, - BridgeWalletBehaviourFactory, - BridgeWalletModule, - BridgetWalletModuleFactory, BridgeWallet, } from "./lib/wallet/wallet.types"; diff --git a/packages/core/src/lib/wallet-modules.ts b/packages/core/src/lib/wallet-modules.ts index 9d3a7a4c6..de9953e4c 100644 --- a/packages/core/src/lib/wallet-modules.ts +++ b/packages/core/src/lib/wallet-modules.ts @@ -37,23 +37,22 @@ export const setupWalletModules = async ({ id: module.id, type: module.type, metadata: module.metadata, - // @ts-ignore: TypeScript is struggling with the module.init type. init: async () => { return { id: module.id, type: module.type, - metadata: module.metadata as never, + metadata: module.metadata, ...(await module.init({ id: module.id, type: module.type, - metadata: module.metadata as never, + metadata: module.metadata, options, provider: new Provider(options.network.nodeUrl), emitter: new EventEmitter(), logger, storage, })), - }; + } as Wallet; }, }); } diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts index 1b52027d1..5dfa7975e 100644 --- a/packages/core/src/lib/wallet/wallet.types.ts +++ b/packages/core/src/lib/wallet/wallet.types.ts @@ -27,17 +27,6 @@ interface BaseWalletMetadata { iconUrl: string; } -export interface WalletOptions { - id: string; - type: WalletType; - metadata: Metadata; - options: Options; - provider: ProviderService; - emitter: EventEmitterService; - logger: LoggerService; - storage: StorageService; -} - type BaseWallet< Type extends string, Metadata extends BaseWalletMetadata, @@ -48,32 +37,6 @@ type BaseWallet< metadata: Metadata; } & Behaviour; -// Type to handle definition of wallet behaviour -type BaseWalletBehaviourFactory< - Wallet extends BaseWallet -> = ( - options: WalletOptions -) => Promise>; - -// Type to handle definition of wallet module. -type BaseWalletModuleFactory< - Wallet extends BaseWallet -> = () => Promise<{ - id: Wallet["id"]; - type: Wallet["type"]; - metadata: Wallet["metadata"]; - init: BaseWalletBehaviourFactory; -} | null>; - -type BaseWalletModule< - Wallet extends BaseWallet -> = { - id: Wallet["id"]; - type: Wallet["type"]; - metadata: Wallet["metadata"]; - init: () => Promise; -}; - // ----- Browser Wallet ----- // export type BrowserWalletMetadata = BaseWalletMetadata; @@ -92,11 +55,6 @@ export type BrowserWallet = BaseWallet< BrowserWalletBehaviour >; -export type BrowserWalletModule = BaseWalletModule; -export type BrowserWalletModuleFactory = BaseWalletModuleFactory; -export type BrowserWalletBehaviourFactory = - BaseWalletBehaviourFactory; - // ----- Injected Wallet ----- // export type InjectedWalletMetadata = BaseWalletMetadata & { @@ -121,12 +79,6 @@ export type InjectedWallet = BaseWallet< InjectedWalletBehaviour >; -export type InjectedWalletModule = BaseWalletModule; -export type InjectedWalletModuleFactory = - BaseWalletModuleFactory; -export type InjectedWalletBehaviourFactory = - BaseWalletBehaviourFactory; - // ----- Hardware Wallet ----- // export type HardwareWalletMetadata = BaseWalletMetadata; @@ -149,12 +101,6 @@ export type HardwareWallet = BaseWallet< HardwareWalletBehaviour >; -export type HardwareWalletModule = BaseWalletModule; -export type HardwareWalletModuleFactory = - BaseWalletModuleFactory; -export type HardwareWalletBehaviourFactory = - BaseWalletBehaviourFactory; - // ----- Bridge Wallet ----- // export type BridgeWalletMetadata = BaseWalletMetadata; @@ -177,11 +123,6 @@ export type BridgeWallet = BaseWallet< BridgeWalletBehaviour >; -export type BridgeWalletModule = BaseWalletModule; -export type BridgetWalletModuleFactory = BaseWalletModuleFactory; -export type BridgeWalletBehaviourFactory = - BaseWalletBehaviourFactory; - // ----- Misc ----- // export type WalletMetadata = @@ -190,18 +131,6 @@ export type WalletMetadata = | HardwareWalletMetadata | BridgeWalletMetadata; -export type WalletModule = - | BrowserWalletModule - | InjectedWalletModule - | HardwareWalletModule - | BridgeWalletModule; - -export type WalletModuleFactory = - | BrowserWalletModuleFactory - | InjectedWalletModuleFactory - | HardwareWalletModuleFactory - | BridgetWalletModuleFactory; - export type Wallet = | BrowserWallet | InjectedWallet @@ -210,23 +139,32 @@ export type Wallet = export type WalletType = Wallet["type"]; -// export const setupMathWallet = (): InjectedWalletModuleFactory => { -// return async () => { -// if (isMobile()) { -// 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: (): any => {}, -// }; -// }; -// }; +export type WalletModule = { + id: Variation["id"]; + type: Variation["type"]; + metadata: Variation["metadata"]; + init: () => Promise; +}; + +export interface WalletOptions { + id: Variation["id"]; + type: Variation["type"]; + metadata: Variation["metadata"]; + options: Options; + provider: ProviderService; + emitter: EventEmitterService; + logger: LoggerService; + storage: StorageService; +} + +export type WalletBehaviourFactory = ( + options: WalletOptions +) => Promise>; + +export type WalletModuleFactory = + () => Promise<{ + id: Variation["id"]; + type: Variation["type"]; + metadata: Variation["metadata"]; + init: WalletBehaviourFactory; + } | null>; diff --git a/packages/math-wallet/src/lib/math-wallet.ts b/packages/math-wallet/src/lib/math-wallet.ts index 87c7955b3..913a83998 100644 --- a/packages/math-wallet/src/lib/math-wallet.ts +++ b/packages/math-wallet/src/lib/math-wallet.ts @@ -1,8 +1,9 @@ import { transactions as nearTransactions, utils } from "near-api-js"; import isMobile from "is-mobile"; import { - InjectedWalletModuleFactory, - InjectedWalletBehaviourFactory, + WalletModuleFactory, + WalletBehaviourFactory, + InjectedWallet, AccountState, transformActions, waitFor, @@ -21,7 +22,7 @@ export interface MathWalletParams { iconUrl?: string; } -const MathWallet: InjectedWalletBehaviourFactory = async ({ +const MathWallet: WalletBehaviourFactory = async ({ options, metadata, provider, @@ -233,7 +234,7 @@ const MathWallet: InjectedWalletBehaviourFactory = async ({ export const setupMathWallet = ({ iconUrl = "./assets/math-wallet-icon.png", -}: MathWalletParams = {}): InjectedWalletModuleFactory => { +}: MathWalletParams = {}): WalletModuleFactory => { return async () => { if (isMobile()) { return null; From 46b62d92e9add2cf9d4599e3f66ddf677864407f Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Sat, 30 Apr 2022 06:53:39 +0100 Subject: [PATCH 23/64] Added default. --- packages/core/src/lib/wallet/wallet.types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts index 5dfa7975e..023893263 100644 --- a/packages/core/src/lib/wallet/wallet.types.ts +++ b/packages/core/src/lib/wallet/wallet.types.ts @@ -146,7 +146,7 @@ export type WalletModule = { init: () => Promise; }; -export interface WalletOptions { +export interface WalletOptions { id: Variation["id"]; type: Variation["type"]; metadata: Variation["metadata"]; From 1d2fe1406c48485d72702c80edb6670c27455688 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Tue, 3 May 2022 11:40:28 +0100 Subject: [PATCH 24/64] Simplified getWallet. --- packages/core/src/lib/wallet-modules.ts | 26 ++++++++++---------- packages/core/src/lib/wallet/wallet.types.ts | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/core/src/lib/wallet-modules.ts b/packages/core/src/lib/wallet-modules.ts index de9953e4c..cdcd1f4ef 100644 --- a/packages/core/src/lib/wallet-modules.ts +++ b/packages/core/src/lib/wallet-modules.ts @@ -37,8 +37,14 @@ export const setupWalletModules = async ({ id: module.id, type: module.type, metadata: module.metadata, - init: async () => { - return { + wallet: async () => { + let instance = instances[module.id]; + + if (instance) { + return instance; + } + + instance = { id: module.id, type: module.type, metadata: module.metadata, @@ -53,28 +59,22 @@ export const setupWalletModules = async ({ storage, })), } as Wallet; + + instances[module.id] = instance; + + return instance; }, }); } const getWallet = async (id: string) => { - let instance = instances[id]; - - if (instance) { - return instances[id]; - } - const module = modules.find((x) => x.id === id); if (!module) { return null; } - instance = await module.init(); - - instances[id] = instance; - - return instance; + return module.wallet(); }; store.dispatch({ diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts index 023893263..7722f6ffb 100644 --- a/packages/core/src/lib/wallet/wallet.types.ts +++ b/packages/core/src/lib/wallet/wallet.types.ts @@ -143,7 +143,7 @@ export type WalletModule = { id: Variation["id"]; type: Variation["type"]; metadata: Variation["metadata"]; - init: () => Promise; + wallet: () => Promise; }; export interface WalletOptions { From fd3dfa8f5a02fa5a1e261a91c03201caae3e6df2 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Tue, 3 May 2022 13:07:21 +0100 Subject: [PATCH 25/64] Added back commented code. --- examples/react/src/components/Content.tsx | 28 ++++---- .../modal/components/LedgerDerivationPath.tsx | 12 ++-- .../core/src/lib/modal/components/Modal.tsx | 11 ++-- .../modal/components/WalletNotInstalled.tsx | 17 +++-- .../lib/modal/components/WalletOptions.tsx | 35 +++++----- packages/core/src/lib/wallet-modules.ts | 66 +++++++++++++++++-- packages/core/src/lib/wallet-selector.ts | 45 ++----------- .../core/src/lib/wallet-selector.types.ts | 9 ++- packages/core/src/lib/wallet/wallet.types.ts | 13 ++-- 9 files changed, 135 insertions(+), 101 deletions(-) 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/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 e60fd07ac..9d8d3efbe 100644 --- a/packages/core/src/lib/modal/components/Modal.tsx +++ b/packages/core/src/lib/modal/components/Modal.tsx @@ -1,6 +1,6 @@ import React, { MouseEvent, useCallback, useEffect, useState } from "react"; -import { WalletMetadata } from "../../wallet/wallet.types"; +import { Wallet } from "../../wallet/wallet.types"; import { WalletSelectorModal, ModalOptions, Theme } from "../modal.types"; import { WalletSelector } from "../../wallet-selector.types"; import { ModalRouteName } from "./Modal.types"; @@ -38,8 +38,9 @@ export const Modal: React.FC = ({ hide, }) => { const [routeName, setRouteName] = useState("WalletOptions"); - const [notInstalledWallet, setNotInstalledWallet] = - useState(null); + const [notInstalledWallet, setNotInstalledWallet] = useState( + null + ); const [alertMessage, setAlertMessage] = useState(null); useEffect(() => { @@ -120,8 +121,8 @@ export const Modal: React.FC = ({ setRouteName("LedgerDerivationPath"); }} onConnected={handleDismissClick} - onError={(message) => { - setAlertMessage(message); + onError={(err) => { + setAlertMessage(err.message); setRouteName("AlertMessage"); }} /> diff --git a/packages/core/src/lib/modal/components/WalletNotInstalled.tsx b/packages/core/src/lib/modal/components/WalletNotInstalled.tsx index 924a07213..643c53911 100644 --- a/packages/core/src/lib/modal/components/WalletNotInstalled.tsx +++ b/packages/core/src/lib/modal/components/WalletNotInstalled.tsx @@ -1,9 +1,9 @@ import React from "react"; -import { WalletMetadata } from "../../wallet/wallet.types"; +import { Wallet } from "../../wallet/wallet.types"; interface WalletNotInstalledProps { - notInstalledWallet: WalletMetadata; + notInstalledWallet: Wallet; onBack: () => void; } @@ -14,11 +14,14 @@ export const WalletNotInstalled: React.FC = ({ return (
    - {notInstalledWallet.name} -

    {notInstalledWallet.name}

    + {notInstalledWallet.metadata.name} +

    {notInstalledWallet.metadata.name}

    - {`You'll need to install ${notInstalledWallet.name} to continue. After installing`} + {`You'll need to install ${notInstalledWallet.metadata.name} to continue. After installing`} window.location.reload()}>  refresh the page. @@ -34,10 +37,10 @@ export const WalletNotInstalled: React.FC = ({ return; } - window.open(notInstalledWallet.downloadUrl, "_blank"); + window.open(notInstalledWallet.metadata.downloadUrl, "_blank"); }} > - {`Open ${notInstalledWallet.name}`} + {`Open ${notInstalledWallet.metadata.name}`}

    diff --git a/packages/core/src/lib/modal/components/WalletOptions.tsx b/packages/core/src/lib/modal/components/WalletOptions.tsx index 8a1a5e770..34900d74a 100644 --- a/packages/core/src/lib/modal/components/WalletOptions.tsx +++ b/packages/core/src/lib/modal/components/WalletOptions.tsx @@ -1,6 +1,6 @@ import React, { Fragment, useEffect, useState } from "react"; -import { WalletMetadata } from "../../wallet/wallet.types"; +import { Wallet, WalletModule } from "../../wallet/wallet.types"; import { WalletSelector } from "../../wallet-selector.types"; import { ModalOptions, WalletSelectorModal } from "../modal.types"; import { logger } from "../../services"; @@ -10,10 +10,10 @@ interface WalletOptionsProps { // TODO: Remove omit once modal is a separate package. selector: Omit; options?: ModalOptions; - onWalletNotInstalled: (module: WalletMetadata) => void; + onWalletNotInstalled: (wallet: Wallet) => void; onConnectHardwareWallet: () => void; onConnected: () => void; - onError: (message: string) => void; + onError: (error: Error) => void; } export const WalletOptions: React.FC = ({ @@ -26,7 +26,7 @@ export const WalletOptions: React.FC = ({ }) => { const [connecting, setConnecting] = useState(false); const [walletInfoVisible, setWalletInfoVisible] = useState(false); - const [modules, setModules] = useState>([]); + const [modules, setModules] = useState>([]); useEffect(() => { const subscription = selector.store.observable.subscribe((state) => { @@ -37,30 +37,33 @@ export const WalletOptions: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const handleWalletClick = (module: WalletMetadata) => () => { + const handleWalletClick = (module: WalletModule) => async () => { if (connecting) { return; } - if (module.type === "hardware") { + setConnecting(true); + + const wallet = await module.wallet(); + + if (wallet.type === "hardware") { return onConnectHardwareWallet(); } - setConnecting(true); - - selector - .wallet(module.id) + wallet .connect() .then(() => onConnected()) .catch((err) => { if (errors.isWalletNotInstalledError(err)) { - return onWalletNotInstalled(module); + return onWalletNotInstalled(wallet); } - logger.log(`Failed to select ${module.name}`); + const { name } = wallet.metadata; + + logger.log(`Failed to select ${name}`); logger.error(err); - onError(`Failed to connect with ${module.name}: ${err.message}`); + onError(new Error(`Failed to connect with ${name}: ${err.message}`)); }) .finally(() => setConnecting(false)); }; @@ -79,13 +82,13 @@ export const WalletOptions: React.FC = ({ > {modules.reduce>((result, module) => { const { selectedWalletId } = selector.store.getState(); - const { id, name, description, iconUrl } = module; + const { name, description, iconUrl } = module.metadata; const selected = module.id === selectedWalletId; result.push(
  • diff --git a/packages/core/src/lib/wallet-modules.ts b/packages/core/src/lib/wallet-modules.ts index cdcd1f4ef..5bc6d8829 100644 --- a/packages/core/src/lib/wallet-modules.ts +++ b/packages/core/src/lib/wallet-modules.ts @@ -1,5 +1,5 @@ import { Options } from "./options.types"; -import { Store } from "./store.types"; +import { AccountState, Store } from "./store.types"; import { logger, storage, Provider, EventEmitter } from "./services"; import { WalletSelectorEvents } from "./wallet-selector.types"; import { @@ -8,6 +8,7 @@ import { WalletModuleFactory, } from "./wallet/wallet.types"; import { WalletEvents } from "./wallet"; +import { LOCAL_STORAGE_SELECTED_WALLET_ID } from "./constants"; interface WalletModulesParams { factories: Array; @@ -16,6 +17,18 @@ interface WalletModulesParams { emitter: EventEmitter; } +const getSelectedWalletId = () => { + return storage.getItem(LOCAL_STORAGE_SELECTED_WALLET_ID); +}; + +const setSelectedWalletId = (walletId: string) => { + storage.setItem(LOCAL_STORAGE_SELECTED_WALLET_ID, walletId); +}; + +const removeSelectedWalletId = () => { + return storage.removeItem(LOCAL_STORAGE_SELECTED_WALLET_ID); +}; + export const setupWalletModules = async ({ factories, options, @@ -67,22 +80,65 @@ export const setupWalletModules = async ({ }); } - const getWallet = async (id: string) => { + const getWallet = async ( + id: string | null + ) => { const module = modules.find((x) => x.id === id); if (!module) { return null; } - return module.wallet(); + const wallet = await module.wallet(); + + return wallet as Variation; }; + let selectedWalletId = getSelectedWalletId(); + let accounts: Array = []; + const selectedWallet = await getWallet(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.getAccounts().catch((err) => { + logger.log( + `Failed to get accounts for ${selectedWallet.id} during setup` + ); + logger.error(err); + + return []; + }); + } + + if (!accounts.length) { + removeSelectedWalletId(); + selectedWalletId = null; + } + store.dispatch({ type: "SETUP_WALLET_MODULES", payload: { modules, - accounts: [], - selectedWalletId: null, + accounts, + selectedWalletId, }, }); + + return { + getWallet: async (id?: string) => { + const walletId = id || store.getState().selectedWalletId; + const wallet = await getWallet(walletId); + + if (!wallet) { + if (walletId) { + throw new Error("Invalid wallet id"); + } + + throw new Error("No wallet selected"); + } + + return wallet; + }, + }; }; diff --git a/packages/core/src/lib/wallet-selector.ts b/packages/core/src/lib/wallet-selector.ts index 96fae1e24..ca5df8ff8 100644 --- a/packages/core/src/lib/wallet-selector.ts +++ b/packages/core/src/lib/wallet-selector.ts @@ -9,6 +9,7 @@ import { WalletSelectorModal } from "./modal/modal.types"; import { setupModal } from "./modal/modal"; import { EventEmitter, Logger } from "./services"; import { setupWalletModules } from "./wallet-modules"; +import { Wallet } from "./wallet/wallet.types"; export const setupWalletSelector = async ( params: WalletSelectorParams @@ -19,7 +20,7 @@ export const setupWalletSelector = async ( const emitter = new EventEmitter(); const store = createStore(); - await setupWalletModules({ + const walletModules = await setupWalletModules({ factories: params.modules, options, store, @@ -38,45 +39,9 @@ export const setupWalletSelector = async ( return Boolean(accounts.length); }, options, - // wallet: (walletId?: string) => { - // const module = null; // controller.getModule(walletId); - // - // if (!module) { - // if (walletId) { - // throw new Error("Invalid wallet id"); - // } - // - // throw new Error("No wallet selected"); - // } - // - // return { - // connect: async (args: never) => { - // const wallet = await module.wallet(); - // - // return wallet.connect(args); - // }, - // disconnect: async () => { - // const wallet = await module.wallet(); - // - // return wallet.disconnect(); - // }, - // getAccounts: async () => { - // const wallet = await module.wallet(); - // - // return wallet.getAccounts(); - // }, - // signAndSendTransaction: async (args: never) => { - // const wallet = await module.wallet(); - // - // return wallet.signAndSendTransaction(args); - // }, - // signAndSendTransactions: async (args: never) => { - // const wallet = await module.wallet(); - // - // return wallet.signAndSendTransactions(args); - // }, - // } as WalletBehaviour; - // }, + wallet: (id?: string) => { + return walletModules.getWallet(id); + }, on: (eventName, callback) => { return emitter.on(eventName, callback); }, diff --git a/packages/core/src/lib/wallet-selector.types.ts b/packages/core/src/lib/wallet-selector.types.ts index 3cab045e7..18db9c5aa 100644 --- a/packages/core/src/lib/wallet-selector.types.ts +++ b/packages/core/src/lib/wallet-selector.types.ts @@ -1,7 +1,6 @@ import { Observable } from "rxjs"; -// import { Wallet, WalletBehaviour } from "./wallet"; -import { WalletModuleFactory } from "./wallet/wallet.types"; +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"; @@ -31,9 +30,9 @@ export interface WalletSelector extends WalletSelectorModal { options: Options; connected: boolean; - // wallet( - // walletId?: string - // ): WalletBehaviour; + wallet( + walletId?: string + ): Promise; on( eventName: EventName, diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts index 7722f6ffb..6116e81e4 100644 --- a/packages/core/src/lib/wallet/wallet.types.ts +++ b/packages/core/src/lib/wallet/wallet.types.ts @@ -143,10 +143,10 @@ export type WalletModule = { id: Variation["id"]; type: Variation["type"]; metadata: Variation["metadata"]; - wallet: () => Promise; + wallet(): Promise; }; -export interface WalletOptions { +export interface WalletBehaviourOptions { id: Variation["id"]; type: Variation["type"]; metadata: Variation["metadata"]; @@ -157,8 +157,9 @@ export interface WalletOptions { storage: StorageService; } -export type WalletBehaviourFactory = ( - options: WalletOptions +// Note: TypeScript doesn't seem to like reusing this in WalletModuleFactory. +export type WalletBehaviourFactory = ( + options: WalletBehaviourOptions ) => Promise>; export type WalletModuleFactory = @@ -166,5 +167,7 @@ export type WalletModuleFactory = id: Variation["id"]; type: Variation["type"]; metadata: Variation["metadata"]; - init: WalletBehaviourFactory; + init( + options: WalletBehaviourOptions + ): Promise>; } | null>; From b14fddd3bff81a51ab858af2c012878af1d0de72 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Tue, 3 May 2022 13:22:11 +0100 Subject: [PATCH 26/64] Initial attempt to decorate wallets. --- packages/core/src/lib/wallet-modules.ts | 41 ++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/packages/core/src/lib/wallet-modules.ts b/packages/core/src/lib/wallet-modules.ts index 5bc6d8829..53ad49826 100644 --- a/packages/core/src/lib/wallet-modules.ts +++ b/packages/core/src/lib/wallet-modules.ts @@ -51,13 +51,13 @@ export const setupWalletModules = async ({ type: module.type, metadata: module.metadata, wallet: async () => { - let instance = instances[module.id]; + const instance = instances[module.id]; if (instance) { return instance; } - instance = { + const wallet = { id: module.id, type: module.type, metadata: module.metadata, @@ -73,9 +73,42 @@ export const setupWalletModules = async ({ })), } as Wallet; - instances[module.id] = instance; + const _connect = wallet.connect; + const _disconnect = wallet.disconnect; - return instance; + wallet.connect = async (params: never) => { + const accounts = await _connect(params); + + setSelectedWalletId(wallet.id); + + store.dispatch({ + type: "WALLET_CONNECTED", + payload: { + walletId: wallet.id, + pending: false, + accounts, + }, + }); + + return accounts; + }; + + wallet.disconnect = async () => { + await _disconnect(); + + removeSelectedWalletId(); + + store.dispatch({ + type: "WALLET_DISCONNECTED", + payload: { + walletId: wallet.id, + }, + }); + }; + + instances[module.id] = wallet; + + return wallet; }, }); } From 48bc573ed3088f8775cc6c09fce97393dab9910f Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Tue, 3 May 2022 14:02:56 +0100 Subject: [PATCH 27/64] Simplified math wallet. --- packages/core/src/lib/wallet-modules.ts | 109 +++++++++++++++++--- packages/math-wallet/src/lib/math-wallet.ts | 104 +++++-------------- 2 files changed, 119 insertions(+), 94 deletions(-) diff --git a/packages/core/src/lib/wallet-modules.ts b/packages/core/src/lib/wallet-modules.ts index 53ad49826..d8a239815 100644 --- a/packages/core/src/lib/wallet-modules.ts +++ b/packages/core/src/lib/wallet-modules.ts @@ -38,6 +38,20 @@ export const setupWalletModules = async ({ const modules: Array = []; const instances: Record = {}; + const getWallet = async ( + id: string | null + ) => { + const module = modules.find((x) => x.id === id); + + if (!module) { + return null; + } + + const wallet = await module.wallet(); + + return wallet as Variation; + }; + for (let i = 0; i < factories.length; i += 1) { const module = await factories[i](); @@ -57,6 +71,85 @@ export const setupWalletModules = async ({ return instance; } + const walletEmitter = new EventEmitter(); + + const handleDisconnected = (walletId: string) => { + removeSelectedWalletId(); + + store.dispatch({ + type: "WALLET_DISCONNECTED", + payload: { walletId }, + }); + }; + + const handleConnected = async ( + walletId: string, + { pending = false, accounts = [] }: WalletEvents["connected"] + ) => { + const { selectedWalletId } = store.getState(); + + if (selectedWalletId && selectedWalletId !== walletId) { + const wallet = (await getWallet(selectedWalletId))!; + + await wallet.disconnect().catch((err) => { + logger.log("Failed to disconnect existing wallet"); + logger.error(err); + + // At least clean up state on our side. + handleDisconnected(wallet.id); + }); + } + + if (pending || accounts.length) { + setSelectedWalletId(walletId); + } + + store.dispatch({ + type: "WALLET_CONNECTED", + payload: { walletId, pending, accounts }, + }); + }; + + const handleAccountsChanged = ( + walletId: string, + { accounts }: WalletEvents["accountsChanged"] + ) => { + const { selectedWalletId } = store.getState(); + + // TODO: Move this check into the store. + if (walletId !== selectedWalletId) { + return; + } + + store.dispatch({ + type: "ACCOUNTS_CHANGED", + payload: { accounts }, + }); + }; + + const handleNetworkChanged = ( + walletId: string, + { networkId }: WalletEvents["networkChanged"] + ) => { + emitter.emit("networkChanged", { walletId, networkId }); + }; + + walletEmitter.on("disconnected", () => { + handleDisconnected(module.id); + }); + + walletEmitter.on("connected", (event) => { + handleConnected(module.id, event); + }); + + walletEmitter.on("accountsChanged", (event) => { + handleAccountsChanged(module.id, event); + }); + + walletEmitter.on("networkChanged", (event) => { + handleNetworkChanged(module.id, event); + }); + const wallet = { id: module.id, type: module.type, @@ -67,7 +160,7 @@ export const setupWalletModules = async ({ metadata: module.metadata, options, provider: new Provider(options.network.nodeUrl), - emitter: new EventEmitter(), + emitter: walletEmitter, logger, storage, })), @@ -113,20 +206,6 @@ export const setupWalletModules = async ({ }); } - const getWallet = async ( - id: string | null - ) => { - const module = modules.find((x) => x.id === id); - - if (!module) { - return null; - } - - const wallet = await module.wallet(); - - return wallet as Variation; - }; - let selectedWalletId = getSelectedWalletId(); let accounts: Array = []; const selectedWallet = await getWallet(selectedWalletId); diff --git a/packages/math-wallet/src/lib/math-wallet.ts b/packages/math-wallet/src/lib/math-wallet.ts index 913a83998..a4a35967f 100644 --- a/packages/math-wallet/src/lib/math-wallet.ts +++ b/packages/math-wallet/src/lib/math-wallet.ts @@ -7,7 +7,6 @@ import { AccountState, transformActions, waitFor, - errors, } from "@near-wallet-selector/core"; import { InjectedMathWallet } from "./injected-math-wallet"; @@ -22,28 +21,28 @@ export interface MathWalletParams { iconUrl?: string; } +const isInstalled = async () => { + try { + return await waitFor(() => !!window.nearWalletApi); + } catch (e) { + return false; + } +}; + const MathWallet: WalletBehaviourFactory = async ({ options, - metadata, provider, - emitter, logger, }) => { - let _wallet: InjectedMathWallet | null = null; - - const isInstalled = async () => { - try { - return await waitFor(() => !!window.nearWalletApi); - } catch (e) { - logger.log("MathWallet:isInstalled:error", e); - - return false; - } - }; - - const cleanup = () => { - _wallet = null; - }; + 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 }); + } const getAccounts = (): Array => { if (!_wallet?.signer.account) { @@ -66,76 +65,22 @@ const MathWallet: WalletBehaviourFactory = async ({ 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 getWallet = (): InjectedMathWallet => { - if (!_wallet) { - throw new Error(`${metadata.name} not connected`); - } - - return _wallet; - }; - return { async connect() { - const wallet = await setupWallet(); const existingAccounts = getAccounts(); if (existingAccounts.length) { return existingAccounts; } - await wallet.login({ contractId: options.contractId }).catch((err) => { - cleanup(); - - throw err; - }); - - const newAccounts = getAccounts(); - emitter.emit("connected", { accounts: newAccounts }); + await _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); }, async getAccounts() { @@ -153,7 +98,6 @@ const MathWallet: WalletBehaviourFactory = async ({ actions, }); - const wallet = getWallet(); const { accountId, publicKey } = getSignedInAccount()!; const [block, accessKey] = await Promise.all([ provider.block({ finality: "final" }), @@ -174,7 +118,7 @@ const MathWallet: WalletBehaviourFactory = async ({ const [hash, signedTx] = await nearTransactions.signTransaction( transaction, - wallet.signer, + _wallet.signer, accountId ); @@ -186,7 +130,6 @@ const MathWallet: WalletBehaviourFactory = async ({ async signAndSendTransactions({ transactions }) { logger.log("MathWallet:signAndSendTransactions", { transactions }); - const wallet = getWallet(); const { accountId, publicKey } = getSignedInAccount()!; const [block, accessKey] = await Promise.all([ provider.block({ finality: "final" }), @@ -211,7 +154,7 @@ const MathWallet: WalletBehaviourFactory = async ({ const [hash, signedTx] = await nearTransactions.signTransaction( transaction, - wallet.signer, + _wallet.signer, accountId ); @@ -236,7 +179,10 @@ export const setupMathWallet = ({ iconUrl = "./assets/math-wallet-icon.png", }: MathWalletParams = {}): WalletModuleFactory => { return async () => { - if (isMobile()) { + const mobile = isMobile(); + const installed = await isInstalled(); + + if (mobile || !installed) { return null; } From a8dfe47b50c3e06dd5f601b191494da38263b83a Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Tue, 3 May 2022 14:09:31 +0100 Subject: [PATCH 28/64] Improved checks in store. --- packages/core/src/lib/store.ts | 12 +++++++++++- packages/core/src/lib/store.types.ts | 1 + packages/core/src/lib/wallet-modules.ts | 9 +-------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/core/src/lib/store.ts b/packages/core/src/lib/store.ts index f54852920..d9daeea54 100644 --- a/packages/core/src/lib/store.ts +++ b/packages/core/src/lib/store.ts @@ -34,6 +34,12 @@ const reducer = ( }; } case "WALLET_DISCONNECTED": { + const { walletId } = action.payload; + + if (walletId !== state.selectedWalletId) { + return state; + } + return { ...state, accounts: [], @@ -41,7 +47,11 @@ const reducer = ( }; } case "ACCOUNTS_CHANGED": { - const { accounts } = action.payload; + const { walletId, accounts } = action.payload; + + if (walletId !== state.selectedWalletId) { + return state; + } return { ...state, diff --git a/packages/core/src/lib/store.types.ts b/packages/core/src/lib/store.types.ts index 4a9e793c6..2fcbf6ba7 100644 --- a/packages/core/src/lib/store.types.ts +++ b/packages/core/src/lib/store.types.ts @@ -38,6 +38,7 @@ export type WalletSelectorAction = | { type: "ACCOUNTS_CHANGED"; payload: { + walletId: string; accounts: Array; }; }; diff --git a/packages/core/src/lib/wallet-modules.ts b/packages/core/src/lib/wallet-modules.ts index d8a239815..d73b0e533 100644 --- a/packages/core/src/lib/wallet-modules.ts +++ b/packages/core/src/lib/wallet-modules.ts @@ -114,16 +114,9 @@ export const setupWalletModules = async ({ walletId: string, { accounts }: WalletEvents["accountsChanged"] ) => { - const { selectedWalletId } = store.getState(); - - // TODO: Move this check into the store. - if (walletId !== selectedWalletId) { - return; - } - store.dispatch({ type: "ACCOUNTS_CHANGED", - payload: { accounts }, + payload: { walletId, accounts }, }); }; From 26b91864ac8a6b2a9e334b76d6193a53132a5ea2 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Tue, 3 May 2022 14:23:45 +0100 Subject: [PATCH 29/64] Cleaned up method decorating. --- packages/core/src/lib/wallet-modules.ts | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/packages/core/src/lib/wallet-modules.ts b/packages/core/src/lib/wallet-modules.ts index d73b0e533..c63cbcfe9 100644 --- a/packages/core/src/lib/wallet-modules.ts +++ b/packages/core/src/lib/wallet-modules.ts @@ -165,31 +165,14 @@ export const setupWalletModules = async ({ wallet.connect = async (params: never) => { const accounts = await _connect(params); - setSelectedWalletId(wallet.id); - - store.dispatch({ - type: "WALLET_CONNECTED", - payload: { - walletId: wallet.id, - pending: false, - accounts, - }, - }); - + await handleConnected(wallet.id, { accounts }); return accounts; }; wallet.disconnect = async () => { await _disconnect(); - removeSelectedWalletId(); - - store.dispatch({ - type: "WALLET_DISCONNECTED", - payload: { - walletId: wallet.id, - }, - }); + handleDisconnected(wallet.id); }; instances[module.id] = wallet; From 5d633c33fe17b6103aef6f992562442ba1cae9bd Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Tue, 3 May 2022 15:17:37 +0100 Subject: [PATCH 30/64] Refactored Sender. --- .../src/contexts/WalletSelectorContext.tsx | 4 +- package.json | 2 +- packages/math-wallet/src/lib/math-wallet.ts | 42 +++-- packages/sender/src/lib/injected-sender.ts | 2 + packages/sender/src/lib/sender.ts | 158 +++++++----------- 5 files changed, 93 insertions(+), 115 deletions(-) diff --git a/examples/react/src/contexts/WalletSelectorContext.tsx b/examples/react/src/contexts/WalletSelectorContext.tsx index e8c19cce3..de4ab5233 100644 --- a/examples/react/src/contexts/WalletSelectorContext.tsx +++ b/examples/react/src/contexts/WalletSelectorContext.tsx @@ -6,7 +6,7 @@ import { AccountState, } from "@near-wallet-selector/core"; // import { setupNearWallet } from "@near-wallet-selector/near-wallet"; -// import { setupSender } from "@near-wallet-selector/sender"; +import { setupSender } from "@near-wallet-selector/sender"; import { setupMathWallet } from "@near-wallet-selector/math-wallet"; // import { setupLedger } from "@near-wallet-selector/ledger"; // import { setupWalletConnect } from "@near-wallet-selector/wallet-connect"; @@ -63,7 +63,7 @@ export const WalletSelectorContextProvider: React.FC = ({ children }) => { debug: true, modules: [ // setupNearWallet(), - // setupSender(), + setupSender(), setupMathWallet(), // setupLedger(), // setupWalletConnect({ diff --git a/package.json b/package.json index 2d3fd271c..0d0c6e3ec 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "types": "./lib/esm/index.d.ts", "scripts": { "prepublishOnly": "yarn build:core && yarn build:all", - "build:all": "nx run-many --target=build --all --exclude=react,angular --configuration=production", + "build:all": "nx run-many --target=build --all --exclude=react,angular,ledger,near-wallet,wallet-connect --configuration=production", "build:core": "nx run-many --target=build --projects=core --configuration=production", "build:ledger": "nx run-many --target=build --projects=ledger --configuration=production", "build:math-wallet": "nx run-many --target=build --projects=math-wallet --configuration=production", diff --git a/packages/math-wallet/src/lib/math-wallet.ts b/packages/math-wallet/src/lib/math-wallet.ts index a4a35967f..214114ed8 100644 --- a/packages/math-wallet/src/lib/math-wallet.ts +++ b/packages/math-wallet/src/lib/math-wallet.ts @@ -21,10 +21,14 @@ export interface MathWalletParams { iconUrl?: string; } +interface MathWalletState { + wallet: InjectedMathWallet; +} + const isInstalled = async () => { try { - return await waitFor(() => !!window.nearWalletApi); - } catch (e) { + return waitFor(() => !!window.nearWalletApi); + } catch (err) { return false; } }; @@ -34,32 +38,40 @@ const MathWallet: WalletBehaviourFactory = async ({ provider, logger, }) => { - const _wallet = window.nearWalletApi!; + const _state: MathWalletState = { + 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 }); + if ( + _state.wallet.signer.account && + "address" in _state.wallet.signer.account + ) { + await _state.wallet.login({ contractId: options.contractId }); } const getAccounts = (): Array => { - if (!_wallet?.signer.account) { + if (!_state.wallet.signer.account) { return []; } const accountId = - "accountId" in _wallet.signer.account - ? _wallet.signer.account.accountId - : _wallet.signer.account.name; + "accountId" in _state.wallet.signer.account + ? _state.wallet.signer.account.accountId + : _state.wallet.signer.account.name; return [{ accountId }]; }; 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; @@ -73,14 +85,14 @@ const MathWallet: WalletBehaviourFactory = async ({ return existingAccounts; } - await _wallet.login({ contractId: options.contractId }); + await _state.wallet.login({ contractId: options.contractId }); return getAccounts(); }, async disconnect() { // Ignore if unsuccessful (returns false). - await _wallet.logout(); + await _state.wallet.logout(); }, async getAccounts() { @@ -118,7 +130,7 @@ const MathWallet: WalletBehaviourFactory = async ({ const [hash, signedTx] = await nearTransactions.signTransaction( transaction, - _wallet.signer, + _state.wallet.signer, accountId ); @@ -154,7 +166,7 @@ const MathWallet: WalletBehaviourFactory = async ({ const [hash, signedTx] = await nearTransactions.signTransaction( transaction, - _wallet.signer, + _state.wallet.signer, accountId ); 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..ce65a5869 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,39 @@ 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 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: SenderState = { + wallet: window.near!, }; 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,52 +69,11 @@ 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); }); @@ -128,16 +84,16 @@ const Sender: WalletBehaviourFactory = ({ 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 +125,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 +151,9 @@ const Sender: WalletBehaviourFactory = ({ ); } - const newAccounts = getAccounts(); - emitter.emit("connected", { accounts: newAccounts }); + setupEvents(); - return newAccounts; + return getAccounts(); }, disconnect, @@ -224,9 +173,7 @@ const Sender: WalletBehaviourFactory = ({ actions, }); - const wallet = getWallet(); - - return wallet + return _state.wallet .signAndSendTransaction({ receiverId, actions: transformActions(actions), @@ -248,9 +195,7 @@ const Sender: WalletBehaviourFactory = ({ async signAndSendTransactions({ transactions }) { logger.log("Sender:signAndSendTransactions", { transactions }); - const wallet = getWallet(); - - return wallet + return _state.wallet .requestSignTransactions({ transactions: transformTransactions(transactions), }) @@ -272,13 +217,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, + }; }; } From b456631a4914f62b69e5e5b20451fdceadc2065f Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Tue, 3 May 2022 15:21:00 +0100 Subject: [PATCH 31/64] Fixed rpc changed logic. --- packages/sender/src/lib/sender.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/sender/src/lib/sender.ts b/packages/sender/src/lib/sender.ts index ce65a5869..630bb8428 100644 --- a/packages/sender/src/lib/sender.ts +++ b/packages/sender/src/lib/sender.ts @@ -78,9 +78,12 @@ const Sender: WalletBehaviourFactory = async ({ }); _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 }); } }); From 3ea72edbce893250b4b2f5f47cab004e1955b434 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Tue, 3 May 2022 15:43:29 +0100 Subject: [PATCH 32/64] Refactored NEAR Wallet. --- .../src/contexts/WalletSelectorContext.tsx | 4 +- package.json | 2 +- packages/core/src/lib/wallet-modules.ts | 6 +- packages/core/src/lib/wallet/wallet.ts | 2 +- packages/core/src/lib/wallet/wallet.types.ts | 7 +- packages/near-wallet/src/lib/near-wallet.ts | 184 +++++++----------- 6 files changed, 88 insertions(+), 117 deletions(-) diff --git a/examples/react/src/contexts/WalletSelectorContext.tsx b/examples/react/src/contexts/WalletSelectorContext.tsx index de4ab5233..7dc35aec1 100644 --- a/examples/react/src/contexts/WalletSelectorContext.tsx +++ b/examples/react/src/contexts/WalletSelectorContext.tsx @@ -5,7 +5,7 @@ import { WalletSelector, AccountState, } from "@near-wallet-selector/core"; -// import { setupNearWallet } from "@near-wallet-selector/near-wallet"; +import { setupNearWallet } from "@near-wallet-selector/near-wallet"; import { setupSender } from "@near-wallet-selector/sender"; import { setupMathWallet } from "@near-wallet-selector/math-wallet"; // import { setupLedger } from "@near-wallet-selector/ledger"; @@ -62,7 +62,7 @@ export const WalletSelectorContextProvider: React.FC = ({ children }) => { contractId: "guest-book.testnet", debug: true, modules: [ - // setupNearWallet(), + setupNearWallet(), setupSender(), setupMathWallet(), // setupLedger(), diff --git a/package.json b/package.json index 0d0c6e3ec..b0a53eb56 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "types": "./lib/esm/index.d.ts", "scripts": { "prepublishOnly": "yarn build:core && yarn build:all", - "build:all": "nx run-many --target=build --all --exclude=react,angular,ledger,near-wallet,wallet-connect --configuration=production", + "build:all": "nx run-many --target=build --all --exclude=react,angular,ledger,wallet-connect --configuration=production", "build:core": "nx run-many --target=build --projects=core --configuration=production", "build:ledger": "nx run-many --target=build --projects=ledger --configuration=production", "build:math-wallet": "nx run-many --target=build --projects=math-wallet --configuration=production", diff --git a/packages/core/src/lib/wallet-modules.ts b/packages/core/src/lib/wallet-modules.ts index c63cbcfe9..31d74a5cd 100644 --- a/packages/core/src/lib/wallet-modules.ts +++ b/packages/core/src/lib/wallet-modules.ts @@ -84,10 +84,14 @@ export const setupWalletModules = async ({ const handleConnected = async ( walletId: string, - { pending = false, accounts = [] }: WalletEvents["connected"] + { accounts = [] }: WalletEvents["connected"] ) => { const { selectedWalletId } = store.getState(); + // 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 pending = module.type === "browser"; + if (selectedWalletId && selectedWalletId !== walletId) { const wallet = (await getWallet(selectedWalletId))!; diff --git a/packages/core/src/lib/wallet/wallet.ts b/packages/core/src/lib/wallet/wallet.ts index 95477851e..98277a583 100644 --- a/packages/core/src/lib/wallet/wallet.ts +++ b/packages/core/src/lib/wallet/wallet.ts @@ -27,7 +27,7 @@ export interface SignAndSendTransactionsParams { } export type WalletEvents = { - connected: { pending?: boolean; accounts?: Array }; + connected: { accounts: Array }; disconnected: null; accountsChanged: { accounts: Array }; networkChanged: { networkId: string }; diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts index 6116e81e4..28a1797c8 100644 --- a/packages/core/src/lib/wallet/wallet.types.ts +++ b/packages/core/src/lib/wallet/wallet.types.ts @@ -158,8 +158,11 @@ export interface WalletBehaviourOptions { } // Note: TypeScript doesn't seem to like reusing this in WalletModuleFactory. -export type WalletBehaviourFactory = ( - options: WalletBehaviourOptions +export type WalletBehaviourFactory< + Variation extends Wallet, + ExtraWalletOptions extends object = object +> = ( + options: WalletBehaviourOptions & ExtraWalletOptions ) => Promise>; export type WalletModuleFactory = diff --git a/packages/near-wallet/src/lib/near-wallet.ts b/packages/near-wallet/src/lib/near-wallet.ts index 1b634478b..ff9d90d87 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,66 @@ 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 = (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"); + } +}; - const getWalletUrl = () => { - if (walletUrl) { - return walletUrl; - } +const setupWalletState = async ( + network: Network, + walletUrl?: string +): Promise => { + const keyStore = new keyStores.BrowserLocalStorageKeyStore(); - 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 near = await connect({ + keyStore, + walletUrl: getWalletUrl(network, 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, + Pick +> = async ({ options, walletUrl, logger }) => { + const _state = await setupWalletState(options.network, walletUrl); - _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 +83,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 +121,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 (_state.wallet.isSignedIn()) { + _state.wallet.signOut(); } - if (!_wallet.isSignedIn()) { - return cleanup(); - } - - _wallet.signOut(); - await cleanup(); - - emitter.emit("disconnected", null); + cleanup(); }, async getAccounts() { @@ -196,8 +159,7 @@ const NearWallet: WalletBehaviourFactory< actions, }); - const wallet = getWallet(); - const account = wallet.account(); + const account = _state.wallet.account(); return account["signAndSendTransaction"]({ receiverId, @@ -211,9 +173,7 @@ const NearWallet: WalletBehaviourFactory< async signAndSendTransactions({ transactions }) { logger.log("NearWallet:signAndSendTransactions", { transactions }); - const wallet = getWallet(); - - return wallet.requestSignTransactions({ + return _state.wallet.requestSignTransactions({ transactions: await transformTransactions(transactions), }); }, @@ -223,13 +183,17 @@ 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) => NearWallet({ ...options, walletUrl }), + }; }; } From 99ce621685b3a381bb2c53a4fb3a3291e91361e8 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Tue, 3 May 2022 16:28:57 +0100 Subject: [PATCH 33/64] Refactored WalletConnect. --- .../src/contexts/WalletSelectorContext.tsx | 20 +- package.json | 2 +- packages/core/src/lib/wallet/wallet.types.ts | 4 +- packages/near-wallet/src/lib/near-wallet.ts | 25 ++- .../wallet-connect/src/lib/wallet-connect.ts | 211 ++++++++---------- 5 files changed, 127 insertions(+), 135 deletions(-) diff --git a/examples/react/src/contexts/WalletSelectorContext.tsx b/examples/react/src/contexts/WalletSelectorContext.tsx index 7dc35aec1..8fd818c79 100644 --- a/examples/react/src/contexts/WalletSelectorContext.tsx +++ b/examples/react/src/contexts/WalletSelectorContext.tsx @@ -9,7 +9,7 @@ import { setupNearWallet } from "@near-wallet-selector/near-wallet"; import { setupSender } from "@near-wallet-selector/sender"; import { setupMathWallet } from "@near-wallet-selector/math-wallet"; // import { setupLedger } from "@near-wallet-selector/ledger"; -// import { setupWalletConnect } from "@near-wallet-selector/wallet-connect"; +import { setupWalletConnect } from "@near-wallet-selector/wallet-connect"; declare global { interface Window { @@ -66,15 +66,15 @@ export const WalletSelectorContextProvider: React.FC = ({ children }) => { setupSender(), setupMathWallet(), // setupLedger(), - // setupWalletConnect({ - // projectId: "c4f79cc...", - // appMetadata: { - // name: "NEAR Wallet Selector", - // description: "Example dApp used by NEAR Wallet Selector", - // url: "https://github.com/near/wallet-selector", - // icons: ["https://avatars.githubusercontent.com/u/37784886"], - // }, - // }), + setupWalletConnect({ + projectId: "d43d7d0e46eea5ee28e1f75e1131f984", + metadata: { + name: "NEAR Wallet Selector", + description: "Example dApp used by NEAR Wallet Selector", + url: "https://github.com/near/wallet-selector", + icons: ["https://avatars.githubusercontent.com/u/37784886"], + }, + }), ], }) .then((instance) => { diff --git a/package.json b/package.json index b0a53eb56..f43048f0d 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "types": "./lib/esm/index.d.ts", "scripts": { "prepublishOnly": "yarn build:core && yarn build:all", - "build:all": "nx run-many --target=build --all --exclude=react,angular,ledger,wallet-connect --configuration=production", + "build:all": "nx run-many --target=build --all --exclude=react,angular,ledger --configuration=production", "build:core": "nx run-many --target=build --projects=core --configuration=production", "build:ledger": "nx run-many --target=build --projects=ledger --configuration=production", "build:math-wallet": "nx run-many --target=build --projects=math-wallet --configuration=production", diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts index 28a1797c8..a8d398a07 100644 --- a/packages/core/src/lib/wallet/wallet.types.ts +++ b/packages/core/src/lib/wallet/wallet.types.ts @@ -160,9 +160,9 @@ export interface WalletBehaviourOptions { // Note: TypeScript doesn't seem to like reusing this in WalletModuleFactory. export type WalletBehaviourFactory< Variation extends Wallet, - ExtraWalletOptions extends object = object + ExtraOptions extends object = object > = ( - options: WalletBehaviourOptions & ExtraWalletOptions + options: WalletBehaviourOptions & ExtraOptions ) => Promise>; export type WalletModuleFactory = diff --git a/packages/near-wallet/src/lib/near-wallet.ts b/packages/near-wallet/src/lib/near-wallet.ts index ff9d90d87..ce8859441 100644 --- a/packages/near-wallet/src/lib/near-wallet.ts +++ b/packages/near-wallet/src/lib/near-wallet.ts @@ -20,6 +20,8 @@ interface NearWalletState { keyStore: keyStores.BrowserLocalStorageKeyStore; } +type NearWalletExtraOptions = Pick; + const getWalletUrl = (network: Network, walletUrl?: string) => { if (walletUrl) { return walletUrl; @@ -33,19 +35,19 @@ const getWalletUrl = (network: Network, walletUrl?: string) => { case "betanet": return "https://wallet.betanet.near.org"; default: - throw new Error("Invalid wallet URL"); + throw new Error("Invalid wallet url"); } }; const setupWalletState = async ( - network: Network, - walletUrl?: string + params: NearWalletExtraOptions, + network: Network ): Promise => { const keyStore = new keyStores.BrowserLocalStorageKeyStore(); const near = await connect({ keyStore, - walletUrl: getWalletUrl(network, walletUrl), + walletUrl: getWalletUrl(network, params.walletUrl), ...network, headers: {}, }); @@ -65,9 +67,9 @@ const setupWalletState = async ( const NearWallet: WalletBehaviourFactory< BrowserWallet, - Pick -> = async ({ options, walletUrl, logger }) => { - const _state = await setupWalletState(options.network, walletUrl); + { params: NearWalletExtraOptions } +> = async ({ options, params, logger }) => { + const _state = await setupWalletState(params, options.network); const cleanup = () => { _state.keyStore.clear(); @@ -193,7 +195,14 @@ export function setupNearWallet({ description: null, iconUrl, }, - init: (options) => NearWallet({ ...options, walletUrl }), + init: (options) => { + return NearWallet({ + ...options, + params: { + walletUrl, + }, + }); + }, }; }; } diff --git a/packages/wallet-connect/src/lib/wallet-connect.ts b/packages/wallet-connect/src/lib/wallet-connect.ts index 759d10591..de98f201d 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,125 @@ 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(); + const session: SessionTypes.Settled | null = null; + + await client.init(params); + + if (client.session.topics.length) { + 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 = []; }; 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 +136,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 +152,9 @@ const WalletConnect: WalletBehaviourFactory< }, }); - const newAccounts = getAccounts(); - emitter.emit("connected", { accounts: newAccounts }); - - return newAccounts; + return getAccounts(); } catch (err) { - await this.disconnect(); + await disconnect(); throw err; } @@ -198,11 +177,9 @@ const WalletConnect: WalletBehaviourFactory< actions, }); - const { wallet, session } = getWallet(); - - 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 +195,9 @@ const WalletConnect: WalletBehaviourFactory< async signAndSendTransactions({ transactions }) { logger.log("WalletConnect:signAndSendTransactions", { transactions }); - const { wallet, session } = getWallet(); - - 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 +210,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, + }, + }); + }, + }; }; } From 94b27ab9afeaf629e14e13400599c889eae56bde Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Tue, 3 May 2022 16:33:28 +0100 Subject: [PATCH 34/64] Minor refactoring. --- packages/math-wallet/src/lib/math-wallet.ts | 31 ++++++++++++--------- packages/sender/src/lib/sender.ts | 12 ++++++-- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/packages/math-wallet/src/lib/math-wallet.ts b/packages/math-wallet/src/lib/math-wallet.ts index 214114ed8..d3f231ae6 100644 --- a/packages/math-wallet/src/lib/math-wallet.ts +++ b/packages/math-wallet/src/lib/math-wallet.ts @@ -33,26 +33,31 @@ const isInstalled = async () => { } }; -const MathWallet: WalletBehaviourFactory = async ({ - options, - provider, - logger, -}) => { - const _state: MathWalletState = { - wallet: window.nearWalletApi!, - }; +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 ( - _state.wallet.signer.account && - "address" in _state.wallet.signer.account - ) { - await _state.wallet.login({ contractId: options.contractId }); + if (wallet.signer.account && "address" in wallet.signer.account) { + await wallet.login({ contractId }); } + return { + wallet, + }; +}; + +const MathWallet: WalletBehaviourFactory = async ({ + options, + provider, + logger, +}) => { + const _state = await setupMathWalletState(options.contractId); + const getAccounts = (): Array => { if (!_state.wallet.signer.account) { return []; diff --git a/packages/sender/src/lib/sender.ts b/packages/sender/src/lib/sender.ts index 630bb8428..2e472e442 100644 --- a/packages/sender/src/lib/sender.ts +++ b/packages/sender/src/lib/sender.ts @@ -34,15 +34,21 @@ const isInstalled = async () => { } }; +const setupSenderState = (): SenderState => { + const wallet = window.near!; + + return { + wallet, + }; +}; + const Sender: WalletBehaviourFactory = async ({ options, metadata, emitter, logger, }) => { - const _state: SenderState = { - wallet: window.near!, - }; + const _state = setupSenderState(); const cleanup = () => { for (const key in _state.wallet.callbacks) { From 0bad4a9805f8f9c3dccb64d4a75caf8dc2b2d2e4 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Tue, 3 May 2022 17:30:44 +0100 Subject: [PATCH 35/64] Refactored Ledger. --- packages/core/src/index.ts | 1 + packages/ledger/src/lib/ledger.ts | 233 +++++++++++++----------------- 2 files changed, 105 insertions(+), 129 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 46e610e4c..2ee5efe21 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -18,6 +18,7 @@ export { WalletModule, WalletModuleFactory, WalletBehaviourFactory, + WalletBehaviourOptions, Wallet, WalletType, BrowserWalletMetadata, diff --git a/packages/ledger/src/lib/ledger.ts b/packages/ledger/src/lib/ledger.ts index e07ac9f6f..21d1e0904 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,17 +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 _state = setupLedgerState(storage); const debugMode = false; @@ -65,66 +75,49 @@ 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); + const setupEvents = () => { + _state.subscriptions.push( + _state.client.on("disconnect", (err) => { + logger.error(err); - disconnect(); - }); + disconnect(); + }) + ); if (debugMode) { - _subscriptions["logs"] = ledgerClient.listen((data) => { - logger.log("Ledger:init:logs", data); - }); + _state.subscriptions.push( + _state.client.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 +126,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 +135,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 +166,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 +194,50 @@ 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; - } - } + if (_state.authData) { + setupEvents(); + } + return { + 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 +257,9 @@ const Ledger: WalletBehaviourFactory = ({ storage.setItem(LOCAL_STORAGE_AUTH_DATA, authData); _state.authData = authData; - const newAccounts = getAccounts(); - emitter.emit("connected", { accounts: newAccounts }); + setupEvents(); - return newAccounts; + return getAccounts(); }) .catch(async (err) => { await disconnect(); @@ -311,35 +285,25 @@ const Ledger: WalletBehaviourFactory = ({ actions, }); - if (!_state.authData) { - throw new Error(`${metadata.name} 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 }); + + // Note: Connection must be triggered by user interaction. + await connectLedgerDevice(); + const signedTransactions = await signTransactions(transactions); return Promise.all( @@ -351,13 +315,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, + }; }; } From 4468bb1f5bee798171e562774a1a3512ed02a3eb Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Tue, 3 May 2022 17:41:35 +0100 Subject: [PATCH 36/64] Added back Ledger. --- examples/react/src/contexts/WalletSelectorContext.tsx | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/react/src/contexts/WalletSelectorContext.tsx b/examples/react/src/contexts/WalletSelectorContext.tsx index 8fd818c79..4d88af7d2 100644 --- a/examples/react/src/contexts/WalletSelectorContext.tsx +++ b/examples/react/src/contexts/WalletSelectorContext.tsx @@ -8,7 +8,7 @@ import { import { setupNearWallet } from "@near-wallet-selector/near-wallet"; import { setupSender } from "@near-wallet-selector/sender"; import { setupMathWallet } from "@near-wallet-selector/math-wallet"; -// import { setupLedger } from "@near-wallet-selector/ledger"; +import { setupLedger } from "@near-wallet-selector/ledger"; import { setupWalletConnect } from "@near-wallet-selector/wallet-connect"; declare global { @@ -65,7 +65,7 @@ export const WalletSelectorContextProvider: React.FC = ({ children }) => { setupNearWallet(), setupSender(), setupMathWallet(), - // setupLedger(), + setupLedger(), setupWalletConnect({ projectId: "d43d7d0e46eea5ee28e1f75e1131f984", metadata: { diff --git a/package.json b/package.json index f43048f0d..2d3fd271c 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "types": "./lib/esm/index.d.ts", "scripts": { "prepublishOnly": "yarn build:core && yarn build:all", - "build:all": "nx run-many --target=build --all --exclude=react,angular,ledger --configuration=production", + "build:all": "nx run-many --target=build --all --exclude=react,angular --configuration=production", "build:core": "nx run-many --target=build --projects=core --configuration=production", "build:ledger": "nx run-many --target=build --projects=ledger --configuration=production", "build:math-wallet": "nx run-many --target=build --projects=math-wallet --configuration=production", From edb01492f47d7ad41504c69245e3d82427b3c82c Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Tue, 3 May 2022 18:06:20 +0100 Subject: [PATCH 37/64] More refactoring. --- packages/core/src/index.ts | 17 +- .../lib/modal/components/WalletOptions.tsx | 7 +- packages/core/src/lib/store.types.ts | 8 +- packages/core/src/lib/wallet-controller.ts | 178 ------------------ packages/core/src/lib/wallet-modules.ts | 11 +- packages/core/src/lib/wallet/actions.ts | 154 --------------- packages/core/src/lib/wallet/index.ts | 4 +- .../{actions.spec.ts => transactions.spec.ts} | 4 +- packages/core/src/lib/wallet/transactions.ts | 80 +++++++- .../core/src/lib/wallet/transactions.types.ts | 86 +++++++++ packages/core/src/lib/wallet/wallet.ts | 119 ------------ packages/core/src/lib/wallet/wallet.types.ts | 37 ++-- 12 files changed, 202 insertions(+), 503 deletions(-) delete mode 100644 packages/core/src/lib/wallet-controller.ts delete mode 100644 packages/core/src/lib/wallet/actions.ts rename packages/core/src/lib/wallet/{actions.spec.ts => transactions.spec.ts} (97%) create mode 100644 packages/core/src/lib/wallet/transactions.types.ts delete mode 100644 packages/core/src/lib/wallet/wallet.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 2ee5efe21..fea143b83 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -10,7 +10,7 @@ export { Optional } from "./lib/utils.types"; export { WalletSelectorState, - // WalletModuleState, + ModuleState, AccountState, } from "./lib/store.types"; @@ -28,25 +28,12 @@ export { InjectedWalletBehaviour, InjectedWallet, HardwareWalletMetadata, + HardwareWalletConnectParams, HardwareWalletBehaviour, HardwareWallet, BridgeWalletMetadata, BridgeWalletBehaviour, BridgeWallet, -} from "./lib/wallet/wallet.types"; - -export { - // Wallet, - // WalletType, - // WalletMetadata, - // WalletBehaviour, - // WalletModule, - // WalletBehaviourFactory, - // BrowserWallet, - // InjectedWallet, - // HardwareWallet, - HardwareWalletConnectParams, - // BridgeWallet, Transaction, Action, ActionType, diff --git a/packages/core/src/lib/modal/components/WalletOptions.tsx b/packages/core/src/lib/modal/components/WalletOptions.tsx index 34900d74a..48c8f717a 100644 --- a/packages/core/src/lib/modal/components/WalletOptions.tsx +++ b/packages/core/src/lib/modal/components/WalletOptions.tsx @@ -1,6 +1,7 @@ import React, { Fragment, useEffect, useState } from "react"; -import { Wallet, WalletModule } from "../../wallet/wallet.types"; +import { Wallet, WalletModule } from "../../wallet"; +import { ModuleState } from "../../store.types"; import { WalletSelector } from "../../wallet-selector.types"; import { ModalOptions, WalletSelectorModal } from "../modal.types"; import { logger } from "../../services"; @@ -26,7 +27,7 @@ export const WalletOptions: React.FC = ({ }) => { const [connecting, setConnecting] = useState(false); const [walletInfoVisible, setWalletInfoVisible] = useState(false); - const [modules, setModules] = useState>([]); + const [modules, setModules] = useState>([]); useEffect(() => { const subscription = selector.store.observable.subscribe((state) => { @@ -37,7 +38,7 @@ export const WalletOptions: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const handleWalletClick = (module: WalletModule) => async () => { + const handleWalletClick = (module: ModuleState) => async () => { if (connecting) { return; } diff --git a/packages/core/src/lib/store.types.ts b/packages/core/src/lib/store.types.ts index 2fcbf6ba7..0fb3b8abf 100644 --- a/packages/core/src/lib/store.types.ts +++ b/packages/core/src/lib/store.types.ts @@ -1,13 +1,15 @@ import { BehaviorSubject } from "rxjs"; -import { WalletModule } from "./wallet/wallet.types"; +import { WalletModule } from "./wallet"; + +export type ModuleState = WalletModule; export interface AccountState { accountId: string; } export interface WalletSelectorState { - modules: Array; + modules: Array; accounts: Array; selectedWalletId: string | null; } @@ -16,7 +18,7 @@ export type WalletSelectorAction = | { type: "SETUP_WALLET_MODULES"; payload: { - modules: Array; + modules: Array; accounts: Array; selectedWalletId: string | null; }; diff --git a/packages/core/src/lib/wallet-controller.ts b/packages/core/src/lib/wallet-controller.ts deleted file mode 100644 index 4a6751e46..000000000 --- a/packages/core/src/lib/wallet-controller.ts +++ /dev/null @@ -1,178 +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, WalletModuleState } from "./store.types"; -import { Options } from "./options.types"; -import { WalletSelectorEvents } from "./wallet-selector.types"; - -class WalletController { - private options: Options; - private modules: 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; - } - - 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); - } - - // TODO: Add "cache" for "wallet" call. - private setupWalletModule(module: WalletModule): WalletModuleState { - const emitter = new EventEmitter(); - const provider = new Provider(this.options.network.nodeUrl); - - emitter.on("connected", this.handleConnected(module.id)); - emitter.on("disconnected", this.handleDisconnected(module.id)); - emitter.on("accountsChanged", this.handleAccountsChanged(module.id)); - emitter.on("networkChanged", this.handleNetworkChanged(module.id)); - - return { - ...module, - wallet: () => { - return module.wallet({ - options: this.options, - metadata: { - id: module.id, - name: module.name, - description: module.description, - iconUrl: module.iconUrl, - type: module.type, - }, - provider, - emitter, - // TODO: Make a scoped logger. - logger, - // TODO: Make a scoped storage. - storage, - }); - }, - }; - } - - // TODO: Move isAvailable to module level and filter out here. - private async setupWalletModules() { - let selectedWalletId = this.getSelectedWalletId(); - let accounts: Array = []; - - const modules = this.modules.map((module) => { - return this.setupWalletModule(module); - }); - - const selectedModule = modules.find((x) => x.id === selectedWalletId); - - if (selectedModule) { - // Ensure our persistent state aligns with the selected wallet. - // For example a wallet is selected, but it returns no accounts (not connected). - const wallet = await selectedModule.wallet(); - - accounts = await wallet.getAccounts().catch((err) => { - logger.log( - `Failed to get accounts for ${selectedModule.id} during setup` - ); - logger.error(err); - - return []; - }); - } - - if (!accounts.length) { - this.removeSelectedWalletId(); - selectedWalletId = null; - } - - this.store.dispatch({ - type: "SETUP_WALLET_MODULES", - payload: { modules, accounts, selectedWalletId }, - }); - } - - private handleConnected = - (walletId: string) => - async ({ pending = false, accounts = [] }: WalletEvents["connected"]) => { - const existingModule = this.getModule(); - - if (existingModule && existingModule.id !== walletId) { - const wallet = await existingModule.wallet(); - - await wallet.disconnect().catch((err) => { - logger.log("Failed to disconnect existing wallet"); - logger.error(err); - - // At least clean up state on our side. - this.handleDisconnected(existingModule.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 { selectedWalletId } = this.store.getState(); - - // TODO: Move this check into the store. - if (walletId !== selectedWalletId) { - 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(); - } - - getModule(walletId?: string) { - const lookupWalletId = walletId || this.getSelectedWalletId(); - const module = this.modules.find((x) => x.id === lookupWalletId) || null; - - return module as WalletModuleState | null; - } -} - -export default WalletController; diff --git a/packages/core/src/lib/wallet-modules.ts b/packages/core/src/lib/wallet-modules.ts index 31d74a5cd..4ac98e2b5 100644 --- a/packages/core/src/lib/wallet-modules.ts +++ b/packages/core/src/lib/wallet-modules.ts @@ -1,13 +1,8 @@ import { Options } from "./options.types"; -import { AccountState, Store } from "./store.types"; +import { AccountState, ModuleState, Store } from "./store.types"; import { logger, storage, Provider, EventEmitter } from "./services"; import { WalletSelectorEvents } from "./wallet-selector.types"; -import { - Wallet, - WalletModule, - WalletModuleFactory, -} from "./wallet/wallet.types"; -import { WalletEvents } from "./wallet"; +import { WalletModuleFactory, Wallet, WalletEvents } from "./wallet"; import { LOCAL_STORAGE_SELECTED_WALLET_ID } from "./constants"; interface WalletModulesParams { @@ -35,7 +30,7 @@ export const setupWalletModules = async ({ store, emitter, }: WalletModulesParams) => { - const modules: Array = []; + const modules: Array = []; const instances: Record = {}; const getWallet = async ( 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 98277a583..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: { 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 - ): Promise>; - }; - -export type WalletBehaviourFactory< - WalletVariation extends Wallet, - ExtraWalletOptions extends object = object -> = ( - options: WalletOptions & ExtraWalletOptions -) => Promise>; diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts index a8d398a07..c9776f086 100644 --- a/packages/core/src/lib/wallet/wallet.types.ts +++ b/packages/core/src/lib/wallet/wallet.types.ts @@ -1,18 +1,5 @@ -// - WalletModules -> Wallets -// - Shouldn't initialise the wallet until we want to connect or already connected. -// - We need the type alongside the methods to help with type checking. -// - We need getDownloadUrl and isAvailable outside the initialisation of a wallet. -// - selector.wallet can remain sync and handle rejecting signing for unselected wallets. -// - WalletModule - import { providers } from "near-api-js"; import { AccountState } from "../store.types"; -import { - HardwareWalletConnectParams, - SignAndSendTransactionParams, - SignAndSendTransactionsParams, - WalletEvents, -} from "./wallet"; import { Options } from "../options.types"; import { EventEmitterService, @@ -20,6 +7,9 @@ import { ProviderService, StorageService, } from "../services"; +import { Action } from "./transactions.types"; +import { Optional } from "../utils.types"; +import { Transaction } from "./transactions.types"; interface BaseWalletMetadata { name: string; @@ -37,6 +27,23 @@ type BaseWallet< 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; @@ -83,6 +90,10 @@ export type InjectedWallet = BaseWallet< export type HardwareWalletMetadata = BaseWalletMetadata; +export interface HardwareWalletConnectParams { + derivationPath: string; +} + export interface HardwareWalletBehaviour { connect(params: HardwareWalletConnectParams): Promise>; disconnect(): Promise; From 4ec002d0eb2f397552c70b44d8fb19ceb048ac6a Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Tue, 3 May 2022 18:07:33 +0100 Subject: [PATCH 38/64] Removed key. --- examples/react/src/contexts/WalletSelectorContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/react/src/contexts/WalletSelectorContext.tsx b/examples/react/src/contexts/WalletSelectorContext.tsx index 4d88af7d2..efdb02c1a 100644 --- a/examples/react/src/contexts/WalletSelectorContext.tsx +++ b/examples/react/src/contexts/WalletSelectorContext.tsx @@ -67,7 +67,7 @@ export const WalletSelectorContextProvider: React.FC = ({ children }) => { setupMathWallet(), setupLedger(), setupWalletConnect({ - projectId: "d43d7d0e46eea5ee28e1f75e1131f984", + projectId: "c4f79cc...", metadata: { name: "NEAR Wallet Selector", description: "Example dApp used by NEAR Wallet Selector", From ca1f03a6dbfde998d9cbc12b1971b1578d361b55 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Tue, 3 May 2022 18:16:37 +0100 Subject: [PATCH 39/64] Removed need for wallet not installed case. --- packages/core/src/index.ts | 2 +- packages/core/src/lib/errors.ts | 30 ++---------- .../core/src/lib/modal/components/Modal.tsx | 18 ------- .../modal/components/WalletNotInstalled.tsx | 48 ------------------- .../lib/modal/components/WalletOptions.tsx | 8 ---- 5 files changed, 4 insertions(+), 102 deletions(-) delete mode 100644 packages/core/src/lib/modal/components/WalletNotInstalled.tsx diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index fea143b83..ef1a26e35 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -47,7 +47,7 @@ export { DeleteAccountAction, } from "./lib/wallet"; -export { errors } from "./lib/errors"; +export { WalletSelectorError } from "./lib/errors"; export { transformActions } from "./lib/wallet"; export { waitFor } from "./lib/helpers"; diff --git a/packages/core/src/lib/errors.ts b/packages/core/src/lib/errors.ts index 035feb2de..07e85ae84 100644 --- a/packages/core/src/lib/errors.ts +++ b/packages/core/src/lib/errors.ts @@ -1,11 +1,7 @@ -import { WalletMetadata } from "./wallet/wallet.types"; +enum ErrorCodes {} -enum ErrorCodes { - WalletNotInstalled = "WalletNotInstalled", -} - -class WalletSelectorError extends Error { - constructor(name: ErrorCodes, message: string) { +export class WalletSelectorError extends Error { + constructor(name: string, message: string) { super(message); this.name = name; @@ -13,23 +9,3 @@ class WalletSelectorError extends Error { 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/Modal.tsx b/packages/core/src/lib/modal/components/Modal.tsx index 9d8d3efbe..7660ea66a 100644 --- a/packages/core/src/lib/modal/components/Modal.tsx +++ b/packages/core/src/lib/modal/components/Modal.tsx @@ -5,7 +5,6 @@ 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 +37,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 +59,6 @@ export const Modal: React.FC = ({ const handleDismissClick = useCallback(() => { setAlertMessage(null); - setNotInstalledWallet(null); setRouteName("WalletOptions"); hide(); }, [hide]); @@ -113,10 +108,6 @@ export const Modal: React.FC = ({ { - setNotInstalledWallet(wallet); - return setRouteName("WalletNotInstalled"); - }} onConnectHardwareWallet={() => { setRouteName("LedgerDerivationPath"); }} @@ -134,15 +125,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.metadata.name} -

    {notInstalledWallet.metadata.name}

    -
    -

    - {`You'll need to install ${notInstalledWallet.metadata.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 48c8f717a..c0cfe1df8 100644 --- a/packages/core/src/lib/modal/components/WalletOptions.tsx +++ b/packages/core/src/lib/modal/components/WalletOptions.tsx @@ -1,17 +1,14 @@ import React, { Fragment, useEffect, useState } from "react"; -import { Wallet, WalletModule } 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: (error: Error) => void; @@ -20,7 +17,6 @@ interface WalletOptionsProps { export const WalletOptions: React.FC = ({ selector, options, - onWalletNotInstalled, onError, onConnectHardwareWallet, onConnected, @@ -55,10 +51,6 @@ export const WalletOptions: React.FC = ({ .connect() .then(() => onConnected()) .catch((err) => { - if (errors.isWalletNotInstalledError(err)) { - return onWalletNotInstalled(wallet); - } - const { name } = wallet.metadata; logger.log(`Failed to select ${name}`); From 6ec2e38a07f903c6b1ca6a92ab7aa1a2dc1913bd Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Tue, 3 May 2022 18:23:23 +0100 Subject: [PATCH 40/64] Missing import. --- packages/core/src/lib/modal/components/Modal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/lib/modal/components/Modal.tsx b/packages/core/src/lib/modal/components/Modal.tsx index 7660ea66a..c9d031dc3 100644 --- a/packages/core/src/lib/modal/components/Modal.tsx +++ b/packages/core/src/lib/modal/components/Modal.tsx @@ -1,6 +1,5 @@ import React, { MouseEvent, useCallback, useEffect, useState } from "react"; -import { Wallet } from "../../wallet/wallet.types"; import { WalletSelectorModal, ModalOptions, Theme } from "../modal.types"; import { WalletSelector } from "../../wallet-selector.types"; import { ModalRouteName } from "./Modal.types"; From 4251a2f59c58f9514465ae693113610d1a924a7a Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Tue, 3 May 2022 18:23:54 +0100 Subject: [PATCH 41/64] Removed unused enum. --- packages/core/src/lib/errors.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/core/src/lib/errors.ts b/packages/core/src/lib/errors.ts index 07e85ae84..04a329925 100644 --- a/packages/core/src/lib/errors.ts +++ b/packages/core/src/lib/errors.ts @@ -1,5 +1,3 @@ -enum ErrorCodes {} - export class WalletSelectorError extends Error { constructor(name: string, message: string) { super(message); From 6cf40dd566ffa8dae8d7ff52a21b49948f4fca22 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Tue, 3 May 2022 18:39:35 +0100 Subject: [PATCH 42/64] Added checks for signAndSendTransaction(s). --- packages/ledger/src/lib/ledger.ts | 8 ++++ packages/math-wallet/src/lib/math-wallet.ts | 39 ++++++++++++------- packages/near-wallet/src/lib/near-wallet.ts | 8 ++++ packages/sender/src/lib/sender.ts | 8 ++++ .../wallet-connect/src/lib/wallet-connect.ts | 12 +++++- 5 files changed, 58 insertions(+), 17 deletions(-) diff --git a/packages/ledger/src/lib/ledger.ts b/packages/ledger/src/lib/ledger.ts index 21d1e0904..552b0f8ea 100644 --- a/packages/ledger/src/lib/ledger.ts +++ b/packages/ledger/src/lib/ledger.ts @@ -285,6 +285,10 @@ const Ledger: WalletBehaviourFactory = async ({ actions, }); + if (!_state.authData) { + throw new Error("Wallet not connected"); + } + // Note: Connection must be triggered by user interaction. await connectLedgerDevice(); @@ -301,6 +305,10 @@ const Ledger: WalletBehaviourFactory = async ({ 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(); diff --git a/packages/math-wallet/src/lib/math-wallet.ts b/packages/math-wallet/src/lib/math-wallet.ts index d3f231ae6..60682aae1 100644 --- a/packages/math-wallet/src/lib/math-wallet.ts +++ b/packages/math-wallet/src/lib/math-wallet.ts @@ -58,19 +58,6 @@ const MathWallet: WalletBehaviourFactory = async ({ }) => { const _state = await setupMathWalletState(options.contractId); - const getAccounts = (): Array => { - if (!_state.wallet.signer.account) { - return []; - } - - const accountId = - "accountId" in _state.wallet.signer.account - ? _state.wallet.signer.account.accountId - : _state.wallet.signer.account.name; - - return [{ accountId }]; - }; - const getSignedInAccount = () => { if ( _state.wallet.signer.account && @@ -82,6 +69,16 @@ const MathWallet: WalletBehaviourFactory = async ({ return null; }; + const getAccounts = (): Array => { + const account = getSignedInAccount(); + + if (!account) { + return []; + } + + return [{ accountId: account.accountId }]; + }; + return { async connect() { const existingAccounts = getAccounts(); @@ -115,7 +112,13 @@ const MathWallet: WalletBehaviourFactory = async ({ actions, }); - 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 }), @@ -147,7 +150,13 @@ const MathWallet: WalletBehaviourFactory = async ({ async signAndSendTransactions({ transactions }) { logger.log("MathWallet:signAndSendTransactions", { transactions }); - 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 }), diff --git a/packages/near-wallet/src/lib/near-wallet.ts b/packages/near-wallet/src/lib/near-wallet.ts index ce8859441..394fee979 100644 --- a/packages/near-wallet/src/lib/near-wallet.ts +++ b/packages/near-wallet/src/lib/near-wallet.ts @@ -161,6 +161,10 @@ const NearWallet: WalletBehaviourFactory< actions, }); + if (!_state.wallet.isSignedIn()) { + throw new Error("Wallet not connected"); + } + const account = _state.wallet.account(); return account["signAndSendTransaction"]({ @@ -175,6 +179,10 @@ const NearWallet: WalletBehaviourFactory< async signAndSendTransactions({ transactions }) { logger.log("NearWallet:signAndSendTransactions", { transactions }); + if (!_state.wallet.isSignedIn()) { + throw new Error("Wallet not connected"); + } + return _state.wallet.requestSignTransactions({ transactions: await transformTransactions(transactions), }); diff --git a/packages/sender/src/lib/sender.ts b/packages/sender/src/lib/sender.ts index 2e472e442..95de3f24c 100644 --- a/packages/sender/src/lib/sender.ts +++ b/packages/sender/src/lib/sender.ts @@ -182,6 +182,10 @@ const Sender: WalletBehaviourFactory = async ({ actions, }); + if (!_state.wallet.isSignedIn()) { + throw new Error("Wallet not connected"); + } + return _state.wallet .signAndSendTransaction({ receiverId, @@ -204,6 +208,10 @@ const Sender: WalletBehaviourFactory = async ({ async signAndSendTransactions({ transactions }) { logger.log("Sender:signAndSendTransactions", { transactions }); + if (!_state.wallet.isSignedIn()) { + throw new Error("Wallet not connected"); + } + return _state.wallet .requestSignTransactions({ transactions: transformTransactions(transactions), diff --git a/packages/wallet-connect/src/lib/wallet-connect.ts b/packages/wallet-connect/src/lib/wallet-connect.ts index de98f201d..0a3cd9dc9 100644 --- a/packages/wallet-connect/src/lib/wallet-connect.ts +++ b/packages/wallet-connect/src/lib/wallet-connect.ts @@ -177,9 +177,13 @@ const WalletConnect: WalletBehaviourFactory< actions, }); + if (!_state.session) { + throw new Error("Wallet not connected"); + } + return _state.client.request({ timeout: 30 * 1000, - topic: _state.session!.topic, + topic: _state.session.topic, chainId: getChainId(), request: { method: "near_signAndSendTransaction", @@ -195,9 +199,13 @@ const WalletConnect: WalletBehaviourFactory< async signAndSendTransactions({ transactions }) { logger.log("WalletConnect:signAndSendTransactions", { transactions }); + if (!_state.session) { + throw new Error("Wallet not connected"); + } + return _state.client.request({ timeout: 30 * 1000, - topic: _state.session!.topic, + topic: _state.session.topic, chainId: getChainId(), request: { method: "near_signAndSendTransactions", From bcd974c859f249def5d7a0276c52f9394efb8e86 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Wed, 4 May 2022 08:55:57 +0100 Subject: [PATCH 43/64] Added session assignment. --- packages/wallet-connect/src/lib/wallet-connect.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/wallet-connect/src/lib/wallet-connect.ts b/packages/wallet-connect/src/lib/wallet-connect.ts index 0a3cd9dc9..47649da8a 100644 --- a/packages/wallet-connect/src/lib/wallet-connect.ts +++ b/packages/wallet-connect/src/lib/wallet-connect.ts @@ -29,12 +29,12 @@ const setupWalletConnectState = async ( params: WalletConnectExtraOptions ): Promise => { const client = new WalletConnectClient(); - const session: SessionTypes.Settled | null = null; + let session: SessionTypes.Settled | null = null; await client.init(params); if (client.session.topics.length) { - await client.session.get(client.session.topics[0]); + session = await client.session.get(client.session.topics[0]); } return { From 698a43a4f7984de9c82d86a15e230c9c775e921d Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Wed, 4 May 2022 08:57:48 +0100 Subject: [PATCH 44/64] More fixes. --- packages/wallet-connect/src/lib/wallet-connect.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/wallet-connect/src/lib/wallet-connect.ts b/packages/wallet-connect/src/lib/wallet-connect.ts index 47649da8a..22e0a1e08 100644 --- a/packages/wallet-connect/src/lib/wallet-connect.ts +++ b/packages/wallet-connect/src/lib/wallet-connect.ts @@ -78,6 +78,7 @@ const WalletConnect: WalletBehaviourFactory< _state.subscriptions.forEach((subscription) => subscription.remove()); _state.subscriptions = []; + _state.session = null; }; const disconnect = async () => { @@ -152,6 +153,8 @@ const WalletConnect: WalletBehaviourFactory< }, }); + setupEvents(); + return getAccounts(); } catch (err) { await disconnect(); From 21f21644e5795142ca5629d8b675e8e8b3510edf Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Wed, 4 May 2022 08:59:20 +0100 Subject: [PATCH 45/64] Removed unused utils. --- packages/core/src/lib/utils.ts | 10 ---------- packages/core/src/lib/utils.types.ts | 10 ---------- 2 files changed, 20 deletions(-) delete mode 100644 packages/core/src/lib/utils.ts diff --git a/packages/core/src/lib/utils.ts b/packages/core/src/lib/utils.ts deleted file mode 100644 index ccd5c3941..000000000 --- a/packages/core/src/lib/utils.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const omit = ( - obj: Value, - keys: Array -): Omit => { - const result = { ...obj }; - - keys.forEach((key) => delete result[key]); - - return result; -}; diff --git a/packages/core/src/lib/utils.types.ts b/packages/core/src/lib/utils.types.ts index c0759f501..12b55bbee 100644 --- a/packages/core/src/lib/utils.types.ts +++ b/packages/core/src/lib/utils.types.ts @@ -1,12 +1,2 @@ -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>; -export type Modify = Omit & R; From 85a1f614bb6cf3a6729820df4867791b69980d8d Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Wed, 4 May 2022 09:00:05 +0100 Subject: [PATCH 46/64] Minor formatting. --- packages/core/src/lib/utils.types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/lib/utils.types.ts b/packages/core/src/lib/utils.types.ts index 12b55bbee..93cb8bda2 100644 --- a/packages/core/src/lib/utils.types.ts +++ b/packages/core/src/lib/utils.types.ts @@ -1,2 +1 @@ export type Optional = Omit & Partial>; - From 7146680a2002f89e98b017db47c318aad944f36a Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Wed, 4 May 2022 09:00:37 +0100 Subject: [PATCH 47/64] Removed error. --- packages/core/src/index.ts | 2 -- packages/core/src/lib/errors.ts | 9 --------- 2 files changed, 11 deletions(-) delete mode 100644 packages/core/src/lib/errors.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ef1a26e35..2a85dd796 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -47,7 +47,5 @@ export { DeleteAccountAction, } from "./lib/wallet"; -export { WalletSelectorError } from "./lib/errors"; - export { transformActions } from "./lib/wallet"; export { waitFor } from "./lib/helpers"; diff --git a/packages/core/src/lib/errors.ts b/packages/core/src/lib/errors.ts deleted file mode 100644 index 04a329925..000000000 --- a/packages/core/src/lib/errors.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class WalletSelectorError extends Error { - constructor(name: string, message: string) { - super(message); - - this.name = name; - - Object.setPrototypeOf(this, WalletSelectorError.prototype); - } -} From 18fd63941f3e0b8a9e989df5cdd34844c8b8ee0d Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Wed, 4 May 2022 10:42:45 +0100 Subject: [PATCH 48/64] Improved browser wallet connect/disconnect flow. --- packages/core/src/index.ts | 1 - packages/core/src/lib/constants.ts | 3 +- .../core/src/lib/modules/wallet-instances.ts | 132 ++++++++++ .../core/src/lib/modules/wallet-modules.ts | 146 +++++++++++ packages/core/src/lib/store.ts | 35 ++- packages/core/src/lib/store.types.ts | 10 +- packages/core/src/lib/wallet-modules.ts | 231 ------------------ packages/core/src/lib/wallet-selector.ts | 6 +- packages/core/src/lib/wallet/wallet.types.ts | 7 - 9 files changed, 321 insertions(+), 250 deletions(-) create mode 100644 packages/core/src/lib/modules/wallet-instances.ts create mode 100644 packages/core/src/lib/modules/wallet-modules.ts delete mode 100644 packages/core/src/lib/wallet-modules.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 2a85dd796..c3e8fc133 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -15,7 +15,6 @@ export { } from "./lib/store.types"; export { - WalletModule, WalletModuleFactory, WalletBehaviourFactory, WalletBehaviourOptions, 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/modules/wallet-instances.ts b/packages/core/src/lib/modules/wallet-instances.ts new file mode 100644 index 000000000..4c55db163 --- /dev/null +++ b/packages/core/src/lib/modules/wallet-instances.ts @@ -0,0 +1,132 @@ +import { EventEmitter, logger, Provider, storage } from "../services"; +import { Wallet, WalletEvents, WalletModuleFactory } from "../wallet"; +import { Store } from "../store.types"; +import { WalletSelectorEvents } from "../wallet-selector.types"; +import { PENDING_SELECTED_WALLET_ID } from "../constants"; +import { Options } from "../options.types"; + +interface WalletInstanceParams { + module: NonNullable>>; + getWallet: (id: string | null) => Promise; + store: Store; + options: Options; + emitter: EventEmitter; +} + +export const setupWalletInstance = async ({ + module, + getWallet, + store, + options, + emitter, +}: WalletInstanceParams) => { + const walletEmitter = new EventEmitter(); + + const handleDisconnected = (walletId: string) => { + store.dispatch({ + type: "WALLET_DISCONNECTED", + payload: { 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) { + const wallet = (await getWallet(selectedWalletId))!; + + await wallet.disconnect().catch((err) => { + logger.log("Failed to disconnect existing wallet"); + logger.error(err); + + // At least clean up state on our side. + handleDisconnected(wallet.id); + }); + } + + store.dispatch({ + type: "WALLET_CONNECTED", + payload: { walletId, accounts }, + }); + }; + + const handleAccountsChanged = ( + walletId: string, + { accounts }: WalletEvents["accountsChanged"] + ) => { + store.dispatch({ + type: "ACCOUNTS_CHANGED", + payload: { walletId, accounts }, + }); + }; + + const handleNetworkChanged = ( + walletId: string, + { networkId }: WalletEvents["networkChanged"] + ) => { + emitter.emit("networkChanged", { walletId, networkId }); + }; + + walletEmitter.on("disconnected", () => { + handleDisconnected(module.id); + }); + + walletEmitter.on("connected", (event) => { + handleConnected(module.id, event); + }); + + walletEmitter.on("accountsChanged", (event) => { + handleAccountsChanged(module.id, event); + }); + + walletEmitter.on("networkChanged", (event) => { + handleNetworkChanged(module.id, event); + }); + + 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..07240e55a --- /dev/null +++ b/packages/core/src/lib/modules/wallet-modules.ts @@ -0,0 +1,146 @@ +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-instances"; +import { PENDING_SELECTED_WALLET_ID, 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; + } + + const wallet = await module.wallet(); + + return 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; + }; + + 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({ + module, + getWallet, + store, + options, + emitter, + }); + + instances[module.id] = instance; + + return instance; + }, + }); + } + + let selectedWalletId = store.getState().selectedWalletId; + const pendingSelectedWalletId = storage.getItem( + PENDING_SELECTED_WALLET_ID + ); + + if (pendingSelectedWalletId) { + const accounts = await validateWallet(pendingSelectedWalletId); + + if (accounts.length) { + const selectedWallet = await getWallet(selectedWalletId); + + if (selectedWallet) { + await selectedWallet.disconnect().catch((err) => { + logger.log("Failed to disconnect existing wallet"); + logger.error(err); + }); + + selectedWalletId = pendingSelectedWalletId; + } + } + + storage.removeItem(PENDING_SELECTED_WALLET_ID); + } + + const accounts = await validateWallet(selectedWalletId); + + if (!accounts.length) { + selectedWalletId = null; + } + + store.dispatch({ + type: "SETUP_WALLET_MODULES", + payload: { + modules, + accounts, + selectedWalletId, + }, + }); + + return { + getWallet: async (id?: string) => { + const walletId = id || store.getState().selectedWalletId; + const wallet = await getWallet(walletId); + + if (!wallet) { + if (walletId) { + throw new Error("Invalid wallet id"); + } + + throw new Error("No wallet selected"); + } + + return wallet; + }, + }; +}; diff --git a/packages/core/src/lib/store.ts b/packages/core/src/lib/store.ts index d9daeea54..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, @@ -25,12 +26,16 @@ const reducer = ( }; } case "WALLET_CONNECTED": { - const { walletId, pending, accounts } = action.payload; + const { walletId, accounts } = action.payload; + + if (!accounts.length) { + return state; + } return { ...state, accounts, - selectedWalletId: !pending && accounts.length ? walletId : null, + selectedWalletId: walletId, }; } case "WALLET_DISCONNECTED": { @@ -63,11 +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: [], - selectedWalletId: null, + 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 0fb3b8abf..db8a9fb7d 100644 --- a/packages/core/src/lib/store.types.ts +++ b/packages/core/src/lib/store.types.ts @@ -1,8 +1,13 @@ import { BehaviorSubject } from "rxjs"; -import { WalletModule } from "./wallet"; +import { Wallet } from "./wallet"; -export type ModuleState = WalletModule; +export type ModuleState = { + id: Variation["id"]; + type: Variation["type"]; + metadata: Variation["metadata"]; + wallet(): Promise; +}; export interface AccountState { accountId: string; @@ -27,7 +32,6 @@ export type WalletSelectorAction = type: "WALLET_CONNECTED"; payload: { walletId: string; - pending: boolean; accounts: Array; }; } diff --git a/packages/core/src/lib/wallet-modules.ts b/packages/core/src/lib/wallet-modules.ts deleted file mode 100644 index 4ac98e2b5..000000000 --- a/packages/core/src/lib/wallet-modules.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { Options } from "./options.types"; -import { AccountState, ModuleState, Store } from "./store.types"; -import { logger, storage, Provider, EventEmitter } from "./services"; -import { WalletSelectorEvents } from "./wallet-selector.types"; -import { WalletModuleFactory, Wallet, WalletEvents } from "./wallet"; -import { LOCAL_STORAGE_SELECTED_WALLET_ID } from "./constants"; - -interface WalletModulesParams { - factories: Array; - options: Options; - store: Store; - emitter: EventEmitter; -} - -const getSelectedWalletId = () => { - return storage.getItem(LOCAL_STORAGE_SELECTED_WALLET_ID); -}; - -const setSelectedWalletId = (walletId: string) => { - storage.setItem(LOCAL_STORAGE_SELECTED_WALLET_ID, walletId); -}; - -const removeSelectedWalletId = () => { - return storage.removeItem(LOCAL_STORAGE_SELECTED_WALLET_ID); -}; - -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; - } - - const wallet = await module.wallet(); - - return wallet as Variation; - }; - - 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 () => { - const instance = instances[module.id]; - - if (instance) { - return instance; - } - - const walletEmitter = new EventEmitter(); - - const handleDisconnected = (walletId: string) => { - removeSelectedWalletId(); - - store.dispatch({ - type: "WALLET_DISCONNECTED", - payload: { walletId }, - }); - }; - - const handleConnected = async ( - walletId: string, - { accounts = [] }: WalletEvents["connected"] - ) => { - const { selectedWalletId } = store.getState(); - - // 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 pending = module.type === "browser"; - - if (selectedWalletId && selectedWalletId !== walletId) { - const wallet = (await getWallet(selectedWalletId))!; - - await wallet.disconnect().catch((err) => { - logger.log("Failed to disconnect existing wallet"); - logger.error(err); - - // At least clean up state on our side. - handleDisconnected(wallet.id); - }); - } - - if (pending || accounts.length) { - setSelectedWalletId(walletId); - } - - store.dispatch({ - type: "WALLET_CONNECTED", - payload: { walletId, pending, accounts }, - }); - }; - - const handleAccountsChanged = ( - walletId: string, - { accounts }: WalletEvents["accountsChanged"] - ) => { - store.dispatch({ - type: "ACCOUNTS_CHANGED", - payload: { walletId, accounts }, - }); - }; - - const handleNetworkChanged = ( - walletId: string, - { networkId }: WalletEvents["networkChanged"] - ) => { - emitter.emit("networkChanged", { walletId, networkId }); - }; - - walletEmitter.on("disconnected", () => { - handleDisconnected(module.id); - }); - - walletEmitter.on("connected", (event) => { - handleConnected(module.id, event); - }); - - walletEmitter.on("accountsChanged", (event) => { - handleAccountsChanged(module.id, event); - }); - - walletEmitter.on("networkChanged", (event) => { - handleNetworkChanged(module.id, event); - }); - - 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); - }; - - instances[module.id] = wallet; - - return wallet; - }, - }); - } - - let selectedWalletId = getSelectedWalletId(); - let accounts: Array = []; - const selectedWallet = await getWallet(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.getAccounts().catch((err) => { - logger.log( - `Failed to get accounts for ${selectedWallet.id} during setup` - ); - logger.error(err); - - return []; - }); - } - - if (!accounts.length) { - removeSelectedWalletId(); - selectedWalletId = null; - } - - store.dispatch({ - type: "SETUP_WALLET_MODULES", - payload: { - modules, - accounts, - selectedWalletId, - }, - }); - - return { - getWallet: async (id?: string) => { - const walletId = id || store.getState().selectedWalletId; - const wallet = await getWallet(walletId); - - if (!wallet) { - if (walletId) { - throw new Error("Invalid wallet id"); - } - - throw new Error("No wallet selected"); - } - - return wallet; - }, - }; -}; diff --git a/packages/core/src/lib/wallet-selector.ts b/packages/core/src/lib/wallet-selector.ts index ca5df8ff8..ca34ffa35 100644 --- a/packages/core/src/lib/wallet-selector.ts +++ b/packages/core/src/lib/wallet-selector.ts @@ -6,10 +6,10 @@ import { WalletSelectorParams, } from "./wallet-selector.types"; import { WalletSelectorModal } from "./modal/modal.types"; -import { setupModal } from "./modal/modal"; import { EventEmitter, Logger } from "./services"; -import { setupWalletModules } from "./wallet-modules"; -import { Wallet } from "./wallet/wallet.types"; +import { Wallet } from "./wallet"; +import { setupWalletModules } from "./modules/wallet-modules"; +import { setupModal } from "./modal/modal"; export const setupWalletSelector = async ( params: WalletSelectorParams diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts index c9776f086..4783e24a5 100644 --- a/packages/core/src/lib/wallet/wallet.types.ts +++ b/packages/core/src/lib/wallet/wallet.types.ts @@ -150,13 +150,6 @@ export type Wallet = export type WalletType = Wallet["type"]; -export type WalletModule = { - id: Variation["id"]; - type: Variation["type"]; - metadata: Variation["metadata"]; - wallet(): Promise; -}; - export interface WalletBehaviourOptions { id: Variation["id"]; type: Variation["type"]; From 0ea07e1d9a0fbe9a3b3cd1a9916f030fb5488a81 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Wed, 4 May 2022 11:10:04 +0100 Subject: [PATCH 49/64] Simplified params. --- packages/core/src/index.ts | 1 + .../core/src/lib/modules/wallet-instances.ts | 17 ++++++++-------- .../core/src/lib/modules/wallet-modules.ts | 4 ++-- packages/core/src/lib/wallet/wallet.types.ts | 20 ++++++++++--------- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c3e8fc133..c8172de99 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -16,6 +16,7 @@ export { export { WalletModuleFactory, + WalletModule, WalletBehaviourFactory, WalletBehaviourOptions, Wallet, diff --git a/packages/core/src/lib/modules/wallet-instances.ts b/packages/core/src/lib/modules/wallet-instances.ts index 4c55db163..55ea24d60 100644 --- a/packages/core/src/lib/modules/wallet-instances.ts +++ b/packages/core/src/lib/modules/wallet-instances.ts @@ -1,21 +1,21 @@ import { EventEmitter, logger, Provider, storage } from "../services"; -import { Wallet, WalletEvents, WalletModuleFactory } from "../wallet"; -import { Store } from "../store.types"; +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 { - module: NonNullable>>; - getWallet: (id: string | null) => Promise; + modules: Array; + module: WalletModule; store: Store; options: Options; emitter: EventEmitter; } export const setupWalletInstance = async ({ + modules, module, - getWallet, store, options, emitter, @@ -46,14 +46,15 @@ export const setupWalletInstance = async ({ } if (selectedWalletId && selectedWalletId !== walletId) { - const wallet = (await getWallet(selectedWalletId))!; + const selectedModule = modules.find((x) => x.id === selectedWalletId)!; + const selectedWallet = await selectedModule.wallet(); - await wallet.disconnect().catch((err) => { + await selectedWallet.disconnect().catch((err) => { logger.log("Failed to disconnect existing wallet"); logger.error(err); // At least clean up state on our side. - handleDisconnected(wallet.id); + handleDisconnected(selectedWallet.id); }); } diff --git a/packages/core/src/lib/modules/wallet-modules.ts b/packages/core/src/lib/modules/wallet-modules.ts index 07240e55a..8d43be5bd 100644 --- a/packages/core/src/lib/modules/wallet-modules.ts +++ b/packages/core/src/lib/modules/wallet-modules.ts @@ -4,7 +4,7 @@ import { logger, EventEmitter, storage } from "../services"; import { WalletSelectorEvents } from "../wallet-selector.types"; import { WalletModuleFactory, Wallet } from "../wallet"; import { setupWalletInstance } from "./wallet-instances"; -import { PENDING_SELECTED_WALLET_ID, SELECTED_WALLET_ID } from "../constants"; +import { PENDING_SELECTED_WALLET_ID } from "../constants"; interface WalletModulesParams { factories: Array; @@ -74,8 +74,8 @@ export const setupWalletModules = async ({ } instance = await setupWalletInstance({ + modules, module, - getWallet, store, options, emitter, diff --git a/packages/core/src/lib/wallet/wallet.types.ts b/packages/core/src/lib/wallet/wallet.types.ts index 4783e24a5..dca77f7a9 100644 --- a/packages/core/src/lib/wallet/wallet.types.ts +++ b/packages/core/src/lib/wallet/wallet.types.ts @@ -161,7 +161,7 @@ export interface WalletBehaviourOptions { storage: StorageService; } -// Note: TypeScript doesn't seem to like reusing this in WalletModuleFactory. +// Note: TypeScript doesn't seem to like reusing this in WalletModule. export type WalletBehaviourFactory< Variation extends Wallet, ExtraOptions extends object = object @@ -169,12 +169,14 @@ export type WalletBehaviourFactory< options: WalletBehaviourOptions & ExtraOptions ) => Promise>; +export type WalletModule = { + id: Variation["id"]; + type: Variation["type"]; + metadata: Variation["metadata"]; + init( + options: WalletBehaviourOptions + ): Promise>; +}; + export type WalletModuleFactory = - () => Promise<{ - id: Variation["id"]; - type: Variation["type"]; - metadata: Variation["metadata"]; - init( - options: WalletBehaviourOptions - ): Promise>; - } | null>; + () => Promise | null>; From 9c34c174c4f12829b0922e794c4061f7de1c59cc Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Wed, 4 May 2022 13:10:10 +0100 Subject: [PATCH 50/64] Fixed tests. --- packages/core/src/lib/testUtils.ts | 22 ++++++++++++++----- packages/ledger/src/lib/ledger.spec.ts | 20 ++++++----------- .../near-wallet/src/lib/near-wallet.spec.ts | 22 ++++++------------- 3 files changed, 30 insertions(+), 34 deletions(-) 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/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/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({ From 7be13d59cfad67fe3ac1332b9d664180ce65a8cd Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Wed, 4 May 2022 13:27:48 +0100 Subject: [PATCH 51/64] Updated example implementation. --- docs/guides/custom-wallets.md | 57 ++++++++++------------------------- 1 file changed, 16 insertions(+), 41 deletions(-) diff --git a/docs/guides/custom-wallets.md b/docs/guides/custom-wallets.md index d4b827fab..b14bc3355 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,35 +19,20 @@ export interface MyWalletParams { const MyWallet: WalletBehaviourFactory = ({ options, - emitter, provider, }) => { 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() { // Return list of connected accounts. - return []; }, @@ -60,14 +43,12 @@ const MyWallet: WalletBehaviourFactory = ({ }) { // Sign a list of NEAR Actions before sending via an RPC endpoint. // An RPC provider is injected to make this process easier and configured based on options.network. - return provider.sendTransaction(signedTx); }, async signAndSendTransactions({ transactions }) { // Sign a list of Transactions before sending via an RPC endpoint. // An RPC provider is injected to make this process easier and configured based on options.network. - const signedTxs = []; return Promise.all( @@ -79,14 +60,20 @@ 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, + }; }; } ``` @@ -106,12 +93,6 @@ 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. @@ -143,9 +124,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. From 9c56919a4532ea3f6c326f6a51767107e25106fd Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Wed, 4 May 2022 13:42:13 +0100 Subject: [PATCH 52/64] Updated reset of guide. --- docs/guides/custom-wallets.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/docs/guides/custom-wallets.md b/docs/guides/custom-wallets.md index b14bc3355..44a030b0a 100644 --- a/docs/guides/custom-wallets.md +++ b/docs/guides/custom-wallets.md @@ -78,11 +78,13 @@ export function setupMyWallet({ } ``` -`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 used 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: @@ -95,17 +97,13 @@ Although we've tried to implement a polymorphic approach to wallets, there are s ### `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` From a358ebc09e40097c0e26824bf899a3497b528cd0 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Wed, 4 May 2022 13:43:25 +0100 Subject: [PATCH 53/64] Minor wording improvement. --- docs/guides/custom-wallets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/custom-wallets.md b/docs/guides/custom-wallets.md index 44a030b0a..bb782d1f1 100644 --- a/docs/guides/custom-wallets.md +++ b/docs/guides/custom-wallets.md @@ -84,7 +84,7 @@ export function setupMyWallet({ - `metadata`: Metadata for displaying information to the user. - `init`: The implementation (behaviour) of the wallet. -A variation of `WalletModule` is added to state during setup under `modules` (`ModuleState`) and used 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`. +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: From ed4a05e68cea69e93c50859bd5d58e4bec0eb6f5 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Wed, 4 May 2022 13:50:29 +0100 Subject: [PATCH 54/64] Improved formatting. --- docs/guides/custom-wallets.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/guides/custom-wallets.md b/docs/guides/custom-wallets.md index bb782d1f1..ac459b068 100644 --- a/docs/guides/custom-wallets.md +++ b/docs/guides/custom-wallets.md @@ -21,9 +21,12 @@ const MyWallet: WalletBehaviourFactory = ({ options, provider, }) => { + // Initialise wallet-sepecific client(s) here. + return { async connect() { // Connect to My Wallet for access to account(s). + return []; }, @@ -33,6 +36,7 @@ const MyWallet: WalletBehaviourFactory = ({ async getAccounts() { // Return list of connected accounts. + return []; }, @@ -43,12 +47,14 @@ const MyWallet: WalletBehaviourFactory = ({ }) { // Sign a list of NEAR Actions before sending via an RPC endpoint. // An RPC provider is injected to make this process easier and configured based on options.network. + return provider.sendTransaction(signedTx); }, async signAndSendTransactions({ transactions }) { // Sign a list of Transactions before sending via an RPC endpoint. // An RPC provider is injected to make this process easier and configured based on options.network. + const signedTxs = []; return Promise.all( From 18ef4816ef2a93a965607348bb409d4d6281b3ca Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Wed, 4 May 2022 16:05:14 +0100 Subject: [PATCH 55/64] Renamed module. --- .../src/lib/modules/{wallet-instances.ts => wallet-instance.ts} | 0 packages/core/src/lib/modules/wallet-modules.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/core/src/lib/modules/{wallet-instances.ts => wallet-instance.ts} (100%) diff --git a/packages/core/src/lib/modules/wallet-instances.ts b/packages/core/src/lib/modules/wallet-instance.ts similarity index 100% rename from packages/core/src/lib/modules/wallet-instances.ts rename to packages/core/src/lib/modules/wallet-instance.ts diff --git a/packages/core/src/lib/modules/wallet-modules.ts b/packages/core/src/lib/modules/wallet-modules.ts index 8d43be5bd..d40549f3c 100644 --- a/packages/core/src/lib/modules/wallet-modules.ts +++ b/packages/core/src/lib/modules/wallet-modules.ts @@ -3,7 +3,7 @@ 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-instances"; +import { setupWalletInstance } from "./wallet-instance"; import { PENDING_SELECTED_WALLET_ID } from "../constants"; interface WalletModulesParams { From abea76f0b367cabce80a5dce1a227002e8f8e213 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Wed, 4 May 2022 16:12:12 +0100 Subject: [PATCH 56/64] Improved accounts changed event. --- packages/core/src/lib/modules/wallet-instance.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/core/src/lib/modules/wallet-instance.ts b/packages/core/src/lib/modules/wallet-instance.ts index 55ea24d60..6ea184eef 100644 --- a/packages/core/src/lib/modules/wallet-instance.ts +++ b/packages/core/src/lib/modules/wallet-instance.ts @@ -64,10 +64,23 @@ export const setupWalletInstance = async ({ }); }; - const handleAccountsChanged = ( + const handleAccountsChanged = async ( walletId: string, { accounts }: WalletEvents["accountsChanged"] ) => { + if (!accounts.length) { + const walletModule = modules.find((x) => x.id === walletId)!; + const wallet = await walletModule.wallet(); + + return wallet.disconnect().catch((err) => { + logger.log("Failed to disconnect existing wallet"); + logger.error(err); + + // At least clean up state on our side. + handleDisconnected(walletId); + }); + } + store.dispatch({ type: "ACCOUNTS_CHANGED", payload: { walletId, accounts }, From 91dc7887ada9b80cc95fbe4344b77343a6d9fc77 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Wed, 4 May 2022 16:44:36 +0100 Subject: [PATCH 57/64] Minor refactor. --- .../core/src/lib/modules/wallet-modules.ts | 19 ++----------------- packages/core/src/lib/wallet-selector.ts | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/packages/core/src/lib/modules/wallet-modules.ts b/packages/core/src/lib/modules/wallet-modules.ts index d40549f3c..9a004e079 100644 --- a/packages/core/src/lib/modules/wallet-modules.ts +++ b/packages/core/src/lib/modules/wallet-modules.ts @@ -31,9 +31,7 @@ export const setupWalletModules = async ({ return null; } - const wallet = await module.wallet(); - - return wallet as Variation; + return (await module.wallet()) as Variation; }; const validateWallet = async (id: string | null) => { @@ -128,19 +126,6 @@ export const setupWalletModules = async ({ }); return { - getWallet: async (id?: string) => { - const walletId = id || store.getState().selectedWalletId; - const wallet = await getWallet(walletId); - - if (!wallet) { - if (walletId) { - throw new Error("Invalid wallet id"); - } - - throw new Error("No wallet selected"); - } - - return wallet; - }, + getWallet, }; }; diff --git a/packages/core/src/lib/wallet-selector.ts b/packages/core/src/lib/wallet-selector.ts index ca34ffa35..313004f8f 100644 --- a/packages/core/src/lib/wallet-selector.ts +++ b/packages/core/src/lib/wallet-selector.ts @@ -39,8 +39,19 @@ export const setupWalletSelector = async ( return Boolean(accounts.length); }, options, - wallet: (id?: string) => { - return walletModules.getWallet(id); + wallet: async (id?: string) => { + const walletId = id || store.getState().selectedWalletId; + const wallet = await walletModules.getWallet(walletId); + + if (!wallet) { + if (walletId) { + throw new Error("Invalid wallet id"); + } + + throw new Error("No wallet selected"); + } + + return wallet; }, on: (eventName, callback) => { return emitter.on(eventName, callback); From 105f0db953c2dfab8b8957571dab36b7135e1b2f Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Wed, 4 May 2022 17:00:09 +0100 Subject: [PATCH 58/64] Minor refactor. --- .../core/src/lib/modules/wallet-instance.ts | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/packages/core/src/lib/modules/wallet-instance.ts b/packages/core/src/lib/modules/wallet-instance.ts index 6ea184eef..c16187c82 100644 --- a/packages/core/src/lib/modules/wallet-instance.ts +++ b/packages/core/src/lib/modules/wallet-instance.ts @@ -29,6 +29,19 @@ export const setupWalletInstance = async ({ }); }; + 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"] @@ -46,16 +59,7 @@ export const setupWalletInstance = async ({ } if (selectedWalletId && selectedWalletId !== walletId) { - const selectedModule = modules.find((x) => x.id === selectedWalletId)!; - const selectedWallet = await selectedModule.wallet(); - - await selectedWallet.disconnect().catch((err) => { - logger.log("Failed to disconnect existing wallet"); - logger.error(err); - - // At least clean up state on our side. - handleDisconnected(selectedWallet.id); - }); + await disconnect(selectedWalletId); } store.dispatch({ @@ -69,16 +73,7 @@ export const setupWalletInstance = async ({ { accounts }: WalletEvents["accountsChanged"] ) => { if (!accounts.length) { - const walletModule = modules.find((x) => x.id === walletId)!; - const wallet = await walletModule.wallet(); - - return wallet.disconnect().catch((err) => { - logger.log("Failed to disconnect existing wallet"); - logger.error(err); - - // At least clean up state on our side. - handleDisconnected(walletId); - }); + return disconnect(walletId); } store.dispatch({ From 0c23edd6712753c812d4a3fd9ca869c64b1cdb45 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Wed, 4 May 2022 17:02:59 +0100 Subject: [PATCH 59/64] More refactoring. --- .../core/src/lib/modules/wallet-instance.ts | 36 ++++++------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/packages/core/src/lib/modules/wallet-instance.ts b/packages/core/src/lib/modules/wallet-instance.ts index c16187c82..8e664a451 100644 --- a/packages/core/src/lib/modules/wallet-instance.ts +++ b/packages/core/src/lib/modules/wallet-instance.ts @@ -68,27 +68,6 @@ export const setupWalletInstance = async ({ }); }; - const handleAccountsChanged = async ( - walletId: string, - { accounts }: WalletEvents["accountsChanged"] - ) => { - if (!accounts.length) { - return disconnect(walletId); - } - - store.dispatch({ - type: "ACCOUNTS_CHANGED", - payload: { walletId, accounts }, - }); - }; - - const handleNetworkChanged = ( - walletId: string, - { networkId }: WalletEvents["networkChanged"] - ) => { - emitter.emit("networkChanged", { walletId, networkId }); - }; - walletEmitter.on("disconnected", () => { handleDisconnected(module.id); }); @@ -97,12 +76,19 @@ export const setupWalletInstance = async ({ handleConnected(module.id, event); }); - walletEmitter.on("accountsChanged", (event) => { - handleAccountsChanged(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", (event) => { - handleNetworkChanged(module.id, event); + walletEmitter.on("networkChanged", ({ networkId }) => { + emitter.emit("networkChanged", { walletId: module.id, networkId }); }); const wallet = { From dfe0c966a78be22ca1cf7d8d617afd4641b0f975 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Wed, 4 May 2022 17:23:30 +0100 Subject: [PATCH 60/64] Cleaned up init validation setup. --- .../core/src/lib/modules/wallet-modules.ts | 67 +++++++++++-------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/packages/core/src/lib/modules/wallet-modules.ts b/packages/core/src/lib/modules/wallet-modules.ts index 9a004e079..bb3f41ae3 100644 --- a/packages/core/src/lib/modules/wallet-modules.ts +++ b/packages/core/src/lib/modules/wallet-modules.ts @@ -52,6 +52,43 @@ export const setupWalletModules = async ({ 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](); @@ -86,35 +123,7 @@ export const setupWalletModules = async ({ }); } - let selectedWalletId = store.getState().selectedWalletId; - const pendingSelectedWalletId = storage.getItem( - PENDING_SELECTED_WALLET_ID - ); - - if (pendingSelectedWalletId) { - const accounts = await validateWallet(pendingSelectedWalletId); - - if (accounts.length) { - const selectedWallet = await getWallet(selectedWalletId); - - if (selectedWallet) { - await selectedWallet.disconnect().catch((err) => { - logger.log("Failed to disconnect existing wallet"); - logger.error(err); - }); - - selectedWalletId = pendingSelectedWalletId; - } - } - - storage.removeItem(PENDING_SELECTED_WALLET_ID); - } - - const accounts = await validateWallet(selectedWalletId); - - if (!accounts.length) { - selectedWalletId = null; - } + const { accounts, selectedWalletId } = await getSelectedWallet(); store.dispatch({ type: "SETUP_WALLET_MODULES", From afbb11342514838bb51101021ce16edcea0a5d05 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Wed, 4 May 2022 17:29:19 +0100 Subject: [PATCH 61/64] More refactoring. --- packages/core/src/lib/wallet-selector.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/core/src/lib/wallet-selector.ts b/packages/core/src/lib/wallet-selector.ts index 313004f8f..5f1454323 100644 --- a/packages/core/src/lib/wallet-selector.ts +++ b/packages/core/src/lib/wallet-selector.ts @@ -40,11 +40,13 @@ export const setupWalletSelector = async ( }, options, wallet: async (id?: string) => { - const walletId = id || store.getState().selectedWalletId; - const wallet = await walletModules.getWallet(walletId); + const { selectedWalletId } = store.getState(); + const wallet = await walletModules.getWallet( + id || selectedWalletId + ); if (!wallet) { - if (walletId) { + if (id) { throw new Error("Invalid wallet id"); } From 211618bd63c9f5b7abb4667043ab80f4920697f6 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Wed, 4 May 2022 18:09:17 +0100 Subject: [PATCH 62/64] Fixed connected logic in LedgerClient --- packages/ledger/src/lib/ledger-client.ts | 49 ++++++++++++++++++------ 1 file changed, 38 insertions(+), 11 deletions(-) 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(); From 6fbe562d0d544ea8cb5068beecd9eff82469822e Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Wed, 4 May 2022 18:09:43 +0100 Subject: [PATCH 63/64] Removed need for setupEvents. --- packages/ledger/src/lib/ledger.ts | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/packages/ledger/src/lib/ledger.ts b/packages/ledger/src/lib/ledger.ts index 552b0f8ea..eb846b494 100644 --- a/packages/ledger/src/lib/ledger.ts +++ b/packages/ledger/src/lib/ledger.ts @@ -60,8 +60,6 @@ const Ledger: WalletBehaviourFactory = async ({ }) => { const _state = setupLedgerState(storage); - const debugMode = false; - const getAccounts = ( authData: AuthData | null = _state.authData ): Array => { @@ -94,24 +92,6 @@ const Ledger: WalletBehaviourFactory = async ({ cleanup(); }; - const setupEvents = () => { - _state.subscriptions.push( - _state.client.on("disconnect", (err) => { - logger.error(err); - - disconnect(); - }) - ); - - if (debugMode) { - _state.subscriptions.push( - _state.client.listen((data) => { - logger.log("Ledger:init:logs", data); - }) - ); - } - }; - const connectLedgerDevice = async () => { if (_state.client.isConnected()) { return; @@ -218,10 +198,6 @@ const Ledger: WalletBehaviourFactory = async ({ return signedTransactions; }; - if (_state.authData) { - setupEvents(); - } - return { async connect({ derivationPath }) { const existingAccounts = getAccounts(); @@ -257,8 +233,6 @@ const Ledger: WalletBehaviourFactory = async ({ storage.setItem(LOCAL_STORAGE_AUTH_DATA, authData); _state.authData = authData; - setupEvents(); - return getAccounts(); }) .catch(async (err) => { From 8c8d0fc4c36d712c69b861bcd67bbe1333bcc7c0 Mon Sep 17 00:00:00 2001 From: Lewis Barnes Date: Wed, 4 May 2022 18:14:59 +0100 Subject: [PATCH 64/64] Fixed tests. --- packages/ledger/src/lib/ledger-client.spec.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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); }); });