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) } } }