@ledgerhq/coin-algorand 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 +92 -0
- package/src/account.ts +76 -0
- package/src/api/algodv2.ts +71 -0
- package/src/api/algodv2.types.ts +27 -0
- package/src/api/index.ts +41 -0
- package/src/api/indexer.ts +102 -0
- package/src/api/indexer.types.ts +41 -0
- package/src/bridge/js.ts +92 -0
- package/src/bridge.integration.test.ts +312 -0
- package/src/buildTransaction.ts +98 -0
- package/src/cli-transaction.ts +124 -0
- package/src/deviceTransactionConfig.ts +146 -0
- package/src/errors.ts +5 -0
- package/src/hw-getAddress.ts +14 -0
- package/src/initAccount.ts +10 -0
- package/src/js-broadcast.ts +21 -0
- package/src/js-createTransaction.ts +21 -0
- package/src/js-estimateMaxSpendable.ts +58 -0
- package/src/js-getFeesForTransaction.ts +28 -0
- package/src/js-getTransactionStatus.ts +189 -0
- package/src/js-prepareTransaction.ts +40 -0
- package/src/js-signOperation.ts +164 -0
- package/src/js-synchronization.ts +429 -0
- package/src/logic.ts +46 -0
- package/src/mock.ts +131 -0
- package/src/serialization.ts +48 -0
- package/src/specs.ts +292 -0
- package/src/speculos-deviceActions.ts +111 -0
- package/src/tokens.ts +5 -0
- package/src/transaction.ts +87 -0
- package/src/types.ts +65 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import {
|
|
2
|
+
InvalidAddressBecauseDestinationIsAlsoSource,
|
|
3
|
+
NotEnoughBalance,
|
|
4
|
+
NotEnoughBalanceBecauseDestinationNotCreated,
|
|
5
|
+
} from "@ledgerhq/errors";
|
|
6
|
+
import type { CurrenciesData, DatasetTest } from "@ledgerhq/types-live";
|
|
7
|
+
import { BigNumber } from "bignumber.js";
|
|
8
|
+
import { AlgorandASANotOptInInRecipient } from "./errors";
|
|
9
|
+
import type { AlgorandTransaction, Transaction } from "./types";
|
|
10
|
+
|
|
11
|
+
const algorand: CurrenciesData<Transaction> = {
|
|
12
|
+
FIXME_ignoreAccountFields: [
|
|
13
|
+
"algorandResources.rewards", // We cant keep track of this since it's always moving
|
|
14
|
+
"balance", // Rewards are included, same as above
|
|
15
|
+
"spendableBalance", // Same since the rewards are included here too
|
|
16
|
+
],
|
|
17
|
+
scanAccounts: [
|
|
18
|
+
{
|
|
19
|
+
name: "algorand seed 1",
|
|
20
|
+
apdus: `
|
|
21
|
+
=> 800300000480000000
|
|
22
|
+
<= c8b672d16c497bb097a48f09a9cccf0c4c7d6391acb7a4e7cd3f236fadbef9c49000
|
|
23
|
+
=> 800300000480000000
|
|
24
|
+
<= c8b672d16c497bb097a48f09a9cccf0c4c7d6391acb7a4e7cd3f236fadbef9c49000
|
|
25
|
+
=> 800300000480000001
|
|
26
|
+
<= 21b3068ca2b9a3b0b1fc68d9ecbd61663f6957c68a9c767aa14a8abb437180e69000
|
|
27
|
+
=> 800300000480000002
|
|
28
|
+
<= cc2b54ea5cbda5de6957086a8435c43a06e26559b2bfaebec56b748ec5a2a0519000
|
|
29
|
+
=> 800300000480000003
|
|
30
|
+
<= 6104eb314f51f4db5733976bd8c066297019ebaa6adcf39b4aa318d553c571cc9000
|
|
31
|
+
=> 800300000480000004
|
|
32
|
+
<= 6549e16ee1e242aba9d116dab058b6e33e4c6d801d3cbfe860195fc6e42940319000
|
|
33
|
+
`,
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
accounts: [
|
|
37
|
+
{
|
|
38
|
+
FIXME_tests: ["balance is sum of ops"],
|
|
39
|
+
// Rewards issues
|
|
40
|
+
raw: {
|
|
41
|
+
id: "js:2:algorand:c8b672d16c497bb097a48f09a9cccf0c4c7d6391acb7a4e7cd3f236fadbef9c4:",
|
|
42
|
+
seedIdentifier:
|
|
43
|
+
"c8b672d16c497bb097a48f09a9cccf0c4c7d6391acb7a4e7cd3f236fadbef9c4",
|
|
44
|
+
name: "Algorand 1",
|
|
45
|
+
xpub: "c8b672d16c497bb097a48f09a9cccf0c4c7d6391acb7a4e7cd3f236fadbef9c4",
|
|
46
|
+
derivationMode: "",
|
|
47
|
+
index: 0,
|
|
48
|
+
freshAddress:
|
|
49
|
+
"ZC3HFULMJF53BF5ER4E2TTGPBRGH2Y4RVS32JZ6NH4RW7LN67HCE6UBS3Q",
|
|
50
|
+
freshAddressPath: "44'/283'/0'/0/0",
|
|
51
|
+
freshAddresses: [
|
|
52
|
+
{
|
|
53
|
+
address:
|
|
54
|
+
"ZC3HFULMJF53BF5ER4E2TTGPBRGH2Y4RVS32JZ6NH4RW7LN67HCE6UBS3Q",
|
|
55
|
+
derivationPath: "44'/283'/0'/0/0",
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
unitMagnitude: 6,
|
|
59
|
+
blockHeight: 8518049,
|
|
60
|
+
operations: [],
|
|
61
|
+
pendingOperations: [],
|
|
62
|
+
currencyId: "algorand",
|
|
63
|
+
lastSyncDate: "",
|
|
64
|
+
balance: "1167089",
|
|
65
|
+
spendableBalance: "567089",
|
|
66
|
+
subAccounts: [],
|
|
67
|
+
},
|
|
68
|
+
transactions: [
|
|
69
|
+
{
|
|
70
|
+
name: "Same as Recipient",
|
|
71
|
+
transaction: (t) => ({
|
|
72
|
+
...t,
|
|
73
|
+
amount: new BigNumber(100),
|
|
74
|
+
recipient:
|
|
75
|
+
"ZC3HFULMJF53BF5ER4E2TTGPBRGH2Y4RVS32JZ6NH4RW7LN67HCE6UBS3Q",
|
|
76
|
+
}),
|
|
77
|
+
expectedStatus: {
|
|
78
|
+
errors: {
|
|
79
|
+
recipient: new InvalidAddressBecauseDestinationIsAlsoSource(),
|
|
80
|
+
},
|
|
81
|
+
warnings: {},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "Account creation minimum amount too low",
|
|
86
|
+
transaction: (t) => ({
|
|
87
|
+
...t,
|
|
88
|
+
amount: new BigNumber("100"),
|
|
89
|
+
recipient:
|
|
90
|
+
"MVE6C3XB4JBKXKORC3NLAWFW4M7EY3MADU6L72DADFP4NZBJIAYXGSLN3Y",
|
|
91
|
+
}),
|
|
92
|
+
expectedStatus: {
|
|
93
|
+
errors: {
|
|
94
|
+
amount: new NotEnoughBalanceBecauseDestinationNotCreated(),
|
|
95
|
+
},
|
|
96
|
+
warnings: {},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "send",
|
|
101
|
+
transaction: (t) => ({
|
|
102
|
+
...t,
|
|
103
|
+
amount: new BigNumber("1000"),
|
|
104
|
+
recipient:
|
|
105
|
+
"MECOWMKPKH2NWVZTS5V5RQDGFFYBT25KNLOPHG2KUMMNKU6FOHGJT24WBI",
|
|
106
|
+
}),
|
|
107
|
+
expectedStatus: {
|
|
108
|
+
errors: {},
|
|
109
|
+
warnings: {},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "send amount more than fees + base reserve",
|
|
114
|
+
transaction: (t, account) => ({
|
|
115
|
+
...t,
|
|
116
|
+
amount: account.balance,
|
|
117
|
+
recipient:
|
|
118
|
+
"MECOWMKPKH2NWVZTS5V5RQDGFFYBT25KNLOPHG2KUMMNKU6FOHGJT24WBI",
|
|
119
|
+
}),
|
|
120
|
+
expectedStatus: {
|
|
121
|
+
errors: {
|
|
122
|
+
amount: new NotEnoughBalance(),
|
|
123
|
+
},
|
|
124
|
+
warnings: {},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "send more than base reserve",
|
|
129
|
+
transaction: (t, account) => ({
|
|
130
|
+
...t,
|
|
131
|
+
amount: account.balance.minus("100"),
|
|
132
|
+
recipient:
|
|
133
|
+
"MECOWMKPKH2NWVZTS5V5RQDGFFYBT25KNLOPHG2KUMMNKU6FOHGJT24WBI",
|
|
134
|
+
}),
|
|
135
|
+
expectedStatus: {
|
|
136
|
+
errors: {
|
|
137
|
+
amount: new NotEnoughBalance(),
|
|
138
|
+
},
|
|
139
|
+
warnings: {},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "optIn",
|
|
144
|
+
transaction: (t) => ({
|
|
145
|
+
...t,
|
|
146
|
+
mode: "optIn",
|
|
147
|
+
assetId: "algorand/asa/31231",
|
|
148
|
+
amount: new BigNumber("1000"),
|
|
149
|
+
recipient:
|
|
150
|
+
"ZC3HFULMJF53BF5ER4E2TTGPBRGH2Y4RVS32JZ6NH4RW7LN67HCE6UBS3Q",
|
|
151
|
+
}),
|
|
152
|
+
expectedStatus: {
|
|
153
|
+
errors: {},
|
|
154
|
+
warnings: {},
|
|
155
|
+
amount: new BigNumber("0"),
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "Can't send ASA to an address that didn't Opt-in",
|
|
160
|
+
transaction: (t) => ({
|
|
161
|
+
...t,
|
|
162
|
+
subAccountId:
|
|
163
|
+
"js:2:algorand:ZC3HFULMJF53BF5ER4E2TTGPBRGH2Y4RVS32JZ6NH4RW7LN67HCE6UBS3Q:+312769",
|
|
164
|
+
amount: new BigNumber("1000"),
|
|
165
|
+
recipient:
|
|
166
|
+
"ZQVVJ2S4XWS542KXBBVIINOEHIDOEZKZWK725PWFNN2I5RNCUBI53RT2EY",
|
|
167
|
+
}),
|
|
168
|
+
expectedStatus: {
|
|
169
|
+
errors: {
|
|
170
|
+
recipient: new AlgorandASANotOptInInRecipient(),
|
|
171
|
+
},
|
|
172
|
+
warnings: {},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: "send Token",
|
|
177
|
+
transaction: (t) => ({
|
|
178
|
+
...t,
|
|
179
|
+
subAccountId:
|
|
180
|
+
"js:2:algorand:ZC3HFULMJF53BF5ER4E2TTGPBRGH2Y4RVS32JZ6NH4RW7LN67HCE6UBS3Q:+312769",
|
|
181
|
+
amount: new BigNumber("1000"),
|
|
182
|
+
recipient:
|
|
183
|
+
"MECOWMKPKH2NWVZTS5V5RQDGFFYBT25KNLOPHG2KUMMNKU6FOHGJT24WBI",
|
|
184
|
+
}),
|
|
185
|
+
expectedStatus: {
|
|
186
|
+
errors: {},
|
|
187
|
+
warnings: {},
|
|
188
|
+
amount: new BigNumber("1000"),
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: "send Token - more than available",
|
|
193
|
+
transaction: (t) => ({
|
|
194
|
+
...t,
|
|
195
|
+
subAccountId:
|
|
196
|
+
"js:2:algorand:ZC3HFULMJF53BF5ER4E2TTGPBRGH2Y4RVS32JZ6NH4RW7LN67HCE6UBS3Q:+312769",
|
|
197
|
+
amount: new BigNumber("100000000000"),
|
|
198
|
+
recipient:
|
|
199
|
+
"MECOWMKPKH2NWVZTS5V5RQDGFFYBT25KNLOPHG2KUMMNKU6FOHGJT24WBI",
|
|
200
|
+
}),
|
|
201
|
+
expectedStatus: {
|
|
202
|
+
errors: {
|
|
203
|
+
amount: new NotEnoughBalance(),
|
|
204
|
+
},
|
|
205
|
+
warnings: {},
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: "send max",
|
|
210
|
+
transaction: (t) => ({
|
|
211
|
+
...t,
|
|
212
|
+
recipient:
|
|
213
|
+
"MECOWMKPKH2NWVZTS5V5RQDGFFYBT25KNLOPHG2KUMMNKU6FOHGJT24WBI",
|
|
214
|
+
useAllAmount: true,
|
|
215
|
+
}),
|
|
216
|
+
expectedStatus: (account, _, status) => {
|
|
217
|
+
return {
|
|
218
|
+
amount: account.spendableBalance.minus(status.estimatedFees),
|
|
219
|
+
warnings: {},
|
|
220
|
+
errors: {},
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
raw: {
|
|
228
|
+
id: "js:2:algorand:MECOWMKPKH2NWVZTS5V5RQDGFFYBT25KNLOPHG2KUMMNKU6FOHGJT24WBI:",
|
|
229
|
+
seedIdentifier:
|
|
230
|
+
"c8b672d16c497bb097a48f09a9cccf0c4c7d6391acb7a4e7cd3f236fadbef9c4",
|
|
231
|
+
xpub: "6104eb314f51f4db5733976bd8c066297019ebaa6adcf39b4aa318d553c571cc",
|
|
232
|
+
name: "Algorand 4",
|
|
233
|
+
derivationMode: "",
|
|
234
|
+
index: 3,
|
|
235
|
+
freshAddress:
|
|
236
|
+
"MECOWMKPKH2NWVZTS5V5RQDGFFYBT25KNLOPHG2KUMMNKU6FOHGJT24WBI",
|
|
237
|
+
freshAddressPath: "44'/283'/3'/0/0",
|
|
238
|
+
freshAddresses: [
|
|
239
|
+
{
|
|
240
|
+
address:
|
|
241
|
+
"MECOWMKPKH2NWVZTS5V5RQDGFFYBT25KNLOPHG2KUMMNKU6FOHGJT24WBI",
|
|
242
|
+
derivationPath: "44'/283'/3'/0/0",
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
unitMagnitude: 6,
|
|
246
|
+
blockHeight: 8518189,
|
|
247
|
+
balance: "0",
|
|
248
|
+
spendableBalance: "0",
|
|
249
|
+
currencyId: "algorand",
|
|
250
|
+
lastSyncDate: "",
|
|
251
|
+
operations: [],
|
|
252
|
+
pendingOperations: [],
|
|
253
|
+
},
|
|
254
|
+
transactions: [
|
|
255
|
+
{
|
|
256
|
+
name: "Can't send funds if balance too low",
|
|
257
|
+
transaction: (t) => ({
|
|
258
|
+
...t,
|
|
259
|
+
amount: new BigNumber("1000"),
|
|
260
|
+
recipient:
|
|
261
|
+
"YWZPDCL5XQPCPGBXKB7KAG7YF2QGCGEX37YTSM55CPEPHKNE2ZSKRAXNQ4",
|
|
262
|
+
}),
|
|
263
|
+
expectedStatus: {
|
|
264
|
+
errors: {},
|
|
265
|
+
warnings: {},
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
name: "Can't send ASA if Algo balance too low",
|
|
270
|
+
transaction: (t) => ({
|
|
271
|
+
...t,
|
|
272
|
+
subAccountId:
|
|
273
|
+
"js:2:algorand:MECOWMKPKH2NWVZTS5V5RQDGFFYBT25KNLOPHG2KUMMNKU6FOHGJT24WBI:+312769",
|
|
274
|
+
amount: new BigNumber("1000000"),
|
|
275
|
+
recipient:
|
|
276
|
+
"YWZPDCL5XQPCPGBXKB7KAG7YF2QGCGEX37YTSM55CPEPHKNE2ZSKRAXNQ4",
|
|
277
|
+
}),
|
|
278
|
+
expectedStatus: {
|
|
279
|
+
errors: {
|
|
280
|
+
amount: new NotEnoughBalance(),
|
|
281
|
+
},
|
|
282
|
+
warnings: {},
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
export const dataset: DatasetTest<AlgorandTransaction> = {
|
|
291
|
+
implementations: ["js"],
|
|
292
|
+
currencies: {
|
|
293
|
+
algorand,
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
describe("Algorand bridge", () => {
|
|
298
|
+
test.todo(
|
|
299
|
+
"This is an empty test to make jest command pass. Remove it once there is a real test."
|
|
300
|
+
);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* NOTE: if tests are added to this file,
|
|
305
|
+
* like done in libs/coin-polkadot/src/bridge.integration.test.ts for example,
|
|
306
|
+
* this file fill need to be imported in ledger-live-common
|
|
307
|
+
* libs/ledger-live-common/src/families/algorand/bridge.integration.test.ts
|
|
308
|
+
* like done for polkadot.
|
|
309
|
+
* cf.
|
|
310
|
+
* - libs/coin-polkadot/src/bridge.integration.test.ts
|
|
311
|
+
* - libs/ledger-live-common/src/families/polkadot/bridge.integration.test.ts
|
|
312
|
+
*/
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { encode as msgpackEncode } from "algo-msgpack-with-bigint";
|
|
2
|
+
import type {
|
|
3
|
+
EncodedSignedTransaction as AlgoSignedTransactionPayload,
|
|
4
|
+
Transaction as AlgoTransaction,
|
|
5
|
+
EncodedTransaction as AlgoTransactionPayload,
|
|
6
|
+
} from "algosdk";
|
|
7
|
+
import {
|
|
8
|
+
makeAssetTransferTxnWithSuggestedParams,
|
|
9
|
+
makePaymentTxnWithSuggestedParams,
|
|
10
|
+
} from "algosdk";
|
|
11
|
+
|
|
12
|
+
import type { Account } from "@ledgerhq/types-live";
|
|
13
|
+
import { AlgorandAPI } from "./api";
|
|
14
|
+
import { extractTokenId } from "./tokens";
|
|
15
|
+
import type { Transaction } from "./types";
|
|
16
|
+
|
|
17
|
+
export const buildTransactionPayload =
|
|
18
|
+
(algorandAPI: AlgorandAPI) =>
|
|
19
|
+
async (
|
|
20
|
+
account: Account,
|
|
21
|
+
transaction: Transaction
|
|
22
|
+
): Promise<AlgoTransactionPayload> => {
|
|
23
|
+
const { amount, recipient, mode, memo, assetId, subAccountId } =
|
|
24
|
+
transaction;
|
|
25
|
+
const subAccount = subAccountId
|
|
26
|
+
? account.subAccounts &&
|
|
27
|
+
account.subAccounts.find((t) => t.id === subAccountId)
|
|
28
|
+
: null;
|
|
29
|
+
|
|
30
|
+
const note = memo ? new TextEncoder().encode(memo) : undefined;
|
|
31
|
+
|
|
32
|
+
const params = await algorandAPI.getTransactionParams();
|
|
33
|
+
|
|
34
|
+
let algoTxn: AlgoTransaction;
|
|
35
|
+
if (subAccount || (assetId && mode === "optIn")) {
|
|
36
|
+
const targetAssetId =
|
|
37
|
+
subAccount && subAccount.type === "TokenAccount"
|
|
38
|
+
? extractTokenId(subAccount.token.id)
|
|
39
|
+
: assetId
|
|
40
|
+
? extractTokenId(assetId)
|
|
41
|
+
: "";
|
|
42
|
+
|
|
43
|
+
if (!targetAssetId) {
|
|
44
|
+
throw new Error("Token Asset Id not found");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
algoTxn = makeAssetTransferTxnWithSuggestedParams(
|
|
48
|
+
account.freshAddress,
|
|
49
|
+
recipient,
|
|
50
|
+
undefined,
|
|
51
|
+
undefined,
|
|
52
|
+
amount.toNumber(),
|
|
53
|
+
note,
|
|
54
|
+
Number(targetAssetId),
|
|
55
|
+
params,
|
|
56
|
+
undefined
|
|
57
|
+
);
|
|
58
|
+
} else {
|
|
59
|
+
algoTxn = makePaymentTxnWithSuggestedParams(
|
|
60
|
+
account.freshAddress,
|
|
61
|
+
recipient,
|
|
62
|
+
amount.toNumber(),
|
|
63
|
+
undefined,
|
|
64
|
+
note,
|
|
65
|
+
params
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Bit of safety: set tx validity to the next 1000 blocks
|
|
70
|
+
algoTxn.firstRound = params.lastRound;
|
|
71
|
+
algoTxn.lastRound = params.lastRound + 1000;
|
|
72
|
+
|
|
73
|
+
// Flaw in the SDK: payload isn't sorted, but it needs to be for msgPack encoding
|
|
74
|
+
const sorted = Object.fromEntries(
|
|
75
|
+
Object.entries(algoTxn.get_obj_for_encoding()).sort()
|
|
76
|
+
) as AlgoTransactionPayload;
|
|
77
|
+
|
|
78
|
+
return sorted;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const encodeToSign = (payload: AlgoTransactionPayload): string => {
|
|
82
|
+
const msgPackEncoded = msgpackEncode(payload);
|
|
83
|
+
|
|
84
|
+
return Buffer.from(msgPackEncoded).toString("hex");
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const encodeToBroadcast = (
|
|
88
|
+
payload: AlgoTransactionPayload,
|
|
89
|
+
signature: Buffer
|
|
90
|
+
): Buffer => {
|
|
91
|
+
const signedPayload: AlgoSignedTransactionPayload = {
|
|
92
|
+
sig: signature,
|
|
93
|
+
txn: payload,
|
|
94
|
+
};
|
|
95
|
+
const msgPackEncoded = msgpackEncode(signedPayload);
|
|
96
|
+
|
|
97
|
+
return Buffer.from(msgPackEncoded);
|
|
98
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { getAccountCurrency } from "@ledgerhq/coin-framework/account/index";
|
|
2
|
+
import type {
|
|
3
|
+
Account,
|
|
4
|
+
AccountLike,
|
|
5
|
+
AccountLikeArray,
|
|
6
|
+
} from "@ledgerhq/types-live";
|
|
7
|
+
import invariant from "invariant";
|
|
8
|
+
import flatMap from "lodash/flatMap";
|
|
9
|
+
import { extractTokenId } from "./tokens";
|
|
10
|
+
import type { AlgorandAccount, Transaction } from "./types";
|
|
11
|
+
|
|
12
|
+
const options = [
|
|
13
|
+
{
|
|
14
|
+
name: "mode",
|
|
15
|
+
type: String,
|
|
16
|
+
desc: "mode of transaction: send, optIn, claimReward",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: "fees",
|
|
20
|
+
type: String,
|
|
21
|
+
desc: "how much fees",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "gasLimit",
|
|
25
|
+
type: String,
|
|
26
|
+
desc: "how much gasLimit. default is estimated with the recipient",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "memo",
|
|
30
|
+
type: String,
|
|
31
|
+
desc: "set a memo",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "token",
|
|
35
|
+
alias: "t",
|
|
36
|
+
type: String,
|
|
37
|
+
desc: "use an token account children of the account",
|
|
38
|
+
multiple: true,
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function inferAccounts(
|
|
43
|
+
account: Account,
|
|
44
|
+
opts: Record<string, any>
|
|
45
|
+
): AccountLikeArray {
|
|
46
|
+
invariant(account.currency.family === "algorand", "algorand family");
|
|
47
|
+
|
|
48
|
+
if (!opts.token || opts.mode === "optIn") {
|
|
49
|
+
const accounts: Account[] = [account];
|
|
50
|
+
return accounts;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return opts.token.map((token?: string) => {
|
|
54
|
+
const subAccounts = account.subAccounts || [];
|
|
55
|
+
|
|
56
|
+
if (token) {
|
|
57
|
+
const subAccount = subAccounts.find((t) => {
|
|
58
|
+
const currency = getAccountCurrency(t);
|
|
59
|
+
return (
|
|
60
|
+
token.toLowerCase() === currency.ticker.toLowerCase() ||
|
|
61
|
+
token.toLowerCase() === extractTokenId(currency.id)
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!subAccount) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
"token account '" +
|
|
68
|
+
token +
|
|
69
|
+
"' not found. Available: " +
|
|
70
|
+
subAccounts.map((t) => getAccountCurrency(t).ticker).join(", ")
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return subAccount;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function inferTransactions(
|
|
80
|
+
transactions: Array<{
|
|
81
|
+
account: AccountLike;
|
|
82
|
+
transaction: Transaction;
|
|
83
|
+
}>,
|
|
84
|
+
opts: Record<string, any>,
|
|
85
|
+
{ inferAmount }: any
|
|
86
|
+
): Transaction[] {
|
|
87
|
+
return flatMap(transactions, ({ transaction, account }) => {
|
|
88
|
+
invariant(transaction.family === "algorand", "algorand family");
|
|
89
|
+
|
|
90
|
+
if (account.type === "Account") {
|
|
91
|
+
invariant(
|
|
92
|
+
(account as AlgorandAccount).algorandResources,
|
|
93
|
+
"unactivated account"
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (account.type === "TokenAccount") {
|
|
98
|
+
const isDelisted = account.token.delisted === true;
|
|
99
|
+
invariant(!isDelisted, "token is delisted");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
...transaction,
|
|
104
|
+
family: "algorand",
|
|
105
|
+
fees: opts.fees ? inferAmount(account, opts.fees) : null,
|
|
106
|
+
memo: opts.memo,
|
|
107
|
+
mode: opts.mode || "send",
|
|
108
|
+
subAccountId: account.type === "TokenAccount" ? account.id : null,
|
|
109
|
+
assetId: opts.token ? "algorand/asa/" + opts.token : null,
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* FIXME: unsued network and cache params are passed to makeCliTools because of how the
|
|
116
|
+
* libs/ledger-live-common/scripts/sync-families-dispatch.mjs script works.
|
|
117
|
+
*/
|
|
118
|
+
export default function makeCliTools(_network: unknown, _cache: unknown) {
|
|
119
|
+
return {
|
|
120
|
+
options,
|
|
121
|
+
inferAccounts,
|
|
122
|
+
inferTransactions,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { getAccountUnit } from "@ledgerhq/coin-framework/account/index";
|
|
2
|
+
import {
|
|
3
|
+
findTokenById,
|
|
4
|
+
formatCurrencyUnit,
|
|
5
|
+
} from "@ledgerhq/coin-framework/currencies/index";
|
|
6
|
+
import type { CommonDeviceTransactionField as DeviceTransactionField } from "@ledgerhq/coin-framework/transaction/common";
|
|
7
|
+
import { TokenCurrency } from "@ledgerhq/types-cryptoassets";
|
|
8
|
+
import { AccountLike } from "@ledgerhq/types-live";
|
|
9
|
+
import { extractTokenId } from "./tokens";
|
|
10
|
+
import type {
|
|
11
|
+
AlgorandTransaction,
|
|
12
|
+
Transaction,
|
|
13
|
+
TransactionStatus,
|
|
14
|
+
} from "./types";
|
|
15
|
+
|
|
16
|
+
export type ExtraDeviceTransactionField = {
|
|
17
|
+
type: "polkadot.validators";
|
|
18
|
+
label: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const displayTokenValue = (token: TokenCurrency) =>
|
|
22
|
+
`${token.name} (#${extractTokenId(token.id)})`;
|
|
23
|
+
|
|
24
|
+
const getSendFields = (
|
|
25
|
+
transaction: Transaction,
|
|
26
|
+
status: TransactionStatus,
|
|
27
|
+
account: AccountLike,
|
|
28
|
+
addRecipient: boolean
|
|
29
|
+
) => {
|
|
30
|
+
const { estimatedFees, amount } = status;
|
|
31
|
+
const fields: {
|
|
32
|
+
type: string;
|
|
33
|
+
label: string;
|
|
34
|
+
value?: string;
|
|
35
|
+
address?: string;
|
|
36
|
+
}[] = [];
|
|
37
|
+
fields.push({
|
|
38
|
+
type: "text",
|
|
39
|
+
label: "Type",
|
|
40
|
+
value: account.type === "TokenAccount" ? "Asset xfer" : "Payment",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (estimatedFees && !estimatedFees.isZero()) {
|
|
44
|
+
fields.push({
|
|
45
|
+
type: "fees",
|
|
46
|
+
label: "Fee",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (addRecipient) {
|
|
51
|
+
fields.push({
|
|
52
|
+
type: "address",
|
|
53
|
+
label: "Recipient",
|
|
54
|
+
address: transaction.recipient,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (account.type === "TokenAccount") {
|
|
59
|
+
fields.push({
|
|
60
|
+
type: "text",
|
|
61
|
+
label: "Asset ID",
|
|
62
|
+
value: displayTokenValue(account.token),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (amount) {
|
|
67
|
+
fields.push({
|
|
68
|
+
type: "text",
|
|
69
|
+
label: account.type === "TokenAccount" ? "Asset amt" : "Amount",
|
|
70
|
+
value: formatCurrencyUnit(getAccountUnit(account), amount, {
|
|
71
|
+
showCode: true,
|
|
72
|
+
disableRounding: true,
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return fields;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function getDeviceTransactionConfig({
|
|
81
|
+
account,
|
|
82
|
+
transaction,
|
|
83
|
+
status,
|
|
84
|
+
}: {
|
|
85
|
+
account: AccountLike;
|
|
86
|
+
transaction: AlgorandTransaction;
|
|
87
|
+
status: TransactionStatus;
|
|
88
|
+
}): Array<DeviceTransactionField> {
|
|
89
|
+
const { mode, assetId } = transaction;
|
|
90
|
+
const { estimatedFees } = status;
|
|
91
|
+
let fields: {
|
|
92
|
+
type: string;
|
|
93
|
+
label: string;
|
|
94
|
+
value?: string;
|
|
95
|
+
address?: string;
|
|
96
|
+
}[] = [];
|
|
97
|
+
|
|
98
|
+
switch (mode) {
|
|
99
|
+
case "send":
|
|
100
|
+
fields = getSendFields(transaction, status, account, false);
|
|
101
|
+
break;
|
|
102
|
+
|
|
103
|
+
case "claimReward":
|
|
104
|
+
fields = getSendFields(transaction, status, account, true);
|
|
105
|
+
break;
|
|
106
|
+
|
|
107
|
+
case "optIn":
|
|
108
|
+
fields.push({
|
|
109
|
+
type: "text",
|
|
110
|
+
label: "Type",
|
|
111
|
+
value: "Asset xfer",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (estimatedFees && !estimatedFees.isZero()) {
|
|
115
|
+
fields.push({
|
|
116
|
+
type: "fees",
|
|
117
|
+
label: "Fee",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (assetId) {
|
|
122
|
+
const token = findTokenById(assetId);
|
|
123
|
+
fields.push({
|
|
124
|
+
type: "text",
|
|
125
|
+
label: "Asset ID",
|
|
126
|
+
value: token
|
|
127
|
+
? displayTokenValue(token)
|
|
128
|
+
: `#${extractTokenId(assetId)}`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fields.push({
|
|
133
|
+
type: "text",
|
|
134
|
+
label: "Asset amt",
|
|
135
|
+
value: "0",
|
|
136
|
+
});
|
|
137
|
+
break;
|
|
138
|
+
|
|
139
|
+
default:
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return fields as Array<DeviceTransactionField>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export default getDeviceTransactionConfig;
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Resolver } from "@ledgerhq/coin-framework/bridge/getAddressWrapper";
|
|
2
|
+
import Algorand from "@ledgerhq/hw-app-algorand";
|
|
3
|
+
|
|
4
|
+
const resolver: Resolver = async (transport, { path, verify }) => {
|
|
5
|
+
const algorand = new Algorand(transport);
|
|
6
|
+
const r = await algorand.getAddress(path, verify || false);
|
|
7
|
+
return {
|
|
8
|
+
address: r.address,
|
|
9
|
+
publicKey: r.publicKey,
|
|
10
|
+
path,
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default resolver;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Account } from "@ledgerhq/types-live";
|
|
2
|
+
import { BigNumber } from "bignumber.js";
|
|
3
|
+
import type { AlgorandAccount } from "./types";
|
|
4
|
+
|
|
5
|
+
export function initAccount(r: Account): void {
|
|
6
|
+
(r as AlgorandAccount).algorandResources = {
|
|
7
|
+
rewards: new BigNumber(0),
|
|
8
|
+
nbAssets: r.subAccounts?.length ?? 0,
|
|
9
|
+
};
|
|
10
|
+
}
|