@ledgerhq/coin-xrp 7.3.0-nightly.1 → 7.3.0-nightly.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,10 @@
1
1
  import { decode } from "ripple-binary-codec";
2
2
  import { createApi } from ".";
3
+ import { Operation } from "@ledgerhq/coin-framework/api/types";
3
4
  //import { decode, encodeForSigning } from "ripple-binary-codec";
4
5
  //import { sign } from "ripple-keypairs";
5
6
 
6
- describe("Xrp Api", () => {
7
+ describe("Xrp Api (testnet)", () => {
7
8
  const SENDER = "rh1HPuRVsYYvThxG2Bs1MfjmrVC73S16Fb";
8
9
  const api = createApi({ node: "https://s.altnet.rippletest.net:51234" });
9
10
 
@@ -70,8 +71,12 @@ describe("Xrp Api", () => {
70
71
 
71
72
  it("returns operations from latest, but in asc order", async () => {
72
73
  // When
73
- const [txDesc] = await api.listOperations(SENDER, { minHeight: 0, order: "desc" });
74
-
74
+ const SENDER_WITH_TRANSACTIONS = "rUxSkt6hQpWxXQwTNRUCYYRQ7BC2yRA3F8";
75
+ const [txDesc] = await api.listOperations(SENDER_WITH_TRANSACTIONS, {
76
+ minHeight: 200,
77
+ order: "desc",
78
+ });
79
+ expect(txDesc.length).toBeGreaterThanOrEqual(200);
75
80
  // Then
76
81
  // Check if the result is sorted in ascending order
77
82
  expect(txDesc[0].tx.block.height).toBeGreaterThanOrEqual(
@@ -96,13 +101,13 @@ describe("Xrp Api", () => {
96
101
  // Account with no transaction (at the time of this writing)
97
102
  const SENDER_WITH_NO_TRANSACTION = "rKtXXTVno77jhu6tto1MAXjepyuaKaLcqB";
98
103
 
99
- it("returns an amount above 0 when address has transactions", async () => {
104
+ it("returns a balance", async () => {
100
105
  // When
101
106
  const result = await api.getBalance(SENDER);
102
107
 
103
108
  // Then
104
109
  expect(result[0].asset).toEqual({ type: "native" });
105
- expect(result[0].value).toBeGreaterThan(BigInt(0));
110
+ expect(result[0].value).toBeGreaterThanOrEqual(BigInt(0));
106
111
  });
107
112
 
108
113
  it("returns 0 when address has no transaction", async () => {
@@ -176,6 +181,169 @@ describe("Xrp Api", () => {
176
181
  });
177
182
  });
178
183
 
184
+ describe("Xrp Api (mainnet)", () => {
185
+ const SENDER = "rn5BQvhksnPfbo277LtFks4iyYStPKGrnJ";
186
+ const api = createApi({ node: "https://xrp.coin.ledger.com" });
187
+
188
+ describe("estimateFees", () => {
189
+ it("returns a default value", async () => {
190
+ // Given
191
+ const amount = BigInt(100);
192
+
193
+ // When
194
+ const result = await api.estimateFees({
195
+ asset: { type: "native" },
196
+ type: "send",
197
+ sender: SENDER,
198
+ amount,
199
+ recipient: "r9m6MwViR4GnUNqoGXGa8eroBrZ9FAPHFS",
200
+ memo: {
201
+ type: "map",
202
+ memos: new Map(),
203
+ },
204
+ });
205
+
206
+ // Then
207
+ expect(result.value).toEqual(BigInt(10));
208
+ });
209
+ });
210
+
211
+ describe("listOperations", () => {
212
+ let ops: Operation[];
213
+
214
+ beforeAll(async () => {
215
+ const resp = await api.listOperations(SENDER, { minHeight: 0 });
216
+ ops = resp[0];
217
+ });
218
+
219
+ it("returns operations", async () => {
220
+ // https://xrpscan.com/account/rn5BQvhksnPfbo277LtFks4iyYStPKGrnJ
221
+ expect(ops.length).toBeGreaterThanOrEqual(200);
222
+ const checkSet = new Set(ops.map(elt => elt.tx.hash));
223
+ expect(checkSet.size).toEqual(ops.length);
224
+ ops.forEach(operation => {
225
+ const isSenderOrReceipt =
226
+ operation.senders.includes(SENDER) || operation.recipients.includes(SENDER);
227
+ expect(isSenderOrReceipt).toBeTruthy();
228
+ });
229
+ });
230
+
231
+ it("returns IN operation", async () => {
232
+ // https://xrpscan.com/tx/805E371FDA0223E8910F831802EE93DBA1A4CA40AC8C1337F26F566CD67788F5
233
+ const inTx = {
234
+ hash: "9E5141DCAE8158E51ED612333FF3EC2D60A3D2DCD2F6DD5E4F92E4A6704C3CE9",
235
+ amount: 7.5,
236
+ recipient: SENDER,
237
+ sender: "rnXQmrXk9HSKdNwkut1k9dpAMVYuBsJV49",
238
+ type: "IN",
239
+ fees: 0.00001,
240
+ };
241
+ const op = ops.find(o => o.tx.hash === inTx.hash) as Operation;
242
+ expect(op.tx.hash).toEqual(inTx.hash);
243
+ expect(op.value).toEqual(BigInt(inTx.amount * 1e6));
244
+ expect(op.recipients).toContain(inTx.recipient);
245
+ expect(op.senders).toContain(inTx.sender);
246
+ expect(op.type).toEqual(inTx.type);
247
+ expect(op.tx.fees).toEqual(BigInt(inTx.fees * 1e6));
248
+ });
249
+
250
+ it("returns OUT operation", async () => {
251
+ // https://xrpscan.com/tx/8D13FD7EE0D28B615905903D033A3DC3839FBAA2F545417E3DE51A1A745C1688
252
+ const outTx = {
253
+ hash: "8D13FD7EE0D28B615905903D033A3DC3839FBAA2F545417E3DE51A1A745C1688",
254
+ amount: 0.1,
255
+ recipient: "r3XzsqzQCC6r4ZzifWnwa32sFR2H9exkew",
256
+ sender: SENDER,
257
+ type: "OUT",
258
+ fees: 0.00001,
259
+ };
260
+ const op = ops.find(o => o.tx.hash === outTx.hash) as Operation;
261
+ expect(op.tx.hash).toEqual(outTx.hash);
262
+ expect(op.value).toEqual(BigInt(outTx.amount * 1e6));
263
+ expect(op.recipients).toContain(outTx.recipient);
264
+ expect(op.senders).toContain(outTx.sender);
265
+ expect(op.type).toEqual(outTx.type);
266
+ expect(op.tx.fees).toEqual(BigInt(outTx.fees * 1e6));
267
+ });
268
+ });
269
+
270
+ describe("lastBlock", () => {
271
+ it("returns last block info", async () => {
272
+ const result = await api.lastBlock();
273
+ expect(result.hash).toBeDefined();
274
+ expect(result.height).toBeDefined();
275
+ expect(result.time).toBeInstanceOf(Date);
276
+ });
277
+ });
278
+
279
+ describe("getBalance", () => {
280
+ it("returns an amount", async () => {
281
+ const result = await api.getBalance(SENDER);
282
+ expect(result[0].asset).toEqual({ type: "native" });
283
+ expect(result[0].value).toBeGreaterThanOrEqual(BigInt(0));
284
+ });
285
+ });
286
+
287
+ describe("craftTransaction", () => {
288
+ const RECIPIENT = "r9m6MwViR4GnUNqoGXGa8eroBrZ9FAPHFS";
289
+
290
+ it("returns a raw transaction", async () => {
291
+ const result = await api.craftTransaction({
292
+ asset: { type: "native" },
293
+ type: "send",
294
+ sender: SENDER,
295
+ recipient: RECIPIENT,
296
+ amount: BigInt(10),
297
+ memo: {
298
+ type: "map",
299
+ memos: new Map([["memos", ["testdata"]]]),
300
+ },
301
+ });
302
+ expect(result.length).toEqual(178);
303
+ });
304
+
305
+ it("should use default fees when user does not provide them for crafting a transaction", async () => {
306
+ const result = await api.craftTransaction({
307
+ asset: { type: "native" },
308
+ type: "send",
309
+ sender: SENDER,
310
+ recipient: RECIPIENT,
311
+ amount: BigInt(10),
312
+ memo: {
313
+ type: "map",
314
+ memos: new Map(),
315
+ },
316
+ });
317
+
318
+ expect(decode(result)).toMatchObject({
319
+ Fee: "10",
320
+ });
321
+ });
322
+
323
+ it("should use custom user fees when user provides it for crafting a transaction", async () => {
324
+ const customFees = 99n;
325
+ const result = await api.craftTransaction(
326
+ {
327
+ asset: { type: "native" },
328
+ type: "send",
329
+ sender: SENDER,
330
+ recipient: RECIPIENT,
331
+ amount: BigInt(10),
332
+ memo: {
333
+ type: "map",
334
+ memos: new Map(),
335
+ },
336
+ },
337
+ { value: customFees },
338
+ );
339
+
340
+ expect(decode(result)).toMatchObject({
341
+ Fee: customFees.toString(),
342
+ });
343
+ });
344
+ });
345
+ });
346
+
179
347
  // To enable this test, you need to fill an `.env` file at the root of this package. Example can be found in `.env.integ.test.example`.
180
348
  // The value hardcoded here depends on the value filled in the `.env` file.
181
349
  /*describe.skip("combine", () => {
@@ -108,12 +108,11 @@ describe("listOperations", () => {
108
108
  mockGetTransactions.mockResolvedValue(mockNetworkTxs(txs, defaultMarker));
109
109
  const [results, _] = await api.listOperations("src", { minHeight: 0, order: "asc" });
110
110
 
111
- // called 10 times because there is a hard limit of 10 iterations in case something goes wrong
112
- // with interpretation of the token (bug / explorer api changed ...)
113
- expect(mockGetServerInfos).toHaveBeenCalledTimes(10);
114
- expect(mockGetTransactions).toHaveBeenCalledTimes(10);
111
+ // called 1 times because the client is expected to do the pagination itself
112
+ expect(mockGetServerInfos).toHaveBeenCalledTimes(1);
113
+ expect(mockGetTransactions).toHaveBeenCalledTimes(1);
115
114
 
116
- expect(results.length).toBe(txs.length * 10);
115
+ expect(results.length).toBe(txs.length);
117
116
  });
118
117
 
119
118
  it("should pass the token returned by previous calls", async () => {
@@ -122,11 +121,10 @@ describe("listOperations", () => {
122
121
  .mockReturnValueOnce(mockNetworkTxs(txs, defaultMarker))
123
122
  .mockReturnValueOnce(mockNetworkTxs(txs, undefined));
124
123
 
125
- const [results, _] = await api.listOperations("src", { minHeight: 0, order: "asc" });
124
+ const [results, token] = await api.listOperations("src", { minHeight: 0, order: "asc" });
126
125
 
127
- // called 2 times because the second time there is no marker
128
- expect(mockGetServerInfos).toHaveBeenCalledTimes(2);
129
- expect(mockGetTransactions).toHaveBeenCalledTimes(2);
126
+ expect(mockGetServerInfos).toHaveBeenCalledTimes(1);
127
+ expect(mockGetTransactions).toHaveBeenCalledTimes(1);
130
128
 
131
129
  // check tokens are passed
132
130
  const baseOptions = {
@@ -135,13 +133,14 @@ describe("listOperations", () => {
135
133
  forward: true,
136
134
  };
137
135
  expect(mockGetTransactions).toHaveBeenNthCalledWith(1, "src", baseOptions);
136
+ await api.listOperations("src", { minHeight: 0, order: "asc", lastPagingToken: token });
138
137
  const optionsWithToken = {
139
138
  ...baseOptions,
140
139
  marker: defaultMarker,
141
140
  };
142
141
  expect(mockGetTransactions).toHaveBeenNthCalledWith(2, "src", optionsWithToken);
143
142
 
144
- expect(results.length).toBe(txs.length * 2);
143
+ expect(results.length).toBe(txs.length);
145
144
  });
146
145
 
147
146
  it.each([
@@ -174,12 +173,9 @@ describe("listOperations", () => {
174
173
  const [results, _] = await api.listOperations(address, { minHeight: 0, order: "asc" });
175
174
 
176
175
  // Then
177
- // called twice because the marker is set the first time
178
- expect(mockGetServerInfos).toHaveBeenCalledTimes(2);
179
- expect(mockGetTransactions).toHaveBeenCalledTimes(2);
176
+ expect(mockGetServerInfos).toHaveBeenCalledTimes(1);
177
+ expect(mockGetTransactions).toHaveBeenCalledTimes(1);
180
178
 
181
- // if expectedType is "OUT", compute value with fees (i.e. delivered_amount + Fee)
182
- const expectedValue = expectedType === "IN" ? deliveredAmount : deliveredAmount + fee;
183
179
  // the order is reversed so that the result is always sorted by newest tx first element of the list
184
180
  expect(results).toEqual([
185
181
  {
@@ -196,7 +192,7 @@ describe("listOperations", () => {
196
192
  },
197
193
  },
198
194
  type: expectedType,
199
- value: expectedValue,
195
+ value: deliveredAmount,
200
196
  senders: [opSender],
201
197
  recipients: [opDestination],
202
198
  details: {
@@ -224,7 +220,7 @@ describe("listOperations", () => {
224
220
  },
225
221
  },
226
222
  type: expectedType,
227
- value: expectedValue,
223
+ value: deliveredAmount,
228
224
  senders: [opSender],
229
225
  recipients: [opDestination],
230
226
  details: {
@@ -247,7 +243,7 @@ describe("listOperations", () => {
247
243
  },
248
244
  },
249
245
  type: expectedType,
250
- value: expectedValue,
246
+ value: deliveredAmount,
251
247
  senders: [opSender],
252
248
  recipients: [opDestination],
253
249
  details: {
package/src/api/index.ts CHANGED
@@ -27,7 +27,6 @@ import {
27
27
  MemoInput,
28
28
  } from "../logic";
29
29
  import { ListOperationsOptions, XrpMapMemo } from "../types";
30
- import { Order } from "../types/model";
31
30
 
32
31
  export function createApi(config: XrpConfig): Api<XrpMapMemo> {
33
32
  coinConfig.setCoinConfig(() => ({ ...config, status: { type: "active" } }));
@@ -103,76 +102,20 @@ async function estimate(): Promise<FeeEstimation> {
103
102
  return { value: estimation.fees };
104
103
  }
105
104
 
106
- type PaginationState = {
107
- readonly pageSize: number; // must be large enough to avoid unnecessary calls to the underlying explorer
108
- readonly maxIterations: number; // a security to avoid infinite loop
109
- currentIteration: number;
110
- readonly minHeight: number;
111
- continueIterations: boolean;
112
- apiNextCursor?: string;
113
- accumulator: Operation[];
114
- };
115
-
116
- async function operationsFromHeight(
117
- address: string,
118
- minHeight: number,
119
- order: Order = "asc",
120
- ): Promise<[Operation[], string]> {
121
- async function fetchNextPage(state: PaginationState): Promise<PaginationState> {
122
- const options: ListOperationsOptions = {
123
- limit: state.pageSize,
124
- minHeight: state.minHeight,
125
- order: order,
126
- };
127
- if (state.apiNextCursor) {
128
- options.token = state.apiNextCursor;
129
- }
130
- const [operations, apiNextCursor] = await listOperations(address, options);
131
- const newCurrentIteration = state.currentIteration + 1;
132
- let continueIteration = true;
133
- if (apiNextCursor === "") {
134
- continueIteration = false;
135
- } else if (newCurrentIteration >= state.maxIterations) {
136
- log("coin:xrp", "(api/operations): max iterations reached", state.maxIterations);
137
- continueIteration = false;
138
- }
139
- const accumulated = state.accumulator.concat(operations);
140
- return {
141
- ...state,
142
- currentIteration: newCurrentIteration,
143
- continueIterations: continueIteration,
144
- apiNextCursor: apiNextCursor,
145
- accumulator: accumulated,
146
- };
147
- }
148
-
149
- const firstState: PaginationState = {
150
- pageSize: 200,
151
- maxIterations: 10,
152
- currentIteration: 0,
153
- minHeight: minHeight,
154
- continueIterations: true,
155
- accumulator: [],
156
- };
157
-
158
- let state = await fetchNextPage(firstState);
159
- while (state.continueIterations) {
160
- state = await fetchNextPage(state);
161
- }
162
- return [state.accumulator, state.apiNextCursor ?? ""];
163
- }
164
-
165
105
  // NOTE: double check
166
106
  async function operations(address: string, pagination: Pagination): Promise<[Operation[], string]> {
167
107
  const { minHeight, lastPagingToken, order } = pagination;
168
- if (minHeight) {
169
- return await operationsFromHeight(address, minHeight, order);
170
- }
171
- const isInitSync = lastPagingToken === "";
172
-
173
- const newPagination = {
174
- minHeight: isInitSync ? 0 : parseInt(lastPagingToken || "0", 10),
108
+ const options: ListOperationsOptions = {
109
+ limit: 200,
110
+ minHeight: minHeight,
111
+ order: order ?? "asc",
175
112
  };
176
- // TODO token must be implemented properly (waiting ack from the design document)
177
- return await operationsFromHeight(address, newPagination.minHeight, order);
113
+ if (lastPagingToken) {
114
+ const token = lastPagingToken.split("-");
115
+ options.token = JSON.stringify({ ledger: Number(token[0]), seq: Number(token[1]) });
116
+ log(options.token);
117
+ }
118
+ const [operations, apiNextCursor] = await listOperations(address, options);
119
+ const next = apiNextCursor ? JSON.parse(apiNextCursor) : null;
120
+ return [operations, next ? next.ledger + "-" + next.seq : ""];
178
121
  }
@@ -215,9 +215,7 @@ describe("listOperations", () => {
215
215
  // Then
216
216
  expect(mockGetServerInfos).toHaveBeenCalledTimes(1);
217
217
  expect(mockNetworkGetTransactions).toHaveBeenCalledTimes(1);
218
- // if expectedType is "OUT", compute value with fees (i.e. delivered_amount + Fee)
219
- const expectedValue =
220
- expectedType === "IN" ? BigInt(deliveredAmount) : BigInt(deliveredAmount + fees);
218
+ const expectedValue = BigInt(deliveredAmount);
221
219
  expect(results).toEqual([
222
220
  {
223
221
  id: "HASH_VALUE",
@@ -120,17 +120,12 @@ const convertToCoreOperation =
120
120
  } = operation;
121
121
 
122
122
  const type = Account === address ? "OUT" : "IN";
123
- let value =
123
+ const value =
124
124
  delivered_amount && typeof delivered_amount === "string"
125
125
  ? BigInt(delivered_amount)
126
126
  : BigInt(0);
127
127
 
128
128
  const fees = BigInt(Fee);
129
- if (type === "OUT") {
130
- if (!Number.isNaN(fees)) {
131
- value = value + fees;
132
- }
133
- }
134
129
 
135
130
  const toEpochDate = (RIPPLE_EPOCH + date) * 1000;
136
131