Skip to content

Commit f178d37

Browse files
committed
rest: add interface for gettxspendingprevout rpc
This creates a REST interface for the gettxspendingprevout rpc and also includes functional tests
1 parent 4da0112 commit f178d37

File tree

3 files changed

+145
-0
lines changed

3 files changed

+145
-0
lines changed

doc/REST-interface.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,30 @@ Refer to the `getrawmempool` RPC help for details. Defaults to setting
150150

151151
*Query parameters for `verbose` and `mempool_sequence` available in 25.0 and up.*
152152

153+
#### Transaction Spending Previous Outputs
154+
`GET /rest/txspendingprevout/<TXID>-<N>/<TXID>-<N>/.../<TXID>-<N>.json`
155+
156+
Scans the mempool to find transactions spending any of the given outputs.
157+
Only supports JSON as output format.
158+
Refer to the `gettxspendingprevout` RPC help for details.
159+
160+
For each queried output, returns an object containing:
161+
- `txid`: the transaction id of the checked output
162+
- `vout`: the output number
163+
- `spendingtxid`: (optional) the transaction id of the mempool transaction spending this output (omitted if unspent)
164+
165+
Example:
166+
```
167+
$ curl localhost:8332/rest/txspendingprevout/a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0-3.json
168+
[
169+
{
170+
"txid": "a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0",
171+
"vout": 3,
172+
"spendingtxid": "b2cdfd7b89def827ff8af7cd9bff7627ff72e5e8b0f71210f92ea7a4000c5d75"
173+
}
174+
]
175+
```
176+
153177

154178
Risks
155179
-------------

src/rest.cpp

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,73 @@ static bool rest_mempool(const std::any& context, HTTPRequest* req, const std::s
810810
}
811811
}
812812

