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 5.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  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. const compressB64 = async (data: string) => (await util.promisify(zlib.deflate)(Buffer.from(data, 'utf-8'))).toString('base64')
  6. const decompressB64 = async (data: string) => (await util.promisify(zlib.inflate)(Buffer.from(data, 'base64'))).toString('utf-8')
  7. const hexDecode = (str: string) => Buffer.from(str, 'hex').toString('utf8')
  8. const hexEncode = (str: string) => Buffer.from(str, 'utf8').toString('hex').toUpperCase()
  9. const chunkString = (str: string, length: number) => str.match(new RegExp('.{1,' + length + '}', 'gs'));
  10. const PAYLOAD_SIZE = 925
  11. export class xrpIO {
  12. private api: Client
  13. constructor(
  14. private server: string,
  15. private options: Options = defaultOptions
  16. ) {
  17. this.options.debug = this.options.debug ? Boolean(this.options.debug) : defaultOptions.debug
  18. this.options.connectionTimeout = this.options.connectionTimeout ? Number(this.options.connectionTimeout) : defaultOptions.connectionTimeout
  19. this.options.readFreshApi = this.options.readFreshApi ? Boolean(this.options.readFreshApi) : defaultOptions.readFreshApi
  20. this.api = new Client(server, {
  21. connectionTimeout: this.options.connectionTimeout
  22. })
  23. }
  24. public async connect(): Promise<void> {
  25. if (!this.api.isConnected())
  26. await this.api.connect()
  27. }
  28. public async disconnect(): Promise<void> {
  29. try {
  30. await this.api.disconnect()
  31. } catch (e) {
  32. console.log("DISCONNECT ERROR", e)
  33. }
  34. }
  35. private async cloneApi(): Promise<Client> {
  36. let _api = new Client(this.server, {
  37. connectionTimeout: this.options.connectionTimeout
  38. })
  39. while (!_api.isConnected()) {
  40. try {
  41. await _api.connect()
  42. return _api
  43. } catch (e) {
  44. this.dbg('CLONEAPI ERR', 'Connection failed', String(e['message']))
  45. await _api.disconnect()
  46. _api = new Client(this.server, {
  47. connectionTimeout: this.options.connectionTimeout
  48. })
  49. }
  50. }
  51. }
  52. private async sendPayment(data: Memo, to: string, secret: string, sequence?: number): Promise<TxResponse> {
  53. const wallet = Wallet.fromSecret(secret)
  54. this.dbg("Sending payment", wallet.address, '->', to)
  55. const _api = await this.cloneApi()
  56. try {
  57. const payment: Payment = await _api.autofill({
  58. TransactionType: 'Payment',
  59. Account: wallet.address,
  60. Destination: to,
  61. Sequence: sequence,
  62. Amount: "1",
  63. Memos: [{
  64. Memo: {
  65. MemoData: hexEncode(data.data || ""),
  66. MemoFormat: hexEncode(data.format || ""),
  67. MemoType: hexEncode(data.type || "")
  68. }
  69. }]
  70. })
  71. const response = await _api.submitAndWait(payment, { wallet })
  72. await _api.disconnect()
  73. this.dbg("Tx finalized", response.result.hash, response.result.Sequence)
  74. return response
  75. } catch (error: any) {
  76. this.dbg("SENDPAYMENT ERROR", error)
  77. await _api.disconnect()
  78. throw error
  79. }
  80. }
  81. public async writeRaw(data: Memo, to: string, secret: string, sequence?: number): Promise<string> {
  82. this.dbg("Writing data", data)
  83. const tx = await this.sendPayment(data, to, secret, sequence)
  84. return tx.result.hash
  85. }
  86. private async getTransaction(hash: string): Promise<TxResponse> {
  87. this.dbg("Getting Tx", hash)
  88. if (this.options.readFreshApi) {
  89. let _api = await this.cloneApi()
  90. while (true) {
  91. try {
  92. const response = await _api.request({
  93. command: 'tx',
  94. transaction: hash,
  95. })
  96. await _api.disconnect()
  97. return response
  98. } catch (e) {
  99. this.dbg("Retrying to get", hash)
  100. await _api.disconnect()
  101. _api = await this.cloneApi()
  102. }
  103. }
  104. }else{
  105. return await this.api.request({
  106. command: 'tx',
  107. transaction: hash,
  108. })
  109. }
  110. }
  111. public async readRaw(hash: string): Promise<Memo> {
  112. const tx = await this.getTransaction(hash)
  113. const memo = tx.result.Memos[0].Memo
  114. const memoParsed = {
  115. data: hexDecode(memo.MemoData),
  116. format: hexDecode(memo.MemoFormat),
  117. type: hexDecode(memo.MemoType)
  118. }
  119. this.dbg(hash, "data", memoParsed)
  120. return memoParsed
  121. }
  122. public async treeWrite(data: string, to: string, secret: string, format: 'L' | 'N' = 'L'): Promise<string> {
  123. const wallet = Wallet.fromSecret(secret)
  124. data = await compressB64(data)
  125. const chunks = chunkString(data, PAYLOAD_SIZE)
  126. const latestSequence = await this.getAccountSequence(wallet.address)
  127. const hashes = await Promise.all(Object.entries(chunks).map(([i, chunk]) => this.writeRaw({ data: chunk, format: format }, to, secret, latestSequence + Number(i))))
  128. if (hashes.length === 1) {
  129. return hashes[0]
  130. }
  131. return await this.treeWrite(JSON.stringify(hashes), to, secret, 'N')
  132. }
  133. public async treeRead(hashes: string[]): Promise<string> {
  134. const memos = await Promise.all(hashes.map(hash => this.readRaw(hash)))
  135. const payload: string = await decompressB64(memos.map(memo => memo.data).join(''))
  136. if (memos.some(memo => memo.format === 'N')) {
  137. return await this.treeRead(JSON.parse(payload))
  138. }
  139. return payload
  140. }
  141. public async getAccountSequence(address: string): Promise<number> {
  142. this.dbg("Getting acc info for", address)
  143. const accountInfo = await this.api.request({
  144. command: 'account_info',
  145. account: address,
  146. strict: true,
  147. })
  148. this.dbg("Got account_info", accountInfo)
  149. return Number(accountInfo.result.account_data.Sequence)
  150. }
  151. private dbg(...args: any[]) {
  152. if (this.options.debug) {
  153. console.log.apply(console, args)
  154. }
  155. }
  156. }