public StandardTransactionPolicy() { ScriptVerify = NBitcoin.ScriptVerify.Standard; MaxTransactionSize = 100000; MaxTxFee = new FeeRate(Money.Coins(0.1m)); MinRelayTxFee = new FeeRate(Money.Satoshis(5000)); CheckFee = true; }
private void ReadSettings(bool bindControl = true) { Algorithm = BotSetting.AlgorithmList.Where(x => x.Id == Settings.Default.algorithm).First(); CandleType = new CandleType(Settings.Default.candleType); FeeRate = Settings.Default.feeRate; TradeRate = Settings.Default.tradeRate; Coin = BotSetting.CoinList.Where(x => x.Ticker.Equals(Settings.Default.coin)).FirstOrDefault(); Interval = Convert.ToInt32(Settings.Default.interval); TriggerRate = Settings.Default.triggerRate; CandleCount = Convert.ToInt32(Settings.Default.candleCount); if (bindControl) { cmbAlgorithm.SelectedItem = Algorithm; cmbCandle.SelectedItem = CandleType; txtFee.Text = FeeRate.ToString(); txtTradeRate.Text = TradeRate.ToString(); cmbCoin.SelectedItem = Coin; txtInterval.Text = Interval.ToString(); txtTriggerRate.Text = TriggerRate.ToString(); txtCandleCount.Text = CandleCount.ToString(); } }
public TrustedBroadcastRequest CreateOfferRedeemTransaction(FeeRate feeRate) { Transaction tx = new Transaction(); tx.LockTime = EscrowScriptPubKeyParameters.GetFromCoin(InternalState.EscrowedCoin).LockTime; tx.Inputs.Add(new TxIn()); tx.Inputs[0].Sequence = 0; tx.Outputs.Add(new TxOut(InternalState.OfferCoin.Amount, InternalState.RedeemDestination)); tx.Inputs[0].ScriptSig = new Script( Op.GetPushOp(TrustedBroadcastRequest.PlaceholderSignature), Op.GetPushOp(InternalState.OfferCoin.Redeem.ToBytes())); tx.Inputs[0].Witnessify(); tx.Outputs[0].Value -= feeRate.GetFee(tx.GetVirtualSize()); var redeemTransaction = new TrustedBroadcastRequest { Key = InternalState.EscrowKey, PreviousScriptPubKey = InternalState.OfferCoin.ScriptPubKey, Transaction = tx }; return(redeemTransaction); }
private void ValidateCustomFee(IValidationErrors errors) { var customFeeString = CustomFee; if (customFeeString is null or "") { return; } if (customFeeString.Any(c => !char.IsDigit(c) && c != '.')) { errors.Add(ErrorSeverity.Error, "The field only accepts numbers."); return; } if (!decimal.TryParse(customFeeString, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var value)) { errors.Add(ErrorSeverity.Error, "The entered fee is not valid."); return; } if (value < decimal.One) { errors.Add(ErrorSeverity.Error, "Cannot be less than 1 sat/vByte."); return; } try { _ = new FeeRate(value); } catch (OverflowException) { errors.Add(ErrorSeverity.Error, "The entered fee is too high."); return; } }
public async Task GenerateCoinsAsync(int numberOfCoins, int seed, CancellationToken cancellationToken) { ThrowIfDisposed(); var feeRate = new FeeRate(4.0m); var(splitTx, spendingCoin) = Wallet.CreateTemplateTransaction(); var availableAmount = spendingCoin.EffectiveValue(feeRate, CoordinationFeeRate.Zero); var rnd = new Random(seed); double NextNotTooSmall() => 0.00001 + (rnd.NextDouble() * 0.99999); var sampling = Enumerable .Range(0, numberOfCoins - 1) .Select(_ => NextNotTooSmall()) .Prepend(0) .Prepend(1) .OrderBy(x => x) .ToArray(); var amounts = sampling .Zip(sampling.Skip(1), (x, y) => y - x) .Select(x => x * availableAmount.Satoshi) .Select(x => Money.Satoshis((long)x)); var scriptPubKey = Wallet.ScriptPubKey; foreach (var amount in amounts) { var effectiveOutputValue = amount - feeRate.GetFee(scriptPubKey.EstimateOutputVsize()); splitTx.Outputs.Add(new TxOut(effectiveOutputValue, scriptPubKey)); } await Wallet.SendRawTransactionAsync(Wallet.SignTransaction(splitTx), cancellationToken).ConfigureAwait(false); SplitTransaction = new SmartTransaction(splitTx, new Height(1)); }
public static ExternalServices CreateUsingFullNode(IRepository repository, Tracker tracker, TumblingState tumblingState) { FeeRate minimumRate = new FeeRate(MempoolValidator.MinRelayTxFee.FeePerK); ExternalServices service = new ExternalServices(); service.FeeService = new FullNodeFeeService() { MinimumFeeRate = minimumRate }; // on regtest the estimatefee always fails if (tumblingState.TumblerNetwork == Network.RegTest) { service.FeeService = new FullNodeFeeService() { MinimumFeeRate = minimumRate, FallBackFeeRate = new FeeRate(Money.Satoshis(50), 1) }; } // TODO: These ultimately need to be brought in from the tumblebit client UI string dummyWalletName = ""; string dummyAccountName = ""; FullNodeWalletCache cache = new FullNodeWalletCache(repository, tumblingState); service.WalletService = new FullNodeWalletService(tumblingState, dummyWalletName, dummyAccountName); service.BroadcastService = new FullNodeBroadcastService(cache, repository, tumblingState); service.BlockExplorerService = new FullNodeBlockExplorerService(cache, repository, tumblingState); service.TrustedBroadcastService = new FullNodeTrustedBroadcastService(service.BroadcastService, service.BlockExplorerService, repository, cache, tracker, tumblingState) { // BlockExplorer will already track the addresses, since they used a shared bitcoind, no need of tracking again (this would overwrite labels) TrackPreviousScriptPubKey = false }; return(service); }
public PowBlockAssembler(ConsensusLoop consensusLoop, Network network, ConcurrentChain chain, MempoolScheduler mempoolScheduler, TxMempool mempool, IDateTimeProvider dateTimeProvider, AssemblerOptions options = null) { options = options ?? new AssemblerOptions(); this.blockMinFeeRate = options.BlockMinFeeRate; // Limit weight to between 4K and MAX_BLOCK_WEIGHT-4K for sanity: this.blockMaxWeight = (uint)Math.Max(4000, Math.Min(PowMining.DefaultBlockMaxWeight - 4000, options.BlockMaxWeight)); // Limit size to between 1K and MAX_BLOCK_SERIALIZED_SIZE-1K for sanity: this.blockMaxSize = (uint)Math.Max(1000, Math.Min(network.Consensus.Option <PowConsensusOptions>().MAX_BLOCK_SERIALIZED_SIZE - 1000, options.BlockMaxSize)); // Whether we need to account for byte usage (in addition to weight usage) this.needSizeAccounting = (blockMaxSize < network.Consensus.Option <PowConsensusOptions>().MAX_BLOCK_SERIALIZED_SIZE - 1000); this.consensusLoop = consensusLoop; this.chain = chain; this.mempoolScheduler = mempoolScheduler; this.mempool = mempool; this.dateTimeProvider = dateTimeProvider; this.options = options; this.network = network; this.inBlock = new TxMempool.SetEntries(); // Reserve space for coinbase tx this.blockSize = 1000; this.blockWeight = 4000; this.blockSigOpsCost = 400; this.fIncludeWitness = false; // These counters do not include coinbase tx this.blockTx = 0; this.fees = 0; this.pblocktemplate = new BlockTemplate { Block = new Block(), VTxFees = new List <Money>() }; }
private async Task <bool> TryConfirmConnectionAsync(long vsizeAllocationToRequest, CancellationToken cancellationToken) { var inputVsize = Coin.ScriptPubKey.EstimateInputVsize(); var vsizesToRequest = new[] { vsizeAllocationToRequest - inputVsize }; var totalFeeToPay = FeeRate.GetFee(Coin.ScriptPubKey.EstimateInputVsize()); var totalAmount = Coin.Amount; var effectiveAmount = totalAmount - totalFeeToPay; if (effectiveAmount <= Money.Zero) { throw new InvalidOperationException($"Round({ RoundId }), Alice({ AliceId}): Not enough funds to pay for the fees."); } var amountsToRequest = new[] { effectiveAmount.Satoshi }; var response = await ArenaClient .ConfirmConnectionAsync( RoundId, AliceId, amountsToRequest, vsizesToRequest, RealAmountCredentials, RealVsizeCredentials, cancellationToken) .ConfigureAwait(false); var isConfirmed = response.Value; if (isConfirmed) { RealAmountCredentials = response.RealAmountCredentials; RealVsizeCredentials = response.RealVsizeCredentials; } return(isConfirmed); }
public override void CheckTransaction(MempoolValidationContext context) { if (context.Transaction.IsCoinBase) { return; } List <byte[]> opReturns = context.Transaction.Outputs.Select(o => o.ScriptPubKey.ToBytes(true)).Where(b => IsOpReturn(b)).ToList(); Money transactionFees = context.Fees; FeeRate OpReturnFeeRate = new FeeRate(((x42Consensus)this.network.Consensus).MinOpReturnFee); // If there is OP_RETURN data, we will want to make sure the fee is correct. if (opReturns.Count() > 0) { var opReturnSize = opReturns.Sum(r => r.Length); if (transactionFees < OpReturnFeeRate.GetFee(opReturnSize)) { this.logger.LogTrace($"(-)[FAIL_{nameof(x42OpReturnFeeMempoolRule)}]".ToUpperInvariant()); x42ConsensusErrors.InsufficientOpReturnFee.Throw(); } } base.CheckTransaction(context); }
public TrustedBroadcastRequest CreateRedeemTransaction(Network network, FeeRate feeRate) { if (feeRate == null) { throw new ArgumentNullException(nameof(feeRate)); } var escrow = EscrowScriptPubKeyParameters.GetFromCoin(InternalState.EscrowedCoin); var escrowCoin = InternalState.EscrowedCoin; Transaction tx = new Transaction(); tx.LockTime = escrow.LockTime; tx.Inputs.Add(new TxIn()); //Put a dummy signature and the redeem script tx.Inputs[0].ScriptSig = new Script( Op.GetPushOp(TrustedBroadcastRequest.PlaceholderSignature), Op.GetPushOp(escrowCoin.Redeem.ToBytes())); tx.Inputs[0].Witnessify(); tx.Inputs[0].Sequence = 0; tx.Outputs.Add(new TxOut(escrowCoin.Amount, InternalState.RedeemDestination)); var virtualSize = tx.HasWitness ? tx.GetVirtualSize(network.Consensus.Options.WitnessScaleFactor) : tx.GetSerializedSize(); tx.Outputs[0].Value -= feeRate.GetFee(tx.GetVirtualSize(virtualSize)); var redeemTransaction = new TrustedBroadcastRequest { Key = InternalState.EscrowKey, PreviousScriptPubKey = escrowCoin.ScriptPubKey, Transaction = tx, KnownPrevious = new Coin[] { escrowCoin } }; return(redeemTransaction); }
public BlockPolicyEstimator(FeeRate minRelayFee, NodeSettings nodeArgs, ILoggerFactory loggerFactory) { this.mapMemPoolTxs = new Dictionary <uint256, TxStatsInfo>(); this.nodeArgs = nodeArgs; this.nBestSeenHeight = 0; this.trackedTxs = 0; this.untrackedTxs = 0; this.logger = loggerFactory.CreateLogger(this.GetType().FullName); this.minTrackedFee = minRelayFee < new FeeRate(new Money(MIN_FEERATE)) ? new FeeRate(new Money(MIN_FEERATE)) : minRelayFee; var vfeelist = new List <double>(); for (double bucketBoundary = this.minTrackedFee.FeePerK.Satoshi; bucketBoundary <= MAX_FEERATE; bucketBoundary *= FEE_SPACING) { vfeelist.Add(bucketBoundary); } vfeelist.Add(INF_FEERATE); this.feeStats = new TxConfirmStats(this.logger); this.feeStats.Initialize(vfeelist, MAX_BLOCK_CONFIRMS, DEFAULT_DECAY); }
// Process a transaction accepted to the mempool public void ProcessTransaction(TxMempoolEntry entry, bool validFeeEstimate) { int txHeight = entry.EntryHeight; uint256 hash = entry.TransactionHash; if (mapMemPoolTxs.ContainsKey(hash)) { Logging.Logs.EstimateFee.LogInformation($"Blockpolicy error mempool tx {hash} already being tracked"); return; } if (txHeight != nBestSeenHeight) { // Ignore side chains and re-orgs; assuming they are random they don't // affect the estimate. We'll potentially double count transactions in 1-block reorgs. // Ignore txs if BlockPolicyEstimator is not in sync with chainActive.Tip(). // It will be synced next time a block is processed. return; } // Only want to be updating estimates when our blockchain is synced, // otherwise we'll miscalculate how many blocks its taking to get included. if (!validFeeEstimate) { untrackedTxs++; return; } trackedTxs++; // Feerates are stored and reported as BTC-per-kb: FeeRate feeRate = new FeeRate(entry.Fee, (int)entry.GetTxSize()); mapMemPoolTxs.Add(hash, new TxStatsInfo()); mapMemPoolTxs[hash].blockHeight = txHeight; mapMemPoolTxs[hash].bucketIndex = feeStats.NewTx(txHeight, (double)feeRate.FeePerK.Satoshi); }
public WalletFeePolicy(BlockPolicyEstimator blockPolicyEstimator, MempoolValidator mempoolValidator, TxMempool mempool, Network network) { this.blockPolicyEstimator = blockPolicyEstimator; this.mempoolValidator = mempoolValidator; this.mempool = mempool; this.minTxFee = new FeeRate(1000); this.fallbackFee = new FeeRate(20000); this.payTxFee = new FeeRate(0); this.maxTxFee = new Money(0.1M, MoneyUnit.BTC); this.minRelayTxFee = MempoolValidator.MinRelayTxFee; // this is a very very ugly hack // the fee constants should be set at the // network level or the consensus options if (network.Name.ToLower().Contains("stratis")) { this.minTxFee = new FeeRate(10000); this.fallbackFee = new FeeRate(40000); this.payTxFee = new FeeRate(0); this.maxTxFee = new Money(0.1M, MoneyUnit.BTC); this.minRelayTxFee = new FeeRate(10000); } }
/// <summary> /// Constructs an instance of the block policy estimator object. /// </summary> /// <param name="mempoolSettings">Mempool settings.</param> /// <param name="loggerFactory">Factory for creating loggers.</param> /// <param name="nodeSettings">Full node settings.</param> public BlockPolicyEstimator(MempoolSettings mempoolSettings, ILoggerFactory loggerFactory, NodeSettings nodeSettings) { this.mapMemPoolTxs = new Dictionary <uint256, TxStatsInfo>(); this.mempoolSettings = mempoolSettings; this.nBestSeenHeight = 0; this.trackedTxs = 0; this.untrackedTxs = 0; this.logger = loggerFactory.CreateLogger(this.GetType().FullName); this.minTrackedFee = nodeSettings.MinRelayTxFeeRate < new FeeRate(new Money(MinFeeRate)) ? new FeeRate(new Money(MinFeeRate)) : nodeSettings.MinRelayTxFeeRate; var vfeelist = new List <double>(); for (double bucketBoundary = this.minTrackedFee.FeePerK.Satoshi; bucketBoundary <= MaxFeeRate; bucketBoundary *= FeeSpacing) { vfeelist.Add(bucketBoundary); } vfeelist.Add(InfFeeRate); this.feeStats = new TxConfirmStats(this.logger); this.feeStats.Initialize(vfeelist, MaxBlockConfirms, DefaultDecay); }
public RoundParameters( WabiSabiConfig wabiSabiConfig, Network network, WasabiRandom random, FeeRate feeRate, CoordinationFeeRate coordinationFeeRate) { Network = network; Random = random; FeeRate = feeRate; CoordinationFeeRate = coordinationFeeRate; MaxInputCountByRound = wabiSabiConfig.MaxInputCountByRound; MinInputCountByRound = wabiSabiConfig.MinInputCountByRound; MinRegistrableAmount = wabiSabiConfig.MinRegistrableAmount; MaxRegistrableAmount = wabiSabiConfig.MaxRegistrableAmount; // Note that input registration timeouts can be modified runtime. StandardInputRegistrationTimeout = wabiSabiConfig.StandardInputRegistrationTimeout; ConnectionConfirmationTimeout = wabiSabiConfig.ConnectionConfirmationTimeout; OutputRegistrationTimeout = wabiSabiConfig.OutputRegistrationTimeout; TransactionSigningTimeout = wabiSabiConfig.TransactionSigningTimeout; BlameInputRegistrationTimeout = wabiSabiConfig.BlameInputRegistrationTimeout; }
protected BlockDefinition( IConsensusManager consensusManager, IDateTimeProvider dateTimeProvider, ILoggerFactory loggerFactory, ITxMempool mempool, MempoolSchedulerLock mempoolLock, MinerSettings minerSettings, Network network) { this.ConsensusManager = consensusManager; this.DateTimeProvider = dateTimeProvider; this.logger = loggerFactory.CreateLogger(this.GetType().FullName); this.Mempool = mempool; this.MempoolLock = mempoolLock; this.Network = network; this.Options = minerSettings.BlockDefinitionOptions; this.BlockMinFeeRate = this.Options.BlockMinFeeRate; // Whether we need to account for byte usage (in addition to weight usage). this.NeedSizeAccounting = (this.Options.BlockMaxSize < network.Consensus.Options.MaxBlockSerializedSize); this.Configure(); }
public async Task <Transaction> ReceiveAsync(ScriptCoin escrowedCoin, TransactionSignature clientSignature, Key escrowKey, FeeRate feeRate) { _ReceiveBatch.FeeRate = feeRate; var task = _ReceiveBatch.WaitTransactionAsync(new ClientEscapeData() { ClientSignature = clientSignature, EscrowedCoin = escrowedCoin, EscrowKey = escrowKey }).ConfigureAwait(false); Logs.Tumbler.LogDebug($"ClientEscape batch count {_ReceiveBatch.BatchCount}"); return(await task); }
public GetFeeRateResult GetFeeRate(int blockCount, FeeRate fallbackFeeRate, CancellationToken cancellation = default) { return(GetFeeRateAsync(blockCount, fallbackFeeRate, cancellation).GetAwaiter().GetResult()); }
public void BlockPolicyEstimates() { var dateTimeSet = new DateTimeProviderSet(); NodeSettings settings = NodeSettings.Default(); var mpool = new TxMempool(DateTimeProvider.Default, new BlockPolicyEstimator(new MempoolSettings(settings), settings.LoggerFactory, settings), settings.LoggerFactory, settings); var entry = new TestMemPoolEntryHelper(); var basefee = new Money(2000); var deltaFee = new Money(100); var feeV = new List <Money>(); // Populate vectors of increasing fees for (int j = 0; j < 10; j++) { feeV.Add(basefee * (j + 1)); } // Store the hashes of transactions that have been // added to the mempool by their associate fee // txHashes[j] is populated with transactions either of // fee = basefee * (j+1) var txHashes = new List <uint256> [10]; for (int i = 0; i < txHashes.Length; i++) { txHashes[i] = new List <uint256>(); } // Create a transaction template var garbage = new Script(Enumerable.Range(0, 128).Select(i => (byte)1).ToArray()); var txf = new Transaction(); txf.AddInput(new TxIn(garbage)); txf.AddOutput(new TxOut(0L, Script.Empty)); var baseRate = new FeeRate(basefee, txf.GetVirtualSize()); // Create a fake block var block = new List <Transaction>(); int blocknum = 0; int answerFound; // Loop through 200 blocks // At a decay .998 and 4 fee transactions per block // This makes the tx count about 1.33 per bucket, above the 1 threshold while (blocknum < 200) { for (int j = 0; j < 10; j++) { // For each fee for (int k = 0; k < 4; k++) { // add 4 fee txs Transaction tx = txf.Clone(false); tx.Inputs[0].PrevOut.N = (uint)(10000 * blocknum + 100 * j + k); // make transaction unique uint256 hash = tx.GetHash(); mpool.AddUnchecked(hash, entry.Fee(feeV[j]).Time(dateTimeSet.GetTime()).Priority(0).Height(blocknum).FromTx(tx, mpool)); txHashes[j].Add(hash); } } //Create blocks where higher fee txs are included more often for (int h = 0; h <= blocknum % 10; h++) { // 10/10 blocks add highest fee transactions // 9/10 blocks add 2nd highest and so on until ... // 1/10 blocks add lowest fee transactions while (txHashes[9 - h].Count > 0) { Transaction ptx = mpool.Get(txHashes[9 - h].Last()); if (ptx != null) { block.Add(ptx); } txHashes[9 - h].Remove(txHashes[9 - h].Last()); } } mpool.RemoveForBlock(block, ++blocknum); block.Clear(); if (blocknum == 30) { // At this point we should need to combine 5 buckets to get enough data points // So estimateFee(1,2,3) should fail and estimateFee(4) should return somewhere around // 8*baserate. estimateFee(4) %'s are 100,100,100,100,90 = average 98% Assert.True(mpool.EstimateFee(1) == new FeeRate(0)); Assert.True(mpool.EstimateFee(2) == new FeeRate(0)); Assert.True(mpool.EstimateFee(3) == new FeeRate(0)); Assert.True(mpool.EstimateFee(4).FeePerK < 8 * baseRate.FeePerK + deltaFee); Assert.True(mpool.EstimateFee(4).FeePerK > 8 * baseRate.FeePerK - deltaFee); Assert.True(mpool.EstimateSmartFee(1, out answerFound) == mpool.EstimateFee(4) && answerFound == 4); Assert.True(mpool.EstimateSmartFee(3, out answerFound) == mpool.EstimateFee(4) && answerFound == 4); Assert.True(mpool.EstimateSmartFee(4, out answerFound) == mpool.EstimateFee(4) && answerFound == 4); Assert.True(mpool.EstimateSmartFee(8, out answerFound) == mpool.EstimateFee(8) && answerFound == 8); } } var origFeeEst = new List <Money>(); // Highest feerate is 10*baseRate and gets in all blocks, // second highest feerate is 9*baseRate and gets in 9/10 blocks = 90%, // third highest feerate is 8*base rate, and gets in 8/10 blocks = 80%, // so estimateFee(1) would return 10*baseRate but is hardcoded to return failure // Second highest feerate has 100% chance of being included by 2 blocks, // so estimateFee(2) should return 9*baseRate etc... for (int i = 1; i < 10; i++) { origFeeEst.Add(mpool.EstimateFee(i).FeePerK); if (i > 2) { // Fee estimates should be monotonically decreasing Assert.True(origFeeEst[i - 1] <= origFeeEst[i - 2]); } int mult = 11 - i; if (i > 1) { Assert.True(origFeeEst[i - 1] < mult * baseRate.FeePerK + deltaFee); Assert.True(origFeeEst[i - 1] > mult * baseRate.FeePerK - deltaFee); } else { Assert.True(origFeeEst[i - 1] == new FeeRate(0).FeePerK); } } // Mine 50 more blocks with no transactions happening, estimates shouldn't change // We haven't decayed the moving average enough so we still have enough data points in every bucket while (blocknum < 250) { mpool.RemoveForBlock(block, ++blocknum); } Assert.True(mpool.EstimateFee(1) == new FeeRate(0)); for (int i = 2; i < 10; i++) { Assert.True(mpool.EstimateFee(i).FeePerK < origFeeEst[i - 1] + deltaFee); Assert.True(mpool.EstimateFee(i).FeePerK > origFeeEst[i - 1] - deltaFee); } // Mine 15 more blocks with lots of transactions happening and not getting mined // Estimates should go up while (blocknum < 265) { for (int j = 0; j < 10; j++) { // For each fee multiple for (int k = 0; k < 4; k++) { // add 4 fee txs Transaction tx = txf.Clone(false); tx.Inputs[0].PrevOut.N = (uint)(10000 * blocknum + 100 * j + k); uint256 hash = tx.GetHash(); mpool.AddUnchecked(hash, entry.Fee(feeV[j]).Time(dateTimeSet.GetTime()).Priority(0).Height(blocknum).FromTx(tx, mpool)); txHashes[j].Add(hash); } } mpool.RemoveForBlock(block, ++blocknum); } for (int i = 1; i < 10; i++) { Assert.True(mpool.EstimateFee(i) == new FeeRate(0) || mpool.EstimateFee(i).FeePerK > origFeeEst[i - 1] - deltaFee); Assert.True(mpool.EstimateSmartFee(i, out answerFound).FeePerK > origFeeEst[answerFound - 1] - deltaFee); } // Mine all those transactions // Estimates should still not be below original for (int j = 0; j < 10; j++) { while (txHashes[j].Count > 0) { Transaction ptx = mpool.Get(txHashes[j].Last()); if (ptx != null) { block.Add(ptx); } txHashes[j].Remove(txHashes[j].Last()); } } mpool.RemoveForBlock(block, 265); block.Clear(); Assert.True(mpool.EstimateFee(1) == new FeeRate(0)); for (int i = 2; i < 10; i++) { Assert.True(mpool.EstimateFee(i).FeePerK > origFeeEst[i - 1] - deltaFee); } // Mine 200 more blocks where everything is mined every block // Estimates should be below original estimates while (blocknum < 465) { for (int j = 0; j < 10; j++) { // For each fee multiple for (int k = 0; k < 4; k++) { // add 4 fee txs Transaction tx = txf.Clone(false); tx.Inputs[0].PrevOut.N = (uint)(10000 * blocknum + 100 * j + k); uint256 hash = tx.GetHash(); mpool.AddUnchecked(hash, entry.Fee(feeV[j]).Time(dateTimeSet.GetTime()).Priority(0).Height(blocknum).FromTx(tx, mpool)); Transaction ptx = mpool.Get(hash); if (ptx != null) { block.Add(ptx); } } } mpool.RemoveForBlock(block, ++blocknum); block.Clear(); } Assert.True(mpool.EstimateFee(1) == new FeeRate(0)); for (int i = 2; i < 10; i++) { Assert.True(mpool.EstimateFee(i).FeePerK < origFeeEst[i - 1] - deltaFee); } // Test that if the mempool is limited, estimateSmartFee won't return a value below the mempool min fee // and that estimateSmartPriority returns essentially an infinite value mpool.AddUnchecked(txf.GetHash(), entry.Fee(feeV[5]).Time(dateTimeSet.GetTime()).Priority(0).Height(blocknum).FromTx(txf, mpool)); // evict that transaction which should set a mempool min fee of minRelayTxFee + feeV[5] mpool.TrimToSize(1); Assert.True(mpool.GetMinFee(1).FeePerK > feeV[5]); for (int i = 1; i < 10; i++) { Assert.True(mpool.EstimateSmartFee(i, out answerFound).FeePerK >= mpool.EstimateFee(i).FeePerK); Assert.True(mpool.EstimateSmartFee(i, out answerFound).FeePerK >= mpool.GetMinFee(1).FeePerK); Assert.True(mpool.EstimateSmartPriority(i, out answerFound) == BlockPolicyEstimator.InfPriority); } }
public async Task <IActionResult> LedgerConnection( string command, // getinfo string cryptoCode = null, // getxpub [ModelBinder(typeof(ModelBinders.DerivationSchemeModelBinder))] DerivationStrategyBase derivationScheme = null, int account = 0, // sendtoaddress string destination = null, string amount = null, string feeRate = null, string substractFees = null ) { if (!HttpContext.WebSockets.IsWebSocketRequest) { return(NotFound()); } var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); using (var normalOperationTimeout = new CancellationTokenSource()) using (var signTimeout = new CancellationTokenSource()) { normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30)); var hw = new HardwareWalletService(webSocket); object result = null; try { BTCPayNetwork network = null; if (cryptoCode != null) { network = _NetworkProvider.GetNetwork(cryptoCode); if (network == null) { throw new FormatException("Invalid value for crypto code"); } } BitcoinAddress destinationAddress = null; if (destination != null) { try { destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork); } catch { } if (destinationAddress == null) { throw new FormatException("Invalid value for destination"); } } FeeRate feeRateValue = null; if (feeRate != null) { try { feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1); } catch { } if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero) { throw new FormatException("Invalid value for fee rate"); } } Money amountBTC = null; if (amount != null) { try { amountBTC = Money.Parse(amount); } catch { } if (amountBTC == null || amountBTC <= Money.Zero) { throw new FormatException("Invalid value for amount"); } } bool subsctractFeesValue = false; if (substractFees != null) { try { subsctractFeesValue = bool.Parse(substractFees); } catch { throw new FormatException("Invalid value for subtract fees"); } } if (command == "test") { result = await hw.Test(normalOperationTimeout.Token); } if (command == "getxpub") { var getxpubResult = await hw.GetExtPubKey(network, account, normalOperationTimeout.Token); result = getxpubResult; } if (command == "getinfo") { var strategy = GetDirectDerivationStrategy(derivationScheme); if (strategy == null || await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token) == null) { throw new Exception($"This store is not configured to use this ledger"); } var feeProvider = _feeRateProvider.CreateFeeProvider(network); var recommendedFees = feeProvider.GetFeeRateAsync(); var balance = _walletProvider.GetWallet(network).GetBalance(derivationScheme); result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi }; } if (command == "sendtoaddress") { if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary)) { throw new Exception($"{network.CryptoCode}: not started or fully synched"); } var strategy = GetDirectDerivationStrategy(derivationScheme); var wallet = _walletProvider.GetWallet(network); var change = wallet.GetChangeAddressAsync(derivationScheme); var unspentCoins = await wallet.GetUnspentCoins(derivationScheme); var changeAddress = await change; var send = new[] { (
public Money CalculateOutputAmount(FeeRate feeRate) => CredentialAmount - feeRate.GetFee(OutputVsize);
public async Task <IActionResult> Submit(string cryptoCode) { var network = _btcPayNetworkProvider.GetNetwork <BTCPayNetwork>(cryptoCode); if (network == null) { return(BadRequest(CreatePayjoinError(400, "invalid-network", "Incorrect network"))); } var explorer = _explorerClientProvider.GetExplorerClient(network); if (Request.ContentLength is long length) { if (length > 1_000_000) { return(this.StatusCode(413, CreatePayjoinError(413, "payload-too-large", "The transaction is too big to be processed"))); } } else { return(StatusCode(411, CreatePayjoinError(411, "missing-content-length", "The http header Content-Length should be filled"))); } string rawBody; using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8)) { rawBody = (await reader.ReadToEndAsync()) ?? string.Empty; } Transaction originalTx = null; FeeRate originalFeeRate = null; bool psbtFormat = true; if (!PSBT.TryParse(rawBody, network.NBitcoinNetwork, out var psbt)) { psbtFormat = false; if (!Transaction.TryParse(rawBody, network.NBitcoinNetwork, out var tx)) { return(BadRequest(CreatePayjoinError(400, "invalid-format", "invalid transaction or psbt"))); } originalTx = tx; psbt = PSBT.FromTransaction(tx, network.NBitcoinNetwork); psbt = (await explorer.UpdatePSBTAsync(new UpdatePSBTRequest() { PSBT = psbt })).PSBT; for (int i = 0; i < tx.Inputs.Count; i++) { psbt.Inputs[i].FinalScriptSig = tx.Inputs[i].ScriptSig; psbt.Inputs[i].FinalScriptWitness = tx.Inputs[i].WitScript; } } else { if (!psbt.IsAllFinalized()) { return(BadRequest(CreatePayjoinError(400, "psbt-not-finalized", "The PSBT should be finalized"))); } originalTx = psbt.ExtractTransaction(); } async Task BroadcastNow() { await _explorerClientProvider.GetExplorerClient(network).BroadcastAsync(originalTx); } var sendersInputType = psbt.GetInputsScriptPubKeyType(); if (sendersInputType is null) { return(BadRequest(CreatePayjoinError(400, "unsupported-inputs", "Payjoin only support segwit inputs (of the same type)"))); } if (psbt.CheckSanity() is var errors && errors.Count != 0) { return(BadRequest(CreatePayjoinError(400, "insane-psbt", $"This PSBT is insane ({errors[0]})"))); } if (!psbt.TryGetEstimatedFeeRate(out originalFeeRate)) { return(BadRequest(CreatePayjoinError(400, "need-utxo-information", "You need to provide Witness UTXO information to the PSBT."))); } // This is actually not a mandatory check, but we don't want implementers // to leak global xpubs if (psbt.GlobalXPubs.Any()) { return(BadRequest(CreatePayjoinError(400, "leaking-data", "GlobalXPubs should not be included in the PSBT"))); } if (psbt.Outputs.Any(o => o.HDKeyPaths.Count != 0) || psbt.Inputs.Any(o => o.HDKeyPaths.Count != 0)) { return(BadRequest(CreatePayjoinError(400, "leaking-data", "Keypath information should not be included in the PSBT"))); } if (psbt.Inputs.Any(o => !o.IsFinalized())) { return(BadRequest(CreatePayjoinError(400, "psbt-not-finalized", "The PSBT Should be finalized"))); } //////////// var mempool = await explorer.BroadcastAsync(originalTx, true); if (!mempool.Success) { return(BadRequest(CreatePayjoinError(400, "invalid-transaction", $"Provided transaction isn't mempool eligible {mempool.RPCCodeMessage}"))); } var paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike); bool paidSomething = false; Money due = null; Dictionary <OutPoint, UTXO> selectedUTXOs = new Dictionary <OutPoint, UTXO>(); async Task UnlockUTXOs() { await _payJoinRepository.TryUnlock(selectedUTXOs.Select(o => o.Key).ToArray()); } PSBTOutput originalPaymentOutput = null; BitcoinAddress paymentAddress = null; InvoiceEntity invoice = null; DerivationSchemeSettings derivationSchemeSettings = null; foreach (var output in psbt.Outputs) { var key = output.ScriptPubKey.Hash + "#" + network.CryptoCode.ToUpperInvariant(); invoice = (await _invoiceRepository.GetInvoicesFromAddresses(new[] { key })).FirstOrDefault(); if (invoice is null) { continue; } derivationSchemeSettings = invoice.GetSupportedPaymentMethod <DerivationSchemeSettings>(paymentMethodId) .SingleOrDefault(); if (derivationSchemeSettings is null) { continue; } var receiverInputsType = derivationSchemeSettings.AccountDerivation.ScriptPubKeyType(); if (!PayjoinClient.SupportedFormats.Contains(receiverInputsType)) { //this should never happen, unless the store owner changed the wallet mid way through an invoice return(StatusCode(500, CreatePayjoinError(500, "unavailable", $"This service is unavailable for now"))); } if (sendersInputType != receiverInputsType) { return(StatusCode(503, CreatePayjoinError(503, "out-of-utxos", "We do not have any UTXO available for making a payjoin with the sender's inputs type"))); } var paymentMethod = invoice.GetPaymentMethod(paymentMethodId); var paymentDetails = paymentMethod.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod; if (paymentDetails is null || !paymentDetails.PayjoinEnabled) { continue; } if (invoice.GetAllBitcoinPaymentData().Any()) { return(UnprocessableEntity(CreatePayjoinError(422, "already-paid", $"The invoice this PSBT is paying has already been partially or completely paid"))); } paidSomething = true; due = paymentMethod.Calculate().TotalDue - output.Value; if (due > Money.Zero) { break; } if (!await _payJoinRepository.TryLockInputs(originalTx.Inputs.Select(i => i.PrevOut).ToArray())) { return(BadRequest(CreatePayjoinError(400, "inputs-already-used", "Some of those inputs have already been used to make payjoin transaction"))); } var utxos = (await explorer.GetUTXOsAsync(derivationSchemeSettings.AccountDerivation)) .GetUnspentUTXOs(false); // In case we are paying ourselves, be need to make sure // we can't take spent outpoints. var prevOuts = originalTx.Inputs.Select(o => o.PrevOut).ToHashSet(); utxos = utxos.Where(u => !prevOuts.Contains(u.Outpoint)).ToArray(); Array.Sort(utxos, UTXODeterministicComparer.Instance); foreach (var utxo in await SelectUTXO(network, utxos, output.Value, psbt.Outputs.Where(o => o.Index != output.Index).Select(o => o.Value).ToArray())) { selectedUTXOs.Add(utxo.Outpoint, utxo); } originalPaymentOutput = output; paymentAddress = paymentDetails.GetDepositAddress(network.NBitcoinNetwork); break; } if (!paidSomething) { return(BadRequest(CreatePayjoinError(400, "invoice-not-found", "This transaction does not pay any invoice with payjoin"))); } if (due is null || due > Money.Zero) { return(BadRequest(CreatePayjoinError(400, "invoice-not-fully-paid", "The transaction must pay the whole invoice"))); } if (selectedUTXOs.Count == 0) { await BroadcastNow(); return(StatusCode(503, CreatePayjoinError(503, "out-of-utxos", "We do not have any UTXO available for making a payjoin for now"))); } var originalPaymentValue = originalPaymentOutput.Value; await _broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(1.0), originalTx, network); //check if wallet of store is configured to be hot wallet var extKeyStr = await explorer.GetMetadataAsync <string>( derivationSchemeSettings.AccountDerivation, WellknownMetadataKeys.AccountHDKey); if (extKeyStr == null) { // This should not happen, as we check the existance of private key before creating invoice with payjoin await UnlockUTXOs(); await BroadcastNow(); return(StatusCode(500, CreatePayjoinError(500, "unavailable", $"This service is unavailable for now"))); } Money contributedAmount = Money.Zero; var newTx = originalTx.Clone(); var ourNewOutput = newTx.Outputs[originalPaymentOutput.Index]; HashSet <TxOut> isOurOutput = new HashSet <TxOut>(); isOurOutput.Add(ourNewOutput); foreach (var selectedUTXO in selectedUTXOs.Select(o => o.Value)) { contributedAmount += (Money)selectedUTXO.Value; newTx.Inputs.Add(selectedUTXO.Outpoint); } ourNewOutput.Value += contributedAmount; var minRelayTxFee = this._dashboard.Get(network.CryptoCode).Status.BitcoinStatus?.MinRelayTxFee ?? new FeeRate(1.0m); // Probably receiving some spare change, let's add an output to make // it looks more like a normal transaction if (newTx.Outputs.Count == 1) { var change = await explorer.GetUnusedAsync(derivationSchemeSettings.AccountDerivation, DerivationFeature.Change); var randomChangeAmount = RandomUtils.GetUInt64() % (ulong)contributedAmount.Satoshi; // Randomly round the amount to make the payment output look like a change output var roundMultiple = (ulong)Math.Pow(10, (ulong)Math.Log10(randomChangeAmount)); while (roundMultiple > 1_000UL) { if (RandomUtils.GetUInt32() % 2 == 0) { roundMultiple = roundMultiple / 10; } else { randomChangeAmount = (randomChangeAmount / roundMultiple) * roundMultiple; break; } } var fakeChange = newTx.Outputs.CreateNewTxOut(randomChangeAmount, change.ScriptPubKey); if (fakeChange.IsDust(minRelayTxFee)) { randomChangeAmount = fakeChange.GetDustThreshold(minRelayTxFee); fakeChange.Value = randomChangeAmount; } if (randomChangeAmount < contributedAmount) { ourNewOutput.Value -= fakeChange.Value; newTx.Outputs.Add(fakeChange); isOurOutput.Add(fakeChange); } } var rand = new Random(); Utils.Shuffle(newTx.Inputs, rand); Utils.Shuffle(newTx.Outputs, rand); // Remove old signatures as they are not valid anymore foreach (var input in newTx.Inputs) { input.WitScript = WitScript.Empty; } Money ourFeeContribution = Money.Zero; // We need to adjust the fee to keep a constant fee rate var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder(); txBuilder.AddCoins(psbt.Inputs.Select(i => i.GetSignableCoin())); txBuilder.AddCoins(selectedUTXOs.Select(o => o.Value.AsCoin(derivationSchemeSettings.AccountDerivation))); Money expectedFee = txBuilder.EstimateFees(newTx, originalFeeRate); Money actualFee = newTx.GetFee(txBuilder.FindSpentCoins(newTx)); Money additionalFee = expectedFee - actualFee; if (additionalFee > Money.Zero) { // If the user overpaid, taking fee on our output (useful if sender dump a full UTXO for privacy) for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero && due < Money.Zero; i++) { if (isOurOutput.Contains(newTx.Outputs[i])) { var outputContribution = Money.Min(additionalFee, -due); outputContribution = Money.Min(outputContribution, newTx.Outputs[i].Value - newTx.Outputs[i].GetDustThreshold(minRelayTxFee)); newTx.Outputs[i].Value -= outputContribution; additionalFee -= outputContribution; due += outputContribution; ourFeeContribution += outputContribution; } } // The rest, we take from user's change for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero; i++) { if (!isOurOutput.Contains(newTx.Outputs[i])) { var outputContribution = Money.Min(additionalFee, newTx.Outputs[i].Value); outputContribution = Money.Min(outputContribution, newTx.Outputs[i].Value - newTx.Outputs[i].GetDustThreshold(minRelayTxFee)); newTx.Outputs[i].Value -= outputContribution; additionalFee -= outputContribution; } } if (additionalFee > Money.Zero) { // We could not pay fully the additional fee, however, as long as // we are not under the relay fee, it should be OK. var newVSize = txBuilder.EstimateSize(newTx, true); var newFeePaid = newTx.GetFee(txBuilder.FindSpentCoins(newTx)); if (new FeeRate(newFeePaid, newVSize) < minRelayTxFee) { await UnlockUTXOs(); await BroadcastNow(); return(UnprocessableEntity(CreatePayjoinError(422, "not-enough-money", "Not enough money is sent to pay for the additional payjoin inputs"))); } } } var accountKey = ExtKey.Parse(extKeyStr, network.NBitcoinNetwork); var newPsbt = PSBT.FromTransaction(newTx, network.NBitcoinNetwork); foreach (var selectedUtxo in selectedUTXOs.Select(o => o.Value)) { var signedInput = newPsbt.Inputs.FindIndexedInput(selectedUtxo.Outpoint); var coin = selectedUtxo.AsCoin(derivationSchemeSettings.AccountDerivation); signedInput.UpdateFromCoin(coin); var privateKey = accountKey.Derive(selectedUtxo.KeyPath).PrivateKey; signedInput.Sign(privateKey); signedInput.FinalizeInput(); newTx.Inputs[signedInput.Index].WitScript = newPsbt.Inputs[(int)signedInput.Index].FinalScriptWitness; } // Add the transaction to the payments with a confirmation of -1. // This will make the invoice paid even if the user do not // broadcast the payjoin. var originalPaymentData = new BitcoinLikePaymentData(paymentAddress, originalPaymentOutput.Value, new OutPoint(originalTx.GetHash(), originalPaymentOutput.Index), originalTx.RBF); originalPaymentData.ConfirmationCount = -1; originalPaymentData.PayjoinInformation = new PayjoinInformation() { CoinjoinTransactionHash = newPsbt.GetGlobalTransaction().GetHash(), CoinjoinValue = originalPaymentValue - ourFeeContribution, ContributedOutPoints = selectedUTXOs.Select(o => o.Key).ToArray() }; var payment = await _invoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, originalPaymentData, network, true); if (payment is null) { await UnlockUTXOs(); await BroadcastNow(); return(UnprocessableEntity(CreatePayjoinError(422, "already-paid", $"The original transaction has already been accounted"))); } await _btcPayWalletProvider.GetWallet(network).SaveOffchainTransactionAsync(originalTx); _eventAggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment) { Payment = payment }); if (psbtFormat && HexEncoder.IsWellFormed(rawBody)) { return(Ok(newPsbt.ToHex())); } else if (psbtFormat) { return(Ok(newPsbt.ToBase64())); } else { return(Ok(newTx.ToHex())); } }
public async Task <IActionResult> LedgerConnection( string storeId, string command, // getinfo string cryptoCode = null, // sendtoaddress string destination = null, string amount = null, string feeRate = null, string substractFees = null ) { if (!HttpContext.WebSockets.IsWebSocketRequest) { return(NotFound()); } var store = await _Repo.FindStore(storeId, GetUserId()); if (store == null) { return(NotFound()); } var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); var hw = new HardwareWalletService(webSocket); object result = null; try { BTCPayNetwork network = null; if (cryptoCode != null) { network = _NetworkProvider.GetNetwork(cryptoCode); if (network == null) { throw new FormatException("Invalid value for crypto code"); } } BitcoinAddress destinationAddress = null; if (destination != null) { try { destinationAddress = BitcoinAddress.Create(destination); } catch { } if (destinationAddress == null) { throw new FormatException("Invalid value for destination"); } } FeeRate feeRateValue = null; if (feeRate != null) { try { feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate)), 1); } catch { } if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero) { throw new FormatException("Invalid value for fee rate"); } } Money amountBTC = null; if (amount != null) { try { amountBTC = Money.Parse(amount); } catch { } if (amountBTC == null || amountBTC <= Money.Zero) { throw new FormatException("Invalid value for amount"); } } bool subsctractFeesValue = false; if (substractFees != null) { try { subsctractFeesValue = bool.Parse(substractFees); } catch { throw new FormatException("Invalid value for substract fees"); } } if (command == "test") { result = await hw.Test(); } if (command == "getxpub") { result = await hw.GetExtPubKey(network); } if (command == "getinfo") { var strategy = GetDirectDerivationStrategy(store, network); var strategyBase = GetDerivationStrategy(store, network); if (!await hw.SupportDerivation(network, strategy)) { throw new Exception($"This store is not configured to use this ledger"); } var feeProvider = _FeeRateProvider.CreateFeeProvider(network); var recommendedFees = feeProvider.GetFeeRateAsync(); var balance = _WalletProvider.GetWallet(network).GetBalance(strategyBase); result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi }; } if (command == "sendtoaddress") { var strategy = GetDirectDerivationStrategy(store, network); var strategyBase = GetDerivationStrategy(store, network); var wallet = _WalletProvider.GetWallet(network); var change = wallet.GetChangeAddressAsync(strategyBase); var unspentCoins = await wallet.GetUnspentCoins(strategyBase); var changeAddress = await change; unspentCoins.Item2.TryAdd(changeAddress.Item1.ScriptPubKey, changeAddress.Item2); var transaction = await hw.SendToAddress(strategy, unspentCoins.Item1, network, new[] { (destinationAddress as IDestination, amountBTC, subsctractFeesValue) },
public bool SetTxFee(FeeRate feeRate) { return SendCommand(RPCOperations.settxfee, new[] { feeRate.FeePerK.ToString() }).Result.ToString() == "true"; }
public void CanEstimateFees() { var alice = new Key(); var bob = new Key(); var satoshi = new Key(); var bobAlice = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(2, alice.PubKey, bob.PubKey); //Alice sends money to bobAlice //Bob sends money to bobAlice //bobAlice sends money to satoshi var aliceCoins = new ICoin[] { RandomCoin("0.4", alice), RandomCoin("0.6", alice) }; var bobCoins = new ICoin[] { RandomCoin("0.2", bob), RandomCoin("0.3", bob) }; var bobAliceCoins = new ICoin[] { RandomCoin("1.5", bobAlice, false), RandomCoin("0.25", bobAlice, true) }; TransactionBuilder builder = new TransactionBuilder(); builder.StandardTransactionPolicy = EasyPolicy; var unsigned = builder .AddCoins(aliceCoins) .Send(bobAlice, "1.0") .Then() .AddCoins(bobCoins) .Send(bobAlice, "0.5") .Then() .AddCoins(bobAliceCoins) .Send(satoshi.PubKey, "1.74") .SetChange(bobAlice) .BuildTransaction(false); builder.AddKeys(alice, bob, satoshi); var signed = builder.BuildTransaction(true); Assert.True(builder.Verify(signed)); Assert.True(Math.Abs(signed.ToBytes().Length - builder.EstimateSize(unsigned)) < 20); var rate = new FeeRate(Money.Coins(0.0004m)); var estimatedFees = builder.EstimateFees(unsigned, rate); builder.SendEstimatedFees(rate); signed = builder.BuildTransaction(true); Assert.True(builder.Verify(signed, estimatedFees)); }
private FeeStrategy(FeeStrategyType type, int?confirmationTarget, FeeRate feeRate) { Type = type; if (type == FeeStrategyType.Rate) { if (confirmationTarget is { })
public bool SetTxFee(FeeRate feeRate) { return(SendCommand(RPCOperations.settxfee, new[] { feeRate.FeePerK.ToString() }).Result.ToString() == "true"); }
/// <exception cref="ArgumentException"></exception> /// <exception cref="ArgumentNullException"></exception> /// <exception cref="ArgumentOutOfRangeException"></exception> public BuildTransactionResult BuildTransaction( PaymentIntent payments, FeeRate feeRate, IEnumerable <OutPoint> allowedInputs = null, IPayjoinClient payjoinClient = null) => BuildTransaction(payments, () => feeRate, allowedInputs, () => LockTime.Zero, payjoinClient);
/// <inheritdoc /> public void Start() { this.asyncLoop = this.asyncLoopFactory.Run(nameof(LightWalletBitcoinExternalFeePolicy), async token => { // This will run evry 3 to 10 minutes randomly // So the API provider is not able to identify our transaction with a timing attack int waitMinutes = new Random().Next(3, 10); HttpResponseMessage response = null; try { // TestNet fee estimation is useless, because the miners don't select transactions, like mainnet miners // Test results on RegTest and TestNet are more illustrative with mainnet fees response = await HttpClient.GetAsync(@"http://api.blockcypher.com/v1/btc/main", HttpCompletionOption.ResponseContentRead, token) .ConfigureAwait(false); } catch (OperationCanceledException) { return; } catch (Exception ex) when(ex is HttpRequestException || ex is TimeoutException) { response = null; } if (response == null || !response.IsSuccessStatusCode) { // If it's already been initialized once just keep using the feerate we already have if (this.initializedOnce) { await Task.Delay(TimeSpan.FromMinutes(waitMinutes), token).ContinueWith(t => { }).ConfigureAwait(false); return; } else // Try again 3 seconds later, first time fee query is critical { await Task.Delay(TimeSpan.FromSeconds(3), token).ContinueWith(t => { }).ConfigureAwait(false); return; } } if (token.IsCancellationRequested) { return; } JObject json = JObject.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false)); this.lowTxFeePerKb = new FeeRate(new Money((int)(json.Value <decimal>("low_fee_per_kb")), MoneyUnit.Satoshi)); this.mediumTxFeePerKb = new FeeRate(new Money((int)(json.Value <decimal>("medium_fee_per_kb")), MoneyUnit.Satoshi)); this.highTxFeePerKb = new FeeRate(new Money((int)(json.Value <decimal>("high_fee_per_kb")), MoneyUnit.Satoshi)); this.initializedOnce = true; if (token.IsCancellationRequested) { return; } await Task.Delay(TimeSpan.FromMinutes(waitMinutes), token).ContinueWith(t => { }).ConfigureAwait(false); }, this.nodeLifetime.ApplicationStopping, repeatEvery: TimeSpans.Second, startAfter: TimeSpans.Second); }
public async Task <IActionResult> Submit(string cryptoCode, long?maxadditionalfeecontribution, int?additionalfeeoutputindex, decimal minfeerate = -1.0m, bool disableoutputsubstitution = false, int v = 1) { var network = _btcPayNetworkProvider.GetNetwork <BTCPayNetwork>(cryptoCode); if (network == null) { return(NotFound()); } if (v != 1) { return(BadRequest(new JObject { new JProperty("errorCode", "version-unsupported"), new JProperty("supported", new JArray(1)), new JProperty("message", "This version of payjoin is not supported.") })); } await using var ctx = new PayjoinReceiverContext(_invoiceRepository, _explorerClientProvider.GetExplorerClient(network), _payJoinRepository); ObjectResult CreatePayjoinErrorAndLog(int httpCode, PayjoinReceiverWellknownErrors err, string debug) { ctx.Logs.Write($"Payjoin error: {debug}"); return(StatusCode(httpCode, CreatePayjoinError(err, debug))); } var explorer = _explorerClientProvider.GetExplorerClient(network); if (Request.ContentLength is long length) { if (length > 1_000_000) { return(this.StatusCode(413, CreatePayjoinError("payload-too-large", "The transaction is too big to be processed"))); } } else { return(StatusCode(411, CreatePayjoinError("missing-content-length", "The http header Content-Length should be filled"))); } string rawBody; using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8)) { rawBody = (await reader.ReadToEndAsync()) ?? string.Empty; } FeeRate originalFeeRate = null; bool psbtFormat = true; if (PSBT.TryParse(rawBody, network.NBitcoinNetwork, out var psbt)) { if (!psbt.IsAllFinalized()) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", "The PSBT should be finalized"))); } ctx.OriginalTransaction = psbt.ExtractTransaction(); } // BTCPay Server implementation support a transaction instead of PSBT else { psbtFormat = false; if (!Transaction.TryParse(rawBody, network.NBitcoinNetwork, out var tx)) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", "invalid transaction or psbt"))); } ctx.OriginalTransaction = tx; psbt = PSBT.FromTransaction(tx, network.NBitcoinNetwork); psbt = (await explorer.UpdatePSBTAsync(new UpdatePSBTRequest() { PSBT = psbt })).PSBT; for (int i = 0; i < tx.Inputs.Count; i++) { psbt.Inputs[i].FinalScriptSig = tx.Inputs[i].ScriptSig; psbt.Inputs[i].FinalScriptWitness = tx.Inputs[i].WitScript; } } FeeRate senderMinFeeRate = minfeerate >= 0.0m ? new FeeRate(minfeerate) : null; Money allowedSenderFeeContribution = Money.Satoshis(maxadditionalfeecontribution is long t && t >= 0 ? t : 0); var sendersInputType = psbt.GetInputsScriptPubKeyType(); if (psbt.CheckSanity() is var errors && errors.Count != 0) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", $"This PSBT is insane ({errors[0]})"))); } if (!psbt.TryGetEstimatedFeeRate(out originalFeeRate)) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", "You need to provide Witness UTXO information to the PSBT."))); } // This is actually not a mandatory check, but we don't want implementers // to leak global xpubs if (psbt.GlobalXPubs.Any()) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", "GlobalXPubs should not be included in the PSBT"))); } if (psbt.Outputs.Any(o => o.HDKeyPaths.Count != 0) || psbt.Inputs.Any(o => o.HDKeyPaths.Count != 0)) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", "Keypath information should not be included in the PSBT"))); } if (psbt.Inputs.Any(o => !o.IsFinalized())) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", "The PSBT Should be finalized"))); } //////////// var mempool = await explorer.BroadcastAsync(ctx.OriginalTransaction, true); if (!mempool.Success) { ctx.DoNotBroadcast(); return(BadRequest(CreatePayjoinError("original-psbt-rejected", $"Provided transaction isn't mempool eligible {mempool.RPCCodeMessage}"))); } var enforcedLowR = ctx.OriginalTransaction.Inputs.All(IsLowR); var paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike); bool paidSomething = false; Money due = null; Dictionary <OutPoint, UTXO> selectedUTXOs = new Dictionary <OutPoint, UTXO>(); PSBTOutput originalPaymentOutput = null; BitcoinAddress paymentAddress = null; InvoiceEntity invoice = null; DerivationSchemeSettings derivationSchemeSettings = null; foreach (var output in psbt.Outputs) { var key = output.ScriptPubKey.Hash + "#" + network.CryptoCode.ToUpperInvariant(); invoice = (await _invoiceRepository.GetInvoicesFromAddresses(new[] { key })).FirstOrDefault(); if (invoice is null) { continue; } derivationSchemeSettings = invoice.GetSupportedPaymentMethod <DerivationSchemeSettings>(paymentMethodId) .SingleOrDefault(); if (derivationSchemeSettings is null) { continue; } var receiverInputsType = derivationSchemeSettings.AccountDerivation.ScriptPubKeyType(); if (!PayjoinClient.SupportedFormats.Contains(receiverInputsType)) { //this should never happen, unless the store owner changed the wallet mid way through an invoice return(CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "Our wallet does not support payjoin")); } if (sendersInputType is ScriptPubKeyType t1 && t1 != receiverInputsType) { return(CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "We do not have any UTXO available for making a payjoin with the sender's inputs type")); } var paymentMethod = invoice.GetPaymentMethod(paymentMethodId); var paymentDetails = paymentMethod.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod; if (paymentDetails is null || !paymentDetails.PayjoinEnabled) { continue; } if (invoice.GetAllBitcoinPaymentData().Any()) { ctx.DoNotBroadcast(); return(UnprocessableEntity(CreatePayjoinError("already-paid", $"The invoice this PSBT is paying has already been partially or completely paid"))); } paidSomething = true; due = paymentMethod.Calculate().TotalDue - output.Value; if (due > Money.Zero) { break; } if (!await _payJoinRepository.TryLockInputs(ctx.OriginalTransaction.Inputs.Select(i => i.PrevOut).ToArray())) { return(CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "Some of those inputs have already been used to make another payjoin transaction")); } var utxos = (await explorer.GetUTXOsAsync(derivationSchemeSettings.AccountDerivation)) .GetUnspentUTXOs(false); // In case we are paying ourselves, be need to make sure // we can't take spent outpoints. var prevOuts = ctx.OriginalTransaction.Inputs.Select(o => o.PrevOut).ToHashSet(); utxos = utxos.Where(u => !prevOuts.Contains(u.Outpoint)).ToArray(); Array.Sort(utxos, UTXODeterministicComparer.Instance); foreach (var utxo in (await SelectUTXO(network, utxos, psbt.Inputs.Select(input => input.WitnessUtxo.Value.ToDecimal(MoneyUnit.BTC)), output.Value.ToDecimal(MoneyUnit.BTC), psbt.Outputs.Where(psbtOutput => psbtOutput.Index != output.Index).Select(psbtOutput => psbtOutput.Value.ToDecimal(MoneyUnit.BTC)))).selectedUTXO) { selectedUTXOs.Add(utxo.Outpoint, utxo); } ctx.LockedUTXOs = selectedUTXOs.Select(u => u.Key).ToArray(); originalPaymentOutput = output; paymentAddress = paymentDetails.GetDepositAddress(network.NBitcoinNetwork); break; } if (!paidSomething) { return(BadRequest(CreatePayjoinError("invoice-not-found", "This transaction does not pay any invoice with payjoin"))); } if (due is null || due > Money.Zero) { return(BadRequest(CreatePayjoinError("invoice-not-fully-paid", "The transaction must pay the whole invoice"))); } if (selectedUTXOs.Count == 0) { return(CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "We do not have any UTXO available for contributing to a payjoin")); } var originalPaymentValue = originalPaymentOutput.Value; await _broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(2.0), ctx.OriginalTransaction, network); //check if wallet of store is configured to be hot wallet var extKeyStr = await explorer.GetMetadataAsync <string>( derivationSchemeSettings.AccountDerivation, WellknownMetadataKeys.AccountHDKey); if (extKeyStr == null) { // This should not happen, as we check the existance of private key before creating invoice with payjoin return(CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "The HD Key of the store changed")); } Money contributedAmount = Money.Zero; var newTx = ctx.OriginalTransaction.Clone(); var ourNewOutput = newTx.Outputs[originalPaymentOutput.Index]; HashSet <TxOut> isOurOutput = new HashSet <TxOut>(); isOurOutput.Add(ourNewOutput); TxOut feeOutput = additionalfeeoutputindex is int feeOutputIndex && maxadditionalfeecontribution is long v3 && v3 >= 0 && feeOutputIndex >= 0 && feeOutputIndex < newTx.Outputs.Count && !isOurOutput.Contains(newTx.Outputs[feeOutputIndex]) ? newTx.Outputs[feeOutputIndex] : null; var rand = new Random(); int senderInputCount = newTx.Inputs.Count; foreach (var selectedUTXO in selectedUTXOs.Select(o => o.Value)) { contributedAmount += (Money)selectedUTXO.Value; var newInput = newTx.Inputs.Add(selectedUTXO.Outpoint); newInput.Sequence = newTx.Inputs[rand.Next(0, senderInputCount)].Sequence; } ourNewOutput.Value += contributedAmount; var minRelayTxFee = this._dashboard.Get(network.CryptoCode).Status.BitcoinStatus?.MinRelayTxFee ?? new FeeRate(1.0m); // Remove old signatures as they are not valid anymore foreach (var input in newTx.Inputs) { input.WitScript = WitScript.Empty; } Money ourFeeContribution = Money.Zero; // We need to adjust the fee to keep a constant fee rate var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder(); var coins = psbt.Inputs.Select(i => i.GetSignableCoin()) .Concat(selectedUTXOs.Select(o => o.Value.AsCoin(derivationSchemeSettings.AccountDerivation))).ToArray(); txBuilder.AddCoins(coins); Money expectedFee = txBuilder.EstimateFees(newTx, originalFeeRate); Money actualFee = newTx.GetFee(txBuilder.FindSpentCoins(newTx)); Money additionalFee = expectedFee - actualFee; if (additionalFee > Money.Zero) { // If the user overpaid, taking fee on our output (useful if sender dump a full UTXO for privacy) for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero && due < Money.Zero; i++) { if (disableoutputsubstitution) { break; } if (isOurOutput.Contains(newTx.Outputs[i])) { var outputContribution = Money.Min(additionalFee, -due); outputContribution = Money.Min(outputContribution, newTx.Outputs[i].Value - newTx.Outputs[i].GetDustThreshold(minRelayTxFee)); newTx.Outputs[i].Value -= outputContribution; additionalFee -= outputContribution; due += outputContribution; ourFeeContribution += outputContribution; } } // The rest, we take from user's change if (feeOutput != null) { var outputContribution = Money.Min(additionalFee, feeOutput.Value); outputContribution = Money.Min(outputContribution, feeOutput.Value - feeOutput.GetDustThreshold(minRelayTxFee)); outputContribution = Money.Min(outputContribution, allowedSenderFeeContribution); feeOutput.Value -= outputContribution; additionalFee -= outputContribution; allowedSenderFeeContribution -= outputContribution; } if (additionalFee > Money.Zero) { // We could not pay fully the additional fee, however, as long as // we are not under the relay fee, it should be OK. var newVSize = txBuilder.EstimateSize(newTx, true); var newFeePaid = newTx.GetFee(txBuilder.FindSpentCoins(newTx)); if (new FeeRate(newFeePaid, newVSize) < (senderMinFeeRate ?? minRelayTxFee)) { return(CreatePayjoinErrorAndLog(422, PayjoinReceiverWellknownErrors.NotEnoughMoney, "Not enough money is sent to pay for the additional payjoin inputs")); } } } var accountKey = ExtKey.Parse(extKeyStr, network.NBitcoinNetwork); var newPsbt = PSBT.FromTransaction(newTx, network.NBitcoinNetwork); foreach (var selectedUtxo in selectedUTXOs.Select(o => o.Value)) { var signedInput = newPsbt.Inputs.FindIndexedInput(selectedUtxo.Outpoint); var coin = selectedUtxo.AsCoin(derivationSchemeSettings.AccountDerivation); signedInput.UpdateFromCoin(coin); var privateKey = accountKey.Derive(selectedUtxo.KeyPath).PrivateKey; signedInput.Sign(privateKey, new SigningOptions() { EnforceLowR = enforcedLowR }); signedInput.FinalizeInput(); newTx.Inputs[signedInput.Index].WitScript = newPsbt.Inputs[(int)signedInput.Index].FinalScriptWitness; } // Add the transaction to the payments with a confirmation of -1. // This will make the invoice paid even if the user do not // broadcast the payjoin. var originalPaymentData = new BitcoinLikePaymentData(paymentAddress, originalPaymentOutput.Value, new OutPoint(ctx.OriginalTransaction.GetHash(), originalPaymentOutput.Index), ctx.OriginalTransaction.RBF); originalPaymentData.ConfirmationCount = -1; originalPaymentData.PayjoinInformation = new PayjoinInformation() { CoinjoinTransactionHash = GetExpectedHash(newPsbt, coins), CoinjoinValue = originalPaymentValue - ourFeeContribution, ContributedOutPoints = selectedUTXOs.Select(o => o.Key).ToArray() }; var payment = await _invoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, originalPaymentData, network, true); if (payment is null) { return(UnprocessableEntity(CreatePayjoinError("already-paid", $"The original transaction has already been accounted"))); } await _btcPayWalletProvider.GetWallet(network).SaveOffchainTransactionAsync(ctx.OriginalTransaction); _eventAggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment) { Payment = payment }); _eventAggregator.Publish(new UpdateTransactionLabel() { WalletId = new WalletId(invoice.StoreId, network.CryptoCode), TransactionLabels = selectedUTXOs.GroupBy(pair => pair.Key.Hash).Select(utxo => new KeyValuePair <uint256, List <(string color, string label)> >(utxo.Key, new List <(string color, string label)>() { UpdateTransactionLabel.PayjoinExposedLabelTemplate(invoice.Id) })) .ToDictionary(pair => pair.Key, pair => pair.Value) });
public void CanSplitFees() { var satoshi = new Key(); var alice = new Key(); var bob = new Key(); var aliceCoins = new ICoin[] { RandomCoin("0.4", alice), RandomCoin("0.6", alice) }; var bobCoins = new ICoin[] { RandomCoin("0.2", bob), RandomCoin("0.3", bob) }; TransactionBuilder builder = new TransactionBuilder(); FeeRate rate = new FeeRate(Money.Coins(0.0004m)); var tx = builder .AddCoins(aliceCoins) .AddKeys(alice) .Send(satoshi, Money.Coins(0.1m)) .SetChange(alice) .Then() .AddCoins(bobCoins) .AddKeys(bob) .Send(satoshi, Money.Coins(0.01m)) .SetChange(bob) .SendEstimatedFeesSplit(rate) .BuildTransaction(true); var estimated = builder.EstimateFees(tx, rate); Assert.True(builder.Verify(tx, estimated)); }
public Money CalculateRemainingAmountCredentials(FeeRate feeRate) => TotalInputAmount - feeRate.GetFee(TotalInputVsize);
public void CanEstimatedFeesCorrectlyIfFeesChangeTransactionSize() { var redeem = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(2, new Key().PubKey, new Key().PubKey, new Key().PubKey); var transactionBuilder = new TransactionBuilder(); transactionBuilder.AddCoins(new Coin(new OutPoint(uint256.Parse("75425c904289f21feef0cffab2081ba22030b633623115adf0780edad443e6c7"), 1), new TxOut("0.00010000", PayToScriptHashTemplate.Instance.GenerateScriptPubKey(redeem).GetDestinationAddress(Network.Main))).ToScriptCoin(redeem)); transactionBuilder.AddCoins(new Coin(new OutPoint(uint256.Parse("75425c904289f21feef0cffab2081ba22030b633623115adf0780edad443e6c7"), 2), new TxOut("0.00091824", PayToScriptHashTemplate.Instance.GenerateScriptPubKey(redeem).GetDestinationAddress(Network.Main))).ToScriptCoin(redeem)); transactionBuilder.AddCoins(new Coin(new OutPoint(uint256.Parse("75425c904289f21feef0cffab2081ba22030b633623115adf0780edad443e6c7"), 3), new TxOut("0.00100000", PayToScriptHashTemplate.Instance.GenerateScriptPubKey(redeem).GetDestinationAddress(Network.Main))).ToScriptCoin(redeem)); transactionBuilder.AddCoins(new Coin(new OutPoint(uint256.Parse("75425c904289f21feef0cffab2081ba22030b633623115adf0780edad443e6c7"), 4), new TxOut("0.00100000", PayToScriptHashTemplate.Instance.GenerateScriptPubKey(redeem).GetDestinationAddress(Network.Main))).ToScriptCoin(redeem)); transactionBuilder.AddCoins(new Coin(new OutPoint(uint256.Parse("75425c904289f21feef0cffab2081ba22030b633623115adf0780edad443e6c7"), 5), new TxOut("0.00246414", PayToScriptHashTemplate.Instance.GenerateScriptPubKey(redeem).GetDestinationAddress(Network.Main))).ToScriptCoin(redeem)); transactionBuilder.AddCoins(new Coin(new OutPoint(uint256.Parse("75425c904289f21feef0cffab2081ba22030b633623115adf0780edad443e6c7"), 6), new TxOut("0.00250980", PayToScriptHashTemplate.Instance.GenerateScriptPubKey(redeem).GetDestinationAddress(Network.Main))).ToScriptCoin(redeem)); transactionBuilder.AddCoins(new Coin(new OutPoint(uint256.Parse("75425c904289f21feef0cffab2081ba22030b633623115adf0780edad443e6c7"), 7), new TxOut("0.01000000", PayToScriptHashTemplate.Instance.GenerateScriptPubKey(redeem).GetDestinationAddress(Network.Main))).ToScriptCoin(redeem)); transactionBuilder.Send(new Key().PubKey.GetAddress(Network.Main), "0.01000000"); transactionBuilder.SetChange(new Key().PubKey.GetAddress(Network.Main)); var feeRate = new FeeRate((long)32563); //Adding the estimated fees will cause 6 more coins to be included, so let's verify the actual sent fees take that into account transactionBuilder.SendEstimatedFees(feeRate); var tx = transactionBuilder.BuildTransaction(false); var estimation = transactionBuilder.EstimateFees(tx, feeRate); Assert.Equal(estimation, tx.GetFee(transactionBuilder.FindSpentCoins(tx))); }
public Transaction FundTransaction(TxOut txOut, FeeRate feeRate) { return(null); }
public void CanBuildColoredTransaction() { var gold = new Key(); var silver = new Key(); var goldId = gold.PubKey.ScriptPubKey.Hash.ToAssetId(); var silverId = silver.PubKey.ScriptPubKey.Hash.ToAssetId(); var satoshi = new Key(); var bob = new Key(); var alice = new Key(); var repo = new NoSqlColoredTransactionRepository(); var init = new Transaction() { Outputs = { new TxOut("1.0", gold.PubKey), new TxOut("1.0", silver.PubKey), new TxOut("1.0", satoshi.PubKey) } }; repo.Transactions.Put(init); var issuanceCoins = init .Outputs .AsCoins() .Take(2) .Select((c, i) => new IssuanceCoin(c)) .OfType<ICoin>().ToArray(); var satoshiBTC = init.Outputs.AsCoins().Last(); var coins = new List<ICoin>(); coins.AddRange(issuanceCoins); var txBuilder = new TransactionBuilder(); txBuilder.StandardTransactionPolicy = RelayPolicy; //Can issue gold to satoshi and bob var tx = txBuilder .AddCoins(coins.ToArray()) .AddKeys(gold) .IssueAsset(satoshi.PubKey, new AssetMoney(goldId, 1000)) .IssueAsset(bob.PubKey, new AssetMoney(goldId, 500)) .SendFees("0.1") .SetChange(gold.PubKey) .BuildTransaction(true); Assert.True(txBuilder.Verify(tx, "0.1")); //Ensure BTC from the IssuanceCoin are returned Assert.Equal(Money.Parse("0.89994240"), tx.Outputs[2].Value); Assert.Equal(gold.PubKey.ScriptPubKey, tx.Outputs[2].ScriptPubKey); repo.Transactions.Put(tx); var colored = tx.GetColoredTransaction(repo); Assert.Equal(2, colored.Issuances.Count); Assert.True(colored.Issuances.All(i => i.Asset.Id == goldId)); AssertHasAsset(tx, colored, colored.Issuances[0], goldId, 500, bob.PubKey); AssertHasAsset(tx, colored, colored.Issuances[1], goldId, 1000, satoshi.PubKey); var coloredCoins = ColoredCoin.Find(tx, colored).ToArray(); Assert.Equal(2, coloredCoins.Length); //Can issue silver to bob, and send some gold to satoshi coins.Add(coloredCoins.First(c => c.ScriptPubKey == bob.PubKey.ScriptPubKey)); txBuilder = new TransactionBuilder(); txBuilder.StandardTransactionPolicy = EasyPolicy; tx = txBuilder .AddCoins(coins.ToArray()) .AddKeys(silver, bob) .SetChange(bob.PubKey) .IssueAsset(bob.PubKey, new AssetMoney(silverId, 10)) .SendAsset(satoshi.PubKey, new AssetMoney(goldId, 30)) .BuildTransaction(true); Assert.True(txBuilder.Verify(tx)); colored = tx.GetColoredTransaction(repo); Assert.Equal(1, colored.Inputs.Count); Assert.Equal(goldId, colored.Inputs[0].Asset.Id); Assert.Equal(500, colored.Inputs[0].Asset.Quantity); Assert.Equal(1, colored.Issuances.Count); Assert.Equal(2, colored.Transfers.Count); AssertHasAsset(tx, colored, colored.Transfers[0], goldId, 470, bob.PubKey); AssertHasAsset(tx, colored, colored.Transfers[1], goldId, 30, satoshi.PubKey); repo.Transactions.Put(tx); //Can swap : //satoshi wants to send 100 gold to bob //bob wants to send 200 silver, 5 gold and 0.9 BTC to satoshi //Satoshi receive gold txBuilder = new TransactionBuilder(); txBuilder.StandardTransactionPolicy = RelayPolicy; tx = txBuilder .AddKeys(gold) .AddCoins(issuanceCoins) .IssueAsset(satoshi.PubKey, new AssetMoney(goldId, 1000UL)) .SetChange(gold.PubKey) .SendFees(Money.Coins(0.0004m)) .BuildTransaction(true); Assert.True(txBuilder.Verify(tx)); repo.Transactions.Put(tx); var satoshiCoin = ColoredCoin.Find(tx, repo).First(); //Gold receive 2.5 BTC tx = new Transaction() { Outputs = { new TxOut("2.5",gold.PubKey) } }; repo.Transactions.Put(tx.GetHash(), tx); //Bob receive silver and 2 btc txBuilder = new TransactionBuilder(); txBuilder.StandardTransactionPolicy = RelayPolicy; tx = txBuilder .AddKeys(silver, gold) .AddCoins(issuanceCoins) .AddCoins(new Coin(new OutPoint(tx.GetHash(), 0), new TxOut("2.5", gold.PubKey.ScriptPubKey))) .IssueAsset(bob.PubKey, new AssetMoney(silverId, 300UL)) .Send(bob.PubKey, "2.00") .SendFees(Money.Coins(0.0004m)) .SetChange(gold.PubKey) .BuildTransaction(true); Assert.True(txBuilder.Verify(tx)); repo.Transactions.Put(tx); var bobSilverCoin = ColoredCoin.Find(tx, repo).First(); var bobBitcoin = new Coin(new OutPoint(tx.GetHash(), 2), tx.Outputs[2]); //Bob receive gold txBuilder = new TransactionBuilder(); txBuilder.StandardTransactionPolicy = RelayPolicy; tx = txBuilder .AddKeys(gold) .AddCoins(issuanceCoins) .IssueAsset(bob.PubKey, new AssetMoney(goldId, 50UL)) .SetChange(gold.PubKey) .SendFees(Money.Coins(0.0004m)) .BuildTransaction(true); Assert.True(txBuilder.Verify(tx)); repo.Transactions.Put(tx.GetHash(), tx); var bobGoldCoin = ColoredCoin.Find(tx, repo).First(); txBuilder = new TransactionBuilder(); txBuilder.StandardTransactionPolicy = RelayPolicy; tx = txBuilder .AddCoins(satoshiCoin) .AddCoins(satoshiBTC) .SendAsset(bob.PubKey, new AssetMoney(goldId, 100)) .SendFees(Money.Coins(0.0004m)) .SetChange(satoshi.PubKey) .Then() .AddCoins(bobSilverCoin, bobGoldCoin, bobBitcoin) .SendAsset(satoshi.PubKey, new AssetMoney(silverId, 200)) .Send(satoshi.PubKey, "0.9") .SendAsset(satoshi.PubKey, new AssetMoney(goldId, 5)) .SetChange(bob.PubKey) .BuildTransaction(false); colored = tx.GetColoredTransaction(repo); AssertHasAsset(tx, colored, colored.Inputs[0], goldId, 1000, null); AssertHasAsset(tx, colored, colored.Inputs[1], silverId, 300, null); AssertHasAsset(tx, colored, colored.Transfers[0], goldId, 900, satoshi.PubKey); AssertHasAsset(tx, colored, colored.Transfers[1], goldId, 100, bob.PubKey); AssertHasAsset(tx, colored, colored.Transfers[2], silverId, 100, bob.PubKey); AssertHasAsset(tx, colored, colored.Transfers[3], silverId, 200, satoshi.PubKey); AssertHasAsset(tx, colored, colored.Transfers[4], goldId, 45, bob.PubKey); AssertHasAsset(tx, colored, colored.Transfers[5], goldId, 5, satoshi.PubKey); Assert.True(tx.Outputs[8].Value == Money.Parse("1.0999424")); Assert.True(tx.Outputs[8].ScriptPubKey == bob.PubKey.ScriptPubKey); Assert.True(tx.Outputs[9].Value == Money.Parse("0.9")); Assert.True(tx.Outputs[9].ScriptPubKey == satoshi.PubKey.ScriptPubKey); tx = txBuilder.AddKeys(satoshi, bob).SignTransaction(tx); Assert.True(txBuilder.Verify(tx)); //Bob send coins to Satoshi, but alice pay for the dust var funding = new TransactionBuilder() { StandardTransactionPolicy = RelayPolicy } .AddCoins(issuanceCoins) .AddKeys(gold) .IssueAsset(bob.PubKey.Hash, new AssetMoney(goldId, 100UL)) .SetChange(gold.PubKey.Hash) .SendFees(Money.Coins(0.0004m)) .BuildTransaction(true); repo.Transactions.Put(funding); var bobGold = ColoredCoin.Find(funding, repo).ToArray(); Transaction transfer = null; try { transfer = new TransactionBuilder() { StandardTransactionPolicy = RelayPolicy } .AddCoins(bobGold) .SendAsset(alice.PubKey.Hash, new AssetMoney(goldId, 40UL)) .SetChange(bob.PubKey.Hash) .BuildTransaction(true); Assert.False(true, "Should have thrown"); } catch(NotEnoughFundsException ex) //Not enough dust to send the change { Assert.True(((Money)ex.Missing).Satoshi == 2730); var rate = new FeeRate(Money.Coins(0.0004m)); txBuilder = new TransactionBuilder(); txBuilder.StandardTransactionPolicy = RelayPolicy; transfer = txBuilder .AddCoins(bobGold) .AddCoins(((IssuanceCoin)issuanceCoins[0]).Bearer) .AddKeys(gold, bob) .SendAsset(alice.PubKey, new AssetMoney(goldId, 40UL)) .SetChange(bob.PubKey, ChangeType.Colored) .SetChange(gold.PubKey.Hash, ChangeType.Uncolored) .SendEstimatedFees(rate) .BuildTransaction(true); var fee = transfer.GetFee(txBuilder.FindSpentCoins(transfer)); Assert.True(txBuilder.Verify(transfer, fee)); repo.Transactions.Put(funding.GetHash(), funding); colored = ColoredTransaction.FetchColors(transfer, repo); AssertHasAsset(transfer, colored, colored.Transfers[0], goldId, 60, bob.PubKey); AssertHasAsset(transfer, colored, colored.Transfers[1], goldId, 40, alice.PubKey); var change = transfer.Outputs.Last(o => o.ScriptPubKey == gold.PubKey.Hash.ScriptPubKey); Assert.Equal(Money.Coins(0.99980450m), change.Value); Assert.Equal(gold.PubKey.Hash, change.ScriptPubKey.GetDestination()); //Verify issuancecoin can have an url var issuanceCoin = (IssuanceCoin)issuanceCoins[0]; issuanceCoin.DefinitionUrl = new Uri("http://toto.com/"); txBuilder = new TransactionBuilder(); tx = txBuilder .AddKeys(gold) .AddCoins(issuanceCoin) .IssueAsset(bob, new AssetMoney(gold.PubKey, 10)) .SetChange(gold) .BuildTransaction(true); Assert.Equal("http://toto.com/", tx.GetColoredMarker().GetMetadataUrl().AbsoluteUri); } }
public void FundTransaction_Given__a_wallet_has_enough_inputs__When__adding_inputs_to_an_existing_transaction__Then__the_transaction_is_funded_successfully() { DataFolder dataFolder = CreateDataFolder(this); Types.Wallet wallet = WalletTestsHelpers.GenerateBlankWallet("myWallet1", "password"); (ExtKey ExtKey, string ExtPubKey)accountKeys = WalletTestsHelpers.GenerateAccountKeys(wallet, "password", "m/44'/0'/0'"); (PubKey PubKey, BitcoinPubKeyAddress Address)spendingKeys = WalletTestsHelpers.GenerateAddressKeys(wallet, accountKeys.ExtPubKey, "0/0"); (PubKey PubKey, BitcoinPubKeyAddress Address)destinationKeys1 = WalletTestsHelpers.GenerateAddressKeys(wallet, accountKeys.ExtPubKey, "0/1"); (PubKey PubKey, BitcoinPubKeyAddress Address)destinationKeys2 = WalletTestsHelpers.GenerateAddressKeys(wallet, accountKeys.ExtPubKey, "0/2"); (PubKey PubKey, BitcoinPubKeyAddress Address)destinationKeys3 = WalletTestsHelpers.GenerateAddressKeys(wallet, accountKeys.ExtPubKey, "0/3"); var address = new HdAddress { Index = 0, HdPath = $"m/44'/0'/0'/0/0", Address = spendingKeys.Address.ToString(), Pubkey = spendingKeys.PubKey.ScriptPubKey, ScriptPubKey = spendingKeys.Address.ScriptPubKey, // Transactions = new List<TransactionData>() }; // wallet with 4 coinbase outputs of 50 = 200 Bitcoin var chain = new ChainIndexer(wallet.Network); WalletTestsHelpers.AddBlocksWithCoinbaseToChain(wallet.walletStore as WalletMemoryStore, wallet.Network, chain, address, 4); wallet.AccountsRoot.ElementAt(0).Accounts.Add(new HdAccount { Index = 0, Name = "account1", HdPath = "m/44'/0'/0'", ExtendedPubKey = accountKeys.ExtPubKey, ExternalAddresses = new List <HdAddress> { address }, InternalAddresses = new List <HdAddress>() }); var walletFeePolicy = new Mock <IWalletFeePolicy>(); walletFeePolicy.Setup(w => w.GetFeeRate(FeeType.Low.ToConfirmations())).Returns(new FeeRate(20000)); var overrideFeeRate = new FeeRate(20000); var walletManager = new WalletManager(this.LoggerFactory.Object, this.Network, chain, new WalletSettings(NodeSettings.Default(this.Network)), dataFolder, walletFeePolicy.Object, new Mock <IAsyncProvider>().Object, new NodeLifetime(), DateTimeProvider.Default, this.scriptAddressReader); var walletTransactionHandler = new WalletTransactionHandler(this.LoggerFactory.Object, walletManager, walletFeePolicy.Object, this.Network, this.standardTransactionPolicy); walletManager.Wallets.Add(wallet); var walletReference = new WalletAccountReference { AccountName = "account1", WalletName = "myWallet1" }; // create a trx with 3 outputs 50 + 50 + 50 = 150 BTC var context = new TransactionBuildContext(this.Network) { AccountReference = walletReference, MinConfirmations = 0, FeeType = FeeType.Low, WalletPassword = "******", Recipients = new[] { new Recipient { Amount = new Money(50, MoneyUnit.BTC), ScriptPubKey = destinationKeys1.PubKey.ScriptPubKey }, new Recipient { Amount = new Money(50, MoneyUnit.BTC), ScriptPubKey = destinationKeys2.PubKey.ScriptPubKey }, new Recipient { Amount = new Money(50, MoneyUnit.BTC), ScriptPubKey = destinationKeys3.PubKey.ScriptPubKey } }.ToList() }; Transaction fundTransaction = walletTransactionHandler.BuildTransaction(context); Assert.Equal(4, fundTransaction.Inputs.Count); // 4 inputs Assert.Equal(4, fundTransaction.Outputs.Count); // 3 outputs with change // remove the change output fundTransaction.Outputs.Remove(fundTransaction.Outputs.First(f => f.ScriptPubKey == context.ChangeAddress.ScriptPubKey)); // remove 3 inputs they will be added back by fund transaction fundTransaction.Inputs.RemoveAt(3); fundTransaction.Inputs.RemoveAt(2); fundTransaction.Inputs.RemoveAt(1); Assert.Single(fundTransaction.Inputs); // 4 inputs Transaction fundTransactionClone = this.Network.CreateTransaction(fundTransaction.ToBytes()); var fundContext = new TransactionBuildContext(this.Network) { AccountReference = walletReference, MinConfirmations = 0, FeeType = FeeType.Low, WalletPassword = "******", Recipients = new List <Recipient>() }; fundContext.OverrideFeeRate = overrideFeeRate; walletTransactionHandler.FundTransaction(fundContext, fundTransaction); foreach (TxIn input in fundTransactionClone.Inputs) // all original inputs are still in the trx { Assert.Contains(fundTransaction.Inputs, a => a.PrevOut == input.PrevOut); } Assert.Equal(4, fundTransaction.Inputs.Count); // we expect 4 inputs Assert.Equal(4, fundTransaction.Outputs.Count); // we expect 4 outputs Assert.Equal(new Money(200, MoneyUnit.BTC) - fundContext.TransactionFee, fundTransaction.TotalOut); Assert.Contains(fundTransaction.Outputs, a => a.ScriptPubKey == destinationKeys1.PubKey.ScriptPubKey); Assert.Contains(fundTransaction.Outputs, a => a.ScriptPubKey == destinationKeys2.PubKey.ScriptPubKey); Assert.Contains(fundTransaction.Outputs, a => a.ScriptPubKey == destinationKeys3.PubKey.ScriptPubKey); }
public void CanSplitFees() { var satoshi = new Key(); var alice = new Key(); var bob = new Key(); var aliceCoins = new ICoin[] { RandomCoin("0.4", alice), RandomCoin("0.6", alice) }; var bobCoins = new ICoin[] { RandomCoin("0.2", bob), RandomCoin("0.3", bob) }; TransactionBuilder builder = new TransactionBuilder(); FeeRate rate = new FeeRate(Money.Coins(0.0004m)); var tx = builder .AddCoins(aliceCoins) .AddKeys(alice) .Send(satoshi, Money.Coins(0.1m)) .SetChange(alice) .Then() .AddCoins(bobCoins) .AddKeys(bob) .Send(satoshi, Money.Coins(0.01m)) .SetChange(bob) .SendEstimatedFeesSplit(rate) .BuildTransaction(true); var estimated = builder.EstimateFees(tx, rate); Assert.True(builder.Verify(tx, estimated)); // Alice should pay two times more fee than bob builder = new TransactionBuilder(); tx = builder .AddCoins(aliceCoins) .AddKeys(alice) .SetFeeWeight(2.0m) .Send(satoshi, Money.Coins(0.1m)) .SetChange(alice) .Then() .AddCoins(bobCoins) .AddKeys(bob) .Send(satoshi, Money.Coins(0.01m)) .SetChange(bob) .SendFeesSplit(Money.Coins(0.6m)) .BuildTransaction(true); var spentAlice = builder.FindSpentCoins(tx).Where(c => aliceCoins.Contains(c)).OfType<Coin>().Select(c => c.Amount).Sum(); var receivedAlice = tx.Outputs.AsCoins().Where(c => c.ScriptPubKey == alice.PubKey.Hash.ScriptPubKey).Select(c => c.Amount).Sum(); Assert.Equal(Money.Coins(0.1m + 0.4m), spentAlice - receivedAlice); var spentBob = builder.FindSpentCoins(tx).Where(c => bobCoins.Contains(c)).OfType<Coin>().Select(c => c.Amount).Sum(); var receivedBob = tx.Outputs.AsCoins().Where(c => c.ScriptPubKey == bob.PubKey.Hash.ScriptPubKey).Select(c => c.Amount).Sum(); Assert.Equal(Money.Coins(0.01m + 0.2m), spentBob - receivedBob); }