UNPKG

@stricahq/typhonjs

Version:

Pure JS Cardano Wallet library

275 lines (274 loc) 12.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.transactionBuilder = void 0; const bignumber_js_1 = __importDefault(require("bignumber.js")); const _ = require("lodash"); const helpers_1 = require("../../utils/helpers"); const utils_1 = require("../../utils/utils"); function transactionBuilder({ transaction, inputs, changeAddress, collateralInputs = [], }) { const verifyCollateral = (currentFee) => { const currentCollateral = transaction.getCollateralAmount(); const requiredCollateral = currentFee .times(transaction.protocolParams.collateralPercent) .div(100); const isPlutusTx = transaction.isPlutusTransaction(); if (isPlutusTx && currentCollateral.lt(requiredCollateral)) { throw new Error("Not enough collateral supplied, collaterals with tokens are not valid collaterals"); } }; const addCollateral = (currentFee) => { if (collateralInputs && collateralInputs.length > 0) { const currentCollateral = transaction.getCollateralAmount(); const requiredCollateral = currentFee .times(transaction.protocolParams.collateralPercent) .div(100); while (currentCollateral.lt(requiredCollateral) && collateralInputs.length > 0) { const col = collateralInputs.pop(); if (col) { transaction.addCollateral(col); // the new Fee also affects collateral, but collateral utxo is in enough amount, that this race condition is unlikely to happen } } } }; const minUtxo = (0, utils_1.calculateMinUtxoAmountBabbage)({ address: (0, utils_1.getAddressFromHex)(Buffer.from("000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "hex")), amount: new bignumber_js_1.default(45000000000000000), tokens: [], }, new bignumber_js_1.default(transaction.protocolParams.utxoCostPerByte)); const utxoInputs = _.cloneDeep(inputs); // Add Inputs // there needs to be min one input, add that first if (utxoInputs.length > 0) { const firstInput = utxoInputs.splice(0, 1)[0]; transaction.addInput(firstInput); } // add first collateral, if collaterals are supplied const col = collateralInputs.pop(); if (col) { transaction.addCollateral(col); } for (const utxo of utxoInputs) { // optimized utxo selection const { ada: totalInputAda, tokens: totalInputTokens } = transaction.getInputAmount(); const { ada: totalOutputAda, tokens: totalOutputTokens } = transaction.getOutputAmount(); const additionalOutput = transaction.getAdditionalOutputAda(); const additionalInput = transaction.getAdditionalInputAda(); let trxFeeWithoutChange = transaction.calculateFee(); addCollateral(trxFeeWithoutChange); trxFeeWithoutChange = transaction.calculateFee(); const currentInput = totalInputAda.plus(additionalInput); const requiredInput = totalOutputAda.plus(additionalOutput).plus(trxFeeWithoutChange); const tokenDiff = (0, helpers_1.getTokenDiff)(totalInputTokens, totalOutputTokens); if (tokenDiff.length === 0) { if (currentInput.eq(requiredInput)) { // we got enough input break; } else if (currentInput.gt(requiredInput)) { // input diff without fee const inputDiff = currentInput.minus(totalOutputAda.plus(additionalOutput)); // not equal to, as there will be a fee above minUtxo if (inputDiff.gt(minUtxo)) { let feeWithChange = transaction.calculateFee([ { address: changeAddress, amount: inputDiff, tokens: [], }, ]); addCollateral(trxFeeWithoutChange); feeWithChange = transaction.calculateFee([ { address: changeAddress, amount: inputDiff, tokens: [], }, ]); if (inputDiff.gte(feeWithChange.plus(minUtxo))) { // we got enough input break; } } } } else if (!tokenDiff.some(({ amount }) => amount.lt(0))) { const tokensTokens = (0, utils_1.getMaximumTokenSets)(tokenDiff, transaction.protocolParams.maxValueSize); const changeOutputs = []; let inputDiff = currentInput.minus(totalOutputAda.plus(additionalOutput)); let extraAdaRequired = new bignumber_js_1.default(0); for (const [index, tokens] of tokensTokens.entries()) { const minUtxo = (0, utils_1.calculateMinUtxoAmountBabbage)({ address: changeAddress, amount: new bignumber_js_1.default(45000000000000000), tokens, }, new bignumber_js_1.default(transaction.protocolParams.utxoCostPerByte)); let outputAmount = minUtxo; if (index === tokensTokens.length - 1) { // last set, add full ada diff as output let feeWithChange = transaction.calculateFee([ ...changeOutputs, { address: changeAddress, amount: inputDiff.lt(minUtxo) ? minUtxo : inputDiff, tokens: tokens, }, ]); addCollateral(feeWithChange); feeWithChange = transaction.calculateFee([ ...changeOutputs, { address: changeAddress, amount: inputDiff.lt(minUtxo) ? minUtxo : inputDiff, tokens: tokens, }, ]); const minADA = minUtxo.plus(feeWithChange); if (inputDiff.gte(minADA)) { outputAmount = inputDiff; } else { extraAdaRequired = extraAdaRequired.plus(minADA.minus(inputDiff)); } } else if (inputDiff.gte(minUtxo)) { inputDiff = inputDiff.minus(minUtxo); } else { extraAdaRequired = extraAdaRequired.plus(minUtxo.minus(inputDiff)); } changeOutputs.push({ address: changeAddress, amount: outputAmount, tokens: tokens, }); } if (extraAdaRequired.eq(0)) { // we got enough input break; } } transaction.addInput(utxo); } // Set Change const { ada: totalInputAda, tokens: totalInputTokens } = transaction.getInputAmount(); const { ada: totalOutputAda, tokens: totalOutputTokens } = transaction.getOutputAmount(); const additionalOutput = transaction.getAdditionalOutputAda(); const additionalInput = transaction.getAdditionalInputAda(); const currentInput = totalInputAda.plus(additionalInput); const currentOutput = totalOutputAda.plus(additionalOutput); const tokenDiff = (0, helpers_1.getTokenDiff)(totalInputTokens, totalOutputTokens); if (tokenDiff.length === 0) { const feeWithoutChange = transaction.calculateFee(); const outputWithFee = currentOutput.plus(feeWithoutChange); if (currentInput.eq(outputWithFee)) { // no change required transaction.setFee(feeWithoutChange); verifyCollateral(feeWithoutChange); } else if (currentInput.gt(outputWithFee)) { const changeADA = currentInput.minus(currentOutput); // not equal to, as there will be a a slightly higher fee with new change if (changeADA.gt(minUtxo.plus(feeWithoutChange))) { const feeWithChange = transaction.calculateFee([ { address: changeAddress, amount: changeADA, tokens: [], }, ]); verifyCollateral(feeWithChange); transaction.setFee(feeWithChange); transaction.addOutput({ address: changeAddress, amount: changeADA.minus(feeWithChange), tokens: [], }); } else if (changeADA.lt(2000000)) { // if change is less than 2 ADA // not enough ADA for a change, set remaining ADA as fee transaction.setFee(changeADA); } else { throw new Error("Not enough ADA"); } } else { throw new Error("Not enough ADA"); } } else if (!tokenDiff.some(({ amount }) => amount.lt(0))) { const tokensTokens = (0, utils_1.getMaximumTokenSets)(_.clone(tokenDiff), transaction.protocolParams.maxValueSize); const changeOutputs = []; { let changeADA = currentInput.minus(currentOutput); tokensTokens.forEach((tokens, index) => { const minUtxo = (0, utils_1.calculateMinUtxoAmountBabbage)({ address: changeAddress, amount: new bignumber_js_1.default(45000000000000000), tokens }, new bignumber_js_1.default(transaction.protocolParams.utxoCostPerByte)); let outputAmount = minUtxo; if (index === tokensTokens.length - 1) { // last set, add full ada diff as output const feeWithChange = transaction.calculateFee([ ...changeOutputs, { address: changeAddress, amount: changeADA.lt(minUtxo) ? minUtxo : changeADA, tokens: tokens, }, ]); const minADA = minUtxo.plus(feeWithChange); if (changeADA.gte(minADA)) { outputAmount = changeADA; } else { throw new Error("Not enough ADA"); } } else if (changeADA.gte(minUtxo)) { changeADA = changeADA.minus(minUtxo); } else { throw new Error("Not enough ADA"); } changeOutputs.push({ address: changeAddress, amount: outputAmount, tokens: tokens, }); }); } const feeWithChange = transaction.calculateFee(changeOutputs); verifyCollateral(feeWithChange); transaction.setFee(feeWithChange); let changeADA = currentInput.minus(currentOutput).minus(feeWithChange); tokensTokens.forEach((tokens, index) => { const minUtxo = (0, utils_1.calculateMinUtxoAmountBabbage)({ address: changeAddress, amount: new bignumber_js_1.default(45000000000000000), tokens }, new bignumber_js_1.default(transaction.protocolParams.utxoCostPerByte)); let outputAmount = minUtxo; if (index === tokensTokens.length - 1) { // last set, add full ada diff as output outputAmount = changeADA; } else if (changeADA.gte(minUtxo)) { changeADA = changeADA.minus(minUtxo); } transaction.addOutput({ address: changeAddress, amount: outputAmount, tokens: tokens, }); }); } else { throw new Error("Not enough tokens"); } const txSize = transaction.calculateTxSize(); if (transaction.protocolParams.maxTxSize && txSize > transaction.protocolParams.maxTxSize) { throw new Error("Tx size limit reached, try spending lesser ADA/Tokens"); } return transaction; } exports.transactionBuilder = transactionBuilder; exports.default = transactionBuilder;