@ledgerhq/coin-aptos 1.9.0-next.0 → 2.0.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.
Files changed (159) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +36 -10
  3. package/lib/__tests__/api/index.test.js +61 -5
  4. package/lib/__tests__/api/index.test.js.map +1 -1
  5. package/lib/__tests__/bridge/buildTransaction.test.js +57 -2
  6. package/lib/__tests__/bridge/buildTransaction.test.js.map +1 -1
  7. package/lib/__tests__/bridge/getFeesForTransaction.test.js +109 -8
  8. package/lib/__tests__/bridge/getFeesForTransaction.test.js.map +1 -1
  9. package/lib/__tests__/bridge/getTransactionStatus.test.js +175 -37
  10. package/lib/__tests__/bridge/getTransactionStatus.test.js.map +1 -1
  11. package/lib/__tests__/bridge/logic.test.js +889 -115
  12. package/lib/__tests__/bridge/logic.test.js.map +1 -1
  13. package/lib/__tests__/bridge/signOperation.test.js +128 -2
  14. package/lib/__tests__/bridge/signOperation.test.js.map +1 -1
  15. package/lib/__tests__/bridge/synchronisation.test.js +1214 -67
  16. package/lib/__tests__/bridge/synchronisation.test.js.map +1 -1
  17. package/lib/api/graphql/queries.js +6 -6
  18. package/lib/api/graphql/types.d.ts +9 -9
  19. package/lib/api/graphql/types.d.ts.map +1 -1
  20. package/lib/api/index.d.ts +5 -2
  21. package/lib/api/index.d.ts.map +1 -1
  22. package/lib/api/index.js +30 -4
  23. package/lib/api/index.js.map +1 -1
  24. package/lib/bridge/bridge.fixture.d.ts +2 -0
  25. package/lib/bridge/bridge.fixture.d.ts.map +1 -1
  26. package/lib/bridge/bridge.fixture.js +85 -13
  27. package/lib/bridge/bridge.fixture.js.map +1 -1
  28. package/lib/bridge/buildTransaction.d.ts.map +1 -1
  29. package/lib/bridge/buildTransaction.js +29 -3
  30. package/lib/bridge/buildTransaction.js.map +1 -1
  31. package/lib/bridge/estimateMaxSpendable.js +1 -1
  32. package/lib/bridge/estimateMaxSpendable.js.map +1 -1
  33. package/lib/bridge/getFeesForTransaction.d.ts.map +1 -1
  34. package/lib/bridge/getFeesForTransaction.js +12 -7
  35. package/lib/bridge/getFeesForTransaction.js.map +1 -1
  36. package/lib/bridge/getTransactionStatus.d.ts.map +1 -1
  37. package/lib/bridge/getTransactionStatus.js +28 -14
  38. package/lib/bridge/getTransactionStatus.js.map +1 -1
  39. package/lib/bridge/logic.d.ts +13 -8
  40. package/lib/bridge/logic.d.ts.map +1 -1
  41. package/lib/bridge/logic.js +152 -54
  42. package/lib/bridge/logic.js.map +1 -1
  43. package/lib/bridge/prepareTransaction.d.ts.map +1 -1
  44. package/lib/bridge/prepareTransaction.js +2 -2
  45. package/lib/bridge/prepareTransaction.js.map +1 -1
  46. package/lib/bridge/signOperation.d.ts.map +1 -1
  47. package/lib/bridge/signOperation.js +17 -3
  48. package/lib/bridge/signOperation.js.map +1 -1
  49. package/lib/bridge/synchronisation.d.ts +15 -0
  50. package/lib/bridge/synchronisation.d.ts.map +1 -1
  51. package/lib/bridge/synchronisation.js +127 -4
  52. package/lib/bridge/synchronisation.js.map +1 -1
  53. package/lib/constants.d.ts +5 -1
  54. package/lib/constants.d.ts.map +1 -1
  55. package/lib/constants.js +6 -2
  56. package/lib/constants.js.map +1 -1
  57. package/lib/test/bot-specs.d.ts.map +1 -1
  58. package/lib/test/bot-specs.js +40 -1
  59. package/lib/test/bot-specs.js.map +1 -1
  60. package/lib/test/bridgeDatasetTest.d.ts.map +1 -1
  61. package/lib/test/bridgeDatasetTest.js +43 -62
  62. package/lib/test/bridgeDatasetTest.js.map +1 -1
  63. package/lib/test/speculos-deviceActions.d.ts +1 -0
  64. package/lib/test/speculos-deviceActions.d.ts.map +1 -1
  65. package/lib/test/speculos-deviceActions.js +37 -5
  66. package/lib/test/speculos-deviceActions.js.map +1 -1
  67. package/lib/types/index.d.ts +18 -7
  68. package/lib/types/index.d.ts.map +1 -1
  69. package/lib-es/__tests__/api/index.test.js +61 -5
  70. package/lib-es/__tests__/api/index.test.js.map +1 -1
  71. package/lib-es/__tests__/bridge/buildTransaction.test.js +58 -3
  72. package/lib-es/__tests__/bridge/buildTransaction.test.js.map +1 -1
  73. package/lib-es/__tests__/bridge/getFeesForTransaction.test.js +110 -9
  74. package/lib-es/__tests__/bridge/getFeesForTransaction.test.js.map +1 -1
  75. package/lib-es/__tests__/bridge/getTransactionStatus.test.js +177 -39
  76. package/lib-es/__tests__/bridge/getTransactionStatus.test.js.map +1 -1
  77. package/lib-es/__tests__/bridge/logic.test.js +891 -117
  78. package/lib-es/__tests__/bridge/logic.test.js.map +1 -1
  79. package/lib-es/__tests__/bridge/signOperation.test.js +128 -2
  80. package/lib-es/__tests__/bridge/signOperation.test.js.map +1 -1
  81. package/lib-es/__tests__/bridge/synchronisation.test.js +1213 -69
  82. package/lib-es/__tests__/bridge/synchronisation.test.js.map +1 -1
  83. package/lib-es/api/graphql/queries.js +6 -6
  84. package/lib-es/api/graphql/types.d.ts +9 -9
  85. package/lib-es/api/graphql/types.d.ts.map +1 -1
  86. package/lib-es/api/index.d.ts +5 -2
  87. package/lib-es/api/index.d.ts.map +1 -1
  88. package/lib-es/api/index.js +30 -4
  89. package/lib-es/api/index.js.map +1 -1
  90. package/lib-es/bridge/bridge.fixture.d.ts +2 -0
  91. package/lib-es/bridge/bridge.fixture.d.ts.map +1 -1
  92. package/lib-es/bridge/bridge.fixture.js +82 -12
  93. package/lib-es/bridge/bridge.fixture.js.map +1 -1
  94. package/lib-es/bridge/buildTransaction.d.ts.map +1 -1
  95. package/lib-es/bridge/buildTransaction.js +30 -4
  96. package/lib-es/bridge/buildTransaction.js.map +1 -1
  97. package/lib-es/bridge/estimateMaxSpendable.js +1 -1
  98. package/lib-es/bridge/estimateMaxSpendable.js.map +1 -1
  99. package/lib-es/bridge/getFeesForTransaction.d.ts.map +1 -1
  100. package/lib-es/bridge/getFeesForTransaction.js +13 -8
  101. package/lib-es/bridge/getFeesForTransaction.js.map +1 -1
  102. package/lib-es/bridge/getTransactionStatus.d.ts.map +1 -1
  103. package/lib-es/bridge/getTransactionStatus.js +28 -14
  104. package/lib-es/bridge/getTransactionStatus.js.map +1 -1
  105. package/lib-es/bridge/logic.d.ts +13 -8
  106. package/lib-es/bridge/logic.d.ts.map +1 -1
  107. package/lib-es/bridge/logic.js +146 -52
  108. package/lib-es/bridge/logic.js.map +1 -1
  109. package/lib-es/bridge/prepareTransaction.d.ts.map +1 -1
  110. package/lib-es/bridge/prepareTransaction.js +2 -2
  111. package/lib-es/bridge/prepareTransaction.js.map +1 -1
  112. package/lib-es/bridge/signOperation.d.ts.map +1 -1
  113. package/lib-es/bridge/signOperation.js +17 -3
  114. package/lib-es/bridge/signOperation.js.map +1 -1
  115. package/lib-es/bridge/synchronisation.d.ts +15 -0
  116. package/lib-es/bridge/synchronisation.d.ts.map +1 -1
  117. package/lib-es/bridge/synchronisation.js +123 -3
  118. package/lib-es/bridge/synchronisation.js.map +1 -1
  119. package/lib-es/constants.d.ts +5 -1
  120. package/lib-es/constants.d.ts.map +1 -1
  121. package/lib-es/constants.js +5 -1
  122. package/lib-es/constants.js.map +1 -1
  123. package/lib-es/test/bot-specs.d.ts.map +1 -1
  124. package/lib-es/test/bot-specs.js +41 -2
  125. package/lib-es/test/bot-specs.js.map +1 -1
  126. package/lib-es/test/bridgeDatasetTest.d.ts.map +1 -1
  127. package/lib-es/test/bridgeDatasetTest.js +43 -59
  128. package/lib-es/test/bridgeDatasetTest.js.map +1 -1
  129. package/lib-es/test/speculos-deviceActions.d.ts +1 -0
  130. package/lib-es/test/speculos-deviceActions.d.ts.map +1 -1
  131. package/lib-es/test/speculos-deviceActions.js +36 -4
  132. package/lib-es/test/speculos-deviceActions.js.map +1 -1
  133. package/lib-es/types/index.d.ts +18 -7
  134. package/lib-es/types/index.d.ts.map +1 -1
  135. package/package.json +9 -8
  136. package/src/__tests__/api/index.test.ts +75 -5
  137. package/src/__tests__/bridge/buildTransaction.test.ts +85 -3
  138. package/src/__tests__/bridge/getFeesForTransaction.test.ts +144 -9
  139. package/src/__tests__/bridge/getTransactionStatus.test.ts +217 -38
  140. package/src/__tests__/bridge/logic.test.ts +922 -118
  141. package/src/__tests__/bridge/signOperation.test.ts +141 -2
  142. package/src/__tests__/bridge/synchronisation.test.ts +1265 -71
  143. package/src/api/graphql/queries.ts +6 -6
  144. package/src/api/graphql/types.ts +9 -9
  145. package/src/api/index.ts +32 -5
  146. package/src/bridge/bridge.fixture.ts +91 -12
  147. package/src/bridge/buildTransaction.ts +39 -6
  148. package/src/bridge/estimateMaxSpendable.ts +1 -1
  149. package/src/bridge/getFeesForTransaction.ts +14 -9
  150. package/src/bridge/getTransactionStatus.ts +35 -13
  151. package/src/bridge/logic.ts +202 -63
  152. package/src/bridge/prepareTransaction.ts +4 -3
  153. package/src/bridge/signOperation.ts +19 -3
  154. package/src/bridge/synchronisation.ts +170 -3
  155. package/src/constants.ts +12 -1
  156. package/src/test/bot-specs.ts +63 -3
  157. package/src/test/bridgeDatasetTest.ts +45 -59
  158. package/src/test/speculos-deviceActions.ts +40 -4
  159. package/src/types/index.ts +15 -1
