diff --git a/web3.js/src/system-program.js b/web3.js/src/system-program.js index 361c868b94..daa0d272df 100644 --- a/web3.js/src/system-program.js +++ b/web3.js/src/system-program.js @@ -6,6 +6,7 @@ import {encodeData} from './instruction'; import type {InstructionType} from './instruction'; import * as Layout from './layout'; import {PublicKey} from './publickey'; +import {SYSVAR_RECENT_BLOCKHASHES_PUBKEY, SYSVAR_RENT_PUBKEY} from './sysvar'; import {Transaction, TransactionInstruction} from './transaction'; import type {TransactionInstructionCtorFields} from './transaction'; @@ -66,6 +67,7 @@ export class SystemInstruction extends TransactionInstruction { if ( this.type == SystemInstructionLayout.Create || this.type == SystemInstructionLayout.CreateWithSeed || + this.type == SystemInstructionLayout.NonceWithdraw || this.type == SystemInstructionLayout.Transfer ) { return this.keys[0].pubkey; @@ -81,6 +83,7 @@ export class SystemInstruction extends TransactionInstruction { if ( this.type == SystemInstructionLayout.Create || this.type == SystemInstructionLayout.CreateWithSeed || + this.type == SystemInstructionLayout.NonceWithdraw || this.type == SystemInstructionLayout.Transfer ) { return this.keys[1].pubkey; @@ -94,11 +97,11 @@ export class SystemInstruction extends TransactionInstruction { */ get amount(): number | null { const data = this.type.layout.decode(this.data); - if (this.type == SystemInstructionLayout.Transfer) { - return data.amount; - } else if ( + if ( this.type == SystemInstructionLayout.Create || - this.type == SystemInstructionLayout.CreateWithSeed + this.type == SystemInstructionLayout.CreateWithSeed || + this.type == SystemInstructionLayout.NonceWithdraw || + this.type == SystemInstructionLayout.Transfer ) { return data.lamports; } @@ -130,7 +133,7 @@ const SystemInstructionLayout = Object.freeze({ index: 2, layout: BufferLayout.struct([ BufferLayout.u32('instruction'), - BufferLayout.ns64('amount'), + BufferLayout.ns64('lamports'), ]), }, CreateWithSeed: { @@ -144,6 +147,31 @@ const SystemInstructionLayout = Object.freeze({ Layout.publicKey('programId'), ]), }, + NonceAdvance: { + index: 4, + layout: BufferLayout.struct([BufferLayout.u32('instruction')]), + }, + NonceWithdraw: { + index: 5, + layout: BufferLayout.struct([ + BufferLayout.u32('instruction'), + BufferLayout.ns64('lamports'), + ]), + }, + NonceInitialize: { + index: 6, + layout: BufferLayout.struct([ + BufferLayout.u32('instruction'), + Layout.publicKey('authorized'), + ]), + }, + NonceAuthorize: { + index: 7, + layout: BufferLayout.struct([ + BufferLayout.u32('instruction'), + Layout.publicKey('authorized'), + ]), + }, }); /** @@ -159,6 +187,13 @@ export class SystemProgram { ); } + /** + * Max space of a Nonce account + */ + static get nonceSpace(): number { + return 68; + } + /** * Generate a Transaction that creates a new account */ @@ -181,7 +216,7 @@ export class SystemProgram { {pubkey: from, isSigner: true, isWritable: true}, {pubkey: newAccount, isSigner: true, isWritable: true}, ], - programId: SystemProgram.programId, + programId: this.programId, data, }); } @@ -189,16 +224,20 @@ export class SystemProgram { /** * Generate a Transaction that transfers lamports from one account to another */ - static transfer(from: PublicKey, to: PublicKey, amount: number): Transaction { + static transfer( + from: PublicKey, + to: PublicKey, + lamports: number, + ): Transaction { const type = SystemInstructionLayout.Transfer; - const data = encodeData(type, {amount}); + const data = encodeData(type, {lamports}); return new Transaction().add({ keys: [ {pubkey: from, isSigner: true, isWritable: true}, {pubkey: to, isSigner: false, isWritable: true}, ], - programId: SystemProgram.programId, + programId: this.programId, data, }); } @@ -212,7 +251,7 @@ export class SystemProgram { return new Transaction().add({ keys: [{pubkey: from, isSigner: true, isWritable: true}], - programId: SystemProgram.programId, + programId: this.programId, data, }); } @@ -244,7 +283,126 @@ export class SystemProgram { {pubkey: from, isSigner: true, isWritable: true}, {pubkey: newAccount, isSigner: false, isWritable: true}, ], - programId: SystemProgram.programId, + programId: this.programId, + data, + }); + } + + /** + * Generate a Transaction that creates a new Nonce account + */ + static createNonceAccount( + from: PublicKey, + nonceAccount: PublicKey, + authorizedPubkey: PublicKey, + lamports: number, + ): Transaction { + let transaction = SystemProgram.createAccount( + from, + nonceAccount, + lamports, + this.nonceSpace, + this.programId, + ); + + const type = SystemInstructionLayout.NonceInitialize; + const data = encodeData(type, { + authorized: authorizedPubkey.toBuffer(), + }); + + return transaction.add({ + keys: [ + {pubkey: nonceAccount, isSigner: false, isWritable: true}, + { + pubkey: SYSVAR_RECENT_BLOCKHASHES_PUBKEY, + isSigner: false, + isWritable: false, + }, + {pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false}, + ], + programId: this.programId, + data, + }); + } + + /** + * Generate an instruction to advance the nonce in a Nonce account + */ + static nonceAdvance( + nonceAccount: PublicKey, + authorizedPubkey: PublicKey, + ): TransactionInstruction { + const type = SystemInstructionLayout.NonceAdvance; + const data = encodeData(type); + const instructionData = { + keys: [ + {pubkey: nonceAccount, isSigner: false, isWritable: true}, + { + pubkey: SYSVAR_RECENT_BLOCKHASHES_PUBKEY, + isSigner: false, + isWritable: false, + }, + {pubkey: authorizedPubkey, isSigner: true, isWritable: false}, + ], + programId: this.programId, + data, + }; + return new TransactionInstruction(instructionData); + } + + /** + * Generate a Transaction that withdraws lamports from a Nonce account + */ + static nonceWithdraw( + nonceAccount: PublicKey, + authorizedPubkey: PublicKey, + to: PublicKey, + lamports: number, + ): Transaction { + const type = SystemInstructionLayout.NonceWithdraw; + const data = encodeData(type, {lamports}); + + return new Transaction().add({ + keys: [ + {pubkey: nonceAccount, isSigner: false, isWritable: true}, + {pubkey: to, isSigner: false, isWritable: true}, + { + pubkey: SYSVAR_RECENT_BLOCKHASHES_PUBKEY, + isSigner: false, + isWritable: false, + }, + { + pubkey: SYSVAR_RENT_PUBKEY, + isSigner: false, + isWritable: false, + }, + {pubkey: authorizedPubkey, isSigner: true, isWritable: false}, + ], + programId: this.programId, + data, + }); + } + + /** + * Generate a Transaction that authorizes a new PublicKey as the authority + * on a Nonce account. + */ + static nonceAuthorize( + nonceAccount: PublicKey, + authorizedPubkey: PublicKey, + newAuthorized: PublicKey, + ): Transaction { + const type = SystemInstructionLayout.NonceAuthorize; + const data = encodeData(type, { + newAuthorized: newAuthorized.toBuffer(), + }); + + return new Transaction().add({ + keys: [ + {pubkey: nonceAccount, isSigner: false, isWritable: true}, + {pubkey: authorizedPubkey, isSigner: true, isWritable: false}, + ], + programId: this.programId, data, }); } diff --git a/web3.js/src/sysvar.js b/web3.js/src/sysvar.js index 47f445a42c..f259834671 100644 --- a/web3.js/src/sysvar.js +++ b/web3.js/src/sysvar.js @@ -5,6 +5,10 @@ export const SYSVAR_CLOCK_PUBKEY = new PublicKey( 'SysvarC1ock11111111111111111111111111111111', ); +export const SYSVAR_RECENT_BLOCKHASHES_PUBKEY = new PublicKey( + 'SysvarRecentB1ockHashes11111111111111111111', +); + export const SYSVAR_RENT_PUBKEY = new PublicKey( 'SysvarRent111111111111111111111111111111111', ); diff --git a/web3.js/test/system-program.test.js b/web3.js/test/system-program.test.js index 3798707e66..32cd1a17de 100644 --- a/web3.js/test/system-program.test.js +++ b/web3.js/test/system-program.test.js @@ -70,6 +70,58 @@ test('createAccountWithSeed', () => { // TODO: Validate transaction contents more }); +test('createNonceAccount', () => { + const from = new Account(); + const nonceAccount = new Account(); + + const transaction = SystemProgram.createNonceAccount( + from.publicKey, + nonceAccount.publicKey, + from.publicKey, + 123, + ); + + expect(transaction.instructions).toHaveLength(2); + expect(transaction.instructions[0].programId).toEqual( + SystemProgram.programId, + ); + expect(transaction.instructions[1].programId).toEqual(SystemProgram.programId); + // TODO: Validate transaction contents more +}); + +test('nonceWithdraw', () => { + const from = new Account(); + const nonceAccount = new Account(); + const to = new Account(); + + const transaction = SystemProgram.nonceWithdraw( + nonceAccount.publicKey, + from.publicKey, + to.publicKey, + 123, + ); + + expect(transaction.keys).toHaveLength(5); + expect(transaction.programId).toEqual(SystemProgram.programId); + // TODO: Validate transaction contents more +}); + +test('nonceAuthorize', () => { + const nonceAccount = new Account(); + const authorized = new Account(); + const newAuthorized = new Account(); + + const transaction = SystemProgram.nonceAuthorize( + nonceAccount.publicKey, + authorized.publicKey, + newAuthorized.publicKey, + ); + + expect(transaction.keys).toHaveLength(2); + expect(transaction.programId).toEqual(SystemProgram.programId); + // TODO: Validate transaction contents more +}); + test('SystemInstruction create', () => { const from = new Account(); const to = new Account(); @@ -149,6 +201,27 @@ test('SystemInstruction createWithSeed', () => { expect(systemInstruction.programId).toEqual(SystemProgram.programId); }); +test('SystemInstruction nonceWithdraw', () => { + const nonceAccount = new Account(); + const authorized = new Account(); + const to = new Account(); + const amount = 42; + const recentBlockhash = 'EETubP5AKHgjPAhzPAFcb8BAY1hMH639CWCFTqi3hq1k'; // Arbitrary known recentBlockhash + const nonceWithdraw = SystemProgram.nonceWithdraw( + nonceAccount.publicKey, + authorized.publicKey, + to.publicKey, + amount, + ); + const transaction = new Transaction({recentBlockhash}).add(nonceWithdraw); + + const systemInstruction = SystemInstruction.from(transaction.instructions[0]); + expect(systemInstruction.fromPublicKey).toEqual(nonceAccount.publicKey); + expect(systemInstruction.toPublicKey).toEqual(to.publicKey); + expect(systemInstruction.amount).toEqual(amount); + expect(systemInstruction.programId).toEqual(SystemProgram.programId); +}); + test('non-SystemInstruction error', () => { const from = new Account(); const program = new Account(); @@ -181,7 +254,7 @@ test('non-SystemInstruction error', () => { SystemInstruction.from(transaction.instructions[1]); }).toThrow(); - transaction.instructions[0].data[0] = 4; + transaction.instructions[0].data[0] = 11; expect(() => { SystemInstruction.from(transaction.instructions[0]); }).toThrow();