gill
Version:
a modern javascript/typescript client library for interacting with the Solana blockchain
448 lines (438 loc) • 17.9 kB
JavaScript
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