feat: add account change notifications

This commit is contained in:
Michael Vines 2018-10-26 21:37:39 -07:00
parent 9839c087d7
commit e7097340f2
12 changed files with 552 additions and 25 deletions

2
web3.js/.gitignore vendored
View File

@ -23,4 +23,4 @@ lib
doc
# VIM swap files
*.sw.
*.sw*

88
web3.js/flow-typed/rpc-websockets.js vendored Normal file
View File

@ -0,0 +1,88 @@
// flow-typed signature: 31b3dc13d06052ea505e8da9ca72c537
// flow-typed version: <<STUB>>/rpc-websockets_v4.3.3/flow_v0.84.0
/**
* This is an autogenerated libdef stub for:
*
* 'rpc-websockets'
*
* Fill this stub out by replacing all the `any` types.
*
* Once filled out, we encourage you to share your work with the
* community by sending a pull request to:
* https://github.com/flowtype/flow-typed
*/
declare module 'rpc-websockets' {
declare module.exports: any;
}
/**
* We include stubs for each file inside this npm package in case you need to
* require those files directly. Feel free to delete any files that aren't
* needed.
*/
declare module 'rpc-websockets/dist/index.browser-bundle' {
declare module.exports: any;
}
declare module 'rpc-websockets/dist/index.browser' {
declare module.exports: any;
}
declare module 'rpc-websockets/dist/index' {
declare module.exports: any;
}
declare module 'rpc-websockets/dist/lib/client' {
declare module.exports: any;
}
declare module 'rpc-websockets/dist/lib/client/websocket.browser' {
declare module.exports: any;
}
declare module 'rpc-websockets/dist/lib/client/websocket' {
declare module.exports: any;
}
declare module 'rpc-websockets/dist/lib/handler' {
declare module.exports: any;
}
declare module 'rpc-websockets/dist/lib/server' {
declare module.exports: any;
}
declare module 'rpc-websockets/dist/lib/utils' {
declare module.exports: any;
}
// Filename aliases
declare module 'rpc-websockets/dist/index.browser-bundle.js' {
declare module.exports: $Exports<'rpc-websockets/dist/index.browser-bundle'>;
}
declare module 'rpc-websockets/dist/index.browser.js' {
declare module.exports: $Exports<'rpc-websockets/dist/index.browser'>;
}
declare module 'rpc-websockets/dist/index.js' {
declare module.exports: $Exports<'rpc-websockets/dist/index'>;
}
declare module 'rpc-websockets/dist/lib/client.js' {
declare module.exports: $Exports<'rpc-websockets/dist/lib/client'>;
}
declare module 'rpc-websockets/dist/lib/client/websocket.browser.js' {
declare module.exports: $Exports<'rpc-websockets/dist/lib/client/websocket.browser'>;
}
declare module 'rpc-websockets/dist/lib/client/websocket.js' {
declare module.exports: $Exports<'rpc-websockets/dist/lib/client/websocket'>;
}
declare module 'rpc-websockets/dist/lib/handler.js' {
declare module.exports: $Exports<'rpc-websockets/dist/lib/handler'>;
}
declare module 'rpc-websockets/dist/lib/server.js' {
declare module.exports: $Exports<'rpc-websockets/dist/lib/server'>;
}
declare module 'rpc-websockets/dist/lib/utils.js' {
declare module.exports: $Exports<'rpc-websockets/dist/lib/utils'>;
}

View File

@ -41,6 +41,8 @@ declare module '@solana/web3.js' {
userdata: Buffer,
}
declare type AccountChangeCallback = (accountInfo: AccountInfo) => void;
declare export type SignatureStatus = 'Confirmed'
| 'AccountInUse'
| 'SignatureNotFound'
@ -58,6 +60,8 @@ declare module '@solana/web3.js' {
getFinality(): Promise<number>;
requestAirdrop(to: PublicKey, amount: number): Promise<TransactionSignature>;
sendTransaction(from: Account, transaction: Transaction): Promise<TransactionSignature>;
onAccountChange(publickey: PublicKey, callback: AccountChangeCallback): Promise<number>;
removeAccountListener(id: number): Promise<void>;
}
// === src/system-program.js ===

