Ejemplo n.º 1
0
        public bool Run(bool txid, IConfiguration cfg, HttpClient httpClient, ITxBuilder txBuilder, Dictionary <string, string> settings, string progressId, string pluginsFolder, string url, string[] command, out bool inProgress, out string msg)
        {
            Debug.Assert(command != null);
            if (command.Length < 2)
            {
                inProgress = false;
                msg        = "invalid parameter count";
                return(false);
            }

            bool erc20 = command[0].Equals("collectCoins", StringComparison.OrdinalIgnoreCase);

            if (erc20 || command[0].Equals("registerTransfer", StringComparison.OrdinalIgnoreCase))
            {
                // ethereum RegisterTransfer gain orderId
                // ethereum CollectCoins amount

                string progress = Path.Combine(pluginsFolder, $"{name}_progress{progressId}.txt");

                inProgress = false;

                if (erc20 && command.Length != 2 || !erc20 && command.Length != 3)
                {
                    msg = "invalid parameter count";
                    return(false);
                }

                string gainString   = "0";
                string orderId      = null;
                string amountString = null;
                if (erc20)
                {
                    amountString = command[1];
                }
                else
                {
                    gainString = command[1];
                    orderId    = command[2];
                }

                string secret = cfg["secret"];
                if (string.IsNullOrWhiteSpace(secret))
                {
                    msg = "ethereum.secret is not set";
                    return(false);
                }
                var ethereumPrivateKey = secret;

#if DEBUG
                string confirmationsCount = cfg["confirmationsCount"];
                if (int.TryParse(confirmationsCount, out int parsedCount))
                {
                    mConfirmationsExpected = parsedCount;
                }
#endif

                string rpcUrl = cfg["rpc"];
                if (string.IsNullOrWhiteSpace(rpcUrl))
                {
                    msg = "ethereum.rpc is not set";
                    return(false);
                }

                string ethSrcAddress = EthECKey.GetPublicAddress(ethereumPrivateKey);
                var    web3          = new Nethereum.Web3.Web3(rpcUrl);

                string srcAddressId = null;
                string dstAddressId = null;
                if (!erc20)
                {
                    var protobuf = RpcHelper.ReadProtobuf(httpClient, $"{url}/state/{orderId}", out msg);
                    if (protobuf == null)
                    {
                        msg = "failed to extract address data through RPC";
                        return(false);
                    }
                    if (orderId.StartsWith(RpcHelper.creditCoinNamespace + RpcHelper.dealOrderPrefix))
                    {
                        var dealOrder = DealOrder.Parser.ParseFrom(protobuf);
                        if (gainString.Equals("0"))
                        {
                            srcAddressId = dealOrder.SrcAddress;
                            dstAddressId = dealOrder.DstAddress;
                        }
                        else
                        {
                            dstAddressId = dealOrder.SrcAddress;
                            srcAddressId = dealOrder.DstAddress;
                        }
                        amountString = dealOrder.Amount;
                    }
                    else if (orderId.StartsWith(RpcHelper.creditCoinNamespace + RpcHelper.repaymentOrderPrefix))
                    {
                        var repaymentOrder = RepaymentOrder.Parser.ParseFrom(protobuf);
                        if (gainString.Equals("0"))
                        {
                            srcAddressId = repaymentOrder.SrcAddress;
                            dstAddressId = repaymentOrder.DstAddress;
                        }
                        else
                        {
                            dstAddressId = repaymentOrder.SrcAddress;
                            srcAddressId = repaymentOrder.DstAddress;
                        }
                        amountString = repaymentOrder.Amount;
                    }
                    else
                    {
                        msg = "unexpected referred order";
                        return(false);
                    }
                }

                string payTxId;
                if (File.Exists(progress))
                {
                    Console.WriteLine("Found unfinished action, retrying...");
                    payTxId = File.ReadAllText(progress);
                }
                else
                {
                    string ethDstAddress = null;
                    if (!erc20)
                    {
                        var protobuf = RpcHelper.ReadProtobuf(httpClient, $"{url}/state/{srcAddressId}", out msg);
                        if (protobuf == null)
                        {
                            msg = "failed to extract address data through RPC";
                            return(false);
                        }
                        var srcAddress = Address.Parser.ParseFrom(protobuf);

                        protobuf = RpcHelper.ReadProtobuf(httpClient, $"{url}/state/{dstAddressId}", out msg);
                        if (protobuf == null)
                        {
                            msg = "failed to extract address data through RPC";
                            return(false);
                        }
                        Address dstAddress = Address.Parser.ParseFrom(protobuf);

                        if (!srcAddress.Blockchain.Equals(name) || !dstAddress.Blockchain.Equals(name))
                        {
                            msg = $"ethereum RegisterTransfer can only transfer ether.\nThis source is registered for {srcAddress.Blockchain} and destination for {dstAddress.Blockchain}";
                            return(false);
                        }
                        ethDstAddress = dstAddress.Value;

                        if (!ethSrcAddress.Equals(srcAddress.Value, StringComparison.OrdinalIgnoreCase))
                        {
                            msg = "The deal is for a different client";
                            return(false);
                        }
                    }

                    BigInteger transferAmount;
                    if (!BigInteger.TryParse(amountString, out transferAmount) || transferAmount <= 0)
                    {
                        msg = "Invalid amount";
                        return(false);
                    }
                    BigInteger gain;
                    if (!BigInteger.TryParse(gainString, out gain))
                    {
                        msg = "Invalid amount";
                        return(false);
                    }
                    transferAmount = transferAmount + gain;
                    if (transferAmount < 0)
                    {
                        msg = "Invalid amount";
                        return(false);
                    }

                    var txCount = web3.Eth.Transactions.GetTransactionCount.SendRequestAsync(ethSrcAddress).Result;
                    TransactionSigner signer = new TransactionSigner();

                    HexBigInteger gasPrice;

                    string gasPriceInGweiString = cfg["gasPriceInGwei"];
                    if (int.TryParse(gasPriceInGweiString, out int gasPriceOverride))
                    {
                        gasPrice = new HexBigInteger(Nethereum.Util.UnitConversion.Convert.ToWei(gasPriceOverride, Nethereum.Util.UnitConversion.EthUnit.Gwei));
                    }
                    else
                    {
                        gasPrice = web3.Eth.GasPrice.SendRequestAsync().Result;
                    }
                    Console.WriteLine("gasPrice: " + gasPrice.Value.ToString());

                    string        to;
                    string        data;
                    BigInteger    amount;
                    HexBigInteger gasLimit;
                    if (erc20)
                    {
                        string creditcoinContract    = cfg["creditcoinContract"];
                        string creditcoinContractAbi = cfg["creditcoinContractAbi"];

                        to = creditcoinContract;

                        var contract      = web3.Eth.GetContract(creditcoinContractAbi, creditcoinContract);
                        var burn          = contract.GetFunction("exchange");
                        var functionInput = new object[] { transferAmount, txBuilder.getSighash() };
                        data     = burn.GetData(functionInput);
                        gasLimit = burn.EstimateGasAsync(functionInput).Result;
                        amount   = 0;
                    }
                    else
                    {
                        gasLimit = web3.Eth.Transactions.EstimateGas.SendRequestAsync(new Nethereum.RPC.Eth.DTOs.CallInput(orderId, ethDstAddress, new Nethereum.Hex.HexTypes.HexBigInteger(transferAmount))).Result;
                        Console.WriteLine("gasLimit: " + gasLimit.Value.ToString());

                        to     = ethDstAddress;
                        data   = orderId;
                        amount = transferAmount;
                    }

                    string txRaw = signer.SignTransaction(ethereumPrivateKey, to, amount, txCount, gasPrice, gasLimit, data);
                    payTxId = web3.Eth.Transactions.SendRawTransaction.SendRequestAsync("0x" + txRaw).Result;
                    Console.WriteLine("Ethereum Transaction ID: " + payTxId);

                    File.WriteAllText(progress, $"{payTxId}");
                }

                inProgress = true;
                while (true)
                {
                    var receipt = web3.Eth.TransactionManager.TransactionReceiptService.PollForReceiptAsync(payTxId).Result;
                    if (receipt.BlockNumber != null)
                    {
                        var blockNumber = web3.Eth.Blocks.GetBlockNumber.SendRequestAsync().Result;
                        if (blockNumber.Value - receipt.BlockNumber.Value >= mConfirmationsExpected)
                        {
                            break;
                        }
                    }
                    Thread.Sleep(1000);
                }
                File.Delete(progress);
                inProgress = false;

                if (erc20)
                {
                    command = new string[] { command[0], ethSrcAddress, amountString, payTxId };
                }
                else
                {
                    command = new string[] { command[0], gainString, orderId, payTxId };
                }
                var tx = txBuilder.BuildTx(command, out msg);
                if (tx == null)
                {
                    return(false);
                }

                Debug.Assert(msg == null);

                var content = new ByteArrayContent(tx);
                content.Headers.Add("Content-Type", "application/octet-stream");

                msg = RpcHelper.CompleteBatch(httpClient, url, "batches", content, txid);

                return(true);
            }
            else
            {
                inProgress = false;
                msg        = "Unknown command: " + command[0];
                return(false);
            }
        }
