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.

ripple-binding.ts 6.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import { Memo, Wallet } from '../util/types'
  2. import { MIN_XRP_FEE, MIN_XRP_TX_VALUE } from '../util/protocol.constants';
  3. import { Instructions, LedgerClosedEvent, RippleAPI } from 'ripple-lib'
  4. import { Payment } from 'ripple-lib/dist/npm/transaction/payment';
  5. import * as zlib from 'zlib'
  6. import * as util from 'util'
  7. const chunkString = (str: string, length: number) => str.match(new RegExp('.{1,' + length + '}', 'gs'));
  8. const PAYLOAD_SIZE = 925
  9. const debug = false
  10. const cloneApi = async (api: RippleAPI): Promise<RippleAPI> => {
  11. try {
  12. const subApi = new RippleAPI({ server: api.connection['_url'] })
  13. await subApi.connect()
  14. return subApi
  15. } catch (e) {
  16. if (debug) {
  17. console.log("CLONEAPI ERR", e)
  18. }
  19. return await cloneApi(api)
  20. }
  21. }
  22. export const getLatestSequence = async (api: RippleAPI, accountAddress: string): Promise<number> => {
  23. if (debug) console.log("Getting acc info for", accountAddress)
  24. const accountInfo = await api.getAccountInfo(accountAddress, {})
  25. return Number(accountInfo.sequence - 1)
  26. }
  27. const compressB64 = async (data: string) => (await util.promisify(zlib.deflate)(Buffer.from(data, 'utf-8'))).toString('base64')
  28. const decompressB64 = async (data: string) => (await util.promisify(zlib.inflate)(Buffer.from(data, 'base64'))).toString('utf-8')
  29. const sendReliably = (api: RippleAPI, signed: any, preparedPayment,): Promise<any> => new Promise((res, rej) => {
  30. const ledgerClosedCallback = async (event: LedgerClosedEvent) => {
  31. let status
  32. try {
  33. status = await api.getTransaction(signed.id, {
  34. minLedgerVersion: 25235454
  35. })
  36. } catch (e) {
  37. // Typical error when the tx hasn't been validated yet:
  38. if ((e as Error).name !== 'MissingLedgerHistoryError') {
  39. //console.log(e)
  40. }
  41. if (event.ledger_index > preparedPayment.instructions.maxLedgerVersion + 3) {
  42. // Assumptions:
  43. // - We are still connected to the same rippled server
  44. // - No ledger gaps occurred
  45. // - All ledgers between the time we submitted the tx and now have been checked for the tx
  46. status = {
  47. finalResult: 'Transaction was not, and never will be, included in a validated ledger'
  48. }
  49. return rej(status);
  50. } else {
  51. // Check again later:
  52. api.connection.once('ledgerClosed', ledgerClosedCallback)
  53. return
  54. }
  55. }
  56. return res(status)
  57. }
  58. api.connection.once('ledgerClosed', ledgerClosedCallback)
  59. })
  60. export const sendPayment = async (api: RippleAPI, data: Memo[], from: string, to: string, secret: string, sequence: number) => {
  61. if (debug) console.log("Sending payment with seq", sequence)
  62. const options: Instructions = {
  63. maxLedgerVersionOffset: 5,
  64. fee: MIN_XRP_FEE,
  65. sequence: sequence,
  66. };
  67. const payment: Payment = {
  68. source: {
  69. address: from,
  70. maxAmount: {
  71. value: MIN_XRP_TX_VALUE,
  72. currency: 'XRP'
  73. },
  74. },
  75. destination: {
  76. address: to,
  77. amount: {
  78. value: MIN_XRP_TX_VALUE,
  79. currency: 'XRP'
  80. },
  81. },
  82. memos: data,
  83. };
  84. const _api = await cloneApi(api)
  85. try {
  86. const prepared = await _api.preparePayment(from, payment, options)
  87. const signed = _api.sign(prepared.txJSON, secret);
  88. const txHash = await _api.submit(signed.signedTransaction)
  89. //if(debug) console.log("Transaction submitted", txHash)
  90. await sendReliably(_api, signed, prepared)
  91. return txHash
  92. } catch (error) {
  93. if (debug)
  94. console.log("SENDPAYMENT ERROR", error)
  95. throw error
  96. } finally {
  97. _api.disconnect()
  98. }
  99. }
  100. export const getTransactions = async (api: RippleAPI, address: string, minLedgerVersion: number = 25235454): Promise<any[]> => {
  101. const txs = await api.getTransactions(address, {
  102. minLedgerVersion: minLedgerVersion,
  103. earliestFirst: true,
  104. excludeFailures: true,
  105. })
  106. return txs
  107. }
  108. export const writeRaw = async (api: RippleAPI, data: Memo, from:
  109. string, to: string, secret: string, sequence?: number): Promise<string> => {
  110. //if (memoSize(data) > 1000) throw new Error("data length exceeds capacity")
  111. try {
  112. if (!sequence) {
  113. const accountInfo = await getLatestSequence(api, from)
  114. sequence = accountInfo + 1
  115. }
  116. const resp = await sendPayment(api, [data], from, to, secret, sequence)
  117. return resp['tx_json'].hash
  118. } catch (error) {
  119. if (debug) {
  120. console.log("WRITERAW ERR", error);
  121. }
  122. throw error
  123. }
  124. }
  125. export const readRaw = async (api: RippleAPI, hash: string): Promise<Memo> => {
  126. api = await cloneApi(api)
  127. let tx
  128. try {
  129. tx = await api.getTransaction(hash, {
  130. minLedgerVersion: 25235454
  131. })
  132. } catch (e) {
  133. // if(debug){
  134. console.log("READRAW ERR", e)
  135. api.isConnected
  136. // }
  137. throw e
  138. } finally {
  139. await api.disconnect()
  140. }
  141. if (!tx || !tx.specification || !tx.specification['memos'] || !tx.specification['memos'][0]) {
  142. console.log(tx)
  143. throw new Error('Invalid Transaction ' + hash)
  144. }
  145. return tx.specification['memos'][0]
  146. }
  147. export const subscribe = async (api: RippleAPI, address: string, callback: (tx) => any) => {
  148. api.connection.on('transaction', (tx) => callback(tx))
  149. await api.connection.request({
  150. command: 'subscribe',
  151. accounts: [address],
  152. })
  153. }
  154. export const treeWrite = async (api: RippleAPI, data: string, wallet: Wallet, to: string, format: 'L' | 'N' = 'L'): Promise<string> => {
  155. data = await compressB64(data)
  156. const chunks = chunkString(data, PAYLOAD_SIZE)
  157. const latestSequence = await getLatestSequence(api, wallet.address)
  158. 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)))
  159. if (hashes.length === 1) {
  160. return hashes[0]
  161. }
  162. return await treeWrite(api, JSON.stringify(hashes), wallet, to, 'N')
  163. }
  164. export const treeRead = async (api: RippleAPI, hashes: string[]): Promise<string> => {
  165. const memos = await Promise.all(hashes.map(hash => readRaw(api, hash)))
  166. const payload: string = await decompressB64(memos.map(memo => memo.data).join(''))
  167. if (memos.some(memo => memo.format === 'N')) {
  168. return await treeRead(api, JSON.parse(payload))
  169. }
  170. return payload
  171. }