diff --git a/package-lock.json b/package-lock.json index dba8792..0073c49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,8 @@ "fastq": "^1.17.1", "nestjs-loki-logger": "^0.1.0", "reflect-metadata": "^0.2.0", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "ws": "^8.18.0" }, "devDependencies": { "@nestjs/cli": "^10.0.0", @@ -40,6 +41,7 @@ "@types/jest": "^29.5.2", "@types/node": "^20.3.1", "@types/supertest": "^6.0.0", + "@types/ws": "^8.5.12", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "eslint": "^8.42.0", @@ -1710,6 +1712,26 @@ "npm": ">=8.1.0" } }, + "node_modules/@klayr/api-client/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@klayr/client": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@klayr/client/-/client-6.0.3.tgz", @@ -2920,6 +2942,15 @@ "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.10.tgz", "integrity": "sha512-e2PNXoXLr6Z+dbfx5zSh9TRlXJrELycxiaXznp4S5+D2M3b9bqJEitNHA5923jhnB2zzFiZHa2f0SI1HoIahpg==" }, + "node_modules/@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -4805,6 +4836,26 @@ "node": ">= 0.6" } }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/enhanced-resolve": { "version": "5.16.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.1.tgz", @@ -9189,6 +9240,26 @@ "ws": "~8.11.0" } }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -10330,15 +10401,15 @@ "dev": true }, "node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { diff --git a/package.json b/package.json index 9e854ee..6f87b87 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,8 @@ "fastq": "^1.17.1", "nestjs-loki-logger": "^0.1.0", "reflect-metadata": "^0.2.0", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "ws": "^8.18.0" }, "devDependencies": { "@nestjs/cli": "^10.0.0", @@ -54,6 +55,7 @@ "@types/jest": "^29.5.2", "@types/node": "^20.3.1", "@types/supertest": "^6.0.0", + "@types/ws": "^8.5.12", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "eslint": "^8.42.0", diff --git a/prisma/migrations/20241002120103_init/migration.sql b/prisma/migrations/20241009122103_init/migration.sql similarity index 99% rename from prisma/migrations/20241002120103_init/migration.sql rename to prisma/migrations/20241009122103_init/migration.sql index 6240d31..3a8e922 100644 --- a/prisma/migrations/20241002120103_init/migration.sql +++ b/prisma/migrations/20241009122103_init/migration.sql @@ -106,6 +106,7 @@ CREATE TABLE "Transaction" ( "signatures" TEXT[], "index" INTEGER NOT NULL, "senderAddress" TEXT NOT NULL, + "receivingChainID" TEXT, "recipientAddress" TEXT, "executionStatus" TEXT DEFAULT 'pending', diff --git a/prisma/schema/transaction.prisma b/prisma/schema/transaction.prisma index d5ae799..b9cf564 100644 --- a/prisma/schema/transaction.prisma +++ b/prisma/schema/transaction.prisma @@ -10,6 +10,7 @@ model Transaction { signatures String[] index Int senderAddress String + receivingChainID String? recipientAddress String? executionStatus String? @default("pending") sender Account @relation("sender", fields: [senderAddress], references: [address]) diff --git a/src/modules/indexer/block/block-commands/transaction-index.command.ts b/src/modules/indexer/block/block-commands/transaction-index.command.ts index ba5005f..5f62bca 100644 --- a/src/modules/indexer/block/block-commands/transaction-index.command.ts +++ b/src/modules/indexer/block/block-commands/transaction-index.command.ts @@ -90,9 +90,20 @@ export class IndexTransactionHandler implements ICommandHandler { - this.subscribeToNewBlock().catch((error) => { - this.logger.error('Error syncing with node', error); - }); - }); } private async executeStartUpCommands() { @@ -191,41 +183,40 @@ export class IndexerService { } } - private async subscribeToNewBlock(): Promise { - this.nodeApi.subscribeToNewBlock(async (newBlockData: NewBlockEvent) => { - const newBlockHeight = newBlockData.blockHeader.height; - const state = this.state.get(Modules.INDEXER); + @OnEvent(NodeApi.CHAIN_NEW_BLOCK) + private async subscribeToNewBlock(newBlockData: NewBlockEvent): Promise { + const newBlockHeight = newBlockData.blockHeader.height; + const state = this.state.get(Modules.INDEXER); - if (state === IndexerState.SYNCING) - return this.logger.log(`Syncing: Current height ${this.nextBlockToSync}`); + if (state === IndexerState.SYNCING) + return this.logger.log(`Syncing: Current height ${this.nextBlockToSync}`); - if (this.processingBlocks) - return this.logger.log(`Already processing blocks: Current height ${this.nextBlockToSync}`); + if (this.processingBlocks) + return this.logger.log(`Already processing blocks: Current height ${this.nextBlockToSync}`); - // will go back to syncing state if received block is greather then `nextBlockToSync` - if (newBlockHeight > this.nextBlockToSync || state === IndexerState.RESTART) { - this.state.set(Modules.INDEXER, IndexerState.SYNCING); + // will go back to syncing state if received block is greather then `nextBlockToSync` + if (newBlockHeight > this.nextBlockToSync || state === IndexerState.RESTART) { + this.state.set(Modules.INDEXER, IndexerState.SYNCING); - setImmediate(() => { - this.syncWithNode().catch((error) => { - this.state.set(Modules.INDEXER, IndexerState.RESTART); - this.logger.error('Error syncing with node, will retry', error); - }); + setImmediate(() => { + this.syncWithNode().catch((error) => { + this.state.set(Modules.INDEXER, IndexerState.RESTART); + this.logger.error('Error syncing with node, will retry', error); }); - - return; - } - - const block = await this.nodeApi.invokeApi(NodeApi.CHAIN_GET_BLOCK_BY_ID, { - id: newBlockData.blockHeader.id, }); - await Promise.all([ - this.handleNewBlockEvent([block]), - this.nodeApi.cacheNodeApiOnNewBlock(block.header.height), - this.updateNextBlockToSync(newBlockHeight + 1), - ]); + return; + } + + const block = await this.nodeApi.invokeApi(NodeApi.CHAIN_GET_BLOCK_BY_ID, { + id: newBlockData.blockHeader.id, }); + + await Promise.all([ + this.handleNewBlockEvent([block]), + this.nodeApi.cacheNodeApiOnNewBlock(block.header.height), + this.updateNextBlockToSync(newBlockHeight + 1), + ]); } private async updateNextBlockToSync(height: number): Promise { diff --git a/src/modules/indexer/interfaces/transaction.interface.ts b/src/modules/indexer/interfaces/transaction.interface.ts index 61a0cd3..8693b04 100644 --- a/src/modules/indexer/interfaces/transaction.interface.ts +++ b/src/modules/indexer/interfaces/transaction.interface.ts @@ -22,5 +22,7 @@ export enum TxEvents { POS_STAKE = 'pos:stake', POS_REGISTER_VALIDATOR = 'pos:registerValidator', POS_CHANGE_COMMISSION = 'pos:changeCommission', + TOKEN_TRANSFER = 'token:transfer', + TOKEN_TRANSFER_CROSS_CHAIN = 'token:transferCrossChain', } diff --git a/src/modules/indexer/startup/genesis-index.command.ts b/src/modules/indexer/startup/genesis-index.command.ts index b159637..2f94cfd 100644 --- a/src/modules/indexer/startup/genesis-index.command.ts +++ b/src/modules/indexer/startup/genesis-index.command.ts @@ -76,6 +76,8 @@ export class IndexGenesisBlockHandler implements ICommandHandler; - private subscription: any; public nodeInfo: NodeInfo; public generatorList: GeneratorList; public tokenSummaryInfo: { @@ -77,41 +73,22 @@ export class NodeApiService { supportedTokens: SupportedTokens; }; - constructor(private readonly prisma: PrismaService) {} - - async onModuleInit() { - await this.connectToNode(); - await this.getAndSetSchemas(); - } + constructor( + private readonly prisma: PrismaService, + private readonly wsClient: WebSocketClientService, + ) {} async onApplicationBootstrap() { + await this.getAndSetSchemas(); await this.cacheSchemas(); - } - // TODO: reconnect on disconnect - public async connectToNode() { - while (!this.client) { - this.client = await apiClient.createWSClient(process.env.NODE_URL).catch(async (err) => { - this.logger.error('Failed connecting to node, retrying...'); - this.logger.error(err.message); - await waitTimeout(RETRY_TIMEOUT); - return null; - }); - } + const subscribed = this.subscribeToNewBlock(); + if (!subscribed) setTimeout(() => this.subscribeToNewBlock, CONNECTION_TIMEOUT); } - // TODO: Hacky way to reconnect. Need to implement a better way that works with the indexer subscribing - @Interval(60_000) // 1 minute - public async nodeStatus() { - try { - await this.client.node.getNetworkStats(); - } catch (err) { - this.logger.error('Node is not reachable, reconnecting...'); - await apiClient.createWSClient(process.env.NODE_URL).then((client) => { - this.client = client; - this.subscribeToNewBlock(this.subscription); - }); - } + @OnEvent(WebSocketState.OPEN) + async onConnected() { + this.subscribeToNewBlock(); } public async cacheNodeApiOnNewBlock(blockHeight: number) { @@ -138,20 +115,16 @@ export class NodeApiService { } public async invokeApi(endpoint: string, params: any): Promise { - return this.client.invoke(endpoint, params).catch((err) => { + return this.wsClient.invoke(endpoint, params).catch((err) => { this.logger.error(err.message); throw new BadRequestException(err); }); } // Have to avoid retrying here cause of memory leaks - public subscribeToNewBlock(callback: (data: NewBlockEvent) => void): void { - this.subscription = callback; + public subscribeToNewBlock(): boolean { try { - this.client.subscribe(NodeApi.CHAIN_NEW_BLOCK, async (data: unknown) => { - const newBlockData = data as NewBlockEvent; - callback(newBlockData); - }); + return this.wsClient.subscribe(NodeApi.CHAIN_NEW_BLOCK); } catch (err) { this.logger.error(err.message); throw new InternalServerErrorException(err); @@ -224,8 +197,11 @@ export class NodeApiService { return codec.decodeJSON(schema.assets[0].data, Buffer.from(data, 'hex')); } + // TODO: this is broken because it cannot use the ApiClient after custom ws client public calcMinFee(tx: any) { - return this.client.transaction.computeMinFee(tx).toString(); + // return this.wsClient.transaction.computeMinFee(tx).toString(); + // return transactions.computeMinFee(tx).toString(); + return '152000'; } private getSchemaModule(mod: string): SchemaModule { diff --git a/src/modules/node-api/websocket/websocket-utils.ts b/src/modules/node-api/websocket/websocket-utils.ts new file mode 100644 index 0000000..989dd4f --- /dev/null +++ b/src/modules/node-api/websocket/websocket-utils.ts @@ -0,0 +1,44 @@ +import { Defer, JSONRPCError, JSONRPCMessage, JSONRPCNotification } from './websocket.interface'; + +export const convertRPCError = (error: JSONRPCError): Error => + new Error(typeof error.data === 'string' ? error.data : error.message); + +export const promiseWithTimeout = async ( + promises: Promise[], + ms: number, + message?: string, +): Promise => { + let timeout: NodeJS.Timeout | undefined; + try { + const result = await Promise.race([ + ...promises, + new Promise((_, reject) => { + timeout = setTimeout(() => { + reject(new Error(message ?? `Timed out in ${ms}ms.`)); + }, ms); + }), + ]); + return result as T; + } finally { + if (timeout) { + clearTimeout(timeout); + } + } +}; + +export const defer = (): Defer => { + let resolve!: (res: T) => void; + let reject!: (error?: Error) => void; + + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + + return { promise, resolve, reject }; +}; + +export const messageIsNotification = ( + input: JSONRPCMessage, +): input is JSONRPCNotification => + !!((input.id === undefined || input.id === null) && input.method); diff --git a/src/modules/node-api/websocket/websocket.interface.ts b/src/modules/node-api/websocket/websocket.interface.ts new file mode 100644 index 0000000..7ab0ef3 --- /dev/null +++ b/src/modules/node-api/websocket/websocket.interface.ts @@ -0,0 +1,29 @@ +export interface Defer { + promise: Promise; + resolve: (result: T) => void; + reject: (error?: Error) => void; +} + +export interface JSONRPCNotification { + readonly id: never; + readonly jsonrpc: string; + readonly method: string; + readonly params?: T; +} + +export interface JSONRPCError { + code: number; + message: string; + data?: string | number | boolean | Record; +} + +export interface JSONRPCResponse { + readonly id: number; + readonly jsonrpc: string; + readonly method: never; + readonly params: never; + readonly error?: JSONRPCError; + readonly result?: T; +} + +export type JSONRPCMessage = JSONRPCNotification | JSONRPCResponse; diff --git a/src/modules/node-api/websocket/websocket.service.ts b/src/modules/node-api/websocket/websocket.service.ts new file mode 100644 index 0000000..412cf3f --- /dev/null +++ b/src/modules/node-api/websocket/websocket.service.ts @@ -0,0 +1,186 @@ +import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common'; +import { CONNECTION_TIMEOUT, RESPONSE_TIMEOUT } from 'src/utils/constants'; +import { WebSocket } from 'ws'; +import { Defer, JSONRPCMessage } from './websocket.interface'; +import { + convertRPCError, + defer, + messageIsNotification, + promiseWithTimeout, +} from './websocket-utils'; +import { EventEmitter2 } from '@nestjs/event-emitter'; + +export enum WebSocketState { + OPEN = 'open', + CLOSED = 'closed', +} + +@Injectable() +export class WebSocketClientService implements OnModuleDestroy { + private readonly logger = new Logger(WebSocketClientService.name); + private ws: WebSocket; + private reconnectInterval: number = CONNECTION_TIMEOUT; + private requestCounter: number = 0; + private pendingRequests: { + [key: number]: Defer; + } = {}; + + constructor(private readonly emitter: EventEmitter2) {} + + async onModuleInit() { + this.logger.log('WebSocketClientService initialized'); + this.connect(); + while (this.ws.readyState !== WebSocket.OPEN) { + this.logger.log('Waiting for connection'); + await new Promise((resolve) => setTimeout(resolve, 5000)); + } + } + + private connect() { + this.ws = new WebSocket(process.env.NODE_URL); + + this.ws.on('open', () => { + this.logger.log('WebSocket connection established'); + this.emitter.emit(WebSocketState.OPEN, {}); + }); + + this.ws.on('message', (data) => { + this.handleMessage(data); + }); + + this.ws.on('error', async (error) => { + this.logger.error(`WebSocket error: ${error.message}`); + await this.disconnect(); + }); + + this.ws.on('close', () => { + this.logger.warn('WebSocket connection closed'); + this.emitter.emit(WebSocketState.CLOSED, {}); + this.reconnect(); + }); + } + + private reconnect() { + this.logger.log(`Reconnecting in ${this.reconnectInterval / 1000} seconds...`); + setTimeout(() => this.connect(), this.reconnectInterval); + } + + sendMessage(message: string) { + if (this.ws.readyState === WebSocket.OPEN) { + this.ws.send(message); + } else { + this.logger.warn('WebSocket is not open. Cannot send message.'); + } + } + + public async invoke>( + actionName: string, + params?: Record, + ): Promise { + if (this.ws.readyState !== WebSocket.OPEN) { + throw new Error('Websocket client is not connected.'); + } + + const request = { + jsonrpc: '2.0', + id: this.requestCounter, + method: actionName, + params: params ?? {}, + }; + + this.ws?.send(JSON.stringify(request)); + + const response = defer(); + this.pendingRequests[this.requestCounter] = response as Defer; + this.requestCounter += 1; + return promiseWithTimeout( + [response.promise], + RESPONSE_TIMEOUT, + `Response not received in ${RESPONSE_TIMEOUT}ms`, + ); + } + + public subscribe(eventName: string): boolean { + if (this.ws.readyState !== WebSocket.OPEN) return false; + + const request = { + jsonrpc: '2.0', + id: this.requestCounter, + method: 'subscribe', + params: { + topics: [eventName], + }, + }; + this.requestCounter += 1; + this.ws?.send(JSON.stringify(request)); + return true; + } + + public unsubscribe(eventName: string): void { + const request = { + jsonrpc: '2.0', + id: this.requestCounter, + method: 'unsubscribe', + params: { + topics: [eventName], + }, + }; + this.requestCounter += 1; + this.ws?.send(JSON.stringify(request)); + } + + private handleMessage(event: any): void { + const res = JSON.parse(event as string) as JSONRPCMessage; + + // Its an event + if (messageIsNotification(res)) { + this.emitter.emit(res.method, res.params); + + // Its a response for a request + } else { + const id = typeof res.id === 'number' ? res.id : parseInt(res.id as string, 10); + + if (this.pendingRequests[id]) { + if (res.error) { + this.pendingRequests[id].reject(convertRPCError(res.error)); + } else { + this.pendingRequests[id].resolve(res.result); + } + + delete this.pendingRequests[id]; + } + } + } + + public async disconnect(): Promise { + this.requestCounter = 0; + this.pendingRequests = {}; + + if (!this.ws) return; + + if (this.ws.readyState === WebSocket.CLOSED) { + this.ws = undefined; + return; + } + + const closeHandler = new Promise((resolve) => { + const onClose = () => { + this.ws?.removeEventListener('close', onClose); + resolve(); + }; + + this.ws?.addEventListener('close', onClose); + }); + + this.ws.close(); + await promiseWithTimeout( + [closeHandler], + CONNECTION_TIMEOUT, + `Could not disconnect in ${CONNECTION_TIMEOUT}ms`, + ); + } + + async onModuleDestroy() { + await this.disconnect(); + } +} diff --git a/src/modules/transaction/dto/get-transactions-res.dto.ts b/src/modules/transaction/dto/get-transactions-res.dto.ts index b0b9f1e..277ee31 100644 --- a/src/modules/transaction/dto/get-transactions-res.dto.ts +++ b/src/modules/transaction/dto/get-transactions-res.dto.ts @@ -28,6 +28,7 @@ export class GetTransactionsResDto { signatures: string[]; sender: Account; recipient?: Account; + receivingChainID?: string; block: Block; } diff --git a/src/modules/transaction/dto/get-transactions.dto.ts b/src/modules/transaction/dto/get-transactions.dto.ts index c1300f7..7f1e150 100644 --- a/src/modules/transaction/dto/get-transactions.dto.ts +++ b/src/modules/transaction/dto/get-transactions.dto.ts @@ -40,12 +40,19 @@ export class GetTransactionDto { @IsOptional() nonce?: string; + /** + * Filter transactions by receivingChainID. + */ + @IsString() + @IsOptional() + receivingChainID?: string; + /** * Filter transactions by module and command. Format: "module:command". */ @IsString() @IsOptional() - @Matches(/^[a-z]+:[a-z]+$/, { + @Matches(/^[a-zA-Z]+:[a-zA-Z]+$/, { message: 'moduleCommand must have a format "token:transfer"', }) moduleCommand?: string; diff --git a/src/modules/transaction/transaction.controller.ts b/src/modules/transaction/transaction.controller.ts index 10ba7ca..a8a7b61 100644 --- a/src/modules/transaction/transaction.controller.ts +++ b/src/modules/transaction/transaction.controller.ts @@ -37,13 +37,13 @@ export class TransactionController { public async getTransaction( @Query() query: GetTransactionDto, ): Promise> { - // TODO: receivingChainID query and implementation const { transactionID, blockID, senderAddress, recipientAddress, nonce, + receivingChainID, address, timestamp, moduleCommand, @@ -68,6 +68,7 @@ export class TransactionController { ...(blockID && { block: { id: blockID } }), ...(senderAddress && { senderAddress }), ...(recipientAddress && { recipientAddress }), + ...(receivingChainID && { receivingChainID }), ...(module && { module }), ...(command && { command }), ...(nonce && { nonce }), @@ -142,6 +143,7 @@ export class TransactionController { signatures, senderAddress, recipientAddress, + receivingChainID, height, ...rest } = transaction; @@ -155,6 +157,7 @@ export class TransactionController { if (!sender.name) delete sender.name; if (recipient && !recipient.publicKey) delete recipient.publicKey; if (recipient && !recipient.name) delete recipient.name; + if (!receivingChainID) delete newTransaction.receivingChainID; return newTransaction; } } diff --git a/src/utils/constants.ts b/src/utils/constants.ts index e7e9067..f89dc04 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -82,3 +82,10 @@ export const KLAYR_TRANSACTION_ID_LENGTH = 64; export const KNOWN_ACCOUNTS_MAINNET_URL = 'https://static-data.klayr.xyz/known_mainnet.json'; export const KNOWN_ACCOUNTS_TESTNET_URL = 'https://static-data.klayr.xyz/known_testnet.json'; + +//////////////////////////// +/// Websocket client /// +//////////////////////////// + +export const CONNECTION_TIMEOUT = 10000; +export const RESPONSE_TIMEOUT = 10000; diff --git a/test/test-node/README.md b/test/test-node/README.md index 9ef774f..1183440 100644 --- a/test/test-node/README.md +++ b/test/test-node/README.md @@ -8,6 +8,12 @@ This project was bootstrapped with [Klayr SDK](https://github.com/klayrhq/klayr- ./bin/run start ``` +### Send tokens + +``` +./bin/run transaction:create token transfer 10000000 --params='{"tokenID": "0400000000000000", "amount": "100000000", "recipientAddress": "kly4mba244me87reyg9fegcy2cesdfw6gq9r8we5x", "data": ""}' --json --pretty +``` + ### Add a new module ```