UNPKG

@turnkey/core

Version:

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

1,048 lines (1,044 loc) 44.5 kB
'use strict'; var sdkTypes = require('@turnkey/sdk-types'); var enums = require('./__types__/enums.js'); var error = require('./__types__/error.js'); var encoding = require('@turnkey/encoding'); var turnkeyHelpers = require('./turnkey-helpers.js'); var crypto$1 = require('@turnkey/crypto'); var ethers = require('ethers'); var version = require('./__generated__/version.js'); /** * Configuration for all supported address formats. * * Includes: * - encoding type * - hash function * - default accounts for the address format * - display name for the address format * * ```ts * // Example usage: * import { addressFormatConfig } from "@turnkey/sdk-core"; * * const config = addressFormatConfig["ADDRESS_FORMAT_ETHEREUM"]; * ``` */ const sessionExpiredErrors = { pubKeyNotFound: "could not find public key in organization or its parent organization", apiKeyExpired: "Unauthenticated desc = expired api key publicKey", }; // Global errors to match against error messages returned from the API const globalErrorsToMatch = Object.freeze({ [sessionExpiredErrors.pubKeyNotFound]: { message: "Session public key could not be found in the sub-organization or parent organization", code: sdkTypes.TurnkeyErrorCodes.SESSION_EXPIRED, }, [sessionExpiredErrors.apiKeyExpired]: { message: "Session API key has expired", code: sdkTypes.TurnkeyErrorCodes.SESSION_EXPIRED, }, }); const addressFormatConfig = { ADDRESS_FORMAT_UNCOMPRESSED: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: null, displayName: "Uncompressed", }, ADDRESS_FORMAT_COMPRESSED: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: null, displayName: "Compressed", }, ADDRESS_FORMAT_ETHEREUM: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_KECCAK256", defaultAccounts: turnkeyHelpers.DEFAULT_ETHEREUM_ACCOUNTS, displayName: "Ethereum", }, ADDRESS_FORMAT_SOLANA: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_NOT_APPLICABLE", defaultAccounts: turnkeyHelpers.DEFAULT_SOLANA_ACCOUNTS, displayName: "Solana", }, ADDRESS_FORMAT_COSMOS: { encoding: "PAYLOAD_ENCODING_TEXT_UTF8", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_COSMOS_ACCOUNTS, displayName: "Cosmos", }, ADDRESS_FORMAT_TRON: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_TRON_ACCOUNTS, displayName: "Tron", }, ADDRESS_FORMAT_SUI: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_NOT_APPLICABLE", defaultAccounts: turnkeyHelpers.DEFAULT_SUI_ACCOUNTS, displayName: "Sui", }, ADDRESS_FORMAT_APTOS: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_NOT_APPLICABLE", defaultAccounts: turnkeyHelpers.DEFAULT_APTOS_ACCOUNTS, displayName: "Aptos", }, ADDRESS_FORMAT_BITCOIN_MAINNET_P2PKH: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_BITCOIN_MAINNET_P2PKH_ACCOUNTS, displayName: "Bitcoin Mainnet P2PKH", }, ADDRESS_FORMAT_BITCOIN_MAINNET_P2SH: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_BITCOIN_MAINNET_P2SH_ACCOUNTS, displayName: "Bitcoin Mainnet P2SH", }, ADDRESS_FORMAT_BITCOIN_MAINNET_P2WPKH: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_BITCOIN_MAINNET_P2WPKH_ACCOUNTS, displayName: "Bitcoin Mainnet P2WPKH", }, ADDRESS_FORMAT_BITCOIN_MAINNET_P2WSH: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_BITCOIN_MAINNET_P2WSH_ACCOUNTS, displayName: "Bitcoin Mainnet P2WSH", }, ADDRESS_FORMAT_BITCOIN_MAINNET_P2TR: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_BITCOIN_MAINNET_P2TR_ACCOUNTS, displayName: "Bitcoin Mainnet P2TR", }, ADDRESS_FORMAT_BITCOIN_TESTNET_P2PKH: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_BITCOIN_TESTNET_P2PKH_ACCOUNTS, displayName: "Bitcoin Testnet P2PKH", }, ADDRESS_FORMAT_BITCOIN_TESTNET_P2SH: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_BITCOIN_TESTNET_P2SH_ACCOUNTS, displayName: "Bitcoin Testnet P2SH", }, ADDRESS_FORMAT_BITCOIN_TESTNET_P2WPKH: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_BITCOIN_TESTNET_P2WPKH_ACCOUNTS, displayName: "Bitcoin Testnet P2WPKH", }, ADDRESS_FORMAT_BITCOIN_TESTNET_P2WSH: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_BITCOIN_TESTNET_P2WSH_ACCOUNTS, displayName: "Bitcoin Testnet P2WSH", }, ADDRESS_FORMAT_BITCOIN_TESTNET_P2TR: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_BITCOIN_TESTNET_P2TR_ACCOUNTS, displayName: "Bitcoin Testnet P2TR", }, ADDRESS_FORMAT_BITCOIN_SIGNET_P2PKH: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_BITCOIN_SIGNET_P2PKH_ACCOUNTS, displayName: "Bitcoin Signet P2PKH", }, ADDRESS_FORMAT_BITCOIN_SIGNET_P2SH: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_BITCOIN_SIGNET_P2SH_ACCOUNTS, displayName: "Bitcoin Signet P2SH", }, ADDRESS_FORMAT_BITCOIN_SIGNET_P2WPKH: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_BITCOIN_SIGNET_P2WPKH_ACCOUNTS, displayName: "Bitcoin Signet P2WPKH", }, ADDRESS_FORMAT_BITCOIN_SIGNET_P2WSH: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_BITCOIN_SIGNET_P2WSH_ACCOUNTS, displayName: "Bitcoin Signet P2WSH", }, ADDRESS_FORMAT_BITCOIN_SIGNET_P2TR: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_BITCOIN_SIGNET_P2TR_ACCOUNTS, displayName: "Bitcoin Signet P2TR", }, ADDRESS_FORMAT_BITCOIN_REGTEST_P2PKH: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_BITCOIN_REGTEST_P2PKH_ACCOUNTS, displayName: "Bitcoin Regtest P2PKH", }, ADDRESS_FORMAT_BITCOIN_REGTEST_P2SH: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_BITCOIN_REGTEST_P2SH_ACCOUNTS, displayName: "Bitcoin Regtest P2SH", }, ADDRESS_FORMAT_BITCOIN_REGTEST_P2WPKH: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_BITCOIN_REGTEST_P2WPKH_ACCOUNTS, displayName: "Bitcoin Regtest P2WPKH", }, ADDRESS_FORMAT_BITCOIN_REGTEST_P2WSH: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_BITCOIN_REGTEST_P2WSH_ACCOUNTS, displayName: "Bitcoin Regtest P2WSH", }, ADDRESS_FORMAT_BITCOIN_REGTEST_P2TR: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_BITCOIN_REGTEST_P2TR_ACCOUNTS, displayName: "Bitcoin Regtest P2TR", }, ADDRESS_FORMAT_SEI: { encoding: "PAYLOAD_ENCODING_TEXT_UTF8", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_SEI_ACCOUNTS, displayName: "Sei", }, ADDRESS_FORMAT_XLM: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_NOT_APPLICABLE", defaultAccounts: turnkeyHelpers.DEFAULT_XLM_ACCOUNTS, displayName: "Xlm", }, ADDRESS_FORMAT_DOGE_MAINNET: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_DOGE_MAINNET_ACCOUNTS, displayName: "Doge Mainnet", }, ADDRESS_FORMAT_DOGE_TESTNET: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_DOGE_TESTNET_ACCOUNTS, displayName: "Doge Testnet", }, ADDRESS_FORMAT_TON_V3R2: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_NOT_APPLICABLE", defaultAccounts: turnkeyHelpers.DEFAULT_TON_V3R2_ACCOUNTS, displayName: "Ton V3R2", }, ADDRESS_FORMAT_TON_V4R2: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_NOT_APPLICABLE", defaultAccounts: turnkeyHelpers.DEFAULT_TON_V4R2_ACCOUNTS, displayName: "Ton V4R2", }, ADDRESS_FORMAT_TON_V5R1: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_NOT_APPLICABLE", defaultAccounts: null, displayName: "Ton V5R1", }, ADDRESS_FORMAT_XRP: { encoding: "PAYLOAD_ENCODING_HEXADECIMAL", hashFunction: "HASH_FUNCTION_SHA256", defaultAccounts: turnkeyHelpers.DEFAULT_XRP_ACCOUNTS, displayName: "XRP", }, }; const googleISS = "https://accounts.google.com"; const isReactNative = () => { const g = typeof globalThis !== "undefined" ? globalThis : global; // if we have a DOM, it's definitely not RN // RN-web has DOM but we want false for that anyway if (typeof document !== "undefined" && typeof window !== "undefined") return false; // check for RN-specific globals // these shouldn't exist in Node, browsers, or webviews return (typeof g?.__fbBatchedBridge !== "undefined" || typeof g?.nativeCallSyncHook !== "undefined" || typeof g?.RN$Bridgeless !== "undefined"); }; const isWeb = () => { return typeof window !== "undefined" && typeof document !== "undefined"; }; const generateRandomBuffer = () => { const arr = new Uint8Array(32); crypto.getRandomValues(arr); return arr.buffer; }; Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0")); const toExternalTimestamp = (date = new Date()) => { const millis = date.getTime(); const seconds = Math.floor(millis / 1000); const nanos = (millis % 1000) * 1_000_000; return { seconds: seconds.toString(), nanos: nanos.toString(), }; }; async function getActiveSessionOrThrowIfRequired(stampWith, getActiveSession) { const session = await getActiveSession(); // the api-key stamper requires an active session // if there is no stampWith defined, the default is api-key stamper if ((!stampWith || stampWith === enums.StamperType.ApiKey) && !session) { throw new sdkTypes.TurnkeyError("No active session found. Please log in first.", sdkTypes.TurnkeyErrorCodes.NO_SESSION_FOUND); } return session; } function parseSession(token) { if (typeof token !== "string") { return token; } const [, payload] = token.split("."); if (!payload) { throw new Error("Invalid JWT: Missing payload"); } const decoded = JSON.parse(atob(payload)); const { exp, public_key: publicKey, session_type: sessionType, user_id: userId, organization_id: organizationId, } = decoded; if (!exp || !publicKey || !sessionType || !userId || !organizationId) { throw new Error("JWT payload missing required fields"); } const expSeconds = Math.ceil((exp * 1000 - Date.now()) / 1000); return { sessionType, userId, organizationId, expiry: exp, expirationSeconds: expSeconds.toString(), publicKey, token, }; } function getHashFunction(addressFormat) { const config = addressFormatConfig[addressFormat]; if (!config) { throw new sdkTypes.TurnkeyError(`Unsupported address format: ${addressFormat}`, sdkTypes.TurnkeyErrorCodes.INVALID_REQUEST); } return config.hashFunction; } function getEncodingType(addressFormat) { const config = addressFormatConfig[addressFormat]; if (!config) { throw new sdkTypes.TurnkeyError(`Unsupported address format: ${addressFormat}`, sdkTypes.TurnkeyErrorCodes.INVALID_REQUEST); } return config.encoding; } function getEncodedMessage(payloadEncoding, rawMessage) { if (payloadEncoding === "PAYLOAD_ENCODING_HEXADECIMAL") { return ("0x" + Array.from(rawMessage) .map((b) => b.toString(16).padStart(2, "0")) .join("")); } // we decode back to a UTF-8 string return ethers.toUtf8String(rawMessage); } function hexSignedTxToBase58(hex) { const bytes = encoding.uint8ArrayFromHexString(hex); return encoding.bs58.encode(bytes); } const broadcastTransaction = async (params) => { const { signedTransaction, rpcUrl, transactionType } = params; switch (transactionType) { case "TRANSACTION_TYPE_SOLANA": { const encodedTx = hexSignedTxToBase58(signedTransaction); const response = await fetch(rpcUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "sendTransaction", params: [encodedTx, { encoding: "base58" }], }), }); const json = await response.json(); if (json.error) { throw new sdkTypes.TurnkeyError(`Solana RPC Error: ${json.error.message}`, sdkTypes.TurnkeyErrorCodes.SIGN_AND_SEND_TRANSACTION_ERROR); } return json.result; } case "TRANSACTION_TYPE_ETHEREUM": { const response = await fetch(rpcUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_sendRawTransaction", params: [signedTransaction], }), }); const json = await response.json(); if (json.error) { throw new sdkTypes.TurnkeyError(`Ethereum RPC Error: ${json.error.message}`, sdkTypes.TurnkeyErrorCodes.SIGN_AND_SEND_TRANSACTION_ERROR); } return json.result; } case "TRANSACTION_TYPE_TRON": { const response = await fetch(`${rpcUrl}/wallet/broadcasthex`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ transaction: signedTransaction }), }); const json = await response.json(); if (!json.result) { throw new sdkTypes.TurnkeyError(`Tron RPC Error: ${json.message}`, sdkTypes.TurnkeyErrorCodes.SIGN_AND_SEND_TRANSACTION_ERROR); } return json.txid; } default: throw new sdkTypes.TurnkeyError(`Unsupported transaction type for broadcasting: ${transactionType}`, sdkTypes.TurnkeyErrorCodes.SIGN_AND_SEND_TRANSACTION_ERROR); } }; function splitSignature(signature, addressFormat) { const hex = signature.replace(/^0x/, ""); if (addressFormat === "ADDRESS_FORMAT_ETHEREUM") { // this is a ECDSA signature if (hex.length === 130) { const r = hex.slice(0, 64); const s = hex.slice(64, 128); const v = hex.slice(128, 130); return { r, s, v }; } // this is a DER-encoded signatures (e.g., Ledger) const raw = crypto$1.fromDerSignature(hex); const r = encoding.uint8ArrayToHexString(raw.slice(0, 32)); const s = encoding.uint8ArrayToHexString(raw.slice(32, 64)); // DER signatures do not have a v component // so we return 00 to match what Turnkey does const v = "00"; return { r, s, v }; } if (addressFormat === "ADDRESS_FORMAT_SOLANA") { if (hex.length !== 128) { throw new Error(`Invalid Solana signature length: expected 64 bytes (128 hex), got ${hex.length}`); } // this is a Ed25519 signature const r = hex.slice(0, 64); const s = hex.slice(64, 128); // solana signatures do not have a v component // so we return 00 to match what Turnkey does return { r, s, v: "00" }; } throw new Error(`Unsupported address format or invalid signature length: ${hex.length}`); } // Type guard to check if accounts is WalletAccount[] function isWalletAccountArray(arr) { return (arr.length === 0 || (typeof arr[0] === "object" && "addressFormat" in arr[0] && "curve" in arr[0] && "path" in arr[0] && "pathFormat" in arr[0])); } function createWalletAccountFromAddressFormat(addressFormat) { const walletAccount = addressFormatConfig[addressFormat]?.defaultAccounts; if (!walletAccount) { throw new Error(`Unsupported address format: ${addressFormat}`); } if (walletAccount[0]) { return walletAccount[0]; } throw new Error(`No default accounts defined for address format: ${addressFormat}`); } /**@internal */ function generateWalletAccountsFromAddressFormat(params) { const { addresses, existingWalletAccounts } = params; const pathMap = new Map(); // Build a lookup for max index per (addressFormat, basePath) const maxIndexMap = new Map(); if (existingWalletAccounts && existingWalletAccounts.length > 0) { for (const acc of existingWalletAccounts) { // Normalize base path (remove account index) const basePath = acc.path.replace(/^((?:[^\/]+\/){3})[^\/]+/, "$1"); const key = `${acc.addressFormat}:${basePath}`; const idxSegment = acc.path.split("/")[3]; const idx = idxSegment ? parseInt(idxSegment.replace(/'/, ""), 10) : -1; if (!isNaN(idx)) { maxIndexMap.set(key, Math.max(maxIndexMap.get(key) ?? -1, idx)); } } } return addresses.map((addressFormat) => { const account = createWalletAccountFromAddressFormat(addressFormat); const basePath = account.path.replace(/^((?:[^\/]+\/){3})[^\/]+/, "$1"); const key = `${addressFormat}:${basePath}`; let nextIndex = 0; if (maxIndexMap.has(key)) { nextIndex = maxIndexMap.get(key) + 1; maxIndexMap.set(key, nextIndex); } else if (pathMap.has(account.path)) { nextIndex = pathMap.get(account.path); } const pathWithIndex = account.path.replace(/^((?:[^\/]*\/){3})(\d+)/, (_, prefix) => `${prefix}${nextIndex}`); pathMap.set(account.path, nextIndex + 1); return { ...account, path: pathWithIndex, }; }); } function buildSignUpBody(params) { const { createSubOrgParams } = params; const authenticatorName = isWeb() ? `${window.location.hostname}-${Date.now()}` : `passkey-${Date.now()}`; let authenticators = []; if (createSubOrgParams?.authenticators?.length) { authenticators = createSubOrgParams?.authenticators?.map((authenticator) => ({ authenticatorName: authenticator?.authenticatorName || authenticatorName, challenge: authenticator.challenge, attestation: authenticator.attestation, })) || []; } let apiKeys = []; if (createSubOrgParams?.apiKeys?.length) { apiKeys = createSubOrgParams.apiKeys .filter((apiKey) => apiKey.curveType !== undefined) .map((apiKey) => ({ apiKeyName: apiKey.apiKeyName || `api-key-${Date.now()}`, publicKey: apiKey.publicKey, curveType: apiKey.curveType, ...(apiKey?.expirationSeconds && { expirationSeconds: apiKey.expirationSeconds, }), })); } return { userName: createSubOrgParams?.userName || createSubOrgParams?.userEmail || `user-${Date.now()}`, ...(createSubOrgParams?.userEmail && { userEmail: createSubOrgParams?.userEmail, }), ...(createSubOrgParams?.authenticators?.length ? { authenticators, } : { authenticators: [] }), ...(createSubOrgParams?.userPhoneNumber && { userPhoneNumber: createSubOrgParams.userPhoneNumber, }), ...(createSubOrgParams?.userTag && { userTag: createSubOrgParams?.userTag, }), organizationName: createSubOrgParams?.subOrgName || `sub-org-${Date.now()}`, ...(createSubOrgParams?.verificationToken && { verificationToken: createSubOrgParams?.verificationToken, }), ...(createSubOrgParams?.apiKeys?.length ? { apiKeys, } : { apiKeys: [] }), ...(createSubOrgParams?.oauthProviders?.length ? { oauthProviders: createSubOrgParams.oauthProviders, } : { oauthProviders: [] }), ...(createSubOrgParams?.customWallet && { wallet: { walletName: createSubOrgParams.customWallet.walletName, accounts: createSubOrgParams.customWallet.walletAccounts, }, }), }; } /** * Extracts the public key from a Turnkey stamp header value. * @param stampHeaderValue - The base64url encoded stamp header value * @returns The public key as a hex string */ function getPublicKeyFromStampHeader(stampHeaderValue) { try { // we decode the base64url string to get the JSON stamp const stampJson = encoding.decodeBase64urlToString(stampHeaderValue); // we parse the JSON to get the stamp object const stamp = JSON.parse(stampJson); return stamp.publicKey; } catch (error) { throw new Error(`Failed to extract public key from stamp header: ${error instanceof Error ? error.message : String(error)}`); } } /**@internal */ function getPolicySignature(policy) { return JSON.stringify({ policyName: policy.policyName, effect: policy.effect, condition: policy.condition ?? null, consensus: policy.consensus ?? null, }); } /**@internal */ function isEthereumProvider(provider) { return provider.chainInfo.namespace === enums.Chain.Ethereum; } /**@internal */ function isSolanaProvider(provider) { return provider.chainInfo.namespace === enums.Chain.Solana; } /** @internal */ function getCurveTypeFromProvider(provider) { if (isEthereumProvider(provider)) { return "API_KEY_CURVE_SECP256K1"; } if (isSolanaProvider(provider)) { return "API_KEY_CURVE_ED25519"; } // we should never hit this case // if we do then it means we added support for a new chain but missed updating this function throw new Error(`Unsupported provider namespace: ${provider.chainInfo.namespace}. Expected Ethereum or Solana.`); } /** @internal */ function getSignatureSchemeFromProvider(provider) { if (isEthereumProvider(provider)) { return "SIGNATURE_SCHEME_TK_API_SECP256K1_EIP191"; } if (isSolanaProvider(provider)) { return "SIGNATURE_SCHEME_TK_API_ED25519"; } // we should never hit this case // if we do then it means we added support for a new chain but missed updating this function throw new Error(`Unsupported provider namespace: ${provider.chainInfo.namespace}. Expected Ethereum or Solana.`); } /** @internal */ function findWalletProviderFromAddress(address, providers) { for (const provider of providers) { if (provider.connectedAddresses.includes(address)) { return provider; } } // no provider found for that address return undefined; } /** * Derives a wallet address from a given public key and chain. * * @param chain - "ethereum" or "solana" * @param publicKey - The raw public key string (can be compressed or uncompressed) * @returns The derived wallet address */ function addressFromPublicKey(chain, publicKey) { if (chain === enums.Chain.Ethereum) { const publicKeyBytes = encoding.uint8ArrayFromHexString(publicKey); let uncompressedKey; if (publicKeyBytes.length === 65 && publicKeyBytes[0] === 0x04) { // it's already uncompressed so we just convert // to hex without the 04 prefix uncompressedKey = encoding.uint8ArrayToHexString(publicKeyBytes.slice(1)); } else { // it's compressed, so we need to uncompress it first // then convert to hex without the 04 prefix const publicKeyUncompressed = crypto$1.uncompressRawPublicKey(publicKeyBytes, enums.Curve.SECP256K1); uncompressedKey = encoding.uint8ArrayToHexString(publicKeyUncompressed.slice(1)); } // hash with Keccak256 and take last 20 bytes const hash = ethers.keccak256(encoding.uint8ArrayFromHexString(uncompressedKey)); return "0x" + hash.slice(-40); } if (chain === enums.Chain.Solana) { return encoding.bs58.encode(encoding.uint8ArrayFromHexString(publicKey)); } throw new Error(`Unsupported chain: ${chain}`); } /**@internal */ function getAuthenticatorAddresses(user) { const ethereum = []; const solana = []; for (const key of user.apiKeys) { const { type, publicKey } = key.credential; switch (type) { case "CREDENTIAL_TYPE_API_KEY_SECP256K1": ethereum.push(addressFromPublicKey(enums.Chain.Ethereum, publicKey)); break; case "CREDENTIAL_TYPE_API_KEY_ED25519": solana.push(addressFromPublicKey(enums.Chain.Solana, publicKey)); break; } } return { ethereum, solana }; } /**@internal */ async function getAuthProxyConfig(authProxyConfigId, authProxyUrl) { const fullUrl = (authProxyUrl ?? "https://authproxy.turnkey.com") + "/v1/wallet_kit_config"; var headers = { "Content-Type": "application/json", "X-Auth-Proxy-Config-ID": authProxyConfigId, }; const response = await fetch(fullUrl, { method: "POST", headers: headers, }); if (!response.ok) { let res; try { res = await response.json(); } catch (_) { throw new Error(`${response.status} ${response.statusText}`); } throw new error.TurnkeyRequestError(res); } const data = await response.json(); return data; } /** * Submits a signed request to Turnkey. * * You can pass in the SignedRequest returned by any of the SDK's * stamping methods (stampStampLogin, stampGetPolicies, etc.). * * @deprecated Use `httpClient.sendSignedRequest()` instead, which includes * automatic activity polling and result extraction. * * @param signedRequest A SignedRequest object returned by a stamping method. * @returns The parsed JSON response from Turnkey. * @throws TurnkeyNetworkError if the request fails. */ // TODO: (breaking change) remove this function async function sendSignedRequest(signedRequest) { const headers = { "Content-Type": "application/json", "X-Client-Version": version.VERSION, [signedRequest.stamp.stampHeaderName]: signedRequest.stamp.stampHeaderValue, }; const res = await fetch(signedRequest.url, { method: "POST", headers, body: signedRequest.body, }); if (!res.ok) { const errorText = await res.text(); throw new sdkTypes.TurnkeyNetworkError("Signed request failed", res.status, sdkTypes.TurnkeyErrorCodes.BAD_RESPONSE, errorText); } return res.json(); } /** * @internal * Executes an async function with error handling. * * @param fn The async function to execute with error handling * @param errorOptions Options for customizing error handling * @param errorOptions.catchFn Optional function to execute in the catch block * @param errorOptions.errorMessage The default error message to use if no custom message is found * @param errorOptions.errorCode The default error code to use if no custom message is found * @param errorOptions.customErrorsByCodes Optional mapping of error codes to custom messages, if you're trying to target a specific error code and surface a custom message, use this * @param errorOptions.customErrorsByMessages Optional mapping of error messages to custom messages, if you're trying to target a specific error message and surface a custom message, use this * @param finallyFn Optional function to execute in the finally block * @returns The result of the async function or throws an error */ async function withTurnkeyErrorHandling(fn, catchOptions, finallyOptions) { const { errorMessage, errorCode, customErrorsByCodes, catchFn } = catchOptions; // Merge global error mappings with any caller-provided ones. // - Start with the globals so they’re always available. // - Spread the caller’s entries last so they override globals on conflicts. // - If the caller didn’t provide any, just fall back to the globals. const customErrorsByMessages = catchOptions.customErrorsByMessages ? { ...globalErrorsToMatch, ...catchOptions.customErrorsByMessages } : globalErrorsToMatch; const finallyFn = finallyOptions?.finallyFn; try { return await fn(); } catch (error$1) { await catchFn?.(); // some things throw plain objects (not Error instances), which would stringify as `[object Object]` // we normalize here to always produce a readable error message before wrapping it in TurnkeyError. const normalizedMessage = error$1 instanceof Error && typeof error$1.message === "string" ? error$1.message : typeof error$1?.message === "string" ? error$1.message : JSON.stringify(error$1); if (error$1 instanceof sdkTypes.TurnkeyError) { const customCodeMessage = customErrorsByCodes?.[error$1.code]; if (customCodeMessage) { throw new sdkTypes.TurnkeyError(customCodeMessage.message, customCodeMessage.code, error$1); } throwMatchingMessage(error$1.message, customErrorsByMessages, error$1); throw error$1; } else if (error$1 instanceof error.TurnkeyRequestError) { throwMatchingMessage(normalizedMessage, customErrorsByMessages, error$1); throw new sdkTypes.TurnkeyError(errorMessage, errorCode, error$1); } else if (error$1 instanceof Error) { throwMatchingMessage(normalizedMessage, customErrorsByMessages, error$1); // Wrap other errors in a TurnkeyError throw new sdkTypes.TurnkeyError(errorMessage, errorCode, error$1); } else { throwMatchingMessage(normalizedMessage, customErrorsByMessages, error$1); // Handle non-Error exceptions throw new sdkTypes.TurnkeyError(normalizedMessage, errorCode, error$1); } } finally { await finallyFn?.(); } } /** * Throws a TurnkeyError with a custom message if the error message matches any key in customMessageByMessages. * If no match is found, it does nothing. * * @param errorMessage The error message to check against the custom messages. * @param customErrorsByMessages An object mapping error messages to custom messages and codes. * @param error The original error that triggered this function. */ const throwMatchingMessage = (errorMessage, customErrorsByMessages, error) => { if (customErrorsByMessages && Object.keys(customErrorsByMessages).length > 0) { Object.keys(customErrorsByMessages).forEach((key) => { if (errorMessage.includes(key)) { throw new sdkTypes.TurnkeyError(customErrorsByMessages[key].message, customErrorsByMessages[key].code, error); } }); } }; /** * @internal * * Asserts that the provided key pair is a valid P-256 ECDSA key pair. * @param pair The key pair to validate. */ async function assertValidP256ECDSAKeyPair(pair) { const { privateKey, publicKey } = pair; // Check basic shape if (!(privateKey instanceof CryptoKey) || !(publicKey instanceof CryptoKey)) { throw new sdkTypes.TurnkeyError("Both keys must be CryptoKey instances.", sdkTypes.TurnkeyErrorCodes.INVALID_REQUEST); } if (privateKey.type !== "private") throw new sdkTypes.TurnkeyError("privateKey.type must be 'private'.", sdkTypes.TurnkeyErrorCodes.INVALID_REQUEST); if (publicKey.type !== "public") throw new sdkTypes.TurnkeyError("publicKey.type must be 'public'.", sdkTypes.TurnkeyErrorCodes.INVALID_REQUEST); // Verify extractability and usages if (privateKey.extractable !== false) { throw new sdkTypes.TurnkeyError("Provided privateKey must be non-extractable.", sdkTypes.TurnkeyErrorCodes.INVALID_REQUEST); } if (!privateKey.usages.includes("sign")) { throw new sdkTypes.TurnkeyError("privateKey must have 'sign' in keyUsages.", sdkTypes.TurnkeyErrorCodes.INVALID_REQUEST); } if (!publicKey.usages.includes("verify")) { throw new sdkTypes.TurnkeyError("publicKey must have 'verify' in keyUsages.", sdkTypes.TurnkeyErrorCodes.INVALID_REQUEST); } // Algorithm checks (must be ECDSA on P-256) const pAlg = privateKey.algorithm; const pubAlg = publicKey.algorithm; if (pAlg.name !== "ECDSA" || pubAlg.name !== "ECDSA") { throw new sdkTypes.TurnkeyError("Keys must be ECDSA keys.", sdkTypes.TurnkeyErrorCodes.INVALID_REQUEST); } if (pAlg.namedCurve !== "P-256" || pubAlg.namedCurve !== "P-256") { throw new sdkTypes.TurnkeyError("Keys must be on the P-256 curve.", sdkTypes.TurnkeyErrorCodes.INVALID_REQUEST); } // Public key export sanity (should be uncompressed 65 bytes starting with 0x04) const rawPub = new Uint8Array(await crypto.subtle.exportKey("raw", publicKey)); if (rawPub.length !== 65 || rawPub[0] !== 0x04) { throw new sdkTypes.TurnkeyError("Public key must be an uncompressed P-256 point (65 bytes, leading 0x04).", sdkTypes.TurnkeyErrorCodes.INVALID_REQUEST); } // Prove the pair matches: sign→verify a test message const msg = crypto.getRandomValues(new Uint8Array(32)); const sig = await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, privateKey, msg); const ok = await crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, publicKey, sig, msg); if (!ok) { throw new sdkTypes.TurnkeyError("publicKey does not match privateKey (verify failed).", sdkTypes.TurnkeyErrorCodes.INVALID_REQUEST); } } function isValidPasskeyName(name) { const nameRegex = isReactNative() ? /^[a-zA-Z0-9 _\-:\/\.]{1,64}$/ : /^[a-zA-Z0-9 _\-:\/\.]+$/; if (!nameRegex.test(name)) { throw new sdkTypes.TurnkeyError("Passkey name must be 1-64 characters and only contain letters, numbers, spaces, dashes, underscores, colons, or slashes.", sdkTypes.TurnkeyErrorCodes.INVALID_REQUEST); } return name; } function mapAccountsToWallet(accounts, walletMap) { // map of walletId to Wallet // map all wallet accounts to their wallets accounts.forEach(async (account) => { if (walletMap.has(account.walletDetails.walletId)) { const wallet = walletMap.get(account.walletDetails.walletId); wallet.accounts.push({ ...account, source: enums.WalletSource.Embedded, }); return; } else { walletMap.set(account.walletDetails.walletId, { source: enums.WalletSource.Embedded, walletId: account.walletDetails.walletId, walletName: account.walletDetails.walletName, createdAt: account.walletDetails.createdAt, updatedAt: account.walletDetails.updatedAt, exported: account.walletDetails.exported, imported: account.walletDetails.imported, accounts: [ { ...account, source: enums.WalletSource.Embedded, }, ], }); } }); return Array.from(walletMap.values()); } async function withTimeout(promise, ms, label) { let timeout; const timer = new Promise((_, reject) => { timeout = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms); }); return Promise.race([promise, timer]).finally(() => clearTimeout(timeout)); } async function fetchAllWalletAccountsWithCursor(httpClient, organizationId, stampWith) { let hasMore = true; let cursor; const accounts = []; const limit = 100; while (hasMore) { const response = await httpClient.getWalletAccounts({ organizationId, includeWalletDetails: true, paginationOptions: { limit: limit.toString(), ...(cursor && { after: cursor }), }, }, stampWith); if (!response || !response.accounts) { throw new sdkTypes.TurnkeyError("No wallet accounts found in the response", sdkTypes.TurnkeyErrorCodes.BAD_RESPONSE); } accounts.push(...response.accounts); hasMore = response.accounts.length === limit; cursor = response.accounts && response.accounts.length > 0 ? response.accounts[response.accounts.length - 1]?.walletAccountId : undefined; } return accounts; } function decodeVerificationToken(verificationToken) { const [, payloadB64] = verificationToken.split("."); if (!payloadB64) { throw new Error("Invalid token: missing payload"); } const json = atob(payloadB64); return JSON.parse(json); } function getClientSignatureMessageForLogin({ verificationToken, sessionPublicKey = undefined, }) { try { const decoded = decodeVerificationToken(verificationToken); if (!decoded.public_key) throw new sdkTypes.TurnkeyError("Invalid verification token: missing publicKey", sdkTypes.TurnkeyErrorCodes.INVALID_REQUEST); const verificationPublicKey = decoded.public_key; // if a session public key is provided, we use it instead const resolvedSessionPublicKey = sessionPublicKey || verificationPublicKey; const usage = { publicKey: resolvedSessionPublicKey }; const payload = { login: usage, tokenId: decoded.id, type: "USAGE_TYPE_LOGIN", }; const json = JSON.stringify(payload); return { message: json, publicKey: verificationPublicKey }; } catch (error) { throw new sdkTypes.TurnkeyError("Failed to create client signature bundle for login", sdkTypes.TurnkeyErrorCodes.UNKNOWN, error); } } function getClientSignatureMessageForSignup({ verificationToken, email, phoneNumber, apiKeys, authenticators, oauthProviders, }) { try { const decoded = decodeVerificationToken(verificationToken); if (!decoded.public_key) throw new sdkTypes.TurnkeyError("Invalid verification token: missing publicKey", sdkTypes.TurnkeyErrorCodes.INVALID_REQUEST); const verificationPublicKey = decoded.public_key; const usage = { ...(apiKeys ? { apiKeys } : {}), ...(authenticators ? { authenticators } : {}), ...(oauthProviders ? { oauthProviders } : {}), ...(email ? { email } : {}), ...(phoneNumber ? { phoneNumber } : {}), }; const payload = { signup: usage, tokenId: decoded.id, type: "USAGE_TYPE_SIGNUP", }; const json = JSON.stringify(payload); return { message: json, publicKey: verificationPublicKey }; } catch (error) { throw new sdkTypes.TurnkeyError("Failed to create client signature bundle for signup", sdkTypes.TurnkeyErrorCodes.UNKNOWN, error); } } /** * Wraps a promise with a timeout. If the promise doesn't resolve within * the specified duration, it resolves to the fallback value instead of throwing. * * @param promise - The promise to wrap. * @param fallback - Value to return if the timeout is reached. * @param timeoutMs - Timeout duration in milliseconds. Defaults to 1000ms. * @returns The result of the promise, or the fallback if timed out. */ const withTimeoutFallback = (promise, fallback, timeoutMs) => { const timeout = 1000; return Promise.race([ promise, new Promise((resolve) => setTimeout(() => resolve(fallback), timeout)), ]); }; exports.addressFormatConfig = addressFormatConfig; exports.addressFromPublicKey = addressFromPublicKey; exports.assertValidP256ECDSAKeyPair = assertValidP256ECDSAKeyPair; exports.broadcastTransaction = broadcastTransaction; exports.buildSignUpBody = buildSignUpBody; exports.createWalletAccountFromAddressFormat = createWalletAccountFromAddressFormat; exports.decodeVerificationToken = decodeVerificationToken; exports.fetchAllWalletAccountsWithCursor = fetchAllWalletAccountsWithCursor; exports.findWalletProviderFromAddress = findWalletProviderFromAddress; exports.generateRandomBuffer = generateRandomBuffer; exports.generateWalletAccountsFromAddressFormat = generateWalletAccountsFromAddressFormat; exports.getActiveSessionOrThrowIfRequired = getActiveSessionOrThrowIfRequired; exports.getAuthProxyConfig = getAuthProxyConfig; exports.getAuthenticatorAddresses = getAuthenticatorAddresses; exports.getClientSignatureMessageForLogin = getClientSignatureMessageForLogin; exports.getClientSignatureMessageForSignup = getClientSignatureMessageForSignup; exports.getCurveTypeFromProvider = getCurveTypeFromProvider; exports.getEncodedMessage = getEncodedMessage; exports.getEncodingType = getEncodingType; exports.getHashFunction = getHashFunction; exports.getPolicySignature = getPolicySignature; exports.getPublicKeyFromStampHeader = getPublicKeyFromStampHeader; exports.getSignatureSchemeFromProvider = getSignatureSchemeFromProvider; exports.googleISS = googleISS; exports.hexSignedTxToBase58 = hexSignedTxToBase58; exports.isEthereumProvider = isEthereumProvider; exports.isReactNative = isReactNative; exports.isSolanaProvider = isSolanaProvider; exports.isValidPasskeyName = isValidPasskeyName; exports.isWalletAccountArray = isWalletAccountArray; exports.isWeb = isWeb; exports.mapAccountsToWallet = mapAccountsToWallet; exports.parseSession = parseSession; exports.sendSignedRequest = sendSignedRequest; exports.splitSignature = splitSignature; exports.toExternalTimestamp = toExternalTimestamp; exports.withTimeout = withTimeout; exports.withTimeoutFallback = withTimeoutFallback; exports.withTurnkeyErrorHandling = withTurnkeyErrorHandling; //# sourceMappingURL=utils.js.map