diff --git a/web3.js/module.flow.js b/web3.js/module.flow.js index 4890bd5b40..f7090c64ee 100644 --- a/web3.js/module.flow.js +++ b/web3.js/module.flow.js @@ -34,8 +34,10 @@ declare module '@solana/web3.js' { // === src/connection.js === declare export type AccountInfo = { - tokens: number, + executable: boolean; + loaderProgramId: PublicKey, programId: PublicKey, + tokens: number, userdata: Buffer, } @@ -67,7 +69,6 @@ declare module '@solana/web3.js' { ): Transaction; static move(from: PublicKey, to: PublicKey, amount: number): Transaction; static assign(from: PublicKey, programId: PublicKey): Transaction; - static load(from: PublicKey, programId: PublicKey, name: string): Transaction; } // === src/transaction.js === @@ -152,4 +153,20 @@ declare module '@solana/web3.js' { ): Promise; } + // === src/loader.js === + declare export class Loader { + constructor(connection: Connection, programId: PublicKey) : Loader; + load(program: Account, offset: number, bytes: Array): Promise; + finalize(program: Account): Promise; + } + + // === src/native-loader.js === + declare export class NativeLoader { + static programId: PublicKey; + static load( + connection: Connection, + owner: Account, + programName: string, + ): Promise; + } } diff --git a/web3.js/src/connection.js b/web3.js/src/connection.js index d37f0d0b25..91e1f1226c 100644 --- a/web3.js/src/connection.js +++ b/web3.js/src/connection.js @@ -83,6 +83,8 @@ function jsonRpcResult(resultDescription: any) { * Expected JSON RPC response for the "getAccountInfo" message */ const GetAccountInfoRpcResult = jsonRpcResult({ + executable: 'boolean', + loader_program_id: 'array', program_id: 'array', tokens: 'number', userdata: 'array', @@ -138,8 +140,9 @@ const SendTokensRpcResult = jsonRpcResult('string'); * @property {?Buffer} userdata Optional userdata assigned to the account */ type AccountInfo = { - tokens: number, + executable: boolean; programId: PublicKey, + tokens: number, userdata: Buffer, } @@ -201,8 +204,10 @@ export class Connection { assert(typeof result !== 'undefined'); return { + executable: result.executable, tokens: result.tokens, programId: new PublicKey(result.program_id), + loaderProgramId: new PublicKey(result.loader_program_id), userdata: Buffer.from(result.userdata), }; } diff --git a/web3.js/src/index.js b/web3.js/src/index.js index 615e4f5f8f..1519ca9d93 100644 --- a/web3.js/src/index.js +++ b/web3.js/src/index.js @@ -2,7 +2,9 @@ export {Account} from './account'; export {BudgetProgram} from './budget-program'; export {Connection} from './connection'; +export {Loader} from './loader'; +export {NativeLoader} from './native-loader'; export {PublicKey} from './publickey'; export {SystemProgram} from './system-program'; -export {Transaction} from './transaction'; export {Token, TokenAmount} from './token-program'; +export {Transaction} from './transaction'; diff --git a/web3.js/src/loader.js b/web3.js/src/loader.js new file mode 100644 index 0000000000..1c9255db73 --- /dev/null +++ b/web3.js/src/loader.js @@ -0,0 +1,97 @@ +// @flow + +import * as BufferLayout from 'buffer-layout'; + +import {PublicKey, Transaction} from '.'; +import {sendAndConfirmTransaction} from './util/send-and-confirm-transaction'; +import type {Account, Connection} from '.'; + +/** + * Program loader interface + */ +export class Loader { + /** + * @private + */ + connection: Connection; + + /** + * @private + */ + programId: PublicKey; + + /** + * @param connection The connection to use + * @param programId Public key that identifies the loader + */ + constructor(connection: Connection, programId: PublicKey) { + Object.assign(this, {connection, programId}); + } + + /** + * Load program data + * + * @param program Account to load the program info + * @param offset Account userdata offset to write `bytes` into + * @param bytes Program data + */ + async load(program: Account, offset: number, bytes: Array) { + const userdataLayout = BufferLayout.struct([ + BufferLayout.u32('instruction'), + BufferLayout.u32('offset'), + BufferLayout.u32('bytesLength'), + BufferLayout.u32('bytesLengthPadding'), + BufferLayout.seq( + BufferLayout.u8('byte'), + BufferLayout.offset(BufferLayout.u32(), -8), + 'bytes' + ), + ]); + + let userdata = Buffer.alloc(bytes.length + 16); + userdataLayout.encode( + { + instruction: 0, // Load instruction + offset, + bytes, + }, + userdata, + ); + + const transaction = new Transaction({ + fee: 0, + keys: [program.publicKey], + programId: this.programId, + userdata, + }); + await sendAndConfirmTransaction(this.connection, program, transaction); + } + + /** + * Finalize an account loaded with program data for execution + * + * @param program `load()`ed Account + */ + async finalize(program: Account) { + const userdataLayout = BufferLayout.struct([ + BufferLayout.u32('instruction'), + ]); + + const userdata = Buffer.alloc(userdataLayout.span); + console.log('sp',userdataLayout.span); + userdataLayout.encode( + { + instruction: 1, // Finalize instruction + }, + userdata, + ); + + const transaction = new Transaction({ + fee: 0, + keys: [program.publicKey], + programId: this.programId, + userdata, + }); + await sendAndConfirmTransaction(this.connection, program, transaction); + } +} diff --git a/web3.js/src/native-loader.js b/web3.js/src/native-loader.js new file mode 100644 index 0000000000..327ba2e136 --- /dev/null +++ b/web3.js/src/native-loader.js @@ -0,0 +1,50 @@ +// @flow + +import {Account, PublicKey, Loader, SystemProgram} from '.'; +import {sendAndConfirmTransaction} from './util/send-and-confirm-transaction'; +import type {Connection} from '.'; + +/** + * Factory class for transactions to interact with a program loader + */ +export class NativeLoader { + /** + * Public key that identifies the NativeLoader + */ + static get programId(): PublicKey { + return new PublicKey('0x0202020202020202020202020202020202020202020202020202020202020202'); + } + + /** + * Loads a native program + * + * @param connection The connection to use + * @param owner User account to load the program with + * @param programName Name of the native program + */ + static async load( + connection: Connection, + owner: Account, + programName: string, + ): Promise { + const bytes = [...Buffer.from(programName)]; + + const programAccount = new Account(); + + // Allocate memory for the program account + const transaction = SystemProgram.createAccount( + owner.publicKey, + programAccount.publicKey, + 1, + bytes.length + 1, + NativeLoader.programId, + ); + await sendAndConfirmTransaction(connection, owner, transaction); + + const loader = new Loader(connection, NativeLoader.programId); + await loader.load(programAccount, 0, bytes); + await loader.finalize(programAccount); + + return programAccount.publicKey; + } +} diff --git a/web3.js/src/system-program.js b/web3.js/src/system-program.js index b5dc3b370d..c8cba56f10 100644 --- a/web3.js/src/system-program.js +++ b/web3.js/src/system-program.js @@ -105,35 +105,4 @@ export class SystemProgram { userdata, }); } - - /** - * Load a dynamic program. Unstable API, will change - * - * @private - */ - static load(from: PublicKey, programId: PublicKey, name: string): Transaction { - const userdataLayout = BufferLayout.struct([ - BufferLayout.u32('instruction'), - Layout.publicKey('programId'), - Layout.rustString('name'), - ]); - - let userdata = Buffer.alloc(1024); - const encodeLength = userdataLayout.encode( - { - instruction: 3, // Load instruction - programId: programId.toBuffer(), - name, - }, - userdata, - ); - userdata = userdata.slice(0, encodeLength); - - return new Transaction({ - fee: 0, - keys: [from], - programId: SystemProgram.programId, - userdata, - }); - } } diff --git a/web3.js/test/connection.test.js b/web3.js/test/connection.test.js index 0e027d3475..ae1b086556 100644 --- a/web3.js/test/connection.test.js +++ b/web3.js/test/connection.test.js @@ -213,6 +213,11 @@ test('request airdrop', async () => { ], tokens: 42, userdata: [], + executable: false, + loader_program_id: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ], } } ]); diff --git a/web3.js/test/native-loader.test.js b/web3.js/test/native-loader.test.js new file mode 100644 index 0000000000..a59e35cd2c --- /dev/null +++ b/web3.js/test/native-loader.test.js @@ -0,0 +1,30 @@ +// @flow + +import { + Connection, + NativeLoader, + Transaction, +} from '../src'; +import {mockRpcEnabled} from './__mocks__/node-fetch'; +import {url} from './url'; +import {newAccountWithTokens} from './new-account-with-tokens'; + +test('unstable - load', async () => { + if (mockRpcEnabled) { + console.log('non-live test skipped'); + return; + } + + const connection = new Connection(url); + const from = await newAccountWithTokens(connection); + + const noopProgramId = await NativeLoader.load(connection, from, 'noop'); + const noopTransaction = new Transaction({ + fee: 0, + keys: [from.publicKey], + programId: noopProgramId, + }); + const signature = await connection.sendTransaction(from, noopTransaction); + expect(connection.confirmTransaction(signature)).resolves.toBe(true); +}); + diff --git a/web3.js/test/system-program.test.js b/web3.js/test/system-program.test.js index 63ac308292..3ece94a3b3 100644 --- a/web3.js/test/system-program.test.js +++ b/web3.js/test/system-program.test.js @@ -3,13 +3,8 @@ import { Account, BudgetProgram, - Connection, SystemProgram, - Transaction, } from '../src'; -import {mockRpcEnabled} from './__mocks__/node-fetch'; -import {url} from './url'; -import {newAccountWithTokens} from './new-account-with-tokens'; test('createAccount', () => { const from = new Account(); @@ -61,33 +56,3 @@ test('assign', () => { // TODO: Validate transaction contents more }); -test('unstable - load', async () => { - if (mockRpcEnabled) { - console.log('non-live test skipped'); - return; - } - - const connection = new Connection(url); - const from = await newAccountWithTokens(connection); - const noopProgramId = (new Account()).publicKey; - - const loadTransaction = SystemProgram.load( - from.publicKey, - noopProgramId, - 'noop', - ); - - let signature = await connection.sendTransaction(from, loadTransaction); - expect(connection.confirmTransaction(signature)).resolves.toBe(true); - - const noopTransaction = new Transaction({ - fee: 0, - keys: [from.publicKey], - programId: noopProgramId, - }); - signature = await connection.sendTransaction(from, noopTransaction); - expect(connection.confirmTransaction(signature)).resolves.toBe(true); - - -}); - diff --git a/web3.js/test/token-program.test.js b/web3.js/test/token-program.test.js index 67e48b5f7c..7fa4706d3e 100644 --- a/web3.js/test/token-program.test.js +++ b/web3.js/test/token-program.test.js @@ -120,6 +120,11 @@ test('create new token', async () => { 10, 0, 0, 0, 0, 0, 0, 0, 84, 101, 115, 116, 32, 116, 111, 107, 101, 110, 4, 0, 0, 0, 0, 0, 0, 0, 84, 69, 83, 84 ], + executable: false, + loader_program_id: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ], } } ]); @@ -153,6 +158,11 @@ test('create new token', async () => { 16, 39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], + executable: false, + loader_program_id: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ], } } ]); @@ -204,6 +214,11 @@ test('create new token account', async () => { 0, 0, 0, 0, 0, 0, 0, 0, 0, ], + executable: false, + loader_program_id: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ], } } ]); @@ -256,6 +271,11 @@ test('transfer', async () => { 123, 0, 0, 0, 0, 0, 0, 0, 0, ], + executable: false, + loader_program_id: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ], } } ]); @@ -293,6 +313,11 @@ test('transfer', async () => { 123, 0, 0, 0, 0, 0, 0, 0, 0, ], + executable: false, + loader_program_id: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ], } } ]); @@ -355,6 +380,11 @@ test('approve/revoke', async () => { 1, ...initialOwnerTokenAccount.toBuffer(), ], + executable: false, + loader_program_id: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ], } } ]); @@ -403,6 +433,11 @@ test('approve/revoke', async () => { 1, ...initialOwnerTokenAccount.toBuffer(), ], + executable: false, + loader_program_id: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ], } } ]);