@ledgerhq/coin-evm 0.2.0-next.0
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.
- package/.eslintrc.js +57 -0
- package/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +18 -0
- package/jest.config.js +6 -0
- package/package.json +102 -0
- package/src/__tests__/adapters.unit.test.ts +527 -0
- package/src/__tests__/broadcast.unit.test.ts +181 -0
- package/src/__tests__/buildOptimisticOperation.unit.test.ts +182 -0
- package/src/__tests__/createTransaction.unit.test.ts +52 -0
- package/src/__tests__/deviceTransactionConfig.unit.test.ts +245 -0
- package/src/__tests__/estimateMaxSpendable.unit.test.ts +123 -0
- package/src/__tests__/getTransactionStatus.unit.test.ts +355 -0
- package/src/__tests__/hw-getAddress.unit.test.ts +24 -0
- package/src/__tests__/logic.unit.test.ts +406 -0
- package/src/__tests__/preload.unit.test.ts +139 -0
- package/src/__tests__/prepareTransaction.unit.test.ts +394 -0
- package/src/__tests__/rpc.unit.test.ts +532 -0
- package/src/__tests__/signOperation.unit.test.ts +157 -0
- package/src/__tests__/synchronization.unit.test.ts +832 -0
- package/src/__tests__/transaction.unit.test.ts +196 -0
- package/src/abis/erc20.abi.json +230 -0
- package/src/abis/optimismGasPriceOracle.abi.json +252 -0
- package/src/adapters.ts +148 -0
- package/src/api/etherscan.ts +124 -0
- package/src/api/rpc.common.ts +354 -0
- package/src/api/rpc.native.ts +5 -0
- package/src/api/rpc.ts +2 -0
- package/src/bridge/js.ts +77 -0
- package/src/bridge.integration.test.ts +93 -0
- package/src/broadcast.ts +40 -0
- package/src/buildOptimisticOperation.ts +113 -0
- package/src/cli-transaction.ts +11 -0
- package/src/createTransaction.ts +25 -0
- package/src/datasets/ethereum.scanAccounts.1.ts +48 -0
- package/src/datasets/ethereum1.ts +20 -0
- package/src/datasets/ethereum2.ts +20 -0
- package/src/datasets/ethereum_classic.ts +68 -0
- package/src/deviceTransactionConfig.ts +64 -0
- package/src/errors.ts +5 -0
- package/src/estimateMaxSpendable.ts +19 -0
- package/src/getTransactionStatus.ts +186 -0
- package/src/hw-getAddress.ts +24 -0
- package/src/logic.ts +149 -0
- package/src/preload.ts +54 -0
- package/src/prepareTransaction.ts +176 -0
- package/src/signOperation.ts +127 -0
- package/src/specs.ts +344 -0
- package/src/speculos-deviceActions.ts +83 -0
- package/src/synchronization.ts +317 -0
- package/src/testUtils.ts +153 -0
- package/src/transaction.ts +193 -0
- package/src/types.ts +132 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decodeAccountId,
|
|
3
|
+
emptyHistoryCache,
|
|
4
|
+
encodeAccountId,
|
|
5
|
+
encodeTokenAccountId,
|
|
6
|
+
shouldRetainPendingOperation,
|
|
7
|
+
} from "@ledgerhq/coin-framework/account/index";
|
|
8
|
+
import {
|
|
9
|
+
AccountShapeInfo,
|
|
10
|
+
GetAccountShape,
|
|
11
|
+
makeSync,
|
|
12
|
+
mergeOps,
|
|
13
|
+
} from "@ledgerhq/coin-framework/bridge/jsHelpers";
|
|
14
|
+
import { log } from "@ledgerhq/logs";
|
|
15
|
+
import { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets";
|
|
16
|
+
import { Account, Operation, SubAccount } from "@ledgerhq/types-live";
|
|
17
|
+
import etherscanLikeApi from "./api/etherscan";
|
|
18
|
+
import {
|
|
19
|
+
getBalanceAndBlock,
|
|
20
|
+
getBlock,
|
|
21
|
+
getTokenBalance,
|
|
22
|
+
getTransaction,
|
|
23
|
+
} from "./api/rpc";
|
|
24
|
+
import { getSyncHash, mergeSubAccounts } from "./logic";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Switch to select one of the compatible explorer
|
|
28
|
+
*/
|
|
29
|
+
const getExplorerApi = (currency: CryptoCurrency) => {
|
|
30
|
+
const apiType = currency.ethereumLikeInfo?.explorer?.type;
|
|
31
|
+
|
|
32
|
+
switch (apiType) {
|
|
33
|
+
case "etherscan":
|
|
34
|
+
case "blockscout":
|
|
35
|
+
return etherscanLikeApi;
|
|
36
|
+
|
|
37
|
+
default:
|
|
38
|
+
throw new Error("API type not supported");
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Main synchronization process
|
|
44
|
+
* Get the main Account and the potential TokenAccounts linked to it
|
|
45
|
+
*/
|
|
46
|
+
export const getAccountShape: GetAccountShape = async (infos) => {
|
|
47
|
+
const { initialAccount, address, derivationMode, currency } = infos;
|
|
48
|
+
const { blockHeight, balance } = await getBalanceAndBlock(currency, address);
|
|
49
|
+
const accountId = encodeAccountId({
|
|
50
|
+
type: "js",
|
|
51
|
+
version: "2",
|
|
52
|
+
currencyId: currency.id,
|
|
53
|
+
xpubOrAddress: address,
|
|
54
|
+
derivationMode,
|
|
55
|
+
});
|
|
56
|
+
const syncHash = getSyncHash(currency);
|
|
57
|
+
// Due to some changes (as of now: new/updated tokens) we could need to force a sync from 0
|
|
58
|
+
const shouldSyncFromScratch = syncHash !== initialAccount?.syncHash;
|
|
59
|
+
|
|
60
|
+
// Get the latest stored operation to know where to start the new sync
|
|
61
|
+
const latestSyncedOperation = shouldSyncFromScratch
|
|
62
|
+
? null
|
|
63
|
+
: initialAccount?.operations?.reduce<Operation | null>((acc, curr) => {
|
|
64
|
+
if (!acc) {
|
|
65
|
+
return curr;
|
|
66
|
+
}
|
|
67
|
+
return (acc?.blockHeight || 0) > (curr?.blockHeight || 0) ? acc : curr;
|
|
68
|
+
}, null);
|
|
69
|
+
|
|
70
|
+
// This method could not be working if the integration doesn't have an API to retreive the operations
|
|
71
|
+
const lastCoinOperations = await (async () => {
|
|
72
|
+
try {
|
|
73
|
+
const { getLastCoinOperations } = getExplorerApi(currency);
|
|
74
|
+
return await getLastCoinOperations(
|
|
75
|
+
currency,
|
|
76
|
+
address,
|
|
77
|
+
accountId,
|
|
78
|
+
latestSyncedOperation?.blockHeight || 0
|
|
79
|
+
);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
log("EVM Family", "Failed to get latest transactions", {
|
|
82
|
+
address,
|
|
83
|
+
currency,
|
|
84
|
+
error: e,
|
|
85
|
+
});
|
|
86
|
+
throw e;
|
|
87
|
+
}
|
|
88
|
+
})();
|
|
89
|
+
|
|
90
|
+
const newSubAccounts = await getSubAccounts(
|
|
91
|
+
infos,
|
|
92
|
+
accountId,
|
|
93
|
+
shouldSyncFromScratch
|
|
94
|
+
);
|
|
95
|
+
// Merging potential new subAccouns while preserving the reference (returned value will be initialAccount.subAccounts)
|
|
96
|
+
const subAccounts = mergeSubAccounts(initialAccount, newSubAccounts);
|
|
97
|
+
|
|
98
|
+
// Trying to confirm pending operations that we are sure of
|
|
99
|
+
// because they were made in the live
|
|
100
|
+
// Useful for integrations without explorers
|
|
101
|
+
const confirmPendingOperations =
|
|
102
|
+
initialAccount?.pendingOperations?.map((op) =>
|
|
103
|
+
getOperationStatus(currency, op)
|
|
104
|
+
) || [];
|
|
105
|
+
const confirmedOperations = await Promise.all(confirmPendingOperations).then(
|
|
106
|
+
(ops) => ops.filter((op): op is Operation => !!op)
|
|
107
|
+
);
|
|
108
|
+
const newOperations = [...confirmedOperations, ...lastCoinOperations];
|
|
109
|
+
const operations = mergeOps(initialAccount?.operations || [], newOperations);
|
|
110
|
+
const lastSyncDate = new Date();
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
type: "Account",
|
|
114
|
+
id: accountId,
|
|
115
|
+
syncHash,
|
|
116
|
+
balance,
|
|
117
|
+
spendableBalance: balance,
|
|
118
|
+
blockHeight,
|
|
119
|
+
operations,
|
|
120
|
+
operationsCount: operations.length,
|
|
121
|
+
subAccounts,
|
|
122
|
+
lastSyncDate,
|
|
123
|
+
} as Partial<Account>;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Getting all token related operations in order to provide TokenAccounts
|
|
128
|
+
*/
|
|
129
|
+
export const getSubAccounts = async (
|
|
130
|
+
infos: AccountShapeInfo,
|
|
131
|
+
accountId: string,
|
|
132
|
+
shouldSyncFromScratch = false
|
|
133
|
+
): Promise<Partial<SubAccount>[]> => {
|
|
134
|
+
const { initialAccount, address, currency } = infos;
|
|
135
|
+
|
|
136
|
+
// Get the latest operation from all subaccounts
|
|
137
|
+
const latestSyncedOperation = shouldSyncFromScratch
|
|
138
|
+
? null
|
|
139
|
+
: initialAccount?.subAccounts
|
|
140
|
+
?.flatMap(({ operations }) => operations)
|
|
141
|
+
.reduce<Operation | null>((acc, curr) => {
|
|
142
|
+
if (!acc) {
|
|
143
|
+
return curr;
|
|
144
|
+
}
|
|
145
|
+
return (acc?.blockHeight || 0) > (curr?.blockHeight || 0)
|
|
146
|
+
? acc
|
|
147
|
+
: curr;
|
|
148
|
+
}, null);
|
|
149
|
+
|
|
150
|
+
// This method could not be working if the integration doesn't have an API to retreive the operations
|
|
151
|
+
const lastERC20OperationsAndCurrencies = await (async () => {
|
|
152
|
+
try {
|
|
153
|
+
const { getLastTokenOperations } = getExplorerApi(currency);
|
|
154
|
+
return await getLastTokenOperations(
|
|
155
|
+
currency,
|
|
156
|
+
address,
|
|
157
|
+
accountId,
|
|
158
|
+
latestSyncedOperation?.blockHeight || 0
|
|
159
|
+
);
|
|
160
|
+
} catch (e) {
|
|
161
|
+
log("EVM Family", "Failed to get latest ERC20 transactions", {
|
|
162
|
+
address,
|
|
163
|
+
currency,
|
|
164
|
+
error: e,
|
|
165
|
+
});
|
|
166
|
+
throw e;
|
|
167
|
+
}
|
|
168
|
+
})();
|
|
169
|
+
|
|
170
|
+
// Creating a Map of Operations by TokenCurrencies in order to know which TokenAccounts should be synced as well
|
|
171
|
+
const erc20OperationsByToken = lastERC20OperationsAndCurrencies.reduce<
|
|
172
|
+
Map<TokenCurrency, Operation[]>
|
|
173
|
+
>((acc, { tokenCurrency, operation }) => {
|
|
174
|
+
if (!tokenCurrency) return acc;
|
|
175
|
+
|
|
176
|
+
if (!acc.has(tokenCurrency)) {
|
|
177
|
+
acc.set(tokenCurrency, []);
|
|
178
|
+
}
|
|
179
|
+
acc.get(tokenCurrency)?.push(operation);
|
|
180
|
+
|
|
181
|
+
return acc;
|
|
182
|
+
}, new Map<TokenCurrency, Operation[]>());
|
|
183
|
+
|
|
184
|
+
// Fetching all TokenAccounts possible and providing already filtered operations
|
|
185
|
+
const subAccountsPromises: Promise<Partial<SubAccount>>[] = [];
|
|
186
|
+
for (const [token, ops] of erc20OperationsByToken.entries()) {
|
|
187
|
+
subAccountsPromises.push(
|
|
188
|
+
getSubAccountShape(currency, accountId, token, ops)
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return Promise.all(subAccountsPromises);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Fetch the balance for a token and creates a TokenAccount based on this and the provided operations
|
|
197
|
+
*/
|
|
198
|
+
export const getSubAccountShape = async (
|
|
199
|
+
currency: CryptoCurrency,
|
|
200
|
+
parentId: string,
|
|
201
|
+
token: TokenCurrency,
|
|
202
|
+
operations: Operation[]
|
|
203
|
+
): Promise<Partial<SubAccount>> => {
|
|
204
|
+
const { xpubOrAddress: address } = decodeAccountId(parentId);
|
|
205
|
+
const tokenAccountId = encodeTokenAccountId(parentId, token);
|
|
206
|
+
const balance = await getTokenBalance(
|
|
207
|
+
currency,
|
|
208
|
+
address,
|
|
209
|
+
token.contractAddress
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
type: "TokenAccount",
|
|
214
|
+
id: tokenAccountId,
|
|
215
|
+
parentId,
|
|
216
|
+
token,
|
|
217
|
+
balance,
|
|
218
|
+
spendableBalance: balance,
|
|
219
|
+
creationDate: new Date(),
|
|
220
|
+
operations,
|
|
221
|
+
operationsCount: operations.length,
|
|
222
|
+
pendingOperations: [],
|
|
223
|
+
balanceHistoryCache: emptyHistoryCache,
|
|
224
|
+
swapHistory: [],
|
|
225
|
+
};
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get a finalized operation depending on it status (confirmed or not)
|
|
230
|
+
*/
|
|
231
|
+
export const getOperationStatus = async (
|
|
232
|
+
currency: CryptoCurrency,
|
|
233
|
+
op: Operation
|
|
234
|
+
): Promise<Operation | null> => {
|
|
235
|
+
try {
|
|
236
|
+
const {
|
|
237
|
+
blockNumber: blockHeight,
|
|
238
|
+
blockHash,
|
|
239
|
+
timestamp,
|
|
240
|
+
nonce,
|
|
241
|
+
} = await getTransaction(currency, op.hash);
|
|
242
|
+
|
|
243
|
+
if (!blockHeight) {
|
|
244
|
+
throw new Error("getOperationStatus: Transaction has no block");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const date = await (async () => {
|
|
248
|
+
// timestamp can be missing depending on the node
|
|
249
|
+
if (timestamp) {
|
|
250
|
+
return new Date(timestamp * 1000);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Without timestamp, we directly look for the block
|
|
254
|
+
const { timestamp: blockTimestamp } = await getBlock(
|
|
255
|
+
currency,
|
|
256
|
+
blockHeight
|
|
257
|
+
);
|
|
258
|
+
return new Date(blockTimestamp * 1000);
|
|
259
|
+
})();
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
...op,
|
|
263
|
+
transactionSequenceNumber: nonce,
|
|
264
|
+
blockHash,
|
|
265
|
+
blockHeight,
|
|
266
|
+
date,
|
|
267
|
+
} as Operation;
|
|
268
|
+
} catch (e) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* After each sync, it might be necessary to remove pending operations
|
|
275
|
+
* inside of subAccounts.
|
|
276
|
+
*/
|
|
277
|
+
export const postSync = (initial: Account, synced: Account): Account => {
|
|
278
|
+
// Set of hashes from the pending operations of the main account
|
|
279
|
+
const coinPendingOperationsHashes = new Set();
|
|
280
|
+
for (const coinPendingOperation of synced.pendingOperations) {
|
|
281
|
+
coinPendingOperationsHashes.add(coinPendingOperation.hash);
|
|
282
|
+
}
|
|
283
|
+
// Set of ids from the already existing subAccount from previous sync
|
|
284
|
+
const initialSubAccountsIds = new Set();
|
|
285
|
+
for (const subAccount of initial.subAccounts || []) {
|
|
286
|
+
initialSubAccountsIds.add(subAccount.id);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
...synced,
|
|
291
|
+
subAccounts: synced.subAccounts?.map((subAccount) => {
|
|
292
|
+
// If the subAccount is new, just return the freshly synced subAccount
|
|
293
|
+
if (!initialSubAccountsIds.has(subAccount.id)) return subAccount;
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
...subAccount,
|
|
297
|
+
pendingOperations: subAccount.pendingOperations.filter(
|
|
298
|
+
(tokenPendingOperation) =>
|
|
299
|
+
// if the pending operation got removed from the main account, remove it as well
|
|
300
|
+
coinPendingOperationsHashes.has(tokenPendingOperation.hash) &&
|
|
301
|
+
// if the transaction has been confirmed, remove it
|
|
302
|
+
!subAccount.operations.some(
|
|
303
|
+
(op) => op.hash === tokenPendingOperation.hash
|
|
304
|
+
) &&
|
|
305
|
+
// common rule for pending operations retention in the live
|
|
306
|
+
shouldRetainPendingOperation(synced, tokenPendingOperation)
|
|
307
|
+
),
|
|
308
|
+
};
|
|
309
|
+
}),
|
|
310
|
+
};
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
export const sync = makeSync({
|
|
314
|
+
getAccountShape,
|
|
315
|
+
postSync,
|
|
316
|
+
shouldMergeOps: false,
|
|
317
|
+
});
|
package/src/testUtils.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decodeAccountId,
|
|
3
|
+
decodeTokenAccountId,
|
|
4
|
+
encodeTokenAccountId,
|
|
5
|
+
shortAddressPreview,
|
|
6
|
+
} from "@ledgerhq/coin-framework/account/index";
|
|
7
|
+
import {
|
|
8
|
+
DerivationMode,
|
|
9
|
+
getDerivationScheme,
|
|
10
|
+
runDerivationScheme,
|
|
11
|
+
} from "@ledgerhq/coin-framework/derivation";
|
|
12
|
+
import { encodeOperationId } from "@ledgerhq/coin-framework/operation";
|
|
13
|
+
import { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets";
|
|
14
|
+
import {
|
|
15
|
+
Account,
|
|
16
|
+
Operation,
|
|
17
|
+
SubAccount,
|
|
18
|
+
TokenAccount,
|
|
19
|
+
} from "@ledgerhq/types-live";
|
|
20
|
+
import BigNumber from "bignumber.js";
|
|
21
|
+
|
|
22
|
+
export const makeAccount = (
|
|
23
|
+
address: string,
|
|
24
|
+
currency: CryptoCurrency,
|
|
25
|
+
subAccounts: SubAccount[] = []
|
|
26
|
+
): Account => {
|
|
27
|
+
const id = `js:2:${currency.id}:${address}:`;
|
|
28
|
+
const { derivationMode, xpubOrAddress } = decodeAccountId(id);
|
|
29
|
+
const scheme = getDerivationScheme({
|
|
30
|
+
derivationMode: derivationMode as DerivationMode,
|
|
31
|
+
currency,
|
|
32
|
+
});
|
|
33
|
+
const index = 0;
|
|
34
|
+
const freshAddressPath = runDerivationScheme(scheme, currency, {
|
|
35
|
+
account: index,
|
|
36
|
+
node: 0,
|
|
37
|
+
address: 0,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const account: Account = {
|
|
41
|
+
type: "Account",
|
|
42
|
+
name:
|
|
43
|
+
currency.name +
|
|
44
|
+
" " +
|
|
45
|
+
(derivationMode || "legacy") +
|
|
46
|
+
" " +
|
|
47
|
+
shortAddressPreview(xpubOrAddress),
|
|
48
|
+
xpub: xpubOrAddress,
|
|
49
|
+
subAccounts,
|
|
50
|
+
seedIdentifier: xpubOrAddress,
|
|
51
|
+
starred: true,
|
|
52
|
+
used: true,
|
|
53
|
+
swapHistory: [],
|
|
54
|
+
id,
|
|
55
|
+
derivationMode,
|
|
56
|
+
currency,
|
|
57
|
+
unit: currency.units[0],
|
|
58
|
+
index,
|
|
59
|
+
freshAddress: xpubOrAddress,
|
|
60
|
+
freshAddressPath,
|
|
61
|
+
freshAddresses: [],
|
|
62
|
+
creationDate: new Date(),
|
|
63
|
+
lastSyncDate: new Date(0),
|
|
64
|
+
blockHeight: 0,
|
|
65
|
+
balance: new BigNumber(0),
|
|
66
|
+
spendableBalance: new BigNumber(0),
|
|
67
|
+
operationsCount: 0,
|
|
68
|
+
operations: [],
|
|
69
|
+
pendingOperations: [],
|
|
70
|
+
balanceHistoryCache: {
|
|
71
|
+
HOUR: {
|
|
72
|
+
latestDate: null,
|
|
73
|
+
balances: [],
|
|
74
|
+
},
|
|
75
|
+
DAY: {
|
|
76
|
+
latestDate: null,
|
|
77
|
+
balances: [],
|
|
78
|
+
},
|
|
79
|
+
WEEK: {
|
|
80
|
+
latestDate: null,
|
|
81
|
+
balances: [],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return account;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const makeTokenAccount = (
|
|
90
|
+
address: string,
|
|
91
|
+
tokenCurrency: TokenCurrency
|
|
92
|
+
): TokenAccount => {
|
|
93
|
+
const { parentCurrency: currency } = tokenCurrency;
|
|
94
|
+
const account = makeAccount(address, currency);
|
|
95
|
+
|
|
96
|
+
const tokenAccountId = encodeTokenAccountId(account.id, tokenCurrency);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
type: "TokenAccount",
|
|
100
|
+
id: tokenAccountId,
|
|
101
|
+
parentId: account.id,
|
|
102
|
+
token: tokenCurrency,
|
|
103
|
+
balance: new BigNumber(0),
|
|
104
|
+
spendableBalance: new BigNumber(0),
|
|
105
|
+
creationDate: new Date(),
|
|
106
|
+
operationsCount: 0,
|
|
107
|
+
operations: [],
|
|
108
|
+
pendingOperations: [],
|
|
109
|
+
starred: false,
|
|
110
|
+
balanceHistoryCache: {
|
|
111
|
+
HOUR: {
|
|
112
|
+
latestDate: null,
|
|
113
|
+
balances: [],
|
|
114
|
+
},
|
|
115
|
+
DAY: {
|
|
116
|
+
latestDate: null,
|
|
117
|
+
balances: [],
|
|
118
|
+
},
|
|
119
|
+
WEEK: {
|
|
120
|
+
latestDate: null,
|
|
121
|
+
balances: [],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
swapHistory: [],
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const makeOperation = (partialOp?: Partial<Operation>): Operation => {
|
|
129
|
+
const accountId = partialOp?.accountId ?? "js:2:ethereum:0xkvn:";
|
|
130
|
+
const { xpubOrAddress } = decodeAccountId(
|
|
131
|
+
accountId.includes("+")
|
|
132
|
+
? decodeTokenAccountId(accountId).accountId
|
|
133
|
+
: accountId
|
|
134
|
+
);
|
|
135
|
+
const hash = partialOp?.hash ?? "0xhash";
|
|
136
|
+
const type = partialOp?.type ?? "OUT";
|
|
137
|
+
return {
|
|
138
|
+
id: encodeOperationId(accountId, hash, type),
|
|
139
|
+
hash,
|
|
140
|
+
type,
|
|
141
|
+
value: new BigNumber(0),
|
|
142
|
+
fee: new BigNumber(0),
|
|
143
|
+
blockHash: null,
|
|
144
|
+
blockHeight: null,
|
|
145
|
+
senders: [xpubOrAddress],
|
|
146
|
+
recipients: ["0xlmb"],
|
|
147
|
+
accountId,
|
|
148
|
+
transactionSequenceNumber: 0,
|
|
149
|
+
date: new Date(),
|
|
150
|
+
extra: {},
|
|
151
|
+
...partialOp,
|
|
152
|
+
};
|
|
153
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { getAccountUnit } from "@ledgerhq/coin-framework/account/index";
|
|
2
|
+
import { formatCurrencyUnit } from "@ledgerhq/coin-framework/currencies/index";
|
|
3
|
+
import {
|
|
4
|
+
formatTransactionStatusCommon as formatTransactionStatus,
|
|
5
|
+
fromTransactionCommonRaw,
|
|
6
|
+
fromTransactionStatusRawCommon as fromTransactionStatusRaw,
|
|
7
|
+
toTransactionCommonRaw,
|
|
8
|
+
toTransactionStatusRawCommon as toTransactionStatusRaw,
|
|
9
|
+
} from "@ledgerhq/coin-framework/transaction/common";
|
|
10
|
+
import type { Account } from "@ledgerhq/types-live";
|
|
11
|
+
import { BigNumber } from "bignumber.js";
|
|
12
|
+
import { ethers } from "ethers";
|
|
13
|
+
import ERC20ABI from "./abis/erc20.abi.json";
|
|
14
|
+
import { transactionToEthersTransaction } from "./adapters";
|
|
15
|
+
import type {
|
|
16
|
+
Transaction as EvmTransaction,
|
|
17
|
+
EvmTransactionEIP1559,
|
|
18
|
+
EvmTransactionLegacy,
|
|
19
|
+
TransactionRaw as EvmTransactionRaw,
|
|
20
|
+
FeeData,
|
|
21
|
+
} from "./types";
|
|
22
|
+
|
|
23
|
+
export const DEFAULT_GAS_LIMIT = new BigNumber(21000);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Format the transaction for the CLI
|
|
27
|
+
*/
|
|
28
|
+
export const formatTransaction = (
|
|
29
|
+
{ mode, amount, recipient, useAllAmount }: EvmTransaction,
|
|
30
|
+
account: Account
|
|
31
|
+
): string =>
|
|
32
|
+
`
|
|
33
|
+
${mode.toUpperCase()} ${
|
|
34
|
+
useAllAmount
|
|
35
|
+
? "MAX"
|
|
36
|
+
: amount.isZero()
|
|
37
|
+
? ""
|
|
38
|
+
: " " +
|
|
39
|
+
formatCurrencyUnit(getAccountUnit(account), amount, {
|
|
40
|
+
showCode: true,
|
|
41
|
+
disableRounding: true,
|
|
42
|
+
})
|
|
43
|
+
}${recipient ? `\nTO ${recipient}` : ""}`;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Serializer raw to transaction
|
|
47
|
+
*/
|
|
48
|
+
export const fromTransactionRaw = (
|
|
49
|
+
rawTx: EvmTransactionRaw
|
|
50
|
+
): EvmTransaction => {
|
|
51
|
+
const common = fromTransactionCommonRaw(rawTx);
|
|
52
|
+
const tx: Partial<EvmTransaction> = {
|
|
53
|
+
...common,
|
|
54
|
+
family: rawTx.family,
|
|
55
|
+
mode: rawTx.mode,
|
|
56
|
+
chainId: rawTx.chainId,
|
|
57
|
+
nonce: rawTx.nonce,
|
|
58
|
+
gasLimit: new BigNumber(rawTx.gasLimit),
|
|
59
|
+
feesStrategy: rawTx.feesStrategy,
|
|
60
|
+
type: rawTx.type ?? 0, // if rawTx.type is undefined, transaction will be considered legacy and therefore type 0
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (rawTx.data) {
|
|
64
|
+
tx.data = Buffer.from(rawTx.data, "hex");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (rawTx.gasPrice) {
|
|
68
|
+
tx.gasPrice = new BigNumber(rawTx.gasPrice);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (rawTx.maxFeePerGas) {
|
|
72
|
+
tx.maxFeePerGas = new BigNumber(rawTx.maxFeePerGas);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (rawTx.maxPriorityFeePerGas) {
|
|
76
|
+
tx.maxPriorityFeePerGas = new BigNumber(rawTx.maxPriorityFeePerGas);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (rawTx.additionalFees) {
|
|
80
|
+
tx.additionalFees = new BigNumber(rawTx.additionalFees);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return tx as EvmTransaction;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Serializer transaction to raw
|
|
88
|
+
*/
|
|
89
|
+
export const toTransactionRaw = (tx: EvmTransaction): EvmTransactionRaw => {
|
|
90
|
+
const common = toTransactionCommonRaw(tx);
|
|
91
|
+
const txRaw: Partial<EvmTransactionRaw> = {
|
|
92
|
+
...common,
|
|
93
|
+
family: tx.family,
|
|
94
|
+
mode: tx.mode,
|
|
95
|
+
chainId: tx.chainId,
|
|
96
|
+
nonce: tx.nonce,
|
|
97
|
+
gasLimit: tx.gasLimit.toFixed(),
|
|
98
|
+
feesStrategy: tx.feesStrategy,
|
|
99
|
+
type: tx.type,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (tx.data) {
|
|
103
|
+
txRaw.data = Buffer.from(tx.data).toString("hex");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (tx.gasPrice) {
|
|
107
|
+
txRaw.gasPrice = tx.gasPrice.toFixed();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (tx.maxFeePerGas) {
|
|
111
|
+
txRaw.maxFeePerGas = tx.maxFeePerGas.toFixed();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (tx.maxPriorityFeePerGas) {
|
|
115
|
+
txRaw.maxPriorityFeePerGas = tx.maxPriorityFeePerGas.toFixed();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (tx.additionalFees) {
|
|
119
|
+
txRaw.additionalFees = tx.additionalFees.toFixed();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return txRaw as EvmTransactionRaw;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Returns the data necessary to execute smart contracts.
|
|
127
|
+
* As of now, only used to create ERC20 transfers' data
|
|
128
|
+
*/
|
|
129
|
+
export const getTransactionData = (
|
|
130
|
+
transaction: EvmTransaction
|
|
131
|
+
): Buffer | undefined => {
|
|
132
|
+
const contract = new ethers.utils.Interface(ERC20ABI);
|
|
133
|
+
const data = contract.encodeFunctionData("transfer", [
|
|
134
|
+
transaction.recipient,
|
|
135
|
+
transaction.amount.toFixed(),
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
return data ? Buffer.from(data.slice(2), "hex") : undefined;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Returns a transaction with the correct type and entries depending
|
|
143
|
+
* on the network compatiblity.
|
|
144
|
+
*/
|
|
145
|
+
export const getTypedTransaction = (
|
|
146
|
+
transaction: EvmTransaction,
|
|
147
|
+
feeData: FeeData
|
|
148
|
+
): EvmTransaction => {
|
|
149
|
+
// If the blockchain is supporting EIP-1559, use maxFeePerGas & maxPriorityFeePerGas
|
|
150
|
+
if (feeData.maxFeePerGas && feeData.maxPriorityFeePerGas) {
|
|
151
|
+
delete transaction.gasPrice;
|
|
152
|
+
return {
|
|
153
|
+
...transaction,
|
|
154
|
+
maxFeePerGas: feeData.maxFeePerGas,
|
|
155
|
+
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
|
|
156
|
+
type: 2,
|
|
157
|
+
} as EvmTransactionEIP1559;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Else just use a legacy transaction
|
|
161
|
+
delete transaction.maxFeePerGas;
|
|
162
|
+
delete transaction.maxPriorityFeePerGas;
|
|
163
|
+
return {
|
|
164
|
+
...transaction,
|
|
165
|
+
gasPrice: feeData.gasPrice || new BigNumber(0),
|
|
166
|
+
type: 0,
|
|
167
|
+
} as EvmTransactionLegacy;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Serialize a Ledger Live transaction into an hex string
|
|
172
|
+
*/
|
|
173
|
+
export const getSerializedTransaction = (
|
|
174
|
+
tx: EvmTransaction,
|
|
175
|
+
signature?: Partial<ethers.Signature>
|
|
176
|
+
): string => {
|
|
177
|
+
const unsignedEthersTransaction = transactionToEthersTransaction(tx);
|
|
178
|
+
|
|
179
|
+
return ethers.utils.serializeTransaction(
|
|
180
|
+
unsignedEthersTransaction,
|
|
181
|
+
signature as ethers.Signature
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export default {
|
|
186
|
+
formatTransaction,
|
|
187
|
+
fromTransactionRaw,
|
|
188
|
+
toTransactionRaw,
|
|
189
|
+
toTransactionStatusRaw,
|
|
190
|
+
formatTransactionStatus,
|
|
191
|
+
fromTransactionStatusRaw,
|
|
192
|
+
getSerializedTransaction,
|
|
193
|
+
};
|