Skip to content

Commit b650c91

Browse files
committed
Merge #21141: wallet: Add new format string placeholders for walletnotify
06e1fb0 Add new format string placeholders for walletnotify to include relevant block information for transactions (Maayan Keshet) Pull request description: This patch includes two new format placeholders for walletnotify: %b - the hash of the block containting the transaction (zeroed if a mempool transaction) %h - the height of the block containing the transaction (zero if a mempool transaction) I've included test suite changes to check and validate the above functional requirements as well as doc/help description changes. **Motivation** The walletnotify option is used to be notified of new transactions relevant to the wallet of the node. A common usage pattern is to perform afterwards additional RPC calls to determine: 1. If this is a mempool transaction or not (i.e. are there any confirmations?) 2. What block was it included in? 3. Did this transaction was seen before and is now seen again because of a fork? All of these questions can be answered with the current features, but the resulting RPC calls may be expensive in a heavily used node. As this information is readily available when calling the walletnotify callback, it makes sense to save expensive round trips by optionally sending this information at that point in time. I can definitely say we would like to use it in Fireblocks, my employer. Please let me know of any questions and suggestions. ACKs for top commit: laanwj: ACK 06e1fb0 Tree-SHA512: d2744e2a7a883f9c3a9fd32237110e048c4b6b11fea8221c33d10b74157f65bbc4351211f441e8c1a4af5d5d38e2ba6b1943a7673dc18860c0553d7b41e00775
2 parents 67ec26c + 06e1fb0 commit b650c91

File tree

3 files changed

+43
-21
lines changed

3 files changed

+43
-21
lines changed

