import { Instructions, LedgerClosedEvent, RippleAPI } from 'ripple-lib' import { Memo, Wallet } from '../util/types' import { MIN_XRP_FEE, MIN_XRP_TX_VALUE } from '../util/protocol.constants'; import * as zlib from 'zlib' import * as util from 'util' import { Payment } from 'ripple-lib/dist/npm/transaction/payment'; 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 => { const subApi = new RippleAPI({ server: api.connection['_url'] }) await subApi.connect() return subApi } export const getLatestSequence = async (api: RippleAPI, accountAddress: string): Promise => { 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 => new Promise((res, rej) => { const ledgerClosedCallback = async (event: LedgerClosedEvent) => { let status try { status = await api.getTransaction(signed.id, { minLedgerVersion: 20368096 }) } 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, }; try { const _api = await cloneApi(api) 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) _api.disconnect() return txHash } catch (error) { //console.log("SENDPAYMENT ERROR", error) throw error } } export const getTransactions = async (api: RippleAPI, address: string, minLedgerVersion: number = 19832467): Promise => { 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 => { //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) { console.log("WRITERAW ERR", error); } } export const readRaw = async (api: RippleAPI, hash: string): Promise => { const tx = await api.getTransaction(hash, { minLedgerVersion: 16392480 }) if (!tx || !tx.specification || !tx.specification['memos'] || !tx.specification['memos'][0]) { 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 => { 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 => { 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 }