mcps-sdk-js
Version:
MCPS JavaScript SDK
512 lines (467 loc) • 15.6 kB
text/typescript
import { SdkError } from '../errors';
import * as types from '../types';
import { Utils, Crypto } from '../utils';
import * as is from 'is_js';
import { WsClient } from './ws-client';
import { EventQueryBuilder, EventKey } from '../types';
import { Client } from '../client';
/** Internal subscription interface */
interface Subscription {
id: string;
query: string;
eventType: types.EventTypes;
callback: (error?: SdkError, data?: any) => void;
}
/** Internal event dao for caching the events */
class EventDAO {
private subscriptions = new Map<string, Subscription>();
setSubscription(id: string, subscription: Subscription): void {
this.subscriptions.set(id, subscription);
}
deleteSubscription(id: string): void {
this.subscriptions.delete(id);
}
getAllSubscriptions(): Map<string, Subscription> {
return this.subscriptions;
}
clear(): void {
this.subscriptions.clear();
}
}
/**
* MCPS Event Listener
* @since v0.17
*/
export class EventListener {
/** @hidden */
private wsClient: WsClient;
/** @hidden */
private eventDAO: EventDAO;
/** @hidden */
private client: Client;
/** @hidden */
constructor(client: Client) {
this.client = client;
this.wsClient = new WsClient(this.client.config.node);
this.eventDAO = new EventDAO();
this.wsClient.eventEmitter.on('open', () => {
const subscriptions = this.eventDAO.getAllSubscriptions();
if (subscriptions) {
subscriptions.forEach(sub => {
// Subscribe all events in context
console.log('Subscribe: ' + sub.query);
this.wsClient.send(types.RpcMethods.Subscribe, sub.id, sub.query);
const event = sub.id + '#event';
this.wsClient.eventEmitter.removeAllListeners(event); // just in case dup of listeners
switch (sub.eventType) {
case types.EventTypes.NewBlock: {
// Listen for new blocks, decode and callback
this.wsClient.eventEmitter.on(event, (error, data) => {
this.newBlockHandler(sub.callback, error, data);
});
return;
}
case types.EventTypes.NewBlockHeader: {
// Listen for new block headers, decode and callback
this.wsClient.eventEmitter.on(event, (error, data) => {
this.newBlockHeaderHandler(sub.callback, error, data);
});
return;
}
case types.EventTypes.ValidatorSetUpdates: {
// Listen for validator set updates, decode and callback
this.wsClient.eventEmitter.on(event, (error, data) => {
this.validatorSetUpdatesHandler(sub.callback, error, data);
});
return;
}
case types.EventTypes.Tx: {
// Listen for txs, decode and callback
this.wsClient.eventEmitter.on(event, (error, data) => {
this.txHandler(sub.callback, error, data);
});
return;
}
default: {
return;
}
}
});
}
});
// If the connection is closed subjectively, this event will not be triggered
// see also: disconnect()
this.wsClient.eventEmitter.on('close', () => {
// Disconnected unexpectedly, try reconnecting
console.log('Reconnecting...');
setTimeout(() => {
this.connect();
}, 5000); // try reconnecting every 5s
});
this.wsClient.eventEmitter.on('error', err => {
// TODO
});
}
/**
* Connect to server
* @since v0.17
*/
connect(): void {
this.wsClient.connect();
}
/**
* Disconnect from server and clear all the listeners
* @since v0.17
*/
async disconnect(): Promise<void> {
return this.wsClient.disconnect().then(() => {
this.eventDAO.clear();
});
}
/**
* Subscribe new block notifications
* @param conditions Query conditions
* @param callback A function to receive notifications
* @returns
* @since v0.17
*/
subscribeNewBlock(
callback: (error?: SdkError, data?: types.EventDataNewBlock) => void,
conditions?: EventQueryBuilder
): types.EventSubscription {
// Build and send subscription
const eventType = types.EventTypes.NewBlock;
const id = eventType + Math.random().toString(16);
const queryBuilder = conditions ? conditions : new EventQueryBuilder();
const query = queryBuilder
.addCondition(new types.Condition(EventKey.Type).eq(eventType))
.build();
if (this.wsClient.isReady()) {
this.wsClient.send(types.RpcMethods.Subscribe, id, query);
// Listen for new blocks, decode and callback
this.wsClient.eventEmitter.on(id + '#event', (error, data) => {
this.newBlockHandler(callback, error, data);
});
}
this.eventDAO.setSubscription(id, {
id,
query,
eventType,
callback,
});
// Return an types.EventSubscription instance, so client could use to unsubscribe this context
return { id, query };
}
/**
* Subscribe new block header notifications
* @param conditions Query conditions
* @param callback A function to receive notifications
* @returns
* @since v0.17
*/
subscribeNewBlockHeader(
callback: (error?: SdkError, data?: types.EventDataNewBlockHeader) => void
): types.EventSubscription {
// Build and send subscription
const eventType = types.EventTypes.NewBlockHeader;
const id = eventType + Math.random().toString(16);
const query = new EventQueryBuilder()
.addCondition(new types.Condition(EventKey.Type).eq(eventType))
.build();
if (this.wsClient.isReady()) {
this.wsClient.send(types.RpcMethods.Subscribe, id, query);
// Listen for new block headers, decode and callback
this.wsClient.eventEmitter.on(id + '#event', (error, data) => {
this.newBlockHeaderHandler(callback, error, data);
});
}
this.eventDAO.setSubscription(id, {
id,
query,
eventType,
callback,
});
// Return an types.EventSubscription instance, so client could use to unsubscribe this context
return { id, query };
}
/**
* Subscribe validator set update notifications
* @param conditions Query conditions
* @param callback A function to receive notifications
* @returns
* @since v0.17
*/
subscribeValidatorSetUpdates(
callback: (
error?: SdkError,
data?: types.EventDataValidatorSetUpdates[]
) => void
): types.EventSubscription {
// Build and send subscription
const eventType = types.EventTypes.ValidatorSetUpdates;
const id = eventType + Math.random().toString(16);
const query = new EventQueryBuilder()
.addCondition(new types.Condition(EventKey.Type).eq(eventType))
.build();
if (this.wsClient.isReady()) {
this.wsClient.send(types.RpcMethods.Subscribe, id, query);
// Listen for validator set updates, decode and callback
this.wsClient.eventEmitter.on(id + '#event', (error, data) => {
this.validatorSetUpdatesHandler(callback, error, data);
});
}
this.eventDAO.setSubscription(id, {
id,
query,
eventType,
callback,
});
// Return an types.EventSubscription instance, so client could use to unsubscribe this context
return { id, query };
}
/**
* Subscribe successful Txs notifications
* @param conditions Query conditions
* @param callback A function to receive notifications
* @returns
* @since v0.17
*/
subscribeTx(
conditions: EventQueryBuilder,
callback: (error?: SdkError, data?: types.EventDataResultTx) => void
): types.EventSubscription {
// Build and send subscription
const eventType = types.EventTypes.Tx;
const id = eventType + Math.random().toString(16);
const queryBuilder = conditions ? conditions : new EventQueryBuilder();
const query = queryBuilder
.addCondition(new types.Condition(EventKey.Type).eq(eventType))
.build();
if (this.wsClient.isReady()) {
this.wsClient.send(types.RpcMethods.Subscribe, id, query);
// Listen for txs, decode and callback
this.wsClient.eventEmitter.on(id + '#event', (error, data) => {
this.txHandler(callback, error, data);
});
}
this.eventDAO.setSubscription(id, {
id,
query,
eventType,
callback,
});
// Return an types.EventSubscription instance, so client could use to unsubscribe this context
return { id, query };
}
/**
* Unsubscribe the specified event
* @param subscription The event subscription instance
* @since v0.17
*/
unsubscribe(subscription: types.EventSubscription): void {
// Unsubscribe the specified event from server
this.wsClient.send(
types.RpcMethods.Unsubscribe,
'unsubscribe#' + subscription.id,
subscription.query
);
this.wsClient.eventEmitter.on(
'unsubscribe#' + subscription.id,
(error, data) => {
console.log(error);
console.log(data);
// Remove the subscription listeners
this.wsClient.eventEmitter.removeAllListeners(
subscription.id + '#event'
);
// Remove the current `unsubscribe` operation listener
this.wsClient.eventEmitter.removeAllListeners(
'unsubscribe#' + subscription.id
);
}
);
}
private newBlockHandler(
callback: (error?: SdkError, data?: types.EventDataNewBlock) => void,
error?: any,
data?: any
): void {
if (error) {
callback(new SdkError(error.message, error.code), undefined);
}
if (!data || !data.data || !data.data.value) {
return;
}
const blockData = data.data.value;
// Decode txs
if (blockData.block && blockData.block.data && blockData.block.data.txs) {
const txs: string[] = blockData.block.data.txs;
const decodedTxs = new Array();
txs.forEach(msg => {
decodedTxs.push(
this.client.protobuf.deserializeTx(msg)
);
});
blockData.block.data.txs = decodedTxs;
}
// Decode proposer address
if (blockData.block) {
blockData.block.header.bech32_proposer_address = Crypto.encodeAddress(
blockData.block.header.proposer_address,
this.client.config.bech32Prefix.ConsAddr
);
}
// Decode begin block tags
if (blockData.result_begin_block) {
blockData.result_begin_block.tags = Utils.decodeTags(
blockData.result_begin_block.tags
);
}
if (blockData.result_end_block) {
// Decode end block tags
blockData.result_end_block.tags = Utils.decodeTags(
blockData.result_end_block.tags
);
// Decode validator updates
if (blockData.result_end_block.validator_updates) {
const validators: types.EventDataValidatorUpdate[] = [];
blockData.result_end_block.validator_updates.forEach((v: any) => {
let type = types.PubkeyType.secp256k1;
switch (v.pub_key.type) {
case 'secp256k1': {
type = types.PubkeyType.secp256k1;
break;
}
case 'ed25519': {
type = types.PubkeyType.ed25519;
break;
}
default:
throw new SdkError(`Unsupported pubkey type: ${v.pub_key.type}`);
}
const valPubkey: types.Pubkey = {
type,
value: v.pub_key.data,
};
const bech32Pubkey = Crypto.encodeAddress(
Crypto.aminoMarshalPubKey(valPubkey, false),
this.client.config.bech32Prefix.ConsPub
);
validators.push({
bech32_pubkey: bech32Pubkey,
pub_key: valPubkey,
voting_power: v.power,
});
});
blockData.result_end_block.validator_updates = validators;
}
}
const eventBlock = blockData as types.EventDataNewBlock;
callback(undefined, eventBlock);
}
private newBlockHeaderHandler(
callback: (error?: SdkError, data?: types.EventDataNewBlockHeader) => void,
error: any,
data: any
) {
if (error) {
callback(new SdkError(error.message, error.code), undefined);
}
if (!data.data || !data.data.value) {
return;
}
const blockHeader = data.data.value;
// Decode proposer address
blockHeader.header.bech32_proposer_address = Crypto.encodeAddress(
blockHeader.header.proposer_address,
this.client.config.bech32Prefix.ConsAddr
);
// Decode begin block tags
if (blockHeader.result_begin_block) {
blockHeader.result_begin_block.tags = Utils.decodeTags(
blockHeader.result_begin_block.tags
);
}
if (blockHeader.result_end_block) {
// Decode end block tags
blockHeader.result_end_block.tags = Utils.decodeTags(
blockHeader.result_end_block.tags
);
// Decode validator updates
if (blockHeader.result_end_block.validator_updates) {
const validators: types.EventDataValidatorUpdate[] = [];
blockHeader.result_end_block.validator_updates.forEach((v: any) => {
const type =
v.pub_key.type === 'secp256k1'
? types.PubkeyType.secp256k1
: types.PubkeyType.ed25519;
const valPubkey: types.Pubkey = {
type,
value: v.pub_key.data,
};
const bech32Pubkey = Crypto.encodeAddress(
Crypto.aminoMarshalPubKey(valPubkey, false),
this.client.config.bech32Prefix.ConsPub
);
validators.push({
bech32_pubkey: bech32Pubkey,
pub_key: valPubkey,
voting_power: v.power,
});
});
blockHeader.result_end_block.validator_updates = validators;
}
}
callback(undefined, blockHeader as types.EventDataNewBlockHeader);
}
private validatorSetUpdatesHandler(
callback: (
error?: SdkError,
data?: types.EventDataValidatorSetUpdates[]
) => void,
error: any,
data: any
) {
if (error) {
callback(new SdkError(error.message, error.code), undefined);
}
if (!data.data || !data.data.value || !data.data.value.validator_updates) {
return;
}
const eventValidatorUpdates = data.data.value
.validator_updates as types.EventDataValidatorSetUpdates[];
callback(undefined, eventValidatorUpdates);
}
private txHandler(
callback: (error?: SdkError, data?: types.EventDataResultTx) => void,
error: any,
data: any
) {
if (error) {
callback(new SdkError(error.message, error.code), undefined);
}
if (!data || !data.data || !data.data.value || !data.data.value.TxResult) {
return;
}
const txResult = data.data.value.TxResult;
txResult.tx = this.client.protobuf.deserializeTx(txResult.tx);
if (txResult.result.tags) {
const tags = txResult.result.tags as types.Tag[];
const decodedTags = new Array<types.Tag>();
tags.forEach(element => {
const key = Utils.base64ToString(element.key);
const value =
!element.value || is.empty(element.value)
? ''
: Utils.base64ToString(element.value);
decodedTags.push({
key,
value,
});
});
txResult.result.tags = decodedTags;
}
txResult.hash = Crypto.generateTxHash(txResult.tx);
callback(undefined, txResult);
}
}