|
@@ -3,14 +3,18 @@ import { Client, Payment, TxResponse, Wallet } from 'xrpl'
|
3
|
3
|
|
4
|
4
|
import * as zlib from 'zlib'
|
5
|
5
|
import * as util from 'util'
|
|
6
|
+import { NON_ZERO_TX_HASH } from '../util/protocol.constants'
|
|
7
|
+import { ERR_BAD_TX_HASH } from '../util/errors'
|
6
|
8
|
|
7
|
9
|
const compressB64 = async (data: string) => (await util.promisify(zlib.deflate)(Buffer.from(data, 'utf-8'))).toString('base64')
|
8
|
10
|
const decompressB64 = async (data: string) => (await util.promisify(zlib.inflate)(Buffer.from(data, 'base64'))).toString('utf-8')
|
9
|
11
|
const hexDecode = (str: string) => Buffer.from(str, 'hex').toString('utf8')
|
10
|
12
|
const hexEncode = (str: string) => Buffer.from(str, 'utf8').toString('hex').toUpperCase()
|
11
|
13
|
const chunkString = (str: string, length: number) => str.match(new RegExp('.{1,' + length + '}', 'gs'));
|
|
14
|
+const genRandHex = size => [...Array(size)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
|
12
|
15
|
const PAYLOAD_SIZE = 925
|
13
|
|
-
|
|
16
|
+const XRP_PER_DROP = 0.000001
|
|
17
|
+const DROP_PER_XRP = 1000000
|
14
|
18
|
|
15
|
19
|
|
16
|
20
|
export class xrpIO {
|
|
@@ -63,7 +67,7 @@ export class xrpIO {
|
63
|
67
|
}
|
64
|
68
|
}
|
65
|
69
|
|
66
|
|
- private async sendPayment(data: Memo, to: string, secret: string, sequence?: number): Promise<TxResponse> {
|
|
70
|
+ private async sendPayment(data: Memo, to: string, secret: string, sequence?: number, amount: string = "1"): Promise<TxResponse> {
|
67
|
71
|
const wallet = Wallet.fromSecret(secret)
|
68
|
72
|
this.dbg("Sending payment", wallet.address, '->', to)
|
69
|
73
|
|
|
@@ -74,7 +78,7 @@ export class xrpIO {
|
74
|
78
|
Account: wallet.address,
|
75
|
79
|
Destination: to,
|
76
|
80
|
Sequence: sequence,
|
77
|
|
- Amount: "1",
|
|
81
|
+ Amount: amount,
|
78
|
82
|
Memos: [{
|
79
|
83
|
Memo: {
|
80
|
84
|
MemoData: hexEncode(data.data || ""),
|
|
@@ -96,32 +100,37 @@ export class xrpIO {
|
96
|
100
|
}
|
97
|
101
|
}
|
98
|
102
|
|
99
|
|
- public async writeRaw(data: Memo, to: string, secret: string, sequence?: number): Promise<string> {
|
|
103
|
+ public async writeRaw(data: Memo, to: string, secret: string, sequence?: number, amount: string = "1"): Promise<string> {
|
100
|
104
|
this.dbg("Writing data", data)
|
101
|
|
- const tx = await this.sendPayment(data, to, secret, sequence)
|
|
105
|
+ const tx = await this.sendPayment(data, to, secret, sequence, amount)
|
102
|
106
|
return tx.result.hash
|
103
|
107
|
}
|
104
|
108
|
|
105
|
|
- private async getTransaction(hash: string, retry=0): Promise<TxResponse> {
|
|
109
|
+ private async getTransaction(hash: string, retry = 0): Promise<TxResponse> {
|
106
|
110
|
this.dbg("Getting Tx", hash)
|
107
|
|
- try{
|
|
111
|
+ try {
|
108
|
112
|
return await this.api.request({
|
109
|
113
|
command: 'tx',
|
110
|
114
|
transaction: hash,
|
111
|
115
|
})
|
112
|
|
- }catch(e){
|
|
116
|
+ } catch (e) {
|
113
|
117
|
this.dbg(e)
|
114
|
|
- if(this.options.readMaxRetry != -1){
|
115
|
|
- if(retry >= this.options.readMaxRetry)
|
116
|
|
- console.error("Retry limit exceeded for", hash, ". this is an irrecoverable error")
|
117
|
|
- throw e
|
|
118
|
+ if (this.options.readMaxRetry != -1) {
|
|
119
|
+ if (retry >= this.options.readMaxRetry)
|
|
120
|
+ console.error("Retry limit exceeded for", hash, ". This is an irrecoverable error")
|
|
121
|
+ throw e
|
118
|
122
|
}
|
119
|
123
|
await new Promise(res => setTimeout(res, this.options.readRetryTimeout))
|
120
|
|
- return await this.getTransaction(hash, retry+1)
|
|
124
|
+ return await this.getTransaction(hash, retry + 1)
|
121
|
125
|
}
|
122
|
126
|
}
|
123
|
127
|
|
124
|
128
|
public async readRaw(hash: string): Promise<Memo> {
|
|
129
|
+
|
|
130
|
+ if (!NON_ZERO_TX_HASH.test(hash)) {
|
|
131
|
+ throw ERR_BAD_TX_HASH(hash)
|
|
132
|
+ }
|
|
133
|
+
|
125
|
134
|
const tx = await this.getTransaction(hash)
|
126
|
135
|
const memo = tx.result.Memos[0].Memo
|
127
|
136
|
const memoParsed = {
|
|
@@ -148,6 +157,10 @@ export class xrpIO {
|
148
|
157
|
}
|
149
|
158
|
|
150
|
159
|
public async treeRead(hashes: string[]): Promise<string> {
|
|
160
|
+ const bad_hash = hashes.find(hash => !NON_ZERO_TX_HASH.test(hash))
|
|
161
|
+ if (bad_hash)
|
|
162
|
+ throw ERR_BAD_TX_HASH(bad_hash)
|
|
163
|
+
|
151
|
164
|
const memos = await Promise.all(hashes.map(hash => this.readRaw(hash)))
|
152
|
165
|
const payload: string = await decompressB64(memos.map(memo => memo.data).join(''))
|
153
|
166
|
|
|
@@ -169,6 +182,25 @@ export class xrpIO {
|
169
|
182
|
return Number(accountInfo.result.account_data.Sequence)
|
170
|
183
|
}
|
171
|
184
|
|
|
185
|
+ public async estimateFee(data: string, denomination: 'XRP' | 'DROPS' = 'DROPS', cost = 0): Promise<number> {
|
|
186
|
+ data = await compressB64(data)
|
|
187
|
+ const chunks = chunkString(data, PAYLOAD_SIZE)
|
|
188
|
+
|
|
189
|
+ if (chunks.length === 1) {
|
|
190
|
+ return (denomination === "DROPS" ? (cost + 1) : this.dropsToXrp(cost + 1))
|
|
191
|
+ }
|
|
192
|
+
|
|
193
|
+ return this.estimateFee(JSON.stringify(chunks.map(_ => genRandHex(64))), denomination, cost + chunks.length)
|
|
194
|
+ }
|
|
195
|
+
|
|
196
|
+ public xrpToDrops(xrp: number): number {
|
|
197
|
+ return xrp * DROP_PER_XRP
|
|
198
|
+ }
|
|
199
|
+
|
|
200
|
+ public dropsToXrp(drops: number): number {
|
|
201
|
+ return drops * XRP_PER_DROP
|
|
202
|
+ }
|
|
203
|
+
|
172
|
204
|
private dbg(...args: any[]) {
|
173
|
205
|
if (this.options.debug) {
|
174
|
206
|
console.log.apply(console, args)
|