Merge branch 'master' into nymkappa/update-doc
This commit is contained in:
		
						commit
						968a26d2cd
					
				
							
								
								
									
										4
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@ -9,7 +9,7 @@ jobs:
 | 
				
			|||||||
    if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
 | 
					    if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
 | 
				
			||||||
    strategy:
 | 
					    strategy:
 | 
				
			||||||
      matrix:
 | 
					      matrix:
 | 
				
			||||||
        node: ["18", "20"]
 | 
					        node: ["20", "21"]
 | 
				
			||||||
        flavor: ["dev", "prod"]
 | 
					        flavor: ["dev", "prod"]
 | 
				
			||||||
      fail-fast: false
 | 
					      fail-fast: false
 | 
				
			||||||
    runs-on: "ubuntu-latest"
 | 
					    runs-on: "ubuntu-latest"
 | 
				
			||||||
@ -160,7 +160,7 @@ jobs:
 | 
				
			|||||||
    if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
 | 
					    if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
 | 
				
			||||||
    strategy:
 | 
					    strategy:
 | 
				
			||||||
      matrix:
 | 
					      matrix:
 | 
				
			||||||
        node: ["18", "20"]
 | 
					        node: ["20", "21"]
 | 
				
			||||||
        flavor: ["dev", "prod"]
 | 
					        flavor: ["dev", "prod"]
 | 
				
			||||||
      fail-fast: false
 | 
					      fail-fast: false
 | 
				
			||||||
    runs-on: "ubuntu-latest"
 | 
					    runs-on: "ubuntu-latest"
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								.github/workflows/on-tag.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/on-tag.yml
									
									
									
									
										vendored
									
									
								
							@ -100,6 +100,5 @@ jobs:
 | 
				
			|||||||
          --cache-to "type=local,dest=/tmp/.buildx-cache" \
 | 
					          --cache-to "type=local,dest=/tmp/.buildx-cache" \
 | 
				
			||||||
          --platform linux/amd64,linux/arm64 \
 | 
					          --platform linux/amd64,linux/arm64 \
 | 
				
			||||||
          --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \
 | 
					          --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \
 | 
				
			||||||
          --tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \
 | 
					 | 
				
			||||||
          --output "type=registry" ./${{ matrix.service }}/ \
 | 
					          --output "type=registry" ./${{ matrix.service }}/ \
 | 
				
			||||||
          --build-arg commitHash=$SHORT_SHA
 | 
					          --build-arg commitHash=$SHORT_SHA
 | 
				
			||||||
 | 
				
			|||||||
