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"); + } +}