From ab4b1709f291bfd2ab0e046ace123123850f6215 Mon Sep 17 00:00:00 2001 From: Mark Nardi Date: Tue, 28 May 2024 08:42:53 +0200 Subject: [PATCH 1/9] add bloom package --- packages/bloom/README.md | 94 +++++++++ packages/bloom/package.json | 77 +++++++ packages/bloom/src/icon.ts | 6 + packages/bloom/src/index.ts | 29 +++ packages/bloom/src/types.ts | 42 ++++ packages/bloom/src/utils.ts | 79 +++++++ packages/bloom/src/validation.ts | 56 +++++ packages/bloom/src/walletConnect.ts | 305 ++++++++++++++++++++++++++++ packages/bloom/tsconfig.json | 15 ++ packages/demo/src/App.svelte | 2 + 10 files changed, 705 insertions(+) create mode 100644 packages/bloom/README.md create mode 100644 packages/bloom/package.json create mode 100644 packages/bloom/src/icon.ts create mode 100644 packages/bloom/src/index.ts create mode 100644 packages/bloom/src/types.ts create mode 100644 packages/bloom/src/utils.ts create mode 100644 packages/bloom/src/validation.ts create mode 100644 packages/bloom/src/walletConnect.ts create mode 100644 packages/bloom/tsconfig.json diff --git a/packages/bloom/README.md b/packages/bloom/README.md new file mode 100644 index 000000000..4f922a1f4 --- /dev/null +++ b/packages/bloom/README.md @@ -0,0 +1,94 @@ +# @web3-onboard/bloom + +## Wallet module for connecting Bloom to web3-onboard + +### Install + +`npm i @web3-onboard/core @web3-onboard/bloom` + +## Options + +```typescript +type WalletConnectOptions = { + /** + * Project ID associated with [WalletConnect account](https://cloud.walletconnect.com) + */ + projectId: string + /** + * Defaults to `appMetadata.explore` that is supplied to the web3-onboard init + * Strongly recommended to provide atleast one URL as it is required by some wallets (i.e. MetaMask) + * To connect with walletconnect + */ + dappUrl?: string + /** + * List of Required Chain(s) ID for wallets to support in number format (integer or hex) + * Defaults to [1] - Ethereum + */ + requiredChains?: number[] | undefined + /** + * List of Optional Chain(s) ID for wallets to support in number format (integer or hex) + * Defaults to the chains provided within the web3-onboard init chain property + */ + optionalChains?: number[] | undefined + /** + * Additional required methods to be added to the default list of ['eth_sendTransaction', 'personal_sign'] + * Passed methods to be included along with the defaults methods - see https://docs.walletconnect.com/2.0/advanced/providers/ethereum#required-and-optional-methods + */ + additionalRequiredMethods?: string[] | undefined + /** + * Additional methods to be added to the default list of ['eth_sendTransaction', 'eth_signTransaction', 'personal_sign', 'eth_sign', 'eth_signTypedData', 'eth_signTypedData_v4'] + * Passed methods to be included along with the defaults methods - see https://docs.walletconnect.com/2.0/web/walletConnectModal/options + */ + additionalOptionalMethods?: string[] | undefined +} +``` + +## Usage + +```typescript +import Onboard from '@web3-onboard/core' +import walletConnectModule from '@web3-onboard/walletconnect' + +const wcInitOptions = { + /** + * Project ID associated with [WalletConnect account](https://cloud.walletconnect.com) + */ + projectId: 'abc123...', + /** + * Chains required to be supported by all wallets connecting to your DApp + */ + requiredChains: [1], + /** + * Chains required to be supported by all wallets connecting to your DApp + */ + optionalChains: [42161, 8453, 10, 137, 56], + /** + * Defaults to `appMetadata.explore` that is supplied to the web3-onboard init + * Strongly recommended to provide atleast one URL as it is required by some wallets (i.e. MetaMask) + * To connect with WalletConnect + */ + dappUrl: 'http://YourAwesomeDapp.com' +} + +// initialize the module with options +const bloom = initBloom(wcInitOptions) + +// can also initialize with no options... + +const onboard = Onboard({ + // ... other Onboard options + wallets: [ + bloom + //... other wallets + ] +}) + +const connectedWallets = await onboard.connectWallet() + +// Assuming only wallet connect is connected, index 0 +// `instance` will give insight into the WalletConnect info +// such as namespaces, methods, chains, etc per wallet connected +const { instance } = connectedWallets[0] + +console.log(connectedWallets) +``` diff --git a/packages/bloom/package.json b/packages/bloom/package.json new file mode 100644 index 000000000..ae850fce7 --- /dev/null +++ b/packages/bloom/package.json @@ -0,0 +1,77 @@ +{ + "name": "@web3-onboard/bloom", + "version": "2.1.2", + "description": "Unstoppable Domains module for connecting to Web3-Onboard. Web3-Onboard makes it simple to connect Ethereum hardware and software wallets to your dapp. Features standardised spec compliant web3 providers for all supported wallets, framework agnostic modern javascript UI with code splitting, CSS customization, multi-chain and multi-account support, reactive wallet state subscriptions and real-time transaction state change notifications.", + "keywords": [ + "Bloom", + "Iota", + "Shimmer", + "Ethereum", + "Web3", + "EVM", + "dapp", + "Multichain", + "Wallet", + "Transaction", + "Provider", + "Hardware Wallet", + "Notifications", + "React", + "Svelte", + "Vue", + "Next", + "Nuxt", + "MetaMask", + "Coinbase", + "WalletConnect", + "Ledger", + "Trezor", + "Connect Wallet", + "Ethereum Hooks", + "Blocknative", + "Mempool", + "pending", + "confirmed", + "Injected Wallet", + "Crypto", + "Crypto Wallet", + "Domain Name", + "Unstoppable Domains", + "Unstoppable" + ], + "repository": { + "type": "git", + "url": "https://github.com/blocknative/web3-onboard.git", + "directory": "packages/bloom" + }, + "homepage": "https://onboard.blocknative.com", + "bugs": "https://github.com/blocknative/web3-onboard/issues", + "module": "dist/index.js", + "browser": "dist/index.js", + "main": "dist/index.js", + "type": "module", + "typings": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "dev": "tsc -w", + "type-check": "tsc --noEmit" + }, + "license": "MIT", + "devDependencies": { + "typescript": "^4.5.5", + "@walletconnect/types": "^2.7.0" + }, + "dependencies": { + "@ethersproject/providers": "^5.5.0", + "@walletconnect/client": "^1.8.0", + "@walletconnect/ethereum-provider": "2.9.1", + "@walletconnect/modal": "2.6.1", + "@walletconnect/qrcode-modal": "^1.8.0", + "@web3-onboard/common": "^2.3.3", + "joi": "17.9.1", + "rxjs": "^7.5.2" + } +} diff --git a/packages/bloom/src/icon.ts b/packages/bloom/src/icon.ts new file mode 100644 index 000000000..072d5a609 --- /dev/null +++ b/packages/bloom/src/icon.ts @@ -0,0 +1,6 @@ +export default ` + + + + +` diff --git a/packages/bloom/src/index.ts b/packages/bloom/src/index.ts new file mode 100644 index 000000000..4c24d5576 --- /dev/null +++ b/packages/bloom/src/index.ts @@ -0,0 +1,29 @@ +import { validateWCInitOptions } from './validation.js' +import type { WalletInit } from '@web3-onboard/common' +import walletConnect from './walletConnect.js' +import type { WalletConnectOptions } from 'types.js' + + +function initBloom(options: WalletConnectOptions): WalletInit { + if (!options) { + throw new Error( + `WalletConnect requires an initialization object to be passed - see the official docs for an example: https://onboard.blocknative.com/docs/wallets/walletconnect` + ) + } + + const error = validateWCInitOptions(options) + if (error) { + throw error + } + + const walletName = "Bloom" + options.handleUri = (uri: string) => { + const deeplink = `bloom://wallet-connect/wc?uri=${encodeURIComponent(uri)}` + window.open(deeplink, '_blank') + return Promise.resolve() + } + + return walletConnect(walletName, options) +} + +export default initBloom diff --git a/packages/bloom/src/types.ts b/packages/bloom/src/types.ts new file mode 100644 index 000000000..3df47bea8 --- /dev/null +++ b/packages/bloom/src/types.ts @@ -0,0 +1,42 @@ +import { EthereumProviderOptions } from '@walletconnect/ethereum-provider/dist/types/EthereumProvider' + +export type WalletConnectOptions = { + /** + * Optional function to handle WalletConnect URI when it becomes available + */ + handleUri?: (uri: string) => Promise + /** + * Project ID associated with [WalletConnect account](https://cloud.walletconnect.com) + */ + projectId: string + /** + * Defaults to `appMetadata.explore` that is supplied to the web3-onboard init + * Strongly recommended to provide atleast one URL as it is required by some wallets (i.e. MetaMask) + * To connect with walletconnect + */ + dappUrl?: string + /** + * List of Required Chain(s) ID for wallets to support in number format (integer or hex) + * Defaults to [1] - Ethereum + */ + requiredChains?: number[] | undefined + /** + * List of Optional Chain(s) ID for wallets to support in number format (integer or hex) + * Defaults to the chains provided within the web3-onboard init chain property + */ + optionalChains?: number[] | undefined + /** + * `undefined` by default, see https://docs.walletconnect.com/2.0/web/walletConnectModal/options + */ + qrModalOptions?: EthereumProviderOptions['qrModalOptions'] + /** + * Additional required methods to be added to the default list of ['eth_sendTransaction', 'personal_sign'] + * Passed methods to be included along with the defaults methods - see https://docs.walletconnect.com/2.0/advanced/providers/ethereum#required-and-optional-methods + */ + additionalRequiredMethods?: string[] | undefined + /** + * Additional methods to be added to the default list of ['eth_sendTransaction', 'eth_signTransaction', 'personal_sign', 'eth_sign', 'eth_signTypedData', 'eth_signTypedData_v4'] + * Passed methods to be included along with the defaults methods - see https://docs.walletconnect.com/2.0/web/walletConnectModal/options + */ + additionalOptionalMethods?: string[] | undefined +} diff --git a/packages/bloom/src/utils.ts b/packages/bloom/src/utils.ts new file mode 100644 index 000000000..a82beec56 --- /dev/null +++ b/packages/bloom/src/utils.ts @@ -0,0 +1,79 @@ +import type { CoreTypes } from '@walletconnect/types' + +export function getMetaData(appMetadata: any): CoreTypes.Metadata | undefined { + if (!appMetadata) return undefined + const wcMetaData: CoreTypes.Metadata = { + name: appMetadata.name, + description: appMetadata.description || '', + url: appMetadata.explore || appMetadata.gettingStartedGuide || '', + icons: [] + } + + if (appMetadata.icon !== undefined && appMetadata.icon.length) { + wcMetaData.icons = [appMetadata.icon] + } + if (appMetadata.logo !== undefined && appMetadata.logo.length) { + wcMetaData.icons = wcMetaData.icons.length + ? [...wcMetaData.icons, appMetadata.logo] + : [appMetadata.logo] + } + + return wcMetaData +} + +export function buildWCChains(requiredChains: number[], optionalChains: number[], chains: { id: string }[]): { + requiredChains: number[]; + optionalChains: number[]; +} { + // default to mainnet + const requiredChainsParsed: number[] = + Array.isArray(requiredChains) && + requiredChains.length && + requiredChains.every(num => !isNaN(num)) + // @ts-ignore + // Required as WC package does not support hex numbers + ? requiredChains.map(chainID => parseInt(chainID)) + : [] + + // Defaults to the chains provided within the web3-onboard init chain property + const optionalChainsParsed: number[] = + Array.isArray(optionalChains) && + optionalChains.length && + optionalChains.every(num => !isNaN(num)) + // @ts-ignore + // Required as WC package does not support hex numbers + ? optionalChains.map(chainID => parseInt(chainID)) + : chains.map(({ id }) => parseInt(id, 16)) + + return { + requiredChains: requiredChainsParsed, + optionalChains: optionalChainsParsed, + } +} + +export function buildWCMethods( + additionalRequiredMethods: string[], + additionalOptionalMethods: string[], + requiredMethods: string[], + allMethods: string[], +): { + requiredMethods: string[]; + optionalMethods: string[] +} { + const requiredMethodsSet = new Set( + additionalRequiredMethods && Array.isArray(additionalRequiredMethods) + ? [...additionalRequiredMethods, ...requiredMethods] + : requiredMethods + ) + const _requiredMethods = Array.from(requiredMethodsSet) + + const optionalMethods = + additionalOptionalMethods && Array.isArray(additionalOptionalMethods) + ? [...additionalOptionalMethods, ...allMethods] + : allMethods + + return { + requiredMethods: _requiredMethods, + optionalMethods + } +} diff --git a/packages/bloom/src/validation.ts b/packages/bloom/src/validation.ts new file mode 100644 index 000000000..5d9d15809 --- /dev/null +++ b/packages/bloom/src/validation.ts @@ -0,0 +1,56 @@ +import Joi from 'joi' +import type { WalletConnectOptions } from './types.js' + +const wcOptions = Joi.object({ + handleUri: Joi.func().optional(), + version: Joi.number() + .optional() + .custom((value, helpers) => { + if (value === 1) { + console.warn( + 'Version 1 of WalletConnect has been fully deprecated. This version of @web3-onboard/walletconnect only supports version 2' + ) + } else if (value !== 2 && value !== undefined) { + return helpers.error('any.invalid', { + message: 'Invalid version number. This version of @web3-onboard/walletconnect only supports version 2' + }) + } + return value // return the value unchanged if it's valid or not provided + }, 'Custom version validation'), + projectId: Joi.string().messages({ + 'any.required': `WalletConnect version 2 requires a projectId. Please visit https://cloud.walletconnect.com to get one.` + }), + dappUrl: Joi.string() + .optional() + .custom((value, helpers) => { + if (!value) { + return helpers.message({ + message: + 'It is strongly recommended to supply a dappUrl as it is required by some wallets (i.e. MetaMask) to allow connection.', + type: 'any.custom' + }) + } + return value // return the value unchanged if it's provided + }, 'Custom dappUrl validation'), + requiredChains: Joi.array().items(Joi.number()).optional(), + optionalChains: Joi.array().items(Joi.number()).optional(), + qrModalOptions: Joi.object().optional(), + additionalRequiredMethods: Joi.array().items(Joi.string()).optional(), + additionalOptionalMethods: Joi.array().items(Joi.string()).optional() +}) + +type ValidateReturn = Joi.ValidationResult | null + +const validate = ( + validator: Joi.AnySchema, + data: unknown +): ValidateReturn => { + const result = validator.validate(data) + return result.error ? result : null +} + +export const validateWCInitOptions = ( + data: WalletConnectOptions +): ValidateReturn => { + return validate(wcOptions, data) +} diff --git a/packages/bloom/src/walletConnect.ts b/packages/bloom/src/walletConnect.ts new file mode 100644 index 000000000..c46ff15aa --- /dev/null +++ b/packages/bloom/src/walletConnect.ts @@ -0,0 +1,305 @@ +import type { EthereumProviderOptions } from '@walletconnect/ethereum-provider/dist/types/EthereumProvider' +import type { JQueryStyleEventEmitter } from 'rxjs/internal/observable/fromEvent' +import type { WalletConnectOptions } from './types.js' +import type { + Chain, + ProviderAccounts, + WalletInit, + EIP1193Provider +} from '@web3-onboard/common' +import { buildWCChains, buildWCMethods, getMetaData } from 'utils.js' + +// methods that require user interaction +const methods = [ + 'eth_sendTransaction', + 'eth_signTransaction', + 'personal_sign', + 'eth_sign', + 'eth_signTypedData', + 'eth_signTypedData_v4', + 'wallet_addEthereumChain', + 'wallet_switchEthereumChain' +] + +declare type ArrayOneOrMore = { + 0: T; +} & Array; + +function walletConnect(walletName: string, options: WalletConnectOptions): WalletInit { + if (!options.dappUrl) { + console.warn( + `It is strongly recommended to supply a dappUrl to the WalletConnect init object as it is required by some wallets (i.e. MetaMask) to allow connection.` + ) + } + const { + qrModalOptions, + handleUri, + } = options + + let instance: unknown + + return () => { + return { + label: walletName, + getIcon: async () => (await import('./icon.js')).default, + getInterface: async ({ chains, EventEmitter, appMetadata }) => { + const { ProviderRpcError, ProviderRpcErrorCode } = await import( + '@web3-onboard/common' + ) + + const { default: EthereumProvider, REQUIRED_METHODS } = await import( + '@walletconnect/ethereum-provider' + ) + + const { Subject, fromEvent } = await import('rxjs') + const { takeUntil, take } = await import('rxjs/operators') + + + const { requiredChains, optionalChains } = buildWCChains(options.requiredChains ?? [], options.optionalChains ?? [], chains) + const { requiredMethods, optionalMethods } = buildWCMethods(options.additionalRequiredMethods ?? [], options.additionalOptionalMethods ?? [], REQUIRED_METHODS, methods) + + + const connector = await EthereumProvider.init({ + projectId: options.projectId, + chains: requiredChains, + methods: requiredMethods, + optionalChains: optionalChains as unknown as ArrayOneOrMore, + optionalMethods, + showQrModal: !options.handleUri, + qrModalOptions: qrModalOptions, + rpcMap: chains + .map(({ id, rpcUrl }) => ({ id, rpcUrl })) + .reduce((rpcMap: Record, { id, rpcUrl }) => { + rpcMap[parseInt(id, 16)] = rpcUrl || '' + return rpcMap + }, {}), + metadata: getMetaData(appMetadata), + } as EthereumProviderOptions) + + const emitter = new EventEmitter() + class EthProvider { + public request: EIP1193Provider['request'] + public connector: InstanceType + public chains: Chain[] + public disconnect: EIP1193Provider['disconnect'] + // @ts-ignore + public emit: typeof EventEmitter['emit'] + // @ts-ignore + public on: typeof EventEmitter['on'] + // @ts-ignore + public removeListener: typeof EventEmitter['removeListener'] + + private disconnected$: InstanceType + + constructor({ + connector, + chains + }: { + connector: InstanceType + chains: Chain[] + }) { + this.emit = emitter.emit.bind(emitter) + this.on = emitter.on.bind(emitter) + this.removeListener = emitter.removeListener.bind(emitter) + + this.connector = connector + this.chains = chains + this.disconnected$ = new Subject() + + // listen for accountsChanged + fromEvent(this.connector, 'accountsChanged', payload => payload) + .pipe(takeUntil(this.disconnected$)) + .subscribe({ + next: payload => { + const accounts = Array.isArray(payload) ? payload : [payload] + this.emit('accountsChanged', accounts) + }, + error: console.warn + }) + + // listen for chainChanged + fromEvent( + this.connector as JQueryStyleEventEmitter, + 'chainChanged', + (payload: number) => payload + ) + .pipe(takeUntil(this.disconnected$)) + .subscribe({ + next: chainId => { + const hexChainId = isHexString(chainId) + ? chainId + : `0x${chainId.toString(16)}` + this.emit('chainChanged', hexChainId) + }, + error: console.warn + }) + + // listen for disconnect event + fromEvent( + this.connector as JQueryStyleEventEmitter, + 'session_delete', + (payload: string) => payload + ) + .pipe(takeUntil(this.disconnected$)) + .subscribe({ + next: () => { + this.emit('accountsChanged', []) + this.disconnected$.next(true) + typeof localStorage !== 'undefined' && + localStorage.removeItem('walletconnect') + }, + error: console.warn + }) + + this.disconnect = () => { + if (this.connector.session) { + this.connector.disconnect() + instance = null + } + } + + if (options?.handleUri) { + // listen for uri event + fromEvent( + this.connector as JQueryStyleEventEmitter, + 'display_uri', + (payload: string) => payload + ) + .pipe(takeUntil(this.disconnected$)) + .subscribe(async uri => { + try { + handleUri && (await handleUri(uri)) + } catch (error) { + throw `An error occurred when handling the URI. Error: ${error}` + } + }) + } + + const checkForSession = () => { + const session = this.connector.session + instance = session + if (session) { + this.emit('accountsChanged', this.connector.accounts) + this.emit('chainChanged', this.connector.chainId) + } + } + checkForSession() + + this.request = async ({ method, params }: { method: string, params: object | unknown[] | undefined }) => { + if (method === 'eth_chainId') { + return isHexString(this.connector.chainId) + ? this.connector.chainId + : `0x${this.connector.chainId.toString(16)}` + } + + if (method === 'eth_requestAccounts') { + return new Promise( + async (resolve, reject) => { + // Subscribe to connection events + fromEvent( + this.connector as JQueryStyleEventEmitter< + any, + { chainId: number } + >, + 'connect', + (payload: { chainId: number | string }) => payload + ) + .pipe(take(1)) + .subscribe({ + next: ({ chainId }) => { + this.emit('accountsChanged', this.connector.accounts) + const hexChainId = isHexString(chainId) + ? chainId + : `0x${chainId.toString(16)}` + this.emit('chainChanged', hexChainId) + resolve(this.connector.accounts) + }, + error: reject + }) + + // Check if connection is already established + if (!this.connector.session) { + // create new session + await this.connector.connect().catch(err => { + console.error('err creating new session: ', err) + reject( + new ProviderRpcError({ + code: 4001, + message: 'User rejected the request.' + }) + ) + }) + } else { + // update ethereum provider to load accounts & chainId + const accounts = this.connector.accounts + const chainId = this.connector.chainId + instance = this.connector.session + const hexChainId = `0x${chainId.toString(16)}` + this.emit('chainChanged', hexChainId) + return resolve(accounts) + } + } + ) + } + + if (method === 'eth_selectAccounts') { + throw new ProviderRpcError({ + code: ProviderRpcErrorCode.UNSUPPORTED_METHOD, + message: `The Provider does not support the requested method: ${method}` + }) + } + + if (method == 'wallet_switchEthereumChain') { + if (!params) { + throw new ProviderRpcError({ + code: ProviderRpcErrorCode.INVALID_PARAMS, + message: `The Provider requires a chainId to be passed in as an argument` + }) + } + const chainIdObj = params[0] as { chainId?: number } + if ( + !chainIdObj.hasOwnProperty('chainId') || + typeof chainIdObj['chainId'] === 'undefined' + ) { + throw new ProviderRpcError({ + code: ProviderRpcErrorCode.INVALID_PARAMS, + message: `The Provider requires a chainId to be passed in as an argument` + }) + } + return this.connector.request({ + method: 'wallet_switchEthereumChain', + params: [ + { + chainId: chainIdObj.chainId + } + ] + }) + } + + return this.connector.request>({ + method, + params + }) + } + } + } + + return { + provider: new EthProvider({ chains, connector }), + instance + } + } + } + } +} + + +const isHexString = (value: string | number) => { + if (typeof value !== 'string' || !value.match(/^0x[0-9A-Fa-f]*$/)) { + return false + } + + return true + } + +export default walletConnect diff --git a/packages/bloom/tsconfig.json b/packages/bloom/tsconfig.json new file mode 100644 index 000000000..503e032ba --- /dev/null +++ b/packages/bloom/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*"], + + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "declarationDir": "dist", + "allowSyntheticDefaultImports": true, + "paths": { + "*": ["./src/*", "./node_modules/*"] + }, + "typeRoots": ["node_modules/@types"] + } +} diff --git a/packages/demo/src/App.svelte b/packages/demo/src/App.svelte index aba37c12d..2cb6069d1 100644 --- a/packages/demo/src/App.svelte +++ b/packages/demo/src/App.svelte @@ -34,6 +34,7 @@ import arcanaAuthModule from '@web3-onboard/arcana-auth' import venlyModule from '@web3-onboard/venly' import bitgetModule from '@web3-onboard/bitget' + import bloomModule from '@web3-onboard/bloom' import particleAuthModule from '@web3-onboard/particle-network' import capsuleModule, { Environment, @@ -177,6 +178,7 @@ const cedeStore = cedeStoreModule() const blocto = bloctoModule() const tallyho = tallyHoModule() + const bloom = bloomModule() const webauthnSigner = new WebauthnSigner({ rpId: 'localhost', From 96c53e36a0c98b6d47cbdaef5c7e1eea66ef3733 Mon Sep 17 00:00:00 2001 From: Mark Nardi Date: Tue, 28 May 2024 08:42:53 +0200 Subject: [PATCH 2/9] fix indentation --- packages/bloom/src/walletConnect.ts | 192 ++++++++++++++-------------- 1 file changed, 96 insertions(+), 96 deletions(-) diff --git a/packages/bloom/src/walletConnect.ts b/packages/bloom/src/walletConnect.ts index c46ff15aa..880411b04 100644 --- a/packages/bloom/src/walletConnect.ts +++ b/packages/bloom/src/walletConnect.ts @@ -111,8 +111,8 @@ function walletConnect(walletName: string, options: WalletConnectOptions): Walle .pipe(takeUntil(this.disconnected$)) .subscribe({ next: payload => { - const accounts = Array.isArray(payload) ? payload : [payload] - this.emit('accountsChanged', accounts) + const accounts = Array.isArray(payload) ? payload : [payload] + this.emit('accountsChanged', accounts) }, error: console.warn }) @@ -143,19 +143,19 @@ function walletConnect(walletName: string, options: WalletConnectOptions): Walle .pipe(takeUntil(this.disconnected$)) .subscribe({ next: () => { - this.emit('accountsChanged', []) - this.disconnected$.next(true) - typeof localStorage !== 'undefined' && - localStorage.removeItem('walletconnect') + this.emit('accountsChanged', []) + this.disconnected$.next(true) + typeof localStorage !== 'undefined' && + localStorage.removeItem('walletconnect') }, error: console.warn }) this.disconnect = () => { - if (this.connector.session) { - this.connector.disconnect() - instance = null - } + if (this.connector.session) { + this.connector.disconnect() + instance = null + } } if (options?.handleUri) { @@ -185,102 +185,102 @@ function walletConnect(walletName: string, options: WalletConnectOptions): Walle } checkForSession() - this.request = async ({ method, params }: { method: string, params: object | unknown[] | undefined }) => { - if (method === 'eth_chainId') { - return isHexString(this.connector.chainId) - ? this.connector.chainId - : `0x${this.connector.chainId.toString(16)}` - } + this.request = async ({ method, params }) => { + if (method === 'eth_chainId') { + return isHexString(this.connector.chainId) + ? this.connector.chainId + : `0x${this.connector.chainId.toString(16)}` + } - if (method === 'eth_requestAccounts') { - return new Promise( - async (resolve, reject) => { - // Subscribe to connection events - fromEvent( - this.connector as JQueryStyleEventEmitter< - any, - { chainId: number } - >, - 'connect', - (payload: { chainId: number | string }) => payload + if (method === 'eth_requestAccounts') { + return new Promise( + async (resolve, reject) => { + // Subscribe to connection events + fromEvent( + this.connector as JQueryStyleEventEmitter< + any, + { chainId: number } + >, + 'connect', + (payload: { chainId: number | string }) => payload + ) + .pipe(take(1)) + .subscribe({ + next: ({ chainId }) => { + this.emit('accountsChanged', this.connector.accounts) + const hexChainId = isHexString(chainId) + ? chainId + : `0x${chainId.toString(16)}` + this.emit('chainChanged', hexChainId) + resolve(this.connector.accounts) + }, + error: reject + }) + + // Check if connection is already established + if (!this.connector.session) { + // create new session + await this.connector.connect().catch(err => { + console.error('err creating new session: ', err) + reject( + new ProviderRpcError({ + code: 4001, + message: 'User rejected the request.' + }) + ) + }) + } else { + // update ethereum provider to load accounts & chainId + const accounts = this.connector.accounts + const chainId = this.connector.chainId + instance = this.connector.session + const hexChainId = `0x${chainId.toString(16)}` + this.emit('chainChanged', hexChainId) + return resolve(accounts) + } + } ) - .pipe(take(1)) - .subscribe({ - next: ({ chainId }) => { - this.emit('accountsChanged', this.connector.accounts) - const hexChainId = isHexString(chainId) - ? chainId - : `0x${chainId.toString(16)}` - this.emit('chainChanged', hexChainId) - resolve(this.connector.accounts) - }, - error: reject + } + + if (method === 'eth_selectAccounts') { + throw new ProviderRpcError({ + code: ProviderRpcErrorCode.UNSUPPORTED_METHOD, + message: `The Provider does not support the requested method: ${method}` }) + } - // Check if connection is already established - if (!this.connector.session) { - // create new session - await this.connector.connect().catch(err => { - console.error('err creating new session: ', err) - reject( - new ProviderRpcError({ - code: 4001, - message: 'User rejected the request.' - }) - ) + if (method == 'wallet_switchEthereumChain') { + if (!params) { + throw new ProviderRpcError({ + code: ProviderRpcErrorCode.INVALID_PARAMS, + message: `The Provider requires a chainId to be passed in as an argument` }) - } else { - // update ethereum provider to load accounts & chainId - const accounts = this.connector.accounts - const chainId = this.connector.chainId - instance = this.connector.session - const hexChainId = `0x${chainId.toString(16)}` - this.emit('chainChanged', hexChainId) - return resolve(accounts) } + const chainIdObj = params[0] as { chainId?: number } + if ( + !chainIdObj.hasOwnProperty('chainId') || + typeof chainIdObj['chainId'] === 'undefined' + ) { + throw new ProviderRpcError({ + code: ProviderRpcErrorCode.INVALID_PARAMS, + message: `The Provider requires a chainId to be passed in as an argument` + }) + } + return this.connector.request({ + method: 'wallet_switchEthereumChain', + params: [ + { + chainId: chainIdObj.chainId + } + ] + }) } - ) - } - if (method === 'eth_selectAccounts') { - throw new ProviderRpcError({ - code: ProviderRpcErrorCode.UNSUPPORTED_METHOD, - message: `The Provider does not support the requested method: ${method}` - }) - } - - if (method == 'wallet_switchEthereumChain') { - if (!params) { - throw new ProviderRpcError({ - code: ProviderRpcErrorCode.INVALID_PARAMS, - message: `The Provider requires a chainId to be passed in as an argument` - }) - } - const chainIdObj = params[0] as { chainId?: number } - if ( - !chainIdObj.hasOwnProperty('chainId') || - typeof chainIdObj['chainId'] === 'undefined' - ) { - throw new ProviderRpcError({ - code: ProviderRpcErrorCode.INVALID_PARAMS, - message: `The Provider requires a chainId to be passed in as an argument` - }) - } - return this.connector.request({ - method: 'wallet_switchEthereumChain', - params: [ - { - chainId: chainIdObj.chainId - } - ] + return this.connector.request>({ + method, + params }) } - - return this.connector.request>({ - method, - params - }) - } } } From 070fda5f7313c51bed9adcd98bd5fb3109fb2e51 Mon Sep 17 00:00:00 2001 From: Mark Nardi Date: Wed, 29 May 2024 10:15:01 +0200 Subject: [PATCH 3/9] add icon + fixes --- packages/bloom/src/icon.ts | 83 +++++++++++++++++++++++++++-- packages/bloom/src/index.ts | 4 +- packages/bloom/src/walletConnect.ts | 2 +- 3 files changed, 82 insertions(+), 7 deletions(-) diff --git a/packages/bloom/src/icon.ts b/packages/bloom/src/icon.ts index 072d5a609..d11ffd11f 100644 --- a/packages/bloom/src/icon.ts +++ b/packages/bloom/src/icon.ts @@ -1,6 +1,81 @@ export default ` - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ` diff --git a/packages/bloom/src/index.ts b/packages/bloom/src/index.ts index 4c24d5576..7907c3c8c 100644 --- a/packages/bloom/src/index.ts +++ b/packages/bloom/src/index.ts @@ -1,7 +1,7 @@ import { validateWCInitOptions } from './validation.js' import type { WalletInit } from '@web3-onboard/common' import walletConnect from './walletConnect.js' -import type { WalletConnectOptions } from 'types.js' +import type { WalletConnectOptions } from './types.js' function initBloom(options: WalletConnectOptions): WalletInit { @@ -19,7 +19,7 @@ function initBloom(options: WalletConnectOptions): WalletInit { const walletName = "Bloom" options.handleUri = (uri: string) => { const deeplink = `bloom://wallet-connect/wc?uri=${encodeURIComponent(uri)}` - window.open(deeplink, '_blank') + window.location = deeplink return Promise.resolve() } diff --git a/packages/bloom/src/walletConnect.ts b/packages/bloom/src/walletConnect.ts index 880411b04..1cb344175 100644 --- a/packages/bloom/src/walletConnect.ts +++ b/packages/bloom/src/walletConnect.ts @@ -7,7 +7,7 @@ import type { WalletInit, EIP1193Provider } from '@web3-onboard/common' -import { buildWCChains, buildWCMethods, getMetaData } from 'utils.js' +import { buildWCChains, buildWCMethods, getMetaData } from './utils.js' // methods that require user interaction const methods = [ From eee7f9e43245a14167c6e9443ebc915e9c3f0694 Mon Sep 17 00:00:00 2001 From: Mark Nardi Date: Wed, 29 May 2024 10:16:33 +0200 Subject: [PATCH 4/9] add bloom to example app --- packages/demo/src/App.svelte | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/demo/src/App.svelte b/packages/demo/src/App.svelte index 2cb6069d1..931c7708c 100644 --- a/packages/demo/src/App.svelte +++ b/packages/demo/src/App.svelte @@ -178,7 +178,10 @@ const cedeStore = cedeStoreModule() const blocto = bloctoModule() const tallyho = tallyHoModule() - const bloom = bloomModule() + const bloom = bloomModule({ + projectId: 'f6bd6e2911b56f5ac3bc8b2d0e2d7ad5', + dappUrl: 'https://www.onboard.blocknative.com' + }) const webauthnSigner = new WebauthnSigner({ rpId: 'localhost', @@ -263,6 +266,7 @@ trust, tallyho, bitget, + bloom, enkrypt, infinityWallet, mewWallet, From 3d97c8dad7c4105fc5c856e6f37e8f1631ce9098 Mon Sep 17 00:00:00 2001 From: Mark Nardi Date: Wed, 29 May 2024 10:37:11 +0200 Subject: [PATCH 5/9] add bloom documentation --- .../docs/[...4]wallets/[...37]bloom/+page.md | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 docs/src/routes/docs/[...4]wallets/[...37]bloom/+page.md diff --git a/docs/src/routes/docs/[...4]wallets/[...37]bloom/+page.md b/docs/src/routes/docs/[...4]wallets/[...37]bloom/+page.md new file mode 100644 index 000000000..e66946d83 --- /dev/null +++ b/docs/src/routes/docs/[...4]wallets/[...37]bloom/+page.md @@ -0,0 +1,116 @@ +--- +title: Bloom +--- + +# {$frontmatter.title} + +Wallet module for connecting Bloom to web3-onboard. + +## Install + + + + +```sh copy +yarn add @web3-onboard/bloom +``` + + + + +```sh copy +npm install @web3-onboard/bloom +``` + + + + + +```typescript +type WalletConnectOptions = { + /** + * Project ID associated with [WalletConnect account](https://cloud.walletconnect.com) + */ + projectId: string + /** + * Defaults to `appMetadata.explore` that is supplied to the web3-onboard init + * Strongly recommended to provide atleast one URL as it is required by some wallets (i.e. MetaMask) + * To connect with walletconnect + */ + dappUrl?: string + /** + * List of Required Chain(s) ID for wallets to support in number format (integer or hex) + * Defaults to [1] - Ethereum + */ + requiredChains?: number[] | undefined + /** + * List of Optional Chain(s) ID for wallets to support in number format (integer or hex) + * Defaults to the chains provided within the web3-onboard init chain property + */ + optionalChains?: number[] | undefined + /** + * Additional required methods to be added to the default list of ['eth_sendTransaction', 'personal_sign'] + * Passed methods to be included along with the defaults methods - see https://docs.walletconnect.com/2.0/advanced/providers/ethereum#required-and-optional-methods + */ + additionalRequiredMethods?: string[] | undefined + /** + * Additional methods to be added to the default list of ['eth_sendTransaction', 'eth_signTransaction', 'personal_sign', 'eth_sign', 'eth_signTypedData', 'eth_signTypedData_v4'] + * Passed methods to be included along with the defaults methods - see https://docs.walletconnect.com/2.0/web/walletConnectModal/options + */ + additionalOptionalMethods?: string[] | undefined +) +``` + +## Usage + +```typescript +import Onboard from '@web3-onboard/core' +import bloomModule from '@web3-onboard/bloom' + +const wcInitOptions = { + /** + * Project ID associated with [WalletConnect account](https://cloud.walletconnect.com) + */ + projectId: 'abc123...', + /** + * Chains required to be supported by all wallets connecting to your DApp + */ + requiredChains: [1], + /** + * Chains required to be supported by all wallets connecting to your DApp + */ + optionalChains: [42161, 8453, 10, 137, 56], + /** + * Defaults to `appMetadata.explore` that is supplied to the web3-onboard init + * Strongly recommended to provide atleast one URL as it is required by some wallets (i.e. MetaMask) + * To connect with WalletConnect + */ + dappUrl: 'http://YourAwesomeDapp.com' +} + +// initialize the module with options +const bloom = bloomModule(wcInitOptions) + +// can also initialize with no options... + +const onboard = Onboard({ + // ... other Onboard options + wallets: [ + bloom + //... other wallets + ] +}) + +const connectedWallets = await onboard.connectWallet() + +// Assuming only wallet connect is connected, index 0 +// `instance` will give insight into the WalletConnect info +// such as namespaces, methods, chains, etc per wallet connected +const { instance } = connectedWallets[0] + +console.log(connectedWallets) +``` + +## Build Environments + +For build env configurations and setups please see the Build Env section [here](/docs/modules/core#build-environments) From e9ac58a35b441439766ffdf482f4447e795a52fd Mon Sep 17 00:00:00 2001 From: Mark Nardi Date: Wed, 29 May 2024 11:04:07 +0200 Subject: [PATCH 6/9] fix typescript issue --- packages/bloom/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bloom/src/index.ts b/packages/bloom/src/index.ts index 7907c3c8c..cc745e82a 100644 --- a/packages/bloom/src/index.ts +++ b/packages/bloom/src/index.ts @@ -19,7 +19,7 @@ function initBloom(options: WalletConnectOptions): WalletInit { const walletName = "Bloom" options.handleUri = (uri: string) => { const deeplink = `bloom://wallet-connect/wc?uri=${encodeURIComponent(uri)}` - window.location = deeplink + window.location.href = deeplink return Promise.resolve() } From fee24fca231adb375e16247b2481ac1962fae71a Mon Sep 17 00:00:00 2001 From: Mark Nardi Date: Thu, 13 Jun 2024 14:11:57 +0200 Subject: [PATCH 7/9] fix bloom dependencies --- packages/bloom/package.json | 14 +++++--------- packages/bloom/src/walletConnect.ts | 4 ++-- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/bloom/package.json b/packages/bloom/package.json index ae850fce7..b7eab4f94 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -1,6 +1,6 @@ { "name": "@web3-onboard/bloom", - "version": "2.1.2", + "version": "2.0.0-alpha.1", "description": "Unstoppable Domains module for connecting to Web3-Onboard. Web3-Onboard makes it simple to connect Ethereum hardware and software wallets to your dapp. Features standardised spec compliant web3 providers for all supported wallets, framework agnostic modern javascript UI with code splitting, CSS customization, multi-chain and multi-account support, reactive wallet state subscriptions and real-time transaction state change notifications.", "keywords": [ "Bloom", @@ -61,16 +61,12 @@ }, "license": "MIT", "devDependencies": { - "typescript": "^4.5.5", - "@walletconnect/types": "^2.7.0" + "typescript": "^5.4.5", + "@walletconnect/types": "^2.13.0" }, "dependencies": { - "@ethersproject/providers": "^5.5.0", - "@walletconnect/client": "^1.8.0", - "@walletconnect/ethereum-provider": "2.9.1", - "@walletconnect/modal": "2.6.1", - "@walletconnect/qrcode-modal": "^1.8.0", - "@web3-onboard/common": "^2.3.3", + "@walletconnect/ethereum-provider": "^2.13.0", + "@web3-onboard/common": "^2.4.1", "joi": "17.9.1", "rxjs": "^7.5.2" } diff --git a/packages/bloom/src/walletConnect.ts b/packages/bloom/src/walletConnect.ts index 1cb344175..7e880d464 100644 --- a/packages/bloom/src/walletConnect.ts +++ b/packages/bloom/src/walletConnect.ts @@ -212,7 +212,7 @@ function walletConnect(walletName: string, options: WalletConnectOptions): Walle ? chainId : `0x${chainId.toString(16)}` this.emit('chainChanged', hexChainId) - resolve(this.connector.accounts) + resolve(this.connector.accounts as ProviderAccounts) }, error: reject }) @@ -236,7 +236,7 @@ function walletConnect(walletName: string, options: WalletConnectOptions): Walle instance = this.connector.session const hexChainId = `0x${chainId.toString(16)}` this.emit('chainChanged', hexChainId) - return resolve(accounts) + return resolve(accounts as ProviderAccounts) } } ) From 23db623d01f16959d4fe2b141a1745d37d34e014 Mon Sep 17 00:00:00 2001 From: Adam Carpenter Date: Thu, 13 Jun 2024 10:16:38 -0600 Subject: [PATCH 8/9] Prettier --- packages/bloom/src/index.ts | 37 +-- packages/bloom/src/utils.ts | 122 +++---- packages/bloom/src/validation.ts | 3 +- packages/bloom/src/walletConnect.ts | 488 ++++++++++++++-------------- packages/demo/package.json | 1 + 5 files changed, 331 insertions(+), 320 deletions(-) diff --git a/packages/bloom/src/index.ts b/packages/bloom/src/index.ts index cc745e82a..c30f3ad76 100644 --- a/packages/bloom/src/index.ts +++ b/packages/bloom/src/index.ts @@ -3,27 +3,26 @@ import type { WalletInit } from '@web3-onboard/common' import walletConnect from './walletConnect.js' import type { WalletConnectOptions } from './types.js' - function initBloom(options: WalletConnectOptions): WalletInit { - if (!options) { - throw new Error( - `WalletConnect requires an initialization object to be passed - see the official docs for an example: https://onboard.blocknative.com/docs/wallets/walletconnect` - ) - } - - const error = validateWCInitOptions(options) - if (error) { - throw error - } - - const walletName = "Bloom" - options.handleUri = (uri: string) => { - const deeplink = `bloom://wallet-connect/wc?uri=${encodeURIComponent(uri)}` - window.location.href = deeplink - return Promise.resolve() - } + if (!options) { + throw new Error( + `WalletConnect requires an initialization object to be passed - see the official docs for an example: https://onboard.blocknative.com/docs/wallets/walletconnect` + ) + } + + const error = validateWCInitOptions(options) + if (error) { + throw error + } + + const walletName = 'Bloom' + options.handleUri = (uri: string) => { + const deeplink = `bloom://wallet-connect/wc?uri=${encodeURIComponent(uri)}` + window.location.href = deeplink + return Promise.resolve() + } - return walletConnect(walletName, options) + return walletConnect(walletName, options) } export default initBloom diff --git a/packages/bloom/src/utils.ts b/packages/bloom/src/utils.ts index a82beec56..b7b5e6a7c 100644 --- a/packages/bloom/src/utils.ts +++ b/packages/bloom/src/utils.ts @@ -1,79 +1,83 @@ import type { CoreTypes } from '@walletconnect/types' export function getMetaData(appMetadata: any): CoreTypes.Metadata | undefined { - if (!appMetadata) return undefined - const wcMetaData: CoreTypes.Metadata = { - name: appMetadata.name, - description: appMetadata.description || '', - url: appMetadata.explore || appMetadata.gettingStartedGuide || '', - icons: [] - } + if (!appMetadata) return undefined + const wcMetaData: CoreTypes.Metadata = { + name: appMetadata.name, + description: appMetadata.description || '', + url: appMetadata.explore || appMetadata.gettingStartedGuide || '', + icons: [] + } - if (appMetadata.icon !== undefined && appMetadata.icon.length) { - wcMetaData.icons = [appMetadata.icon] - } - if (appMetadata.logo !== undefined && appMetadata.logo.length) { - wcMetaData.icons = wcMetaData.icons.length - ? [...wcMetaData.icons, appMetadata.logo] - : [appMetadata.logo] - } + if (appMetadata.icon !== undefined && appMetadata.icon.length) { + wcMetaData.icons = [appMetadata.icon] + } + if (appMetadata.logo !== undefined && appMetadata.logo.length) { + wcMetaData.icons = wcMetaData.icons.length + ? [...wcMetaData.icons, appMetadata.logo] + : [appMetadata.logo] + } - return wcMetaData + return wcMetaData } -export function buildWCChains(requiredChains: number[], optionalChains: number[], chains: { id: string }[]): { - requiredChains: number[]; - optionalChains: number[]; +export function buildWCChains( + requiredChains: number[], + optionalChains: number[], + chains: { id: string }[] +): { + requiredChains: number[] + optionalChains: number[] } { - // default to mainnet - const requiredChainsParsed: number[] = - Array.isArray(requiredChains) && - requiredChains.length && - requiredChains.every(num => !isNaN(num)) - // @ts-ignore - // Required as WC package does not support hex numbers - ? requiredChains.map(chainID => parseInt(chainID)) - : [] + // default to mainnet + const requiredChainsParsed: number[] = + Array.isArray(requiredChains) && + requiredChains.length && + requiredChains.every(num => !isNaN(num)) + ? // @ts-ignore + // Required as WC package does not support hex numbers + requiredChains.map(chainID => parseInt(chainID)) + : [] - // Defaults to the chains provided within the web3-onboard init chain property - const optionalChainsParsed: number[] = - Array.isArray(optionalChains) && - optionalChains.length && - optionalChains.every(num => !isNaN(num)) - // @ts-ignore - // Required as WC package does not support hex numbers - ? optionalChains.map(chainID => parseInt(chainID)) - : chains.map(({ id }) => parseInt(id, 16)) + // Defaults to the chains provided within the web3-onboard init chain property + const optionalChainsParsed: number[] = + Array.isArray(optionalChains) && + optionalChains.length && + optionalChains.every(num => !isNaN(num)) + ? // @ts-ignore + // Required as WC package does not support hex numbers + optionalChains.map(chainID => parseInt(chainID)) + : chains.map(({ id }) => parseInt(id, 16)) - return { - requiredChains: requiredChainsParsed, - optionalChains: optionalChainsParsed, - } + return { + requiredChains: requiredChainsParsed, + optionalChains: optionalChainsParsed + } } export function buildWCMethods( - additionalRequiredMethods: string[], - additionalOptionalMethods: string[], - requiredMethods: string[], - allMethods: string[], + additionalRequiredMethods: string[], + additionalOptionalMethods: string[], + requiredMethods: string[], + allMethods: string[] ): { - requiredMethods: string[]; - optionalMethods: string[] + requiredMethods: string[] + optionalMethods: string[] } { - const requiredMethodsSet = new Set( + const requiredMethodsSet = new Set( additionalRequiredMethods && Array.isArray(additionalRequiredMethods) - ? [...additionalRequiredMethods, ...requiredMethods] - : requiredMethods - ) - const _requiredMethods = Array.from(requiredMethodsSet) + ? [...additionalRequiredMethods, ...requiredMethods] + : requiredMethods + ) + const _requiredMethods = Array.from(requiredMethodsSet) - const optionalMethods = + const optionalMethods = additionalOptionalMethods && Array.isArray(additionalOptionalMethods) - ? [...additionalOptionalMethods, ...allMethods] - : allMethods + ? [...additionalOptionalMethods, ...allMethods] + : allMethods - return { - requiredMethods: _requiredMethods, - optionalMethods - } + return { + requiredMethods: _requiredMethods, + optionalMethods + } } diff --git a/packages/bloom/src/validation.ts b/packages/bloom/src/validation.ts index 5d9d15809..33e5cf7ef 100644 --- a/packages/bloom/src/validation.ts +++ b/packages/bloom/src/validation.ts @@ -12,7 +12,8 @@ const wcOptions = Joi.object({ ) } else if (value !== 2 && value !== undefined) { return helpers.error('any.invalid', { - message: 'Invalid version number. This version of @web3-onboard/walletconnect only supports version 2' + message: + 'Invalid version number. This version of @web3-onboard/walletconnect only supports version 2' }) } return value // return the value unchanged if it's valid or not provided diff --git a/packages/bloom/src/walletConnect.ts b/packages/bloom/src/walletConnect.ts index 7e880d464..341bf7e55 100644 --- a/packages/bloom/src/walletConnect.ts +++ b/packages/bloom/src/walletConnect.ts @@ -22,284 +22,290 @@ const methods = [ ] declare type ArrayOneOrMore = { - 0: T; -} & Array; + 0: T +} & Array -function walletConnect(walletName: string, options: WalletConnectOptions): WalletInit { +function walletConnect( + walletName: string, + options: WalletConnectOptions +): WalletInit { if (!options.dappUrl) { console.warn( `It is strongly recommended to supply a dappUrl to the WalletConnect init object as it is required by some wallets (i.e. MetaMask) to allow connection.` ) } - const { - qrModalOptions, - handleUri, - } = options + const { qrModalOptions, handleUri } = options let instance: unknown return () => { - return { - label: walletName, - getIcon: async () => (await import('./icon.js')).default, - getInterface: async ({ chains, EventEmitter, appMetadata }) => { - const { ProviderRpcError, ProviderRpcErrorCode } = await import( - '@web3-onboard/common' - ) - - const { default: EthereumProvider, REQUIRED_METHODS } = await import( - '@walletconnect/ethereum-provider' - ) + return { + label: walletName, + getIcon: async () => (await import('./icon.js')).default, + getInterface: async ({ chains, EventEmitter, appMetadata }) => { + const { ProviderRpcError, ProviderRpcErrorCode } = await import( + '@web3-onboard/common' + ) - const { Subject, fromEvent } = await import('rxjs') - const { takeUntil, take } = await import('rxjs/operators') + const { default: EthereumProvider, REQUIRED_METHODS } = await import( + '@walletconnect/ethereum-provider' + ) + const { Subject, fromEvent } = await import('rxjs') + const { takeUntil, take } = await import('rxjs/operators') - const { requiredChains, optionalChains } = buildWCChains(options.requiredChains ?? [], options.optionalChains ?? [], chains) - const { requiredMethods, optionalMethods } = buildWCMethods(options.additionalRequiredMethods ?? [], options.additionalOptionalMethods ?? [], REQUIRED_METHODS, methods) - + const { requiredChains, optionalChains } = buildWCChains( + options.requiredChains ?? [], + options.optionalChains ?? [], + chains + ) + const { requiredMethods, optionalMethods } = buildWCMethods( + options.additionalRequiredMethods ?? [], + options.additionalOptionalMethods ?? [], + REQUIRED_METHODS, + methods + ) - const connector = await EthereumProvider.init({ - projectId: options.projectId, - chains: requiredChains, - methods: requiredMethods, - optionalChains: optionalChains as unknown as ArrayOneOrMore, - optionalMethods, - showQrModal: !options.handleUri, - qrModalOptions: qrModalOptions, - rpcMap: chains - .map(({ id, rpcUrl }) => ({ id, rpcUrl })) - .reduce((rpcMap: Record, { id, rpcUrl }) => { - rpcMap[parseInt(id, 16)] = rpcUrl || '' - return rpcMap - }, {}), - metadata: getMetaData(appMetadata), - } as EthereumProviderOptions) + const connector = await EthereumProvider.init({ + projectId: options.projectId, + chains: requiredChains, + methods: requiredMethods, + optionalChains: optionalChains as unknown as ArrayOneOrMore, + optionalMethods, + showQrModal: !options.handleUri, + qrModalOptions: qrModalOptions, + rpcMap: chains + .map(({ id, rpcUrl }) => ({ id, rpcUrl })) + .reduce((rpcMap: Record, { id, rpcUrl }) => { + rpcMap[parseInt(id, 16)] = rpcUrl || '' + return rpcMap + }, {}), + metadata: getMetaData(appMetadata) + } as EthereumProviderOptions) - const emitter = new EventEmitter() - class EthProvider { - public request: EIP1193Provider['request'] - public connector: InstanceType - public chains: Chain[] - public disconnect: EIP1193Provider['disconnect'] - // @ts-ignore - public emit: typeof EventEmitter['emit'] - // @ts-ignore - public on: typeof EventEmitter['on'] - // @ts-ignore - public removeListener: typeof EventEmitter['removeListener'] + const emitter = new EventEmitter() + class EthProvider { + public request: EIP1193Provider['request'] + public connector: InstanceType + public chains: Chain[] + public disconnect: EIP1193Provider['disconnect'] + // @ts-ignore + public emit: typeof EventEmitter['emit'] + // @ts-ignore + public on: typeof EventEmitter['on'] + // @ts-ignore + public removeListener: typeof EventEmitter['removeListener'] - private disconnected$: InstanceType + private disconnected$: InstanceType - constructor({ - connector, - chains - }: { - connector: InstanceType - chains: Chain[] - }) { - this.emit = emitter.emit.bind(emitter) - this.on = emitter.on.bind(emitter) - this.removeListener = emitter.removeListener.bind(emitter) + constructor({ + connector, + chains + }: { + connector: InstanceType + chains: Chain[] + }) { + this.emit = emitter.emit.bind(emitter) + this.on = emitter.on.bind(emitter) + this.removeListener = emitter.removeListener.bind(emitter) - this.connector = connector - this.chains = chains - this.disconnected$ = new Subject() + this.connector = connector + this.chains = chains + this.disconnected$ = new Subject() - // listen for accountsChanged - fromEvent(this.connector, 'accountsChanged', payload => payload) - .pipe(takeUntil(this.disconnected$)) - .subscribe({ - next: payload => { - const accounts = Array.isArray(payload) ? payload : [payload] - this.emit('accountsChanged', accounts) - }, - error: console.warn - }) + // listen for accountsChanged + fromEvent(this.connector, 'accountsChanged', payload => payload) + .pipe(takeUntil(this.disconnected$)) + .subscribe({ + next: payload => { + const accounts = Array.isArray(payload) ? payload : [payload] + this.emit('accountsChanged', accounts) + }, + error: console.warn + }) - // listen for chainChanged - fromEvent( - this.connector as JQueryStyleEventEmitter, - 'chainChanged', - (payload: number) => payload - ) - .pipe(takeUntil(this.disconnected$)) - .subscribe({ - next: chainId => { - const hexChainId = isHexString(chainId) - ? chainId - : `0x${chainId.toString(16)}` - this.emit('chainChanged', hexChainId) - }, - error: console.warn - }) - - // listen for disconnect event - fromEvent( - this.connector as JQueryStyleEventEmitter, - 'session_delete', - (payload: string) => payload - ) - .pipe(takeUntil(this.disconnected$)) - .subscribe({ - next: () => { - this.emit('accountsChanged', []) - this.disconnected$.next(true) - typeof localStorage !== 'undefined' && - localStorage.removeItem('walletconnect') - }, - error: console.warn - }) + // listen for chainChanged + fromEvent( + this.connector as JQueryStyleEventEmitter, + 'chainChanged', + (payload: number) => payload + ) + .pipe(takeUntil(this.disconnected$)) + .subscribe({ + next: chainId => { + const hexChainId = isHexString(chainId) + ? chainId + : `0x${chainId.toString(16)}` + this.emit('chainChanged', hexChainId) + }, + error: console.warn + }) - this.disconnect = () => { - if (this.connector.session) { - this.connector.disconnect() - instance = null - } - } + // listen for disconnect event + fromEvent( + this.connector as JQueryStyleEventEmitter, + 'session_delete', + (payload: string) => payload + ) + .pipe(takeUntil(this.disconnected$)) + .subscribe({ + next: () => { + this.emit('accountsChanged', []) + this.disconnected$.next(true) + typeof localStorage !== 'undefined' && + localStorage.removeItem('walletconnect') + }, + error: console.warn + }) - if (options?.handleUri) { - // listen for uri event - fromEvent( - this.connector as JQueryStyleEventEmitter, - 'display_uri', - (payload: string) => payload - ) - .pipe(takeUntil(this.disconnected$)) - .subscribe(async uri => { - try { - handleUri && (await handleUri(uri)) - } catch (error) { - throw `An error occurred when handling the URI. Error: ${error}` - } - }) - } + this.disconnect = () => { + if (this.connector.session) { + this.connector.disconnect() + instance = null + } + } - const checkForSession = () => { - const session = this.connector.session - instance = session - if (session) { - this.emit('accountsChanged', this.connector.accounts) - this.emit('chainChanged', this.connector.chainId) - } - } - checkForSession() + if (options?.handleUri) { + // listen for uri event + fromEvent( + this.connector as JQueryStyleEventEmitter, + 'display_uri', + (payload: string) => payload + ) + .pipe(takeUntil(this.disconnected$)) + .subscribe(async uri => { + try { + handleUri && (await handleUri(uri)) + } catch (error) { + throw `An error occurred when handling the URI. Error: ${error}` + } + }) + } - this.request = async ({ method, params }) => { - if (method === 'eth_chainId') { - return isHexString(this.connector.chainId) - ? this.connector.chainId - : `0x${this.connector.chainId.toString(16)}` - } + const checkForSession = () => { + const session = this.connector.session + instance = session + if (session) { + this.emit('accountsChanged', this.connector.accounts) + this.emit('chainChanged', this.connector.chainId) + } + } + checkForSession() - if (method === 'eth_requestAccounts') { - return new Promise( - async (resolve, reject) => { - // Subscribe to connection events - fromEvent( - this.connector as JQueryStyleEventEmitter< - any, - { chainId: number } - >, - 'connect', - (payload: { chainId: number | string }) => payload - ) - .pipe(take(1)) - .subscribe({ - next: ({ chainId }) => { - this.emit('accountsChanged', this.connector.accounts) - const hexChainId = isHexString(chainId) - ? chainId - : `0x${chainId.toString(16)}` - this.emit('chainChanged', hexChainId) - resolve(this.connector.accounts as ProviderAccounts) - }, - error: reject - }) + this.request = async ({ method, params }) => { + if (method === 'eth_chainId') { + return isHexString(this.connector.chainId) + ? this.connector.chainId + : `0x${this.connector.chainId.toString(16)}` + } - // Check if connection is already established - if (!this.connector.session) { - // create new session - await this.connector.connect().catch(err => { - console.error('err creating new session: ', err) - reject( - new ProviderRpcError({ - code: 4001, - message: 'User rejected the request.' - }) - ) - }) - } else { - // update ethereum provider to load accounts & chainId - const accounts = this.connector.accounts - const chainId = this.connector.chainId - instance = this.connector.session - const hexChainId = `0x${chainId.toString(16)}` - this.emit('chainChanged', hexChainId) - return resolve(accounts as ProviderAccounts) - } - } - ) - } + if (method === 'eth_requestAccounts') { + return new Promise( + async (resolve, reject) => { + // Subscribe to connection events + fromEvent( + this.connector as JQueryStyleEventEmitter< + any, + { chainId: number } + >, + 'connect', + (payload: { chainId: number | string }) => payload + ) + .pipe(take(1)) + .subscribe({ + next: ({ chainId }) => { + this.emit('accountsChanged', this.connector.accounts) + const hexChainId = isHexString(chainId) + ? chainId + : `0x${chainId.toString(16)}` + this.emit('chainChanged', hexChainId) + resolve(this.connector.accounts as ProviderAccounts) + }, + error: reject + }) - if (method === 'eth_selectAccounts') { - throw new ProviderRpcError({ - code: ProviderRpcErrorCode.UNSUPPORTED_METHOD, - message: `The Provider does not support the requested method: ${method}` - }) - } + // Check if connection is already established + if (!this.connector.session) { + // create new session + await this.connector.connect().catch(err => { + console.error('err creating new session: ', err) + reject( + new ProviderRpcError({ + code: 4001, + message: 'User rejected the request.' + }) + ) + }) + } else { + // update ethereum provider to load accounts & chainId + const accounts = this.connector.accounts + const chainId = this.connector.chainId + instance = this.connector.session + const hexChainId = `0x${chainId.toString(16)}` + this.emit('chainChanged', hexChainId) + return resolve(accounts as ProviderAccounts) + } + } + ) + } - if (method == 'wallet_switchEthereumChain') { - if (!params) { - throw new ProviderRpcError({ - code: ProviderRpcErrorCode.INVALID_PARAMS, - message: `The Provider requires a chainId to be passed in as an argument` - }) - } - const chainIdObj = params[0] as { chainId?: number } - if ( - !chainIdObj.hasOwnProperty('chainId') || - typeof chainIdObj['chainId'] === 'undefined' - ) { - throw new ProviderRpcError({ - code: ProviderRpcErrorCode.INVALID_PARAMS, - message: `The Provider requires a chainId to be passed in as an argument` - }) - } - return this.connector.request({ - method: 'wallet_switchEthereumChain', - params: [ - { - chainId: chainIdObj.chainId - } - ] - }) - } + if (method === 'eth_selectAccounts') { + throw new ProviderRpcError({ + code: ProviderRpcErrorCode.UNSUPPORTED_METHOD, + message: `The Provider does not support the requested method: ${method}` + }) + } - return this.connector.request>({ - method, - params - }) - } - } + if (method == 'wallet_switchEthereumChain') { + if (!params) { + throw new ProviderRpcError({ + code: ProviderRpcErrorCode.INVALID_PARAMS, + message: `The Provider requires a chainId to be passed in as an argument` + }) } - - return { - provider: new EthProvider({ chains, connector }), - instance + const chainIdObj = params[0] as { chainId?: number } + if ( + !chainIdObj.hasOwnProperty('chainId') || + typeof chainIdObj['chainId'] === 'undefined' + ) { + throw new ProviderRpcError({ + code: ProviderRpcErrorCode.INVALID_PARAMS, + message: `The Provider requires a chainId to be passed in as an argument` + }) } + return this.connector.request({ + method: 'wallet_switchEthereumChain', + params: [ + { + chainId: chainIdObj.chainId + } + ] + }) + } + + return this.connector.request>({ + method, + params + }) } + } + } + + return { + provider: new EthProvider({ chains, connector }), + instance } + } } + } } - const isHexString = (value: string | number) => { - if (typeof value !== 'string' || !value.match(/^0x[0-9A-Fa-f]*$/)) { - return false - } - - return true + if (typeof value !== 'string' || !value.match(/^0x[0-9A-Fa-f]*$/)) { + return false } + return true +} + export default walletConnect diff --git a/packages/demo/package.json b/packages/demo/package.json index fc7b3b746..5df373e11 100644 --- a/packages/demo/package.json +++ b/packages/demo/package.json @@ -30,6 +30,7 @@ "@web3-onboard/arcana-auth": "^2.1.0", "@web3-onboard/bitget": "2.1.1", "@web3-onboard/blocto": "^2.1.1", + "@web3-onboard/bloom": "2.0.0-alpha.1", "@web3-onboard/capsule": "2.2.0-alpha.1", "@web3-onboard/cede-store": "^2.3.1", "@web3-onboard/core": "2.22.1", From 25c900041ff8ddfcd4d7a369e972d396311c3f4d Mon Sep 17 00:00:00 2001 From: Mark Nardi Date: Wed, 19 Jun 2024 15:09:31 +0200 Subject: [PATCH 9/9] add download prompt --- packages/bloom/src/index.ts | 10 +++++--- packages/bloom/src/walletConnect.ts | 36 +++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/packages/bloom/src/index.ts b/packages/bloom/src/index.ts index c30f3ad76..1c89f4077 100644 --- a/packages/bloom/src/index.ts +++ b/packages/bloom/src/index.ts @@ -15,14 +15,18 @@ function initBloom(options: WalletConnectOptions): WalletInit { throw error } - const walletName = 'Bloom' + const wallet = { + name: 'Bloom', + protocol: 'bloom', + downloadLink: 'https://bloomwallet.io/', + } options.handleUri = (uri: string) => { - const deeplink = `bloom://wallet-connect/wc?uri=${encodeURIComponent(uri)}` + const deeplink = `${wallet.protocol}://wallet-connect/wc?uri=${encodeURIComponent(uri)}` window.location.href = deeplink return Promise.resolve() } - return walletConnect(walletName, options) + return walletConnect(wallet, options) } export default initBloom diff --git a/packages/bloom/src/walletConnect.ts b/packages/bloom/src/walletConnect.ts index 341bf7e55..09db43167 100644 --- a/packages/bloom/src/walletConnect.ts +++ b/packages/bloom/src/walletConnect.ts @@ -7,6 +7,7 @@ import type { WalletInit, EIP1193Provider } from '@web3-onboard/common' +import { createDownloadMessage } from '@web3-onboard/common' import { buildWCChains, buildWCMethods, getMetaData } from './utils.js' // methods that require user interaction @@ -26,7 +27,11 @@ declare type ArrayOneOrMore = { } & Array function walletConnect( - walletName: string, + wallet: { + name: string, + protocol: string, + downloadLink: string, + }, options: WalletConnectOptions ): WalletInit { if (!options.dappUrl) { @@ -40,9 +45,16 @@ function walletConnect( return () => { return { - label: walletName, + label: wallet.name, getIcon: async () => (await import('./icon.js')).default, getInterface: async ({ chains, EventEmitter, appMetadata }) => { + + // Check if the wallet can be opened by a deeplink like protocol://XYZ + const isInstalled = checkIfProtocolIsSupported(wallet.protocol) + if (!isInstalled) { + throw new Error(createDownloadMessage(wallet.name, wallet.downloadLink)) + } + const { ProviderRpcError, ProviderRpcErrorCode } = await import( '@web3-onboard/common' ) @@ -308,4 +320,24 @@ const isHexString = (value: string | number) => { return true } +// This is a workaround to determine if a protocol is supported by the users device +// This doesn't work for Linux devices, as for that not error are thrown when the protocol is not supported +function checkIfProtocolIsSupported(protocol: string) { + let iframe = document.createElement('iframe'); + iframe.style.display = 'none'; + document.body.appendChild(iframe); + + if (!iframe.contentWindow) { + return false; + } + try { + iframe.contentWindow.location.href = protocol + '://'; + return true; + } catch(e) { + return false; + } finally { + document.body.removeChild(iframe); + } +} + export default walletConnect