@metaplex-foundation/umi-rpc-web3js
Version:
An RPC implementation relying on Solana's web3.js
509 lines (472 loc) • 15 kB
text/typescript
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;
}