Spaces:
Sleeping
Sleeping
| 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; | |
| } | |
| } | |