Ejemplo n.º 2
0
        public bool Run(IConfiguration cfg, HttpClient httpClient, ITxBuilder txBuilder, Dictionary <string, string> settings, string pluginsFolder, string url, string[] command, out bool inProgress, out string msg)
        {
            Debug.Assert(command != null);
            Debug.Assert(command.Length > 1);
            if (command[0].Equals("registerTransfer", StringComparison.OrdinalIgnoreCase))
            {
                // bitcoin RegisterTransfer registeredSourceId amount sourceTxId

                string progress = Path.Combine(pluginsFolder, $"{name}_progress.txt");

                inProgress = false;

                Debug.Assert(command.Length == 4);
                string registeredSourceId = command[1];
                string amountString       = command[2];
                string sourceTxIdString   = command[3];

                string secret = cfg["secret"];
                if (string.IsNullOrWhiteSpace(secret))
                {
                    msg = "bitcoin.secret is not set";
                    return(false);
                }
                string rpcAddress = cfg["rpc"];
                if (string.IsNullOrWhiteSpace(rpcAddress))
                {
                    msg = "bitcoin.rpc is not set";
                    return(false);
                }

                string credential = cfg["credential"];
                if (string.IsNullOrWhiteSpace(credential))
                {
                    msg = "bitcoin.credential is not set";
                    return(false);
                }

                //TODO disable confirmation count config for release build
                string confirmationsCount = cfg["confirmationsCount"];
                if (string.IsNullOrWhiteSpace(confirmationsCount))
                {
                    msg = "bitcoin.confirmationsCount is not set";
                    return(false);
                }
                int confirmationsExpected = 1;
                if (!int.TryParse(confirmationsCount, out confirmationsExpected))
                {
                    msg = "bitcoin.confirmationsCount is not an int";
                    return(false);
                }

                string feeString = cfg["fee"];
                if (string.IsNullOrWhiteSpace(feeString))
                {
                    msg = "bitcoin.fee is not set";
                    return(false);
                }
                if (!int.TryParse(feeString, out int fee))
                {
                    msg = "bitcoin.fee is not an int";
                    return(false);
                }

                var bitcoinPrivateKey = new BitcoinSecret(secret);
                var network           = bitcoinPrivateKey.Network;
                var rpcClient         = new RPCClient(credential, new Uri(rpcAddress), network);

                string  payTxIdHash;
                uint256 payTxId;
                if (File.Exists(progress))
                {
                    Console.WriteLine("Found unfinished action, retrying...");
                    payTxIdHash = File.ReadAllText(progress);
                    if (!uint256.TryParse(payTxIdHash, out payTxId))
                    {
                        msg = "corrupted progress file";
                        return(false);
                    }
                }
                else
                {
                    var protobuf = RpcHelper.ReadProtobuf(httpClient, $"{url}/state/{registeredSourceId}", out msg);
                    if (protobuf == null)
                    {
                        return(false);
                    }

                    var address = Address.Parser.ParseFrom(protobuf);

                    string destinationAddress;
                    if (!settings.TryGetValue("sawtooth.escrow." + name, out destinationAddress))
                    {
                        msg = "Escrow not found for " + name;
                        return(false);
                    }

                    Money transferAmount;
                    if (!Money.TryParse(amountString, out transferAmount) || transferAmount <= 0)
                    {
                        msg = "Invalid amount";
                        return(false);
                    }

                    if (!address.Blockchain.Equals(name))
                    {
                        msg = $"bitcoin RegisterTransfer can only transfer bitcoins.\nThis source is registered for {address.Blockchain}";
                        return(false);
                    }

                    var sourceAddress = bitcoinPrivateKey.GetAddress();
                    if (!sourceAddress.ToString().Equals(address.Address_, StringComparison.OrdinalIgnoreCase))
                    {
                        msg = "The deal is for a different client";
                        return(false);
                    }

#if RELEASE
                    if (network != NBitcoin.Network.Main)
                    {
                        msg = "bitcoin.secret is not defined for main network";
                        return(false);
                    }
#endif

                    var sourceTxId          = uint256.Parse(sourceTxIdString);
                    var transactionResponse = rpcClient.GetRawTransaction(sourceTxId);

                    //TODO: fix (assuming that the transaction has one output intended for the private key with the exact amount)
                    var      receivedCoins   = transactionResponse.Outputs.AsCoins();
                    OutPoint outPointToSpend = null;
                    TxOut    outTxToSpend    = null;
                    Money    amount          = null;
                    foreach (var coin in receivedCoins)
                    {
                        if (coin.TxOut.ScriptPubKey == bitcoinPrivateKey.ScriptPubKey)
                        {
                            outPointToSpend = coin.Outpoint;
                            outTxToSpend    = coin.TxOut;
                            amount          = (Money)coin.Amount;
                            if (amount.CompareTo(transferAmount + fee * 2) < 0)
                            {
                                msg = $"Invalid transaction - needed: {transferAmount}, has: {amount.ToString()}";
                                return(false);
                            }
                            break;
                        }
                    }
                    if (outPointToSpend == null)
                    {
                        msg = "Invalid transaction - no outputs that the client can spend";
                        return(false);
                    }

                    var addressFrom = BitcoinAddress.Create(address.Address_, network);
                    var addressTo   = BitcoinAddress.Create(destinationAddress, network);
                    var message     = address.Sighash;
                    var bytes       = Encoding.UTF8.GetBytes(message);

                    var bitcoinTransactionBuilder = new TransactionBuilder();

                    var transaction = bitcoinTransactionBuilder
                                      .AddCoins(new Coin(outPointToSpend, outTxToSpend))
                                      .AddKeys(bitcoinPrivateKey)
                                      .Send(addressTo.ScriptPubKey, transferAmount + fee)
                                      .Send(TxNullDataTemplate.Instance.GenerateScriptPubKey(bytes), Money.Zero)
                                      .SendFees(new Money(fee, MoneyUnit.Satoshi))
                                      .SetChange(addressFrom.ScriptPubKey)
                                      .BuildTransaction(true);

                    if (!bitcoinTransactionBuilder.Verify(transaction))
                    {
                        msg = "failed verify transaction";
                        return(false);
                    }

                    try
                    {
                        payTxId = rpcClient.SendRawTransaction(transaction);
                    }
                    catch (RPCException e)
                    {
                        msg = $"failed to broadcast - error: {e.RPCCode}, reason: {e.RPCCodeMessage}";
                        return(false);
                    }

                    if (payTxId == null)
                    {
                        msg = "failed to broadcast - unknown error";
                        return(false);
                    }

                    payTxIdHash = payTxId.ToString();
                    File.WriteAllText(progress, payTxIdHash);
                }

                inProgress = true;
                while (true)
                {
                    var transactionResponse = rpcClient.GetRawTransactionInfo(payTxId);
                    if (transactionResponse != null && transactionResponse.BlockHash != null && transactionResponse.Confirmations >= confirmationsExpected)
                    {
                        break;
                    }

                    Thread.Sleep(1000);
                }

                command = new string[] { command[0], registeredSourceId, amountString, feeString, payTxIdHash, ((network == Network.Main) ? "1" : "0") };
                var tx = txBuilder.BuildTx(command, out msg);
                Debug.Assert(tx != null);
                Debug.Assert(msg == null);

                var content = new ByteArrayContent(tx);
                content.Headers.Add("Content-Type", "application/octet-stream");

                msg = RpcHelper.CompleteBatch(httpClient, $"{url}/batches", content);

                File.Delete(progress);
                inProgress = false;

                return(true);
            }
            else
            {
                inProgress = false;
                msg        = "Unknown command: " + command[0];
                return(false);
            }
        }
