@ledgerhq/coin-xrp 7.2.1-nightly.0 → 7.3.0-nightly.2

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
 
@@ -33,7 +34,7 @@ describe("Xrp Api", () => {
33
34
  describe("listOperations", () => {
34
35
  it.skip("returns a list regarding address parameter", async () => {
35
36
  // When
36
- const [tx, _] = await api.listOperations(SENDER, { minHeight: 200 });
37
+ const [tx, _] = await api.listOperations(SENDER, { minHeight: 200, order: "asc" });
37
38
 
38
39
  // https://blockexplorer.one/xrp/testnet/address/rh1HPuRVsYYvThxG2Bs1MfjmrVC73S16Fb
39
40
  // as of 2025-03-18, the address has 287 transactions
@@ -51,7 +52,10 @@ describe("Xrp Api", () => {
51
52
  const SENDER_WITH_TRANSACTIONS = "rUxSkt6hQpWxXQwTNRUCYYRQ7BC2yRA3F8";
52
53
 
53
54
  // When
54
- const [ops, _] = await api.listOperations(SENDER_WITH_TRANSACTIONS, { minHeight: 0 });
55
+ const [ops, _] = await api.listOperations(SENDER_WITH_TRANSACTIONS, {
56
+ minHeight: 0,
57
+ order: "asc",
58
+ });
55
59
  // Then
56
60
  const checkSet = new Set(ops.map(elt => elt.tx.hash));
57
61
  expect(checkSet.size).toEqual(ops.length);
@@ -64,6 +68,17 @@ describe("Xrp Api", () => {
64
68
  // so here we are checking that this limit is bypassed
65
69
  expect(ops.length).toBeGreaterThan(200);
66
70
  });
71
+
72
+ it("returns operations from latest, but in asc order", async () => {
73
+ // When
74
+ const [txDesc] = await api.listOperations(SENDER, { minHeight: 0, order: "desc" });
75
+
76
+ // Then
77
+ // Check if the result is sorted in ascending order
78
+ expect(txDesc[0].tx.block.height).toBeGreaterThanOrEqual(
79
+ txDesc[txDesc.length - 1].tx.block.height,
80
+ );
81
+ });
67
82
  });
68
83
 
69
84
  describe("lastBlock", () => {
@@ -82,13 +97,13 @@ describe("Xrp Api", () => {
82
97
  // Account with no transaction (at the time of this writing)
83
98
  const SENDER_WITH_NO_TRANSACTION = "rKtXXTVno77jhu6tto1MAXjepyuaKaLcqB";
84
99
 
85
- it("returns an amount above 0 when address has transactions", async () => {
100
+ it("returns a balance", async () => {
86
101
  // When
87
102
  const result = await api.getBalance(SENDER);
88
103
 
89
104
  // Then
90
105
  expect(result[0].asset).toEqual({ type: "native" });
91
- expect(result[0].value).toBeGreaterThan(BigInt(0));
106
+ expect(result[0].value).toBeGreaterThanOrEqual(BigInt(0));
92
107
  });
93
108
 
94
109
  it("returns 0 when address has no transaction", async () => {
@@ -162,6 +177,170 @@ describe("Xrp Api", () => {
162
177
  });
163
178
  });
164
179
 
180
+ describe("Xrp Api (mainnet)", () => {
181
+ const SENDER = "rn5BQvhksnPfbo277LtFks4iyYStPKGrnJ";
182
+ const api = createApi({ node: "https://xrp.coin.ledger.com" });
183
+
184
+ describe("estimateFees", () => {
185
+ it("returns a default value", async () => {
186
+ // Given
187
+ const amount = BigInt(100);
188
+
189
+ // When
190
+ const result = await api.estimateFees({
191
+ asset: { type: "native" },
192
+ type: "send",
193
+ sender: SENDER,
194
+ amount,
195
+ recipient: "r9m6MwViR4GnUNqoGXGa8eroBrZ9FAPHFS",
196
+ memo: {
197
+ type: "map",
198
+ memos: new Map(),
199
+ },
200
+ });
201
+
202
+ // Then
203
+ expect(result.value).toEqual(BigInt(10));
204
+ });
205
+ });
206
+
207
+ describe("listOperations", () => {
208
+ let ops: Operation[];
209
+
210
+ beforeAll(async () => {
211
+ const resp = await api.listOperations(SENDER, { minHeight: 0 });
212
+ ops = resp[0];
213
+ });
214
+
215
+ it("returns operations", async () => {
216
+ // https://xrpscan.com/account/rn5BQvhksnPfbo277LtFks4iyYStPKGrnJ
217
+ // as of 2025-08-29, the address has 398 transactions, 1 op per tx
218
+ expect(ops.length).toBeGreaterThanOrEqual(398);
219
+ const checkSet = new Set(ops.map(elt => elt.tx.hash));
220
+ expect(checkSet.size).toEqual(ops.length);
221
+ ops.forEach(operation => {
222
+ const isSenderOrReceipt =
223
+ operation.senders.includes(SENDER) || operation.recipients.includes(SENDER);
224
+ expect(isSenderOrReceipt).toBeTruthy();
225
+ });
226
+ });
227
+
228
+ it("returns IN operation", async () => {
229
+ // https://xrpscan.com/tx/8116AC2E4C6FE35C7C2AADA1FE88C32E1FFD0DA8D348855BAC3903B7153F1EFF
230
+ const inTx = {
231
+ hash: "8116AC2E4C6FE35C7C2AADA1FE88C32E1FFD0DA8D348855BAC3903B7153F1EFF",
232
+ amount: 6.713757,
233
+ recipient: SENDER,
234
+ sender: "r9m6MwViR4GnUNqoGXGa8eroBrZ9FAPHFS",
235
+ type: "IN",
236
+ fees: 0.000012,
237
+ };
238
+ const op = ops.find(o => o.tx.hash === inTx.hash) as Operation;
239
+ expect(op.tx.hash).toEqual(inTx.hash);
240
+ expect(op.value).toEqual(BigInt(inTx.amount * 1e6));
241
+ expect(op.recipients).toContain(inTx.recipient);
242
+ expect(op.senders).toContain(inTx.sender);
243
+ expect(op.type).toEqual(inTx.type);
244
+ expect(op.tx.fees).toEqual(BigInt(inTx.fees * 1e6));
245
+ });
246
+
247
+ it("returns OUT operation", async () => {
248
+ // https://xrpscan.com/tx/F9B0E5CC0A303C6099AFCE3C1DD9D8D80034F8C33041BF1E792C8236D2DF3F79
249
+ const outTx = {
250
+ hash: "F9B0E5CC0A303C6099AFCE3C1DD9D8D80034F8C33041BF1E792C8236D2DF3F79",
251
+ amount: 7.8,
252
+ recipient: "r9m6MwViR4GnUNqoGXGa8eroBrZ9FAPHFS",
253
+ sender: SENDER,
254
+ type: "OUT",
255
+ fees: 0.00001,
256
+ };
257
+ const op = ops.find(o => o.tx.hash === outTx.hash) as Operation;
258
+ expect(op.tx.hash).toEqual(outTx.hash);
259
+ expect(op.value).toEqual(BigInt(outTx.amount * 1e6));
260
+ expect(op.recipients).toContain(outTx.recipient);
261
+ expect(op.senders).toContain(outTx.sender);
262
+ expect(op.type).toEqual(outTx.type);
263
+ expect(op.tx.fees).toEqual(BigInt(outTx.fees * 1e6));
264
+ });
265
+ });
266
+
267
+ describe("lastBlock", () => {
268
+ it("returns last block info", async () => {
269
+ const result = await api.lastBlock();
270
+ expect(result.hash).toBeDefined();
271
+ expect(result.height).toBeDefined();
272
+ expect(result.time).toBeInstanceOf(Date);
273
+ });
274
+ });
275
+
276
+ describe("getBalance", () => {
277
+ it("returns an amount", async () => {
278
+ const result = await api.getBalance(SENDER);
279
+ expect(result[0].asset).toEqual({ type: "native" });
280
+ expect(result[0].value).toBeGreaterThanOrEqual(BigInt(0));
281
+ });
282
+ });
283
+
284
+ describe("craftTransaction", () => {
285
+ const RECIPIENT = "r9m6MwViR4GnUNqoGXGa8eroBrZ9FAPHFS";
286
+
287
+ it("returns a raw transaction", async () => {
288
+ const result = await api.craftTransaction({
289
+ asset: { type: "native" },
290
+ type: "send",
291
+ sender: SENDER,
292
+ recipient: RECIPIENT,
293
+ amount: BigInt(10),
294
+ memo: {
295
+ type: "map",
296
+ memos: new Map([["memos", ["testdata"]]]),
297
+ },
298
+ });
299
+ expect(result.length).toEqual(178);
300
+ });
301
+
302
+ it("should use default fees when user does not provide them for crafting a transaction", async () => {
303
+ const result = await api.craftTransaction({
304
+ asset: { type: "native" },
305
+ type: "send",
306
+ sender: SENDER,
307
+ recipient: RECIPIENT,
308
+ amount: BigInt(10),
309
+ memo: {
310
+ type: "map",
311
+ memos: new Map(),
312
+ },
313
+ });
314
+
315
+ expect(decode(result)).toMatchObject({
316
+ Fee: "10",
317
+ });
318
+ });
319
+
320
+ it("should use custom user fees when user provides it for crafting a transaction", async () => {
321
+ const customFees = 99n;
322
+ const result = await api.craftTransaction(
323
+ {
324
+ asset: { type: "native" },
325
+ type: "send",
326
+ sender: SENDER,
327
+ recipient: RECIPIENT,
328
+ amount: BigInt(10),
329
+ memo: {
330
+ type: "map",
331
+ memos: new Map(),
332
+ },
333
+ },
334
+ { value: customFees },
335
+ );
336
+
337
+ expect(decode(result)).toMatchObject({
338
+ Fee: customFees.toString(),
339
+ });
340
+ });
341
+ });
342
+ });
343
+
165
344
  // 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`.
166
345
  // The value hardcoded here depends on the value filled in the `.env` file.
167
346
  /*describe.skip("combine", () => {
@@ -106,7 +106,7 @@ describe("listOperations", () => {
106
106
  const txs = givenTxs(BigInt(10), BigInt(10), "src", "dest");
107
107
  // each time it's called it returns a marker, so in theory it would loop forever
108
108
  mockGetTransactions.mockResolvedValue(mockNetworkTxs(txs, defaultMarker));
109
- const [results, _] = await api.listOperations("src", { minHeight: 0 });
109
+ const [results, _] = await api.listOperations("src", { minHeight: 0, order: "asc" });
110
110
 
111
111
  // called 10 times because there is a hard limit of 10 iterations in case something goes wrong
112
112
  // with interpretation of the token (bug / explorer api changed ...)
@@ -122,7 +122,7 @@ describe("listOperations", () => {
122
122
  .mockReturnValueOnce(mockNetworkTxs(txs, defaultMarker))
123
123
  .mockReturnValueOnce(mockNetworkTxs(txs, undefined));
124
124
 
125
- const [results, _] = await api.listOperations("src", { minHeight: 0 });
125
+ const [results, _] = await api.listOperations("src", { minHeight: 0, order: "asc" });
126
126
 
127
127
  // called 2 times because the second time there is no marker
128
128
  expect(mockGetServerInfos).toHaveBeenCalledTimes(2);
@@ -171,15 +171,13 @@ describe("listOperations", () => {
171
171
  mockGetTransactions.mockResolvedValue(mockNetworkTxs([], undefined));
172
172
 
173
173
  // When
174
- const [results, _] = await api.listOperations(address, { minHeight: 0 });
174
+ const [results, _] = await api.listOperations(address, { minHeight: 0, order: "asc" });
175
175
 
176
176
  // Then
177
177
  // called twice because the marker is set the first time
178
178
  expect(mockGetServerInfos).toHaveBeenCalledTimes(2);
179
179
  expect(mockGetTransactions).toHaveBeenCalledTimes(2);
180
180
 
181
- // if expectedType is "OUT", compute value with fees (i.e. delivered_amount + Fee)
182
- const expectedValue = expectedType === "IN" ? deliveredAmount : deliveredAmount + fee;
183
181
  // the order is reversed so that the result is always sorted by newest tx first element of the list
184
182
  expect(results).toEqual([
185
183
  {
@@ -196,7 +194,7 @@ describe("listOperations", () => {
196
194
  },
197
195
  },
198
196
  type: expectedType,
199
- value: expectedValue,
197
+ value: deliveredAmount,
200
198
  senders: [opSender],
201
199
  recipients: [opDestination],
202
200
  details: {
@@ -224,7 +222,7 @@ describe("listOperations", () => {
224
222
  },
225
223
  },
226
224
  type: expectedType,
227
- value: expectedValue,
225
+ value: deliveredAmount,
228
226
  senders: [opSender],
229
227
  recipients: [opDestination],
230
228
  details: {
@@ -247,7 +245,7 @@ describe("listOperations", () => {
247
245
  },
248
246
  },
249
247
  type: expectedType,
250
- value: expectedValue,
248
+ value: deliveredAmount,
251
249
  senders: [opSender],
252
250
  recipients: [opDestination],
253
251
  details: {
package/src/api/index.ts CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  MemoInput,
28
28
  } from "../logic";
29
29
  import { ListOperationsOptions, XrpMapMemo } from "../types";
30
+ import { Order } from "../types/model";
30
31
 
31
32
  export function createApi(config: XrpConfig): Api<XrpMapMemo> {
32
33
  coinConfig.setCoinConfig(() => ({ ...config, status: { type: "active" } }));
@@ -115,12 +116,13 @@ type PaginationState = {
115
116
  async function operationsFromHeight(
116
117
  address: string,
117
118
  minHeight: number,
119
+ order: Order = "asc",
118
120
  ): Promise<[Operation[], string]> {
119
121
  async function fetchNextPage(state: PaginationState): Promise<PaginationState> {
120
122
  const options: ListOperationsOptions = {
121
123
  limit: state.pageSize,
122
124
  minHeight: state.minHeight,
123
- order: "asc",
125
+ order: order,
124
126
  };
125
127
  if (state.apiNextCursor) {
126
128
  options.token = state.apiNextCursor;
@@ -162,9 +164,9 @@ async function operationsFromHeight(
162
164
 
163
165
  // NOTE: double check
164
166
  async function operations(address: string, pagination: Pagination): Promise<[Operation[], string]> {
165
- const { minHeight, lastPagingToken } = pagination;
167
+ const { minHeight, lastPagingToken, order } = pagination;
166
168
  if (minHeight) {
167
- return await operationsFromHeight(address, minHeight);
169
+ return await operationsFromHeight(address, minHeight, order);
168
170
  }
169
171
  const isInitSync = lastPagingToken === "";
170
172
 
@@ -172,5 +174,5 @@ async function operations(address: string, pagination: Pagination): Promise<[Ope
172
174
  minHeight: isInitSync ? 0 : parseInt(lastPagingToken || "0", 10),
173
175
  };
174
176
  // TODO token must be implemented properly (waiting ack from the design document)
175
- return await operationsFromHeight(address, newPagination.minHeight);
177
+ return await operationsFromHeight(address, newPagination.minHeight, order);
176
178
  }
@@ -41,7 +41,7 @@ describe("listOperations", () => {
41
41
  // Given
42
42
  mockNetworkGetTransactions.mockResolvedValue(mockNetworkTxs([]));
43
43
  // When
44
- const [results, token] = await listOperations("any address", { minHeight: 0 });
44
+ const [results, token] = await listOperations("any address", { minHeight: 0, order: "asc" });
45
45
  // Then
46
46
  expect(mockGetServerInfos).toHaveBeenCalledTimes(1);
47
47
  expect(mockNetworkGetTransactions).toHaveBeenCalledTimes(1);
@@ -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
 
@@ -21,7 +21,7 @@ export type XrpMemoValueMap = {
21
21
  };
22
22
  export type XrpMapMemo = TypedMapMemo<XrpMemoValueMap>;
23
23
 
24
- type Order = "asc" | "desc";
24
+ export type Order = "asc" | "desc";
25
25
 
26
26
  export type ListOperationsOptions = {
27
27
  // pagination: