@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,127 @@
|
|
|
1
|
+
import { Observable } from "rxjs";
|
|
2
|
+
import Eth, { ledgerService } from "@ledgerhq/hw-app-eth";
|
|
3
|
+
import {
|
|
4
|
+
Account,
|
|
5
|
+
SignOperationFnSignature,
|
|
6
|
+
SignOperationEvent,
|
|
7
|
+
} from "@ledgerhq/types-live";
|
|
8
|
+
import { ResolutionConfig } from "@ledgerhq/hw-app-eth/lib/services/types";
|
|
9
|
+
import { buildOptimisticOperation } from "./buildOptimisticOperation";
|
|
10
|
+
import { prepareForSignOperation } from "./prepareTransaction";
|
|
11
|
+
import { getSerializedTransaction } from "./transaction";
|
|
12
|
+
import { Transaction } from "./types";
|
|
13
|
+
import { DeviceCommunication } from "@ledgerhq/coin-framework/bridge/jsHelpers";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Transforms the ECDSA signature paremeter v hexadecimal string received
|
|
17
|
+
* from the nano into an EIP155 compatible number.
|
|
18
|
+
*
|
|
19
|
+
* Reminder EIP155 transforms v this way:
|
|
20
|
+
* v = chainId * 2 + 35
|
|
21
|
+
* (+ parity 1 or 0)
|
|
22
|
+
*/
|
|
23
|
+
export const applyEIP155 = (vAsHex: string, chainId: number): number => {
|
|
24
|
+
const v = parseInt(vAsHex, 16);
|
|
25
|
+
|
|
26
|
+
if (v === 0 || v === 1) {
|
|
27
|
+
// if v is 0 or 1, it's already representing parity
|
|
28
|
+
return chainId * 2 + 35 + v;
|
|
29
|
+
} else if (v === 27 || v === 28) {
|
|
30
|
+
const parity = v - 27; // transforming v into 0 or 1 to become the parity
|
|
31
|
+
return chainId * 2 + 35 + parity;
|
|
32
|
+
}
|
|
33
|
+
// When chainId is lower than 109, hw-app-eth *can* return a v with EIP155 already applied
|
|
34
|
+
// e.g. bsc's chainId is 56 -> v then equals to 147/148
|
|
35
|
+
// optimism's chainId is 10 -> v equals to 55/56
|
|
36
|
+
// ethereum's chainId is 1 -> v equals to 0/1
|
|
37
|
+
// goerli's chainId is 5 -> v equals to 0/1
|
|
38
|
+
return v;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Sign Transaction with Ledger hardware
|
|
43
|
+
*/
|
|
44
|
+
export const buildSignOperation =
|
|
45
|
+
(withDevice: DeviceCommunication): SignOperationFnSignature<Transaction> =>
|
|
46
|
+
({
|
|
47
|
+
account,
|
|
48
|
+
deviceId,
|
|
49
|
+
transaction,
|
|
50
|
+
}: {
|
|
51
|
+
account: Account;
|
|
52
|
+
deviceId: any;
|
|
53
|
+
transaction: Transaction;
|
|
54
|
+
}): Observable<SignOperationEvent> =>
|
|
55
|
+
withDevice(deviceId)(
|
|
56
|
+
(transport) =>
|
|
57
|
+
new Observable((o) => {
|
|
58
|
+
async function main() {
|
|
59
|
+
const preparedTransaction = await prepareForSignOperation(
|
|
60
|
+
account,
|
|
61
|
+
transaction
|
|
62
|
+
);
|
|
63
|
+
const serializedTxHexString =
|
|
64
|
+
getSerializedTransaction(preparedTransaction).slice(2); // Remove 0x prefix
|
|
65
|
+
|
|
66
|
+
// Configure type of resolutions necessary for the clear signing
|
|
67
|
+
const resolutionConfig: ResolutionConfig = {
|
|
68
|
+
externalPlugins: true,
|
|
69
|
+
erc20: true,
|
|
70
|
+
domains: transaction.recipientDomain
|
|
71
|
+
? [transaction.recipientDomain]
|
|
72
|
+
: [],
|
|
73
|
+
};
|
|
74
|
+
// Look for resolutions for external plugins and ERC20
|
|
75
|
+
const resolution = await ledgerService.resolveTransaction(
|
|
76
|
+
serializedTxHexString,
|
|
77
|
+
{},
|
|
78
|
+
resolutionConfig
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
o.next({
|
|
82
|
+
type: "device-signature-requested",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Instanciate Eth app bindings
|
|
86
|
+
const eth = new Eth(transport);
|
|
87
|
+
// Request signature on the nano
|
|
88
|
+
const sig = await eth.signTransaction(
|
|
89
|
+
account.freshAddressPath,
|
|
90
|
+
serializedTxHexString,
|
|
91
|
+
resolution
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
o.next({ type: "device-signature-granted" }); // Signature is done
|
|
95
|
+
|
|
96
|
+
const { chainId = 0 } = account.currency.ethereumLikeInfo || {};
|
|
97
|
+
// Create a new serialized tx with the signature now
|
|
98
|
+
const signature = await getSerializedTransaction(
|
|
99
|
+
preparedTransaction,
|
|
100
|
+
{
|
|
101
|
+
r: "0x" + sig.r,
|
|
102
|
+
s: "0x" + sig.s,
|
|
103
|
+
v: applyEIP155(sig.v, chainId),
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const operation = buildOptimisticOperation(account, {
|
|
108
|
+
...transaction,
|
|
109
|
+
nonce: preparedTransaction.nonce,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
o.next({
|
|
113
|
+
type: "signed",
|
|
114
|
+
signedOperation: {
|
|
115
|
+
operation,
|
|
116
|
+
signature,
|
|
117
|
+
expirationDate: null,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
main().then(
|
|
123
|
+
() => o.complete(),
|
|
124
|
+
(e) => o.error(e)
|
|
125
|
+
);
|
|
126
|
+
})
|
|
127
|
+
);
|
package/src/specs.ts
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import expect from "expect";
|
|
2
|
+
import invariant from "invariant";
|
|
3
|
+
import sample from "lodash/sample";
|
|
4
|
+
import BigNumber from "bignumber.js";
|
|
5
|
+
import {
|
|
6
|
+
MutationSpec,
|
|
7
|
+
TransactionDestinationTestInput,
|
|
8
|
+
} from "@ledgerhq/coin-framework/bot/types";
|
|
9
|
+
import { DeviceModelId } from "@ledgerhq/devices";
|
|
10
|
+
import { CryptoCurrencyIds } from "@ledgerhq/types-live";
|
|
11
|
+
import { cryptocurrenciesById } from "@ledgerhq/cryptoassets/currencies";
|
|
12
|
+
import {
|
|
13
|
+
botTest,
|
|
14
|
+
genericTestDestination,
|
|
15
|
+
pickSiblings,
|
|
16
|
+
} from "@ledgerhq/coin-framework/bot/specs";
|
|
17
|
+
import {
|
|
18
|
+
getCryptoCurrencyById,
|
|
19
|
+
parseCurrencyUnit,
|
|
20
|
+
} from "@ledgerhq/coin-framework/currencies/index";
|
|
21
|
+
import { acceptTransaction } from "./speculos-deviceActions";
|
|
22
|
+
import { Transaction as EvmTransaction } from "./types";
|
|
23
|
+
|
|
24
|
+
const testTimeout = 10 * 60 * 1000;
|
|
25
|
+
|
|
26
|
+
const ETH_UNIT = { code: "ETH", name: "ETH", magnitude: 18 };
|
|
27
|
+
const MBTC_UNIT = { name: "mBTC", code: "mBTC", magnitude: 5 };
|
|
28
|
+
|
|
29
|
+
const minBalancePerCurrencyId: Record<CryptoCurrencyIds, BigNumber> = {
|
|
30
|
+
arbitrum: parseCurrencyUnit(ETH_UNIT, "0.001"),
|
|
31
|
+
arbitrum_goerli: parseCurrencyUnit(ETH_UNIT, "0.001"),
|
|
32
|
+
optimism: parseCurrencyUnit(ETH_UNIT, "0.001"),
|
|
33
|
+
optimism_goerli: parseCurrencyUnit(ETH_UNIT, "0.001"),
|
|
34
|
+
boba: parseCurrencyUnit(ETH_UNIT, "0.001"),
|
|
35
|
+
metis: parseCurrencyUnit(ETH_UNIT, "0.01"),
|
|
36
|
+
moonriver: parseCurrencyUnit(ETH_UNIT, "0.1"),
|
|
37
|
+
rsk: parseCurrencyUnit(MBTC_UNIT, "0.05"),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Method in charge of verifying that both the recipient and sender of a *coin* transaction
|
|
42
|
+
* have received/lost the expected amount and have now the right balances
|
|
43
|
+
*
|
|
44
|
+
* ⚠️ Some blockchains specific rules are included
|
|
45
|
+
*/
|
|
46
|
+
const testCoinDestination = (
|
|
47
|
+
args: TransactionDestinationTestInput<EvmTransaction>
|
|
48
|
+
) => {
|
|
49
|
+
const { sendingAccount } = args;
|
|
50
|
+
const { currency } = sendingAccount;
|
|
51
|
+
|
|
52
|
+
// Because Arbitrum is an L2, gas is used in a specific way to ensure both the L2 and the L1 are getting paid.
|
|
53
|
+
// But as of right now the `arbiscan.io` API is only returning the proposed gas price of
|
|
54
|
+
// the transaction and not the effectively used gas price, which might differ.
|
|
55
|
+
// This leads to not being able to correctly cost an operation and
|
|
56
|
+
// therefore makes it impossible infer the sender's balance
|
|
57
|
+
if (["arbitrum", "arbitrum_goerli"].includes(currency.id)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return genericTestDestination(args);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Method in charge of verifying the balance of a coin account after
|
|
66
|
+
* the transaction has been confirmed
|
|
67
|
+
*
|
|
68
|
+
* ⚠️ Some blockchains specific rules are included
|
|
69
|
+
*/
|
|
70
|
+
const testCoinBalance: MutationSpec<EvmTransaction>["test"] = ({
|
|
71
|
+
account,
|
|
72
|
+
accountBeforeTransaction,
|
|
73
|
+
operation,
|
|
74
|
+
}) => {
|
|
75
|
+
// Optimism works in a way where a transaction as a L2 cost and a L1 settlement cost
|
|
76
|
+
// The explorer API is not capable of returning the L1 cost, therefore
|
|
77
|
+
// the operation value will always be less than what
|
|
78
|
+
// has been removed from the account balance
|
|
79
|
+
//
|
|
80
|
+
// Remark is also true for Abritrum but because of the arbiscan API not returning the
|
|
81
|
+
// effectively used gas but the "bid"/proposition of gas of the transaction
|
|
82
|
+
// resulting in inconsistencies regarding the cumulated value of a tx.
|
|
83
|
+
// value + gasLimit * gasPrice <-- gasPrice can be wrong here.
|
|
84
|
+
const underValuedFeesCurrencies = ["optimism", "optimism_goerli"];
|
|
85
|
+
const overValuedFeesCurrencies = ["arbitrum", "arbitrum_goerli"];
|
|
86
|
+
const currenciesWithFlakyBehaviour = [
|
|
87
|
+
...underValuedFeesCurrencies,
|
|
88
|
+
...overValuedFeesCurrencies,
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
// Classic test verifying exactly the balance
|
|
92
|
+
if (!currenciesWithFlakyBehaviour.includes(account.currency.id)) {
|
|
93
|
+
botTest("account balance moved with operation value", () =>
|
|
94
|
+
expect(account.balance.toString()).toBe(
|
|
95
|
+
accountBeforeTransaction.balance.minus(operation.value).toString()
|
|
96
|
+
)
|
|
97
|
+
);
|
|
98
|
+
} else {
|
|
99
|
+
// fallback test verifying the balance moved between the maximum and minimum possible values of the operation
|
|
100
|
+
botTest(
|
|
101
|
+
"account balance moved at least operation value and less than operation value plus fees",
|
|
102
|
+
() => {
|
|
103
|
+
// If the fee is undervalued like it is for optimism for example
|
|
104
|
+
// the only doable check is to verify that the account
|
|
105
|
+
// has lost *at least* the operation value + fee
|
|
106
|
+
if (underValuedFeesCurrencies.includes(account.currency.id)) {
|
|
107
|
+
const maxBalance = accountBeforeTransaction.balance.minus(
|
|
108
|
+
operation.value.minus(operation.fee) // type OUT operations value includes fees so we remove it
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect({
|
|
112
|
+
lessThanMaxBalance: account.balance.lte(maxBalance),
|
|
113
|
+
}).toEqual({
|
|
114
|
+
lessThanMaxBalance: true,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// If the fee is overvalue like it is for arbitrum for example
|
|
119
|
+
// we make sure the account has now a balance between
|
|
120
|
+
// previous balance minus only operation value &
|
|
121
|
+
// previous balance minus operation value + fee
|
|
122
|
+
if (overValuedFeesCurrencies.includes(account.currency.id)) {
|
|
123
|
+
const minBalance = accountBeforeTransaction.balance.minus(
|
|
124
|
+
operation.value // type OUT operations value includes fees
|
|
125
|
+
);
|
|
126
|
+
const maxBalance = accountBeforeTransaction.balance.minus(
|
|
127
|
+
operation.value.minus(operation.fee)
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
expect({
|
|
131
|
+
greaterThanMinBalance: account.balance.gte(minBalance),
|
|
132
|
+
lessThanMaxBalance: account.balance.lte(maxBalance),
|
|
133
|
+
}).toEqual({
|
|
134
|
+
greaterThanMinBalance: true,
|
|
135
|
+
lessThanMaxBalance: true,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const transactionCheck =
|
|
144
|
+
(currencyId: string) =>
|
|
145
|
+
({ maxSpendable }: { maxSpendable: BigNumber }) => {
|
|
146
|
+
const currency = getCryptoCurrencyById(currencyId);
|
|
147
|
+
invariant(
|
|
148
|
+
maxSpendable.gt(
|
|
149
|
+
minBalancePerCurrencyId[currency.id] ||
|
|
150
|
+
parseCurrencyUnit(currency.units[0], "1")
|
|
151
|
+
),
|
|
152
|
+
`${currencyId} balance is too low`
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const evmBasicMutations: ({
|
|
157
|
+
maxAccount,
|
|
158
|
+
}: {
|
|
159
|
+
maxAccount: number;
|
|
160
|
+
}) => MutationSpec<EvmTransaction>[] = ({ maxAccount }) => [
|
|
161
|
+
{
|
|
162
|
+
name: "move 50%",
|
|
163
|
+
maxRun: 2,
|
|
164
|
+
testDestination: testCoinDestination,
|
|
165
|
+
transaction: ({ account, siblings, bridge, maxSpendable }) => {
|
|
166
|
+
const sibling = pickSiblings(siblings, maxAccount);
|
|
167
|
+
const recipient = sibling.freshAddress;
|
|
168
|
+
const amount = maxSpendable.div(2).integerValue();
|
|
169
|
+
return {
|
|
170
|
+
transaction: bridge.createTransaction(account),
|
|
171
|
+
updates: [
|
|
172
|
+
{
|
|
173
|
+
recipient,
|
|
174
|
+
amount,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
test: ({
|
|
180
|
+
account,
|
|
181
|
+
accountBeforeTransaction,
|
|
182
|
+
operation,
|
|
183
|
+
transaction,
|
|
184
|
+
status,
|
|
185
|
+
optimisticOperation,
|
|
186
|
+
}) => {
|
|
187
|
+
// workaround for buggy explorer behavior (nodes desync)
|
|
188
|
+
invariant(
|
|
189
|
+
Date.now() - operation.date.getTime() > 60000,
|
|
190
|
+
"operation time to be older than 60s"
|
|
191
|
+
);
|
|
192
|
+
const estimatedGas = transaction.gasLimit.times(
|
|
193
|
+
transaction.gasPrice || transaction.maxFeePerGas || 0
|
|
194
|
+
);
|
|
195
|
+
botTest("operation fee is not exceeding estimated gas", () =>
|
|
196
|
+
expect(operation.fee.toNumber()).toBeLessThanOrEqual(
|
|
197
|
+
estimatedGas.toNumber()
|
|
198
|
+
)
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
testCoinBalance({
|
|
202
|
+
account,
|
|
203
|
+
accountBeforeTransaction,
|
|
204
|
+
operation,
|
|
205
|
+
transaction,
|
|
206
|
+
status,
|
|
207
|
+
optimisticOperation,
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: "send max",
|
|
213
|
+
maxRun: 1,
|
|
214
|
+
testDestination: testCoinDestination,
|
|
215
|
+
transaction: ({ account, siblings, bridge }) => {
|
|
216
|
+
const sibling = pickSiblings(siblings, maxAccount);
|
|
217
|
+
const recipient = sibling.freshAddress;
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
transaction: bridge.createTransaction(account),
|
|
221
|
+
updates: [
|
|
222
|
+
{
|
|
223
|
+
recipient,
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
useAllAmount: true,
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
};
|
|
230
|
+
},
|
|
231
|
+
test: ({
|
|
232
|
+
account,
|
|
233
|
+
accountBeforeTransaction,
|
|
234
|
+
operation,
|
|
235
|
+
transaction,
|
|
236
|
+
status,
|
|
237
|
+
optimisticOperation,
|
|
238
|
+
}) => {
|
|
239
|
+
// workaround for buggy explorer behavior (nodes desync)
|
|
240
|
+
invariant(
|
|
241
|
+
Date.now() - operation.date.getTime() > 60000,
|
|
242
|
+
"operation time to be older than 60s"
|
|
243
|
+
);
|
|
244
|
+
const estimatedGas = transaction.gasLimit.times(
|
|
245
|
+
transaction.gasPrice || transaction.maxFeePerGas || 0
|
|
246
|
+
);
|
|
247
|
+
botTest("operation fee is not exceeding estimated gas", () =>
|
|
248
|
+
expect(operation.fee.toNumber()).toBeLessThanOrEqual(
|
|
249
|
+
estimatedGas.toNumber()
|
|
250
|
+
)
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
testCoinBalance({
|
|
254
|
+
account,
|
|
255
|
+
accountBeforeTransaction,
|
|
256
|
+
operation,
|
|
257
|
+
transaction,
|
|
258
|
+
status,
|
|
259
|
+
optimisticOperation,
|
|
260
|
+
});
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: "move some ERC20",
|
|
265
|
+
maxRun: 1,
|
|
266
|
+
transaction: ({ account, siblings, bridge }) => {
|
|
267
|
+
const erc20Account = sample(
|
|
268
|
+
(account.subAccounts || []).filter((a) => a.balance.gt(0))
|
|
269
|
+
);
|
|
270
|
+
invariant(erc20Account, "no erc20 account");
|
|
271
|
+
const sibling = pickSiblings(siblings, 3);
|
|
272
|
+
const recipient = sibling.freshAddress;
|
|
273
|
+
return {
|
|
274
|
+
transaction: bridge.createTransaction(account),
|
|
275
|
+
updates: [
|
|
276
|
+
{
|
|
277
|
+
recipient,
|
|
278
|
+
subAccountId: erc20Account!.id,
|
|
279
|
+
},
|
|
280
|
+
Math.random() < 0.5
|
|
281
|
+
? {
|
|
282
|
+
useAllAmount: true,
|
|
283
|
+
}
|
|
284
|
+
: {
|
|
285
|
+
amount: erc20Account!.balance
|
|
286
|
+
.times(Math.random())
|
|
287
|
+
.integerValue(),
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
};
|
|
291
|
+
},
|
|
292
|
+
test: ({ accountBeforeTransaction, account, transaction, operation }) => {
|
|
293
|
+
// workaround for buggy explorer behavior (nodes desync)
|
|
294
|
+
invariant(
|
|
295
|
+
Date.now() - operation.date.getTime() > 60000,
|
|
296
|
+
"operation time to be older than 60s"
|
|
297
|
+
);
|
|
298
|
+
invariant(accountBeforeTransaction.subAccounts, "sub accounts before");
|
|
299
|
+
const erc20accountBefore = accountBeforeTransaction.subAccounts?.find(
|
|
300
|
+
(s) => s.id === transaction.subAccountId
|
|
301
|
+
);
|
|
302
|
+
invariant(erc20accountBefore, "erc20 acc was here before");
|
|
303
|
+
invariant(account.subAccounts, "sub accounts");
|
|
304
|
+
const erc20account = account.subAccounts!.find(
|
|
305
|
+
(s) => s.id === transaction.subAccountId
|
|
306
|
+
);
|
|
307
|
+
invariant(erc20account, "erc20 acc is still here");
|
|
308
|
+
|
|
309
|
+
if (transaction.useAllAmount) {
|
|
310
|
+
botTest("erc20 account is empty", () =>
|
|
311
|
+
expect(erc20account!.balance.toString()).toBe("0")
|
|
312
|
+
);
|
|
313
|
+
} else {
|
|
314
|
+
botTest("account balance moved with tx amount", () =>
|
|
315
|
+
expect(erc20account!.balance.toString()).toBe(
|
|
316
|
+
erc20accountBefore!.balance.minus(transaction.amount).toString()
|
|
317
|
+
)
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
export default Object.values(cryptocurrenciesById)
|
|
325
|
+
.filter((currency) => currency.family === "evm")
|
|
326
|
+
.reduce((acc, currency) => {
|
|
327
|
+
// @ts-expect-error FIXME: fix typings
|
|
328
|
+
acc[currency.id] = {
|
|
329
|
+
name: currency.name,
|
|
330
|
+
currency,
|
|
331
|
+
appQuery: {
|
|
332
|
+
model: DeviceModelId.nanoS,
|
|
333
|
+
appName: "Ethereum",
|
|
334
|
+
appVersion: "1.10.2",
|
|
335
|
+
},
|
|
336
|
+
testTimeout,
|
|
337
|
+
transactionCheck: transactionCheck(currency.id),
|
|
338
|
+
mutations: evmBasicMutations({
|
|
339
|
+
maxAccount: 3,
|
|
340
|
+
}),
|
|
341
|
+
genericDeviceAction: acceptTransaction,
|
|
342
|
+
};
|
|
343
|
+
return acc;
|
|
344
|
+
}, {});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { findSubAccountById } from "@ledgerhq/coin-framework/account/index";
|
|
2
|
+
import {
|
|
3
|
+
deviceActionFlow,
|
|
4
|
+
formatDeviceAmount,
|
|
5
|
+
SpeculosButton,
|
|
6
|
+
} from "@ledgerhq/coin-framework/bot/specs";
|
|
7
|
+
import type { DeviceAction } from "@ledgerhq/coin-framework/bot/types";
|
|
8
|
+
import type { Transaction } from "./types";
|
|
9
|
+
|
|
10
|
+
// FIXME: fix types
|
|
11
|
+
const maxFeesExpectedValue = ({
|
|
12
|
+
account,
|
|
13
|
+
status,
|
|
14
|
+
}: {
|
|
15
|
+
account: any;
|
|
16
|
+
status: any;
|
|
17
|
+
}) => formatDeviceAmount(account.currency, status.estimatedFees);
|
|
18
|
+
|
|
19
|
+
export const acceptTransaction: DeviceAction<Transaction, any> =
|
|
20
|
+
deviceActionFlow({
|
|
21
|
+
steps: [
|
|
22
|
+
{
|
|
23
|
+
title: "Review",
|
|
24
|
+
button: SpeculosButton.RIGHT,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
title: "Type",
|
|
28
|
+
button: SpeculosButton.RIGHT,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
title: "Amount",
|
|
32
|
+
button: SpeculosButton.RIGHT,
|
|
33
|
+
expectedValue: ({ account, status, transaction }) => {
|
|
34
|
+
const subAccount = findSubAccountById(
|
|
35
|
+
account,
|
|
36
|
+
transaction.subAccountId || ""
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (subAccount && subAccount.type === "TokenAccount") {
|
|
40
|
+
return formatDeviceAmount(subAccount.token, status.amount);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return formatDeviceAmount(account.currency, status.amount);
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
title: "Contract",
|
|
48
|
+
button: SpeculosButton.RIGHT,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
title: "Network",
|
|
52
|
+
button: SpeculosButton.RIGHT,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
title: "Max fees",
|
|
56
|
+
button: SpeculosButton.RIGHT,
|
|
57
|
+
expectedValue: maxFeesExpectedValue,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
// Legacy (ETC..)
|
|
61
|
+
title: "Max Fees",
|
|
62
|
+
button: SpeculosButton.RIGHT,
|
|
63
|
+
expectedValue: maxFeesExpectedValue,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
title: "Address",
|
|
67
|
+
button: SpeculosButton.RIGHT,
|
|
68
|
+
expectedValue: ({ transaction }) => transaction.recipient,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
title: "Accept",
|
|
72
|
+
button: SpeculosButton.BOTH,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
title: "Approve",
|
|
76
|
+
button: SpeculosButton.BOTH,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export default {
|
|
82
|
+
acceptTransaction,
|
|
83
|
+
};
|