diff --git a/web3.js/module.d.ts b/web3.js/module.d.ts index 1b38e85b4..e492fd5dd 100644 --- a/web3.js/module.d.ts +++ b/web3.js/module.d.ts @@ -962,7 +962,7 @@ declare module '@solana/web3.js' { program: Account, programId: PublicKey, data: Buffer | Uint8Array | Array, - ): Promise; + ): Promise; } // === src/bpf-loader.js === @@ -975,7 +975,7 @@ declare module '@solana/web3.js' { program: Account, elfBytes: Buffer | Uint8Array | Array, loaderProgramId: PublicKey, - ): Promise; + ): Promise; } // === src/bpf-loader-deprecated.js === diff --git a/web3.js/module.flow.js b/web3.js/module.flow.js index 4e443dfc0..8db0a2522 100644 --- a/web3.js/module.flow.js +++ b/web3.js/module.flow.js @@ -969,7 +969,7 @@ declare module '@solana/web3.js' { program: Account, programId: PublicKey, data: Buffer | Uint8Array | Array, - ): Promise; + ): Promise; } // === src/bpf-loader.js === @@ -982,7 +982,7 @@ declare module '@solana/web3.js' { program: Account, elfBytes: Buffer | Uint8Array | Array, loaderProgramId: PublicKey, - ): Promise; + ): Promise; } // === src/bpf-loader-deprecated.js === diff --git a/web3.js/src/bpf-loader.js b/web3.js/src/bpf-loader.js index 2cefc1b2a..26aae3b1d 100644 --- a/web3.js/src/bpf-loader.js +++ b/web3.js/src/bpf-loader.js @@ -31,6 +31,7 @@ export class BpfLoader { * @param program Account to load the program into * @param elf The entire ELF containing the BPF program * @param loaderProgramId The program id of the BPF loader to use + * @return true if program was loaded successfully, false if program was already loaded */ static load( connection: Connection, @@ -38,7 +39,7 @@ export class BpfLoader { program: Account, elf: Buffer | Uint8Array | Array, loaderProgramId: PublicKey, - ): Promise { + ): Promise { return Loader.load(connection, payer, program, loaderProgramId, elf); } } diff --git a/web3.js/src/loader.js b/web3.js/src/loader.js index 51cedfdb4..628459358 100644 --- a/web3.js/src/loader.js +++ b/web3.js/src/loader.js @@ -45,6 +45,7 @@ export class Loader { * @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, @@ -52,29 +53,80 @@ export class Loader { program: Account, programId: PublicKey, data: Buffer | Uint8Array | Array, - ): Promise { + ): Promise { { const balanceNeeded = await connection.getMinimumBalanceForRentExemption( data.length, ); - const transaction = new Transaction().add( - SystemProgram.createAccount({ - fromPubkey: payer.publicKey, - newAccountPubkey: program.publicKey, - lamports: balanceNeeded > 0 ? balanceNeeded : 1, - space: data.length, - programId, - }), - ); - await sendAndConfirmTransaction( - connection, - transaction, - [payer, program], - { - commitment: 'single', - skipPreflight: true, - }, + + // Fetch program account info to check if it has already been created + const programInfo = await connection.getAccountInfo( + program.publicKey, + 'single', ); + + 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: 'single', + skipPreflight: true, + }, + ); + } } const dataLayout = BufferLayout.struct([ @@ -165,5 +217,8 @@ export class Loader { }, ); } + + // success + return true; } } diff --git a/web3.js/test/bpf-loader.test.js b/web3.js/test/bpf-loader.test.js index 6127a3bb7..6301773c8 100644 --- a/web3.js/test/bpf-loader.test.js +++ b/web3.js/test/bpf-loader.test.js @@ -62,18 +62,19 @@ describe('load BPF Rust program', () => { let program: Account; let signature: string; let payerAccount: Account; + let programData: Buffer; beforeAll(async () => { - const data = await fs.readFile( + programData = await fs.readFile( 'test/fixtures/noop-rust/solana_bpf_rust_noop.so', ); const {feeCalculator} = await connection.getRecentBlockhash(); const fees = feeCalculator.lamportsPerSignature * - (BpfLoader.getMinNumSignatures(data.length) + NUM_RETRIES); + (BpfLoader.getMinNumSignatures(programData.length) + NUM_RETRIES); const balanceNeeded = await connection.getMinimumBalanceForRentExemption( - data.length, + programData.length, ); payerAccount = await newAccountWithLamports( @@ -81,12 +82,30 @@ describe('load BPF Rust program', () => { fees + balanceNeeded, ); - program = new Account(); + // Create program account with low balance + program = await newAccountWithLamports(connection, balanceNeeded - 1); + + // First load will fail part way due to lack of funds + const insufficientPayerAccount = await newAccountWithLamports( + connection, + 2 * feeCalculator.lamportsPerSignature * 8, + ); + + const failedLoad = BpfLoader.load( + connection, + insufficientPayerAccount, + program, + programData, + BPF_LOADER_PROGRAM_ID, + ); + await expect(failedLoad).rejects.toThrow('Transaction was not confirmed'); + + // Second load will succeed await BpfLoader.load( connection, payerAccount, program, - data, + programData, BPF_LOADER_PROGRAM_ID, ); @@ -196,4 +215,16 @@ describe('load BPF Rust program', () => { expect(logs.length).toEqual(0); }); + + test('reload program', async () => { + expect( + await BpfLoader.load( + connection, + payerAccount, + program, + programData, + BPF_LOADER_PROGRAM_ID, + ), + ).toBe(false); + }); });