View File

@ -4,6 +4,23 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"101": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/101/-/101-1.6.3.tgz",
"integrity": "sha512-4dmQ45yY0Dx24Qxp+zAsNLlMF6tteCyfVzgbulvSyC7tCyd3V8sW76sS0tHq8NpcbXfWTKasfyfzU1Kd86oKzw==",
"requires": {
"clone": "1.0.4",
"deep-eql": "0.1.3",
"keypather": "1.10.2"
},
"dependencies": {
"clone": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
"integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4="
}
}
},
"@babel/code-frame": {
"version": "7.0.0-beta.44",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.44.tgz",
@ -917,6 +934,19 @@
"minimalistic-assert": "1.0.1"
}
},
"assert-args": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/assert-args/-/assert-args-1.2.1.tgz",
"integrity": "sha1-QEEDoUUqMv53iYgR5U5ZCoqTc70=",
"requires": {
"101": "1.6.3",
"compound-subject": "0.0.1",
"debug": "2.6.9",
"get-prototype-of": "0.0.0",
"is-capitalized": "1.0.0",
"is-class": "0.0.4"
}
},
"assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
@ -947,8 +977,7 @@
"async-limiter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
"integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==",
"dev": true
"integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg=="
},
"asynckit": {
"version": "0.4.0",
@ -2964,6 +2993,11 @@
"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
"dev": true
},
"compound-subject": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/compound-subject/-/compound-subject-0.0.1.tgz",
"integrity": "sha1-JxVUaYoVrmCLHfyv0wt7oeqJLEs="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -3277,7 +3311,6 @@
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"requires": {
"ms": "2.0.0"
}
@ -3321,6 +3354,14 @@
"mimic-response": "1.0.1"
}
},
"deep-eql": {
"version": "0.1.3",
"resolved": "http://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz",
"integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=",
"requires": {
"type-detect": "0.1.1"
}
},
"deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
@ -4631,6 +4672,11 @@
"integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
"dev": true
},
"eventemitter3": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz",
"integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA=="
},
"evp_bytestokey": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
@ -6077,6 +6123,11 @@
"integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=",
"dev": true
},
"get-prototype-of": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/get-prototype-of/-/get-prototype-of-0.0.0.tgz",
"integrity": "sha1-mHcr0QcW0W3rSzIlFsRp78oorEQ="
},
"get-stdin": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
@ -6866,6 +6917,11 @@
"integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=",
"dev": true
},
"is-capitalized": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-capitalized/-/is-capitalized-1.0.0.tgz",
"integrity": "sha1-TIRktNkdPk7rRIid0s2PGwrEwTY="
},
"is-ci": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.1.0.tgz",
@ -6875,6 +6931,11 @@
"ci-info": "1.1.3"
}
},
"is-class": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/is-class/-/is-class-0.0.4.tgz",
"integrity": "sha1-4FdFFwW7NOOePjNZjJOpg3KWtzY="
},
"is-data-descriptor": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
@ -8085,6 +8146,16 @@
"resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
"integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==",
"dev": true
},
"ws": {
"version": "4.1.0",
"resolved": "http://registry.npmjs.org/ws/-/ws-4.1.0.tgz",
"integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==",
"dev": true,
"requires": {
"async-limiter": "1.0.0",
"safe-buffer": "5.1.1"
}
}
}
},
@ -8155,6 +8226,14 @@
"verror": "1.10.0"
}
},
"keypather": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/keypather/-/keypather-1.10.2.tgz",
"integrity": "sha1-4ESWMtSz5RbyHMAUznxWRP3c5hQ=",
"requires": {
"101": "1.6.3"
}
},
"kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
@ -9046,6 +9125,15 @@
"minimist": "0.0.8"
}
},
"mock-socket": {
"version": "8.0.4",
"resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-8.0.4.tgz",
"integrity": "sha512-xuO+Ep0xI60sXfBwIXezHuBnSj3Rafh6TSeTH81k2aFuNf64JW46PeDaViWMXQ/nMInknUzfMriPkoVhmh5Aag==",
"dev": true,
"requires": {
"url-parse": "1.4.3"
}
},
"modify-values": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz",
@ -9055,8 +9143,7 @@
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"mute-stream": {
"version": "0.0.7",
@ -13001,6 +13088,12 @@
"integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==",
"dev": true
},
"querystringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.0.tgz",
"integrity": "sha512-sluvZZ1YiTLD5jsqZcDmFyV2EwToyXZBfpoVOmktMmW+VEnhgakFHnasVph65fOjGPTWN0Nw3+XQaSeMayr0kg==",
"dev": true
},
"quick-lru": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz",
@ -13376,6 +13469,12 @@
"resolve-from": "1.0.1"
}
},
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
"dev": true
},
"reselect": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-3.0.1.tgz",
@ -13690,6 +13789,39 @@
"minimatch": "3.0.4"
}
},
"rpc-websockets": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-4.3.3.tgz",
"integrity": "sha512-Pq+tubbPkY63e0b1jukKkEQNWa5twpSvdd5izvtAK8OAw4qGGbWzJytqOdtProzpZQhFzynaHLNwfocmx8tmbg==",
"requires": {
"assert-args": "1.2.1",
"babel-runtime": "6.26.0",
"circular-json": "0.5.9",
"eventemitter3": "3.1.0",
"uuid": "3.3.2",
"ws": "5.2.2"
},
"dependencies": {
"circular-json": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.5.9.tgz",
"integrity": "sha512-4ivwqHpIFJZBuhN3g/pEcdbnGUywkBblloGbkglyloVjjR3uT6tieI89MVOfbP2tHX5sgb01FuLgAOzebNlJNQ=="
},
"uuid": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
},
"ws": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz",
"integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==",
"requires": {
"async-limiter": "1.0.0"
}
}
}
},
"rst-selector-parser": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz",
@ -15511,6 +15643,11 @@
"prelude-ls": "1.1.2"
}
},
"type-detect": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz",
"integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI="
},
"typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
@ -15758,6 +15895,16 @@
"integrity": "sha1-TTNA6AfTdzvamZH4MFrNzCpmXSo=",
"dev": true
},
"url-parse": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.3.tgz",
"integrity": "sha512-rh+KuAW36YKo0vClhQzLLveoj8FwPJNu65xLb7Mrt+eZht0IPT0IXgSv8gcMegZ6NvjJUALf6Mf25POlMwD1Fw==",
"dev": true,
"requires": {
"querystringify": "2.1.0",
"requires-port": "1.0.0"
}
},
"url-parse-lax": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz",
@ -16041,13 +16188,11 @@
}
},
"ws": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-4.1.0.tgz",
"integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==",
"dev": true,
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.1.0.tgz",
"integrity": "sha512-H3dGVdGvW2H8bnYpIDc3u3LH8Wue3Qh+Zto6aXXFzvESkTVT6rAfKR6tR/+coaUvxs8yHtmNV0uioBF62ZGSTg==",
"requires": {
"async-limiter": "1.0.0",
"safe-buffer": "5.1.1"
"async-limiter": "1.0.0"
}
},
"xml-name-validator": {

View File

@ -59,8 +59,10 @@
"jayson": "^2.0.6",
"mz": "^2.7.0",
"node-fetch": "^2.2.0",
"rpc-websockets": "^4.3.3",
"superstruct": "^0.6.0",
"tweetnacl": "^1.0.0"
"tweetnacl": "^1.0.0",
"ws": "^6.1.0"
},
"devDependencies": {
"babel-core": "6.26.3",

View File

@ -70,18 +70,31 @@ function generateConfig(configType) {
// maintained.
config.external = [
'assert',
'babel-runtime/core-js/get-iterator',
'babel-runtime/core-js/json/stringify',
'babel-runtime/core-js/object/assign',
'babel-runtime/core-js/object/get-prototype-of',
'babel-runtime/core-js/object/keys',
'babel-runtime/core-js/promise',
'babel-runtime/helpers/asyncToGenerator',
'babel-runtime/helpers/classCallCheck',
'babel-runtime/helpers/createClass',
'babel-runtime/helpers/get',
'babel-runtime/helpers/inherits',
'babel-runtime/helpers/possibleConstructorReturn',
'babel-runtime/helpers/toConsumableArray',
'babel-runtime/helpers/typeof',
'babel-runtime/regenerator',
'bn.js',
'bs58',
'buffer-layout',
'elfy',
'jayson/lib/client/browser',
'node-fetch',
'rpc-websockets',
'superstruct',
'tweetnacl',
'url',
];
break;
default:

View File

@ -1,9 +1,14 @@
// @flow
import assert from 'assert';
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 {Client as RpcWebSocketClient} from 'rpc-websockets';
import {Transaction} from './transaction';
import {PublicKey} from './publickey';
@ -11,6 +16,7 @@ import {sleep} from './util/sleep';
import type {Account} from './account';
import type {TransactionSignature, TransactionId} from './transaction';
type RpcRequest = (methodName: string, args: Array<any>) => any;
function createRpcRequest(url): RpcRequest {
@ -79,11 +85,10 @@ function jsonRpcResult(resultDescription: any) {
]);
}
/**
* Expected JSON RPC response for the "getAccountInfo" message
* @private
*/
const GetAccountInfoRpcResult = jsonRpcResult({
const AccountInfoResult = struct({
executable: 'boolean',
loader_program_id: 'array',
program_id: 'array',
@ -91,6 +96,18 @@ const GetAccountInfoRpcResult = jsonRpcResult({
userdata: 'array',
});
/**
* Expected JSON RPC response for the "getAccountInfo" message
*/
const GetAccountInfoRpcResult = jsonRpcResult(AccountInfoResult);
/***
* Expected JSON RPC response for the "accountNotification" message
*/
const AccountNotificationResult = struct({
subscription: 'number',
result: AccountInfoResult,
});
/**
* Expected JSON RPC response for the "confirmTransaction" message
@ -148,6 +165,20 @@ type AccountInfo = {
userdata: Buffer,
}
/**
* Callback function for account change notifications
*/
export type AccountChangeCallback = (accountInfo: AccountInfo) => void;
/**
* @private
*/
type AccountSubscriptionInfo = {
publicKey: string; // PublicKey of the account as a base 58 string
callback: AccountChangeCallback,
subscriptionId: null | number; // null when there's no current server subscription id
}
/**
* Possible signature status values
*
@ -164,6 +195,8 @@ export type SignatureStatus = 'Confirmed'
*/
export class Connection {
_rpcRequest: RpcRequest;
_rpcWebSocket: RpcWebSocketClient;
_rpcWebSocketConnected: boolean = false;
_lastIdInfo: {
lastId: TransactionId | null,
@ -171,6 +204,8 @@ export class Connection {
transactionSignatures: Array<string>,
};
_disableLastIdCaching: boolean = false
_accountChangeSubscriptions: {[number]: AccountSubscriptionInfo} = {};
_accountChangeSubscriptionCounter: number = 0;
/**
* Establish a JSON RPC connection
@ -178,15 +213,29 @@ export class Connection {
* @param endpoint URL to the fullnode JSON RPC endpoint
*/
constructor(endpoint: string) {
if (typeof endpoint !== 'string') {
throw new Error('Connection endpoint not specified');
}
this._rpcRequest = createRpcRequest(endpoint);
let url = urlParse(endpoint);
this._rpcRequest = createRpcRequest(url.href);
this._lastIdInfo = {
lastId: null,
seconds: -1,
transactionSignatures: [],
};
url.protocol = 'ws';
url.host = '';
url.port = String(Number(url.port) + 1);
this._rpcWebSocket = new RpcWebSocketClient(
urlFormat(url),
{
autoconnect: false,
max_reconnects: Infinity,
}
);
this._rpcWebSocket.on('open', this._wsOnOpen.bind(this));
this._rpcWebSocket.on('error', this._wsOnError.bind(this));
this._rpcWebSocket.on('close', this._wsOnClose.bind(this));
this._rpcWebSocket.on('accountNotification', this._wsOnAccountNotification.bind(this));
}
/**
@ -371,4 +420,133 @@ export class Connection {
assert(res.result);
return res.result;
}
/**
* @private
*/
_wsOnOpen() {
this._rpcWebSocketConnected = true;
this._updateSubscriptions();
}
/**
* @private
*/
_wsOnError(err: Error) {
console.log('ws error:', err.message);
}
/**
* @private
*/
_wsOnClose(code: number, message: string) {
// 1000 means _rpcWebSocket.close() was called explicitly
if (code !== 1000) {
console.log('ws close:', code, message);
}
this._rpcWebSocketConnected = false;
}
/**
* @private
*/
_wsOnAccountNotification(notification: Object) {
const res = AccountNotificationResult(notification);
if (res.error) {
throw new Error(res.error.message);
}
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;
assert(typeof result !== 'undefined');
sub.callback({
executable: result.executable,
tokens: result.tokens,
programId: new PublicKey(result.program_id),
loaderProgramId: new PublicKey(result.loader_program_id),
userdata: Buffer.from(result.userdata),
});
return true;
}
}
}
/**
* @private
*/
async _updateSubscriptions() {
const keys = Object.keys(this._accountChangeSubscriptions).map(Number);
if (keys.length === 0) {
this._rpcWebSocket.close();
return;
}
if (!this._rpcWebSocketConnected) {
for (let id of keys) {
this._accountChangeSubscriptions[id].subscriptionId = null;
}
this._rpcWebSocket.connect();
return;
}
for (let id of keys) {
const {subscriptionId, publicKey} = this._accountChangeSubscriptions[id];
if (subscriptionId === null) {
try {
this._accountChangeSubscriptions[id].subscriptionId =
await this._rpcWebSocket.call(
'accountSubscribe',
[publicKey]
);
} catch (err) {
console.log(`accountSubscribe error for ${publicKey}: ${err.message}`);
}
}
}
}
/**
* Register a callback to be invoked whenever the specified account changes
*
* @param publickey Public key of the account to monitor
* @param callback Function to invoke whenever the account is changed
* @return subscription id
*/
onAccountChange(publicKey: PublicKey, callback: AccountChangeCallback): number {
const id = ++this._accountChangeSubscriptionCounter;
this._accountChangeSubscriptions[id] = {
publicKey: publicKey.toBase58(),
callback,
subscriptionId: null
};
this._updateSubscriptions();
return id;
}
/**
* Deregister an account notification callback
*
* @param id subscription id to deregister
*/
async removeAccountChangeListener(id: number): Promise<void> {
if (this._accountChangeSubscriptions[id]) {
const {subscriptionId} = this._accountChangeSubscriptions[id];
delete this._accountChangeSubscriptions[id];
if (subscriptionId !== null) {
try {
await this._rpcWebSocket.call('accountUnsubscribe', [subscriptionId]);
} catch (err) {
console.log('accountUnsubscribe error:', err.message);
}
}
this._updateSubscriptions();
} else {
throw new Error(`Unknown account change id: ${id}`);
}
}
}

View File

@ -22,12 +22,17 @@ export const mockRpc: Array<[string, RpcRequest, RpcResponse]> = [];
// identified by `url` instead of using the mock
export const mockRpcEnabled = !process.env.DOITLIVE;
let mockNotice = true;
// Suppress lint: 'JestMockFn' is not defined
// eslint-disable-next-line no-undef
const mock: JestMockFn<any, any> = jest.fn(
(fetchUrl, fetchOptions) => {
if (!mockRpcEnabled) {
console.log(`Note: node-fetch mock is disabled, testing live against ${fetchUrl}`);
if (mockNotice) {
console.log(`Note: node-fetch mock is disabled, testing live against ${fetchUrl}`);
mockNotice = false;
}
return fetch(fetchUrl, fetchOptions);
}

View File

@ -0,0 +1,48 @@
import {Client as RpcWebSocketClient} from 'rpc-websockets';
// Define DOITLIVE in the environment to test against the real full node
// identified by `url` instead of using the mock
export const mockRpcEnabled = !process.env.DOITLIVE;
let mockNotice = true;
export class Client {
client: RpcWebSocketClient;
constructor(url, options) {
//console.log('MockClient', url, options);
if (!mockRpcEnabled) {
if (mockNotice) {
console.log('Note: rpc-websockets mock is disabled, testing live against', url);
mockNotice = false;
}
this.client = new RpcWebSocketClient(url, options);
}
}
connect() {
if (!mockRpcEnabled) {
return this.client.connect();
}
}
close() {
if (!mockRpcEnabled) {
return this.client.close();
}
}
on(event: string, callback: Function) {
if (!mockRpcEnabled) {
return this.client.on(event, callback);
}
//console.log('on', event);
}
async call(method: string, params: Object): Promise<Object> {
if (!mockRpcEnabled) {
return await this.client.call(method, params);
}
throw new Error('call unsupported');
}
}

View File

@ -1,9 +1,11 @@
// @flow
import {
Account,
Connection,
BpfLoader,
Loader,
SystemProgram,
sendAndConfirmTransaction,
} from '../src';
import {mockRpc, mockRpcEnabled} from './__mocks__/node-fetch';
import {mockGetLastId} from './mockrpc/getlastid';
@ -410,3 +412,46 @@ test('multi-instruction transaction', async () => {
expect(await connection.getBalance(accountTo.publicKey)).toBe(21);
});
test('account change notification', async () => {
if (mockRpcEnabled) {
console.log('non-live test skipped');
return;
}
const connection = new Connection(url);
const owner = new Account();
const programAccount = new Account();
const mockCallback = jest.fn();
const subscriptionId = connection.onAccountChange(programAccount.publicKey, mockCallback);
await connection.requestAirdrop(owner.publicKey, 42);
const transaction = SystemProgram.createAccount(
owner.publicKey,
programAccount.publicKey,
42,
3,
BpfLoader.programId,
);
await sendAndConfirmTransaction(connection, owner, transaction);
const loader = new Loader(connection, BpfLoader.programId);
await loader.load(programAccount, [1, 2, 3]);
await connection.removeAccountChangeListener(subscriptionId);
// mockCallback should be called twice
expect(mockCallback.mock.calls).toHaveLength(2);
// First mockCallback call is due to SystemProgram.createAccount()
expect(mockCallback.mock.calls[0][0].tokens).toBe(42);
expect(mockCallback.mock.calls[0][0].executable).toBe(false);
expect(mockCallback.mock.calls[0][0].userdata).toEqual(Buffer.from([0, 0, 0]));
expect(mockCallback.mock.calls[0][0].programId).toEqual(BpfLoader.programId);
// Second mockCallback call is due to loader.load()
expect(mockCallback.mock.calls[1][0].userdata).toEqual(Buffer.from([1, 2, 3]));
});

View File

@ -3,7 +3,6 @@
/**
* The connection url to use when running unit tests against a live network
*/
export const url = 'http://localhost:8899';
//export const url = 'http://testnet.solana.com:8899';
//export const url = 'http://master.testnet.solana.com:8899';
export const url = 'http://localhost:8899/';
//export const url = 'http://testnet.solana.com:8899/';