123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177 |
- import { defaultOptions, 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 = defaultOptions
- ) {
-
- this.options.debug = options.debug ? Boolean(options.debug) : defaultOptions.debug
- this.options.connectionTimeout = options.connectionTimeout ? Number(options.connectionTimeout) : defaultOptions.connectionTimeout
- this.options.readMaxRetry = options.readMaxRetry ? Number(options.readMaxRetry) : defaultOptions.readMaxRetry
- this.options.readRetryTimeout = options.readRetryTimeout ? Number(options.readRetryTimeout) : defaultOptions.readRetryTimeout
-
- this.api = new Client(server, {
- connectionTimeout: this.options.connectionTimeout
- })
- }
-
- public async connect(): Promise<void> {
- if (!this.api.isConnected())
- await this.api.connect()
- }
-
- public async disconnect(): Promise<void> {
- try {
- await this.api.disconnect()
- } catch (e) {
- console.log("DISCONNECT ERROR", e)
- }
- }
-
- private async cloneApi(): Promise<Client> {
- 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<TxResponse> {
- 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<string> {
- this.dbg("Writing data", data)
- const tx = await this.sendPayment(data, to, secret, sequence)
- return tx.result.hash
- }
-
- private async getTransaction(hash: string, retry=0): Promise<TxResponse> {
- this.dbg("Getting Tx", hash)
- try{
- return await this.api.request({
- command: 'tx',
- transaction: hash,
- })
- }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
- }
- await new Promise(res => setTimeout(res, this.options.readRetryTimeout))
- return await this.getTransaction(hash, retry+1)
- }
- }
-
- public async readRaw(hash: string): Promise<Memo> {
- 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<string> {
- 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<string> {
- 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<number> {
- 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)
- }
- }
- }
|