mirror of
https://github.com/mempool/mempool.git
synced 2025-02-23 22:46:54 +01:00
Merge branch 'master' into nymkappa/update-doc
This commit is contained in:
commit
968a26d2cd
31 changed files with 299 additions and 102 deletions
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) => {
|
||||||
const version = await this.client.get('schema_version');
|
logger.err(`Error in Redis client: ${e instanceof Error ? e.message : e}`);
|
||||||
if (version !== this.schemaVersion) {
|
this.connected = false;
|
||||||
// schema changed
|
await this.client.disconnect();
|
||||||
// perform migrations or flush DB if necessary
|
});
|
||||||
logger.info(`Redis schema version changed from ${version} to ${this.schemaVersion}`);
|
await this.client.connect().then(async () => {
|
||||||
await this.client.set('schema_version', this.schemaVersion);
|
try {
|
||||||
}
|
const version = await this.client.get('schema_version');
|
||||||
});
|
this.connected = true;
|
||||||
|
if (version !== this.schemaVersion) {
|
||||||
|
// schema changed
|
||||||
|
// perform migrations or flush DB if necessary
|
||||||
|
logger.info(`Redis schema version changed from ${version} to ${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) {
|
||||||
await this.$flushTransactions();
|
if (!this.pauseFlush) {
|
||||||
|
await this.$flushTransactions();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async $flushTransactions() {
|
async $flushTransactions(): Promise<void> {
|
||||||
const success = await this.$addTransactions(this.cacheQueue);
|
if (!config.REDIS.ENABLED) {
|
||||||
if (success) {
|
return;
|
||||||
logger.debug(`Saved ${this.cacheQueue.length} transactions to Redis cache`);
|
}
|
||||||
this.cacheQueue = [];
|
if (!this.cacheQueue.length) {
|
||||||
} else {
|
return;
|
||||||
logger.err(`Failed to save ${this.cacheQueue.length} transactions to Redis cache`);
|
}
|
||||||
|
if (!this.connected) {
|
||||||
|
logger.warn(`Failed to add ${this.cacheQueue.length} transactions to Redis cache: Redis not connected`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private async $addTransactions(newTransactions: MempoolTransactionExtended[]): Promise<boolean> {
|
this.pauseFlush = false;
|
||||||
if (!newTransactions.length) {
|
|
||||||
return true;
|
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 toRemove = this.removeQueue.concat(transactions);
|
||||||
|
this.removeQueue = [];
|
||||||
|
let failed: string[] = [];
|
||||||
|
let numRemoved = 0;
|
||||||
|
if (this.connected) {
|
||||||
const sliceLength = config.REDIS.BATCH_QUERY_BASE_SIZE;
|
const sliceLength = config.REDIS.BATCH_QUERY_BASE_SIZE;
|
||||||
for (let i = 0; i < Math.ceil(transactions.length / sliceLength); i++) {
|
for (let i = 0; i < Math.ceil(toRemove.length / sliceLength); i++) {
|
||||||
const slice = transactions.slice(i * sliceLength, (i + 1) * sliceLength);
|
const slice = toRemove.slice(i * sliceLength, (i + 1) * sliceLength);
|
||||||
await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`));
|
try {
|
||||||
logger.debug(`Deleted ${slice.length} transactions from the Redis cache`);
|
await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`));
|
||||||
|
numRemoved+= sliceLength;
|
||||||
|
logger.debug(`Deleted ${slice.length} transactions from the Redis cache`);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Failed to remove ${slice.length} transactions from Redis cache: ${e instanceof Error ? e.message : e}`);
|
||||||
|
failed = failed.concat(slice);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
// concat instead of replace, in case more txs have been added in the meantime
|
||||||
logger.warn(`Failed to remove ${transactions.length} transactions from Redis cache: ${e instanceof Error ? e.message : e}`);
|
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…
Add table
Reference in a new issue