feat: slot change callback (provides real-time fork information)
This commit is contained in:
committed by
Michael Vines
parent
ddce1d3c9f
commit
286891fa51
@ -96,10 +96,17 @@ declare module '@solana/web3.js' {
|
|||||||
commission: number,
|
commission: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
declare export type SlotInfo = {
|
||||||
|
parent: 'number',
|
||||||
|
slot: 'number',
|
||||||
|
root: 'number',
|
||||||
|
};
|
||||||
|
|
||||||
declare type AccountChangeCallback = (accountInfo: AccountInfo) => void;
|
declare type AccountChangeCallback = (accountInfo: AccountInfo) => void;
|
||||||
declare type ProgramAccountChangeCallback = (
|
declare type ProgramAccountChangeCallback = (
|
||||||
keyedAccountInfo: KeyedAccountInfo,
|
keyedAccountInfo: KeyedAccountInfo,
|
||||||
) => void;
|
) => void;
|
||||||
|
declare type SlotChangeCallback = (slotInfo: SlotInfo) => void;
|
||||||
|
|
||||||
declare export type SignatureSuccess = {|
|
declare export type SignatureSuccess = {|
|
||||||
Ok: null,
|
Ok: null,
|
||||||
@ -191,6 +198,7 @@ declare module '@solana/web3.js' {
|
|||||||
programId: PublicKey,
|
programId: PublicKey,
|
||||||
callback: ProgramAccountChangeCallback,
|
callback: ProgramAccountChangeCallback,
|
||||||
): number;
|
): number;
|
||||||
|
onSlotChange(callback: SlotChangeCallback): number;
|
||||||
removeProgramAccountChangeListener(id: number): Promise<void>;
|
removeProgramAccountChangeListener(id: number): Promise<void>;
|
||||||
validatorExit(): Promise<boolean>;
|
validatorExit(): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
@ -318,6 +318,23 @@ const ProgramAccountNotificationResult = struct({
|
|||||||
result: ProgramAccountInfoResult,
|
result: ProgramAccountInfoResult,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
const SlotInfo = struct({
|
||||||
|
parent: 'number',
|
||||||
|
slot: 'number',
|
||||||
|
root: 'number',
|
||||||
|
});
|
||||||
|
|
||||||
|
/***
|
||||||
|
* Expected JSON RPC response for the "slotNotification" message
|
||||||
|
*/
|
||||||
|
const SlotNotificationResult = struct({
|
||||||
|
subscription: 'number',
|
||||||
|
result: SlotInfo,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expected JSON RPC response for the "getProgramAccounts" message
|
* Expected JSON RPC response for the "getProgramAccounts" message
|
||||||
*/
|
*/
|
||||||
@ -569,6 +586,19 @@ type ProgramAccountSubscriptionInfo = {
|
|||||||
subscriptionId: null | number, // null when there's no current server subscription id
|
subscriptionId: null | number, // null when there's no current server subscription id
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
type SlotSubscriptionInfo = {
|
||||||
|
callback: SlotChangeCallback,
|
||||||
|
subscriptionId: null | number, // null when there's no current server subscription id
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback function for slot change notifications
|
||||||
|
*/
|
||||||
|
export type SlotChangeCallback = (slotInfo: SlotInfo) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signature status: Success
|
* Signature status: Success
|
||||||
*
|
*
|
||||||
@ -618,6 +648,10 @@ export class Connection {
|
|||||||
[number]: ProgramAccountSubscriptionInfo,
|
[number]: ProgramAccountSubscriptionInfo,
|
||||||
} = {};
|
} = {};
|
||||||
_programAccountChangeSubscriptionCounter: number = 0;
|
_programAccountChangeSubscriptionCounter: number = 0;
|
||||||
|
_slotSubscriptions: {
|
||||||
|
[number]: SlotSubscriptionInfo,
|
||||||
|
} = {};
|
||||||
|
_slotSubscriptionCounter: number = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Establish a JSON RPC connection
|
* Establish a JSON RPC connection
|
||||||
@ -657,6 +691,10 @@ export class Connection {
|
|||||||
'programNotification',
|
'programNotification',
|
||||||
this._wsOnProgramAccountNotification.bind(this),
|
this._wsOnProgramAccountNotification.bind(this),
|
||||||
);
|
);
|
||||||
|
this._rpcWebSocket.on(
|
||||||
|
'slotNotification',
|
||||||
|
this._wsOnSlotNotification.bind(this),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1256,7 +1294,12 @@ export class Connection {
|
|||||||
const programKeys = Object.keys(
|
const programKeys = Object.keys(
|
||||||
this._programAccountChangeSubscriptions,
|
this._programAccountChangeSubscriptions,
|
||||||
).map(Number);
|
).map(Number);
|
||||||
if (accountKeys.length === 0 && programKeys.length === 0) {
|
const slotKeys = Object.keys(this._slotSubscriptions).map(Number);
|
||||||
|
if (
|
||||||
|
accountKeys.length === 0 &&
|
||||||
|
programKeys.length === 0 &&
|
||||||
|
slotKeys.length === 0
|
||||||
|
) {
|
||||||
this._rpcWebSocket.close();
|
this._rpcWebSocket.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1268,6 +1311,9 @@ export class Connection {
|
|||||||
for (let id of programKeys) {
|
for (let id of programKeys) {
|
||||||
this._programAccountChangeSubscriptions[id].subscriptionId = null;
|
this._programAccountChangeSubscriptions[id].subscriptionId = null;
|
||||||
}
|
}
|
||||||
|
for (let id of slotKeys) {
|
||||||
|
this._slotSubscriptions[id].subscriptionId = null;
|
||||||
|
}
|
||||||
this._rpcWebSocket.connect();
|
this._rpcWebSocket.connect();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1307,6 +1353,18 @@ export class Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (let id of slotKeys) {
|
||||||
|
const {subscriptionId} = this._slotSubscriptions[id];
|
||||||
|
if (subscriptionId === null) {
|
||||||
|
try {
|
||||||
|
this._slotSubscriptions[
|
||||||
|
id
|
||||||
|
].subscriptionId = await this._rpcWebSocket.call('slotSubscribe', []);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`slotSubscribe error: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1455,6 +1513,69 @@ export class Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_wsOnSlotNotification(notification: Object) {
|
||||||
|
const res = SlotNotificationResult(notification);
|
||||||
|
if (res.error) {
|
||||||
|
throw new Error(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,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a callback to be invoked upon slot changes
|
||||||
|
*
|
||||||
|
* @param callback Function to invoke whenever the slot changes
|
||||||
|
* @return subscription id
|
||||||
|
*/
|
||||||
|
onSlotChange(callback: SlotChangeCallback): number {
|
||||||
|
const id = ++this._slotSubscriptionCounter;
|
||||||
|
this._slotSubscriptions[id] = {
|
||||||
|
callback,
|
||||||
|
subscriptionId: id,
|
||||||
|
};
|
||||||
|
this._updateSubscriptions();
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deregister a slot notification callback
|
||||||
|
*
|
||||||
|
* @param id subscription id to deregister
|
||||||
|
*/
|
||||||
|
async removeSlotChangeListener(id: number): Promise<void> {
|
||||||
|
if (this._slotSubscriptions[id]) {
|
||||||
|
const {subscriptionId} = this._slotSubscriptions[id];
|
||||||
|
delete this._slotSubscriptions[id];
|
||||||
|
if (subscriptionId !== null) {
|
||||||
|
try {
|
||||||
|
await this._rpcWebSocket.call('slotUnsubscribe', [subscriptionId]);
|
||||||
|
} catch (err) {
|
||||||
|
console.log('slotUnsubscribe error:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._updateSubscriptions();
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown slot change id: ${id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_argsWithCommitment(args: Array<any>, override: ?Commitment): Array<any> {
|
_argsWithCommitment(args: Array<any>, override: ?Commitment): Array<any> {
|
||||||
const commitment = override || this._commitment;
|
const commitment = override || this._commitment;
|
||||||
if (commitment) {
|
if (commitment) {
|
||||||
|
@ -931,3 +931,39 @@ test('program account change notification', async () => {
|
|||||||
|
|
||||||
await connection.removeProgramAccountChangeListener(subscriptionId);
|
await connection.removeProgramAccountChangeListener(subscriptionId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('slot notification', async () => {
|
||||||
|
if (mockRpcEnabled) {
|
||||||
|
console.log('non-live test skipped');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const connection = new Connection(url, 'recent');
|
||||||
|
|
||||||
|
// let notified = false;
|
||||||
|
const subscriptionId = connection.onSlotChange(slotInfo => {
|
||||||
|
expect(slotInfo.parent).toBeDefined();
|
||||||
|
expect(slotInfo.slot).toBeDefined();
|
||||||
|
expect(slotInfo.root).toBeDefined();
|
||||||
|
expect(slotInfo.slot).toBeGreaterThan(slotInfo.parent);
|
||||||
|
expect(slotInfo.slot).toBeGreaterThanOrEqual(slotInfo.root);
|
||||||
|
// notified = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// FIXME: enable this check when slotNotification is live
|
||||||
|
//
|
||||||
|
// // Wait for mockCallback to receive a call
|
||||||
|
// let i = 0;
|
||||||
|
// while (!notified) {
|
||||||
|
// //for (;;) {
|
||||||
|
// if (++i === 30) {
|
||||||
|
// throw new Error('Slot change notification not observed');
|
||||||
|
// }
|
||||||
|
// // Sleep for a 1/4 of a slot, notifications only occur after a block is
|
||||||
|
// // processed
|
||||||
|
// await sleep((250 * DEFAULT_TICKS_PER_SLOT) / NUM_TICKS_PER_SECOND);
|
||||||
|
// }
|
||||||
|
|
||||||
|
await connection.removeSlotChangeListener(subscriptionId);
|
||||||
|
});
|
||||||
|
Reference in New Issue
Block a user