From 33201638ad418668bc7c609809eafe68232b2200 Mon Sep 17 00:00:00 2001 From: nitowa Date: Sun, 27 Mar 2022 18:55:14 +0200 Subject: [PATCH] move to new xrpl library --- README.md | 63 +++++-------- package-lock.json | 141 ++++++++++++++++++++++++++++- package.json | 8 +- src/util/types.ts | 4 + src/xrpIO/ripple-binding.ts | 1 + src/xrpIO/xrpl-binding.ts | 175 ++++++++++++++++++++++++++++++++++++ test/julcMfc.gif | Bin 0 -> 9917 bytes test/primitives.ts | 122 ++++++++++++------------- xrpio.gif | Bin 0 -> 9917 bytes 9 files changed, 401 insertions(+), 113 deletions(-) create mode 100644 src/xrpIO/xrpl-binding.ts create mode 100644 test/julcMfc.gif create mode 100644 xrpio.gif 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 0000000000000000000000000000000000000000..495b3a0c4917b36bf1f4f749b4c796d069fd820c GIT binary patch literal 9917 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..495b3a0c4917b36bf1f4f749b4c796d069fd820c GIT binary patch literal 9917 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 literal 0 HcmV?d00001