diff --git a/wallets/core/src/hub/namespaces/mod.ts b/wallets/core/src/hub/namespaces/mod.ts index 0e7f4d1a39..23da8b0ce3 100644 --- a/wallets/core/src/hub/namespaces/mod.ts +++ b/wallets/core/src/hub/namespaces/mod.ts @@ -1,5 +1,6 @@ export type { Subscriber, + SubscriberCleanUp, State, RegisteredActions as ActionsMap, Context, diff --git a/wallets/core/src/namespaces/common/mod.ts b/wallets/core/src/namespaces/common/mod.ts index 82aec39630..8b50a6b7fd 100644 --- a/wallets/core/src/namespaces/common/mod.ts +++ b/wallets/core/src/namespaces/common/mod.ts @@ -16,3 +16,8 @@ export type { Accounts, AccountsWithActiveChain, } from '../../types/accounts.js'; + +export type { + Subscriber, + SubscriberCleanUp, +} from '../../hub/namespaces/mod.js'; diff --git a/wallets/provider-all/src/index.ts b/wallets/provider-all/src/index.ts index ccb9a2eb0b..68b4534cb0 100644 --- a/wallets/provider-all/src/index.ts +++ b/wallets/provider-all/src/index.ts @@ -7,7 +7,7 @@ import * as argentx from '@rango-dev/provider-argentx'; import * as bitget from '@rango-dev/provider-bitget'; import * as braavos from '@rango-dev/provider-braavos'; import * as brave from '@rango-dev/provider-brave'; -import * as clover from '@rango-dev/provider-clover'; +import { versions as clover } from '@rango-dev/provider-clover'; import * as coin98 from '@rango-dev/provider-coin98'; import * as coinbase from '@rango-dev/provider-coinbase'; import * as cosmostation from '@rango-dev/provider-cosmostation'; @@ -119,7 +119,7 @@ export const allProviders = (options?: Options): VersionedProviders[] => { legacyProviderImportsToVersionsInterface(bitget), legacyProviderImportsToVersionsInterface(enkrypt), legacyProviderImportsToVersionsInterface(xdefi), - legacyProviderImportsToVersionsInterface(clover), + clover, legacyProviderImportsToVersionsInterface(safepal), legacyProviderImportsToVersionsInterface(brave), legacyProviderImportsToVersionsInterface(coin98), diff --git a/wallets/provider-clover/package.json b/wallets/provider-clover/package.json index 94d4177fab..84cb0e5565 100644 --- a/wallets/provider-clover/package.json +++ b/wallets/provider-clover/package.json @@ -3,18 +3,18 @@ "version": "0.41.1-next.0", "license": "MIT", "type": "module", - "source": "./src/index.ts", - "main": "./dist/index.js", + "source": "./src/mod.ts", + "main": "./dist/mod.js", "exports": { - ".": "./dist/index.js" + ".": "./dist/mod.js" }, - "typings": "dist/index.d.ts", + "typings": "dist/mod.d.ts", "files": [ "dist", "src" ], "scripts": { - "build": "node ../../scripts/build/command.mjs --path wallets/provider-clover", + "build": "node ../../scripts/build/command.mjs --path wallets/provider-clover --inputs src/mod.ts", "ts-check": "tsc --declaration --emitDeclarationOnly -p ./tsconfig.json", "clean": "rimraf dist", "format": "prettier --write '{.,src}/**/*.{ts,tsx}'", @@ -30,4 +30,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/wallets/provider-clover/src/actions/solana.ts b/wallets/provider-clover/src/actions/solana.ts new file mode 100644 index 0000000000..8b08490305 --- /dev/null +++ b/wallets/provider-clover/src/actions/solana.ts @@ -0,0 +1,69 @@ +import type { + Subscriber, + SubscriberCleanUp, +} from '@rango-dev/wallets-core/namespaces/common'; + +import { + CAIP_NAMESPACE, + CAIP_SOLANA_CHAIN_ID, + type SolanaActions, +} from '@rango-dev/wallets-core/namespaces/solana'; +import { AccountId } from 'caip'; + +import { evmClover, solanaClover } from '../utils.js'; + +/* + * The EVM instance is used to listen for the accountsChanged event, + * because Clover itself did not have a chain change event for the Solana namespace. + */ +export function changeAccountSubscriberAction(): [ + Subscriber, + SubscriberCleanUp +] { + let eventCallback: () => void; + + // subscriber can be passed to `or`, it will get the error and should rethrow error to pass the error to next `or` or throw error. + return [ + (context, err) => { + const solanaInstance = solanaClover(); + const evmInstance = evmClover(); + if (!solanaInstance) { + throw new Error( + 'Trying to subscribe to your Solana wallet, but seems its instance is not available.' + ); + } + + const [, setState] = context.state(); + + eventCallback = async () => { + const solanaInstance = solanaClover(); + const solanaAccount = await solanaInstance.getAccount(); + + setState('accounts', [ + AccountId.format({ + address: solanaAccount, + chainId: { + namespace: CAIP_NAMESPACE, + reference: CAIP_SOLANA_CHAIN_ID, + }, + }), + ]); + }; + evmInstance.on('accountsChanged', eventCallback); + + if (err instanceof Error) { + throw err; + } + }, + (_context, err) => { + const evmInstance = evmClover(); + if (eventCallback && evmInstance) { + evmInstance.removeListener('accountsChanged', eventCallback); + } + + if (err instanceof Error) { + throw err; + } + }, + ]; +} diff --git a/wallets/provider-clover/src/constants.ts b/wallets/provider-clover/src/constants.ts new file mode 100644 index 0000000000..d554d1b181 --- /dev/null +++ b/wallets/provider-clover/src/constants.ts @@ -0,0 +1,22 @@ +import { type ProviderInfo } from '@rango-dev/wallets-core'; + +export const WALLET_ID = 'clover'; + +export const info: ProviderInfo = { + name: 'CLV', + icon: 'https://raw.githubusercontent.com/rango-exchange/assets/main/wallets/clover/icon.svg', + extensions: { + chrome: + 'https://chrome.google.com/webstore/detail/clover-wallet/nhnkbkgjikgcigadomkphalanndcapjk', + brave: + 'https://chrome.google.com/webstore/detail/clover-wallet/nhnkbkgjikgcigadomkphalanndcapjk', + homepage: 'https://wallet.clover.finance', + }, + properties: [ + { + name: 'detached', + // if you are adding a new namespace, don't forget to also update `getWalletInfo` + value: ['solana', 'evm'], + }, + ], +}; diff --git a/wallets/provider-clover/src/helpers.ts b/wallets/provider-clover/src/helpers.ts deleted file mode 100644 index 3213bb52e6..0000000000 --- a/wallets/provider-clover/src/helpers.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { Network, ProviderConnectResult } from '@rango-dev/wallets-shared'; - -import { Networks } from '@rango-dev/wallets-shared'; - -export function clover() { - const { clover, clover_solana } = window; - - if (!clover) { - return null; - } - - const instances = new Map(); - if (clover) { - instances.set(Networks.ETHEREUM, clover); - } - if (clover_solana) { - instances.set(Networks.SOLANA, clover_solana); - } - - return instances; -} - -type Provider = Map; - -export async function getNonEvmAccounts( - instances: Provider -): Promise { - const solanaInstance = instances.get(Networks.SOLANA); - const results: ProviderConnectResult[] = []; - - if (solanaInstance) { - const solanaAccounts = await solanaInstance.getAccount(); - - results.push({ - accounts: [solanaAccounts], - chainId: Networks.SOLANA, - }); - } - - return results; -} diff --git a/wallets/provider-clover/src/index.ts b/wallets/provider-clover/src/legacy/index.ts similarity index 88% rename from wallets/provider-clover/src/index.ts rename to wallets/provider-clover/src/legacy/index.ts index 0a559b8acd..2c2c9b80ee 100644 --- a/wallets/provider-clover/src/index.ts +++ b/wallets/provider-clover/src/legacy/index.ts @@ -1,3 +1,4 @@ +import type { LegacyProviderInterface } from '@rango-dev/wallets-core/legacy'; import type { CanEagerConnect, CanSwitchNetwork, @@ -20,7 +21,8 @@ import { } from '@rango-dev/wallets-shared'; import { evmBlockchains, isEvmBlockchain, solanaBlockchain } from 'rango-types'; -import { clover as clover_instance, getNonEvmAccounts } from './helpers.js'; +import { clover as clover_instance, getSolanaAccounts } from '../utils.js'; + import signer from './signer.js'; const WALLET = WalletTypes.CLOVER; @@ -44,7 +46,7 @@ export const connect: Connect = async ({ instance, meta }) => { }); } - const nonEvmResults = await getNonEvmAccounts(instance); + const nonEvmResults = await getSolanaAccounts(instance); results = [...results, ...nonEvmResults]; return results; @@ -118,3 +120,17 @@ export const getWalletInfo: (allBlockChains: BlockchainMeta[]) => WalletInfo = ( supportedChains: [...evms, ...solana], }; }; + +const legacyProvider: LegacyProviderInterface = { + config, + getInstance, + connect, + subscribe, + canSwitchNetworkTo, + getSigners, + getWalletInfo, + canEagerConnect, + switchNetwork, +}; + +export { legacyProvider }; diff --git a/wallets/provider-clover/src/signer.ts b/wallets/provider-clover/src/legacy/signer.ts similarity index 100% rename from wallets/provider-clover/src/signer.ts rename to wallets/provider-clover/src/legacy/signer.ts diff --git a/wallets/provider-clover/src/solana-signer.ts b/wallets/provider-clover/src/legacy/solana-signer.ts similarity index 100% rename from wallets/provider-clover/src/solana-signer.ts rename to wallets/provider-clover/src/legacy/solana-signer.ts diff --git a/wallets/provider-clover/src/mod.ts b/wallets/provider-clover/src/mod.ts new file mode 100644 index 0000000000..c762b39d44 --- /dev/null +++ b/wallets/provider-clover/src/mod.ts @@ -0,0 +1,11 @@ +import { defineVersions } from '@rango-dev/wallets-core/utils'; + +import { legacyProvider } from './legacy/index.js'; +import { provider } from './provider.js'; + +const versions = defineVersions() + .version('0.0.0', legacyProvider) + .version('1.0.0', provider) + .build(); + +export { versions }; diff --git a/wallets/provider-clover/src/namespaces/evm.ts b/wallets/provider-clover/src/namespaces/evm.ts new file mode 100644 index 0000000000..9aeb9aa152 --- /dev/null +++ b/wallets/provider-clover/src/namespaces/evm.ts @@ -0,0 +1,36 @@ +import type { EvmActions } from '@rango-dev/wallets-core/namespaces/evm'; + +import { NamespaceBuilder } from '@rango-dev/wallets-core'; +import { builders as commonBuilders } from '@rango-dev/wallets-core/namespaces/common'; +import { actions, builders } from '@rango-dev/wallets-core/namespaces/evm'; + +import { WALLET_ID } from '../constants.js'; +import { evmClover } from '../utils.js'; + +const [changeAccountSubscriber, changeAccountCleanup] = + actions.changeAccountSubscriber(evmClover); + +const [changeChainSubscriber, changeChainCleanup] = + actions.changeChainSubscriber(evmClover); + +const connect = builders + .connect() + .action(actions.connect(evmClover)) + .before(changeAccountSubscriber) + .before(changeChainSubscriber) + .or(changeAccountCleanup) + .or(changeChainCleanup) + .build(); + +const disconnect = commonBuilders + .disconnect() + .after(changeAccountCleanup) + .after(changeChainCleanup) + .build(); + +const evm = new NamespaceBuilder('EVM', WALLET_ID) + .action(connect) + .action(disconnect) + .build(); + +export { evm }; diff --git a/wallets/provider-clover/src/namespaces/solana.ts b/wallets/provider-clover/src/namespaces/solana.ts new file mode 100644 index 0000000000..8031b6648b --- /dev/null +++ b/wallets/provider-clover/src/namespaces/solana.ts @@ -0,0 +1,58 @@ +import type { CaipAccount } from '@rango-dev/wallets-core/namespaces/common'; +import type { SolanaActions } from '@rango-dev/wallets-core/namespaces/solana'; + +import { NamespaceBuilder } from '@rango-dev/wallets-core'; +import { LegacyNetworks } from '@rango-dev/wallets-core/legacy'; +import { builders as commonBuilders } from '@rango-dev/wallets-core/namespaces/common'; +import { + builders, + CAIP_NAMESPACE, + CAIP_SOLANA_CHAIN_ID, +} from '@rango-dev/wallets-core/namespaces/solana'; +import { CAIP } from '@rango-dev/wallets-core/utils'; + +import { changeAccountSubscriberAction } from '../actions/solana.js'; +import { WALLET_ID } from '../constants.js'; +import { solanaClover } from '../utils.js'; + +const [changeAccountSubscriber, changeAccountCleanup] = + changeAccountSubscriberAction(); + +const connect = builders + .connect() + .action(async function () { + const solanaInstance = solanaClover(); + const solanaAccounts = await solanaInstance.getAccount(); + const result = { + accounts: [solanaAccounts], + chainId: LegacyNetworks.SOLANA, + }; + + const formatAccounts = result.accounts.map( + (account) => + CAIP.AccountId.format({ + address: account, + chainId: { + namespace: CAIP_NAMESPACE, + reference: CAIP_SOLANA_CHAIN_ID, + }, + }) as CaipAccount + ); + + return formatAccounts; + }) + .before(changeAccountSubscriber) + .or(changeAccountCleanup) + .build(); + +const disconnect = commonBuilders + .disconnect() + .after(changeAccountCleanup) + .build(); + +const solana = new NamespaceBuilder('Solana', WALLET_ID) + .action(connect) + .action(disconnect) + .build(); + +export { solana }; diff --git a/wallets/provider-clover/src/provider.ts b/wallets/provider-clover/src/provider.ts new file mode 100644 index 0000000000..8c8cc426c8 --- /dev/null +++ b/wallets/provider-clover/src/provider.ts @@ -0,0 +1,22 @@ +import { ProviderBuilder } from '@rango-dev/wallets-core'; + +import { info, WALLET_ID } from './constants.js'; +import { evm } from './namespaces/evm.js'; +import { solana } from './namespaces/solana.js'; +import { clover as cloverInstance } from './utils.js'; + +const provider = new ProviderBuilder(WALLET_ID) + .init(function (context) { + const [, setState] = context.state(); + + if (cloverInstance()) { + setState('installed', true); + console.debug('[clover] instance detected.', context); + } + }) + .config('info', info) + .add('solana', solana) + .add('evm', evm) + .build(); + +export { provider }; diff --git a/wallets/provider-clover/src/utils.ts b/wallets/provider-clover/src/utils.ts new file mode 100644 index 0000000000..e8166d0bad --- /dev/null +++ b/wallets/provider-clover/src/utils.ts @@ -0,0 +1,70 @@ +import type { ProviderAPI as EvmProviderApi } from '@rango-dev/wallets-core/namespaces/evm'; +import type { ProviderAPI as SolanaProviderApi } from '@rango-dev/wallets-core/namespaces/solana'; +import type { ProviderConnectResult } from '@rango-dev/wallets-shared'; + +import { LegacyNetworks } from '@rango-dev/wallets-core/legacy'; + +type Provider = Map; + +export function clover() { + const { clover, clover_solana } = window; + + if (!clover) { + return null; + } + + const instances: Provider = new Map(); + + if (clover) { + instances.set(LegacyNetworks.ETHEREUM, clover); + } + if (clover_solana) { + instances.set(LegacyNetworks.SOLANA, clover_solana); + } + + return instances; +} + +export function evmClover(): EvmProviderApi { + const instances = clover(); + + const evmInstance = instances?.get(LegacyNetworks.ETHEREUM); + + if (!evmInstance) { + throw new Error( + 'Clover not injected or EVM not enabled. Please check your wallet.' + ); + } + + return evmInstance as EvmProviderApi; +} + +export function solanaClover(): SolanaProviderApi { + const instance = clover(); + const solanaInstance = instance?.get(LegacyNetworks.SOLANA); + + if (!solanaInstance) { + throw new Error( + 'Clover not injected or Solana not enabled. Please check your wallet.' + ); + } + + return solanaInstance; +} + +export async function getSolanaAccounts( + instances: Provider +): Promise { + const solanaInstance = instances.get(LegacyNetworks.SOLANA); + const results: ProviderConnectResult[] = []; + if (solanaInstance) { + const solanaAccounts = await solanaInstance.getAccount(); + + results.push({ + accounts: [solanaAccounts], + chainId: LegacyNetworks.SOLANA, + }); + } + + return results; +}