UNPKG

gill

Version:

a modern javascript/typescript client library for interacting with the Solana blockchain

448 lines (438 loc) 17.9 kB
import { createNoopSigner, isTransactionSigner, assertIsTransactionSigner, pipe, createTransactionMessage, setTransactionMessageLifetimeUsingBlockhash, setTransactionMessageFeePayerSigner, setTransactionMessageFeePayer, appendTransactionMessageInstruction, appendTransactionMessageInstructions, sendAndConfirmTransactionFactory, signTransactionMessageWithSigners, getSignatureFromTransaction, getBase64EncodedWireTransaction, compileTransaction, partiallySignTransactionMessageWithSigners, getComputeUnitEstimateForTransactionMessageFactory, assertIsTransactionMessageWithBlockhashLifetime, createSolanaRpc, createSolanaRpcSubscriptions, createSignerFromKeyPair, createKeyPairFromBytes, getBase58Encoder, getTransactionDecoder, getBase64Encoder, SolanaError, SOLANA_ERROR__TRANSACTION_ERROR__UNKNOWN, isSolanaError, SOLANA_ERROR__INSTRUCTION_ERROR__GENERIC_ERROR, AccountRole, isInstructionForProgram, isInstructionWithData } from '@solana/kit'; export * from '@solana/kit'; import { getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction, COMPUTE_BUDGET_PROGRAM_ADDRESS, ComputeBudgetInstruction } from '@solana-program/compute-budget'; import { assertKeyExporterIsAvailable, assertKeyGenerationIsAvailable } from '@solana/assertions'; // src/index.ts // src/core/debug.ts var GILL_LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 }; var getMinLogLevel = () => process.env.GILL_DEBUG_LEVEL || global.__GILL_DEBUG_LEVEL__ || typeof window !== "undefined" && window.__GILL_DEBUG_LEVEL__ || "info"; var isDebugEnabled = () => Boolean( process.env.GILL_DEBUG_LEVEL || global.__GILL_DEBUG_LEVEL__ || process.env.GILL_DEBUG === "true" || process.env.GILL_DEBUG === "1" || global.__GILL_DEBUG__ === true || typeof window !== "undefined" && window.__GILL_DEBUG__ === true ); function debug(message, level = "info", prefix = "[GILL]") { if (!isDebugEnabled()) return; if (GILL_LOG_LEVELS[level] < GILL_LOG_LEVELS[getMinLogLevel()]) return; const formattedMessage = typeof message === "string" ? message : JSON.stringify(message, null, 2); switch (level) { case "debug": console.log(prefix, formattedMessage); break; case "info": console.info(prefix, formattedMessage); break; case "warn": console.warn(prefix, formattedMessage); break; case "error": console.error(prefix, formattedMessage); break; } } // src/core/const.ts var LAMPORTS_PER_SOL = 1e9; var GENESIS_HASH = { mainnet: "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d", devnet: "EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG", testnet: "4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY" }; function getMonikerFromGenesisHash(hash) { switch (hash) { case GENESIS_HASH.mainnet: return "mainnet"; case GENESIS_HASH.devnet: return "devnet"; case GENESIS_HASH.testnet: return "testnet"; default: return "unknown"; } } function checkedAddress(input) { return typeof input == "string" ? input : input.address; } function checkedTransactionSigner(input) { if (typeof input === "string" || "address" in input == false) input = createNoopSigner(input); if (!isTransactionSigner(input)) throw new Error("A signer or address is required"); assertIsTransactionSigner(input); return input; } function lamportsToSol(lamports) { return new Intl.NumberFormat("en-US", { maximumFractionDigits: 9 }).format(`${lamports}E-9`); } // src/core/rpc.ts function localnet(putativeString) { return putativeString; } function getPublicSolanaRpcUrl(cluster) { switch (cluster) { case "devnet": return "https://api.devnet.solana.com"; case "testnet": return "https://api.testnet.solana.com"; case "mainnet-beta": case "mainnet": return "https://api.mainnet-beta.solana.com"; case "localnet": case "localhost": return "http://127.0.0.1:8899"; default: throw new Error("Invalid cluster moniker"); } } // src/core/explorer.ts function getExplorerLink(props = {}) { let url = new URL("https://explorer.solana.com"); if (!props.cluster || props.cluster == "mainnet") props.cluster = "mainnet-beta"; if ("address" in props) { url.pathname = `/address/${props.address}`; } else if ("transaction" in props) { url.pathname = `/tx/${props.transaction}`; } else if ("block" in props) { url.pathname = `/block/${props.block}`; } if (props.cluster !== "mainnet-beta") { if (props.cluster === "localnet" || props.cluster === "localhost") { url.searchParams.set("cluster", "custom"); url.searchParams.set("customUrl", "http://localhost:8899"); } else { url.searchParams.set("cluster", props.cluster); } } return url.toString(); } function createTransaction({ version, feePayer, instructions, latestBlockhash, computeUnitLimit, computeUnitPrice }) { return pipe( createTransactionMessage({ version }), (tx) => { const withLifetime = latestBlockhash ? setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx) : tx; if (typeof feePayer !== "string" && "address" in feePayer && isTransactionSigner(feePayer)) { return setTransactionMessageFeePayerSigner(feePayer, withLifetime); } else return setTransactionMessageFeePayer(feePayer, withLifetime); }, (tx) => { const withComputeLimit = typeof computeUnitLimit !== "undefined" ? appendTransactionMessageInstruction( getSetComputeUnitLimitInstruction({ units: Number(computeUnitLimit) }), tx ) : tx; const withComputePrice = typeof computeUnitPrice !== "undefined" ? appendTransactionMessageInstruction( getSetComputeUnitPriceInstruction({ microLamports: Number(computeUnitPrice) }), withComputeLimit ) : withComputeLimit; return appendTransactionMessageInstructions(instructions, withComputePrice); } ); } function sendAndConfirmTransactionWithSignersFactory({ rpc, rpcSubscriptions }) { const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions }); return async function sendAndConfirmTransactionWithSigners(transaction, config = { commitment: "confirmed" }) { if ("messageBytes" in transaction == false) { transaction = await signTransactionMessageWithSigners(transaction); } debug(`Sending transaction: ${getExplorerLink({ transaction: getSignatureFromTransaction(transaction) })}`); debug(`Transaction as base64: ${getBase64EncodedWireTransaction(transaction)}`, "debug"); await sendAndConfirmTransaction(transaction, config); return getSignatureFromTransaction(transaction); }; } function isSetComputeLimitInstruction(instruction) { return isInstructionForProgram(instruction, COMPUTE_BUDGET_PROGRAM_ADDRESS) && isInstructionWithData(instruction) && instruction.data[0] === ComputeBudgetInstruction.SetComputeUnitLimit; } function transactionToBase64(tx) { if ("messageBytes" in tx) return pipe(tx, getBase64EncodedWireTransaction); else return pipe(tx, compileTransaction, getBase64EncodedWireTransaction); } async function transactionToBase64WithSigners(tx) { if ("messageBytes" in tx) return transactionToBase64(tx); else return transactionToBase64(await partiallySignTransactionMessageWithSigners(tx)); } // src/core/prepare-transaction.ts async function prepareTransaction(config) { if (!config.computeUnitLimitMultiplier) config.computeUnitLimitMultiplier = 1.1; if (config.blockhashReset !== false) config.blockhashReset = true; const computeBudgetIndex = { limit: -1, price: -1 }; config.transaction.instructions.map((ix, index) => { if (ix.programAddress != COMPUTE_BUDGET_PROGRAM_ADDRESS) return; if (isSetComputeLimitInstruction(ix)) { computeBudgetIndex.limit = index; } }); if (computeBudgetIndex.limit < 0 || config.computeUnitLimitReset) { const units = await getComputeUnitEstimateForTransactionMessageFactory({ rpc: config.rpc })(config.transaction); debug(`Obtained compute units from simulation: ${units}`, "debug"); const ix = getSetComputeUnitLimitInstruction({ units: units * config.computeUnitLimitMultiplier }); if (computeBudgetIndex.limit < 0) { config.transaction = appendTransactionMessageInstruction(ix, config.transaction); } else if (config.computeUnitLimitReset) { const nextInstructions = [...config.transaction.instructions]; nextInstructions.splice(computeBudgetIndex.limit, 1, ix); config.transaction = Object.freeze({ ...config.transaction, instructions: nextInstructions }); } } if (config.blockhashReset || "lifetimeConstraint" in config.transaction == false) { const { value: latestBlockhash } = await config.rpc.getLatestBlockhash().send(); if ("lifetimeConstraint" in config.transaction == false) { debug("Transaction missing latest blockhash, fetching one.", "debug"); config.transaction = setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, config.transaction); } else if (config.blockhashReset) { debug("Auto resetting the latest blockhash.", "debug"); config.transaction = Object.freeze({ ...config.transaction, lifetimeConstraint: latestBlockhash }); } } assertIsTransactionMessageWithBlockhashLifetime(config.transaction); if (isDebugEnabled()) { debug(`Transaction as base64: ${await transactionToBase64WithSigners(config.transaction)}`, "debug"); } return config.transaction; } function simulateTransactionFactory({ rpc }) { return async function simulateTransaction(transaction, config) { if ("messageBytes" in transaction == false) { transaction = await partiallySignTransactionMessageWithSigners(transaction); } return rpc.simulateTransaction(getBase64EncodedWireTransaction(transaction), { replaceRecentBlockhash: true, // innerInstructions: true, ...config, sigVerify: false, encoding: "base64" }).send(); }; } // src/core/create-solana-client.ts function createSolanaClient({ urlOrMoniker, rpcConfig, rpcSubscriptionsConfig }) { if (!urlOrMoniker) throw new Error("Cluster url or moniker is required"); if (urlOrMoniker instanceof URL == false) { try { urlOrMoniker = new URL(urlOrMoniker.toString()); } catch (err) { try { urlOrMoniker = new URL(getPublicSolanaRpcUrl(urlOrMoniker.toString())); } catch (err2) { throw new Error("Invalid URL or cluster moniker"); } } } if (!urlOrMoniker.protocol.match(/^https?/i)) { throw new Error("Unsupported protocol. Only HTTP and HTTPS are supported"); } if (rpcConfig?.port) { urlOrMoniker.port = rpcConfig.port.toString(); } const rpc = createSolanaRpc(urlOrMoniker.toString(), rpcConfig); urlOrMoniker.protocol = urlOrMoniker.protocol.replace("http", "ws"); if (rpcSubscriptionsConfig?.port) { urlOrMoniker.port = rpcSubscriptionsConfig.port.toString(); } else if (urlOrMoniker.hostname == "localhost" || urlOrMoniker.hostname.startsWith("127")) { urlOrMoniker.port = "8900"; } const rpcSubscriptions = createSolanaRpcSubscriptions( urlOrMoniker.toString(), rpcSubscriptionsConfig ); return { rpc, rpcSubscriptions, sendAndConfirmTransaction: sendAndConfirmTransactionWithSignersFactory({ // @ts-ignore - TODO(FIXME:nick) rpc, // @ts-ignore - TODO(FIXME:nick) rpcSubscriptions }), // @ts-ignore simulateTransaction: simulateTransactionFactory({ rpc }) }; } // src/core/accounts.ts function getMinimumBalanceForRentExemption(space = 0) { const RENT = { /** * Account storage overhead for calculation of base rent. (aka the number of bytes required to store an account with no data. */ ACCOUNT_STORAGE_OVERHEAD: 128n, /** * Amount of time (in years) a balance must include rent for the account to * be rent exempt. */ DEFAULT_EXEMPTION_THRESHOLD: BigInt(Math.floor(2 * 1e3)) / 1000n, /** * Default rental rate in lamports/byte-year. This calculation is based on: * - 10^9 lamports per SOL * - $1 per SOL * - $0.01 per megabyte day * - $3.65 per megabyte year */ DEFAULT_LAMPORTS_PER_BYTE_YEAR: BigInt( Math.floor(1e9 / 100 * 365 / (1024 * 1024)) ) }; return (RENT.ACCOUNT_STORAGE_OVERHEAD + BigInt(space)) * RENT.DEFAULT_LAMPORTS_PER_BYTE_YEAR * RENT.DEFAULT_EXEMPTION_THRESHOLD / 1n; } function assertKeyPairIsExtractable(keyPair) { assertKeyExporterIsAvailable(); if (!keyPair.privateKey) { throw new Error("Keypair is missing private key"); } if (!keyPair.publicKey) { throw new Error("Keypair is missing public key"); } if (!keyPair.privateKey.extractable) { throw new Error("Private key is not extractable"); } } async function generateExtractableKeyPair() { await assertKeyGenerationIsAvailable(); return crypto.subtle.generateKey( /* algorithm */ "Ed25519", // Native implementation status: https://github.com/WICG/webcrypto-secure-curves/issues/20 /* extractable */ true, /* allowed uses */ ["sign", "verify"] ); } async function generateExtractableKeyPairSigner() { return createSignerFromKeyPair(await generateExtractableKeyPair()); } async function extractBytesFromKeyPair(keypair) { assertKeyPairIsExtractable(keypair); const [publicKeyBytes, privateKeyJwk] = await Promise.all([ crypto.subtle.exportKey("raw", keypair.publicKey), crypto.subtle.exportKey("jwk", keypair.privateKey) ]); if (!privateKeyJwk.d) throw new Error("Failed to get private key bytes"); return new Uint8Array([...Buffer.from(privateKeyJwk.d, "base64"), ...new Uint8Array(publicKeyBytes)]); } async function extractBytesFromKeyPairSigner(keypairSigner) { return extractBytesFromKeyPair(keypairSigner.keyPair); } async function createKeypairFromBase58(punitiveSecretKey) { return createKeyPairFromBytes(getBase58Encoder().encode(punitiveSecretKey)); } async function createKeypairSignerFromBase58(punitiveSecretKey) { return createSignerFromKeyPair(await createKeypairFromBase58(punitiveSecretKey)); } function transactionFromBase64(base64EncodedTransaction) { return getTransactionDecoder().decode(getBase64Encoder().encode(base64EncodedTransaction)); } async function getOldestSignatureForAddress(rpc, address, config) { const signatures = await rpc.getSignaturesForAddress(address, config).send({ abortSignal: config?.abortSignal }); if (!signatures.length) { throw new SolanaError(SOLANA_ERROR__TRANSACTION_ERROR__UNKNOWN, { errorName: "OldestSignatureNotFound" }); } const oldest = signatures[signatures.length - 1]; if (signatures.length < (config?.limit || 1e3)) return oldest; try { return await getOldestSignatureForAddress(rpc, address, { ...config, before: oldest.signature }); } catch (err) { if (isSolanaError(err, SOLANA_ERROR__TRANSACTION_ERROR__UNKNOWN)) return oldest; throw err; } } function insertReferenceKeyToTransactionMessage(reference, transaction) { return insertReferenceKeysToTransactionMessage([reference], transaction); } function insertReferenceKeysToTransactionMessage(references, transaction) { const nonMemoIndex = transaction.instructions.findIndex( (ix) => ix.programAddress !== "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr" ); if (transaction.instructions.length == 0 || nonMemoIndex == -1) { throw new SolanaError(SOLANA_ERROR__INSTRUCTION_ERROR__GENERIC_ERROR, { index: transaction.instructions.length || nonMemoIndex, cause: "At least one non-memo instruction is required" }); } const modifiedIx = { ...transaction.instructions[nonMemoIndex], accounts: [ ...transaction.instructions[nonMemoIndex].accounts || [], // actually insert the reference keys ...references.map((ref) => ({ address: ref, role: AccountRole.READONLY })) ] }; const instructions = [...transaction.instructions]; instructions.splice(nonMemoIndex, 1, modifiedIx); return Object.freeze({ ...transaction, instructions: Object.freeze(instructions) }); } // src/core/create-codama-config.ts var GILL_EXTERNAL_MODULE_MAP = { solanaAccounts: "gill", solanaAddresses: "gill", solanaCodecsCore: "gill", solanaCodecsDataStructures: "gill", solanaCodecsNumbers: "gill", solanaCodecsStrings: "gill", solanaErrors: "gill", solanaInstructions: "gill", solanaOptions: "gill", solanaPrograms: "gill", solanaRpcTypes: "gill", solanaSigners: "gill" }; function createCodamaConfig({ idl, clientJs, clientRust, dependencyMap = GILL_EXTERNAL_MODULE_MAP }) { return { idl, scripts: { js: { args: [clientJs, { dependencyMap }], from: "@codama/renderers-js" }, rust: clientRust ? { from: "@codama/renderers-rust", args: [ clientRust, { crateFolder: "clients/rust", formatCode: true } ] } : void 0 } }; } export { GENESIS_HASH, GILL_EXTERNAL_MODULE_MAP, LAMPORTS_PER_SOL, assertKeyPairIsExtractable, checkedAddress, checkedTransactionSigner, createCodamaConfig, createKeypairFromBase58, createKeypairSignerFromBase58, createSolanaClient, createTransaction, debug, extractBytesFromKeyPair, extractBytesFromKeyPairSigner, generateExtractableKeyPair, generateExtractableKeyPairSigner, getExplorerLink, getMinimumBalanceForRentExemption, getMonikerFromGenesisHash, getOldestSignatureForAddress, getPublicSolanaRpcUrl, insertReferenceKeyToTransactionMessage, insertReferenceKeysToTransactionMessage, isDebugEnabled, lamportsToSol, localnet, prepareTransaction, sendAndConfirmTransactionWithSignersFactory, simulateTransactionFactory, transactionFromBase64, transactionToBase64, transactionToBase64WithSigners }; //# sourceMappingURL=index.native.mjs.map //# sourceMappingURL=index.native.mjs.map