diff --git a/package.json b/package.json index d16a440..d927e46 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xrpio", - "version": "0.1.8", + "version": "0.2.0", "repository": { "type": "git", "url": "https://gitea.nitowa.xyz/npm-packages/xrpio.git" diff --git a/src/util/errors.ts b/src/util/errors.ts new file mode 100644 index 0000000..9597ec1 --- /dev/null +++ b/src/util/errors.ts @@ -0,0 +1 @@ +export const ERR_BAD_TX_HASH = (hash:string) => new Error(`Bad tx hash format: "${hash}"`) \ No newline at end of file diff --git a/src/xrpIO/xrpl-binding.ts b/src/xrpIO/xrpl-binding.ts index eb642a1..b55a3d8 100644 --- a/src/xrpIO/xrpl-binding.ts +++ b/src/xrpIO/xrpl-binding.ts @@ -3,14 +3,18 @@ import { Client, Payment, TxResponse, Wallet } from 'xrpl' import * as zlib from 'zlib' import * as util from 'util' +import { NON_ZERO_TX_HASH } from '../util/protocol.constants' +import { ERR_BAD_TX_HASH } from '../util/errors' 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 hexDecode = (str: string) => Buffer.from(str, 'hex').toString('utf8') const hexEncode = (str: string) => Buffer.from(str, 'utf8').toString('hex').toUpperCase() const chunkString = (str: string, length: number) => str.match(new RegExp('.{1,' + length + '}', 'gs')); +const genRandHex = size => [...Array(size)].map(() => Math.floor(Math.random() * 16).toString(16)).join(''); const PAYLOAD_SIZE = 925 - +const XRP_PER_DROP = 0.000001 +const DROP_PER_XRP = 1000000 export class xrpIO { @@ -63,7 +67,7 @@ export class xrpIO { } } - private async sendPayment(data: Memo, to: string, secret: string, sequence?: number): Promise { + private 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) @@ -74,7 +78,7 @@ export class xrpIO { Account: wallet.address, Destination: to, Sequence: sequence, - Amount: "1", + Amount: amount, Memos: [{ Memo: { MemoData: hexEncode(data.data || ""), @@ -96,32 +100,37 @@ export class xrpIO { } } - public async writeRaw(data: Memo, to: string, secret: string, sequence?: number): Promise { + public async writeRaw(data: Memo, to: string, secret: string, sequence?: number, amount: string = "1"): Promise { this.dbg("Writing data", data) - const tx = await this.sendPayment(data, to, secret, sequence) + const tx = await this.sendPayment(data, to, secret, sequence, amount) return tx.result.hash } - private async getTransaction(hash: string, retry=0): Promise { + private async getTransaction(hash: string, retry = 0): Promise { this.dbg("Getting Tx", hash) - try{ + try { return await this.api.request({ command: 'tx', transaction: hash, }) - }catch(e){ + } 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 + if (this.options.readMaxRetry != -1) { + if (retry >= this.options.readMaxRetry) + console.error("Retry limit exceeded for", hash, ". This is an irrecoverable error") + throw e } await new Promise(res => setTimeout(res, this.options.readRetryTimeout)) - return await this.getTransaction(hash, retry+1) + return await this.getTransaction(hash, retry + 1) } } public async readRaw(hash: string): Promise { + + if (!NON_ZERO_TX_HASH.test(hash)) { + throw ERR_BAD_TX_HASH(hash) + } + const tx = await this.getTransaction(hash) const memo = tx.result.Memos[0].Memo const memoParsed = { @@ -148,6 +157,10 @@ export class xrpIO { } public async treeRead(hashes: string[]): Promise { + const bad_hash = hashes.find(hash => !NON_ZERO_TX_HASH.test(hash)) + if (bad_hash) + throw ERR_BAD_TX_HASH(bad_hash) + const memos = await Promise.all(hashes.map(hash => this.readRaw(hash))) const payload: string = await decompressB64(memos.map(memo => memo.data).join('')) @@ -169,6 +182,25 @@ export class xrpIO { return Number(accountInfo.result.account_data.Sequence) } + public async estimateFee(data: string, denomination: 'XRP' | 'DROPS' = 'DROPS', cost = 0): Promise { + data = await compressB64(data) + const chunks = chunkString(data, PAYLOAD_SIZE) + + if (chunks.length === 1) { + return (denomination === "DROPS" ? (cost + 1) : this.dropsToXrp(cost + 1)) + } + + return this.estimateFee(JSON.stringify(chunks.map(_ => genRandHex(64))), denomination, cost + chunks.length) + } + + public xrpToDrops(xrp: number): number { + return xrp * DROP_PER_XRP + } + + public dropsToXrp(drops: number): number { + return drops * XRP_PER_DROP + } + private dbg(...args: any[]) { if (this.options.debug) { console.log.apply(console, args) diff --git a/test/primitives.ts b/test/primitives.ts index 1a5973e..b1ace18 100644 --- a/test/primitives.ts +++ b/test/primitives.ts @@ -1,8 +1,8 @@ +var wtf = require('wtfnode'); import { htmlTxt, longText, makeTestnetWallet, TEST_CONFIG, TEST_DATA } from './CONSTANTS' import { xrpIO } from '../src/xrpIO/xrpl-binding' import * as chai from 'chai'; import { Wallet } from '../src/util/types'; -var wtf = require('wtfnode'); const expect = chai.expect let sendWallet: Wallet @@ -36,16 +36,24 @@ describe('XRPIO', () => { }) it('getAccountSequence', async function(){ -// this.skip() + //this.skip() this.timeout(10000) const seq = await api.getAccountSequence(sendWallet.address) expect(seq).to.exist expect(seq).to.be.a('number') }) + it('estimateFee', async function () { + this.timeout(2000) + const cost = await api.estimateFee(longText) + expect(cost).to.be.a('number') + expect(cost).to.be.lessThan(50) + expect(cost).to.be.greaterThan(30) + }) + let txHash it('writeRaw', async function(){ -// this.skip() + //this.skip() this.timeout(15000) txHash = await api.writeRaw({data: TEST_DATA}, receiveWallet.address, sendWallet.secret); expect(txHash).to.exist @@ -53,13 +61,20 @@ describe('XRPIO', () => { }) it('readRaw', async function () { -// this.skip() + //this.skip() this.timeout(15000) const memo = await api.readRaw(txHash) expect(memo).to.exist expect(memo.data).to.be.equal(TEST_DATA) }) + it('readRaw bad hash', function (done){ + this.timeout(150000) + api.readRaw("123") + .then(_ => done(new Error('Expected error but succeeded'))) + .catch(_ => done()) + }) + it('treeWrite', async function(){ // this.skip() this.timeout(45000) @@ -77,7 +92,7 @@ describe('XRPIO', () => { }) it('treeWrite XL', async function(){ -// this.skip() + //this.skip() this.timeout(450000) txHash = await api.treeWrite(htmlTxt, receiveWallet.address, sendWallet.secret) expect(txHash).to.exist @@ -85,7 +100,7 @@ describe('XRPIO', () => { }) it('treeRead XL', async function(){ -// this.skip() + //this.skip() this.timeout(450000) const data = await api.treeRead([txHash]) expect(data).to.exist @@ -93,7 +108,8 @@ describe('XRPIO', () => { }) it('print open handles', function(){ - wtf.dump() - console.log(txHash) + api.disconnect().then(_ => { + wtf.dump() + }) }) }) \ No newline at end of file