Example #1
0
        /// <summary>
        /// Given a collection of private keys and a transaction,
        /// sign the transaction
        ///
        /// </summary>
        /// <param name="privateKeys"></param>
        /// <param name="rawTransaction"></param>
        /// <returns></returns>
        public string SignRawTransaction(string[] privateKeys, byte[] rawTransaction)
        {
            // Deserialize wifs and generate privatekey/publickey/pubkeyhash mappings
            var keys =
                (from wif in privateKeys
                 let privKey = Wif.Deserialize(_network, wif)
                               select ExpandPrivateKey(privKey)).ToArray();

            // This is the transaction that will have properly signed inputs.
            var transaction = DecodeTransaction(rawTransaction);

            foreach (var input in transaction.TxIn)
            {
                // Clone the base transaction
                var txCopy = DecodeTransaction(rawTransaction);

                // Match the private key with the public key script for this input
                // Note: the public key script is embedded in the signature script portion of the transaction.
                var publicKeyHash = GetPublicKeyHash(input.SignatureScript);

                var signingKeys = keys
                                  .Where(k => k.PublicKeyHash.SequenceEqual(publicKeyHash))
                                  .ToList();

                // Ensure one of the known private keys is able to sign the transaction.
                if (!signingKeys.Any())
                {
                    throw new SigningException("Not able to unlock utxo with provided keys");
                }

                var key = signingKeys.First();

                // Zero out all scripts except the current one.
                foreach (var txCopyIn in txCopy.TxIn)
                {
                    // Skip the current TxIn
                    if (input.PreviousOutPoint.Hash.SequenceEqual <byte>(txCopyIn.PreviousOutPoint.Hash) &&
                        input.PreviousOutPoint.Index == txCopyIn.PreviousOutPoint.Index)
                    {
                        continue;
                    }

                    txCopyIn.SignatureScript = new byte[0];
                }

                // Calculate the tx signature for this input,
                // and sign the hash
                var txHash    = CalculateTxHash(txCopy);
                var signature = _securityService.Sign(key.PrivateKey, txHash).MakeCanonical().ToDer();
                var sigBytes  = signature.Concat(new[] { (byte)SignatureHashType.All }).ToArray();

                input.SignatureScript = GetSignatureScript(sigBytes, key.PublicKey);
            }

            return(HexUtil.FromByteArray(transaction.Encode()));
        }
        public async Task BuildSingleTransactionAsync_WithSingleUnspentOutput_BuildsExpectedTx()
        {
            var fromAddr = "Tso2MVTUeVrjHTBFedFhiyM7yVTbieqp91h";
            var toAddr   = "TsntCvtbzaDtx4DwGehWcM3Ydb6Muc79YbV";

            // Send 1 decred out of 2 total
            var amountToSend  = 100000000;
            var unspentOutput = new UnspentTxOutput()
            {
                BlockHeight   = 0,
                BlockIndex    = 0,
                Hash          = HexUtil.FromByteArray(new byte[32]),
                OutputIndex   = 1,
                OutputValue   = 2 * 100000000,
                OutputVersion = 0,
                Tree          = 0,
                PkScript      = new byte[0]
            };

            _mockTxRepo.Setup(m => m.GetConfirmedUtxos(fromAddr)).ReturnsAsync(new[] { unspentOutput });
            _mockTxRepo.Setup(m => m.GetMempoolUtxos(fromAddr)).ReturnsAsync(new[] { unspentOutput });
            _mockDcrdClient.Setup(m => m.EstimateFeeAsync(It.IsAny <int>())).ReturnsAsync(0.001m);
            _mockBroadcastedOutpointRepo.Setup(m => m.GetAsync($"{unspentOutput.Hash}:1"))
            .ReturnsAsync((BroadcastedOutpoint)null);

            var txFeeService = new TransactionFeeService(_mockDcrdClient.Object);
            var subject      = new TransactionBuilder(
                txFeeService,
                _mockTxRepo.Object,
                _mockBroadcastedOutpointRepo.Object
                );

            var request = new BuildSingleTransactionRequest
            {
                Amount      = amountToSend.ToString(),
                AssetId     = "DCR",
                FromAddress = fromAddr,
                ToAddress   = toAddr,
                IncludeFee  = true
            };

            var result = await subject.BuildSingleTransactionAsync(request, 1);

            var transaction = Transaction.Deserialize(HexUtil.ToByteArray(result.TransactionContext));
            var expectedFee = txFeeService.CalculateFee(100000, 1, 2, 1);

            Assert.Equal(expectedFee, 2 * 100000000 - transaction.Outputs.Sum(o => o.Amount));
            Assert.Equal(2, transaction.Outputs.Length);

            // Sent amount - fee
            Assert.Equal(1, transaction.Outputs.Count(o => o.Amount + expectedFee == 100000000));

            // Change
            Assert.Equal(1, transaction.Outputs.Count(o => o.Amount == 100000000));
        }