813+
static bool rest_txspendingprevout(const std::any& context, HTTPRequest* req, const std::string& uri_part)
814+
{
815+
if (!CheckWarmup(req))
816+
return false;
817+
818+
std::string param;
819+
const RESTResponseFormat rf = ParseDataFormat(param, uri_part);
820+
821+
std::vector<std::string> uriParts = SplitString(param, '/');
822+
823+
// Check that we have at least one outpoint
824+
if (uriParts.size() == 0 || (uriParts.size() == 1 && uriParts[0].empty()))
825+
return RESTERR(req, HTTP_BAD_REQUEST, "Error: empty request");
826+
827+
std::vector<COutPoint> prevouts;
828+
829+
// Parse outpoints from URI in the format txid-vout
830+
for (size_t i = 0; i < uriParts.size(); i++)
831+
{
832+
const auto txid_out{util::Split<std::string_view>(uriParts[i], '-')};
833+
if (txid_out.size() != 2) {
834+
return RESTERR(req, HTTP_BAD_REQUEST, "Parse error");
835+
}
836+
auto txid{Txid::FromHex(txid_out.at(0))};
837+
auto output{ToIntegral<uint32_t>(txid_out.at(1))};
838+
839+
if (!txid || !output.has_value()) {
840+
return RESTERR(req, HTTP_BAD_REQUEST, "Parse error");
841+
}
842+
843+
prevouts.emplace_back(*txid, *output);
844+
}
845+
846+
switch (rf) {
847+
case RESTResponseFormat::JSON: {
848+
const CTxMemPool* mempool = GetMemPool(context, req);
849+
if (!mempool) return false;
850+
851+
LOCK(mempool->cs);
852+
853+
UniValue result{UniValue::VARR};
854+
855+
for (const COutPoint& prevout : prevouts) {
856+
UniValue o(UniValue::VOBJ);
857+
o.pushKV("txid", prevout.hash.ToString());
858+
o.pushKV("vout", (uint64_t)prevout.n);
859+
860+
const CTransaction* spendingTx = mempool->GetConflictTx(prevout);
861+
if (spendingTx != nullptr) {
862+
o.pushKV("spendingtxid", spendingTx->GetHash().ToString());
863+
}
864+
865+
result.push_back(std::move(o));
866+
}
867+
868+
std::string strJSON = result.write() + "\n";
869+
req->WriteHeader("Content-Type", "application/json");
870+
req->WriteReply(HTTP_OK, strJSON);
871+
return true;
872+
}
873+
874+
default: {
875+
return RESTERR(req, HTTP_NOT_FOUND, "output format not found (available: json)");
876+
}
877+
}
878+
}
879+
813880
static bool rest_tx(const std::any& context, HTTPRequest* req, const std::string& uri_part)
814881
{
815882
if (!CheckWarmup(req))
@@ -1130,6 +1197,7 @@ static const struct {
11301197
{"/rest/deploymentinfo", rest_deploymentinfo},
11311198
{"/rest/blockhashbyheight/", rest_blockhash_by_height},
11321199
{"/rest/spenttxouts/", rest_spent_txouts},
1200+
{"/rest/txspendingprevout/", rest_txspendingprevout},
11331201
};
11341202

11351203
void StartREST(const std::any& context)

test/functional/interface_rest.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,59 @@ def run_test(self):
412412
for tx in txs:
413413
assert tx in json_obj['tx']
414414

415+
self.log.info("Test the /txspendingprevout URI")
416+
417+
# Create two new transactions - one to spend and one spending transaction
418+
# First, create a transaction that will have an output to spend
419+
utxo_for_new_tx = self.wallet.get_utxo()
420+
base_tx = self.wallet.send_self_transfer(from_node=self.nodes[0], utxo_to_spend=utxo_for_new_tx)
421+
base_txid = base_tx['txid']
422+
base_vout = 0 # The change output
423+
self.sync_all()
424+
425+
# Now create a transaction that spends from base_tx
426+
spending_tx = self.wallet.send_self_transfer(from_node=self.nodes[0], utxo_to_spend=self.wallet.get_utxo(txid=base_txid))
427+
spending_txid = spending_tx['txid']
428+
self.sync_all()
429+
430+
# Test with single outpoint - should find the spending transaction
431+
json_obj = self.test_rest_request(f"/txspendingprevout/{base_txid}-{base_vout}")
432+
assert_equal(len(json_obj), 1)
433+
assert_equal(json_obj[0]['txid'], base_txid)
434+
assert_equal(json_obj[0]['vout'], base_vout)
435+
assert_equal(json_obj[0]['spendingtxid'], spending_txid)
436+
437+
# Test with unspent output - should not have spendingtxid
438+
utxo_unspent = self.wallet.get_utxo()
439+
json_obj = self.test_rest_request(f"/txspendingprevout/{utxo_unspent['txid']}-{utxo_unspent['vout']}")
440+
assert_equal(len(json_obj), 1)
441+
assert_equal(json_obj[0]['txid'], utxo_unspent['txid'])
442+
assert_equal(json_obj[0]['vout'], utxo_unspent['vout'])
443+
assert 'spendingtxid' not in json_obj[0]
444+
445+
# Test with multiple outpoints
446+
json_obj = self.test_rest_request(f"/txspendingprevout/{base_txid}-{base_vout}/{utxo_unspent['txid']}-{utxo_unspent['vout']}")
447+
assert_equal(len(json_obj), 2)
448+
# First output is spent
449+
assert_equal(json_obj[0]['txid'], base_txid)
450+
assert_equal(json_obj[0]['vout'], base_vout)
451+
assert_equal(json_obj[0]['spendingtxid'], spending_txid)
452+
# Second output is unspent
453+
assert_equal(json_obj[1]['txid'], utxo_unspent['txid'])
454+
assert_equal(json_obj[1]['vout'], utxo_unspent['vout'])
455+
assert 'spendingtxid' not in json_obj[1]
456+
457+
# Test with invalid outpoint format
458+
resp = self.test_rest_request(f"/txspendingprevout/{INVALID_PARAM}", ret_type=RetType.OBJ, status=400)
459+
assert_equal(resp.read().decode('utf-8').strip(), 'Parse error')
460+
461+
# Test with empty request
462+
resp = self.test_rest_request("/txspendingprevout/", ret_type=RetType.OBJ, status=400)
463+
assert_equal(resp.read().decode('utf-8').strip(), 'Error: empty request')
464+
465+
# Mine the transactions to clean up mempool
466+
self.generate(self.nodes[0], 1)
467+
415468
self.log.info("Test the /chaininfo URI")
416469

417470
bb_hash = self.nodes[0].getbestblockhash()

0 commit comments

Comments
 (0)