@@ -4,20 +4,37 @@ import {
4
4
  InputEntryFunctionData,
5
5
  MoveResource,
6
6
  WriteSetChange,
7
+ WriteSetChangeWriteResource,
7
8
  } from "@aptos-labs/ts-sdk";
8
9
  import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets/currencies";
9
- import type { Operation, OperationType } from "@ledgerhq/types-live";
10
+ import type { Account, Operation, OperationType, TokenAccount } from "@ledgerhq/types-live";
11
+ import {
12
+ decodeTokenAccountId,
13
+ encodeTokenAccountId,
14
+ findSubAccountById,
15
+ isTokenAccount,
16
+ } from "@ledgerhq/coin-framework/account/index";
10
17
  import BigNumber from "bignumber.js";
11
18
  import { encodeOperationId } from "@ledgerhq/coin-framework/operation";
12
19
  import {
13
- APTOS_COIN_CHANGE,
20
+ APTOS_ASSET_ID,
21
+ APTOS_FUNGIBLE_STORE,
14
22
  BATCH_TRANSFER_TYPES,
15
23
  DELEGATION_POOL_TYPES,
16
24
  DIRECTION,
17
- TRANSFER_TYPES,
18
- WRITE_RESOURCE,
25
+ COIN_TRANSFER_TYPES,
26
+ FA_TRANSFER_TYPES,
27
+ APTOS_OBJECT_CORE,
19
28
  } from "../constants";
