diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 07faeb9b5f1..52c6b0839ba 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -325,6 +325,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "spv_claimhtlc", 3, "feerate" }, { "spv_refundhtlc", 2, "feerate" }, + { "spv_refundhtlcall", 1, "feerate" }, { "decodecustomtx", 1, "iswitness" }, { "setmockcheckpoint", 0, "height" }, diff --git a/src/spv/bitcoin/BRTransaction.cpp b/src/spv/bitcoin/BRTransaction.cpp index 43ece9ad67c..60e1469bb4e 100644 --- a/src/spv/bitcoin/BRTransaction.cpp +++ b/src/spv/bitcoin/BRTransaction.cpp @@ -659,7 +659,7 @@ int BRTransactionIsSigned(const BRTransaction *tx) // adds signatures to any inputs with NULL signatures that can be signed with any keys // forkId is 0 for bitcoin, 0x40 for b-cash, 0x4f for b-gold // returns true if tx is signed -int BRTransactionSign(BRTransaction *tx, int forkId, BRKey keys[], size_t keysCount, HTLCScriptType htlcType, const uint8_t* seed, const uint8_t *redeemScript) +int BRTransactionSign(BRTransaction *tx, int forkId, BRKey keys[], size_t keysCount, HTLCScriptType htlcType, const uint8_t* seed) { UInt160 pkh[keysCount]; size_t i, j; @@ -694,7 +694,7 @@ int BRTransactionSign(BRTransaction *tx, int forkId, BRKey keys[], size_t keysCo size_t elemsCount = BRScriptElements(elems, sizeof(elems)/sizeof(*elems), input->script, input->scriptLen); uint8_t pubKey[BRKeyPubKey(&keys[j], NULL, 0)]; size_t pkLen = BRKeyPubKey(&keys[j], pubKey, sizeof(pubKey)); - uint8_t sig[73], script[1 + sizeof(sig) + 1 + (seed ? seed[0] + (htlcType == ScriptTypeSeller ? 1 /* OP_1 */ : 0) + 1 /* pushdata */ + redeemScript[0]: sizeof(pubKey))]; + uint8_t sig[73], script[1 + sizeof(sig) + 1 + (seed ? seed[0] + (htlcType == ScriptTypeSeller ? 1 /* OP_1 */ : 0) + 1 /* pushdata */ + input->scriptLen /* length */ + input->script[0] : sizeof(pubKey))]; size_t sigLen, scriptLen; UInt256 md = UINT256_ZERO; @@ -747,7 +747,7 @@ int BRTransactionSign(BRTransaction *tx, int forkId, BRKey keys[], size_t keysCo } // Add redeemscript - scriptLen += BRScriptPushData(&script[scriptLen], sizeof(script) - scriptLen, &redeemScript[1], redeemScript[0]); + scriptLen += BRScriptPushData(&script[scriptLen], sizeof(script) - scriptLen, input->script, input->scriptLen); BRTxInputSetSignature(input, script, scriptLen); BRTxInputSetWitness(input, script, 0); diff --git a/src/spv/bitcoin/BRTransaction.h b/src/spv/bitcoin/BRTransaction.h index 3768ca5ff73..784508b76ad 100644 --- a/src/spv/bitcoin/BRTransaction.h +++ b/src/spv/bitcoin/BRTransaction.h @@ -142,7 +142,7 @@ int BRTransactionIsSigned(const BRTransaction *tx); // adds signatures to any inputs with NULL signatures that can be signed with any keys // forkId is 0 for bitcoin, 0x40 for b-cash, 0x4f for b-gold // returns true if tx is signed -int BRTransactionSign(BRTransaction *tx, int forkId, BRKey keys[], size_t keysCount, HTLCScriptType htlcType = ScriptTypeNone, const uint8_t* seed = nullptr, const uint8_t* redeemScript = nullptr); +int BRTransactionSign(BRTransaction *tx, int forkId, BRKey keys[], size_t keysCount, HTLCScriptType htlcType = ScriptTypeNone, const uint8_t* seed = nullptr); // true if tx meets IsStandard() rules: https://bitcoin.org/en/developer-guide#standard-transactions int BRTransactionIsStandard(const BRTransaction *tx); diff --git a/src/spv/bitcoin/BRWallet.cpp b/src/spv/bitcoin/BRWallet.cpp index bdfdcc69686..edda8d51fe3 100644 --- a/src/spv/bitcoin/BRWallet.cpp +++ b/src/spv/bitcoin/BRWallet.cpp @@ -230,12 +230,12 @@ static int _BRWalletContainsUserTx(BRWallet *wallet, const BRTransaction *tx, co return r; } -static bool _BRWalletContainsHTLCOutput(BRWallet *wallet, const BRTransaction *tx, size_t &output, const UInt160& addressFilter) +static bool _BRWalletContainsHTLCOutput(BRWallet *wallet, const BRTransaction *tx, std::vector &output, const UInt160& addressFilter) { bool r = false; const uint8_t *pkh; - for (size_t i = 0; ! r && i < tx->outCount; i++) + for (size_t i = 0; i < tx->outCount; i++) { pkh = BRScriptPKH(tx->outputs[i].script, tx->outputs[i].scriptLen); if (pkh) @@ -250,7 +250,7 @@ static bool _BRWalletContainsHTLCOutput(BRWallet *wallet, const BRTransaction *t continue; } - output = i; + output.push_back(i); r = true; } } @@ -345,15 +345,17 @@ std::vector> BRListHTLCReceived(BRWallet *wall { std::vector> htlcTransactions; BRTransaction *tx; - size_t output{0}; for (size_t i = 0; i < array_count(wallet->transactions); ++i) { + std::vector outputs; tx = wallet->transactions[i]; - if (_BRWalletContainsHTLCOutput(wallet, tx, output, addr)) + if (_BRWalletContainsHTLCOutput(wallet, tx, outputs, addr)) { - htlcTransactions.emplace_back(tx, output); + for (const auto& out : outputs) { + htlcTransactions.emplace_back(tx, out); + } } } diff --git a/src/spv/spv_rpc.cpp b/src/spv/spv_rpc.cpp index d8f74af9ba4..ea195c8d178 100644 --- a/src/spv/spv_rpc.cpp +++ b/src/spv/spv_rpc.cpp @@ -192,7 +192,7 @@ UniValue spv_createanchor(const JSONRPCRequest& request) result.pushKV("cost", cost); if (send) { result.pushKV("sendResult", sendResult); - result.pushKV("sendMessage", DecodeSendResult(sendResult)); + result.pushKV("sendMessage", sendResult != 0 ? DecodeSendResult(sendResult) : ""); } return result; @@ -1047,7 +1047,7 @@ UniValue spv_claimhtlc(const JSONRPCRequest& request) RPCResult{ "{\n" " \"txid\" (string) The transaction id\n" - " \"sendmessage\" (string) Send message result\n" + " \"sendmessage\" (string) Error message on failure\n" "}\n" }, RPCExamples{ @@ -1066,8 +1066,13 @@ UniValue spv_claimhtlc(const JSONRPCRequest& request) throw JSONRPCError(RPC_MISC_ERROR, "spv not connected"); } - return spv::pspv->CreateHTLCTransaction(pwallet, request.params[0].get_str().c_str(), request.params[1].get_str().c_str(), + const auto pair = spv::pspv->PrepareHTLCTransaction(pwallet, request.params[0].get_str().c_str(), request.params[1].get_str().c_str(), request.params[2].get_str(), request.params[3].isNull() ? spv::DEFAULT_BTC_FEE_PER_KB : request.params[3].get_int64(), true); + + UniValue result(UniValue::VOBJ); + result.pushKV("txid", pair.first); + result.pushKV("sendmessage", pair.second); + return result; } UniValue spv_refundhtlc(const JSONRPCRequest& request) @@ -1084,7 +1089,7 @@ UniValue spv_refundhtlc(const JSONRPCRequest& request) RPCResult{ "{\n" " \"txid\" (string) The transaction id\n" - " \"sendmessage\" (string) Send message result\n" + " \"sendmessage\" (string) Error message on failure\n" "}\n" }, RPCExamples{ @@ -1103,8 +1108,49 @@ UniValue spv_refundhtlc(const JSONRPCRequest& request) throw JSONRPCError(RPC_MISC_ERROR, "spv not connected"); } - return spv::pspv->CreateHTLCTransaction(pwallet, request.params[0].get_str().c_str(), request.params[1].get_str().c_str(), + const auto pair = spv::pspv->PrepareHTLCTransaction(pwallet, request.params[0].get_str().c_str(), request.params[1].get_str().c_str(), "", request.params[3].isNull() ? spv::DEFAULT_BTC_FEE_PER_KB : request.params[3].get_int64(), false); + + UniValue result(UniValue::VOBJ); + result.pushKV("txid", pair.first); + result.pushKV("sendmessage", pair.second); + return result; +} + +UniValue spv_refundhtlcall(const JSONRPCRequest& request) +{ + CWallet* const pwallet = GetWallet(request); + + RPCHelpMan{"spv_refundhtlcall", + "\nGets all HTLC contracts stored in wallet and creates refunds transactions for all that have expired\n", + { + {"destinationaddress", RPCArg::Type::STR, RPCArg::Optional::NO, "Destination for funds in the HTLC"}, + {"feerate", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "Feerate (satoshis) per KB (Default: " + std::to_string(spv::DEFAULT_BTC_FEE_PER_KB) + ")"}, + }, + RPCResult{ + "{\n" + " \"txid\" (string) The transaction id\n" + "}\n" + }, + RPCExamples{ + HelpExampleCli("spv_refundhtlcall", "100000") + + HelpExampleRpc("spv_refundhtlcall", "100000") + }, + }.Check(request); + + if (!spv::pspv) + { + throw JSONRPCError(RPC_INVALID_REQUEST, "spv module disabled"); + } + + if (!spv::pspv->IsConnected()) + { + throw JSONRPCError(RPC_MISC_ERROR, "spv not connected"); + } + + const auto feeRate = request.params[1].isNull() ? spv::DEFAULT_BTC_FEE_PER_KB : request.params[1].get_int64(); + + return spv::pspv->RefundAllHTLC(pwallet, request.params[0].get_str().c_str(), feeRate); } UniValue spv_fundaddress(const JSONRPCRequest& request) @@ -1546,6 +1592,7 @@ static const CRPCCommand commands[] = { "spv", "spv_createhtlc", &spv_createhtlc, { "seller_key", "refund_key", "hash", "timeout" } }, { "spv", "spv_claimhtlc", &spv_claimhtlc, { "scriptaddress", "destinationaddress", "seed", "feerate" } }, { "spv", "spv_refundhtlc", &spv_refundhtlc, { "scriptaddress", "destinationaddress", "feerate" } }, + { "spv", "spv_refundhtlcall", &spv_refundhtlcall, { "destinationaddress", "feerate" } }, { "spv", "spv_listhtlcoutputs", &spv_listhtlcoutputs, { "address" } }, { "spv", "spv_decodehtlcscript", &spv_decodehtlcscript, { "redeemscript" } }, { "spv", "spv_gethtlcseed", &spv_gethtlcseed, { "address" } }, diff --git a/src/spv/spv_wrapper.cpp b/src/spv/spv_wrapper.cpp index 3bc2c697f1b..ae377351691 100644 --- a/src/spv/spv_wrapper.cpp +++ b/src/spv/spv_wrapper.cpp @@ -682,18 +682,20 @@ int64_t CSpvWrapper::GetBitcoinBalance() } // Helper function to convert key and populate vector -void ConvertPrivKeys(std::vector &inputKeys, const CKey &key) +void ConvertPrivKeys(std::vector &inputKeys, const std::vector &keys) { - UInt256 rawKey; - memcpy(&rawKey, &(*key.begin()), key.size()); + for (const auto& key : keys) { + UInt256 rawKey; + memcpy(&rawKey, &(*key.begin()), key.size()); - BRKey inputKey; - if (!BRKeySetSecret(&inputKey, &rawKey, key.IsCompressed())) - { - throw JSONRPCError(RPC_WALLET_ERROR, "Failed to create SPV private key"); - } + BRKey inputKey; + if (!BRKeySetSecret(&inputKey, &rawKey, key.IsCompressed())) + { + throw JSONRPCError(RPC_WALLET_ERROR, "Failed to create SPV private key"); + } - inputKeys.push_back(inputKey); + inputKeys.push_back(inputKey); + } } UniValue CSpvWrapper::SendBitcoins(CWallet* const pwallet, std::string address, int64_t amount, uint64_t feeRate) @@ -745,7 +747,7 @@ UniValue CSpvWrapper::SendBitcoins(CWallet* const pwallet, std::string address, throw JSONRPCError(RPC_WALLET_ERROR, "Failed to get address private key."); } - ConvertPrivKeys(inputKeys, vchSecret); + ConvertPrivKeys(inputKeys, {vchSecret}); } if (!BRTransactionSign(tx, 0, inputKeys.data(), inputKeys.size())) { @@ -765,7 +767,7 @@ UniValue CSpvWrapper::SendBitcoins(CWallet* const pwallet, std::string address, UniValue result(UniValue::VOBJ); result.pushKV("txid", txid); - result.pushKV("sendmessage", DecodeSendResult(sendResult)); + result.pushKV("sendmessage", sendResult != 0 ? DecodeSendResult(sendResult) : ""); return result; } @@ -883,6 +885,11 @@ UniValue CSpvWrapper::GetHTLCReceived(const std::string& addr) auto htlcTransactions = BRListHTLCReceived(wallet, addressFilter); + // Sort by Bitcoin address + std::sort(htlcTransactions.begin(), htlcTransactions.end(), [](const std::pair& lhs, const std::pair& rhs){ + return strcmp(lhs.first->outputs[lhs.second].address, rhs.first->outputs[rhs.second].address) < 0; + }); + UniValue result(UniValue::VARR); for (const auto& txInfo : htlcTransactions) { @@ -1031,6 +1038,27 @@ HTLCDetails GetHTLCDetails(CScript& redeemScript) return script; } +HTLCDetails GetHTLCScript(CWallet* const pwallet, const uint160& hash160, CScript &redeemScript) +{ + // Read redeem script from wallet DB + WalletBatch batch(pwallet->GetDBHandle()); + if (!batch.ReadCScript(hash160, redeemScript)) + { + throw JSONRPCError(RPC_WALLET_ERROR, "Redeem script not found in wallet"); + } + + // Make sure stored script is at least expected length. + const unsigned int minScriptLength{110}; // With compressed public keys and small int block count + const unsigned int maxScriptLength{177}; // With uncompressed public keys and four bits for block count (1 size / 3 value) + if (redeemScript.size() < minScriptLength && redeemScript.size() > maxScriptLength) + { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Stored redeem script incorrect length, rerun spv_createhtlc"); + } + + // Get script details + return GetHTLCDetails(redeemScript); +} + HTLCDetails HTLCScriptRequest(CWallet* const pwallet, const char* address, CScript &redeemScript) { // Validate HTLC address @@ -1050,23 +1078,7 @@ HTLCDetails HTLCScriptRequest(CWallet* const pwallet, const char* address, CScri uint160 hash160; memcpy(&hash160, &data[1], sizeof(uint160)); - // Read redeem script from wallet DB - WalletBatch batch(pwallet->GetDBHandle()); - if (!batch.ReadCScript(hash160, redeemScript)) - { - throw JSONRPCError(RPC_WALLET_ERROR, "Redeem script not found in wallet"); - } - - // Make sure stored script is at least expected length. - const unsigned int minScriptLength{110}; // With compressed public keys and small int block count - const unsigned int maxScriptLength{177}; // With uncompressed public keys and four bits for block count (1 size / 3 value) - if (redeemScript.size() < minScriptLength && redeemScript.size() > maxScriptLength) - { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Stored redeem script incorrect length, rerun spv_createhtlc"); - } - - // Get script details - return GetHTLCDetails(redeemScript); + return GetHTLCScript(pwallet, hash160, redeemScript); } UniValue CSpvWrapper::GetAddressPubkey(const CWallet* pwallet, const char *addr) @@ -1164,11 +1176,11 @@ struct TxOutput { TBytes script; }; -BRTransaction* CreateTx(std::vector const & inputs, std::vector const & outputs, uint32_t version = TX_VERSION, uint32_t sequence = TXIN_SEQUENCE) +BRTransaction* CreateTx(std::vector> const & inputs, std::vector const & outputs, uint32_t version = TX_VERSION) { BRTransaction *tx = BRTransactionNew(version); for (auto input : inputs) { - BRTransactionAddInput(tx, input.txHash, input.index, input.amount, input.script.data(), input.script.size(), NULL, 0, NULL, 0, sequence); + BRTransactionAddInput(tx, input.first.txHash, input.first.index, input.first.amount, input.first.script.data(), input.first.script.size(), NULL, 0, NULL, 0, input.second); } for (auto output : outputs) { BRTransactionAddOutput(tx, output.amount, output.script.data(), output.script.size()); @@ -1176,7 +1188,7 @@ BRTransaction* CreateTx(std::vector const & inputs, std::vector const & inputs, std::vector const & outputs) +TBytes CreateRawTx(std::vector> const & inputs, std::vector const & outputs) { BRTransaction *tx = CreateTx(inputs, outputs); size_t len = BRTransactionSerialize(tx, NULL, 0); @@ -1186,8 +1198,22 @@ TBytes CreateRawTx(std::vector const & inputs, std::vector co return len ? buf : TBytes{}; } -UniValue CSpvWrapper::CreateHTLCTransaction(CWallet* const pwallet, const char* scriptAddress, const char *destinationAddress, const std::string& seed, uint64_t feerate, bool seller) +std::pair CSpvWrapper::PrepareHTLCTransaction(CWallet* const pwallet, const char* scriptAddress, const char *destinationAddress, const std::string& seed, uint64_t feerate, bool seller) +{ + // Get redeemscript and parsed details from script + CScript redeemScript; + const auto scriptDetails = HTLCScriptRequest(pwallet, scriptAddress, redeemScript); + + return CreateHTLCTransaction(pwallet, {{scriptDetails, redeemScript}}, destinationAddress, seed, feerate, seller); +} + +std::pair CSpvWrapper::CreateHTLCTransaction(CWallet* const pwallet, const std::vector>& scriptDetails, const char *destinationAddress, const std::string& seed, uint64_t feerate, bool seller) { + // Should not get here but let's double check to avoid segfault. + if (scriptDetails.empty()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Redeem script details not found."); + } + // Validate HTLC address if (!BRAddressIsValid(destinationAddress)) { @@ -1200,10 +1226,6 @@ UniValue CSpvWrapper::CreateHTLCTransaction(CWallet* const pwallet, const char* throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Provided seed is not in hex form"); } - // Get redeemscript and parsed details from script - CScript redeemScript; - auto scriptDetails = HTLCScriptRequest(pwallet, scriptAddress, redeemScript); - // Calculate seed hash and make sure it matches hash in contract auto seedBytes = ParseHex(seed); if (seller) @@ -1213,58 +1235,85 @@ UniValue CSpvWrapper::CreateHTLCTransaction(CWallet* const pwallet, const char* hash.Write(seedBytes.data(), seedBytes.size()); hash.Finalize(calcSeedBytes.data()); - if (scriptDetails.hash != calcSeedBytes) + // Only one script expected on seller + if (scriptDetails.begin()->first.hash != calcSeedBytes) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Seed provided does not match seed hash in contract"); } } - // Get private key - CKey key; - if (!pwallet->GetKey(seller ? scriptDetails.sellerKey.GetID() : scriptDetails.buyerKey.GetID(), key)) - { - throw JSONRPCError(RPC_WALLET_ERROR, "Private key for seller pubkey " + HexStr(scriptDetails.sellerKey) + " is not known"); - } - - // Convert private key to SPV key + // Private keys + std::vector sourceKeys; std::vector inputKeys; - ConvertPrivKeys(inputKeys, key); - // Get script address and any outputs related to it - CScriptID innerID(redeemScript); - UInt160 addressFilter; - UIntConvert(innerID.begin(), addressFilter); - auto htlcTransactions = BRListHTLCReceived(wallet, addressFilter); - - // Create inputs - std::vector inputs; - uint256 spent; - std::vector script(redeemScript.begin(), redeemScript.end()); + // Inputs + std::vector> inputs; CAmount inputTotal{0}; + int64_t sigSize{0}; - for (const auto& output : htlcTransactions) - { - if (!BRWalletTxSpent(wallet, output.first, output.second, spent)) + for (const auto& script : scriptDetails) { + + // Get private keys + CKey privKey; + if (!pwallet->GetKey(seller ? script.first.sellerKey.GetID() : script.first.buyerKey.GetID(), privKey)) { + continue; + } + sourceKeys.push_back(privKey); + + // Get script address + CScriptID innerID(script.second); + UInt160 addressFilter; + UIntConvert(innerID.begin(), addressFilter); + + // Find related outputs + const auto htlcTransactions = BRListHTLCReceived(wallet, addressFilter); + + uint256 spent; + std::vector redeemScript(script.second.begin(), script.second.end()); + + // Loop over HTLC TXs and create inputs + for (const auto& output : htlcTransactions) { - inputs.push_back({output.first->txHash, static_cast(output.second), output.first->outputs[output.second].amount, script}); - inputTotal += output.first->outputs[output.second].amount; + // Skip outputs without enough confirms to meet contract requirements + if (!seller) { + uint32_t blockHeight = ReadTxBlockHeight(to_uint256(output.first->txHash)); + uint64_t confirmations = blockHeight != std::numeric_limits::max() ? spv::pspv->GetLastBlockHeight() - blockHeight + 1 : 0; + if (confirmations < script.first.locktime) { + continue; + } + } + + // If output unspent add as input for HTLC TX + if (!BRWalletTxSpent(wallet, output.first, output.second, spent)) + { + inputs.push_back({{output.first->txHash, static_cast(output.second), output.first->outputs[output.second].amount, redeemScript}, + seller ? TXIN_SEQUENCE : script.first.locktime}); + inputTotal += output.first->outputs[output.second].amount; + sigSize += 73 /* sig */ + 1 /* sighash */ + seedBytes.size() + 1 /* OP_1 || size */ + 1 /* pushdata */ + redeemScript.size(); + } } } + if (sourceKeys.empty()) { + throw JSONRPCError(RPC_WALLET_ERROR, "Private key relating to a HTLC pubkey is not available in the wallet"); + } + if (inputs.empty()) { throw JSONRPCError(RPC_WALLET_ERROR, "No unspent HTLC outputs found"); } + // Convert source private key to SPV private keys + ConvertPrivKeys(inputKeys, sourceKeys); + // Create output std::vector outputs; outputs.push_back({P2PKH_DUST, CreateScriptForAddress(destinationAddress)}); // Create transaction - auto tx = CreateTx(inputs, outputs, TX_VERSION_V2, seller ? TXIN_SEQUENCE : scriptDetails.locktime); + auto tx = CreateTx(inputs, outputs, TX_VERSION_V2); // Calculate and set fee - int64_t sigSize = 73 /* sig */ + 1 /* sighash */ + seedBytes.size() + 1 /* OP_1 || size */ + 1 /* pushdata */ + script.size(); feerate = std::max(feerate, BRWalletFeePerKb(wallet)); CAmount const minFee = BRTransactionHTLCSize(tx, sigSize) * feerate / TX_FEE_PER_KB; @@ -1277,11 +1326,8 @@ UniValue CSpvWrapper::CreateHTLCTransaction(CWallet* const pwallet, const char* // Add seed length seedBytes.insert(seedBytes.begin(), static_cast(seedBytes.size())); - // Add redeemscript length - script.insert(script.begin(), static_cast(script.size())); - // Sign transaction - if (!BRTransactionSign(tx, 0, inputKeys.data(), inputKeys.size(), seller ? ScriptTypeSeller : ScriptTypeBuyer, seedBytes.data(), script.data())) + if (!BRTransactionSign(tx, 0, inputKeys.data(), inputKeys.size(), seller ? ScriptTypeSeller : ScriptTypeBuyer, seedBytes.data())) { BRTransactionFree(tx); throw JSONRPCError(RPC_WALLET_ERROR, "Failed to sign transaction."); @@ -1300,9 +1346,33 @@ UniValue CSpvWrapper::CreateHTLCTransaction(CWallet* const pwallet, const char* sendResult = EPARSINGTX; } - UniValue result(UniValue::VOBJ); - result.pushKV("txid", txid); - result.pushKV("sendmessage", DecodeSendResult(sendResult)); + return {txid, sendResult != 0 ? DecodeSendResult(sendResult) : ""}; +} + +UniValue CSpvWrapper::RefundAllHTLC(CWallet* const pwallet, const char *destinationAddress, uint64_t feeRate) +{ + // Get all HTLC scripts + std::set htlcAddresses; + const auto wallets = GetWallets(); + for (const auto& item : wallets) { + for (const auto& entry : item->mapAddressBook) { + if (entry.second.purpose == "htlc") { + htlcAddresses.insert(*boost::get(&entry.first)); + } + } + } + + // Loop over HTLC addresses and get HTLC details + std::vector> scriptDetails; + for (const auto& address : htlcAddresses) { + CScript script; + scriptDetails.emplace_back(GetHTLCScript(pwallet, address, script), script); + } + + const auto pair = spv::pspv->CreateHTLCTransaction(pwallet, scriptDetails, destinationAddress, "", feeRate, false); + + UniValue result(UniValue::VARR); + result.push_back(pair.first); return result; } @@ -1369,7 +1439,7 @@ uint64_t EstimateAnchorCost(TBytes const & meta, uint64_t feerate) outputs.push_back({ P2PKH_DUST, dummyScript}); TxInput dummyInput { toUInt256("1111111111111111111111111111111111111111111111111111111111111111"), 0, 1000000, dummyScript }; - auto rawtx = CreateRawTx({dummyInput}, outputs); + auto rawtx = CreateRawTx({{dummyInput, TXIN_SEQUENCE}}, outputs); BRTransaction *tx = BRTransactionParse(rawtx.data(), rawtx.size()); if (!tx) { LogPrint(BCLog::SPV, "***FAILED*** %s:\n", __func__); @@ -1388,7 +1458,7 @@ std::tuple CreateAnchorTx(std::vector co assert(meta.size() > 0); uint64_t inputTotal = 0; - std::vector inputs; + std::vector> inputs; std::vector inputKeys; for (TxInputData const & input : inputsData) { UInt256 inHash = UInt256Reverse(toUInt256(input.txhash.c_str())); @@ -1406,7 +1476,7 @@ std::tuple CreateAnchorTx(std::vector co TBytes inputScript(CreateScriptForAddress(address.s)); inputTotal += input.amount; - inputs.push_back({ inHash, input.txn, input.amount, inputScript}); + inputs.push_back({{ inHash, input.txn, input.amount, inputScript}, TXIN_SEQUENCE}); } auto consensus = Params().GetConsensus(); @@ -1460,7 +1530,7 @@ std::tuple CreateAnchorTx(std::vector co auto change = inputTotal - totalCost; if (change > P2PKH_DUST) { - BRTransactionAddOutput(tx, change, inputs[0].script.data(), inputs[0].script.size()); + BRTransactionAddOutput(tx, change, inputs[0].first.script.data(), inputs[0].first.script.size()); totalCost += 34; // 34 is an estimated cost of change output itself } else { diff --git a/src/spv/spv_wrapper.h b/src/spv/spv_wrapper.h index d20c34a87ba..8bb0365092c 100644 --- a/src/spv/spv_wrapper.h +++ b/src/spv/spv_wrapper.h @@ -169,7 +169,9 @@ class CSpvWrapper // Bitcoin HTLC calls UniValue GetHTLCReceived(const std::string &addr); std::string GetHTLCSeed(uint8_t* md20); - UniValue CreateHTLCTransaction(CWallet* const pwallet, const char *scriptAddress, const char *destinationAddress, const std::string& seed, uint64_t feerate, bool seller); + std::pair PrepareHTLCTransaction(CWallet* const pwallet, const char *scriptAddress, const char *destinationAddress, const std::string& seed, uint64_t feerate, bool seller); + std::pair CreateHTLCTransaction(CWallet* const pwallet, const std::vector>& scriptDetails, const char *destinationAddress, const std::string& seed, uint64_t feerate, bool seller); + UniValue RefundAllHTLC(CWallet* const pwallet, const char *destinationAddress, uint64_t feeRate); // Get and set DB version int GetDBVersion(); diff --git a/test/functional/feature_bitcoin_htlc.py b/test/functional/feature_bitcoin_htlc.py index 9c1a77fb2a4..85fd866582a 100755 --- a/test/functional/feature_bitcoin_htlc.py +++ b/test/functional/feature_bitcoin_htlc.py @@ -48,21 +48,21 @@ def run_test(self): self.nodes[0].spv_createhtlc("0224e7de2f3a9d4cdc4fdc14601c75176287297c212aae9091404956955f1aea86", "035fb3eadde611a39036e61d4c8288d1b896f2c94cee49e60a3d1c02236f4be490", "4194304", seed_hash) except JSONRPCException as e: errorString = e.error['message'] - assert("Invalid block denominated relative timeout" in errorString) + assert("Invalid block denominated relative timeout" in errorString) # Try annd create a HTLC script below min blocks try: self.nodes[0].spv_createhtlc("0224e7de2f3a9d4cdc4fdc14601c75176287297c212aae9091404956955f1aea86", "035fb3eadde611a39036e61d4c8288d1b896f2c94cee49e60a3d1c02236f4be490", "8", seed_hash) except JSONRPCException as e: errorString = e.error['message'] - assert("Timeout below minimum of" in errorString) + assert("Timeout below minimum of" in errorString) # Try annd create a HTLC script with incorrect pubkey try: self.nodes[0].spv_createhtlc("0224e7de2f3a9d4cdc4fdc14601c75176287297c212aae9091404956955f1aea", "035fb3eadde611a39036e61d4c8288d1b896f2c94cee49e60a3d1c02236f4be490", "10", seed_hash) except JSONRPCException as e: errorString = e.error['message'] - assert("Invalid public key" in errorString) + assert("Invalid public key" in errorString) # Create and learn HTLC script htlc_script = self.nodes[0].spv_createhtlc("0224e7de2f3a9d4cdc4fdc14601c75176287297c212aae9091404956955f1aea86", "035fb3eadde611a39036e61d4c8288d1b896f2c94cee49e60a3d1c02236f4be490", "10", seed_hash) @@ -83,11 +83,11 @@ def run_test(self): self.nodes[0].spv_claimhtlc("2N1WoHKzHY59uNpXouLQc32h9k5Y3hXK4Ku", address, seed, 1000) except JSONRPCException as e: errorString = e.error['message'] - assert("No unspent HTLC outputs found" in errorString) + assert("No unspent HTLC outputs found" in errorString) # Send to contract for seller claim result = self.nodes[0].spv_sendtoaddress(htlc_script['address'], 0.1) - assert_equal(result['sendmessage'], "Success") + assert_equal(result['sendmessage'], "") # Make sure output present in HTLC address output = self.nodes[0].spv_listhtlcoutputs("2N1WoHKzHY59uNpXouLQc32h9k5Y3hXK4Ku") @@ -99,18 +99,18 @@ def run_test(self): self.nodes[0].spv_claimhtlc("2N1WoHKzHY59uNpXouLQc32h9k5Y3hXK4Ku", address, "deadbeef", 1000) except JSONRPCException as e: errorString = e.error['message'] - assert("Seed provided does not match seed hash in contract" in errorString) + assert("Seed provided does not match seed hash in contract" in errorString) - # Try and claim with unknown script address + # Try and claim with unknown script address try: self.nodes[0].spv_claimhtlc("2NGT3gZvc75NX8DWGqfuEvniHGj5LiY33Ui", address, "deadbeef", 1000) except JSONRPCException as e: errorString = e.error['message'] - assert("Redeem script not found in wallet" in errorString) + assert("Redeem script not found in wallet" in errorString) # seller claim HTLC result = self.nodes[0].spv_claimhtlc("2N1WoHKzHY59uNpXouLQc32h9k5Y3hXK4Ku", address, seed, 1000) - assert_equal(result['sendmessage'], "Success") + assert_equal(result['sendmessage'], "") # Check output spent output = self.nodes[0].spv_listhtlcoutputs("2N1WoHKzHY59uNpXouLQc32h9k5Y3hXK4Ku") @@ -125,7 +125,7 @@ def run_test(self): # Send to contract for buyer refund result = self.nodes[0].spv_sendtoaddress(htlc_script['address'], 0.1) - assert_equal(result['sendmessage'], "Success") + assert_equal(result['sendmessage'], "") # Make sure output present in HTLC address output = self.nodes[0].spv_listhtlcoutputs("2N1WoHKzHY59uNpXouLQc32h9k5Y3hXK4Ku") @@ -133,9 +133,21 @@ def run_test(self): assert_equal(output[0]['amount'], Decimal("0.1")) assert_equal(output[1]['amount'], Decimal("0.1")) + # Try and refund before expiration + try: + self.nodes[0].spv_refundhtlc("2N1WoHKzHY59uNpXouLQc32h9k5Y3hXK4Ku", address, 1000) + except JSONRPCException as e: + errorString = e.error['message'] + assert("No unspent HTLC outputs found" in errorString) + + # Move confirtmation count to meet refund requirement + self.nodes[0].spv_setlastheight(10) + + print(self.nodes[0].spv_listhtlcoutputs("2N1WoHKzHY59uNpXouLQc32h9k5Y3hXK4Ku")) + # seller claim HTLC result = self.nodes[0].spv_refundhtlc("2N1WoHKzHY59uNpXouLQc32h9k5Y3hXK4Ku", address, 1000) - assert_equal(result['sendmessage'], "Success") + assert_equal(result['sendmessage'], "") # Check outputs spent output = self.nodes[0].spv_listhtlcoutputs("2N1WoHKzHY59uNpXouLQc32h9k5Y3hXK4Ku") @@ -163,7 +175,7 @@ def run_test(self): # Send multiple TX to script address for _ in range(3): result = self.nodes[0].spv_sendtoaddress(htlc_script['address'], 0.1) - assert_equal(result['sendmessage'], "Success") + assert_equal(result['sendmessage'], "") # Make sure output present in HTLC address output = self.nodes[0].spv_listhtlcoutputs(htlc_script['address']) @@ -174,7 +186,7 @@ def run_test(self): # seller claim HTLC result = self.nodes[0].spv_claimhtlc(htlc_script['address'], address, seed, 1000) - assert_equal(result['sendmessage'], "Success") + assert_equal(result['sendmessage'], "") # Check output spent output = self.nodes[0].spv_listhtlcoutputs(htlc_script['address']) @@ -194,7 +206,7 @@ def run_test(self): # Send multiple TX to script address for _ in range(3): result = self.nodes[0].spv_sendtoaddress(htlc_script['address'], 0.1) - assert_equal(result['sendmessage'], "Success") + assert_equal(result['sendmessage'], "") # Make sure output present in HTLC address output = self.nodes[0].spv_listhtlcoutputs(htlc_script['address']) @@ -203,9 +215,12 @@ def run_test(self): assert_equal(output[1]['amount'], Decimal("0.1")) assert_equal(output[2]['amount'], Decimal("0.1")) + # Move confirtmation count to meet refund requirement + self.nodes[0].spv_setlastheight(20) + # seller claim HTLC result = self.nodes[0].spv_refundhtlc(htlc_script['address'], address, 1000) - assert_equal(result['sendmessage'], "Success") + assert_equal(result['sendmessage'], "") # Check output spent output = self.nodes[0].spv_listhtlcoutputs(htlc_script['address']) diff --git a/test/functional/feature_bitcoin_wallet.py b/test/functional/feature_bitcoin_wallet.py index 1c850e4142b..ee443b55b08 100755 --- a/test/functional/feature_bitcoin_wallet.py +++ b/test/functional/feature_bitcoin_wallet.py @@ -49,7 +49,7 @@ def run_test(self): # Send to external address dummy_address = "bcrt1qfpnmx6jrn30yvscrw9spudj5aphyrc8es6epva" result = self.nodes[0].spv_sendtoaddress(dummy_address, 0.1) - assert_equal(result['sendmessage'], "Success") + assert_equal(result['sendmessage'], "") # Make sure tx is present in wallet txs = self.nodes[0].spv_listtransactions() @@ -61,7 +61,7 @@ def run_test(self): # Send to self result = self.nodes[0].spv_sendtoaddress(address, 0.1) - assert_equal(result['sendmessage'], "Success") + assert_equal(result['sendmessage'], "") # Make sure tx is present in wallet txs = self.nodes[0].spv_listtransactions()