Added several improvements to failed read/write recovery strategies
This commit is contained in:
@@ -1,6 +1,3 @@
|
|||||||
//export * from './xrpIO/ripple-binding'
|
|
||||||
export * from './util/types'
|
export * from './util/types'
|
||||||
export * from './util/protocol.constants'
|
export * from './util/protocol.constants'
|
||||||
//export * from 'ripple-lib'
|
|
||||||
//export { RippleAPI } from 'ripple-lib'
|
|
||||||
export * from './xrpIO/xrpl-binding'
|
export * from './xrpIO/xrpl-binding'
|
||||||
+11
-2
@@ -1,2 +1,11 @@
|
|||||||
export const ERR_BAD_TX_HASH = (hash:string) => new Error(`Bad tx hash format: "${hash}"`)
|
export class BadTxHashError extends Error{
|
||||||
export const ERR_NO_VERIFY_OWNER = (hash: string, actualAccount: string, desiredAccount: string) => new Error(`Expected tx "${hash}" to be initiated by ${desiredAccount} but was ${actualAccount}`)
|
constructor(hash:string){
|
||||||
|
super(`Bad tx hash format: "${hash}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CannotVerifyOwnerError extends Error{
|
||||||
|
constructor(hash: string, actualAccount: string, desiredAccount: string){
|
||||||
|
super((`Expected tx "${hash}" to be initiated by ${desiredAccount} but was ${actualAccount}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-2
@@ -19,12 +19,16 @@ export type Options = {
|
|||||||
readMaxRetry?: number
|
readMaxRetry?: number
|
||||||
readRetryTimeout?: number,
|
readRetryTimeout?: number,
|
||||||
readFreshApi?:boolean,
|
readFreshApi?:boolean,
|
||||||
|
writeMaxRetry?: number
|
||||||
|
writeRetryTimeout?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultOptions = {
|
export const defaultOptions: Options = {
|
||||||
debug: false,
|
debug: false,
|
||||||
connectionTimeout: 100000,
|
connectionTimeout: 100000,
|
||||||
readFreshApi: true,
|
readFreshApi: true,
|
||||||
readMaxRetry: 50,
|
readMaxRetry: 50,
|
||||||
readRetryTimeout: 750
|
readRetryTimeout: 750,
|
||||||
|
writeMaxRetry: 10,
|
||||||
|
writeRetryTimeout: 10000
|
||||||
}
|
}
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
import { Memo, Wallet } from '../util/types'
|
|
||||||
import { MIN_XRP_FEE, MIN_XRP_TX_VALUE } from '../util/protocol.constants';
|
|
||||||
|
|
||||||
import { Instructions, LedgerClosedEvent, RippleAPI } from 'ripple-lib'
|
|
||||||
import { Payment } from 'ripple-lib/dist/npm/transaction/payment';
|
|
||||||
|
|
||||||
import * as zlib from 'zlib'
|
|
||||||
import * as util from 'util'
|
|
||||||
|
|
||||||
const chunkString = (str: string, length: number) => str.match(new RegExp('.{1,' + length + '}', 'gs'));
|
|
||||||
const PAYLOAD_SIZE = 925
|
|
||||||
const debug = false
|
|
||||||
|
|
||||||
const cloneApi = async (api: RippleAPI): Promise<RippleAPI> => {
|
|
||||||
try {
|
|
||||||
const subApi = new RippleAPI({ server: api.connection['_url'] })
|
|
||||||
await subApi.connect()
|
|
||||||
return subApi
|
|
||||||
} catch (e) {
|
|
||||||
if (debug) {
|
|
||||||
console.log("CLONEAPI ERR", e)
|
|
||||||
}
|
|
||||||
return await cloneApi(api)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getLatestSequence = async (api: RippleAPI, accountAddress: string): Promise<number> => {
|
|
||||||
if (debug) console.log("Getting acc info for", accountAddress)
|
|
||||||
const accountInfo = await api.getAccountInfo(accountAddress, {})
|
|
||||||
return Number(accountInfo.sequence - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const compressB64 = async (data: string) => (await util.promisify(zlib.deflate)(Buffer.from(data, 'utf-8'))).toString('base64')
|
|
||||||
const decompressB64 = async (data: string) => (await util.promisify(zlib.inflate)(Buffer.from(data, 'base64'))).toString('utf-8')
|
|
||||||
|
|
||||||
const sendReliably = (api: RippleAPI, signed: any, preparedPayment,): Promise<any> => new Promise((res, rej) => {
|
|
||||||
const ledgerClosedCallback = async (event: LedgerClosedEvent) => {
|
|
||||||
let status
|
|
||||||
try {
|
|
||||||
status = await api.getTransaction(signed.id, {
|
|
||||||
minLedgerVersion: 25235454
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
|
|
||||||
// Typical error when the tx hasn't been validated yet:
|
|
||||||
if ((e as Error).name !== 'MissingLedgerHistoryError') {
|
|
||||||
//console.log(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.ledger_index > preparedPayment.instructions.maxLedgerVersion + 3) {
|
|
||||||
// Assumptions:
|
|
||||||
// - We are still connected to the same rippled server
|
|
||||||
// - No ledger gaps occurred
|
|
||||||
// - All ledgers between the time we submitted the tx and now have been checked for the tx
|
|
||||||
status = {
|
|
||||||
finalResult: 'Transaction was not, and never will be, included in a validated ledger'
|
|
||||||
}
|
|
||||||
return rej(status);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Check again later:
|
|
||||||
api.connection.once('ledgerClosed', ledgerClosedCallback)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res(status)
|
|
||||||
}
|
|
||||||
api.connection.once('ledgerClosed', ledgerClosedCallback)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const sendPayment = async (api: RippleAPI, data: Memo[], from: string, to: string, secret: string, sequence: number) => {
|
|
||||||
if (debug) console.log("Sending payment with seq", sequence)
|
|
||||||
const options: Instructions = {
|
|
||||||
maxLedgerVersionOffset: 5,
|
|
||||||
fee: MIN_XRP_FEE,
|
|
||||||
sequence: sequence,
|
|
||||||
};
|
|
||||||
const payment: Payment = {
|
|
||||||
source: {
|
|
||||||
address: from,
|
|
||||||
maxAmount: {
|
|
||||||
value: MIN_XRP_TX_VALUE,
|
|
||||||
currency: 'XRP'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
destination: {
|
|
||||||
address: to,
|
|
||||||
amount: {
|
|
||||||
value: MIN_XRP_TX_VALUE,
|
|
||||||
currency: 'XRP'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
memos: data,
|
|
||||||
};
|
|
||||||
|
|
||||||
const _api = await cloneApi(api)
|
|
||||||
try {
|
|
||||||
const prepared = await _api.preparePayment(from, payment, options)
|
|
||||||
const signed = _api.sign(prepared.txJSON, secret);
|
|
||||||
const txHash = await _api.submit(signed.signedTransaction)
|
|
||||||
//if(debug) console.log("Transaction submitted", txHash)
|
|
||||||
await sendReliably(_api, signed, prepared)
|
|
||||||
|
|
||||||
return txHash
|
|
||||||
} catch (error) {
|
|
||||||
if (debug)
|
|
||||||
console.log("SENDPAYMENT ERROR", error)
|
|
||||||
throw error
|
|
||||||
} finally {
|
|
||||||
_api.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getTransactions = async (api: RippleAPI, address: string, minLedgerVersion: number = 25235454): Promise<any[]> => {
|
|
||||||
const txs = await api.getTransactions(address, {
|
|
||||||
minLedgerVersion: minLedgerVersion,
|
|
||||||
earliestFirst: true,
|
|
||||||
excludeFailures: true,
|
|
||||||
})
|
|
||||||
return txs
|
|
||||||
}
|
|
||||||
|
|
||||||
export const writeRaw = async (api: RippleAPI, data: Memo, from:
|
|
||||||
string, to: string, secret: string, sequence?: number): Promise<string> => {
|
|
||||||
//if (memoSize(data) > 1000) throw new Error("data length exceeds capacity")
|
|
||||||
try {
|
|
||||||
if (!sequence) {
|
|
||||||
const accountInfo = await getLatestSequence(api, from)
|
|
||||||
sequence = accountInfo + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
const resp = await sendPayment(api, [data], from, to, secret, sequence)
|
|
||||||
return resp['tx_json'].hash
|
|
||||||
} catch (error) {
|
|
||||||
if (debug) {
|
|
||||||
console.log("WRITERAW ERR", error);
|
|
||||||
}
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const readRaw = async (api: RippleAPI, hash: string): Promise<Memo> => {
|
|
||||||
api = await cloneApi(api)
|
|
||||||
let tx
|
|
||||||
try {
|
|
||||||
tx = await api.getTransaction(hash, {
|
|
||||||
minLedgerVersion: 25235454
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
// if(debug){
|
|
||||||
console.log("READRAW ERR", e)
|
|
||||||
api.isConnected
|
|
||||||
// }
|
|
||||||
throw e
|
|
||||||
} finally {
|
|
||||||
await api.disconnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tx || !tx.specification || !tx.specification['memos'] || !tx.specification['memos'][0]) {
|
|
||||||
console.log(tx)
|
|
||||||
throw new Error('Invalid Transaction ' + hash)
|
|
||||||
}
|
|
||||||
return tx.specification['memos'][0]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const subscribe = async (api: RippleAPI, address: string, callback: (tx) => any) => {
|
|
||||||
api.connection.on('transaction', (tx) => callback(tx))
|
|
||||||
await api.connection.request({
|
|
||||||
command: 'subscribe',
|
|
||||||
accounts: [address],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const treeWrite = async (api: RippleAPI, data: string, wallet: Wallet, to: string, format: 'L' | 'N' = 'L'): Promise<string> => {
|
|
||||||
data = await compressB64(data)
|
|
||||||
const chunks = chunkString(data, PAYLOAD_SIZE)
|
|
||||||
const latestSequence = await getLatestSequence(api, wallet.address)
|
|
||||||
const hashes = await Promise.all(Object.entries(chunks).map(([i, chunk]) => writeRaw(api, { data: chunk, format: format }, wallet.address, to, wallet.secret, latestSequence + Number(i) + 1)))
|
|
||||||
|
|
||||||
if (hashes.length === 1) {
|
|
||||||
return hashes[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
return await treeWrite(api, JSON.stringify(hashes), wallet, to, 'N')
|
|
||||||
}
|
|
||||||
|
|
||||||
export const treeRead = async (api: RippleAPI, hashes: string[]): Promise<string> => {
|
|
||||||
const memos = await Promise.all(hashes.map(hash => readRaw(api, hash)))
|
|
||||||
const payload: string = await decompressB64(memos.map(memo => memo.data).join(''))
|
|
||||||
|
|
||||||
if (memos.some(memo => memo.format === 'N')) {
|
|
||||||
return await treeRead(api, JSON.parse(payload))
|
|
||||||
}
|
|
||||||
|
|
||||||
return payload
|
|
||||||
}
|
|
||||||
+31
-11
@@ -1,10 +1,10 @@
|
|||||||
import { defaultOptions, Memo, Options } from '../util/types'
|
import { defaultOptions, Memo, Options } from '../util/types'
|
||||||
import { Client, Payment, RippledError, TxResponse, Wallet } from 'xrpl'
|
import { Client, DisconnectedError, Payment, TxResponse, Wallet, } from 'xrpl'
|
||||||
|
|
||||||
import * as zlib from 'zlib'
|
import * as zlib from 'zlib'
|
||||||
import * as util from 'util'
|
import * as util from 'util'
|
||||||
import { NON_ZERO_TX_HASH } from '../util/protocol.constants'
|
import { NON_ZERO_TX_HASH } from '../util/protocol.constants'
|
||||||
import { ERR_BAD_TX_HASH, ERR_NO_VERIFY_OWNER } from '../util/errors'
|
import { BadTxHashError, CannotVerifyOwnerError } from '../util/errors'
|
||||||
|
|
||||||
const compressB64 = async (data: string) => (await util.promisify(zlib.deflate)(Buffer.from(data, 'utf-8'))).toString('base64')
|
const compressB64 = async (data: string) => (await util.promisify(zlib.deflate)(Buffer.from(data, 'utf-8'))).toString('base64')
|
||||||
const decompressB64 = async (data: string) => (await util.promisify(zlib.inflate)(Buffer.from(data, 'base64'))).toString('utf-8')
|
const decompressB64 = async (data: string) => (await util.promisify(zlib.inflate)(Buffer.from(data, 'base64'))).toString('utf-8')
|
||||||
@@ -30,6 +30,8 @@ export class xrpIO {
|
|||||||
this.options.readMaxRetry = options.readMaxRetry ? Number(options.readMaxRetry) : defaultOptions.readMaxRetry
|
this.options.readMaxRetry = options.readMaxRetry ? Number(options.readMaxRetry) : defaultOptions.readMaxRetry
|
||||||
this.options.readRetryTimeout = options.readRetryTimeout ? Number(options.readRetryTimeout) : defaultOptions.readRetryTimeout
|
this.options.readRetryTimeout = options.readRetryTimeout ? Number(options.readRetryTimeout) : defaultOptions.readRetryTimeout
|
||||||
this.options.readFreshApi = options.readFreshApi ? Boolean(options.readFreshApi) : defaultOptions.readFreshApi
|
this.options.readFreshApi = options.readFreshApi ? Boolean(options.readFreshApi) : defaultOptions.readFreshApi
|
||||||
|
this.options.writeMaxRetry = options.writeMaxRetry ? Number(options.readFreshApi) : defaultOptions.writeMaxRetry
|
||||||
|
this.options.writeRetryTimeout = options.writeRetryTimeout ? Number(options.writeRetryTimeout) : defaultOptions.writeRetryTimeout
|
||||||
|
|
||||||
this.api = new Client(server, {
|
this.api = new Client(server, {
|
||||||
connectionTimeout: this.options.connectionTimeout
|
connectionTimeout: this.options.connectionTimeout
|
||||||
@@ -68,7 +70,7 @@ export class xrpIO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async sendPayment(data: Memo, to: string, secret: string, sequence?: number, amount: string = "1"): Promise<TxResponse> {
|
public async sendPayment(data: Memo, to: string, secret: string, sequence?: number, amount: string = "1", retry = 0): Promise<TxResponse> {
|
||||||
const wallet = Wallet.fromSecret(secret)
|
const wallet = Wallet.fromSecret(secret)
|
||||||
this.dbg("Sending payment", wallet.address, '->', to)
|
this.dbg("Sending payment", wallet.address, '->', to)
|
||||||
|
|
||||||
@@ -88,17 +90,35 @@ export class xrpIO {
|
|||||||
}]
|
}]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let response: TxResponse;
|
||||||
try {
|
try {
|
||||||
const response = await _api.submitAndWait(payment, { wallet })
|
response = await _api.submitAndWait(payment, { wallet })
|
||||||
this.dbg("Tx finalized", response.result.hash, response.result.Sequence)
|
|
||||||
return response
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.dbg("SENDPAYMENT ERROR", error)
|
this.dbg("SENDPAYMENT ERROR", error)
|
||||||
console.log("SENDPAYMENT ERROR", error)
|
|
||||||
|
if(error instanceof DisconnectedError){
|
||||||
|
//Usually caused by hitting rate limits. Recoverable.
|
||||||
|
if(this.options.writeMaxRetry != -1 && retry >= this.options.writeMaxRetry){
|
||||||
|
//exceeded retry quota
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
await new Promise(res => setTimeout(res, this.options.writeRetryTimeout))
|
||||||
|
return await this.sendPayment(data, to, secret, sequence, amount, retry+1)
|
||||||
|
}
|
||||||
|
|
||||||
throw error
|
throw error
|
||||||
}finally{
|
}finally{
|
||||||
await _api.disconnect()
|
await _api.disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//We cannot afford this payment. Irrecoverable error
|
||||||
|
if(response.result.meta && response.result.meta['TransactionResult'] && response.result.meta['TransactionResult'] === 'tecUNFUNDED_PAYMENT'){
|
||||||
|
throw new Error(`Insufficient funds to send transaction. See tx ${response.result.hash}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dbg("Tx finalized", response.result.hash, response.result.Sequence)
|
||||||
|
return response
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async writeRaw(data: Memo, to: string, secret: string, sequence?: number, amount: string = "1"): Promise<string> {
|
public async writeRaw(data: Memo, to: string, secret: string, sequence?: number, amount: string = "1"): Promise<string> {
|
||||||
@@ -109,7 +129,7 @@ export class xrpIO {
|
|||||||
|
|
||||||
public async getTransaction(hash: string, retry = 0): Promise<TxResponse> {
|
public async getTransaction(hash: string, retry = 0): Promise<TxResponse> {
|
||||||
if (!NON_ZERO_TX_HASH.test(hash)) {
|
if (!NON_ZERO_TX_HASH.test(hash)) {
|
||||||
throw ERR_BAD_TX_HASH(hash)
|
throw new BadTxHashError(hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dbg("Getting Tx", hash)
|
this.dbg("Getting Tx", hash)
|
||||||
@@ -163,12 +183,12 @@ export class xrpIO {
|
|||||||
|
|
||||||
public async readRaw(hash: string, verifyOwner?: string): Promise<Memo> {
|
public async readRaw(hash: string, verifyOwner?: string): Promise<Memo> {
|
||||||
if (!NON_ZERO_TX_HASH.test(hash)) {
|
if (!NON_ZERO_TX_HASH.test(hash)) {
|
||||||
throw ERR_BAD_TX_HASH(hash)
|
throw new BadTxHashError(hash)
|
||||||
}
|
}
|
||||||
const tx = await this.getTransaction(hash)
|
const tx = await this.getTransaction(hash)
|
||||||
|
|
||||||
if(verifyOwner && tx.result.Account != verifyOwner){
|
if(verifyOwner && tx.result.Account != verifyOwner){
|
||||||
throw ERR_NO_VERIFY_OWNER(hash, tx.result.Account, verifyOwner)
|
throw new CannotVerifyOwnerError(hash, tx.result.Account, verifyOwner)
|
||||||
}
|
}
|
||||||
|
|
||||||
const memo = tx.result.Memos[0].Memo
|
const memo = tx.result.Memos[0].Memo
|
||||||
@@ -198,7 +218,7 @@ export class xrpIO {
|
|||||||
public async treeRead(hashes: string[], verifyOwner?:string): Promise<string> {
|
public async treeRead(hashes: string[], verifyOwner?:string): Promise<string> {
|
||||||
const bad_hash = hashes.find(hash => !NON_ZERO_TX_HASH.test(hash))
|
const bad_hash = hashes.find(hash => !NON_ZERO_TX_HASH.test(hash))
|
||||||
if (bad_hash)
|
if (bad_hash)
|
||||||
throw ERR_BAD_TX_HASH(bad_hash)
|
throw new BadTxHashError(bad_hash)
|
||||||
|
|
||||||
const memos = await Promise.all(hashes.map(hash => this.readRaw(hash, verifyOwner)))
|
const memos = await Promise.all(hashes.map(hash => this.readRaw(hash, verifyOwner)))
|
||||||
const payload: string = await decompressB64(memos.map(memo => memo.data).join(''))
|
const payload: string = await decompressB64(memos.map(memo => memo.data).join(''))
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const expect = chai.expect
|
|||||||
|
|
||||||
let sendWallet: Wallet
|
let sendWallet: Wallet
|
||||||
let receiveWallet: Wallet
|
let receiveWallet: Wallet
|
||||||
|
let poorWallet: Wallet
|
||||||
let api: xrpIO
|
let api: xrpIO
|
||||||
|
|
||||||
describe('XRPIO', () => {
|
describe('XRPIO', () => {
|
||||||
@@ -14,6 +15,7 @@ describe('XRPIO', () => {
|
|||||||
this.timeout(15000)
|
this.timeout(15000)
|
||||||
sendWallet = await makeTestnetWallet()
|
sendWallet = await makeTestnetWallet()
|
||||||
receiveWallet = await makeTestnetWallet()
|
receiveWallet = await makeTestnetWallet()
|
||||||
|
poorWallet = await makeTestnetWallet()
|
||||||
await new Promise((res, rej) => setTimeout(res, 10000)) //it takes a moment for the wallets to become active
|
await new Promise((res, rej) => setTimeout(res, 10000)) //it takes a moment for the wallets to become active
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -35,6 +37,15 @@ describe('XRPIO', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
it('throws error if not enough funds', function(done){
|
||||||
|
this.timeout(15000)
|
||||||
|
console.log(poorWallet)
|
||||||
|
api.sendPayment({}, receiveWallet.address, poorWallet.secret, undefined, String(1000000 * 10001))
|
||||||
|
.then(_ => done(new Error('Expected error but succeeded')))
|
||||||
|
.catch(_ => done())
|
||||||
|
})
|
||||||
|
|
||||||
it('getTransaction with bad hash', function(done){
|
it('getTransaction with bad hash', function(done){
|
||||||
this.timeout(10000)
|
this.timeout(10000)
|
||||||
api.getTransaction('73FECDA37ABBB2FC17460C5C2467BE6A0A8E1F4EB081FFFFFFFFFFFFFFFFFFFF') //technically this hash could exist, but probably never will
|
api.getTransaction('73FECDA37ABBB2FC17460C5C2467BE6A0A8E1F4EB081FFFFFFFFFFFFFFFFFFFF') //technically this hash could exist, but probably never will
|
||||||
@@ -143,6 +154,24 @@ describe('XRPIO', () => {
|
|||||||
expect(data).to.be.equal(htmlTxt)
|
expect(data).to.be.equal(htmlTxt)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('sends even if rate limit exceeded', async function(){
|
||||||
|
this.timeout(10 * 60 * 1000) //10m
|
||||||
|
console.log("Testing if sending past rate limits correctly recovers and finishes. This may take a while.")
|
||||||
|
await api.treeWrite(htmlTxt, receiveWallet.address, sendWallet.secret)
|
||||||
|
console.log("1/7")
|
||||||
|
await api.treeWrite(htmlTxt, receiveWallet.address, sendWallet.secret)
|
||||||
|
console.log("2/7")
|
||||||
|
await api.treeWrite(htmlTxt, receiveWallet.address, sendWallet.secret)
|
||||||
|
console.log("3/7")
|
||||||
|
await api.treeWrite(htmlTxt, receiveWallet.address, sendWallet.secret)
|
||||||
|
console.log("4/7")
|
||||||
|
await api.treeWrite(htmlTxt, receiveWallet.address, sendWallet.secret)
|
||||||
|
console.log("5/7")
|
||||||
|
await api.treeWrite(htmlTxt, receiveWallet.address, sendWallet.secret)
|
||||||
|
console.log("6/7")
|
||||||
|
await api.treeWrite(htmlTxt, receiveWallet.address, sendWallet.secret)
|
||||||
|
})
|
||||||
|
|
||||||
it('print open handles', function(){
|
it('print open handles', function(){
|
||||||
api.disconnect().then(_ => {
|
api.disconnect().then(_ => {
|
||||||
wtf.dump()
|
wtf.dump()
|
||||||
|
|||||||
Reference in New Issue
Block a user