Compare commits
1 Commits
v2.4.1
...
knorrium/f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
897049903b |
60
.github/workflows/ci.yml
vendored
60
.github/workflows/ci.yml
vendored
@@ -2,93 +2,91 @@ name: CI Pipeline for the Backend and Frontend
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, review_requested, synchronize]
|
||||
types: [ opened, review_requested, synchronize ]
|
||||
env:
|
||||
NODE_VERSION: 16.15.0
|
||||
|
||||
jobs:
|
||||
backend:
|
||||
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
||||
strategy:
|
||||
matrix:
|
||||
node: ["16.16.0", "18.5.0"]
|
||||
flavor: ["dev", "prod"]
|
||||
fail-fast: false
|
||||
runs-on: "ubuntu-latest"
|
||||
flavor: ['dev', 'prod']
|
||||
runs-on: 'ubuntu-latest'
|
||||
|
||||
name: Backend (${{ matrix.flavor }}) - node ${{ matrix.node }}
|
||||
name: Backend (${{ matrix.flavor }})
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: ${{ matrix.node }}/${{ matrix.flavor }}
|
||||
path: ${{ matrix.flavor }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install
|
||||
if: ${{ matrix.flavor == 'dev'}}
|
||||
run: npm ci
|
||||
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend
|
||||
working-directory: ${{ matrix.flavor }}/backend
|
||||
|
||||
- name: Install (Prod dependencies only)
|
||||
if: ${{ matrix.flavor == 'prod'}}
|
||||
run: npm ci --omit=dev --omit=optional
|
||||
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend
|
||||
run: npm ci --prod --no-optional
|
||||
working-directory: ${{ matrix.flavor }}/backend
|
||||
|
||||
- name: Lint
|
||||
- name: Lint
|
||||
if: ${{ matrix.flavor == 'dev'}}
|
||||
run: npm run lint
|
||||
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend
|
||||
working-directory: ${{ matrix.flavor }}/backend
|
||||
|
||||
# - name: Test
|
||||
# run: npm run test
|
||||
# - name: Test
|
||||
# run: npm run test
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/backend
|
||||
working-directory: ${{ matrix.flavor }}/backend
|
||||
|
||||
frontend:
|
||||
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
|
||||
strategy:
|
||||
matrix:
|
||||
node: ["16.15.0", "18.5.0"]
|
||||
flavor: ["dev", "prod"]
|
||||
fail-fast: false
|
||||
runs-on: "ubuntu-latest"
|
||||
flavor: ['dev', 'prod']
|
||||
runs-on: 'ubuntu-latest'
|
||||
|
||||
name: Frontend (${{ matrix.flavor }}) - node ${{ matrix.node }}
|
||||
name: Frontend (${{ matrix.flavor }})
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: ${{ matrix.node }}/${{ matrix.flavor }}
|
||||
path: ${{ matrix.flavor }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install (Prod dependencies only)
|
||||
run: npm ci --omit=dev --omit=optional
|
||||
run: npm ci --prod --no-optional
|
||||
if: ${{ matrix.flavor == 'prod'}}
|
||||
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/frontend
|
||||
working-directory: ${{ matrix.flavor }}/frontend
|
||||
|
||||
- name: Install
|
||||
if: ${{ matrix.flavor == 'dev'}}
|
||||
run: npm ci
|
||||
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/frontend
|
||||
working-directory: ${{ matrix.flavor }}/frontend
|
||||
|
||||
- name: Lint
|
||||
if: ${{ matrix.flavor == 'dev'}}
|
||||
run: npm run lint
|
||||
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/frontend
|
||||
working-directory: ${{ matrix.flavor }}/frontend
|
||||
|
||||
# - name: Test
|
||||
# - name: Test
|
||||
# run: npm run test
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}/frontend
|
||||
working-directory: ${{ matrix.flavor }}/frontend
|
||||
|
||||
@@ -20,8 +20,7 @@
|
||||
"EXTERNAL_MAX_RETRY": 1,
|
||||
"EXTERNAL_RETRY_INTERVAL": 0,
|
||||
"USER_AGENT": "mempool",
|
||||
"STDOUT_LOG_MIN_PRIORITY": "debug",
|
||||
"AUTOMATIC_BLOCK_REINDEXING": false
|
||||
"STDOUT_LOG_MIN_PRIORITY": "debug"
|
||||
},
|
||||
"CORE_RPC": {
|
||||
"HOST": "127.0.0.1",
|
||||
|
||||
30
backend/package-lock.json
generated
30
backend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mempool-backend",
|
||||
"version": "2.4.1",
|
||||
"version": "2.4.1-dev",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mempool-backend",
|
||||
"version": "2.4.1",
|
||||
"version": "2.4.1-dev",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"dependencies": {
|
||||
"@mempool/electrum-client": "^1.1.7",
|
||||
@@ -33,6 +33,32 @@
|
||||
"prettier": "^2.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz",
|
||||
"integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/highlight": "^7.10.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
|
||||
"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@babel/highlight": {
|
||||
"version": "7.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz",
|
||||
"integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.10.4",
|
||||
"chalk": "^2.0.0",
|
||||
"js-tokens": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mempool-backend",
|
||||
"version": "2.4.1",
|
||||
"version": "2.4.1-dev",
|
||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"homepage": "https://mempool.space",
|
||||
|
||||
@@ -168,7 +168,7 @@ class Blocks {
|
||||
blockExtended.extras.avgFeeRate = stats.avgfeerate;
|
||||
}
|
||||
|
||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
|
||||
if (['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) {
|
||||
let pool: PoolTag;
|
||||
if (blockExtended.extras?.coinbaseTx !== undefined) {
|
||||
pool = await this.$findBlockMiner(blockExtended.extras?.coinbaseTx);
|
||||
@@ -280,7 +280,8 @@ class Blocks {
|
||||
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
|
||||
const progress = Math.round(totalIndexed / indexedBlocks.length * 10000) / 100;
|
||||
logger.debug(`Indexing block summary for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds`);
|
||||
const timeLeft = Math.round((indexedBlocks.length - totalIndexed) / blockPerSeconds);
|
||||
logger.debug(`Indexing block summary for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds | left: ~${timeLeft} seconds`);
|
||||
timer = new Date().getTime() / 1000;
|
||||
indexedThisRun = 0;
|
||||
}
|
||||
@@ -292,11 +293,7 @@ class Blocks {
|
||||
totalIndexed++;
|
||||
newlyIndexed++;
|
||||
}
|
||||
if (newlyIndexed > 0) {
|
||||
logger.notice(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`);
|
||||
} else {
|
||||
logger.debug(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`);
|
||||
}
|
||||
logger.notice(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`);
|
||||
} catch (e) {
|
||||
logger.err(`Blocks summaries indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||
throw e;
|
||||
@@ -351,7 +348,8 @@ class Blocks {
|
||||
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
|
||||
const progress = Math.round(totalIndexed / indexingBlockAmount * 10000) / 100;
|
||||
logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${runningFor} seconds`);
|
||||
const timeLeft = Math.round((indexingBlockAmount - totalIndexed) / blockPerSeconds);
|
||||
logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${runningFor} seconds | left: ~${timeLeft} seconds`);
|
||||
timer = new Date().getTime() / 1000;
|
||||
indexedThisRun = 0;
|
||||
loadingIndicators.setProgress('block-indexing', progress, false);
|
||||
@@ -367,11 +365,7 @@ class Blocks {
|
||||
|
||||
currentBlockHeight -= chunkSize;
|
||||
}
|
||||
if (newlyIndexed > 0) {
|
||||
logger.notice(`Block indexing completed: indexed ${newlyIndexed} blocks`);
|
||||
} else {
|
||||
logger.debug(`Block indexing completed: indexed ${newlyIndexed} blocks`);
|
||||
}
|
||||
logger.notice(`Block indexing completed: indexed ${newlyIndexed} blocks`);
|
||||
loadingIndicators.setProgress('block-indexing', 100);
|
||||
} catch (e) {
|
||||
logger.err('Block indexing failed. Trying again in 10 seconds. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
@@ -411,7 +405,7 @@ class Blocks {
|
||||
|
||||
if (blockHeightTip >= 2016) {
|
||||
const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016);
|
||||
const previousPeriodBlock = await bitcoinClient.getBlock(previousPeriodBlockHash)
|
||||
const previousPeriodBlock = await bitcoinApi.$getBlock(previousPeriodBlockHash);
|
||||
this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100;
|
||||
logger.debug(`Initial difficulty adjustment data set.`);
|
||||
}
|
||||
@@ -533,15 +527,13 @@ class Blocks {
|
||||
}
|
||||
}
|
||||
|
||||
let block = await bitcoinClient.getBlock(hash);
|
||||
const block = await bitcoinApi.$getBlock(hash);
|
||||
|
||||
// Not Bitcoin network, return the block as it
|
||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
|
||||
return block;
|
||||
}
|
||||
|
||||
block = prepareBlock(block);
|
||||
|
||||
// Bitcoin network, add our custom data on top
|
||||
const transactions = await this.$getTransactionsExtended(hash, block.height, true);
|
||||
const blockExtended = await this.$getBlockExtended(block, transactions);
|
||||
@@ -585,39 +577,47 @@ class Blocks {
|
||||
}
|
||||
|
||||
public async $getBlocks(fromHeight?: number, limit: number = 15): Promise<BlockExtended[]> {
|
||||
let currentHeight = fromHeight !== undefined ? fromHeight : await blocksRepository.$mostRecentBlockHeight();
|
||||
const returnBlocks: BlockExtended[] = [];
|
||||
try {
|
||||
let currentHeight = fromHeight !== undefined ? fromHeight : this.getCurrentBlockHeight();
|
||||
const returnBlocks: BlockExtended[] = [];
|
||||
|
||||
if (currentHeight < 0) {
|
||||
return returnBlocks;
|
||||
}
|
||||
|
||||
// Check if block height exist in local cache to skip the hash lookup
|
||||
const blockByHeight = this.getBlocks().find((b) => b.height === currentHeight);
|
||||
let startFromHash: string | null = null;
|
||||
if (blockByHeight) {
|
||||
startFromHash = blockByHeight.id;
|
||||
} else if (!Common.indexingEnabled()) {
|
||||
startFromHash = await bitcoinApi.$getBlockHash(currentHeight);
|
||||
}
|
||||
|
||||
let nextHash = startFromHash;
|
||||
for (let i = 0; i < limit && currentHeight >= 0; i++) {
|
||||
let block = this.getBlocks().find((b) => b.height === currentHeight);
|
||||
if (block) {
|
||||
returnBlocks.push(block);
|
||||
} else if (Common.indexingEnabled()) {
|
||||
block = await this.$indexBlock(currentHeight);
|
||||
returnBlocks.push(block);
|
||||
} else if (nextHash != null) {
|
||||
block = prepareBlock(await bitcoinClient.getBlock(nextHash));
|
||||
nextHash = block.previousblockhash;
|
||||
returnBlocks.push(block);
|
||||
if (currentHeight < 0) {
|
||||
return returnBlocks;
|
||||
}
|
||||
currentHeight--;
|
||||
}
|
||||
|
||||
return returnBlocks;
|
||||
if (currentHeight === 0 && Common.indexingEnabled()) {
|
||||
currentHeight = await blocksRepository.$mostRecentBlockHeight();
|
||||
}
|
||||
|
||||
// Check if block height exist in local cache to skip the hash lookup
|
||||
const blockByHeight = this.getBlocks().find((b) => b.height === currentHeight);
|
||||
let startFromHash: string | null = null;
|
||||
if (blockByHeight) {
|
||||
startFromHash = blockByHeight.id;
|
||||
} else if (!Common.indexingEnabled()) {
|
||||
startFromHash = await bitcoinApi.$getBlockHash(currentHeight);
|
||||
}
|
||||
|
||||
let nextHash = startFromHash;
|
||||
for (let i = 0; i < limit && currentHeight >= 0; i++) {
|
||||
let block = this.getBlocks().find((b) => b.height === currentHeight);
|
||||
if (block) {
|
||||
returnBlocks.push(block);
|
||||
} else if (Common.indexingEnabled()) {
|
||||
block = await this.$indexBlock(currentHeight);
|
||||
returnBlocks.push(block);
|
||||
} else if (nextHash != null) {
|
||||
block = prepareBlock(await bitcoinApi.$getBlock(nextHash));
|
||||
nextHash = block.previousblockhash;
|
||||
returnBlocks.push(block);
|
||||
}
|
||||
currentHeight--;
|
||||
}
|
||||
|
||||
return returnBlocks;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public getLastDifficultyAdjustmentTime(): number {
|
||||
|
||||
@@ -173,25 +173,26 @@ class Mining {
|
||||
*/
|
||||
public async $generatePoolHashrateHistory(): Promise<void> {
|
||||
const now = new Date();
|
||||
const lastestRunDate = await HashratesRepository.$getLatestRun('last_weekly_hashrates_indexing');
|
||||
|
||||
// Run only if:
|
||||
// * lastestRunDate is set to 0 (node backend restart, reorg)
|
||||
// * we started a new week (around Monday midnight)
|
||||
const runIndexing = lastestRunDate === 0 || now.getUTCDay() === 1 && lastestRunDate !== now.getUTCDate();
|
||||
if (!runIndexing) {
|
||||
return;
|
||||
try {
|
||||
const lastestRunDate = await HashratesRepository.$getLatestRun('last_weekly_hashrates_indexing');
|
||||
|
||||
// Run only if:
|
||||
// * lastestRunDate is set to 0 (node backend restart, reorg)
|
||||
// * we started a new week (around Monday midnight)
|
||||
const runIndexing = lastestRunDate === 0 || now.getUTCDay() === 1 && lastestRunDate !== now.getUTCDate();
|
||||
if (!runIndexing) {
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp;
|
||||
|
||||
const genesisBlock = await bitcoinClient.getBlock(await bitcoinClient.getBlockHash(0));
|
||||
const genesisTimestamp = genesisBlock.time * 1000;
|
||||
|
||||
const indexedTimestamp = await HashratesRepository.$getWeeklyHashrateTimestamps();
|
||||
const hashrates: any[] = [];
|
||||
|
||||
const genesisTimestamp = 1231006505000; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
|
||||
|
||||
const lastMonday = new Date(now.setDate(now.getDate() - (now.getDay() + 6) % 7));
|
||||
const lastMondayMidnight = this.getDateMidnight(lastMonday);
|
||||
let toTimestamp = lastMondayMidnight.getTime();
|
||||
@@ -206,7 +207,7 @@ class Mining {
|
||||
logger.debug(`Indexing weekly mining pool hashrate`);
|
||||
loadingIndicators.setProgress('weekly-hashrate-indexing', 0);
|
||||
|
||||
while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) {
|
||||
while (toTimestamp > genesisTimestamp) {
|
||||
const fromTimestamp = toTimestamp - 604800000;
|
||||
|
||||
// Skip already indexed weeks
|
||||
@@ -216,6 +217,14 @@ class Mining {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if we have blocks for the previous week (which mean that the week
|
||||
// we are currently indexing has complete data)
|
||||
const blockStatsPreviousWeek: any = await BlocksRepository.$blockCountBetweenTimestamp(
|
||||
null, (fromTimestamp - 604800000) / 1000, (toTimestamp - 604800000) / 1000);
|
||||
if (blockStatsPreviousWeek.blockCount === 0) { // We are done indexing
|
||||
break;
|
||||
}
|
||||
|
||||
const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp(
|
||||
null, fromTimestamp / 1000, toTimestamp / 1000);
|
||||
const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount,
|
||||
@@ -223,35 +232,34 @@ class Mining {
|
||||
|
||||
let pools = await PoolsRepository.$getPoolsInfoBetween(fromTimestamp / 1000, toTimestamp / 1000);
|
||||
const totalBlocks = pools.reduce((acc, pool) => acc + pool.blockCount, 0);
|
||||
if (totalBlocks > 0) {
|
||||
pools = pools.map((pool: any) => {
|
||||
pool.hashrate = (pool.blockCount / totalBlocks) * lastBlockHashrate;
|
||||
pool.share = (pool.blockCount / totalBlocks);
|
||||
return pool;
|
||||
pools = pools.map((pool: any) => {
|
||||
pool.hashrate = (pool.blockCount / totalBlocks) * lastBlockHashrate;
|
||||
pool.share = (pool.blockCount / totalBlocks);
|
||||
return pool;
|
||||
});
|
||||
|
||||
for (const pool of pools) {
|
||||
hashrates.push({
|
||||
hashrateTimestamp: toTimestamp / 1000,
|
||||
avgHashrate: pool['hashrate'],
|
||||
poolId: pool.poolId,
|
||||
share: pool['share'],
|
||||
type: 'weekly',
|
||||
});
|
||||
|
||||
for (const pool of pools) {
|
||||
hashrates.push({
|
||||
hashrateTimestamp: toTimestamp / 1000,
|
||||
avgHashrate: pool['hashrate'] ,
|
||||
poolId: pool.poolId,
|
||||
share: pool['share'],
|
||||
type: 'weekly',
|
||||
});
|
||||
}
|
||||
|
||||
newlyIndexed += hashrates.length;
|
||||
await HashratesRepository.$saveHashrates(hashrates);
|
||||
hashrates.length = 0;
|
||||
}
|
||||
|
||||
newlyIndexed += hashrates.length;
|
||||
await HashratesRepository.$saveHashrates(hashrates);
|
||||
hashrates.length = 0;
|
||||
|
||||
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
|
||||
if (elapsedSeconds > 1) {
|
||||
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||
const weeksPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
|
||||
const progress = Math.round(totalIndexed / totalWeekIndexed * 10000) / 100;
|
||||
const timeLeft = Math.round((totalWeekIndexed - totalIndexed) / weeksPerSeconds);
|
||||
const formattedDate = new Date(fromTimestamp).toUTCString();
|
||||
logger.debug(`Getting weekly pool hashrate for ${formattedDate} | ~${weeksPerSeconds.toFixed(2)} weeks/sec | total: ~${totalIndexed}/${Math.round(totalWeekIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`);
|
||||
logger.debug(`Getting weekly pool hashrate for ${formattedDate} | ~${weeksPerSeconds.toFixed(2)} weeks/sec | total: ~${totalIndexed}/${Math.round(totalWeekIndexed)} (${progress}%) | elapsed: ${runningFor} seconds | left: ~${timeLeft} seconds`);
|
||||
timer = new Date().getTime() / 1000;
|
||||
indexedThisRun = 0;
|
||||
loadingIndicators.setProgress('weekly-hashrate-indexing', progress, false);
|
||||
@@ -264,8 +272,6 @@ class Mining {
|
||||
await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', new Date().getUTCDate());
|
||||
if (newlyIndexed > 0) {
|
||||
logger.notice(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`);
|
||||
} else {
|
||||
logger.debug(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`);
|
||||
}
|
||||
loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
|
||||
} catch (e) {
|
||||
@@ -279,19 +285,20 @@ class Mining {
|
||||
* [INDEXING] Generate daily hashrate data
|
||||
*/
|
||||
public async $generateNetworkHashrateHistory(): Promise<void> {
|
||||
// We only run this once a day around midnight
|
||||
const latestRunDate = await HashratesRepository.$getLatestRun('last_hashrates_indexing');
|
||||
const now = new Date().getUTCDate();
|
||||
if (now === latestRunDate) {
|
||||
return;
|
||||
try {
|
||||
// We only run this once a day around midnight
|
||||
const latestRunDate = await HashratesRepository.$getLatestRun('last_hashrates_indexing');
|
||||
const now = new Date().getUTCDate();
|
||||
if (now === latestRunDate) {
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
const oldestConsecutiveBlockTimestamp = 1000 * (await BlocksRepository.$getOldestConsecutiveBlock()).timestamp;
|
||||
|
||||
try {
|
||||
const genesisBlock = await bitcoinClient.getBlock(await bitcoinClient.getBlockHash(0));
|
||||
const genesisTimestamp = genesisBlock.time * 1000;
|
||||
const indexedTimestamp = (await HashratesRepository.$getRawNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp);
|
||||
const indexedTimestamp = (await HashratesRepository.$getNetworkDailyHashrate(null)).map(hashrate => hashrate.timestamp);
|
||||
const genesisTimestamp = 1231006505000; // bitcoin-cli getblock 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
|
||||
const lastMidnight = this.getDateMidnight(new Date());
|
||||
let toTimestamp = Math.round(lastMidnight.getTime());
|
||||
const hashrates: any[] = [];
|
||||
@@ -306,7 +313,7 @@ class Mining {
|
||||
logger.debug(`Indexing daily network hashrate`);
|
||||
loadingIndicators.setProgress('daily-hashrate-indexing', 0);
|
||||
|
||||
while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) {
|
||||
while (toTimestamp > genesisTimestamp) {
|
||||
const fromTimestamp = toTimestamp - 86400000;
|
||||
|
||||
// Skip already indexed days
|
||||
@@ -316,9 +323,17 @@ class Mining {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if we have blocks for the previous day (which mean that the day
|
||||
// we are currently indexing has complete data)
|
||||
const blockStatsPreviousDay: any = await BlocksRepository.$blockCountBetweenTimestamp(
|
||||
null, (fromTimestamp - 86400000) / 1000, (toTimestamp - 86400000) / 1000);
|
||||
if (blockStatsPreviousDay.blockCount === 0 && config.MEMPOOL.NETWORK === 'mainnet') { // We are done indexing
|
||||
break;
|
||||
}
|
||||
|
||||
const blockStats: any = await BlocksRepository.$blockCountBetweenTimestamp(
|
||||
null, fromTimestamp / 1000, toTimestamp / 1000);
|
||||
const lastBlockHashrate = blockStats.blockCount === 0 ? 0 : await bitcoinClient.getNetworkHashPs(blockStats.blockCount,
|
||||
const lastBlockHashrate = await bitcoinClient.getNetworkHashPs(blockStats.blockCount,
|
||||
blockStats.lastBlockHeight);
|
||||
|
||||
hashrates.push({
|
||||
@@ -340,8 +355,9 @@ class Mining {
|
||||
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||
const daysPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
|
||||
const progress = Math.round(totalIndexed / totalDayIndexed * 10000) / 100;
|
||||
const timeLeft = Math.round((totalDayIndexed - totalIndexed) / daysPerSeconds);
|
||||
const formattedDate = new Date(fromTimestamp).toUTCString();
|
||||
logger.debug(`Getting network daily hashrate for ${formattedDate} | ~${daysPerSeconds.toFixed(2)} days/sec | total: ~${totalIndexed}/${Math.round(totalDayIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`);
|
||||
logger.debug(`Getting network daily hashrate for ${formattedDate} | ~${daysPerSeconds.toFixed(2)} days/sec | total: ~${totalIndexed}/${Math.round(totalDayIndexed)} (${progress}%) | elapsed: ${runningFor} seconds | left: ~${timeLeft} seconds`);
|
||||
timer = new Date().getTime() / 1000;
|
||||
indexedThisRun = 0;
|
||||
loadingIndicators.setProgress('daily-hashrate-indexing', progress);
|
||||
@@ -353,7 +369,7 @@ class Mining {
|
||||
}
|
||||
|
||||
// Add genesis block manually
|
||||
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT === -1 && !indexedTimestamp.includes(genesisTimestamp / 1000)) {
|
||||
if (toTimestamp <= genesisTimestamp && !indexedTimestamp.includes(genesisTimestamp)) {
|
||||
hashrates.push({
|
||||
hashrateTimestamp: genesisTimestamp / 1000,
|
||||
avgHashrate: await bitcoinClient.getNetworkHashPs(1, 1),
|
||||
@@ -369,8 +385,6 @@ class Mining {
|
||||
await HashratesRepository.$setLatestRun('last_hashrates_indexing', new Date().getUTCDate());
|
||||
if (newlyIndexed > 0) {
|
||||
logger.notice(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`);
|
||||
} else {
|
||||
logger.debug(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`);
|
||||
}
|
||||
loadingIndicators.setProgress('daily-hashrate-indexing', 100);
|
||||
} catch (e) {
|
||||
@@ -391,37 +405,27 @@ class Mining {
|
||||
}
|
||||
|
||||
const blocks: any = await BlocksRepository.$getBlocksDifficulty();
|
||||
const genesisBlock = await bitcoinClient.getBlock(await bitcoinClient.getBlockHash(0));
|
||||
let currentDifficulty = genesisBlock.difficulty;
|
||||
|
||||
let currentDifficulty = 0;
|
||||
let totalIndexed = 0;
|
||||
|
||||
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT === -1 && indexedHeights[0] !== true) {
|
||||
if (indexedHeights[0] === false) {
|
||||
await DifficultyAdjustmentsRepository.$saveAdjustments({
|
||||
time: genesisBlock.time,
|
||||
time: 1231006505,
|
||||
height: 0,
|
||||
difficulty: currentDifficulty,
|
||||
difficulty: 1.0,
|
||||
adjustment: 0.0,
|
||||
});
|
||||
}
|
||||
|
||||
const oldestConsecutiveBlock = await BlocksRepository.$getOldestConsecutiveBlock();
|
||||
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== -1) {
|
||||
currentDifficulty = oldestConsecutiveBlock.difficulty;
|
||||
}
|
||||
|
||||
let totalBlockChecked = 0;
|
||||
let timer = new Date().getTime() / 1000;
|
||||
|
||||
for (const block of blocks) {
|
||||
if (block.difficulty !== currentDifficulty) {
|
||||
if (indexedHeights[block.height] === true) { // Already indexed
|
||||
if (block.height >= oldestConsecutiveBlock.height) {
|
||||
currentDifficulty = block.difficulty;
|
||||
}
|
||||
if (block.height === 0 || indexedHeights[block.height] === true) { // Already indexed
|
||||
currentDifficulty = block.difficulty;
|
||||
continue;
|
||||
}
|
||||
|
||||
let adjustment = block.difficulty / currentDifficulty;
|
||||
let adjustment = block.difficulty / Math.max(1, currentDifficulty);
|
||||
adjustment = Math.round(adjustment * 1000000) / 1000000; // Remove float point noise
|
||||
|
||||
await DifficultyAdjustmentsRepository.$saveAdjustments({
|
||||
@@ -432,24 +436,12 @@ class Mining {
|
||||
});
|
||||
|
||||
totalIndexed++;
|
||||
if (block.height >= oldestConsecutiveBlock.height) {
|
||||
currentDifficulty = block.difficulty;
|
||||
}
|
||||
}
|
||||
|
||||
totalBlockChecked++;
|
||||
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
|
||||
if (elapsedSeconds > 5) {
|
||||
const progress = Math.round(totalBlockChecked / blocks.length * 100);
|
||||
logger.info(`Indexing difficulty adjustment at block #${block.height} | Progress: ${progress}%`);
|
||||
timer = new Date().getTime() / 1000;
|
||||
currentDifficulty = block.difficulty;
|
||||
}
|
||||
}
|
||||
|
||||
if (totalIndexed > 0) {
|
||||
logger.notice(`Indexed ${totalIndexed} difficulty adjustments`);
|
||||
} else {
|
||||
logger.debug(`Indexed ${totalIndexed} difficulty adjustments`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import DB from '../database';
|
||||
import logger from '../logger';
|
||||
import config from '../config';
|
||||
import BlocksRepository from '../repositories/BlocksRepository';
|
||||
|
||||
interface Pool {
|
||||
name: string;
|
||||
@@ -33,6 +32,7 @@ class PoolsParser {
|
||||
// First we save every entries without paying attention to pool duplication
|
||||
const poolsDuplicated: Pool[] = [];
|
||||
|
||||
logger.debug('Parse coinbase_tags');
|
||||
const coinbaseTags = Object.entries(poolsJson['coinbase_tags']);
|
||||
for (let i = 0; i < coinbaseTags.length; ++i) {
|
||||
poolsDuplicated.push({
|
||||
@@ -43,6 +43,7 @@ class PoolsParser {
|
||||
'slug': ''
|
||||
});
|
||||
}
|
||||
logger.debug('Parse payout_addresses');
|
||||
const addressesTags = Object.entries(poolsJson['payout_addresses']);
|
||||
for (let i = 0; i < addressesTags.length; ++i) {
|
||||
poolsDuplicated.push({
|
||||
@@ -55,6 +56,7 @@ class PoolsParser {
|
||||
}
|
||||
|
||||
// Then, we find unique mining pool names
|
||||
logger.debug('Identify unique mining pools');
|
||||
const poolNames: string[] = [];
|
||||
for (let i = 0; i < poolsDuplicated.length; ++i) {
|
||||
if (poolNames.indexOf(poolsDuplicated[i].name) === -1) {
|
||||
@@ -117,15 +119,8 @@ class PoolsParser {
|
||||
'slug': slug
|
||||
};
|
||||
|
||||
const existingPool = existingPools.find((pool) => pool.name === poolNames[i]);
|
||||
if (existingPool !== undefined) {
|
||||
// Check if any data was actually updated
|
||||
const equals = (a, b) =>
|
||||
a.length === b.length &&
|
||||
a.every((v, i) => v === b[i]);
|
||||
if (!equals(JSON.parse(existingPool.addresses), poolObj.addresses) || !equals(JSON.parse(existingPool.regexes), poolObj.regexes)) {
|
||||
finalPoolDataUpdate.push(poolObj);
|
||||
}
|
||||
if (existingPools.find((pool) => pool.name === poolNames[i]) !== undefined) {
|
||||
finalPoolDataUpdate.push(poolObj);
|
||||
} else {
|
||||
logger.debug(`Add '${finalPoolName}' mining pool`);
|
||||
finalPoolDataAdd.push(poolObj);
|
||||
@@ -145,51 +140,40 @@ class PoolsParser {
|
||||
return;
|
||||
}
|
||||
|
||||
if (finalPoolDataAdd.length > 0 || finalPoolDataUpdate.length > 0) {
|
||||
logger.debug(`Update pools table now`);
|
||||
logger.debug(`Update pools table now`);
|
||||
|
||||
// Add new mining pools into the database
|
||||
let queryAdd: string = 'INSERT INTO pools(name, link, regexes, addresses, slug) VALUES ';
|
||||
for (let i = 0; i < finalPoolDataAdd.length; ++i) {
|
||||
queryAdd += `('${finalPoolDataAdd[i].name}', '${finalPoolDataAdd[i].link}',
|
||||
'${JSON.stringify(finalPoolDataAdd[i].regexes)}', '${JSON.stringify(finalPoolDataAdd[i].addresses)}',
|
||||
${JSON.stringify(finalPoolDataAdd[i].slug)}),`;
|
||||
}
|
||||
queryAdd = queryAdd.slice(0, -1) + ';';
|
||||
// Add new mining pools into the database
|
||||
let queryAdd: string = 'INSERT INTO pools(name, link, regexes, addresses, slug) VALUES ';
|
||||
for (let i = 0; i < finalPoolDataAdd.length; ++i) {
|
||||
queryAdd += `('${finalPoolDataAdd[i].name}', '${finalPoolDataAdd[i].link}',
|
||||
'${JSON.stringify(finalPoolDataAdd[i].regexes)}', '${JSON.stringify(finalPoolDataAdd[i].addresses)}',
|
||||
${JSON.stringify(finalPoolDataAdd[i].slug)}),`;
|
||||
}
|
||||
queryAdd = queryAdd.slice(0, -1) + ';';
|
||||
|
||||
// Updated existing mining pools in the database
|
||||
const updateQueries: string[] = [];
|
||||
for (let i = 0; i < finalPoolDataUpdate.length; ++i) {
|
||||
updateQueries.push(`
|
||||
UPDATE pools
|
||||
SET name='${finalPoolDataUpdate[i].name}', link='${finalPoolDataUpdate[i].link}',
|
||||
regexes='${JSON.stringify(finalPoolDataUpdate[i].regexes)}', addresses='${JSON.stringify(finalPoolDataUpdate[i].addresses)}',
|
||||
slug='${finalPoolDataUpdate[i].slug}'
|
||||
WHERE name='${finalPoolDataUpdate[i].name}'
|
||||
;`);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.$deleteBlocskToReindex(finalPoolDataUpdate);
|
||||
|
||||
if (finalPoolDataAdd.length > 0) {
|
||||
await DB.query({ sql: queryAdd, timeout: 120000 });
|
||||
}
|
||||
for (const query of updateQueries) {
|
||||
await DB.query({ sql: query, timeout: 120000 });
|
||||
}
|
||||
await this.insertUnknownPool();
|
||||
logger.info('Mining pools.json import completed');
|
||||
} catch (e) {
|
||||
logger.err(`Cannot import pools in the database`);
|
||||
throw e;
|
||||
}
|
||||
// Updated existing mining pools in the database
|
||||
const updateQueries: string[] = [];
|
||||
for (let i = 0; i < finalPoolDataUpdate.length; ++i) {
|
||||
updateQueries.push(`
|
||||
UPDATE pools
|
||||
SET name='${finalPoolDataUpdate[i].name}', link='${finalPoolDataUpdate[i].link}',
|
||||
regexes='${JSON.stringify(finalPoolDataUpdate[i].regexes)}', addresses='${JSON.stringify(finalPoolDataUpdate[i].addresses)}',
|
||||
slug='${finalPoolDataUpdate[i].slug}'
|
||||
WHERE name='${finalPoolDataUpdate[i].name}'
|
||||
;`);
|
||||
}
|
||||
|
||||
try {
|
||||
if (finalPoolDataAdd.length > 0) {
|
||||
await DB.query({ sql: queryAdd, timeout: 120000 });
|
||||
}
|
||||
for (const query of updateQueries) {
|
||||
await DB.query({ sql: query, timeout: 120000 });
|
||||
}
|
||||
await this.insertUnknownPool();
|
||||
logger.info('Mining pools.json import completed');
|
||||
} catch (e) {
|
||||
logger.err(`Cannot insert unknown pool in the database`);
|
||||
logger.err(`Cannot import pools in the database`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -217,36 +201,6 @@ class PoolsParser {
|
||||
logger.err('Unable to insert "Unknown" mining pool');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete blocks which needs to be reindexed
|
||||
*/
|
||||
private async $deleteBlocskToReindex(finalPoolDataUpdate: any[]) {
|
||||
if (config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blockCount = await BlocksRepository.$blockCount(null, null);
|
||||
if (blockCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const updatedPool of finalPoolDataUpdate) {
|
||||
const [pool]: any[] = await DB.query(`SELECT id, name from pools where slug = "${updatedPool.slug}"`);
|
||||
if (pool.length > 0) {
|
||||
logger.notice(`Deleting blocks from ${pool[0].name} mining pool for future re-indexing`);
|
||||
await DB.query(`DELETE FROM blocks WHERE pool_id = ${pool[0].id}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore early days of Bitcoin as there were not mining pool yet
|
||||
logger.notice('Deleting blocks with unknown mining pool from height 130635 for future re-indexing');
|
||||
const [unknownPool] = await DB.query(`SELECT id from pools where slug = "unknown"`);
|
||||
await DB.query(`DELETE FROM blocks WHERE pool_id = ${unknownPool[0].id} AND height > 130635`);
|
||||
|
||||
logger.notice('Truncating hashrates for future re-indexing');
|
||||
await DB.query(`DELETE FROM hashrates`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new PoolsParser();
|
||||
|
||||
@@ -23,7 +23,6 @@ interface IConfig {
|
||||
EXTERNAL_RETRY_INTERVAL: number;
|
||||
USER_AGENT: string;
|
||||
STDOUT_LOG_MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
|
||||
AUTOMATIC_BLOCK_REINDEXING: boolean;
|
||||
};
|
||||
ESPLORA: {
|
||||
REST_API_URL: string;
|
||||
@@ -114,7 +113,6 @@ const defaults: IConfig = {
|
||||
'EXTERNAL_RETRY_INTERVAL': 0,
|
||||
'USER_AGENT': 'mempool',
|
||||
'STDOUT_LOG_MIN_PRIORITY': 'debug',
|
||||
'AUTOMATIC_BLOCK_REINDEXING': false,
|
||||
},
|
||||
'ESPLORA': {
|
||||
'REST_API_URL': 'http://127.0.0.1:3000',
|
||||
|
||||
@@ -35,8 +35,6 @@ class Indexer {
|
||||
this.runIndexer = false;
|
||||
this.indexerRunning = true;
|
||||
|
||||
logger.debug(`Running mining indexer`);
|
||||
|
||||
try {
|
||||
const chainValid = await blocks.$generateBlockDatabase();
|
||||
if (chainValid === false) {
|
||||
@@ -56,15 +54,9 @@ class Indexer {
|
||||
this.indexerRunning = false;
|
||||
logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
setTimeout(() => this.reindex(), 10000);
|
||||
this.indexerRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.indexerRunning = false;
|
||||
|
||||
const runEvery = 1000 * 3600; // 1 hour
|
||||
logger.debug(`Indexing completed. Next run planned at ${new Date(new Date().getTime() + runEvery).toUTCString()}`);
|
||||
setTimeout(() => this.reindex(), runEvery);
|
||||
}
|
||||
|
||||
async $resetHashratesIndexingState() {
|
||||
|
||||
@@ -446,7 +446,7 @@ class BlocksRepository {
|
||||
++idx;
|
||||
}
|
||||
|
||||
logger.debug(`${idx} blocks hash validated in ${new Date().getTime() - start} ms`);
|
||||
logger.info(`${idx} blocks hash validated in ${new Date().getTime() - start} ms`);
|
||||
return true;
|
||||
} catch (e) {
|
||||
logger.err('Cannot validate chain of block hash. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
@@ -610,24 +610,6 @@ class BlocksRepository {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the oldest block from a consecutive chain of block from the most recent one
|
||||
*/
|
||||
public async $getOldestConsecutiveBlock(): Promise<any> {
|
||||
try {
|
||||
const [rows]: any = await DB.query(`SELECT height, UNIX_TIMESTAMP(blockTimestamp) as timestamp, difficulty FROM blocks ORDER BY height DESC`);
|
||||
for (let i = 0; i < rows.length - 1; ++i) {
|
||||
if (rows[i].height - rows[i + 1].height > 1) {
|
||||
return rows[i];
|
||||
}
|
||||
}
|
||||
return rows[rows.length - 1];
|
||||
} catch (e) {
|
||||
logger.err('Cannot generate block size and weight history. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new BlocksRepository();
|
||||
|
||||
@@ -46,38 +46,9 @@ class DifficultyAdjustmentsRepository {
|
||||
query += ` GROUP BY UNIX_TIMESTAMP(time) DIV ${86400}`;
|
||||
|
||||
if (descOrder === true) {
|
||||
query += ` ORDER BY height DESC`;
|
||||
query += ` ORDER BY time DESC`;
|
||||
} else {
|
||||
query += ` ORDER BY height`;
|
||||
}
|
||||
|
||||
try {
|
||||
const [rows] = await DB.query(query);
|
||||
return rows as IndexedDifficultyAdjustment[];
|
||||
} catch (e) {
|
||||
logger.err(`Cannot get difficulty adjustments from the database. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async $getRawAdjustments(interval: string | null, descOrder: boolean = false): Promise<IndexedDifficultyAdjustment[]> {
|
||||
interval = Common.getSqlInterval(interval);
|
||||
|
||||
let query = `SELECT
|
||||
UNIX_TIMESTAMP(time) as time,
|
||||
height as height,
|
||||
difficulty as difficulty,
|
||||
adjustment as adjustment
|
||||
FROM difficulty_adjustments`;
|
||||
|
||||
if (interval) {
|
||||
query += ` WHERE time BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
|
||||
}
|
||||
|
||||
if (descOrder === true) {
|
||||
query += ` ORDER BY height DESC`;
|
||||
} else {
|
||||
query += ` ORDER BY height`;
|
||||
query += ` ORDER BY time`;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { escape } from 'mysql2';
|
||||
import { Common } from '../api/common';
|
||||
import config from '../config';
|
||||
import DB from '../database';
|
||||
import logger from '../logger';
|
||||
import PoolsRepository from './PoolsRepository';
|
||||
@@ -29,32 +30,6 @@ class HashratesRepository {
|
||||
}
|
||||
}
|
||||
|
||||
public async $getRawNetworkDailyHashrate(interval: string | null): Promise<any[]> {
|
||||
interval = Common.getSqlInterval(interval);
|
||||
|
||||
let query = `SELECT
|
||||
UNIX_TIMESTAMP(hashrate_timestamp) as timestamp,
|
||||
avg_hashrate as avgHashrate
|
||||
FROM hashrates`;
|
||||
|
||||
if (interval) {
|
||||
query += ` WHERE hashrate_timestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()
|
||||
AND hashrates.type = 'daily'`;
|
||||
} else {
|
||||
query += ` WHERE hashrates.type = 'daily'`;
|
||||
}
|
||||
|
||||
query += ` ORDER by hashrate_timestamp`;
|
||||
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(query);
|
||||
return rows;
|
||||
} catch (e) {
|
||||
logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async $getNetworkDailyHashrate(interval: string | null): Promise<any[]> {
|
||||
interval = Common.getSqlInterval(interval);
|
||||
|
||||
|
||||
@@ -4,12 +4,6 @@ import { Prices } from '../tasks/price-updater';
|
||||
|
||||
class PricesRepository {
|
||||
public async $savePrices(time: number, prices: Prices): Promise<void> {
|
||||
if (prices.USD === -1) {
|
||||
// Some historical price entries have not USD prices, so we just ignore them to avoid future UX issues
|
||||
// As of today there are only 4 (on 2013-09-05, 2013-09-19, 2013-09-12 and 2013-09-26) so that's fine
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await DB.query(`
|
||||
INSERT INTO prices(time, USD, EUR, GBP, CAD, CHF, AUD, JPY)
|
||||
@@ -23,17 +17,17 @@ class PricesRepository {
|
||||
}
|
||||
|
||||
public async $getOldestPriceTime(): Promise<number> {
|
||||
const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != -1 ORDER BY time LIMIT 1`);
|
||||
const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices ORDER BY time LIMIT 1`);
|
||||
return oldestRow[0] ? oldestRow[0].time : 0;
|
||||
}
|
||||
|
||||
public async $getLatestPriceTime(): Promise<number> {
|
||||
const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != -1 ORDER BY time DESC LIMIT 1`);
|
||||
const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices ORDER BY time DESC LIMIT 1`);
|
||||
return oldestRow[0] ? oldestRow[0].time : 0;
|
||||
}
|
||||
|
||||
public async $getPricesTimes(): Promise<number[]> {
|
||||
const [times]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != -1`);
|
||||
const [times]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices`);
|
||||
return times.map(time => time.time);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -734,7 +734,7 @@ class Routes {
|
||||
|
||||
public async $getDifficultyAdjustments(req: Request, res: Response) {
|
||||
try {
|
||||
const difficulty = await DifficultyAdjustmentsRepository.$getRawAdjustments(req.params.interval, true);
|
||||
const difficulty = await DifficultyAdjustmentsRepository.$getAdjustments(req.params.interval, true);
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||
@@ -790,7 +790,7 @@ class Routes {
|
||||
|
||||
public async getBlocks(req: Request, res: Response) {
|
||||
try {
|
||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin
|
||||
if (['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin
|
||||
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(await blocks.$getBlocks(height, 15));
|
||||
|
||||
@@ -177,8 +177,6 @@ class PriceUpdater {
|
||||
}
|
||||
if (insertedCount > 0) {
|
||||
logger.notice(`Inserted ${insertedCount} MtGox USD weekly price history into db`);
|
||||
} else {
|
||||
logger.debug(`Inserted ${insertedCount} MtGox USD weekly price history into db`);
|
||||
}
|
||||
|
||||
// Insert Kraken weekly prices
|
||||
@@ -253,8 +251,6 @@ class PriceUpdater {
|
||||
|
||||
if (totalInserted > 0) {
|
||||
logger.notice(`Inserted ${totalInserted} hourly historical prices into the db`);
|
||||
} else {
|
||||
logger.debug(`Inserted ${totalInserted} hourly historical prices into the db`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@ import { BlockExtended } from '../mempool.interfaces';
|
||||
export function prepareBlock(block: any): BlockExtended {
|
||||
return <BlockExtended>{
|
||||
id: block.id ?? block.hash, // hash for indexed block
|
||||
timestamp: block.timestamp ?? block.time ?? block.blockTimestamp, // blockTimestamp for indexed block
|
||||
timestamp: block.timestamp ?? block.blockTimestamp, // blockTimestamp for indexed block
|
||||
height: block.height,
|
||||
version: block.version,
|
||||
bits: (typeof block.bits === 'string' ? parseInt(block.bits, 16): block.bits),
|
||||
bits: block.bits,
|
||||
nonce: block.nonce,
|
||||
difficulty: block.difficulty,
|
||||
merkle_root: block.merkle_root ?? block.merkleroot,
|
||||
tx_count: block.tx_count ?? block.nTx,
|
||||
merkle_root: block.merkle_root,
|
||||
tx_count: block.tx_count,
|
||||
size: block.size,
|
||||
weight: block.weight,
|
||||
previousblockhash: block.previousblockhash,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:16.16.0-buster-slim AS builder
|
||||
FROM node:16.15.0-buster-slim AS builder
|
||||
|
||||
ARG commitHash
|
||||
ENV DOCKER_COMMIT_HASH=${commitHash}
|
||||
@@ -8,10 +8,10 @@ COPY . .
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y build-essential python3 pkg-config
|
||||
RUN npm install --omit=dev --omit=optional
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
FROM node:16.16.0-buster-slim
|
||||
FROM node:16.15.0-buster-slim
|
||||
|
||||
WORKDIR /backend
|
||||
|
||||
|
||||
@@ -20,8 +20,7 @@
|
||||
"USER_AGENT": "__MEMPOOL_USER_AGENT__",
|
||||
"STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__",
|
||||
"INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__,
|
||||
"BLOCKS_SUMMARIES_INDEXING": __MEMPOOL_BLOCKS_SUMMARIES_INDEXING__,
|
||||
"AUTOMATIC_BLOCK_REINDEXING": __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__
|
||||
"BLOCKS_SUMMARIES_INDEXING": __MEMPOOL_BLOCKS_SUMMARIES_INDEXING__
|
||||
},
|
||||
"CORE_RPC": {
|
||||
"HOST": "__CORE_RPC_HOST__",
|
||||
|
||||
@@ -22,8 +22,6 @@ __MEMPOOL_EXTERNAL_MAX_RETRY__=${MEMPOOL_EXTERNAL_MAX_RETRY:=1}
|
||||
__MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0}
|
||||
__MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool}
|
||||
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
|
||||
__MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=false}
|
||||
__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__=${MEMPOOL_AUTOMATIC_BLOCK_REINDEXING:=false}
|
||||
|
||||
# CORE_RPC
|
||||
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
|
||||
@@ -112,8 +110,6 @@ sed -i "s!__MEMPOOL_EXTERNAL_MAX_RETRY__!${__MEMPOOL_EXTERNAL_MAX_RETRY__}!g" me
|
||||
sed -i "s!__MEMPOOL_EXTERNAL_RETRY_INTERVAL__!${__MEMPOOL_EXTERNAL_RETRY_INTERVAL__}!g" mempool-config.json
|
||||
sed -i "s!__MEMPOOL_USER_AGENT__!${__MEMPOOL_USER_AGENT__}!g" mempool-config.json
|
||||
sed -i "s/__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__/${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}/g" mempool-config.json
|
||||
sed -i "s/__MEMPOOL_INDEXING_BLOCKS_AMOUNT__/${__MEMPOOL_INDEXING_BLOCKS_AMOUNT__}/g" mempool-config.json
|
||||
sed -i "s/__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__/${__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__}/g" mempool-config.json
|
||||
|
||||
sed -i "s/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json
|
||||
sed -i "s/__CORE_RPC_PORT__/${__CORE_RPC_PORT__}/g" mempool-config.json
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:16.16.0-buster-slim AS builder
|
||||
FROM node:16.15.0-buster-slim AS builder
|
||||
|
||||
ARG commitHash
|
||||
ENV DOCKER_COMMIT_HASH=${commitHash}
|
||||
@@ -8,7 +8,7 @@ WORKDIR /build
|
||||
COPY . .
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y build-essential rsync
|
||||
RUN npm install --omit=dev --omit=optional
|
||||
RUN npm i
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.17.8-alpine
|
||||
|
||||
55
frontend/package-lock.json
generated
55
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mempool-frontend",
|
||||
"version": "2.4.1",
|
||||
"version": "2.4.1-dev",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mempool-frontend",
|
||||
"version": "2.4.1",
|
||||
"version": "2.4.1-dev",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"dependencies": {
|
||||
"@angular-devkit/build-angular": "~13.3.7",
|
||||
@@ -36,6 +36,7 @@
|
||||
"echarts": "~5.3.2",
|
||||
"express": "^4.17.1",
|
||||
"lightweight-charts": "~3.8.0",
|
||||
"ngx-bootrap-multiselect": "^2.0.0",
|
||||
"ngx-echarts": "8.0.1",
|
||||
"ngx-infinite-scroll": "^10.0.1",
|
||||
"qrcode": "1.5.0",
|
||||
@@ -11041,12 +11042,6 @@
|
||||
"@sideway/pinpoint": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jquery": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
|
||||
"integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -12793,6 +12788,19 @@
|
||||
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
|
||||
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
|
||||
},
|
||||
"node_modules/ngx-bootrap-multiselect": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ngx-bootrap-multiselect/-/ngx-bootrap-multiselect-2.0.0.tgz",
|
||||
"integrity": "sha512-GV/2MigCS5oi6P+zWtFSmq1TLWW1kcKsJNAXLP3hHXxmY3HgMKeUPk57o3T+YHje73JRp5reXMhEIlYuoOmoRg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^10.0.6",
|
||||
"@angular/core": "^10.0.6",
|
||||
"@angular/forms": "^10.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ngx-echarts": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ngx-echarts/-/ngx-echarts-8.0.1.tgz",
|
||||
@@ -13797,17 +13805,6 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/popper.js": {
|
||||
"version": "1.16.1",
|
||||
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
|
||||
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==",
|
||||
"deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/portfinder": {
|
||||
"version": "1.0.28",
|
||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
|
||||
@@ -26086,12 +26083,6 @@
|
||||
"@sideway/pinpoint": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"jquery": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
|
||||
"integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==",
|
||||
"peer": true
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -27427,6 +27418,14 @@
|
||||
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
|
||||
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
|
||||
},
|
||||
"ngx-bootrap-multiselect": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ngx-bootrap-multiselect/-/ngx-bootrap-multiselect-2.0.0.tgz",
|
||||
"integrity": "sha512-GV/2MigCS5oi6P+zWtFSmq1TLWW1kcKsJNAXLP3hHXxmY3HgMKeUPk57o3T+YHje73JRp5reXMhEIlYuoOmoRg==",
|
||||
"requires": {
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"ngx-echarts": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ngx-echarts/-/ngx-echarts-8.0.1.tgz",
|
||||
@@ -28188,12 +28187,6 @@
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="
|
||||
},
|
||||
"popper.js": {
|
||||
"version": "1.16.1",
|
||||
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
|
||||
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==",
|
||||
"peer": true
|
||||
},
|
||||
"portfinder": {
|
||||
"version": "1.0.28",
|
||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mempool-frontend",
|
||||
"version": "2.4.1",
|
||||
"version": "2.4.1-dev",
|
||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"homepage": "https://mempool.space",
|
||||
@@ -90,6 +90,7 @@
|
||||
"echarts": "~5.3.2",
|
||||
"express": "^4.17.1",
|
||||
"lightweight-charts": "~3.8.0",
|
||||
"ngx-bootrap-multiselect": "^2.0.0",
|
||||
"ngx-echarts": "8.0.1",
|
||||
"ngx-infinite-scroll": "^10.0.1",
|
||||
"qrcode": "1.5.0",
|
||||
|
||||
@@ -25,7 +25,7 @@ PROXY_CONFIG = [
|
||||
'!/bisq', '!/bisq/**', '!/bisq/',
|
||||
'!/liquid', '!/liquid/**', '!/liquid/',
|
||||
'!/liquidtestnet', '!/liquidtestnet/**', '!/liquidtestnet/',
|
||||
'/testnet/api/**', '/signet/api/**'
|
||||
'/testnet/api/**', '/signet/api/**', '!/api/v1/asset/**', '!/api/asset/**'
|
||||
],
|
||||
target: "https://mempool.space",
|
||||
ws: true,
|
||||
@@ -59,6 +59,12 @@ PROXY_CONFIG = [
|
||||
secure: false,
|
||||
changeOrigin: true
|
||||
},
|
||||
{
|
||||
context: ['/api/v1/asset/**/icon', '/api/asset/**'],
|
||||
target: "https://liquid.network",
|
||||
secure: false,
|
||||
changeOrigin: true
|
||||
},
|
||||
{
|
||||
context: ['/api/liquidtestnet**', '/liquidtestnet/api/**'],
|
||||
target: "https://liquid.network",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<div class="d-block float-right" id="filter">
|
||||
<form [formGroup]="radioGroupForm">
|
||||
<ngx-bootstrap-multiselect [options]="txTypeOptions" [settings]="txTypeDropdownSettings" [texts]="txTypeDropdownTexts" formControlName="txTypes"></ngx-bootstrap-multiselect>
|
||||
<ngx-bootrap-multiselect [options]="txTypeOptions" [settings]="txTypeDropdownSettings" [texts]="txTypeDropdownTexts" formControlName="txTypes"></ngx-bootrap-multiselect>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BisqApiService } from '../bisq-api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { FormGroup, FormBuilder } from '@angular/forms';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from 'src/app/components/ngx-bootstrap-multiselect/types'
|
||||
import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from 'ngx-bootrap-multiselect';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BisqRoutingModule } from './bisq.routing.module';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { NgxBootstrapMultiselectModule } from 'ngx-bootrap-multiselect';
|
||||
|
||||
import { LightweightChartsComponent } from './lightweight-charts/lightweight-charts.component';
|
||||
import { LightweightChartsAreaComponent } from './lightweight-charts-area/lightweight-charts-area.component';
|
||||
@@ -23,10 +24,6 @@ import { BisqStatsComponent } from './bisq-stats/bisq-stats.component';
|
||||
import { BsqAmountComponent } from './bsq-amount/bsq-amount.component';
|
||||
import { BisqTradesComponent } from './bisq-trades/bisq-trades.component';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { AutofocusDirective } from '../components/ngx-bootstrap-multiselect/autofocus.directive';
|
||||
import { MultiSelectSearchFilter } from '../components/ngx-bootstrap-multiselect/search-filter.pipe';
|
||||
import { OffClickDirective } from '../components/ngx-bootstrap-multiselect/off-click.directive';
|
||||
import { NgxDropdownMultiselectComponent } from '../components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -47,21 +44,16 @@ import { NgxDropdownMultiselectComponent } from '../components/ngx-bootstrap-mul
|
||||
BisqMarketComponent,
|
||||
BisqTradesComponent,
|
||||
BisqMainDashboardComponent,
|
||||
NgxDropdownMultiselectComponent,
|
||||
AutofocusDirective,
|
||||
OffClickDirective,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
BisqRoutingModule,
|
||||
SharedModule,
|
||||
FontAwesomeModule,
|
||||
NgxBootstrapMultiselectModule,
|
||||
],
|
||||
providers: [
|
||||
BisqApiService,
|
||||
MultiSelectSearchFilter,
|
||||
AutofocusDirective,
|
||||
OffClickDirective,
|
||||
]
|
||||
})
|
||||
export class BisqModule {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createChart, CrosshairMode, isBusinessDay } from 'lightweight-charts';
|
||||
import { ChangeDetectionStrategy, Component, ElementRef, HostListener, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-lightweight-charts-area',
|
||||
@@ -25,15 +25,6 @@ export class LightweightChartsAreaComponent implements OnInit, OnChanges, OnDest
|
||||
private element: ElementRef,
|
||||
) { }
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
resizeCanvas(): void {
|
||||
this.width = this.element.nativeElement.parentElement.offsetWidth;
|
||||
this.chart.applyOptions({
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.width = this.element.nativeElement.parentElement.offsetWidth;
|
||||
this.container = document.createElement('div');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createChart, CrosshairMode } from 'lightweight-charts';
|
||||
import { ChangeDetectionStrategy, Component, ElementRef, HostListener, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-lightweight-charts',
|
||||
@@ -21,14 +21,6 @@ export class LightweightChartsComponent implements OnInit, OnChanges, OnDestroy
|
||||
private element: ElementRef,
|
||||
) { }
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
resizeCanvas(): void {
|
||||
this.chart.applyOptions({
|
||||
width: this.element.nativeElement.parentElement.offsetWidth,
|
||||
height: this.height,
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.chart = createChart(this.element.nativeElement, {
|
||||
width: this.element.nativeElement.parentElement.offsetWidth,
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width" i18n="shared.transaction">Transaction</td>
|
||||
<td i18n="shared.transaction">Transaction</td>
|
||||
<td>
|
||||
<a [routerLink]="['/tx/' | relativeUrl, txid]">{{ txid | shortenString : 16}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="td-width" i18n="dashboard.latest-transactions.amount">Amount</td>
|
||||
<td class="td-width" i18n="transaction.value|Transaction value">Value</td>
|
||||
<td><app-amount [satoshis]="value"></app-amount></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -23,13 +23,13 @@
|
||||
<td>{{ fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [value]="fee"></app-fiat></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="td-width" i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
|
||||
<td i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
|
||||
<td>
|
||||
{{ feeRate | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="td-width" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
|
||||
<td i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
|
||||
<td [innerHTML]="'‎' + (vsize | vbytes: 2)"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -9,14 +9,10 @@
|
||||
justify-content: space-between;
|
||||
padding: 10px 15px;
|
||||
text-align: left;
|
||||
min-width: 320px;
|
||||
width: 320px;
|
||||
pointer-events: none;
|
||||
|
||||
&.clickable {
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
.td-width {
|
||||
padding-right: 10px;
|
||||
}
|
||||
@@ -143,7 +143,7 @@ export class BlockPredictionGraphComponent implements OnInit {
|
||||
boundaryGap: false,
|
||||
axisLine: { onZero: true },
|
||||
axisLabel: {
|
||||
formatter: val => formatterXAxisTimeCategory(this.locale, this.timespan, parseInt(val, 10) * 1000),
|
||||
formatter: val => formatterXAxisTimeCategory(this.locale, this.timespan, parseInt(val, 10)),
|
||||
align: 'center',
|
||||
fontSize: 11,
|
||||
lineHeight: 12,
|
||||
|
||||
@@ -114,11 +114,7 @@
|
||||
}
|
||||
|
||||
.flashing {
|
||||
/* force compositing */
|
||||
will-change: opacity;
|
||||
transform: translateZ(0);
|
||||
/* effective max frame rate = (#keyframes - 1) x steps / duration */
|
||||
animation: opacityPulse 2s steps(30, end);
|
||||
animation: opacityPulse 2s ease-out;
|
||||
animation-iteration-count: infinite;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -17,11 +17,7 @@
|
||||
}
|
||||
|
||||
.flashing {
|
||||
/* force compositing */
|
||||
will-change: opacity;
|
||||
transform: translateZ(0);
|
||||
/* effective max frame rate = (#keyframes - 1) x steps / duration */
|
||||
animation: opacityPulse 2s steps(30, end);
|
||||
animation: opacityPulse 2s ease-out;
|
||||
animation-iteration-count: infinite;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Directive, ElementRef, Host, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[ssAutofocus]'
|
||||
})
|
||||
export class AutofocusDirective implements OnInit, OnChanges {
|
||||
|
||||
/**
|
||||
* Will set focus if set to falsy value or not set at all
|
||||
*/
|
||||
@Input() ssAutofocus: any;
|
||||
|
||||
get element(): { focus?: Function } {
|
||||
return this.elemRef.nativeElement;
|
||||
}
|
||||
|
||||
constructor(
|
||||
@Host() private elemRef: ElementRef,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.focus();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
const ssAutofocusChange = changes.ssAutofocus;
|
||||
|
||||
if (ssAutofocusChange && !ssAutofocusChange.isFirstChange()) {
|
||||
this.focus();
|
||||
}
|
||||
}
|
||||
|
||||
focus() {
|
||||
if (this.ssAutofocus) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.element.focus && this.element.focus();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
a {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.dropdown-inline {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-toggle .caret {
|
||||
margin-left: 4px;
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.chunkydropdown-menu {
|
||||
min-width: 20em;
|
||||
}
|
||||
|
||||
.chunkyrow {
|
||||
line-height: 2;
|
||||
margin-left: 1em;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.slider {
|
||||
width:3.8em;
|
||||
height:3.8em;
|
||||
display:block;
|
||||
-webkit-transition: all 0.125s linear;
|
||||
-moz-transition: all 0.125s linear;
|
||||
-o-transition: all 0.125s linear;
|
||||
transition: all 0.125s linear;
|
||||
margin-left: 0.125em;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.slideron {
|
||||
margin-left: 1.35em;
|
||||
}
|
||||
|
||||
.content_wrapper{
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
padding: 0px 5px 5px 5px;
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
<div *ngIf="options" class="dropdown" [ngClass]="settings.containerClasses" [class.open]="isVisible" (offClick)="clickedOutside()">
|
||||
<button type="button" class="dropdown-toggle" [ngClass]="settings.buttonClasses" (click)="toggleDropdown($event)" [disabled]="disabled"
|
||||
[ssAutofocus]="!focusBack">
|
||||
{{ title }}
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<div #scroller *ngIf="isVisible" class="dropdown-menu" [ngClass]="{'chunkydropdown-menu': settings.checkedStyle == 'visual' }"
|
||||
(scroll)="settings.isLazyLoad ? checkScrollPosition($event) : null" (wheel)="settings.stopScrollPropagation ? checkScrollPropagation($event, scroller) : null"
|
||||
[class.pull-right]="settings.pullRight" [class.dropdown-menu-right]="settings.pullRight" [style.max-height]="settings.maxHeight"
|
||||
style="display: block; height: auto; overflow-y: auto;" (keydown.tab)="focusItem(1, $event)" (keydown.shift.tab)="focusItem(-1, $event)">
|
||||
<div class="input-group search-container" *ngIf="settings.enableSearch && (renderFilteredOptions.length > 1 || filterControl.value.length > 0)">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text" id="basic-addon1">
|
||||
<i class="fa fa-search" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
<input type="text" class="form-control" ssAutofocus [formControl]="filterControl" [placeholder]="texts.searchPlaceholder"
|
||||
class="form-control">
|
||||
<div class="input-group-append" *ngIf="filterControl.value.length>0">
|
||||
<button class="btn btn-default btn-secondary" type="button" (click)="clearSearch($event)">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<a role="menuitem" href="javascript:;" tabindex="-1" class="dropdown-item check-control check-control-check" *ngIf="settings.showCheckAll && !disabledSelection && renderFilteredOptions.length > 1"
|
||||
(click)="checkAll()">
|
||||
<span style="width: 16px;"><span [ngClass]="{'glyphicon glyphicon-ok': settings.checkedStyle !== 'fontawesome','fa fa-check': settings.checkedStyle === 'fontawesome'}"></span></span>
|
||||
{{ texts.checkAll }}
|
||||
</a>
|
||||
<a role="menuitem" href="javascript:;" tabindex="-1" class="dropdown-item check-control check-control-uncheck" *ngIf="settings.showUncheckAll && !disabledSelection && renderFilteredOptions.length > 1"
|
||||
(click)="uncheckAll()">
|
||||
<span style="width: 16px;"><span [ngClass]="{'glyphicon glyphicon-remove': settings.checkedStyle !== 'fontawesome','fa fa-times': settings.checkedStyle === 'fontawesome'}"></span></span>
|
||||
{{ texts.uncheckAll }}
|
||||
</a>
|
||||
<a *ngIf="settings.showCheckAll || settings.showUncheckAll" href="javascript:;" class="dropdown-divider divider"></a>
|
||||
<a *ngIf="!renderItems" href="javascript:;" class="dropdown-item empty">{{ texts.searchNoRenderText }}</a>
|
||||
<a *ngIf="renderItems && !renderFilteredOptions.length" href="javascript:;" class="dropdown-item empty">{{ texts.searchEmptyResult }}</a>
|
||||
<a class="dropdown-item" href="javascript:;" *ngFor="let option of renderFilteredOptions; trackBy: trackById" [class.active]="isSelected(option)"
|
||||
[ngStyle]="getItemStyle(option)" [ngClass]="option.classes" [class.dropdown-header]="option.isLabel" [ssAutofocus]="option !== focusedItem"
|
||||
tabindex="-1" (click)="setSelected($event, option)" (keydown.space)="setSelected($event, option)" (keydown.enter)="setSelected($event, option)">
|
||||
<span *ngIf="!option.isLabel; else label" role="menuitem" tabindex="-1" [style.padding-left]="this.parents.length>0&&this.parents.indexOf(option.id)<0&&'30px'"
|
||||
[ngStyle]="getItemStyleSelectionDisabled()">
|
||||
<ng-container [ngSwitch]="settings.checkedStyle">
|
||||
<input *ngSwitchCase="'checkboxes'" type="checkbox" [checked]="isSelected(option)" (click)="preventCheckboxCheck($event, option)"
|
||||
[disabled]="isCheckboxDisabled(option)" [ngStyle]="getItemStyleSelectionDisabled()" />
|
||||
<span *ngSwitchCase="'glyphicon'" style="width: 16px;" class="glyphicon" [class.glyphicon-ok]="isSelected(option)" [class.glyphicon-lock]="isCheckboxDisabled(option)"></span>
|
||||
<span *ngSwitchCase="'fontawesome'" style="width: 16px;display: inline-block;">
|
||||
<span *ngIf="isSelected(option)"><i class="fa fa-check" aria-hidden="true"></i></span>
|
||||
<span *ngIf="isCheckboxDisabled(option)"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
||||
</span>
|
||||
<span *ngSwitchCase="'visual'" style="display:block;float:left; border-radius: 0.2em; border: 0.1em solid rgba(44, 44, 44, 0.63);background:rgba(0, 0, 0, 0.1);width: 5.5em;">
|
||||
<div class="slider" [ngClass]="{'slideron': isSelected(option)}">
|
||||
<img *ngIf="option.image != null" [src]="option.image" style="height: 100%; width: 100%; object-fit: contain" />
|
||||
<div *ngIf="option.image == null" style="height: 100%; width: 100%;text-align: center; display: table; background-color:rgba(0, 0, 0, 0.74)">
|
||||
<div class="content_wrapper">
|
||||
<span style="font-size:3em;color:white" class="glyphicon glyphicon-eye-close"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</ng-container>
|
||||
<span [ngClass]="{'chunkyrow': settings.checkedStyle == 'visual' }" [class.disabled]="isCheckboxDisabled(option)" [ngClass]="settings.itemClasses"
|
||||
[style.font-weight]="this.parents.indexOf(option.id)>=0?'bold':'normal'">
|
||||
{{ option.name }}
|
||||
</span>
|
||||
</span>
|
||||
<ng-template #label>
|
||||
<span [class.disabled]="isCheckboxDisabled(option)">{{ option.name }}</span>
|
||||
</ng-template>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,710 +0,0 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
DoCheck,
|
||||
EventEmitter,
|
||||
forwardRef,
|
||||
Input,
|
||||
IterableDiffers,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from '@angular/core';
|
||||
|
||||
import {
|
||||
AbstractControl,
|
||||
ControlValueAccessor,
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
NG_VALUE_ACCESSOR,
|
||||
Validator,
|
||||
} from '@angular/forms';
|
||||
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { MultiSelectSearchFilter } from './search-filter.pipe';
|
||||
import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts, } from './types';
|
||||
import { Subject, Observable } from 'rxjs';
|
||||
|
||||
const MULTISELECT_VALUE_ACCESSOR: any = {
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => NgxDropdownMultiselectComponent),
|
||||
multi: true,
|
||||
};
|
||||
|
||||
// tslint:disable-next-line: no-conflicting-lifecycle
|
||||
@Component({
|
||||
selector: 'ngx-bootstrap-multiselect',
|
||||
templateUrl: './ngx-bootstrap-multiselect.component.html',
|
||||
styleUrls: ['./ngx-bootstrap-multiselect.component.css'],
|
||||
providers: [MULTISELECT_VALUE_ACCESSOR, MultiSelectSearchFilter],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class NgxDropdownMultiselectComponent implements OnInit,
|
||||
OnChanges,
|
||||
DoCheck,
|
||||
OnDestroy,
|
||||
ControlValueAccessor,
|
||||
Validator {
|
||||
|
||||
private localIsVisible = false;
|
||||
private workerDocClicked = false;
|
||||
|
||||
filterControl: FormControl = this.fb.control('');
|
||||
|
||||
@Input() options: Array<IMultiSelectOption>;
|
||||
@Input() settings: IMultiSelectSettings;
|
||||
@Input() texts: IMultiSelectTexts;
|
||||
@Input() disabled = false;
|
||||
@Input() disabledSelection = false;
|
||||
@Input() searchFunction: (str: string) => RegExp = this._escapeRegExp;
|
||||
|
||||
@Output() selectionLimitReached = new EventEmitter();
|
||||
@Output() dropdownClosed = new EventEmitter();
|
||||
@Output() dropdownOpened = new EventEmitter();
|
||||
@Output() added = new EventEmitter();
|
||||
@Output() removed = new EventEmitter();
|
||||
@Output() lazyLoad = new EventEmitter();
|
||||
@Output() filter: Observable<string> = this.filterControl.valueChanges;
|
||||
|
||||
get focusBack(): boolean {
|
||||
return this.settings.focusBack && this._focusBack;
|
||||
}
|
||||
|
||||
destroyed$ = new Subject<any>();
|
||||
|
||||
filteredOptions: IMultiSelectOption[] = [];
|
||||
lazyLoadOptions: IMultiSelectOption[] = [];
|
||||
renderFilteredOptions: IMultiSelectOption[] = [];
|
||||
model: any[] = [];
|
||||
prevModel: any[] = [];
|
||||
parents: any[];
|
||||
title: string;
|
||||
differ: any;
|
||||
numSelected = 0;
|
||||
set isVisible(val: boolean) {
|
||||
this.localIsVisible = val;
|
||||
this.workerDocClicked = val ? false : this.workerDocClicked;
|
||||
}
|
||||
get isVisible(): boolean {
|
||||
return this.localIsVisible;
|
||||
}
|
||||
renderItems = true;
|
||||
checkAllSearchRegister = new Set();
|
||||
checkAllStatus = false;
|
||||
loadedValueIds = [];
|
||||
_focusBack = false;
|
||||
focusedItem: IMultiSelectOption | undefined;
|
||||
|
||||
defaultSettings: IMultiSelectSettings = {
|
||||
closeOnClickOutside: true,
|
||||
pullRight: false,
|
||||
enableSearch: false,
|
||||
searchRenderLimit: 0,
|
||||
searchRenderAfter: 1,
|
||||
searchMaxLimit: 0,
|
||||
searchMaxRenderedItems: 0,
|
||||
checkedStyle: 'checkboxes',
|
||||
buttonClasses: 'btn btn-primary dropdown-toggle',
|
||||
containerClasses: 'dropdown-inline',
|
||||
selectionLimit: 0,
|
||||
minSelectionLimit: 0,
|
||||
closeOnSelect: false,
|
||||
autoUnselect: false,
|
||||
showCheckAll: false,
|
||||
showUncheckAll: false,
|
||||
fixedTitle: false,
|
||||
dynamicTitleMaxItems: 3,
|
||||
maxHeight: '300px',
|
||||
isLazyLoad: false,
|
||||
stopScrollPropagation: false,
|
||||
loadViewDistance: 1,
|
||||
selectAddedValues: false,
|
||||
ignoreLabels: false,
|
||||
maintainSelectionOrderInTitle: false,
|
||||
focusBack: true
|
||||
};
|
||||
defaultTexts: IMultiSelectTexts = {
|
||||
checkAll: 'Select all',
|
||||
uncheckAll: 'Unselect all',
|
||||
checked: 'selected',
|
||||
checkedPlural: 'selected',
|
||||
searchPlaceholder: 'Search...',
|
||||
searchEmptyResult: 'Nothing found...',
|
||||
searchNoRenderText: 'Type in search box to see results...',
|
||||
defaultTitle: 'Select',
|
||||
allSelected: 'All selected',
|
||||
};
|
||||
|
||||
get searchLimit(): number | undefined {
|
||||
return this.settings.searchRenderLimit;
|
||||
}
|
||||
|
||||
get searchRenderAfter(): number | undefined {
|
||||
return this.settings.searchRenderAfter;
|
||||
}
|
||||
|
||||
get searchLimitApplied(): boolean {
|
||||
return this.searchLimit > 0 && this.options.length > this.searchLimit;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private searchFilter: MultiSelectSearchFilter,
|
||||
differs: IterableDiffers,
|
||||
private cdRef: ChangeDetectorRef
|
||||
) {
|
||||
this.differ = differs.find([]).create(null);
|
||||
this.settings = this.defaultSettings;
|
||||
this.texts = this.defaultTexts;
|
||||
}
|
||||
|
||||
clickedOutside(): void {
|
||||
if (!this.isVisible || !this.settings.closeOnClickOutside) { return; }
|
||||
|
||||
this.isVisible = false;
|
||||
this._focusBack = true;
|
||||
this.dropdownClosed.emit();
|
||||
}
|
||||
|
||||
getItemStyle(option: IMultiSelectOption): any {
|
||||
const style = {};
|
||||
if (!option.isLabel) {
|
||||
style['cursor'] = 'pointer';
|
||||
}
|
||||
if (option.disabled) {
|
||||
style['cursor'] = 'default';
|
||||
}
|
||||
}
|
||||
|
||||
getItemStyleSelectionDisabled(): any {
|
||||
if (this.disabledSelection) {
|
||||
return { cursor: 'default' };
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.title = this.texts.defaultTitle || '';
|
||||
|
||||
this.filterControl.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe(() => {
|
||||
this.updateRenderItems();
|
||||
if (this.settings.isLazyLoad) {
|
||||
this.load();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (changes['options']) {
|
||||
this.options = this.options || [];
|
||||
this.parents = this.options
|
||||
.filter(option => typeof option.parentId === 'number')
|
||||
.map(option => option.parentId);
|
||||
this.updateRenderItems();
|
||||
|
||||
if (
|
||||
this.settings.isLazyLoad &&
|
||||
this.settings.selectAddedValues &&
|
||||
this.loadedValueIds.length === 0
|
||||
) {
|
||||
this.loadedValueIds = this.loadedValueIds.concat(
|
||||
changes.options.currentValue.map(value => value.id)
|
||||
);
|
||||
}
|
||||
if (
|
||||
this.settings.isLazyLoad &&
|
||||
this.settings.selectAddedValues &&
|
||||
changes.options.previousValue
|
||||
) {
|
||||
const addedValues = changes.options.currentValue.filter(
|
||||
value => this.loadedValueIds.indexOf(value.id) === -1
|
||||
);
|
||||
this.loadedValueIds.concat(addedValues.map(value => value.id));
|
||||
if (this.checkAllStatus) {
|
||||
this.addChecks(addedValues);
|
||||
} else if (this.checkAllSearchRegister.size > 0) {
|
||||
this.checkAllSearchRegister.forEach((searchValue: string) =>
|
||||
this.addChecks(this.applyFilters(addedValues, searchValue))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.texts) {
|
||||
this.updateTitle();
|
||||
}
|
||||
|
||||
this.fireModelChange();
|
||||
}
|
||||
|
||||
if (changes['settings']) {
|
||||
this.settings = { ...this.defaultSettings, ...this.settings };
|
||||
}
|
||||
|
||||
if (changes['texts']) {
|
||||
this.texts = { ...this.defaultTexts, ...this.texts };
|
||||
if (!changes['texts'].isFirstChange()) { this.updateTitle(); }
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroyed$.next(false);
|
||||
}
|
||||
|
||||
updateRenderItems() {
|
||||
this.renderItems =
|
||||
!this.searchLimitApplied ||
|
||||
this.filterControl.value.length >= this.searchRenderAfter;
|
||||
this.filteredOptions = this.applyFilters(
|
||||
this.options,
|
||||
this.settings.isLazyLoad ? '' : this.filterControl.value
|
||||
);
|
||||
this.renderFilteredOptions = this.renderItems ? this.filteredOptions : [];
|
||||
this.focusedItem = undefined;
|
||||
}
|
||||
|
||||
applyFilters(options: IMultiSelectOption[], value: string): IMultiSelectOption[] {
|
||||
return this.searchFilter.transform(
|
||||
options,
|
||||
value,
|
||||
this.settings.searchMaxLimit,
|
||||
this.settings.searchMaxRenderedItems,
|
||||
this.searchFunction
|
||||
);
|
||||
}
|
||||
|
||||
fireModelChange(): void {
|
||||
if (this.model != this.prevModel) {
|
||||
this.prevModel = this.model;
|
||||
this.onModelChange(this.model);
|
||||
this.onModelTouched();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
onModelChange: Function = (_: any) => { };
|
||||
onModelTouched: Function = () => { };
|
||||
|
||||
writeValue(value: any): void {
|
||||
if (value !== undefined && value !== null) {
|
||||
this.model = Array.isArray(value) ? value : [value];
|
||||
this.ngDoCheck();
|
||||
} else {
|
||||
this.model = [];
|
||||
}
|
||||
}
|
||||
|
||||
registerOnChange(fn: Function): void {
|
||||
this.onModelChange = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: Function): void {
|
||||
this.onModelTouched = fn;
|
||||
}
|
||||
|
||||
setDisabledState(isDisabled: boolean) {
|
||||
this.disabled = isDisabled;
|
||||
}
|
||||
|
||||
ngDoCheck() {
|
||||
const changes = this.differ.diff(this.model);
|
||||
if (changes) {
|
||||
this.updateNumSelected();
|
||||
this.updateTitle();
|
||||
}
|
||||
}
|
||||
|
||||
validate(_c: AbstractControl): { [key: string]: any } {
|
||||
if (this.model && this.model.length) {
|
||||
return {
|
||||
required: {
|
||||
valid: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (this.options.filter(o => this.model.indexOf(o.id) && !o.disabled).length === 0) {
|
||||
return {
|
||||
selection: {
|
||||
valid: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
registerOnValidatorChange(_fn: () => void): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
clearSearch(event: Event) {
|
||||
this.maybeStopPropagation(event);
|
||||
this.filterControl.setValue('');
|
||||
}
|
||||
|
||||
toggleDropdown(e?: Event) {
|
||||
if (this.isVisible) {
|
||||
this._focusBack = true;
|
||||
}
|
||||
|
||||
this.isVisible = !this.isVisible;
|
||||
this.isVisible ? this.dropdownOpened.emit() : this.dropdownClosed.emit();
|
||||
this.focusedItem = undefined;
|
||||
}
|
||||
|
||||
closeDropdown(e?: Event) {
|
||||
this.isVisible = true;
|
||||
this.toggleDropdown(e);
|
||||
}
|
||||
|
||||
isSelected(option: IMultiSelectOption): boolean {
|
||||
return this.model && this.model.indexOf(option.id) > -1;
|
||||
}
|
||||
|
||||
setSelected(_event: Event, option: IMultiSelectOption) {
|
||||
if (option.isLabel) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (option.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.disabledSelection) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.maybeStopPropagation(_event);
|
||||
this.maybePreventDefault(_event);
|
||||
const index = this.model.indexOf(option.id);
|
||||
const isAtSelectionLimit =
|
||||
this.settings.selectionLimit > 0 &&
|
||||
this.model.length >= this.settings.selectionLimit;
|
||||
const removeItem = (idx, id): void => {
|
||||
this.model.splice(idx, 1);
|
||||
this.removed.emit(id);
|
||||
if (
|
||||
this.settings.isLazyLoad &&
|
||||
this.lazyLoadOptions.some(val => val.id === id)
|
||||
) {
|
||||
this.lazyLoadOptions.splice(
|
||||
this.lazyLoadOptions.indexOf(
|
||||
this.lazyLoadOptions.find(val => val.id === id)
|
||||
),
|
||||
1
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (index > -1) {
|
||||
if (
|
||||
this.settings.minSelectionLimit === undefined ||
|
||||
this.numSelected > this.settings.minSelectionLimit
|
||||
) {
|
||||
removeItem(index, option.id);
|
||||
}
|
||||
const parentIndex =
|
||||
option.parentId && this.model.indexOf(option.parentId);
|
||||
if (parentIndex > -1) {
|
||||
removeItem(parentIndex, option.parentId);
|
||||
} else if (this.parents.indexOf(option.id) > -1) {
|
||||
this.options
|
||||
.filter(
|
||||
child =>
|
||||
this.model.indexOf(child.id) > -1 &&
|
||||
child.parentId === option.id
|
||||
)
|
||||
.forEach(child =>
|
||||
removeItem(this.model.indexOf(child.id), child.id)
|
||||
);
|
||||
}
|
||||
} else if (isAtSelectionLimit && !this.settings.autoUnselect) {
|
||||
this.selectionLimitReached.emit(this.model.length);
|
||||
return;
|
||||
} else {
|
||||
const addItem = (id): void => {
|
||||
this.model.push(id);
|
||||
this.added.emit(id);
|
||||
if (
|
||||
this.settings.isLazyLoad &&
|
||||
!this.lazyLoadOptions.some(val => val.id === id)
|
||||
) {
|
||||
this.lazyLoadOptions.push(option);
|
||||
}
|
||||
};
|
||||
|
||||
addItem(option.id);
|
||||
if (!isAtSelectionLimit) {
|
||||
if (option.parentId && !this.settings.ignoreLabels) {
|
||||
const children = this.options.filter(
|
||||
child =>
|
||||
child.id !== option.id && child.parentId === option.parentId
|
||||
);
|
||||
if (children.every(child => this.model.indexOf(child.id) > -1)) {
|
||||
addItem(option.parentId);
|
||||
}
|
||||
} else if (this.parents.indexOf(option.id) > -1) {
|
||||
const children = this.options.filter(
|
||||
child =>
|
||||
this.model.indexOf(child.id) < 0 && child.parentId === option.id
|
||||
);
|
||||
children.forEach(child => addItem(child.id));
|
||||
}
|
||||
} else {
|
||||
removeItem(0, this.model[0]);
|
||||
}
|
||||
}
|
||||
if (this.settings.closeOnSelect) {
|
||||
this.toggleDropdown();
|
||||
}
|
||||
this.model = this.model.slice();
|
||||
this.fireModelChange();
|
||||
|
||||
}, 0)
|
||||
}
|
||||
|
||||
updateNumSelected() {
|
||||
this.numSelected =
|
||||
this.model.filter(id => this.parents.indexOf(id) < 0).length || 0;
|
||||
}
|
||||
|
||||
updateTitle() {
|
||||
let numSelectedOptions = this.options.length;
|
||||
if (this.settings.ignoreLabels) {
|
||||
numSelectedOptions = this.options.filter(
|
||||
(option: IMultiSelectOption) => !option.isLabel
|
||||
).length;
|
||||
}
|
||||
if (this.numSelected === 0 || this.settings.fixedTitle) {
|
||||
this.title = this.texts ? this.texts.defaultTitle : '';
|
||||
} else if (
|
||||
this.settings.displayAllSelectedText &&
|
||||
this.model.length === numSelectedOptions
|
||||
) {
|
||||
this.title = this.texts ? this.texts.allSelected : '';
|
||||
} else if (
|
||||
this.settings.dynamicTitleMaxItems &&
|
||||
this.settings.dynamicTitleMaxItems >= this.numSelected
|
||||
) {
|
||||
const useOptions =
|
||||
this.settings.isLazyLoad && this.lazyLoadOptions.length
|
||||
? this.lazyLoadOptions
|
||||
: this.options;
|
||||
|
||||
let titleSelections: Array<IMultiSelectOption>;
|
||||
|
||||
if (this.settings.maintainSelectionOrderInTitle) {
|
||||
const optionIds = useOptions.map((selectOption: IMultiSelectOption, idx: number) => selectOption.id);
|
||||
titleSelections = this.model
|
||||
.map((selectedId) => optionIds.indexOf(selectedId))
|
||||
.filter((optionIndex) => optionIndex > -1)
|
||||
.map((optionIndex) => useOptions[optionIndex]);
|
||||
} else {
|
||||
titleSelections = useOptions.filter((option: IMultiSelectOption) => this.model.indexOf(option.id) > -1);
|
||||
}
|
||||
|
||||
this.title = titleSelections.map((option: IMultiSelectOption) => option.name).join(', ');
|
||||
} else {
|
||||
this.title =
|
||||
this.numSelected +
|
||||
' ' +
|
||||
(this.numSelected === 1
|
||||
? this.texts.checked
|
||||
: this.texts.checkedPlural);
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
searchFilterApplied() {
|
||||
return (
|
||||
this.settings.enableSearch &&
|
||||
this.filterControl.value &&
|
||||
this.filterControl.value.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
addChecks(options) {
|
||||
const checkedOptions = options
|
||||
.filter((option: IMultiSelectOption) => {
|
||||
if (
|
||||
!option.disabled &&
|
||||
(
|
||||
this.model.indexOf(option.id) === -1 &&
|
||||
!(this.settings.ignoreLabels && option.isLabel)
|
||||
)
|
||||
) {
|
||||
this.added.emit(option.id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.map((option: IMultiSelectOption) => option.id);
|
||||
|
||||
this.model = this.model.concat(checkedOptions);
|
||||
}
|
||||
|
||||
checkAll(): void {
|
||||
if (!this.disabledSelection) {
|
||||
this.addChecks(
|
||||
!this.searchFilterApplied() ? this.options : this.filteredOptions
|
||||
);
|
||||
if (this.settings.isLazyLoad && this.settings.selectAddedValues) {
|
||||
if (this.searchFilterApplied() && !this.checkAllStatus) {
|
||||
this.checkAllSearchRegister.add(this.filterControl.value);
|
||||
} else {
|
||||
this.checkAllSearchRegister.clear();
|
||||
this.checkAllStatus = true;
|
||||
}
|
||||
this.load();
|
||||
}
|
||||
this.fireModelChange();
|
||||
}
|
||||
}
|
||||
|
||||
uncheckAll(): void {
|
||||
if (!this.disabledSelection) {
|
||||
const checkedOptions = this.model;
|
||||
let unCheckedOptions = !this.searchFilterApplied()
|
||||
? this.model
|
||||
: this.filteredOptions.map((option: IMultiSelectOption) => option.id);
|
||||
// set unchecked options only to the ones that were checked
|
||||
unCheckedOptions = checkedOptions.filter(item => unCheckedOptions.indexOf(item) > -1);
|
||||
this.model = this.model.filter((id: number) => {
|
||||
if (
|
||||
(unCheckedOptions.indexOf(id) < 0 &&
|
||||
this.settings.minSelectionLimit === undefined) ||
|
||||
unCheckedOptions.indexOf(id) < this.settings.minSelectionLimit
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
this.removed.emit(id);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (this.settings.isLazyLoad && this.settings.selectAddedValues) {
|
||||
if (this.searchFilterApplied()) {
|
||||
if (this.checkAllSearchRegister.has(this.filterControl.value)) {
|
||||
this.checkAllSearchRegister.delete(this.filterControl.value);
|
||||
this.checkAllSearchRegister.forEach(function(searchTerm) {
|
||||
const filterOptions = this.applyFilters(this.options.filter(option => unCheckedOptions.indexOf(option.id) > -1), searchTerm);
|
||||
this.addChecks(filterOptions);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.checkAllSearchRegister.clear();
|
||||
this.checkAllStatus = false;
|
||||
}
|
||||
this.load();
|
||||
}
|
||||
this.fireModelChange();
|
||||
}
|
||||
}
|
||||
|
||||
preventCheckboxCheck(event: Event, option: IMultiSelectOption): void {
|
||||
if (
|
||||
option.disabled ||
|
||||
(
|
||||
this.settings.selectionLimit &&
|
||||
!this.settings.autoUnselect &&
|
||||
this.model.length >= this.settings.selectionLimit &&
|
||||
this.model.indexOf(option.id) === -1 &&
|
||||
this.maybePreventDefault(event)
|
||||
)
|
||||
) {
|
||||
this.maybePreventDefault(event);
|
||||
}
|
||||
}
|
||||
|
||||
isCheckboxDisabled(option?: IMultiSelectOption): boolean {
|
||||
return this.disabledSelection || option && option.disabled;
|
||||
}
|
||||
|
||||
checkScrollPosition(ev): void {
|
||||
const scrollTop = ev.target.scrollTop;
|
||||
const scrollHeight = ev.target.scrollHeight;
|
||||
const scrollElementHeight = ev.target.clientHeight;
|
||||
const roundingPixel = 1;
|
||||
const gutterPixel = 1;
|
||||
|
||||
if (
|
||||
scrollTop >=
|
||||
scrollHeight -
|
||||
(1 + this.settings.loadViewDistance) * scrollElementHeight -
|
||||
roundingPixel -
|
||||
gutterPixel
|
||||
) {
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
checkScrollPropagation(ev, element): void {
|
||||
const scrollTop = element.scrollTop;
|
||||
const scrollHeight = element.scrollHeight;
|
||||
const scrollElementHeight = element.clientHeight;
|
||||
|
||||
if (
|
||||
(ev.deltaY > 0 && scrollTop + scrollElementHeight >= scrollHeight) ||
|
||||
(ev.deltaY < 0 && scrollTop <= 0)
|
||||
) {
|
||||
ev = ev || window.event;
|
||||
this.maybePreventDefault(ev);
|
||||
ev.returnValue = false;
|
||||
}
|
||||
}
|
||||
|
||||
trackById(idx: number, selectOption: IMultiSelectOption): void {
|
||||
return selectOption.id;
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.lazyLoad.emit({
|
||||
length: this.options.length,
|
||||
filter: this.filterControl.value,
|
||||
checkAllSearches: this.checkAllSearchRegister,
|
||||
checkAllStatus: this.checkAllStatus,
|
||||
});
|
||||
}
|
||||
|
||||
focusItem(dir: number, e?: Event): void {
|
||||
if (!this.isVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.maybePreventDefault(e);
|
||||
|
||||
const idx = this.filteredOptions.indexOf(this.focusedItem);
|
||||
|
||||
if (idx === -1) {
|
||||
this.focusedItem = this.filteredOptions[0];
|
||||
return;
|
||||
}
|
||||
|
||||
const nextIdx = idx + dir;
|
||||
const newIdx =
|
||||
nextIdx < 0
|
||||
? this.filteredOptions.length - 1
|
||||
: nextIdx % this.filteredOptions.length;
|
||||
|
||||
this.focusedItem = this.filteredOptions[newIdx];
|
||||
}
|
||||
|
||||
private maybePreventDefault(e?: Event): void {
|
||||
if (e && e.preventDefault) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
private maybeStopPropagation(e?: Event): void {
|
||||
if (e && e.stopPropagation) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
private _escapeRegExp(str: string): RegExp {
|
||||
const regExpStr = str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
|
||||
return new RegExp(regExpStr, 'i');
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Directive, HostListener } from '@angular/core';
|
||||
import { EventEmitter } from '@angular/core';
|
||||
import { Output } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
// tslint:disable-next-line:directive-selector
|
||||
selector: '[offClick]',
|
||||
})
|
||||
|
||||
export class OffClickDirective {
|
||||
@Output('offClick') onOffClick = new EventEmitter<any>();
|
||||
|
||||
private _clickEvent: MouseEvent;
|
||||
private _touchEvent: TouchEvent;
|
||||
|
||||
@HostListener('click', ['$event'])
|
||||
public onClick(event: MouseEvent): void {
|
||||
this._clickEvent = event;
|
||||
}
|
||||
|
||||
@HostListener('touchstart', ['$event'])
|
||||
public onTouch(event: TouchEvent): void {
|
||||
this._touchEvent = event;
|
||||
}
|
||||
|
||||
@HostListener('document:click', ['$event'])
|
||||
public onDocumentClick(event: MouseEvent): void {
|
||||
if (event !== this._clickEvent) {
|
||||
this.onOffClick.emit(event);
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('document:touchstart', ['$event'])
|
||||
public onDocumentTouch(event: TouchEvent): void {
|
||||
if (event !== this._touchEvent) {
|
||||
this.onOffClick.emit(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { IMultiSelectOption } from './types';
|
||||
|
||||
interface StringHashMap<T> {
|
||||
[k: string]: T;
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'searchFilter'
|
||||
})
|
||||
export class MultiSelectSearchFilter implements PipeTransform {
|
||||
|
||||
private _lastOptions: IMultiSelectOption[];
|
||||
private _searchCache: StringHashMap<IMultiSelectOption[]> = {};
|
||||
private _searchCacheInclusive: StringHashMap<boolean | number> = {};
|
||||
private _prevSkippedItems: StringHashMap<number> = {};
|
||||
|
||||
transform(
|
||||
options: IMultiSelectOption[],
|
||||
str = '',
|
||||
limit = 0,
|
||||
renderLimit = 0,
|
||||
searchFunction: (str: string) => RegExp,
|
||||
): IMultiSelectOption[] {
|
||||
str = str.toLowerCase();
|
||||
|
||||
// Drop cache because options were updated
|
||||
if (options !== this._lastOptions) {
|
||||
this._lastOptions = options;
|
||||
this._searchCache = {};
|
||||
this._searchCacheInclusive = {};
|
||||
this._prevSkippedItems = {};
|
||||
}
|
||||
|
||||
const filteredOpts = this._searchCache.hasOwnProperty(str)
|
||||
? this._searchCache[str]
|
||||
: this._doSearch(options, str, limit, searchFunction);
|
||||
|
||||
const isUnderLimit = options.length <= limit;
|
||||
|
||||
return isUnderLimit
|
||||
? filteredOpts
|
||||
: this._limitRenderedItems(filteredOpts, renderLimit);
|
||||
}
|
||||
|
||||
private _getSubsetOptions(
|
||||
options: IMultiSelectOption[],
|
||||
prevOptions: IMultiSelectOption[],
|
||||
prevSearchStr: string
|
||||
) {
|
||||
const prevInclusiveOrIdx = this._searchCacheInclusive[prevSearchStr];
|
||||
|
||||
if (prevInclusiveOrIdx === true) {
|
||||
// If have previous results and it was inclusive, do only subsearch
|
||||
return prevOptions;
|
||||
} else if (typeof prevInclusiveOrIdx === 'number') {
|
||||
// Or reuse prev results with unchecked ones
|
||||
return [...prevOptions, ...options.slice(prevInclusiveOrIdx)];
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
private _doSearch(options: IMultiSelectOption[], str: string, limit: number, searchFunction: (str: string) => RegExp) {
|
||||
const prevStr = str.slice(0, -1);
|
||||
const prevResults = this._searchCache[prevStr];
|
||||
const prevResultShift = this._prevSkippedItems[prevStr] || 0;
|
||||
|
||||
if (prevResults) {
|
||||
options = this._getSubsetOptions(options, prevResults, prevStr);
|
||||
}
|
||||
|
||||
const optsLength = options.length;
|
||||
const maxFound = limit > 0 ? Math.min(limit, optsLength) : optsLength;
|
||||
const regexp = searchFunction(str);
|
||||
const filteredOpts: IMultiSelectOption[] = [];
|
||||
|
||||
let i = 0, founded = 0, removedFromPrevResult = 0;
|
||||
|
||||
const doesOptionMatch = (option: IMultiSelectOption) => regexp.test(option.name);
|
||||
const getChildren = (option: IMultiSelectOption) =>
|
||||
options.filter(child => child.parentId === option.id);
|
||||
const getParent = (option: IMultiSelectOption) =>
|
||||
options.find(parent => option.parentId === parent.id);
|
||||
const foundFn = (item: any) => { filteredOpts.push(item); founded++; };
|
||||
const notFoundFn = prevResults ? () => removedFromPrevResult++ : () => { };
|
||||
|
||||
for (; i < optsLength && founded < maxFound; ++i) {
|
||||
const option = options[i];
|
||||
const directMatch = doesOptionMatch(option);
|
||||
|
||||
if (directMatch) {
|
||||
foundFn(option);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof option.parentId === 'undefined') {
|
||||
const childrenMatch = getChildren(option).some(doesOptionMatch);
|
||||
|
||||
if (childrenMatch) {
|
||||
foundFn(option);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof option.parentId !== 'undefined') {
|
||||
const parentMatch = doesOptionMatch(getParent(option));
|
||||
|
||||
if (parentMatch) {
|
||||
foundFn(option);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
notFoundFn();
|
||||
}
|
||||
|
||||
const totalIterations = i + prevResultShift;
|
||||
|
||||
this._searchCache[str] = filteredOpts;
|
||||
this._searchCacheInclusive[str] = i === optsLength || totalIterations;
|
||||
this._prevSkippedItems[str] = removedFromPrevResult + prevResultShift;
|
||||
|
||||
return filteredOpts;
|
||||
}
|
||||
|
||||
private _limitRenderedItems<T>(items: T[], limit: number): T[] {
|
||||
return items.length > limit && limit > 0 ? items.slice(0, limit) : items;
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
export interface IMultiSelectOption {
|
||||
id: any;
|
||||
name: string;
|
||||
disabled?: boolean;
|
||||
isLabel?: boolean;
|
||||
parentId?: any;
|
||||
params?: any;
|
||||
classes?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
export interface IMultiSelectSettings {
|
||||
pullRight?: boolean;
|
||||
enableSearch?: boolean;
|
||||
closeOnClickOutside?: boolean;
|
||||
/**
|
||||
* 0 - By default
|
||||
* If `enableSearch=true` and total amount of items more then `searchRenderLimit` (0 - No limit)
|
||||
* then render items only when user typed more then or equal `searchRenderAfter` charachters
|
||||
*/
|
||||
searchRenderLimit?: number;
|
||||
/**
|
||||
* 3 - By default
|
||||
*/
|
||||
searchRenderAfter?: number;
|
||||
/**
|
||||
* 0 - By default
|
||||
* If >0 will render only N first items
|
||||
*/
|
||||
searchMaxLimit?: number;
|
||||
/**
|
||||
* 0 - By default
|
||||
* Used with searchMaxLimit to further limit rendering for optimization
|
||||
* Should be less than searchMaxLimit to take effect
|
||||
*/
|
||||
searchMaxRenderedItems?: number;
|
||||
checkedStyle?: 'checkboxes' | 'glyphicon' | 'fontawesome' | 'visual';
|
||||
buttonClasses?: string;
|
||||
itemClasses?: string;
|
||||
containerClasses?: string;
|
||||
selectionLimit?: number;
|
||||
minSelectionLimit?: number;
|
||||
closeOnSelect?: boolean;
|
||||
autoUnselect?: boolean;
|
||||
showCheckAll?: boolean;
|
||||
showUncheckAll?: boolean;
|
||||
fixedTitle?: boolean;
|
||||
dynamicTitleMaxItems?: number;
|
||||
maxHeight?: string;
|
||||
displayAllSelectedText?: boolean;
|
||||
isLazyLoad?: boolean;
|
||||
loadViewDistance?: number;
|
||||
stopScrollPropagation?: boolean;
|
||||
selectAddedValues?: boolean;
|
||||
/**
|
||||
* false - By default
|
||||
* If activated label IDs don't count and won't be written to the model.
|
||||
*/
|
||||
ignoreLabels?: boolean;
|
||||
/**
|
||||
* false - By default
|
||||
* If activated, the title will show selections in the order they were selected.
|
||||
*/
|
||||
maintainSelectionOrderInTitle?: boolean;
|
||||
/**
|
||||
* @default true
|
||||
* Set the focus back to the input control when the dropdown closed
|
||||
*/
|
||||
focusBack?: boolean;
|
||||
}
|
||||
|
||||
export interface IMultiSelectTexts {
|
||||
checkAll?: string;
|
||||
uncheckAll?: string;
|
||||
checked?: string;
|
||||
checkedPlural?: string;
|
||||
searchPlaceholder?: string;
|
||||
searchEmptyResult?: string;
|
||||
searchNoRenderText?: string;
|
||||
defaultTitle?: string;
|
||||
allSelected?: string;
|
||||
}
|
||||
@@ -27,7 +27,15 @@ $width: 500;
|
||||
$height: 500;
|
||||
|
||||
// Create the explosion...
|
||||
|
||||
$box-shadow: ();
|
||||
$box-shadow2: ();
|
||||
@for $i from 0 through $particles {
|
||||
$box-shadow: $box-shadow,
|
||||
random($width) - math.div($width, 1.2) + px
|
||||
random($height) - math.div($height, 1.2) + px
|
||||
hsl(random(360), 100%, 50%);
|
||||
$box-shadow2: $box-shadow2, 0 0 #fff
|
||||
}
|
||||
@mixin keyframes ($animationName) {
|
||||
@-webkit-keyframes #{$animationName} {
|
||||
@content;
|
||||
@@ -95,6 +103,7 @@ body {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
box-shadow: $box-shadow2;
|
||||
@include animation((1s bang ease-out infinite backwards, 1s gravity ease-in infinite backwards, 5s position linear infinite backwards));
|
||||
}
|
||||
|
||||
@@ -103,9 +112,9 @@ body {
|
||||
@include animation-duration((1.25s, 1.25s, 6.25s));
|
||||
}
|
||||
|
||||
@keyframes bang{
|
||||
to{
|
||||
box-shadow:-314.6666666667px -362.6666666667px red,-51.6666666667px 32.3333333333px #ff3700,-354.6666666667px -264.6666666667px #7b00ff,-319.6666666667px -73.6666666667px #00f7ff,-135.6666666667px -154.6666666667px #00ff48,57.3333333333px -402.6666666667px #0d00ff,-126.6666666667px -121.6666666667px #00ff7b,-335.6666666667px -5.6666666667px #00fff2,-291.6666666667px -.6666666667px #4f0,-126.6666666667px -187.6666666667px #7f0,-413.6666666667px -224.6666666667px #00ffbf,-283.6666666667px -391.6666666667px #00ff3c,-340.6666666667px -345.6666666667px #02f,-168.6666666667px -179.6666666667px #eaff00,7.3333333333px -153.6666666667px #26ff00,-175.6666666667px -234.6666666667px #8400ff,-324.6666666667px -254.6666666667px #0048ff,-335.6666666667px -9.6666666667px #00ff59,-304.6666666667px -8.6666666667px #001eff,-331.6666666667px -44.6666666667px #3f0,.3333333333px -49.6666666667px #0fc,-370.6666666667px -60.6666666667px #0015ff,29.3333333333px -13.6666666667px #8cff00,-168.6666666667px -281.6666666667px #f80,-48.6666666667px -61.6666666667px #f0b,33.3333333333px -113.6666666667px #ff00e1,-193.6666666667px -196.6666666667px #ff7b00,-14.6666666667px -24.6666666667px #ff0037,-149.6666666667px -273.6666666667px #0fa,-19.6666666667px -63.6666666667px #ff0004,13.3333333333px -227.6666666667px #7f0,-265.6666666667px -43.6666666667px #ff4800,-121.6666666667px -95.6666666667px #bfff00,-241.6666666667px -90.6666666667px #6200ff,-307.6666666667px -231.6666666667px #ff0062,78.3333333333px -128.6666666667px #ffbf00,27.3333333333px 44.3333333333px #95ff00,-81.6666666667px 6.3333333333px #ffc800,-343.6666666667px -247.6666666667px #2f0,-225.6666666667px -250.6666666667px #08f,-9.6666666667px -243.6666666667px #ff1a00,83.3333333333px -409.6666666667px #04f,-380.6666666667px -331.6666666667px #84ff00,-103.6666666667px -51.6666666667px #f02,-174.6666666667px -169.6666666667px #ffc800,20.3333333333px -191.6666666667px #ff0059,-40.6666666667px -55.6666666667px #0400ff,-199.6666666667px -66.6666666667px #ffd500,-358.6666666667px -5.6666666667px #0051ff,-84.6666666667px -289.6666666667px #f7ff00,-193.6666666667px -184.6666666667px #80f
|
||||
@include keyframes(bang) {
|
||||
to {
|
||||
box-shadow:$box-shadow;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -878,10 +878,6 @@
|
||||
<context context-type="sourcefile">src/app/bisq/bisq-transactions/bisq-transactions.component.html</context>
|
||||
<context context-type="linenumber">20,21</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/block-overview-tooltip/block-overview-tooltip.component.html</context>
|
||||
<context context-type="linenumber">18</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/dashboard/dashboard.component.html</context>
|
||||
<context context-type="linenumber">124,125</context>
|
||||
@@ -1267,14 +1263,14 @@
|
||||
<source>Trades</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/bisq/lightweight-charts-area/lightweight-charts-area.component.ts</context>
|
||||
<context context-type="linenumber">99</context>
|
||||
<context context-type="linenumber">90</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="bisq-graph-volume" datatype="html">
|
||||
<source>Volume</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/bisq/lightweight-charts-area/lightweight-charts-area.component.ts</context>
|
||||
<context context-type="linenumber">100</context>
|
||||
<context context-type="linenumber">91</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4b137ec8bf73a47063740b75c0c40d5fd3c48015" datatype="html">
|
||||
@@ -1922,6 +1918,15 @@
|
||||
<context context-type="linenumber">264,266</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="bc4c61d3713989e3c8c6610fca3ea1ca1cb19edb" datatype="html">
|
||||
<source>Value</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/block-overview-tooltip/block-overview-tooltip.component.html</context>
|
||||
<context context-type="linenumber">18</context>
|
||||
</context-group>
|
||||
<note priority="1" from="description">Transaction value</note>
|
||||
<note priority="1" from="meaning">transaction.value</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="cb1b52c13b95fa29ea4044f2bbe0ac623b890c80" datatype="html">
|
||||
<source>Fee</source>
|
||||
<context-group purpose="location">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1046,7 +1046,3 @@ th {
|
||||
box-shadow: -10px -15px 75px rgba(#eba814, 1);
|
||||
transition: 100ms all ease-in;
|
||||
}
|
||||
|
||||
.page-item {
|
||||
font-family: monospace;
|
||||
}
|
||||
@@ -82,11 +82,11 @@ pkg install -y zsh sudo git screen curl wget neovim rsync nginx openssl openssh-
|
||||
|
||||
### Node.js + npm
|
||||
|
||||
Build Node.js v16.16.0 and npm v8 from source using `nvm`:
|
||||
Build Node.js v16.15 and npm v8 from source using `nvm`:
|
||||
```
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | zsh
|
||||
source $HOME/.zshrc
|
||||
nvm install v16.16.0 --shared-zlib
|
||||
nvm install v16.15.0
|
||||
nvm alias default node
|
||||
```
|
||||
|
||||
|
||||
@@ -183,9 +183,6 @@ case $OS in
|
||||
TOR_PKG=tor
|
||||
TOR_USER=_tor
|
||||
TOR_GROUP=_tor
|
||||
NGINX_USER=www
|
||||
NGINX_ETC_FOLDER=/usr/local/etc/nginx
|
||||
NGINX_CONFIGURATION=/usr/local/etc/nginx/nginx.conf
|
||||
CERTBOT_PKG=py39-certbot
|
||||
;;
|
||||
|
||||
@@ -200,8 +197,6 @@ case $OS in
|
||||
TOR_USER=debian-tor
|
||||
TOR_GROUP=debian-tor
|
||||
CERTBOT_PKG=python3-certbot-nginx
|
||||
NGINX_USER=www-data
|
||||
NGINX_ETC_FOLDER=/etc/nginx
|
||||
NGINX_CONFIGURATION=/etc/nginx/nginx.conf
|
||||
;;
|
||||
esac
|
||||
@@ -651,193 +646,193 @@ ext4CreateDir()
|
||||
|
||||
# does bitcoin exist?
|
||||
|
||||
###########
|
||||
## dialog #
|
||||
###########
|
||||
#
|
||||
#: ${DIALOG=dialog}
|
||||
#
|
||||
#: ${DIALOG_OK=0}
|
||||
#: ${DIALOG_CANCEL=1}
|
||||
#: ${DIALOG_HELP=2}
|
||||
#: ${DIALOG_EXTRA=3}
|
||||
#: ${DIALOG_ITEM_HELP=4}
|
||||
#: ${DIALOG_ESC=255}
|
||||
#
|
||||
#: ${SIG_OFFNE=0}
|
||||
#: ${SIG_HUP=1}
|
||||
#: ${SIG_INT=2}
|
||||
#: ${SIG_QUIT=3}
|
||||
#: ${SIG_KILL=9}
|
||||
#: ${SIG_TERM=15}
|
||||
#
|
||||
#input=`tempfile 2>/dev/null` || input=/tmp/input$$
|
||||
#output=`tempfile 2>/dev/null` || output=/tmp/test$$
|
||||
#trap "rm -f $input $output" $SIG_OFFNE $SIG_HUP $SIG_INT $SIG_TRAP $SIG_TERM
|
||||
#
|
||||
#DIALOG_ERROR=254
|
||||
#export DIALOG_ERROR
|
||||
#
|
||||
#backtitle="Mempool Fullnode Installer"
|
||||
#title="Mempool Fullnode Installer"
|
||||
#returncode=0
|
||||
#
|
||||
##################
|
||||
## dialog part 1 #
|
||||
##################
|
||||
#
|
||||
#$CUT >$input <<-EOF
|
||||
#Tor:Enable Tor v3 HS Onion:ON
|
||||
#Certbot:Enable HTTPS using Certbot:ON
|
||||
#Mainnet:Enable Bitcoin Mainnet:ON
|
||||
#Mainnet-Minfee:Enable Bitcoin Mainnet Minfee:ON
|
||||
#Testnet:Enable Bitcoin Testnet:ON
|
||||
#Liquid:Enable Elements Liquid:ON
|
||||
#Bisq:Enable Bisq:ON
|
||||
#Lightmode:Enable Electrs Lightmode to save disk space:ON
|
||||
#Smalldisk:Disable Electrs Compaction to save disk space:ON
|
||||
#Firewall:Enable Firewall:ON
|
||||
#EOF
|
||||
#
|
||||
#cat $input | sed -e 's/^/"/' -e 's/:/" "/g' -e 's/$/"/' >$output
|
||||
#cat $output >$input
|
||||
#
|
||||
#$DIALOG --backtitle "${backtitle}" \
|
||||
# --title "${title}" "$@" \
|
||||
# --checklist "Toggle the features below to configure your fullnode:\n" \
|
||||
# 20 80 10 \
|
||||
# --file $input 2> $output
|
||||
#
|
||||
#retval=$?
|
||||
#
|
||||
#tempfile=$output
|
||||
#if [ $retval != $DIALOG_OK ];then
|
||||
# echo "Installation aborted."
|
||||
# exit 1
|
||||
#fi
|
||||
#
|
||||
#if grep Tor $tempfile >/dev/null 2>&1;then
|
||||
# TOR_INSTALL=ON
|
||||
#else
|
||||
# TOR_INSTALL=OFF
|
||||
#fi
|
||||
#
|
||||
#if grep Certbot $tempfile >/dev/null 2>&1;then
|
||||
# CERTBOT_INSTALL=ON
|
||||
#else
|
||||
# CERTBOT_INSTALL=OFF
|
||||
#fi
|
||||
#
|
||||
#if grep Mainnet $tempfile >/dev/null 2>&1;then
|
||||
# BITCOIN_MAINNET_ENABLE=ON
|
||||
#else
|
||||
# BITCOIN_MAINNET_ENABLE=OFF
|
||||
#fi
|
||||
#
|
||||
#if grep Mainnet-Minfee $tempfile >/dev/null 2>&1;then
|
||||
# BITCOIN_MAINNET_MINFEE_ENABLE=ON
|
||||
#else
|
||||
# BITCOIN_MAINNET_MINFEE_ENABLE=OFF
|
||||
#fi
|
||||
#
|
||||
#if grep Testnet $tempfile >/dev/null 2>&1;then
|
||||
# BITCOIN_TESTNET_ENABLE=ON
|
||||
#else
|
||||
# BITCOIN_TESTNET_ENABLE=OFF
|
||||
#fi
|
||||
#
|
||||
#if grep Liquid $tempfile >/dev/null 2>&1;then
|
||||
# ELEMENTS_INSTALL=ON
|
||||
# ELEMENTS_LIQUID_ENABLE=ON
|
||||
#else
|
||||
# ELEMENTS_INSTALL=OFF
|
||||
# ELEMENTS_LIQUID_ENABLE=OFF
|
||||
#fi
|
||||
#
|
||||
#if grep Bisq $tempfile >/dev/null 2>&1;then
|
||||
# BISQ_INSTALL=ON
|
||||
# BISQ_MAINNET_ENABLE=ON
|
||||
#else
|
||||
# BISQ_INSTALL=OFF
|
||||
# BISQ_MAINNET_ENABLE=OFF
|
||||
#fi
|
||||
#
|
||||
#if grep Lightmode $tempfile >/dev/null 2>&1;then
|
||||
# BITCOIN_ELECTRS_LIGHT_MODE=ON
|
||||
#else
|
||||
# BITCOIN_ELECTRS_LIGHT_MODE=OFF
|
||||
#fi
|
||||
#
|
||||
#if grep Smalldisk $tempfile >/dev/null 2>&1;then
|
||||
# BITCOIN_ELECTRS_LIGHT_MODE=ON
|
||||
#else
|
||||
# BITCOIN_ELECTRS_LIGHT_MODE=OFF
|
||||
#fi
|
||||
#
|
||||
##################
|
||||
## dialog part 2 #
|
||||
##################
|
||||
#
|
||||
#$DIALOG --cr-wrap \
|
||||
# --title "INPUT BOX" --clear \
|
||||
# --inputbox "$@" \
|
||||
#"Enter the FQDN hostname for obtaining an SSL certificate using Certbot:" 0 0 "${HOSTNAME}" 2> $tempfile
|
||||
#HOSTNAME=$(cat $tempfile)
|
||||
#
|
||||
##################
|
||||
## dialog part 3 #
|
||||
##################
|
||||
#
|
||||
## --form text height width formheight
|
||||
## [ label y x item y x flen ilen ]
|
||||
# #"BISQ_BLOCKNOTIFY_HOST" 0 1 "${BISQ_BLOCKNOTIFY_HOST}" 0 30 0 0 \
|
||||
#
|
||||
#$DIALOG --ok-label "Submit" \
|
||||
# --backtitle "$backtitle" "$@" \
|
||||
# --form "Your fullnode will be installed as follows:" 0 0 0 \
|
||||
# "BISQ_LATEST_RELEASE" 1 1 "${BISQ_LATEST_RELEASE}" 1 35 35 0 \
|
||||
# "BISQ_REPO_BRANCH" 2 1 "${BISQ_REPO_BRANCH}" 2 35 35 0 \
|
||||
# "BISQ_REPO_NAME" 3 1 "${BISQ_REPO_NAME}" 3 35 35 0 \
|
||||
# "BISQ_REPO_URL" 4 1 "${BISQ_REPO_URL}" 4 35 35 0 \
|
||||
# "BITCOIN_ELECTRS_LATEST_RELEASE" 5 1 "${BITCOIN_ELECTRS_LATEST_RELEASE}" 5 35 35 0 \
|
||||
# "BITCOIN_ELECTRS_LIGHT_MODE" 6 1 "${BITCOIN_ELECTRS_LIGHT_MODE}" 6 35 35 0 \
|
||||
# "BITCOIN_ELECTRS_REPO_BRANCH" 7 1 "${BITCOIN_ELECTRS_REPO_BRANCH}" 7 35 35 0 \
|
||||
# "BITCOIN_ELECTRS_REPO_NAME" 8 1 "${BITCOIN_ELECTRS_REPO_NAME}" 8 35 35 0 \
|
||||
# "BITCOIN_ELECTRS_REPO_URL" 9 1 "${BITCOIN_ELECTRS_REPO_URL}" 9 35 35 0 \
|
||||
# "BITCOIN_LATEST_RELEASE" 10 1 "${BITCOIN_LATEST_RELEASE}" 10 35 35 0 \
|
||||
# "BITCOIN_MAINNET_ENABLE" 11 1 "${BITCOIN_MAINNET_ENABLE}" 11 35 35 0 \
|
||||
# "BITCOIN_REPO_BRANCH" 12 1 "${BITCOIN_REPO_BRANCH}" 12 35 35 0 \
|
||||
# "BITCOIN_REPO_NAME" 13 1 "${BITCOIN_REPO_NAME}" 13 35 35 0 \
|
||||
# "BITCOIN_REPO_URL" 14 1 "${BITCOIN_REPO_URL}" 14 35 35 0 \
|
||||
# "BITCOIN_TESTNET_ENABLE" 15 1 "${BITCOIN_TESTNET_ENABLE}" 15 35 35 0 \
|
||||
# "ELEMENTS_INSTALL" 16 1 "${ELEMENTS_INSTALL}" 16 35 35 0 \
|
||||
# "ELEMENTS_LATEST_RELEASE" 17 1 "${ELEMENTS_LATEST_RELEASE}" 17 35 35 0 \
|
||||
# "ELEMENTS_LIQUID_ENABLE" 18 1 "${ELEMENTS_LIQUID_ENABLE}" 18 35 35 0 \
|
||||
# "ELEMENTS_REPO_BRANCH" 19 1 "${ELEMENTS_REPO_BRANCH}" 19 35 35 0 \
|
||||
# "ELEMENTS_REPO_NAME" 20 1 "${ELEMENTS_REPO_NAME}" 20 35 35 0 \
|
||||
# "ELEMENTS_REPO_URL" 21 1 "${ELEMENTS_REPO_URL}" 21 35 35 0 \
|
||||
# "MEMPOOL_LATEST_RELEASE" 22 1 "${MEMPOOL_LATEST_RELEASE}" 22 35 35 0 \
|
||||
# "MEMPOOL_LIQUID_HTTP_HOST" 23 1 "${MEMPOOL_LIQUID_HTTP_HOST}" 23 35 35 0 \
|
||||
# "MEMPOOL_LIQUID_HTTP_PORT" 24 1 "${MEMPOOL_LIQUID_HTTP_PORT}" 24 35 35 0 \
|
||||
# "MEMPOOL_MAINNET_HTTP_HOST" 25 1 "${MEMPOOL_MAINNET_HTTP_HOST}" 25 35 35 0 \
|
||||
# "MEMPOOL_MAINNET_HTTP_PORT" 26 1 "${MEMPOOL_MAINNET_HTTP_PORT}" 26 35 35 0 \
|
||||
# "MEMPOOL_REPO_BRANCH" 27 1 "${MEMPOOL_REPO_BRANCH}" 27 35 35 0 \
|
||||
# "MEMPOOL_REPO_NAME" 28 1 "${MEMPOOL_REPO_NAME}" 28 35 35 0 \
|
||||
# "MEMPOOL_REPO_URL" 29 1 "${MEMPOOL_REPO_URL}" 29 35 35 0 \
|
||||
# "MEMPOOL_TESTNET_HTTP_HOST" 30 1 "${MEMPOOL_TESTNET_HTTP_HOST}" 30 35 35 0 \
|
||||
# "MEMPOOL_TESTNET_HTTP_PORT" 31 1 "${MEMPOOL_TESTNET_HTTP_PORT}" 31 35 35 0 \
|
||||
# "MEMPOOL_TOR_HS" 32 1 "${MEMPOOL_TOR_HS}" 32 35 35 0 \
|
||||
# "HOSTNAME" 33 1 "${HOSTNAME}" 33 35 35 0 \
|
||||
# "TOR_INSTALL" 34 1 "${TOR_INSTALL}" 34 35 35 0 \
|
||||
# "CERTBOT_INSTALL" 35 1 "${CERTBOT_INSTALL}" 35 35 35 0 \
|
||||
#2> $tempfile
|
||||
#
|
||||
#retval=$?
|
||||
#
|
||||
#if [ $retval != $DIALOG_OK ];then
|
||||
# echo "Installation aborted."
|
||||
# exit 1
|
||||
#fi
|
||||
##########
|
||||
# dialog #
|
||||
##########
|
||||
|
||||
: ${DIALOG=dialog}
|
||||
|
||||
: ${DIALOG_OK=0}
|
||||
: ${DIALOG_CANCEL=1}
|
||||
: ${DIALOG_HELP=2}
|
||||
: ${DIALOG_EXTRA=3}
|
||||
: ${DIALOG_ITEM_HELP=4}
|
||||
: ${DIALOG_ESC=255}
|
||||
|
||||
: ${SIG_OFFNE=0}
|
||||
: ${SIG_HUP=1}
|
||||
: ${SIG_INT=2}
|
||||
: ${SIG_QUIT=3}
|
||||
: ${SIG_KILL=9}
|
||||
: ${SIG_TERM=15}
|
||||
|
||||
input=`tempfile 2>/dev/null` || input=/tmp/input$$
|
||||
output=`tempfile 2>/dev/null` || output=/tmp/test$$
|
||||
trap "rm -f $input $output" $SIG_OFFNE $SIG_HUP $SIG_INT $SIG_TRAP $SIG_TERM
|
||||
|
||||
DIALOG_ERROR=254
|
||||
export DIALOG_ERROR
|
||||
|
||||
backtitle="Mempool Fullnode Installer"
|
||||
title="Mempool Fullnode Installer"
|
||||
returncode=0
|
||||
|
||||
#################
|
||||
# dialog part 1 #
|
||||
#################
|
||||
|
||||
$CUT >$input <<-EOF
|
||||
Tor:Enable Tor v3 HS Onion:ON
|
||||
Certbot:Enable HTTPS using Certbot:ON
|
||||
Mainnet:Enable Bitcoin Mainnet:ON
|
||||
Mainnet-Minfee:Enable Bitcoin Mainnet Minfee:ON
|
||||
Testnet:Enable Bitcoin Testnet:ON
|
||||
Liquid:Enable Elements Liquid:ON
|
||||
Bisq:Enable Bisq:ON
|
||||
Lightmode:Enable Electrs Lightmode to save disk space:ON
|
||||
Smalldisk:Disable Electrs Compaction to save disk space:ON
|
||||
Firewall:Enable Firewall:ON
|
||||
EOF
|
||||
|
||||
cat $input | sed -e 's/^/"/' -e 's/:/" "/g' -e 's/$/"/' >$output
|
||||
cat $output >$input
|
||||
|
||||
$DIALOG --backtitle "${backtitle}" \
|
||||
--title "${title}" "$@" \
|
||||
--checklist "Toggle the features below to configure your fullnode:\n" \
|
||||
20 80 10 \
|
||||
--file $input 2> $output
|
||||
|
||||
retval=$?
|
||||
|
||||
tempfile=$output
|
||||
if [ $retval != $DIALOG_OK ];then
|
||||
echo "Installation aborted."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep Tor $tempfile >/dev/null 2>&1;then
|
||||
TOR_INSTALL=ON
|
||||
else
|
||||
TOR_INSTALL=OFF
|
||||
fi
|
||||
|
||||
if grep Certbot $tempfile >/dev/null 2>&1;then
|
||||
CERTBOT_INSTALL=ON
|
||||
else
|
||||
CERTBOT_INSTALL=OFF
|
||||
fi
|
||||
|
||||
if grep Mainnet $tempfile >/dev/null 2>&1;then
|
||||
BITCOIN_MAINNET_ENABLE=ON
|
||||
else
|
||||
BITCOIN_MAINNET_ENABLE=OFF
|
||||
fi
|
||||
|
||||
if grep Mainnet-Minfee $tempfile >/dev/null 2>&1;then
|
||||
BITCOIN_MAINNET_MINFEE_ENABLE=ON
|
||||
else
|
||||
BITCOIN_MAINNET_MINFEE_ENABLE=OFF
|
||||
fi
|
||||
|
||||
if grep Testnet $tempfile >/dev/null 2>&1;then
|
||||
BITCOIN_TESTNET_ENABLE=ON
|
||||
else
|
||||
BITCOIN_TESTNET_ENABLE=OFF
|
||||
fi
|
||||
|
||||
if grep Liquid $tempfile >/dev/null 2>&1;then
|
||||
ELEMENTS_INSTALL=ON
|
||||
ELEMENTS_LIQUID_ENABLE=ON
|
||||
else
|
||||
ELEMENTS_INSTALL=OFF
|
||||
ELEMENTS_LIQUID_ENABLE=OFF
|
||||
fi
|
||||
|
||||
if grep Bisq $tempfile >/dev/null 2>&1;then
|
||||
BISQ_INSTALL=ON
|
||||
BISQ_MAINNET_ENABLE=ON
|
||||
else
|
||||
BISQ_INSTALL=OFF
|
||||
BISQ_MAINNET_ENABLE=OFF
|
||||
fi
|
||||
|
||||
if grep Lightmode $tempfile >/dev/null 2>&1;then
|
||||
BITCOIN_ELECTRS_LIGHT_MODE=ON
|
||||
else
|
||||
BITCOIN_ELECTRS_LIGHT_MODE=OFF
|
||||
fi
|
||||
|
||||
if grep Smalldisk $tempfile >/dev/null 2>&1;then
|
||||
BITCOIN_ELECTRS_LIGHT_MODE=ON
|
||||
else
|
||||
BITCOIN_ELECTRS_LIGHT_MODE=OFF
|
||||
fi
|
||||
|
||||
#################
|
||||
# dialog part 2 #
|
||||
#################
|
||||
|
||||
$DIALOG --cr-wrap \
|
||||
--title "INPUT BOX" --clear \
|
||||
--inputbox "$@" \
|
||||
"Enter the FQDN hostname for obtaining an SSL certificate using Certbot:" 0 0 "${HOSTNAME}" 2> $tempfile
|
||||
HOSTNAME=$(cat $tempfile)
|
||||
|
||||
#################
|
||||
# dialog part 3 #
|
||||
#################
|
||||
|
||||
# --form text height width formheight
|
||||
# [ label y x item y x flen ilen ]
|
||||
#"BISQ_BLOCKNOTIFY_HOST" 0 1 "${BISQ_BLOCKNOTIFY_HOST}" 0 30 0 0 \
|
||||
|
||||
$DIALOG --ok-label "Submit" \
|
||||
--backtitle "$backtitle" "$@" \
|
||||
--form "Your fullnode will be installed as follows:" 0 0 0 \
|
||||
"BISQ_LATEST_RELEASE" 1 1 "${BISQ_LATEST_RELEASE}" 1 35 35 0 \
|
||||
"BISQ_REPO_BRANCH" 2 1 "${BISQ_REPO_BRANCH}" 2 35 35 0 \
|
||||
"BISQ_REPO_NAME" 3 1 "${BISQ_REPO_NAME}" 3 35 35 0 \
|
||||
"BISQ_REPO_URL" 4 1 "${BISQ_REPO_URL}" 4 35 35 0 \
|
||||
"BITCOIN_ELECTRS_LATEST_RELEASE" 5 1 "${BITCOIN_ELECTRS_LATEST_RELEASE}" 5 35 35 0 \
|
||||
"BITCOIN_ELECTRS_LIGHT_MODE" 6 1 "${BITCOIN_ELECTRS_LIGHT_MODE}" 6 35 35 0 \
|
||||
"BITCOIN_ELECTRS_REPO_BRANCH" 7 1 "${BITCOIN_ELECTRS_REPO_BRANCH}" 7 35 35 0 \
|
||||
"BITCOIN_ELECTRS_REPO_NAME" 8 1 "${BITCOIN_ELECTRS_REPO_NAME}" 8 35 35 0 \
|
||||
"BITCOIN_ELECTRS_REPO_URL" 9 1 "${BITCOIN_ELECTRS_REPO_URL}" 9 35 35 0 \
|
||||
"BITCOIN_LATEST_RELEASE" 10 1 "${BITCOIN_LATEST_RELEASE}" 10 35 35 0 \
|
||||
"BITCOIN_MAINNET_ENABLE" 11 1 "${BITCOIN_MAINNET_ENABLE}" 11 35 35 0 \
|
||||
"BITCOIN_REPO_BRANCH" 12 1 "${BITCOIN_REPO_BRANCH}" 12 35 35 0 \
|
||||
"BITCOIN_REPO_NAME" 13 1 "${BITCOIN_REPO_NAME}" 13 35 35 0 \
|
||||
"BITCOIN_REPO_URL" 14 1 "${BITCOIN_REPO_URL}" 14 35 35 0 \
|
||||
"BITCOIN_TESTNET_ENABLE" 15 1 "${BITCOIN_TESTNET_ENABLE}" 15 35 35 0 \
|
||||
"ELEMENTS_INSTALL" 16 1 "${ELEMENTS_INSTALL}" 16 35 35 0 \
|
||||
"ELEMENTS_LATEST_RELEASE" 17 1 "${ELEMENTS_LATEST_RELEASE}" 17 35 35 0 \
|
||||
"ELEMENTS_LIQUID_ENABLE" 18 1 "${ELEMENTS_LIQUID_ENABLE}" 18 35 35 0 \
|
||||
"ELEMENTS_REPO_BRANCH" 19 1 "${ELEMENTS_REPO_BRANCH}" 19 35 35 0 \
|
||||
"ELEMENTS_REPO_NAME" 20 1 "${ELEMENTS_REPO_NAME}" 20 35 35 0 \
|
||||
"ELEMENTS_REPO_URL" 21 1 "${ELEMENTS_REPO_URL}" 21 35 35 0 \
|
||||
"MEMPOOL_LATEST_RELEASE" 22 1 "${MEMPOOL_LATEST_RELEASE}" 22 35 35 0 \
|
||||
"MEMPOOL_LIQUID_HTTP_HOST" 23 1 "${MEMPOOL_LIQUID_HTTP_HOST}" 23 35 35 0 \
|
||||
"MEMPOOL_LIQUID_HTTP_PORT" 24 1 "${MEMPOOL_LIQUID_HTTP_PORT}" 24 35 35 0 \
|
||||
"MEMPOOL_MAINNET_HTTP_HOST" 25 1 "${MEMPOOL_MAINNET_HTTP_HOST}" 25 35 35 0 \
|
||||
"MEMPOOL_MAINNET_HTTP_PORT" 26 1 "${MEMPOOL_MAINNET_HTTP_PORT}" 26 35 35 0 \
|
||||
"MEMPOOL_REPO_BRANCH" 27 1 "${MEMPOOL_REPO_BRANCH}" 27 35 35 0 \
|
||||
"MEMPOOL_REPO_NAME" 28 1 "${MEMPOOL_REPO_NAME}" 28 35 35 0 \
|
||||
"MEMPOOL_REPO_URL" 29 1 "${MEMPOOL_REPO_URL}" 29 35 35 0 \
|
||||
"MEMPOOL_TESTNET_HTTP_HOST" 30 1 "${MEMPOOL_TESTNET_HTTP_HOST}" 30 35 35 0 \
|
||||
"MEMPOOL_TESTNET_HTTP_PORT" 31 1 "${MEMPOOL_TESTNET_HTTP_PORT}" 31 35 35 0 \
|
||||
"MEMPOOL_TOR_HS" 32 1 "${MEMPOOL_TOR_HS}" 32 35 35 0 \
|
||||
"HOSTNAME" 33 1 "${HOSTNAME}" 33 35 35 0 \
|
||||
"TOR_INSTALL" 34 1 "${TOR_INSTALL}" 34 35 35 0 \
|
||||
"CERTBOT_INSTALL" 35 1 "${CERTBOT_INSTALL}" 35 35 35 0 \
|
||||
2> $tempfile
|
||||
|
||||
retval=$?
|
||||
|
||||
if [ $retval != $DIALOG_OK ];then
|
||||
echo "Installation aborted."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
############################
|
||||
# START DOING ACTUAL STUFF #
|
||||
@@ -846,6 +841,8 @@ ext4CreateDir()
|
||||
date
|
||||
echo "[*] Mempool installation script for ${OS}"
|
||||
|
||||
set -x
|
||||
|
||||
###################################
|
||||
# create filesystems if necessary #
|
||||
###################################
|
||||
@@ -909,7 +906,7 @@ echo "[*] Installing nvm.sh from GitHub"
|
||||
osSudo "${MEMPOOL_USER}" sh -c 'curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | zsh'
|
||||
|
||||
echo "[*] Building NodeJS via nvm.sh"
|
||||
osSudo "${MEMPOOL_USER}" zsh -c 'source ~/.zshrc ; nvm install v16.16.0 --shared-zlib'
|
||||
osSudo "${MEMPOOL_USER}" zsh -c 'source ~/.zshrc ; nvm install v16.15.0'
|
||||
|
||||
####################
|
||||
# Tor installation #
|
||||
@@ -928,8 +925,6 @@ if [ "${TOR_INSTALL}" = ON ];then
|
||||
osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServiceDir ${TOR_RESOURCES}/${MEMPOOL_TOR_HS}/ >> ${TOR_CONFIGURATION}"
|
||||
osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServicePort 80 127.0.0.1:81 >> ${TOR_CONFIGURATION}"
|
||||
osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServiceVersion 3 >> ${TOR_CONFIGURATION}"
|
||||
else
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s!__TOR_RESOURCES__!${TOR_RESOURCES}!" "${TOR_CONFIGURATION}"
|
||||
fi
|
||||
|
||||
case $OS in
|
||||
@@ -941,7 +936,7 @@ if [ "${TOR_INSTALL}" = ON ];then
|
||||
|
||||
# start tor now so it can bootstrap in time for bitcoin starting a few mins later
|
||||
echo "[*] Starting Tor service"
|
||||
osSudo "${ROOT_USER}" service tor restart
|
||||
osSudo "${ROOT_USER}" service tor start
|
||||
fi
|
||||
|
||||
########################
|
||||
@@ -1287,25 +1282,7 @@ if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then
|
||||
;;
|
||||
|
||||
Debian)
|
||||
osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/linux/elements-liquid.service" "${DEBIAN_SERVICE_HOME}"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
#######################################
|
||||
# Bitcoin instance for Liquid Testnet #
|
||||
#######################################
|
||||
|
||||
if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then
|
||||
echo "[*] Installing Bitcoin Liquid service"
|
||||
case $OS in
|
||||
|
||||
FreeBSD)
|
||||
echo "[*] FIXME: Bitcoin Liquid service must be installed manually on FreeBSD"
|
||||
;;
|
||||
|
||||
Debian)
|
||||
osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/linux/elements-liquidtestnet.service" "${DEBIAN_SERVICE_HOME}"
|
||||
osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/linux/liquid.service" "${DEBIAN_SERVICE_HOME}"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
@@ -1318,6 +1295,18 @@ if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then
|
||||
echo "[*] Installing Bitcoin Mainnet electrs start script"
|
||||
osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-mainnet" "${BITCOIN_ELECTRS_HOME}"
|
||||
|
||||
echo "[*] Installing Bitcoin crontab"
|
||||
case $OS in
|
||||
FreeBSD)
|
||||
echo [*] FIXME: must only crontab enabled daemons
|
||||
osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/bitcoin.crontab"
|
||||
osSudo "${ROOT_USER}" crontab -u "${MINFEE_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/minfee.crontab"
|
||||
;;
|
||||
Debian)
|
||||
(crontab -l ; echo "@reboot sleep 30 ; screen -dmS mainnet /bitcoin/electrs/electrs-start-mainnet") | osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" -
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "[*] Configuring Bitcoin Mainnet RPC credentials in electrs start script"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-mainnet"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-mainnet"
|
||||
@@ -1332,6 +1321,13 @@ if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then
|
||||
echo "[*] Installing Bitcoin Testnet electrs start script"
|
||||
osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-testnet" "${BITCOIN_ELECTRS_HOME}"
|
||||
|
||||
case $OS in
|
||||
Debian)
|
||||
echo "[*] Installing Bitcoin-testnet crontab"
|
||||
(crontab -l ; echo "@reboot sleep 70 ; screen -dmS testnet /bitcoin/electrs/electrs-start-testnet") | osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" -
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "[*] Configuring Bitcoin Testnet RPC credentials in electrs start script"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-testnet"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-testnet"
|
||||
@@ -1346,6 +1342,13 @@ if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then
|
||||
echo "[*] Installing Bitcoin Signet electrs start script"
|
||||
osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-signet" "${BITCOIN_ELECTRS_HOME}"
|
||||
|
||||
case $OS in
|
||||
Debian)
|
||||
echo "[*] Installing Bitcoin-signet crontab"
|
||||
(crontab -l ; echo "@reboot sleep 90 ; screen -dmS signet /bitcoin/electrs/electrs-start-signet") | osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" -
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "[*] Configuring Bitcoin Signet RPC credentials in electrs start script"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-signet"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-signet"
|
||||
@@ -1363,9 +1366,12 @@ if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then
|
||||
echo "[*] Installing Elements crontab"
|
||||
case $OS in
|
||||
FreeBSD)
|
||||
echo "[*] FIXME: must only crontab enabled daemons"
|
||||
echo [*] FIXME: must only crontab enabled daemons
|
||||
osSudo "${ROOT_USER}" crontab -u "${ELEMENTS_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/elements.crontab"
|
||||
;;
|
||||
Debian)
|
||||
(crontab -l ; echo "6 * * * * cd $HOME/asset_registry_db && git pull origin master >/dev/null 2>&1") | osSudo "${ROOT_USER}" crontab -u "${ELEMENTS_USER}" -
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "[*] Configuring Elements Liquid RPC credentials in electrs start script"
|
||||
@@ -1382,6 +1388,13 @@ if [ "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then
|
||||
echo "[*] Installing Elements Liquid Testnet electrs start script"
|
||||
osSudo "${ROOT_USER}" install -c -o "${ELEMENTS_USER}" -g "${ELEMENTS_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-liquidtestnet" "${ELEMENTS_ELECTRS_HOME}"
|
||||
|
||||
case $OS in
|
||||
Debian)
|
||||
echo "[*] Installing Elements-testnet crontab"
|
||||
(crontab -l ; echo "6 * * * * cd $HOME/asset_registry_testnet_db && git pull origin master >/dev/null 2>&1") | osSudo "${ROOT_USER}" crontab -u "${ELEMENTS_USER}" -
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "[*] Installing Elements Liquid Testnet RPC credentials"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${ELEMENTS_HOME}/elements.conf"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${ELEMENTS_HOME}/elements.conf"
|
||||
@@ -1394,45 +1407,6 @@ if [ "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s!__ELECTRS_DATA_ROOT__!${ELECTRS_DATA_ROOT}!" "${ELEMENTS_ELECTRS_HOME}/electrs-start-liquidtestnet"
|
||||
fi
|
||||
|
||||
################################
|
||||
# Install all Electrs Cronjobs #
|
||||
################################
|
||||
echo "[*] Installing crontabs"
|
||||
case $OS in
|
||||
FreeBSD)
|
||||
echo "[*] FIXME: must only crontab enabled daemons"
|
||||
osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/bitcoin.crontab"
|
||||
osSudo "${ROOT_USER}" crontab -u "${MINFEE_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/minfee.crontab"
|
||||
;;
|
||||
Debian)
|
||||
crontab_bitcoin=()
|
||||
if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then
|
||||
echo "[*] Installing Electrs Mainnet Cronjob"
|
||||
crontab_bitcoin+="@reboot sleep 30 ; screen -dmS mainnet /bitcoin/electrs/electrs-start-mainnet\n"
|
||||
fi
|
||||
if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then
|
||||
echo "[*] Installing Electrs Testnet Cronjob"
|
||||
crontab_bitcoin+="@reboot sleep 70 ; screen -dmS testnet /bitcoin/electrs/electrs-start-testnet\n"
|
||||
fi
|
||||
if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then
|
||||
echo "[*] Installing Electrs Signet Cronjob"
|
||||
crontab_bitcoin+="@reboot sleep 90 ; screen -dmS signet /bitcoin/electrs/electrs-start-signet\n"
|
||||
fi
|
||||
echo "${crontab_bitcoin}" | crontab -u "${BITCOIN_USER}" -
|
||||
|
||||
crontab_elements=()
|
||||
if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then
|
||||
echo "[*] Installing Liquid Asset Mainnet Cronjob"
|
||||
crontab_elements+="6 * * * * cd $HOME/asset_registry_db && git pull origin master >/dev/null 2>&1\n"
|
||||
fi
|
||||
if [ "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then
|
||||
echo "[*] Installing Liquid Asset Testnet Cronjob"
|
||||
crontab_elements+="6 * * * * cd $HOME/asset_registry_testnet_db && git pull origin master >/dev/null 2>&1\n"
|
||||
fi
|
||||
echo "${crontab_elements}" | crontab -u "${ELEMENTS_USER}" -
|
||||
;;
|
||||
esac
|
||||
|
||||
#####################################
|
||||
# Bisq instance for Bitcoin Mainnet #
|
||||
#####################################
|
||||
@@ -1531,24 +1505,19 @@ _EOF_
|
||||
|
||||
##### nginx
|
||||
|
||||
|
||||
echo "[*] Read tor v3 onion hostnames"
|
||||
NGINX_MEMPOOL_ONION=$(cat "${TOR_RESOURCES}/mempool/hostname")
|
||||
NGINX_BISQ_ONION=$(cat "${TOR_RESOURCES}/bisq/hostname")
|
||||
NGINX_LIQUID_ONION=$(cat "${TOR_RESOURCES}/liquid/hostname")
|
||||
|
||||
echo "[*] Adding Nginx configuration"
|
||||
osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/nginx/nginx.conf" "${NGINX_CONFIGURATION}"
|
||||
mkdir -p /var/cache/nginx/services /var/cache/nginx/api
|
||||
chown ${NGINX_USER}: /var/cache/nginx/services /var/cache/nginx/api
|
||||
ln -s /mempool/mempool /etc/nginx/mempool
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_USER__!${NGINX_USER}!" "${NGINX_CONFIGURATION}"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_ETC_FOLDER__!${NGINX_ETC_FOLDER}!" "${NGINX_CONFIGURATION}"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_MEMPOOL_ONION__!${NGINX_MEMPOOL_ONION%.onion}!" "${NGINX_CONFIGURATION}"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_BISQ_ONION__!${NGINX_BISQ_ONION%.onion}!" "${NGINX_CONFIGURATION}"
|
||||
osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_LIQUID_ONION__!${NGINX_LIQUID_ONIONi%.onion}!" "${NGINX_CONFIGURATION}"
|
||||
echo "[*] Restarting Nginx"
|
||||
osSudo "${ROOT_USER}" service nginx restart
|
||||
case $OS in
|
||||
|
||||
FreeBSD)
|
||||
echo "[*] FIXME: nginx must be configured manually on FreeBSD"
|
||||
;;
|
||||
|
||||
Debian)
|
||||
osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/nginx/nginx.conf" "${NGINX_CONFIGURATION}"
|
||||
#echo "[*] Restarting Nginx"
|
||||
#osSudo "${ROOT_USER}" service nginx restart
|
||||
;;
|
||||
esac
|
||||
|
||||
##### OS systemd
|
||||
|
||||
@@ -1579,40 +1548,11 @@ case $OS in
|
||||
osSudo "${ROOT_USER}" systemctl enable bisq.service
|
||||
fi
|
||||
if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then
|
||||
osSudo "${ROOT_USER}" systemctl enable elements-liquid.service
|
||||
fi
|
||||
if [ "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then
|
||||
osSudo "${ROOT_USER}" systemctl enable elements-liquidtestnet.service
|
||||
osSudo "${ROOT_USER}" systemctl enable liquid.service
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
##### OS set Linux user ulimits
|
||||
|
||||
echo "[*] Setting ulimits for users"
|
||||
case $OS in
|
||||
|
||||
FreeBSD)
|
||||
;;
|
||||
|
||||
Debian)
|
||||
cat >> /etc/security/limits.conf <<EOF
|
||||
* soft nproc 200000
|
||||
* hard nproc 200000
|
||||
* soft nofile 200000
|
||||
* hard nofile 200000
|
||||
EOF
|
||||
echo "session required pam_limits.so" >> /etc/pam.d/common-session
|
||||
;;
|
||||
esac
|
||||
|
||||
##### Build Mempool
|
||||
|
||||
echo "[*] Build Mempool"
|
||||
osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME} && ./upgrade"
|
||||
|
||||
|
||||
|
||||
##### OS services
|
||||
|
||||
#if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then
|
||||
@@ -1688,21 +1628,6 @@ osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME} && ./upgrade"
|
||||
|
||||
##### finish
|
||||
|
||||
case $OS in
|
||||
|
||||
FreeBSD)
|
||||
;;
|
||||
|
||||
Debian)
|
||||
echo "This are the generated Tor addresses:"
|
||||
echo "${NGINX_MEMPOOL_ONION}"
|
||||
echo "${NGINX_BISQ_ONION}"
|
||||
echo "${NGINX_LIQUID_ONION}"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo
|
||||
echo 'Please reboot to start all the services.'
|
||||
echo '[*] Done!'
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
[Unit]
|
||||
Description=Elementsd-liquid
|
||||
Description=Elementsd
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/bin/elementsd -daemon -printtoconsole -chain=liquidv1 -pid=/elements/elements-liquid.pid
|
||||
ExecStart=/usr/local/bin/elementsd -daemon -printtoconsole -chain=liquidtestnet -pid=/elements/elements-testnet.pid
|
||||
ExecStop=/usr/local/bin/elements-cli stop
|
||||
|
||||
Type=forking
|
||||
PIDFile=/elements/elements-liquid.pid
|
||||
PIDFile=/elements/elements-testnet.pid
|
||||
Restart=on-failure
|
||||
|
||||
User=elements
|
||||
@@ -1,13 +1,13 @@
|
||||
[Unit]
|
||||
Description=Elementsd-liquidtestnet
|
||||
Description=Elementsd
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/bin/elementsd -daemon -printtoconsole -chain=liquidtestnet -pid=/elements/elements-liquidtestnet.pid
|
||||
ExecStart=/usr/local/bin/elementsd -daemon -printtoconsole -chain=liquidv1 -pid=/elements/elements.pid
|
||||
ExecStop=/usr/local/bin/elements-cli stop
|
||||
|
||||
Type=forking
|
||||
PIDFile=/elements/elements-liquidtestnet.pid
|
||||
PIDFile=/elements/elements.pid
|
||||
Restart=on-failure
|
||||
|
||||
User=elements
|
||||
@@ -56,7 +56,7 @@ build_frontend()
|
||||
if [ ! -e "mempool-frontend-config.json" ];then
|
||||
cp "${HOME}/mempool/production/mempool-frontend-config.${site}.json" "mempool-frontend-config.json"
|
||||
fi
|
||||
npm install --omit=dev --omit=optional || exit 1
|
||||
npm install --no-optional || exit 1
|
||||
npm run build || exit 1
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ build_backend()
|
||||
-e "s!__ELEMENTS_RPC_PASS__!${ELEMENTS_RPC_PASS}!" \
|
||||
"mempool-config.json"
|
||||
fi
|
||||
npm install --omit=dev --omit=optional || exit 1
|
||||
npm install --no-optional || exit 1
|
||||
npm run build || exit 1
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
user __NGINX_USER__;
|
||||
user nobody;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
worker_processes auto;
|
||||
@@ -10,11 +10,11 @@ events {
|
||||
}
|
||||
|
||||
http {
|
||||
# DNS servers for on-demand resolution, change if desired
|
||||
# DNS servers for on-demand recursive resolver
|
||||
resolver 8.8.8.8;
|
||||
|
||||
# include default mime types
|
||||
include __NGINX_ETC_FOLDER__/mime.types;
|
||||
include /usr/local/etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# HTTP basic configuration
|
||||
@@ -32,13 +32,9 @@ http {
|
||||
# MEMPOOL.NINJA
|
||||
server {
|
||||
# clearnet v4/v6
|
||||
#listen 443 ssl http2;
|
||||
#listen [::]:443 ssl http2;
|
||||
server_name _;
|
||||
|
||||
# tor v3
|
||||
listen 127.0.0.1:81;
|
||||
set $onion "__NGINX_MEMPOOL_ONION__";
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name mempool.ninja;
|
||||
|
||||
# for services from mempool.space like contributors on about page
|
||||
set $mempoolSpaceServices "https://mempool.space";
|
||||
@@ -56,24 +52,30 @@ http {
|
||||
set $esploraTestnet "http://esplora-bitcoin-testnet";
|
||||
set $esploraSignet "http://esplora-bitcoin-signet";
|
||||
|
||||
# tor v3
|
||||
listen 127.0.0.1:81;
|
||||
set $onion "mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad";
|
||||
|
||||
# filesystem paths
|
||||
root /mempool/public_html/mainnet/;
|
||||
access_log /var/log/nginx/mempool-access.log;
|
||||
error_log /var/log/nginx/mempool-error.log;
|
||||
|
||||
# ssl configuration
|
||||
ssl_certificate /usr/local/etc/letsencrypt/live/mempool.ninja/fullchain.pem;
|
||||
ssl_certificate_key /usr/local/etc/letsencrypt/live/mempool.ninja/privkey.pem;
|
||||
include /usr/local/etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /usr/local/etc/letsencrypt/ssl-dhparams.pem;
|
||||
|
||||
# site configuration
|
||||
include mempool/production/nginx/server-mempool.conf;
|
||||
}
|
||||
# BISQ.NINJA
|
||||
server {
|
||||
# clearnet v4/v6
|
||||
#listen 443 ssl http2;
|
||||
#listen [::]:443 ssl http2;
|
||||
server_name _;
|
||||
|
||||
# tor v3
|
||||
listen 127.0.0.1:82;
|
||||
set $onion "__NGINX_BISQ_ONION__";
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name bisq.ninja;
|
||||
|
||||
# for services from mempool.space like contributors on about page
|
||||
set $mempoolSpaceServices "https://mempool.space";
|
||||
@@ -84,24 +86,30 @@ http {
|
||||
# for blockstream/esplora daemon, see upstream-esplora.conf
|
||||
set $esploraMainnet "http://esplora-bitcoin-mainnet";
|
||||
|
||||
# tor v3
|
||||
listen 127.0.0.1:82;
|
||||
set $onion "bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd";
|
||||
|
||||
# filesystem paths
|
||||
root /mempool/public_html/bisq/;
|
||||
access_log /var/log/nginx/bisq-access.log;
|
||||
error_log /var/log/nginx/bisq-error.log;
|
||||
|
||||
# ssl configuration
|
||||
ssl_certificate /usr/local/etc/letsencrypt/live/bisq.ninja/fullchain.pem;
|
||||
ssl_certificate_key /usr/local/etc/letsencrypt/live/bisq.ninja/privkey.pem;
|
||||
include /usr/local/etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /usr/local/etc/letsencrypt/ssl-dhparams.pem;
|
||||
|
||||
# site configuration
|
||||
include mempool/production/nginx/server-bisq.conf;
|
||||
}
|
||||
# LIQUID.PLACE
|
||||
server {
|
||||
# clearnet v4/v6
|
||||
#listen 443 ssl http2;
|
||||
#listen [::]:443 ssl http2;
|
||||
server_name _;
|
||||
|
||||
# tor v3
|
||||
listen 127.0.0.1:83;
|
||||
set $onion "__NGINX_LIQUID_ONION__";
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name liquid.place;
|
||||
|
||||
# for services from mempool.space like contributors on about page
|
||||
set $mempoolSpaceServices "https://mempool.space";
|
||||
@@ -114,12 +122,36 @@ http {
|
||||
set $esploraMainnet "http://esplora-liquid-mainnet";
|
||||
set $esploraTestnet "http://esplora-liquid-testnet";
|
||||
|
||||
# tor v3
|
||||
listen 127.0.0.1:83;
|
||||
set $onion "liquidmom47f6s3m53ebfxn47p76a6tlnxib3wp6deux7wuzotdr6cyd";
|
||||
|
||||
# filesystem paths
|
||||
root /mempool/public_html/liquid/;
|
||||
access_log /var/log/nginx/liquid-access.log;
|
||||
error_log /var/log/nginx/liquid-error.log;
|
||||
|
||||
# ssl configuration
|
||||
ssl_certificate /usr/local/etc/letsencrypt/live/liquid.place/fullchain.pem;
|
||||
ssl_certificate_key /usr/local/etc/letsencrypt/live/liquid.place/privkey.pem;
|
||||
include /usr/local/etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /usr/local/etc/letsencrypt/ssl-dhparams.pem;
|
||||
|
||||
# site configuration
|
||||
include mempool/production/nginx/server-liquid.conf;
|
||||
}
|
||||
# HTTP to HTTPS redirect
|
||||
server {
|
||||
# clearnet v4/v6
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name _;
|
||||
|
||||
# only redirect for our hosted domains
|
||||
if ($host ~ "^(mempool.ninja|bisq.ninja|liquid.place)$")
|
||||
{
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
return 503;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,21 +3,21 @@ SOCKSPort 9050
|
||||
ControlPort 9051
|
||||
Log notice syslog
|
||||
|
||||
DataDirectory __TOR_RESOURCES__
|
||||
DataDirectory /var/db/tor
|
||||
DataDirectoryGroupReadable 1
|
||||
|
||||
CookieAuthentication 1
|
||||
CookieAuthFile __TOR_RESOURCES__/control_auth_cookie
|
||||
CookieAuthFile /var/db/tor/control_auth_cookie
|
||||
CookieAuthFileGroupReadable 1
|
||||
|
||||
HiddenServiceDir __TOR_RESOURCES__/mempool
|
||||
HiddenServiceDir /var/db/tor/mempool
|
||||
HiddenServicePort 80 127.0.0.1:81
|
||||
HiddenServiceVersion 3
|
||||
|
||||
HiddenServiceDir __TOR_RESOURCES__/bisq
|
||||
HiddenServiceDir /var/db/tor/bisq
|
||||
HiddenServicePort 80 127.0.0.1:82
|
||||
HiddenServiceVersion 3
|
||||
|
||||
HiddenServiceDir __TOR_RESOURCES__/liquid
|
||||
HiddenServiceDir /var/db/tor/liquid
|
||||
HiddenServicePort 80 127.0.0.1:83
|
||||
HiddenServiceVersion 3
|
||||
|
||||
Reference in New Issue
Block a user