From b229e705047971c0ac3367d7b33b424535a28424 Mon Sep 17 00:00:00 2001 From: nitowa Date: Thu, 4 May 2023 13:06:28 +0200 Subject: [PATCH] sending payment always logs errors to find possible failure causes --- package.json | 2 +- src/util/types.ts | 7 +-- src/xrpIO/xrpl-binding.ts | 96 +++++++++++++++++++++++++++------------ test/primitives.ts | 15 ++++++ 4 files changed, 86 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index bd45260..098f7ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xrpio", - "version": "0.2.1", + "version": "0.2.2", "repository": { "type": "git", "url": "https://gitea.nitowa.xyz/npm-packages/xrpio.git" diff --git a/src/util/types.ts b/src/util/types.ts index c6a9295..95c67e7 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -17,13 +17,14 @@ export type Options = { debug?: boolean connectionTimeout?: number readMaxRetry?: number - readRetryTimeout?: number + readRetryTimeout?: number, + readFreshApi?:boolean, } export const defaultOptions = { debug: false, connectionTimeout: 100000, readFreshApi: true, - readMaxRetry: -1, - readRetryTimeout: 1000 + readMaxRetry: 50, + readRetryTimeout: 750 } \ No newline at end of file diff --git a/src/xrpIO/xrpl-binding.ts b/src/xrpIO/xrpl-binding.ts index cbe7259..ffe7b89 100644 --- a/src/xrpIO/xrpl-binding.ts +++ b/src/xrpIO/xrpl-binding.ts @@ -1,5 +1,5 @@ import { defaultOptions, Memo, Options } from '../util/types' -import { Client, Payment, TxResponse, Wallet } from 'xrpl' +import { Client, Payment, RippledError, TxResponse, Wallet } from 'xrpl' import * as zlib from 'zlib' import * as util from 'util' @@ -29,6 +29,7 @@ export class xrpIO { this.options.connectionTimeout = options.connectionTimeout ? Number(options.connectionTimeout) : defaultOptions.connectionTimeout this.options.readMaxRetry = options.readMaxRetry ? Number(options.readMaxRetry) : defaultOptions.readMaxRetry this.options.readRetryTimeout = options.readRetryTimeout ? Number(options.readRetryTimeout) : defaultOptions.readRetryTimeout + this.options.readFreshApi = options.readFreshApi ? Boolean(options.readFreshApi) : defaultOptions.readFreshApi this.api = new Client(server, { connectionTimeout: this.options.connectionTimeout @@ -67,36 +68,36 @@ export class xrpIO { } } - private async sendPayment(data: Memo, to: string, secret: string, sequence?: number, amount: string = "1"): Promise { + public async sendPayment(data: Memo, to: string, secret: string, sequence?: number, amount: string = "1"): Promise { const wallet = Wallet.fromSecret(secret) this.dbg("Sending payment", wallet.address, '->', to) const _api = await this.cloneApi() + const payment: Payment = await _api.autofill({ + TransactionType: 'Payment', + Account: wallet.address, + Destination: to, + Sequence: sequence, + Amount: amount, + Memos: [{ + Memo: { + MemoData: hexEncode(data.data || ""), + MemoFormat: hexEncode(data.format || ""), + MemoType: hexEncode(data.type || "") + } + }] + }) + try { - const payment: Payment = await _api.autofill({ - TransactionType: 'Payment', - Account: wallet.address, - Destination: to, - Sequence: sequence, - Amount: amount, - Memos: [{ - Memo: { - MemoData: hexEncode(data.data || ""), - MemoFormat: hexEncode(data.format || ""), - MemoType: hexEncode(data.type || "") - } - }] - }) - - const response = await _api.submitAndWait(payment, { wallet }) - await _api.disconnect() this.dbg("Tx finalized", response.result.hash, response.result.Sequence) return response } catch (error: any) { this.dbg("SENDPAYMENT ERROR", error) - await _api.disconnect() + console.log("SENDPAYMENT ERROR", error) throw error + }finally{ + await _api.disconnect() } } @@ -106,22 +107,57 @@ export class xrpIO { return tx.result.hash } - private async getTransaction(hash: string, retry = 0): Promise { + public async getTransaction(hash: string, retry = 0): Promise { + if (!NON_ZERO_TX_HASH.test(hash)) { + throw ERR_BAD_TX_HASH(hash) + } + this.dbg("Getting Tx", hash) + + const _api = this.options.readFreshApi ? await this.cloneApi() : this.api + try { - return await this.api.request({ + return await _api.request({ command: 'tx', transaction: hash, }) - } catch (e) { - this.dbg(e) - if (this.options.readMaxRetry != -1) { - if (retry >= this.options.readMaxRetry) - console.error("Retry limit exceeded for", hash, ". This is an irrecoverable error") - throw e + } catch (e: any) { + this.dbg("getTransaction err", e) + + if(e.data){ //RippledError + switch(e.data.error){ + //irrecoverable errors + case 'amendmentBlocked': //server is amendment blocked and needs to be updated to the latest version to stay synced with the XRP Ledger network. + case 'invalid_API_version': //The server does not support the API version number from the request. + case 'jsonInvalid': //(WebSocket only) The request is not a proper JSON object. + case 'missingCommand': //(WebSocket only) The request did not specify a command field + case 'noClosed': //The server does not have a closed ledger, typically because it has not finished starting up. + case 'txnNotFound': //Either the transaction does not exist, or it was part of an ledger version that rippled does not have available + case 'unknownCmd': //The request does not contain a command that the rippled server recognizes + case 'wsTextRequired': //(WebSocket only) The request's opcode is not text. + case 'invalidParams': //One or more fields are specified incorrectly, or one or more required fields are missing. + case 'excessiveLgrRange': //The min_ledger and max_ledger fields of the request are more than 1000 apart + case 'invalidLgrRange': //The specified min_ledger is larger than the max_ledger, or one of those parameters is not a valid ledger index + throw e + + //potentially recoverable errors + case 'failedToForward': //(Reporting Mode servers only) The server tried to forward this request to a P2P Mode server, but the connection failed + case 'noCurrent': //The server does not know what the current ledger is, due to high load, network problems, validator failures, incorrect configuration, or some other problem. + case 'noNetwork': //The server is having trouble connecting to the rest of the XRP Ledger peer-to-peer network (and is not running in stand-alone mode). + case 'tooBusy': //The server is under too much load to do this command right now. Generally not returned if you are connected as an admin + default: //some undocumented error, might as well give it a re-try + //fall through + } } - await new Promise(res => setTimeout(res, this.options.readRetryTimeout)) - return await this.getTransaction(hash, retry + 1) + //some other error, potentially recoverable + if (this.options.readMaxRetry != -1 && retry >= this.options.readMaxRetry) { //not doing infinite retries and exhausted retry quota + throw e + }else{ + await new Promise(res => setTimeout(res, this.options.readRetryTimeout)) + return await this.getTransaction(hash, retry + 1) + } + }finally{ + if(this.options.readFreshApi) await _api.disconnect() } } diff --git a/test/primitives.ts b/test/primitives.ts index 46723c2..b6702c3 100644 --- a/test/primitives.ts +++ b/test/primitives.ts @@ -35,6 +35,19 @@ describe('XRPIO', () => { } }) + it('getTransaction with bad hash', function(done){ + this.timeout(10000) + api.getTransaction('73FECDA37ABBB2FC17460C5C2467BE6A0A8E1F4EB081FFFFFFFFFFFFFFFFFFFF') //technically this hash could exist, but probably never will + .then(_ => done(new Error('Expected error but succeeded'))) + .catch(_ => done()) + }) + + it('sendPayment errors on bad request sequence', function(done){ + api.sendPayment({}, receiveWallet.address, sendWallet.secret, -12) + .then(_ => done(new Error('Expected error but succeeded'))) + .catch(_ => done()) + }) + it('getAccountSequence', async function(){ this.timeout(10000) const seq = await api.getAccountSequence(sendWallet.address) @@ -50,6 +63,8 @@ describe('XRPIO', () => { expect(cost).to.be.greaterThan(30) }) + + let txHash it('writeRaw', async function(){ this.timeout(15000)