UNPKG

@solana/web3.js

Version:
268 lines (246 loc) • 7.84 kB
import {Buffer} from 'buffer'; import * as BufferLayout from '@solana/buffer-layout'; import {PublicKey} from './publickey'; import {Transaction, PACKET_DATA_SIZE} from './transaction'; import {MS_PER_SLOT} from './timing'; 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, }); const deployCommitment = 'processed'; const finalizeSignature = await connection.sendTransaction( transaction, [payer, program], {preflightCommitment: deployCommitment}, ); const {context, value} = await connection.confirmTransaction( { signature: finalizeSignature, lastValidBlockHeight: transaction.lastValidBlockHeight!, blockhash: transaction.recentBlockhash!, }, deployCommitment, ); if (value.err) { throw new Error( `Transaction ${finalizeSignature} failed (${JSON.stringify(value)})`, ); } // We prevent programs from being usable until the slot after their deployment. // See https://github.com/solana-labs/solana/pull/29654 while ( true // eslint-disable-line no-constant-condition ) { try { const currentSlot = await connection.getSlot({ commitment: deployCommitment, }); if (currentSlot > context.slot) { break; } } catch { /* empty */ } await new Promise(resolve => setTimeout(resolve, Math.round(MS_PER_SLOT / 2)), ); } } // success return true; } }