diff --git a/package.json b/package.json
index a162844e94..59b473ff19 100644
--- a/package.json
+++ b/package.json
@@ -49,6 +49,7 @@
"@cosmjs/proto-signing": "^0.26.5",
"@cosmjs/stargate": "^0.26.5",
"@crypto-com/chain-jslib": "0.0.19",
+ "@elrondnetwork/erdjs": "^9.2.4",
"@ethereumjs/common": "^2.6.2",
"@ethereumjs/tx": "^3.5.0",
"@ledgerhq/compressjs": "1.3.2",
@@ -177,4 +178,4 @@
"typescript": "^4.5.5",
"typescript-eslint-parser": "^22.0.0"
}
-}
+}
\ No newline at end of file
diff --git a/src/data/icons/svg/MEX.svg b/src/data/icons/svg/MEX.svg
new file mode 100644
index 0000000000..b679331ea5
--- /dev/null
+++ b/src/data/icons/svg/MEX.svg
@@ -0,0 +1,6 @@
+
+
+
diff --git a/src/data/icons/svg/RIDE.svg b/src/data/icons/svg/RIDE.svg
new file mode 100644
index 0000000000..ae32393597
--- /dev/null
+++ b/src/data/icons/svg/RIDE.svg
@@ -0,0 +1,8 @@
+
+
+
diff --git a/src/env.ts b/src/env.ts
index 5ed20036d1..6fa09736aa 100644
--- a/src/env.ts
+++ b/src/env.ts
@@ -95,6 +95,11 @@ const envDefinitions = {
def: "https://elrond.coin.ledger.com",
desc: "Elrond API url",
},
+ ELROND_DELEGATION_API_ENDPOINT: {
+ parser: stringParser,
+ def: "https://delegation-api.elrond.com",
+ desc: "Elrond DELEGATION API url",
+ },
API_STELLAR_HORIZON: {
parser: stringParser,
def: "https://stellar.coin.ledger.com",
diff --git a/src/families/elrond/api/apiCalls.ts b/src/families/elrond/api/apiCalls.ts
index 69599b40ae..ee0a33e751 100644
--- a/src/families/elrond/api/apiCalls.ts
+++ b/src/families/elrond/api/apiCalls.ts
@@ -1,14 +1,29 @@
+import BigNumber from "bignumber.js";
import network from "../../../network";
+import { Operation } from "../../../types";
+import { BinaryUtils } from "../utils/binary.utils";
import {
HASH_TRANSACTION,
METACHAIN_SHARD,
- TRANSACTIONS_SIZE,
+ MAX_PAGINATION_SIZE,
+ GAS,
} from "../constants";
+import {
+ ElrondDelegation,
+ ElrondProtocolTransaction,
+ ElrondTransferOptions,
+ ESDTToken,
+ NetworkInfo,
+ Transaction,
+} from "../types";
+import { decodeTransaction } from "./sdk";
export default class ElrondApi {
private API_URL: string;
+ private DELEGATION_API_URL: string;
- constructor(API_URL: string) {
+ constructor(API_URL: string, DELEGATION_API_URL: string) {
this.API_URL = API_URL;
+ this.DELEGATION_API_URL = DELEGATION_API_URL;
}
async getAccountDetails(addr: string) {
@@ -18,6 +33,7 @@ export default class ElrondApi {
method: "GET",
url: `${this.API_URL}/accounts/${addr}`,
});
+
return {
balance,
nonce,
@@ -42,7 +58,7 @@ export default class ElrondApi {
return data;
}
- async getNetworkConfig() {
+ async getNetworkConfig(): Promise {
const {
data: {
data: {
@@ -52,6 +68,7 @@ export default class ElrondApi {
erd_min_gas_limit: gasLimit,
erd_min_gas_price: gasPrice,
erd_gas_per_data_byte: gasPerByte,
+ erd_gas_price_modifier: gasPriceModifier,
},
},
},
@@ -59,23 +76,76 @@ export default class ElrondApi {
method: "GET",
url: `${this.API_URL}/network/config`,
});
+
return {
- chainId,
+ chainID: chainId,
denomination,
gasLimit,
gasPrice,
gasPerByte,
+ gasPriceModifier,
};
}
- async submit({ operation, signature }) {
- const { chainId, gasLimit, gasPrice } = await this.getNetworkConfig();
+ async submit(operation: Operation, signature: string): Promise {
+ const networkConfig: NetworkInfo = await this.getNetworkConfig();
+ const { chainID, gasPrice } = networkConfig;
+ let gasLimit = networkConfig.gasLimit;
+
const {
senders: [sender],
recipients: [receiver],
- value,
transactionSequenceNumber: nonce,
+ extra: { data },
} = operation;
+ let { value } = operation;
+
+ if (data) {
+ const dataDecoded = BinaryUtils.base64Decode(data);
+
+ const funcName: string = dataDecoded.split("@")[0];
+ switch (funcName) {
+ case "ESDTTransfer":
+ value = new BigNumber(0);
+ gasLimit = GAS.ESDT_TRANSFER;
+ break;
+ case "delegate":
+ gasLimit = GAS.DELEGATE;
+ break;
+ case "claimRewards":
+ value = new BigNumber(0);
+ gasLimit = GAS.CLAIM;
+ break;
+ case "withdraw":
+ value = new BigNumber(0);
+ gasLimit = GAS.DELEGATE;
+ break;
+ case "reDelegateRewards":
+ value = new BigNumber(0);
+ gasLimit = GAS.DELEGATE;
+ break;
+ case "unDelegate":
+ value = new BigNumber(0);
+ gasLimit = GAS.DELEGATE;
+ break;
+ default:
+ throw new Error(`Invalid function name ${funcName}`);
+ }
+ }
+
+ const transaction: ElrondProtocolTransaction = {
+ nonce: nonce ?? 0,
+ value: value.toString(),
+ receiver,
+ sender,
+ gasPrice,
+ gasLimit,
+ chainID,
+ signature,
+ data,
+ ...HASH_TRANSACTION,
+ };
+
const {
data: {
data: { txHash: hash },
@@ -83,46 +153,112 @@ export default class ElrondApi {
} = await network({
method: "POST",
url: `${this.API_URL}/transaction/send`,
- data: {
- nonce,
- value,
- receiver,
- sender,
- gasPrice,
- gasLimit,
- chainID: chainId,
- signature,
- ...HASH_TRANSACTION,
- },
+ data: transaction,
});
- return {
- hash,
- };
+
+ return hash;
}
- async getHistory(addr: string, startAt: number) {
+ async getHistory(addr: string, startAt: number): Promise {
const { data: transactionsCount } = await network({
method: "GET",
- url: `${this.API_URL}/transactions/count?condition=should&sender=${addr}&receiver=${addr}&after=${startAt}`,
+ url: `${this.API_URL}/accounts/${addr}/transactions/count?after=${startAt}`,
});
- let allTransactions: any[] = [];
+ let allTransactions: Transaction[] = [];
let from = 0;
+ let before = Math.floor(Date.now() / 1000);
while (from <= transactionsCount) {
- const { data: transactions } = await network({
+ let { data: transactions } = await network({
method: "GET",
- url: `${this.API_URL}/transactions?condition=should&sender=${addr}&receiver=${addr}&after=${startAt}&from=${from}&size=${TRANSACTIONS_SIZE}`,
+ url: `${this.API_URL}/accounts/${addr}/transactions?after=${startAt}&before=${before}&size=${MAX_PAGINATION_SIZE}`,
});
+ transactions = transactions.map((transaction) =>
+ decodeTransaction(transaction)
+ );
+
allTransactions = [...allTransactions, ...transactions];
- from = from + TRANSACTIONS_SIZE;
+ from = from + MAX_PAGINATION_SIZE;
+ before = transactions.slice(-1).timestamp;
}
return allTransactions;
}
- async getBlockchainBlockHeight() {
+ async getAccountDelegations(addr: string): Promise {
+ const { data: delegations } = await network({
+ method: "GET",
+ url: `${this.DELEGATION_API_URL}/accounts/${addr}/delegations`,
+ });
+
+ return delegations;
+ }
+
+ async getESDTTransactionsForAddress(
+ addr: string,
+ token: string
+ ): Promise {
+ const { data: tokenTransactionsCount } = await network({
+ method: "GET",
+ url: `${this.API_URL}/accounts/${addr}/transactions/count?token=${token}`,
+ });
+
+ let allTokenTransactions: Transaction[] = [];
+ let from = 0;
+ let before = Math.floor(Date.now() / 1000);
+ while (from <= tokenTransactionsCount) {
+ const { data: tokenTransactions } = await network({
+ method: "GET",
+ url: `${this.API_URL}/accounts/${addr}/transactions?token=${token}&before=${before}&size=${MAX_PAGINATION_SIZE}`,
+ });
+
+ allTokenTransactions = [...allTokenTransactions, ...tokenTransactions];
+
+ from = from + MAX_PAGINATION_SIZE;
+ before = tokenTransactions.slice(-1).timestamp;
+ }
+
+ for (const esdtTransaction of allTokenTransactions) {
+ esdtTransaction.transfer = ElrondTransferOptions.esdt;
+ }
+
+ return allTokenTransactions;
+ }
+
+ async getESDTTokensForAddress(addr: string): Promise {
+ const { data: tokensCount } = await network({
+ method: "GET",
+ url: `${this.API_URL}/accounts/${addr}/tokens/count`,
+ });
+
+ let allTokens: ESDTToken[] = [];
+ let from = 0;
+ while (from <= tokensCount) {
+ const { data: tokens } = await network({
+ method: "GET",
+ url: `${this.API_URL}/accounts/${addr}/tokens?from=${from}&size=${MAX_PAGINATION_SIZE}`,
+ });
+
+ allTokens = [...allTokens, ...tokens];
+
+ from = from + MAX_PAGINATION_SIZE;
+ }
+
+ return allTokens;
+ }
+
+ async getESDTTokensCountForAddress(addr: string): Promise {
+ const { data: tokensCount } = await network({
+ method: "GET",
+ url: `${this.API_URL}/accounts/${addr}/tokens/count`,
+ });
+
+ return tokensCount;
+ }
+
+ async getBlockchainBlockHeight(): Promise {
const {
data: [{ round: blockHeight }],
} = await network({
diff --git a/src/families/elrond/api/index.ts b/src/families/elrond/api/index.ts
index 65398ce1f9..36a61c5bf2 100644
--- a/src/families/elrond/api/index.ts
+++ b/src/families/elrond/api/index.ts
@@ -5,4 +5,8 @@ export {
getOperations,
getFees,
broadcastTransaction,
+ getAccountESDTTokens,
+ getAccountDelegations,
+ getAccountESDTOperations,
+ hasESDTTokens,
} from "./sdk";
diff --git a/src/families/elrond/api/sdk.ts b/src/families/elrond/api/sdk.ts
index 262878ce3a..655b5f49c7 100644
--- a/src/families/elrond/api/sdk.ts
+++ b/src/families/elrond/api/sdk.ts
@@ -1,11 +1,28 @@
import { BigNumber } from "bignumber.js";
import ElrondApi from "./apiCalls";
-import type { Transaction } from "../types";
+import {
+ ElrondDelegation,
+ ElrondTransferOptions,
+ ESDTToken,
+ Transaction,
+} from "../types";
import type { Operation, OperationType } from "../../../types";
import { getEnv } from "../../../env";
import { encodeOperationId } from "../../../operation";
-import { getTransactionParams } from "../cache";
-const api = new ElrondApi(getEnv("ELROND_API_ENDPOINT"));
+import {
+ Address,
+ GasLimit,
+ NetworkConfig,
+ ProxyProvider,
+ Transaction as ElrondSdkTransaction,
+ TransactionPayload,
+} from "@elrondnetwork/erdjs/out";
+const api = new ElrondApi(
+ getEnv("ELROND_API_ENDPOINT"),
+ getEnv("ELROND_DELEGATION_API_ENDPOINT")
+);
+
+const proxy = new ProxyProvider(getEnv("ELROND_API_ENDPOINT"));
/**
* Get account balances and nonce
@@ -19,14 +36,18 @@ export const getAccount = async (addr: string) => {
nonce,
};
};
+
export const getValidators = async () => {
const validators = await api.getValidators();
return {
validators,
};
};
-export const getNetworkConfig = async () => {
- return await api.getNetworkConfig();
+
+export const getNetworkConfig = async (): Promise => {
+ await NetworkConfig.getDefault().sync(proxy);
+
+ return NetworkConfig.getDefault();
};
/**
@@ -43,16 +64,58 @@ function getOperationType(
transaction: Transaction,
addr: string
): OperationType {
+ if (transaction.mode !== "send") {
+ switch (transaction.mode) {
+ case "delegate":
+ return "DELEGATE";
+ case "unDelegate":
+ return "UNDELEGATE";
+ case "withdraw":
+ return "WITHDRAW_UNBONDED";
+ case "claimRewards":
+ return "REWARD";
+ case "reDelegateRewards":
+ return "DELEGATE";
+ }
+ }
return isSender(transaction, addr) ? "OUT" : "IN";
}
/**
* Map transaction to a correct Operation Value (affecting account balance)
*/
-function getOperationValue(transaction: Transaction, addr: string): BigNumber {
- return isSender(transaction, addr)
- ? new BigNumber(transaction.value ?? 0).plus(transaction.fee ?? 0)
- : new BigNumber(transaction.value ?? 0);
+function getOperationValue(
+ transaction: Transaction,
+ addr: string,
+ tokenIdentifier?: string
+): BigNumber {
+ if (transaction.transfer === ElrondTransferOptions.esdt) {
+ if (transaction.action) {
+ let token1, token2;
+ switch (transaction.action.name) {
+ case "transfer":
+ return new BigNumber(
+ transaction.action.arguments.transfers[0].value ?? 0
+ );
+ case "swap":
+ token1 = transaction.action.arguments.transfers[0];
+ token2 = transaction.action.arguments.transfers[1];
+ if (token1.token === tokenIdentifier) {
+ return new BigNumber(token1.value);
+ } else {
+ return new BigNumber(token2.value);
+ }
+ default:
+ return new BigNumber(transaction.tokenValue ?? 0);
+ }
+ }
+ }
+
+ if (!isSender(transaction, addr)) {
+ return new BigNumber(transaction.value ?? 0);
+ }
+
+ return new BigNumber(transaction.value ?? 0).plus(transaction.fee ?? 0);
}
/**
@@ -61,14 +124,15 @@ function getOperationValue(transaction: Transaction, addr: string): BigNumber {
function transactionToOperation(
accountId: string,
addr: string,
- transaction: Transaction
+ transaction: Transaction,
+ tokenIdentifier?: string
): Operation {
const type = getOperationType(transaction, addr);
return {
id: encodeOperationId(accountId, transaction.txHash ?? "", type),
accountId,
fee: new BigNumber(transaction.fee || 0),
- value: getOperationValue(transaction, addr),
+ value: getOperationValue(transaction, addr, tokenIdentifier),
type,
hash: transaction.txHash ?? "",
blockHash: transaction.miniBlockHash,
@@ -102,25 +166,80 @@ export const getOperations = async (
);
};
+export const getAccountESDTTokens = async (
+ address: string
+): Promise => {
+ return await api.getESDTTokensForAddress(address);
+};
+
+export const getAccountDelegations = async (
+ address: string
+): Promise => {
+ return await api.getAccountDelegations(address);
+};
+
+export const hasESDTTokens = async (address: string): Promise => {
+ const tokensCount = await api.getESDTTokensCountForAddress(address);
+ return tokensCount > 0;
+};
+
+export const getAccountESDTOperations = async (
+ accountId: string,
+ address: string,
+ tokenIdentifier: string
+): Promise => {
+ const accountESDTTransactions = await api.getESDTTransactionsForAddress(
+ address,
+ tokenIdentifier
+ );
+
+ return accountESDTTransactions.map((transaction) =>
+ transactionToOperation(accountId, address, transaction, tokenIdentifier)
+ );
+};
+
/**
* Obtain fees from blockchain
*/
-export const getFees = async (unsigned): Promise => {
- const { data } = unsigned;
- const { gasLimit, gasPerByte, gasPrice } = await getTransactionParams();
+export const getFees = async (t: Transaction): Promise => {
+ await NetworkConfig.getDefault().sync(proxy);
- if (!data) {
- return new BigNumber(gasLimit * gasPrice);
- }
+ const transaction = new ElrondSdkTransaction({
+ data: new TransactionPayload(t.data),
+ receiver: new Address(t.receiver),
+ chainID: NetworkConfig.getDefault().ChainID,
+ gasLimit: new GasLimit(t.gasLimit),
+ });
+
+ const feesStr = transaction.computeFee(NetworkConfig.getDefault()).toFixed();
- return new BigNumber((gasLimit + gasPerByte * data.length) * gasPrice);
+ return new BigNumber(feesStr);
};
/**
* Broadcast blob to blockchain
*/
-export const broadcastTransaction = async (blob: any) => {
- const { hash } = await api.submit(blob);
- // Transaction hash is likely to be returned
- return hash;
+export const broadcastTransaction = async (
+ operation: Operation,
+ signature: string
+): Promise => {
+ return await api.submit(operation, signature);
+};
+
+export const decodeTransaction = (transaction: any): Transaction => {
+ if (!transaction.action) {
+ return transaction;
+ }
+
+ if (!transaction.action.category) {
+ return transaction;
+ }
+
+ if (transaction.action.category !== "stake") {
+ return transaction;
+ }
+
+ transaction.mode = transaction.action.name;
+
+ return transaction;
};
diff --git a/src/families/elrond/cli-transaction.ts b/src/families/elrond/cli-transaction.ts
index 9805948cd2..fac776eba7 100644
--- a/src/families/elrond/cli-transaction.ts
+++ b/src/families/elrond/cli-transaction.ts
@@ -5,7 +5,7 @@ const options = [
{
name: "mode",
type: String,
- desc: "mode of transaction: send",
+ desc: "mode of transaction: send, delegate, unDelegate, claimRewards",
},
];
@@ -25,7 +25,10 @@ function inferTransactions(
transaction.family = "elrond";
- return transaction;
+ return {
+ ...transaction,
+ mode: _opts.mode || "send",
+ };
});
}
diff --git a/src/families/elrond/constants.ts b/src/families/elrond/constants.ts
index eda5414658..ac3b7ad023 100644
--- a/src/families/elrond/constants.ts
+++ b/src/families/elrond/constants.ts
@@ -1,6 +1,20 @@
+import BigNumber from "bignumber.js";
+
export const HASH_TRANSACTION = {
version: 2,
options: 1,
};
export const METACHAIN_SHARD = 4294967295;
-export const TRANSACTIONS_SIZE = 10000;
+export const MAX_PAGINATION_SIZE = 10000;
+export const GAS = {
+ ESDT_TRANSFER: 500000,
+ DELEGATE: 12000000,
+ CLAIM: 6000000,
+};
+export const CHAIN_ID = "1";
+export const MIN_DELEGATION_AMOUNT: BigNumber = new BigNumber(
+ 1000000000000000000
+);
+export const MIN_DELEGATION_AMOUNT_DENOMINATED: BigNumber = new BigNumber(1);
+export const FEES_BALANCE: BigNumber = new BigNumber("5000000000000000"); // 0.005 EGLD for future transactions
+export const DECIMALS_LIMIT = 18;
diff --git a/src/families/elrond/deviceTransactionConfig.ts b/src/families/elrond/deviceTransactionConfig.ts
index 0dc4070402..e4c6587974 100644
--- a/src/families/elrond/deviceTransactionConfig.ts
+++ b/src/families/elrond/deviceTransactionConfig.ts
@@ -1,9 +1,12 @@
import type { TransactionStatus } from "../../types";
import type { DeviceTransactionField } from "../../transaction";
+import { Transaction } from "./types";
function getDeviceTransactionConfig({
+ transaction: { mode, recipient },
status: { amount, estimatedFees },
}: {
+ transaction: Transaction;
status: TransactionStatus;
}): Array {
const fields: Array = [];
@@ -21,6 +24,15 @@ function getDeviceTransactionConfig({
label: "Fees",
});
}
+
+ const isDelegationOperation = mode !== "send";
+ if (isDelegationOperation) {
+ fields.push({
+ type: "address",
+ label: "Validator",
+ address: recipient,
+ });
+ }
return fields;
}
diff --git a/src/families/elrond/encode.ts b/src/families/elrond/encode.ts
new file mode 100644
index 0000000000..e1caf0064f
--- /dev/null
+++ b/src/families/elrond/encode.ts
@@ -0,0 +1,47 @@
+import { SubAccount } from "../../types";
+import type { Transaction } from "./types";
+
+export class ElrondEncodeTransaction {
+ static ESDTTransfer(t: Transaction, ta: SubAccount): string {
+ const tokenIdentifierHex = ta.id.split("/")[2];
+ let amountHex = t.useAllAmount
+ ? ta.balance.toString(16)
+ : t.amount.toString(16);
+
+ //hex amount length must be even so protocol would treat it as an ESDT transfer
+ if (amountHex.length % 2 !== 0) {
+ amountHex = "0" + amountHex;
+ }
+
+ return Buffer.from(
+ `ESDTTransfer@${tokenIdentifierHex}@${amountHex}`
+ ).toString("base64");
+ }
+
+ static delegate(): string {
+ return Buffer.from(`delegate`).toString("base64");
+ }
+
+ static claimRewards(): string {
+ return Buffer.from(`claimRewards`).toString("base64");
+ }
+
+ static withdraw(): string {
+ return Buffer.from(`withdraw`).toString("base64");
+ }
+
+ static reDelegateRewards(): string {
+ return Buffer.from(`reDelegateRewards`).toString("base64");
+ }
+
+ static unDelegate(t: Transaction): string {
+ let amountHex = t.amount.toString(16);
+
+ //hex amount length must be even
+ if (amountHex.length % 2 !== 0) {
+ amountHex = "0" + amountHex;
+ }
+
+ return Buffer.from(`unDelegate@${amountHex}`).toString("base64");
+ }
+}
diff --git a/src/families/elrond/hw-app-elrond/index.ts b/src/families/elrond/hw-app-elrond/index.ts
index 1fed843297..29b043661b 100644
--- a/src/families/elrond/hw-app-elrond/index.ts
+++ b/src/families/elrond/hw-app-elrond/index.ts
@@ -7,6 +7,7 @@ const INS = {
GET_VERSION: 0x02,
GET_ADDRESS: 0x03,
SET_ADDRESS: 0x05,
+ PROVIDE_ESDT_INFO: 0x08,
};
const SIGN_RAW_TX_INS = 0x04;
const SIGN_HASH_TX_INS = 0x07;
@@ -27,6 +28,7 @@ export default class Elrond {
"signTransaction",
"signMessage",
"getAppConfiguration",
+ "provideESDTInfo",
],
scrambleKey
);
@@ -180,4 +182,60 @@ export default class Elrond {
const signature = response.slice(1, response.length - 2).toString("hex");
return signature;
}
+
+ serializeESDTInfo(
+ ticker: string,
+ id: string,
+ decimals: number,
+ chainId: string,
+ signature: string
+ ): Buffer {
+ const tickerLengthBuffer = Buffer.from([ticker.length]);
+ const tickerBuffer = Buffer.from(ticker);
+ const idLengthBuffer = Buffer.from([id.length]);
+ const idBuffer = Buffer.from(id);
+ const decimalsBuffer = Buffer.from([decimals]);
+ const chainIdLengthBuffer = Buffer.from([chainId.length]);
+ const chainIdBuffer = Buffer.from(chainId);
+ const signatureBuffer = Buffer.from(signature, "hex");
+ const infoBuffer = [
+ tickerLengthBuffer,
+ tickerBuffer,
+ idLengthBuffer,
+ idBuffer,
+ decimalsBuffer,
+ chainIdLengthBuffer,
+ chainIdBuffer,
+ signatureBuffer,
+ ];
+ return Buffer.concat(infoBuffer);
+ }
+
+ async provideESDTInfo(
+ ticker?: string,
+ id?: string,
+ decimals?: number,
+ chainId?: string,
+ signature?: string
+ ): Promise {
+ if (!ticker || !id || !decimals || !chainId || !signature) {
+ throw new Error("Invalid ESDT token credentials!");
+ }
+
+ const data = this.serializeESDTInfo(
+ ticker,
+ id,
+ decimals,
+ chainId,
+ signature
+ );
+
+ return await this.transport.send(
+ CLA,
+ INS.PROVIDE_ESDT_INFO,
+ 0x00,
+ 0x00,
+ data
+ );
+ }
}
diff --git a/src/families/elrond/js-broadcast.ts b/src/families/elrond/js-broadcast.ts
index 3cf68bb715..5464c9303b 100644
--- a/src/families/elrond/js-broadcast.ts
+++ b/src/families/elrond/js-broadcast.ts
@@ -11,10 +11,7 @@ const broadcast = async ({
}: {
signedOperation: SignedOperation;
}): Promise => {
- const hash = await broadcastTransaction({
- operation,
- signature,
- });
+ const hash = await broadcastTransaction(operation, signature);
return patchOperationWithHash(operation, hash);
};
diff --git a/src/families/elrond/js-buildSubAccounts.ts b/src/families/elrond/js-buildSubAccounts.ts
new file mode 100644
index 0000000000..5fb120e16e
--- /dev/null
+++ b/src/families/elrond/js-buildSubAccounts.ts
@@ -0,0 +1,126 @@
+import {
+ CryptoCurrency,
+ findTokenById,
+ listTokensForCryptoCurrency,
+ TokenCurrency,
+} from "@ledgerhq/cryptoassets";
+import BigNumber from "bignumber.js";
+import { emptyHistoryCache } from "../../account";
+import { Account, SyncConfig, TokenAccount } from "../../types";
+import { getAccountESDTOperations, getAccountESDTTokens } from "./api";
+
+async function buildElrondESDTTokenAccount({
+ parentAccountId,
+ accountAddress,
+ token,
+ balance,
+}: {
+ parentAccountId: string;
+ accountAddress: string;
+ token: TokenCurrency;
+ balance: BigNumber;
+}) {
+ const extractedId = token.id;
+ const id = parentAccountId + "+" + extractedId;
+ const tokenIdentifierHex = token.id.split("/")[2];
+ const tokenIdentifier = Buffer.from(tokenIdentifierHex, "hex").toString();
+
+ const operations = await getAccountESDTOperations(
+ parentAccountId,
+ accountAddress,
+ tokenIdentifier
+ );
+
+ const tokenAccount: TokenAccount = {
+ type: "TokenAccount",
+ id,
+ parentId: parentAccountId,
+ starred: false,
+ token,
+ operationsCount: operations.length,
+ operations,
+ pendingOperations: [],
+ balance,
+ spendableBalance: balance,
+ swapHistory: [],
+ creationDate:
+ operations.length > 0
+ ? operations[operations.length - 1].date
+ : new Date(),
+ balanceHistoryCache: emptyHistoryCache, // calculated in the jsHelpers
+ };
+ return tokenAccount;
+}
+
+async function elrondBuildESDTTokenAccounts({
+ currency,
+ accountId,
+ accountAddress,
+ existingAccount,
+ syncConfig,
+}: {
+ currency: CryptoCurrency;
+ accountId: string;
+ accountAddress: string;
+ existingAccount: Account | null | undefined;
+ syncConfig: SyncConfig;
+}): Promise {
+ const { blacklistedTokenIds = [] } = syncConfig;
+ if (listTokensForCryptoCurrency(currency).length === 0) {
+ return undefined;
+ }
+
+ const tokenAccounts: TokenAccount[] = [];
+
+ const existingAccountByTicker = {}; // used for fast lookup
+
+ const existingAccountTickers: string[] = []; // used to keep track of ordering
+
+ if (existingAccount && existingAccount.subAccounts) {
+ for (const existingSubAccount of existingAccount.subAccounts) {
+ if (existingSubAccount.type === "TokenAccount") {
+ const { ticker, id } = existingSubAccount.token;
+
+ if (!blacklistedTokenIds.includes(id)) {
+ existingAccountTickers.push(ticker);
+ existingAccountByTicker[ticker] = existingSubAccount;
+ }
+ }
+ }
+ }
+
+ const accountESDTs = await getAccountESDTTokens(accountAddress);
+ for (const esdt of accountESDTs) {
+ const esdtIdentifierHex = Buffer.from(esdt.identifier).toString("hex");
+ const token = findTokenById(`elrond/esdt/${esdtIdentifierHex}`);
+
+ if (token && !blacklistedTokenIds.includes(token.id)) {
+ const tokenAccount = await buildElrondESDTTokenAccount({
+ parentAccountId: accountId,
+ accountAddress,
+ token,
+ balance: new BigNumber(esdt.balance),
+ });
+
+ if (tokenAccount) {
+ tokenAccounts.push(tokenAccount);
+ existingAccountTickers.push(token.ticker);
+ existingAccountByTicker[token.ticker] = tokenAccount;
+ }
+ }
+ }
+
+ // Preserve order of tokenAccounts from the existing token accounts
+ tokenAccounts.sort((a, b) => {
+ const i = existingAccountTickers.indexOf(a.token.ticker);
+ const j = existingAccountTickers.indexOf(b.token.ticker);
+ if (i === j) return 0;
+ if (i < 0) return 1;
+ if (j < 0) return -1;
+ return i - j;
+ });
+
+ return tokenAccounts;
+}
+
+export default elrondBuildESDTTokenAccounts;
diff --git a/src/families/elrond/js-buildTransaction.ts b/src/families/elrond/js-buildTransaction.ts
index 24f730176b..dc95b273f7 100644
--- a/src/families/elrond/js-buildTransaction.ts
+++ b/src/families/elrond/js-buildTransaction.ts
@@ -1,32 +1,106 @@
-import type { Transaction } from "./types";
-import type { Account } from "../../types";
+import type { ElrondProtocolTransaction, Transaction } from "./types";
+import type { Account, SubAccount } from "../../types";
import { getNonce } from "./logic";
import { getNetworkConfig } from "./api";
-import { HASH_TRANSACTION } from "./constants";
+import {
+ GAS,
+ HASH_TRANSACTION,
+ MIN_DELEGATION_AMOUNT,
+ MIN_DELEGATION_AMOUNT_DENOMINATED,
+} from "./constants";
import BigNumber from "bignumber.js";
-
+import { ElrondEncodeTransaction } from "./encode";
+import { NetworkConfig } from "@elrondnetwork/erdjs/out";
/**
*
* @param {Account} a
* @param {Transaction} t
*/
-export const buildTransaction = async (a: Account, t: Transaction) => {
+export const buildTransaction = async (
+ a: Account,
+ ta: SubAccount | null | undefined,
+ t: Transaction
+): Promise => {
const address = a.freshAddress;
const nonce = getNonce(a);
- const { gasPrice, gasLimit, chainId } = await getNetworkConfig();
+ const networkConfig: NetworkConfig = await getNetworkConfig();
+ const chainID = networkConfig.ChainID.valueOf();
+ const gasPrice = networkConfig.MinGasPrice.valueOf();
+ t.gasLimit = networkConfig.MinGasLimit.valueOf();
- const unsigned = {
- nonce,
- value: t.useAllAmount
+ let transactionValue: BigNumber;
+
+ if (ta) {
+ t.data = ElrondEncodeTransaction.ESDTTransfer(t, ta);
+ t.gasLimit = GAS.ESDT_TRANSFER; //gasLimit for and ESDT transfer
+
+ transactionValue = new BigNumber(0); //amount of EGLD to be sent should be 0 in an ESDT transfer
+ } else {
+ transactionValue = t.useAllAmount
? a.balance.minus(t.fees ? t.fees : new BigNumber(0))
- : t.amount,
+ : t.amount;
+
+ switch (t.mode) {
+ case "delegate":
+ if (transactionValue.lt(MIN_DELEGATION_AMOUNT)) {
+ throw new Error(
+ `Delegation amount should be minimum ${MIN_DELEGATION_AMOUNT_DENOMINATED} EGLD`
+ );
+ }
+
+ t.gasLimit = GAS.DELEGATE;
+ t.data = ElrondEncodeTransaction.delegate();
+
+ break;
+ case "claimRewards":
+ t.gasLimit = GAS.CLAIM;
+ t.data = ElrondEncodeTransaction.claimRewards();
+
+ transactionValue = new BigNumber(0); //amount of EGLD to be sent should be 0 in a claimRewards transaction
+ break;
+ case "withdraw":
+ t.gasLimit = GAS.DELEGATE;
+ t.data = ElrondEncodeTransaction.withdraw();
+
+ transactionValue = new BigNumber(0); //amount of EGLD to be sent should be 0 in a withdraw transaction
+ break;
+ case "reDelegateRewards":
+ t.gasLimit = GAS.DELEGATE;
+ t.data = ElrondEncodeTransaction.reDelegateRewards();
+
+ transactionValue = new BigNumber(0); //amount of EGLD to be sent should be 0 in a reDelegateRewards transaction
+ break;
+ case "unDelegate":
+ if (transactionValue.lt(MIN_DELEGATION_AMOUNT)) {
+ throw new Error(
+ `Undelegated amount should be minimum ${MIN_DELEGATION_AMOUNT_DENOMINATED} EGLD`
+ );
+ }
+
+ t.gasLimit = GAS.DELEGATE;
+ t.data = ElrondEncodeTransaction.unDelegate(t);
+
+ transactionValue = new BigNumber(0); //amount of EGLD to be sent should be 0 in a unDelegate transaction
+ break;
+ case "send":
+ break;
+ default:
+ throw new Error("Unsupported transaction.mode = " + t.mode);
+ }
+ }
+
+ const unsigned: ElrondProtocolTransaction = {
+ nonce,
+ value: transactionValue.toString(),
receiver: t.recipient,
sender: address,
gasPrice,
- gasLimit,
- chainID: chainId,
+ gasLimit: t.gasLimit,
+ data: t.data,
+ chainID,
...HASH_TRANSACTION,
};
+
// Will likely be a call to Elrond SDK
return JSON.stringify(unsigned);
};
diff --git a/src/families/elrond/js-estimateMaxSpendable.ts b/src/families/elrond/js-estimateMaxSpendable.ts
index 6dcd07764c..7442ce178f 100644
--- a/src/families/elrond/js-estimateMaxSpendable.ts
+++ b/src/families/elrond/js-estimateMaxSpendable.ts
@@ -4,6 +4,8 @@ import { getMainAccount } from "../../account";
import type { Transaction } from "./types";
import { createTransaction } from "./js-transaction";
import getEstimatedFees from "./js-getFeesForTransaction";
+import { GAS } from "./constants";
+import { ElrondEncodeTransaction } from "./encode";
/**
* Returns the maximum possible amount for transaction
@@ -19,22 +21,61 @@ const estimateMaxSpendable = async ({
parentAccount: Account | null | undefined;
transaction: Transaction | null | undefined;
}): Promise => {
- const a = getMainAccount(account, parentAccount);
- const t = {
+ const mainAccount = getMainAccount(account, parentAccount);
+ const tx = {
...createTransaction(),
+ subAccountId: account.type === "Account" ? null : account.id,
...transaction,
- amount: a.spendableBalance,
};
- const fees = await getEstimatedFees({
- a,
- t,
- });
- if (fees.gt(a.spendableBalance)) {
+ const tokenAccount =
+ tx.subAccountId &&
+ mainAccount.subAccounts &&
+ mainAccount.subAccounts.find((ta) => ta.id === tx.subAccountId);
+
+ if (tokenAccount) {
+ return tokenAccount.balance;
+ }
+
+ switch (tx?.mode) {
+ case "reDelegateRewards":
+ tx.gasLimit = GAS.DELEGATE;
+
+ tx.data = ElrondEncodeTransaction.reDelegateRewards();
+ break;
+ case "withdraw":
+ tx.gasLimit = GAS.DELEGATE;
+
+ tx.data = ElrondEncodeTransaction.withdraw();
+ break;
+ case "unDelegate":
+ tx.gasLimit = GAS.DELEGATE;
+
+ tx.data = ElrondEncodeTransaction.unDelegate(tx);
+ break;
+ case "delegate":
+ tx.gasLimit = GAS.DELEGATE;
+
+ tx.data = ElrondEncodeTransaction.delegate();
+ break;
+
+ case "claimRewards":
+ tx.gasLimit = GAS.CLAIM;
+
+ tx.data = ElrondEncodeTransaction.claimRewards();
+ break;
+
+ default:
+ break;
+ }
+
+ const fees = await getEstimatedFees(tx);
+
+ if (fees.gt(mainAccount.balance)) {
return new BigNumber(0);
}
- return a.spendableBalance.minus(fees);
+ return mainAccount.spendableBalance.minus(fees);
};
export default estimateMaxSpendable;
diff --git a/src/families/elrond/js-getFeesForTransaction.ts b/src/families/elrond/js-getFeesForTransaction.ts
index e20bffba08..66e4f0f999 100644
--- a/src/families/elrond/js-getFeesForTransaction.ts
+++ b/src/families/elrond/js-getFeesForTransaction.ts
@@ -1,8 +1,6 @@
import { BigNumber } from "bignumber.js";
-import type { Account } from "../../types";
import type { Transaction } from "./types";
import { getFees } from "./api";
-import { buildTransaction } from "./js-buildTransaction";
/**
* Fetch the transaction fees for a transaction
@@ -10,15 +8,8 @@ import { buildTransaction } from "./js-buildTransaction";
* @param {Account} a
* @param {Transaction} t
*/
-const getEstimatedFees = async ({
- a,
- t,
-}: {
- a: Account;
- t: Transaction;
-}): Promise => {
- const unsigned = await buildTransaction(a, t);
- return await getFees(JSON.parse(unsigned));
+const getEstimatedFees = async (t: Transaction): Promise => {
+ return await getFees(t);
};
export default getEstimatedFees;
diff --git a/src/families/elrond/js-getTransactionStatus.ts b/src/families/elrond/js-getTransactionStatus.ts
index 0dc931bf35..0b0435f829 100644
--- a/src/families/elrond/js-getTransactionStatus.ts
+++ b/src/families/elrond/js-getTransactionStatus.ts
@@ -1,4 +1,3 @@
-import { BigNumber } from "bignumber.js";
import {
NotEnoughBalance,
RecipientRequired,
@@ -9,7 +8,12 @@ import {
} from "@ledgerhq/errors";
import type { Account, TransactionStatus } from "../../types";
import type { Transaction } from "./types";
-import { isValidAddress, isSelfTransaction } from "./logic";
+import {
+ isValidAddress,
+ isSelfTransaction,
+ computeTransactionValue,
+} from "./logic";
+import { DECIMALS_LIMIT } from "./constants";
const getTransactionStatus = async (
a: Account,
@@ -17,7 +21,6 @@ const getTransactionStatus = async (
): Promise => {
const errors: Record = {};
const warnings: Record = {};
- const useAllAmount = !!t.useAllAmount;
if (!t.recipient) {
errors.recipient = new RecipientRequired();
@@ -31,24 +34,36 @@ const getTransactionStatus = async (
errors.fees = new FeeNotLoaded();
}
- const estimatedFees = t.fees || new BigNumber(0);
+ const tokenAccount =
+ (t.subAccountId &&
+ a.subAccounts &&
+ a.subAccounts.find((ta) => ta.id === t.subAccountId)) ||
+ null;
+
+ const { amount, totalSpent, estimatedFees } = await computeTransactionValue(
+ t,
+ a,
+ tokenAccount
+ );
if (estimatedFees.gt(a.balance)) {
errors.amount = new NotEnoughBalance();
}
- const totalSpent = useAllAmount
- ? a.balance
- : new BigNumber(t.amount).plus(estimatedFees);
- const amount = useAllAmount
- ? a.balance.minus(estimatedFees)
- : new BigNumber(t.amount);
-
- if (totalSpent.gt(a.balance)) {
- errors.amount = new NotEnoughBalance();
- }
+ if (tokenAccount) {
+ if (totalSpent.gt(tokenAccount.balance)) {
+ errors.amount = new NotEnoughBalance();
+ }
+ if (!totalSpent.decimalPlaces(DECIMALS_LIMIT).isEqualTo(totalSpent)) {
+ errors.amount = new Error(`Maximum '${DECIMALS_LIMIT}' decimals allowed`);
+ }
+ } else {
+ if (totalSpent.gt(a.balance)) {
+ errors.amount = new NotEnoughBalance();
+ }
- if (amount.div(10).lt(estimatedFees)) {
- warnings.feeTooHigh = new FeeTooHigh();
+ if (amount.div(10).lt(estimatedFees)) {
+ warnings.feeTooHigh = new FeeTooHigh();
+ }
}
return Promise.resolve({
diff --git a/src/families/elrond/js-reconciliation.ts b/src/families/elrond/js-reconciliation.ts
new file mode 100644
index 0000000000..1a0a3012f7
--- /dev/null
+++ b/src/families/elrond/js-reconciliation.ts
@@ -0,0 +1,74 @@
+import type { Account, SubAccount, TokenAccount } from "../../types";
+import { log } from "@ledgerhq/logs";
+
+export function reconciliateSubAccounts(
+ tokenAccounts: TokenAccount[],
+ initialAccount: Account | undefined
+) {
+ let subAccounts;
+
+ if (initialAccount) {
+ const initialSubAccounts: SubAccount[] | undefined =
+ initialAccount.subAccounts;
+ let anySubAccountHaveChanged = false;
+ const stats: string[] = [];
+
+ if (
+ initialSubAccounts &&
+ tokenAccounts.length !== initialSubAccounts.length
+ ) {
+ stats.push("length differ");
+ anySubAccountHaveChanged = true;
+ }
+
+ subAccounts = tokenAccounts.map((ta: TokenAccount) => {
+ const existingTokenAccount = initialSubAccounts?.find(
+ (a) => a.id === ta.id
+ );
+
+ if (existingTokenAccount) {
+ let sameProperties = true;
+
+ if (existingTokenAccount !== ta) {
+ for (const property in existingTokenAccount) {
+ if (existingTokenAccount[property] !== ta[property]) {
+ sameProperties = false;
+ stats.push(`field ${property} changed for ${ta.id}`);
+ break;
+ }
+ }
+ }
+
+ if (sameProperties) {
+ return existingTokenAccount;
+ } else {
+ anySubAccountHaveChanged = true;
+ }
+ } else {
+ anySubAccountHaveChanged = true;
+ stats.push(`new token account ${ta.id}`);
+ }
+
+ return ta;
+ });
+
+ if (!anySubAccountHaveChanged && initialSubAccounts) {
+ log(
+ "elrond",
+ "incremental sync: " +
+ String(initialSubAccounts.length) +
+ " sub accounts have not changed"
+ );
+ subAccounts = initialSubAccounts;
+ } else {
+ log(
+ "elrond",
+ "incremental sync: sub accounts changed: " + stats.join(", ")
+ );
+ }
+ } else {
+ subAccounts = tokenAccounts.map((a: TokenAccount) => a);
+ }
+
+ return subAccounts;
+}
diff --git a/src/families/elrond/js-signOperation.ts b/src/families/elrond/js-signOperation.ts
index a58047d49b..b27f7fef0b 100644
--- a/src/families/elrond/js-signOperation.ts
+++ b/src/families/elrond/js-signOperation.ts
@@ -8,6 +8,8 @@ import { encodeOperationId } from "../../operation";
import Elrond from "./hw-app-elrond";
import { buildTransaction } from "./js-buildTransaction";
import { getNonce } from "./logic";
+import { findTokenById } from "@ledgerhq/cryptoassets";
+import { CHAIN_ID } from "./constants";
const buildOptimisticOperation = (
account: Account,
@@ -15,9 +17,20 @@ const buildOptimisticOperation = (
fee: BigNumber
): Operation => {
const type = "OUT";
- const value = transaction.useAllAmount
+ const tokenAccount =
+ (transaction.subAccountId &&
+ account.subAccounts &&
+ account.subAccounts.find((ta) => ta.id === transaction.subAccountId)) ||
+ null;
+
+ let value = transaction.useAllAmount
? account.balance.minus(fee)
: new BigNumber(transaction.amount);
+
+ if (tokenAccount) {
+ value = transaction.amount;
+ }
+
const operation: Operation = {
id: encodeOperationId(account.id, "", type),
hash: "",
@@ -31,7 +44,9 @@ const buildOptimisticOperation = (
accountId: account.id,
transactionSequenceNumber: getNonce(account),
date: new Date(),
- extra: {},
+ extra: {
+ data: transaction.data,
+ },
};
return operation;
};
@@ -54,11 +69,37 @@ const signOperation = ({
if (!transaction.fees) {
throw new FeeNotLoaded();
}
+ // Collect data for an ESDT transfer
+ const { subAccounts } = account;
+ const { subAccountId } = transaction;
+ const tokenAccount = !subAccountId
+ ? null
+ : subAccounts && subAccounts.find((ta) => ta.id === subAccountId);
const elrond = new Elrond(transport);
await elrond.setAddress(account.freshAddressPath);
- const unsigned = await buildTransaction(account, transaction);
+ if (tokenAccount) {
+ const tokenIdentifier = tokenAccount.id.split("+")[1];
+ const token = findTokenById(`${tokenIdentifier}`);
+
+ if (token?.ticker && token.id && token.ledgerSignature) {
+ const collectionIdentifierHex = token.id.split("/")[2];
+ await elrond.provideESDTInfo(
+ token.ticker,
+ collectionIdentifierHex,
+ token?.units[0].magnitude,
+ CHAIN_ID,
+ token.ledgerSignature
+ );
+ }
+ }
+
+ const unsignedTx: string = await buildTransaction(
+ account,
+ tokenAccount,
+ transaction
+ );
o.next({
type: "device-signature-requested",
@@ -66,7 +107,7 @@ const signOperation = ({
const r = await elrond.signTransaction(
account.freshAddressPath,
- unsigned,
+ unsignedTx,
true
);
diff --git a/src/families/elrond/js-synchronisation.ts b/src/families/elrond/js-synchronisation.ts
index 0189f4e6a4..45d573ae93 100644
--- a/src/families/elrond/js-synchronisation.ts
+++ b/src/families/elrond/js-synchronisation.ts
@@ -1,7 +1,16 @@
+import type { Account, TokenAccount } from "../../types";
import { encodeAccountId } from "../../account";
import type { GetAccountShape } from "../../bridge/jsHelpers";
import { makeSync, makeScanAccounts, mergeOps } from "../../bridge/jsHelpers";
-import { getAccount, getOperations } from "./api";
+import {
+ getAccount,
+ getAccountDelegations,
+ getOperations,
+ hasESDTTokens,
+} from "./api";
+import elrondBuildESDTTokenAccounts from "./js-buildSubAccounts";
+import { reconciliateSubAccounts } from "./js-reconciliation";
+import { FEES_BALANCE } from "./constants";
const getAccountShape: GetAccountShape = async (info) => {
const { address, initialAccount, currency, derivationMode } = info;
@@ -21,15 +30,40 @@ const getAccountShape: GetAccountShape = async (info) => {
// Merge new operations with the previously synced ones
const newOperations = await getOperations(accountId, address, startAt);
const operations = mergeOps(oldOperations, newOperations);
+
+ let subAccounts: TokenAccount[] | undefined = [];
+ const hasTokens = await hasESDTTokens(address);
+ if (hasTokens) {
+ const tokenAccounts = await elrondBuildESDTTokenAccounts({
+ currency,
+ accountId: accountId,
+ accountAddress: address,
+ existingAccount: initialAccount,
+ syncConfig: {
+ paginationConfig: {},
+ },
+ });
+
+ if (tokenAccounts) {
+ subAccounts = reconciliateSubAccounts(tokenAccounts, initialAccount);
+ }
+ }
+
+ const delegations = await getAccountDelegations(address);
+
const shape = {
id: accountId,
balance,
- spendableBalance: balance,
+ spendableBalance: balance.gt(FEES_BALANCE)
+ ? balance.minus(FEES_BALANCE)
+ : balance,
operationsCount: operations.length,
blockHeight,
elrondResources: {
nonce,
+ delegations,
},
+ subAccounts,
};
return { ...shape, operations };
diff --git a/src/families/elrond/js-transaction.ts b/src/families/elrond/js-transaction.ts
index 144a5007b7..1a3b11c704 100644
--- a/src/families/elrond/js-transaction.ts
+++ b/src/families/elrond/js-transaction.ts
@@ -3,6 +3,7 @@ import { BigNumber } from "bignumber.js";
import type { Account } from "../../types";
import type { Transaction } from "./types";
import getEstimatedFees from "./js-getFeesForTransaction";
+import { NetworkConfig } from "@elrondnetwork/erdjs/out";
const sameFees = (a, b) => (!a || !b ? false : a === b);
@@ -19,6 +20,7 @@ export const createTransaction = (): Transaction => {
recipient: "",
useAllAmount: false,
fees: new BigNumber(50000),
+ gasLimit: NetworkConfig.getDefault().MinGasLimit.valueOf(),
};
};
@@ -43,10 +45,7 @@ export const updateTransaction = (
*/
export const prepareTransaction = async (a: Account, t: Transaction) => {
let fees = t.fees;
- fees = await getEstimatedFees({
- a,
- t,
- });
+ fees = await getEstimatedFees(t);
if (!sameFees(t.fees, fees)) {
return { ...t, fees };
diff --git a/src/families/elrond/logic.ts b/src/families/elrond/logic.ts
index 0ff2a107d9..61fd4356ba 100644
--- a/src/families/elrond/logic.ts
+++ b/src/families/elrond/logic.ts
@@ -1,6 +1,9 @@
-import type { Account } from "../../types";
+import type { Account, SubAccount } from "../../types";
import type { Transaction } from "./types";
import * as bech32 from "bech32";
+import BigNumber from "bignumber.js";
+import { buildTransaction } from "./js-buildTransaction";
+import getEstimatedFees from "./js-getFeesForTransaction";
/**
* The human-readable-part of the bech32 addresses.
@@ -66,3 +69,35 @@ export const getNonce = (a: Account): number => {
);
return nonce;
};
+
+export const computeTransactionValue = async (
+ t: Transaction,
+ a: Account,
+ ta: SubAccount | null
+): Promise<{
+ amount: BigNumber;
+ totalSpent: BigNumber;
+ estimatedFees: BigNumber;
+}> => {
+ let amount, totalSpent;
+
+ await buildTransaction(a, ta, t);
+
+ const estimatedFees = await getEstimatedFees(t);
+
+ if (ta) {
+ amount = t.useAllAmount ? ta.balance : t.amount;
+
+ totalSpent = amount;
+ } else {
+ totalSpent = t.useAllAmount
+ ? a.balance
+ : new BigNumber(t.amount).plus(estimatedFees);
+
+ amount = t.useAllAmount
+ ? a.balance.minus(estimatedFees)
+ : new BigNumber(t.amount);
+ }
+
+ return { amount, totalSpent, estimatedFees };
+};
diff --git a/src/families/elrond/serialization.ts b/src/families/elrond/serialization.ts
index 3e89729118..0cca81408c 100644
--- a/src/families/elrond/serialization.ts
+++ b/src/families/elrond/serialization.ts
@@ -1,13 +1,15 @@
import type { ElrondResourcesRaw, ElrondResources } from "./types";
export function toElrondResourcesRaw(r: ElrondResources): ElrondResourcesRaw {
- const { nonce } = r;
+ const { nonce, delegations } = r;
return {
nonce,
+ delegations,
};
}
export function fromElrondResourcesRaw(r: ElrondResourcesRaw): ElrondResources {
- const { nonce } = r;
+ const { nonce, delegations } = r;
return {
nonce,
+ delegations,
};
}
diff --git a/src/families/elrond/test-dataset.ts b/src/families/elrond/test-dataset.ts
index 2952920212..13f3e98129 100644
--- a/src/families/elrond/test-dataset.ts
+++ b/src/families/elrond/test-dataset.ts
@@ -52,6 +52,7 @@ const elrond: CurrenciesData = {
amount: "100000000",
mode: "send",
fees: null,
+ gasLimit: 0,
}),
expectedStatus: {
amount: new BigNumber("100000000"),
@@ -69,6 +70,7 @@ const elrond: CurrenciesData = {
amount: "100000000",
mode: "send",
fees: null,
+ gasLimit: 0,
}),
expectedStatus: {
errors: {
@@ -86,6 +88,7 @@ const elrond: CurrenciesData = {
amount: "1000000000000000000000000",
mode: "send",
fees: null,
+ gasLimit: 0,
}),
expectedStatus: {
errors: {
diff --git a/src/families/elrond/transaction.ts b/src/families/elrond/transaction.ts
index 8424fac232..7672ec2c80 100644
--- a/src/families/elrond/transaction.ts
+++ b/src/families/elrond/transaction.ts
@@ -29,6 +29,7 @@ export const fromTransactionRaw = (tr: TransactionRaw): Transaction => {
family: tr.family,
mode: tr.mode,
fees: tr.fees ? new BigNumber(tr.fees) : null,
+ gasLimit: tr.gasLimit,
};
};
export const toTransactionRaw = (t: Transaction): TransactionRaw => {
@@ -38,6 +39,7 @@ export const toTransactionRaw = (t: Transaction): TransactionRaw => {
family: t.family,
mode: t.mode,
fees: t.fees?.toString() || null,
+ gasLimit: t.gasLimit,
};
};
export default {
diff --git a/src/families/elrond/types.ts b/src/families/elrond/types.ts
index e31cda479d..f896a19451 100644
--- a/src/families/elrond/types.ts
+++ b/src/families/elrond/types.ts
@@ -1,5 +1,4 @@
import type { BigNumber } from "bignumber.js";
-import { Range, RangeRaw } from "../../range";
import type {
TransactionCommon,
TransactionCommonRaw,
@@ -7,6 +6,21 @@ import type {
export type ElrondResources = {
nonce: number;
+ delegations: ElrondDelegation[];
+};
+
+export type ElrondDelegation = {
+ address: string;
+ contract: string;
+ userUnBondable: string;
+ userActiveStake: string;
+ claimableRewards: string;
+ userUndelegatedList: UserUndelegated[];
+};
+
+export type UserUndelegated = {
+ amount: string;
+ seconds: number;
};
/**
@@ -14,13 +28,40 @@ export type ElrondResources = {
*/
export type ElrondResourcesRaw = {
nonce: number;
+ delegations: ElrondDelegation[];
};
+export type ElrondProtocolTransaction = {
+ nonce: number;
+ value: string;
+ receiver: string;
+ sender: string;
+ gasPrice: number;
+ gasLimit: number;
+ chainID: string;
+ signature?: string;
+ data?: string; //for ESDT or stake transactions
+ version: number;
+ options: number;
+};
+
+/**
+ * Elrond mode of transaction
+ */
+export type ElrondTransactionMode =
+ | "send"
+ | "delegate"
+ | "reDelegateRewards"
+ | "unDelegate"
+ | "claimRewards"
+ | "withdraw";
+
/**
* Elrond transaction
*/
export type Transaction = TransactionCommon & {
- mode: string;
+ mode: ElrondTransactionMode;
+ transfer?: ElrondTransferOptions;
family: "elrond";
fees: BigNumber | null | undefined;
txHash?: string;
@@ -31,10 +72,26 @@ export type Transaction = TransactionCommon & {
blockHeight?: number;
timestamp?: number;
nonce?: number;
+ gasLimit: number;
status?: string;
fee?: BigNumber;
round?: number;
miniBlockHash?: string;
+ data?: string;
+ tokenIdentifier?: string;
+ tokenValue?: string;
+ action?: any;
+};
+
+export enum ElrondTransferOptions {
+ egld = "egld",
+ esdt = "esdt",
+}
+
+export type ESDTToken = {
+ identifier: string;
+ name: string;
+ balance: string;
};
/**
@@ -42,8 +99,9 @@ export type Transaction = TransactionCommon & {
*/
export type TransactionRaw = TransactionCommonRaw & {
family: "elrond";
- mode: string;
+ mode: ElrondTransactionMode;
fees: string | null | undefined;
+ gasLimit: number;
};
export type ElrondValidator = {
bls: string;
@@ -60,12 +118,22 @@ export type ElrondValidator = {
};
export type NetworkInfo = {
- family: "elrond";
- gasPrice: Range;
+ family?: "elrond";
+ chainID: string;
+ denomination: number;
+ gasLimit: number;
+ gasPrice: number;
+ gasPerByte: number;
+ gasPriceModifier: string;
};
+
export type NetworkInfoRaw = {
- family: "elrond";
- gasPrice: RangeRaw;
+ family?: "elrond";
+ chainID: string;
+ denomination: number;
+ gasLimit: number;
+ gasPrice: number;
+ gasPerByte: number;
};
export type ElrondPreloadData = {
diff --git a/src/families/elrond/utils/binary.utils.ts b/src/families/elrond/utils/binary.utils.ts
new file mode 100644
index 0000000000..21d85a997a
--- /dev/null
+++ b/src/families/elrond/utils/binary.utils.ts
@@ -0,0 +1,13 @@
+function base64DecodeBinary(str: string): Buffer {
+ return Buffer.from(str, "base64");
+}
+
+export class BinaryUtils {
+ static base64Encode(str: string) {
+ return Buffer.from(str).toString("base64");
+ }
+
+ static base64Decode(str: string): string {
+ return base64DecodeBinary(str).toString("binary");
+ }
+}