@relay-federation/bridge 0.3.14 → 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.
@@ -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
+ }
@@ -145,6 +145,21 @@ export class BSVNodeClient extends EventEmitter {
145
145
  return Promise.reject(new Error('not connected to BSV node'))
146
146
  }
147
147
 
148
+ /**
149
+ * Fetch a transaction from a specific peer (the one that announced it via inv).
150
+ * Falls back to any connected peer if the target peer is unavailable.
151
+ * @param {import('./bsv-peer.js').BSVPeer} peer
152
+ * @param {string} txid
153
+ * @param {number} [timeoutMs=5000]
154
+ * @returns {Promise<{ txid, rawHex }>}
155
+ */
156
+ getTxFromPeer (peer, txid, timeoutMs = 5000) {
157
+ if (peer && peer._handshakeComplete) {
158
+ return peer.getTx(txid, timeoutMs)
159
+ }
160
+ return this.getTx(txid, timeoutMs)
161
+ }
162
+
148
163
  /**
149
164
  * Trigger header sync on connected peers.
150
165
  */
package/lib/bsv-peer.js CHANGED
@@ -593,7 +593,7 @@ export class BSVPeer extends EventEmitter {
593
593
  }
594
594
 
595
595
  if (txids.length > 0) {
596
- this.emit('tx:inv', { txids })
596
+ this.emit('tx:inv', { txids, peer: this })
597
597
  }
598
598
  }
599
599
 
@@ -723,7 +723,7 @@ export class BSVPeer extends EventEmitter {
723
723
  offset += writeVarInt(payload, offset, userAgentBuf.length)
724
724
  userAgentBuf.copy(payload, offset); offset += userAgentBuf.length
725
725
  payload.writeInt32LE(this._bestHeight, offset); offset += 4
726
- payload[offset] = 0; offset += 1
726
+ payload[offset] = 1; offset += 1
727
727
 
728
728
  this._sendMessage('version', payload.subarray(0, offset))
729
729
  }