UNPKG

proximity-wallet-connect

Version:

Wallet Connect package for NEAR Wallet Selector (Proximity fork with transaction fixes).

1,185 lines (1,001 loc) 37.8 kB
import type { KeyPair, providers } from "near-api-js"; import * as nearAPI from "near-api-js"; // @ts-ignore - BN.js doesn't have proper ESM types import BN from "bn.js"; // @ts-ignore - js-sha256 for browser-compatible SHA-256 import { sha256 } from "js-sha256"; import type { AccessKeyView } from "near-api-js/lib/providers/provider.js"; import type { SignClientTypes, SessionTypes } from "@walletconnect/types"; import type { WalletModuleFactory, WalletBehaviourFactory, BridgeWallet, Subscription, Transaction, WalletEvents, EventEmitterService, VerifiedOwner, Account, SignMessageParams, SignedMessage, Action, AddKeyPermission, } from "proximity-dex-core"; import { getActiveAccount } from "proximity-dex-core"; import WalletConnectClient from "./wallet-connect-client"; import icon from "./icon"; // 🚨 CRITICAL TEST: This should appear IMMEDIATELY when the module loads console.log('🚨🚨🚨 PROXIMITY-WALLET-CONNECT MODULE LOADED - VERSION: v19-IMPORTED-SHA256-2024-10-07 🚨🚨🚨'); console.log('🚨 If you see this message, the updated code from SOURCE is being loaded!'); console.log('🚨 Using near_signTransactions (Fireblocks signs, WE broadcast)'); console.log('🚨 Properly imported js-sha256 instead of require()'); // PROXIMITY FIX: Inline createAction using BN (not BigInt) for near-api-js@0.44.2 compatibility const { transactions, utils } = nearAPI; const getAccessKey = (permission: AddKeyPermission) => { if (permission === "FullAccess") { return transactions.fullAccessKey(); } const { receiverId, methodNames = [] } = permission; const allowance = permission.allowance ? (new BN(permission.allowance) as any) : undefined; return transactions.functionCallAccessKey(receiverId, methodNames, allowance as any); }; const createAction = (action: Action) => { switch (action.type) { case "CreateAccount": return transactions.createAccount(); case "DeployContract": { const { code } = action.params; return transactions.deployContract(code); } case "FunctionCall": { const { methodName, args, gas, deposit } = action.params; return transactions.functionCall( methodName, args, new BN(gas) as any, new BN(deposit) as any ); } case "Transfer": { const { deposit } = action.params; return transactions.transfer(new BN(deposit) as any); } case "Stake": { const { stake, publicKey } = action.params; return transactions.stake(new BN(stake) as any, utils.PublicKey.from(publicKey)); } case "AddKey": { const { publicKey, accessKey } = action.params; return transactions.addKey( utils.PublicKey.from(publicKey), getAccessKey(accessKey.permission) ); } case "DeleteKey": { const { publicKey } = action.params; return transactions.deleteKey(utils.PublicKey.from(publicKey)); } case "DeleteAccount": { const { beneficiaryId } = action.params; return transactions.deleteAccount(beneficiaryId); } default: throw new Error("Invalid action type"); } }; export interface WalletConnectParams { projectId: string; metadata: SignClientTypes.Metadata; relayUrl?: string; iconUrl?: string; chainId?: string; deprecated?: boolean; methods?: Array<string>; events?: Array<string>; } interface WalletConnectExtraOptions { chainId?: string; projectId: string; metadata: SignClientTypes.Metadata; relayUrl: string; methods?: Array<string>; events?: Array<string>; } interface LimitedAccessKeyPair { accountId: string; keyPair: KeyPair; } interface LimitedAccessAccount { accountId: string; publicKey: string; } interface WalletConnectAccount { accountId: string; publicKey: string; } interface WalletConnectState { client: WalletConnectClient; session: SessionTypes.Struct | null; keystore: nearAPI.keyStores.KeyStore; subscriptions: Array<Subscription>; } interface ConnectParams { state: WalletConnectState; chainId: string; qrCodeModal: boolean; projectId: string; methods?: Array<string>; events?: Array<string>; } const WC_METHODS = [ "near_signIn", "near_signOut", "near_getAccounts", "near_signTransaction", "near_signTransactions", "near_signAndSendTransaction", "near_signAndSendTransactions", // disabling these two due to WalletConnect not supporting it // see https://docs.reown.com/advanced/multichain/rpc-reference/near-rpc // "near_verifyOwner", // "near_signMessage", ]; const WC_EVENTS = ["chainChanged", "accountsChanged"]; const setupWalletConnectState = async ( id: string, params: WalletConnectExtraOptions, emitter: EventEmitterService<WalletEvents> ): Promise<WalletConnectState> => { const client = new WalletConnectClient(emitter); let session: SessionTypes.Struct | null = null; const keystore = new nearAPI.keyStores.BrowserLocalStorageKeyStore( window.localStorage, `near-wallet-selector:${id}:keystore:` ); await client.init({ projectId: params.projectId, metadata: params.metadata, relayUrl: params.relayUrl, }); if (client.session.length) { const lastKeyIndex = client.session.keys.length - 1; session = client.session.get(client.session.keys[lastKeyIndex]); } return { client, session, keystore, subscriptions: [], }; }; const connect = async ({ state, chainId, qrCodeModal, projectId, methods, events, }: ConnectParams) => { return await state.client.connect( { requiredNamespaces: { near: { chains: [chainId], methods: methods || WC_METHODS, events: events || WC_EVENTS, }, }, }, qrCodeModal, projectId, chainId ); }; const disconnect = async ({ state }: { state: WalletConnectState }) => { await state.client.disconnect({ topic: state.session!.topic, reason: { code: 5900, message: "User disconnected", }, }); }; const getSignatureData = (result: Uint8Array) => { if (result instanceof Uint8Array) { return result; } else if (Array.isArray(result)) { return new Uint8Array(result); } else if (typeof result === "object" && result !== null) { return new Uint8Array(Object.values(result)); } else { throw new Error("Unexpected result type from near_signTransaction"); } }; const WalletConnect: WalletBehaviourFactory< BridgeWallet, { params: WalletConnectExtraOptions } > = async ({ id, options, store, params, provider, emitter, logger, metadata, }) => { const _state = await setupWalletConnectState(id, params, emitter); const getChainId = () => { if (params.chainId) { return params.chainId; } const { networkId } = options.network; if (["mainnet", "testnet"].includes(networkId)) { return `near:${networkId}`; } throw new Error("Invalid chain id"); }; const getAccounts = async (): Promise<Array<Account>> => { const accounts = _state.session?.namespaces["near"].accounts || []; const newAccounts = []; for (let i = 0; i < accounts.length; i++) { const signer = new nearAPI.InMemorySigner(_state.keystore); const publicKey = await signer.getPublicKey( accounts[i].split(":")[2], options.network.networkId ); newAccounts.push({ accountId: accounts[i].split(":")[2], publicKey: publicKey ? publicKey.toString() : "", }); } return newAccounts; }; const cleanup = async () => { _state.subscriptions.forEach((subscription) => subscription.remove()); _state.subscriptions = []; _state.session = null; }; const validateAccessKey = ( transaction: Transaction, accessKey: AccessKeyView ) => { if (accessKey.permission === "FullAccess") { return accessKey; } // eslint-disable-next-line @typescript-eslint/naming-convention const { receiver_id, method_names } = accessKey.permission.FunctionCall; if (transaction.receiverId !== receiver_id) { return null; } return transaction.actions.every((action) => { if (action.type !== "FunctionCall") { return false; } const { methodName, deposit } = action.params; if (method_names.length && method_names.includes(methodName)) { return false; } return parseFloat(deposit) <= 0; }); }; const signTransactions = async (transactions: Array<Transaction>) => { const signer = new nearAPI.InMemorySigner(_state.keystore); const signedTransactions: Array<nearAPI.transactions.SignedTransaction> = []; const block = await provider.block({ finality: "final" }); for (let i = 0; i < transactions.length; i += 1) { const transaction = transactions[i]; const publicKey = await signer.getPublicKey( transaction.signerId, options.network.networkId ); if (!publicKey) { throw new Error("No public key found"); } const accessKey = await provider.query<AccessKeyView>({ request_type: "view_access_key", finality: "final", account_id: transaction.signerId, public_key: publicKey.toString(), }); if (!validateAccessKey(transaction, accessKey)) { throw new Error("Invalid access key"); } const tx = nearAPI.transactions.createTransaction( transactions[i].signerId, nearAPI.utils.PublicKey.from(publicKey.toString()), transactions[i].receiverId, accessKey.nonce + i + 1, transaction.actions.map((action) => createAction(action)), nearAPI.utils.serialize.base_decode(block.header.hash) ); const [, signedTx] = await nearAPI.transactions.signTransaction( tx, signer, transactions[i].signerId, options.network.networkId ); signedTransactions.push(signedTx); } return signedTransactions; }; const requestAccounts = async () => { return _state.client.request<Array<WalletConnectAccount>>({ topic: _state.session!.topic, chainId: getChainId(), request: { method: "near_getAccounts", params: {}, }, }); }; const requestVerifyOwner = async (accountId: string, message: string) => { return _state.client.request<VerifiedOwner>({ topic: _state.session!.topic, chainId: getChainId(), request: { method: "near_verifyOwner", params: { accountId, message }, }, }); }; const requestSignMessage = async ( messageParams: SignMessageParams & { accountId?: string } ) => { const { message, nonce, recipient, callbackUrl, accountId } = messageParams; return _state.client.request<SignedMessage>({ topic: _state.session!.topic, chainId: getChainId(), request: { method: "near_signMessage", params: { message, nonce, recipient, ...(callbackUrl && { callbackUrl }), ...(accountId && { accountId }), }, }, }); }; const requestSignTransaction = async (transaction: Transaction) => { const accounts = await requestAccounts(); const account = accounts.find((x) => x.accountId === transaction.signerId); if (!account) { throw new Error("Invalid signer id"); } const [block, accessKey] = await Promise.all([ provider.block({ finality: "final" }), provider.query<AccessKeyView>({ request_type: "view_access_key", finality: "final", account_id: transaction.signerId, public_key: account.publicKey, }), ]); const tx = nearAPI.transactions.createTransaction( transaction.signerId, nearAPI.utils.PublicKey.from(account.publicKey), transaction.receiverId, accessKey.nonce + 1, transaction.actions.map((action) => createAction(action)), nearAPI.utils.serialize.base_decode(block.header.hash) ); const result = await _state.client.request<Uint8Array>({ topic: _state.session!.topic, chainId: getChainId(), request: { method: "near_signTransaction", params: { transaction: tx.encode() }, }, }); const signatureData = getSignatureData(result); // PROXIMITY FIX: Create a SignedTransaction from raw bytes // We can't decode because Fireblocks may use a different near-api-js version // But provider.sendTransaction() needs the actual bytes, which we have const bytes = Buffer.from(signatureData); // Create a minimal SignedTransaction-like object that works with sendTransaction const signedTx: any = { transaction: null as any, signature: null as any, encode: () => bytes }; return signedTx; }; // PROXIMITY: Helper function to sign and send transactions via WalletConnect // Now using near_signTransactions (just get signatures) and WE broadcast to NEAR const signAndSendViaWalletConnect = async (transactions: Array<Transaction>): Promise<Array<providers.FinalExecutionOutcome>> => { console.log('🎯🎯🎯 signAndSendViaWalletConnect CALLED - v19 WITH IMPORTED SHA256! 🎯🎯🎯'); if (!transactions.length) { return []; } const txs: Array<nearAPI.transactions.Transaction> = []; // SKIP near_getAccounts entirely - it causes a 5-second delay that breaks Fireblocks // Just use cached session data from initial sign-in console.log(`🔑 Using cached account data from session (no near_getAccounts delay)`); const [block, accounts] = await Promise.all([ provider.block({ finality: "final" }), getAccounts() ]); console.log(`🔑 Got ${accounts.length} account(s) from cached session`); accounts.forEach((acc, idx) => { console.log(` Account #${idx + 1}:`); console.log(` accountId: ${acc.accountId}`); console.log(` publicKey: ${acc.publicKey}`); console.log(` ⚠️ This is the key we'll use to BUILD the transaction`); console.log(` ⚠️ Fireblocks MUST sign with this same key, or NEAR will reject it`); }); for (let i = 0; i < transactions.length; i += 1) { const transaction = transactions[i]; const account = accounts.find( (x) => x.accountId === transaction.signerId ); if (!account || !account.publicKey) { throw new Error("Invalid signer id or missing public key"); } logger.log(`🔑 Building transaction #${i + 1} with publicKey: ${account.publicKey}`); logger.log(` 📡 Querying access key for account: ${transaction.signerId}, key: ${account.publicKey}`); try { const accessKey = await provider.query<AccessKeyView>({ request_type: "view_access_key", finality: "final", account_id: transaction.signerId, public_key: account.publicKey, }); logger.log(` ✅ Access key found, nonce: ${accessKey.nonce}`); txs.push( nearAPI.transactions.createTransaction( transaction.signerId, nearAPI.utils.PublicKey.from(account.publicKey), transaction.receiverId, accessKey.nonce + i + 1, transaction.actions.map((action) => createAction(action)), nearAPI.utils.serialize.base_decode(block.header.hash) ) ); logger.log(` ✅ Transaction #${i + 1} built successfully`); } catch (error: any) { logger.error(` ❌ Failed to build transaction #${i + 1}:`, error.message || error); throw error; } } console.log(`🔥 WALLET-CONNECT VERSION: v19-IMPORTED-SHA256-2024-10-07`); console.log(`📤 Sending ${txs.length} transaction(s) to Fireblocks via near_signTransactions`); console.log(`📝 NOTE: Using near_signTransactions (Fireblocks signs, WE broadcast)`); // Use near_signTransactions - Fireblocks signs, we broadcast // Convert Uint8Array to plain Array for WalletConnect compatibility const encodedTxs = txs.map((x, idx) => { const encoded = x.encode(); console.log(`📦 Transaction #${idx + 1} BEFORE sending to Fireblocks:`); console.log(` Length: ${encoded.length} bytes`); console.log(` First 40 bytes (hex):`, Buffer.from(encoded.slice(0, 40)).toString('hex')); console.log(` Last 40 bytes (hex):`, Buffer.from(encoded.slice(-40)).toString('hex')); console.log(` Full bytes (for verification):`, Buffer.from(encoded).toString('hex')); return Array.from(encoded); }); console.log(`🔍 Encoded ${encodedTxs.length} transactions for signing`); // Request Fireblocks to ONLY sign (not broadcast) logger.log(`📡 About to call _state.client.request with near_signTransactions...`); const signedTxsEncoded = await _state.client.request<Array<any>>({ topic: _state.session!.topic, chainId: getChainId(), request: { method: "near_signTransactions", params: { transactions: encodedTxs }, }, }); console.log(`✅ Received ${signedTxsEncoded.length} signed transaction(s) from Fireblocks`); console.log(`🔍 First signed tx type:`, typeof signedTxsEncoded[0], 'isArray:', Array.isArray(signedTxsEncoded[0])); // Decode signed transactions const signedTxs: Array<nearAPI.transactions.SignedTransaction> = signedTxsEncoded.map((encoded, idx) => { console.log(`🔧 Decoding transaction #${idx + 1}...`); // Handle different formats Fireblocks might return let arrayData: number[]; if (Array.isArray(encoded)) { // Already an array arrayData = encoded; console.log(` ✓ Direct array, length: ${arrayData.length}`); } else if (encoded && typeof encoded === 'object' && encoded.type === 'Buffer' && Array.isArray(encoded.data)) { // Serialized Buffer object: {type: "Buffer", data: [...]} arrayData = encoded.data; console.log(` ✓ Buffer object detected, extracted data array, length: ${arrayData.length}`); } else { // Try Array.from as fallback arrayData = Array.from(encoded as any); console.log(` ⚠ Unknown format, used Array.from, length: ${arrayData.length}`); } // Convert to Node.js Buffer for BinaryReader const buffer = Buffer.from(arrayData); console.log(` ✓ Created Buffer for deserialization, length: ${buffer.length}`); console.log(`📦 Transaction #${idx + 1} AFTER Fireblocks signed:`); console.log(` Length: ${buffer.length} bytes`); console.log(` First 40 bytes (hex):`, buffer.slice(0, 40).toString('hex')); console.log(` Last 40 bytes (hex):`, buffer.slice(-40).toString('hex')); console.log(` Full bytes (for verification):`, buffer.toString('hex')); const decoded = nearAPI.transactions.SignedTransaction.decode(buffer); console.log(` ✅ Successfully decoded transaction #${idx + 1}`); // CRITICAL: Check what public key Fireblocks used to sign const signedWithKey = decoded.transaction.publicKey.toString(); const originalTx = txs[idx]; const builtWithKey = originalTx.publicKey.toString(); console.log(` 🔑 PUBLIC KEY COMPARISON:`); console.log(` Built with: ${builtWithKey}`); console.log(` Signed with: ${signedWithKey}`); console.log(` Match: ${builtWithKey === signedWithKey ? '✅ YES' : '❌ NO - THIS IS THE PROBLEM!'}`); // CRITICAL: Check the signature itself console.log(` 🔏 SIGNATURE INSPECTION:`); console.log(` Signature type:`, typeof decoded.signature); console.log(` Signature keyType:`, decoded.signature?.keyType); console.log(` Signature data length:`, decoded.signature?.data?.length || 'N/A'); if (decoded.signature?.data) { const sigHex = Buffer.from(decoded.signature.data).toString('hex'); console.log(` Signature (hex, full):`, sigHex); } // CRITICAL: Manually verify the signature try { // Compute transaction hash using js-sha256 (SHA-256 of serialized transaction) const txBytes = decoded.transaction.encode(); const txHashBytes = new Uint8Array(sha256.array(txBytes)); console.log(` 🔐 MANUAL SIGNATURE VERIFICATION:`); console.log(` Transaction bytes length:`, txBytes.length); console.log(` Transaction hash (hex):`, Buffer.from(txHashBytes).toString('hex')); console.log(` Public key (base58):`, decoded.transaction.publicKey.toString()); console.log(` Signature (hex):`, Buffer.from(decoded.signature.data).toString('hex')); // Try to verify using near-api-js PublicKey.verify() const isValid = decoded.transaction.publicKey.verify( txHashBytes, decoded.signature.data ); console.log(` Signature valid: ${isValid ? '✅ YES - Fireblocks signature is VALID!' : '❌ NO - FIREBLOCKS SIGNATURE IS INVALID!'}`); if (!isValid) { console.error(` 🚨 SIGNATURE VERIFICATION FAILED!`); console.error(` Fireblocks returned a signature that doesn't verify!`); console.error(` The signature does NOT match the transaction hash + public key.`); console.error(` This is the root cause of the NEAR rejection.`); console.error(` 🔍 POSSIBLE CAUSES:`); console.error(` 1. Fireblocks is signing with a different private key`); console.error(` 2. Fireblocks is signing different data (e.g., base64 encoded)`); console.error(` 3. Fireblocks' Ed25519 implementation has a bug`); } else { console.log(` ✅ Signature is cryptographically valid!`); console.log(` Yet NEAR still rejects it... investigating why...`); } } catch (verifyError: any) { console.error(` ❌ Could not verify signature:`, verifyError.message || verifyError); console.error(` Stack:`, verifyError.stack); } if (builtWithKey !== signedWithKey) { console.error(` 🚨 PUBLIC KEY MISMATCH DETECTED!`); console.error(` This will cause "Transaction is not signed with the given public key" error`); console.error(` We built the tx with one key, but Fireblocks signed with another`); } return decoded; }); console.log(`📡 Broadcasting ${signedTxs.length} transaction(s) to NEAR blockchain...`); // Broadcast each signed transaction to NEAR const results: Array<providers.FinalExecutionOutcome> = []; for (let i = 0; i < signedTxs.length; i++) { const signedTx = signedTxs[i]; console.log(` 📤 Broadcasting transaction #${i + 1}...`); console.log(` Signer: ${signedTx.transaction.signerId}`); console.log(` Receiver: ${signedTx.transaction.receiverId}`); console.log(` PublicKey: ${signedTx.transaction.publicKey.toString()}`); console.log(` Nonce: ${signedTx.transaction.nonce}`); console.log(` Signature exists: ${!!signedTx.signature}`); console.log(` Signature length: ${signedTx.signature?.data?.length || 0}`); // CRITICAL: Verify this access key actually exists on NEAR try { console.log(` 🔍 Verifying access key exists on NEAR...`); const accessKey = await provider.query<AccessKeyView>({ request_type: "view_access_key", finality: "final", account_id: signedTx.transaction.signerId, public_key: signedTx.transaction.publicKey.toString(), }); console.log(` ✅ Access key exists on NEAR!`); console.log(` Nonce on NEAR: ${accessKey.nonce}`); console.log(` Permission: ${accessKey.permission}`); } catch (accessKeyError: any) { console.error(` ❌ Access key NOT found on NEAR!`); console.error(` This is the root cause of the error!`); console.error(` Error:`, accessKeyError.message || accessKeyError); throw new Error(`Access key ${signedTx.transaction.publicKey.toString()} does not exist for account ${signedTx.transaction.signerId}`); } try { const result = await provider.sendTransaction(signedTx); console.log(` ✅ Transaction #${i + 1} broadcast successfully`); console.log(` Hash: ${result.transaction.hash}`); console.log(` Status: ${JSON.stringify(result.status)}`); results.push(result); } catch (error: any) { console.error(` ❌ Failed to broadcast transaction #${i + 1}:`, error.message || error); console.error(` Full error:`, error); throw error; } } logger.log(`🎉 All ${results.length} transaction(s) successfully signed and broadcast!`); return results; }; const requestSignTransactions = async (transactions: Array<Transaction>) => { if (!transactions.length) { return []; } const txs: Array<nearAPI.transactions.Transaction> = []; const [block, accounts] = await Promise.all([ provider.block({ finality: "final" }), getAccounts(), // FASTNEAR FIX: Use cached accounts instead of making WC request ]); for (let i = 0; i < transactions.length; i += 1) { const transaction = transactions[i]; const account = accounts.find( (x) => x.accountId === transaction.signerId ); if (!account || !account.publicKey) { throw new Error("Invalid signer id or missing public key"); } const accessKey = await provider.query<AccessKeyView>({ request_type: "view_access_key", finality: "final", account_id: transaction.signerId, public_key: account.publicKey, }); txs.push( nearAPI.transactions.createTransaction( transaction.signerId, nearAPI.utils.PublicKey.from(account.publicKey), transaction.receiverId, accessKey.nonce + i + 1, transaction.actions.map((action) => createAction(action)), nearAPI.utils.serialize.base_decode(block.header.hash) ) ); } const results = await _state.client.request<Array<Uint8Array>>({ topic: _state.session!.topic, chainId: getChainId(), request: { method: "near_signTransactions", params: { transactions: txs.map((x) => x.encode()) }, }, }); // PROXIMITY FIX: Don't decode the signed transaction - just pass the raw bytes // Fireblocks may be using a different near-api-js version, causing Borsh schema mismatches // The provider.sendTransaction() only needs the encoded bytes anyway return results.map((result) => { const signatureData = getSignatureData(result); const bytes = Buffer.from(signatureData); // Create a minimal SignedTransaction-like object that works with sendTransaction const signedTx: any = { transaction: null as any, signature: null as any, encode: () => bytes }; return signedTx; }); }; const createLimitedAccessKeyPairs = async (): Promise< Array<LimitedAccessKeyPair> > => { const accounts = await getAccounts(); return accounts.map(({ accountId }) => ({ accountId, keyPair: nearAPI.utils.KeyPair.fromRandom("ed25519"), })); }; const requestSignIn = async ( permission: nearAPI.transactions.FunctionCallPermission ) => { const keyPairs = await createLimitedAccessKeyPairs(); const limitedAccessAccounts: Array<LimitedAccessAccount> = keyPairs.map( ({ accountId, keyPair }) => ({ accountId, publicKey: keyPair.getPublicKey().toString(), }) ); await _state.client.request({ topic: _state.session!.topic, chainId: getChainId(), request: { method: "near_signIn", params: { permission: permission, accounts: limitedAccessAccounts, }, }, }); for (let i = 0; i < keyPairs.length; i += 1) { const { accountId, keyPair } = keyPairs[i]; await _state.keystore.setKey( options.network.networkId, accountId, keyPair ); } }; const requestSignOut = async () => { const accounts = await getAccounts(); const limitedAccessAccounts: Array<LimitedAccessAccount> = []; for (let i = 0; i < accounts.length; i += 1) { const account = accounts[i]; const keyPair = await _state.keystore.getKey( options.network.networkId, account.accountId ); if (!keyPair) { continue; } limitedAccessAccounts.push({ accountId: account.accountId, publicKey: keyPair.getPublicKey().toString(), }); } if (!limitedAccessAccounts.length) { return; } await _state.client.request({ topic: _state.session!.topic, chainId: getChainId(), request: { method: "near_signOut", params: { accounts: limitedAccessAccounts, }, }, }); }; const signOut = async () => { if (_state.session) { await requestSignOut(); await disconnect({ state: _state }); } await cleanup(); }; const setupEvents = async () => { _state.subscriptions.push( _state.client.on("session_update", async (event) => { logger.log("Session Update", event); if (event.topic === _state.session?.topic) { _state.session = { ..._state.client.session.get(event.topic), namespaces: event.params.namespaces, }; emitter.emit("accountsChanged", { accounts: await getAccounts() }); } }) ); _state.subscriptions.push( _state.client.on("session_delete", async (event) => { logger.log("Session Deleted", event); if (event.topic === _state.session?.topic) { await cleanup(); emitter.emit("signedOut", null); } }) ); // Add comprehensive debugging for WalletConnect events const clientEvents = [ "session_event", "session_ping", "session_expire", "session_extend", "session_proposal", "session_request", "session_request_sent", ]; clientEvents.forEach((eventName) => { try { _state.subscriptions.push( _state.client.on(eventName as any, (event: any) => { console.log(`🔔 [WC-EVENT] ${eventName}:`, { topic: event.topic, timestamp: new Date().toISOString(), event: event, }); }) ); } catch (e) { // Event might not be supported console.log(`⚠️ [WC-EVENT] Could not subscribe to ${eventName}`); } }); // Monitor the underlying SignClient core events for transport issues try { const coreClient = (_state.client as any).core; if (coreClient) { console.log('🔌 [WC-SETUP] Monitoring core client events'); // Listen to transport/relay events const coreEvents = ['relayer_connect', 'relayer_disconnect', 'relayer_error', 'relayer_message']; coreEvents.forEach(eventName => { try { coreClient.relayer?.on(eventName, (data: any) => { console.log(`🔌 [WC-RELAYER] ${eventName}:`, { timestamp: new Date().toISOString(), data: data, }); }); } catch (e) { console.log(`⚠️ [WC-RELAYER] Could not subscribe to ${eventName}`); } }); } } catch (e) { console.log('⚠️ [WC-SETUP] Could not access core client for event monitoring'); } }; if (_state.session) { await setupEvents(); } return { async signIn({ contractId, methodNames = [], qrCodeModal = true }) { try { const { contract } = store.getState(); if (_state.session && !contract) { await disconnect({ state: _state }); await cleanup(); } const chainId = getChainId(); _state.session = await connect({ state: _state, chainId, qrCodeModal, projectId: params.projectId, methods: params.methods, events: params.events, }); await requestSignIn({ receiverId: contractId || "", methodNames }); await setupEvents(); return await getAccounts(); } catch (err) { await signOut(); throw err; } }, signOut, async getAccounts() { return getAccounts(); }, async verifyOwner({ message }) { logger.log("WalletConnect:verifyOwner", { message }); const { contract } = store.getState(); if (!_state.session || !contract) { throw new Error("Wallet not signed in"); } const account = getActiveAccount(store.getState()); if (!account) { throw new Error("No active account"); } return requestVerifyOwner(account.accountId, message); }, async signMessage({ message, nonce, recipient, callbackUrl }) { logger.log("WalletConnect:signMessage", { message, nonce, recipient }); try { const chainId = getChainId(); if (!_state.session) { _state.session = _state.session = await connect({ state: _state, chainId, qrCodeModal: true, projectId: params.projectId, }); } const account = getActiveAccount(store.getState()); return await requestSignMessage({ message, nonce, recipient, callbackUrl, accountId: account?.accountId, }); } catch (err) { await disconnect({ state: _state }); await cleanup(); throw err; } }, async signAndSendTransaction({ signerId, receiverId, actions }) { logger.log("signAndSendTransaction", { signerId, receiverId, actions }); const { contract } = store.getState(); if (!_state.session || !contract) { throw new Error("Wallet not signed in"); } const account = getActiveAccount(store.getState()); if (!account) { throw new Error("No active account"); } const resolvedTransaction: Transaction = { signerId: signerId || account.accountId, receiverId: receiverId || contract.contractId, actions, }; // PROXIMITY FIX: Use near_signAndSendTransaction (singular) to let Fireblocks handle both signing and broadcasting const [result] = await signAndSendViaWalletConnect([resolvedTransaction]); return result; }, async signAndSendTransactions({ transactions }) { logger.log("🚀 PROXIMITY-WALLET-CONNECT v14 signAndSendTransactions called!"); logger.log("signAndSendTransactions", { transactions }); const { contract } = store.getState(); if (!_state.session || !contract) { throw new Error("Wallet not signed in"); } const account = getActiveAccount(store.getState()); if (!account) { throw new Error("No active account"); } const resolvedTransactions = transactions.map((x) => ({ signerId: x.signerId || account.accountId, receiverId: x.receiverId, actions: x.actions, })); // PROXIMITY FIX: Use near_signAndSendTransactions to let Fireblocks handle BOTH signing AND broadcasting // This avoids Borsh deserialization issues from version mismatches return await signAndSendViaWalletConnect(resolvedTransactions); }, async createSignedTransaction(receiverId, actions) { logger.log("createSignedTransaction", { receiverId, actions }); throw new Error(`Method not supported by ${metadata.name}`); }, async signTransaction(transaction) { logger.log("signTransaction", { transaction }); throw new Error(`Method not supported by ${metadata.name}`); }, async getPublicKey() { logger.log("getPublicKey", {}); throw new Error(`Method not supported by ${metadata.name}`); }, async signNep413Message(message, accountId, recipient, nonce, callbackUrl) { logger.log("signNep413Message", { message, accountId, recipient, nonce, callbackUrl, }); throw new Error(`Method not supported by ${metadata.name}`); }, async signDelegateAction(delegateAction) { logger.log("signDelegateAction", { delegateAction }); throw new Error(`Method not supported by ${metadata.name}`); }, }; }; export function setupWalletConnect({ projectId, metadata, chainId, relayUrl = "wss://relay.walletconnect.com", iconUrl = icon, deprecated = false, methods, events, }: WalletConnectParams): WalletModuleFactory<BridgeWallet> { return async () => { return { id: "wallet-connect", type: "bridge", metadata: { name: "WalletConnect", description: "Bridge wallet for NEAR.", iconUrl, deprecated, available: true, }, init: (options) => { return WalletConnect({ ...options, params: { projectId, metadata, relayUrl, chainId, methods, events, }, }); }, }; }; }