solana/web3.js/src/loader.ts

237 lines
6.6 KiB
TypeScript

import {Buffer} from 'buffer';
import * as BufferLayout from '@solana/buffer-layout';
import {PublicKey} from './publickey';
import {Transaction, PACKET_DATA_SIZE} from './transaction';
import {SYSVAR_RENT_PUBKEY} from './sysvar';
import {sendAndConfirmTransaction} from './utils/send-and-confirm-transaction';
import {sleep} from './utils/sleep';
import type {Connection} from './connection';
import type {Signer} from './keypair';
import {SystemProgram} from './programs/system';
import {IInstructionInputData} from './instruction';
// Keep program chunks under PACKET_DATA_SIZE, leaving enough room for the
// rest of the Transaction fields
//
// TODO: replace 300 with a proper constant for the size of the other
// Transaction fields
const CHUNK_SIZE = PACKET_DATA_SIZE - 300;
/**
* Program loader interface
*/
export class Loader {
/**
* @internal
*/
constructor() {}
/**
* Amount of program data placed in each load Transaction
*/
static chunkSize: number = CHUNK_SIZE;
/**
* Minimum number of signatures required to load a program not including
* retries
*
* Can be used to calculate transaction fees
*/
static getMinNumSignatures(dataLength: number): number {
return (
2 * // Every transaction requires two signatures (payer + program)
(Math.ceil(dataLength / Loader.chunkSize) +
1 + // Add one for Create transaction
1) // Add one for Finalize transaction
);
}
/**
* Loads a generic program
*
* @param connection The connection to use
* @param payer System account that pays to load the program
* @param program Account to load the program into
* @param programId Public key that identifies the loader
* @param data Program octets
* @return true if program was loaded successfully, false if program was already loaded
*/
static async load(
connection: Connection,
payer: Signer,
program: Signer,
programId: PublicKey,
data: Buffer | Uint8Array | Array<number>,
): Promise<boolean> {
{
const balanceNeeded = await connection.getMinimumBalanceForRentExemption(
data.length,
);
// Fetch program account info to check if it has already been created
const programInfo = await connection.getAccountInfo(
program.publicKey,
'confirmed',
);
let transaction: Transaction | null = null;
if (programInfo !== null) {
if (programInfo.executable) {
console.error('Program load failed, account is already executable');
return false;
}
if (programInfo.data.length !== data.length) {
transaction = transaction || new Transaction();
transaction.add(
SystemProgram.allocate({
accountPubkey: program.publicKey,
space: data.length,
}),
);
}
if (!programInfo.owner.equals(programId)) {
transaction = transaction || new Transaction();
transaction.add(
SystemProgram.assign({
accountPubkey: program.publicKey,
programId,
}),
);
}
if (programInfo.lamports < balanceNeeded) {
transaction = transaction || new Transaction();
transaction.add(
SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: program.publicKey,
lamports: balanceNeeded - programInfo.lamports,
}),
);
}
} else {
transaction = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: program.publicKey,
lamports: balanceNeeded > 0 ? balanceNeeded : 1,
space: data.length,
programId,
}),
);
}
// If the account is already created correctly, skip this step
// and proceed directly to loading instructions
if (transaction !== null) {
await sendAndConfirmTransaction(
connection,
transaction,
[payer, program],
{
commitment: 'confirmed',
},
);
}
}
const dataLayout = BufferLayout.struct<
Readonly<{
bytes: number[];
bytesLength: number;
bytesLengthPadding: number;
instruction: number;
offset: number;
}>
>([
BufferLayout.u32('instruction'),
BufferLayout.u32('offset'),
BufferLayout.u32('bytesLength'),
BufferLayout.u32('bytesLengthPadding'),
BufferLayout.seq(
BufferLayout.u8('byte'),
BufferLayout.offset(BufferLayout.u32(), -8),
'bytes',
),
]);
const chunkSize = Loader.chunkSize;
let offset = 0;
let array = data;
let transactions = [];
while (array.length > 0) {
const bytes = array.slice(0, chunkSize);
const data = Buffer.alloc(chunkSize + 16);
dataLayout.encode(
{
instruction: 0, // Load instruction
offset,
bytes: bytes as number[],
bytesLength: 0,
bytesLengthPadding: 0,
},
data,
);
const transaction = new Transaction().add({
keys: [{pubkey: program.publicKey, isSigner: true, isWritable: true}],
programId,
data,
});
transactions.push(
sendAndConfirmTransaction(connection, transaction, [payer, program], {
commitment: 'confirmed',
}),
);
// Delay between sends in an attempt to reduce rate limit errors
if (connection._rpcEndpoint.includes('solana.com')) {
const REQUESTS_PER_SECOND = 4;
await sleep(1000 / REQUESTS_PER_SECOND);
}
offset += chunkSize;
array = array.slice(chunkSize);
}
await Promise.all(transactions);
// Finalize the account loaded with program data for execution
{
const dataLayout = BufferLayout.struct<IInstructionInputData>([
BufferLayout.u32('instruction'),
]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: 1, // Finalize instruction
},
data,
);
const transaction = new Transaction().add({
keys: [
{pubkey: program.publicKey, isSigner: true, isWritable: true},
{pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false},
],
programId,
data,
});
await sendAndConfirmTransaction(
connection,
transaction,
[payer, program],
{
commitment: 'confirmed',
},
);
}
// success
return true;
}
}