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
text/typescript
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,
},
});
},
};
};
}