anchor/ts/src/provider.ts

290 lines
7.5 KiB
TypeScript

import {
Connection,
Account,
PublicKey,
Transaction,
TransactionSignature,
ConfirmOptions,
sendAndConfirmRawTransaction,
RpcResponseAndContext,
SimulatedTransactionResponse,
Commitment,
} from "@solana/web3.js";
/**
* The network and wallet context used to send transactions paid for and signed
* by the provider.
*/
export default class Provider {
/**
* @param connection The cluster connection where the program is deployed.
* @param wallet The wallet used to pay for and sign all transactions.
* @param opts Transaction confirmation options to use by default.
*/
constructor(
readonly connection: Connection,
readonly wallet: Wallet,
readonly opts: ConfirmOptions
) {}
static defaultOptions(): ConfirmOptions {
return {
preflightCommitment: "recent",
commitment: "recent",
};
}
/**
* Returns a `Provider` with a wallet read from the local filesystem.
*
* @param url The network cluster url.
* @param opts The default transaction confirmation options.
*
* (This api is for Node only.)
*/
static local(url?: string, opts?: ConfirmOptions): Provider {
opts = opts || Provider.defaultOptions();
const connection = new Connection(
url || "http://localhost:8899",
opts.preflightCommitment
);
const wallet = NodeWallet.local();
return new Provider(connection, wallet, opts);
}
/**
* Returns a `Provider` read from the `ANCHOR_PROVIDER_URL` envirnment
* variable
*
* (This api is for Node only.)
*/
static env(): Provider {
const process = require("process");
const url = process.env.ANCHOR_PROVIDER_URL;
if (url === undefined) {
throw new Error("ANCHOR_PROVIDER_URL is not defined");
}
const options = Provider.defaultOptions();
const connection = new Connection(url, options.commitment);
const wallet = NodeWallet.local();
return new Provider(connection, wallet, options);
}
/**
* Sends the given transaction, ppaid for and signed by the provider's wallet.
*
* @param tx The transaction to send.
* @param signers The set of signers in addition to the provdier wallet that
* will sign the transaction.
* @param opts Transaction confirmation options.
*/
async send(
tx: Transaction,
signers?: Array<Account | undefined>,
opts?: ConfirmOptions
): Promise<TransactionSignature> {
if (signers === undefined) {
signers = [];
}
if (opts === undefined) {
opts = this.opts;
}
const signerKps = signers.filter((s) => s !== undefined) as Array<Account>;
const signerPubkeys = [this.wallet.publicKey].concat(
signerKps.map((s) => s.publicKey)
);
tx.setSigners(...signerPubkeys);
tx.recentBlockhash = (
await this.connection.getRecentBlockhash(opts.preflightCommitment)
).blockhash;
await this.wallet.signTransaction(tx);
signerKps.forEach((kp) => {
tx.partialSign(kp);
});
const rawTx = tx.serialize();
const txId = await sendAndConfirmRawTransaction(
this.connection,
rawTx,
opts
);
return txId;
}
/**
* Similar to `send`, but for an array of transactions and signers.
*/
async sendAll(
reqs: Array<SendTxRequest>,
opts?: ConfirmOptions
): Promise<Array<TransactionSignature>> {
if (opts === undefined) {
opts = this.opts;
}
const blockhash = await this.connection.getRecentBlockhash(
opts.preflightCommitment
);
let txs = reqs.map((r) => {
let tx = r.tx;
let signers = r.signers;
if (signers === undefined) {
signers = [];
}
const signerKps = signers.filter(
(s) => s !== undefined
) as Array<Account>;
const signerPubkeys = [this.wallet.publicKey].concat(
signerKps.map((s) => s.publicKey)
);
tx.setSigners(...signerPubkeys);
tx.recentBlockhash = blockhash.blockhash;
signerKps.forEach((kp) => {
tx.partialSign(kp);
});
return tx;
});
const signedTxs = await this.wallet.signAllTransactions(txs);
const sigs = [];
for (let k = 0; k < txs.length; k += 1) {
const tx = signedTxs[k];
const rawTx = tx.serialize();
sigs.push(
await sendAndConfirmRawTransaction(this.connection, rawTx, opts)
);
}
return sigs;
}
/**
* Simulates the given transaction, returning emitted logs from execution.
*
* @param tx The transaction to send.
* @param signers The set of signers in addition to the provdier wallet that
* will sign the transaction.
* @param opts Transaction confirmation options.
*/
async simulate(
tx: Transaction,
signers?: Array<Account | undefined>,
opts?: ConfirmOptions
): Promise<RpcResponseAndContext<SimulatedTransactionResponse>> {
if (signers === undefined) {
signers = [];
}
if (opts === undefined) {
opts = this.opts;
}
const signerKps = signers.filter((s) => s !== undefined) as Array<Account>;
const signerPubkeys = [this.wallet.publicKey].concat(
signerKps.map((s) => s.publicKey)
);
tx.setSigners(...signerPubkeys);
tx.recentBlockhash = (
await this.connection.getRecentBlockhash(opts.preflightCommitment)
).blockhash;
await this.wallet.signTransaction(tx);
signerKps.forEach((kp) => {
tx.partialSign(kp);
});
return await simulateTransaction(this.connection, tx, opts.commitment);
}
}
export type SendTxRequest = {
tx: Transaction;
signers: Array<Account | undefined>;
};
/**
* Wallet interface for objects that can be used to sign provider transactions.
*/
export interface Wallet {
signTransaction(tx: Transaction): Promise<Transaction>;
signAllTransactions(txs: Transaction[]): Promise<Transaction[]>;
publicKey: PublicKey;
}
/**
* Node only wallet.
*/
export class NodeWallet implements Wallet {
constructor(readonly payer: Account) {}
static local(): NodeWallet {
const payer = new Account(
Buffer.from(
JSON.parse(
require("fs").readFileSync(
require("os").homedir() + "/.config/solana/id.json",
{
encoding: "utf-8",
}
)
)
)
);
return new NodeWallet(payer);
}
async signTransaction(tx: Transaction): Promise<Transaction> {
tx.partialSign(this.payer);
return tx;
}
async signAllTransactions(txs: Transaction[]): Promise<Transaction[]> {
return txs.map((t) => {
t.partialSign(this.payer);
return t;
});
}
get publicKey(): PublicKey {
return this.payer.publicKey;
}
}
// Copy of Connection.simulateTransaction that takes a commitment parameter.
async function simulateTransaction(
connection: Connection,
transaction: Transaction,
commitment: Commitment
): Promise<RpcResponseAndContext<SimulatedTransactionResponse>> {
// @ts-ignore
transaction.recentBlockhash = await connection._recentBlockhash(
// @ts-ignore
connection._disableBlockhashCaching
);
const signData = transaction.serializeMessage();
// @ts-ignore
const wireTransaction = transaction._serialize(signData);
const encodedTransaction = wireTransaction.toString("base64");
const config: any = { encoding: "base64", commitment };
const args = [encodedTransaction, config];
// @ts-ignore
const res = await connection._rpcRequest("simulateTransaction", args);
if (res.error) {
throw new Error("failed to simulate transaction: " + res.error.message);
}
return res.result;
}