@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,429 @@
|
|
|
1
|
+
import {
|
|
2
|
+
emptyHistoryCache,
|
|
3
|
+
encodeAccountId,
|
|
4
|
+
inferSubOperations,
|
|
5
|
+
} from "@ledgerhq/coin-framework/account/index";
|
|
6
|
+
import type { GetAccountShape } from "@ledgerhq/coin-framework/bridge/jsHelpers";
|
|
7
|
+
import { mergeOps } from "@ledgerhq/coin-framework/bridge/jsHelpers";
|
|
8
|
+
import {
|
|
9
|
+
findTokenById,
|
|
10
|
+
listTokensForCryptoCurrency,
|
|
11
|
+
} from "@ledgerhq/coin-framework/currencies/index";
|
|
12
|
+
import { encodeOperationId } from "@ledgerhq/coin-framework/operation";
|
|
13
|
+
import { promiseAllBatched } from "@ledgerhq/live-promise";
|
|
14
|
+
import { BigNumber } from "bignumber.js";
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
AlgoAsset,
|
|
18
|
+
AlgoAssetTransferInfo,
|
|
19
|
+
AlgoPaymentInfo,
|
|
20
|
+
AlgoTransaction,
|
|
21
|
+
AlgorandAPI,
|
|
22
|
+
} from "./api";
|
|
23
|
+
|
|
24
|
+
import { AlgoTransactionType } from "./api";
|
|
25
|
+
|
|
26
|
+
import { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets";
|
|
27
|
+
import type {
|
|
28
|
+
Account,
|
|
29
|
+
Operation,
|
|
30
|
+
OperationType,
|
|
31
|
+
SyncConfig,
|
|
32
|
+
TokenAccount,
|
|
33
|
+
} from "@ledgerhq/types-live";
|
|
34
|
+
import { computeAlgoMaxSpendable } from "./logic";
|
|
35
|
+
import { addPrefixToken, extractTokenId } from "./tokens";
|
|
36
|
+
|
|
37
|
+
const getASAOperationAmount = (
|
|
38
|
+
transaction: AlgoTransaction,
|
|
39
|
+
accountAddress: string
|
|
40
|
+
): BigNumber => {
|
|
41
|
+
let assetAmount = new BigNumber(0);
|
|
42
|
+
if (transaction.type === AlgoTransactionType.ASSET_TRANSFER) {
|
|
43
|
+
const details = transaction.details as AlgoAssetTransferInfo;
|
|
44
|
+
const assetSender = details.assetSenderAddress
|
|
45
|
+
? details.assetSenderAddress
|
|
46
|
+
: transaction.senderAddress;
|
|
47
|
+
|
|
48
|
+
// Account is either sender or recipient (if both the balance is unchanged)
|
|
49
|
+
if (
|
|
50
|
+
(assetSender === accountAddress) !==
|
|
51
|
+
(details.assetRecipientAddress == accountAddress)
|
|
52
|
+
) {
|
|
53
|
+
assetAmount = assetAmount.plus(details.assetAmount);
|
|
54
|
+
}
|
|
55
|
+
// Account is either sender or close-to, but not both
|
|
56
|
+
if (
|
|
57
|
+
(assetSender === accountAddress) !==
|
|
58
|
+
(details.assetCloseToAddress &&
|
|
59
|
+
details.assetCloseToAddress === accountAddress)
|
|
60
|
+
) {
|
|
61
|
+
if (details.assetCloseAmount) {
|
|
62
|
+
assetAmount = assetAmount.plus(details.assetCloseAmount);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return assetAmount;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const getOperationAmounts = (
|
|
70
|
+
transaction: AlgoTransaction,
|
|
71
|
+
accountAddress: string
|
|
72
|
+
): { amount: BigNumber; rewards: BigNumber } => {
|
|
73
|
+
let amount = new BigNumber(0);
|
|
74
|
+
let rewards = new BigNumber(0);
|
|
75
|
+
|
|
76
|
+
if (transaction.senderAddress === accountAddress) {
|
|
77
|
+
const senderRewards = transaction.senderRewards;
|
|
78
|
+
amount = amount.minus(senderRewards).plus(transaction.fee);
|
|
79
|
+
rewards = rewards.plus(senderRewards);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (transaction.type === AlgoTransactionType.PAYMENT) {
|
|
83
|
+
const details = transaction.details as AlgoPaymentInfo;
|
|
84
|
+
if (transaction.senderAddress == details.recipientAddress) {
|
|
85
|
+
return {
|
|
86
|
+
amount,
|
|
87
|
+
rewards,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (transaction.senderAddress === accountAddress) {
|
|
91
|
+
amount = amount.plus(details.amount);
|
|
92
|
+
}
|
|
93
|
+
if (details.recipientAddress == accountAddress) {
|
|
94
|
+
const recipientRewards = transaction.recipientRewards;
|
|
95
|
+
amount = amount.plus(details.amount).plus(recipientRewards);
|
|
96
|
+
rewards = rewards.plus(recipientRewards);
|
|
97
|
+
}
|
|
98
|
+
if (
|
|
99
|
+
transaction.closeRewards &&
|
|
100
|
+
details.closeAmount &&
|
|
101
|
+
details.closeToAddress === accountAddress
|
|
102
|
+
) {
|
|
103
|
+
const closeRewards = transaction.closeRewards;
|
|
104
|
+
amount = amount.plus(details.closeAmount).plus(closeRewards);
|
|
105
|
+
rewards = rewards.plus(closeRewards);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
amount,
|
|
111
|
+
rewards,
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const getASAOperationType = (
|
|
116
|
+
transaction: AlgoTransaction,
|
|
117
|
+
accountAddress: string
|
|
118
|
+
): OperationType => {
|
|
119
|
+
return transaction.senderAddress === accountAddress ? "OUT" : "IN";
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const getOperationType = (
|
|
123
|
+
transaction: AlgoTransaction,
|
|
124
|
+
accountAddress: string
|
|
125
|
+
): OperationType => {
|
|
126
|
+
if (transaction.type === AlgoTransactionType.ASSET_TRANSFER) {
|
|
127
|
+
const details = transaction.details as AlgoAssetTransferInfo;
|
|
128
|
+
if (
|
|
129
|
+
details.assetAmount.isZero() &&
|
|
130
|
+
transaction.senderAddress == details.assetRecipientAddress
|
|
131
|
+
) {
|
|
132
|
+
return "OPT_IN";
|
|
133
|
+
} else if (
|
|
134
|
+
details.assetCloseToAddress &&
|
|
135
|
+
transaction.senderAddress == accountAddress
|
|
136
|
+
) {
|
|
137
|
+
return "OPT_OUT";
|
|
138
|
+
} else {
|
|
139
|
+
return "FEES";
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return transaction.senderAddress === accountAddress ? "OUT" : "IN";
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const getOperationSenders = (transaction: AlgoTransaction): string[] => {
|
|
147
|
+
return [transaction.senderAddress];
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const getOperationRecipients = (transaction: AlgoTransaction): string[] => {
|
|
151
|
+
const recipients: string[] = [];
|
|
152
|
+
if (transaction.type === AlgoTransactionType.PAYMENT) {
|
|
153
|
+
const details = transaction.details as AlgoPaymentInfo;
|
|
154
|
+
recipients.push(details.recipientAddress);
|
|
155
|
+
if (details.closeToAddress) {
|
|
156
|
+
recipients.push(details.closeToAddress);
|
|
157
|
+
}
|
|
158
|
+
} else if (transaction.type === AlgoTransactionType.ASSET_TRANSFER) {
|
|
159
|
+
const details = transaction.details as AlgoAssetTransferInfo;
|
|
160
|
+
|
|
161
|
+
recipients.push(details.assetRecipientAddress);
|
|
162
|
+
if (details.assetCloseToAddress) {
|
|
163
|
+
recipients.push(details.assetCloseToAddress);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return recipients;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const getOperationAssetId = (
|
|
170
|
+
transaction: AlgoTransaction
|
|
171
|
+
): string | undefined => {
|
|
172
|
+
if (transaction.type === AlgoTransactionType.ASSET_TRANSFER) {
|
|
173
|
+
const details = transaction.details as AlgoAssetTransferInfo;
|
|
174
|
+
return details.assetId;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const mapTransactionToOperation = (
|
|
179
|
+
tx: AlgoTransaction,
|
|
180
|
+
accountId: string,
|
|
181
|
+
accountAddress: string,
|
|
182
|
+
subAccounts?: TokenAccount[]
|
|
183
|
+
): Partial<Operation> => {
|
|
184
|
+
const hash = tx.id;
|
|
185
|
+
const blockHeight = tx.round;
|
|
186
|
+
const date = new Date(parseInt(tx.timestamp) * 1000);
|
|
187
|
+
const fee = tx.fee;
|
|
188
|
+
const memo = tx.note;
|
|
189
|
+
const senders: string[] = getOperationSenders(tx);
|
|
190
|
+
const recipients: string[] = getOperationRecipients(tx);
|
|
191
|
+
const { amount, rewards } = getOperationAmounts(tx, accountAddress);
|
|
192
|
+
const type = getOperationType(tx, accountAddress);
|
|
193
|
+
const assetId = getOperationAssetId(tx);
|
|
194
|
+
|
|
195
|
+
const subOperations = subAccounts
|
|
196
|
+
? inferSubOperations(tx.id, subAccounts)
|
|
197
|
+
: undefined;
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
id: encodeOperationId(accountId, hash, type),
|
|
201
|
+
hash,
|
|
202
|
+
date,
|
|
203
|
+
type,
|
|
204
|
+
value: amount,
|
|
205
|
+
fee,
|
|
206
|
+
senders,
|
|
207
|
+
recipients,
|
|
208
|
+
blockHeight,
|
|
209
|
+
accountId,
|
|
210
|
+
subOperations,
|
|
211
|
+
extra: {
|
|
212
|
+
rewards,
|
|
213
|
+
memo,
|
|
214
|
+
assetId,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const mapTransactionToASAOperation = (
|
|
220
|
+
tx: AlgoTransaction,
|
|
221
|
+
accountId: string,
|
|
222
|
+
accountAddress: string
|
|
223
|
+
): Partial<Operation> => {
|
|
224
|
+
const hash = tx.id;
|
|
225
|
+
const blockHeight = tx.round;
|
|
226
|
+
const date = new Date(parseInt(tx.timestamp) * 1000);
|
|
227
|
+
const fee = tx.fee;
|
|
228
|
+
const senders: string[] = getOperationSenders(tx);
|
|
229
|
+
const recipients: string[] = getOperationRecipients(tx);
|
|
230
|
+
const type = getASAOperationType(tx, accountAddress);
|
|
231
|
+
const amount = getASAOperationAmount(tx, accountAddress);
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
id: encodeOperationId(accountId, hash, type),
|
|
235
|
+
hash,
|
|
236
|
+
date,
|
|
237
|
+
type,
|
|
238
|
+
value: amount,
|
|
239
|
+
fee,
|
|
240
|
+
senders,
|
|
241
|
+
recipients,
|
|
242
|
+
blockHeight,
|
|
243
|
+
accountId,
|
|
244
|
+
extra: {},
|
|
245
|
+
};
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
export function makeGetAccountShape(algorandAPI: AlgorandAPI): GetAccountShape {
|
|
249
|
+
return async (info, syncConfig): Promise<Partial<Account>> => {
|
|
250
|
+
const { address, initialAccount, currency, derivationMode } = info;
|
|
251
|
+
const oldOperations = initialAccount?.operations || [];
|
|
252
|
+
const startAt = oldOperations.length
|
|
253
|
+
? (oldOperations[0].blockHeight || 0) + 1
|
|
254
|
+
: 0;
|
|
255
|
+
const accountId = encodeAccountId({
|
|
256
|
+
type: "js",
|
|
257
|
+
version: "2",
|
|
258
|
+
currencyId: currency.id,
|
|
259
|
+
xpubOrAddress: address,
|
|
260
|
+
derivationMode,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const { round, balance, pendingRewards, assets } =
|
|
264
|
+
await algorandAPI.getAccount(address);
|
|
265
|
+
|
|
266
|
+
const nbAssets = assets.length;
|
|
267
|
+
|
|
268
|
+
// NOTE Actual spendable amount depends on the transaction
|
|
269
|
+
const spendableBalance = computeAlgoMaxSpendable({
|
|
270
|
+
accountBalance: balance,
|
|
271
|
+
nbAccountAssets: nbAssets,
|
|
272
|
+
mode: "send",
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const newTransactions: AlgoTransaction[] =
|
|
276
|
+
await algorandAPI.getAccountTransactions(address, startAt);
|
|
277
|
+
|
|
278
|
+
const subAccounts = await buildSubAccounts({
|
|
279
|
+
currency,
|
|
280
|
+
accountId,
|
|
281
|
+
initialAccount,
|
|
282
|
+
initialAccountAddress: address,
|
|
283
|
+
assets,
|
|
284
|
+
newTransactions,
|
|
285
|
+
syncConfig,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const newOperations = newTransactions.map((tx) =>
|
|
289
|
+
mapTransactionToOperation(tx, accountId, address, subAccounts)
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const operations = mergeOps(oldOperations, newOperations as Operation[]);
|
|
293
|
+
|
|
294
|
+
const shape = {
|
|
295
|
+
id: accountId,
|
|
296
|
+
xpub: address,
|
|
297
|
+
blockHeight: round,
|
|
298
|
+
balance,
|
|
299
|
+
spendableBalance,
|
|
300
|
+
operations,
|
|
301
|
+
operationsCount: operations.length,
|
|
302
|
+
subAccounts,
|
|
303
|
+
algorandResources: {
|
|
304
|
+
rewards: pendingRewards,
|
|
305
|
+
nbAssets,
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
return shape;
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function buildSubAccount({
|
|
313
|
+
parentAccountId,
|
|
314
|
+
parentAccountAddress,
|
|
315
|
+
token,
|
|
316
|
+
initialTokenAccount,
|
|
317
|
+
newTransactions,
|
|
318
|
+
balance,
|
|
319
|
+
}: {
|
|
320
|
+
parentAccountId: string;
|
|
321
|
+
parentAccountAddress: string;
|
|
322
|
+
token: TokenCurrency;
|
|
323
|
+
initialTokenAccount: TokenAccount;
|
|
324
|
+
newTransactions: AlgoTransaction[];
|
|
325
|
+
balance: BigNumber;
|
|
326
|
+
}) {
|
|
327
|
+
const extractedId = extractTokenId(token.id);
|
|
328
|
+
const tokenAccountId = parentAccountId + "+" + extractedId;
|
|
329
|
+
|
|
330
|
+
const oldOperations = initialTokenAccount?.operations || [];
|
|
331
|
+
|
|
332
|
+
const newOperations = newTransactions
|
|
333
|
+
.filter((tx) => tx.type === AlgoTransactionType.ASSET_TRANSFER)
|
|
334
|
+
.filter((tx) => {
|
|
335
|
+
const details = tx.details as AlgoAssetTransferInfo;
|
|
336
|
+
return Number(details.assetId) === Number(extractedId);
|
|
337
|
+
})
|
|
338
|
+
.filter((tx) => getOperationType(tx, parentAccountAddress) != "OPT_IN")
|
|
339
|
+
.map((tx) =>
|
|
340
|
+
mapTransactionToASAOperation(tx, tokenAccountId, parentAccountAddress)
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
const operations = mergeOps(oldOperations, newOperations as Operation[]);
|
|
344
|
+
|
|
345
|
+
const tokenAccount: TokenAccount = {
|
|
346
|
+
type: "TokenAccount",
|
|
347
|
+
id: tokenAccountId,
|
|
348
|
+
parentId: parentAccountId,
|
|
349
|
+
starred: false,
|
|
350
|
+
token,
|
|
351
|
+
operationsCount: operations.length,
|
|
352
|
+
operations,
|
|
353
|
+
pendingOperations: [],
|
|
354
|
+
balance,
|
|
355
|
+
spendableBalance: balance,
|
|
356
|
+
swapHistory: [],
|
|
357
|
+
creationDate:
|
|
358
|
+
operations.length > 0
|
|
359
|
+
? operations[operations.length - 1].date
|
|
360
|
+
: new Date(),
|
|
361
|
+
balanceHistoryCache: emptyHistoryCache,
|
|
362
|
+
};
|
|
363
|
+
return tokenAccount;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function buildSubAccounts({
|
|
367
|
+
currency,
|
|
368
|
+
accountId,
|
|
369
|
+
initialAccount,
|
|
370
|
+
initialAccountAddress,
|
|
371
|
+
assets,
|
|
372
|
+
newTransactions,
|
|
373
|
+
syncConfig,
|
|
374
|
+
}: {
|
|
375
|
+
currency: CryptoCurrency;
|
|
376
|
+
accountId: string;
|
|
377
|
+
initialAccount: Account | null | undefined;
|
|
378
|
+
initialAccountAddress: string;
|
|
379
|
+
assets: AlgoAsset[];
|
|
380
|
+
newTransactions: AlgoTransaction[];
|
|
381
|
+
syncConfig: SyncConfig;
|
|
382
|
+
}): Promise<TokenAccount[] | undefined> {
|
|
383
|
+
const { blacklistedTokenIds = [] } = syncConfig;
|
|
384
|
+
if (listTokensForCryptoCurrency(currency).length === 0) return undefined;
|
|
385
|
+
const tokenAccounts: TokenAccount[] = [];
|
|
386
|
+
const existingAccountByTicker: { [ticker: string]: TokenAccount } = {}; // used for fast lookup
|
|
387
|
+
const existingAccountTickers: string[] = []; // used to keep track of ordering
|
|
388
|
+
|
|
389
|
+
if (initialAccount && initialAccount.subAccounts) {
|
|
390
|
+
for (const existingSubAccount of initialAccount.subAccounts) {
|
|
391
|
+
if (existingSubAccount.type === "TokenAccount") {
|
|
392
|
+
const { ticker, id } = existingSubAccount.token;
|
|
393
|
+
|
|
394
|
+
if (!blacklistedTokenIds.includes(id)) {
|
|
395
|
+
existingAccountTickers.push(ticker);
|
|
396
|
+
existingAccountByTicker[ticker] = existingSubAccount;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// filter by token existence
|
|
403
|
+
await promiseAllBatched(3, assets, async (asset) => {
|
|
404
|
+
const token = findTokenById(addPrefixToken(asset.assetId));
|
|
405
|
+
|
|
406
|
+
if (token && !blacklistedTokenIds.includes(token.id)) {
|
|
407
|
+
const initialTokenAccount = existingAccountByTicker[token.ticker];
|
|
408
|
+
const tokenAccount = await buildSubAccount({
|
|
409
|
+
parentAccountId: accountId,
|
|
410
|
+
parentAccountAddress: initialAccountAddress,
|
|
411
|
+
initialTokenAccount,
|
|
412
|
+
token,
|
|
413
|
+
newTransactions,
|
|
414
|
+
balance: asset.balance,
|
|
415
|
+
});
|
|
416
|
+
if (tokenAccount) tokenAccounts.push(tokenAccount);
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
// Preserve order of tokenAccounts from the existing token accounts
|
|
420
|
+
tokenAccounts.sort((a, b) => {
|
|
421
|
+
const i = existingAccountTickers.indexOf(a.token.ticker);
|
|
422
|
+
const j = existingAccountTickers.indexOf(b.token.ticker);
|
|
423
|
+
if (i === j) return 0;
|
|
424
|
+
if (i < 0) return 1;
|
|
425
|
+
if (j < 0) return -1;
|
|
426
|
+
return i - j;
|
|
427
|
+
});
|
|
428
|
+
return tokenAccounts;
|
|
429
|
+
}
|
package/src/logic.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { BigNumber } from "bignumber.js";
|
|
2
|
+
import { AlgorandAPI } from "./api";
|
|
3
|
+
import type { AlgorandOperationMode } from "./types";
|
|
4
|
+
|
|
5
|
+
export const ALGORAND_MAX_MEMO_SIZE = 32;
|
|
6
|
+
export const ALGORAND_MIN_ACCOUNT_BALANCE = 100000;
|
|
7
|
+
|
|
8
|
+
export const recipientHasAsset =
|
|
9
|
+
(algorandAPI: AlgorandAPI) =>
|
|
10
|
+
async (recipientAddress: string, assetId: string): Promise<boolean> => {
|
|
11
|
+
const recipientAccount = await algorandAPI.getAccount(recipientAddress);
|
|
12
|
+
return recipientAccount.assets.map((a) => a.assetId).includes(assetId);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const isAmountValid =
|
|
16
|
+
(algorandAPI: AlgorandAPI) =>
|
|
17
|
+
async (recipientAddress: string, amount: BigNumber): Promise<boolean> => {
|
|
18
|
+
const recipientAccount = await algorandAPI.getAccount(recipientAddress);
|
|
19
|
+
return recipientAccount.balance.isZero()
|
|
20
|
+
? amount.gte(ALGORAND_MIN_ACCOUNT_BALANCE)
|
|
21
|
+
: true;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const computeAlgoMaxSpendable = ({
|
|
25
|
+
accountBalance,
|
|
26
|
+
nbAccountAssets,
|
|
27
|
+
mode,
|
|
28
|
+
}: {
|
|
29
|
+
accountBalance: BigNumber;
|
|
30
|
+
nbAccountAssets: number;
|
|
31
|
+
mode: AlgorandOperationMode;
|
|
32
|
+
}): BigNumber => {
|
|
33
|
+
const minBalance = computeMinimumAlgoBalance(mode, nbAccountAssets);
|
|
34
|
+
const maxSpendable = accountBalance.minus(minBalance);
|
|
35
|
+
return maxSpendable.gte(0) ? maxSpendable : new BigNumber(0);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const computeMinimumAlgoBalance = (
|
|
39
|
+
mode: AlgorandOperationMode,
|
|
40
|
+
nbAccountAssets: number
|
|
41
|
+
): BigNumber => {
|
|
42
|
+
const base = 100000; // 0.1 algo = 100000 malgo
|
|
43
|
+
const currentAssets = nbAccountAssets;
|
|
44
|
+
const newAsset = mode === "optIn" ? 1 : 0;
|
|
45
|
+
return new BigNumber(base * (1 + currentAssets + newAsset));
|
|
46
|
+
};
|
package/src/mock.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { genAddress, genHex } from "@ledgerhq/coin-framework/mocks/helpers";
|
|
2
|
+
import { Account, Operation, OperationType } from "@ledgerhq/types-live";
|
|
3
|
+
import { BigNumber } from "bignumber.js";
|
|
4
|
+
import Prando from "prando";
|
|
5
|
+
import type { AlgorandAccount } from "./types";
|
|
6
|
+
|
|
7
|
+
function setAlgorandResources(account: Account): Account {
|
|
8
|
+
/** format algorandResources given the new delegations */
|
|
9
|
+
(account as AlgorandAccount).algorandResources = {
|
|
10
|
+
rewards: account.balance.multipliedBy(0.01),
|
|
11
|
+
nbAssets: account.subAccounts?.length ?? 0,
|
|
12
|
+
};
|
|
13
|
+
return account;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function genBaseOperation(
|
|
17
|
+
account: Account,
|
|
18
|
+
rng: Prando,
|
|
19
|
+
type: OperationType,
|
|
20
|
+
index: number
|
|
21
|
+
): Operation {
|
|
22
|
+
const { operations: ops } = account;
|
|
23
|
+
const address = genAddress(account.currency, rng);
|
|
24
|
+
const lastOp = ops[index];
|
|
25
|
+
const date = new Date(
|
|
26
|
+
(lastOp ? lastOp.date.valueOf() : Date.now()) -
|
|
27
|
+
rng.nextInt(0, 100000000 * rng.next() * rng.next())
|
|
28
|
+
);
|
|
29
|
+
const hash = genHex(64, rng);
|
|
30
|
+
|
|
31
|
+
/** generate given operation */
|
|
32
|
+
return {
|
|
33
|
+
id: String(`mock_op_${ops.length}_${type}_${account.id}`),
|
|
34
|
+
hash,
|
|
35
|
+
type,
|
|
36
|
+
value: new BigNumber(0),
|
|
37
|
+
fee: new BigNumber(0),
|
|
38
|
+
senders: [address],
|
|
39
|
+
recipients: [address],
|
|
40
|
+
blockHash: genHex(64, rng),
|
|
41
|
+
blockHeight:
|
|
42
|
+
account.blockHeight -
|
|
43
|
+
// FIXME: always the same, valueOf for arithmetics operation on date in typescript
|
|
44
|
+
Math.floor((Date.now().valueOf() - date.valueOf()) / 900000),
|
|
45
|
+
accountId: account.id,
|
|
46
|
+
date,
|
|
47
|
+
extra: {
|
|
48
|
+
rewards: new BigNumber(0),
|
|
49
|
+
memo: "memo",
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function addOptIn(account: Account, rng: Prando): Account {
|
|
55
|
+
/** select position on the operation stack where we will insert the new claim rewards */
|
|
56
|
+
const opIndex = rng.next(0, 10);
|
|
57
|
+
const opt = genBaseOperation(account, rng, "OPT_IN", opIndex);
|
|
58
|
+
opt.extra = { ...opt.extra, rewards: new BigNumber(0), assetId: "Thether" };
|
|
59
|
+
account.operations.splice(opIndex, 0, opt);
|
|
60
|
+
account.operationsCount++;
|
|
61
|
+
return account;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function addOptOut(account: Account, rng: Prando): Account {
|
|
65
|
+
/** select position on the operation stack where we will insert the new claim rewards */
|
|
66
|
+
const opIndex = rng.next(0, 10);
|
|
67
|
+
const opt = genBaseOperation(account, rng, "OPT_OUT", opIndex);
|
|
68
|
+
opt.extra = { ...opt.extra, rewards: new BigNumber(0), assetId: "Thether" };
|
|
69
|
+
account.operations.splice(opIndex, 0, opt);
|
|
70
|
+
account.operationsCount++;
|
|
71
|
+
return account;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* add in specific algorand operations
|
|
76
|
+
* @memberof algorand/mock
|
|
77
|
+
* @param {Account} account
|
|
78
|
+
* @param {Prando} rng
|
|
79
|
+
*/
|
|
80
|
+
function genAccountEnhanceOperations(account: Account, rng: Prando): Account {
|
|
81
|
+
addOptIn(account, rng);
|
|
82
|
+
addOptOut(account, rng);
|
|
83
|
+
setAlgorandResources(account);
|
|
84
|
+
return account;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Update spendable balance for the account based on delegation data
|
|
89
|
+
* @memberof algorand/mock
|
|
90
|
+
* @param {Account} account
|
|
91
|
+
*/
|
|
92
|
+
function postSyncAccount(account: Account): Account {
|
|
93
|
+
const algorandResources = (account as AlgorandAccount).algorandResources || {
|
|
94
|
+
rewards: undefined,
|
|
95
|
+
};
|
|
96
|
+
const rewards = algorandResources.rewards || new BigNumber(0);
|
|
97
|
+
account.spendableBalance = account.balance.plus(rewards);
|
|
98
|
+
return account;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* post account scan data logic
|
|
103
|
+
* clears account algorand resources if supposed to be empty
|
|
104
|
+
* @memberof algorand/mock
|
|
105
|
+
* @param {AlgorandAccount} account
|
|
106
|
+
*/
|
|
107
|
+
function postScanAccount(
|
|
108
|
+
account: Account,
|
|
109
|
+
{
|
|
110
|
+
isEmpty,
|
|
111
|
+
}: {
|
|
112
|
+
isEmpty: boolean;
|
|
113
|
+
}
|
|
114
|
+
): Account {
|
|
115
|
+
if (isEmpty) {
|
|
116
|
+
(account as AlgorandAccount).algorandResources = {
|
|
117
|
+
rewards: new BigNumber(0),
|
|
118
|
+
nbAssets: account.subAccounts?.length ?? 0,
|
|
119
|
+
};
|
|
120
|
+
account.operations = [];
|
|
121
|
+
account.subAccounts = [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return account;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export default {
|
|
128
|
+
genAccountEnhanceOperations,
|
|
129
|
+
postSyncAccount,
|
|
130
|
+
postScanAccount,
|
|
131
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Account, AccountRaw } from "@ledgerhq/types-live";
|
|
2
|
+
import { BigNumber } from "bignumber.js";
|
|
3
|
+
import type {
|
|
4
|
+
AlgorandAccount,
|
|
5
|
+
AlgorandAccountRaw,
|
|
6
|
+
AlgorandResources,
|
|
7
|
+
AlgorandResourcesRaw,
|
|
8
|
+
} from "./types";
|
|
9
|
+
|
|
10
|
+
function toResourcesRaw(r: AlgorandResources): AlgorandResourcesRaw {
|
|
11
|
+
const { rewards, nbAssets } = r;
|
|
12
|
+
return {
|
|
13
|
+
rewards: rewards.toString(),
|
|
14
|
+
nbAssets,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function fromResourcesRaw(r: AlgorandResourcesRaw): AlgorandResources {
|
|
18
|
+
const { rewards, nbAssets } = r;
|
|
19
|
+
return {
|
|
20
|
+
rewards: new BigNumber(rewards),
|
|
21
|
+
nbAssets,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function assignToAccountRaw(
|
|
26
|
+
account: Account,
|
|
27
|
+
accountRaw: AccountRaw
|
|
28
|
+
): void {
|
|
29
|
+
const algorandAccount = account as AlgorandAccount;
|
|
30
|
+
const algorandAccountRaw = accountRaw as AlgorandAccountRaw;
|
|
31
|
+
if (algorandAccount.algorandResources) {
|
|
32
|
+
algorandAccountRaw.algorandResources = toResourcesRaw(
|
|
33
|
+
algorandAccount.algorandResources
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function assignFromAccountRaw(
|
|
39
|
+
accountRaw: AccountRaw,
|
|
40
|
+
account: Account
|
|
41
|
+
): void {
|
|
42
|
+
const algorandResourcesRaw = (accountRaw as AlgorandAccountRaw)
|
|
43
|
+
.algorandResources;
|
|
44
|
+
const algorandAccount = account as AlgorandAccount;
|
|
45
|
+
if (algorandResourcesRaw) {
|
|
46
|
+
algorandAccount.algorandResources = fromResourcesRaw(algorandResourcesRaw);
|
|
47
|
+
}
|
|
48
|
+
}
|