20
- import type { AptosMoveResource, AptosTransaction, TransactionOptions } from "../types";
29
+ import type {
30
+ AptosFungibleoObjectCoreResourceData,
31
+ AptosFungibleStoreResourceData,
32
+ AptosMoveResource,
33
+ AptosTransaction,
34
+ Transaction,
35
+ TransactionOptions,
36
+ } from "../types";
37
+ import { findTokenByAddressInCurrency } from "@ledgerhq/cryptoassets";
21
38
 
22
39
  export const DEFAULT_GAS = new BigNumber(200);
23
40
  export const DEFAULT_GAS_PRICE = new BigNumber(100);
@@ -30,13 +47,21 @@ export function isTestnet(currencyId: string): boolean {
30
47
  }
31
48
 
32
49
  export const getMaxSendBalance = (
33
- amount: BigNumber,
34
50
  gas: BigNumber,
35
51
  gasPrice: BigNumber,
52
+ account: Account,
53
+ transaction?: Transaction,
36
54
  ): BigNumber => {
55
+ const tokenAccount = findSubAccountById(account, transaction?.subAccountId ?? "");
56
+ const fromTokenAccount = tokenAccount && isTokenAccount(tokenAccount);
57
+
37
58
  const totalGas = gas.multipliedBy(gasPrice);
38
59
 
39
- return amount.gt(totalGas) ? amount.minus(totalGas) : new BigNumber(0);
60
+ return fromTokenAccount
61
+ ? tokenAccount.spendableBalance
62
+ : account.spendableBalance.gt(totalGas)
63
+ ? account.spendableBalance.minus(totalGas)
64
+ : new BigNumber(0);
40
65
  };
41
66
 
