diff --git a/web3.js/src/connection.js b/web3.js/src/connection.js index 5f280b9cb8..b3d8bc730f 100644 --- a/web3.js/src/connection.js +++ b/web3.js/src/connection.js @@ -7,8 +7,9 @@ import jayson from 'jayson/lib/client/browser'; import {struct} from 'superstruct'; import {Client as RpcWebSocketClient} from 'rpc-websockets'; -import {DEFAULT_TICKS_PER_SLOT, NUM_TICKS_PER_SECOND} from './timing'; +import {NonceAccount} from './nonce-account'; import {PublicKey} from './publickey'; +import {DEFAULT_TICKS_PER_SLOT, NUM_TICKS_PER_SECOND} from './timing'; import {Transaction} from './transaction'; import {sleep} from './util/sleep'; import type {Blockhash} from './blockhash'; @@ -1149,6 +1150,55 @@ export class Connection { }; } + /** + * Fetch the contents of a Nonce account from the cluster + */ + async getNonceAndContext( + nonceAccount: PublicKey, + commitment: ?Commitment, + ): Promise> { + const args = this._argsWithCommitment( + [nonceAccount.toBase58()], + commitment, + ); + const unsafeRes = await this._rpcRequest('getAccountInfo', args); + const res = GetAccountInfoAndContextRpcResult(unsafeRes); + if (res.error) { + throw new Error(res.error.message); + } + assert(typeof res.result !== 'undefined'); + + const isV021 = + typeof res.result.context !== 'undefined' && + typeof res.result.value !== 'undefined'; + + const slot = isV021 ? res.result.context.slot : NaN; + const resultValue = isV021 ? res.result.value : res.result; + + if (!resultValue) { + throw new Error('Invalid request'); + } + + const value = NonceAccount.fromAccountData(Buffer.from(resultValue.data)); + + return { + context: { + slot, + }, + value, + }; + } + async getNonce( + nonceAccount: PublicKey, + commitment: ?Commitment, + ): Promise { + return await this.getNonceAndContext(nonceAccount, commitment) + .then(x => x.value) + .catch(e => { + throw e; + }); + } + /** * Request an allocation of lamports to the specified account */ diff --git a/web3.js/src/index.js b/web3.js/src/index.js index 0700703883..0fbbac7cc2 100644 --- a/web3.js/src/index.js +++ b/web3.js/src/index.js @@ -4,6 +4,7 @@ export {BpfLoader} from './bpf-loader'; export {BudgetProgram} from './budget-program'; export {Connection} from './connection'; export {Loader} from './loader'; +export {NonceAccount} from './nonce-account'; export {PublicKey} from './publickey'; export { STAKE_CONFIG_ID, diff --git a/web3.js/src/nonce-account.js b/web3.js/src/nonce-account.js new file mode 100644 index 0000000000..074c7a521c --- /dev/null +++ b/web3.js/src/nonce-account.js @@ -0,0 +1,40 @@ +// @flow +import * as BufferLayout from 'buffer-layout'; + +import type {Blockhash} from './blockhash'; +import * as Layout from './layout'; +import {PublicKey} from './publickey'; + +/** + * See https://github.com/solana-labs/solana/blob/0ea2843ec9cdc517572b8e62c959f41b55cf4453/sdk/src/nonce_state.rs#L29-L32 + * + * @private + */ +const NonceAccountLayout = BufferLayout.struct([ + BufferLayout.u32('state'), + Layout.publicKey('authorizedPubkey'), + Layout.publicKey('hash'), +]); + +/** + * NonceAccount class + */ +export class NonceAccount { + authorizedPubkey: PublicKey; + nonce: Blockhash; + + /** + * Deserialize NonceAccount from the account data. + * + * @param buffer account data + * @return NonceAccount + */ + static fromAccountData(buffer: Buffer): NonceAccount { + const nonceAccount = NonceAccountLayout.decode(buffer, 0); + nonceAccount.authorizedPubkey = new PublicKey( + nonceAccount.authorizedPubkey, + ); + nonceAccount.nonce = new PublicKey(nonceAccount.nonce).toString(); + return nonceAccount; + } +} diff --git a/web3.js/test/nonce.test.js b/web3.js/test/nonce.test.js new file mode 100644 index 0000000000..32d7b5536a --- /dev/null +++ b/web3.js/test/nonce.test.js @@ -0,0 +1,154 @@ +// @flow + +import bs58 from 'bs58'; + +import {Account, Connection, SystemProgram} from '../src'; +import {mockRpc, mockRpcEnabled} from './__mocks__/node-fetch'; +import {mockGetRecentBlockhash} from './mockrpc/get-recent-blockhash'; +import {url} from './url'; + +if (!mockRpcEnabled) { + // Testing max commitment level takes around 20s to complete + jest.setTimeout(30000); +} + +test('create and query nonce account', async () => { + const from = new Account(); + const nonceAccount = new Account(); + const connection = new Connection(url, 'recent'); + + mockRpc.push([ + url, + { + method: 'getMinimumBalanceForRentExemption', + params: [68, {commitment: 'recent'}], + }, + { + error: null, + result: 50, + }, + ]); + + const minimumAmount = await connection.getMinimumBalanceForRentExemption( + SystemProgram.nonceSpace, + 'recent', + ); + + mockRpc.push([ + url, + { + method: 'requestAirdrop', + params: [ + from.publicKey.toBase58(), + minimumAmount * 2, + {commitment: 'recent'}, + ], + }, + { + error: null, + result: + '1WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', + }, + ]); + + await connection.requestAirdrop(from.publicKey, minimumAmount * 2); + + mockRpc.push([ + url, + { + method: 'getBalance', + params: [from.publicKey.toBase58(), {commitment: 'recent'}], + }, + { + error: null, + result: minimumAmount * 2, + }, + ]); + + const balance = await connection.getBalance(from.publicKey); + expect(balance).toBe(minimumAmount * 2); + + mockGetRecentBlockhash('recent'); + mockRpc.push([ + url, + { + method: 'sendTransaction', + }, + { + error: null, + result: + '3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', + }, + ]); + + const transaction = SystemProgram.createNonceAccount( + from.publicKey, + nonceAccount.publicKey, + from.publicKey, + minimumAmount, + ); + await connection.sendTransaction(transaction, from, nonceAccount); + + const expectedData = Buffer.alloc(68); + expectedData.writeInt32LE(1, 0); + from.publicKey.toBuffer().copy(expectedData, 4); + const mockNonce = new Account(); + mockNonce.publicKey.toBuffer().copy(expectedData, 36); + + mockRpc.push([ + url, + { + method: 'getAccountInfo', + params: [nonceAccount.publicKey.toBase58(), {commitment: 'recent'}], + }, + { + error: null, + result: { + owner: [ + 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, + ], + lamports: minimumAmount, + data: [...expectedData], + executable: false, + }, + }, + ]); + // + const nonceAccountData = await connection.getNonce( + nonceAccount.publicKey, + 'recent', + ); + expect(nonceAccountData.authorizedPubkey).toEqual(from.publicKey); + expect(bs58.decode(nonceAccountData.nonce).length).toBeGreaterThan(30); +});