diff --git a/web3.js/module.flow.js b/web3.js/module.flow.js index ecc7e11c16..013d98d695 100644 --- a/web3.js/module.flow.js +++ b/web3.js/module.flow.js @@ -43,6 +43,13 @@ declare module '@solana/web3.js' { data: Buffer, }; + declare export type ContactInfo = { + id: string, + gossip: string, + tpu: string | null, + rpc: string | null, + }; + declare export type KeyedAccountInfo = { accountId: PublicKey, accountInfo: AccountInfo, @@ -62,9 +69,11 @@ declare module '@solana/web3.js' { declare export class Connection { constructor(endpoint: string): Connection; - getBalance(publicKey: PublicKey): Promise; getAccountInfo(publicKey: PublicKey): Promise; + getBalance(publicKey: PublicKey): Promise; + getClusterNodes(): Promise>; confirmTransaction(signature: TransactionSignature): Promise; + getSlotLeader(): Promise; getSignatureStatus( signature: TransactionSignature, ): Promise; @@ -102,7 +111,11 @@ declare module '@solana/web3.js' { space: number, programId: PublicKey, ): Transaction; - static transfer(from: PublicKey, to: PublicKey, amount: number): Transaction; + static transfer( + from: PublicKey, + to: PublicKey, + amount: number, + ): Transaction; static assign(from: PublicKey, programId: PublicKey): Transaction; } diff --git a/web3.js/package-lock.json b/web3.js/package-lock.json index 848299f044..39fe01d9e4 100644 --- a/web3.js/package-lock.json +++ b/web3.js/package-lock.json @@ -7522,8 +7522,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -7544,14 +7543,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7566,20 +7563,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -7696,8 +7690,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -7709,7 +7702,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -7724,7 +7716,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -7732,14 +7723,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -7758,7 +7747,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -7839,8 +7827,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -7852,7 +7839,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -7938,8 +7924,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -7975,7 +7960,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -7995,7 +7979,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -8039,14 +8022,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -8537,8 +8518,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==", - "dev": true, - "optional": true + "dev": true }, "hook-std": { "version": "1.2.0", @@ -8656,8 +8636,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=", - "dev": true, - "optional": true + "dev": true }, "acorn-globals": { "version": "1.0.9", diff --git a/web3.js/src/connection.js b/web3.js/src/connection.js index 98ca7010a5..0d101c4cb7 100644 --- a/web3.js/src/connection.js +++ b/web3.js/src/connection.js @@ -17,6 +17,22 @@ import type {TransactionSignature} from './transaction'; type RpcRequest = (methodName: string, args: Array) => any; +/** + * Information describing a cluster node + * + * @typedef {Object} ContactInfo + * @property {string} id Unique identifier of the node + * @property {string} gossip Gossip network address for the node + * @property {string} tpu TPU network address for the node (null if not available) + * @property {string|null} rpc JSON RPC network address for the node (null if not available) + */ +type ContactInfo = { + id: string, + gossip: string, + tpu: string | null, + rpc: string | null, +}; + function createRpcRequest(url): RpcRequest { const server = jayson(async (request, callback) => { const options = { @@ -120,6 +136,25 @@ const ProgramAccountNotificationResult = struct({ */ const ConfirmTransactionRpcResult = jsonRpcResult('boolean'); +/** + * Expected JSON RPC response for the "getSlotLeader" message + */ +const GetSlotLeader = jsonRpcResult('string'); + +/** + * Expected JSON RPC response for the "getClusterNodes" message + */ +const GetClusterNodes = jsonRpcResult( + struct.list([ + struct({ + id: 'string', + gossip: 'string', + tpu: struct.union(['null', 'string']), + rpc: struct.union(['null', 'string']), + }), + ]), +); + /** * Expected JSON RPC response for the "getSignatureStatus" message */ @@ -336,6 +371,32 @@ export class Connection { return res.result; } + /** + * Fetch the current slot leader of the cluster + */ + async getSlotLeader(): Promise { + const unsafeRes = await this._rpcRequest('getSlotLeader', []); + const res = GetSlotLeader(unsafeRes); + if (res.error) { + throw new Error(res.error.message); + } + assert(typeof res.result !== 'undefined'); + return res.result; + } + + /** + * Fetch the current slot leader of the cluster + */ + async getClusterNodes(): Promise> { + const unsafeRes = await this._rpcRequest('getClusterNodes', []); + const res = GetClusterNodes(unsafeRes); + if (res.error) { + throw new Error(res.error.message); + } + assert(typeof res.result !== 'undefined'); + return res.result; + } + /** * Fetch the current transaction count of the cluster */ diff --git a/web3.js/src/util/send-and-confirm-raw-transaction.js b/web3.js/src/util/send-and-confirm-raw-transaction.js index befe712a39..ec405fca65 100644 --- a/web3.js/src/util/send-and-confirm-raw-transaction.js +++ b/web3.js/src/util/send-and-confirm-raw-transaction.js @@ -41,5 +41,7 @@ export async function sendAndConfirmRawTransaction( return signature; } - throw new Error(`Raw transaction ${signature} failed (${JSON.stringify(status)})`); + throw new Error( + `Raw transaction ${signature} failed (${JSON.stringify(status)})`, + ); } diff --git a/web3.js/src/util/send-and-confirm-transaction.js b/web3.js/src/util/send-and-confirm-transaction.js index 299bb93b70..48d16b6a3f 100644 --- a/web3.js/src/util/send-and-confirm-transaction.js +++ b/web3.js/src/util/send-and-confirm-transaction.js @@ -52,7 +52,9 @@ export async function sendAndConfirmTransaction( } if (status && status.Err && !('AccountInUse' in status.Err)) { - throw new Error(`Transaction ${signature} failed (${JSON.stringify(status)})`); + throw new Error( + `Transaction ${signature} failed (${JSON.stringify(status)})`, + ); } // Retry in 0..100ms to try to avoid another AccountInUse collision diff --git a/web3.js/test/connection.test.js b/web3.js/test/connection.test.js index 3a56e37bdd..ab7a57a48b 100644 --- a/web3.js/test/connection.test.js +++ b/web3.js/test/connection.test.js @@ -64,6 +64,64 @@ test('get balance', async () => { expect(balance).toBeGreaterThanOrEqual(0); }); +test('get slot leader', async () => { + const connection = new Connection(url); + + mockRpc.push([ + url, + { + method: 'getSlotLeader', + }, + { + error: null, + result: '11111111111111111111111111111111', + }, + ]); + + const slotLeader = await connection.getSlotLeader(); + if (mockRpcEnabled) { + expect(slotLeader).toBe('11111111111111111111111111111111'); + } else { + // No idea what the correct slotLeader value should be on a live cluster, so + // just check the type + expect(typeof slotLeader).toBe('string'); + } +}); + +test('get cluster nodes', async () => { + const connection = new Connection(url); + + mockRpc.push([ + url, + { + method: 'getClusterNodes', + }, + { + error: null, + result: [ + { + id: '11111111111111111111111111111111', + gossip: '127.0.0.0:1234', + tpu: '127.0.0.0:1235', + rpc: null, + }, + ], + }, + ]); + + const clusterNodes = await connection.getClusterNodes(); + if (mockRpcEnabled) { + expect(clusterNodes).toHaveLength(1); + expect(clusterNodes[0].id).toBe('11111111111111111111111111111111'); + expect(typeof clusterNodes[0].gossip).toBe('string'); + expect(typeof clusterNodes[0].tpu).toBe('string'); + expect(clusterNodes[0].rpc).toBeNull(); + } else { + // There should be at least one node (the node that we're talking to) + expect(clusterNodes.length).toBeGreaterThan(0); + } +}); + test('confirm transaction - error', () => { const connection = new Connection(url); diff --git a/web3.js/test/native-loader.test.js b/web3.js/test/native-loader.test.js index 4e2aae75e8..9f092bca12 100644 --- a/web3.js/test/native-loader.test.js +++ b/web3.js/test/native-loader.test.js @@ -23,7 +23,11 @@ test('load native program', async () => { const connection = new Connection(url); const from = await newAccountWithLamports(connection, 1024); - const programId = await NativeLoader.load(connection, from, 'solana_noop_program'); + const programId = await NativeLoader.load( + connection, + from, + 'solana_noop_program', + ); const transaction = new Transaction().add({ keys: [{pubkey: from.publicKey, isSigner: true}], programId, diff --git a/web3.js/test/transaction.test.js b/web3.js/test/transaction.test.js index c7374cc295..cac5a73164 100644 --- a/web3.js/test/transaction.test.js +++ b/web3.js/test/transaction.test.js @@ -10,7 +10,11 @@ test('signPartial', () => { const account1 = new Account(); const account2 = new Account(); const recentBlockhash = account1.publicKey.toBase58(); // Fake recentBlockhash - const transfer = SystemProgram.transfer(account1.publicKey, account2.publicKey, 123); + const transfer = SystemProgram.transfer( + account1.publicKey, + account2.publicKey, + 123, + ); const transaction = new Transaction({recentBlockhash}).add(transfer); transaction.sign(account1, account2); @@ -27,10 +31,21 @@ test('transfer signatures', () => { const account1 = new Account(); const account2 = new Account(); const recentBlockhash = account1.publicKey.toBase58(); // Fake recentBlockhash - const transfer1 = SystemProgram.transfer(account1.publicKey, account2.publicKey, 123); - const transfer2 = SystemProgram.transfer(account2.publicKey, account1.publicKey, 123); + const transfer1 = SystemProgram.transfer( + account1.publicKey, + account2.publicKey, + 123, + ); + const transfer2 = SystemProgram.transfer( + account2.publicKey, + account1.publicKey, + 123, + ); - const orgTransaction = new Transaction({recentBlockhash}).add(transfer1, transfer2); + const orgTransaction = new Transaction({recentBlockhash}).add( + transfer1, + transfer2, + ); orgTransaction.sign(account1, account2); const newTransaction = new Transaction({