Merge pull request #4137 from mempool/mononaut/multi-address-tracking
Multi address tracking
This commit is contained in:
		
						commit
						4fe745817c
					
				@ -33,7 +33,8 @@
 | 
			
		||||
    "DISK_CACHE_BLOCK_INTERVAL": 6,
 | 
			
		||||
    "MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
 | 
			
		||||
    "ALLOW_UNREACHABLE": true,
 | 
			
		||||
    "PRICE_UPDATES_PER_HOUR": 1
 | 
			
		||||
    "PRICE_UPDATES_PER_HOUR": 1,
 | 
			
		||||
    "MAX_TRACKED_ADDRESSES": 100
 | 
			
		||||
  },
 | 
			
		||||
  "CORE_RPC": {
 | 
			
		||||
    "HOST": "127.0.0.1",
 | 
			
		||||
 | 
			
		||||
@ -34,7 +34,8 @@
 | 
			
		||||
    "DISK_CACHE_BLOCK_INTERVAL": 999,
 | 
			
		||||
    "MAX_PUSH_TX_SIZE_WEIGHT": 4000000,
 | 
			
		||||
    "ALLOW_UNREACHABLE": true,
 | 
			
		||||
    "PRICE_UPDATES_PER_HOUR": 1
 | 
			
		||||
    "PRICE_UPDATES_PER_HOUR": 1,
 | 
			
		||||
    "MAX_TRACKED_ADDRESSES": 1
 | 
			
		||||
  },
 | 
			
		||||
  "CORE_RPC": {
 | 
			
		||||
    "HOST": "__CORE_RPC_HOST__",
 | 
			
		||||
 | 
			
		||||
@ -48,6 +48,7 @@ describe('Mempool Backend Config', () => {
 | 
			
		||||
        MAX_PUSH_TX_SIZE_WEIGHT: 400000,
 | 
			
		||||
        ALLOW_UNREACHABLE: true,
 | 
			
		||||
        PRICE_UPDATES_PER_HOUR: 1,
 | 
			
		||||
        MAX_TRACKED_ADDRESSES: 1,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
 | 
			
		||||
 | 
			
		||||
@ -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: MempoolTransactionExtended[],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// valid 'want' subscriptions
 | 
			
		||||
const wantable = [
 | 
			
		||||
  'blocks',
 | 
			
		||||
@ -195,24 +201,49 @@ class WebsocketHandler {
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (parsedMessage && parsedMessage['track-address']) {
 | 
			
		||||
            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(parsedMessage['track-address'])) {
 | 
			
		||||
              let matchedAddress = parsedMessage['track-address'];
 | 
			
		||||
              if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(parsedMessage['track-address'])) {
 | 
			
		||||
                matchedAddress = matchedAddress.toLowerCase();
 | 
			
		||||
              }
 | 
			
		||||
              if (/^04[a-fA-F0-9]{128}$/.test(parsedMessage['track-address'])) {
 | 
			
		||||
                client['track-address'] = '41' + matchedAddress + 'ac';
 | 
			
		||||
              } else if (/^(02|03)[a-fA-F0-9]{64}$/.test(parsedMessage['track-address'])) {
 | 
			
		||||
                client['track-address'] = '21' + matchedAddress + 'ac';
 | 
			
		||||
              } else {
 | 
			
		||||
                client['track-address'] = matchedAddress;
 | 
			
		||||
              }
 | 
			
		||||
            const validAddress = this.testAddress(parsedMessage['track-address']);
 | 
			
		||||
            if (validAddress) {
 | 
			
		||||
              client['track-address'] = validAddress;
 | 
			
		||||
            } else {
 | 
			
		||||
              client['track-address'] = null;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (parsedMessage && parsedMessage['track-addresses'] && Array.isArray(parsedMessage['track-addresses'])) {
 | 
			
		||||
            const addressMap: { [address: string]: string } = {};
 | 
			
		||||
            for (const address of parsedMessage['track-addresses']) {
 | 
			
		||||
              const validAddress = this.testAddress(address);
 | 
			
		||||
              if (validAddress) {
 | 
			
		||||
                addressMap[address] = validAddress;
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            if (Object.keys(addressMap).length > config.MEMPOOL.MAX_TRACKED_ADDRESSES) {
 | 
			
		||||
              response['track-addresses-error'] = `"too many addresses requested, this connection supports tracking a maximum of ${config.MEMPOOL.MAX_TRACKED_ADDRESSES} addresses"`;
 | 
			
		||||
              client['track-addresses'] = null;
 | 
			
		||||
            } else if (Object.keys(addressMap).length > 0) {
 | 
			
		||||
              client['track-addresses'] = addressMap;
 | 
			
		||||
            } else {
 | 
			
		||||
              client['track-addresses'] = null;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (parsedMessage && parsedMessage['track-scriptpubkeys'] && Array.isArray(parsedMessage['track-scriptpubkeys'])) {
 | 
			
		||||
            const spks: string[] = [];
 | 
			
		||||
            for (const spk of parsedMessage['track-scriptpubkeys']) {
 | 
			
		||||
              if (/^[a-fA-F0-9]+$/.test(spk)) {
 | 
			
		||||
                spks.push(spk.toLowerCase());
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            if (spks.length > config.MEMPOOL.MAX_TRACKED_ADDRESSES) {
 | 
			
		||||
              response['track-scriptpubkeys-error'] = `"too many scriptpubkeys requested, this connection supports tracking a maximum of ${config.MEMPOOL.MAX_TRACKED_ADDRESSES} scriptpubkeys"`;
 | 
			
		||||
              client['track-scriptpubkeys'] = null;
 | 
			
		||||
            } else if (spks.length) {
 | 
			
		||||
              client['track-scriptpubkeys'] = spks;
 | 
			
		||||
            } else {
 | 
			
		||||
              client['track-scriptpubkeys'] = null;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (parsedMessage && parsedMessage['track-asset']) {
 | 
			
		||||
            if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-asset'])) {
 | 
			
		||||
              client['track-asset'] = parsedMessage['track-asset'];
 | 
			
		||||
@ -544,6 +575,50 @@ class WebsocketHandler {
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (client['track-addresses']) {
 | 
			
		||||
        const addressMap: { [address: string]: AddressTransactions } = {};
 | 
			
		||||
        for (const [address, key] of Object.entries(client['track-addresses'] || {})) {
 | 
			
		||||
          const newTransactions = Array.from(addressCache[key as string]?.values() || []);
 | 
			
		||||
          const removedTransactions = Array.from(removedAddressCache[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(newTransactions) : newTransactions;
 | 
			
		||||
          if (fullTransactions?.length) {
 | 
			
		||||
            addressMap[address] = {
 | 
			
		||||
              mempool: fullTransactions,
 | 
			
		||||
              confirmed: [],
 | 
			
		||||
              removed: removedTransactions,
 | 
			
		||||
            };
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (Object.keys(addressMap).length > 0) {
 | 
			
		||||
          response['multi-address-transactions'] = JSON.stringify(addressMap);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (client['track-scriptpubkeys']) {
 | 
			
		||||
        const spkMap: { [spk: string]: AddressTransactions } = {};
 | 
			
		||||
        for (const spk of client['track-scriptpubkeys'] || []) {
 | 
			
		||||
          const newTransactions = Array.from(addressCache[spk as string]?.values() || []);
 | 
			
		||||
          const removedTransactions = Array.from(removedAddressCache[spk 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(newTransactions) : newTransactions;
 | 
			
		||||
          if (fullTransactions?.length) {
 | 
			
		||||
            spkMap[spk] = {
 | 
			
		||||
              mempool: fullTransactions,
 | 
			
		||||
              confirmed: [],
 | 
			
		||||
              removed: removedTransactions,
 | 
			
		||||
            };
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (Object.keys(spkMap).length > 0) {
 | 
			
		||||
          response['multi-scriptpubkey-transactions'] = JSON.stringify(spkMap);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (client['track-asset']) {
 | 
			
		||||
        const foundTransactions: TransactionExtended[] = [];
 | 
			
		||||
 | 
			
		||||
@ -844,6 +919,42 @@ 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-scriptpubkeys']) {
 | 
			
		||||
        const spkMap: { [spk: string]: AddressTransactions } = {};
 | 
			
		||||
        for (const spk of client['track-scriptpubkeys'] || []) {
 | 
			
		||||
          const fullTransactions = Array.from(addressCache[spk as string]?.values() || []);
 | 
			
		||||
          if (fullTransactions?.length) {
 | 
			
		||||
            spkMap[spk] = {
 | 
			
		||||
              mempool: [],
 | 
			
		||||
              confirmed: fullTransactions,
 | 
			
		||||
              removed: [],
 | 
			
		||||
            };
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (Object.keys(spkMap).length > 0) {
 | 
			
		||||
          response['multi-scriptpubkey-transactions'] = JSON.stringify(spkMap);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (client['track-asset']) {
 | 
			
		||||
        const foundTransactions: TransactionExtended[] = [];
 | 
			
		||||
 | 
			
		||||
@ -913,6 +1024,28 @@ class WebsocketHandler {
 | 
			
		||||
        + '}';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // checks if an address conforms to a valid format
 | 
			
		||||
  // returns the canonical form:
 | 
			
		||||
  //  - lowercase for bech32(m)
 | 
			
		||||
  //  - lowercase scriptpubkey for P2PK
 | 
			
		||||
  // or false if invalid
 | 
			
		||||
  private testAddress(address): string | false {
 | 
			
		||||
    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)) {
 | 
			
		||||
      if (/^[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)) {
 | 
			
		||||
        address = address.toLowerCase();
 | 
			
		||||
      }
 | 
			
		||||
      if (/^04[a-fA-F0-9]{128}$/.test(address)) {
 | 
			
		||||
        return '41' + address + 'ac';
 | 
			
		||||
      } else if (/^(02|03)[a-fA-F0-9]{64}$/.test(address)) {
 | 
			
		||||
        return '21' + address + 'ac';
 | 
			
		||||
      } else {
 | 
			
		||||
        return address;
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private makeAddressCache(transactions: MempoolTransactionExtended[]): { [address: string]: Set<MempoolTransactionExtended> } {
 | 
			
		||||
    const addressCache: { [address: string]: Set<MempoolTransactionExtended> } = {};
 | 
			
		||||
    for (const tx of transactions) {
 | 
			
		||||
 | 
			
		||||
@ -39,6 +39,7 @@ interface IConfig {
 | 
			
		||||
    MAX_PUSH_TX_SIZE_WEIGHT: number;
 | 
			
		||||
    ALLOW_UNREACHABLE: boolean;
 | 
			
		||||
    PRICE_UPDATES_PER_HOUR: number;
 | 
			
		||||
    MAX_TRACKED_ADDRESSES: number;
 | 
			
		||||
  };
 | 
			
		||||
  ESPLORA: {
 | 
			
		||||
    REST_API_URL: string;
 | 
			
		||||
@ -193,6 +194,7 @@ const defaults: IConfig = {
 | 
			
		||||
    'MAX_PUSH_TX_SIZE_WEIGHT': 400000,
 | 
			
		||||
    'ALLOW_UNREACHABLE': true,
 | 
			
		||||
    'PRICE_UPDATES_PER_HOUR': 1,
 | 
			
		||||
    'MAX_TRACKED_ADDRESSES': 1,
 | 
			
		||||
  },
 | 
			
		||||
  'ESPLORA': {
 | 
			
		||||
    'REST_API_URL': 'http://127.0.0.1:3000',
 | 
			
		||||
 | 
			
		||||
@ -35,6 +35,7 @@
 | 
			
		||||
    "POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
 | 
			
		||||
    "POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
 | 
			
		||||
    "PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__
 | 
			
		||||
    "MAX_TRACKED_ADDRESSES": __MEMPOOL_MAX_TRACKED_ADDRESSES__
 | 
			
		||||
  },
 | 
			
		||||
  "CORE_RPC": {
 | 
			
		||||
    "HOST": "__CORE_RPC_HOST__",
 | 
			
		||||
 | 
			
		||||
@ -36,6 +36,7 @@ __MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__=${MEMPOOL_DISK_CACHE_BLOCK_INTERVAL:=6}
 | 
			
		||||
__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__=${MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT:=4000000}
 | 
			
		||||
__MEMPOOL_ALLOW_UNREACHABLE__=${MEMPOOL_ALLOW_UNREACHABLE:=true}
 | 
			
		||||
__MEMPOOL_PRICE_UPDATES_PER_HOUR__=${MEMPOOL_PRICE_UPDATES_PER_HOUR:=1}
 | 
			
		||||
__MEMPOOL_MAX_TRACKED_ADDRESSES__=${MEMPOOL_MAX_TRACKED_ADDRESSES:=1}
 | 
			
		||||
 | 
			
		||||
# CORE_RPC
 | 
			
		||||
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
 | 
			
		||||
@ -188,6 +189,7 @@ sed -i "s!__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__!${__MEMPOOL_DISK_CACHE_BLOCK_INT
 | 
			
		||||
sed -i "s!__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__!${__MEMPOOL_MAX_PUSH_TX_SIZE_WEIGHT__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_ALLOW_UNREACHABLE__!${__MEMPOOL_ALLOW_UNREACHABLE__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_PRICE_UPDATES_PER_HOUR__!${__MEMPOOL_PRICE_UPDATES_PER_HOUR__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__MEMPOOL_MAX_TRACKED_ADDRESSES__!${__MEMPOOL_MAX_TRACKED_ADDRESSES__}!g" mempool-config.json
 | 
			
		||||
 | 
			
		||||
sed -i "s!__CORE_RPC_HOST__!${__CORE_RPC_HOST__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__CORE_RPC_PORT__!${__CORE_RPC_PORT__}!g" mempool-config.json
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,8 @@ export interface WebsocketResponse {
 | 
			
		||||
  fees?: Recommendedfees;
 | 
			
		||||
  'track-tx'?: string;
 | 
			
		||||
  'track-address'?: string;
 | 
			
		||||
  'track-addresses'?: string[];
 | 
			
		||||
  'track-scriptpubkeys'?: string[];
 | 
			
		||||
  'track-asset'?: string;
 | 
			
		||||
  'track-mempool-block'?: number;
 | 
			
		||||
  'track-rbf'?: string;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user