Exemple #1
0
        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);
        }
Exemple #2
0
        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);
        }
Exemple #3
0
        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);
        }
Exemple #4
0
        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());
        }
Exemple #5
0
        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);
        }