From bb06817e1612f9a5296deddb831b7819b67d28ee Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Fri, 26 Feb 2021 15:06:12 +0800 Subject: [PATCH] feat: update superstruct and future proof type validations (#15491) * chore: update superstruct * fix: fix program account notification type coercion --- web3.js/package-lock.json | 36 +- web3.js/package.json | 2 +- web3.js/src/connection.js | 1463 ++++++++++++------------------- web3.js/src/validator-info.js | 20 +- web3.js/test/connection.test.js | 7 +- web3.js/test/nonce.test.js | 2 + 6 files changed, 591 insertions(+), 939 deletions(-) diff --git a/web3.js/package-lock.json b/web3.js/package-lock.json index c61ef0810c..a17916d43e 100644 --- a/web3.js/package-lock.json +++ b/web3.js/package-lock.json @@ -3741,6 +3741,16 @@ "base64-js": "^1.3.1", "ieee754": "^1.1.13" } + }, + "superstruct": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.8.4.tgz", + "integrity": "sha512-48Ors8IVWZm/tMr8r0Si6+mJiB7mkD7jqvIzktjJ4+EnP5tBp0qOpiM1J8sCUorKx+TXWrfb3i1UcjdD1YK/wA==", + "dev": true, + "requires": { + "kind-of": "^6.0.2", + "tiny-invariant": "^1.0.6" + } } } }, @@ -5914,7 +5924,7 @@ }, "is-obj": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "resolved": "http://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", "dev": true } @@ -18426,20 +18436,9 @@ } }, "superstruct": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.8.3.tgz", - "integrity": "sha512-LbtbFpktW1FcwxVIJlxdk7bCyBq/GzOx2FSFLRLTUhWIA1gHkYPIl3aXRG5mBdGZtnPNT6t+4eEcLDCMOuBHww==", - "requires": { - "kind-of": "^6.0.2", - "tiny-invariant": "^1.0.6" - }, - "dependencies": { - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" - } - } + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz", + "integrity": "sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ==" }, "supports-color": { "version": "2.0.0", @@ -18857,9 +18856,10 @@ } }, "tiny-invariant": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.6.tgz", - "integrity": "sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", + "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==", + "dev": true }, "to-fast-properties": { "version": "1.0.3", diff --git a/web3.js/package.json b/web3.js/package.json index f447c0d87d..f995f25061 100644 --- a/web3.js/package.json +++ b/web3.js/package.json @@ -94,7 +94,7 @@ "node-fetch": "^2.6.1", "rpc-websockets": "^7.4.2", "secp256k1": "^4.0.2", - "superstruct": "^0.8.3", + "superstruct": "^0.14.2", "tweetnacl": "^1.0.0" }, "devDependencies": { diff --git a/web3.js/src/connection.js b/web3.js/src/connection.js index 04520bae44..d85169b13e 100644 --- a/web3.js/src/connection.js +++ b/web3.js/src/connection.js @@ -6,7 +6,24 @@ import {Buffer} from 'buffer'; import {parse as urlParse, format as urlFormat} from 'url'; import fetch from 'node-fetch'; import jayson from 'jayson/lib/client/browser'; -import {struct} from 'superstruct'; +import { + type as pick, + number, + string, + array, + boolean, + literal, + record, + union, + optional, + nullable, + coerce, + instance, + create, + tuple, + unknown, +} from 'superstruct'; +import type {Struct} from 'superstruct'; import {Client as RpcWebSocketClient} from 'rpc-websockets'; import {AgentManager} from './agent-manager'; @@ -24,6 +41,20 @@ import type {Account} from './account'; import type {TransactionSignature} from './transaction'; import type {CompiledInstruction} from './message'; +const PublicKeyFromString = coerce( + instance(PublicKey), + string(), + value => new PublicKey(value), +); + +const RawAccountDataResult = tuple([string(), literal('base64')]); + +const BufferFromRawAccountData = coerce( + instance(Buffer), + RawAccountDataResult, + value => Buffer.from(value[0], 'base64'), +); + export const BLOCKHASH_CACHE_TIMEOUT_MS = 30 * 1000; type RpcRequest = (methodName: string, args: Array) => any; @@ -101,44 +132,66 @@ type RpcResponseAndContext = { /** * @private */ -function jsonRpcResultAndContext(resultDescription: any) { - return jsonRpcResult({ - context: struct({ - slot: 'number', +function createRpcResult(result: Struct) { + return union([ + pick({ + jsonrpc: literal('2.0'), + id: string(), + result, }), - value: resultDescription, + pick({ + jsonrpc: literal('2.0'), + id: string(), + error: pick({ + code: unknown(), + message: string(), + data: optional(unknown()), + }), + }), + ]); +} + +const UnknownRpcResult = createRpcResult(unknown()); + +/** + * @private + */ +function jsonRpcResult(schema: Struct) { + return coerce(createRpcResult(schema), UnknownRpcResult, value => { + if ('error' in value) { + return value; + } else { + return { + ...value, + result: create(value.result, schema), + }; + } }); } /** * @private */ -function jsonRpcResult(resultDescription: any) { - const jsonRpcVersion = struct.literal('2.0'); - return struct.union([ - struct({ - jsonrpc: jsonRpcVersion, - id: 'string', - error: 'any', +function jsonRpcResultAndContext(value: Struct) { + return jsonRpcResult( + pick({ + context: pick({ + slot: number(), + }), + value, }), - struct({ - jsonrpc: jsonRpcVersion, - id: 'string', - error: 'null?', - result: resultDescription, - }), - ]); + ); } /** * @private */ -function notificationResultAndContext(resultDescription: any) { - return struct({ - context: struct({ - slot: 'number', +function notificationResultAndContext(value: Struct) { + return pick({ + context: pick({ + slot: number(), }), - value: resultDescription, + value, }); } @@ -266,12 +319,12 @@ type InflationGovernor = { terminal: number, }; -const GetInflationGovernorResult = struct({ - foundation: 'number', - foundationTerm: 'number', - initial: 'number', - taper: 'number', - terminal: 'number', +const GetInflationGovernorResult = pick({ + foundation: number(), + foundationTerm: number(), + initial: number(), + taper: number(), + terminal: number(), }); /** @@ -294,13 +347,13 @@ type EpochInfo = { transactionCount: number | null, }; -const GetEpochInfoResult = struct({ - epoch: 'number', - slotIndex: 'number', - slotsInEpoch: 'number', - absoluteSlot: 'number', - blockHeight: 'number?', - transactionCount: 'number?', +const GetEpochInfoResult = pick({ + epoch: number(), + slotIndex: number(), + slotsInEpoch: number(), + absoluteSlot: number(), + blockHeight: optional(number()), + transactionCount: optional(number()), }); /** @@ -322,12 +375,12 @@ type EpochSchedule = { firstNormalSlot: number, }; -const GetEpochScheduleResult = struct({ - slotsPerEpoch: 'number', - leaderScheduleSlotOffset: 'number', - warmup: 'boolean', - firstNormalEpoch: 'number', - firstNormalSlot: 'number', +const GetEpochScheduleResult = pick({ + slotsPerEpoch: number(), + leaderScheduleSlotOffset: number(), + warmup: boolean(), + firstNormalEpoch: number(), + firstNormalSlot: number(), }); /** @@ -340,20 +393,20 @@ type LeaderSchedule = { [address: string]: number[], }; -const GetLeaderScheduleResult = struct.record([ - 'string', - 'any', // validating struct.array(['number']) is extremely slow -]); +// TODO: check if validating array(number()) is still extremely slow +const GetLeaderScheduleResult = record(string(), unknown()); /** * Transaction error or null */ -const TransactionErrorResult = struct.union(['null', 'object']); +const TransactionErrorResult = nullable(pick({})); /** * Signature status for a transaction */ -const SignatureStatusResult = struct({err: TransactionErrorResult}); +const SignatureStatusResult = pick({ + err: TransactionErrorResult, +}); /** * Version info for a node @@ -361,9 +414,9 @@ const SignatureStatusResult = struct({err: TransactionErrorResult}); * @typedef {Object} Version * @property {string} solana-core Version of solana-core */ -const Version = struct.pick({ - 'solana-core': 'string', - 'feature-set': 'number?', +const Version = pick({ + 'solana-core': string(), + 'feature-set': optional(nullable(number())), }); type SimulatedTransactionResponse = { @@ -371,10 +424,10 @@ type SimulatedTransactionResponse = { logs: Array | null, }; -const SimulatedTransactionResponseValidator = jsonRpcResultAndContext( - struct.pick({ - err: struct.union(['null', 'object', 'string']), - logs: struct.union(['null', struct.array(['string'])]), +const SimulatedTransactionResponseStruct = jsonRpcResultAndContext( + pick({ + err: nullable(union([pick({}), string()])), + logs: nullable(array(string())), }), ); @@ -650,62 +703,27 @@ function createRpcRequest(url: string, useHttps: boolean): RpcRequest { /** * Expected JSON RPC response for the "getInflationGovernor" message */ -const GetInflationGovernorRpcResult = struct({ - jsonrpc: struct.literal('2.0'), - id: 'string', - error: 'any?', - result: GetInflationGovernorResult, -}); +const GetInflationGovernorRpcResult = jsonRpcResult(GetInflationGovernorResult); /** * Expected JSON RPC response for the "getEpochInfo" message */ -const GetEpochInfoRpcResult = struct({ - jsonrpc: struct.literal('2.0'), - id: 'string', - error: 'any?', - result: GetEpochInfoResult, -}); +const GetEpochInfoRpcResult = jsonRpcResult(GetEpochInfoResult); /** * Expected JSON RPC response for the "getEpochSchedule" message */ -const GetEpochScheduleRpcResult = struct({ - jsonrpc: struct.literal('2.0'), - id: 'string', - error: 'any?', - result: GetEpochScheduleResult, -}); +const GetEpochScheduleRpcResult = jsonRpcResult(GetEpochScheduleResult); /** * Expected JSON RPC response for the "getLeaderSchedule" message */ const GetLeaderScheduleRpcResult = jsonRpcResult(GetLeaderScheduleResult); -/** - * Expected JSON RPC response for the "getBalance" message - */ -const GetBalanceAndContextRpcResult = jsonRpcResultAndContext('number?'); - -/** - * Expected JSON RPC response for the "getBlockTime" message - */ -const GetBlockTimeRpcResult = struct({ - jsonrpc: struct.literal('2.0'), - id: 'string', - error: 'any?', - result: struct.union(['null', 'number', 'undefined']), -}); - /** * Expected JSON RPC response for the "minimumLedgerSlot" and "getFirstAvailableBlock" messages */ -const SlotRpcResult = struct({ - jsonrpc: struct.literal('2.0'), - id: 'string', - error: 'any?', - result: 'number', -}); +const SlotRpcResult = jsonRpcResult(number()); /** * Supply @@ -727,11 +745,11 @@ type Supply = { * Expected JSON RPC response for the "getSupply" message */ const GetSupplyRpcResult = jsonRpcResultAndContext( - struct({ - total: 'number', - circulating: 'number', - nonCirculating: 'number', - nonCirculatingAccounts: struct.array(['string']), + pick({ + total: number(), + circulating: number(), + nonCirculating: number(), + nonCirculatingAccounts: array(PublicKeyFromString), }), ); @@ -753,10 +771,10 @@ type TokenAmount = { /** * Expected JSON RPC structure for token amounts */ -const TokenAmountResult = struct.object({ - amount: 'string', - uiAmount: 'number', - decimals: 'number', +const TokenAmountResult = pick({ + amount: string(), + uiAmount: number(), + decimals: number(), }); /** @@ -779,64 +797,56 @@ type TokenAccountBalancePair = { * Expected JSON RPC response for the "getTokenLargestAccounts" message */ const GetTokenLargestAccountsResult = jsonRpcResultAndContext( - struct.array([ - struct.pick({ - address: 'string', - amount: 'string', - uiAmount: 'number', - decimals: 'number', + array( + pick({ + address: PublicKeyFromString, + amount: string(), + uiAmount: number(), + decimals: number(), }), - ]), + ), ); -/** - * Expected JSON RPC response for the "getTokenAccountBalance" message - */ -const GetTokenAccountBalance = jsonRpcResultAndContext(TokenAmountResult); - -/** - * Expected JSON RPC response for the "getTokenSupply" message - */ -const GetTokenSupplyRpcResult = jsonRpcResultAndContext(TokenAmountResult); - /** * Expected JSON RPC response for the "getTokenAccountsByOwner" message */ const GetTokenAccountsByOwner = jsonRpcResultAndContext( - struct.array([ - struct.object({ - pubkey: 'string', - account: struct.object({ - executable: 'boolean', - owner: 'string', - lamports: 'number', - data: ['string', struct.literal('base64')], - rentEpoch: 'number?', + array( + pick({ + pubkey: PublicKeyFromString, + account: pick({ + executable: boolean(), + owner: PublicKeyFromString, + lamports: number(), + data: BufferFromRawAccountData, + rentEpoch: number(), }), }), - ]), + ), ); +const ParsedAccountDataResult = pick({ + program: string(), + parsed: unknown(), + space: number(), +}); + /** * Expected JSON RPC response for the "getTokenAccountsByOwner" message with parsed data */ const GetParsedTokenAccountsByOwner = jsonRpcResultAndContext( - struct.array([ - struct.object({ - pubkey: 'string', - account: struct.object({ - executable: 'boolean', - owner: 'string', - lamports: 'number', - data: struct.pick({ - program: 'string', - parsed: 'any', - space: 'number', - }), - rentEpoch: 'number?', + array( + pick({ + pubkey: PublicKeyFromString, + account: pick({ + executable: boolean(), + owner: PublicKeyFromString, + lamports: number(), + data: ParsedAccountDataResult, + rentEpoch: number(), }), }), - ]), + ), ); /** @@ -855,91 +865,80 @@ type AccountBalancePair = { * Expected JSON RPC response for the "getLargestAccounts" message */ const GetLargestAccountsRpcResult = jsonRpcResultAndContext( - struct.array([ - struct({ - lamports: 'number', - address: 'string', + array( + pick({ + lamports: number(), + address: PublicKeyFromString, }), - ]), + ), ); /** - * Expected JSON RPC response for the "getVersion" message + * @private */ -const GetVersionRpcResult = struct({ - jsonrpc: struct.literal('2.0'), - id: 'string', - error: 'any?', - result: Version, +const AccountInfoResult = pick({ + executable: boolean(), + owner: PublicKeyFromString, + lamports: number(), + data: BufferFromRawAccountData, + rentEpoch: number(), }); /** * @private */ -const AccountInfoResult = struct({ - executable: 'boolean', - owner: 'string', - lamports: 'number', - data: 'any', - rentEpoch: 'number?', +const KeyedAccountInfoResult = pick({ + pubkey: PublicKeyFromString, + account: AccountInfoResult, +}); + +const ParsedOrRawAccountData = coerce( + union([instance(Buffer), ParsedAccountDataResult]), + union([RawAccountDataResult, ParsedAccountDataResult]), + value => { + if (Array.isArray(value)) { + return create(value, BufferFromRawAccountData); + } else { + return value; + } + }, +); + +/** + * @private + */ +const ParsedAccountInfoResult = pick({ + executable: boolean(), + owner: PublicKeyFromString, + lamports: number(), + data: ParsedOrRawAccountData, + rentEpoch: number(), +}); + +const KeyedParsedAccountInfoResult = pick({ + pubkey: PublicKeyFromString, + account: ParsedAccountInfoResult, }); /** * @private */ -const ParsedAccountInfoResult = struct.object({ - executable: 'boolean', - owner: 'string', - lamports: 'number', - data: struct.union([ - ['string', struct.literal('base64')], - struct.pick({ - program: 'string', - parsed: 'any', - space: 'number', - }), +const StakeActivationResult = pick({ + state: union([ + literal('active'), + literal('inactive'), + literal('activating'), + literal('deactivating'), ]), - rentEpoch: 'number?', + active: number(), + inactive: number(), }); -/** - * @private - */ -const StakeActivationResult = struct.object({ - state: struct.union([ - struct.literal('active'), - struct.literal('inactive'), - struct.literal('activating'), - struct.literal('deactivating'), - ]), - active: 'number', - inactive: 'number', -}); - -/** - * Expected JSON RPC response for the "getAccountInfo" message - */ -const GetAccountInfoAndContextRpcResult = jsonRpcResultAndContext( - struct.union(['null', AccountInfoResult]), -); - -/** - * Expected JSON RPC response for the "getAccountInfo" message with jsonParsed param - */ -const GetParsedAccountInfoResult = jsonRpcResultAndContext( - struct.union(['null', ParsedAccountInfoResult]), -); - -/** - * Expected JSON RPC response for the "getStakeActivation" message with jsonParsed param - */ -const GetStakeActivationResult = jsonRpcResult(StakeActivationResult); - /** * Expected JSON RPC response for the "getConfirmedSignaturesForAddress" message */ const GetConfirmedSignaturesForAddressRpcResult = jsonRpcResult( - struct.array(['string']), + array(string()), ); /** @@ -947,479 +946,377 @@ const GetConfirmedSignaturesForAddressRpcResult = jsonRpcResult( */ const GetConfirmedSignaturesForAddress2RpcResult = jsonRpcResult( - struct.array([ - struct.pick({ - signature: 'string', - slot: 'number', + array( + pick({ + signature: string(), + slot: number(), err: TransactionErrorResult, - memo: struct.union(['null', 'string']), - blockTime: struct.union(['undefined', 'null', 'number']), + memo: nullable(string()), + blockTime: optional(nullable(number())), }), - ]), + ), ); /*** * Expected JSON RPC response for the "accountNotification" message */ -const AccountNotificationResult = struct({ - subscription: 'number', +const AccountNotificationResult = pick({ + subscription: number(), result: notificationResultAndContext(AccountInfoResult), }); /** * @private */ -const ProgramAccountInfoResult = struct({ - pubkey: 'string', +const ProgramAccountInfoResult = pick({ + pubkey: PublicKeyFromString, account: AccountInfoResult, }); -/** - * @private - */ -const ParsedProgramAccountInfoResult = struct({ - pubkey: 'string', - account: ParsedAccountInfoResult, -}); - /*** * Expected JSON RPC response for the "programNotification" message */ -const ProgramAccountNotificationResult = struct({ - subscription: 'number', +const ProgramAccountNotificationResult = pick({ + subscription: number(), result: notificationResultAndContext(ProgramAccountInfoResult), }); /** * @private */ -const SlotInfoResult = struct({ - parent: 'number', - slot: 'number', - root: 'number', +const SlotInfoResult = pick({ + parent: number(), + slot: number(), + root: number(), }); /** * Expected JSON RPC response for the "slotNotification" message */ -const SlotNotificationResult = struct({ - subscription: 'number', +const SlotNotificationResult = pick({ + subscription: number(), result: SlotInfoResult, }); /** * Expected JSON RPC response for the "signatureNotification" message */ -const SignatureNotificationResult = struct({ - subscription: 'number', +const SignatureNotificationResult = pick({ + subscription: number(), result: notificationResultAndContext(SignatureStatusResult), }); /** * Expected JSON RPC response for the "rootNotification" message */ -const RootNotificationResult = struct({ - subscription: 'number', - result: 'number', +const RootNotificationResult = pick({ + subscription: number(), + result: number(), }); -/** - * Expected JSON RPC response for the "getProgramAccounts" message - */ -const GetProgramAccountsRpcResult = jsonRpcResult( - struct.array([ProgramAccountInfoResult]), -); +const ContactInfoResult = pick({ + pubkey: string(), + gossip: nullable(string()), + tpu: nullable(string()), + rpc: nullable(string()), + version: nullable(string()), +}); -/** - * Expected JSON RPC response for the "getProgramAccounts" message - */ -const GetParsedProgramAccountsRpcResult = jsonRpcResult( - struct.array([ParsedProgramAccountInfoResult]), -); - -/** - * Expected JSON RPC response for the "getSlot" message - */ -const GetSlot = jsonRpcResult('number'); - -/** - * Expected JSON RPC response for the "getSlotLeader" message - */ -const GetSlotLeader = jsonRpcResult('string'); - -/** - * Expected JSON RPC response for the "getClusterNodes" message - */ -const GetClusterNodes = jsonRpcResult( - struct.array([ - struct.pick({ - pubkey: 'string', - gossip: struct.union(['null', 'string']), - tpu: struct.union(['null', 'string']), - rpc: struct.union(['null', 'string']), - version: struct.union(['null', 'string']), - }), - ]), -); +const VoteAccountInfoResult = pick({ + votePubkey: string(), + nodePubkey: string(), + activatedStake: number(), + epochVoteAccount: boolean(), + epochCredits: array(tuple([number(), number(), number()])), + commission: number(), + lastVote: number(), + rootSlot: nullable(number()), +}); /** * Expected JSON RPC response for the "getVoteAccounts" message */ const GetVoteAccounts = jsonRpcResult( - struct({ - current: struct.array([ - struct.pick({ - votePubkey: 'string', - nodePubkey: 'string', - activatedStake: 'number', - epochVoteAccount: 'boolean', - epochCredits: struct.array([ - struct.tuple(['number', 'number', 'number']), - ]), - commission: 'number', - lastVote: 'number', - rootSlot: 'number?', - }), - ]), - delinquent: struct.array([ - struct.pick({ - votePubkey: 'string', - nodePubkey: 'string', - activatedStake: 'number', - epochVoteAccount: 'boolean', - epochCredits: struct.array([ - struct.tuple(['number', 'number', 'number']), - ]), - commission: 'number', - lastVote: 'number', - rootSlot: 'number?', - }), - ]), + pick({ + current: array(VoteAccountInfoResult), + delinquent: array(VoteAccountInfoResult), }), ); +const SignatureStatusResponse = pick({ + slot: number(), + confirmations: nullable(number()), + err: TransactionErrorResult, + confirmationStatus: optional(nullable(string())), +}); + /** * Expected JSON RPC response for the "getSignatureStatuses" message */ const GetSignatureStatusesRpcResult = jsonRpcResultAndContext( - struct.array([ - struct.union([ - 'null', - struct.pick({ - slot: 'number', - confirmations: struct.union(['number', 'null']), - err: TransactionErrorResult, - confirmationStatus: 'string?', - }), - ]), - ]), + array(nullable(SignatureStatusResponse)), ); -/** - * Expected JSON RPC response for the "getTransactionCount" message - */ -const GetTransactionCountRpcResult = jsonRpcResult('number'); - -/** - * Expected JSON RPC response for the "getTotalSupply" message - */ -const GetTotalSupplyRpcResult = jsonRpcResult('number'); - /** * Expected JSON RPC response for the "getMinimumBalanceForRentExemption" message */ -const GetMinimumBalanceForRentExemptionRpcResult = jsonRpcResult('number'); +const GetMinimumBalanceForRentExemptionRpcResult = jsonRpcResult(number()); /** * @private */ -const ConfirmedTransactionResult = struct({ - signatures: struct.array(['string']), - message: struct({ - accountKeys: struct.array(['string']), - header: struct({ - numRequiredSignatures: 'number', - numReadonlySignedAccounts: 'number', - numReadonlyUnsignedAccounts: 'number', +const ConfirmedTransactionResult = pick({ + signatures: array(string()), + message: pick({ + accountKeys: array(string()), + header: pick({ + numRequiredSignatures: number(), + numReadonlySignedAccounts: number(), + numReadonlyUnsignedAccounts: number(), }), - instructions: struct.array([ - struct({ - accounts: struct.array(['number']), - data: 'string', - programIdIndex: 'number', + instructions: array( + pick({ + accounts: array(number()), + data: string(), + programIdIndex: number(), }), - ]), - recentBlockhash: 'string', + ), + recentBlockhash: string(), }), }); +const TransactionFromConfirmed = coerce( + instance(Transaction), + ConfirmedTransactionResult, + result => { + const {message, signatures} = result; + return Transaction.populate(new Message(message), signatures); + }, +); + +const ParsedInstructionResult = pick({ + parsed: unknown(), + program: string(), + programId: PublicKeyFromString, +}); + +const RawInstructionResult = pick({ + accounts: array(PublicKeyFromString), + data: string(), + programId: PublicKeyFromString, +}); + +const InstructionResult = union([ + RawInstructionResult, + ParsedInstructionResult, +]); + +const UnknownInstructionResult = union([ + pick({ + parsed: unknown(), + program: string(), + programId: string(), + }), + pick({ + accounts: array(string()), + data: string(), + programId: string(), + }), +]); + +const ParsedOrRawInstruction = coerce( + InstructionResult, + UnknownInstructionResult, + value => { + if ('accounts' in value) { + return create(value, RawInstructionResult); + } else { + return create(value, ParsedInstructionResult); + } + }, +); + +/** + * @private + */ +const ParsedConfirmedTransactionResult = pick({ + signatures: array(string()), + message: pick({ + accountKeys: array( + pick({ + pubkey: PublicKeyFromString, + signer: boolean(), + writable: boolean(), + }), + ), + instructions: array(ParsedOrRawInstruction), + recentBlockhash: string(), + }), +}); + +const TokenBalanceResult = pick({ + accountIndex: number(), + mint: string(), + uiTokenAmount: TokenAmountResult, +}); + /** * @private */ -const ParsedConfirmedTransactionResult = struct({ - signatures: struct.array(['string']), - message: struct({ - accountKeys: struct.array([ - struct({ - pubkey: 'string', - signer: 'boolean', - writable: 'boolean', - }), - ]), - instructions: struct.array([ - struct.union([ - struct({ - accounts: struct.array(['string']), - data: 'string', - programId: 'string', - }), - struct({ - parsed: 'any', - program: 'string', - programId: 'string', - }), - ]), - ]), - recentBlockhash: 'string', - }), -}); - -/** - * @private - */ -const ConfirmedTransactionMetaResult = struct.union([ - 'null', - struct.pick({ - err: TransactionErrorResult, - fee: 'number', - innerInstructions: struct.union([ - struct.array([ - struct({ - index: 'number', - instructions: struct.array([ - struct({ - accounts: struct.array(['number']), - data: 'string', - programIdIndex: 'number', +const ConfirmedTransactionMetaResult = pick({ + err: TransactionErrorResult, + fee: number(), + innerInstructions: optional( + nullable( + array( + pick({ + index: number(), + instructions: array( + pick({ + accounts: array(number()), + data: string(), + programIdIndex: number(), }), - ]), + ), }), - ]), - 'null', - 'undefined', - ]), - preBalances: struct.array(['number']), - postBalances: struct.array(['number']), - logMessages: struct.union([struct.array(['string']), 'null', 'undefined']), - preTokenBalances: struct.union([ - struct.array([ - struct.pick({ - accountIndex: 'number', - mint: 'string', - uiTokenAmount: struct.pick({ - amount: 'string', - decimals: 'number', - uiAmount: 'number', - }), - }), - ]), - 'null', - 'undefined', - ]), - postTokenBalances: struct.union([ - struct.array([ - struct.pick({ - accountIndex: 'number', - mint: 'string', - uiTokenAmount: struct.pick({ - amount: 'string', - decimals: 'number', - uiAmount: 'number', - }), - }), - ]), - 'null', - 'undefined', - ]), - }), -]); + ), + ), + ), + preBalances: array(number()), + postBalances: array(number()), + logMessages: optional(nullable(array(string()))), + preTokenBalances: optional(nullable(array(TokenBalanceResult))), + postTokenBalances: optional(nullable(array(TokenBalanceResult))), +}); + /** * @private */ -const ParsedConfirmedTransactionMetaResult = struct.union([ - 'null', - struct.pick({ - err: TransactionErrorResult, - fee: 'number', - innerInstructions: struct.union([ - struct.array([ - struct({ - index: 'number', - instructions: struct.array([ - struct.union([ - struct({ - accounts: struct.array(['string']), - data: 'string', - programId: 'string', - }), - struct({ - parsed: 'any', - program: 'string', - programId: 'string', - }), - ]), - ]), +const ParsedConfirmedTransactionMetaResult = pick({ + err: TransactionErrorResult, + fee: number(), + innerInstructions: optional( + nullable( + array( + pick({ + index: number(), + instructions: array(ParsedOrRawInstruction), }), - ]), - 'null', - 'undefined', - ]), - preBalances: struct.array(['number']), - postBalances: struct.array(['number']), - logMessages: struct.union([struct.array(['string']), 'null', 'undefined']), - preTokenBalances: struct.union([ - struct.array([ - struct.pick({ - accountIndex: 'number', - mint: 'string', - uiTokenAmount: struct.pick({ - amount: 'string', - decimals: 'number', - uiAmount: 'number', - }), - }), - ]), - 'null', - 'undefined', - ]), - postTokenBalances: struct.union([ - struct.array([ - struct.pick({ - accountIndex: 'number', - mint: 'string', - uiTokenAmount: struct.pick({ - amount: 'string', - decimals: 'number', - uiAmount: 'number', - }), - }), - ]), - 'null', - 'undefined', - ]), - }), -]); + ), + ), + ), + preBalances: array(number()), + postBalances: array(number()), + logMessages: optional(nullable(array(string()))), + preTokenBalances: optional(nullable(array(TokenBalanceResult))), + postTokenBalances: optional(nullable(array(TokenBalanceResult))), +}); /** * Expected JSON RPC response for the "getConfirmedBlock" message */ export const GetConfirmedBlockRpcResult = jsonRpcResult( - struct.union([ - 'null', - struct.pick({ - blockhash: 'string', - previousBlockhash: 'string', - parentSlot: 'number', - transactions: struct.array([ - struct({ - transaction: ConfirmedTransactionResult, - meta: ConfirmedTransactionMetaResult, + nullable( + pick({ + blockhash: string(), + previousBlockhash: string(), + parentSlot: number(), + transactions: array( + pick({ + transaction: TransactionFromConfirmed, + meta: nullable(ConfirmedTransactionMetaResult), }), - ]), - rewards: struct.union([ - 'undefined', - struct.array([ - struct({ - pubkey: 'string', - lamports: 'number', - postBalance: struct.union(['number', 'undefined']), - rewardType: struct.union(['string', 'undefined']), + ), + rewards: optional( + array( + pick({ + pubkey: string(), + lamports: number(), + postBalance: nullable(number()), + rewardType: nullable(string()), }), - ]), - ]), + ), + ), }), - ]), + ), ); /** * Expected JSON RPC response for the "getConfirmedTransaction" message */ const GetConfirmedTransactionRpcResult = jsonRpcResult( - struct.union([ - 'null', - struct.pick({ - slot: 'number', - transaction: ConfirmedTransactionResult, + nullable( + pick({ + slot: number(), + transaction: TransactionFromConfirmed, meta: ConfirmedTransactionMetaResult, - blockTime: struct.union(['number', 'null', 'undefined']), + blockTime: optional(nullable(number())), }), - ]), + ), ); /** * Expected JSON RPC response for the "getConfirmedTransaction" message */ const GetParsedConfirmedTransactionRpcResult = jsonRpcResult( - struct.union([ - 'null', - struct.pick({ - slot: 'number', + nullable( + pick({ + slot: number(), transaction: ParsedConfirmedTransactionResult, - meta: ParsedConfirmedTransactionMetaResult, - blockTime: struct.union(['number', 'null', 'undefined']), + meta: nullable(ParsedConfirmedTransactionMetaResult), + blockTime: optional(nullable(number())), }), - ]), + ), ); /** * Expected JSON RPC response for the "getRecentBlockhash" message */ const GetRecentBlockhashAndContextRpcResult = jsonRpcResultAndContext( - struct({ - blockhash: 'string', - feeCalculator: struct({ - lamportsPerSignature: 'number', + pick({ + blockhash: string(), + feeCalculator: pick({ + lamportsPerSignature: number(), }), }), ); +const PerfSampleResult = pick({ + slot: number(), + numTransactions: number(), + numSlots: number(), + samplePeriodSecs: number(), +}); + /* * Expected JSON RPC response for "getRecentPerformanceSamples" message */ const GetRecentPerformanceSamplesRpcResult = jsonRpcResult( - struct.array([ - struct.pick({ - slot: 'number', - numTransactions: 'number', - numSlots: 'number', - samplePeriodSecs: 'number', - }), - ]), + array(PerfSampleResult), ); /** * Expected JSON RPC response for the "getFeeCalculatorForBlockhash" message */ const GetFeeCalculatorRpcResult = jsonRpcResultAndContext( - struct.union([ - 'null', - struct({ - feeCalculator: struct({ - lamportsPerSignature: 'number', + nullable( + pick({ + feeCalculator: pick({ + lamportsPerSignature: number(), }), }), - ]), + ), ); /** * Expected JSON RPC response for the "requestAirdrop" message */ -const RequestAirdropRpcResult = jsonRpcResult('string'); +const RequestAirdropRpcResult = jsonRpcResult(string()); /** * Expected JSON RPC response for the "sendTransaction" message */ -const SendTransactionRpcResult = jsonRpcResult('string'); +const SendTransactionRpcResult = jsonRpcResult(string()); /** * Information about the latest slot being processed by a node @@ -1742,7 +1639,7 @@ export class Connection { ): Promise> { const args = this._buildArgs([publicKey.toBase58()], commitment); const unsafeRes = await this._rpcRequest('getBalance', args); - const res = GetBalanceAndContextRpcResult(unsafeRes); + const res = create(unsafeRes, jsonRpcResultAndContext(number())); if (res.error) { throw new Error( 'failed to get balance for ' + @@ -1751,7 +1648,6 @@ export class Connection { res.error.message, ); } - assert(typeof res.result !== 'undefined'); return res.result; } @@ -1776,13 +1672,12 @@ export class Connection { */ async getBlockTime(slot: number): Promise { const unsafeRes = await this._rpcRequest('getBlockTime', [slot]); - const res = GetBlockTimeRpcResult(unsafeRes); + const res = create(unsafeRes, jsonRpcResult(nullable(number()))); if (res.error) { throw new Error( 'failed to get block time for slot ' + slot + ': ' + res.error.message, ); } - assert(typeof res.result !== 'undefined'); return res.result; } @@ -1792,13 +1687,12 @@ export class Connection { */ async getMinimumLedgerSlot(): Promise { const unsafeRes = await this._rpcRequest('minimumLedgerSlot', []); - const res = SlotRpcResult(unsafeRes); + const res = create(unsafeRes, jsonRpcResult(number())); if (res.error) { throw new Error( 'failed to get minimum ledger slot: ' + res.error.message, ); } - assert(typeof res.result !== 'undefined'); return res.result; } @@ -1807,13 +1701,12 @@ export class Connection { */ async getFirstAvailableBlock(): Promise { const unsafeRes = await this._rpcRequest('getFirstAvailableBlock', []); - const res = SlotRpcResult(unsafeRes); + const res = create(unsafeRes, SlotRpcResult); if (res.error) { throw new Error( 'failed to get first available block: ' + res.error.message, ); } - assert(typeof res.result !== 'undefined'); return res.result; } @@ -1825,14 +1718,10 @@ export class Connection { ): Promise> { const args = this._buildArgs([], commitment); const unsafeRes = await this._rpcRequest('getSupply', args); - const res = GetSupplyRpcResult(unsafeRes); + const res = create(unsafeRes, GetSupplyRpcResult); if (res.error) { throw new Error('failed to get supply: ' + res.error.message); } - assert(typeof res.result !== 'undefined'); - res.result.value.nonCirculatingAccounts = res.result.value.nonCirculatingAccounts.map( - account => new PublicKey(account), - ); return res.result; } @@ -1845,11 +1734,10 @@ export class Connection { ): Promise> { const args = this._buildArgs([tokenMintAddress.toBase58()], commitment); const unsafeRes = await this._rpcRequest('getTokenSupply', args); - const res = GetTokenSupplyRpcResult(unsafeRes); + const res = create(unsafeRes, jsonRpcResultAndContext(TokenAmountResult)); if (res.error) { throw new Error('failed to get token supply: ' + res.error.message); } - assert(typeof res.result !== 'undefined'); return res.result; } @@ -1862,13 +1750,12 @@ export class Connection { ): Promise> { const args = this._buildArgs([tokenAddress.toBase58()], commitment); const unsafeRes = await this._rpcRequest('getTokenAccountBalance', args); - const res = GetTokenAccountBalance(unsafeRes); + const res = create(unsafeRes, jsonRpcResultAndContext(TokenAmountResult)); if (res.error) { throw new Error( 'failed to get token account balance: ' + res.error.message, ); } - assert(typeof res.result !== 'undefined'); return res.result; } @@ -1895,7 +1782,7 @@ export class Connection { const args = this._buildArgs(_args, commitment, 'base64'); const unsafeRes = await this._rpcRequest('getTokenAccountsByOwner', args); - const res = GetTokenAccountsByOwner(unsafeRes); + const res = create(unsafeRes, GetTokenAccountsByOwner); if (res.error) { throw new Error( 'failed to get token accounts owned by account ' + @@ -1904,26 +1791,7 @@ export class Connection { res.error.message, ); } - - const {result} = res; - const {context, value} = result; - assert(typeof result !== 'undefined'); - - return { - context, - value: value.map(result => { - assert(result.account.data[1] === 'base64'); - return { - pubkey: new PublicKey(result.pubkey), - account: { - executable: result.account.executable, - owner: new PublicKey(result.account.owner), - lamports: result.account.lamports, - data: Buffer.from(result.account.data[0], 'base64'), - }, - }; - }), - }; + return res.result; } /** @@ -1949,7 +1817,7 @@ export class Connection { const args = this._buildArgs(_args, commitment, 'jsonParsed'); const unsafeRes = await this._rpcRequest('getTokenAccountsByOwner', args); - const res = GetParsedTokenAccountsByOwner(unsafeRes); + const res = create(unsafeRes, GetParsedTokenAccountsByOwner); if (res.error) { throw new Error( 'failed to get token accounts owned by account ' + @@ -1958,23 +1826,7 @@ export class Connection { res.error.message, ); } - - const {result} = res; - const {context, value} = result; - assert(typeof result !== 'undefined'); - - return { - context, - value: value.map(result => ({ - pubkey: new PublicKey(result.pubkey), - account: { - executable: result.account.executable, - owner: new PublicKey(result.account.owner), - lamports: result.account.lamports, - data: result.account.data, - }, - })), - }; + return res.result; } /** @@ -1989,15 +1841,10 @@ export class Connection { }; const args = arg.filter || arg.commitment ? [arg] : []; const unsafeRes = await this._rpcRequest('getLargestAccounts', args); - const res = GetLargestAccountsRpcResult(unsafeRes); + const res = create(unsafeRes, GetLargestAccountsRpcResult); if (res.error) { throw new Error('failed to get largest accounts: ' + res.error.message); } - assert(typeof res.result !== 'undefined'); - res.result.value = res.result.value.map(({address, lamports}) => ({ - address: new PublicKey(address), - lamports, - })); return res.result; } @@ -2011,17 +1858,12 @@ export class Connection { ): Promise>> { const args = this._buildArgs([mintAddress.toBase58()], commitment); const unsafeRes = await this._rpcRequest('getTokenLargestAccounts', args); - const res = GetTokenLargestAccountsResult(unsafeRes); + const res = create(unsafeRes, GetTokenLargestAccountsResult); if (res.error) { throw new Error( 'failed to get token largest accounts: ' + res.error.message, ); } - assert(typeof res.result !== 'undefined'); - res.result.value = res.result.value.map(pair => ({ - ...pair, - address: new PublicKey(pair.address), - })); return res.result; } @@ -2034,7 +1876,10 @@ export class Connection { ): Promise | null>> { const args = this._buildArgs([publicKey.toBase58()], commitment, 'base64'); const unsafeRes = await this._rpcRequest('getAccountInfo', args); - const res = GetAccountInfoAndContextRpcResult(unsafeRes); + const res = create( + unsafeRes, + jsonRpcResultAndContext(nullable(AccountInfoResult)), + ); if (res.error) { throw new Error( 'failed to get info about account ' + @@ -2043,26 +1888,7 @@ export class Connection { res.error.message, ); } - assert(typeof res.result !== 'undefined'); - - let value = null; - if (res.result.value) { - const {executable, owner, lamports, data} = res.result.value; - assert(data[1] === 'base64'); - value = { - executable, - owner: new PublicKey(owner), - lamports, - data: Buffer.from(data[0], 'base64'), - }; - } - - return { - context: { - slot: res.result.context.slot, - }, - value, - }; + return res.result; } /** @@ -2080,7 +1906,10 @@ export class Connection { 'jsonParsed', ); const unsafeRes = await this._rpcRequest('getAccountInfo', args); - const res = GetParsedAccountInfoResult(unsafeRes); + const res = create( + unsafeRes, + jsonRpcResultAndContext(nullable(ParsedAccountInfoResult)), + ); if (res.error) { throw new Error( 'failed to get info about account ' + @@ -2089,32 +1918,7 @@ export class Connection { res.error.message, ); } - assert(typeof res.result !== 'undefined'); - - let value = null; - if (res.result.value) { - const {executable, owner, lamports, data: resultData} = res.result.value; - - let data = resultData; - if (!data.program) { - assert(data[1] === 'base64'); - data = Buffer.from(data[0], 'base64'); - } - - value = { - executable, - owner: new PublicKey(owner), - lamports, - data, - }; - } - - return { - context: { - slot: res.result.context.slot, - }, - value, - }; + return res.result; } /** @@ -2124,13 +1928,14 @@ export class Connection { publicKey: PublicKey, commitment: ?Commitment, ): Promise | null> { - return await this.getAccountInfoAndContext(publicKey, commitment) - .then(x => x.value) - .catch(e => { - throw new Error( - 'failed to get info about account ' + publicKey.toBase58() + ': ' + e, - ); - }); + try { + const res = await this.getAccountInfoAndContext(publicKey, commitment); + return res.value; + } catch (e) { + throw new Error( + 'failed to get info about account ' + publicKey.toBase58() + ': ' + e, + ); + } } /** @@ -2149,7 +1954,7 @@ export class Connection { ); const unsafeRes = await this._rpcRequest('getStakeActivation', args); - const res = GetStakeActivationResult(unsafeRes); + const res = create(unsafeRes, jsonRpcResult(StakeActivationResult)); if (res.error) { throw new Error( `failed to get Stake Activation ${publicKey.toBase58()}: ${ @@ -2157,10 +1962,7 @@ export class Connection { }`, ); } - assert(typeof res.result !== 'undefined'); - - const {state, active, inactive} = res.result; - return {state, active, inactive}; + return res.result; } /** @@ -2174,7 +1976,7 @@ export class Connection { ): Promise}>> { const args = this._buildArgs([programId.toBase58()], commitment, 'base64'); const unsafeRes = await this._rpcRequest('getProgramAccounts', args); - const res = GetProgramAccountsRpcResult(unsafeRes); + const res = create(unsafeRes, jsonRpcResult(array(KeyedAccountInfoResult))); if (res.error) { throw new Error( 'failed to get accounts owned by program ' + @@ -2183,22 +1985,7 @@ export class Connection { res.error.message, ); } - - const {result} = res; - assert(typeof result !== 'undefined'); - - return result.map(result => { - assert(result.account.data[1] === 'base64'); - return { - pubkey: new PublicKey(result.pubkey), - account: { - executable: result.account.executable, - owner: new PublicKey(result.account.owner), - lamports: result.account.lamports, - data: Buffer.from(result.account.data[0], 'base64'), - }, - }; - }); + return res.result; } /** @@ -2221,7 +2008,10 @@ export class Connection { 'jsonParsed', ); const unsafeRes = await this._rpcRequest('getProgramAccounts', args); - const res = GetParsedProgramAccountsRpcResult(unsafeRes); + const res = create( + unsafeRes, + jsonRpcResult(array(KeyedParsedAccountInfoResult)), + ); if (res.error) { throw new Error( 'failed to get accounts owned by program ' + @@ -2230,29 +2020,7 @@ export class Connection { res.error.message, ); } - - const {result} = res; - assert(typeof result !== 'undefined'); - - return result.map(result => { - const resultData = result.account.data; - - let data = resultData; - if (!data.program) { - assert(data[1] === 'base64'); - data = Buffer.from(data[0], 'base64'); - } - - return { - pubkey: new PublicKey(result.pubkey), - account: { - executable: result.account.executable, - owner: new PublicKey(result.account.owner), - lamports: result.account.lamports, - data, - }, - }; - }); + return res.result; } /** @@ -2336,12 +2104,10 @@ export class Connection { */ async getClusterNodes(): Promise> { const unsafeRes = await this._rpcRequest('getClusterNodes', []); - - const res = GetClusterNodes(unsafeRes); + const res = create(unsafeRes, jsonRpcResult(array(ContactInfoResult))); if (res.error) { throw new Error('failed to get cluster nodes: ' + res.error.message); } - assert(typeof res.result !== 'undefined'); return res.result; } @@ -2351,12 +2117,10 @@ export class Connection { async getVoteAccounts(commitment: ?Commitment): Promise { const args = this._buildArgs([], commitment); const unsafeRes = await this._rpcRequest('getVoteAccounts', args); - const res = GetVoteAccounts(unsafeRes); - //const res = unsafeRes; + const res = create(unsafeRes, GetVoteAccounts); if (res.error) { throw new Error('failed to get vote accounts: ' + res.error.message); } - assert(typeof res.result !== 'undefined'); return res.result; } @@ -2366,11 +2130,10 @@ export class Connection { async getSlot(commitment: ?Commitment): Promise { const args = this._buildArgs([], commitment); const unsafeRes = await this._rpcRequest('getSlot', args); - const res = GetSlot(unsafeRes); + const res = create(unsafeRes, jsonRpcResult(number())); if (res.error) { throw new Error('failed to get slot: ' + res.error.message); } - assert(typeof res.result !== 'undefined'); return res.result; } @@ -2380,11 +2143,10 @@ export class Connection { async getSlotLeader(commitment: ?Commitment): Promise { const args = this._buildArgs([], commitment); const unsafeRes = await this._rpcRequest('getSlotLeader', args); - const res = GetSlotLeader(unsafeRes); + const res = create(unsafeRes, jsonRpcResult(string())); if (res.error) { throw new Error('failed to get slot leader: ' + res.error.message); } - assert(typeof res.result !== 'undefined'); return res.result; } @@ -2395,12 +2157,13 @@ export class Connection { signature: TransactionSignature, config: ?SignatureStatusConfig, ): Promise> { - const {context, value} = await this.getSignatureStatuses( + const {context, value: values} = await this.getSignatureStatuses( [signature], config, ); - assert(value.length === 1); - return {context, value: value[0]}; + assert(values.length === 1); + const value = values[0]; + return {context, value}; } /** @@ -2415,11 +2178,10 @@ export class Connection { params.push(config); } const unsafeRes = await this._rpcRequest('getSignatureStatuses', params); - const res = GetSignatureStatusesRpcResult(unsafeRes); + const res = create(unsafeRes, GetSignatureStatusesRpcResult); if (res.error) { throw new Error('failed to get signature status: ' + res.error.message); } - assert(typeof res.result !== 'undefined'); return res.result; } @@ -2429,12 +2191,11 @@ export class Connection { async getTransactionCount(commitment: ?Commitment): Promise { const args = this._buildArgs([], commitment); const unsafeRes = await this._rpcRequest('getTransactionCount', args); - const res = GetTransactionCountRpcResult(unsafeRes); + const res = create(unsafeRes, jsonRpcResult(number())); if (res.error) { throw new Error('failed to get transaction count: ' + res.error.message); } - assert(typeof res.result !== 'undefined'); - return Number(res.result); + return res.result; } /** @@ -2443,12 +2204,11 @@ export class Connection { async getTotalSupply(commitment: ?Commitment): Promise { const args = this._buildArgs([], commitment); const unsafeRes = await this._rpcRequest('getTotalSupply', args); - const res = GetTotalSupplyRpcResult(unsafeRes); + const res = create(unsafeRes, jsonRpcResult(number())); if (res.error) { - throw new Error('faied to get total supply: ' + res.error.message); + throw new Error('failed to get total supply: ' + res.error.message); } - assert(typeof res.result !== 'undefined'); - return Number(res.result); + return res.result; } /** @@ -2459,12 +2219,11 @@ export class Connection { ): Promise { const args = this._buildArgs([], commitment); const unsafeRes = await this._rpcRequest('getInflationGovernor', args); - const res = GetInflationGovernorRpcResult(unsafeRes); + const res = create(unsafeRes, GetInflationGovernorRpcResult); if (res.error) { throw new Error('failed to get inflation: ' + res.error.message); } - assert(typeof res.result !== 'undefined'); - return GetInflationGovernorResult(res.result); + return res.result; } /** @@ -2473,12 +2232,11 @@ export class Connection { async getEpochInfo(commitment: ?Commitment): Promise { const args = this._buildArgs([], commitment); const unsafeRes = await this._rpcRequest('getEpochInfo', args); - const res = GetEpochInfoRpcResult(unsafeRes); + const res = create(unsafeRes, GetEpochInfoRpcResult); if (res.error) { throw new Error('failed to get epoch info: ' + res.error.message); } - assert(typeof res.result !== 'undefined'); - return GetEpochInfoResult(res.result); + return res.result; } /** @@ -2486,12 +2244,11 @@ export class Connection { */ async getEpochSchedule(): Promise { const unsafeRes = await this._rpcRequest('getEpochSchedule', []); - const res = GetEpochScheduleRpcResult(unsafeRes); + const res = create(unsafeRes, GetEpochScheduleRpcResult); if (res.error) { throw new Error('failed to get epoch schedule: ' + res.error.message); } - assert(typeof res.result !== 'undefined'); - return GetEpochScheduleResult(res.result); + return res.result; } /** @@ -2500,11 +2257,10 @@ export class Connection { */ async getLeaderSchedule(): Promise { const unsafeRes = await this._rpcRequest('getLeaderSchedule', []); - const res = GetLeaderScheduleRpcResult(unsafeRes); + const res = create(unsafeRes, GetLeaderScheduleRpcResult); if (res.error) { throw new Error('failed to get leader schedule: ' + res.error.message); } - assert(typeof res.result !== 'undefined'); return res.result; } @@ -2521,13 +2277,12 @@ export class Connection { 'getMinimumBalanceForRentExemption', args, ); - const res = GetMinimumBalanceForRentExemptionRpcResult(unsafeRes); + const res = create(unsafeRes, GetMinimumBalanceForRentExemptionRpcResult); if (res.error) { console.warn('Unable to fetch minimum balance for rent exemption'); return 0; } - assert(typeof res.result !== 'undefined'); - return Number(res.result); + return res.result; } /** @@ -2541,12 +2296,10 @@ export class Connection { > { const args = this._buildArgs([], commitment); const unsafeRes = await this._rpcRequest('getRecentBlockhash', args); - - const res = GetRecentBlockhashAndContextRpcResult(unsafeRes); + const res = create(unsafeRes, GetRecentBlockhashAndContextRpcResult); if (res.error) { throw new Error('failed to get recent blockhash: ' + res.error.message); } - assert(typeof res.result !== 'undefined'); return res.result; } @@ -2562,15 +2315,13 @@ export class Connection { 'getRecentPerformanceSamples', args, ); - - const res = GetRecentPerformanceSamplesRpcResult(unsafeRes); + const res = create(unsafeRes, GetRecentPerformanceSamplesRpcResult); if (res.error) { throw new Error( 'failed to get recent performance samples: ' + res.error.message, ); } - assert(typeof res.result !== 'undefined'); return res.result; } @@ -2587,15 +2338,14 @@ export class Connection { args, ); - const res = GetFeeCalculatorRpcResult(unsafeRes); + const res = create(unsafeRes, GetFeeCalculatorRpcResult); if (res.error) { throw new Error('failed to get fee calculator: ' + res.error.message); } - assert(typeof res.result !== 'undefined'); const {context, value} = res.result; return { context, - value: value && value.feeCalculator, + value: value.feeCalculator, }; } @@ -2606,11 +2356,12 @@ export class Connection { async getRecentBlockhash( commitment: ?Commitment, ): Promise<{blockhash: Blockhash, feeCalculator: FeeCalculator}> { - return await this.getRecentBlockhashAndContext(commitment) - .then(x => x.value) - .catch(e => { - throw new Error('failed to get recent blockhash: ' + e); - }); + try { + const res = await this.getRecentBlockhashAndContext(commitment); + return res.value; + } catch (e) { + throw new Error('failed to get recent blockhash: ' + e); + } } /** @@ -2618,11 +2369,10 @@ export class Connection { */ async getVersion(): Promise { const unsafeRes = await this._rpcRequest('getVersion', []); - const res = GetVersionRpcResult(unsafeRes); + const res = create(unsafeRes, jsonRpcResult(Version)); if (res.error) { throw new Error('failed to get version: ' + res.error.message); } - assert(typeof res.result !== 'undefined'); return res.result; } @@ -2632,28 +2382,15 @@ export class Connection { */ async getConfirmedBlock(slot: number): Promise { const unsafeRes = await this._rpcRequest('getConfirmedBlock', [slot]); - const res = GetConfirmedBlockRpcResult(unsafeRes); + const res = create(unsafeRes, GetConfirmedBlockRpcResult); if (res.error) { throw new Error('failed to get confirmed block: ' + res.error.message); } const result = res.result; - assert(typeof result !== 'undefined'); if (!result) { throw new Error('Confirmed block ' + slot + ' not found'); } - return { - blockhash: new PublicKey(result.blockhash).toString(), - previousBlockhash: new PublicKey(result.previousBlockhash).toString(), - parentSlot: result.parentSlot, - transactions: result.transactions.map(result => { - const {message, signatures} = result.transaction; - return { - transaction: Transaction.populate(new Message(message), signatures), - meta: result.meta, - }; - }), - rewards: result.rewards || [], - }; + return result; } /** @@ -2665,21 +2402,13 @@ export class Connection { const unsafeRes = await this._rpcRequest('getConfirmedTransaction', [ signature, ]); - const {result, error} = GetConfirmedTransactionRpcResult(unsafeRes); - if (error) { - throw new Error('failed to get confirmed transaction: ' + error.message); + const res = create(unsafeRes, GetConfirmedTransactionRpcResult); + if (res.error) { + throw new Error( + 'failed to get confirmed transaction: ' + res.error.message, + ); } - assert(typeof result !== 'undefined'); - if (result === null) { - return result; - } - - const {message, signatures} = result.transaction; - return { - slot: result.slot, - transaction: Transaction.populate(new Message(message), signatures), - meta: result.meta, - }; + return res.result; } /** @@ -2692,56 +2421,13 @@ export class Connection { signature, 'jsonParsed', ]); - const {result, error} = GetParsedConfirmedTransactionRpcResult(unsafeRes); - if (error) { - throw new Error('failed to get confirmed transaction: ' + error.message); + const res = create(unsafeRes, GetParsedConfirmedTransactionRpcResult); + if (res.error) { + throw new Error( + 'failed to get confirmed transaction: ' + res.error.message, + ); } - assert(typeof result !== 'undefined'); - if (result === null) return result; - - if (result.meta.innerInstructions) { - result.meta.innerInstructions.forEach(inner => { - inner.instructions.forEach(ix => { - ix.programId = new PublicKey(ix.programId); - - if (ix.accounts) { - ix.accounts = ix.accounts.map(account => new PublicKey(account)); - } - }); - }); - } - - const { - accountKeys, - instructions, - recentBlockhash, - } = result.transaction.message; - return { - slot: result.slot, - meta: result.meta, - transaction: { - signatures: result.transaction.signatures, - message: { - accountKeys: accountKeys.map(accountKey => ({ - pubkey: new PublicKey(accountKey.pubkey), - signer: accountKey.signer, - writable: accountKey.writable, - })), - instructions: instructions.map(ix => { - let mapped: any = {programId: new PublicKey(ix.programId)}; - if ('accounts' in ix) { - mapped.accounts = ix.accounts.map(key => new PublicKey(key)); - } - - return { - ...ix, - ...mapped, - }; - }), - recentBlockhash, - }, - }, - }; + return res.result; } /** @@ -2761,15 +2447,13 @@ export class Connection { 'getConfirmedSignaturesForAddress', [address.toBase58(), startSlot, endSlot], ); - const result = GetConfirmedSignaturesForAddressRpcResult(unsafeRes); - if (result.error) { + const res = create(unsafeRes, GetConfirmedSignaturesForAddressRpcResult); + if (res.error) { throw new Error( - 'failed to get confirmed signatures for address: ' + - result.error.message, + 'failed to get confirmed signatures for address: ' + res.error.message, ); } - assert(typeof result.result !== 'undefined'); - return result.result; + return res.result; } /** @@ -2788,15 +2472,13 @@ export class Connection { 'getConfirmedSignaturesForAddress2', [address.toBase58(), options], ); - const result = GetConfirmedSignaturesForAddress2RpcResult(unsafeRes); - if (result.error) { + const res = create(unsafeRes, GetConfirmedSignaturesForAddress2RpcResult); + if (res.error) { throw new Error( - 'failed to get confirmed signatures for address: ' + - result.error.message, + 'failed to get confirmed signatures for address: ' + res.error.message, ); } - assert(typeof result.result !== 'undefined'); - return result.result; + return res.result; } /** @@ -2852,13 +2534,12 @@ export class Connection { to.toBase58(), amount, ]); - const res = RequestAirdropRpcResult(unsafeRes); + const res = create(unsafeRes, RequestAirdropRpcResult); if (res.error) { throw new Error( 'airdrop to ' + to.toBase58() + ' failed: ' + res.error.message, ); } - assert(typeof res.result !== 'undefined'); return res.result; } @@ -2959,12 +2640,10 @@ export class Connection { } const unsafeRes = await this._rpcRequest('simulateTransaction', args); - const res = SimulatedTransactionResponseValidator(unsafeRes); + const res = create(unsafeRes, SimulatedTransactionResponseStruct); if (res.error) { throw new Error('failed to simulate transaction: ' + res.error.message); } - assert(typeof res.result !== 'undefined'); - assert(res.result); return res.result; } @@ -3008,11 +2687,10 @@ export class Connection { */ async validatorExit(): Promise { const unsafeRes = await this._rpcRequest('validatorExit', []); - const res = jsonRpcResult('boolean')(unsafeRes); + const res = create(unsafeRes, jsonRpcResult(boolean())); if (res.error) { throw new Error('validator exit failed: ' + res.error.message); } - assert(typeof res.result !== 'undefined'); return res.result; } @@ -3054,7 +2732,7 @@ export class Connection { } const unsafeRes = await this._rpcRequest('sendTransaction', args); - const res = SendTransactionRpcResult(unsafeRes); + const res = create(unsafeRes, SendTransactionRpcResult); if (res.error) { if (res.error.data) { const logs = res.error.data.logs; @@ -3066,8 +2744,6 @@ export class Connection { } throw new Error('failed to send transaction: ' + res.error.message); } - assert(typeof res.result !== 'undefined'); - assert(res.result); return res.result; } @@ -3255,28 +2931,15 @@ export class Connection { * @private */ _wsOnAccountNotification(notification: Object) { - const res = AccountNotificationResult(notification); + const res = create(notification, AccountNotificationResult); if (res.error) { throw new Error('account notification failed: ' + res.error.message); } - assert(typeof res.result !== 'undefined'); const keys = Object.keys(this._accountChangeSubscriptions).map(Number); for (let id of keys) { const sub = this._accountChangeSubscriptions[id]; if (sub.subscriptionId === res.subscription) { - const {result} = res; - const {value, context} = result; - - assert(value.data[1] === 'base64'); - sub.callback( - { - executable: value.executable, - owner: new PublicKey(value.owner), - lamports: value.lamports, - data: Buffer.from(value.data[0], 'base64'), - }, - context, - ); + sub.callback(res.result.value, res.result.context); return true; } } @@ -3326,32 +2989,23 @@ export class Connection { * @private */ _wsOnProgramAccountNotification(notification: Object) { - const res = ProgramAccountNotificationResult(notification); + const res = create(notification, ProgramAccountNotificationResult); if (res.error) { throw new Error( 'program account notification failed: ' + res.error.message, ); } - assert(typeof res.result !== 'undefined'); const keys = Object.keys(this._programAccountChangeSubscriptions).map( Number, ); for (let id of keys) { const sub = this._programAccountChangeSubscriptions[id]; if (sub.subscriptionId === res.subscription) { - const {result} = res; - const {value, context} = result; - - assert(value.account.data[1] === 'base64'); + const {value, context} = res.result; sub.callback( { accountId: value.pubkey, - accountInfo: { - executable: value.account.executable, - owner: new PublicKey(value.account.owner), - lamports: value.account.lamports, - data: Buffer.from(value.account.data[0], 'base64'), - }, + accountInfo: value.account, }, context, ); @@ -3405,21 +3059,15 @@ export class Connection { * @private */ _wsOnSlotNotification(notification: Object) { - const res = SlotNotificationResult(notification); + const res = create(notification, SlotNotificationResult); if (res.error) { throw new Error('slot notification failed: ' + res.error.message); } - assert(typeof res.result !== 'undefined'); - const {parent, slot, root} = res.result; const keys = Object.keys(this._slotSubscriptions).map(Number); for (let id of keys) { const sub = this._slotSubscriptions[id]; if (sub.subscriptionId === res.subscription) { - sub.callback({ - parent, - slot, - root, - }); + sub.callback(res.result); return true; } } @@ -3484,11 +3132,10 @@ export class Connection { * @private */ _wsOnSignatureNotification(notification: Object) { - const res = SignatureNotificationResult(notification); + const res = create(notification, SignatureNotificationResult); if (res.error) { throw new Error('signature notification failed: ' + res.error.message); } - assert(typeof res.result !== 'undefined'); const keys = Object.keys(this._signatureSubscriptions).map(Number); for (let id of keys) { const sub = this._signatureSubscriptions[id]; @@ -3547,17 +3194,15 @@ export class Connection { * @private */ _wsOnRootNotification(notification: Object) { - const res = RootNotificationResult(notification); + const res = create(notification, RootNotificationResult); if (res.error) { throw new Error('root notification failed: ' + res.error.message); } - assert(typeof res.result !== 'undefined'); - const root = res.result; const keys = Object.keys(this._rootSubscriptions).map(Number); for (let id of keys) { const sub = this._rootSubscriptions[id]; if (sub.subscriptionId === res.subscription) { - sub.callback(root); + sub.callback(res.result); return true; } } diff --git a/web3.js/src/validator-info.js b/web3.js/src/validator-info.js index 0417137bcb..82d0b35d63 100644 --- a/web3.js/src/validator-info.js +++ b/web3.js/src/validator-info.js @@ -1,7 +1,12 @@ // @flow import {Buffer} from 'buffer'; -import {struct} from 'superstruct'; +import { + assert as assertType, + optional, + string, + type as pick, +} from 'superstruct'; import * as Layout from './layout'; import * as shortvec from './util/shortvec-encoding'; @@ -35,11 +40,11 @@ export type Info = {| keybaseUsername?: string, |}; -const InfoString = struct({ - name: 'string', - website: 'string?', - details: 'string?', - keybaseUsername: 'string?', +const InfoString = pick({ + name: string(), + website: optional(string()), + details: optional(string()), + keybaseUsername: optional(string()), }); /** @@ -94,7 +99,8 @@ export class ValidatorInfo { if (configKeys[0].publicKey.equals(VALIDATOR_INFO_KEY)) { if (configKeys[1].isSigner) { const rawInfo = Layout.rustString().decode(Buffer.from(byteArray)); - const info = InfoString(JSON.parse(rawInfo)); + const info = JSON.parse(rawInfo); + assertType(info, InfoString); return new ValidatorInfo(configKeys[1].publicKey, info); } } diff --git a/web3.js/test/connection.test.js b/web3.js/test/connection.test.js index 3a6dcacedc..4ac03e06c1 100644 --- a/web3.js/test/connection.test.js +++ b/web3.js/test/connection.test.js @@ -1542,6 +1542,7 @@ describe('Connection', () => { lamports: LAMPORTS_PER_SOL, data: ['', 'base64'], executable: false, + rentEpoch: 20, }, withContext: true, }); @@ -1569,6 +1570,7 @@ describe('Connection', () => { lamports: LAMPORTS_PER_SOL, data: ['', 'base64'], executable: false, + rentEpoch: 20, }, withContext: true, }); @@ -1903,10 +1905,7 @@ describe('Connection', () => { const subscriptionId = connection.onProgramAccountChange( SystemProgram.programId, (keyedAccountInfo: KeyedAccountInfo) => { - // FIX: accountId should be `PublicKey` not `string` - if ( - keyedAccountInfo.accountId === programAccount.publicKey.toBase58() - ) { + if (keyedAccountInfo.accountId.equals(programAccount.publicKey)) { expect(keyedAccountInfo.accountInfo.lamports).to.eq(balanceNeeded); expect( keyedAccountInfo.accountInfo.owner.equals( diff --git a/web3.js/test/nonce.test.js b/web3.js/test/nonce.test.js index 04f68d4f60..dcef9af9e9 100644 --- a/web3.js/test/nonce.test.js +++ b/web3.js/test/nonce.test.js @@ -92,6 +92,7 @@ describe('Nonce', () => { lamports: minimumAmount, data: expectedData(from.publicKey), executable: false, + rentEpoch: 20, }, withContext: true, }); @@ -162,6 +163,7 @@ describe('Nonce', () => { lamports: minimumAmount, data: expectedData(from.publicKey), executable: false, + rentEpoch: 20, }, withContext: true, });