Example #3
0
        /// <summary>
        /// Broadcasts a signed transaction to the Decred network
        /// </summary>
        /// <param name="operationId"></param>
        /// <param name="hexTransaction"></param>
        /// <returns></returns>
        /// <exception cref="TransactionBroadcastException"></exception>
        public async Task Broadcast(Guid operationId, string hexTransaction)
        {
            if (operationId == Guid.Empty)
            {
                throw new BusinessException(ErrorReason.BadRequest, "Operation id is invalid");
            }
            if (string.IsNullOrWhiteSpace(hexTransaction))
            {
                throw new BusinessException(ErrorReason.BadRequest, "SignedTransaction is invalid");
            }

            var txBytes = HexUtil.ToByteArray(hexTransaction);
            var msgTx   = new MsgTx();

            msgTx.Decode(txBytes);

            // If the operation exists in the cache, throw exception
            var cachedResult = await _broadcastTxRepo.GetAsync(operationId.ToString());

            if (cachedResult != null)
            {
                throw new BusinessException(ErrorReason.DuplicateRecord, "Operation already broadcast");
            }

            var txHash     = HexUtil.FromByteArray(msgTx.GetHash().Reverse().ToArray());
            var txWasMined = await _txRepo.GetTxInfoByHash(txHash, long.MaxValue) != null;

            if (txWasMined)
            {
                await SaveBroadcastedTransaction(new BroadcastedTransaction
                {
                    OperationId        = operationId,
                    Hash               = txHash,
                    EncodedTransaction = hexTransaction
                });

                throw new BusinessException(ErrorReason.DuplicateRecord, "Operation already broadcast");
            }

            // Submit the transaction to the network via dcrd
            var result = await _dcrdClient.SendRawTransactionAsync(hexTransaction);

            if (result.Error != null)
            {
                throw new TransactionBroadcastException($"[{result.Error.Code}] {result.Error.Message}");
            }

            await SaveBroadcastedTransaction(new BroadcastedTransaction
            {
                OperationId        = operationId,
                Hash               = txHash,
                EncodedTransaction = hexTransaction
            });
        }