Ejemplo n.º 3
0
        public bool Run(IConfiguration cfg, HttpClient httpClient, ITxBuilder txBuilder, Dictionary <string, string> settings, string pluginsFolder, string url, string[] command, out bool inProgress, out string msg)
        {
            Debug.Assert(command != null);
            Debug.Assert(command.Length > 1);
            if (command[0].Equals("registerTransfer", StringComparison.OrdinalIgnoreCase))
            {
                // ethereum RegisterTransfer registeredSourceId amount

                string progress = Path.Combine(pluginsFolder, $"{name}_progress.txt");

                inProgress = false;

                Debug.Assert(command.Length == 3 || command.Length == 4);
                string registeredSourceId = command[1];
                string amountString       = command[2];
                bool   erc20 = command.Length == 4;

                string secret = cfg["secret"];
                if (string.IsNullOrWhiteSpace(secret))
                {
                    msg = "ethereum.secret is not set";

                    return(false);
                }
                var ethereumPrivateKey = secret;

                // TODO disable confirmation count config for release build
                string confirmationsCount = cfg["confirmationsCount"];
                if (string.IsNullOrWhiteSpace(confirmationsCount))
                {
                    msg = "ethereum.confirmationsCount is not set";

                    return(false);
                }
                if (!int.TryParse(confirmationsCount, out int confirmationsExpected))
                {
                    msg = "ethereum.confirmationsCount is not an int";

                    return(false);
                }

                string rpcUrl = cfg["rpc"];
                if (string.IsNullOrWhiteSpace(rpcUrl))
                {
                    msg = "ethereum.rpc is not set";

                    return(false);
                }

                var web3 = new Nethereum.Web3.Web3(rpcUrl);

                string     payTxId;
                BigInteger fee;
                if (File.Exists(progress))
                {
                    Console.WriteLine("Found unfinished action, retrying...");
                    var data = File.ReadAllText(progress).Split(':');
                    if (data.Length != 2)
                    {
                        msg = "Invalid progress data";
                        return(false);
                    }
                    payTxId = data[0];
                    if (!BigInteger.TryParse(data[1], out fee))
                    {
                        msg = "Invalid progress data";
                        return(false);
                    }
                }
                else
                {
                    var protobuf = RpcHelper.ReadProtobuf(httpClient, $"{url}/state/{registeredSourceId}", out msg);
                    if (protobuf == null)
                    {
                        return(false);
                    }
                    var address = Address.Parser.ParseFrom(protobuf);

                    string destinationAddress;
                    if (!settings.TryGetValue("sawtooth.escrow." + name, out destinationAddress))
                    {
                        msg = "Escrow not found for " + name;
                        return(false);
                    }

                    BigInteger transferAmount;
                    if (!BigInteger.TryParse(amountString, out transferAmount) || transferAmount <= 0)
                    {
                        msg = "Invalid amount";
                        return(false);
                    }

                    if (!address.Blockchain.Equals(name))
                    {
                        msg = $"ethereum RegisterTransfer can only transfer ether.\nThis source is registered for {address.Blockchain}";
                        return(false);
                    }

                    string sourceAddress = EthECKey.GetPublicAddress(ethereumPrivateKey);

                    if (!sourceAddress.Equals(address.Address_, StringComparison.OrdinalIgnoreCase))
                    {
                        msg = "The deal is for a different client";
                        return(false);
                    }

                    var txCount = web3.Eth.Transactions.GetTransactionCount.SendRequestAsync(sourceAddress).Result;
                    TransactionSigner signer = new TransactionSigner();

                    // TODO maybe add a y/n choice for user to decline or configurable gas price override
                    var gasPrice = web3.Eth.GasPrice.SendRequestAsync().Result;
                    Console.WriteLine("gasPrice: " + gasPrice.Value.ToString());

                    string        to;
                    string        data;
                    BigInteger    amount;
                    HexBigInteger gasLimit;
                    if (erc20)
                    {
                        string creditcoinContract    = cfg["creditcoinContract"];
                        string creditcoinContractAbi = cfg["creditcoinContractAbi"];

                        to = creditcoinContract;

                        var contract      = web3.Eth.GetContract(creditcoinContractAbi, creditcoinContract);
                        var burn          = contract.GetFunction("exchange");
                        var functionInput = new object[] { transferAmount, address.Sighash };
                        data     = burn.GetData(functionInput);
                        gasLimit = burn.EstimateGasAsync(functionInput).Result;
                        fee      = gasLimit.Value * gasPrice.Value;
                        amount   = 0;
                    }
                    else
                    {
                        gasLimit = web3.Eth.Transactions.EstimateGas.SendRequestAsync(new Nethereum.RPC.Eth.DTOs.CallInput(registeredSourceId, destinationAddress, new Nethereum.Hex.HexTypes.HexBigInteger(transferAmount))).Result;
                        Console.WriteLine("gasLimit: " + gasLimit.Value.ToString());

                        fee    = gasLimit.Value * gasPrice.Value;
                        to     = destinationAddress;
                        data   = address.Sighash;
                        amount = transferAmount + fee;
                    }

                    string txRaw = signer.SignTransaction(ethereumPrivateKey, to, amount, txCount, gasPrice, gasLimit, data);
                    payTxId = web3.Eth.Transactions.SendRawTransaction.SendRequestAsync("0x" + txRaw).Result;
                    Console.WriteLine("Ethereum Transaction ID: " + payTxId);

                    File.WriteAllText(progress, $"{payTxId}:{fee.ToString()}");
                }

                inProgress = true;
                while (true)
                {
                    var receipt = web3.Eth.TransactionManager.TransactionReceiptService.PollForReceiptAsync(payTxId).Result;
                    if (receipt.BlockNumber != null)
                    {
                        var blockNumber = web3.Eth.Blocks.GetBlockNumber.SendRequestAsync().Result;
                        if (blockNumber.Value - receipt.BlockNumber.Value >= confirmationsExpected)
                        {
                            break;
                        }
                    }
                    Thread.Sleep(1000);
                }
                File.Delete(progress);
                inProgress = false;

                command = new string[] { command[0], registeredSourceId, amountString, erc20 ? "0" : fee.ToString(), payTxId, erc20 ? "creditcoin" : "1" };
                var tx = txBuilder.BuildTx(command, out msg);
                if (tx == null)
                {
                    return(false);
                }

                Debug.Assert(msg == null);

                var content = new ByteArrayContent(tx);
                content.Headers.Add("Content-Type", "application/octet-stream");

                msg = RpcHelper.CompleteBatch(httpClient, $"{url}/batches", content);

                return(true);
            }
            else
            {
                inProgress = false;
                msg        = "Unknown command: " + command[0];
                return(false);
            }
        }
