UNPKG

@metaplex-foundation/umi-rpc-web3js

Version:

An RPC implementation relying on Solana's web3.js

509 lines (472 loc) 15 kB
import { ACCOUNT_HEADER_SIZE, BlockhashWithExpiryBlockHeight, Cluster, Commitment, CompiledInstruction, Context, createAmount, DateTime, dateTime, ErrorWithLogs, isZeroAmount, lamports, MaybeRpcAccount, ProgramError, PublicKey, resolveClusterFromEndpoint, RpcAccount, RpcAccountExistsOptions, RpcAirdropOptions, RpcCallOptions, RpcConfirmTransactionOptions, RpcConfirmTransactionResult, RpcDataFilter, RpcGetAccountOptions, RpcGetAccountsOptions, RpcGetBalanceOptions, RpcGetBlockTimeOptions, RpcGetLatestBlockhashOptions, RpcGetProgramAccountsOptions, RpcGetRentOptions, RpcGetSignatureStatusesOptions, RpcGetSlotOptions, RpcGetTransactionOptions, RpcInterface, RpcSendTransactionOptions, RpcSimulateTransactionOptions, RpcSimulateTransactionResult, SolAmount, Transaction, TransactionMetaInnerInstruction, TransactionMetaTokenBalance, TransactionSignature, TransactionStatus, TransactionWithMeta, } from '@metaplex-foundation/umi'; import { fromWeb3JsMessage, fromWeb3JsPublicKey, toWeb3JsPublicKey, toWeb3JsTransaction, } from '@metaplex-foundation/umi-web3js-adapters'; import { base58 } from '@metaplex-foundation/umi/serializers'; import { AccountInfo as Web3JsAccountInfo, Connection as Web3JsConnection, ConnectionConfig as Web3JsConnectionConfig, GetProgramAccountsFilter as Web3JsGetProgramAccountsFilter, PublicKey as Web3JsPublicKey, TokenBalance as Web3JsTokenBalance, TransactionConfirmationStrategy as Web3JsTransactionConfirmationStrategy, } from '@solana/web3.js'; import type { JSONRPCCallbackTypePlain } from 'jayson'; import type RpcClient from 'jayson/lib/client/browser'; export type Web3JsRpcOptions = Commitment | Web3JsConnectionConfig; export function createWeb3JsRpc( context: Pick<Context, 'programs' | 'transactions'>, endpoint: string, rpcOptions?: Web3JsRpcOptions ): RpcInterface & { connection: Web3JsConnection }; export function createWeb3JsRpc( context: Pick<Context, 'programs' | 'transactions'>, connection: Web3JsConnection ): RpcInterface & { connection: Web3JsConnection }; export function createWeb3JsRpc( context: Pick<Context, 'programs' | 'transactions'>, endpointOrConnection: string | Web3JsConnection, rpcOptions?: Web3JsRpcOptions ): RpcInterface & { connection: Web3JsConnection } { let connection: Web3JsConnection | null = null; const getConnection = () => { if (connection) { return connection; } if (typeof endpointOrConnection === 'string') { connection = new Web3JsConnection(endpointOrConnection, rpcOptions); } else { connection = endpointOrConnection; } return connection; }; const cluster = resolveClusterFromEndpoint(getConnection().rpcEndpoint); const getAccount = async ( publicKey: PublicKey, options: RpcGetAccountOptions = {} ): Promise<MaybeRpcAccount> => { const account = await getConnection().getAccountInfo( toWeb3JsPublicKey(publicKey), options ); return parseMaybeAccount(account, publicKey); }; const getAccounts = async ( publicKeys: PublicKey[], options: RpcGetAccountsOptions = {} ): Promise<MaybeRpcAccount[]> => { const accounts = await getConnection().getMultipleAccountsInfo( publicKeys.map(toWeb3JsPublicKey), options ); return accounts.map((account, index) => parseMaybeAccount(account, publicKeys[index]) ); }; const getProgramAccounts = async ( programId: PublicKey, options: RpcGetProgramAccountsOptions = {} ): Promise<RpcAccount[]> => { const accounts = await getConnection().getProgramAccounts( toWeb3JsPublicKey(programId), { ...options, filters: options.filters?.map((filter) => parseDataFilter(filter)), } ); return accounts.map(({ pubkey, account }) => parseAccount(account, fromWeb3JsPublicKey(pubkey)) ); }; const getBlockTime = async ( slot: number, // eslint-disable-next-line @typescript-eslint/no-unused-vars _options: RpcGetBlockTimeOptions = {} ): Promise<DateTime | null> => { const blockTime = await getConnection().getBlockTime(slot); return blockTime ? dateTime(blockTime) : null; }; const getBalance = async ( publicKey: PublicKey, options: RpcGetBalanceOptions = {} ): Promise<SolAmount> => { const balanceInLamports = await getConnection().getBalance( toWeb3JsPublicKey(publicKey), options ); return lamports(balanceInLamports); }; const getGenesisHash = async (): Promise<string> => { const genesisHash = await getConnection().getGenesisHash(); return genesisHash; }; const getRent = async ( bytes: number, options: RpcGetRentOptions = {} ): Promise<SolAmount> => { const rentFor = (bytes: number) => getConnection().getMinimumBalanceForRentExemption( bytes, options.commitment ); if (options.includesHeaderBytes ?? false) { const headerRent = await rentFor(0); const rentPerByte = BigInt(headerRent) / BigInt(ACCOUNT_HEADER_SIZE); return lamports(rentPerByte * BigInt(bytes)); } return lamports(await rentFor(bytes)); }; const getLatestBlockhash = async ( options: RpcGetLatestBlockhashOptions = {} ): Promise<BlockhashWithExpiryBlockHeight> => getConnection().getLatestBlockhash(options); const getTransaction = async ( signature: TransactionSignature, options: RpcGetTransactionOptions = {} ): Promise<TransactionWithMeta | null> => { const response = await getConnection().getTransaction( base58.deserialize(signature)[0], { commitment: options.commitment as 'confirmed' | 'finalized' | undefined, maxSupportedTransactionVersion: 0, } ); if (!response) { return null; } if (!response.meta) { // TODO: Custom error. throw new Error('Transaction meta is missing.'); } const { transaction, meta } = response; const message = fromWeb3JsMessage(transaction.message); const mapPublicKey = (key: string) => fromWeb3JsPublicKey(new Web3JsPublicKey(key)); const mapTokenBalance = ( tokenBalance: Web3JsTokenBalance ): TransactionMetaTokenBalance => ({ accountIndex: tokenBalance.accountIndex, amount: createAmount( tokenBalance.uiTokenAmount.amount, 'splToken', tokenBalance.uiTokenAmount.decimals ), mint: mapPublicKey(tokenBalance.mint), owner: tokenBalance.owner ? mapPublicKey(tokenBalance.owner) : null, }); return { message, serializedMessage: context.transactions.serializeMessage(message), signatures: transaction.signatures.map(base58.serialize), meta: { fee: lamports(meta.fee), logs: meta.logMessages ?? [], preBalances: meta.preBalances.map(lamports), postBalances: meta.postBalances.map(lamports), preTokenBalances: (meta.preTokenBalances ?? []).map(mapTokenBalance), postTokenBalances: (meta.postTokenBalances ?? []).map(mapTokenBalance), innerInstructions: meta.innerInstructions?.map( (inner): TransactionMetaInnerInstruction => ({ index: inner.index, instructions: inner.instructions.map( (instruction): CompiledInstruction => ({ programIndex: instruction.programIdIndex, accountIndexes: instruction.accounts, data: base58.serialize(instruction.data), }) ), }) ) ?? null, loadedAddresses: { writable: (meta.loadedAddresses?.writable ?? []).map( fromWeb3JsPublicKey ), readonly: (meta.loadedAddresses?.readonly ?? []).map( fromWeb3JsPublicKey ), }, computeUnitsConsumed: meta.computeUnitsConsumed ? BigInt(meta.computeUnitsConsumed) : null, err: meta.err, }, }; }; const getSignatureStatuses = async ( signatures: TransactionSignature[], options: RpcGetSignatureStatusesOptions = {} ): Promise<Array<TransactionStatus | null>> => { const response = await getConnection().getSignatureStatuses( signatures.map((signature) => base58.deserialize(signature)[0]), { searchTransactionHistory: options?.searchTransactionHistory ?? false } ); return response.value.map((status) => { if (!status) return null; return { slot: status.slot, confirmations: status.confirmations, error: status.err, commitment: status.confirmationStatus ?? null, }; }); }; const accountExists = async ( publicKey: PublicKey, options: RpcAccountExistsOptions = {} ): Promise<boolean> => !isZeroAmount(await getBalance(publicKey, options)); const airdrop = async ( publicKey: PublicKey, amount: SolAmount, options: RpcAirdropOptions = {} ): Promise<void> => { const signature = await getConnection().requestAirdrop( toWeb3JsPublicKey(publicKey), Number(amount.basisPoints) ); if (options.strategy) { await confirmTransaction( base58.serialize(signature), options as RpcConfirmTransactionOptions ); return; } await confirmTransaction(base58.serialize(signature), { ...options, strategy: { type: 'blockhash', ...(await getLatestBlockhash()) }, }); }; const call = async < Result, Params extends any[] | Record<string, any> = any[] >( method: string, params?: Params, options: RpcCallOptions = {} ): Promise<Result> => { const client = (getConnection() as any)._rpcClient as RpcClient; // Handle both array and object params const resolvedParams = Array.isArray(params) ? resolveCallParams( [...params] as any[], options.commitment, options.extra ) : resolveNamedCallParams( params as Record<string, any>, options.commitment, options.extra ); return new Promise((resolve, reject) => { const callback: JSONRPCCallbackTypePlain = (error, response) => { if (error) { reject(error); } else { resolve(response.result); } }; if (options.id) { client.request(method, resolvedParams, options.id, callback); } else { client.request(method, resolvedParams, callback); } }); }; const sendTransaction = async ( transaction: Transaction, options: RpcSendTransactionOptions = {} ): Promise<TransactionSignature> => { try { const signature = await getConnection().sendRawTransaction( context.transactions.serialize(transaction), options ); return base58.serialize(signature); } catch (error: any) { let resolvedError: ProgramError | null = null; if (error instanceof Error && 'logs' in error) { resolvedError = context.programs.resolveError( error as ErrorWithLogs, transaction ); } throw resolvedError || error; } }; const simulateTransaction = async ( transaction: Transaction, options: RpcSimulateTransactionOptions = {} ): Promise<RpcSimulateTransactionResult> => { try { const tx = toWeb3JsTransaction(transaction); const result = await getConnection().simulateTransaction(tx, { sigVerify: options.verifySignatures, accounts: { addresses: options.accounts || [], encoding: 'base64', }, }); return result.value; } catch (error: any) { let resolvedError: ProgramError | null = null; if (error instanceof Error && 'logs' in error) { resolvedError = context.programs.resolveError( error as ErrorWithLogs, transaction ); } throw resolvedError || error; } }; const confirmTransaction = async ( signature: TransactionSignature, options: RpcConfirmTransactionOptions ): Promise<RpcConfirmTransactionResult> => getConnection().confirmTransaction( parseConfirmStrategy(signature, options), options.commitment ); return { getEndpoint: (): string => getConnection().rpcEndpoint, getCluster: (): Cluster => cluster, getAccount, getAccounts, getProgramAccounts, getBlockTime, getGenesisHash, getBalance, getRent, getSlot: async (options: RpcGetSlotOptions = {}) => getConnection().getSlot(options), getLatestBlockhash, getTransaction, getSignatureStatuses, accountExists, airdrop, call, sendTransaction, simulateTransaction, confirmTransaction, get connection() { return getConnection(); }, }; } function parseAccount( account: Web3JsAccountInfo<Uint8Array>, publicKey: PublicKey ): RpcAccount { return { executable: account.executable, owner: fromWeb3JsPublicKey(account.owner), lamports: lamports(account.lamports), rentEpoch: account.rentEpoch ? BigInt(account.rentEpoch) : undefined, publicKey, data: new Uint8Array(account.data), }; } function parseMaybeAccount( account: Web3JsAccountInfo<Uint8Array> | null, publicKey: PublicKey ): MaybeRpcAccount { return account ? { ...parseAccount(account, publicKey), exists: true } : { exists: false, publicKey }; } function parseDataFilter( filter: RpcDataFilter ): Web3JsGetProgramAccountsFilter { if (!('memcmp' in filter)) return filter; const { bytes, ...rest } = filter.memcmp; return { memcmp: { ...rest, bytes: base58.deserialize(bytes)[0] } }; } function parseConfirmStrategy( signature: TransactionSignature, options: RpcConfirmTransactionOptions ): Web3JsTransactionConfirmationStrategy { if (options.strategy.type === 'blockhash') { return { ...options.strategy, signature: base58.deserialize(signature)[0], }; } return { ...options.strategy, signature: base58.deserialize(signature)[0], nonceAccountPubkey: toWeb3JsPublicKey(options.strategy.nonceAccountPubkey), }; } function resolveCallParams<Params extends any[]>( args: Params, commitment?: Commitment, extra?: object ): Params { if (!commitment && !extra) return args; let options: any = {}; if (commitment) options.commitment = commitment; if (extra) options = { ...options, ...extra }; args.push(options); return args; } function resolveNamedCallParams( params: Record<string, any> = {}, commitment?: Commitment, extra?: object ): Record<string, any> { if (!commitment && !extra) return params; // Create a new object with all original parameters const result = { ...params }; // Add commitment and extra options directly into the params object if (commitment) result.commitment = commitment; if (extra) Object.assign(result, extra); return result; }