From 1eb7681a859b137e02a1bb0ceda7f0b59660830a Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Thu, 14 Jan 2021 09:59:31 -0700 Subject: [PATCH] solana-web3: add TransferWithSeed implementation (#14570) * fix: add handling for TransferWithSeed system instruction * chore: add failing Assign/AllocateWithSeed test * fix: broken Allocate/AssignWithSeed methods --- web3.js/module.d.ts | 17 ++- web3.js/module.flow.js | 17 ++- web3.js/src/system-program.js | 99 ++++++++++++++-- web3.js/test/system-program.test.js | 169 ++++++++++++++++++++++++++++ 4 files changed, 291 insertions(+), 11 deletions(-) diff --git a/web3.js/module.d.ts b/web3.js/module.d.ts index 5097fa2d7b..865a440418 100644 --- a/web3.js/module.d.ts +++ b/web3.js/module.d.ts @@ -891,6 +891,15 @@ declare module '@solana/web3.js' { lamports: number; }; + export type TransferWithSeedParams = { + fromPubkey: PublicKey; + basePubkey: PublicKey; + toPubkey: PublicKey; + lamports: number; + seed: string; + programId: PublicKey; + }; + export type CreateNonceAccountParams = { fromPubkey: PublicKey; noncePubkey: PublicKey; @@ -943,7 +952,9 @@ declare module '@solana/web3.js' { static assign( params: AssignParams | AssignWithSeedParams, ): TransactionInstruction; - static transfer(params: TransferParams): TransactionInstruction; + static transfer( + params: TransferParams | TransferWithSeedParams, + ): TransactionInstruction; static createNonceAccount( params: CreateNonceAccountParams | CreateNonceAccountWithSeedParams, ): Transaction; @@ -960,6 +971,7 @@ declare module '@solana/web3.js' { | 'Assign' | 'AssignWithSeed' | 'Transfer' + | 'TransferWithSeed' | 'AdvanceNonceAccount' | 'WithdrawNonceAccount' | 'InitializeNonceAccount' @@ -988,6 +1000,9 @@ declare module '@solana/web3.js' { instruction: TransactionInstruction, ): AssignWithSeedParams; static decodeTransfer(instruction: TransactionInstruction): TransferParams; + static decodeTransferWithSeed( + instruction: TransactionInstruction, + ): TransferWithSeedParams; static decodeNonceInitialize( instruction: TransactionInstruction, ): InitializeNonceParams; diff --git a/web3.js/module.flow.js b/web3.js/module.flow.js index 68d2bacf93..1fa61ed21e 100644 --- a/web3.js/module.flow.js +++ b/web3.js/module.flow.js @@ -899,6 +899,15 @@ declare module '@solana/web3.js' { lamports: number, |}; + declare export type TransferWithSeedParams = {| + fromPubkey: PublicKey, + basePubkey: PublicKey, + toPubkey: PublicKey, + lamports: number, + seed: string, + programId: PublicKey, + |}; + declare export type CreateNonceAccountParams = {| fromPubkey: PublicKey, noncePubkey: PublicKey, @@ -951,7 +960,9 @@ declare module '@solana/web3.js' { static assign( params: AssignParams | AssignWithSeedParams, ): TransactionInstruction; - static transfer(params: TransferParams): TransactionInstruction; + static transfer( + params: TransferParams | TransferWithSeedParams, + ): TransactionInstruction; static createNonceAccount( params: CreateNonceAccountParams | CreateNonceAccountWithSeedParams, ): Transaction; @@ -968,6 +979,7 @@ declare module '@solana/web3.js' { | 'Assign' | 'AssignWithSeed' | 'Transfer' + | 'TransferWithSeed' | 'AdvanceNonceAccount' | 'WithdrawNonceAccount' | 'InitializeNonceAccount' @@ -996,6 +1008,9 @@ declare module '@solana/web3.js' { instruction: TransactionInstruction, ): AssignWithSeedParams; static decodeTransfer(instruction: TransactionInstruction): TransferParams; + static decodeTransferWithSeed( + instruction: TransactionInstruction, + ): TransferWithSeedParams; static decodeNonceInitialize( instruction: TransactionInstruction, ): InitializeNonceParams; diff --git a/web3.js/src/system-program.js b/web3.js/src/system-program.js index c4f382413d..cf20a27ac9 100644 --- a/web3.js/src/system-program.js +++ b/web3.js/src/system-program.js @@ -198,6 +198,23 @@ export type AssignWithSeedParams = {| programId: PublicKey, |}; +/** + * Transfer with seed system transaction params + * @typedef {Object} TransferWithSeedParams + * @property {PublicKey} accountPubkey + * @property {PublicKey} basePubkey + * @property {string} seed + * @property {PublicKey} programId + */ +export type TransferWithSeedParams = {| + fromPubkey: PublicKey, + basePubkey: PublicKey, + toPubkey: PublicKey, + lamports: number, + seed: string, + programId: PublicKey, +|}; + /** * System Instruction class */ @@ -269,6 +286,30 @@ export class SystemInstruction { }; } + /** + * Decode a transfer with seed system instruction and retrieve the instruction params. + */ + static decodeTransferWithSeed( + instruction: TransactionInstruction, + ): TransferWithSeedParams { + this.checkProgramId(instruction.programId); + this.checkKeyLength(instruction.keys, 3); + + const {lamports, seed, programId} = decodeData( + SYSTEM_INSTRUCTION_LAYOUTS.TransferWithSeed, + instruction.data, + ); + + return { + fromPubkey: instruction.keys[0].pubkey, + basePubkey: instruction.keys[1].pubkey, + toPubkey: instruction.keys[2].pubkey, + lamports, + seed, + programId: new PublicKey(programId), + }; + } + /** * Decode an allocate system instruction and retrieve the instruction params. */ @@ -576,6 +617,15 @@ export const SYSTEM_INSTRUCTION_LAYOUTS = Object.freeze({ Layout.publicKey('programId'), ]), }, + TransferWithSeed: { + index: 11, + layout: BufferLayout.struct([ + BufferLayout.u32('instruction'), + BufferLayout.ns64('lamports'), + Layout.rustString('seed'), + Layout.publicKey('programId'), + ]), + }, }); /** @@ -613,15 +663,34 @@ export class SystemProgram { /** * Generate a transaction instruction that transfers lamports from one account to another */ - static transfer(params: TransferParams): TransactionInstruction { - const type = SYSTEM_INSTRUCTION_LAYOUTS.Transfer; - const data = encodeData(type, {lamports: params.lamports}); - - return new TransactionInstruction({ - keys: [ + static transfer( + params: TransferParams | TransferWithSeedParams, + ): TransactionInstruction { + let data; + let keys; + if (params.basePubkey) { + const type = SYSTEM_INSTRUCTION_LAYOUTS.TransferWithSeed; + data = encodeData(type, { + lamports: params.lamports, + seed: params.seed, + programId: params.programId.toBuffer(), + }); + keys = [ + {pubkey: params.fromPubkey, isSigner: false, isWritable: true}, + {pubkey: params.basePubkey, isSigner: true, isWritable: false}, + {pubkey: params.toPubkey, isSigner: false, isWritable: true}, + ]; + } else { + const type = SYSTEM_INSTRUCTION_LAYOUTS.Transfer; + data = encodeData(type, {lamports: params.lamports}); + keys = [ {pubkey: params.fromPubkey, isSigner: true, isWritable: true}, {pubkey: params.toPubkey, isSigner: false, isWritable: true}, - ], + ]; + } + + return new TransactionInstruction({ + keys, programId: this.programId, data, }); @@ -634,6 +703,7 @@ export class SystemProgram { params: AssignParams | AssignWithSeedParams, ): TransactionInstruction { let data; + let keys; if (params.basePubkey) { const type = SYSTEM_INSTRUCTION_LAYOUTS.AssignWithSeed; data = encodeData(type, { @@ -641,13 +711,18 @@ export class SystemProgram { seed: params.seed, programId: params.programId.toBuffer(), }); + keys = [ + {pubkey: params.accountPubkey, isSigner: false, isWritable: true}, + {pubkey: params.basePubkey, isSigner: true, isWritable: false}, + ]; } else { const type = SYSTEM_INSTRUCTION_LAYOUTS.Assign; data = encodeData(type, {programId: params.programId.toBuffer()}); + keys = [{pubkey: params.accountPubkey, isSigner: true, isWritable: true}]; } return new TransactionInstruction({ - keys: [{pubkey: params.accountPubkey, isSigner: true, isWritable: true}], + keys, programId: this.programId, data, }); @@ -822,6 +897,7 @@ export class SystemProgram { params: AllocateParams | AllocateWithSeedParams, ): TransactionInstruction { let data; + let keys; if (params.basePubkey) { const type = SYSTEM_INSTRUCTION_LAYOUTS.AllocateWithSeed; data = encodeData(type, { @@ -830,15 +906,20 @@ export class SystemProgram { space: params.space, programId: params.programId.toBuffer(), }); + keys = [ + {pubkey: params.accountPubkey, isSigner: false, isWritable: true}, + {pubkey: params.basePubkey, isSigner: true, isWritable: false}, + ]; } else { const type = SYSTEM_INSTRUCTION_LAYOUTS.Allocate; data = encodeData(type, { space: params.space, }); + keys = [{pubkey: params.accountPubkey, isSigner: true, isWritable: true}]; } return new TransactionInstruction({ - keys: [{pubkey: params.accountPubkey, isSigner: true, isWritable: true}], + keys, programId: this.programId, data, }); diff --git a/web3.js/test/system-program.test.js b/web3.js/test/system-program.test.js index c31f67c4b2..9979a23843 100644 --- a/web3.js/test/system-program.test.js +++ b/web3.js/test/system-program.test.js @@ -3,6 +3,7 @@ import { Account, Connection, + PublicKey, StakeProgram, SystemInstruction, SystemProgram, @@ -52,6 +53,23 @@ test('transfer', () => { expect(params).toEqual(SystemInstruction.decodeTransfer(systemInstruction)); }); +test('transferWithSeed', () => { + const params = { + fromPubkey: new Account().publicKey, + basePubkey: new Account().publicKey, + toPubkey: new Account().publicKey, + lamports: 123, + seed: '你好', + programId: new Account().publicKey, + }; + const transaction = new Transaction().add(SystemProgram.transfer(params)); + expect(transaction.instructions).toHaveLength(1); + const [systemInstruction] = transaction.instructions; + expect(params).toEqual( + SystemInstruction.decodeTransferWithSeed(systemInstruction), + ); +}); + test('allocate', () => { const params = { accountPubkey: new Account().publicKey, @@ -402,3 +420,154 @@ test('live Nonce actions', async () => { ); expect(withdrawBalance).toEqual(minimumAmount); }); + +test('live withSeed actions', async () => { + if (mockRpcEnabled) { + console.log('non-live test skipped'); + return; + } + + const connection = new Connection(url, 'singleGossip'); + const baseAccount = await newAccountWithLamports( + connection, + 2 * LAMPORTS_PER_SOL, + ); + const basePubkey = baseAccount.publicKey; + const seed = 'hi there'; + const programId = new Account().publicKey; + const createAccountWithSeedAddress = await PublicKey.createWithSeed( + basePubkey, + seed, + programId, + ); + const space = 0; + + const minimumAmount = await connection.getMinimumBalanceForRentExemption( + space, + ); + + const createAccountWithSeedParams = { + fromPubkey: basePubkey, + newAccountPubkey: createAccountWithSeedAddress, + basePubkey, + seed, + lamports: minimumAmount, + space, + programId, + }; + const createAccountWithSeedTransaction = new Transaction().add( + SystemProgram.createAccountWithSeed(createAccountWithSeedParams), + ); + await sendAndConfirmTransaction( + connection, + createAccountWithSeedTransaction, + [baseAccount], + {commitment: 'singleGossip', preflightCommitment: 'singleGossip'}, + ); + const createAccountWithSeedBalance = await connection.getBalance( + createAccountWithSeedAddress, + ); + expect(createAccountWithSeedBalance).toEqual(minimumAmount); + + // Transfer to a derived address + const programId2 = new Account().publicKey; + const transferWithSeedAddress = await PublicKey.createWithSeed( + basePubkey, + seed, + programId2, + ); + await sendAndConfirmTransaction( + connection, + new Transaction().add( + SystemProgram.transfer({ + fromPubkey: baseAccount.publicKey, + toPubkey: transferWithSeedAddress, + lamports: 3 * minimumAmount, + }), + ), + [baseAccount], + {commitment: 'singleGossip', preflightCommitment: 'singleGossip'}, + ); + let transferWithSeedAddressBalance = await connection.getBalance( + transferWithSeedAddress, + ); + expect(transferWithSeedAddressBalance).toEqual(3 * minimumAmount); + + // Test TransferWithSeed + const programId3 = new Account(); + const toPubkey = await PublicKey.createWithSeed( + basePubkey, + seed, + programId3.publicKey, + ); + const transferWithSeedParams = { + fromPubkey: transferWithSeedAddress, + basePubkey, + toPubkey, + lamports: 2 * minimumAmount, + seed, + programId: programId2, + }; + const transferWithSeedTransaction = new Transaction().add( + SystemProgram.transfer(transferWithSeedParams), + ); + await sendAndConfirmTransaction( + connection, + transferWithSeedTransaction, + [baseAccount], + {commitment: 'singleGossip', preflightCommitment: 'singleGossip'}, + ); + const toBalance = await connection.getBalance(toPubkey); + expect(toBalance).toEqual(2 * minimumAmount); + transferWithSeedAddressBalance = await connection.getBalance( + createAccountWithSeedAddress, + ); + expect(transferWithSeedAddressBalance).toEqual(minimumAmount); + + // Test AllocateWithSeed + const allocateWithSeedParams = { + accountPubkey: toPubkey, + basePubkey, + seed, + space: 10, + programId: programId3.publicKey, + }; + const allocateWithSeedTransaction = new Transaction().add( + SystemProgram.allocate(allocateWithSeedParams), + ); + await sendAndConfirmTransaction( + connection, + allocateWithSeedTransaction, + [baseAccount], + {commitment: 'singleGossip', preflightCommitment: 'singleGossip'}, + ); + let account = await connection.getAccountInfo(toPubkey); + if (account === null) { + expect(account).not.toBeNull(); + return; + } + expect(account.data).toHaveLength(10); + + // Test AssignWithSeed + const assignWithSeedParams = { + accountPubkey: toPubkey, + basePubkey, + seed, + programId: programId3.publicKey, + }; + const assignWithSeedTransaction = new Transaction().add( + SystemProgram.assign(assignWithSeedParams), + ); + await sendAndConfirmTransaction( + connection, + assignWithSeedTransaction, + [baseAccount], + {commitment: 'singleGossip', preflightCommitment: 'singleGossip'}, + ); + account = await connection.getAccountInfo(toPubkey); + if (account === null) { + expect(account).not.toBeNull(); + return; + } + expect(account.owner).toEqual(programId3.publicKey); +});