@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 +1 -0
- package/lib/address-watcher.js +172 -161
- package/lib/persistent-store.js +896 -883
- package/lib/status-server.js +135 -40
- package/package.json +1 -1
package/cli.js
CHANGED
package/lib/address-watcher.js
CHANGED
|
@@ -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
|
-
*
|
|
67
|
-
* @param {string}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
*
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
+
}
|