Skip to content

Commit 06e1fb0

Browse files
committed
Add new format string placeholders for walletnotify to include relevant block information for transactions
1 parent c771fc0 commit 06e1fb0

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)