Example #4
0
        /// <summary>
        /// Builds a transaction that sends value from one address to another.
        /// Change is spent to the source address, if necessary.
        /// </summary>
        /// <param name="request"></param>
        /// <param name="feeFactor"></param>
        /// <returns></returns>
        /// <exception cref="BusinessException"></exception>
        public async Task <BuildTransactionResponse> BuildSingleTransactionAsync(BuildSingleTransactionRequest request, decimal feeFactor)
        {
            if (string.IsNullOrWhiteSpace(request.FromAddress))
            {
                throw new BusinessException(ErrorReason.BadRequest, "FromAddress missing");
            }
            if (string.IsNullOrWhiteSpace(request.ToAddress))
            {
                throw new BusinessException(ErrorReason.BadRequest, "ToAddress missing");
            }
            if (!long.TryParse(request.Amount, out var amount) || amount <= 0)
            {
                throw new BusinessException(ErrorReason.BadRequest, $"Invalid amount {amount}");
            }

            var feePerKb = await _feeService.GetFeePerKb();

            if (TransactionRules.IsDustAmount(amount, Transaction.PayToPubKeyHashPkScriptSize, new Amount(feePerKb)))
            {
                throw new BusinessException(ErrorReason.AmountTooSmall, "Amount is dust");
            }

            const int outputVersion = 0;
            const int lockTime      = 0;
            const int expiry        = 0;

            // Number of outputs newly build tx will contain,
            // not including the change address
            const int numOutputs = 1;

            // Lykke api doesn't have option to specify a change address.
            var changeAddress = Address.Decode(request.FromAddress);
            var toAddress     = Address.Decode(request.ToAddress);
            var allInputs     = (await GetUtxosForAddress(request.FromAddress)).ToList();

            long estFee         = 0;
            long totalSpent     = 0;
            var  consumedInputs = new List <Transaction.Input>();

            bool HasEnoughInputs(out long fee)
            {
                var calculateWithChange = false;

                while (true)
                {
                    var changeCount = calculateWithChange ? 1 : 0;
                    fee = _feeService.CalculateFee(feePerKb, consumedInputs.Count, numOutputs + changeCount, feeFactor);
                    var estAmount = amount + (request.IncludeFee ? 0 : fee);

                    if (totalSpent < estAmount)
                    {
                        return(false);
                    }
                    if (totalSpent == estAmount)
                    {
                        return(true);
                    }
                    if (totalSpent > estAmount && calculateWithChange)
                    {
                        return(true);
                    }

                    // Loop one more time but make sure change is accounted for this time.
                    if (totalSpent > estAmount)
                    {
                        calculateWithChange = true;
                    }
                }
            }

            // Accumulate inputs until we have enough to cover the cost
            // of the amount + fee
            foreach (var input in allInputs)
            {
                // Don't consume an outpoint if it's spent.
                if (await IsBroadcastedUtxo(input.PreviousOutpoint))
                {
                    continue;
                }

                consumedInputs.Add(input);
                totalSpent += input.InputAmount;

                if (HasEnoughInputs(out estFee))
                {
                    break;
                }
            }

            // If all inputs do not have enough value to fund the transaction.
            if (totalSpent < amount + (request.IncludeFee ? 0 : estFee))
            {
                throw new BusinessException(ErrorReason.NotEnoughBalance, "Address balance too low");
            }

            // The fee either comes from the change or the sent amount
            var send   = amount - (request.IncludeFee ? estFee : 0);
            var change = (totalSpent - amount) - (request.IncludeFee ? 0 : estFee);

            // If all inputs do not have enough value to fund the transaction, throw error.
            if (request.IncludeFee && estFee > amount)
            {
                throw new BusinessException(ErrorReason.AmountTooSmall, "Amount not enough to include fee");
            }

            // If all inputs do not have enough value to fund the transaction, throw error.
            if (totalSpent < amount + (request.IncludeFee ? 0 : estFee))
            {
                throw new BusinessException(ErrorReason.NotEnoughBalance, "Address balance too low");
            }

            // Build outputs to address + change address.
            // If any of the outputs is zero value, exclude it.  For example, if there is no change.
            var outputs = new[] {
                new Transaction.Output(send, outputVersion, toAddress.BuildScript().Script),
                new Transaction.Output(change, outputVersion, changeAddress.BuildScript().Script)
            }.Where(o => o.Amount != 0).ToArray();

            var newTx = new Transaction(
                Transaction.SupportedVersion,
                consumedInputs.ToArray(),
                outputs,
                lockTime,
                expiry
                );

            return(new BuildTransactionResponse
            {
                TransactionContext = HexUtil.FromByteArray(newTx.Serialize())
            });
        }