diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 937d4a7c5..c0493d6c1 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -24,6 +24,12 @@ import { ApiPrice } from '../repositories/PricesRepository'; import accelerationApi from './services/acceleration'; import mempool from './mempool'; +interface AddressTransactions { + mempool: MempoolTransactionExtended[], + confirmed: MempoolTransactionExtended[], + removed: string[], +} + // valid 'want' subscriptions const wantable = [ 'blocks', @@ -213,6 +219,32 @@ class WebsocketHandler { } } + if (parsedMessage && parsedMessage['track-addresses'] && Array.isArray(parsedMessage['track-addresses'])) { + const addressMap: { [address: string]: string } = {}; + for (const address of parsedMessage['track-addresses']) { + if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64})$/.test(address)) { + let matchedAddress = address; + if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(address)) { + matchedAddress = matchedAddress.toLowerCase(); + } + if (/^04[a-fA-F0-9]{128}$/.test(address)) { + addressMap[address] = '41' + matchedAddress + 'ac'; + } else if (/^(02|03)[a-fA-F0-9]{64}$/.test(address)) { + addressMap[address] = '21' + matchedAddress + 'ac'; + } else { + addressMap[address] = matchedAddress; + } + } else { + // skip invalid address formats + } + } + if (Object.keys(addressMap).length > 0) { + client['track-addresses'] = addressMap; + } else { + client['track-addresses'] = null; + } + } + if (parsedMessage && parsedMessage['track-asset']) { if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-asset'])) { client['track-asset'] = parsedMessage['track-asset']; @@ -544,6 +576,27 @@ class WebsocketHandler { } } + if (client['track-addresses']) { + const addressMap: { [address: string]: AddressTransactions } = {}; + for (const [address, key] of Object.entries(client['track-addresses'] || {})) { + const foundTransactions = Array.from(addressCache[key as string]?.values() || []); + // txs may be missing prevouts in non-esplora backends + // so fetch the full transactions now + const fullTransactions = (config.MEMPOOL.BACKEND !== 'esplora') ? await this.getFullTransactions(foundTransactions) : foundTransactions; + if (fullTransactions?.length) { + addressMap[address] = { + mempool: fullTransactions, + confirmed: [], + removed: [], + }; + } + } + + if (Object.keys(addressMap).length > 0) { + response['multi-address-transactions'] = JSON.stringify(addressMap); + } + } + if (client['track-asset']) { const foundTransactions: TransactionExtended[] = []; @@ -843,6 +896,24 @@ class WebsocketHandler { } } + if (client['track-addresses']) { + const addressMap: { [address: string]: AddressTransactions } = {}; + for (const [address, key] of Object.entries(client['track-addresses'] || {})) { + const fullTransactions = Array.from(addressCache[key as string]?.values() || []); + if (fullTransactions?.length) { + addressMap[address] = { + mempool: [], + confirmed: fullTransactions, + removed: [], + }; + } + } + + if (Object.keys(addressMap).length > 0) { + response['multi-address-transactions'] = JSON.stringify(addressMap); + } + } + if (client['track-asset']) { const foundTransactions: TransactionExtended[] = []; diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts index 20bc42bde..fb488e705 100644 --- a/frontend/src/app/interfaces/websocket.interface.ts +++ b/frontend/src/app/interfaces/websocket.interface.ts @@ -27,6 +27,7 @@ export interface WebsocketResponse { fees?: Recommendedfees; 'track-tx'?: string; 'track-address'?: string; + 'track-addresses'?: string[]; 'track-asset'?: string; 'track-mempool-block'?: number; 'track-rbf'?: string;