42
67
  export function normalizeTransactionOptions(options: TransactionOptions): TransactionOptions {
@@ -88,9 +113,10 @@ export const txsToOps = (
88
113
  info: { address: string },
89
114
  id: string,
90
115
  txs: (AptosTransaction | null)[],
91
- ): Operation[] => {
116
+ ): [Operation[], Operation[]] => {
92
117
  const { address } = info;
93
118
  const ops: Operation[] = [];
119
+ const opsTokens: Operation[] = [];
94
120
 
95
121
  txs.forEach(tx => {
96
122
  if (tx !== null) {
@@ -107,10 +133,12 @@ export const txsToOps = (
107
133
  return; // skip transaction without functions in payload
108
134
  }
109
135
 
110
- const { amount_in, amount_out } = getAptosAmounts(tx, address);
111
- op.value = calculateAmount(tx.sender, address, op.fee, amount_in, amount_out);
136
+ const { coin_id, amount_in, amount_out } = getCoinAndAmounts(tx, address);
137
+ op.value = calculateAmount(tx.sender, address, amount_in, amount_out);
112
138
  op.type = compareAddress(tx.sender, address) ? DIRECTION.OUT : DIRECTION.IN;
113
139
  op.senders.push(tx.sender);
140
+ op.hasFailed = !tx.success;
141
+ op.id = encodeOperationId(op.accountId, tx.hash, op.type);
114
142
 
115
143
  processRecipients(payload, address, op, function_address);
116
144
 
@@ -119,13 +147,30 @@ export const txsToOps = (
119
147
  op.type = DIRECTION.UNKNOWN;
120
148
  }
121
149
 
122
- op.hasFailed = !tx.success;
123
- op.id = encodeOperationId(id, tx.hash, op.type);
124
- if (op.type !== DIRECTION.UNKNOWN) ops.push(op);
150
+ if (op.type !== DIRECTION.UNKNOWN && coin_id !== null) {
151
+ if (coin_id === APTOS_ASSET_ID) {
152
+ ops.push(op);
153
+ } else {
154
+ const token = findTokenByAddressInCurrency(coin_id.toLowerCase(), "aptos");
155
+ if (token !== undefined) {
156
+ op.accountId = encodeTokenAccountId(id, token);
157
+ opsTokens.push(op);
158
+
159
+ if (op.type === DIRECTION.OUT) {
160
+ ops.push({
161
+ ...op,
162
+ accountId: decodeTokenAccountId(op.accountId).accountId,
163
+ value: op.fee,
164
+ type: "FEES",
165
+ });
166
+ }
167
+ }
168
+ }
169
+ }
125
170
  }
126
171
  });
127
172
 
128
- return ops;
173
+ return [ops, opsTokens];
129
174
  };
130
175
 
131
176
  export function compareAddress(addressA: string, addressB: string) {
@@ -151,7 +196,7 @@ export function processRecipients(
151
196
  ): void {
152
197
  // get recipients buy 3 groups
153
198
  if (
154
- (TRANSFER_TYPES.includes(payload.function) ||
199
+ (COIN_TRANSFER_TYPES.includes(payload.function) ||
155
200
  DELEGATION_POOL_TYPES.includes(payload.function)) &&
156
201
  payload.functionArguments &&
157
202
  payload.functionArguments.length > 0 &&
@@ -159,6 +204,15 @@ export function processRecipients(
159
204
  ) {
160
205
  // 1. Transfer like functions (includes some delegation pool functions)
161
206
  op.recipients.push(payload.functionArguments[0].toString());
207
+ } else if (
208
+ FA_TRANSFER_TYPES.includes(payload.function) &&
209
+ payload.functionArguments &&
210
+ payload.functionArguments.length > 1 &&
211
+ typeof payload.functionArguments[0] === "object" &&
212
+ typeof payload.functionArguments[1] === "string"
213
+ ) {
214
+ // 1. Transfer like functions (includes some delegation pool functions)
215
+ op.recipients.push(payload.functionArguments[1].toString());
162
216
  } else if (
163
217
  BATCH_TRANSFER_TYPES.includes(payload.function) &&
164
218
  payload.functionArguments &&
@@ -179,87 +233,172 @@ export function processRecipients(
179
233
  }
180
234
  }
181
235
 
182
- function checkWriteSets(tx: AptosTransaction, event: Event, event_name: string): boolean {
183
- return tx.changes.some(change => {
184
- return isChangeOfAptos(change, event, event_name);
185
- });
186
- }
187
-
188
- export function isChangeOfAptos(
189
- writeSetChange: WriteSetChange,
236
+ export function getEventCoinAddress(
237
+ change: WriteSetChangeWriteResource,
190
238
  event: Event,
191
239
  event_name: string,
192
- ): boolean {
193
- // to validate the event is related to Aptos Tokens we need to find change of type "write_resource"
194
- // with the same guid as event
195
- if (writeSetChange.type !== WRITE_RESOURCE) {
196
- return false;
240
+ ): string | null {
241
+ const change_data = change.data;
242
+
243
+ const mr = change_data as MoveResource<AptosMoveResource>; // -> this is data that we want to parse
244
+
245
+ if (!(event_name in mr.data)) {
246
+ return null;
197
247
  }
198
248
 
199
- if (!("data" in writeSetChange)) {
200
- return false;
249
+ const change_event_data = mr.data[event_name];
250
+ if (
251
+ change_event_data.guid.id.addr !== event.guid.account_address ||
252
+ change_event_data.guid.id.creation_num !== event.guid.creation_number
253
+ ) {
254
+ return null;
201
255
  }
202
256
 
203
- const change_data = writeSetChange.data;
257
+ const address = extractAddress(mr.type);
258
+
259
+ return address;
260
+ }
204
261
 
205
- if (!("type" in change_data)) {
206
- return false;
262
+ export function getEventFAAddress(
263
+ change: WriteSetChangeWriteResource,
264
+ event: Event,
265
+ _event_name: string,
266
+ ): string | null {
267
+ const change_data = change.data;
268
+
269
+ if (change_data.type !== APTOS_FUNGIBLE_STORE) {
270
+ return null;
207
271
  }
208
272
 
209
- const mr = change_data as MoveResource<AptosMoveResource>;
273
+ const mr = change_data as MoveResource<AptosFungibleStoreResourceData>;
210
274
 
211
- if (mr.type !== APTOS_COIN_CHANGE) {
212
- return false;
275
+ if (change.address !== event.data.store) {
276
+ return null;
213
277
  }
214
278
 
215
- const change_event_data = mr.data[event_name];
279
+ return mr.data.metadata.inner;
280
+ }
216
281
 
217
- return (
218
- change_event_data.guid.id.addr === event.guid.account_address &&
219
- change_event_data.guid.id.creation_num === event.guid.creation_number
220
- );
282
+ export function getResourceAddress(
283
+ tx: AptosTransaction,
284
+ event: Event,
285
+ event_name: string,
286
+ getAddressProcessor: (
287
+ change: WriteSetChangeWriteResource,
288
+ event: Event,
289
+ event_name: string,
290
+ ) => string | null,
291
+ ): string | null {
292
+ for (const change of tx.changes) {
293
+ if (isWriteSetChangeWriteResource(change)) {
294
+ const address = getAddressProcessor(change, event, event_name);
295
+ if (address !== null) {
296
+ return address;
297
+ }
298
+ }
299
+ }
300
+ return null;
301
+ }
302
+
303
+ function isWriteSetChangeWriteResource(
304
+ change: WriteSetChange,
305
+ ): change is WriteSetChangeWriteResource {
306
+ return (change as WriteSetChangeWriteResource).data !== undefined;
307
+ }
308
+
309
+ export function checkFAOwner(tx: AptosTransaction, event: Event, user_address: string): boolean {
310
+ for (const change of tx.changes) {
311
+ if (isWriteSetChangeWriteResource(change)) {
312
+ const storeData = change.data as MoveResource<AptosFungibleoObjectCoreResourceData>;
313
+ if (
314
+ change.address == event.data.store &&
315
+ storeData.type == APTOS_OBJECT_CORE &&
316
+ storeData.data.owner == user_address
317
+ ) {
318
+ return true;
319
+ }
320
+ }
321
+ }
322
+ return false;
221
323
  }
222
324
 
223
- export function getAptosAmounts(
325
+ export function getCoinAndAmounts(
224
326
  tx: AptosTransaction,
225
327
  address: string,
226
- ): { amount_in: BigNumber; amount_out: BigNumber } {
227
- let amount_in = new BigNumber(0);
228
- let amount_out = new BigNumber(0);
328
+ ): { coin_id: string | null; amount_in: BigNumber; amount_out: BigNumber } {
329
+ let coin_id: string | null = null;
330
+ let amount_in = BigNumber(0);
331
+ let amount_out = BigNumber(0);
332
+
229
333
  // collect all events related to the address and calculate the overall amounts
230
334
  tx.events.forEach(event => {
231
- if (compareAddress(event.guid.account_address, address)) {
232
- switch (event.type) {
233
- case "0x1::coin::WithdrawEvent":
234
- if (checkWriteSets(tx, event, "withdraw_events")) {
235
- amount_out = amount_out.plus(event.data.amount);
236
- }
237
- break;
238
- case "0x1::coin::DepositEvent":
239
- if (checkWriteSets(tx, event, "deposit_events")) {
240
- amount_in = amount_in.plus(event.data.amount);
335
+ switch (event.type) {
336
+ case "0x1::coin::WithdrawEvent":
337
+ if (compareAddress(event.guid.account_address, address)) {
338
+ coin_id = getResourceAddress(tx, event, "withdraw_events", getEventCoinAddress);
339
+ amount_out = amount_out.plus(event.data.amount);
340
+ }
341
+ break;
342
+ case "0x1::coin::DepositEvent":
343
+ if (compareAddress(event.guid.account_address, address)) {
344
+ coin_id = getResourceAddress(tx, event, "deposit_events", getEventCoinAddress);
345
+ amount_in = amount_in.plus(event.data.amount);
346
+ }
347
+ break;
348
+ case "0x1::fungible_asset::Withdraw":
349
+ if (checkFAOwner(tx, event, address)) {
350
+ coin_id = getResourceAddress(tx, event, "withdraw_events", getEventFAAddress);
351
+ amount_out = amount_out.plus(event.data.amount);
352
+ }
353
+ break;
354
+ case "0x1::fungible_asset::Deposit":
355
+ if (checkFAOwner(tx, event, address)) {
356
+ coin_id = getResourceAddress(tx, event, "deposit_events", getEventFAAddress);
357
+ amount_in = amount_in.plus(event.data.amount);
358
+ }
359
+ break;
360
+ case "0x1::transaction_fee::FeeStatement":
361
+ if (tx.sender === address) {
362
+ if (coin_id === null) coin_id = APTOS_ASSET_ID;
363
+ if (coin_id === APTOS_ASSET_ID) {
364
+ const fees = BigNumber(tx.gas_unit_price).times(BigNumber(tx.gas_used));
365
+ amount_out = amount_out.plus(fees);
241
366
  }
242
- break;
243
- }
367
+ }
368
+ break;
244
369
  }
245
370
  });
246
- return { amount_in, amount_out };
371
+ return { coin_id, amount_in, amount_out }; // TODO: manage situation when there are several coinID from the events parsing
247
372
  }
248
373
 
249
374
  export function calculateAmount(
250
375
  sender: string,
251
376
  address: string,
252
- fee: BigNumber,
253
377
  amount_in: BigNumber,
254
378
  amount_out: BigNumber,
255
379
  ): BigNumber {
256
380
  const is_sender: boolean = compareAddress(sender, address);
257
- // Include fees if our address is the sender
258
- if (is_sender) {
259
- amount_out = amount_out.plus(fee);
260
- }
261
381
  // LL negates the amount for SEND transactions
262
382
  // to show positive amount on the send transaction (ex: in "cancel" tx, when amount will be returned to our account)
263
383
  // we need to make it negative
264
384
  return is_sender ? amount_out.minus(amount_in) : amount_in.minus(amount_out);
265
385
  }
386
+
387
+ /**
388
+ * Extracts the address from a string like "0x1::coin::CoinStore<address::module::type>"
389
+ * @param {string} str - The input string containing the address.
390
+ * @returns {string | null} - The extracted address or null if not found.
391
+ */
392
+ function extractAddress(str: string): string | null {
393
+ const match = str.match(/<([^<>]+)>{1}$/);
394
+ return match ? match[1] : null;
395
+ }
396
+
397
+ export function getTokenAccount(
398
+ account: Account,
399
+ transaction: Transaction,
400
+ ): TokenAccount | undefined {
401
+ const tokenAccount = findSubAccountById(account, transaction.subAccountId ?? "");
402
+ const fromTokenAccount = tokenAccount && isTokenAccount(tokenAccount);
403
+ return fromTokenAccount ? tokenAccount : undefined;
404
+ }
@@ -1,6 +1,5 @@
1
1
  import type { Account } from "@ledgerhq/types-live";
2
2
  import BigNumber from "bignumber.js";
3
-
4
3
  import { AptosAPI } from "../api";
5
4
  import { getEstimatedGas } from "./getFeesForTransaction";
6
5
  import type { Transaction } from "../types";
@@ -28,9 +27,10 @@ const prepareTransaction = async (
28
27
  if (transaction.useAllAmount) {
29
28
  // we will use this amount in simulation, to estimate gas
30
29
  transaction.amount = getMaxSendBalance(
31
- account.spendableBalance,
32
30
  new BigNumber(DEFAULT_GAS),
33
31
  new BigNumber(DEFAULT_GAS_PRICE),
32
+ account,
33
+ transaction,
34
34
  );
35
35
  }
36
36
 
@@ -39,9 +39,10 @@ const prepareTransaction = async (
39
39
  if (transaction.useAllAmount) {
40
40
  // correct the transaction amount according to estimated fees
41
41
  transaction.amount = getMaxSendBalance(
42
- account.spendableBalance,
43
42
  BigNumber(estimate.maxGasAmount),
44
43
  BigNumber(estimate.gasUnitPrice),
44
+ account,
45
+ transaction,
45
46
  );
46
47
  }
47
48
 
@@ -9,6 +9,7 @@ import { AptosAPI } from "../api";
9
9
  import { SignerContext } from "@ledgerhq/coin-framework/signer";
10
10
  import { AptosSigner } from "../types";
11
11
  import { signTransaction } from "../network";
12
+ import { findSubAccountById } from "@ledgerhq/coin-framework/account/helpers";
12
13
 
13
14
  export const getAddress = (a: Account) => ({
14
15
  address: a.freshAddress,
@@ -45,14 +46,15 @@ const buildSignOperation =
45
46
  recipients.push(transaction.recipient);
46
47
  }
47
48
 
49
+ const subAccount =
50
+ !!transaction.subAccountId && findSubAccountById(account, transaction.subAccountId);
51
+
48
52
  // build optimistic operation
49
53
  const operation: Operation = {
50
54
  id: encodeOperationId(accountId, hash, type),
51
55
  hash,
52
56
  type,
53
- value: transaction.useAllAmount
54
- ? account.balance.minus(fee)
55
- : transaction.amount.plus(fee),
57
+ value: subAccount ? fee : transaction.amount.plus(fee),
56
58
  fee,
57
59
  extra,
58
60
  blockHash: null,
@@ -62,6 +64,20 @@ const buildSignOperation =
62
64
  accountId,
63
65
  date: new Date(),
64
66
  transactionSequenceNumber: Number(rawTx.sequence_number),
67
+ subOperations: subAccount
68
+ ? [
69
+ {
70
+ id: encodeOperationId(subAccount.id, "", "OUT"),
71
+ type: "OUT",
72
+ accountId: transaction.subAccountId,
73
+ senders: [account.freshAddress],
74
+ recipients: [transaction.recipient],
75
+ value: transaction.amount,
76
+ fee,
77
+ date: new Date(),
78
+ } as Operation,
79
+ ]
80
+ : [],
65
81
  };
66
82
 
67
83
  o.next({
@@ -1,9 +1,160 @@
1
+ import { inferSubOperations } from "@ledgerhq/coin-framework/serialization/index";
1
2
  import { decodeAccountId, encodeAccountId } from "@ledgerhq/coin-framework/account";
2
3
  import type { GetAccountShape } from "@ledgerhq/coin-framework/bridge/jsHelpers";
3
4
  import { mergeOps } from "@ledgerhq/coin-framework/bridge/jsHelpers";
4
5
  import { AptosAPI } from "../api";
5
6
  import { txsToOps } from "./logic";
6
7
  import type { AptosAccount } from "../types";
8
+ import { Account, Operation, TokenAccount } from "@ledgerhq/types-live";
9
+ import { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets";
10
+ import {
11
+ decodeTokenAccountId,
12
+ emptyHistoryCache,
13
+ encodeTokenAccountId,
14
+ } from "@ledgerhq/coin-framework/account/index";
15
+ import { AccountShapeInfo } from "@ledgerhq/coin-framework/bridge/jsHelpers";
16
+
17
+ /**
18
+ * List of properties of a sub account that can be updated when 2 "identical" accounts are found
19
+ */
20
+ const updatableSubAccountProperties: { name: string; isOps: boolean }[] = [
21
+ { name: "balance", isOps: false },
22
+ { name: "spendableBalance", isOps: false },
23
+ { name: "balanceHistoryCache", isOps: false },
24
+ { name: "operations", isOps: true },
25
+ { name: "pendingOperations", isOps: true },
26
+ ];
27
+ /**
28
+ * In charge of smartly merging sub accounts while maintaining references as much as possible
29
+ */
30
+ export const mergeSubAccounts = (
31
+ initialAccount: Account | undefined,
32
+ newSubAccounts: TokenAccount[],
33
+ ): Array<TokenAccount> => {
34
+ const oldSubAccounts: Array<TokenAccount> | undefined = initialAccount?.subAccounts;
35
+ if (!oldSubAccounts) {
36
+ return newSubAccounts;
37
+ }
38
+
39
+ // Creating a map of already existing sub accounts by id
40
+ const oldSubAccountsById: { [key: string]: TokenAccount } = {};
41
+ for (const oldSubAccount of oldSubAccounts) {
42
+ oldSubAccountsById[oldSubAccount.id!] = oldSubAccount;
43
+ }
44
+
45
+ // Looping on new sub accounts to compare them with already existing ones
46
+ // Already existing will be updated if necessary (see `updatableSubAccountProperties`)
47
+ // Fresh new sub accounts will be added/pushed after already existing
48
+ const newSubAccountsToAdd: TokenAccount[] = [];
49
+ for (const newSubAccount of newSubAccounts) {
50
+ const duplicatedAccount: TokenAccount | undefined = oldSubAccountsById[newSubAccount.id!];
51
+
52
+ // If this sub account was not already in the initialAccount
53
+ if (!duplicatedAccount) {
54
+ // We'll add it later
55
+ newSubAccountsToAdd.push(newSubAccount);
56
+ continue;
57
+ }
58
+
59
+ const updates: Partial<TokenAccount> & { [key: string]: any } = {};
60
+ for (const { name, isOps } of updatableSubAccountProperties) {
61
+ if (!isOps) {
62
+ if (
63
+ newSubAccount[name as keyof TokenAccount] !==
64
+ duplicatedAccount[name as keyof TokenAccount]
65
+ ) {
66
+ updates[name] = newSubAccount[name as keyof TokenAccount];
67
+ }
68
+ } else {
69
+ updates[name] =
70
+ mergeOps(
71
+ duplicatedAccount[name as keyof TokenAccount] as Operation[],
72
+ newSubAccount[name as keyof TokenAccount] as Operation[],
73
+ ) || [];
74
+ }
75
+ }
76
+
77
+ // Updating the operationsCount in case the mergeOps changed it
78
+ updates.operationsCount =
79
+ updates.operations?.length || duplicatedAccount?.operations?.length || 0;
80
+
81
+ // Modifying the Map with the updated sub account with a new ref
82
+ oldSubAccountsById[newSubAccount.id!] = {
83
+ ...duplicatedAccount,
84
+ ...updates,
85
+ };
86
+ }
87
+ const updatedSubAccounts = Object.values(oldSubAccountsById);
88
+ return [...updatedSubAccounts, ...newSubAccountsToAdd];
89
+ };
90
+
91
+ /**
92
+ * Fetch the balance for a token and creates a TokenAccount based on this and the provided operations
93
+ */
94
+ export const getSubAccountShape = async (
95
+ currency: CryptoCurrency,
96
+ address: string,
97
+ parentId: string,
98
+ token: TokenCurrency,
99
+ operations: Operation[],
100
+ ): Promise<TokenAccount> => {
101
+ const aptosClient = new AptosAPI(currency.id);
102
+ const tokenAccountId = encodeTokenAccountId(parentId, token);
103
+ const balance = await aptosClient.getBalance(address, token);
104
+ const firstOperation = operations
105
+ .sort((a, b) => b.date.getTime() - a.date.getTime())
106
+ .at(operations.length - 1);
107
+
108
+ return {
109
+ type: "TokenAccount",
110
+ id: tokenAccountId,
111
+ parentId,
112
+ token,
113
+ balance,
114
+ spendableBalance: balance,
115
+ creationDate: firstOperation?.date || new Date(0),
116
+ operations,
117
+ operationsCount: operations.length,
118
+ pendingOperations: [],
119
+ balanceHistoryCache: emptyHistoryCache,
120
+ swapHistory: [],
121
+ };
122
+ };
123
+
124
+ /**
125
+ * Getting all token related operations in order to provide TokenAccounts
126
+ */
127
+ export const getSubAccounts = async (
128
+ infos: AccountShapeInfo<Account>,
129
+ address: string,
130
+ accountId: string,
131
+ lastTokenOperations: Operation[],
132
+ ): Promise<TokenAccount[]> => {
133
+ const { currency } = infos;
134
+
135
+ // Creating a Map of Operations by TokenCurrencies in order to know which TokenAccounts should be synced as well
136
+ const operationsByToken = lastTokenOperations.reduce<Map<TokenCurrency, Operation[]>>(
137
+ (acc, operation) => {
138
+ const { token } = decodeTokenAccountId(operation.accountId);
139
+ if (!token) return acc; // TODO: do we need to check blacklistedTokenIds
140
+
141
+ if (!acc.has(token)) {
142
+ acc.set(token, []);
143
+ }
144
+ acc.get(token)?.push(operation);
145
+ return acc;
146
+ },
147
+ new Map<TokenCurrency, Operation[]>(),
148
+ );
149
+
150
+ // Fetching all TokenAccounts possible and providing already filtered operations
151
+ const subAccountsPromises: Promise<TokenAccount>[] = [];
152
+ for (const [token, ops] of operationsByToken.entries()) {
153
+ subAccountsPromises.push(getSubAccountShape(currency, address, accountId, token, ops));
154
+ }
155
+
156
+ return Promise.all(subAccountsPromises);
157
+ };
7
158
 
8
159
  export const getAccountShape: GetAccountShape = async info => {
9
160
  const { address, initialAccount, currency, derivationMode, rest } = info;
@@ -26,14 +177,29 @@ export const getAccountShape: GetAccountShape = async info => {
26
177
  const xpub = initialAccount?.xpub || publicKey || "";
27
178
 
28
179
  const oldOperations = initialAccount?.operations || [];
29
- const startAt = (oldOperations[0]?.extra as any)?.version;
30
180
 
31
181
  const aptosClient = new AptosAPI(currency.id);
32
- const { balance, transactions, blockHeight } = await aptosClient.getAccountInfo(address, startAt);
182
+ const { balance, transactions, blockHeight } = await aptosClient.getAccountInfo(address);
33
183
 
34
- const newOperations = txsToOps(info, accountId, transactions);
184
+ const [newOperations, tokenOperations]: [Operation[], Operation[]] = txsToOps(
185
+ info,
186
+ accountId,
187
+ transactions,
188
+ );
35
189
  const operations = mergeOps(oldOperations, newOperations);
36
190
 
191
+ const newSubAccounts = await getSubAccounts(info, address, accountId, tokenOperations);
192
+ const shouldSyncFromScratch = initialAccount === undefined;
193
+ const subAccounts = shouldSyncFromScratch
194
+ ? newSubAccounts
195
+ : mergeSubAccounts(initialAccount, newSubAccounts);
196
+
197
+ operations.forEach(op => {
198
+ const subOperations = inferSubOperations(op.hash, subAccounts);
199
+ op.subOperations =
200
+ subOperations.length === 1 ? subOperations : subOperations.filter(op => !!op.blockHash);
201
+ });
202
+
37
203
  const shape: Partial<AptosAccount> = {
38
204
  type: "Account",
39
205
  id: accountId,
@@ -44,6 +210,7 @@ export const getAccountShape: GetAccountShape = async info => {
44
210
  operationsCount: operations.length,
45
211
  blockHeight,
46
212
  lastSyncDate: new Date(),
213
+ subAccounts,
47
214
  };
48
215
 
49
216
  return shape;
package/src/constants.ts CHANGED
@@ -10,25 +10,36 @@ export enum TX_STATUS {
10
10
 
11
11
  export const WRITE_RESOURCE = "write_resource";
12
12
 
13
- export const TRANSFER_TYPES: MoveStructId[] = [
13
+ export const COIN_TRANSFER_TYPES: MoveStructId[] = [
14
14
  "0x1::aptos_account::transfer",
15
15
  "0x1::aptos_account::transfer_coins",
16
16
  "0x1::coin::transfer",
17
17
  ];
18
+
19
+ export const FA_TRANSFER_TYPES: MoveStructId[] = ["0x1::primary_fungible_store::transfer"];
20
+
18
21
  export const BATCH_TRANSFER_TYPES: MoveStructId[] = [
19
22
  "0x1::aptos_account::batch_transfer",
20
23
  "0x1::aptos_account::batch_transfer_coins",
21
24
  ];
25
+
22
26
  export const DELEGATION_POOL_TYPES: MoveStructId[] = [
23
27
  "0x1::delegation_pool::add_stake",
24
28
  "0x1::delegation_pool::withdraw",
25
29
  ];
26
30
 
27
31
  export const APTOS_ASSET_ID: MoveStructId = "0x1::aptos_coin::AptosCoin";
32
+
28
33
  export const APTOS_COIN_CHANGE: MoveStructId = `0x1::coin::CoinStore<${APTOS_ASSET_ID}>`;
29
34
 
35
+ export const APTOS_FUNGIBLE_STORE: MoveStructId = "0x1::fungible_asset::FungibleStore";
36
+
37
+ export const APTOS_OBJECT_CORE: MoveStructId = "0x1::object::ObjectCore";
38
+
30
39
  export enum DIRECTION {
31
40
  IN = "IN",
32
41
  OUT = "OUT",
33
42
  UNKNOWN = "UNKNOWN",
34
43
  }
44
+
45
+ export const SUPPORTED_TOKEN_TYPES = ["coin", "fungible_asset"];