@relay-federation/bridge 0.3.15 → 0.3.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.js CHANGED
@@ -891,6 +891,7 @@ async function cmdStart () {
891
891
  peerHealth,
892
892
  bsvNodeClient: bsvNode,
893
893
  store,
894
+ addressWatcher: watcher,
894
895
  performOutboundHandshake,
895
896
  registeredPubkeys,
896
897
  gossipManager
@@ -1,161 +1,172 @@
1
- import { EventEmitter } from 'node:events'
2
- import { checkTxForWatched, pubkeyToHash160 } from './output-parser.js'
3
-
4
- /**
5
- * AddressWatcher — monitors transactions for outputs to watched addresses.
6
- *
7
- * Listens to TxRelay's 'tx:new' events, parses each transaction, and
8
- * checks outputs against a set of watched hash160s. When a match is
9
- * found, it:
10
- * 1. Stores the UTXO in PersistentStore
11
- * 2. Checks inputs for spent UTXOs and marks them
12
- * 3. Records the watched-address match
13
- * 4. Emits events for upstream consumers
14
- *
15
- * This replaces the HTTP-based address queries (fetchUtxos,
16
- * fetchAddressHistory) with pure local P2P-based tracking.
17
- *
18
- * Events:
19
- * 'utxo:received' — { txid, vout, satoshis, address, hash160 }
20
- * 'utxo:spent' — { txid, vout, spentByTxid }
21
- * 'tx:watched' — { txid, address, direction, matches }
22
- */
23
- export class AddressWatcher extends EventEmitter {
24
- /**
25
- * @param {import('./tx-relay.js').TxRelay} txRelay
26
- * @param {import('./persistent-store.js').PersistentStore} store
27
- */
28
- constructor (txRelay, store) {
29
- super()
30
- this.txRelay = txRelay
31
- this.store = store
32
- /** @type {Map<string, string>} hash160 → address (human-readable) */
33
- this._watched = new Map()
34
- /** @type {Set<string>} hash160s for fast lookup */
35
- this._hash160Set = new Set()
36
-
37
- this.txRelay.on('tx:new', ({ txid, rawHex }) => {
38
- this._processTx(txid, rawHex).catch(err => {
39
- this.emit('error', err)
40
- })
41
- })
42
- }
43
-
44
- /**
45
- * Watch an address by its compressed public key hex.
46
- * @param {string} pubkeyHex — 33-byte compressed public key
47
- * @param {string} [label] — optional human-readable label
48
- */
49
- watchPubkey (pubkeyHex, label) {
50
- const hash160 = pubkeyToHash160(pubkeyHex)
51
- this._watched.set(hash160, label || pubkeyHex)
52
- this._hash160Set.add(hash160)
53
- }
54
-
55
- /**
56
- * Watch an address by its hash160 directly.
57
- * @param {string} hash160 — 20-byte hash160 as hex
58
- * @param {string} [label] — optional human-readable label
59
- */
60
- watchHash160 (hash160, label) {
61
- this._watched.set(hash160, label || hash160)
62
- this._hash160Set.add(hash160)
63
- }
64
-
65
- /**
66
- * Stop watching an address.
67
- * @param {string} hash160
68
- */
69
- unwatch (hash160) {
70
- this._watched.delete(hash160)
71
- this._hash160Set.delete(hash160)
72
- }
73
-
74
- /**
75
- * Get all watched hash160s.
76
- * @returns {Array<{ hash160: string, label: string }>}
77
- */
78
- getWatched () {
79
- const result = []
80
- for (const [hash160, label] of this._watched) {
81
- result.push({ hash160, label })
82
- }
83
- return result
84
- }
85
-
86
- /**
87
- * Manually process a raw transaction (e.g., from fund command).
88
- * @param {string} rawHex
89
- */
90
- async processTxManual (rawHex) {
91
- const { checkTxForWatched: check } = await import('./output-parser.js')
92
- const result = check(rawHex, this._hash160Set)
93
- await this._handleResult(result, rawHex)
94
- }
95
-
96
- /** @private */
97
- async _processTx (txid, rawHex) {
98
- if (this._hash160Set.size === 0) return
99
-
100
- const result = checkTxForWatched(rawHex, this._hash160Set)
101
- await this._handleResult(result, rawHex)
102
- }
103
-
104
- /** @private */
105
- async _handleResult (result, rawHex) {
106
- // Check inputs — are any of our UTXOs being spent?
107
- for (const spend of result.spends) {
108
- const utxoKey = `${spend.prevTxid}:${spend.prevVout}`
109
- // Check if this input spends one of our tracked UTXOs
110
- const existing = await this.store._utxos.get(utxoKey)
111
- if (existing !== undefined && !existing.spent) {
112
- await this.store.spendUtxo(spend.prevTxid, spend.prevVout)
113
- this.emit('utxo:spent', {
114
- txid: spend.prevTxid,
115
- vout: spend.prevVout,
116
- spentByTxid: result.txid
117
- })
118
- }
119
- }
120
-
121
- // Check outputs any paying to our watched addresses?
122
- if (result.matches.length > 0) {
123
- // Store the raw tx for future reference
124
- await this.store.putTx(result.txid, rawHex)
125
-
126
- for (const match of result.matches) {
127
- const address = this._watched.get(match.hash160) || match.hash160
128
-
129
- // Store as UTXO
130
- await this.store.putUtxo({
131
- txid: result.txid,
132
- vout: match.vout,
133
- satoshis: match.satoshis,
134
- scriptHex: match.scriptHex,
135
- address
136
- })
137
-
138
- // Record watched-address match
139
- await this.store.putWatchedTx({
140
- txid: result.txid,
141
- address,
142
- direction: 'in',
143
- timestamp: Date.now()
144
- })
145
-
146
- this.emit('utxo:received', {
147
- txid: result.txid,
148
- vout: match.vout,
149
- satoshis: match.satoshis,
150
- address,
151
- hash160: match.hash160
152
- })
153
- }
154
-
155
- this.emit('tx:watched', {
156
- txid: result.txid,
157
- matches: result.matches.length
158
- })
159
- }
160
- }
161
- }
1
+ import { EventEmitter } from 'node:events'
2
+ import { checkTxForWatched, pubkeyToHash160, addressToHash160 } from './output-parser.js'
3
+
4
+ /**
5
+ * AddressWatcher — monitors transactions for outputs to watched addresses.
6
+ *
7
+ * Listens to TxRelay's 'tx:new' events, parses each transaction, and
8
+ * checks outputs against a set of watched hash160s. When a match is
9
+ * found, it:
10
+ * 1. Stores the UTXO in PersistentStore
11
+ * 2. Checks inputs for spent UTXOs and marks them
12
+ * 3. Records the watched-address match
13
+ * 4. Emits events for upstream consumers
14
+ *
15
+ * This replaces the HTTP-based address queries (fetchUtxos,
16
+ * fetchAddressHistory) with pure local P2P-based tracking.
17
+ *
18
+ * Events:
19
+ * 'utxo:received' — { txid, vout, satoshis, address, hash160 }
20
+ * 'utxo:spent' — { txid, vout, spentByTxid }
21
+ * 'tx:watched' — { txid, address, direction, matches }
22
+ */
23
+ export class AddressWatcher extends EventEmitter {
24
+ /**
25
+ * @param {import('./tx-relay.js').TxRelay} txRelay
26
+ * @param {import('./persistent-store.js').PersistentStore} store
27
+ */
28
+ constructor (txRelay, store) {
29
+ super()
30
+ this.txRelay = txRelay
31
+ this.store = store
32
+ /** @type {Map<string, string>} hash160 → address (human-readable) */
33
+ this._watched = new Map()
34
+ /** @type {Set<string>} hash160s for fast lookup */
35
+ this._hash160Set = new Set()
36
+
37
+ this.txRelay.on('tx:new', ({ txid, rawHex }) => {
38
+ this._processTx(txid, rawHex).catch(err => {
39
+ this.emit('error', err)
40
+ })
41
+ })
42
+ }
43
+
44
+ /**
45
+ * Watch an address by its compressed public key hex.
46
+ * @param {string} pubkeyHex — 33-byte compressed public key
47
+ * @param {string} [label] — optional human-readable label
48
+ */
49
+ watchPubkey (pubkeyHex, label) {
50
+ const hash160 = pubkeyToHash160(pubkeyHex)
51
+ this._watched.set(hash160, label || pubkeyHex)
52
+ this._hash160Set.add(hash160)
53
+ }
54
+
55
+ /**
56
+ * Watch an address by its hash160 directly.
57
+ * @param {string} hash160 — 20-byte hash160 as hex
58
+ * @param {string} [label] — optional human-readable label
59
+ */
60
+ watchHash160 (hash160, label) {
61
+ this._watched.set(hash160, label || hash160)
62
+ this._hash160Set.add(hash160)
63
+ }
64
+
65
+ /**
66
+ * Watch a BSV address (auto-converts to hash160).
67
+ * @param {string} address — BSV address (e.g. '1KhH4V...')
68
+ * @param {string} [label] — optional human-readable label
69
+ */
70
+ watchAddress (address, label) {
71
+ const hash160 = addressToHash160(address)
72
+ this._watched.set(hash160, label || address)
73
+ this._hash160Set.add(hash160)
74
+ }
75
+
76
+ /**
77
+ * Stop watching an address.
78
+ * @param {string} hash160
79
+ */
80
+ unwatch (hash160) {
81
+ this._watched.delete(hash160)
82
+ this._hash160Set.delete(hash160)
83
+ }
84
+
85
+ /**
86
+ * Get all watched hash160s.
87
+ * @returns {Array<{ hash160: string, label: string }>}
88
+ */
89
+ getWatched () {
90
+ const result = []
91
+ for (const [hash160, label] of this._watched) {
92
+ result.push({ hash160, label })
93
+ }
94
+ return result
95
+ }
96
+
97
+ /**
98
+ * Manually process a raw transaction (e.g., from fund command).
99
+ * @param {string} rawHex
100
+ */
101
+ async processTxManual (rawHex) {
102
+ const { checkTxForWatched: check } = await import('./output-parser.js')
103
+ const result = check(rawHex, this._hash160Set)
104
+ await this._handleResult(result, rawHex)
105
+ }
106
+
107
+ /** @private */
108
+ async _processTx (txid, rawHex) {
109
+ if (this._hash160Set.size === 0) return
110
+
111
+ const result = checkTxForWatched(rawHex, this._hash160Set)
112
+ await this._handleResult(result, rawHex)
113
+ }
114
+
115
+ /** @private */
116
+ async _handleResult (result, rawHex) {
117
+ // Check inputs — are any of our UTXOs being spent?
118
+ for (const spend of result.spends) {
119
+ const utxoKey = `${spend.prevTxid}:${spend.prevVout}`
120
+ // Check if this input spends one of our tracked UTXOs
121
+ const existing = await this.store._utxos.get(utxoKey)
122
+ if (existing !== undefined && !existing.spent) {
123
+ await this.store.spendUtxo(spend.prevTxid, spend.prevVout)
124
+ this.emit('utxo:spent', {
125
+ txid: spend.prevTxid,
126
+ vout: spend.prevVout,
127
+ spentByTxid: result.txid
128
+ })
129
+ }
130
+ }
131
+
132
+ // Check outputs — any paying to our watched addresses?
133
+ if (result.matches.length > 0) {
134
+ // Store the raw tx for future reference
135
+ await this.store.putTx(result.txid, rawHex)
136
+
137
+ for (const match of result.matches) {
138
+ const address = this._watched.get(match.hash160) || match.hash160
139
+
140
+ // Store as UTXO
141
+ await this.store.putUtxo({
142
+ txid: result.txid,
143
+ vout: match.vout,
144
+ satoshis: match.satoshis,
145
+ scriptHex: match.scriptHex,
146
+ address
147
+ })
148
+
149
+ // Record watched-address match
150
+ await this.store.putWatchedTx({
151
+ txid: result.txid,
152
+ address,
153
+ direction: 'in',
154
+ timestamp: Date.now()
155
+ })
156
+
157
+ this.emit('utxo:received', {
158
+ txid: result.txid,
159
+ vout: match.vout,
160
+ satoshis: match.satoshis,
161
+ address,
162
+ hash160: match.hash160
163
+ })
164
+ }
165
+
166
+ this.emit('tx:watched', {
167
+ txid: result.txid,
168
+ matches: result.matches.length
169
+ })
170
+ }
171
+ }
172
+ }