UNPKG

@turnkey/core

Version:

A core JavaScript web and React Native package for interfacing with Turnkey's infrastructure.

290 lines (286 loc) 12.2 kB
'use strict'; var viem = require('viem'); var ethers = require('ethers'); var crypto = require('@turnkey/crypto'); var base = require('../../../__types__/base.js'); var encoding = require('@turnkey/encoding'); /** * Abstract base class for Ethereum wallet implementations. * * Provides shared logic for: * - Provider discovery via EIP-6963 (request/announce events) * - Connecting/disconnecting via EIP-1193 * - Recovering compressed secp256k1 public keys from EIP-191 signatures */ class BaseEthereumWallet { constructor() { this.interfaceType = base.WalletInterfaceType.Ethereum; /** * Retrieves the compressed secp256k1 public key by signing a known message. * * - Signs the fixed string "GET_PUBLIC_KEY" (EIP-191) and recovers the key. * - Returns the compressed public key as a hex string (no 0x prefix). * * @param provider - The wallet provider to use. * @returns A promise that resolves to the compressed public key (hex-encoded). */ this.getPublicKey = async (provider) => { const message = "GET_PUBLIC_KEY"; const signature = await this.sign(message, provider, base.SignIntent.SignMessage); return getCompressedPublicKey(signature, message); }; /** * Discovers EIP-1193 providers using the EIP-6963 standard. * * - Dispatches "eip6963:requestProvider" and listens for "eip6963:announceProvider". * - For each discovered provider, attempts to read `eth_accounts` and `eth_chainId` * (silently ignored if unavailable), defaulting to chainId "0x1". * - Returns providers discovered during this discovery cycle; may be empty. * * @returns A promise that resolves to a list of available wallet providers. */ this.getProviders = async () => { const discovered = []; const providerPromises = []; const handler = (ev) => { const { provider, info } = ev.detail; const promise = (async () => { let connectedAddresses = []; // we default to Ethereum mainnet let chainId = "0x1"; try { const accounts = await provider.request?.({ method: "eth_accounts", }); if (Array.isArray(accounts)) connectedAddresses = accounts; chainId = await provider.request({ method: "eth_chainId", }); } catch { // fail silently } discovered.push({ interfaceType: base.WalletInterfaceType.Ethereum, chainInfo: { namespace: base.Chain.Ethereum, chainId, }, info, provider, connectedAddresses, }); })(); providerPromises.push(promise); }; window.addEventListener("eip6963:announceProvider", handler); window.dispatchEvent(new Event("eip6963:requestProvider")); window.removeEventListener("eip6963:announceProvider", handler); await Promise.all(providerPromises); return discovered; }; /** * Requests or verifies account connection via `eth_requestAccounts`. * * - If the wallet is already connected, resolves immediately (no prompt). * - If not connected, the wallet will typically prompt the user to authorize. * * @param provider - The wallet provider to use. * @returns A promise that resolves once at least one account is connected. * @throws {Error} If the wallet returns no accounts after the request. */ this.connectWalletAccount = async (provider) => { const wallet = asEip1193(provider); await getAccount(wallet); }; /** * Attempts to disconnect the wallet by revoking `eth_accounts` permission. * * - Calls `wallet_revokePermissions` with `{ eth_accounts: {} }`. * - Provider behavior is wallet-specific: some implement it, some ignore it, * and others (e.g., Phantom) throw “method not supported”. * * @param provider - The wallet provider to disconnect. * @returns A promise that resolves once the request completes (or rejects if the provider errors). */ this.disconnectWalletAccount = async (provider) => { const wallet = asEip1193(provider); await wallet.request({ method: "wallet_revokePermissions", params: [{ eth_accounts: {} }], }); }; } /** * Switches to a new EVM chain, with add-then-switch fallback. * * - Tries `wallet_switchEthereumChain` first. * - If the wallet returns error code 4902 (unknown chain): * - If `chainOrId` is a string (hex chainId), throws and asks the caller to pass metadata. * - If `chainOrId` is a `SwitchableChain`, calls `wallet_addEthereumChain` and then retries the switch. * * @param provider - The wallet provider to use. * @param chainOrId - Hex chain ID string or full `SwitchableChain` metadata. * @throws {Error} If provider is non-EVM, adding/switching fails, or metadata is missing. */ async switchChain(provider, chainOrId) { if (provider.chainInfo.namespace !== base.Chain.Ethereum) { throw new Error("Only EVM wallets can switch chains"); } const wallet = asEip1193(provider); const chainId = typeof chainOrId === "string" ? chainOrId : chainOrId.id; try { // first we just try switching await wallet.request({ method: "wallet_switchEthereumChain", params: [{ chainId }], }); } catch (err) { // if the error is not “chain not found” // we just re-throw it if (err.code !== 4902) { throw err; } // no metadata was provided so we throw an error // telling them to pass it in if (typeof chainOrId === "string") { throw new Error(`Chain ${chainId} not recognized. ` + `If you want to add it, call switchChain with a SwitchableChain object.`); } // we have full metadata and can add the chain and switch const { name, rpcUrls, blockExplorerUrls, iconUrls, nativeCurrency } = chainOrId; // add the chain await wallet.request({ method: "wallet_addEthereumChain", params: [ { chainId, chainName: name, rpcUrls, blockExplorerUrls, iconUrls, nativeCurrency, }, ], }); // then switch again await wallet.request({ method: "wallet_switchEthereumChain", params: [{ chainId }], }); } } } /** * EthereumWallet implementation using EIP-1193 compatible providers. * * Handles message signing and transaction submission. */ class EthereumWallet extends BaseEthereumWallet { constructor() { super(...arguments); /** * Signs a message or sends a transaction depending on intent. * * - `SignMessage` → `personal_sign` (hex signature). * - `SignAndSendTransaction` → `eth_sendTransaction` (tx hash). * - For transactions, `message` must be a raw tx that `Transaction.from(...)` can parse. * - May prompt the user (account access, signing, or send). * * @param payload - The payload or raw transaction to be signed/sent. * @param provider - The wallet provider to use. * @param intent - Signing intent (SignMessage or SignAndSendTransaction). * @returns A promise that resolves to a hex string (signature or tx hash). * @throws {Error} If the intent is unsupported or the wallet rejects the request. */ this.sign = async (payload, provider, intent) => { const selectedProvider = asEip1193(provider); const account = await getAccount(selectedProvider); switch (intent) { case base.SignIntent.SignMessage: return await selectedProvider.request({ method: "personal_sign", params: [payload, account], }); case base.SignIntent.SignAndSendTransaction: const tx = ethers.Transaction.from(payload); const txParams = { from: account, to: tx.to?.toString(), value: viem.toHex(tx.value), gas: viem.toHex(tx.gasLimit), maxFeePerGas: viem.toHex(tx.maxFeePerGas ?? 0n), maxPriorityFeePerGas: viem.toHex(tx.maxPriorityFeePerGas ?? 0n), nonce: viem.toHex(tx.nonce), chainId: viem.toHex(tx.chainId), data: tx.data?.toString() ?? "0x", }; return await selectedProvider.request({ method: "eth_sendTransaction", params: [txParams], }); default: throw new Error(`Unsupported sign intent: ${intent}`); } }; } } /** * Retrieves the active Ethereum account from a provider. * * - Calls `eth_requestAccounts` (usually no prompt). * * @param provider - EIP-1193 compliant provider. * @returns A promise resolving to the connected Ethereum address. * @throws {Error} If no connected account is found. */ const getAccount = async (provider) => { const [connectedAccount] = await provider.request({ method: "eth_requestAccounts", }); if (!connectedAccount) throw new Error("No connected account found"); return connectedAccount; }; /** * Recovers and compresses the public key from a signed message. * * - Recovers the secp256k1 public key from an EIP-191 signature and compresses it. * - Returns a hex string with no 0x prefix. * * @param signature - The signature as a hex string. * @param message - The original signed message. * @returns A promise resolving to the compressed public key (hex-encoded). */ const getCompressedPublicKey = async (signature, message) => { const secp256k1PublicKey = await viem.recoverPublicKey({ hash: viem.hashMessage(message), signature: signature, }); const publicKeyHex = secp256k1PublicKey.startsWith("0x") ? secp256k1PublicKey.slice(2) : secp256k1PublicKey; const publicKeyBytes = encoding.uint8ArrayFromHexString(publicKeyHex); const publicKeyBytesCompressed = crypto.compressRawPublicKey(publicKeyBytes); return encoding.uint8ArrayToHexString(publicKeyBytesCompressed); }; /** * Validates and casts a `WalletProvider` to an EIP-1193 provider. * * - Expects `provider.request` to be a function. * * @param p - The wallet provider to validate. * @returns An EIP-1193 provider. * @throws {Error} If the provider does not implement `request()`. */ const asEip1193 = (p) => { if (p.provider && typeof p.provider.request === "function") { return p.provider; } throw new Error("Expected an EIP-1193 provider (Ethereum wallet)"); }; exports.BaseEthereumWallet = BaseEthereumWallet; exports.EthereumWallet = EthereumWallet; //# sourceMappingURL=ethereum.js.map