From 42c6f87eb40087c082dec2ee174e337d2b8812d9 Mon Sep 17 00:00:00 2001 From: 1cedrus Date: Fri, 17 Oct 2025 11:31:32 +0700 Subject: [PATCH 1/9] Add experimental handle of WalletConnect --- assets/wallets/wallet-connect-logo.svg | 11 + examples/demo-inkv5/src/main.tsx | 31 +- packages/typink/package.json | 3 + packages/typink/src/atoms/walletActions.ts | 19 +- .../src/providers/WalletSetupProvider.tsx | 14 +- packages/typink/src/utils/chains.ts | 59 +++ packages/typink/src/utils/index.ts | 1 + packages/typink/src/wallets/Wallet.ts | 3 +- packages/typink/src/wallets/WalletConnect.ts | 343 ++++++++++++++++++ packages/typink/src/wallets/index.ts | 1 + yarn.lock | 324 ++++++++++++++++- 11 files changed, 773 insertions(+), 36 deletions(-) create mode 100644 assets/wallets/wallet-connect-logo.svg create mode 100644 packages/typink/src/utils/chains.ts create mode 100644 packages/typink/src/wallets/WalletConnect.ts diff --git a/assets/wallets/wallet-connect-logo.svg b/assets/wallets/wallet-connect-logo.svg new file mode 100644 index 00000000..0af59717 --- /dev/null +++ b/assets/wallets/wallet-connect-logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/examples/demo-inkv5/src/main.tsx b/examples/demo-inkv5/src/main.tsx index 740fd7ac..37dbdebc 100644 --- a/examples/demo-inkv5/src/main.tsx +++ b/examples/demo-inkv5/src/main.tsx @@ -6,26 +6,39 @@ import App from '@/App'; import { theme } from '@/theme'; import { deployments } from '@/contracts/deployments'; import { - alephZeroTestnet, - development, + alephZero, ExtensionWallet, polkadotjs, - popTestnet, ReactToastifyAdapter, setupTxToaster, subwallet, talisman, TypinkProvider, + WalletConnect, } from 'typink'; import { toast } from 'react-toastify'; setupTxToaster({ adapter: new ReactToastifyAdapter(toast) }); const DEFAULT_CALLER = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'; // Alice -const SUPPORTED_NETWORK = [popTestnet, alephZeroTestnet]; -if (process.env.NODE_ENV === 'development') { - SUPPORTED_NETWORK.push(development); -} +const SUPPORTED_NETWORK = [alephZero]; +// if (process.env.NODE_ENV === 'development') { +// SUPPORTED_NETWORK.push(development); +// } + +export const walletConnect = new WalletConnect({ + name: 'WalletConnect', + id: 'walletconnect', + logo: 'https://raw.githubusercontent.com/Luno-lab/LunoKit/be5c713a42e099a6825e73c94c02c01de1a78a41/packages/core/src/config/logos/wallets/walletconnect.svg', + projectId: 'b56e18d47c72ab683b10814fe9495694', // Default + relayUrl: 'wss://relay.walletconnect.com', + metadata: { + name: 'Typink Dapp', + description: 'Typink powered dApp', + url: typeof window !== 'undefined' ? window.location.origin : 'https://typink.dev', + icons: ['https://raw.githubusercontent.com/dedotdev/typink/main/assets/typink/typink-pink-logo.png'], + }, +}); const enkrypt = new ExtensionWallet({ name: 'Enkrypt', @@ -43,9 +56,9 @@ function Root() { deployments={deployments} defaultCaller={DEFAULT_CALLER} supportedNetworks={SUPPORTED_NETWORK} - defaultNetworkId={popTestnet.id} + defaultNetworkId={alephZero.id} cacheMetadata={true} - wallets={[subwallet, talisman, polkadotjs, enkrypt]}> + wallets={[subwallet, talisman, polkadotjs, enkrypt, walletConnect]}> { +export const disconnectWalletAtom = atom(null, async (get, set, walletId?: string) => { const walletIds = get(connectedWalletIdsAtom); const connectedAccount = get(connectedAccountAtom); @@ -100,6 +100,11 @@ export const disconnectWalletAtom = atom(null, (get, set, walletId?: string) => const connectionAtom = walletConnectionsAtomFamily(walletId); const connection = get(connectionAtom); + // Clean up WalletConnect session if needed + if (connection?.wallet instanceof WalletConnect) { + await connection.wallet.disconnect(); + } + // Clean up subscription if (connection?.subscription) { connection.subscription(); @@ -118,14 +123,20 @@ export const disconnectWalletAtom = atom(null, (get, set, walletId?: string) => } } else { // Disconnect all wallets - walletIds.forEach((id) => { + for (const id of walletIds) { const connectionAtom = walletConnectionsAtomFamily(id); const connection = get(connectionAtom); + + // Clean up WalletConnect session if needed + if (connection?.wallet instanceof WalletConnect) { + await connection.wallet.disconnect(); + } + if (connection?.subscription) { connection.subscription(); } set(connectionAtom, null); - }); + } set(connectedWalletIdsAtom, []); set(connectedAccountAtom, undefined); diff --git a/packages/typink/src/providers/WalletSetupProvider.tsx b/packages/typink/src/providers/WalletSetupProvider.tsx index 001bb3c5..e3c5bfd0 100644 --- a/packages/typink/src/providers/WalletSetupProvider.tsx +++ b/packages/typink/src/providers/WalletSetupProvider.tsx @@ -1,7 +1,7 @@ import { createContext, useCallback, useContext, useEffect, useState } from 'react'; import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import { TypinkAccount } from '../types.js'; -import { polkadotjs, subwallet, talisman, Wallet } from '../wallets/index.js'; +import { polkadotjs, subwallet, talisman, Wallet, WalletConnect } from '../wallets/index.js'; import { noop } from '../utils/index.js'; import { WalletProvider, WalletProviderProps } from './WalletProvider.js'; import { @@ -19,6 +19,7 @@ import { initializeWalletsAtom, setExternalSignerAtom, } from '../atoms/walletActions.js'; +import { supportedNetworksAtom } from '../atoms/clientAtoms.js'; // Split these into 2 separate context (one for setup & one for signer & connected account) export interface WalletSetupContextProps { @@ -86,6 +87,7 @@ export function WalletSetupProvider({ const accounts = useAtomValue(allAccountsAtom); const availableWallets = useAtomValue(availableWalletsAtom); const finalEffectiveSigner = useAtomValue(finalEffectiveSignerAtom); + const supportedNetworks = useAtomValue(supportedNetworksAtom); // Use atom actions const connectWallet = useSetAtom(connectWalletAtom); @@ -95,8 +97,16 @@ export function WalletSetupProvider({ // Initialize wallets and app name useEffect(() => { const walletsToUse = initialWallets || DEFAULT_WALLETS; + + // Configure WalletConnect with supported networks + for (const wallet of walletsToUse) { + if (wallet instanceof WalletConnect && supportedNetworks.length > 0) { + wallet.setSupportedNetworks(supportedNetworks); + } + } + initializeWallets(walletsToUse); - }, [initialWallets, initializeWallets]); + }, [initialWallets, supportedNetworks, initializeWallets]); useEffect(() => { initializeAppName(appName); diff --git a/packages/typink/src/utils/chains.ts b/packages/typink/src/utils/chains.ts new file mode 100644 index 00000000..bae8c71f --- /dev/null +++ b/packages/typink/src/utils/chains.ts @@ -0,0 +1,59 @@ +import { NetworkInfo } from '../types.js'; + +/** + * Known chain genesis hashes for WalletConnect + * Format: "polkadot:{genesisHash}" + */ +const CHAIN_GENESIS_HASHES: Record = { + // Mainnet chains + polkadot: '91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3', + polkadot_asset_hub: '68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f', + kusama: 'b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe', + kusama_asset_hub: '48239ef607d7928874027a43a67689209727dfb3d3dc5e5b03a39bdc2eda771a', + alephzero: '70255b4d28de0fc4e1a193d7e175ad1ccef431598211c55538f1018651a0307e', + astar: '9eb76c5184c4ab8679d2d5d819fdf90b9c001403e9e17da2e14b6d8aec4029c6', + shiden: 'f1cf9022c7ebb34b162d5b5e34e705a5a740b2d0ecc1009fb89023e62a488108', + hydration: 'afdc188f45c71dacbaa0b62e16a91f726c7b8699a9748cdf715459de6b7f366d', + basilisk: 'a85cfb9b9fd4d622a5b28289a02347af987d8f73fa3108450e2b4a11c1ce5755', + + // Testnet chains + westend: 'e143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e', + westend_asset_hub: '67f9723393ef76214df0118c34bbbd3dbebc8ed46a10973a8c969d48fe7598c9', + // pop_testnet: Use westend for now since POP is a parachain on Paseo/Westend + pop_testnet: 'e143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e', + alephzero_testnet: '05d5279c52c484cc80396535a316add7d47b1c5b9e0398dd1f584149341460c5', +}; + +/** + * Convert a NetworkInfo to a WalletConnect chain identifier + * @param network - The network info from typink + * @returns WalletConnect chain identifier in format "polkadot:{genesisHash}" or undefined if not found + */ +export function networkInfoToWalletConnectChain(network: NetworkInfo): string | undefined { + const genesisHash = CHAIN_GENESIS_HASHES[network.id]; + + if (!genesisHash) { + console.warn(`No genesis hash mapping found for network: ${network.id}`); + return undefined; + } + + return genesisHashToWalletConnectChain(genesisHash); +} + +/** + * Convert a genesis hash to a WalletConnect chain identifier + * @param genesisHash - The genesis hash of the network + * @returns WalletConnect chain identifier in format "polkadot:{genesisHash}" + */ +export function genesisHashToWalletConnectChain(genesisHash: string): string { + return `polkadot:${genesisHash.replace('0x', '').slice(0, 32)}`; +} + +/** + * Convert an array of NetworkInfo to WalletConnect chain identifiers + * @param networks - Array of network info from typink + * @returns Array of WalletConnect chain identifiers + */ +export function getWalletConnectChains(networks: NetworkInfo[]): string[] { + return networks.map(networkInfoToWalletConnectChain).filter((chain): chain is string => chain !== undefined); +} diff --git a/packages/typink/src/utils/index.ts b/packages/typink/src/utils/index.ts index 300f39fc..8ded758c 100644 --- a/packages/typink/src/utils/index.ts +++ b/packages/typink/src/utils/index.ts @@ -1,4 +1,5 @@ export * from './errors.js'; export * from './misc.js'; export * from './accounts.js'; +export * from './chains.js'; export { formatBalance } from '@dedot/utils'; diff --git a/packages/typink/src/wallets/Wallet.ts b/packages/typink/src/wallets/Wallet.ts index 67fd5750..2d3d8889 100644 --- a/packages/typink/src/wallets/Wallet.ts +++ b/packages/typink/src/wallets/Wallet.ts @@ -1,4 +1,5 @@ import { InjectedWindow } from '../types.js'; +import { InjectedWindowProvider } from '../pjs-types.js'; export interface WalletOptions { id: string; @@ -35,7 +36,7 @@ export abstract class Wallet { return injectedWindow.injectedWeb3; } - get injectedProvider() { + get injectedProvider(): InjectedWindowProvider | undefined { return this.injectedWeb3[this.id]; } diff --git a/packages/typink/src/wallets/WalletConnect.ts b/packages/typink/src/wallets/WalletConnect.ts new file mode 100644 index 00000000..a8d6100d --- /dev/null +++ b/packages/typink/src/wallets/WalletConnect.ts @@ -0,0 +1,343 @@ +import { Wallet, WalletOptions } from './Wallet.js'; +import type { SignClientTypes } from '@walletconnect/types'; +import type { IUniversalProvider, Metadata } from '@walletconnect/universal-provider'; +import { + InjectedAccount, + InjectedAccounts, + InjectedSigner, + InjectedWindowProvider, + SignerPayloadJSON, + SignerPayloadRaw, + SignerResult, +} from '../pjs-types.js'; +import { NetworkInfo } from '../types.js'; +import { WalletConnectModal } from '@walletconnect/modal'; +import { assert, DedotError } from 'dedot/utils'; +import { genesisHashToWalletConnectChain, getWalletConnectChains } from 'src/utils/chains.js'; + +export interface WalletConnectOptions extends WalletOptions { + projectId: string; + metadata: Metadata; + relayUrl: string; +} + +export class WalletConnect extends Wallet { + #provider?: IUniversalProvider; + #modal?: WalletConnectModal; + + #injectedProvider?: InjectedWindowProvider; + #supportedNetworks: NetworkInfo[] = []; + + #accountSubscribers: Set<(accounts: InjectedAccount[]) => void | Promise> = new Set(); + #accounts: InjectedAccount[] = []; + + constructor(public options: WalletConnectOptions) { + super(options); + } + + get injectedProvider(): InjectedWindowProvider | undefined { + return this.#injectedProvider; + } + + get ready(): boolean { + // WalletConnect is always "ready" since it's a protocol, not a browser extension + // The actual initialization happens lazily when connect() is called + return true; + } + + get installed(): boolean { + // WalletConnect is always "installed" since it's a protocol, not a browser extension + return true; + } + + get provider(): IUniversalProvider | undefined { + return this.#provider; + } + + setSupportedNetworks(networks: NetworkInfo[]): void { + this.#supportedNetworks = networks; + } + + async waitUntilReady(): Promise { + await this.#initializeModal(); + await this.#initialize(); + } + + async #initialize(): Promise { + const { projectId, relayUrl, metadata } = this.options; + + assert( + this.options.projectId, + `${this.name} requires a projectId. Please visit https://cloud.walletconnect.com to get one.`, + ); + + const { UniversalProvider } = await import('@walletconnect/universal-provider'); + + try { + if (!this.#provider) { + this.#provider = await UniversalProvider.init({ + projectId, + relayUrl, + metadata, + }); + } + + if (this.#provider.session) { + this.#accounts = this.#getAccounts(); + } else { + const chains = getWalletConnectChains(this.#supportedNetworks); + + const { uri, approval } = await this.#provider.client!.connect({ + requiredNamespaces: { + polkadot: { + chains, + methods: [ + 'polkadot_signMessage', + 'polkadot_sendTransaction', + 'polkadot_signTransaction', + 'polkadot_requestAccounts', + ], + events: ['accountsChanged', 'chainChanged', 'connect'], + }, + }, + optionalNamespaces: { + polkadot: { + chains, + methods: [ + 'polkadot_signMessage', + 'polkadot_sendTransaction', + 'polkadot_signTransaction', + 'polkadot_requestAccounts', + ], + events: ['accountsChanged', 'chainChanged', 'connect'], + }, + }, + }); + + if (uri) { + await this.#modal!.openModal({ uri }); + } + + this.#provider.session = await approval(); + this.#modal!.closeModal(); + this.#accounts = this.#getAccounts(); + } + + if (this.#provider.session && this.#provider.client) { + this.#subscribeToSessionEvents(); + this.#notifyAccountSubscribers(this.#accounts); + this.#createInjectedProvider(); + } + } catch (error) { + throw new DedotError(`Failed to initialize WalletConnect: ${(error as Error).message}`); + } + } + + async #initializeModal(): Promise { + if (this.#modal) return; + + try { + const WalletConnectModalModule = await import('@walletconnect/modal'); + const WalletConnectModal = WalletConnectModalModule.WalletConnectModal; + + this.#modal = new WalletConnectModal({ + projectId: this.options.projectId, + }); + + // Find out a way to do it properly + this.#modal!.setTheme({ + themeVariables: { + '--wcm-z-index': '9999', + }, + }); + } catch (error) { + throw new DedotError(`Failed to initialize WalletConnect Modal: ${(error as Error).message}`); + } + } + + async disconnect(): Promise { + try { + if (this.#provider) { + await Promise.race([ + await this.#provider.disconnect(), + new Promise((_, reject) => setTimeout(() => reject(new Error('Disconnect timeout')), 5000)), + ]); + } + } catch (e) {} + + this.#cleanUp(); + } + + #cleanUp() { + this.#provider = undefined; + this.#injectedProvider = undefined; + this.#accountSubscribers.clear(); + } + + #getAccounts(): InjectedAccount[] { + if (!this.#provider?.session) return []; + + const accounts: InjectedAccount[] = []; + const polkadotNamespace = this.#provider.session.namespaces.polkadot; + + // TODO: Handle multiple chains and genesisHashes + + if (polkadotNamespace && polkadotNamespace.accounts) { + // Get unique addresses across all chains + const uniqueAddresses = new Set(); + + for (const account of polkadotNamespace.accounts) { + // WalletConnect account format: "polkadot:{genesisHash}:{address}" + const parts = account.split(':'); + if (parts.length >= 3) { + const address = parts.slice(2).join(':'); // Handle addresses with colons + uniqueAddresses.add(address); + } + } + + uniqueAddresses.forEach((address) => { + accounts.push({ address }); + }); + } + + return accounts; + } + + #getSigner(): InjectedSigner { + return { + signPayload: async (payload: SignerPayloadJSON): Promise => { + assert(this.#provider?.client && this.#provider?.session, 'Provider client or session not initialized'); + + console.log('Signing message with WalletConnect:', payload); + + try { + const result = (await this.#provider.client.request({ + topic: this.#provider.session.topic, + chainId: genesisHashToWalletConnectChain(payload.genesisHash), + request: { + method: 'polkadot_signTransaction', + params: { + address: payload.address, + transactionPayload: payload, + }, + }, + })) as { signature: string }; + + return { + id: 0, + signature: result.signature as `0x${string}`, + }; + } catch (error) { + throw new DedotError(`Failed to sign payload with WalletConnect: ${(error as Error).message}`); + } + }, + + signRaw: async (raw: SignerPayloadRaw): Promise => { + assert(this.#provider?.client && this.#provider?.session, 'Provider client or session not initialized'); + + try { + // Use the first chain from the session + const sessionChains = this.#provider.session.namespaces.polkadot?.chains || []; + const chainId = + sessionChains[0] || 'polkadot:91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3'; + + console.log('Signing raw message with WalletConnect:', raw); + + const result = (await this.#provider.client.request({ + topic: this.#provider.session.topic, + chainId, + request: { + method: 'polkadot_signMessage', + params: { + address: raw.address, + message: raw.data, + type: raw.type, + }, + }, + })) as { signature: string }; + + return { + id: 0, + signature: result.signature as `0x${string}`, + }; + } catch (error) { + throw new DedotError(`Failed to sign raw message with WalletConnect: ${(error as Error).message}`); + } + }, + }; + } + + #getInjectedAccounts(): InjectedAccounts { + return { + get: async (): Promise => { + return this.#getAccounts(); + }, + + subscribe: (callback: (accounts: InjectedAccount[]) => void | Promise): (() => void) => { + this.#accountSubscribers.add(callback); + + // Immediately call with current accounts + callback(this.#getAccounts()); + + // Return unsubscribe function + return () => { + this.#accountSubscribers.delete(callback); + }; + }, + }; + } + + #createInjectedProvider(): void { + const signer = this.#getSigner(); + const accounts = this.#getInjectedAccounts(); + + this.#injectedProvider = { + enable: async (): Promise<{ accounts: InjectedAccounts; signer: InjectedSigner }> => { + if (!this.#provider?.session) { + throw new Error('WalletConnect session not established'); + } + + return { + accounts, + signer, + }; + }, + version: '1.0.0', + }; + } + + #subscribeToSessionEvents(): void { + assert(this.#provider?.client && this.#provider?.session, 'Provider client or session not initialized'); + + this.#provider.client.on( + 'session_update', + ({ topic, params }: SignClientTypes.EventArguments['session_update']) => { + const { namespaces } = params; + const session = this.#provider!.client!.session.get(topic); + this.#provider!.session! = { ...session, namespaces }; + + // Notify account subscribers + const accounts = this.#getAccounts(); + this.#notifyAccountSubscribers(accounts); + }, + ); + + this.#provider.client.on('session_delete', () => { + this.#provider!.session = undefined; + this.#injectedProvider = undefined; + + // Notify subscribers that accounts are gone + this.#notifyAccountSubscribers([]); + }); + } + + #notifyAccountSubscribers(accounts: InjectedAccount[]): void { + this.#accountSubscribers.forEach((callback) => { + try { + callback(accounts); + } catch (error) { + console.error('Error in account subscriber callback:', error); + } + }); + } +} diff --git a/packages/typink/src/wallets/index.ts b/packages/typink/src/wallets/index.ts index c88cb75b..028f326e 100644 --- a/packages/typink/src/wallets/index.ts +++ b/packages/typink/src/wallets/index.ts @@ -2,6 +2,7 @@ import { ExtensionWallet } from './ExtensionWallet.js'; export * from './Wallet.js'; export * from './ExtensionWallet.js'; +export * from './WalletConnect.js'; export const subwallet = new ExtensionWallet({ name: 'SubWallet', diff --git a/yarn.lock b/yarn.lock index 15d3674a..db7e53da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3206,6 +3206,13 @@ __metadata: languageName: node linkType: hard +"@msgpack/msgpack@npm:3.1.2": + version: 3.1.2 + resolution: "@msgpack/msgpack@npm:3.1.2" + checksum: 10/e04ff37d7c89ffdd6b4fbcd1770af60b16c98afdf1c3c16190170dfe34764048eb45e3654016ac62cc616c7e4b09e611f8863317ca5f18b3a72974fb131e562e + languageName: node + linkType: hard + "@napi-rs/wasm-runtime@npm:^0.2.11": version: 0.2.12 resolution: "@napi-rs/wasm-runtime@npm:0.2.12" @@ -3217,13 +3224,22 @@ __metadata: languageName: node linkType: hard -"@noble/ciphers@npm:^1.3.0": +"@noble/ciphers@npm:1.3.0, @noble/ciphers@npm:^1.3.0": version: 1.3.0 resolution: "@noble/ciphers@npm:1.3.0" checksum: 10/051660051e3e9e2ca5fb9dece2885532b56b7e62946f89afa7284a0fb8bc02e2bd1c06554dba68162ff42d295b54026456084198610f63c296873b2f1cd7a586 languageName: node linkType: hard +"@noble/curves@npm:1.8.0": + version: 1.8.0 + resolution: "@noble/curves@npm:1.8.0" + dependencies: + "@noble/hashes": "npm:1.7.0" + checksum: 10/c54ce84cf54b8bda1a37a10dfae2e49e5b6cdf5dd98b399efa8b8a80a286b3f8f27bde53202cb308353bfd98719938991a78bed6e43f81f13b17f8181b7b82eb + languageName: node + linkType: hard + "@noble/curves@npm:1.9.1": version: 1.9.1 resolution: "@noble/curves@npm:1.9.1" @@ -3233,6 +3249,15 @@ __metadata: languageName: node linkType: hard +"@noble/curves@npm:1.9.7, @noble/curves@npm:~1.9.0": + version: 1.9.7 + resolution: "@noble/curves@npm:1.9.7" + dependencies: + "@noble/hashes": "npm:1.8.0" + checksum: 10/3cfe2735ea94972988ca9e217e0ebb2044372a7160b2079bf885da789492a6291fc8bf76ca3d8bf8dee477847ee2d6fac267d1e6c4f555054059f5e8c4865d44 + languageName: node + linkType: hard + "@noble/curves@npm:^1.3.0": version: 1.7.0 resolution: "@noble/curves@npm:1.7.0" @@ -3242,15 +3267,6 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:~1.9.0": - version: 1.9.7 - resolution: "@noble/curves@npm:1.9.7" - dependencies: - "@noble/hashes": "npm:1.8.0" - checksum: 10/3cfe2735ea94972988ca9e217e0ebb2044372a7160b2079bf885da789492a6291fc8bf76ca3d8bf8dee477847ee2d6fac267d1e6c4f555054059f5e8c4865d44 - languageName: node - linkType: hard - "@noble/hashes@npm:1.6.0": version: 1.6.0 resolution: "@noble/hashes@npm:1.6.0" @@ -3258,6 +3274,13 @@ __metadata: languageName: node linkType: hard +"@noble/hashes@npm:1.7.0": + version: 1.7.0 + resolution: "@noble/hashes@npm:1.7.0" + checksum: 10/ab038a816c8c9bb986e92797e3d9c5a5b37c020e0c3edc55bcae5061dbdd457f1f0a22787f83f4787c17415ba0282a20a1e455d36ed0cdcace4ce21ef1869f60 + languageName: node + linkType: hard + "@noble/hashes@npm:1.8.0, @noble/hashes@npm:^1.7.1, @noble/hashes@npm:^1.8.0, @noble/hashes@npm:~1.8.0": version: 1.8.0 resolution: "@noble/hashes@npm:1.8.0" @@ -4768,6 +4791,13 @@ __metadata: languageName: node linkType: hard +"@scure/base@npm:1.2.6, @scure/base@npm:^1.2.4, @scure/base@npm:^1.2.6, @scure/base@npm:~1.2.5": + version: 1.2.6 + resolution: "@scure/base@npm:1.2.6" + checksum: 10/c1a7bd5e0b0c8f94c36fbc220f4a67cc832b00e2d2065c7d8a404ed81ab1c94c5443def6d361a70fc382db3496e9487fb9941728f0584782b274c18a4bed4187 + languageName: node + linkType: hard + "@scure/base@npm:^1.1.1, @scure/base@npm:^1.1.5, @scure/base@npm:^1.1.7": version: 1.2.1 resolution: "@scure/base@npm:1.2.1" @@ -4775,13 +4805,6 @@ __metadata: languageName: node linkType: hard -"@scure/base@npm:^1.2.4, @scure/base@npm:^1.2.6, @scure/base@npm:~1.2.5": - version: 1.2.6 - resolution: "@scure/base@npm:1.2.6" - checksum: 10/c1a7bd5e0b0c8f94c36fbc220f4a67cc832b00e2d2065c7d8a404ed81ab1c94c5443def6d361a70fc382db3496e9487fb9941728f0584782b274c18a4bed4187 - languageName: node - linkType: hard - "@scure/bip32@npm:1.7.0, @scure/bip32@npm:^1.7.0": version: 1.7.0 resolution: "@scure/bip32@npm:1.7.0" @@ -5979,6 +6002,31 @@ __metadata: languageName: node linkType: hard +"@walletconnect/core@npm:2.22.4": + version: 2.22.4 + resolution: "@walletconnect/core@npm:2.22.4" + dependencies: + "@walletconnect/heartbeat": "npm:1.2.2" + "@walletconnect/jsonrpc-provider": "npm:1.0.14" + "@walletconnect/jsonrpc-types": "npm:1.0.4" + "@walletconnect/jsonrpc-utils": "npm:1.0.8" + "@walletconnect/jsonrpc-ws-connection": "npm:1.0.16" + "@walletconnect/keyvaluestorage": "npm:1.1.1" + "@walletconnect/logger": "npm:3.0.0" + "@walletconnect/relay-api": "npm:1.0.11" + "@walletconnect/relay-auth": "npm:1.1.0" + "@walletconnect/safe-json": "npm:1.0.2" + "@walletconnect/time": "npm:1.0.2" + "@walletconnect/types": "npm:2.22.4" + "@walletconnect/utils": "npm:2.22.4" + "@walletconnect/window-getters": "npm:1.0.1" + es-toolkit: "npm:1.39.3" + events: "npm:3.3.0" + uint8arrays: "npm:3.1.1" + checksum: 10/d34e617532843a6c18dc67b4c308f9f50aa259ec9357c35b63c0c481bdebe834da1a7e7b6fa96e2ad37a63d421e662086a71aae5d00208a6e469a060b926cd9b + languageName: node + linkType: hard + "@walletconnect/environment@npm:^1.0.1": version: 1.0.1 resolution: "@walletconnect/environment@npm:1.0.1" @@ -6065,6 +6113,18 @@ __metadata: languageName: node linkType: hard +"@walletconnect/jsonrpc-ws-connection@npm:1.0.16": + version: 1.0.16 + resolution: "@walletconnect/jsonrpc-ws-connection@npm:1.0.16" + dependencies: + "@walletconnect/jsonrpc-utils": "npm:^1.0.6" + "@walletconnect/safe-json": "npm:^1.0.2" + events: "npm:^3.3.0" + ws: "npm:^7.5.1" + checksum: 10/98e06097588f895c4ba14b6feb64ed9b5c125d57a4ea3ad3fa6f52fd090fccce60808252c8cefaddc022cfa7fde7551a3aec3bb36e6b08c622207d7554d93e40 + languageName: node + linkType: hard + "@walletconnect/keyvaluestorage@npm:1.1.1": version: 1.1.1 resolution: "@walletconnect/keyvaluestorage@npm:1.1.1" @@ -6091,6 +6151,16 @@ __metadata: languageName: node linkType: hard +"@walletconnect/logger@npm:3.0.0": + version: 3.0.0 + resolution: "@walletconnect/logger@npm:3.0.0" + dependencies: + "@walletconnect/safe-json": "npm:^1.0.2" + pino: "npm:10.0.0" + checksum: 10/30c3c6029c6cf9669e9b10a4b6633b0b20402d4535eddc7604f707e1725cec057cbc2ee84fed3afba5f0d3cb457ec60f5afd5a6a529c3f3f808c15ea177a6451 + languageName: node + linkType: hard + "@walletconnect/modal-core@npm:2.7.0": version: 2.7.0 resolution: "@walletconnect/modal-core@npm:2.7.0" @@ -6112,7 +6182,7 @@ __metadata: languageName: node linkType: hard -"@walletconnect/modal@npm:^2.6.2": +"@walletconnect/modal@npm:^2.6.2, @walletconnect/modal@npm:^2.7.0": version: 2.7.0 resolution: "@walletconnect/modal@npm:2.7.0" dependencies: @@ -6145,6 +6215,19 @@ __metadata: languageName: node linkType: hard +"@walletconnect/relay-auth@npm:1.1.0": + version: 1.1.0 + resolution: "@walletconnect/relay-auth@npm:1.1.0" + dependencies: + "@noble/curves": "npm:1.8.0" + "@noble/hashes": "npm:1.7.0" + "@walletconnect/safe-json": "npm:^1.0.1" + "@walletconnect/time": "npm:^1.0.2" + uint8arrays: "npm:^3.0.0" + checksum: 10/0fd6c2e05ced76fbc8e6a84c0a8e73458779662aea55568f51cd9066c337d8a12f2869f0bd717024bbe5955cc605241e68505ebac40406ed2a1bdacba42431b1 + languageName: node + linkType: hard + "@walletconnect/safe-json@npm:1.0.2, @walletconnect/safe-json@npm:^1.0.1, @walletconnect/safe-json@npm:^1.0.2": version: 1.0.2 resolution: "@walletconnect/safe-json@npm:1.0.2" @@ -6171,6 +6254,23 @@ __metadata: languageName: node linkType: hard +"@walletconnect/sign-client@npm:^2.22.4": + version: 2.22.4 + resolution: "@walletconnect/sign-client@npm:2.22.4" + dependencies: + "@walletconnect/core": "npm:2.22.4" + "@walletconnect/events": "npm:1.0.1" + "@walletconnect/heartbeat": "npm:1.2.2" + "@walletconnect/jsonrpc-utils": "npm:1.0.8" + "@walletconnect/logger": "npm:3.0.0" + "@walletconnect/time": "npm:1.0.2" + "@walletconnect/types": "npm:2.22.4" + "@walletconnect/utils": "npm:2.22.4" + events: "npm:3.3.0" + checksum: 10/0361788319b983b90e79c4b86afe108eeece8dee9a053bab6e23727186041610ed173f3c8bab8127da6e2c1441d8ae3a90c002ddbbd256e0e0507a3a9783f1cc + languageName: node + linkType: hard + "@walletconnect/time@npm:1.0.2, @walletconnect/time@npm:^1.0.2": version: 1.0.2 resolution: "@walletconnect/time@npm:1.0.2" @@ -6194,6 +6294,20 @@ __metadata: languageName: node linkType: hard +"@walletconnect/types@npm:2.22.4": + version: 2.22.4 + resolution: "@walletconnect/types@npm:2.22.4" + dependencies: + "@walletconnect/events": "npm:1.0.1" + "@walletconnect/heartbeat": "npm:1.2.2" + "@walletconnect/jsonrpc-types": "npm:1.0.4" + "@walletconnect/keyvaluestorage": "npm:1.1.1" + "@walletconnect/logger": "npm:3.0.0" + events: "npm:3.3.0" + checksum: 10/d6f99583648726016ad273dc586bbdc123251b1b0bc2fa41b03e706598c5cf8a7ea7f07ac1b449cb6abe8626fc2f12957527cf55cdd8f3e5df2322496f2dedef + languageName: node + linkType: hard + "@walletconnect/universal-provider@npm:^2.11.2": version: 2.17.2 resolution: "@walletconnect/universal-provider@npm:2.17.2" @@ -6242,6 +6356,34 @@ __metadata: languageName: node linkType: hard +"@walletconnect/utils@npm:2.22.4, @walletconnect/utils@npm:^2.22.4": + version: 2.22.4 + resolution: "@walletconnect/utils@npm:2.22.4" + dependencies: + "@msgpack/msgpack": "npm:3.1.2" + "@noble/ciphers": "npm:1.3.0" + "@noble/curves": "npm:1.9.7" + "@noble/hashes": "npm:1.8.0" + "@scure/base": "npm:1.2.6" + "@walletconnect/jsonrpc-utils": "npm:1.0.8" + "@walletconnect/keyvaluestorage": "npm:1.1.1" + "@walletconnect/logger": "npm:3.0.0" + "@walletconnect/relay-api": "npm:1.0.11" + "@walletconnect/relay-auth": "npm:1.1.0" + "@walletconnect/safe-json": "npm:1.0.2" + "@walletconnect/time": "npm:1.0.2" + "@walletconnect/types": "npm:2.22.4" + "@walletconnect/window-getters": "npm:1.0.1" + "@walletconnect/window-metadata": "npm:1.0.1" + blakejs: "npm:1.2.1" + bs58: "npm:6.0.0" + detect-browser: "npm:5.3.0" + ox: "npm:0.9.3" + uint8arrays: "npm:3.1.1" + checksum: 10/8990552743ee23d2ff09e0ea0f19e70ae76e0833d2b2c74bcbe6c30b657dc4a800cb0affe07cb9000d51715f2e2617ee2b07883baffe468da3b1533fcb7939b3 + languageName: node + linkType: hard + "@walletconnect/window-getters@npm:1.0.1, @walletconnect/window-getters@npm:^1.0.1": version: 1.0.1 resolution: "@walletconnect/window-getters@npm:1.0.1" @@ -6825,6 +6967,13 @@ __metadata: languageName: node linkType: hard +"base-x@npm:^5.0.0": + version: 5.0.1 + resolution: "base-x@npm:5.0.1" + checksum: 10/6e4f847ef842e0a71c6b6020a6ec482a2a5e727f5a98534dbfd5d5a4e8afbc0d1bdf1fd57174b3f0455d107f10a932c3c7710bec07e2878f80178607f8f605c8 + languageName: node + linkType: hard + "base64-js@npm:^1.3.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" @@ -6871,6 +7020,13 @@ __metadata: languageName: node linkType: hard +"blakejs@npm:1.2.1": + version: 1.2.1 + resolution: "blakejs@npm:1.2.1" + checksum: 10/0638b1bd058b21892633929c43005aa6a4cc4b2ac5b338a146c3c076622f1b360795bd7a4d1f077c9b01863ed2df0c1504a81c5b520d164179120434847e6cd7 + languageName: node + linkType: hard + "bn.js@npm:^4.11.9": version: 4.12.1 resolution: "bn.js@npm:4.12.1" @@ -6969,6 +7125,15 @@ __metadata: languageName: node linkType: hard +"bs58@npm:6.0.0": + version: 6.0.0 + resolution: "bs58@npm:6.0.0" + dependencies: + base-x: "npm:^5.0.0" + checksum: 10/7c9bb2b2d93d997a8c652de3510d89772007ac64ee913dc4e16ba7ff47624caad3128dcc7f360763eb6308760c300b3e9fd91b8bcbd489acd1a13278e7949c4e + languageName: node + linkType: hard + "buffer-from@npm:^1.0.0": version: 1.1.2 resolution: "buffer-from@npm:1.1.2" @@ -8827,6 +8992,18 @@ __metadata: languageName: node linkType: hard +"es-toolkit@npm:1.39.3": + version: 1.39.3 + resolution: "es-toolkit@npm:1.39.3" + dependenciesMeta: + "@trivago/prettier-plugin-sort-imports@4.3.0": + unplugged: true + prettier-plugin-sort-re-exports@0.0.1: + unplugged: true + checksum: 10/18cf6dee69170f802a50d447cac3af0026c199b501fe9a151633a402ea0523463b73aacbde53072cc610914d68031e2856fbb8a305b020e34bd7f6ac24d37e6d + languageName: node + linkType: hard + "es5-ext@npm:^0.10.35, es5-ext@npm:^0.10.46, es5-ext@npm:^0.10.62, es5-ext@npm:^0.10.64, es5-ext@npm:~0.10.14, es5-ext@npm:~0.10.2": version: 0.10.64 resolution: "es5-ext@npm:0.10.64" @@ -13581,6 +13758,13 @@ __metadata: languageName: node linkType: hard +"on-exit-leak-free@npm:^2.1.0": + version: 2.1.2 + resolution: "on-exit-leak-free@npm:2.1.2" + checksum: 10/f7b4b7200026a08f6e4a17ba6d72e6c5cbb41789ed9cf7deaf9d9e322872c7dc5a7898549a894651ee0ee9ae635d34a678115bf8acdfba8ebd2ba2af688b563c + languageName: node + linkType: hard + "on-headers@npm:~1.1.0": version: 1.1.0 resolution: "on-headers@npm:1.1.0" @@ -13701,6 +13885,27 @@ __metadata: languageName: node linkType: hard +"ox@npm:0.9.3": + version: 0.9.3 + resolution: "ox@npm:0.9.3" + dependencies: + "@adraffy/ens-normalize": "npm:^1.11.0" + "@noble/ciphers": "npm:^1.3.0" + "@noble/curves": "npm:1.9.1" + "@noble/hashes": "npm:^1.8.0" + "@scure/bip32": "npm:^1.7.0" + "@scure/bip39": "npm:^1.6.0" + abitype: "npm:^1.0.9" + eventemitter3: "npm:5.0.1" + peerDependencies: + typescript: ">=5.4.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/c485c810d5b1e78fcb89d6f19dbf01ae90ad39d7746593620ccf4bf5ddb799b369dc3b56d625c80b9546ab2eb7231b841b46c4c6776f87926bde07999e0b82be + languageName: node + linkType: hard + "ox@npm:0.9.6": version: 0.9.6 resolution: "ox@npm:0.9.6" @@ -14107,6 +14312,15 @@ __metadata: languageName: node linkType: hard +"pino-abstract-transport@npm:^2.0.0": + version: 2.0.0 + resolution: "pino-abstract-transport@npm:2.0.0" + dependencies: + split2: "npm:^4.0.0" + checksum: 10/e5699ecb06c7121055978e988e5cecea5b6892fc2589c64f1f86df5e7386bbbfd2ada268839e911b021c6b3123428aed7c6be3ac7940eee139556c75324c7e83 + languageName: node + linkType: hard + "pino-abstract-transport@npm:v0.5.0": version: 0.5.0 resolution: "pino-abstract-transport@npm:0.5.0" @@ -14124,6 +14338,34 @@ __metadata: languageName: node linkType: hard +"pino-std-serializers@npm:^7.0.0": + version: 7.0.0 + resolution: "pino-std-serializers@npm:7.0.0" + checksum: 10/884e08f65aa5463d820521ead3779d4472c78fc434d8582afb66f9dcb8d8c7119c69524b68106cb8caf92c0487be7794cf50e5b9c0383ae65b24bf2a03480951 + languageName: node + linkType: hard + +"pino@npm:10.0.0": + version: 10.0.0 + resolution: "pino@npm:10.0.0" + dependencies: + atomic-sleep: "npm:^1.0.0" + on-exit-leak-free: "npm:^2.1.0" + pino-abstract-transport: "npm:^2.0.0" + pino-std-serializers: "npm:^7.0.0" + process-warning: "npm:^5.0.0" + quick-format-unescaped: "npm:^4.0.3" + real-require: "npm:^0.2.0" + safe-stable-stringify: "npm:^2.3.1" + slow-redact: "npm:^0.3.0" + sonic-boom: "npm:^4.0.1" + thread-stream: "npm:^3.0.0" + bin: + pino: bin.js + checksum: 10/32c7da274526706a2f1ad329bd44f2e2d2f0e0484e63dc290f8fb6986ed23e97ba5f30e6a80af3fa5f30a58e70a46d40a08b5731e1dfcb6f17a465f595fa361d + languageName: node + linkType: hard + "pino@npm:7.11.0": version: 7.11.0 resolution: "pino@npm:7.11.0" @@ -14290,6 +14532,13 @@ __metadata: languageName: node linkType: hard +"process-warning@npm:^5.0.0": + version: 5.0.0 + resolution: "process-warning@npm:5.0.0" + checksum: 10/10f3e00ac9fc1943ec4566ff41fff2b964e660f853c283e622257719839d340b4616e707d62a02d6aa0038761bb1fa7c56bc7308d602d51bd96f05f9cd305dcd + languageName: node + linkType: hard + "promise-inflight@npm:^1.0.1": version: 1.0.1 resolution: "promise-inflight@npm:1.0.1" @@ -14791,6 +15040,13 @@ __metadata: languageName: node linkType: hard +"real-require@npm:^0.2.0": + version: 0.2.0 + resolution: "real-require@npm:0.2.0" + checksum: 10/ddf44ee76301c774e9c9f2826da8a3c5c9f8fc87310f4a364e803ef003aa1a43c378b4323051ced212097fff1af459070f4499338b36a7469df1d4f7e8c0ba4c + languageName: node + linkType: hard + "redent@npm:^3.0.0": version: 3.0.0 resolution: "redent@npm:3.0.0" @@ -15238,7 +15494,7 @@ __metadata: languageName: node linkType: hard -"safe-stable-stringify@npm:^2.1.0": +"safe-stable-stringify@npm:^2.1.0, safe-stable-stringify@npm:^2.3.1": version: 2.5.0 resolution: "safe-stable-stringify@npm:2.5.0" checksum: 10/2697fa186c17c38c3ca5309637b4ac6de2f1c3d282da27cd5e1e3c88eca0fb1f9aea568a6aabdf284111592c8782b94ee07176f17126031be72ab1313ed46c5c @@ -15567,6 +15823,13 @@ __metadata: languageName: node linkType: hard +"slow-redact@npm:^0.3.0": + version: 0.3.2 + resolution: "slow-redact@npm:0.3.2" + checksum: 10/53889cc43128fd49dfa54dd39a858b215b075b0e2020f93fb0210bc419e5c0de9416300a885de8542238f253b622578ea8a58f9a55f548a9f3300119fd555064 + languageName: node + linkType: hard + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -15633,6 +15896,15 @@ __metadata: languageName: node linkType: hard +"sonic-boom@npm:^4.0.1": + version: 4.2.0 + resolution: "sonic-boom@npm:4.2.0" + dependencies: + atomic-sleep: "npm:^1.0.0" + checksum: 10/385ef7fb5ea5976c1d2a1fef0b6df8df6b7caba8696d2d67f689d60c05e3ea2d536752ce7e1c69b9fad844635f1036d07c446f8e8149f5c6a80e0040a455b310 + languageName: node + linkType: hard + "sonner@npm:^2.0.7": version: 2.0.7 resolution: "sonner@npm:2.0.7" @@ -16223,6 +16495,15 @@ __metadata: languageName: node linkType: hard +"thread-stream@npm:^3.0.0": + version: 3.1.0 + resolution: "thread-stream@npm:3.1.0" + dependencies: + real-require: "npm:^0.2.0" + checksum: 10/ea2d816c4f6077a7062fac5414a88e82977f807c82ee330938fb9691fe11883bb03f078551c0518bb649c239e47ba113d44014fcbb5db42c5abd5996f35e4213 + languageName: node + linkType: hard + "throttle-debounce@npm:^3.0.1": version: 3.0.1 resolution: "throttle-debounce@npm:3.0.1" @@ -16718,6 +16999,9 @@ __metadata: "@testing-library/react": "npm:^16.3.0" "@types/react": "npm:^19.1.1" "@vitest/ui": "npm:^3.2.4" + "@walletconnect/modal": "npm:^2.7.0" + "@walletconnect/sign-client": "npm:^2.22.4" + "@walletconnect/utils": "npm:^2.22.4" fast-deep-equal: "npm:^3.1.3" jotai: "npm:^2.15.0" jsdom: "npm:^25.0.1" @@ -16769,7 +17053,7 @@ __metadata: languageName: node linkType: hard -"uint8arrays@npm:^3.0.0": +"uint8arrays@npm:3.1.1, uint8arrays@npm:^3.0.0": version: 3.1.1 resolution: "uint8arrays@npm:3.1.1" dependencies: From d337e537d220e2997b5d762db72b6f0bbf05ef44 Mon Sep 17 00:00:00 2001 From: 1cedrus Date: Fri, 17 Oct 2025 14:26:53 +0700 Subject: [PATCH 2/9] Refactor code --- examples/demo-inkv5/.eslintrc.cjs | 16 +++---- packages/typink/src/networks/mainnet.ts | 12 +++++ packages/typink/src/networks/testnet.ts | 10 +++++ packages/typink/src/types.ts | 1 + packages/typink/src/utils/chains.ts | 46 ++------------------ packages/typink/src/wallets/WalletConnect.ts | 34 ++++++--------- 6 files changed, 44 insertions(+), 75 deletions(-) diff --git a/examples/demo-inkv5/.eslintrc.cjs b/examples/demo-inkv5/.eslintrc.cjs index c03c28c9..6ba48c34 100644 --- a/examples/demo-inkv5/.eslintrc.cjs +++ b/examples/demo-inkv5/.eslintrc.cjs @@ -1,19 +1,13 @@ module.exports = { root: true, env: { browser: true, es2020: true }, - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:react-hooks/recommended', - ], + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended'], ignorePatterns: ['dist', '.eslintrc.cjs'], parser: '@typescript-eslint/parser', plugins: ['react-refresh'], rules: { - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - "@typescript-eslint/no-explicit-any": "off" + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-expressions': 'off', }, -} +}; diff --git a/packages/typink/src/networks/mainnet.ts b/packages/typink/src/networks/mainnet.ts index 7aaafd2d..4d6aaee7 100644 --- a/packages/typink/src/networks/mainnet.ts +++ b/packages/typink/src/networks/mainnet.ts @@ -11,6 +11,7 @@ export const alephZero: NetworkInfo = { decimals: 12, jsonRpcApi: JsonRpcApi.LEGACY, subscanUrl: 'https://alephzero.subscan.io', + genesisHash: '0x70255b4d28de0fc4e1a193d7e175ad1ccef431598211c55538f1018651a0307e', }; export const astar: NetworkInfo = { @@ -22,6 +23,7 @@ export const astar: NetworkInfo = { symbol: 'ASTR', decimals: 18, subscanUrl: 'https://astar.subscan.io', + genesisHash: '0x9eb76c5184c4ab8679d2d5d819fdf90b9c001403e9e17da2e14b6d8aec4029c6', }; export const shiden: NetworkInfo = { @@ -33,6 +35,7 @@ export const shiden: NetworkInfo = { symbol: 'SDN', decimals: 18, subscanUrl: 'https://shiden.subscan.io', + genesisHash: '0xf1cf9022c7ebb34b162d5b5e34e705a5a740b2d0ecc1009fb89023e62a488108', }; export const polkadot: NetworkInfo = { @@ -60,6 +63,7 @@ export const polkadot: NetworkInfo = { chainSpec: async () => { return (await import('@substrate/connect-known-chains/polkadot')).chainSpec; }, + genesisHash: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3', }; export const polkadotAssetHub: NetworkInfo = { @@ -85,6 +89,7 @@ export const polkadotAssetHub: NetworkInfo = { return (await import('@substrate/connect-known-chains/polkadot_asset_hub')).chainSpec; }, relayChain: polkadot, + genesisHash: '0x68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f', }; export const polkadotPeople: NetworkInfo = { @@ -106,6 +111,7 @@ export const polkadotPeople: NetworkInfo = { return (await import('@substrate/connect-known-chains/polkadot_people')).chainSpec; }, relayChain: polkadot, + genesisHash: '0x67fa177a097bfa18f77ea95ab56e9bcdfeb0e5b8a40e46298bb93e16b6fc5008', }; export const kusama: NetworkInfo = { @@ -130,6 +136,7 @@ export const kusama: NetworkInfo = { chainSpec: async () => { return (await import('@substrate/connect-known-chains/ksmcc3')).chainSpec; }, + genesisHash: '0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe', }; export const kusamaAssetHub: NetworkInfo = { @@ -154,6 +161,7 @@ export const kusamaAssetHub: NetworkInfo = { return (await import('@substrate/connect-known-chains/ksmcc3_asset_hub')).chainSpec; }, relayChain: kusama, + genesisHash: '0x48239ef607d7928874027a43a67689209727dfb3d3dc5e5b03a39bdc2eda771a', }; export const kusamaPeople: NetworkInfo = { @@ -178,6 +186,7 @@ export const kusamaPeople: NetworkInfo = { return (await import('@substrate/connect-known-chains/ksmcc3_people')).chainSpec; }, relayChain: kusama, + genesisHash: '0xc1af4cb4eb3918e5db15086c0cc5ec17fb334f728b7c65dd44bfe1e174ff8b3f', }; export const hydration: NetworkInfo = { @@ -195,6 +204,7 @@ export const hydration: NetworkInfo = { symbol: 'HDX', decimals: 12, subscanUrl: 'https://hydration.subscan.io', + genesisHash: '0xafdc188f45c71dacbaa0b62e16a91f726c7b8699a9748cdf715459de6b7f366d', }; export const basilisk: NetworkInfo = { @@ -206,6 +216,7 @@ export const basilisk: NetworkInfo = { symbol: 'BSX', decimals: 12, subscanUrl: 'https://basilisk.subscan.io', + genesisHash: '0xa85cfb9b9fd4d622a5b28289a02347af987d8f73fa3108450e2b4a11c1ce5755', }; export const vara: NetworkInfo = { @@ -217,4 +228,5 @@ export const vara: NetworkInfo = { symbol: 'VARA', decimals: 12, subscanUrl: 'https://vara.subscan.io', + genesisHash: '0xfe1b4c55fd4d668101126434206571a7838a8b6b93a6d1b95d607e78e6c53763', }; diff --git a/packages/typink/src/networks/testnet.ts b/packages/typink/src/networks/testnet.ts index 5568c57f..266d7f82 100644 --- a/packages/typink/src/networks/testnet.ts +++ b/packages/typink/src/networks/testnet.ts @@ -11,6 +11,7 @@ export const popTestnet: NetworkInfo = { decimals: 10, faucetUrl: 'https://onboard.popnetwork.xyz', pjsUrl: 'https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Frpc1.paseo.popnetwork.xyz', + genesisHash: '0xe8b2d197b82a0da1fffca832c050894ebe343b289c61ef439aa694bdcef78aa1', }; export const alephZeroTestnet: NetworkInfo = { @@ -36,6 +37,7 @@ export const shibuyaTestnet: NetworkInfo = { decimals: 18, faucetUrl: 'https://docs.astar.network/docs/build/environment/faucet', subscanUrl: 'https://shibuya.subscan.io', + genesisHash: '0xddb89973361a170839f80f152d2e9e38a376a5a7eccefcade763f46a8e567019', }; export const westend: NetworkInfo = { @@ -51,6 +53,7 @@ export const westend: NetworkInfo = { chainSpec: async () => { return (await import('@substrate/connect-known-chains/westend2')).chainSpec; }, + genesisHash: '0xe143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e', }; export const westendAssetHub: NetworkInfo = { @@ -67,6 +70,7 @@ export const westendAssetHub: NetworkInfo = { return (await import('@substrate/connect-known-chains/westend2_asset_hub')).chainSpec; }, relayChain: westend, + genesisHash: '0x67f9723393ef76214df0118c34bbbd3dbebc8ed46a10973a8c969d48fe7598c9', }; export const westendPeople: NetworkInfo = { @@ -83,6 +87,7 @@ export const westendPeople: NetworkInfo = { return (await import('@substrate/connect-known-chains/westend_people')).chainSpec; }, relayChain: westend, + genesisHash: '0x1eb6fb0ba5187434de017a70cb84d4f47142df1d571d0ef9e7e1407f2b80b93c', }; export const paseo: NetworkInfo = { @@ -103,6 +108,7 @@ export const paseo: NetworkInfo = { chainSpec: async () => { return (await import('@substrate/connect-known-chains/paseo')).chainSpec; }, + genesisHash: '0x77afd6190f1554ad45fd0d31aee62aacc33c6db0ea801129acb813f913e0764f', }; export const paseoPeople: NetworkInfo = { @@ -118,6 +124,7 @@ export const paseoPeople: NetworkInfo = { decimals: 10, faucetUrl: 'https://faucet.polkadot.io', subscanUrl: 'https://people-paseo.subscan.io', + genesisHash: '0xe6c30d6e148f250b887105237bcaa5cb9f16dd203bf7b5b9d4f1da7387cb86ec', }; export const paseoAssetHub: NetworkInfo = { @@ -136,6 +143,7 @@ export const paseoAssetHub: NetworkInfo = { decimals: 10, faucetUrl: 'https://faucet.polkadot.io', subscanUrl: 'https://assethub-paseo.subscan.io', + genesisHash: '0xd6eec26135305a8ad257a20d003357284c8aa03d0bdb2b357ab0a22371e11ef2', }; export const passetHub: NetworkInfo = { @@ -150,6 +158,7 @@ export const passetHub: NetworkInfo = { symbol: 'PAS', decimals: 10, faucetUrl: 'https://faucet.polkadot.io/?parachain=1111', + genesisHash: '0xfd974cf9eaf028f5e44b9fdd1949ab039c6cf9cc54449b0b60d71b042e79aeb6', }; export const paseoHydration: NetworkInfo = { @@ -161,4 +170,5 @@ export const paseoHydration: NetworkInfo = { symbol: 'HDX', decimals: 12, faucetUrl: 'https://faucet.polkadot.io', + genesisHash: '0x05fab032a899566268235e3c89c847fb4644558a0d856282d47d17ff06cb2020', }; diff --git a/packages/typink/src/types.ts b/packages/typink/src/types.ts index 047e66d5..f78ada03 100644 --- a/packages/typink/src/types.ts +++ b/packages/typink/src/types.ts @@ -79,6 +79,7 @@ export interface NetworkInfo { pjsUrl?: string; faucetUrl?: string; jsonRpcApi?: JsonRpcApi; // default to new + genesisHash?: string; chainSpec?: () => Promise; relayChain?: NetworkInfo; } diff --git a/packages/typink/src/utils/chains.ts b/packages/typink/src/utils/chains.ts index bae8c71f..01ead8da 100644 --- a/packages/typink/src/utils/chains.ts +++ b/packages/typink/src/utils/chains.ts @@ -1,51 +1,11 @@ import { NetworkInfo } from '../types.js'; -/** - * Known chain genesis hashes for WalletConnect - * Format: "polkadot:{genesisHash}" - */ -const CHAIN_GENESIS_HASHES: Record = { - // Mainnet chains - polkadot: '91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3', - polkadot_asset_hub: '68d56f15f85d3136970ec16946040bc1752654e906147f7e43e9d539d7c3de2f', - kusama: 'b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe', - kusama_asset_hub: '48239ef607d7928874027a43a67689209727dfb3d3dc5e5b03a39bdc2eda771a', - alephzero: '70255b4d28de0fc4e1a193d7e175ad1ccef431598211c55538f1018651a0307e', - astar: '9eb76c5184c4ab8679d2d5d819fdf90b9c001403e9e17da2e14b6d8aec4029c6', - shiden: 'f1cf9022c7ebb34b162d5b5e34e705a5a740b2d0ecc1009fb89023e62a488108', - hydration: 'afdc188f45c71dacbaa0b62e16a91f726c7b8699a9748cdf715459de6b7f366d', - basilisk: 'a85cfb9b9fd4d622a5b28289a02347af987d8f73fa3108450e2b4a11c1ce5755', - - // Testnet chains - westend: 'e143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e', - westend_asset_hub: '67f9723393ef76214df0118c34bbbd3dbebc8ed46a10973a8c969d48fe7598c9', - // pop_testnet: Use westend for now since POP is a parachain on Paseo/Westend - pop_testnet: 'e143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e', - alephzero_testnet: '05d5279c52c484cc80396535a316add7d47b1c5b9e0398dd1f584149341460c5', -}; - -/** - * Convert a NetworkInfo to a WalletConnect chain identifier - * @param network - The network info from typink - * @returns WalletConnect chain identifier in format "polkadot:{genesisHash}" or undefined if not found - */ -export function networkInfoToWalletConnectChain(network: NetworkInfo): string | undefined { - const genesisHash = CHAIN_GENESIS_HASHES[network.id]; - - if (!genesisHash) { - console.warn(`No genesis hash mapping found for network: ${network.id}`); - return undefined; - } - - return genesisHashToWalletConnectChain(genesisHash); -} - /** * Convert a genesis hash to a WalletConnect chain identifier * @param genesisHash - The genesis hash of the network * @returns WalletConnect chain identifier in format "polkadot:{genesisHash}" */ -export function genesisHashToWalletConnectChain(genesisHash: string): string { +export function genesisHashToCaipId(genesisHash: string): string { return `polkadot:${genesisHash.replace('0x', '').slice(0, 32)}`; } @@ -54,6 +14,6 @@ export function genesisHashToWalletConnectChain(genesisHash: string): string { * @param networks - Array of network info from typink * @returns Array of WalletConnect chain identifiers */ -export function getWalletConnectChains(networks: NetworkInfo[]): string[] { - return networks.map(networkInfoToWalletConnectChain).filter((chain): chain is string => chain !== undefined); +export function convertNetworkInfoToCaipId(networks: NetworkInfo[]): string[] { + return networks.filter((o) => !!o.genesisHash).map((o) => genesisHashToCaipId(o.genesisHash!)); } diff --git a/packages/typink/src/wallets/WalletConnect.ts b/packages/typink/src/wallets/WalletConnect.ts index a8d6100d..ec2856b4 100644 --- a/packages/typink/src/wallets/WalletConnect.ts +++ b/packages/typink/src/wallets/WalletConnect.ts @@ -13,7 +13,8 @@ import { import { NetworkInfo } from '../types.js'; import { WalletConnectModal } from '@walletconnect/modal'; import { assert, DedotError } from 'dedot/utils'; -import { genesisHashToWalletConnectChain, getWalletConnectChains } from 'src/utils/chains.js'; +import { genesisHashToCaipId, convertNetworkInfoToCaipId } from 'src/utils/chains.js'; +import { polkadot } from '../networks/index.js'; export interface WalletConnectOptions extends WalletOptions { projectId: string; @@ -74,18 +75,16 @@ export class WalletConnect extends Wallet { const { UniversalProvider } = await import('@walletconnect/universal-provider'); try { - if (!this.#provider) { - this.#provider = await UniversalProvider.init({ - projectId, - relayUrl, - metadata, - }); - } + this.#provider = await UniversalProvider.init({ + projectId, + relayUrl, + metadata, + }); if (this.#provider.session) { this.#accounts = this.#getAccounts(); } else { - const chains = getWalletConnectChains(this.#supportedNetworks); + const chains = convertNetworkInfoToCaipId(this.#supportedNetworks); const { uri, approval } = await this.#provider.client!.connect({ requiredNamespaces: { @@ -123,11 +122,9 @@ export class WalletConnect extends Wallet { this.#accounts = this.#getAccounts(); } - if (this.#provider.session && this.#provider.client) { - this.#subscribeToSessionEvents(); - this.#notifyAccountSubscribers(this.#accounts); - this.#createInjectedProvider(); - } + this.#subscribeToSessionEvents(); + this.#notifyAccountSubscribers(this.#accounts); + this.#createInjectedProvider(); } catch (error) { throw new DedotError(`Failed to initialize WalletConnect: ${(error as Error).message}`); } @@ -208,12 +205,10 @@ export class WalletConnect extends Wallet { signPayload: async (payload: SignerPayloadJSON): Promise => { assert(this.#provider?.client && this.#provider?.session, 'Provider client or session not initialized'); - console.log('Signing message with WalletConnect:', payload); - try { const result = (await this.#provider.client.request({ topic: this.#provider.session.topic, - chainId: genesisHashToWalletConnectChain(payload.genesisHash), + chainId: genesisHashToCaipId(payload.genesisHash), request: { method: 'polkadot_signTransaction', params: { @@ -238,10 +233,7 @@ export class WalletConnect extends Wallet { try { // Use the first chain from the session const sessionChains = this.#provider.session.namespaces.polkadot?.chains || []; - const chainId = - sessionChains[0] || 'polkadot:91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3'; - - console.log('Signing raw message with WalletConnect:', raw); + const chainId = sessionChains[0] || genesisHashToCaipId(polkadot.genesisHash!); const result = (await this.#provider.client.request({ topic: this.#provider.session.topic, From be0590a9eee0e551500b02cd5b73f7f9cc150357 Mon Sep 17 00:00:00 2001 From: 1cedrus Date: Fri, 17 Oct 2025 16:20:07 +0700 Subject: [PATCH 3/9] Refactor code --- assets/wallets/wallet-connect-logo.svg | 2 +- examples/demo-inkv5/src/main.tsx | 2 +- packages/typink/src/wallets/WalletConnect.ts | 108 +++++++++---------- 3 files changed, 56 insertions(+), 56 deletions(-) diff --git a/assets/wallets/wallet-connect-logo.svg b/assets/wallets/wallet-connect-logo.svg index 0af59717..1c1d9629 100644 --- a/assets/wallets/wallet-connect-logo.svg +++ b/assets/wallets/wallet-connect-logo.svg @@ -1,5 +1,5 @@ +> diff --git a/examples/demo-inkv5/src/main.tsx b/examples/demo-inkv5/src/main.tsx index 37dbdebc..767c72fe 100644 --- a/examples/demo-inkv5/src/main.tsx +++ b/examples/demo-inkv5/src/main.tsx @@ -29,7 +29,7 @@ const SUPPORTED_NETWORK = [alephZero]; export const walletConnect = new WalletConnect({ name: 'WalletConnect', id: 'walletconnect', - logo: 'https://raw.githubusercontent.com/Luno-lab/LunoKit/be5c713a42e099a6825e73c94c02c01de1a78a41/packages/core/src/config/logos/wallets/walletconnect.svg', + logo: 'https://github.com/dedotdev/typink/blob/42c6f87eb40087c082dec2ee174e337d2b8812d9/assets/wallets/wallet-connect-logo.svg', projectId: 'b56e18d47c72ab683b10814fe9495694', // Default relayUrl: 'wss://relay.walletconnect.com', metadata: { diff --git a/packages/typink/src/wallets/WalletConnect.ts b/packages/typink/src/wallets/WalletConnect.ts index ec2856b4..2c0fb468 100644 --- a/packages/typink/src/wallets/WalletConnect.ts +++ b/packages/typink/src/wallets/WalletConnect.ts @@ -11,8 +11,8 @@ import { SignerResult, } from '../pjs-types.js'; import { NetworkInfo } from '../types.js'; -import { WalletConnectModal } from '@walletconnect/modal'; -import { assert, DedotError } from 'dedot/utils'; +import type { WalletConnectModal } from '@walletconnect/modal'; +import { assert, DedotError, HexString } from 'dedot/utils'; import { genesisHashToCaipId, convertNetworkInfoToCaipId } from 'src/utils/chains.js'; import { polkadot } from '../networks/index.js'; @@ -60,27 +60,15 @@ export class WalletConnect extends Wallet { } async waitUntilReady(): Promise { - await this.#initializeModal(); await this.#initialize(); + await this.#setupModal(); + await this.#connect(); } - async #initialize(): Promise { - const { projectId, relayUrl, metadata } = this.options; - - assert( - this.options.projectId, - `${this.name} requires a projectId. Please visit https://cloud.walletconnect.com to get one.`, - ); - - const { UniversalProvider } = await import('@walletconnect/universal-provider'); + async #connect(): Promise { + assert(this.#provider, 'WalletConnect provider not initialized'); try { - this.#provider = await UniversalProvider.init({ - projectId, - relayUrl, - metadata, - }); - if (this.#provider.session) { this.#accounts = this.#getAccounts(); } else { @@ -99,38 +87,55 @@ export class WalletConnect extends Wallet { events: ['accountsChanged', 'chainChanged', 'connect'], }, }, - optionalNamespaces: { - polkadot: { - chains, - methods: [ - 'polkadot_signMessage', - 'polkadot_sendTransaction', - 'polkadot_signTransaction', - 'polkadot_requestAccounts', - ], - events: ['accountsChanged', 'chainChanged', 'connect'], - }, - }, }); if (uri) { await this.#modal!.openModal({ uri }); } - this.#provider.session = await approval(); + const race = new Promise((_, reject) => { + this.#modal!.subscribeModal((state: { open: boolean }) => { + !state.open && reject(new DedotError('User closed WalletConnect modal')); + }); + }); + + this.#provider.session = await Promise.race([approval(), race]); this.#modal!.closeModal(); this.#accounts = this.#getAccounts(); } this.#subscribeToSessionEvents(); this.#notifyAccountSubscribers(this.#accounts); - this.#createInjectedProvider(); - } catch (error) { - throw new DedotError(`Failed to initialize WalletConnect: ${(error as Error).message}`); + this.#injectedProvider = this.#getInjectedProvider(); + } catch (e) { + throw new DedotError(`Failed to connect WalletConnect: ${(e as Error).message}`); } } - async #initializeModal(): Promise { + async #initialize(): Promise { + if (this.#provider) return; + + const { projectId, relayUrl, metadata } = this.options; + + assert( + this.options.projectId, + `${this.name} requires a projectId. Please visit https://cloud.walletconnect.com to get one.`, + ); + + const { UniversalProvider } = await import('@walletconnect/universal-provider'); + + try { + this.#provider = await UniversalProvider.init({ + projectId, + relayUrl, + metadata, + }); + } catch (e) { + throw new DedotError(`Failed to initialize WalletConnect: ${(e as Error).message}`); + } + } + + async #setupModal(): Promise { if (this.#modal) return; try { @@ -147,19 +152,19 @@ export class WalletConnect extends Wallet { '--wcm-z-index': '9999', }, }); - } catch (error) { - throw new DedotError(`Failed to initialize WalletConnect Modal: ${(error as Error).message}`); + } catch (e) { + throw new DedotError(`Failed to initialize WalletConnect Modal: ${(e as Error).message}`); } } async disconnect(): Promise { + if (!this.#provider) return; + try { - if (this.#provider) { - await Promise.race([ - await this.#provider.disconnect(), - new Promise((_, reject) => setTimeout(() => reject(new Error('Disconnect timeout')), 5000)), - ]); - } + await Promise.race([ + await this.#provider.disconnect(), + new Promise((_, reject) => setTimeout(() => reject(new Error('Disconnect timeout')), 5000)), + ]); } catch (e) {} this.#cleanUp(); @@ -177,8 +182,6 @@ export class WalletConnect extends Wallet { const accounts: InjectedAccount[] = []; const polkadotNamespace = this.#provider.session.namespaces.polkadot; - // TODO: Handle multiple chains and genesisHashes - if (polkadotNamespace && polkadotNamespace.accounts) { // Get unique addresses across all chains const uniqueAddresses = new Set(); @@ -220,7 +223,7 @@ export class WalletConnect extends Wallet { return { id: 0, - signature: result.signature as `0x${string}`, + signature: result.signature as HexString, }; } catch (error) { throw new DedotError(`Failed to sign payload with WalletConnect: ${(error as Error).message}`); @@ -231,13 +234,9 @@ export class WalletConnect extends Wallet { assert(this.#provider?.client && this.#provider?.session, 'Provider client or session not initialized'); try { - // Use the first chain from the session - const sessionChains = this.#provider.session.namespaces.polkadot?.chains || []; - const chainId = sessionChains[0] || genesisHashToCaipId(polkadot.genesisHash!); - const result = (await this.#provider.client.request({ topic: this.#provider.session.topic, - chainId, + chainId: genesisHashToCaipId(polkadot.genesisHash!), request: { method: 'polkadot_signMessage', params: { @@ -250,7 +249,7 @@ export class WalletConnect extends Wallet { return { id: 0, - signature: result.signature as `0x${string}`, + signature: result.signature as HexString, }; } catch (error) { throw new DedotError(`Failed to sign raw message with WalletConnect: ${(error as Error).message}`); @@ -279,11 +278,11 @@ export class WalletConnect extends Wallet { }; } - #createInjectedProvider(): void { + #getInjectedProvider(): InjectedWindowProvider { const signer = this.#getSigner(); const accounts = this.#getInjectedAccounts(); - this.#injectedProvider = { + return { enable: async (): Promise<{ accounts: InjectedAccounts; signer: InjectedSigner }> => { if (!this.#provider?.session) { throw new Error('WalletConnect session not established'); @@ -294,6 +293,7 @@ export class WalletConnect extends Wallet { signer, }; }, + version: '1.0.0', }; } From aac3b60b37749ba650ae20d76c732d8046edec5e Mon Sep 17 00:00:00 2001 From: 1cedrus Date: Fri, 17 Oct 2025 16:26:33 +0700 Subject: [PATCH 4/9] Fix logo path --- examples/demo-inkv5/src/main.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/demo-inkv5/src/main.tsx b/examples/demo-inkv5/src/main.tsx index 767c72fe..6bf09484 100644 --- a/examples/demo-inkv5/src/main.tsx +++ b/examples/demo-inkv5/src/main.tsx @@ -29,7 +29,7 @@ const SUPPORTED_NETWORK = [alephZero]; export const walletConnect = new WalletConnect({ name: 'WalletConnect', id: 'walletconnect', - logo: 'https://github.com/dedotdev/typink/blob/42c6f87eb40087c082dec2ee174e337d2b8812d9/assets/wallets/wallet-connect-logo.svg', + logo: 'https://raw.githubusercontent.com/dedotdev/typink/feature/wallet-connect/assets/wallets/wallet-connect-logo.svg', projectId: 'b56e18d47c72ab683b10814fe9495694', // Default relayUrl: 'wss://relay.walletconnect.com', metadata: { From 570f39a1e89b05cf2c91eddf0c62110da98ef233 Mon Sep 17 00:00:00 2001 From: 1cedrus Date: Fri, 17 Oct 2025 16:31:24 +0700 Subject: [PATCH 5/9] Correct import path in WalletConnect and update mock atom in tests --- .../typink/src/providers/__tests__/WalletSetupProvider.test.tsx | 1 + packages/typink/src/wallets/WalletConnect.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/typink/src/providers/__tests__/WalletSetupProvider.test.tsx b/packages/typink/src/providers/__tests__/WalletSetupProvider.test.tsx index 42a143b0..1d43a81e 100644 --- a/packages/typink/src/providers/__tests__/WalletSetupProvider.test.tsx +++ b/packages/typink/src/providers/__tests__/WalletSetupProvider.test.tsx @@ -17,6 +17,7 @@ vi.mock('jotai', () => ({ useAtomValue: (atom: any) => mockUseAtomValue(atom), useSetAtom: (atom: any) => mockUseSetAtom(atom), createStore: vi.fn(() => ({})), + atom: (initialValue: any) => initialValue, Provider: ({ children }: any) => children, })); diff --git a/packages/typink/src/wallets/WalletConnect.ts b/packages/typink/src/wallets/WalletConnect.ts index 2c0fb468..c4a5bb16 100644 --- a/packages/typink/src/wallets/WalletConnect.ts +++ b/packages/typink/src/wallets/WalletConnect.ts @@ -13,7 +13,7 @@ import { import { NetworkInfo } from '../types.js'; import type { WalletConnectModal } from '@walletconnect/modal'; import { assert, DedotError, HexString } from 'dedot/utils'; -import { genesisHashToCaipId, convertNetworkInfoToCaipId } from 'src/utils/chains.js'; +import { genesisHashToCaipId, convertNetworkInfoToCaipId } from '../utils/chains.js'; import { polkadot } from '../networks/index.js'; export interface WalletConnectOptions extends WalletOptions { From d17d83b3da3fdc57d39ca8b1750f0861dfaf74ba Mon Sep 17 00:00:00 2001 From: 1cedrus Date: Sat, 18 Oct 2025 14:38:53 +0700 Subject: [PATCH 6/9] Refactor code and add unit-tests --- examples/demo-inkv5/src/main.tsx | 16 +- .../typink/src/utils/__tests__/chains.test.ts | 121 ++++ packages/typink/src/wallets/WalletConnect.ts | 7 +- .../wallets/__tests__/WalletConnect.test.ts | 521 ++++++++++++++++++ packages/typink/src/wallets/index.ts | 15 + 5 files changed, 662 insertions(+), 18 deletions(-) create mode 100644 packages/typink/src/utils/__tests__/chains.test.ts create mode 100644 packages/typink/src/wallets/__tests__/WalletConnect.test.ts diff --git a/examples/demo-inkv5/src/main.tsx b/examples/demo-inkv5/src/main.tsx index 6bf09484..18d3ce3a 100644 --- a/examples/demo-inkv5/src/main.tsx +++ b/examples/demo-inkv5/src/main.tsx @@ -14,7 +14,7 @@ import { subwallet, talisman, TypinkProvider, - WalletConnect, + walletConnect, } from 'typink'; import { toast } from 'react-toastify'; @@ -26,20 +26,6 @@ const SUPPORTED_NETWORK = [alephZero]; // SUPPORTED_NETWORK.push(development); // } -export const walletConnect = new WalletConnect({ - name: 'WalletConnect', - id: 'walletconnect', - logo: 'https://raw.githubusercontent.com/dedotdev/typink/feature/wallet-connect/assets/wallets/wallet-connect-logo.svg', - projectId: 'b56e18d47c72ab683b10814fe9495694', // Default - relayUrl: 'wss://relay.walletconnect.com', - metadata: { - name: 'Typink Dapp', - description: 'Typink powered dApp', - url: typeof window !== 'undefined' ? window.location.origin : 'https://typink.dev', - icons: ['https://raw.githubusercontent.com/dedotdev/typink/main/assets/typink/typink-pink-logo.png'], - }, -}); - const enkrypt = new ExtensionWallet({ name: 'Enkrypt', id: 'enkrypt', diff --git a/packages/typink/src/utils/__tests__/chains.test.ts b/packages/typink/src/utils/__tests__/chains.test.ts new file mode 100644 index 00000000..f99d465b --- /dev/null +++ b/packages/typink/src/utils/__tests__/chains.test.ts @@ -0,0 +1,121 @@ +import { describe, expect, it } from 'vitest'; +import { genesisHashToCaipId, convertNetworkInfoToCaipId } from '../chains.js'; +import { NetworkInfo, NetworkType } from '../../types.js'; + +describe('chains utilities', () => { + describe('genesisHashToCaipId', () => { + it('should convert genesis hash to CAIP-2 format', () => { + const genesisHash = '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3'; + const result = genesisHashToCaipId(genesisHash); + + expect(result).toBe('polkadot:91b171bb158e2d3848fa23a9f1c25182'); + }); + + it('should remove 0x prefix and take first 32 characters', () => { + const genesisHash = '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'; + const result = genesisHashToCaipId(genesisHash); + + expect(result).toBe('polkadot:abcdef1234567890abcdef1234567890'); + expect(result.split(':')[1]).toHaveLength(32); + }); + + it('should handle genesis hash without 0x prefix', () => { + const genesisHash = '91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3'; + const result = genesisHashToCaipId(genesisHash); + + expect(result).toBe('polkadot:91b171bb158e2d3848fa23a9f1c25182'); + }); + + it('should handle short genesis hash', () => { + const genesisHash = '0xabcd1234'; + const result = genesisHashToCaipId(genesisHash); + + expect(result).toBe('polkadot:abcd1234'); + }); + }); + + describe('convertNetworkInfoToCaipId', () => { + it('should convert multiple networks to CAIP-2 identifiers', () => { + const networks: NetworkInfo[] = [ + { + id: 'polkadot', + type: NetworkType.MAINNET, + name: 'Polkadot', + logo: 'polkadot.png', + providers: ['wss://rpc.polkadot.io'], + symbol: 'DOT', + decimals: 10, + genesisHash: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3', + }, + { + id: 'kusama', + type: NetworkType.MAINNET, + name: 'Kusama', + logo: 'kusama.png', + providers: ['wss://kusama-rpc.polkadot.io'], + symbol: 'KSM', + decimals: 12, + genesisHash: '0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe', + }, + ]; + + const result = convertNetworkInfoToCaipId(networks); + + expect(result).toHaveLength(2); + expect(result[0]).toBe('polkadot:91b171bb158e2d3848fa23a9f1c25182'); + expect(result[1]).toBe('polkadot:b0a8d493285c2df73290dfb7e61f870f'); + }); + + it('should filter out networks without genesis hash', () => { + const networks: NetworkInfo[] = [ + { + id: 'polkadot', + type: NetworkType.MAINNET, + name: 'Polkadot', + logo: 'polkadot.png', + providers: ['wss://rpc.polkadot.io'], + symbol: 'DOT', + decimals: 10, + genesisHash: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3', + }, + { + id: 'custom', + name: 'Custom Network', + logo: 'custom.png', + providers: ['wss://custom.io'], + symbol: 'CUST', + decimals: 10, + // No genesisHash + }, + ]; + + const result = convertNetworkInfoToCaipId(networks); + + expect(result).toHaveLength(1); + expect(result[0]).toBe('polkadot:91b171bb158e2d3848fa23a9f1c25182'); + }); + + it('should return empty array for networks without genesis hash', () => { + const networks: NetworkInfo[] = [ + { + id: 'custom', + name: 'Custom Network', + logo: 'custom.png', + providers: ['wss://custom.io'], + symbol: 'CUST', + decimals: 10, + }, + ]; + + const result = convertNetworkInfoToCaipId(networks); + + expect(result).toHaveLength(0); + }); + + it('should return empty array for empty input', () => { + const result = convertNetworkInfoToCaipId([]); + + expect(result).toHaveLength(0); + }); + }); +}); diff --git a/packages/typink/src/wallets/WalletConnect.ts b/packages/typink/src/wallets/WalletConnect.ts index c4a5bb16..dd064b00 100644 --- a/packages/typink/src/wallets/WalletConnect.ts +++ b/packages/typink/src/wallets/WalletConnect.ts @@ -67,6 +67,7 @@ export class WalletConnect extends Wallet { async #connect(): Promise { assert(this.#provider, 'WalletConnect provider not initialized'); + assert(this.#modal, 'WalletConnect modal not initialized'); try { if (this.#provider.session) { @@ -93,13 +94,13 @@ export class WalletConnect extends Wallet { await this.#modal!.openModal({ uri }); } - const race = new Promise((_, reject) => { + const userCloseModal = new Promise((_, reject) => { this.#modal!.subscribeModal((state: { open: boolean }) => { !state.open && reject(new DedotError('User closed WalletConnect modal')); }); }); - this.#provider.session = await Promise.race([approval(), race]); + this.#provider.session = await Promise.race([approval(), userCloseModal]); this.#modal!.closeModal(); this.#accounts = this.#getAccounts(); } @@ -283,7 +284,7 @@ export class WalletConnect extends Wallet { const accounts = this.#getInjectedAccounts(); return { - enable: async (): Promise<{ accounts: InjectedAccounts; signer: InjectedSigner }> => { + enable: async (_origin: string): Promise<{ accounts: InjectedAccounts; signer: InjectedSigner }> => { if (!this.#provider?.session) { throw new Error('WalletConnect session not established'); } diff --git a/packages/typink/src/wallets/__tests__/WalletConnect.test.ts b/packages/typink/src/wallets/__tests__/WalletConnect.test.ts new file mode 100644 index 00000000..6e4881bc --- /dev/null +++ b/packages/typink/src/wallets/__tests__/WalletConnect.test.ts @@ -0,0 +1,521 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { WalletConnect, WalletConnectOptions } from '../WalletConnect.js'; +import type { IUniversalProvider } from '@walletconnect/universal-provider'; +import type { WalletConnectModal } from '@walletconnect/modal'; +import type { SignClientTypes } from '@walletconnect/types'; +import { SignerPayloadJSON, SignerPayloadRaw } from '../../pjs-types.js'; +import { NetworkInfo, NetworkType } from '../../types.js'; + +// Mock the dependencies +vi.mock('@walletconnect/universal-provider', () => ({ + UniversalProvider: { + init: vi.fn(), + }, +})); + +vi.mock('@walletconnect/modal', () => ({ + WalletConnectModal: vi.fn(), +})); + +describe('WalletConnect', () => { + let walletConnect: WalletConnect; + let mockOptions: WalletConnectOptions; + let mockProvider: Partial; + let mockModal: Partial; + let mockClient: any; + + const mockNetworks: NetworkInfo[] = [ + { + id: 'polkadot', + type: NetworkType.MAINNET, + name: 'Polkadot', + logo: 'polkadot.png', + providers: ['wss://rpc.polkadot.io'], + symbol: 'DOT', + decimals: 10, + genesisHash: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3', + }, + { + id: 'kusama', + type: NetworkType.MAINNET, + name: 'Kusama', + logo: 'kusama.png', + providers: ['wss://kusama-rpc.polkadot.io'], + symbol: 'KSM', + decimals: 12, + genesisHash: '0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe', + }, + ]; + + beforeEach(() => { + vi.clearAllMocks(); + + mockOptions = { + id: 'walletconnect', + name: 'WalletConnect', + logo: 'walletconnect.svg', + projectId: 'test-project-id', + metadata: { + name: 'Test DApp', + description: 'Test DApp Description', + url: 'https://test.com', + icons: ['https://test.com/icon.png'], + }, + relayUrl: 'wss://relay.walletconnect.com', + }; + + // Mock client + mockClient = { + connect: vi.fn(), + request: vi.fn(), + on: vi.fn(), + session: { + get: vi.fn(), + }, + }; + + // Mock provider + mockProvider = { + client: mockClient, + session: undefined, + disconnect: vi.fn().mockResolvedValue(undefined), + }; + + // Mock modal + mockModal = { + openModal: vi.fn().mockResolvedValue(undefined), + closeModal: vi.fn(), + subscribeModal: vi.fn(), + setTheme: vi.fn(), + }; + + walletConnect = new WalletConnect(mockOptions); + }); + + describe('Basic Properties', () => { + it('should always be ready and installed', () => { + expect(walletConnect.ready).toBe(true); + expect(walletConnect.installed).toBe(true); + }); + }); + + describe('Initialization and Connection', () => { + it('should initialize and connect with existing session', async () => { + const { UniversalProvider } = await import('@walletconnect/universal-provider'); + const { WalletConnectModal: WCModal } = await import('@walletconnect/modal'); + + vi.mocked(UniversalProvider.init).mockResolvedValue(mockProvider as any); + vi.mocked(WCModal).mockImplementation(() => mockModal as WalletConnectModal); + + mockProvider.session = { + topic: 'test-topic', + namespaces: { + polkadot: { + accounts: ['polkadot:91b171bb158e2d3848fa23a9f1c25182:5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'], + chains: ['polkadot:91b171bb158e2d3848fa23a9f1c25182'], + methods: ['polkadot_signTransaction'], + events: [], + }, + }, + } as any; + + walletConnect.setSupportedNetworks(mockNetworks); + await walletConnect.waitUntilReady(); + + expect(UniversalProvider.init).toHaveBeenCalledWith({ + projectId: mockOptions.projectId, + relayUrl: mockOptions.relayUrl, + metadata: mockOptions.metadata, + }); + expect(walletConnect.injectedProvider).toBeDefined(); + }); + + it('should establish new connection when no existing session', async () => { + const { UniversalProvider } = await import('@walletconnect/universal-provider'); + const { WalletConnectModal: WCModal } = await import('@walletconnect/modal'); + + vi.mocked(UniversalProvider.init).mockResolvedValue(mockProvider as any); + vi.mocked(WCModal).mockImplementation(() => mockModal as WalletConnectModal); + + const mockSession = { + topic: 'test-topic', + namespaces: { + polkadot: { + accounts: ['polkadot:91b171bb158e2d3848fa23a9f1c25182:5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'], + chains: ['polkadot:91b171bb158e2d3848fa23a9f1c25182'], + methods: ['polkadot_signTransaction'], + events: [], + }, + }, + }; + + mockClient.connect.mockResolvedValue({ + uri: 'wc:test-uri', + approval: vi.fn().mockResolvedValue(mockSession), + }); + + walletConnect.setSupportedNetworks(mockNetworks); + await walletConnect.waitUntilReady(); + + expect(mockClient.connect).toHaveBeenCalledWith({ + requiredNamespaces: { + polkadot: { + chains: expect.any(Array), + methods: [ + 'polkadot_signMessage', + 'polkadot_sendTransaction', + 'polkadot_signTransaction', + 'polkadot_requestAccounts', + ], + events: ['accountsChanged', 'chainChanged', 'connect'], + }, + }, + }); + expect(mockModal.openModal).toHaveBeenCalledWith({ uri: 'wc:test-uri' }); + expect(mockModal.closeModal).toHaveBeenCalled(); + }); + + it('should throw error when projectId is missing', async () => { + const invalidOptions = { ...mockOptions, projectId: '' }; + const invalidWallet = new WalletConnect(invalidOptions); + + await expect(invalidWallet.waitUntilReady()).rejects.toThrow(); + }); + + it('should throw error when provider initialization fails', async () => { + const { UniversalProvider } = await import('@walletconnect/universal-provider'); + vi.mocked(UniversalProvider.init).mockRejectedValue(new Error('Init failed')); + + await expect(walletConnect.waitUntilReady()).rejects.toThrow('Failed to initialize WalletConnect: Init failed'); + }); + + it('should handle user closing modal', async () => { + const { UniversalProvider } = await import('@walletconnect/universal-provider'); + const { WalletConnectModal: WCModal } = await import('@walletconnect/modal'); + + vi.mocked(UniversalProvider.init).mockResolvedValue(mockProvider as any); + + let modalCallback: (state: { open: boolean }) => void; + mockModal.subscribeModal = vi.fn().mockImplementation((cb) => { + modalCallback = cb; + }); + + vi.mocked(WCModal).mockImplementation(() => mockModal as WalletConnectModal); + + mockClient.connect.mockResolvedValue({ + uri: 'wc:test-uri', + approval: vi.fn().mockImplementation( + () => + new Promise((resolve) => { + setTimeout(() => resolve({ topic: 'test' }), 1000); + }), + ), + }); + + walletConnect.setSupportedNetworks(mockNetworks); + + const connectPromise = walletConnect.waitUntilReady(); + + await new Promise((resolve) => setTimeout(resolve, 10)); + modalCallback!({ open: false }); + + await expect(connectPromise).rejects.toThrow('Failed to connect WalletConnect: User closed WalletConnect modal'); + }); + }); + + describe('Disconnect', () => { + it('should disconnect and cleanup state', async () => { + const { UniversalProvider } = await import('@walletconnect/universal-provider'); + const { WalletConnectModal: WCModal } = await import('@walletconnect/modal'); + + vi.mocked(UniversalProvider.init).mockResolvedValue(mockProvider as any); + vi.mocked(WCModal).mockImplementation(() => mockModal as WalletConnectModal); + + mockProvider.session = { + topic: 'test-topic', + namespaces: { + polkadot: { + accounts: ['polkadot:91b171bb158e2d3848fa23a9f1c25182:5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'], + chains: [], + methods: [], + events: [], + }, + }, + } as any; + + walletConnect.setSupportedNetworks(mockNetworks); + await walletConnect.waitUntilReady(); + + await walletConnect.disconnect(); + + expect(mockProvider.disconnect).toHaveBeenCalled(); + expect(walletConnect.provider).toBeUndefined(); + expect(walletConnect.injectedProvider).toBeUndefined(); + }); + + it('should handle disconnect errors gracefully', async () => { + const { UniversalProvider } = await import('@walletconnect/universal-provider'); + const { WalletConnectModal: WCModal } = await import('@walletconnect/modal'); + + mockProvider.disconnect = vi.fn().mockRejectedValue(new Error('Disconnect failed')); + + vi.mocked(UniversalProvider.init).mockResolvedValue(mockProvider as any); + vi.mocked(WCModal).mockImplementation(() => mockModal as WalletConnectModal); + + mockProvider.session = { + topic: 'test-topic', + namespaces: { + polkadot: { + accounts: ['polkadot:91b171bb158e2d3848fa23a9f1c25182:5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'], + chains: [], + methods: [], + events: [], + }, + }, + } as any; + + walletConnect.setSupportedNetworks(mockNetworks); + await walletConnect.waitUntilReady(); + + await expect(walletConnect.disconnect()).resolves.toBeUndefined(); + expect(walletConnect.provider).toBeUndefined(); + }); + }); + + describe('InjectedProvider', () => { + beforeEach(async () => { + const { UniversalProvider } = await import('@walletconnect/universal-provider'); + const { WalletConnectModal: WCModal } = await import('@walletconnect/modal'); + + vi.mocked(UniversalProvider.init).mockResolvedValue(mockProvider as any); + vi.mocked(WCModal).mockImplementation(() => mockModal as WalletConnectModal); + + mockProvider.session = { + topic: 'test-topic', + namespaces: { + polkadot: { + accounts: [ + 'polkadot:91b171bb158e2d3848fa23a9f1c25182:5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', + 'polkadot:91b171bb158e2d3848fa23a9f1c25182:5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty', + ], + chains: ['polkadot:91b171bb158e2d3848fa23a9f1c25182'], + methods: ['polkadot_signTransaction'], + events: [], + }, + }, + } as any; + + walletConnect.setSupportedNetworks(mockNetworks); + await walletConnect.waitUntilReady(); + }); + + it('should provide enable method and version', async () => { + const injectedProvider = walletConnect.injectedProvider!; + + expect(injectedProvider.enable).toBeDefined(); + expect(typeof injectedProvider.enable).toBe('function'); + expect(injectedProvider.version).toBeDefined(); + }); + + it('should enable and return accounts and signer', async () => { + const injectedProvider = walletConnect.injectedProvider!; + const result = await injectedProvider.enable!('test-origin'); + + expect(result.accounts).toBeDefined(); + expect(result.signer).toBeDefined(); + + const accountList = await result.accounts.get(); + expect(accountList).toHaveLength(2); + expect(accountList[0].address).toBe('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'); + }); + + it('should support account subscription', async () => { + const injectedProvider = walletConnect.injectedProvider!; + const { accounts } = await injectedProvider.enable!('test-origin'); + + const callback = vi.fn(); + const unsubscribe = accounts.subscribe(callback); + + expect(callback).toHaveBeenCalled(); + expect(typeof unsubscribe).toBe('function'); + }); + + it('should sign transaction payload', async () => { + const injectedProvider = walletConnect.injectedProvider!; + const { signer } = await injectedProvider.enable!('test-origin'); + + const payload: SignerPayloadJSON = { + address: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', + blockHash: '0x1234', + blockNumber: '0x01', + era: '0x00', + genesisHash: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3', + method: '0xabcd', + nonce: '0x00', + specVersion: '0x01', + tip: '0x00', + transactionVersion: '0x01', + signedExtensions: [], + version: 4, + }; + + mockClient.request.mockResolvedValue({ signature: '0xmocksignature' }); + + const result = await signer.signPayload!(payload); + + expect(mockClient.request).toHaveBeenCalledWith({ + topic: 'test-topic', + chainId: expect.stringContaining('polkadot:'), + request: { + method: 'polkadot_signTransaction', + params: { + address: payload.address, + transactionPayload: payload, + }, + }, + }); + expect(result.signature).toBe('0xmocksignature'); + }); + + it('should sign raw message', async () => { + const injectedProvider = walletConnect.injectedProvider!; + const { signer } = await injectedProvider.enable!('test-origin'); + + const raw: SignerPayloadRaw = { + address: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', + data: '0x1234567890', + type: 'bytes', + }; + + mockClient.request.mockResolvedValue({ signature: '0xrawsignature' }); + + const result = await signer.signRaw!(raw); + + expect(mockClient.request).toHaveBeenCalledWith({ + topic: 'test-topic', + chainId: expect.stringContaining('polkadot:'), + request: { + method: 'polkadot_signMessage', + params: { + address: raw.address, + message: raw.data, + type: raw.type, + }, + }, + }); + expect(result.signature).toBe('0xrawsignature'); + }); + + it('should handle signing errors', async () => { + const injectedProvider = walletConnect.injectedProvider!; + const { signer } = await injectedProvider.enable!('test-origin'); + + const payload: SignerPayloadJSON = { + address: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', + blockHash: '0x1234', + blockNumber: '0x01', + era: '0x00', + genesisHash: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3', + method: '0xabcd', + nonce: '0x00', + specVersion: '0x01', + tip: '0x00', + transactionVersion: '0x01', + signedExtensions: [], + version: 4, + }; + + mockClient.request.mockRejectedValue(new Error('User rejected')); + + await expect(signer.signPayload!(payload)).rejects.toThrow( + 'Failed to sign payload with WalletConnect: User rejected', + ); + }); + }); + + describe('Session Events', () => { + let sessionUpdateCallback: (args: SignClientTypes.EventArguments['session_update']) => void; + let sessionDeleteCallback: () => void; + + beforeEach(async () => { + const { UniversalProvider } = await import('@walletconnect/universal-provider'); + const { WalletConnectModal: WCModal } = await import('@walletconnect/modal'); + + mockClient.on = vi.fn().mockImplementation((event: string, callback: any) => { + if (event === 'session_update') { + sessionUpdateCallback = callback; + } else if (event === 'session_delete') { + sessionDeleteCallback = callback; + } + }); + + vi.mocked(UniversalProvider.init).mockResolvedValue(mockProvider as any); + vi.mocked(WCModal).mockImplementation(() => mockModal as WalletConnectModal); + + mockProvider.session = { + topic: 'test-topic', + namespaces: { + polkadot: { + accounts: ['polkadot:91b171bb158e2d3848fa23a9f1c25182:5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'], + chains: ['polkadot:91b171bb158e2d3848fa23a9f1c25182'], + methods: ['polkadot_signTransaction'], + events: [], + }, + }, + } as any; + + walletConnect.setSupportedNetworks(mockNetworks); + await walletConnect.waitUntilReady(); + }); + + it('should handle session_update event', async () => { + const injectedProvider = walletConnect.injectedProvider!; + const { accounts } = await injectedProvider.enable!('test-origin'); + + const callback = vi.fn(); + accounts.subscribe(callback); + callback.mockClear(); + + const newNamespaces = { + polkadot: { + accounts: [ + 'polkadot:91b171bb158e2d3848fa23a9f1c25182:5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', + 'polkadot:91b171bb158e2d3848fa23a9f1c25182:5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty', + ], + chains: ['polkadot:91b171bb158e2d3848fa23a9f1c25182'], + methods: ['polkadot_signTransaction'], + events: [], + }, + }; + + mockClient.session.get.mockReturnValue({ + topic: 'test-topic', + namespaces: newNamespaces, + }); + + sessionUpdateCallback({ + id: 1, + topic: 'test-topic', + params: { namespaces: newNamespaces }, + }); + + expect(callback).toHaveBeenCalled(); + const calledAccounts = callback.mock.calls[0][0]; + expect(calledAccounts).toHaveLength(2); + }); + + it('should handle session_delete event', async () => { + const injectedProvider = walletConnect.injectedProvider!; + const { accounts } = await injectedProvider.enable!('test-origin'); + + const callback = vi.fn(); + accounts.subscribe(callback); + callback.mockClear(); + + sessionDeleteCallback(); + + expect(callback).toHaveBeenCalledWith([]); + expect(walletConnect.injectedProvider).toBeUndefined(); + }); + }); +}); diff --git a/packages/typink/src/wallets/index.ts b/packages/typink/src/wallets/index.ts index 028f326e..9d948ec7 100644 --- a/packages/typink/src/wallets/index.ts +++ b/packages/typink/src/wallets/index.ts @@ -1,4 +1,5 @@ import { ExtensionWallet } from './ExtensionWallet.js'; +import { WalletConnect } from './WalletConnect.js'; export * from './Wallet.js'; export * from './ExtensionWallet.js'; @@ -27,3 +28,17 @@ export const polkadotjs = new ExtensionWallet({ installUrl: 'https://polkadot.js.org/extension', websiteUrl: 'https://polkadot.js.org', }); + +export const walletConnect = new WalletConnect({ + name: 'WalletConnect', + id: 'walletconnect', + logo: 'https://raw.githubusercontent.com/dedotdev/typink/feature/wallet-connect/assets/wallets/wallet-connect-logo.svg', + projectId: 'b56e18d47c72ab683b10814fe9495694', // Default project ID, please create your own at https://cloud.walletconnect.com + relayUrl: 'wss://relay.walletconnect.com', + metadata: { + name: 'Typink Dapp', + description: 'Typink powered dApp', + url: typeof window !== 'undefined' ? window.location.origin : 'https://typink.dev', + icons: ['https://raw.githubusercontent.com/dedotdev/typink/main/assets/typink/typink-pink-logo.png'], + }, +}); From a7b7884539bd52542040e2a52a305f16e92ce848 Mon Sep 17 00:00:00 2001 From: 1cedrus Date: Sun, 19 Oct 2025 17:12:28 +0700 Subject: [PATCH 7/9] Refactor code --- packages/typink/package.json | 4 +-- packages/typink/src/wallets/WalletConnect.ts | 9 ++++-- yarn.lock | 30 ++++++++++++++++---- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/packages/typink/package.json b/packages/typink/package.json index a325fde7..821aa94a 100644 --- a/packages/typink/package.json +++ b/packages/typink/package.json @@ -20,8 +20,8 @@ "dependencies": { "@substrate/connect-known-chains": "^1.10.3", "@walletconnect/modal": "^2.7.0", - "@walletconnect/sign-client": "^2.22.4", - "@walletconnect/utils": "^2.22.4", + "@walletconnect/types": "^2.22.4", + "@walletconnect/universal-provider": "^2.22.4", "fast-deep-equal": "^3.1.3", "jotai": "^2.15.0" }, diff --git a/packages/typink/src/wallets/WalletConnect.ts b/packages/typink/src/wallets/WalletConnect.ts index dd064b00..8ad32f66 100644 --- a/packages/typink/src/wallets/WalletConnect.ts +++ b/packages/typink/src/wallets/WalletConnect.ts @@ -14,7 +14,6 @@ import { NetworkInfo } from '../types.js'; import type { WalletConnectModal } from '@walletconnect/modal'; import { assert, DedotError, HexString } from 'dedot/utils'; import { genesisHashToCaipId, convertNetworkInfoToCaipId } from '../utils/chains.js'; -import { polkadot } from '../networks/index.js'; export interface WalletConnectOptions extends WalletOptions { projectId: string; @@ -163,7 +162,7 @@ export class WalletConnect extends Wallet { try { await Promise.race([ - await this.#provider.disconnect(), + this.#provider.disconnect(), new Promise((_, reject) => setTimeout(() => reject(new Error('Disconnect timeout')), 5000)), ]); } catch (e) {} @@ -235,9 +234,13 @@ export class WalletConnect extends Wallet { assert(this.#provider?.client && this.#provider?.session, 'Provider client or session not initialized'); try { + // We choose the first one in the list because it is required to be one of the chain IDs + // that were requested during session creation, so we cannot make it to use a default one (e.g., polkadot mainnet) + const chainId = this.#provider.session.namespaces.polkadot.chains![0]; + const result = (await this.#provider.client.request({ topic: this.#provider.session.topic, - chainId: genesisHashToCaipId(polkadot.genesisHash!), + chainId, request: { method: 'polkadot_signMessage', params: { diff --git a/yarn.lock b/yarn.lock index db7e53da..4a2217d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6254,7 +6254,7 @@ __metadata: languageName: node linkType: hard -"@walletconnect/sign-client@npm:^2.22.4": +"@walletconnect/sign-client@npm:2.22.4": version: 2.22.4 resolution: "@walletconnect/sign-client@npm:2.22.4" dependencies: @@ -6294,7 +6294,7 @@ __metadata: languageName: node linkType: hard -"@walletconnect/types@npm:2.22.4": +"@walletconnect/types@npm:2.22.4, @walletconnect/types@npm:^2.22.4": version: 2.22.4 resolution: "@walletconnect/types@npm:2.22.4" dependencies: @@ -6328,6 +6328,26 @@ __metadata: languageName: node linkType: hard +"@walletconnect/universal-provider@npm:^2.22.4": + version: 2.22.4 + resolution: "@walletconnect/universal-provider@npm:2.22.4" + dependencies: + "@walletconnect/events": "npm:1.0.1" + "@walletconnect/jsonrpc-http-connection": "npm:1.0.8" + "@walletconnect/jsonrpc-provider": "npm:1.0.14" + "@walletconnect/jsonrpc-types": "npm:1.0.4" + "@walletconnect/jsonrpc-utils": "npm:1.0.8" + "@walletconnect/keyvaluestorage": "npm:1.1.1" + "@walletconnect/logger": "npm:3.0.0" + "@walletconnect/sign-client": "npm:2.22.4" + "@walletconnect/types": "npm:2.22.4" + "@walletconnect/utils": "npm:2.22.4" + es-toolkit: "npm:1.39.3" + events: "npm:3.3.0" + checksum: 10/5ed4a354015904289d8728f0cc189d57493d2af9221a8b06bbf20c6d644cecb08cab14060ce9d8129a3d4a38e4820a5e98ce8ae7736c6e051c65913222a2fbe8 + languageName: node + linkType: hard + "@walletconnect/utils@npm:2.17.2": version: 2.17.2 resolution: "@walletconnect/utils@npm:2.17.2" @@ -6356,7 +6376,7 @@ __metadata: languageName: node linkType: hard -"@walletconnect/utils@npm:2.22.4, @walletconnect/utils@npm:^2.22.4": +"@walletconnect/utils@npm:2.22.4": version: 2.22.4 resolution: "@walletconnect/utils@npm:2.22.4" dependencies: @@ -17000,8 +17020,8 @@ __metadata: "@types/react": "npm:^19.1.1" "@vitest/ui": "npm:^3.2.4" "@walletconnect/modal": "npm:^2.7.0" - "@walletconnect/sign-client": "npm:^2.22.4" - "@walletconnect/utils": "npm:^2.22.4" + "@walletconnect/types": "npm:^2.22.4" + "@walletconnect/universal-provider": "npm:^2.22.4" fast-deep-equal: "npm:^3.1.3" jotai: "npm:^2.15.0" jsdom: "npm:^25.0.1" From dc469dc91dcbdd3a4b51417ec25df27c044accb5 Mon Sep 17 00:00:00 2001 From: 1cedrus Date: Sun, 19 Oct 2025 17:16:03 +0700 Subject: [PATCH 8/9] use optionalNamespaces instead --- packages/typink/src/wallets/WalletConnect.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typink/src/wallets/WalletConnect.ts b/packages/typink/src/wallets/WalletConnect.ts index 8ad32f66..1e4208de 100644 --- a/packages/typink/src/wallets/WalletConnect.ts +++ b/packages/typink/src/wallets/WalletConnect.ts @@ -75,7 +75,7 @@ export class WalletConnect extends Wallet { const chains = convertNetworkInfoToCaipId(this.#supportedNetworks); const { uri, approval } = await this.#provider.client!.connect({ - requiredNamespaces: { + optionalNamespaces: { polkadot: { chains, methods: [ From 67e3f789648cf859b11dd2dc98d02f00db4ad01d Mon Sep 17 00:00:00 2001 From: 1cedrus Date: Sun, 19 Oct 2025 17:19:34 +0700 Subject: [PATCH 9/9] Fix tests --- packages/typink/src/wallets/__tests__/WalletConnect.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typink/src/wallets/__tests__/WalletConnect.test.ts b/packages/typink/src/wallets/__tests__/WalletConnect.test.ts index 6e4881bc..f51da389 100644 --- a/packages/typink/src/wallets/__tests__/WalletConnect.test.ts +++ b/packages/typink/src/wallets/__tests__/WalletConnect.test.ts @@ -158,7 +158,7 @@ describe('WalletConnect', () => { await walletConnect.waitUntilReady(); expect(mockClient.connect).toHaveBeenCalledWith({ - requiredNamespaces: { + optionalNamespaces: { polkadot: { chains: expect.any(Array), methods: [