UNPKG

@ledgerhq/coin-hedera

Version:
313 lines (268 loc) 9.7 kB
import { AccountUpdateTransaction, ContractExecuteTransaction, ContractFunctionParameters, ContractId, Hbar, TokenAssociateTransaction, TransactionId, TransferTransaction, } from "@hashgraph/sdk"; import type { FeeEstimation, TransactionIntent } from "@ledgerhq/coin-module-framework/api/index"; import BigNumber from "bignumber.js"; import invariant from "invariant"; import type { HederaConfig } from "../config"; import { DEFAULT_GAS_LIMIT, HEDERA_TRANSACTION_MODES, TRANSACTION_VALID_DURATION_SECONDS, } from "../constants"; import { rpcClient } from "../network/rpc"; import { createTransactionId } from "../network/utils"; import type { HederaMemo, HederaTxData } from "../types"; import { hasSpecificIntentData, serializeTransaction, toEVMAddress } from "./utils"; interface BuilderOperator { accountId: string; } interface BuilderCommonTransactionFields { transactionId: TransactionId; maxFee: BigNumber | undefined; memo: string; } interface BuilderCoinTransferTransaction extends BuilderCommonTransactionFields { type: HEDERA_TRANSACTION_MODES.Send; amount: BigNumber; recipient: string; } interface BuilderHTSTokenTransferTransaction extends BuilderCommonTransactionFields { type: HEDERA_TRANSACTION_MODES.Send; tokenAddress: string; amount: BigNumber; recipient: string; } interface BuilderERC20TokenTransferTransaction extends BuilderCommonTransactionFields { type: HEDERA_TRANSACTION_MODES.Send; tokenAddress: string; amount: BigNumber; recipient: string; gasLimit: BigNumber; } interface BuilderTokenAssociateTransaction extends BuilderCommonTransactionFields { type: HEDERA_TRANSACTION_MODES.TokenAssociate; tokenId: string; } interface BuilderUpdateAccountTransaction extends BuilderCommonTransactionFields { type: | HEDERA_TRANSACTION_MODES.Delegate | HEDERA_TRANSACTION_MODES.Undelegate | HEDERA_TRANSACTION_MODES.Redelegate; stakingNodeId: number | null | undefined; } async function buildUnsignedCoinTransaction({ account, transaction, }: { account: BuilderOperator; transaction: BuilderCoinTransferTransaction; }): Promise<TransferTransaction> { const accountId = account.accountId; const hbarAmount = Hbar.fromTinybars(transaction.amount); const tx = new TransferTransaction() .setTransactionValidDuration(TRANSACTION_VALID_DURATION_SECONDS) .setTransactionId(transaction.transactionId) .setTransactionMemo(transaction.memo) .addHbarTransfer(accountId, hbarAmount.negated()) .addHbarTransfer(transaction.recipient, hbarAmount); if (transaction.maxFee) { tx.setMaxTransactionFee(Hbar.fromTinybars(transaction.maxFee.toNumber())); } return tx.freezeWith(await rpcClient.getInstance()); } async function buildUnsignedHTSTokenTransaction({ account, transaction, }: { account: BuilderOperator; transaction: BuilderHTSTokenTransferTransaction; }): Promise<TransferTransaction> { const accountId = account.accountId; const tokenId = transaction.tokenAddress; const tx = new TransferTransaction() .setTransactionValidDuration(TRANSACTION_VALID_DURATION_SECONDS) .setTransactionId(transaction.transactionId) .setTransactionMemo(transaction.memo) .addTokenTransfer(tokenId, accountId, transaction.amount.negated().toNumber()) .addTokenTransfer(tokenId, transaction.recipient, transaction.amount.toNumber()); if (transaction.maxFee) { tx.setMaxTransactionFee(Hbar.fromTinybars(transaction.maxFee.toNumber())); } return tx.freezeWith(await rpcClient.getInstance()); } async function buildUnsignedERC20TokenTransaction({ transaction, }: { transaction: BuilderERC20TokenTransferTransaction; }): Promise<ContractExecuteTransaction> { const contractId = ContractId.fromEvmAddress(0, 0, transaction.tokenAddress); const recipientEvmAddress = await toEVMAddress(transaction.recipient); invariant(recipientEvmAddress, `hedera: EVM address is missing ${transaction.recipient}`); const gas = transaction.gasLimit.toNumber(); // create function parameters for ERC20 transfer function // transfer(address to, uint256 amount) returns (bool) const functionParameters = new ContractFunctionParameters() .addAddress(recipientEvmAddress) .addUint256(transaction.amount.toNumber()); const tx = new ContractExecuteTransaction() .setTransactionValidDuration(TRANSACTION_VALID_DURATION_SECONDS) .setTransactionId(transaction.transactionId) .setTransactionMemo(transaction.memo ?? "") .setContractId(contractId) .setGas(gas) .setFunction("transfer", functionParameters); if (transaction.maxFee) { tx.setMaxTransactionFee(Hbar.fromTinybars(transaction.maxFee.toNumber())); } return tx.freezeWith(await rpcClient.getInstance()); } async function buildTokenAssociateTransaction({ account, transaction, }: { account: BuilderOperator; transaction: BuilderTokenAssociateTransaction; }): Promise<TokenAssociateTransaction> { const accountId = account.accountId; const tx = new TokenAssociateTransaction() .setTransactionValidDuration(TRANSACTION_VALID_DURATION_SECONDS) .setTransactionId(transaction.transactionId) .setTransactionMemo(transaction.memo) .setAccountId(accountId) .setTokenIds([transaction.tokenId]); if (transaction.maxFee) { tx.setMaxTransactionFee(Hbar.fromTinybars(transaction.maxFee.toNumber())); } return tx.freezeWith(await rpcClient.getInstance()); } async function buildUnsignedUpdateAccountTransaction({ account, transaction, }: { account: BuilderOperator; transaction: BuilderUpdateAccountTransaction; }): Promise<AccountUpdateTransaction> { const accountId = account.accountId; const tx = new AccountUpdateTransaction() .setTransactionValidDuration(TRANSACTION_VALID_DURATION_SECONDS) .setTransactionId(transaction.transactionId) .setTransactionMemo(transaction.memo ?? "") .setAccountId(accountId); if (transaction.maxFee) { tx.setMaxTransactionFee(Hbar.fromTinybars(transaction.maxFee.toNumber())); } if (typeof transaction.stakingNodeId === "number") { tx.setStakedNodeId(transaction.stakingNodeId); } if (transaction.stakingNodeId === null) { tx.clearStakedNodeId(); } return tx.freezeWith(await rpcClient.getInstance()); } function isStakingMode(type: string): type is BuilderUpdateAccountTransaction["type"] { return ( type === HEDERA_TRANSACTION_MODES.Redelegate || type === HEDERA_TRANSACTION_MODES.Undelegate || type === HEDERA_TRANSACTION_MODES.Delegate ); } export async function craftTransaction({ txIntent, customFees, config, }: { txIntent: TransactionIntent<HederaMemo, HederaTxData>; customFees?: FeeEstimation; config: HederaConfig; }) { const account = { accountId: txIntent.sender }; const maxFee = customFees ? new BigNumber(customFees.value.toString()) : undefined; const transactionId = await createTransactionId(account.accountId, config); let tx; if (txIntent.type === HEDERA_TRANSACTION_MODES.TokenAssociate) { invariant(txIntent.asset.type !== "native", "hedera: invalid asset type"); invariant("assetReference" in txIntent.asset, "hedera: assetReference is missing"); tx = await buildTokenAssociateTransaction({ account, transaction: { type: txIntent.type, transactionId, tokenId: txIntent.asset.assetReference, memo: txIntent.memo.value, maxFee, }, }); } else if (txIntent.type === HEDERA_TRANSACTION_MODES.Send && txIntent.asset.type === "hts") { invariant("assetReference" in txIntent.asset, "hedera: no assetReference in token transfer"); const amount = new BigNumber(txIntent.amount.toString()); tx = await buildUnsignedHTSTokenTransaction({ account, transaction: { type: txIntent.type, transactionId, tokenAddress: txIntent.asset.assetReference, amount, recipient: txIntent.recipient, memo: txIntent.memo.value, maxFee, }, }); } else if (txIntent.type === HEDERA_TRANSACTION_MODES.Send && txIntent.asset.type === "erc20") { invariant("assetReference" in txIntent.asset, "hedera: no assetReference in token transfer"); const amount = new BigNumber(txIntent.amount.toString()); const gasLimit = hasSpecificIntentData(txIntent, "erc20") ? new BigNumber(txIntent.data.gasLimit.toString()) : DEFAULT_GAS_LIMIT; tx = await buildUnsignedERC20TokenTransaction({ transaction: { type: txIntent.type, transactionId, tokenAddress: txIntent.asset.assetReference, amount, recipient: txIntent.recipient, memo: txIntent.memo.value, maxFee, gasLimit, }, }); } else if (isStakingMode(txIntent.type)) { const stakingNodeId = hasSpecificIntentData(txIntent, "staking") ? txIntent.data.stakingNodeId : undefined; tx = await buildUnsignedUpdateAccountTransaction({ account, transaction: { type: txIntent.type, transactionId, memo: txIntent.memo.value, maxFee, stakingNodeId, }, }); } // HEDERA_TRANSACTION_MODES.ClaimRewards is just a coin transfer that triggers staking rewards claim else { const amount = new BigNumber(txIntent.amount.toString()); tx = await buildUnsignedCoinTransaction({ account, transaction: { type: HEDERA_TRANSACTION_MODES.Send, transactionId, amount, recipient: txIntent.recipient, memo: txIntent.memo.value, maxFee, }, }); } const serializedTx = serializeTransaction(tx); return { tx, serializedTx }; }