Ejemplo n.º 4
0
        public bool Run(bool txid, IConfiguration cfg, HttpClient httpClient, ITxBuilder txBuilder, Dictionary <string, string> settings, string progressId, string pluginsFolder, string url, string[] command, out bool inProgress, out string msg)
        {
            Debug.Assert(command != null);
            if (command.Length < 2)
            {
                inProgress = false;
                msg        = "invalid parameter count";
                return(false);
            }

            if (command[0].Equals("registerTransfer", StringComparison.OrdinalIgnoreCase))
            {
                // bitcoin RegisterTransfer srcAddressId dstAddressId orderId amount sourceTxId

                string progress = Path.Combine(pluginsFolder, $"{name}_progress{progressId}.txt");

                inProgress = false;

                if (command.Length != 4)
                {
                    msg = "invalid parameter count";
                    return(false);
                }

                string gainString       = command[1];
                string orderId          = command[2];
                string sourceTxIdString = command[3];

                string secret = cfg["secret"];
                if (string.IsNullOrWhiteSpace(secret))
                {
                    msg = "bitcoin.secret is not set";
                    return(false);
                }
                string rpcAddress = cfg["rpc"];
                if (string.IsNullOrWhiteSpace(rpcAddress))
                {
                    msg = "bitcoin.rpc is not set";
                    return(false);
                }

                string credential = cfg["credential"];
                if (string.IsNullOrWhiteSpace(credential))
                {
                    msg = "bitcoin.credential is not set";
                    return(false);
                }

#if DEBUG
                string confirmationsCount = cfg["confirmationsCount"];
                if (int.TryParse(confirmationsCount, out int parsedCount))
                {
                    mConfirmationsExpected = parsedCount;
                }
#endif

                string feeString = cfg["fee"];
                if (string.IsNullOrWhiteSpace(feeString))
                {
                    msg = "bitcoin.fee is not set";
                    return(false);
                }
                if (!int.TryParse(feeString, out int fee))
                {
                    msg = "bitcoin.fee is not an int";
                    return(false);
                }

                var bitcoinPrivateKey = new BitcoinSecret(secret);
                var network           = bitcoinPrivateKey.Network;
                var rpcClient         = new RPCClient(credential, new Uri(rpcAddress), network);

                string srcAddressId;
                string dstAddressId;
                string amountString;

                var protobuf = RpcHelper.ReadProtobuf(httpClient, $"{url}/state/{orderId}", out msg);
                if (protobuf == null)
                {
                    msg = "failed to extract address data through RPC";
                    return(false);
                }
                if (orderId.StartsWith(RpcHelper.creditCoinNamespace + RpcHelper.dealOrderPrefix))
                {
                    var dealOrder = DealOrder.Parser.ParseFrom(protobuf);
                    if (gainString.Equals("0"))
                    {
                        srcAddressId = dealOrder.SrcAddress;
                        dstAddressId = dealOrder.DstAddress;
                    }
                    else
                    {
                        dstAddressId = dealOrder.SrcAddress;
                        srcAddressId = dealOrder.DstAddress;
                    }
                    amountString = dealOrder.Amount;
                }
                else if (orderId.StartsWith(RpcHelper.creditCoinNamespace + RpcHelper.repaymentOrderPrefix))
                {
                    var repaymentOrder = RepaymentOrder.Parser.ParseFrom(protobuf);
                    if (gainString.Equals("0"))
                    {
                        srcAddressId = repaymentOrder.SrcAddress;
                        dstAddressId = repaymentOrder.DstAddress;
                    }
                    else
                    {
                        dstAddressId = repaymentOrder.SrcAddress;
                        srcAddressId = repaymentOrder.DstAddress;
                    }
                    amountString = repaymentOrder.Amount;
                }
                else
                {
                    msg = "unexpected referred order";
                    return(false);
                }

                string  payTxIdHash;
                uint256 payTxId;
                if (File.Exists(progress))
                {
                    Console.WriteLine("Found unfinished action, retrying...");
                    payTxIdHash = File.ReadAllText(progress);
                    if (!uint256.TryParse(payTxIdHash, out payTxId))
                    {
                        msg = "corrupted progress file";
                        return(false);
                    }
                }
                else
                {
                    protobuf = RpcHelper.ReadProtobuf(httpClient, $"{url}/state/{srcAddressId}", out msg);
                    if (protobuf == null)
                    {
                        msg = "failed to extract address data through RPC";
                        return(false);
                    }

                    var srcAddress = Address.Parser.ParseFrom(protobuf);

                    protobuf = RpcHelper.ReadProtobuf(httpClient, $"{url}/state/{dstAddressId}", out msg);
                    if (protobuf == null)
                    {
                        msg = "failed to extract address data through RPC";
                        return(false);
                    }

                    var dstAddress = Address.Parser.ParseFrom(protobuf);

                    long transferAmount;
                    if (!long.TryParse(amountString, out transferAmount) || transferAmount <= 0)
                    {
                        msg = "Invalid amount";
                        return(false);
                    }
                    long gain;
                    if (!long.TryParse(gainString, out gain))
                    {
                        msg = "Invalid amount";
                        return(false);
                    }
                    if (transferAmount + gain < transferAmount)
                    {
                        msg = "Overflow";
                        return(false);
                    }

                    transferAmount = transferAmount + gain;

                    if (transferAmount < 0)
                    {
                        msg = "Invalid amount";
                        return(false);
                    }

                    if (transferAmount + fee < transferAmount)
                    {
                        msg = "Overflow";
                        return(false);
                    }

                    if (!srcAddress.Blockchain.Equals(name) || !dstAddress.Blockchain.Equals(name))
                    {
                        msg = $"bitcoin RegisterTransfer can only transfer bitcoins.\nThis source is registered for {srcAddress.Blockchain} and destination for {dstAddress.Blockchain}";
                        return(false);
                    }

                    var sourceAddress = bitcoinPrivateKey.GetAddress();
                    if (!sourceAddress.ToString().Equals(srcAddress.Value, StringComparison.OrdinalIgnoreCase))
                    {
                        msg = "The deal is for a different client";
                        return(false);
                    }

#if RELEASE
                    if (network != NBitcoin.Network.Main)
                    {
                        msg = "bitcoin.secret is not defined for main network";
                        return(false);
                    }
#endif

                    var sourceTxId          = uint256.Parse(sourceTxIdString);
                    var transactionResponse = rpcClient.GetRawTransaction(sourceTxId);

                    //TODO: fix (assuming that the transaction has one output intended for the private key with the exact amount)
                    var      receivedCoins   = transactionResponse.Outputs.AsCoins();
                    OutPoint outPointToSpend = null;
                    TxOut    outTxToSpend    = null;
                    foreach (var coin in receivedCoins)
                    {
                        if (coin.TxOut.ScriptPubKey == bitcoinPrivateKey.ScriptPubKey)
                        {
                            outPointToSpend = coin.Outpoint;
                            outTxToSpend    = coin.TxOut;
                            long amount = coin.Amount.Satoshi;
                            if (amount < transferAmount + fee)
                            {
                                msg = $"Invalid transaction - needed: {transferAmount} + {fee}, has: {amount.ToString()}";
                                return(false);
                            }
                            break;
                        }
                    }
                    if (outPointToSpend == null)
                    {
                        msg = "Invalid transaction - no outputs that the client can spend";
                        return(false);
                    }

                    var addressFrom = BitcoinAddress.Create(srcAddress.Value, network);
                    var addressTo   = BitcoinAddress.Create(dstAddress.Value, network);
                    var message     = orderId;
                    var bytes       = Encoding.UTF8.GetBytes(message);

                    var bitcoinTransactionBuilder = new TransactionBuilder();

                    var transaction = bitcoinTransactionBuilder
                                      .AddCoins(new Coin(outPointToSpend, outTxToSpend))
                                      .AddKeys(bitcoinPrivateKey)
                                      .Send(addressTo.ScriptPubKey, transferAmount)
                                      .Send(TxNullDataTemplate.Instance.GenerateScriptPubKey(bytes), Money.Zero)
                                      .SendFees(new Money(fee, MoneyUnit.Satoshi))
                                      .SetChange(addressFrom.ScriptPubKey)
                                      .BuildTransaction(true);

                    if (!bitcoinTransactionBuilder.Verify(transaction))
                    {
                        msg = "failed verify transaction";
                        return(false);
                    }

                    try
                    {
                        payTxId = rpcClient.SendRawTransaction(transaction);
                    }
                    catch (RPCException e)
                    {
                        msg = $"failed to broadcast - error: {e.RPCCode}, reason: {e.RPCCodeMessage}";
                        return(false);
                    }

                    if (payTxId == null)
                    {
                        msg = "failed to broadcast - unknown error";
                        return(false);
                    }

                    payTxIdHash = payTxId.ToString();
                    File.WriteAllText(progress, payTxIdHash);
                }

                inProgress = true;
                while (true)
                {
                    var transactionResponse = rpcClient.GetRawTransactionInfo(payTxId);
                    if (transactionResponse != null && transactionResponse.BlockHash != null && transactionResponse.Confirmations >= mConfirmationsExpected)
                    {
                        break;
                    }

                    Thread.Sleep(1000);
                }

                command = new string[] { command[0], gainString, orderId, payTxIdHash };
                var tx = txBuilder.BuildTx(command, out msg);
                Debug.Assert(tx != null);
                Debug.Assert(msg == null);

                var content = new ByteArrayContent(tx);
                content.Headers.Add("Content-Type", "application/octet-stream");

                msg = RpcHelper.CompleteBatch(httpClient, url, "batches", content, txid);

                File.Delete(progress);
                inProgress = false;

                return(true);
            }
            else
            {
                inProgress = false;
                msg        = "Unknown command: " + command[0];
                return(false);
            }
        }