You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

xrpl-binding.ts 7.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import { defaultOptions, Memo, Options } from '../util/types'
  2. import { Client, Payment, TxResponse, Wallet } from 'xrpl'
  3. import * as zlib from 'zlib'
  4. import * as util from 'util'
  5. import { NON_ZERO_TX_HASH } from '../util/protocol.constants'
  6. import { ERR_BAD_TX_HASH, ERR_NO_VERIFY_OWNER } from '../util/errors'
  7. const compressB64 = async (data: string) => (await util.promisify(zlib.deflate)(Buffer.from(data, 'utf-8'))).toString('base64')
  8. const decompressB64 = async (data: string) => (await util.promisify(zlib.inflate)(Buffer.from(data, 'base64'))).toString('utf-8')
  9. const hexDecode = (str: string) => Buffer.from(str, 'hex').toString('utf8')
  10. const hexEncode = (str: string) => Buffer.from(str, 'utf8').toString('hex').toUpperCase()
  11. const chunkString = (str: string, length: number) => str.match(new RegExp('.{1,' + length + '}', 'gs'));
  12. const genRandHex = size => [...Array(size)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
  13. const PAYLOAD_SIZE = 925
  14. const XRP_PER_DROP = 0.000001
  15. const DROP_PER_XRP = 1000000
  16. export class xrpIO {
  17. private api: Client
  18. constructor(
  19. private server: string,
  20. private options: Options = defaultOptions
  21. ) {
  22. this.options.debug = options.debug ? Boolean(options.debug) : defaultOptions.debug
  23. this.options.connectionTimeout = options.connectionTimeout ? Number(options.connectionTimeout) : defaultOptions.connectionTimeout
  24. this.options.readMaxRetry = options.readMaxRetry ? Number(options.readMaxRetry) : defaultOptions.readMaxRetry
  25. this.options.readRetryTimeout = options.readRetryTimeout ? Number(options.readRetryTimeout) : defaultOptions.readRetryTimeout
  26. this.api = new Client(server, {
  27. connectionTimeout: this.options.connectionTimeout
  28. })
  29. }
  30. public async connect(): Promise<void> {
  31. if (!this.api.isConnected())
  32. await this.api.connect()
  33. }
  34. public async disconnect(): Promise<void> {
  35. try {
  36. await this.api.disconnect()
  37. } catch (e) {
  38. console.log("DISCONNECT ERROR", e)
  39. }
  40. }
  41. private async cloneApi(): Promise<Client> {
  42. let _api = new Client(this.server, {
  43. connectionTimeout: this.options.connectionTimeout
  44. })
  45. while (!_api.isConnected()) {
  46. try {
  47. await _api.connect()
  48. return _api
  49. } catch (e) {
  50. this.dbg('CLONEAPI ERR', 'Connection failed', String(e['message']))
  51. await _api.disconnect()
  52. _api = new Client(this.server, {
  53. connectionTimeout: this.options.connectionTimeout
  54. })
  55. }
  56. }
  57. }
  58. private async sendPayment(data: Memo, to: string, secret: string, sequence?: number, amount: string = "1"): Promise<TxResponse> {
  59. const wallet = Wallet.fromSecret(secret)
  60. this.dbg("Sending payment", wallet.address, '->', to)
  61. const _api = await this.cloneApi()
  62. try {
  63. const payment: Payment = await _api.autofill({
  64. TransactionType: 'Payment',
  65. Account: wallet.address,
  66. Destination: to,
  67. Sequence: sequence,
  68. Amount: amount,
  69. Memos: [{
  70. Memo: {
  71. MemoData: hexEncode(data.data || ""),
  72. MemoFormat: hexEncode(data.format || ""),
  73. MemoType: hexEncode(data.type || "")
  74. }
  75. }]
  76. })
  77. const response = await _api.submitAndWait(payment, { wallet })
  78. await _api.disconnect()
  79. this.dbg("Tx finalized", response.result.hash, response.result.Sequence)
  80. return response
  81. } catch (error: any) {
  82. this.dbg("SENDPAYMENT ERROR", error)
  83. await _api.disconnect()
  84. throw error
  85. }
  86. }
  87. public async writeRaw(data: Memo, to: string, secret: string, sequence?: number, amount: string = "1"): Promise<string> {
  88. this.dbg("Writing data", data)
  89. const tx = await this.sendPayment(data, to, secret, sequence, amount)
  90. return tx.result.hash
  91. }
  92. private async getTransaction(hash: string, retry = 0): Promise<TxResponse> {
  93. this.dbg("Getting Tx", hash)
  94. try {
  95. return await this.api.request({
  96. command: 'tx',
  97. transaction: hash,
  98. })
  99. } catch (e) {
  100. this.dbg(e)
  101. if (this.options.readMaxRetry != -1) {
  102. if (retry >= this.options.readMaxRetry)
  103. console.error("Retry limit exceeded for", hash, ". This is an irrecoverable error")
  104. throw e
  105. }
  106. await new Promise(res => setTimeout(res, this.options.readRetryTimeout))
  107. return await this.getTransaction(hash, retry + 1)
  108. }
  109. }
  110. public async readRaw(hash: string, verifyOwner?: string): Promise<Memo> {
  111. if (!NON_ZERO_TX_HASH.test(hash)) {
  112. throw ERR_BAD_TX_HASH(hash)
  113. }
  114. const tx = await this.getTransaction(hash)
  115. if(verifyOwner && tx.result.Account != verifyOwner){
  116. throw ERR_NO_VERIFY_OWNER(hash, tx.result.Account, verifyOwner)
  117. }
  118. const memo = tx.result.Memos[0].Memo
  119. const memoParsed = {
  120. data: hexDecode(memo.MemoData),
  121. format: hexDecode(memo.MemoFormat),
  122. type: hexDecode(memo.MemoType)
  123. }
  124. this.dbg(hash, "data", memoParsed)
  125. return memoParsed
  126. }
  127. public async treeWrite(data: string, to: string, secret: string, format: 'L' | 'N' = 'L'): Promise<string> {
  128. const wallet = Wallet.fromSecret(secret)
  129. data = await compressB64(data)
  130. const chunks = chunkString(data, PAYLOAD_SIZE)
  131. const latestSequence = await this.getAccountSequence(wallet.address)
  132. const hashes = await Promise.all(Object.entries(chunks).map(([i, chunk]) => this.writeRaw({ data: chunk, format: format }, to, secret, latestSequence + Number(i))))
  133. if (hashes.length === 1) {
  134. return hashes[0]
  135. }
  136. return await this.treeWrite(JSON.stringify(hashes), to, secret, 'N')
  137. }
  138. public async treeRead(hashes: string[], verifyOwner?:string): Promise<string> {
  139. const bad_hash = hashes.find(hash => !NON_ZERO_TX_HASH.test(hash))
  140. if (bad_hash)
  141. throw ERR_BAD_TX_HASH(bad_hash)
  142. const memos = await Promise.all(hashes.map(hash => this.readRaw(hash, verifyOwner)))
  143. const payload: string = await decompressB64(memos.map(memo => memo.data).join(''))
  144. if (memos.some(memo => memo.format === 'N')) {
  145. return await this.treeRead(JSON.parse(payload), verifyOwner)
  146. }
  147. return payload
  148. }
  149. public async getAccountSequence(address: string): Promise<number> {
  150. this.dbg("Getting acc info for", address)
  151. const accountInfo = await this.api.request({
  152. command: 'account_info',
  153. account: address,
  154. strict: true,
  155. })
  156. this.dbg("Got account_info", accountInfo)
  157. return Number(accountInfo.result.account_data.Sequence)
  158. }
  159. public async estimateFee(data: string, denomination: 'XRP' | 'DROPS' = 'DROPS', cost = 0): Promise<number> {
  160. data = await compressB64(data)
  161. const chunks = chunkString(data, PAYLOAD_SIZE)
  162. if (chunks.length === 1) {
  163. return (denomination === "DROPS" ? (cost + 1) : this.dropsToXrp(cost + 1))
  164. }
  165. return this.estimateFee(JSON.stringify(chunks.map(_ => genRandHex(64))), denomination, cost + chunks.length)
  166. }
  167. public xrpToDrops(xrp: number): number {
  168. return xrp * DROP_PER_XRP
  169. }
  170. public dropsToXrp(drops: number): number {
  171. return drops * XRP_PER_DROP
  172. }
  173. private dbg(...args: any[]) {
  174. if (this.options.debug) {
  175. console.log.apply(console, args)
  176. }
  177. }
  178. }