@ -40,6 +40,7 @@ class Blocks {
 | 
				
			|||||||
  private quarterEpochBlockTime: number | null = null;
 | 
					  private quarterEpochBlockTime: number | null = null;
 | 
				
			||||||
  private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
 | 
					  private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
 | 
				
			||||||
  private newAsyncBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: MempoolTransactionExtended[]) => Promise<void>)[] = [];
 | 
					  private newAsyncBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: MempoolTransactionExtended[]) => Promise<void>)[] = [];
 | 
				
			||||||
 | 
					  private classifyingBlocks: boolean = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private mainLoopTimeout: number = 120000;
 | 
					  private mainLoopTimeout: number = 120000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -568,6 +569,11 @@ class Blocks {
 | 
				
			|||||||
   * [INDEXING] Index transaction classification flags for Goggles
 | 
					   * [INDEXING] Index transaction classification flags for Goggles
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  public async $classifyBlocks(): Promise<void> {
 | 
					  public async $classifyBlocks(): Promise<void> {
 | 
				
			||||||
 | 
					    if (this.classifyingBlocks) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this.classifyingBlocks = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // classification requires an esplora backend
 | 
					    // classification requires an esplora backend
 | 
				
			||||||
    if (!Common.gogglesIndexingEnabled() || config.MEMPOOL.BACKEND !== 'esplora') {
 | 
					    if (!Common.gogglesIndexingEnabled() || config.MEMPOOL.BACKEND !== 'esplora') {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
@ -679,6 +685,8 @@ class Blocks {
 | 
				
			|||||||
        indexedThisRun = 0;
 | 
					        indexedThisRun = 0;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.classifyingBlocks = false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
 | 
				
			|||||||
@ -19,45 +19,90 @@ class RedisCache {
 | 
				
			|||||||
  private client;
 | 
					  private client;
 | 
				
			||||||
  private connected = false;
 | 
					  private connected = false;
 | 
				
			||||||
  private schemaVersion = 1;
 | 
					  private schemaVersion = 1;
 | 
				
			||||||
 | 
					  private redisConfig: any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private pauseFlush: boolean = false;
 | 
				
			||||||
  private cacheQueue: MempoolTransactionExtended[] = [];
 | 
					  private cacheQueue: MempoolTransactionExtended[] = [];
 | 
				
			||||||
 | 
					  private removeQueue: string[] = [];
 | 
				
			||||||
 | 
					  private rbfCacheQueue: { type: string, txid: string, value: any }[] = [];
 | 
				
			||||||
 | 
					  private rbfRemoveQueue: { type: string, txid: string }[] = [];
 | 
				
			||||||
  private txFlushLimit: number = 10000;
 | 
					  private txFlushLimit: number = 10000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor() {
 | 
					  constructor() {
 | 
				
			||||||
    if (config.REDIS.ENABLED) {
 | 
					    if (config.REDIS.ENABLED) {
 | 
				
			||||||
      const redisConfig = {
 | 
					      this.redisConfig = {
 | 
				
			||||||
        socket: {
 | 
					        socket: {
 | 
				
			||||||
          path: config.REDIS.UNIX_SOCKET_PATH
 | 
					          path: config.REDIS.UNIX_SOCKET_PATH
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        database: NetworkDB[config.MEMPOOL.NETWORK],
 | 
					        database: NetworkDB[config.MEMPOOL.NETWORK],
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
      this.client = createClient(redisConfig);
 | 
					 | 
				
			||||||
      this.client.on('error', (e) => {
 | 
					 | 
				
			||||||
        logger.err(`Error in Redis client: ${e instanceof Error ? e.message : e}`);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      this.$ensureConnected();
 | 
					      this.$ensureConnected();
 | 
				
			||||||
 | 
					      setInterval(() => { this.$ensureConnected(); }, 10000);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async $ensureConnected(): Promise<void> {
 | 
					  private async $ensureConnected(): Promise<boolean> {
 | 
				
			||||||
    if (!this.connected && config.REDIS.ENABLED) {
 | 
					    if (!this.connected && config.REDIS.ENABLED) {
 | 
				
			||||||
      return this.client.connect().then(async () => {
 | 
					      try {
 | 
				
			||||||
        this.connected = true;
 | 
					        this.client = createClient(this.redisConfig);
 | 
				
			||||||
        logger.info(`Redis client connected`);
 | 
					        this.client.on('error', async (e) => {
 | 
				
			||||||
 | 
					          logger.err(`Error in Redis client: ${e instanceof Error ? e.message : e}`);
 | 
				
			||||||
 | 
					          this.connected = false;
 | 
				
			||||||
 | 
					          await this.client.disconnect();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        await this.client.connect().then(async () => {
 | 
				
			||||||
 | 
					          try {
 | 
				
			||||||
            const version = await this.client.get('schema_version');
 | 
					            const version = await this.client.get('schema_version');
 | 
				
			||||||
 | 
					            this.connected = true;
 | 
				
			||||||
            if (version !== this.schemaVersion) {
 | 
					            if (version !== this.schemaVersion) {
 | 
				
			||||||
              // schema changed
 | 
					              // schema changed
 | 
				
			||||||
              // perform migrations or flush DB if necessary
 | 
					              // perform migrations or flush DB if necessary
 | 
				
			||||||
              logger.info(`Redis schema version changed from ${version} to ${this.schemaVersion}`);
 | 
					              logger.info(`Redis schema version changed from ${version} to ${this.schemaVersion}`);
 | 
				
			||||||
              await this.client.set('schema_version', this.schemaVersion);
 | 
					              await this.client.set('schema_version', this.schemaVersion);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            logger.info(`Redis client connected`);
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					          } catch (e) {
 | 
				
			||||||
 | 
					            this.connected = false;
 | 
				
			||||||
 | 
					            logger.warn('Failed to connect to Redis');
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					        await this.$onConnected();
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        logger.warn('Error connecting to Redis: ' + (e instanceof Error ? e.message : e));
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        // test connection
 | 
				
			||||||
 | 
					        await this.client.get('schema_version');
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        logger.warn('Lost connection to Redis: ' + (e instanceof Error ? e.message : e));
 | 
				
			||||||
 | 
					        logger.warn('Attempting to reconnect in 10 seconds');
 | 
				
			||||||
 | 
					        this.connected = false;
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async $updateBlocks(blocks: BlockExtended[]) {
 | 
					  private async $onConnected(): Promise<void> {
 | 
				
			||||||
 | 
					    await this.$flushTransactions();
 | 
				
			||||||
 | 
					    await this.$removeTransactions([]);
 | 
				
			||||||
 | 
					    await this.$flushRbfQueues();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async $updateBlocks(blocks: BlockExtended[]): Promise<void> {
 | 
				
			||||||
 | 
					    if (!config.REDIS.ENABLED) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!this.connected) {
 | 
				
			||||||
 | 
					      logger.warn(`Failed to update blocks in Redis cache: Redis is not connected`);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      await this.$ensureConnected();
 | 
					 | 
				
			||||||
      await this.client.set('blocks', JSON.stringify(blocks));
 | 
					      await this.client.set('blocks', JSON.stringify(blocks));
 | 
				
			||||||
      logger.debug(`Saved latest blocks to Redis cache`);
 | 
					      logger.debug(`Saved latest blocks to Redis cache`);
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
@ -65,9 +110,15 @@ class RedisCache {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async $updateBlockSummaries(summaries: BlockSummary[]) {
 | 
					  async $updateBlockSummaries(summaries: BlockSummary[]): Promise<void> {
 | 
				
			||||||
 | 
					    if (!config.REDIS.ENABLED) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!this.connected) {
 | 
				
			||||||
 | 
					      logger.warn(`Failed to update block summaries in Redis cache: Redis is not connected`);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      await this.$ensureConnected();
 | 
					 | 
				
			||||||
      await this.client.set('block-summaries', JSON.stringify(summaries));
 | 
					      await this.client.set('block-summaries', JSON.stringify(summaries));
 | 
				
			||||||
      logger.debug(`Saved latest block summaries to Redis cache`);
 | 
					      logger.debug(`Saved latest block summaries to Redis cache`);
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
@ -75,30 +126,35 @@ class RedisCache {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async $addTransaction(tx: MempoolTransactionExtended) {
 | 
					  async $addTransaction(tx: MempoolTransactionExtended): Promise<void> {
 | 
				
			||||||
 | 
					    if (!config.REDIS.ENABLED) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    this.cacheQueue.push(tx);
 | 
					    this.cacheQueue.push(tx);
 | 
				
			||||||
    if (this.cacheQueue.length >= this.txFlushLimit) {
 | 
					    if (this.cacheQueue.length >= this.txFlushLimit) {
 | 
				
			||||||
 | 
					      if (!this.pauseFlush) {
 | 
				
			||||||
        await this.$flushTransactions();
 | 
					        await this.$flushTransactions();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  async $flushTransactions() {
 | 
					 | 
				
			||||||
    const success = await this.$addTransactions(this.cacheQueue);
 | 
					 | 
				
			||||||
    if (success) {
 | 
					 | 
				
			||||||
      logger.debug(`Saved ${this.cacheQueue.length} transactions to Redis cache`);
 | 
					 | 
				
			||||||
      this.cacheQueue = [];
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      logger.err(`Failed to save ${this.cacheQueue.length} transactions to Redis cache`);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async $addTransactions(newTransactions: MempoolTransactionExtended[]): Promise<boolean> {
 | 
					  async $flushTransactions(): Promise<void> {
 | 
				
			||||||
    if (!newTransactions.length) {
 | 
					    if (!config.REDIS.ENABLED) {
 | 
				
			||||||
      return true;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (!this.cacheQueue.length) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!this.connected) {
 | 
				
			||||||
 | 
					      logger.warn(`Failed to add ${this.cacheQueue.length} transactions to Redis cache: Redis not connected`);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.pauseFlush = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const toAdd = this.cacheQueue.slice(0, this.txFlushLimit);
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      await this.$ensureConnected();
 | 
					      const msetData = toAdd.map(tx => {
 | 
				
			||||||
      const msetData = newTransactions.map(tx => {
 | 
					 | 
				
			||||||
        const minified: any = { ...tx };
 | 
					        const minified: any = { ...tx };
 | 
				
			||||||
        delete minified.hex;
 | 
					        delete minified.hex;
 | 
				
			||||||
        for (const vin of minified.vin) {
 | 
					        for (const vin of minified.vin) {
 | 
				
			||||||
@ -112,30 +168,53 @@ class RedisCache {
 | 
				
			|||||||
        return [`mempool:tx:${tx.txid}`, JSON.stringify(minified)];
 | 
					        return [`mempool:tx:${tx.txid}`, JSON.stringify(minified)];
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      await this.client.MSET(msetData);
 | 
					      await this.client.MSET(msetData);
 | 
				
			||||||
      return true;
 | 
					      // successful, remove transactions from cache queue
 | 
				
			||||||
 | 
					      this.cacheQueue = this.cacheQueue.slice(toAdd.length);
 | 
				
			||||||
 | 
					      logger.debug(`Saved ${toAdd.length} transactions to Redis cache, ${this.cacheQueue.length} left in queue`);
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      logger.warn(`Failed to add ${newTransactions.length} transactions to Redis cache: ${e instanceof Error ? e.message : e}`);
 | 
					      logger.warn(`Failed to add ${toAdd.length} transactions to Redis cache: ${e instanceof Error ? e.message : e}`);
 | 
				
			||||||
      return false;
 | 
					      this.pauseFlush = true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async $removeTransactions(transactions: string[]) {
 | 
					  async $removeTransactions(transactions: string[]): Promise<void> {
 | 
				
			||||||
    try {
 | 
					    if (!config.REDIS.ENABLED) {
 | 
				
			||||||
      await this.$ensureConnected();
 | 
					      return;
 | 
				
			||||||
      const sliceLength = config.REDIS.BATCH_QUERY_BASE_SIZE;
 | 
					 | 
				
			||||||
      for (let i = 0; i < Math.ceil(transactions.length / sliceLength); i++) {
 | 
					 | 
				
			||||||
        const slice = transactions.slice(i * sliceLength, (i + 1) * sliceLength);
 | 
					 | 
				
			||||||
        await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`));
 | 
					 | 
				
			||||||
        logger.debug(`Deleted ${slice.length} transactions from the Redis cache`);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    const toRemove = this.removeQueue.concat(transactions);
 | 
				
			||||||
 | 
					    this.removeQueue = [];
 | 
				
			||||||
 | 
					    let failed: string[] = [];
 | 
				
			||||||
 | 
					    let numRemoved = 0;
 | 
				
			||||||
 | 
					    if (this.connected) {
 | 
				
			||||||
 | 
					      const sliceLength = config.REDIS.BATCH_QUERY_BASE_SIZE;
 | 
				
			||||||
 | 
					      for (let i = 0; i < Math.ceil(toRemove.length / sliceLength); i++) {
 | 
				
			||||||
 | 
					        const slice = toRemove.slice(i * sliceLength, (i + 1) * sliceLength);
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`));
 | 
				
			||||||
 | 
					          numRemoved+= sliceLength;
 | 
				
			||||||
 | 
					          logger.debug(`Deleted ${slice.length} transactions from the Redis cache`);
 | 
				
			||||||
        } catch (e) {
 | 
					        } catch (e) {
 | 
				
			||||||
      logger.warn(`Failed to remove ${transactions.length} transactions from Redis cache: ${e instanceof Error ? e.message : e}`);
 | 
					          logger.warn(`Failed to remove ${slice.length} transactions from Redis cache: ${e instanceof Error ? e.message : e}`);
 | 
				
			||||||
 | 
					          failed = failed.concat(slice);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      // concat instead of replace, in case more txs have been added in the meantime
 | 
				
			||||||
 | 
					      this.removeQueue = this.removeQueue.concat(failed);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      this.removeQueue = this.removeQueue.concat(toRemove);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async $setRbfEntry(type: string, txid: string, value: any): Promise<void> {
 | 
					  async $setRbfEntry(type: string, txid: string, value: any): Promise<void> {
 | 
				
			||||||
 | 
					    if (!config.REDIS.ENABLED) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!this.connected) {
 | 
				
			||||||
 | 
					      this.rbfCacheQueue.push({ type, txid, value });
 | 
				
			||||||
 | 
					      logger.warn(`Failed to set RBF ${type} in Redis cache: Redis is not connected`);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      await this.$ensureConnected();
 | 
					 | 
				
			||||||
      await this.client.set(`rbf:${type}:${txid}`, JSON.stringify(value));
 | 
					      await this.client.set(`rbf:${type}:${txid}`, JSON.stringify(value));
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      logger.warn(`Failed to set RBF ${type} in Redis cache: ${e instanceof Error ? e.message : e}`);
 | 
					      logger.warn(`Failed to set RBF ${type} in Redis cache: ${e instanceof Error ? e.message : e}`);
 | 
				
			||||||
@ -143,17 +222,55 @@ class RedisCache {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async $removeRbfEntry(type: string, txid: string): Promise<void> {
 | 
					  async $removeRbfEntry(type: string, txid: string): Promise<void> {
 | 
				
			||||||
 | 
					    if (!config.REDIS.ENABLED) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!this.connected) {
 | 
				
			||||||
 | 
					      this.rbfRemoveQueue.push({ type, txid });
 | 
				
			||||||
 | 
					      logger.warn(`Failed to remove RBF ${type} from Redis cache: Redis is not connected`);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      await this.$ensureConnected();
 | 
					 | 
				
			||||||
      await this.client.unlink(`rbf:${type}:${txid}`);
 | 
					      await this.client.unlink(`rbf:${type}:${txid}`);
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      logger.warn(`Failed to remove RBF ${type} from Redis cache: ${e instanceof Error ? e.message : e}`);
 | 
					      logger.warn(`Failed to remove RBF ${type} from Redis cache: ${e instanceof Error ? e.message : e}`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async $getBlocks(): Promise<BlockExtended[]> {
 | 
					  private async $flushRbfQueues(): Promise<void> {
 | 
				
			||||||
 | 
					    if (!config.REDIS.ENABLED) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!this.connected) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const toAdd = this.rbfCacheQueue;
 | 
				
			||||||
 | 
					      this.rbfCacheQueue = [];
 | 
				
			||||||
 | 
					      for (const { type, txid, value } of toAdd) {
 | 
				
			||||||
 | 
					        await this.$setRbfEntry(type, txid, value);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      logger.debug(`Saved ${toAdd.length} queued RBF entries to the Redis cache`);
 | 
				
			||||||
 | 
					      const toRemove = this.rbfRemoveQueue;
 | 
				
			||||||
 | 
					      this.rbfRemoveQueue = [];
 | 
				
			||||||
 | 
					      for (const { type, txid } of toRemove) {
 | 
				
			||||||
 | 
					        await this.$removeRbfEntry(type, txid);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      logger.debug(`Removed ${toRemove.length} queued RBF entries from the Redis cache`);
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      logger.warn(`Failed to flush RBF cache event queues after reconnecting to Redis: ${e instanceof Error ? e.message : e}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async $getBlocks(): Promise<BlockExtended[]> {
 | 
				
			||||||
 | 
					    if (!config.REDIS.ENABLED) {
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!this.connected) {
 | 
				
			||||||
 | 
					      logger.warn(`Failed to retrieve blocks from Redis cache: Redis is not connected`);
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      await this.$ensureConnected();
 | 
					 | 
				
			||||||
      const json = await this.client.get('blocks');
 | 
					      const json = await this.client.get('blocks');
 | 
				
			||||||
      return JSON.parse(json);
 | 
					      return JSON.parse(json);
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
@ -163,8 +280,14 @@ class RedisCache {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async $getBlockSummaries(): Promise<BlockSummary[]> {
 | 
					  async $getBlockSummaries(): Promise<BlockSummary[]> {
 | 
				
			||||||
 | 
					    if (!config.REDIS.ENABLED) {
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!this.connected) {
 | 
				
			||||||
 | 
					      logger.warn(`Failed to retrieve blocks from Redis cache: Redis is not connected`);
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      await this.$ensureConnected();
 | 
					 | 
				
			||||||
      const json = await this.client.get('block-summaries');
 | 
					      const json = await this.client.get('block-summaries');
 | 
				
			||||||
      return JSON.parse(json);
 | 
					      return JSON.parse(json);
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
@ -174,10 +297,16 @@ class RedisCache {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async $getMempool(): Promise<{ [txid: string]: MempoolTransactionExtended }> {
 | 
					  async $getMempool(): Promise<{ [txid: string]: MempoolTransactionExtended }> {
 | 
				
			||||||
 | 
					    if (!config.REDIS.ENABLED) {
 | 
				
			||||||
 | 
					      return {};
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!this.connected) {
 | 
				
			||||||
 | 
					      logger.warn(`Failed to retrieve mempool from Redis cache: Redis is not connected`);
 | 
				
			||||||
 | 
					      return {};
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    const start = Date.now();
 | 
					    const start = Date.now();
 | 
				
			||||||
    const mempool = {};
 | 
					    const mempool = {};
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      await this.$ensureConnected();
 | 
					 | 
				
			||||||
      const mempoolList = await this.scanKeys<MempoolTransactionExtended>('mempool:tx:*');
 | 
					      const mempoolList = await this.scanKeys<MempoolTransactionExtended>('mempool:tx:*');
 | 
				
			||||||
      for (const tx of mempoolList) {
 | 
					      for (const tx of mempoolList) {
 | 
				
			||||||
        mempool[tx.key] = tx.value;
 | 
					        mempool[tx.key] = tx.value;
 | 
				
			||||||
@ -191,8 +320,14 @@ class RedisCache {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async $getRbfEntries(type: string): Promise<any[]> {
 | 
					  async $getRbfEntries(type: string): Promise<any[]> {
 | 
				
			||||||
 | 
					    if (!config.REDIS.ENABLED) {
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!this.connected) {
 | 
				
			||||||
 | 
					      logger.warn(`Failed to retrieve Rbf ${type}s from Redis cache: Redis is not connected`);
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      await this.$ensureConnected();
 | 
					 | 
				
			||||||
      const rbfEntries = await this.scanKeys<MempoolTransactionExtended[]>(`rbf:${type}:*`);
 | 
					      const rbfEntries = await this.scanKeys<MempoolTransactionExtended[]>(`rbf:${type}:*`);
 | 
				
			||||||
      return rbfEntries;
 | 
					      return rbfEntries;
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
@ -201,7 +336,10 @@ class RedisCache {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async $loadCache() {
 | 
					  async $loadCache(): Promise<void> {
 | 
				
			||||||
 | 
					    if (!config.REDIS.ENABLED) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    logger.info('Restoring mempool and blocks data from Redis cache');
 | 
					    logger.info('Restoring mempool and blocks data from Redis cache');
 | 
				
			||||||
    // Load block data
 | 
					    // Load block data
 | 
				
			||||||
    const loadedBlocks = await this.$getBlocks();
 | 
					    const loadedBlocks = await this.$getBlocks();
 | 
				
			||||||
@ -226,7 +364,7 @@ class RedisCache {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private inflateLoadedTxs(mempool: { [txid: string]: MempoolTransactionExtended }) {
 | 
					  private inflateLoadedTxs(mempool: { [txid: string]: MempoolTransactionExtended }): void {
 | 
				
			||||||
    for (const tx of Object.values(mempool)) {
 | 
					    for (const tx of Object.values(mempool)) {
 | 
				
			||||||
      for (const vin of tx.vin) {
 | 
					      for (const vin of tx.vin) {
 | 
				
			||||||
        if (vin.scriptsig) {
 | 
					        if (vin.scriptsig) {
 | 
				
			||||||
 | 
				
			|||||||
@ -185,7 +185,8 @@ class Indexer {
 | 
				
			|||||||
      await blocks.$generateCPFPDatabase();
 | 
					      await blocks.$generateCPFPDatabase();
 | 
				
			||||||
      await blocks.$generateAuditStats();
 | 
					      await blocks.$generateAuditStats();
 | 
				
			||||||
      await auditReplicator.$sync();
 | 
					      await auditReplicator.$sync();
 | 
				
			||||||
      await blocks.$classifyBlocks();
 | 
					      // do not wait for classify blocks to finish
 | 
				
			||||||
 | 
					      blocks.$classifyBlocks();
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      this.indexerRunning = false;
 | 
					      this.indexerRunning = false;
 | 
				
			||||||
      logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e));
 | 
					      logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e));
 | 
				
			||||||
 | 
				
			|||||||
@ -416,7 +416,7 @@
 | 
				
			|||||||
      This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the full license terms for more details.<br>
 | 
					      This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the full license terms for more details.<br>
 | 
				
			||||||
    </p>
 | 
					    </p>
 | 
				
			||||||
    <p>
 | 
					    <p>
 | 
				
			||||||
      This program incorporates software and other components licensed from third parties. See the full list of <a href="https://mempool.space/3rdpartylicenses.txt">Third-Party Licenses</a> for legal notices from those projects.
 | 
					      This program incorporates software and other components licensed from third parties. See the full list of <a href="/3rdpartylicenses.txt">Third-Party Licenses</a> for legal notices from those projects.
 | 
				
			||||||
    </p>
 | 
					    </p>
 | 
				
			||||||
    <div class="title">
 | 
					    <div class="title">
 | 
				
			||||||
      Trademark Notice<br>
 | 
					      Trademark Notice<br>
 | 
				
			||||||
@ -429,10 +429,6 @@
 | 
				
			|||||||
    </p>
 | 
					    </p>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="footer-links">
 | 
					 | 
				
			||||||
    <a href="/3rdpartylicenses.txt">Third-party Licenses</a>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  <br>
 | 
					  <br>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -129,7 +129,7 @@
 | 
				
			|||||||
              </tr>
 | 
					              </tr>
 | 
				
			||||||
              <tr class="info">
 | 
					              <tr class="info">
 | 
				
			||||||
                <td class="info">
 | 
					                <td class="info">
 | 
				
			||||||
                  <i><small>mempool.space fee</small></i>
 | 
					                  <i><small>Accelerator Service Fee</small></i>
 | 
				
			||||||
                </td>
 | 
					                </td>
 | 
				
			||||||
                <td class="amt">
 | 
					                <td class="amt">
 | 
				
			||||||
                  +{{ estimate.mempoolBaseFee | number }}
 | 
					                  +{{ estimate.mempoolBaseFee | number }}
 | 
				
			||||||
@ -141,7 +141,7 @@
 | 
				
			|||||||
              </tr>
 | 
					              </tr>
 | 
				
			||||||
              <tr class="info group-last">
 | 
					              <tr class="info group-last">
 | 
				
			||||||
                <td class="info">
 | 
					                <td class="info">
 | 
				
			||||||
                  <i><small>Transaction vsize fee</small></i>
 | 
					                  <i><small>Transaction Size Surcharge</small></i>
 | 
				
			||||||
                </td>
 | 
					                </td>
 | 
				
			||||||
                <td class="amt">
 | 
					                <td class="amt">
 | 
				
			||||||
                  +{{ estimate.vsizeFee | number }}
 | 
					                  +{{ estimate.vsizeFee | number }}
 | 
				
			||||||
 | 
				
			|||||||
@ -11,6 +11,7 @@
 | 
				
			|||||||
  text-align: left;
 | 
					  text-align: left;
 | 
				
			||||||
  min-width: 320px;
 | 
					  min-width: 320px;
 | 
				
			||||||
  pointer-events: none;
 | 
					  pointer-events: none;
 | 
				
			||||||
 | 
					  z-index: 11;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &.clickable {
 | 
					  &.clickable {
 | 
				
			||||||
    pointer-events: all;
 | 
					    pointer-events: all;
 | 
				
			||||||
 | 
				
			|||||||
@ -46,15 +46,13 @@
 | 
				
			|||||||
        <div class="item" *ngIf="showHalving">
 | 
					        <div class="item" *ngIf="showHalving">
 | 
				
			||||||
          <h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5>
 | 
					          <h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5>
 | 
				
			||||||
          <div class="card-text" i18n-ngbTooltip="mining.average-fee" [ngbTooltip]="halvingBlocksLeft" [tooltipContext]="{ epochData: epochData }" placement="bottom">
 | 
					          <div class="card-text" i18n-ngbTooltip="mining.average-fee" [ngbTooltip]="halvingBlocksLeft" [tooltipContext]="{ epochData: epochData }" placement="bottom">
 | 
				
			||||||
            <ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container>
 | 
					            <span>{{ timeUntilHalving | date }}</span>
 | 
				
			||||||
            <ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
 | 
					 | 
				
			||||||
            <ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
 | 
					 | 
				
			||||||
            <div class="symbol" *ngIf="blocksUntilHalving === 1; else approxTime">
 | 
					            <div class="symbol" *ngIf="blocksUntilHalving === 1; else approxTime">
 | 
				
			||||||
              <app-time kind="until" [time]="epochData.adjustedTimeAvg + now" [fastRender]="false" [fixedRender]="true" [precision]="1" minUnit="minute"></app-time>
 | 
					              <app-time kind="until" [time]="epochData.adjustedTimeAvg + now" [fastRender]="false" [fixedRender]="true" [precision]="1" minUnit="minute"></app-time>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <ng-template #approxTime>
 | 
					            <ng-template #approxTime>
 | 
				
			||||||
              <div class="symbol">
 | 
					              <div class="symbol">
 | 
				
			||||||
                <span>{{ timeUntilHalving | date }}</span>
 | 
					                <app-time kind="until" [time]="timeUntilHalving" [fastRender]="false" [fixedRender]="true" [precision]="0" [numUnits]="2" [units]="['year', 'day', 'hour', 'minute']"></app-time>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </ng-template>
 | 
					            </ng-template>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -163,7 +163,7 @@ export class PoolRankingComponent implements OnInit {
 | 
				
			|||||||
            const i = pool.blockCount.toString();
 | 
					            const i = pool.blockCount.toString();
 | 
				
			||||||
            if (this.miningWindowPreference === '24h') {
 | 
					            if (this.miningWindowPreference === '24h') {
 | 
				
			||||||
              return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
 | 
					              return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
 | 
				
			||||||
                pool.lastEstimatedHashrate.toString() + ' PH/s' +
 | 
					                pool.lastEstimatedHashrate.toString() + ' ' + miningStats.miningUnits.hashrateUnit +
 | 
				
			||||||
                `<br>` + $localize`${ i }:INTERPOLATION: blocks`;
 | 
					                `<br>` + $localize`${ i }:INTERPOLATION: blocks`;
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
              return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
 | 
					              return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
 | 
				
			||||||
@ -201,7 +201,7 @@ export class PoolRankingComponent implements OnInit {
 | 
				
			|||||||
          const i = totalBlockOther.toString();
 | 
					          const i = totalBlockOther.toString();
 | 
				
			||||||
          if (this.miningWindowPreference === '24h') {
 | 
					          if (this.miningWindowPreference === '24h') {
 | 
				
			||||||
            return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +
 | 
					            return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +
 | 
				
			||||||
              totalEstimatedHashrateOther.toString() + ' PH/s' +
 | 
					              totalEstimatedHashrateOther.toString() + ' ' + miningStats.miningUnits.hashrateUnit +
 | 
				
			||||||
              `<br>` + $localize`${ i }:INTERPOLATION: blocks`;
 | 
					              `<br>` + $localize`${ i }:INTERPOLATION: blocks`;
 | 
				
			||||||
          } else {
 | 
					          } else {
 | 
				
			||||||
            return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +
 | 
					            return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +
 | 
				
			||||||
 | 
				
			|||||||
@ -26,6 +26,7 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi
 | 
				
			|||||||
import { Price, PriceService } from '../../services/price.service';
 | 
					import { Price, PriceService } from '../../services/price.service';
 | 
				
			||||||
import { isFeatureActive } from '../../bitcoin.utils';
 | 
					import { isFeatureActive } from '../../bitcoin.utils';
 | 
				
			||||||
import { ServicesApiServices } from '../../services/services-api.service';
 | 
					import { ServicesApiServices } from '../../services/services-api.service';
 | 
				
			||||||
 | 
					import { EnterpriseService } from '../../services/enterprise.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Component({
 | 
					@Component({
 | 
				
			||||||
  selector: 'app-transaction',
 | 
					  selector: 'app-transaction',
 | 
				
			||||||
@ -116,12 +117,15 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
				
			|||||||
    private servicesApiService: ServicesApiServices,
 | 
					    private servicesApiService: ServicesApiServices,
 | 
				
			||||||
    private seoService: SeoService,
 | 
					    private seoService: SeoService,
 | 
				
			||||||
    private priceService: PriceService,
 | 
					    private priceService: PriceService,
 | 
				
			||||||
    private storageService: StorageService
 | 
					    private storageService: StorageService,
 | 
				
			||||||
 | 
					    private enterpriseService: EnterpriseService,
 | 
				
			||||||
  ) {}
 | 
					  ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ngOnInit() {
 | 
					  ngOnInit() {
 | 
				
			||||||
    this.acceleratorAvailable = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
 | 
					    this.acceleratorAvailable = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.enterpriseService.page();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.websocketService.want(['blocks', 'mempool-blocks']);
 | 
					    this.websocketService.want(['blocks', 'mempool-blocks']);
 | 
				
			||||||
    this.stateService.networkChanged$.subscribe(
 | 
					    this.stateService.networkChanged$.subscribe(
 | 
				
			||||||
      (network) => {
 | 
					      (network) => {
 | 
				
			||||||
@ -527,6 +531,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
				
			|||||||
    if (!this.txId) {
 | 
					    if (!this.txId) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    this.enterpriseService.goal(8);
 | 
				
			||||||
    this.showAccelerationSummary = true && this.acceleratorAvailable;
 | 
					    this.showAccelerationSummary = true && this.acceleratorAvailable;
 | 
				
			||||||
    this.scrollIntoAccelPreview = !this.scrollIntoAccelPreview;
 | 
					    this.scrollIntoAccelPreview = !this.scrollIntoAccelPreview;
 | 
				
			||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
 | 
				
			|||||||
@ -88,7 +88,7 @@ export class NodesMap implements OnInit, OnChanges {
 | 
				
			|||||||
            node.public_key,
 | 
					            node.public_key,
 | 
				
			||||||
            node.alias,
 | 
					            node.alias,
 | 
				
			||||||
            node.capacity,
 | 
					            node.capacity,
 | 
				
			||||||
            node.active_channel_count,
 | 
					            node.channels,
 | 
				
			||||||
            node.country,
 | 
					            node.country,
 | 
				
			||||||
            node.iso_code,
 | 
					            node.iso_code,
 | 
				
			||||||
          ]);
 | 
					          ]);
 | 
				
			||||||
 | 
				
			|||||||
@ -64,8 +64,8 @@
 | 
				
			|||||||
        <th class="channels text-right" i18n="lightning.channels">Channels</th>
 | 
					        <th class="channels text-right" i18n="lightning.channels">Channels</th>
 | 
				
			||||||
        <th class="city text-right" i18n="lightning.location">Location</th>
 | 
					        <th class="city text-right" i18n="lightning.location">Location</th>
 | 
				
			||||||
      </thead>
 | 
					      </thead>
 | 
				
			||||||
      <tbody *ngIf="nodes$ | async as countryNodes; else skeleton">
 | 
					      <tbody *ngIf="nodesPagination$ | async as countryNodes; else skeleton">
 | 
				
			||||||
        <tr *ngFor="let node of countryNodes.nodes; let i= index; trackBy: trackByPublicKey">
 | 
					        <tr *ngFor="let node of countryNodes; let i= index; trackBy: trackByPublicKey">
 | 
				
			||||||
          <td class="alias text-left text-truncate">
 | 
					          <td class="alias text-left text-truncate">
 | 
				
			||||||
            <a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a>
 | 
					            <a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a>
 | 
				
			||||||
          </td>
 | 
					          </td>
 | 
				
			||||||
@ -116,5 +116,10 @@
 | 
				
			|||||||
      </ng-template>
 | 
					      </ng-template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    </table>
 | 
					    </table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <ngb-pagination *ngIf="nodes$ | async as countryNodes" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
 | 
				
			||||||
 | 
					        [collectionSize]="countryNodes.nodes.length" [rotate]="true" [maxSize]="maxSize" [pageSize]="pageSize" [(page)]="page"
 | 
				
			||||||
 | 
					        (pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
 | 
				
			||||||
 | 
					      </ngb-pagination>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
				
			|||||||
@ -22,14 +22,14 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.timestamp-first {
 | 
					.timestamp-first {
 | 
				
			||||||
  width: 20%;
 | 
					  width: 20%;
 | 
				
			||||||
  @media (max-width: 576px) {
 | 
					  @media (max-width: 1060px) {
 | 
				
			||||||
    display: none
 | 
					    display: none
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.timestamp-update {
 | 
					.timestamp-update {
 | 
				
			||||||
  width: 16%;
 | 
					  width: 16%;
 | 
				
			||||||
  @media (max-width: 576px) {
 | 
					  @media (max-width: 1060px) {
 | 
				
			||||||
    display: none
 | 
					    display: none
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -50,7 +50,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.city {
 | 
					.city {
 | 
				
			||||||
  max-width: 150px;
 | 
					  max-width: 150px;
 | 
				
			||||||
  @media (max-width: 576px) {
 | 
					  @media (max-width: 675px) {
 | 
				
			||||||
    display: none
 | 
					    display: none
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
 | 
					import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
 | 
				
			||||||
import { ActivatedRoute } from '@angular/router';
 | 
					import { ActivatedRoute } from '@angular/router';
 | 
				
			||||||
import { map, Observable, share } from 'rxjs';
 | 
					import { BehaviorSubject, combineLatest, map, Observable, share, tap } from 'rxjs';
 | 
				
			||||||
import { ApiService } from '../../services/api.service';
 | 
					import { ApiService } from '../../services/api.service';
 | 
				
			||||||
import { SeoService } from '../../services/seo.service';
 | 
					import { SeoService } from '../../services/seo.service';
 | 
				
			||||||
import { getFlagEmoji } from '../../shared/common.utils';
 | 
					import { getFlagEmoji } from '../../shared/common.utils';
 | 
				
			||||||
@ -15,6 +15,12 @@ import { GeolocationData } from '../../shared/components/geolocation/geolocation
 | 
				
			|||||||
export class NodesPerCountry implements OnInit {
 | 
					export class NodesPerCountry implements OnInit {
 | 
				
			||||||
  nodes$: Observable<any>;
 | 
					  nodes$: Observable<any>;
 | 
				
			||||||
  country: {name: string, flag: string};
 | 
					  country: {name: string, flag: string};
 | 
				
			||||||
 | 
					  nodesPagination$: Observable<any>;
 | 
				
			||||||
 | 
					  startingIndexSubject: BehaviorSubject<number> = new BehaviorSubject(0);
 | 
				
			||||||
 | 
					  page = 1;
 | 
				
			||||||
 | 
					  pageSize = 15;
 | 
				
			||||||
 | 
					  maxSize = window.innerWidth <= 767.98 ? 3 : 5;
 | 
				
			||||||
 | 
					  isLoading = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  skeletonLines: number[] = [];
 | 
					  skeletonLines: number[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -23,7 +29,7 @@ export class NodesPerCountry implements OnInit {
 | 
				
			|||||||
    private seoService: SeoService,
 | 
					    private seoService: SeoService,
 | 
				
			||||||
    private route: ActivatedRoute,
 | 
					    private route: ActivatedRoute,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    for (let i = 0; i < 20; ++i) {
 | 
					    for (let i = 0; i < this.pageSize; ++i) {
 | 
				
			||||||
      this.skeletonLines.push(i);
 | 
					      this.skeletonLines.push(i);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -31,6 +37,7 @@ export class NodesPerCountry implements OnInit {
 | 
				
			|||||||
  ngOnInit(): void {
 | 
					  ngOnInit(): void {
 | 
				
			||||||
    this.nodes$ = this.apiService.getNodeForCountry$(this.route.snapshot.params.country)
 | 
					    this.nodes$ = this.apiService.getNodeForCountry$(this.route.snapshot.params.country)
 | 
				
			||||||
      .pipe(
 | 
					      .pipe(
 | 
				
			||||||
 | 
					        tap(() => this.isLoading = true),
 | 
				
			||||||
        map(response => {
 | 
					        map(response => {
 | 
				
			||||||
          this.seoService.setTitle($localize`Lightning nodes in ${response.country.en}`);
 | 
					          this.seoService.setTitle($localize`Lightning nodes in ${response.country.en}`);
 | 
				
			||||||
          this.seoService.setDescription($localize`:@@meta.description.lightning.nodes-country:Explore all the Lightning nodes hosted in ${response.country.en} and see an overview of each node's capacity, number of open channels, and more.`);
 | 
					          this.seoService.setDescription($localize`:@@meta.description.lightning.nodes-country:Explore all the Lightning nodes hosted in ${response.country.en} and see an overview of each node's capacity, number of open channels, and more.`);
 | 
				
			||||||
@ -87,11 +94,21 @@ export class NodesPerCountry implements OnInit {
 | 
				
			|||||||
            ispCount: Object.keys(isps).length
 | 
					            ispCount: Object.keys(isps).length
 | 
				
			||||||
          };
 | 
					          };
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
 | 
					        tap(() => this.isLoading = false),
 | 
				
			||||||
        share()
 | 
					        share()
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.nodesPagination$ = combineLatest([this.nodes$, this.startingIndexSubject]).pipe(
 | 
				
			||||||
 | 
					      map(([response, startingIndex]) => response.nodes.slice(startingIndex, startingIndex + this.pageSize))
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  trackByPublicKey(index: number, node: any): string {
 | 
					  trackByPublicKey(index: number, node: any): string {
 | 
				
			||||||
    return node.public_key;
 | 
					    return node.public_key;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pageChange(page: number): void {
 | 
				
			||||||
 | 
					    this.startingIndexSubject.next((page - 1) * this.pageSize);
 | 
				
			||||||
 | 
					    this.page = page;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -61,8 +61,8 @@
 | 
				
			|||||||
        <th class="channels text-right" i18n="lightning.channels">Channels</th>
 | 
					        <th class="channels text-right" i18n="lightning.channels">Channels</th>
 | 
				
			||||||
        <th class="city text-right" i18n="lightning.location">Location</th>
 | 
					        <th class="city text-right" i18n="lightning.location">Location</th>
 | 
				
			||||||
      </thead>
 | 
					      </thead>
 | 
				
			||||||
      <tbody *ngIf="nodes$ | async as ispNodes; else skeleton">
 | 
					      <tbody *ngIf="nodesPagination$ | async as ispNodes; else skeleton">
 | 
				
			||||||
        <tr *ngFor="let node of ispNodes.nodes; let i= index; trackBy: trackByPublicKey">
 | 
					        <tr *ngFor="let node of ispNodes; let i= index; trackBy: trackByPublicKey">
 | 
				
			||||||
          <td class="alias text-left text-truncate">
 | 
					          <td class="alias text-left text-truncate">
 | 
				
			||||||
            <a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a>
 | 
					            <a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a>
 | 
				
			||||||
          </td>
 | 
					          </td>
 | 
				
			||||||
@ -113,5 +113,10 @@
 | 
				
			|||||||
      </ng-template>
 | 
					      </ng-template>
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
    </table>
 | 
					    </table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <ngb-pagination *ngIf="nodes$ | async as ispNodes" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
 | 
				
			||||||
 | 
					        [collectionSize]="ispNodes.nodes.length" [rotate]="true" [maxSize]="maxSize" [pageSize]="pageSize" [(page)]="page"
 | 
				
			||||||
 | 
					        (pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
 | 
				
			||||||
 | 
					      </ngb-pagination>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
				
			|||||||
@ -24,7 +24,7 @@
 | 
				
			|||||||
.timestamp-first {
 | 
					.timestamp-first {
 | 
				
			||||||
  width: 20%;
 | 
					  width: 20%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @media (max-width: 576px) {
 | 
					  @media (max-width: 1060px) {
 | 
				
			||||||
    display: none
 | 
					    display: none
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -32,7 +32,7 @@
 | 
				
			|||||||
.timestamp-update {
 | 
					.timestamp-update {
 | 
				
			||||||
  width: 16%;
 | 
					  width: 16%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @media (max-width: 576px) {
 | 
					  @media (max-width: 1060px) {
 | 
				
			||||||
    display: none
 | 
					    display: none
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -56,7 +56,7 @@
 | 
				
			|||||||
.city {
 | 
					.city {
 | 
				
			||||||
  max-width: 150px;
 | 
					  max-width: 150px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @media (max-width: 576px) {
 | 
					  @media (max-width: 675px) {
 | 
				
			||||||
    display: none
 | 
					    display: none
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 | 
					import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 | 
				
			||||||
import { ActivatedRoute } from '@angular/router';
 | 
					import { ActivatedRoute } from '@angular/router';
 | 
				
			||||||
import { map, Observable, share } from 'rxjs';
 | 
					import { BehaviorSubject, combineLatest, map, Observable, share, tap } from 'rxjs';
 | 
				
			||||||
import { ApiService } from '../../services/api.service';
 | 
					import { ApiService } from '../../services/api.service';
 | 
				
			||||||
import { SeoService } from '../../services/seo.service';
 | 
					import { SeoService } from '../../services/seo.service';
 | 
				
			||||||
import { getFlagEmoji } from '../../shared/common.utils';
 | 
					import { getFlagEmoji } from '../../shared/common.utils';
 | 
				
			||||||
@ -15,6 +15,12 @@ import { GeolocationData } from '../../shared/components/geolocation/geolocation
 | 
				
			|||||||
export class NodesPerISP implements OnInit {
 | 
					export class NodesPerISP implements OnInit {
 | 
				
			||||||
  nodes$: Observable<any>;
 | 
					  nodes$: Observable<any>;
 | 
				
			||||||
  isp: {name: string, id: number};
 | 
					  isp: {name: string, id: number};
 | 
				
			||||||
 | 
					  nodesPagination$: Observable<any>;
 | 
				
			||||||
 | 
					  startingIndexSubject: BehaviorSubject<number> = new BehaviorSubject(0);
 | 
				
			||||||
 | 
					  page = 1;
 | 
				
			||||||
 | 
					  pageSize = 15;
 | 
				
			||||||
 | 
					  maxSize = window.innerWidth <= 767.98 ? 3 : 5;
 | 
				
			||||||
 | 
					  isLoading = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  skeletonLines: number[] = [];
 | 
					  skeletonLines: number[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -23,7 +29,7 @@ export class NodesPerISP implements OnInit {
 | 
				
			|||||||
    private seoService: SeoService,
 | 
					    private seoService: SeoService,
 | 
				
			||||||
    private route: ActivatedRoute,
 | 
					    private route: ActivatedRoute,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    for (let i = 0; i < 20; ++i) {
 | 
					    for (let i = 0; i < this.pageSize; ++i) {
 | 
				
			||||||
      this.skeletonLines.push(i);
 | 
					      this.skeletonLines.push(i);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -31,6 +37,7 @@ export class NodesPerISP implements OnInit {
 | 
				
			|||||||
  ngOnInit(): void {
 | 
					  ngOnInit(): void {
 | 
				
			||||||
    this.nodes$ = this.apiService.getNodeForISP$(this.route.snapshot.params.isp)
 | 
					    this.nodes$ = this.apiService.getNodeForISP$(this.route.snapshot.params.isp)
 | 
				
			||||||
      .pipe(
 | 
					      .pipe(
 | 
				
			||||||
 | 
					        tap(() => this.isLoading = true),
 | 
				
			||||||
        map(response => {
 | 
					        map(response => {
 | 
				
			||||||
          this.isp = {
 | 
					          this.isp = {
 | 
				
			||||||
            name: response.isp,
 | 
					            name: response.isp,
 | 
				
			||||||
@ -77,11 +84,21 @@ export class NodesPerISP implements OnInit {
 | 
				
			|||||||
            topCountry: topCountry,
 | 
					            topCountry: topCountry,
 | 
				
			||||||
          };
 | 
					          };
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
 | 
					        tap(() => this.isLoading = false),
 | 
				
			||||||
        share()
 | 
					        share()
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.nodesPagination$ = combineLatest([this.nodes$, this.startingIndexSubject]).pipe(
 | 
				
			||||||
 | 
					      map(([response, startingIndex]) => response.nodes.slice(startingIndex, startingIndex + this.pageSize))
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  trackByPublicKey(index: number, node: any): string {
 | 
					  trackByPublicKey(index: number, node: any): string {
 | 
				
			||||||
    return node.public_key;
 | 
					    return node.public_key;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pageChange(page: number): void {
 | 
				
			||||||
 | 
					    this.startingIndexSubject.next((page - 1) * this.pageSize);
 | 
				
			||||||
 | 
					    this.page = page;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -139,6 +139,14 @@ export class EnterpriseService {
 | 
				
			|||||||
    this.getMatomo()?.trackGoal(id);
 | 
					    this.getMatomo()?.trackGoal(id);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  page() {
 | 
				
			||||||
 | 
					    const matomo = this.getMatomo();
 | 
				
			||||||
 | 
					    if (matomo) {
 | 
				
			||||||
 | 
					      matomo.setCustomUrl(this.getCustomUrl());
 | 
				
			||||||
 | 
					      matomo.trackPageView();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private getCustomUrl(): string {
 | 
					  private getCustomUrl(): string {
 | 
				
			||||||
    let url = window.location.origin + '/';
 | 
					    let url = window.location.origin + '/';
 | 
				
			||||||
    let route = this.activatedRoute;
 | 
					    let route = this.activatedRoute;
 | 
				
			||||||
 | 
				
			|||||||
@ -77,6 +77,7 @@
 | 
				
			|||||||
          <p><a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a></p>
 | 
					          <p><a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a></p>
 | 
				
			||||||
          <p><a [routerLink]="['/privacy-policy']" i18n="shared.privacy-policy|Privacy Policy">Privacy Policy</a></p>
 | 
					          <p><a [routerLink]="['/privacy-policy']" i18n="shared.privacy-policy|Privacy Policy">Privacy Policy</a></p>
 | 
				
			||||||
          <p><a [routerLink]="['/trademark-policy']" i18n="shared.trademark-policy|Trademark Policy">Trademark Policy</a></p>
 | 
					          <p><a [routerLink]="['/trademark-policy']" i18n="shared.trademark-policy|Trademark Policy">Trademark Policy</a></p>
 | 
				
			||||||
 | 
					          <p><a [routerLink]="['/3rdpartylicenses.txt']" i18n="shared.trademark-policy|Third-party Licenses">Third-party Licenses</a></p>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="row social-links">
 | 
					    <div class="row social-links">
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,6 @@ discover=1
 | 
				
			|||||||
par=16
 | 
					par=16
 | 
				
			||||||
dbcache=8192
 | 
					dbcache=8192
 | 
				
			||||||
maxmempool=4096
 | 
					maxmempool=4096
 | 
				
			||||||
mempoolexpiry=999999
 | 
					 | 
				
			||||||
mempoolfullrbf=1
 | 
					mempoolfullrbf=1
 | 
				
			||||||
maxconnections=100
 | 
					maxconnections=100
 | 
				
			||||||
onion=127.0.0.1:9050
 | 
					onion=127.0.0.1:9050
 | 
				
			||||||
@ -20,6 +19,7 @@ whitelist=2401:b140::/32
 | 
				
			|||||||
#uacomment=@wiz
 | 
					#uacomment=@wiz
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[main]
 | 
					[main]
 | 
				
			||||||
 | 
					mempoolexpiry=999999
 | 
				
			||||||
rpcbind=127.0.0.1:8332
 | 
					rpcbind=127.0.0.1:8332
 | 
				
			||||||
rpcbind=[::1]:8332
 | 
					rpcbind=[::1]:8332
 | 
				
			||||||
bind=0.0.0.0:8333
 | 
					bind=0.0.0.0:8333
 | 
				
			||||||
 | 
				
			|||||||
@ -20,5 +20,10 @@ for pid in `ps uaxww|grep warmer|grep zsh|awk '{print $2}'`;do
 | 
				
			|||||||
    kill $pid
 | 
					    kill $pid
 | 
				
			||||||
done
 | 
					done
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# kill nginx cache heater scripts
 | 
				
			||||||
 | 
					for pid in `ps uaxww|grep heater|grep zsh|awk '{print $2}'`;do
 | 
				
			||||||
 | 
					    kill $pid
 | 
				
			||||||
 | 
					done
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# always exit successfully despite above errors
 | 
					# always exit successfully despite above errors
 | 
				
			||||||
exit 0
 | 
					exit 0
 | 
				
			||||||
 | 
				
			|||||||
@ -251,7 +251,8 @@ class Server {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      if (!img) {
 | 
					      if (!img) {
 | 
				
			||||||
        // send local fallback image file
 | 
					        // send local fallback image file
 | 
				
			||||||
        res.sendFile(nodejsPath.join(__dirname, matchedRoute.fallbackFile));
 | 
					        res.set('Cache-control', 'no-cache');
 | 
				
			||||||
 | 
					        res.sendFile(nodejsPath.join(__dirname, matchedRoute.fallbackImg));
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        res.contentType('image/png');
 | 
					        res.contentType('image/png');
 | 
				
			||||||
        res.send(img);
 | 
					        res.send(img);
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								unfurler/src/resources
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						
									
										1
									
								
								unfurler/src/resources
									
									
									
									
									
										Symbolic link
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					../../frontend/src/resources
 | 
				
			||||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 94 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 289 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 1.8 MiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 96 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 289 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 607 KiB  | 
@ -2,7 +2,6 @@ interface Match {
 | 
				
			|||||||
  render: boolean;
 | 
					  render: boolean;
 | 
				
			||||||
  title: string;
 | 
					  title: string;
 | 
				
			||||||
  fallbackImg: string;
 | 
					  fallbackImg: string;
 | 
				
			||||||
  fallbackFile: string;
 | 
					 | 
				
			||||||
  staticImg?: string;
 | 
					  staticImg?: string;
 | 
				
			||||||
  networkMode: string;
 | 
					  networkMode: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -32,7 +31,6 @@ const routes = {
 | 
				
			|||||||
  lightning: {
 | 
					  lightning: {
 | 
				
			||||||
    title: "Lightning",
 | 
					    title: "Lightning",
 | 
				
			||||||
    fallbackImg: '/resources/previews/lightning.png',
 | 
					    fallbackImg: '/resources/previews/lightning.png',
 | 
				
			||||||
    fallbackFile: '/resources/img/lightning.png',
 | 
					 | 
				
			||||||
    routes: {
 | 
					    routes: {
 | 
				
			||||||
      node: {
 | 
					      node: {
 | 
				
			||||||
        render: true,
 | 
					        render: true,
 | 
				
			||||||
@ -71,7 +69,6 @@ const routes = {
 | 
				
			|||||||
  mining: {
 | 
					  mining: {
 | 
				
			||||||
    title: "Mining",
 | 
					    title: "Mining",
 | 
				
			||||||
    fallbackImg: '/resources/previews/mining.png',
 | 
					    fallbackImg: '/resources/previews/mining.png',
 | 
				
			||||||
    fallbackFile: '/resources/img/mining.png',
 | 
					 | 
				
			||||||
    routes: {
 | 
					    routes: {
 | 
				
			||||||
      pool: {
 | 
					      pool: {
 | 
				
			||||||
        render: true,
 | 
					        render: true,
 | 
				
			||||||
@ -87,14 +84,12 @@ const routes = {
 | 
				
			|||||||
const networks = {
 | 
					const networks = {
 | 
				
			||||||
  bitcoin: {
 | 
					  bitcoin: {
 | 
				
			||||||
    fallbackImg: '/resources/previews/dashboard.png',
 | 
					    fallbackImg: '/resources/previews/dashboard.png',
 | 
				
			||||||
    fallbackFile: '/resources/img/dashboard.png',
 | 
					 | 
				
			||||||
    routes: {
 | 
					    routes: {
 | 
				
			||||||
      ...routes // all routes supported
 | 
					      ...routes // all routes supported
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  liquid: {
 | 
					  liquid: {
 | 
				
			||||||
    fallbackImg: '/resources/liquid/liquid-network-preview.png',
 | 
					    fallbackImg: '/resources/liquid/liquid-network-preview.png',
 | 
				
			||||||
    fallbackFile: '/resources/img/liquid',
 | 
					 | 
				
			||||||
    routes: { // only block, address & tx routes supported
 | 
					    routes: { // only block, address & tx routes supported
 | 
				
			||||||
      block: routes.block,
 | 
					      block: routes.block,
 | 
				
			||||||
      address: routes.address,
 | 
					      address: routes.address,
 | 
				
			||||||
@ -103,7 +98,6 @@ const networks = {
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  bisq: {
 | 
					  bisq: {
 | 
				
			||||||
    fallbackImg: '/resources/bisq/bisq-markets-preview.png',
 | 
					    fallbackImg: '/resources/bisq/bisq-markets-preview.png',
 | 
				
			||||||
    fallbackFile: '/resources/img/bisq.png',
 | 
					 | 
				
			||||||
    routes: {} // no routes supported
 | 
					    routes: {} // no routes supported
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@ -113,7 +107,6 @@ export function matchRoute(network: string, path: string): Match {
 | 
				
			|||||||
    render: false,
 | 
					    render: false,
 | 
				
			||||||
    title: '',
 | 
					    title: '',
 | 
				
			||||||
    fallbackImg: '',
 | 
					    fallbackImg: '',
 | 
				
			||||||
    fallbackFile: '',
 | 
					 | 
				
			||||||
    networkMode: 'mainnet'
 | 
					    networkMode: 'mainnet'
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -128,7 +121,6 @@ export function matchRoute(network: string, path: string): Match {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  let route = networks[network] || networks.bitcoin;
 | 
					  let route = networks[network] || networks.bitcoin;
 | 
				
			||||||
  match.fallbackImg = route.fallbackImg;
 | 
					  match.fallbackImg = route.fallbackImg;
 | 
				
			||||||
  match.fallbackFile = route.fallbackFile;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // traverse the route tree until we run out of route or tree, or hit a renderable match
 | 
					  // traverse the route tree until we run out of route or tree, or hit a renderable match
 | 
				
			||||||
  while (!route.render && route.routes && parts.length && route.routes[parts[0]]) {
 | 
					  while (!route.render && route.routes && parts.length && route.routes[parts[0]]) {
 | 
				
			||||||
@ -136,7 +128,6 @@ export function matchRoute(network: string, path: string): Match {
 | 
				
			|||||||
    parts.shift();
 | 
					    parts.shift();
 | 
				
			||||||
    if (route.fallbackImg) {
 | 
					    if (route.fallbackImg) {
 | 
				
			||||||
      match.fallbackImg = route.fallbackImg;
 | 
					      match.fallbackImg = route.fallbackImg;
 | 
				
			||||||
      match.fallbackFile = route.fallbackFile;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user