Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions doc/REST-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,30 @@ Refer to the `getrawmempool` RPC help for details. Defaults to setting

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

#### Transaction Spending Previous Outputs
`GET /rest/txspendingprevout/<TXID>-<N>/<TXID>-<N>/.../<TXID>-<N>.json`

Scans the mempool to find transactions spending any of the given outputs.
Only supports JSON as output format.
Refer to the `gettxspendingprevout` RPC help for details.

For each queried output, returns an object containing:
- `txid`: the transaction id of the checked output
- `vout`: the output number
- `spendingtxid`: (optional) the transaction id of the mempool transaction spending this output (omitted if unspent)

Example:
```
$ curl localhost:8332/rest/txspendingprevout/a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0-3.json
[
{
"txid": "a08e6907dbbd3d809776dbfc5d82e371b764ed838b5655e72f463568df1aadf0",
"vout": 3,
"spendingtxid": "b2cdfd7b89def827ff8af7cd9bff7627ff72e5e8b0f71210f92ea7a4000c5d75"
}
]
```


Risks
-------------
Expand Down
68 changes: 68 additions & 0 deletions src/rest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,73 @@ static bool rest_mempool(const std::any& context, HTTPRequest* req, const std::s
}
}

static bool rest_txspendingprevout(const std::any& context, HTTPRequest* req, const std::string& uri_part)
{
if (!CheckWarmup(req))
return false;

std::string param;
const RESTResponseFormat rf = ParseDataFormat(param, uri_part);

std::vector<std::string> uriParts = SplitString(param, '/');

// Check that we have at least one outpoint
if (uriParts.size() == 0 || (uriParts.size() == 1 && uriParts[0].empty()))
return RESTERR(req, HTTP_BAD_REQUEST, "Error: empty request");

std::vector<COutPoint> prevouts;

// Parse outpoints from URI in the format txid-vout
for (size_t i = 0; i < uriParts.size(); i++)
{
const auto txid_out{util::Split<std::string_view>(uriParts[i], '-')};
if (txid_out.size() != 2) {
return RESTERR(req, HTTP_BAD_REQUEST, "Parse error");
}
auto txid{Txid::FromHex(txid_out.at(0))};
auto output{ToIntegral<uint32_t>(txid_out.at(1))};

if (!txid || !output.has_value()) {
return RESTERR(req, HTTP_BAD_REQUEST, "Parse error");
}

prevouts.emplace_back(*txid, *output);
}

switch (rf) {
case RESTResponseFormat::JSON: {
const CTxMemPool* mempool = GetMemPool(context, req);
if (!mempool) return false;

LOCK(mempool->cs);

UniValue result{UniValue::VARR};

for (const COutPoint& prevout : prevouts) {
UniValue o(UniValue::VOBJ);
o.pushKV("txid", prevout.hash.ToString());
o.pushKV("vout", (uint64_t)prevout.n);

const CTransaction* spendingTx = mempool->GetConflictTx(prevout);
if (spendingTx != nullptr) {
o.pushKV("spendingtxid", spendingTx->GetHash().ToString());
}

result.push_back(std::move(o));
}

std::string strJSON = result.write() + "\n";
req->WriteHeader("Content-Type", "application/json");
req->WriteReply(HTTP_OK, strJSON);
return true;
}

default: {
return RESTERR(req, HTTP_NOT_FOUND, "output format not found (available: json)");
}
}
}

static bool rest_tx(const std::any& context, HTTPRequest* req, const std::string& uri_part)
{
if (!CheckWarmup(req))
Expand Down Expand Up @@ -1130,6 +1197,7 @@ static const struct {
{"/rest/deploymentinfo", rest_deploymentinfo},
{"/rest/blockhashbyheight/", rest_blockhash_by_height},
{"/rest/spenttxouts/", rest_spent_txouts},
{"/rest/txspendingprevout/", rest_txspendingprevout},
};

void StartREST(const std::any& context)
Expand Down
53 changes: 53 additions & 0 deletions test/functional/interface_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,59 @@ def run_test(self):
for tx in txs:
assert tx in json_obj['tx']

self.log.info("Test the /txspendingprevout URI")

# Create two new transactions - one to spend and one spending transaction
# First, create a transaction that will have an output to spend
utxo_for_new_tx = self.wallet.get_utxo()
base_tx = self.wallet.send_self_transfer(from_node=self.nodes[0], utxo_to_spend=utxo_for_new_tx)
base_txid = base_tx['txid']
base_vout = 0 # The change output
self.sync_all()

# Now create a transaction that spends from base_tx
spending_tx = self.wallet.send_self_transfer(from_node=self.nodes[0], utxo_to_spend=self.wallet.get_utxo(txid=base_txid))
spending_txid = spending_tx['txid']
self.sync_all()

# Test with single outpoint - should find the spending transaction
json_obj = self.test_rest_request(f"/txspendingprevout/{base_txid}-{base_vout}")
assert_equal(len(json_obj), 1)
assert_equal(json_obj[0]['txid'], base_txid)
assert_equal(json_obj[0]['vout'], base_vout)
assert_equal(json_obj[0]['spendingtxid'], spending_txid)

# Test with unspent output - should not have spendingtxid
utxo_unspent = self.wallet.get_utxo()
json_obj = self.test_rest_request(f"/txspendingprevout/{utxo_unspent['txid']}-{utxo_unspent['vout']}")
assert_equal(len(json_obj), 1)
assert_equal(json_obj[0]['txid'], utxo_unspent['txid'])
assert_equal(json_obj[0]['vout'], utxo_unspent['vout'])
assert 'spendingtxid' not in json_obj[0]

# Test with multiple outpoints
json_obj = self.test_rest_request(f"/txspendingprevout/{base_txid}-{base_vout}/{utxo_unspent['txid']}-{utxo_unspent['vout']}")
assert_equal(len(json_obj), 2)
# First output is spent
assert_equal(json_obj[0]['txid'], base_txid)
assert_equal(json_obj[0]['vout'], base_vout)
assert_equal(json_obj[0]['spendingtxid'], spending_txid)
# Second output is unspent
assert_equal(json_obj[1]['txid'], utxo_unspent['txid'])
assert_equal(json_obj[1]['vout'], utxo_unspent['vout'])
assert 'spendingtxid' not in json_obj[1]

# Test with invalid outpoint format
resp = self.test_rest_request(f"/txspendingprevout/{INVALID_PARAM}", ret_type=RetType.OBJ, status=400)
assert_equal(resp.read().decode('utf-8').strip(), 'Parse error')

# Test with empty request
resp = self.test_rest_request("/txspendingprevout/", ret_type=RetType.OBJ, status=400)
assert_equal(resp.read().decode('utf-8').strip(), 'Error: empty request')

# Mine the transactions to clean up mempool
self.generate(self.nodes[0], 1)

self.log.info("Test the /chaininfo URI")

bb_hash = self.nodes[0].getbestblockhash()
Expand Down
Loading