public void SendTest( BitcoinBasedCurrency currency, decimal available, decimal amount, decimal fee, DustUsagePolicy dustUsagePolicy) { var error = Send( currency: currency, available: available, amount: amount, fee: fee, dustUsagePolicy: dustUsagePolicy, apiSetup: apiMock => { apiMock.Setup(a => a.TryBroadcastAsync(It.IsAny <IBlockchainTransaction>(), 10, 1000, CancellationToken.None)) .Returns(Task.FromResult(new Result <string>("<txid>"))); }, repositorySetup: (repositoryMock, fromAddress) => { repositoryMock.Setup(r => r.GetWalletAddressAsync(It.IsAny <string>(), fromAddress.Address)) .Returns(Task.FromResult(fromAddress)); }); Assert.Null(error); }
public void SendDustAsFeeTest( BitcoinBasedCurrency currency, decimal available, decimal amount, decimal fee, DustUsagePolicy dustUsagePolicy) { var change = available - amount - fee; var broadcastCallback = new Action <IBlockchainTransaction, int, int, CancellationToken>((tx, attempts, attemptsInterval, token) => { var btcBasedTx = (IBitcoinBasedTransaction)tx; Assert.True(btcBasedTx.Fees == currency.CoinToSatoshi(fee + change)); }); var error = Send( currency: currency, available: available, amount: amount, fee: fee, dustUsagePolicy: dustUsagePolicy, apiSetup: apiMock => { apiMock.Setup(a => a.TryBroadcastAsync(It.IsAny <IBlockchainTransaction>(), 10, 1000, CancellationToken.None)) .Callback(broadcastCallback) .Returns(Task.FromResult(new Result <string>("<txid>"))); }, repositorySetup: (repositoryMock, fromAddress) => { repositoryMock.Setup(r => r.GetWalletAddressAsync(It.IsAny <string>(), fromAddress.Address)) .Returns(Task.FromResult(fromAddress)); }); Assert.Null(error); }
public void SendDustChangeFailTest( BitcoinBasedCurrency currency, decimal available, decimal amount, decimal fee, DustUsagePolicy dustUsagePolicy) { var error = Send( currency: currency, available: available, amount: amount, fee: fee, dustUsagePolicy: dustUsagePolicy); Assert.NotNull(error); Assert.Equal(Errors.InsufficientAmount, error.Code); }
private Error Send( BitcoinBasedConfig currency, decimal available, decimal amount, decimal fee, DustUsagePolicy dustUsagePolicy, Action <Mock <IBlockchainApi> > apiSetup = null, Action <Mock <IAccountDataRepository>, WalletAddress> repositorySetup = null) { var apiMock = new Mock <IBlockchainApi>(); apiSetup?.Invoke(apiMock); var wallet = new HdWallet(Network.TestNet); var fromAddress = wallet.GetAddress( currency: currency, account: 0, chain: 0, index: 0, keyType: CurrencyConfig.StandardKey); var fromOutputs = GetOutputs(fromAddress.Address, NBitcoin.Network.TestNet, currency.CoinToSatoshi(available)).ToList(); var repositoryMock = new Mock <IAccountDataRepository>(); repositorySetup?.Invoke(repositoryMock, fromAddress); var currencies = Common.CurrenciesTestNet; currencies.GetByName(currency.Name).BlockchainApi = apiMock.Object; var account = new BitcoinBasedAccount( currency: currency.Name, currencies: currencies, wallet: wallet, dataRepository: repositoryMock.Object); return(account .SendAsync( from: fromOutputs, to: currency.TestAddress(), amount: amount, fee: fee, dustUsagePolicy: dustUsagePolicy) .WaitForResult()); }
public async Task <Error> SendAsync( IEnumerable <BitcoinBasedTxOutput> from, string to, decimal amount, decimal fee, DustUsagePolicy dustUsagePolicy, CancellationToken cancellationToken = default) { var config = Config; var amountInSatoshi = config.CoinToSatoshi(amount); var feeInSatoshi = config.CoinToSatoshi(fee); var requiredInSatoshi = amountInSatoshi + feeInSatoshi; // minimum amount and fee control if (amountInSatoshi < config.GetDust()) { return(new Error( code: Errors.InsufficientAmount, description: $"Insufficient amount to send. Min non-dust amount {config.SatoshiToCoin(config.GetDust())}, actual {config.SatoshiToCoin(amountInSatoshi)}")); } from = from .SelectOutputsForAmount(requiredInSatoshi) .ToList(); var availableInSatoshi = from.Sum(o => o.Value); if (!from.Any()) { return(new Error( code: Errors.InsufficientFunds, description: $"Insufficient funds. Required {config.SatoshiToCoin(requiredInSatoshi)}, available {config.SatoshiToCoin(availableInSatoshi)}")); } var changeAddress = await GetFreeInternalAddressAsync(cancellationToken) .ConfigureAwait(false); // minimum change control var changeInSatoshi = availableInSatoshi - requiredInSatoshi; if (changeInSatoshi > 0 && changeInSatoshi < config.GetDust()) { switch (dustUsagePolicy) { case DustUsagePolicy.Warning: return(new Error( code: Errors.InsufficientAmount, description: $"Change {config.SatoshiToCoin(changeInSatoshi)} can be definded by the network as dust and the transaction will be rejected")); case DustUsagePolicy.AddToDestination: amountInSatoshi += changeInSatoshi; break; case DustUsagePolicy.AddToFee: feeInSatoshi += changeInSatoshi; break; default: return(new Error( code: Errors.InternalError, description: $"Unknown dust usage policy value {dustUsagePolicy}")); } } var tx = config.CreatePaymentTx( unspentOutputs: from, destinationAddress: to, changeAddress: changeAddress.Address, amount: amountInSatoshi, fee: feeInSatoshi, lockTime: DateTimeOffset.MinValue); var signResult = await Wallet .SignAsync( tx : tx, spentOutputs : from, addressResolver : this, currencyConfig : config, cancellationToken : cancellationToken) .ConfigureAwait(false); if (!signResult) { return(new Error( code: Errors.TransactionSigningError, description: "Transaction signing error")); } if (!tx.Verify(from, out var errors, config)) { return(new Error( code: Errors.TransactionVerificationError, description: $"Transaction verification error: {string.Join(", ", errors.Select(e => e.Description))}")); } var broadcastResult = await config.BlockchainApi .TryBroadcastAsync(tx, cancellationToken : cancellationToken) .ConfigureAwait(false); if (broadcastResult.HasError) { return(broadcastResult.Error); } var txId = broadcastResult.Value; if (txId == null) { return(new Error( code: Errors.TransactionBroadcastError, description: "Transaction id is null")); } Log.Debug("Transaction successfully sent with txId: {@id}", txId); await UpsertTransactionAsync( tx : tx, updateBalance : false, notifyIfUnconfirmed : true, notifyIfBalanceUpdated : false, cancellationToken : cancellationToken) .ConfigureAwait(false); _ = UpdateBalanceAsync(cancellationToken); return(null); }