/// <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)); }
/// <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 }); }
/// <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()) }); }