UNPKG

@turnkey/core

Version:

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

213 lines (210 loc) 9.35 kB
import { getWallets } from '@wallet-standard/app'; import { uint8ArrayFromHexString, uint8ArrayToHexString, bs58 } from '@turnkey/encoding'; import { SignIntent, WalletInterfaceType, Chain } from '../../../__types__/enums.mjs'; import { withTimeoutFallback } from '../../../utils.mjs'; /** * Abstract base class for Solana wallet implementations using Wallet Standard. * * Provides shared logic for: * - Provider discovery via `@wallet-standard/app` (`getWallets()`). * - Connecting via `standard:connect` and disconnecting via `standard:disconnect`. * - Public key retrieval from the wallet's account address (base58 → hex). */ class BaseSolanaWallet { constructor() { this.interfaceType = WalletInterfaceType.Solana; /** * Retrieves the ed25519 public key for the active account as hex (no 0x prefix). * * - Ensures the wallet is connected (calls `standard:connect` if needed). * - Decodes the Wallet Standard account address (base58) to raw bytes. * * @param provider - The wallet provider to use. * @returns Hex-encoded ed25519 public key (no 0x prefix). * @throws {Error} If no account is available. */ this.getPublicKey = async (provider) => { const wallet = asSolana(provider); await connectAccount(wallet); const account = wallet.accounts[0]; if (!account) { throw new Error("No account in wallet"); } const rawBytes = bs58.decode(account.address); return uint8ArrayToHexString(rawBytes); }; /** * Discovers Solana-capable Wallet Standard providers. * * - Uses `getWallets().get()` and filters wallets with at least one `chains` entry * starting with `"solana:"`. * - For each wallet, collects branding info and any currently connected addresses. * * @returns A list of discovered Solana `WalletProvider`s (may be empty). */ this.getProviders = async () => { const discovered = []; const walletsApi = getWallets(); const providers = walletsApi .get() .filter((w) => w.chains.some((c) => c.startsWith("solana:"))); await Promise.all(providers.map(async (wallet) => { // wrap account access in a timeout to prevent hanging providers from blocking discovery const connectedAddresses = await withTimeoutFallback((async () => { try { return wallet.accounts?.map((a) => a.address) ?? []; } catch { return []; } })(), []); discovered.push({ interfaceType: WalletInterfaceType.Solana, chainInfo: { namespace: Chain.Solana, }, // Some wallet providers include leading whitespace in their icon URLs. // Next.js 15+ and other frameworks are strict about URL formatting, // so we trim info: { name: wallet.name, ...(wallet.icon && { icon: wallet.icon.trim() }), }, provider: wallet, connectedAddresses, }); })); return discovered; }; /** * Connects the wallet account, prompting the user if necessary. * * - Calls `standard:connect` only if no accounts are present. This will prompt the user to connect their wallet. * * @param provider - The wallet provider to connect. * @returns A promise that resolves with the connected wallet's address. * @throws {Error} If the wallet does not implement `standard:connect`. */ this.connectWalletAccount = async (provider) => { const wallet = asSolana(provider); return await connectAccount(wallet); }; /** * Disconnects the wallet account using Wallet Standard. * * - Calls `standard:disconnect` if implemented. * - Throws if the wallet does not implement `standard:disconnect`. * * @param provider - The wallet provider to disconnect. * @returns A promise that resolves once the wallet disconnects. * @throws {Error} If `standard:disconnect` is not supported by the wallet. */ this.disconnectWalletAccount = async (provider) => { const wallet = asSolana(provider); const disconnectFeature = wallet.features["standard:disconnect"]; if (disconnectFeature) { await disconnectFeature.disconnect(); } else { throw new Error("Wallet does not support standard:disconnect"); } }; } } /** * Signs a message or transaction with the connected Solana wallet. * * - Ensures the wallet is connected (may prompt via `standard:connect` if its not). * - `SignMessage` → `solana:signMessage` (returns hex signature). * - `SignTransaction` → `solana:signTransaction` (returns hex signature). * * @param payload - UTF-8 string (for message) or hex string (for transaction bytes). * @param provider - The wallet provider to use. * @param intent - The signing intent. * @returns Hex-encoded signature (no 0x prefix). * @throws {Error} If the provider lacks required features or intent is unsupported. */ class SolanaWallet extends BaseSolanaWallet { constructor() { super(...arguments); this.sign = async (payload, provider, intent) => { const wallet = asSolana(provider); await connectAccount(wallet); const account = wallet.accounts[0]; if (!account) throw new Error("No account available"); switch (intent) { case SignIntent.SignMessage: { const signFeature = wallet.features["solana:signMessage"]; if (!signFeature) throw new Error("Provider does not support solana:signMessage"); const data = new TextEncoder().encode(payload); const results = await signFeature.signMessage({ account, message: data, }); if (!results?.length || !results[0]?.signature) { throw new Error("No signature returned from signMessage"); } return uint8ArrayToHexString(results[0].signature); } case SignIntent.SignTransaction: { const signFeature = wallet.features["solana:signTransaction"]; if (!signFeature) throw new Error("Provider does not support solana:signTransaction"); const data = uint8ArrayFromHexString(payload); const results = await signFeature.signTransaction({ account, transaction: data, }); if (!results?.length || !results[0]?.signedTransaction) { throw new Error("No signature returned from signTransaction"); } return uint8ArrayToHexString(results[0].signedTransaction); } default: throw new Error(`Unsupported sign intent: ${intent}`); } }; } } /** * Casts a WalletRpcProvider to a Wallet Standard Solana wallet. * * - Validates presence of the Wallet Standard `features` map and `solana:signMessage`. * - Use this before calling Solana-specific features (signMessage, signTransaction, etc.). * * @param provider - The wallet provider to cast. * @returns The Wallet Standard wallet object. * @throws {Error} If the provider is not a Wallet Standard Solana wallet. */ const asSolana = (provider) => { if (provider.provider && "features" in provider.provider && "solana:signMessage" in provider.provider.features) { return provider.provider; } throw new Error("Expected a Wallet-Standard provider (Solana wallet)"); }; /** * Ensures the given Wallet Standard wallet has at least one connected account. * * - If accounts already exist, resolves immediately. * - If not, attempts `standard:connect`, which may prompt the user. * * @param wallet - The Wallet Standard wallet to connect. * @returns A promise that resolves with the connected wallet's address. * @throws {Error} If the wallet does not implement `standard:connect`. */ const connectAccount = async (wallet) => { if (wallet.accounts.length) return wallet.accounts[0].address; const stdConnect = wallet.features["standard:connect"]; if (stdConnect) { await stdConnect.connect(); return wallet.accounts[0].address; } throw new Error("Wallet is not connected and does not implement standard:connect"); }; export { BaseSolanaWallet, SolanaWallet }; //# sourceMappingURL=solana.mjs.map