src/wallet/init.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ void WalletInit::AddWalletOptions(ArgsManager& argsman) const
7070
argsman.AddArg("-walletbroadcast", strprintf("Make the wallet broadcast transactions (default: %u)", DEFAULT_WALLETBROADCAST), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
7171
argsman.AddArg("-walletdir=<dir>", "Specify directory to hold wallets (default: <datadir>/wallets if it exists, otherwise <datadir>)", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::WALLET);
7272
#if HAVE_SYSTEM
73-
argsman.AddArg("-walletnotify=<cmd>", "Execute command when a wallet transaction changes. %s in cmd is replaced by TxID and %w is replaced by wallet name. %w is not currently implemented on windows. On systems where %w is supported, it should NOT be quoted because this would break shell escaping used to invoke the command.", ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
73+
argsman.AddArg("-walletnotify=<cmd>", "Execute command when a wallet transaction changes. %s in cmd is replaced by TxID, %w is replaced by wallet name, %b is replaced by the hash of the block including the transaction (set to 'unconfirmed' if the transaction is not included) and %h is replaced by the block height (-1 if not included). %w is not currently implemented on windows. On systems where %w is supported, it should NOT be quoted because this would break shell escaping used to invoke the command.", ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
7474
#endif
7575
argsman.AddArg("-walletrbf", strprintf("Send transactions with full-RBF opt-in enabled (RPC only, default: %u)", DEFAULT_WALLET_RBF), ArgsManager::ALLOW_ANY, OptionsCategory::WALLET);
7676

src/wallet/wallet.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,14 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const CWalletTx::Confirmatio
944944
if (!strCmd.empty())
945945
{
946946
boost::replace_all(strCmd, "%s", hash.GetHex());
947+
if (confirm.status == CWalletTx::Status::CONFIRMED)
948+
{
949+
boost::replace_all(strCmd, "%b", confirm.hashBlock.GetHex());
950+
boost::replace_all(strCmd, "%h", ToString(confirm.block_height));
951+
} else {
952+
boost::replace_all(strCmd, "%b", "unconfirmed");
953+
boost::replace_all(strCmd, "%h", "-1");
954+
}
947955
#ifndef WIN32
948956
// Substituting the wallet name isn't currently supported on windows
949957
// because windows shell escaping has not been implemented yet:

test/functional/feature_notifications.py

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
FILE_CHAR_START = 32 if os.name == 'nt' else 1
1818
FILE_CHAR_END = 128
1919
FILE_CHARS_DISALLOWED = '/\\?%*:|"<>' if os.name == 'nt' else '/'
20-
20+
UNCONFIRMED_HASH_STRING = 'unconfirmed'
2121

2222
def notify_outputname(walletname, txid):
2323
return txid if os.name == 'nt' else '{}_{}'.format(walletname, txid)
@@ -43,7 +43,7 @@ def setup_network(self):
4343
"-blocknotify=echo > {}".format(os.path.join(self.blocknotify_dir, '%s')),
4444
], [
4545
"-rescan",
46-
"-walletnotify=echo > {}".format(os.path.join(self.walletnotify_dir, notify_outputname('%w', '%s'))),
46+
"-walletnotify=echo %h_%b > {}".format(os.path.join(self.walletnotify_dir, notify_outputname('%w', '%s'))),
4747
]]
4848
self.wallet_names = [self.default_wallet_name, self.wallet]
4949
super().setup_network()
@@ -90,11 +90,9 @@ def run_test(self):
9090
self.wait_until(lambda: len(os.listdir(self.walletnotify_dir)) == block_count, timeout=10)
9191

9292
# directory content should equal the generated transaction hashes
93-
txids_rpc = list(map(lambda t: notify_outputname(self.wallet, t['txid']), self.nodes[1].listtransactions("*", block_count)))
94-
assert_equal(sorted(txids_rpc), sorted(os.listdir(self.walletnotify_dir)))
93+
tx_details = list(map(lambda t: (t['txid'], t['blockheight'], t['blockhash']), self.nodes[1].listtransactions("*", block_count)))
9594
self.stop_node(1)
96-
for tx_file in os.listdir(self.walletnotify_dir):
97-
os.remove(os.path.join(self.walletnotify_dir, tx_file))
95+
self.expect_wallet_notify(tx_details)
9896

9997
self.log.info("test -walletnotify after rescan")
10098
# restart node to rescan to force wallet notifications
@@ -104,10 +102,8 @@ def run_test(self):
104102
self.wait_until(lambda: len(os.listdir(self.walletnotify_dir)) == block_count, timeout=10)
105103

106104
# directory content should equal the generated transaction hashes
107-
txids_rpc = list(map(lambda t: notify_outputname(self.wallet, t['txid']), self.nodes[1].listtransactions("*", block_count)))
108-
assert_equal(sorted(txids_rpc), sorted(os.listdir(self.walletnotify_dir)))
109-
for tx_file in os.listdir(self.walletnotify_dir):
110-
os.remove(os.path.join(self.walletnotify_dir, tx_file))
105+
tx_details = list(map(lambda t: (t['txid'], t['blockheight'], t['blockhash']), self.nodes[1].listtransactions("*", block_count)))
106+
self.expect_wallet_notify(tx_details)
111107

112108
# Conflicting transactions tests.
113109
# Generate spends from node 0, and check notifications
@@ -122,7 +118,7 @@ def run_test(self):
122118
tx1 = self.nodes[0].sendtoaddress(address=ADDRESS_BCRT1_UNSPENDABLE, amount=1, replaceable=True)
123119
assert_equal(tx1 in self.nodes[0].getrawmempool(), True)
124120
self.sync_mempools()
125-
self.expect_wallet_notify([tx1])
121+
self.expect_wallet_notify([(tx1, -1, UNCONFIRMED_HASH_STRING)])
126122

127123
# Generate bump transaction, sync mempools, and check for bump1
128124
# notification. In the future, per
@@ -131,39 +127,57 @@ def run_test(self):
131127
bump1 = self.nodes[0].bumpfee(tx1)["txid"]
132128
assert_equal(bump1 in self.nodes[0].getrawmempool(), True)
133129
self.sync_mempools()
134-
self.expect_wallet_notify([bump1])
130+
self.expect_wallet_notify([(bump1, -1, UNCONFIRMED_HASH_STRING)])
135131

136132
# Add bump1 transaction to new block, checking for a notification
137133
# and the correct number of confirmations.
138-
self.nodes[0].generatetoaddress(1, ADDRESS_BCRT1_UNSPENDABLE)
134+
blockhash1 = self.nodes[0].generatetoaddress(1, ADDRESS_BCRT1_UNSPENDABLE)[0]
135+
blockheight1 = self.nodes[0].getblockcount()
139136
self.sync_blocks()
140-
self.expect_wallet_notify([bump1])
137+
self.expect_wallet_notify([(bump1, blockheight1, blockhash1)])
141138
assert_equal(self.nodes[1].gettransaction(bump1)["confirmations"], 1)
142139

143140
# Generate a second transaction to be bumped.
144141
tx2 = self.nodes[0].sendtoaddress(address=ADDRESS_BCRT1_UNSPENDABLE, amount=1, replaceable=True)
145142
assert_equal(tx2 in self.nodes[0].getrawmempool(), True)
146143
self.sync_mempools()
147-
self.expect_wallet_notify([tx2])
144+
self.expect_wallet_notify([(tx2, -1, UNCONFIRMED_HASH_STRING)])
148145

149146
# Bump tx2 as bump2 and generate a block on node 0 while
150147
# disconnected, then reconnect and check for notifications on node 1
151148
# about newly confirmed bump2 and newly conflicted tx2.
152149
self.disconnect_nodes(0, 1)
153150
bump2 = self.nodes[0].bumpfee(tx2)["txid"]
154-
self.nodes[0].generatetoaddress(1, ADDRESS_BCRT1_UNSPENDABLE)
151+
blockhash2 = self.nodes[0].generatetoaddress(1, ADDRESS_BCRT1_UNSPENDABLE)[0]
152+
blockheight2 = self.nodes[0].getblockcount()
155153
assert_equal(self.nodes[0].gettransaction(bump2)["confirmations"], 1)
156154
assert_equal(tx2 in self.nodes[1].getrawmempool(), True)
157155
self.connect_nodes(0, 1)
158156
self.sync_blocks()
159-
self.expect_wallet_notify([bump2, tx2])
157+
self.expect_wallet_notify([(bump2, blockheight2, blockhash2), (tx2, -1, UNCONFIRMED_HASH_STRING)])
160158
assert_equal(self.nodes[1].gettransaction(bump2)["confirmations"], 1)
161159

162160
# TODO: add test for `-alertnotify` large fork notifications
163161

164-
def expect_wallet_notify(self, tx_ids):
165-
self.wait_until(lambda: len(os.listdir(self.walletnotify_dir)) >= len(tx_ids), timeout=10)
166-
assert_equal(sorted(notify_outputname(self.wallet, tx_id) for tx_id in tx_ids), sorted(os.listdir(self.walletnotify_dir)))
162+
def expect_wallet_notify(self, tx_details):
163+
self.wait_until(lambda: len(os.listdir(self.walletnotify_dir)) >= len(tx_details), timeout=10)
164+
# Should have no more and no less files than expected
165+
assert_equal(sorted(notify_outputname(self.wallet, tx_id) for tx_id, _, _ in tx_details), sorted(os.listdir(self.walletnotify_dir)))
166+
# Should now verify contents of each file
167+
for tx_id, blockheight, blockhash in tx_details:
168+
fname = os.path.join(self.walletnotify_dir, notify_outputname(self.wallet, tx_id))
169+
with open(fname, 'rt', encoding='utf-8') as f:
170+
text = f.read()
171+
# Universal newline ensures '\n' on 'nt'
172+
assert_equal(text[-1], '\n')
173+
text = text[:-1]
174+
if os.name == 'nt':
175+
# On Windows, echo as above will append a whitespace
176+
assert_equal(text[-1], ' ')
177+
text = text[:-1]
178+
expected = str(blockheight) + '_' + blockhash
179+
assert_equal(text, expected)
180+
167181
for tx_file in os.listdir(self.walletnotify_dir):
168182
os.remove(os.path.join(self.walletnotify_dir, tx_file))
169183

0 commit comments

Comments
 (0)