public Task <IBitcoinBasedTransaction> CreateSwapRedeemTxAsync( IBitcoinBasedTransaction paymentTx, long amount, string redeemAddress, byte[] redeemScript, uint sequenceNumber = 0) { var currency = (BitcoinBasedCurrency)paymentTx.Currency; var swapOutput = paymentTx.Outputs .Cast <BitcoinBasedTxOutput>() .FirstOrDefault(o => o.IsPayToScriptHash(redeemScript)); if (swapOutput == null) { throw new Exception("Can't find pay to script hash output"); } var estimatedSigSize = BitcoinBasedCurrency.EstimateSigSize(swapOutput, forRedeem: true); var tx = currency.CreateP2PkhTx( unspentOutputs: new ITxOutput[] { swapOutput }, destinationAddress: redeemAddress, changeAddress: redeemAddress, amount: amount, fee: 0, lockTime: DateTimeOffset.MinValue); // fee = txSize * feeRate without dust, because all coins will be send to one address var fee = (long)((tx.VirtualSize() + estimatedSigSize) * currency.FeeRate); if (amount - fee < 0) { throw new Exception($"Insufficient funds for fee. Available {amount}, required {fee}"); } tx = currency.CreateP2PkhTx( unspentOutputs: new ITxOutput[] { swapOutput }, destinationAddress: redeemAddress, changeAddress: redeemAddress, amount: amount - fee, fee: fee, lockTime: DateTimeOffset.MinValue); if (sequenceNumber > 0) { tx.SetSequenceNumber(sequenceNumber); } return(Task.FromResult(tx)); }
public async Task <FundsUsageEstimation> EstimateMaxAmountAsync( string to, BlockchainTransactionType type, CancellationToken cancellationToken = default) { var currency = BtcBasedCurrency; var unspentOutputs = (await DataRepository .GetAvailableOutputsAsync(Currency, currency.OutputType(), currency.TransactionType) .ConfigureAwait(false)) .ToList(); if (!unspentOutputs.Any()) { return(null); } var availableAmountInSatoshi = unspentOutputs.Sum(o => o.Value); var estimatedSigSize = BitcoinBasedCurrency.EstimateSigSize(unspentOutputs); var testTx = currency .CreatePaymentTx( unspentOutputs: unspentOutputs, destinationAddress: currency.TestAddress(), changeAddress: currency.TestAddress(), amount: availableAmountInSatoshi, fee: 0, lockTime: DateTimeOffset.MinValue); // requiredFee = txSize * feeRate without dust, because all coins must be send to one address var requiredFeeInSatoshi = (long)((testTx.VirtualSize() + estimatedSigSize) * currency.FeeRate); var amount = currency.SatoshiToCoin(Math.Max(availableAmountInSatoshi - requiredFeeInSatoshi, 0)); var fee = currency.SatoshiToCoin(requiredFeeInSatoshi); return(new FundsUsageEstimation { TotalAmount = amount, TotalFee = fee, UsedAddresses = unspentOutputs.Select(o => new AddressUsageEstimation { Address = $"{o.TxId}:{o.Index}", Amount = currency.SatoshiToCoin(o.Value) }) }); }
public override async Task <(decimal, decimal, decimal)> EstimateMaxAmountToSendAsync( string to, BlockchainTransactionType type, decimal fee = 0, decimal feePrice = 0, bool reserve = false, CancellationToken cancellationToken = default) { var currency = BtcBasedCurrency; var unspentOutputs = (await DataRepository .GetAvailableOutputsAsync(Currency, currency.OutputType(), currency.TransactionType) .ConfigureAwait(false)) .ToList(); if (!unspentOutputs.Any()) { return(0m, 0m, 0m); } var availableAmountInSatoshi = unspentOutputs.Sum(o => o.Value); var estimatedSigSize = BitcoinBasedCurrency.EstimateSigSize(unspentOutputs); var testTx = currency .CreatePaymentTx( unspentOutputs: unspentOutputs, destinationAddress: currency.TestAddress(), changeAddress: currency.TestAddress(), amount: availableAmountInSatoshi, fee: 0, lockTime: DateTimeOffset.MinValue); // requiredFee = txSize * feeRate without dust, because all coins must be send to one address var requiredFeeInSatoshi = (long)((testTx.VirtualSize() + estimatedSigSize) * currency.FeeRate); var amount = currency.SatoshiToCoin(Math.Max(availableAmountInSatoshi - requiredFeeInSatoshi, 0)); fee = currency.SatoshiToCoin(requiredFeeInSatoshi); return(amount, fee, 0m); }
public async Task <(IBitcoinBasedTransaction, byte[])> CreateSwapPaymentTxAsync( BitcoinBasedCurrency currency, long amount, IEnumerable <string> fromWallets, string refundAddress, string toAddress, DateTimeOffset lockTime, byte[] secretHash, int secretSize, ITxOutputSource outputsSource) { var availableOutputs = (await outputsSource .GetAvailableOutputsAsync(fromWallets) .ConfigureAwait(false)) .ToList(); if (!availableOutputs.Any()) { throw new Exception($"Insufficient funds. Available 0."); } var feeInSatoshi = 0L; ITxOutput[] selectedOutputs = null; for (var i = 1; i <= availableOutputs.Count; ++i) { selectedOutputs = availableOutputs .Take(i) .ToArray(); var estimatedSigSize = BitcoinBasedCurrency.EstimateSigSize(selectedOutputs); var selectedInSatoshi = selectedOutputs.Sum(o => o.Value); if (selectedInSatoshi < amount) // insufficient funds { continue; } var maxFeeInSatoshi = selectedInSatoshi - amount; var estimatedTx = currency .CreateHtlcP2PkhScriptSwapPaymentTx( unspentOutputs: selectedOutputs, aliceRefundAddress: refundAddress, bobAddress: toAddress, lockTime: lockTime, secretHash: secretHash, secretSize: secretSize, amount: amount, fee: maxFeeInSatoshi, redeemScript: out _); var estimatedTxVirtualSize = estimatedTx.VirtualSize(); var estimatedTxSize = estimatedTxVirtualSize + estimatedSigSize; var estimatedTxSizeWithChange = estimatedTxVirtualSize + estimatedSigSize + BitcoinBasedCurrency.OutputSize; var estimatedFeeInSatoshi = (long)(estimatedTxSize * currency.FeeRate); if (estimatedFeeInSatoshi > maxFeeInSatoshi) // insufficient funds { continue; } var estimatedChangeInSatoshi = selectedInSatoshi - amount - estimatedFeeInSatoshi; // if estimated change is dust if (estimatedChangeInSatoshi >= 0 && estimatedChangeInSatoshi < currency.GetDust()) { feeInSatoshi = estimatedFeeInSatoshi + estimatedChangeInSatoshi; break; } // if estimated change > dust var estimatedFeeWithChangeInSatoshi = (long)(estimatedTxSizeWithChange * currency.FeeRate); if (estimatedFeeWithChangeInSatoshi > maxFeeInSatoshi) // insufficient funds { continue; } var esitmatedNewChangeInSatoshi = selectedInSatoshi - amount - estimatedFeeWithChangeInSatoshi; // if new estimated change is dust if (esitmatedNewChangeInSatoshi >= 0 && esitmatedNewChangeInSatoshi < currency.GetDust()) { feeInSatoshi = estimatedFeeWithChangeInSatoshi + esitmatedNewChangeInSatoshi; break; } // if new estimated change > dust feeInSatoshi = estimatedFeeWithChangeInSatoshi; break; } if (selectedOutputs == null || feeInSatoshi == 0L) { throw new Exception($"Insufficient funds. Available {availableOutputs.Sum(o => o.Value)}"); } var tx = currency .CreateHtlcP2PkhScriptSwapPaymentTx( unspentOutputs: selectedOutputs, aliceRefundAddress: refundAddress, bobAddress: toAddress, lockTime: lockTime, secretHash: secretHash, secretSize: secretSize, amount: amount, fee: feeInSatoshi, redeemScript: out var redeemScript); return(tx, redeemScript); }
public async Task <FundsUsageEstimation> EstimateFeeExAsync( string to, decimal amount, BlockchainTransactionType type, CancellationToken cancellationToken = default) { var currency = BtcBasedCurrency; var amountInSatoshi = currency.CoinToSatoshi(amount); var availableOutputs = (await DataRepository .GetAvailableOutputsAsync(Currency, currency.OutputType(), currency.TransactionType) .ConfigureAwait(false)) .ToList(); if (!availableOutputs.Any()) { return(null); // insufficient funds } for (var i = 1; i <= availableOutputs.Count; ++i) { var selectedOutputs = availableOutputs .Take(i) .ToArray(); var estimatedSigSize = BitcoinBasedCurrency.EstimateSigSize(selectedOutputs); var selectedInSatoshi = selectedOutputs.Sum(o => o.Value); if (selectedInSatoshi < amountInSatoshi) // insufficient funds { continue; } var maxFeeInSatoshi = selectedInSatoshi - amountInSatoshi; var estimatedTx = currency .CreatePaymentTx( unspentOutputs: selectedOutputs, destinationAddress: currency.TestAddress(), changeAddress: currency.TestAddress(), amount: amountInSatoshi, fee: maxFeeInSatoshi, lockTime: DateTimeOffset.MinValue); var estimatedTxVirtualSize = estimatedTx.VirtualSize(); var estimatedTxSize = estimatedTxVirtualSize + estimatedSigSize; var estimatedTxSizeWithChange = estimatedTxVirtualSize + estimatedSigSize + BitcoinBasedCurrency.OutputSize; var estimatedFeeInSatoshi = (long)(estimatedTxSize * currency.FeeRate); if (estimatedFeeInSatoshi > maxFeeInSatoshi) // insufficient funds { continue; } var estimatedChangeInSatoshi = selectedInSatoshi - amountInSatoshi - estimatedFeeInSatoshi; // if estimated change is dust if (estimatedChangeInSatoshi >= 0 && estimatedChangeInSatoshi < currency.GetDust()) { return(new FundsUsageEstimation { TotalAmount = amount, TotalFee = currency.SatoshiToCoin(estimatedFeeInSatoshi + estimatedChangeInSatoshi), UsedAddresses = selectedOutputs.Select(o => new AddressUsageEstimation { Address = $"{o.TxId}:{o.Index}", Amount = currency.SatoshiToCoin(o.Value) }) }); } // if estimated change > dust var estimatedFeeWithChangeInSatoshi = (long)(estimatedTxSizeWithChange * currency.FeeRate); if (estimatedFeeWithChangeInSatoshi > maxFeeInSatoshi) // insufficient funds { continue; } var esitmatedNewChangeInSatoshi = selectedInSatoshi - amountInSatoshi - estimatedFeeWithChangeInSatoshi; // if new estimated change is dust if (esitmatedNewChangeInSatoshi >= 0 && esitmatedNewChangeInSatoshi < currency.GetDust()) { return(new FundsUsageEstimation { TotalAmount = amount, TotalFee = currency.SatoshiToCoin(estimatedFeeWithChangeInSatoshi + esitmatedNewChangeInSatoshi), UsedAddresses = selectedOutputs.Select(o => new AddressUsageEstimation { Address = $"{o.TxId}:{o.Index}", Amount = currency.SatoshiToCoin(o.Value) }) }); } // if new estimated change > dust return(new FundsUsageEstimation { TotalAmount = amount, TotalFee = currency.SatoshiToCoin(estimatedFeeWithChangeInSatoshi), UsedAddresses = selectedOutputs.Select(o => new AddressUsageEstimation { Address = $"{o.TxId}:{o.Index}", Amount = currency.SatoshiToCoin(o.Value) }) }); } return(null); // insufficient funds }
public async Task <int?> EstimateTxSizeAsync( decimal amount, decimal fee, CancellationToken cancellationToken = default) { var currency = BtcBasedCurrency; var amountInSatoshi = currency.CoinToSatoshi(amount); var feeInSatoshi = currency.CoinToSatoshi(fee); var availableOutputs = (await DataRepository .GetAvailableOutputsAsync(Currency, currency.OutputType(), currency.TransactionType) .ConfigureAwait(false)) .ToList(); if (!availableOutputs.Any()) { return(null); // insufficient funds } for (var i = 1; i <= availableOutputs.Count; ++i) { var selectedOutputs = availableOutputs .Take(i) .ToArray(); var estimatedSigSize = BitcoinBasedCurrency.EstimateSigSize(selectedOutputs); var selectedInSatoshi = selectedOutputs.Sum(o => o.Value); if (selectedInSatoshi < amountInSatoshi) // insufficient funds { continue; } var maxFeeInSatoshi = selectedInSatoshi - amountInSatoshi; if (maxFeeInSatoshi < fee) { continue; // insufficient funds } //var changeInSatoshi = selectedInSatoshi - amountInSatoshi - feeInSatoshi; try { var estimatedTx = currency .CreatePaymentTx( unspentOutputs: selectedOutputs, destinationAddress: currency.TestAddress(), changeAddress: currency.TestAddress(), amount: amountInSatoshi, fee: feeInSatoshi, lockTime: DateTimeOffset.MinValue); var estimatedTxVirtualSize = estimatedTx.VirtualSize(); return(estimatedTxVirtualSize + estimatedSigSize); } catch (Exception) { continue; } } return(null); // insufficient funds }