diff --git a/README.md b/README.md index 3028cdb..cfae64b 100644 --- a/README.md +++ b/README.md @@ -12,61 +12,40 @@ xrpio is a library that allows you to write and read arbitrary data in the rippl npm i xrpio ``` +# How it works + +Transactions on the ripple blockchain are allowed to carry up to 1kB of arbitrary data via the memo field. +We can use this to store data of any size by building a tree of references between these transactions that can then be reassembled by reading them back from the blockchain. +In order to generate these transactions xrpio sends payments with the minimum denomination between two wallets controlled by the user. + +xrpio automatically takes care of the logistics behind this technique as well as compression of the data. + +Highly simplified, you can visualize the process like this: + +![xrpio treewrite](https://i.imgur.com/G2HofSE.gif) + +In practice each node does of course store significantly more data. + + # Caution This library is in an early stage of development and **breaking changes may occur spontaneously and without regard of semantic versioning until the v1.0.0 release**. -## Operation on the main-net is untested and should not be used in production! +## Operation on the main-net is untested and should not be used in production! If you want to deploy this library with the main-net please download the sources and modify them to your needs.
# Quickstart ```typescript -import {RippleAPI} from 'ripple-lib' -import {treeRead, treeWrite} from 'xrpio' +import { xrpIO } from "xrpio" -const api = new RippleAPI({ server: "..." }) +const api = new xrpIO("wss://some_ripple_node.net:51233") await api.connect() -const dataRootHash = await treeWrite( - api, - "Arbitrary string data 123", - { - address: "Sender address", - secret: "Sender private key" - }, - "Receiver address" -) +txHash = await api.treeWrite("arbitrary text 123", receiveWallet.address, sendWallet.secret) +data = await api.treeRead([txHash]) -const data = await treeRead(api, [dataRootHash]) -console.log(data) //"Arbitrary string data 123" -``` +console.log(data) //"arbitrary text 123" -# A simple ready-to-run example for the testnet -```typescript -import { treeRead, treeWrite, Wallet } from 'xrpio' -import { RippleAPI } from 'ripple-lib' -import fetch from 'node-fetch' -export const makeTestnetWallet = () : Promise => fetch('https://faucet.altnet.rippletest.net/accounts', { - method: 'POST', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, -}).then((raw:any) => { - return raw.json().then(content => content.account) -}); - -(async()=>{ - const api = new RippleAPI({server: 'wss://s.altnet.rippletest.net:51233'}) - await api.connect() - const fromWallet = await makeTestnetWallet() - const toWallet = await makeTestnetWallet() - await new Promise((res, rej) => setTimeout(res, 10000)) //it takes a moment for the wallets to become active - - const rootHash = await treeWrite(api, "test123", fromWallet, toWallet.address) - const data = await treeRead(api, [rootHash]) - console.log(data) -})() ``` # [Full documentation](https://gitea.nitowa.xyz/docs/xrpio) diff --git a/package-lock.json b/package-lock.json index 5aacee7..3be7093 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "xrpio", - "version": "0.0.7", + "version": "0.0.10", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -174,6 +174,53 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bip32": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/bip32/-/bip32-2.0.6.tgz", + "integrity": "sha512-HpV5OMLLGTjSVblmrtYRfFFKuQB+GArM0+XP8HGWfJ5vxYBqo+DesvJwOdC2WJ3bCkZShGf0QIfoIpeomVzVdA==", + "requires": { + "@types/node": "10.12.18", + "bs58check": "^2.1.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "tiny-secp256k1": "^1.1.3", + "typeforce": "^1.11.5", + "wif": "^2.0.6" + }, + "dependencies": { + "@types/node": { + "version": "10.12.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.18.tgz", + "integrity": "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==" + } + } + }, + "bip39": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.4.tgz", + "integrity": "sha512-YZKQlb752TrUWqHWj7XAwCSjYEgGAk+/Aas3V7NyjQeZYsztO8JnQUaCWhcnL4T+jL8nvB8typ2jRPzTlgugNw==", + "requires": { + "@types/node": "11.11.6", + "create-hash": "^1.1.0", + "pbkdf2": "^3.0.9", + "randombytes": "^2.0.1" + }, + "dependencies": { + "@types/node": { + "version": "11.11.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", + "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==" + } + } + }, "bn.js": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", @@ -290,6 +337,24 @@ } } }, + "bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=", + "requires": { + "base-x": "^3.0.2" + } + }, + "bs58check": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", + "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", + "requires": { + "bs58": "^4.0.0", + "create-hash": "^1.1.0", + "safe-buffer": "^5.1.2" + } + }, "buffer": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", @@ -447,7 +512,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, "requires": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", @@ -653,6 +717,11 @@ "safe-buffer": "^5.1.1" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -1326,6 +1395,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "nan": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", + "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==" + }, "nanoid": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz", @@ -1445,7 +1519,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", "integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==", - "dev": true, "requires": { "create-hash": "^1.1.2", "create-hmac": "^1.1.4", @@ -1504,7 +1577,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, "requires": { "safe-buffer": "^5.1.0" } @@ -1740,6 +1812,25 @@ "has-flag": "^4.0.0" } }, + "tiny-secp256k1": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-1.1.6.tgz", + "integrity": "sha512-FmqJZGduTyvsr2cF3375fqGHUovSwDi/QytexX1Se4BPuPZpTE5Ftp5fg+EFSuEf3lhZqgCRjEG3ydUQ/aNiwA==", + "requires": { + "bindings": "^1.3.0", + "bn.js": "^4.11.8", + "create-hmac": "^1.1.7", + "elliptic": "^6.4.0", + "nan": "^2.13.2" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -1831,6 +1922,11 @@ "handlebars": "^4.7.7" } }, + "typeforce": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", + "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" + }, "typescript": { "version": "4.5.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", @@ -1941,6 +2037,14 @@ "is-typed-array": "^1.1.7" } }, + "wif": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", + "integrity": "sha1-CNP1IFbGZnkplyb63g1DKudLRwQ=", + "requires": { + "bs58check": "<3.0.0" + } + }, "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -1975,6 +2079,35 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz", "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==" }, + "wtfnode": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/wtfnode/-/wtfnode-0.9.1.tgz", + "integrity": "sha512-Ip6C2KeQPl/F3aP1EfOnPoQk14Udd9lffpoqWDNH3Xt78svxPbv53ngtmtfI0q2Te3oTq79XKTnRNXVIn/GsPA==", + "dev": true + }, + "xrpl": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/xrpl/-/xrpl-2.1.1.tgz", + "integrity": "sha512-s8qg5ll0m1WuPji+qLOO02uhk8tu12zA68vspwwabH2XH4kzzDnABxTeQQlZisjr+B1EBalmlt0yMI+Gv7fnbg==", + "requires": { + "bignumber.js": "^9.0.0", + "bip32": "^2.0.6", + "bip39": "^3.0.4", + "https-proxy-agent": "^5.0.0", + "lodash": "^4.17.4", + "ripple-address-codec": "^4.2.3", + "ripple-binary-codec": "^1.3.0", + "ripple-keypairs": "^1.1.3", + "ws": "^8.2.2" + }, + "dependencies": { + "ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==" + } + } + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 7b54f85..5c26936 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xrpio", - "version": "0.0.10", + "version": "0.1.0", "repository": { "type": "git", "url": "https://gitea.nitowa.xyz/npm-packages/xrpio.git" @@ -28,7 +28,8 @@ "author": "nitowa", "license": "MIT", "dependencies": { - "ripple-lib": "^1.10.0" + "ripple-lib": "^1.10.0", + "xrpl": "^2.1.1" }, "devDependencies": { "@types/chai": "^4.2.21", @@ -45,6 +46,7 @@ "typedoc": "^0.22.11", "typedoc-plugin-markdown": "^3.11.12", "typescript": "^4.5.0", - "utf8": "^3.0.0" + "utf8": "^3.0.0", + "wtfnode": "^0.9.1" } } diff --git a/src/util/types.ts b/src/util/types.ts index faaf554..a447ad6 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -13,3 +13,7 @@ export type Secret = string export type PublicKey = string export type Amount = number export type TxHash = string +export type Options = { + debug: boolean, + connectionTimeout: number +} \ No newline at end of file diff --git a/src/xrpIO/ripple-binding.ts b/src/xrpIO/ripple-binding.ts index 675946c..50002b8 100644 --- a/src/xrpIO/ripple-binding.ts +++ b/src/xrpIO/ripple-binding.ts @@ -150,6 +150,7 @@ export const readRaw = async (api: RippleAPI, hash: string): Promise => { } catch (e) { // if(debug){ console.log("READRAW ERR", e) + api.isConnected // } throw e } finally { diff --git a/src/xrpIO/xrpl-binding.ts b/src/xrpIO/xrpl-binding.ts new file mode 100644 index 0000000..f275d5a --- /dev/null +++ b/src/xrpIO/xrpl-binding.ts @@ -0,0 +1,175 @@ +import { Memo, Options } from '../util/types' +import { Client, Payment, TxResponse, Wallet } from 'xrpl' + +import * as zlib from 'zlib' +import * as util from 'util' + +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 PAYLOAD_SIZE = 925 + + + +export class xrpIO { + private api: Client + + constructor( + private server: string, + private options: Options = { + debug: false, + connectionTimeout: 100000 + } + ) { + this.api = new Client(server, { + connectionTimeout: this.options.connectionTimeout + }) + } + + public async connect(): Promise { + if (!this.api.isConnected()) + await this.api.connect() + } + + public async disconnect(): Promise { + try{ + await this.api.disconnect() + }catch(e){ + console.log("DISCONNECT ERROR", e) + } + } + + private async cloneApi(): Promise { + let _api = new Client(this.server, { + connectionTimeout: this.options.connectionTimeout + }) + + while(!_api.isConnected()){ + try{ + await _api.connect() + return _api + }catch(e){ + this.dbg('CLONEAPI ERR', 'Connection failed', String(e['message'])) + await _api.disconnect() + _api = new Client(this.server, { + connectionTimeout: this.options.connectionTimeout + }) + } + } + } + + private async sendPayment(data: Memo, to: string, secret: string, sequence?: number): Promise { + const wallet = Wallet.fromSecret(secret) + this.dbg("Sending payment", wallet.address, '->', to) + + const _api = await this.cloneApi() + try { + const payment: Payment = await _api.autofill({ + TransactionType: 'Payment', + Account: wallet.address, + Destination: to, + Sequence: sequence, + Amount: "1", + 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() + throw error + } + } + + public async writeRaw(data: Memo, to: string, secret: string, sequence?: number): Promise { + this.dbg("Writing data", data) + const tx = await this.sendPayment(data, to, secret, sequence) + return tx.result.hash + } + + private async getTransaction(hash: string): Promise { + this.dbg("Getting Tx", hash) + let _api = await this.cloneApi() + + while(true){ + try{ + const response = await _api.request({ + command: 'tx', + transaction: hash, + }) + await _api.disconnect() + return response + }catch(e){ + this.dbg("Retrying to get", hash) + await _api.disconnect() + _api = await this.cloneApi() + } + } + } + + public async readRaw(hash: string): Promise { + const tx = await this.getTransaction(hash) + const memo = tx.result.Memos[0].Memo + const memoParsed = { + data: hexDecode(memo.MemoData), + format: hexDecode(memo.MemoFormat), + type: hexDecode(memo.MemoType) + } + this.dbg(hash, "data", memoParsed) + return memoParsed + } + + public async treeWrite(data: string, to: string, secret: string, format: 'L' | 'N' = 'L'): Promise { + const wallet = Wallet.fromSecret(secret) + data = await compressB64(data) + const chunks = chunkString(data, PAYLOAD_SIZE) + const latestSequence = await this.getAccountSequence(wallet.address) + const hashes = await Promise.all(Object.entries(chunks).map(([i, chunk]) => this.writeRaw({ data: chunk, format: format }, to, secret, latestSequence+Number(i)))) + + if (hashes.length === 1) { + return hashes[0] + } + + return await this.treeWrite(JSON.stringify(hashes), to, secret, 'N') + } + + public async treeRead(hashes: string[]): Promise{ + const memos = await Promise.all(hashes.map(hash => this.readRaw(hash))) + const payload: string = await decompressB64(memos.map(memo => memo.data).join('')) + + if (memos.some(memo => memo.format === 'N')) { + return await this.treeRead(JSON.parse(payload)) + } + + return payload + } + + public async getAccountSequence(address: string): Promise{ + this.dbg("Getting acc info for", address) + const accountInfo = await this.api.request({ + command: 'account_info', + account: address, + strict: true, + }) + this.dbg("Got account_info", accountInfo) + return Number(accountInfo.result.account_data.Sequence) + } + + private dbg(...args: any[]) { + if (this.options.debug) { + console.log.apply(console, args) + } + } +} \ No newline at end of file diff --git a/test/julcMfc.gif b/test/julcMfc.gif new file mode 100644 index 0000000..495b3a0 Binary files /dev/null and b/test/julcMfc.gif differ diff --git a/test/primitives.ts b/test/primitives.ts index 41413bb..7ca7d32 100644 --- a/test/primitives.ts +++ b/test/primitives.ts @@ -1,18 +1,18 @@ -import { RippleAPI } from 'ripple-lib' import { htmlTxt, longText, makeTestnetWallet, TEST_CONFIG, TEST_DATA } from './CONSTANTS' -import { getLatestSequence, sendPayment, treeRead, treeWrite } from '../src/xrpIO/ripple-binding' +import { xrpIO } from '../src/xrpIO/xrpl-binding' import * as chai from 'chai'; import { Wallet } from '../src/util/types'; -const utf8 = require('utf8') -const base64 = require('base-64') +var wtf = require('wtfnode'); const expect = chai.expect let sendWallet: Wallet let receiveWallet: Wallet -let api: RippleAPI +let api: xrpIO + +const ONLY_LARGE_TESTS = true describe('XRPIO', () => { - before(async function () { + before(async function(){ this.timeout(15000) sendWallet = await makeTestnetWallet() receiveWallet = await makeTestnetWallet() @@ -21,7 +21,7 @@ describe('XRPIO', () => { beforeEach(async () => { try { - api = new RippleAPI({ server: TEST_CONFIG.rippleNode }) + api = new xrpIO(TEST_CONFIG.rippleNode, {debug: false, connectionTimeout: 100000}) return await api.connect() } catch (e) { console.log(e) @@ -37,70 +37,64 @@ describe('XRPIO', () => { } }) - let latestSeq - describe('plumbing', () => { - it('getLatestSequence', async () => { - latestSeq = await getLatestSequence(api, sendWallet.address) - expect(latestSeq).to.exist - expect(latestSeq).to.be.a('number') - expect(latestSeq).to.be.greaterThan(0) - }) + it('getAccountSequence', async function(){ +// this.skip() + this.timeout(10000) + const seq = await api.getAccountSequence(sendWallet.address) + expect(seq).to.exist + expect(seq).to.be.a('number') }) - describe('payment', () => { - it('sendPayment', async function () { - this.timeout(10000) - const result = await sendPayment(api, [{ data: "123" }], sendWallet.address, receiveWallet.address, sendWallet.secret, latestSeq + 1) - expect(result).to.exist - expect(result.resultCode).to.be.equal('tesSUCCESS') - }) + let txHash + it('writeRaw', async function(){ + this.skip() + this.timeout(15000) + txHash = await api.writeRaw({data: TEST_DATA}, receiveWallet.address, sendWallet.secret); + expect(txHash).to.exist + expect(txHash).to.be.a('string') }) - describe('I/O', () => { - let treeRoot - it(`can tree write (${TEST_DATA.length} bytes)`, async function () { - this.timeout(10000) - treeRoot = await treeWrite(api, TEST_DATA, sendWallet, receiveWallet.address) - }) + it('readRaw', async function () { + this.skip() + this.timeout(15000) + const memo = await api.readRaw(txHash) + expect(memo).to.exist + expect(memo.data).to.be.equal(TEST_DATA) + }) - it('can tree read', async function () { - this.timeout(15000) - const data = await treeRead(api, [treeRoot]) - expect(data).to.be.equal(TEST_DATA) - }) + it('treeWrite', async function(){ + this.skip() + this.timeout(45000) + txHash = await api.treeWrite(longText, receiveWallet.address, sendWallet.secret) + expect(txHash).to.exist + expect(txHash).to.be.a('string') + }) - it(`can tree write large (${longText.length} bytes)`, async function () { - this.timeout(30000) - treeRoot = await treeWrite(api, longText, sendWallet, receiveWallet.address) - }) + it('treeRead', async function(){ + this.skip() + this.timeout(45000) + txHash = await api.treeRead([txHash]) + expect(txHash).to.exist + expect(txHash).to.be.equal(longText) + }) - it('can tree read large', async function () { - this.timeout(30000) - const data = await treeRead(api, [treeRoot]) - expect(data).to.be.equal(longText) - }) + it('treeWrite XL', async function(){ +// this.skip() + this.timeout(450000) + txHash = await api.treeWrite(htmlTxt, receiveWallet.address, sendWallet.secret) + expect(txHash).to.exist + expect(txHash).to.be.a('string') + }) - it(`can tree write extra large (${htmlTxt.length} bytes)`, async function () { - this.timeout(300000) - treeRoot = await treeWrite(api, htmlTxt, sendWallet, receiveWallet.address) - }) - - it('can tree read extra large', async function () { - this.timeout(300000) - const data = await treeRead(api, [treeRoot]) - expect(data).to.be.equal(htmlTxt) - }) - - it("can r/w binary", async function () { - this.timeout(10000); - - const bytes = utf8.encode(TEST_DATA) - const encoded = base64.encode(bytes) - const txHash = await treeWrite(api, encoded, sendWallet, receiveWallet.address) - const res = await treeRead(api, [txHash]) - const decoded_bytes = base64.decode(res) - const text = utf8.decode(decoded_bytes) - expect(text).to.be.equal(TEST_DATA) - }) + it('treeRead XL', async function(){ +// this.skip() + this.timeout(450000) + txHash = await api.treeRead([txHash]) + expect(txHash).to.exist + expect(txHash).to.be.equal(htmlTxt) + }) + + it('print open handles', function(){ + wtf.dump() }) }) \ No newline at end of file diff --git a/xrpio.gif b/xrpio.gif new file mode 100644 index 0000000..495b3a0 Binary files /dev/null and b/xrpio.gif differ