Merge remote-tracking branch 'origin/master' into natsoni/decode-tx

This commit is contained in:
natsoni 2025-02-09 18:08:00 +01:00
commit 4e45b55c3a
No known key found for this signature in database
GPG Key ID: C65917583181743B
101 changed files with 4680 additions and 742 deletions

View File

@ -251,17 +251,7 @@ jobs:
strategy:
fail-fast: false
matrix:
module: ["mempool", "liquid"]
include:
- module: "mempool"
spec: |
cypress/e2e/mainnet/*.spec.ts
cypress/e2e/signet/*.spec.ts
cypress/e2e/testnet4/*.spec.ts
- module: "liquid"
spec: |
cypress/e2e/liquid/liquid.spec.ts
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
module: ["mempool", "liquid", "testnet4"]
name: E2E tests for ${{ matrix.module }}
steps:
@ -310,8 +300,10 @@ jobs:
- name: Unzip assets before building (src/resources)
run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video
# mempool
- name: Chrome browser tests (${{ matrix.module }})
if: ${{ matrix.module == 'mempool' }}
uses: cypress-io/github-action@v5
with:
tag: ${{ github.event_name }}
@ -322,7 +314,9 @@ jobs:
wait-on-timeout: 120
record: true
parallel: true
spec: ${{ matrix.spec }}
spec: |
cypress/e2e/mainnet/*.spec.ts
cypress/e2e/signet/*.spec.ts
group: Tests on Chrome (${{ matrix.module }})
browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
@ -332,6 +326,56 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
# liquid
- name: Chrome browser tests (${{ matrix.module }})
if: ${{ matrix.module == 'liquid' }}
uses: cypress-io/github-action@v5
with:
tag: ${{ github.event_name }}
working-directory: ${{ matrix.module }}/frontend
build: npm run config:defaults:${{ matrix.module }}
start: npm run start:local-staging
wait-on: "http://localhost:4200"
wait-on-timeout: 120
record: true
parallel: true
spec: |
cypress/e2e/liquid/liquid.spec.ts
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
group: Tests on Chrome (${{ matrix.module }})
browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
env:
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
# testnet
- name: Chrome browser tests (${{ matrix.module }})
if: ${{ matrix.module == 'testnet4' }}
uses: cypress-io/github-action@v5
with:
tag: ${{ github.event_name }}
working-directory: ${{ matrix.module }}/frontend
build: npm run config:defaults:mempool
start: npm run start:local-staging
wait-on: "http://localhost:4200"
wait-on-timeout: 120
record: true
parallel: true
spec: |
cypress/e2e/testnet4/*.spec.ts
group: Tests on Chrome (${{ matrix.module }})
browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
env:
CYPRESS_REROUTE_TESTNET: true
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
validate_docker_json:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
runs-on: "ubuntu-latest"
@ -359,4 +403,4 @@ jobs:
- name: Validate JSON syntax
run: |
cat mempool-config.json | jq
working-directory: docker/docker/backend
working-directory: docker/docker/backend

View File

@ -7,7 +7,7 @@ const config: Config.InitialOptions = {
automock: false,
collectCoverage: true,
collectCoverageFrom: ["./src/**/**.ts"],
coverageProvider: "babel",
coverageProvider: "v8",
coverageThreshold: {
global: {
lines: 1

View File

@ -155,6 +155,10 @@
"API": "https://mempool.space/api/v1/services",
"ACCELERATIONS": false
},
"STRATUM": {
"ENABLED": false,
"API": "http://localhost:1234"
},
"FIAT_PRICE": {
"ENABLED": true,
"PAID": false,

View File

@ -10,7 +10,6 @@
"hasInstallScript": true,
"license": "GNU Affero General Public License v3.0",
"dependencies": {
"@babel/core": "^7.25.2",
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "1.7.2",
@ -18,7 +17,7 @@
"crypto-js": "~4.2.0",
"express": "~4.21.1",
"maxmind": "~4.3.11",
"mysql2": "~3.11.0",
"mysql2": "~3.12.0",
"redis": "^4.7.0",
"rust-gbt": "file:./rust-gbt",
"socks-proxy-agent": "~7.0.0",
@ -26,8 +25,6 @@
"ws": "~8.18.0"
},
"devDependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/core": "^7.25.2",
"@types/compression": "^1.7.2",
"@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.17",
@ -6000,6 +5997,21 @@
"yallist": "^3.0.2"
}
},
"node_modules/lru.min": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.1.tgz",
"integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==",
"license": "MIT",
"engines": {
"bun": ">=1.0.0",
"deno": ">=1.30.0",
"node": ">=8.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wellwelwel"
}
},
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@ -6161,16 +6173,17 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/mysql2": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.0.tgz",
"integrity": "sha512-J9phbsXGvTOcRVPR95YedzVSxJecpW5A5+cQ57rhHIFXteTP10HCs+VBjS7DHIKfEaI1zQ5tlVrquCd64A6YvA==",
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz",
"integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==",
"license": "MIT",
"dependencies": {
"aws-ssl-profiles": "^1.1.1",
"denque": "^2.1.0",
"generate-function": "^2.3.1",
"iconv-lite": "^0.6.3",
"long": "^5.2.1",
"lru-cache": "^8.0.0",
"lru.min": "^1.0.0",
"named-placeholders": "^1.1.3",
"seq-queue": "^0.0.5",
"sqlstring": "^2.3.2"
@ -6190,14 +6203,6 @@
"node": ">=0.10.0"
}
},
"node_modules/mysql2/node_modules/lru-cache": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz",
"integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==",
"engines": {
"node": ">=16.14"
}
},
"node_modules/named-placeholders": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz",
@ -12213,6 +12218,11 @@
"yallist": "^3.0.2"
}
},
"lru.min": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.1.tgz",
"integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q=="
},
"make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@ -12327,16 +12337,16 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"mysql2": {
"version": "3.11.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.0.tgz",
"integrity": "sha512-J9phbsXGvTOcRVPR95YedzVSxJecpW5A5+cQ57rhHIFXteTP10HCs+VBjS7DHIKfEaI1zQ5tlVrquCd64A6YvA==",
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz",
"integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==",
"requires": {
"aws-ssl-profiles": "^1.1.1",
"denque": "^2.1.0",
"generate-function": "^2.3.1",
"iconv-lite": "^0.6.3",
"long": "^5.2.1",
"lru-cache": "^8.0.0",
"lru.min": "^1.0.0",
"named-placeholders": "^1.1.3",
"seq-queue": "^0.0.5",
"sqlstring": "^2.3.2"
@ -12349,11 +12359,6 @@
"requires": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
}
},
"lru-cache": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz",
"integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA=="
}
}
},

View File

@ -39,7 +39,6 @@
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
},
"dependencies": {
"@babel/core": "^7.25.2",
"@mempool/electrum-client": "1.1.9",
"@types/node": "^18.15.3",
"axios": "1.7.2",
@ -47,7 +46,7 @@
"crypto-js": "~4.2.0",
"express": "~4.21.1",
"maxmind": "~4.3.11",
"mysql2": "~3.11.0",
"mysql2": "~3.12.0",
"rust-gbt": "file:./rust-gbt",
"redis": "^4.7.0",
"socks-proxy-agent": "~7.0.0",
@ -55,8 +54,6 @@
"ws": "~8.18.0"
},
"devDependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/core": "^7.25.2",
"@types/compression": "^1.7.2",
"@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.17",

View File

@ -151,5 +151,9 @@
"ENABLED": true,
"PAID": false,
"API_KEY": "__MEMPOOL_CURRENCY_API_KEY__"
},
"STRATUM": {
"ENABLED": false,
"API": "http://localhost:1234"
}
}

View File

@ -159,6 +159,11 @@ describe('Mempool Backend Config', () => {
PAID: false,
API_KEY: '',
});
expect(config.STRATUM).toStrictEqual({
ENABLED: false,
API: 'http://localhost:1234',
});
});
});

View File

@ -21,6 +21,7 @@ import transactionRepository from '../../repositories/TransactionRepository';
import rbfCache from '../rbf-cache';
import { calculateMempoolTxCpfp } from '../cpfp';
import { handleError } from '../../utils/api';
import poolsUpdater from '../../tasks/pools-updater';
const TXID_REGEX = /^[a-f0-9]{64}$/i;
const BLOCK_HASH_REGEX = /^[a-f0-9]{64}$/i;
@ -58,6 +59,10 @@ class BitcoinRoutes {
.post(config.MEMPOOL.API_URL_PREFIX + 'cpfp', this.getCpfpLocalTxs)
// Temporarily add txs/package endpoint for all backends until esplora supports it
.post(config.MEMPOOL.API_URL_PREFIX + 'txs/package', this.$submitPackage)
// Internal routes
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/blocks/definition/list', this.getBlockDefinitionHashes)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/blocks/definition/current', this.getCurrentBlockDefinitionHash)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/blocks/:definitionHash', this.getBlocksByDefinitionHash)
;
if (config.MEMPOOL.BACKEND !== 'esplora') {
@ -741,6 +746,52 @@ class BitcoinRoutes {
}
}
private async getBlockDefinitionHashes(req: Request, res: Response): Promise<void> {
try {
const result = await blocks.$getBlockDefinitionHashes();
if (!result) {
handleError(req, res, 503, `Service Temporarily Unavailable`);
return;
}
res.setHeader('content-type', 'application/json');
res.send(result);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getCurrentBlockDefinitionHash(req: Request, res: Response): Promise<void> {
try {
const currentSha = await poolsUpdater.getShaFromDb();
if (!currentSha) {
handleError(req, res, 503, `Service Temporarily Unavailable`);
return;
}
res.setHeader('content-type', 'text/plain');
res.send(currentSha);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private async getBlocksByDefinitionHash(req: Request, res: Response): Promise<void> {
try {
if (typeof(req.params.definitionHash) !== 'string') {
res.status(400).send('Parameter "hash" must be a valid string');
return;
}
const blocksHash = await blocks.$getBlocksByDefinitionHash(req.params.definitionHash as string);
if (!blocksHash) {
handleError(req, res, 503, `Service Temporarily Unavailable`);
return;
}
res.setHeader('content-type', 'application/json');
res.send(blocksHash);
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
}
}
private getBlockTipHeight(req: Request, res: Response) {
try {
const result = blocks.getCurrentBlockHeight();

View File

@ -33,8 +33,8 @@ import AccelerationRepository from '../repositories/AccelerationRepository';
import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp';
import mempool from './mempool';
import CpfpRepository from '../repositories/CpfpRepository';
import accelerationApi from './services/acceleration';
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
import database from '../database';
class Blocks {
private blocks: BlockExtended[] = [];
@ -1462,6 +1462,36 @@ class Blocks {
// not a fatal error, we'll try again next time the indexer runs
}
}
public async $getBlockDefinitionHashes(): Promise<string[] | null> {
try {
const [rows]: any = await database.query(`SELECT DISTINCT(definition_hash) FROM blocks`);
if (rows && Array.isArray(rows)) {
return rows.map(r => r.definition_hash);
} else {
logger.debug(`Unable to retreive list of blocks.definition_hash from db (no result)`);
return null;
}
} catch (e) {
logger.debug(`Unable to retreive list of blocks.definition_hash from db (exception: ${e})`);
return null;
}
}
public async $getBlocksByDefinitionHash(definitionHash: string): Promise<string[] | null> {
try {
const [rows]: any = await database.query(`SELECT hash FROM blocks WHERE definition_hash = ?`, [definitionHash]);
if (rows && Array.isArray(rows)) {
return rows.map(r => r.hash);
} else {
logger.debug(`Unable to retreive list of blocks for definition hash ${definitionHash} from db (no result)`);
return null;
}
} catch (e) {
logger.debug(`Unable to retreive list of blocks for definition hash ${definitionHash} from db (exception: ${e})`);
return null;
}
}
}
export default new Blocks();

View File

@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
private static currentVersion = 94;
private static currentVersion = 95;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@ -1118,6 +1118,18 @@ class DatabaseMigration {
}
await this.updateToSchemaVersion(94);
}
// blocks pools-v2.json hash
if (databaseSchemaVersion < 95) {
let poolJsonSha = 'f737d86571d190cf1a1a3cf5fd86b33ba9624254';
const [poolJsonShaDb]: any[] = await DB.query(`SELECT string FROM state WHERE name = 'pools_json_sha'`);
if (poolJsonShaDb?.length > 0) {
poolJsonSha = poolJsonShaDb[0].string;
}
await this.$executeQuery(`ALTER TABLE blocks ADD definition_hash varchar(255) NOT NULL DEFAULT "${poolJsonSha}"`);
await this.$executeQuery('ALTER TABLE blocks ADD INDEX `definition_hash` (`definition_hash`)');
await this.updateToSchemaVersion(95);
}
}
/**

View File

@ -19,15 +19,6 @@ class PoolsParser {
'addresses': '[]',
'slug': 'unknown'
};
private uniqueLogs: string[] = [];
private uniqueLog(loggerFunction: any, msg: string): void {
if (this.uniqueLogs.includes(msg)) {
return;
}
this.uniqueLogs.push(msg);
loggerFunction(msg);
}
public setMiningPools(pools): void {
for (const pool of pools) {

View File

@ -119,7 +119,11 @@ class RbfCache {
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
if ( !newTxExtended
|| !replaced?.length
|| this.txs.has(newTxExtended.txid)
|| !(replaced.some(tx => !this.replacedBy.has(tx.txid)))
) {
return;
}

View File

@ -0,0 +1,105 @@
import { WebSocket } from 'ws';
import logger from '../../logger';
import config from '../../config';
import websocketHandler from '../websocket-handler';
export interface StratumJob {
pool: number;
height: number;
coinbase: string;
scriptsig: string;
reward: number;
jobId: string;
extraNonce: string;
extraNonce2Size: number;
prevHash: string;
coinbase1: string;
coinbase2: string;
merkleBranches: string[];
version: string;
bits: string;
time: string;
timestamp: number;
cleanJobs: boolean;
received: number;
}
function isStratumJob(obj: any): obj is StratumJob {
return obj
&& typeof obj === 'object'
&& 'pool' in obj
&& 'prevHash' in obj
&& 'height' in obj
&& 'received' in obj
&& 'version' in obj
&& 'timestamp' in obj
&& 'bits' in obj
&& 'merkleBranches' in obj
&& 'cleanJobs' in obj;
}
class StratumApi {
private ws: WebSocket | null = null;
private runWebsocketLoop: boolean = false;
private startedWebsocketLoop: boolean = false;
private websocketConnected: boolean = false;
private jobs: Record<string, StratumJob> = {};
public constructor() {}
public getJobs(): Record<string, StratumJob> {
return this.jobs;
}
private handleWebsocketMessage(msg: any): void {
if (isStratumJob(msg)) {
this.jobs[msg.pool] = msg;
websocketHandler.handleNewStratumJob(this.jobs[msg.pool]);
}
}
public async connectWebsocket(): Promise<void> {
if (!config.STRATUM.ENABLED) {
return;
}
this.runWebsocketLoop = true;
if (this.startedWebsocketLoop) {
return;
}
while (this.runWebsocketLoop) {
this.startedWebsocketLoop = true;
if (!this.ws) {
this.ws = new WebSocket(`${config.STRATUM.API}`);
this.websocketConnected = true;
this.ws.on('open', () => {
logger.info('Stratum websocket opened');
});
this.ws.on('error', (error) => {
logger.err('Stratum websocket error: ' + error);
this.ws = null;
this.websocketConnected = false;
});
this.ws.on('close', () => {
logger.info('Stratum websocket closed');
this.ws = null;
this.websocketConnected = false;
});
this.ws.on('message', (data, isBinary) => {
try {
const parsedMsg = JSON.parse((isBinary ? data : data.toString()) as string);
this.handleWebsocketMessage(parsedMsg);
} catch (e) {
logger.warn('Failed to parse stratum websocket message: ' + (e instanceof Error ? e.message : e));
}
});
}
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
}
export default new StratumApi();

View File

@ -38,6 +38,7 @@ interface AddressTransactions {
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import { calculateMempoolTxCpfp } from './cpfp';
import { getRecentFirstSeen } from '../utils/file-read';
import stratumApi, { StratumJob } from './services/stratum';
// valid 'want' subscriptions
const wantable = [
@ -403,6 +404,16 @@ class WebsocketHandler {
delete client['track-mempool'];
}
if (parsedMessage && parsedMessage['track-stratum'] != null) {
if (parsedMessage['track-stratum']) {
const sub = parsedMessage['track-stratum'];
client['track-stratum'] = sub;
response['stratumJobs'] = this.socketData['stratumJobs'];
} else {
client['track-stratum'] = false;
}
}
if (Object.keys(response).length) {
client.send(this.serializeResponse(response));
}
@ -1384,6 +1395,23 @@ class WebsocketHandler {
await statistics.runStatistics();
}
public handleNewStratumJob(job: StratumJob): void {
this.updateSocketDataFields({ 'stratumJobs': stratumApi.getJobs() });
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
if (client['track-stratum'] && (client['track-stratum'] === 'all' || client['track-stratum'] === job.pool)) {
client.send(JSON.stringify({
'stratumJob': job
}));
}
});
}
}
// takes a dictionary of JSON serialized values
// and zips it together into a valid JSON object
private serializeResponse(response): string {

View File

@ -165,6 +165,10 @@ interface IConfig {
WALLETS: {
ENABLED: boolean;
WALLETS: string[];
},
STRATUM: {
ENABLED: boolean;
API: string;
}
}
@ -332,6 +336,10 @@ const defaults: IConfig = {
'ENABLED': false,
'WALLETS': [],
},
'STRATUM': {
'ENABLED': false,
'API': 'http://localhost:1234',
}
};
class Config implements IConfig {
@ -354,6 +362,7 @@ class Config implements IConfig {
REDIS: IConfig['REDIS'];
FIAT_PRICE: IConfig['FIAT_PRICE'];
WALLETS: IConfig['WALLETS'];
STRATUM: IConfig['STRATUM'];
constructor() {
const configs = this.merge(configFromFile, defaults);
@ -376,6 +385,7 @@ class Config implements IConfig {
this.REDIS = configs.REDIS;
this.FIAT_PRICE = configs.FIAT_PRICE;
this.WALLETS = configs.WALLETS;
this.STRATUM = configs.STRATUM;
}
merge = (...objects: object[]): IConfig => {

View File

@ -48,6 +48,7 @@ import accelerationRoutes from './api/acceleration/acceleration.routes';
import aboutRoutes from './api/about.routes';
import mempoolBlocks from './api/mempool-blocks';
import walletApi from './api/services/wallets';
import stratumApi from './api/services/stratum';
class Server {
private wss: WebSocket.Server | undefined;
@ -320,6 +321,9 @@ class Server {
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
accelerationApi.connectWebsocket();
if (config.STRATUM.ENABLED) {
stratumApi.connectWebsocket();
}
}
setUpHttpApiRoutes(): void {

View File

@ -325,6 +325,8 @@ export interface BlockExtension {
// Requires coinstatsindex, will be set to NULL otherwise
utxoSetSize: number | null;
totalInputAmt: number | null;
// pools-v2.json git hash
definitionHash: string | undefined;
}
/**

View File

@ -15,6 +15,7 @@ import blocks from '../api/blocks';
import BlocksAuditsRepository from './BlocksAuditsRepository';
import transactionUtils from '../api/transaction-utils';
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
import poolsUpdater from '../tasks/pools-updater';
interface DatabaseBlock {
id: string;
@ -114,16 +115,16 @@ class BlocksRepository {
try {
const query = `INSERT INTO blocks(
height, hash, blockTimestamp, size,
weight, tx_count, coinbase_raw, difficulty,
pool_id, fees, fee_span, median_fee,
reward, version, bits, nonce,
merkle_root, previous_block_hash, avg_fee, avg_fee_rate,
median_timestamp, header, coinbase_address, coinbase_addresses,
coinbase_signature, utxoset_size, utxoset_change, avg_tx_size,
total_inputs, total_outputs, total_input_amt, total_output_amt,
fee_percentiles, segwit_total_txs, segwit_total_size, segwit_total_weight,
median_fee_amt, coinbase_signature_ascii
height, hash, blockTimestamp, size,
weight, tx_count, coinbase_raw, difficulty,
pool_id, fees, fee_span, median_fee,
reward, version, bits, nonce,
merkle_root, previous_block_hash, avg_fee, avg_fee_rate,
median_timestamp, header, coinbase_address, coinbase_addresses,
coinbase_signature, utxoset_size, utxoset_change, avg_tx_size,
total_inputs, total_outputs, total_input_amt, total_output_amt,
fee_percentiles, segwit_total_txs, segwit_total_size, segwit_total_weight,
median_fee_amt, coinbase_signature_ascii, definition_hash
) VALUE (
?, ?, FROM_UNIXTIME(?), ?,
?, ?, ?, ?,
@ -134,7 +135,7 @@ class BlocksRepository {
?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?, ?,
?, ?
?, ?, ?
)`;
const poolDbId = await PoolsRepository.$getPoolByUniqueId(block.extras.pool.id);
@ -181,6 +182,7 @@ class BlocksRepository {
block.extras.segwitTotalWeight,
block.extras.medianFeeAmt,
truncatedCoinbaseSignatureAscii,
poolsUpdater.currentSha
];
await DB.query(query, params);
@ -1013,9 +1015,9 @@ class BlocksRepository {
public async $savePool(id: string, poolId: number): Promise<void> {
try {
await DB.query(`
UPDATE blocks SET pool_id = ?
UPDATE blocks SET pool_id = ?, definition_hash = ?
WHERE hash = ?`,
[poolId, id]
[poolId, poolsUpdater.currentSha, id]
);
} catch (e) {
logger.err(`Cannot update block pool. Reason: ` + (e instanceof Error ? e.message : e));

View File

@ -88,8 +88,8 @@ class PoolsUpdater {
try {
await DB.query('START TRANSACTION;');
await poolsParser.migratePoolsJson();
await this.updateDBSha(githubSha);
await poolsParser.migratePoolsJson();
await DB.query('COMMIT;');
} catch (e) {
logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, this.tag);
@ -121,7 +121,7 @@ class PoolsUpdater {
/**
* Fetch our latest pools-v2.json sha from the db
*/
private async getShaFromDb(): Promise<string | null> {
public async getShaFromDb(): Promise<string | null> {
try {
const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"');
return (rows.length > 0 ? rows[0].string : null);

View File

@ -148,6 +148,10 @@
"API": "__MEMPOOL_SERVICES_API__",
"ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
},
"STRATUM": {
"ENABLED": __STRATUM_ENABLED__,
"API": "__STRATUM_API__"
},
"REDIS": {
"ENABLED": __REDIS_ENABLED__,
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__",

View File

@ -149,6 +149,10 @@ __REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"}
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
# STRATUM
__STRATUM_ENABLED__=${STRATUM_ENABLED:=false}
__STRATUM_API__=${STRATUM_API:="http://localhost:1234"}
# REDIS
__REDIS_ENABLED__=${REDIS_ENABLED:=false}
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=""}
@ -300,6 +304,10 @@ sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.j
sed -i "s!__MEMPOOL_SERVICES_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json
sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json
# STRATUM
sed -i "s!__STRATUM_ENABLED__!${__STRATUM_ENABLED__}!g" mempool-config.json
sed -i "s!__STRATUM_API__!${__STRATUM_API__}!g" mempool-config.json
# REDIS
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json

View File

@ -45,6 +45,7 @@ __SERVICES_API__=${SERVICES_API:=https://mempool.space/api/v1/services}
__PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false}
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
__STRATUM_ENABLED__=${STRATUM_ENABLED:=false}
# Export as environment variables to be used by envsubst
export __MAINNET_ENABLED__
@ -76,6 +77,7 @@ export __SERVICES_API__
export __PUBLIC_ACCELERATIONS__
export __HISTORICAL_PRICE__
export __ADDITIONAL_CURRENCIES__
export __STRATUM_ENABLED__
folder=$(find /var/www/mempool -name "config.js" | xargs dirname)
echo ${folder}

View File

@ -344,7 +344,9 @@ describe('Mainnet', () => {
cy.visit('/');
cy.waitForSkeletonGone();
cy.changeNetwork('testnet4');
//TODO(knorrium): add a check for the proxied server
// cy.changeNetwork('testnet4');
cy.changeNetwork('signet');
cy.changeNetwork('mainnet');
});

View File

@ -27,5 +27,6 @@
"ACCELERATOR": false,
"ACCELERATOR_BUTTON": true,
"PUBLIC_ACCELERATIONS": false,
"STRATUM_ENABLED": false,
"SERVICES_API": "https://mempool.space/api/v1/services"
}

View File

@ -33,7 +33,7 @@
"browserify": "^17.0.0",
"clipboard": "^2.0.11",
"domino": "^2.1.6",
"echarts": "~5.5.0",
"echarts": "~5.6.0",
"esbuild": "^0.24.0",
"ngx-echarts": "~17.2.0",
"ngx-infinite-scroll": "^17.0.0",
@ -8724,12 +8724,12 @@
}
},
"node_modules/echarts": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.0.tgz",
"integrity": "sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==",
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
"dependencies": {
"tslib": "2.3.0",
"zrender": "5.5.0"
"zrender": "5.6.1"
}
},
"node_modules/echarts/node_modules/tslib": {
@ -18366,9 +18366,9 @@
}
},
"node_modules/zrender": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.5.0.tgz",
"integrity": "sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==",
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
"integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
"dependencies": {
"tslib": "2.3.0"
}
@ -24485,12 +24485,12 @@
}
},
"echarts": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.0.tgz",
"integrity": "sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==",
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
"requires": {
"tslib": "2.3.0",
"zrender": "5.5.0"
"zrender": "5.6.1"
},
"dependencies": {
"tslib": {
@ -31485,9 +31485,9 @@
}
},
"zrender": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.5.0.tgz",
"integrity": "sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==",
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
"integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
"requires": {
"tslib": "2.3.0"
},

View File

@ -86,7 +86,7 @@
"browserify": "^17.0.0",
"clipboard": "^2.0.11",
"domino": "^2.1.6",
"echarts": "~5.5.0",
"echarts": "~5.6.0",
"ngx-echarts": "~17.2.0",
"ngx-infinite-scroll": "^17.0.0",
"qrcode": "1.5.1",

View File

@ -3,8 +3,10 @@ const fs = require('fs');
let PROXY_CONFIG = require('./proxy.conf');
PROXY_CONFIG.forEach(entry => {
entry.target = entry.target.replace("mempool.space", "mempool-staging.fra.mempool.space");
entry.target = entry.target.replace("liquid.network", "liquid-staging.fra.mempool.space");
const hostname = process.env.CYPRESS_REROUTE_TESTNET === 'true' ? 'mempool-staging.fra.mempool.space' : 'node201.fmt.mempool.space';
console.log(`e2e tests running against ${hostname}`);
entry.target = entry.target.replace("mempool.space", hostname);
entry.target = entry.target.replace("liquid.network", "liquid-staging.fmt.mempool.space");
});
module.exports = PROXY_CONFIG;

View File

@ -217,7 +217,7 @@
<ng-container>
<ng-template ngFor let-sponsor [ngForOf]="profiles.whales">
<a [href]="'https://x.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</ng-container>
@ -229,7 +229,7 @@
<div class="wrapper">
<ng-template ngFor let-sponsor [ngForOf]="profiles.chads">
<a [href]="'https://x.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</div>

View File

@ -1,10 +1,18 @@
<div class="box card w-100" style="background: var(--box-bg)" id=acceleratePreviewAnchor>
<div class="box card w-100 accelerate-checkout-inner" [class.input-disabled]="isCheckoutLocked > 0" style="background: var(--box-bg)" id=acceleratePreviewAnchor>
@if (accelerateError) {
<div class="row mb-1 text-center">
<div class="col-sm">
<h1 style="font-size: larger;" i18n="accelerator.sorry-error-title">Sorry, something went wrong!</h1>
@if (accelerateError.includes('Payment declined')) {
<div class="row mb-1 text-center">
<div class="col-sm">
<h1 style="font-size: larger;">{{ accelerateError }}</h1>
</div>
</div>
</div>
} @else {
<div class="row mb-1 text-center">
<div class="col-sm">
<h1 style="font-size: larger;" i18n="accelerator.sorry-error-title">Sorry, something went wrong!</h1>
</div>
</div>
}
<div class="row text-center mt-1">
<div class="col-sm">
<div class="d-flex flex-row justify-content-center align-items-center">
@ -357,11 +365,11 @@
<app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true" class="ml-2"></app-active-acceleration-box>
</div>
</div>
<div class="payment-area mt-2 p-2" style="font-size: 14px;">
<div class="payment-area" style="font-size: 14px;">
<div class="row text-center justify-content-center mx-2">
<p i18n="accelerator.payment-to-mempool-space">Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + tx.txid" target="_blank">{{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}</a></p>
<span i18n="accelerator.payment-to-mempool-space">Payment to mempool.space for acceleration of txid <a [routerLink]="'/tx/' + tx.txid" target="_blank">{{ tx.txid.substr(0, 10) }}..{{ tx.txid.substr(-10) }}</a></span>
</div>
@if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp)) {
@if (canPayWithBalance || !(canPayWithBitcoin || canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay)) {
<div class="row">
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
<p><ng-container i18n="accelerator.your-account-will-be-debited">Your account will be debited no more than</ng-container>&nbsp;<small style="font-family: monospace;">{{ cost | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats">sats</span></p>
@ -378,9 +386,12 @@
<p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container>&nbsp;<span><small style="font-family: monospace;">{{ ((invoice.btcDue * 100_000_000) || cost) | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats">sats</span></span></p>
<app-bitcoin-invoice style="width: 100%;" [invoice]="invoice" [minimal]="true" (completed)="bitcoinPaymentCompleted()"></app-bitcoin-invoice>
} @else if (btcpayInvoiceFailed) {
<p i18n="accelerator.failed-to-load-invoice">Failed to load invoice</p>
<div class="d-flex flex-column align-items-center justify-content-center" style="width: 100%; height: 292px;">
<fa-icon style="font-size: 24px; color: var(--red)" [icon]="['fas', 'circle-xmark']"></fa-icon>
<div class="btcpay-invoice">
<fa-icon style="font-size: 20px; color: var(--red)" [icon]="['fas', 'circle-xmark']"></fa-icon>
<span i18n="accelerator.failed-to-load-invoice">Failed to load invoice</span>
@if (!loadingBtcpayInvoice) {
<button class="btn btn-sm btn-secondary mt-0 mt-md-1" (click)="requestBTCPayInvoice()">Retry ↻</button>
}
</div>
} @else {
<p i18n="accelerator.loading-invoice">Loading invoice...</p>
@ -389,13 +400,13 @@
</div>
}
</div>
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {
<div class="col-sm text-center flex-grow-0 d-flex flex-column justify-content-center align-items-center">
<p class="text-nowrap">&mdash;<span i18n="or">OR</span>&mdash;</p>
<p class="text-nowrap">&mdash;&mdash;<span i18n="or"> OR </span>&mdash;&mdash;</p>
</div>
}
}
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay || canPayWithCardOnFile) {
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
<p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container>&nbsp;<app-fiat [value]="cost"></app-fiat> with</p>
@if (canPayWithCashapp) {
@ -413,6 +424,17 @@
<img src="/resources/google-pay.png" height=37>
</div>
}
@if (canPayWithCardOnFile) {
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) { <span class="mt-1 mb-1"></span> }
<div class="paymentMethod mx-2 d-flex justify-content-center align-items-center" style="width: 200px; height: 55px" (click)="moveToStep('cardonfile')">
@if (['VISA', 'MASTERCARD', 'JCB', 'DISCOVER', 'DISCOVER_DINERS', 'AMERICAN_EXPRESS'].includes(estimate?.availablePaymentMethods?.cardOnFile?.card?.brand)) {
<app-svg-images [name]="estimate?.availablePaymentMethods?.cardOnFile?.card?.brand" height="33" class="mr-2"></app-svg-images>
} @else {
<app-svg-images name="OTHER_BRAND" height="33" class="mr-2"></app-svg-images>
}
<span style="font-size: 22px; padding-bottom: 3px">{{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }}</span>
</div>
}
</div>
}
</div>
@ -435,7 +457,7 @@
<button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="moveToStep('summary')" i18n="go-back">Go back</button>
</div>
</div>
} @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay') {
} @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay' || step === 'cardonfile') {
<!-- Show checkout page -->
<div class="row mb-md-1 text-center" id="confirm-title">
<div class="col-sm" id="confirm-payment-title">
@ -451,7 +473,7 @@
</div>
</div>
@if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay) {
@if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay || step === 'cardonfile' && !loadingCardOnFile) {
<div class="row text-center mt-1">
<div class="col-sm">
<div class="form-group w-100">
@ -476,14 +498,24 @@
<div id="cash-app-pay" class="d-inline-block" style="height: 50px" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
} @else if (step === 'googlepay') {
<div id="google-pay-button" class="d-inline-block" style="height: 50px" [style]="loadingGooglePay ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
} @else if (step === 'cardonfile') {
<div class="paymentMethod mx-2 d-flex justify-content-center align-items-center ml-auto mr-auto" style="width: 200px; height: 55px" (click)="requestCardOnFilePayment()" [style]="loadingCardOnFile ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''">
<fa-icon style="font-size: 24px; color: white" [icon]="['fas', 'credit-card']"></fa-icon>
<span class="ml-2" style="font-size: 22px">{{ estimate?.availablePaymentMethods?.cardOnFile?.card?.brand }} {{ estimate?.availablePaymentMethods?.cardOnFile?.card?.last_4 }}</span>
</div>
}
@if (loadingCashapp || loadingApplePay || loadingGooglePay) {
@if (loadingCashapp || loadingApplePay || loadingGooglePay || loadingCardOnFile) {
<div display="d-flex flex-row justify-content-center">
<span i18n="accelerator.loading-payment-method">Loading payment method...</span>
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
}
</div>
@if (isTokenizing > 0) {
<div class="d-flex flex-row justify-content-center">
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
</div>
}
</div>
</div>

View File

@ -8,6 +8,13 @@
color: var(--green)
}
.accelerate-checkout-inner {
&.input-disabled {
pointer-events: none;
opacity: 0.75;
}
}
.paymentMethod {
padding: 10px;
background-color: var(--secondary);
@ -146,6 +153,11 @@
.payment-area {
background: var(--bg);
margin-top: 0.5rem;
padding: 0.5rem;
@media (max-width: 575px) {
padding-bottom: 1.25rem;
}
}
.col.pie {
@ -212,4 +224,17 @@
}
.apple-pay-button-white-with-line {
-apple-pay-button-style: white-outline;
}
.btcpay-invoice {
display: flex;
height: 292px;
flex-direction: column;
justify-content: center;
align-items: center;
@media (max-width: 575px) {
height: 75px;
flex-direction: row;
gap: 5px;
}
}

View File

@ -13,7 +13,7 @@ import { EnterpriseService } from '@app/services/enterprise.service';
import { ApiService } from '@app/services/api.service';
import { isDevMode } from '@angular/core';
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay';
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay' | 'cardOnFile';
export type AccelerationEstimate = {
hasAccess: boolean;
@ -26,7 +26,7 @@ export type AccelerationEstimate = {
mempoolBaseFee: number;
vsizeFee: number;
pools: number[];
availablePaymentMethods: Record<PaymentMethod, {min: number, max: number}>;
availablePaymentMethods: Record<PaymentMethod, {min: number, max: number, card?: {card_id: string, last_4: string, brand: string, name: string, billing: any}}>;
unavailable?: boolean;
options: { // recommended bid options
fee: number; // recommended userBid in sats
@ -49,7 +49,7 @@ export const MIN_BID_RATIO = 1;
export const DEFAULT_BID_RATIO = 2;
export const MAX_BID_RATIO = 4;
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'googlepay' | 'processing' | 'paid' | 'success';
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'googlepay' | 'cardonfile' | 'processing' | 'paid' | 'success';
@Component({
selector: 'app-accelerate-checkout',
@ -62,9 +62,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
@Input() miningStats: MiningStats;
@Input() eta: ETA;
@Input() scrollEvent: boolean;
@Input() cashappEnabled: boolean = true;
@Input() applePayEnabled: boolean = false;
@Input() googlePayEnabled: boolean = true;
@Input() cardOnFileEnabled: boolean = true;
@Input() advancedEnabled: boolean = false;
@Input() forceMobile: boolean = false;
@Input() showDetails: boolean = false;
@ -76,6 +76,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
calculating = true;
processing = false;
isCheckoutLocked = 0; // reference counter, 0 = unlocked, >0 = locked
isTokenizing = 0; // reference counter, 0 = false, >0 = true
selectedOption: 'wait' | 'accel';
cantPayReason = '';
quoteError = ''; // error fetching estimate or initial data
@ -115,6 +117,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
loadingCashapp = false;
loadingApplePay = false;
loadingGooglePay = false;
loadingCardOnFile = false;
payments: any;
cashAppPay: any;
applePay: any;
@ -154,7 +157,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.accelerateError = null;
this.timePaid = 0;
this.btcpayInvoiceFailed = false;
this.moveToStep('summary');
this.moveToStep('summary', true);
} else {
this.auth = auth;
}
@ -163,11 +166,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('cash_request_id')) { // Redirected from cashapp
this.moveToStep('processing');
this.moveToStep('processing', true);
this.insertSquare();
this.setupSquare();
} else {
this.moveToStep('summary');
this.moveToStep('summary', true);
}
this.conversionsSubscription = this.stateService.conversions$.subscribe(
@ -192,20 +195,23 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
if (changes.accelerating && this.accelerating) {
if (this.step === 'processing' || this.step === 'paid') {
this.moveToStep('success');
this.moveToStep('success', true);
} else { // Edge case where the transaction gets accelerated by someone else or on another session
this.closeModal();
}
}
}
moveToStep(step: CheckoutStep): void {
moveToStep(step: CheckoutStep, force: boolean = false): void {
if (this.isCheckoutLocked > 0 && !force) {
return;
}
this.processing = false;
this._step = step;
if (this.timeoutTimer) {
clearTimeout(this.timeoutTimer);
}
if (!this.estimate && ['quote', 'summary', 'checkout'].includes(this.step)) {
if (!this.estimate && ['quote', 'summary', 'checkout', 'processing'].includes(this.step)) {
this.fetchEstimate();
}
if (this._step === 'checkout') {
@ -214,10 +220,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
if (this._step === 'checkout' && this.canPayWithBitcoin) {
this.btcpayInvoiceFailed = false;
this.loadingBtcpayInvoice = true;
this.invoice = null;
this.requestBTCPayInvoice();
} else if (this._step === 'cashapp' && this.cashappEnabled) {
} else if (this._step === 'cashapp') {
this.loadingCashapp = true;
this.setupSquare();
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
@ -229,6 +234,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.loadingGooglePay = true;
this.setupSquare();
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
} else if (this._step === 'cardonfile' && this.cardOnFileEnabled) {
this.loadingCardOnFile = true;
this.setupSquare();
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
} else if (this._step === 'paid') {
this.timePaid = Date.now();
this.timeoutTimer = setTimeout(() => {
@ -242,7 +251,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
closeModal(): void {
this.completed.emit(true);
this.moveToStep('summary');
this.moveToStep('summary', true);
}
/**
@ -323,7 +332,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
if (this.step === 'checkout' && this.canPayWithBitcoin && !this.loadingBtcpayInvoice) {
this.loadingBtcpayInvoice = true;
this.requestBTCPayInvoice();
}
@ -393,7 +401,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.audioService.playSound('ascend-chime-cartoon');
this.showSuccess = true;
this.estimateSubscription.unsubscribe();
this.moveToStep('paid');
this.moveToStep('paid', true);
},
error: (response) => {
this.processing = false;
@ -449,6 +457,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
await this.requestApplePayPayment();
} else if (this._step === 'googlepay') {
await this.requestGooglePayPayment();
} else if (this._step === 'cardonfile') {
this.loadingCardOnFile = false;
}
},
error: () => {
@ -503,56 +513,75 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
this.loadingApplePay = false;
applePayButton.addEventListener('click', async event => {
if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
return;
}
event.preventDefault();
const tokenResult = await this.applePay.tokenize();
if (tokenResult?.status === 'OK') {
const card = tokenResult.details?.card;
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`);
this.accelerateError = 'apple_pay_no_card_details';
this.processing = false;
return;
}
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
this.servicesApiService.accelerateWithApplePay$(
this.tx.txid,
tokenResult.token,
cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
costUSD
).subscribe({
next: () => {
try {
// lock the checkout UI and show a loading spinner until the square modals are finished
this.isCheckoutLocked++;
this.isTokenizing++;
const tokenResult = await this.applePay.tokenize();
if (tokenResult?.status === 'OK') {
const card = tokenResult.details?.card;
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`);
this.accelerateError = 'apple_pay_no_card_details';
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
if (this.applePay) {
this.applePay.destroy();
}
setTimeout(() => {
this.moveToStep('paid');
}, 1000);
},
error: (response) => {
this.processing = false;
this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {
// Reset everything by reloading the page :D, can be improved
const urlParams = new URLSearchParams(window.location.search);
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
}, 3000);
}
return;
}
});
} else {
this.processing = false;
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
if (tokenResult.errors) {
errorMessage += ` and errors: ${JSON.stringify(
tokenResult.errors,
)}`;
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
// keep checkout in loading state until the acceleration request completes
this.isTokenizing++;
this.isCheckoutLocked++;
this.servicesApiService.accelerateWithApplePay$(
this.tx.txid,
tokenResult.token,
cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
costUSD
).subscribe({
next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
if (this.applePay) {
this.applePay.destroy();
}
setTimeout(() => {
this.isTokenizing--;
this.isCheckoutLocked--;
this.moveToStep('paid', true);
}, 1000);
},
error: (response) => {
this.processing = false;
this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {
this.isTokenizing--;
this.isCheckoutLocked--;
// Reset everything by reloading the page :D, can be improved
const urlParams = new URLSearchParams(window.location.search);
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
}, 10000);
}
}
});
} else {
this.processing = false;
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
if (tokenResult.errors) {
errorMessage += ` and errors: ${JSON.stringify(
tokenResult.errors,
)}`;
}
throw new Error(errorMessage);
}
throw new Error(errorMessage);
} finally {
// always unlock the checkout once we're finished
this.isTokenizing--;
this.isCheckoutLocked--;
}
});
} catch (e) {
@ -602,70 +631,193 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.loadingGooglePay = false;
document.getElementById('google-pay-button').addEventListener('click', async event => {
if (this.isCheckoutLocked > 0 || this.isTokenizing > 0) {
return;
}
event.preventDefault();
const tokenResult = await this.googlePay.tokenize();
if (tokenResult?.status === 'OK') {
const card = tokenResult.details?.card;
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`);
this.accelerateError = 'apple_pay_no_card_details';
this.processing = false;
return;
}
const verificationToken = await this.$verifyBuyer(this.payments, tokenResult.token, tokenResult.details, costUSD.toFixed(2));
if (!verificationToken) {
console.error(`SCA verification failed`);
this.accelerateError = 'SCA Verification Failed. Payment Declined.';
this.processing = false;
return;
}
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
this.servicesApiService.accelerateWithGooglePay$(
this.tx.txid,
tokenResult.token,
verificationToken,
cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
costUSD
).subscribe({
next: () => {
try {
// lock the checkout UI and show a loading spinner until the square modals are finished
this.isCheckoutLocked++;
this.isTokenizing++;
const tokenResult = await this.googlePay.tokenize();
if (tokenResult?.status === 'OK') {
const card = tokenResult.details?.card;
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`);
this.accelerateError = 'apple_pay_no_card_details';
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
if (this.googlePay) {
this.googlePay.destroy();
}
setTimeout(() => {
this.moveToStep('paid');
}, 1000);
},
error: (response) => {
this.processing = false;
this.accelerateError = response.error;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {
// Reset everything by reloading the page :D, can be improved
const urlParams = new URLSearchParams(window.location.search);
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
}, 3000);
}
return;
}
});
} else {
this.processing = false;
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
if (tokenResult.errors) {
errorMessage += ` and errors: ${JSON.stringify(
tokenResult.errors,
)}`;
const verificationToken = await this.$verifyBuyer(this.payments, tokenResult.token, tokenResult.details, costUSD.toFixed(2));
if (!verificationToken || !verificationToken.token) {
console.error(`SCA verification failed`);
this.accelerateError = 'SCA Verification Failed. Payment Declined.';
this.processing = false;
return;
}
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
// keep checkout in loading state until the acceleration request completes
this.isCheckoutLocked++;
this.isTokenizing++;
this.servicesApiService.accelerateWithGooglePay$(
this.tx.txid,
tokenResult.token,
verificationToken.token,
cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
costUSD,
verificationToken.userChallenged
).subscribe({
next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
if (this.googlePay) {
this.googlePay.destroy();
}
setTimeout(() => {
this.isTokenizing--;
this.isCheckoutLocked--;
this.moveToStep('paid', true);
}, 1000);
},
error: (response) => {
this.processing = false;
this.accelerateError = response.error;
this.isTokenizing--;
this.isCheckoutLocked--;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {
// Reset everything by reloading the page :D, can be improved
const urlParams = new URLSearchParams(window.location.search);
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
}, 10000);
}
}
});
} else {
this.processing = false;
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
if (tokenResult.errors) {
errorMessage += ` and errors: ${JSON.stringify(
tokenResult.errors,
)}`;
}
throw new Error(errorMessage);
}
throw new Error(errorMessage);
} finally {
// always unlock the checkout once we're finished
this.isTokenizing--;
this.isCheckoutLocked--;
}
});
}
);
}
/**
* Card On File
*/
async requestCardOnFilePayment(): Promise<void> {
if (this.processing) {
return;
}
if (this.conversionsSubscription) {
this.conversionsSubscription.unsubscribe();
}
this.processing = true;
this.conversionsSubscription = this.stateService.conversions$.subscribe(
async (conversions) => {
this.conversions = conversions;
const costUSD = this.cost / 100_000_000 * conversions.USD;
if (this.isCheckoutLocked > 0) {
return;
}
const cardOnFile = this.estimate?.availablePaymentMethods?.cardOnFile;
if (!cardOnFile?.card) {
this.accelerateError = 'card_on_file_not_found';
return;
}
this.loadingCardOnFile = false;
try {
this.isCheckoutLocked += 2;
this.isTokenizing += 2;
const nameParts = cardOnFile.card.name.split(' ');
const assumedGivenName = nameParts[0];
const assumedFamilyName = nameParts.length > 1 ? nameParts[1] : undefined;
const verificationDetails = {
card: {
billing: {
givenName: assumedGivenName,
familyName: assumedFamilyName,
addressLines: [cardOnFile.card.billing.addressLine1 ?? ''],
city: cardOnFile.card.billing.locality ?? '',
state: cardOnFile.card.billing.administrativeDistrictLevel1 ?? '',
countyCode: cardOnFile.card.billing.country,
}
}
};
const verificationToken = await this.$verifyBuyer(this.payments, cardOnFile.card.card_id, verificationDetails, costUSD.toFixed(2));
if (!verificationToken || !verificationToken.token) {
console.error(`SCA verification failed`);
this.accelerateError = 'SCA Verification Failed. Payment Declined.';
this.processing = false;
return;
}
this.servicesApiService.accelerateWithCardOnFile$(
this.tx.txid,
cardOnFile.card.card_id,
verificationToken.token,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
costUSD,
verificationToken.userChallenged
).subscribe({
next: () => {
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
setTimeout(() => {
this.isCheckoutLocked--;
this.isTokenizing--;
this.moveToStep('paid', true);
}, 1000);
},
error: (response) => {
this.processing = false;
this.accelerateError = response.error;
this.isCheckoutLocked--;
this.isTokenizing--;
if (!(response.status === 403 && response.error === 'not_available')) {
setTimeout(() => {
// Reset everything by reloading the page :D, can be improved
const urlParams = new URLSearchParams(window.location.search);
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
}, 3000);
}
}
});
} catch (e) {
console.log(e);
this.isCheckoutLocked--;
this.isTokenizing--;
this.processing = false;
this.accelerateError = e.message;
} finally {
// always unlock the checkout once we're finished
this.isCheckoutLocked--;
this.isTokenizing--;
}
}
);
}
/**
* CASHAPP
*/
@ -686,7 +838,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
const redirectHostname = document.location.hostname === 'localhost' ? `http://localhost:4200`: `https://${document.location.hostname}`;
const costUSD = this.step === 'processing' ? 69.69 : (this.cost / 100_000_000 * conversions.USD); // When we're redirected to this component, the payment data is already linked to the payment token, so does not matter what amonut we put in there, therefore it's 69.69
const costUSD = this.cost / 100_000_000 * conversions.USD;
const paymentRequest = this.payments.paymentRequest({
countryCode: 'US',
currencyCode: 'USD',
@ -726,7 +878,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.cashAppPay.destroy();
}
setTimeout(() => {
this.moveToStep('paid');
this.moveToStep('paid', true);
if (window.history.replaceState) {
const urlParams = new URLSearchParams(window.location.search);
window.history.replaceState(null, null, window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ''));
@ -741,7 +893,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
// Reset everything by reloading the page :D, can be improved
const urlParams = new URLSearchParams(window.location.search);
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
}, 3000);
}, 10000);
}
}
});
@ -752,9 +904,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
/**
* Required in SCA Mandated Regions: Learn more at https://developer.squareup.com/docs/sca-overview
* https://developer.squareup.com/docs/sca-overview
*/
async $verifyBuyer(payments, token, details, amount) {
async $verifyBuyer(payments, token, details, amount): Promise<{token: string, userChallenged: boolean}> {
const verificationDetails = {
amount: amount,
currencyCode: 'USD',
@ -774,23 +926,26 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
token,
verificationDetails,
);
return verificationResults.token;
return verificationResults;
}
/**
* BTCPay
*/
async requestBTCPayInvoice(): Promise<void> {
this.loadingBtcpayInvoice = true;
this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe(
switchMap(response => {
return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId);
}),
catchError(error => {
console.log(error);
this.loadingBtcpayInvoice = false;
this.btcpayInvoiceFailed = true;
return of(null);
})
).subscribe((invoice) => {
this.loadingBtcpayInvoice = false;
this.invoice = invoice;
this.cd.markForCheck();
});
@ -800,7 +955,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
this.estimateSubscription.unsubscribe();
this.moveToStep('paid');
this.moveToStep('paid', true);
}
isLoggedIn(): boolean {
@ -827,9 +982,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
get couldPayWithCashapp(): boolean {
if (!this.cashappEnabled) {
return false;
}
return !!this.estimate?.availablePaymentMethods?.cashapp;
}
@ -864,7 +1016,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
get canPayWithCashapp(): boolean {
if (!this.cashappEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
if (!this.conversions || (!this.isProdDomain && !isDevMode())) {
return false;
}
@ -911,6 +1063,22 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
return false;
}
get canPayWithCardOnFile(): boolean {
if (!this.cardOnFileEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
return false;
}
const paymentMethod = this.estimate?.availablePaymentMethods?.cardOnFile;
if (paymentMethod) {
const costUSD = (this.cost / 100_000_000 * this.conversions.USD);
if (costUSD >= paymentMethod.min && costUSD <= paymentMethod.max) {
return true;
}
}
return false;
}
get canPayWithBalance(): boolean {
if (!this.hasAccessToBalanceMode) {
return false;

View File

@ -46,6 +46,8 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
aggregatedHistory$: Observable<any>;
statsSubscription: Subscription;
aggregatedHistorySubscription: Subscription;
fragmentSubscription: Subscription;
isLoading = true;
formatNumber = formatNumber;
timespan = '';
@ -79,8 +81,8 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
}
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
this.route.fragment.subscribe((fragment) => {
this.fragmentSubscription = this.route.fragment.subscribe((fragment) => {
if (['24h', '3d', '1w', '1m', '3m', 'all'].indexOf(fragment) > -1) {
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
}
@ -113,7 +115,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
share(),
);
this.aggregatedHistory$.subscribe();
this.aggregatedHistorySubscription = this.aggregatedHistory$.subscribe();
}
ngOnChanges(changes: SimpleChanges): void {
@ -335,8 +337,8 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
}
ngOnDestroy(): void {
if (this.statsSubscription) {
this.statsSubscription.unsubscribe();
}
this.aggregatedHistorySubscription?.unsubscribe();
this.fragmentSubscription?.unsubscribe();
this.statsSubscription?.unsubscribe();
}
}

View File

@ -14,7 +14,7 @@
<th class="time text-right" i18n="accelerator.requested">Requested</th>
</ng-container>
<ng-container *ngIf="!pending">
<th class="fee text-right" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th>
<th class="fee text-right text-truncate" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th>
<th class="block text-right" i18n="shared.block-title">Block</th>
<th class="pool text-right" i18n="mining.pool-name" *ngIf="!this.widget">Pool</th>
<th class="status text-right" i18n="transaction.status|Transaction Status">Status</th>
@ -64,7 +64,8 @@
<span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span>
<span *ngIf="acceleration.status.includes('completed') && acceleration.minedByPoolUniqueId && pools[acceleration.minedByPoolUniqueId]" class="badge badge-success"><ng-container i18n="accelerator.completed">Completed</ng-container><span *ngIf="acceleration.status === 'completed_provisional'">&nbsp;</span></span>
<span *ngIf="acceleration.status.includes('completed') && (!acceleration.minedByPoolUniqueId || !pools[acceleration.minedByPoolUniqueId])" class="badge badge-success"><ng-container i18n="transaction.rbf.mined">Mined</ng-container><span *ngIf="acceleration.status === 'completed_provisional'">&nbsp;</span></span>
<span *ngIf="acceleration.status.includes('failed')" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Canceled</ng-container><span *ngIf="acceleration.status === 'failed_provisional'">&nbsp;</span></span>
<span *ngIf="acceleration.status.includes('failed') && acceleration.canceled" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Canceled</ng-container><span *ngIf="acceleration.status === 'failed_provisional'">&nbsp;</span></span>
<span *ngIf="acceleration.status.includes('failed') && !acceleration.canceled" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Failed</ng-container><span *ngIf="acceleration.status === 'failed_provisional'">&nbsp;</span></span>
</td>
<td class="date text-right" *ngIf="!this.widget">
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>

View File

@ -478,25 +478,30 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
}
extendSummary(summary) {
let extendedSummary = summary.slice();
const extendedSummary = summary.slice();
// Add a point at today's date to make the graph end at the current time
extendedSummary.unshift({ time: Date.now() / 1000, value: 0 });
extendedSummary.reverse();
let oneHour = 60 * 60;
let maxTime = Date.now() / 1000;
const oneHour = 60 * 60;
// Fill gaps longer than interval
for (let i = 0; i < extendedSummary.length - 1; i++) {
let hours = Math.floor((extendedSummary[i + 1].time - extendedSummary[i].time) / oneHour);
if (extendedSummary[i].time > maxTime) {
extendedSummary[i].time = maxTime - 30;
}
maxTime = extendedSummary[i].time;
const hours = Math.floor((extendedSummary[i].time - extendedSummary[i + 1].time) / oneHour);
if (hours > 1) {
for (let j = 1; j < hours; j++) {
let newTime = extendedSummary[i].time + oneHour * j;
const newTime = extendedSummary[i].time - oneHour * j;
extendedSummary.splice(i + j, 0, { time: newTime, value: 0 });
}
i += hours - 1;
}
}
return extendedSummary.reverse();
return extendedSummary;
}
}

View File

@ -41,7 +41,7 @@ export class AppComponent implements OnInit {
@HostListener('document:keydown', ['$event'])
handleKeyboardEvents(event: KeyboardEvent) {
if (event.target instanceof HTMLInputElement) {
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
return;
}
// prevent arrow key horizontal scrolling

View File

@ -10,6 +10,10 @@
</span>
}
<div class="d-flex justify-content-center">
<app-mempool-error *ngIf="paymentErrorMessage" [error]="paymentErrorMessage"></app-mempool-error>
</div>
<div *ngIf="paymentStatus === 2">
<form [formGroup]="paymentForm">

View File

@ -1,9 +1,8 @@
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { ActivatedRoute } from '@angular/router';
import { Subscription, of, timer } from 'rxjs';
import { filter, repeat, retry, switchMap, take, tap } from 'rxjs/operators';
import { Subscription, of, catchError } from 'rxjs';
import { retry, tap } from 'rxjs/operators';
import { ServicesApiServices } from '@app/services/services-api.service';
@Component({
@ -18,30 +17,17 @@ export class BitcoinInvoiceComponent implements OnInit, OnChanges, OnDestroy {
@Output() completed = new EventEmitter();
paymentForm: FormGroup;
requestSubscription: Subscription | undefined;
paymentStatusSubscription: Subscription | undefined;
paymentStatus = 1; // 1 - Waiting for invoice | 2 - Pending payment | 3 - Payment completed
paramMapSubscription: Subscription | undefined;
invoiceSubscription: Subscription | undefined;
invoiceTimeout; // Wait for angular to load all the things before making a request
paymentErrorMessage: string = '';
constructor(
private formBuilder: FormBuilder,
private apiService: ServicesApiServices,
private sanitizer: DomSanitizer,
private activatedRoute: ActivatedRoute
private sanitizer: DomSanitizer
) { }
ngOnDestroy() {
if (this.requestSubscription) {
this.requestSubscription.unsubscribe();
}
if (this.paramMapSubscription) {
this.paramMapSubscription.unsubscribe();
}
if (this.invoiceSubscription) {
this.invoiceSubscription.unsubscribe();
}
if (this.paymentStatusSubscription) {
this.paymentStatusSubscription.unsubscribe();
}
@ -72,15 +58,39 @@ export class BitcoinInvoiceComponent implements OnInit, OnChanges, OnDestroy {
} else {
this.paymentStatus = 4;
}
this.monitorPendingInvoice();
}
monitorPendingInvoice(): void {
if (!this.invoice) {
return;
}
if (this.paymentStatusSubscription) {
this.paymentStatusSubscription.unsubscribe();
}
this.paymentStatusSubscription = this.apiService.getPaymentStatus$(this.invoice.btcpayInvoiceId).pipe(
retry({ delay: () => timer(2000)}),
repeat({delay: 2000}),
filter((response) => response.status !== 204 && response.status !== 404),
take(1),
).subscribe(() => {
this.paymentStatus = 3;
this.completed.emit();
});
tap(result => {
if (result.status === 204) { // Manually trigger an error in that case so we can retry
throw result;
} else if (result.status === 200) { // Invoice settled
this.paymentStatus = 3;
this.completed.emit();
}
}),
catchError(err => {
if (err.status === 204 || err.status === 504) {
throw err; // Will trigger the retry
} else if (err.status === 400) {
this.paymentErrorMessage = 'Invoice has expired';
} else if (err.status === 404) {
this.paymentErrorMessage = 'Invoice is no longer valid';
}
this.paymentStatus = -1;
return of(null);
}),
retry({ delay: 1000 }),
).subscribe();
}
get availableMethods(): string[] {

View File

@ -172,13 +172,19 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
ngOnDestroy(): void {
if (this.animationFrameRequest) {
cancelAnimationFrame(this.animationFrameRequest);
clearTimeout(this.animationHeartBeat);
}
clearTimeout(this.animationHeartBeat);
if (this.canvas) {
this.canvas.nativeElement.removeEventListener('webglcontextlost', this.handleContextLost);
this.canvas.nativeElement.removeEventListener('webglcontextrestored', this.handleContextRestored);
this.themeChangedSubscription?.unsubscribe();
}
if (this.scene) {
this.scene.destroy();
}
this.vertexArray.destroy();
this.vertexArray = null;
this.themeChangedSubscription?.unsubscribe();
this.searchSubscription?.unsubscribe();
}
clear(direction): void {
@ -447,7 +453,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
this.applyQueuedUpdates();
// skip re-render if there's no change to the scene
if (this.scene && this.gl) {
if (this.scene && this.gl && this.vertexArray) {
/* SET UP SHADER UNIFORMS */
// screen dimensions
this.gl.uniform2f(this.gl.getUniformLocation(this.shaderProgram, 'screenSize'), this.displayWidth, this.displayHeight);
@ -489,9 +495,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
if (this.running && this.scene && now <= (this.scene.animateUntil + 500)) {
this.doRun();
} else {
if (this.animationHeartBeat) {
clearTimeout(this.animationHeartBeat);
}
clearTimeout(this.animationHeartBeat);
this.animationHeartBeat = window.setTimeout(() => {
this.start();
}, 1000);

View File

@ -19,6 +19,7 @@ export class FastVertexArray {
freeSlots: number[];
lastSlot: number;
dirty = false;
destroyed = false;
constructor(length, stride) {
this.length = length;
@ -32,6 +33,9 @@ export class FastVertexArray {
}
insert(sprite: TxSprite): number {
if (this.destroyed) {
return;
}
this.count++;
let position;
@ -45,11 +49,14 @@ export class FastVertexArray {
}
}
this.sprites[position] = sprite;
return position;
this.dirty = true;
return position;
}
remove(index: number): void {
if (this.destroyed) {
return;
}
this.count--;
this.clearData(index);
this.freeSlots.push(index);
@ -61,20 +68,26 @@ export class FastVertexArray {
}
setData(index: number, dataChunk: number[]): void {
if (this.destroyed) {
return;
}
this.data.set(dataChunk, (index * this.stride));
this.dirty = true;
}
clearData(index: number): void {
private clearData(index: number): void {
this.data.fill(0, (index * this.stride), ((index + 1) * this.stride));
this.dirty = true;
}
getData(index: number): Float32Array {
if (this.destroyed) {
return;
}
return this.data.subarray(index, this.stride);
}
expand(): void {
private expand(): void {
this.length *= 2;
const newData = new Float32Array(this.length * this.stride);
newData.set(this.data);
@ -82,7 +95,7 @@ export class FastVertexArray {
this.dirty = true;
}
compact(): void {
private compact(): void {
// New array length is the smallest power of 2 larger than the sprite count (but no smaller than 512)
const newLength = Math.max(512, Math.pow(2, Math.ceil(Math.log2(this.count))));
if (newLength !== this.length) {
@ -110,4 +123,13 @@ export class FastVertexArray {
getVertexData(): Float32Array {
return this.data;
}
destroy(): void {
this.data = null;
this.sprites = null;
this.freeSlots = null;
this.lastSlot = 0;
this.dirty = false;
this.destroyed = true;
}
}

View File

@ -116,7 +116,7 @@ export class BlockViewComponent implements OnInit, OnDestroy {
this.isLoadingBlock = false;
this.isLoadingOverview = true;
}),
shareReplay(1)
shareReplay({ bufferSize: 1, refCount: true })
);
this.overviewSubscription = block$.pipe(
@ -176,5 +176,8 @@ export class BlockViewComponent implements OnInit, OnDestroy {
if (this.queryParamsSubscription) {
this.queryParamsSubscription.unsubscribe();
}
if (this.blockGraph) {
this.blockGraph.destroy();
}
}
}

View File

@ -117,7 +117,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
this.openGraphService.waitOver('block-data-' + this.rawId);
}),
throttleTime(50, asyncScheduler, { leading: true, trailing: true }),
shareReplay(1)
shareReplay({ bufferSize: 1, refCount: true })
);
this.overviewSubscription = block$.pipe(

View File

@ -1,8 +1,8 @@
import { Component, OnInit, OnDestroy, ViewChildren, QueryList, ChangeDetectorRef } from '@angular/core';
import { Location } from '@angular/common';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { ActivatedRoute, ParamMap, Params, Router } from '@angular/router';
import { ElectrsApiService } from '@app/services/electrs-api.service';
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, filter } from 'rxjs/operators';
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, filter, take } from 'rxjs/operators';
import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest, forkJoin } from 'rxjs';
import { StateService } from '@app/services/state.service';
import { SeoService } from '@app/services/seo.service';
@ -68,6 +68,7 @@ export class BlockComponent implements OnInit, OnDestroy {
paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
numUnexpected: number = 0;
mode: 'projected' | 'actual' = 'projected';
currentQueryParams: Params;
overviewSubscription: Subscription;
accelerationsSubscription: Subscription;
@ -80,8 +81,8 @@ export class BlockComponent implements OnInit, OnDestroy {
timeLtr: boolean;
childChangeSubscription: Subscription;
auditPrefSubscription: Subscription;
isAuditEnabledSubscription: Subscription;
oobSubscription: Subscription;
priceSubscription: Subscription;
blockConversion: Price;
@ -118,7 +119,7 @@ export class BlockComponent implements OnInit, OnDestroy {
this.setAuditAvailable(this.auditSupported);
if (this.auditSupported) {
this.isAuditEnabledFromParam().subscribe(auditParam => {
this.isAuditEnabledSubscription = this.isAuditEnabledFromParam().subscribe(auditParam => {
if (this.auditParamEnabled) {
this.auditModeEnabled = auditParam;
} else {
@ -281,7 +282,7 @@ export class BlockComponent implements OnInit, OnDestroy {
}
}),
throttleTime(300, asyncScheduler, { leading: true, trailing: true }),
shareReplay(1)
shareReplay({ bufferSize: 1, refCount: true })
);
this.overviewSubscription = this.block$.pipe(
@ -363,6 +364,7 @@ export class BlockComponent implements OnInit, OnDestroy {
.subscribe((network) => this.network = network);
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
this.currentQueryParams = params;
if (params.showDetails === 'true') {
this.showDetails = true;
} else {
@ -414,6 +416,7 @@ export class BlockComponent implements OnInit, OnDestroy {
ngOnDestroy(): void {
this.stateService.markBlock$.next({});
this.overviewSubscription?.unsubscribe();
this.accelerationsSubscription?.unsubscribe();
this.keyNavigationSubscription?.unsubscribe();
this.blocksSubscription?.unsubscribe();
this.cacheBlocksSubscription?.unsubscribe();
@ -421,8 +424,16 @@ export class BlockComponent implements OnInit, OnDestroy {
this.queryParamsSubscription?.unsubscribe();
this.timeLtrSubscription?.unsubscribe();
this.childChangeSubscription?.unsubscribe();
this.priceSubscription?.unsubscribe();
this.auditPrefSubscription?.unsubscribe();
this.isAuditEnabledSubscription?.unsubscribe();
this.oobSubscription?.unsubscribe();
this.priceSubscription?.unsubscribe();
this.blockGraphProjected.forEach(graph => {
graph.destroy();
});
this.blockGraphActual.forEach(graph => {
graph.destroy();
});
}
// TODO - Refactor this.fees/this.reward for liquid because it is not
@ -733,19 +744,18 @@ export class BlockComponent implements OnInit, OnDestroy {
toggleAuditMode(): void {
this.stateService.hideAudit.next(this.auditModeEnabled);
this.route.queryParams.subscribe(params => {
const queryParams = { ...params };
delete queryParams['audit'];
const queryParams = { ...this.currentQueryParams };
delete queryParams['audit'];
let newUrl = this.router.url.split('?')[0];
const queryString = new URLSearchParams(queryParams).toString();
if (queryString) {
newUrl += '?' + queryString;
}
this.location.replaceState(newUrl);
});
let newUrl = this.router.url.split('?')[0];
const queryString = new URLSearchParams(queryParams).toString();
if (queryString) {
newUrl += '?' + queryString;
}
this.location.replaceState(newUrl);
// avoid duplicate subscriptions
this.auditPrefSubscription?.unsubscribe();
this.auditPrefSubscription = this.stateService.hideAudit.subscribe((hide) => {
this.auditModeEnabled = !hide;
this.showAudit = this.auditAvailable && this.auditModeEnabled;
@ -762,7 +772,7 @@ export class BlockComponent implements OnInit, OnDestroy {
return this.route.queryParams.pipe(
map(params => {
this.auditParamEnabled = 'audit' in params;
return this.auditParamEnabled ? !(params['audit'] === 'false') : true;
})
);

View File

@ -281,9 +281,11 @@
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card">
<div class="card-body">
<span class="title-link">
<a class="title-link mb-0" style="margin-top: -2px" href="" [routerLink]="['/wallet/' + widget.props.wallet | relativeUrl]">
<h5 class="card-title d-inline" i18n="dashboard.treasury-transactions">Treasury Transactions</h5>
</span>
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
</a>
<app-address-transactions-widget [addressSummary$]="walletSummary$"></app-address-transactions-widget>
</div>
</div>

View File

@ -162,6 +162,9 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
this.cacheBlocksSubscription?.unsubscribe();
this.networkChangedSubscription?.unsubscribe();
this.queryParamsSubscription?.unsubscribe();
this.blockGraphs.forEach(graph => {
graph.destroy();
});
}
shiftTestBlocks(): void {

View File

@ -19,12 +19,10 @@
} @else if (!user) {
<!-- User not logged in -->
<div class="alert alert-mempool d-block text-center w-100">
<div class="d-inline align-middle">
<span>To use the faucet, please&nbsp;</span>
<a routerLink="/login" [queryParams]="{'redirectTo': '/testnet4/faucet'}">login</a>
<span class="mr-2">&nbsp;or</span>
<div class="d-inline align-middle pr-2">
<span>To use the faucet, please</span>
</div>
<app-twitter-login customClass="btn btn-sm" width="220px" redirectTo="/testnet4/faucet" buttonString="Sign up with Twitter"></app-twitter-login>
<app-github-login customClass="btn btn-sm" width="150px" redirectTo="/testnet4/faucet" buttonString="Sign up with"></app-github-login>
</div>
}
@else if (user && user.status === 'pending' && !user.email && user.snsId) {
@ -36,18 +34,18 @@
</div>
}
@else if (error === 'not_available') {
<!-- User logged in but not a paid user or did not link its Twitter account -->
<!-- User logged in but not a paid user or did not link its Github account -->
<div class="alert alert-mempool d-block text-center w-100">
<div class="d-inline align-middle">
<span class="mb-2 mr-2">To use the faucet, please</span>
</div>
<app-twitter-login customClass="btn btn-sm" width="180px" redirectTo="/testnet4/faucet" buttonString="Link your Twitter"></app-twitter-login>
<app-github-login customClass="btn btn-sm" width="180px" redirectTo="/testnet4/faucet" buttonString="Link your"></app-github-login>
</div>
}
@else if (error === 'account_limited') {
<div class="alert alert-mempool d-block text-center w-100">
<div class="d-inline align-middle">
<span class="mb-2 mr-2">Your Twitter account does not allow you to access the faucet</span>
<span class="mb-2 mr-2">Your account does not allow you to access the faucet</span>
</div>
</div>
}

View File

@ -24,7 +24,7 @@ export class FaucetComponent implements OnInit, OnDestroy {
min: number; // minimum amount to request at once (in sats)
max: number; // maximum amount to request at once
address?: string; // faucet address
code: 'ok' | 'faucet_not_available' | 'faucet_maximum_reached' | 'faucet_too_soon';
code: 'ok' | 'faucet_not_available' | 'faucet_maximum_reached' | 'faucet_too_soon' | 'faucet_not_available_no_utxo';
} | null = null;
faucetForm: FormGroup;

View File

@ -0,0 +1,6 @@
<a href="#" (click)="githubLogin()" [class]="(disabled ? 'disabled': '') + (customClass ? customClass : 'w-100 btn mt-1 d-flex justify-content-center align-items-center')" style="background-color: rgb(31, 35, 40)" [style]="width ? 'width: ' + width : ''">
<span class="ml-2 text-light align-middle">{{ buttonString }}</span>
<svg height="32" viewBox="0 0 18 16" width="32" style="fill: white; padding-left: 5px">
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
</svg>
</a>

View File

@ -0,0 +1,25 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-github-login',
templateUrl: './github-login.component.html',
})
export class GithubLogin {
@Input() width: string | null = null;
@Input() customClass: string | null = null;
@Input() buttonString: string= 'unset';
@Input() redirectTo: string | null = null;
@Output() clicked = new EventEmitter<boolean>();
@Input() disabled: boolean = false;
constructor() {}
githubLogin() {
this.clicked.emit(true);
if (this.redirectTo) {
location.replace(`/api/v1/services/auth/login/github?redirectTo=${encodeURIComponent(this.redirectTo)}`);
} else {
location.replace(`/api/v1/services/auth/login/github?redirectTo=${location.href}`);
}
return false;
}
}

View File

@ -4,9 +4,8 @@
<nav class="navbar navbar-expand-md navbar-dark">
<!-- Hamburger -->
<ng-container *ngIf="servicesEnabled">
<div *ngIf="user" class="profile_image_container" [class]="{'anon': !user.imageMd5}" (click)="hamburgerClick($event)">
<img *ngIf="user.imageMd5" [src]="'/api/v1/services/account/images/' + user.username + '/md5=' + user.imageMd5" class="profile_image">
<app-svg-images style="color: lightgrey; fill: lightgray" *ngIf="!user.imageMd5" name="anon"></app-svg-images>
<div *ngIf="user" class="profile_image_container" (click)="hamburgerClick($event)">
<img [src]="'/api/v1/services/account/images/' + user.username" class="profile_image" onError="this.src = '/resources/anon.svg'; this.className = 'anon'" />
</div>
<div *ngIf="false && user === null" class="profile_image_container" (click)="hamburgerClick($event)">
<app-svg-images name="hamburger" height="40"></app-svg-images>
@ -23,7 +22,7 @@
} @else {
<ng-template [ngIf]="subdomain && enterpriseInfo">
<div class="subdomain_container">
<img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
<img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo'" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
</div>
<div class="vertical-line"></div>
</ng-template>
@ -43,7 +42,7 @@
} @else {
<ng-template [ngIf]="subdomain && enterpriseInfo">
<div class="subdomain_container">
<img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
<img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo'" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
</div>
<div class="vertical-line"></div>
</ng-template>

View File

@ -269,7 +269,7 @@ nav {
text-align: center;
align-self: center;
cursor: pointer;
&.anon {
.anon {
border: 1.5px solid lightgrey;
color: lightgrey;
border-radius: 5px;

View File

@ -120,6 +120,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
}
ngOnDestroy(): void {
this.blockGraph?.destroy();
this.blockSub.unsubscribe();
this.timeLtrSubscription.unsubscribe();
this.websocketService.stopTrackMempoolBlock();

View File

@ -10,7 +10,7 @@
<h1 class="m-0 pt-1 pt-md-0">{{ poolStats.pool.name }}</h1>
</div>
<div class="box">
<div class="box pool-details">
<div class="row">
<div class="col-lg-6">
@ -173,7 +173,125 @@
<div class="spinner-border text-light"></div>
</div>
<!-- Stratum Job -->
<ng-container *ngIf="(job$ | async) as job;">
<h2 i18n="pool.next_block">Next block</h2>
<div class="box mb-3">
<div class="row" >
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td>
<table class="job-table table table-xs table-borderless table-fixed table-data">
<thead>
<tr>
<th class="data-title clip text-center height" i18n="latest-blocks.height">Height</th>
<th class="data-title clip text-center expected" i18n="next-block.expected-time">Expected</th>
<th class="data-title clip text-center reward" i18n="latest-blocks.reward">Reward</th>
<th class="data-title clip text-center timestamp" i18n="next-block.timestamp">Timestamp</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center height">
{{ job.height }}
</td>
<td class="text-center expected">
<ng-container *ngIf="(expectedBlockTime$ | async) as expectedBlockTime; else expectedPlaceholder">
<app-time kind="until" [time]="expectedBlockTime" [fastRender]="false" [fixedRender]="true" [precision]="1" minUnit="minute"></app-time>
</ng-container>
<ng-template #expectedPlaceholder>~</ng-template>
</td>
<td class="text-center reward">
<app-amount [satoshis]="job.reward"></app-amount>
</td>
<td class="text-center timestamp">
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="job.timestamp" [precision]="1" minUnit="minute" [hideTimeSince]="true"></app-timestamp>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td>
<table class="job-table table table-xs table-borderless table-fixed table-data">
<thead>
<tr>
<th class="data-title clip text-center coinbase" i18n="latest-blocks.coinbasetag">Coinbase tag</th>
<th class="data-title clip text-center clean" i18n="next-block.clean">Clean</th>
<th class="data-title clip text-center prevhash" i18n="next-block.prevhash">Prevhash</th>
<th class="data-title clip text-center job-received" i18n="next-block.job-received">Job Received</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-center coinbase">
{{ job.scriptsig | hex2ascii }}
</td>
<td class="text-center clean">
@if (job.cleanJobs) {
<fa-icon [icon]="['fas', 'check-circle']" [fixedWidth]="true"></fa-icon>
} @else {
<fa-icon [icon]="['fas', 'times-circle']" [fixedWidth]="true"></fa-icon>
}
</td>
<td class="text-center prevhash">
<a [routerLink]="['/block' | relativeUrl, job.prevHash]">
<app-truncate [text]="job.prevHash" [lastChars]="8"></app-truncate>
</a>
</td>
<td class="text-center job-received">
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="job.received / 1000" [precision]="1" minUnit="minute" [hideTimeSince]="true"></app-timestamp>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td>
<table class="stratum-table">
<thead>
<tr>
<th class="data-title clip text-center" [attr.colspan]="Math.max(job.merkleBranches.length, 12)">
<a class="title-link" href="" [routerLink]="['/stratum' | relativeUrl]">
Merkle Branches
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
</a>
</th>
</tr>
</thead>
<tbody>
<tr>
@for (branch of job.merkleBranches; track $index) {
<td class="merkle" [style.background-color]="branch ? '#' + branch.slice(0, 6) : ''">
@if ($index === 0 && branch) {
<a [routerLink]="['/tx' | relativeUrl, reverseHash(branch)]" class="cell-link">
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 14px; color: white"></fa-icon>
</a>
}
</td>
}
@for (_ of [].constructor(Math.max(0, 12 - job.merkleBranches.length)); track $index) {
<td class="merkle empty-branch"></td>
}
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</ng-container>
<!-- Blocks list -->
<h2 i18n="master-page.blocks">Blocks</h2>
<table class="table table-borderless" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5"
[infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="loadMore()">
<ng-container *ngIf="blocks$ | async as blocks; else skeleton">

View File

@ -49,111 +49,110 @@ div.scrollable {
max-height: 75px;
}
.box {
padding-bottom: 5px;
.pool-details {
@media (min-width: 767.98px) {
min-height: 187px;
}
}
.label {
width: 25%;
@media (min-width: 767.98px) {
vertical-align: middle;
.label {
width: 25%;
@media (min-width: 767.98px) {
vertical-align: middle;
}
@media (max-width: 767.98px) {
font-weight: bold;
}
}
@media (max-width: 767.98px) {
font-weight: bold;
.label.addresses {
vertical-align: top;
padding-top: 25px;
}
.addresses-data {
vertical-align: top;
font-family: monospace;
font-size: 14px;
}
}
.label.addresses {
vertical-align: top;
padding-top: 25px;
}
.addresses-data {
vertical-align: top;
font-family: monospace;
font-size: 14px;
}
.data {
text-align: right;
padding-left: 5%;
@media (max-width: 992px) {
text-align: left;
padding-left: 12px;
}
@media (max-width: 450px) {
.data {
text-align: right;
padding-left: 5%;
@media (max-width: 992px) {
text-align: left;
padding-left: 12px;
}
@media (max-width: 450px) {
text-align: right;
}
}
}
.progress {
background-color: var(--secondary);
}
.progress {
background-color: var(--secondary);
}
.coinbase {
width: 20%;
@media (max-width: 875px) {
display: none;
}
}
.height {
width: 10%;
}
.timestamp {
@media (max-width: 875px) {
padding-left: 50px;
}
@media (max-width: 685px) {
display: none;
}
}
.mined {
width: 13%;
@media (max-width: 1100px) {
display: none;
}
}
.txs {
padding-right: 40px;
@media (max-width: 1100px) {
padding-right: 10px;
}
@media (max-width: 875px) {
padding-right: 20px;
}
@media (max-width: 567px) {
padding-right: 10px;
}
}
.size {
width: 12%;
@media (max-width: 1000px) {
width: 15%;
}
@media (max-width: 875px) {
.coinbase {
width: 20%;
@media (max-width: 875px) {
display: none;
}
}
@media (max-width: 650px) {
width: 20%;
}
@media (max-width: 450px) {
display: none;
}
}
.scriptmessage {
overflow: hidden;
display: inline-block;
text-overflow: ellipsis;
vertical-align: middle;
width: auto;
text-align: left;
.height {
width: 10%;
}
.timestamp {
@media (max-width: 875px) {
padding-left: 50px;
}
@media (max-width: 685px) {
display: none;
}
}
.mined {
width: 13%;
@media (max-width: 1100px) {
display: none;
}
}
.txs {
padding-right: 40px;
@media (max-width: 1100px) {
padding-right: 10px;
}
@media (max-width: 875px) {
padding-right: 20px;
}
@media (max-width: 567px) {
padding-right: 10px;
}
}
.size {
width: 12%;
@media (max-width: 1000px) {
width: 15%;
}
@media (max-width: 875px) {
width: 20%;
}
@media (max-width: 650px) {
width: 20%;
}
@media (max-width: 450px) {
display: none;
}
}
.scriptmessage {
overflow: hidden;
display: inline-block;
text-overflow: ellipsis;
vertical-align: middle;
width: auto;
text-align: left;
}
}
.skeleton-loader {
@ -214,4 +213,69 @@ div.scrollable {
.taller-row {
height: 75px;
}
.stratum-table {
width: 100%;
.merkle {
width: 100px;
text-align: center;
}
.empty-branch {
outline: solid 1px white;
outline-offset: -1px;
&::after {
content: "";
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
background: linear-gradient(to top left, transparent, transparent 48%, white 49%, white 51%, transparent 52%, transparent);
}
}
td {
position: relative;
height: 2em;
.cell-link {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
color: inherit;
text-decoration: none;
display: flex;
justify-content: center;
align-items: center;
}
}
}
.job-table {
td, th {
width: 25%;
max-width: 25%;
min-width: 25%;
overflow: hidden;
text-overflow: ellipsis;
padding: 0.1rem 0.2rem;
}
@media (max-width: 767.98px) {
.expected, .timestamp, .clean, .job-received {
display: none;
}
}
}
.title-link, .title-link:hover, .title-link:focus, .title-link:active {
display: block;
text-decoration: none;
color: inherit;
}

View File

@ -10,6 +10,9 @@ import { selectPowerOfTen } from '@app/bitcoin.utils';
import { formatNumber } from '@angular/common';
import { SeoService } from '@app/services/seo.service';
import { HttpErrorResponse } from '@angular/common/http';
import { StratumJob } from '../../interfaces/websocket.interface';
import { WebsocketService } from '../../services/websocket.service';
import { MiningService } from '../../services/mining.service';
interface AccelerationTotal {
cost: number,
@ -27,12 +30,16 @@ export class PoolComponent implements OnInit {
@Input() left: number | string = 75;
gfg = true;
stratumEnabled = this.stateService.env.STRATUM_ENABLED;
formatNumber = formatNumber;
Math = Math;
slugSubscription: Subscription;
poolStats$: Observable<PoolStat>;
blocks$: Observable<BlockExtended[]>;
oobFees$: Observable<AccelerationTotal[]>;
job$: Observable<StratumJob | null>;
expectedBlockTime$: Observable<number>;
isLoading = true;
error: HttpErrorResponse | null = null;
@ -53,6 +60,8 @@ export class PoolComponent implements OnInit {
private apiService: ApiService,
private route: ActivatedRoute,
public stateService: StateService,
private websocketService: WebsocketService,
private miningService: MiningService,
private seoService: SeoService,
) {
this.auditAvailable = this.stateService.env.AUDIT;
@ -62,7 +71,7 @@ export class PoolComponent implements OnInit {
this.slugSubscription = this.route.params.pipe(map((params) => params.slug)).subscribe((slug) => {
this.isLoading = true;
this.blocks = [];
this.chartOptions = {};
this.chartOptions = {};
this.slug = slug;
this.initializeObservables();
});
@ -129,6 +138,31 @@ export class PoolComponent implements OnInit {
}),
filter(oob => oob.length === 3 && oob[2].count > 0)
);
if (this.stratumEnabled) {
this.job$ = combineLatest([
this.poolStats$.pipe(
tap((poolStats) => {
this.websocketService.startTrackStratum(poolStats.pool.unique_id);
})
),
this.stateService.stratumJobs$
]).pipe(
map(([poolStats, jobs]) => {
return jobs[poolStats.pool.unique_id];
})
);
this.expectedBlockTime$ = combineLatest([
this.miningService.getMiningStats('1w'),
this.poolStats$,
this.stateService.difficultyAdjustment$
]).pipe(
map(([miningStats, poolStat, da]) => {
return (da.timeAvg / ((poolStat.estimatedHashrate || 0) / (miningStats.lastEstimatedHashrate * 1_000_000_000_000_000_000))) + Date.now() + da.timeOffset;
})
);
}
}
prepareChartOptions(hashrate, share) {
@ -327,6 +361,10 @@ export class PoolComponent implements OnInit {
return block.height;
}
reverseHash(hash: string) {
return hash.match(/../g).reverse().join('');
}
ngOnDestroy(): void {
this.slugSubscription.unsubscribe();
}

View File

@ -82,6 +82,10 @@ export class ServerHealthComponent implements OnInit {
return '🇺🇸';
} else if (host.includes('.va1.')) {
return '🇺🇸';
} else if (host.includes('.sg1.')) {
return '🇸🇬';
} else if (host.includes('.hnl.')) {
return '🤙';
} else {
return '';
}

View File

@ -194,14 +194,16 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy {
applyScrollLeft(): void {
if (this.blockchainContainer?.nativeElement?.scrollWidth) {
let lastScrollLeft = null;
while (this.scrollLeft < 0 && this.shiftPagesForward() && lastScrollLeft !== this.scrollLeft) {
lastScrollLeft = this.scrollLeft;
this.scrollLeft += this.pageWidth;
}
lastScrollLeft = null;
while (this.scrollLeft > this.blockchainContainer.nativeElement.scrollWidth && this.shiftPagesBack() && lastScrollLeft !== this.scrollLeft) {
lastScrollLeft = this.scrollLeft;
this.scrollLeft -= this.pageWidth;
if (!this.timeLtr) {
while (this.scrollLeft < 0 && this.shiftPagesForward() && lastScrollLeft !== this.scrollLeft) {
lastScrollLeft = this.scrollLeft;
this.scrollLeft += this.pageWidth;
}
lastScrollLeft = null;
while (this.scrollLeft > this.blockchainContainer.nativeElement.scrollWidth && this.shiftPagesBack() && lastScrollLeft !== this.scrollLeft) {
lastScrollLeft = this.scrollLeft;
this.scrollLeft -= this.pageWidth;
}
}
this.blockchainContainer.nativeElement.scrollLeft = this.scrollLeft;
}

View File

@ -0,0 +1,55 @@
<div class="container-xl" style="min-height: 335px">
<h1 class="float-left" i18n="master-page.blocks">Stratum Jobs</h1>
<div class="clearfix"></div>
<div style="min-height: 295px">
<table *ngIf="poolsReady && (rows$ | async) as rows;" class="stratum-table">
<thead>
<tr>
<td class="merkle" [attr.colspan]="rows[0]?.merkleCells?.length || 4">
Merkle Branches
</td>
<td class="pool">Pool</td>
<td class="tag">Coinbase Tag</td>
<td class="reward">Reward</td>
<td class="height">Height</td>
</tr>
</thead>
<tbody>
@for (row of rows; track row.job.pool) {
<tr>
@for (cell of row.merkleCells; track $index) {
<td class="merkle" [style.background-color]="cell.hash ? '#' + cell.hash.slice(0, 6) : ''">
@if ($index === 0 && cell.hash) {
<a [routerLink]="['/tx' | relativeUrl, reverseHash(cell.hash)]" class="cell-link">
<div class="pipe-segment" [class]="pipeToClass(cell.type)"></div>
</a>
} @else {
<div class="pipe-segment" [class]="pipeToClass(cell.type)"></div>
}
</td>
}
<td class="pool">
@if (pools[row.job.pool]) {
<a class="badge" [routerLink]="[('/mining/pool/' + pools[row.job.pool].slug) | relativeUrl]">
<img class="pool-logo" [src]="'/resources/mining-pools/' + pools[row.job.pool].slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + pools[row.job.pool].name + ' mining pool'">
{{ pools[row.job.pool].name}}
</a>
}
</td>
<td class="tag">
{{ row.job.tag }}
</td>
<td class="reward">
<app-amount [satoshis]="row.job.reward"></app-amount>
</td>
<td class="height">
{{ row.job.height }}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,138 @@
.stratum-table {
width: 100%;
}
td {
position: relative;
height: 2em;
&.height, &.reward, &.tag {
padding: 0 5px;
}
&.tag {
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&.pool {
padding-left: 5px;
padding-right: 20px;
}
&.merkle {
width: 100px;
.pipe-segment {
position: absolute;
border-color: white;
box-sizing: content-box;
&.vertical {
top: 0;
right: 0;
width: 50%;
height: 100%;
border-left: solid 4px;
}
&.horizontal {
bottom: 0;
left: 0;
width: 100%;
height: 50%;
border-top: solid 4px;
}
&.branch-top {
bottom: 0;
right: 0;
width: 100%;
height: 50%;
border-top: solid 4px;
&::after {
content: "";
position: absolute;
box-sizing: content-box;
top: -4px;
right: 0px;
bottom: 0;
width: 50%;
border-top: solid 4px;
border-left: solid 4px;
border-top-left-radius: 5px;
}
}
&.branch-mid {
bottom: 0;
right: 0px;
width: 50%;
height: 100%;
border-left: solid 4px;
&::after {
content: "";
position: absolute;
box-sizing: content-box;
top: -4px;
left: -4px;
width: 100%;
height: 50%;
border-bottom: solid 4px;
border-left: solid 4px;
border-bottom-left-radius: 5px;
}
}
&.branch-end {
top: -4px;
right: 0;
width: 50%;
height: 50%;
border-bottom-left-radius: 5px;
border-bottom: solid 4px;
border-left: solid 4px;
}
}
}
.cell-link {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
color: inherit;
text-decoration: none;
}
}
@media (max-width: 800px) {
.stratum-table {
td {
&.tag {
display: none;
}
}
}
}
@media (max-width: 650px) {
.stratum-table {
td {
&.reward {
display: none;
}
}
}
}
.badge {
position: relative;
color: #FFF;
}
.pool-logo {
width: 15px;
height: 15px;
position: relative;
top: -1px;
margin-right: 2px;
}

View File

@ -0,0 +1,230 @@
import { Component, OnInit, ChangeDetectionStrategy, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { StateService } from '../../../services/state.service';
import { WebsocketService } from '../../../services/websocket.service';
import { map, Observable } from 'rxjs';
import { StratumJob } from '../../../interfaces/websocket.interface';
import { MiningService } from '../../../services/mining.service';
import { SinglePoolStats } from '../../../interfaces/node-api.interface';
type MerkleCellType = ' ' | '┬' | '├' | '└' | '│' | '─' | 'leaf';
interface TaggedStratumJob extends StratumJob {
tag: string;
merkleBranchIds: string[];
}
interface MerkleCell {
hash: string;
type: MerkleCellType;
job?: TaggedStratumJob;
}
interface MerkleTree {
hash?: string;
job: string;
size: number;
children?: MerkleTree[];
}
interface PoolRow {
job: TaggedStratumJob;
merkleCells: MerkleCell[];
}
function parseTag(scriptSig: string): string {
const hex = scriptSig.slice(8).replace(/6d6d.{64}/, '');
const bytes: number[] = [];
for (let i = 0; i < hex.length; i += 2) {
bytes.push(parseInt(hex.substr(i, 2), 16));
}
// eslint-disable-next-line no-control-regex
const ascii = new TextDecoder('utf8').decode(Uint8Array.from(bytes)).replace(/\uFFFD/g, '').replace(/\\0/g, '').replace(/[\x00-\x1F\x7F-\x9F]/g, '');
if (ascii.includes('/ViaBTC/')) {
return '/ViaBTC/';
} else if (ascii.includes('SpiderPool/')) {
return 'SpiderPool/';
}
return (ascii.match(/\/.*\//)?.[0] || ascii).trim();
}
function getMerkleBranchIds(merkleBranches: string[], numBranches: number, poolId: number): string[] {
let lastHash = '';
const ids: string[] = [];
for (let i = 0; i < numBranches; i++) {
if (merkleBranches[i]) {
lastHash = merkleBranches[i];
ids.push(`${i}-${lastHash}`);
} else {
ids.push(`${i}-${lastHash}-${poolId}`);
}
}
return ids;
}
@Component({
selector: 'app-stratum-list',
templateUrl: './stratum-list.component.html',
styleUrls: ['./stratum-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StratumList implements OnInit, OnDestroy {
rows$: Observable<PoolRow[]>;
pools: { [id: number]: SinglePoolStats } = {};
poolsReady: boolean = false;
constructor(
private stateService: StateService,
private websocketService: WebsocketService,
private miningService: MiningService,
private cd: ChangeDetectorRef,
) {}
ngOnInit(): void {
this.websocketService.want(['stats', 'blocks', 'mempool-blocks']);
this.miningService.getPools().subscribe(pools => {
this.pools = {};
for (const pool of pools) {
this.pools[pool.unique_id] = pool;
}
this.poolsReady = true;
this.cd.markForCheck();
});
this.rows$ = this.stateService.stratumJobs$.pipe(
map((jobs) => this.processJobs(jobs)),
);
this.websocketService.startTrackStratum('all');
}
processJobs(rawJobs: Record<string, StratumJob>): PoolRow[] {
const numBranches = Math.max(...Object.values(rawJobs).map(job => job.merkleBranches.length));
const jobs: Record<string, TaggedStratumJob> = {};
for (const [id, job] of Object.entries(rawJobs)) {
jobs[id] = { ...job, tag: parseTag(job.scriptsig), merkleBranchIds: getMerkleBranchIds(job.merkleBranches, numBranches, job.pool) };
}
if (Object.keys(jobs).length === 0) {
return [];
}
let trees: MerkleTree[] = Object.keys(jobs).map(job => ({
job,
size: 1,
}));
// build tree from bottom up
for (let col = numBranches - 1; col >= 0; col--) {
const groups: Record<string, MerkleTree[]> = {};
for (const tree of trees) {
const branchId = jobs[tree.job].merkleBranchIds[col];
if (!groups[branchId]) {
groups[branchId] = [];
}
groups[branchId].push(tree);
}
trees = Object.values(groups).map(group => ({
hash: jobs[group[0].job].merkleBranches[col],
job: group[0].job,
children: group,
size: group.reduce((acc, tree) => acc + tree.size, 0),
}));
}
// initialize grid of cells
const rows: (MerkleCell | null)[][] = [];
for (let i = 0; i < Object.keys(jobs).length; i++) {
const row: (MerkleCell | null)[] = [];
for (let j = 0; j <= numBranches; j++) {
row.push(null);
}
rows.push(row);
}
// fill in the cells
let colTrees = [trees.sort((a, b) => {
if (a.size !== b.size) {
return b.size - a.size;
}
return a.job.localeCompare(b.job);
})];
for (let col = 0; col <= numBranches; col++) {
let row = 0;
const nextTrees: MerkleTree[][] = [];
for (let g = 0; g < colTrees.length; g++) {
for (let t = 0; t < colTrees[g].length; t++) {
const tree = colTrees[g][t];
const isFirstTree = (t === 0);
const isLastTree = (t === colTrees[g].length - 1);
for (let i = 0; i < tree.size; i++) {
const isFirstCell = (i === 0);
const isLeaf = (col === numBranches);
rows[row][col] = {
hash: tree.hash,
job: isLeaf ? jobs[tree.job] : undefined,
type: 'leaf',
};
if (col > 0) {
rows[row][col - 1].type = getCellType(isFirstCell, isFirstTree, isLastTree);
}
row++;
}
if (tree.children) {
nextTrees.push(tree.children.sort((a, b) => {
if (a.size !== b.size) {
return b.size - a.size;
}
return a.job.localeCompare(b.job);
}));
}
}
}
colTrees = nextTrees;
}
return rows.map(row => ({
job: row[row.length - 1].job,
merkleCells: row.slice(0, -1),
}));
}
pipeToClass(type: MerkleCellType): string {
return {
' ': 'empty',
'┬': 'branch-top',
'├': 'branch-mid',
'└': 'branch-end',
'│': 'vertical',
'─': 'horizontal',
'leaf': 'leaf'
}[type];
}
reverseHash(hash: string) {
return hash.match(/../g).reverse().join('');
}
ngOnDestroy(): void {
this.websocketService.stopTrackStratum();
}
}
function getCellType(isFirstCell, isFirstTree, isLastTree): MerkleCellType {
if (isFirstCell) {
if (isFirstTree) {
if (isLastTree) {
return '─';
} else {
return '┬';
}
} else if (isLastTree) {
return '└';
} else {
return '├';
}
} else {
if (isLastTree) {
return ' ';
} else {
return '│';
}
}
}

View File

@ -1,4 +1,47 @@
<ng-container [ngSwitch]="name">
<ng-container *ngSwitchCase="'VISA'">
<svg xmlns="http://www.w3.org/2000/svg" [attr.width]="width" [attr.height]="height" viewBox="0 0 576 512" fill="white" xmlns="http://www.w3.org/2000/svg">
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
<path d="M470.1 231.3s7.6 37.2 9.3 45H446c3.3-8.9 16-43.5 16-43.5-.2 .3 3.3-9.1 5.3-14.9l2.8 13.4zM576 80v352c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V80c0-26.5 21.5-48 48-48h480c26.5 0 48 21.5 48 48zM152.5 331.2L215.7 176h-42.5l-39.3 106-4.3-21.5-14-71.4c-2.3-9.9-9.4-12.7-18.2-13.1H32.7l-.7 3.1c15.8 4 29.9 9.8 42.2 17.1l35.8 135h42.5zm94.4 .2L272.1 176h-40.2l-25.1 155.4h40.1zm139.9-50.8c.2-17.7-10.6-31.2-33.7-42.3-14.1-7.1-22.7-11.9-22.7-19.2 .2-6.6 7.3-13.4 23.1-13.4 13.1-.3 22.7 2.8 29.9 5.9l3.6 1.7 5.5-33.6c-7.9-3.1-20.5-6.6-36-6.6-39.7 0-67.6 21.2-67.8 51.4-.3 22.3 20 34.7 35.2 42.2 15.5 7.6 20.8 12.6 20.8 19.3-.2 10.4-12.6 15.2-24.1 15.2-16 0-24.6-2.5-37.7-8.3l-5.3-2.5-5.6 34.9c9.4 4.3 26.8 8.1 44.8 8.3 42.2 .1 69.7-20.8 70-53zM528 331.4L495.6 176h-31.1c-9.6 0-16.9 2.8-21 12.9l-59.7 142.5H426s6.9-19.2 8.4-23.3H486c1.2 5.5 4.8 23.3 4.8 23.3H528z"/>
</svg>
</ng-container>
<ng-container *ngSwitchCase="'MASTERCARD'">
<svg xmlns="http://www.w3.org/2000/svg" [attr.width]="width" [attr.height]="height" viewBox="0 0 576 512" fill="white" xmlns="http://www.w3.org/2000/svg">
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M482.9 410.3c0 6.8-4.6 11.7-11.2 11.7-6.8 0-11.2-5.2-11.2-11.7 0-6.5 4.4-11.7 11.2-11.7 6.6 0 11.2 5.2 11.2 11.7zm-310.8-11.7c-7.1 0-11.2 5.2-11.2 11.7 0 6.5 4.1 11.7 11.2 11.7 6.5 0 10.9-4.9 10.9-11.7-.1-6.5-4.4-11.7-10.9-11.7zm117.5-.3c-5.4 0-8.7 3.5-9.5 8.7h19.1c-.9-5.7-4.4-8.7-9.6-8.7zm107.8 .3c-6.8 0-10.9 5.2-10.9 11.7 0 6.5 4.1 11.7 10.9 11.7 6.8 0 11.2-4.9 11.2-11.7 0-6.5-4.4-11.7-11.2-11.7zm105.9 26.1c0 .3 .3 .5 .3 1.1 0 .3-.3 .5-.3 1.1-.3 .3-.3 .5-.5 .8-.3 .3-.5 .5-1.1 .5-.3 .3-.5 .3-1.1 .3-.3 0-.5 0-1.1-.3-.3 0-.5-.3-.8-.5-.3-.3-.5-.5-.5-.8-.3-.5-.3-.8-.3-1.1 0-.5 0-.8 .3-1.1 0-.5 .3-.8 .5-1.1 .3-.3 .5-.3 .8-.5 .5-.3 .8-.3 1.1-.3 .5 0 .8 0 1.1 .3 .5 .3 .8 .3 1.1 .5s.2 .6 .5 1.1zm-2.2 1.4c.5 0 .5-.3 .8-.3 .3-.3 .3-.5 .3-.8 0-.3 0-.5-.3-.8-.3 0-.5-.3-1.1-.3h-1.6v3.5h.8V426h.3l1.1 1.4h.8l-1.1-1.3zM576 81v352c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V81c0-26.5 21.5-48 48-48h480c26.5 0 48 21.5 48 48zM64 220.6c0 76.5 62.1 138.5 138.5 138.5 27.2 0 53.9-8.2 76.5-23.1-72.9-59.3-72.4-171.2 0-230.5-22.6-15-49.3-23.1-76.5-23.1-76.4-.1-138.5 62-138.5 138.2zm224 108.8c70.5-55 70.2-162.2 0-217.5-70.2 55.3-70.5 162.6 0 217.5zm-142.3 76.3c0-8.7-5.7-14.4-14.7-14.7-4.6 0-9.5 1.4-12.8 6.5-2.4-4.1-6.5-6.5-12.2-6.5-3.8 0-7.6 1.4-10.6 5.4V392h-8.2v36.7h8.2c0-18.9-2.5-30.2 9-30.2 10.2 0 8.2 10.2 8.2 30.2h7.9c0-18.3-2.5-30.2 9-30.2 10.2 0 8.2 10 8.2 30.2h8.2v-23zm44.9-13.7h-7.9v4.4c-2.7-3.3-6.5-5.4-11.7-5.4-10.3 0-18.2 8.2-18.2 19.3 0 11.2 7.9 19.3 18.2 19.3 5.2 0 9-1.9 11.7-5.4v4.6h7.9V392zm40.5 25.6c0-15-22.9-8.2-22.9-15.2 0-5.7 11.9-4.8 18.5-1.1l3.3-6.5c-9.4-6.1-30.2-6-30.2 8.2 0 14.3 22.9 8.3 22.9 15 0 6.3-13.5 5.8-20.7 .8l-3.5 6.3c11.2 7.6 32.6 6 32.6-7.5zm35.4 9.3l-2.2-6.8c-3.8 2.1-12.2 4.4-12.2-4.1v-16.6h13.1V392h-13.1v-11.2h-8.2V392h-7.6v7.3h7.6V416c0 17.6 17.3 14.4 22.6 10.9zm13.3-13.4h27.5c0-16.2-7.4-22.6-17.4-22.6-10.6 0-18.2 7.9-18.2 19.3 0 20.5 22.6 23.9 33.8 14.2l-3.8-6c-7.8 6.4-19.6 5.8-21.9-4.9zm59.1-21.5c-4.6-2-11.6-1.8-15.2 4.4V392h-8.2v36.7h8.2V408c0-11.6 9.5-10.1 12.8-8.4l2.4-7.6zm10.6 18.3c0-11.4 11.6-15.1 20.7-8.4l3.8-6.5c-11.6-9.1-32.7-4.1-32.7 15 0 19.8 22.4 23.8 32.7 15l-3.8-6.5c-9.2 6.5-20.7 2.6-20.7-8.6zm66.7-18.3H408v4.4c-8.3-11-29.9-4.8-29.9 13.9 0 19.2 22.4 24.7 29.9 13.9v4.6h8.2V392zm33.7 0c-2.4-1.2-11-2.9-15.2 4.4V392h-7.9v36.7h7.9V408c0-11 9-10.3 12.8-8.4l2.4-7.6zm40.3-14.9h-7.9v19.3c-8.2-10.9-29.9-5.1-29.9 13.9 0 19.4 22.5 24.6 29.9 13.9v4.6h7.9v-51.7zm7.6-75.1v4.6h.8V302h1.9v-.8h-4.6v.8h1.9zm6.6 123.8c0-.5 0-1.1-.3-1.6-.3-.3-.5-.8-.8-1.1-.3-.3-.8-.5-1.1-.8-.5 0-1.1-.3-1.6-.3-.3 0-.8 .3-1.4 .3-.5 .3-.8 .5-1.1 .8-.5 .3-.8 .8-.8 1.1-.3 .5-.3 1.1-.3 1.6 0 .3 0 .8 .3 1.4 0 .3 .3 .8 .8 1.1 .3 .3 .5 .5 1.1 .8 .5 .3 1.1 .3 1.4 .3 .5 0 1.1 0 1.6-.3 .3-.3 .8-.5 1.1-.8 .3-.3 .5-.8 .8-1.1 .3-.6 .3-1.1 .3-1.4zm3.2-124.7h-1.4l-1.6 3.5-1.6-3.5h-1.4v5.4h.8v-4.1l1.6 3.5h1.1l1.4-3.5v4.1h1.1v-5.4zm4.4-80.5c0-76.2-62.1-138.3-138.5-138.3-27.2 0-53.9 8.2-76.5 23.1 72.1 59.3 73.2 171.5 0 230.5 22.6 15 49.5 23.1 76.5 23.1 76.4 .1 138.5-61.9 138.5-138.4z"/>
</svg>
</ng-container>
<ng-container *ngSwitchCase="'JCB'">
<svg xmlns="http://www.w3.org/2000/svg" [attr.width]="width" [attr.height]="height" viewBox="0 0 576 512" fill="white" xmlns="http://www.w3.org/2000/svg">
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M431.5 244.3V212c41.2 0 38.5 .2 38.5 .2 7.3 1.3 13.3 7.3 13.3 16 0 8.8-6 14.5-13.3 15.8-1.2 .4-3.3 .3-38.5 .3zm42.8 20.2c-2.8-.7-3.3-.5-42.8-.5v35c39.6 0 40 .2 42.8-.5 7.5-1.5 13.5-8 13.5-17 0-8.7-6-15.5-13.5-17zM576 80v352c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V80c0-26.5 21.5-48 48-48h480c26.5 0 48 21.5 48 48zM182 192.3h-57c0 67.1 10.7 109.7-35.8 109.7-19.5 0-38.8-5.7-57.2-14.8v28c30 8.3 68 8.3 68 8.3 97.9 0 82-47.7 82-131.2zm178.5 4.5c-63.4-16-165-14.9-165 59.3 0 77.1 108.2 73.6 165 59.2V287C312.9 311.7 253 309 253 256s59.8-55.6 107.5-31.2v-28zM544 286.5c0-18.5-16.5-30.5-38-32v-.8c19.5-2.7 30.3-15.5 30.3-30.2 0-19-15.7-30-37-31 0 0 6.3-.3-120.3-.3v127.5h122.7c24.3 .1 42.3-12.9 42.3-33.2z"/>
</svg>
</ng-container>
<ng-container *ngSwitchCase="'DISCOVER'">
<svg xmlns="http://www.w3.org/2000/svg" [attr.width]="width" [attr.height]="height" viewBox="0 0 576 512" fill="white" xmlns="http://www.w3.org/2000/svg">
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M520.4 196.1c0-7.9-5.5-12.1-15.6-12.1h-4.9v24.9h4.7c10.3 0 15.8-4.4 15.8-12.8zM528 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h480c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-44.1 138.9c22.6 0 52.9-4.1 52.9 24.4 0 12.6-6.6 20.7-18.7 23.2l25.8 34.4h-19.6l-22.2-32.8h-2.2v32.8h-16zm-55.9 .1h45.3v14H444v18.2h28.3V217H444v22.2h29.3V253H428zm-68.7 0l21.9 55.2 22.2-55.2h17.5l-35.5 84.2h-8.6l-35-84.2zm-55.9-3c24.7 0 44.6 20 44.6 44.6 0 24.7-20 44.6-44.6 44.6-24.7 0-44.6-20-44.6-44.6 0-24.7 20-44.6 44.6-44.6zm-49.3 6.1v19c-20.1-20.1-46.8-4.7-46.8 19 0 25 27.5 38.5 46.8 19.2v19c-29.7 14.3-63.3-5.7-63.3-38.2 0-31.2 33.1-53 63.3-38zm-97.2 66.3c11.4 0 22.4-15.3-3.3-24.4-15-5.5-20.2-11.4-20.2-22.7 0-23.2 30.6-31.4 49.7-14.3l-8.4 10.8c-10.4-11.6-24.9-6.2-24.9 2.5 0 4.4 2.7 6.9 12.3 10.3 18.2 6.6 23.6 12.5 23.6 25.6 0 29.5-38.8 37.4-56.6 11.3l10.3-9.9c3.7 7.1 9.9 10.8 17.5 10.8zM55.4 253H32v-82h23.4c26.1 0 44.1 17 44.1 41.1 0 18.5-13.2 40.9-44.1 40.9zm67.5 0h-16v-82h16zM544 433c0 8.2-6.8 15-15 15H128c189.6-35.6 382.7-139.2 416-160zM74.1 191.6c-5.2-4.9-11.6-6.6-21.9-6.6H48v54.2h4.2c10.3 0 17-2 21.9-6.4 5.7-5.2 8.9-12.8 8.9-20.7s-3.2-15.5-8.9-20.5z"/>
</svg>
</ng-container>
<ng-container *ngSwitchCase="'DISCOVER_DINERS'">
<svg xmlns="http://www.w3.org/2000/svg" [attr.width]="width" [attr.height]="height" viewBox="0 0 576 512" fill="white" xmlns="http://www.w3.org/2000/svg">
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M239.7 79.9c-96.9 0-175.8 78.6-175.8 175.8 0 96.9 78.9 175.8 175.8 175.8 97.2 0 175.8-78.9 175.8-175.8 0-97.2-78.6-175.8-175.8-175.8zm-39.9 279.6c-41.7-15.9-71.4-56.4-71.4-103.8s29.7-87.9 71.4-104.1v207.9zm79.8 .3V151.6c41.7 16.2 71.4 56.7 71.4 104.1s-29.7 87.9-71.4 104.1zM528 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h480c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zM329.7 448h-90.3c-106.2 0-193.8-85.5-193.8-190.2C45.6 143.2 133.2 64 239.4 64h90.3c105 0 200.7 79.2 200.7 193.8 0 104.7-95.7 190.2-200.7 190.2z"/>
</svg>
</ng-container>
<ng-container *ngSwitchCase="'AMERICAN_EXPRESS'">
<svg xmlns="http://www.w3.org/2000/svg" [attr.width]="width" [attr.height]="height" viewBox="0 0 576 512" fill="white" xmlns="http://www.w3.org/2000/svg">
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M0 432c0 26.5 21.5 48 48 48H528c26.5 0 48-21.5 48-48v-1.1H514.3l-31.9-35.1-31.9 35.1H246.8V267.1H181L262.7 82.4h78.6l28.1 63.2V82.4h97.2L483.5 130l17-47.6H576V80c0-26.5-21.5-48-48-48H48C21.5 32 0 53.5 0 80V432zm440.4-21.7L482.6 364l42 46.3H576l-68-72.1 68-72.1H525.4l-42 46.7-41.5-46.7H390.5L458 338.6l-67.4 71.6V377.1h-83V354.9h80.9V322.6H307.6V300.2h83V267.1h-122V410.3H440.4zm96.3-72L576 380.2V296.9l-39.3 41.4zm-36.3-92l36.9-100.6V246.3H576V103H515.8l-32.2 89.3L451.7 103H390.5V246.1L327.3 103H276.1L213.7 246.3h43l11.9-28.7h65.9l12 28.7h82.7V146L466 246.3h34.4zM282 185.4l19.5-46.9 19.4 46.9H282z"/>
</svg>
</ng-container>
<ng-container *ngSwitchCase="'OTHER_BRAND'">
<svg xmlns="http://www.w3.org/2000/svg" [attr.width]="width" [attr.height]="height" viewBox="0 0 576 512" fill="white" xmlns="http://www.w3.org/2000/svg">
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M512 80c8.8 0 16 7.2 16 16l0 32L48 128l0-32c0-8.8 7.2-16 16-16l448 0zm16 144l0 192c0 8.8-7.2 16-16 16L64 432c-8.8 0-16-7.2-16-16l0-192 480 0zM64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l448 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64L64 32zm56 304c-13.3 0-24 10.7-24 24s10.7 24 24 24l48 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-48 0zm128 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l112 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-112 0z"/>
</svg>
</ng-container>
<ng-container *ngSwitchCase="'officialMempoolSpace'">
<svg [class]="class" [style]="style" [attr.width]="width" [attr.height]="height" [attr.viewBox]="viewBox" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M163.658 113.263C161.089 113.263 158.992 111.146 158.992 108.535C158.992 105.966 161.048 103.951 163.658 103.951C166.269 103.951 168.325 105.966 168.325 108.535C168.325 111.125 166.228 113.263 163.658 113.263Z" fill="#9857FF"/>

View File

@ -8,7 +8,7 @@
</a>
} @else if (enterpriseInfo?.img || enterpriseInfo?.imageMd5) {
<a [routerLink]="['/' | relativeUrl]">
<img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + enterpriseInfo.name + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
<img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + enterpriseInfo.name + '/logo'" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
</a>
<div class="vertical-line"></div>
}
@ -124,7 +124,6 @@
<ng-container *ngIf="(ETA$ | async) as eta;">
<app-accelerate-checkout
*ngIf="(da$ | async) as da;"
[cashappEnabled]="cashappEligible"
[advancedEnabled]="false"
[forceMobile]="true"
[tx]="tx"

View File

@ -756,10 +756,6 @@ export class TrackerComponent implements OnInit, OnDestroy {
}
}
get cashappEligible(): boolean {
return this.mempoolPosition?.block > 0 && this.tx.weight < 4000;
}
get showAccelerationSummary(): boolean {
return (
this.tx

View File

@ -24,6 +24,7 @@
[height]="tx?.status?.block_height"
[replaced]="replaced"
[removed]="this.rbfInfo?.mined && !this.tx?.status?.confirmed"
[cached]="isCached"
></app-confirmations>
</div>
</ng-container>
@ -81,7 +82,6 @@
<app-accelerate-checkout
*ngIf="(da$ | async) as da;"
[cashappEnabled]="cashappEligible"
[advancedEnabled]="true"
[tx]="tx"
[accelerating]="isAcceleration"

View File

@ -156,7 +156,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
showAccelerationDetails = false;
hasAccelerationDetails = false;
scrollIntoAccelPreview = false;
cashappEligible = false;
auditEnabled: boolean = this.stateService.env.AUDIT && this.stateService.env.BASE_MODULE === 'mempool' && this.stateService.env.MINING_DASHBOARD === true;
isMempoolSpaceBuild = this.stateService.isMempoolSpaceBuild;
@ -240,7 +239,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
retry({ count: 2, delay: 2000 }),
// Try again until we either get a valid response, or the transaction is confirmed
repeat({ delay: 2000 }),
filter((transactionTimes) => transactionTimes?.length && transactionTimes[0] > 0 && !this.tx.status?.confirmed),
filter((transactionTimes) => transactionTimes?.[0] > 0 || this.tx.status?.confirmed),
take(1),
)),
)
@ -528,9 +527,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.miningStats = stats;
});
}
if (txPosition.position?.block > 0 && this.tx.weight < 4000) {
this.cashappEligible = true;
}
if (!this.gotInitialPosition && txPosition.position?.block === 0 && txPosition.position?.vsize < 750_000) {
this.accelerationFlowCompleted = true;
}
@ -1036,7 +1032,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.showAccelerationDetails = false;
this.accelerationFlowCompleted = false;
this.accelerationInfo = null;
this.cashappEligible = false;
this.txInBlockIndex = null;
this.mempoolPosition = null;
this.pool = null;

View File

@ -206,12 +206,12 @@ export class TransactionsListComponent implements OnInit, OnChanges {
for (const address of this.addresses) {
switch (address.length) {
case 130: {
if (v.scriptpubkey === '21' + address + 'ac') {
if (v.scriptpubkey === '41' + address + 'ac') {
return v.value;
}
} break;
case 66: {
if (v.scriptpubkey === '41' + address + 'ac') {
if (v.scriptpubkey === '21' + address + 'ac') {
return v.value;
}
} break;
@ -228,12 +228,12 @@ export class TransactionsListComponent implements OnInit, OnChanges {
for (const address of this.addresses) {
switch (address.length) {
case 130: {
if (v.prevout?.scriptpubkey === '21' + address + 'ac') {
if (v.prevout?.scriptpubkey === '41' + address + 'ac') {
return v.prevout?.value;
}
} break;
case 66: {
if (v.prevout?.scriptpubkey === '41' + address + 'ac') {
if (v.prevout?.scriptpubkey === '21' + address + 'ac') {
return v.prevout?.value;
}
} break;

View File

@ -1,6 +1,6 @@
<a href="#" (click)="twitterLogin()"
[class]="(disabled ? 'disabled': '') + (customClass ? customClass : 'w-100 btn mt-1 d-flex justify-content-center align-items-center')"
style="background-color: #1DA1F2" [style]="width ? 'width: ' + width : ''">
<img src="./resources/twitter.svg" height="25" style="padding: 2px" [alt]="buttonString + ' with Twitter'" />
style="background-color: rgb(31, 35, 40)" [style]="width ? 'width: ' + width : ''">
<span class="ml-2 text-light align-middle">{{ buttonString }}</span>
<img src="./resources/x.svg" height="25" style="padding: 2px; padding-left: 5px" [alt]="buttonString + ' with Twitter'" />
</a>

View File

@ -1,6 +1,6 @@
<div class="container-xl" [class.liquid-address]="network === 'liquid' || network === 'liquidtestnet'">
<div class="title-address">
<h1 i18n="shared.wallet">Wallet</h1>
<h1>{{ walletName }}</h1>
</div>
<div class="clearfix"></div>
@ -74,6 +74,36 @@
</ng-container>
</ng-container>
<br>
<div class="title-tx">
<h2 class="text-left" i18n="address.transactions">Transactions</h2>
</div>
<app-transactions-list [transactions]="transactions" [showConfirmations]="true" [addresses]="addressStrings" (loadMore)="loadMore()"></app-transactions-list>
<div class="text-center">
<ng-template [ngIf]="isLoadingTransactions">
<div class="header-bg box">
<div class="row" style="height: 107px;">
<div class="col-sm">
<span class="skeleton-loader"></span>
</div>
<div class="col-sm">
<span class="skeleton-loader"></span>
</div>
</div>
</div>
</ng-template>
<ng-template [ngIf]="retryLoadMore">
<br>
<button type="button" class="btn btn-outline-info btn-sm" (click)="loadMore()"><fa-icon [icon]="['fas', 'redo-alt']" [fixedWidth]="true"></fa-icon></button>
</ng-template>
</div>
<ng-template #loadingTemplate>
<div class="box" *ngIf="!error; else errorTemplate">

View File

@ -9,6 +9,8 @@ import { of, Observable, Subscription } from 'rxjs';
import { SeoService } from '@app/services/seo.service';
import { seoDescriptionNetwork } from '@app/shared/common.utils';
import { WalletAddress } from '@interfaces/node-api.interface';
import { ElectrsApiService } from '@app/services/electrs-api.service';
import { AudioService } from '@app/services/audio.service';
class WalletStats implements ChainStats {
addresses: string[];
@ -24,6 +26,7 @@ class WalletStats implements ChainStats {
acc.funded_txo_sum += stat.funded_txo_sum;
acc.spent_txo_count += stat.spent_txo_count;
acc.spent_txo_sum += stat.spent_txo_sum;
acc.tx_count += stat.tx_count;
return acc;
}, {
funded_txo_count: 0,
@ -109,12 +112,17 @@ export class WalletComponent implements OnInit, OnDestroy {
addressStrings: string[] = [];
walletName: string;
isLoadingWallet = true;
isLoadingTransactions = true;
transactions: Transaction[];
totalTransactionCount: number;
retryLoadMore = false;
wallet$: Observable<Record<string, WalletAddress>>;
walletAddresses$: Observable<Record<string, Address>>;
walletSummary$: Observable<AddressTxSummary[]>;
walletStats$: Observable<WalletStats>;
error: any;
walletSubscription: Subscription;
transactionSubscription: Subscription;
collapseAddresses: boolean = true;
@ -129,6 +137,8 @@ export class WalletComponent implements OnInit, OnDestroy {
private websocketService: WebsocketService,
private stateService: StateService,
private apiService: ApiService,
private electrsApiService: ElectrsApiService,
private audioService: AudioService,
private seoService: SeoService,
) { }
@ -172,6 +182,21 @@ export class WalletComponent implements OnInit, OnDestroy {
}),
switchMap(initial => this.stateService.walletTransactions$.pipe(
startWith(null),
tap((transactions) => {
if (!transactions?.length) {
return;
}
for (const transaction of transactions) {
const tx = this.transactions.find((t) => t.txid === transaction.txid);
if (tx) {
tx.status = transaction.status;
} else {
this.transactions.unshift(transaction);
}
}
this.transactions = this.transactions.slice();
this.audioService.playSound('magic');
}),
scan((wallet, walletTransactions) => {
for (const tx of (walletTransactions || [])) {
const funded: Record<string, number> = {};
@ -267,8 +292,57 @@ export class WalletComponent implements OnInit, OnDestroy {
return stats;
}, walletStats),
);
}),
})
);
this.transactionSubscription = this.wallet$.pipe(
switchMap(wallet => {
const addresses = Object.keys(wallet).map(addr => this.normalizeAddress(addr));
return this.electrsApiService.getAddressesTransactions$(addresses);
}),
map(transactions => {
// only confirmed transactions supported for now
return transactions.filter(tx => tx.status.confirmed).sort((a, b) => b.status.block_height - a.status.block_height);
}),
catchError((error) => {
console.log(error);
this.error = error;
this.seoService.logSoft404();
this.isLoadingWallet = false;
return of([]);
})
).subscribe((transactions: Transaction[] | null) => {
if (!transactions) {
return;
}
this.transactions = transactions;
this.isLoadingTransactions = false;
});
}
loadMore(): void {
if (this.isLoadingTransactions || this.fullyLoaded) {
return;
}
this.isLoadingTransactions = true;
this.retryLoadMore = false;
this.electrsApiService.getAddressesTransactions$(this.addressStrings, this.transactions[this.transactions.length - 1].txid)
.subscribe((transactions: Transaction[]) => {
if (transactions && transactions.length) {
this.transactions = this.transactions.concat(transactions.sort((a, b) => b.status.block_height - a.status.block_height));
} else {
this.fullyLoaded = true;
}
this.isLoadingTransactions = false;
},
(error) => {
this.isLoadingTransactions = false;
this.retryLoadMore = true;
// In the unlikely event of the txid wasn't found in the mempool anymore and we must reload the page.
if (error.status === 422) {
window.location.reload();
}
});
}
deduplicateWalletTransactions(walletTransactions: AddressTxSummary[]): AddressTxSummary[] {
@ -299,5 +373,6 @@ export class WalletComponent implements OnInit, OnDestroy {
ngOnDestroy(): void {
this.websocketService.stopTrackingWallet();
this.walletSubscription.unsubscribe();
this.transactionSubscription.unsubscribe();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { Env, StateService } from '@app/services/state.service';
import { restApiDocsData } from '@app/docs/api-docs/api-docs-data';
import { restApiDocsData, wsApiDocsData } from '@app/docs/api-docs/api-docs-data';
import { faqData } from '@app/docs/api-docs/api-docs-data';
@Component({
@ -28,6 +28,8 @@ export class ApiDocsNavComponent implements OnInit {
this.auditEnabled = this.env.AUDIT;
if (this.whichTab === 'rest') {
this.tabData = restApiDocsData;
} else if (this.whichTab === 'websocket') {
this.tabData = wsApiDocsData;
} else if (this.whichTab === 'faq') {
this.tabData = faqData;
}

View File

@ -108,18 +108,43 @@
</div>
</div>
<div id="websocketAPI" *ngIf="( whichTab === 'websocket' )">
<div class="api-category">
<div class="websocket">
<div class="endpoint">
<div class="subtitle" i18n="Api docs endpoint">Endpoint</div>
{{ wrapUrl(network.val, wsDocs, true) }}
<div id="websocketAPI" *ngIf="whichTab === 'websocket'">
<div id="doc-nav-desktop" class="hide-on-mobile" [ngClass]="desktopDocsNavPosition">
<app-api-docs-nav (navLinkClickEvent)="anchorLinkClick( $event )" [network]="{ val: network$ | async }" [whichTab]="whichTab"></app-api-docs-nav>
</div>
<div class="doc-content">
<div id="enterprise-cta-mobile" *ngIf="officialMempoolInstance && showMobileEnterpriseUpsell">
<p>Get higher API limits with <span class="no-line-break">Mempool Enterprise®</span></p>
<div class="button-group">
<a class="btn btn-small btn-secondary" (click)="showMobileEnterpriseUpsell = false">No Thanks</a>
<a class="btn btn-small btn-purple" href="https://mempool.space/enterprise">More Info <fa-icon [icon]="['fas', 'angle-right']" [styles]="{'font-size': '12px'}"></fa-icon></a>
</div>
<div class="description">
<div class="subtitle" i18n>Description</div>
<div i18n="api-docs.websocket.websocket">Default push: <code>{{ '{' }} action: 'want', data: ['blocks', ...] {{ '}' }}</code> to express what you want pushed. Available: <code>blocks</code>, <code>mempool-blocks</code>, <code>live-2h-chart</code>, and <code>stats</code>.<br><br>Push transactions related to address: <code>{{ '{' }} 'track-address': '3PbJ...bF9B' {{ '}' }}</code> to receive all new transactions containing that address as input or output. Returns an array of transactions. <code>address-transactions</code> for new mempool transactions, and <code>block-transactions</code> for new block confirmed transactions.</div>
</div>
<p class="doc-welcome-note">Below is a reference for the {{ network.val === '' ? 'Bitcoin' : network.val.charAt(0).toUpperCase() + network.val.slice(1) }} <ng-container i18n="api-docs.title-websocket">Websocket service</ng-container> running at {{ websocketUrl(network.val) }}.</p>
<p class="doc-welcome-note api-note" *ngIf="officialMempoolInstance">Note that usage limits apply to our WebSocket API. Consider an <a href="https://mempool.space/enterprise">enterprise sponsorship</a> if you need higher API limits, such as higher tracking limits.</p>
<div class="doc-item-container" *ngFor="let item of wsDocs">
<div *ngIf="!item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('officialOnly') && item.options.officialOnly && officialMempoolInstance )">
<h3 *ngIf="( item.type === 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )">{{ item.title }}</h3>
<div *ngIf="( item.type !== 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )" class="endpoint-container" id="{{ item.fragment }}">
<a id="{{ item.fragment + '-tab-header' }}" class="section-header" (click)="anchorLinkClick({event: $event, fragment: item.fragment})">{{ item.title }} <span>{{ item.category }}</span></a>
<div class="endpoint-content">
<div class="description">
<div class="subtitle" i18n>Description</div>
<div [innerHTML]="item.description.default" i18n></div>
</div>
<div class="description">
<div class="subtitle" i18n>Payload</div>
<pre><code [innerText]="item.payload"></code></pre>
</div>
<app-code-template [hostname]="hostname" [baseNetworkUrl]="baseNetworkUrl" [method]="item.httpRequestMethod" [code]="item.codeExample.default" [network]="network.val" [showCodeExample]="item.showJsExamples"></app-code-template>
</div>
</div>
</div>
<app-code-template [method]="'websocket'" [hostname]="hostname" [code]="wsDocs" [network]="network.val" [showCodeExample]="wsDocs.showJsExamples"></app-code-template>
</div>
</div>
</div>

View File

@ -470,3 +470,21 @@ dd {
margin-left: 1em;
}
}
code {
background-color: var(--bg);
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New;
}
pre {
display: block;
font-size: 87.5%;
color: #f18920;
background-color: var(--bg);
padding: 30px;
code{
background-color: transparent;
white-space: break-spaces;
word-break: break-all;
}
}

View File

@ -145,7 +145,7 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
if (document.getElementById( targetId + "-tab-header" )) {
tabHeaderHeight = document.getElementById( targetId + "-tab-header" ).scrollHeight;
}
if( ( window.innerWidth <= 992 ) && ( ( this.whichTab === 'rest' ) || ( this.whichTab === 'faq' ) ) && targetId ) {
if( ( window.innerWidth <= 992 ) && ( ( this.whichTab === 'rest' ) || ( this.whichTab === 'faq' ) || ( this.whichTab === 'websocket' ) ) && targetId ) {
const endpointContainerEl = document.querySelector<HTMLElement>( "#" + targetId );
const endpointContentEl = document.querySelector<HTMLElement>( "#" + targetId + " .endpoint-content" );
const endPointContentElHeight = endpointContentEl.clientHeight;
@ -207,13 +207,29 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
text = text.replace('%{' + indexNumber + '}', curlText);
}
if (websocket) {
const wsHostname = this.hostname.replace('https://', 'wss://');
wsHostname.replace('http://', 'ws://');
return `${wsHostname}${curlNetwork}${text}`;
}
return `${this.hostname}${curlNetwork}${text}`;
}
websocketUrl(network: string) {
let curlNetwork = '';
if (this.env.BASE_MODULE === 'mempool') {
if (!['', 'mainnet'].includes(network)) {
curlNetwork = `/${network}`;
}
} else if (this.env.BASE_MODULE === 'liquid') {
if (!['', 'liquid'].includes(network)) {
curlNetwork = `/${network}`;
}
}
if (network === this.env.ROOT_NETWORK) {
curlNetwork = '';
}
let wsHostname = this.hostname.replace('https://', 'wss://');
wsHostname = wsHostname.replace('http://', 'ws://');
return `${wsHostname}${curlNetwork}/api/v1/ws`;
}
}

View File

@ -1,4 +1,4 @@
import { AddressTxSummary, Block, ChainStats, Transaction } from "./electrs.interface";
import { AddressTxSummary, Block, ChainStats } from "./electrs.interface";
export interface OptimizedMempoolStats {
added: number;
@ -412,13 +412,13 @@ export interface Acceleration {
feeDelta: number;
blockHash: string;
blockHeight: number;
acceleratedFeeRate?: number;
boost?: number;
bidBoost?: number;
boostCost?: number;
boostRate?: number;
minedByPoolUniqueId?: number;
canceled?: number;
}
export interface AccelerationHistoryParams {

View File

@ -21,6 +21,8 @@ export interface WebsocketResponse {
rbfInfo?: RbfTree;
rbfLatest?: RbfTree[];
rbfLatestSummary?: ReplacementInfo[];
stratumJob?: StratumJob;
stratumJobs?: Record<number, StratumJob>;
utxoSpent?: object;
transactions?: TransactionStripped[];
loadingIndicators?: ILoadingIndicators;
@ -37,6 +39,7 @@ export interface WebsocketResponse {
'track-rbf-summary'?: boolean;
'track-accelerations'?: boolean;
'track-wallet'?: string;
'track-stratum'?: string | number;
'watch-mempool'?: boolean;
'refresh-blocks'?: boolean;
}
@ -150,3 +153,24 @@ export interface HealthCheckHost {
electrs?: string;
}
}
export interface StratumJob {
pool: number;
height: number;
coinbase: string;
scriptsig: string;
reward: number;
jobId: string;
extraNonce: string;
extraNonce2Size: number;
prevHash: string;
coinbase1: string;
coinbase2: string;
merkleBranches: string[];
version: string;
bits: string;
time: string;
timestamp: number;
cleanJobs: boolean;
received: number;
}

View File

@ -10,9 +10,10 @@ import { TestTransactionsComponent } from '@components/test-transactions/test-tr
import { CalculatorComponent } from '@components/calculator/calculator.component';
import { BlocksList } from '@components/blocks-list/blocks-list.component';
import { RbfList } from '@components/rbf-list/rbf-list.component';
import { StratumList } from '@components/stratum/stratum-list/stratum-list.component';
import { ServerHealthComponent } from '@components/server-health/server-health.component';
import { ServerStatusComponent } from '@components/server-health/server-status.component';
import { FaucetComponent } from '@components/faucet/faucet.component'
import { FaucetComponent } from '@components/faucet/faucet.component';
const browserWindow = window || {};
// @ts-ignore
@ -56,6 +57,16 @@ const routes: Routes = [
path: 'rbf',
component: RbfList,
},
...(browserWindowEnv.STRATUM_ENABLED ? [{
path: 'stratum',
component: StartComponent,
children: [
{
path: '',
component: StratumList,
}
]
}] : []),
{
path: 'terms-of-service',
loadChildren: () => import('@components/terms-of-service/terms-of-service.module').then(m => m.TermsOfServiceModule),

View File

@ -58,6 +58,7 @@ export class AuthServiceMempool {
setAuth(auth: any) {
if (!auth) {
localStorage.removeItem('auth');
localStorage.removeItem('authenticatorStatus');
} else {
localStorage.setItem('auth', JSON.stringify(auth));
}

View File

@ -142,12 +142,16 @@ export class ElectrsApiService {
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs', { params });
}
getAddressesTransactions$(addresses: string[], txid?: string): Observable<Transaction[]> {
getAddressesTransactions$(addresses: string[], txid?: string): Observable<Transaction[]> {
let params = new HttpParams();
if (txid) {
params = params.append('after_txid', txid);
}
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + `/api/addresses/txs?addresses=${addresses.join(',')}`, { params });
return this.httpClient.post<Transaction[]>(
this.apiBaseUrl + this.apiBasePath + '/api/addresses/txs',
addresses,
{ params }
);
}
getAddressSummary$(address: string, txid?: string): Observable<AddressTxSummary[]> {
@ -163,7 +167,7 @@ export class ElectrsApiService {
if (txid) {
params = params.append('after_txid', txid);
}
return this.httpClient.get<AddressTxSummary[]>(this.apiBaseUrl + this.apiBasePath + `/api/addresses/txs/summary?addresses=${addresses.join(',')}`, { params });
return this.httpClient.post<AddressTxSummary[]>(this.apiBaseUrl + this.apiBasePath + '/api/addresses/txs/summary', addresses, { params });
}
getScriptHashTransactions$(script: string, txid?: string): Observable<Transaction[]> {
@ -182,7 +186,7 @@ export class ElectrsApiService {
params = params.append('after_txid', txid);
}
return from(Promise.all(scripts.map(script => calcScriptHash$(script)))).pipe(
switchMap(scriptHashes => this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + `/api/scripthashes/txs?scripthashes=${scriptHashes.join(',')}`, { params })),
switchMap(scriptHashes => this.httpClient.post<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/scripthashes/txs', scriptHashes, { params })),
);
}
@ -212,7 +216,7 @@ export class ElectrsApiService {
params = params.append('after_txid', txid);
}
return from(Promise.all(scripts.map(script => calcScriptHash$(script)))).pipe(
switchMap(scriptHashes => this.httpClient.get<AddressTxSummary[]>(this.apiBaseUrl + this.apiBasePath + `/api/scripthashes/txs/summary?scripthashes=${scriptHashes.join(',')}`, { params })),
switchMap(scriptHashes => this.httpClient.post<AddressTxSummary[]>(this.apiBaseUrl + this.apiBasePath + '/api/scripthashes/txs/summary', scriptHashes, { params })),
);
}

View File

@ -64,8 +64,8 @@ export class MiningService {
);
}
}
/**
/**
* Get names and slugs of all pools
*/
public getPools(): Observable<any[]> {
@ -75,7 +75,6 @@ export class MiningService {
return this.poolsData;
})
);
}
/**
* Set the hashrate power of ten we want to display

View File

@ -18,7 +18,6 @@ export interface IUser {
subscription_tag: string;
status: 'pending' | 'verified' | 'disabled';
features: string | null;
fullName: string | null;
countryCode: string | null;
imageMd5: string;
ogRank: number | null;
@ -143,8 +142,12 @@ export class ServicesApiServices {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/applePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, userApprovedUSD: userApprovedUSD });
}
accelerateWithGooglePay$(txInput: string, token: string, verificationToken: string, cardTag: string, referenceId: string, userApprovedUSD: number) {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, verificationToken: verificationToken, referenceId: referenceId, userApprovedUSD: userApprovedUSD });
accelerateWithGooglePay$(txInput: string, token: string, verificationToken: string, cardTag: string, referenceId: string, userApprovedUSD: number, userChallenged: boolean) {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, verificationToken: verificationToken, referenceId: referenceId, userApprovedUSD: userApprovedUSD, userChallenged: userChallenged });
}
accelerateWithCardOnFile$(txInput: string, token: string, verificationToken: string, referenceId: string, userApprovedUSD: number, userChallenged: boolean) {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/cardOnFile`, { txInput: txInput, token: token, verificationToken: verificationToken, referenceId: referenceId, userApprovedUSD: userApprovedUSD, userChallenged: userChallenged });
}
getAccelerations$(): Observable<Acceleration[]> {

View File

@ -1,7 +1,7 @@
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
import { AddressTxSummary, Transaction } from '@interfaces/electrs.interface';
import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, isMempoolState } from '@interfaces/websocket.interface';
import { Transaction } from '@interfaces/electrs.interface';
import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, StratumJob, isMempoolState } from '@interfaces/websocket.interface';
import { Acceleration, AccelerationPosition, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '@interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router';
import { isPlatformBrowser } from '@angular/common';
@ -81,6 +81,7 @@ export interface Env {
ADDITIONAL_CURRENCIES: boolean;
GIT_COMMIT_HASH_MEMPOOL_SPACE?: string;
PACKAGE_JSON_VERSION_MEMPOOL_SPACE?: string;
STRATUM_ENABLED: boolean;
SERVICES_API?: string;
customize?: Customization;
PROD_DOMAINS: string[];
@ -123,6 +124,7 @@ const defaultEnv: Env = {
'ACCELERATOR_BUTTON': true,
'PUBLIC_ACCELERATIONS': false,
'ADDITIONAL_CURRENCIES': false,
'STRATUM_ENABLED': false,
'SERVICES_API': 'https://mempool.space/api/v1/services',
'PROD_DOMAINS': [],
};
@ -159,6 +161,8 @@ export class StateService {
liveMempoolBlockTransactions$: Observable<{ block: number, transactions: { [txid: string]: TransactionStripped} }>;
accelerations$ = new Subject<AccelerationDelta>();
liveAccelerations$: Observable<Acceleration[]>;
stratumJobUpdate$ = new Subject<{ state: Record<string, StratumJob> } | { job: StratumJob }>();
stratumJobs$ = new BehaviorSubject<Record<string, StratumJob>>({});
txConfirmed$ = new Subject<[string, BlockExtended]>();
txReplaced$ = new Subject<ReplacedTransaction>();
txRbfInfo$ = new Subject<RbfTree>();
@ -303,6 +307,24 @@ export class StateService {
map((accMap) => Object.values(accMap).sort((a,b) => b.added - a.added))
);
this.stratumJobUpdate$.pipe(
scan((acc: Record<string, StratumJob>, update: { state: Record<string, StratumJob> } | { job: StratumJob }) => {
if ('state' in update) {
// Replace the entire state
return update.state;
} else {
// Update or create a single job entry
return {
...acc,
[update.job.pool]: update.job
};
}
}, {}),
shareReplay(1)
).subscribe(val => {
this.stratumJobs$.next(val);
});
this.networkChanged$.subscribe((network) => {
this.transactions$ = new BehaviorSubject<TransactionStripped[]>(null);
this.blocksSubject$.next([]);

View File

@ -36,6 +36,7 @@ export class WebsocketService {
private isTrackingAccelerations: boolean = false;
private isTrackingWallet: boolean = false;
private trackingWalletName: string;
private isTrackingStratum: string | number | false = false;
private trackingMempoolBlock: number;
private trackingMempoolBlockNetwork: string;
private stoppingTrackMempoolBlock: any | null = null;
@ -143,6 +144,9 @@ export class WebsocketService {
if (this.isTrackingWallet) {
this.startTrackingWallet(this.trackingWalletName);
}
if (this.isTrackingStratum !== false) {
this.startTrackStratum(this.isTrackingStratum);
}
this.stateService.connectionState$.next(2);
}
@ -289,6 +293,18 @@ export class WebsocketService {
}
}
startTrackStratum(pool: number | string) {
this.websocketSubject.next({ 'track-stratum': pool });
this.isTrackingStratum = pool;
}
stopTrackStratum() {
if (this.isTrackingStratum) {
this.websocketSubject.next({ 'track-stratum': null });
this.isTrackingStratum = false;
}
}
fetchStatistics(historicalDate: string) {
this.websocketSubject.next({ historicalDate });
}
@ -512,6 +528,14 @@ export class WebsocketService {
this.stateService.previousRetarget$.next(response.previousRetarget);
}
if (response.stratumJobs) {
this.stateService.stratumJobUpdate$.next({ state: response.stratumJobs });
}
if (response.stratumJob) {
this.stateService.stratumJobUpdate$.next({ job: response.stratumJob });
}
if (response['tomahawk']) {
this.stateService.serverHealth$.next(response['tomahawk']);
}

View File

@ -11,9 +11,9 @@
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && replaced">
<button type="button" class="btn btn-sm btn-warning no-cursor {{buttonClass}}" i18n="transaction.replaced|Transaction replaced state">Replaced</button>
</ng-template>
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && !replaced && removed">
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && !replaced && (removed || cached)">
<button type="button" class="btn btn-sm btn-warning no-cursor {{buttonClass}}" i18n="transaction.audit.removed|Transaction removed state">Removed</button>
</ng-template>
<ng-template [ngIf]="!hideUnconfirmed && chainTip != null && !confirmations && !replaced && !removed">
<ng-template [ngIf]="!hideUnconfirmed && chainTip != null && !confirmations && !replaced && !(removed || cached)">
<button type="button" class="btn btn-sm btn-danger no-cursor {{buttonClass}}" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
</ng-template>

View File

@ -12,6 +12,7 @@ export class ConfirmationsComponent implements OnChanges {
@Input() height: number;
@Input() replaced: boolean = false;
@Input() removed: boolean = false;
@Input() cached: boolean = false;
@Input() hideUnconfirmed: boolean = false;
@Input() buttonClass: string = '';

View File

@ -26,6 +26,7 @@ export const MempoolErrors = {
'unauthorized': `You are not authorized to do this`,
'faucet_too_soon': `You cannot request any more coins right now. Try again later.`,
'faucet_not_available': `The faucet is not available right now. Try again later.`,
'faucet_not_available_no_utxo': `The faucet is not available right now. Please try again once a new block has been mined.`,
'faucet_maximum_reached': `You are not allowed to request more coins`,
'faucet_address_not_allowed': `You cannot use this address`,
'faucet_below_minimum': `Requested amount is too small`,

View File

@ -4,7 +4,10 @@ import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstra
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, faCircleXmark, faCalendarCheck } from '@fortawesome/free-solid-svg-icons';
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft,
faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck,
faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline,
faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot, faShareNodes, faCreditCard, faMicroscope } from '@fortawesome/free-solid-svg-icons';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { MenuComponent } from '@components/menu/menu.component';
import { PreviewTitleComponent } from '@components/master-page-preview/preview-title.component';
@ -80,6 +83,7 @@ import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe';
import { DifficultyAdjustmentsTable } from '@components/difficulty-adjustments-table/difficulty-adjustments-table.components';
import { BlocksList } from '@components/blocks-list/blocks-list.component';
import { RbfList } from '@components/rbf-list/rbf-list.component';
import { StratumList } from '@components/stratum/stratum-list/stratum-list.component';
import { RewardStatsComponent } from '@components/reward-stats/reward-stats.component';
import { DataCyDirective } from '@app/data-cy.directive';
import { LoadingIndicatorComponent } from '@components/loading-indicator/loading-indicator.component';
@ -121,6 +125,7 @@ import { TwitterLogin } from '@components/twitter-login/twitter-login.component'
import { BitcoinInvoiceComponent } from '@components/bitcoin-invoice/bitcoin-invoice.component';
import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/weight-directives/weight-directives';
import { GithubLogin } from '../components/github-login.component/github-login.component';
@NgModule({
declarations: [
@ -198,6 +203,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/
DifficultyAdjustmentsTable,
BlocksList,
RbfList,
StratumList,
DataCyDirective,
RewardStatsComponent,
LoadingIndicatorComponent,
@ -237,6 +243,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/
TwitterWidgetComponent,
FaucetComponent,
TwitterLogin,
GithubLogin,
BitcoinInvoiceComponent,
],
imports: [
@ -342,6 +349,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/
AmountShortenerPipe,
DifficultyAdjustmentsTable,
BlocksList,
StratumList,
DataCyDirective,
RewardStatsComponent,
LoadingIndicatorComponent,
@ -370,6 +378,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from '@app/shared/components/
HttpErrorComponent,
TwitterWidgetComponent,
TwitterLogin,
GithubLogin,
BitcoinInvoiceComponent,
BitcoinsatoshisPipe,
@ -451,5 +460,10 @@ export class SharedModule {
library.addIcons(faTimeline);
library.addIcons(faCircleXmark);
library.addIcons(faCalendarCheck);
library.addIcons(faMoneyBillTrendUp);
library.addIcons(faRobot);
library.addIcons(faShareNodes);
library.addIcons(faCreditCard);
library.addIcons(faMicroscope);
}
}

View File

@ -15,6 +15,7 @@ whitelist=127.0.0.1
whitelist=103.99.168.0/22
whitelist=2401:b140::/32
blocksxor=0
logtimemicros=1
#uacomment=@wiz
[main]

View File

@ -26,12 +26,14 @@
"ESPLORA": {
"UNIX_SOCKET_PATH": "/elements/socket/esplora-elements-liquid",
"FALLBACK": [
"http://node201.fmt.mempool.space:3001",
"http://node202.fmt.mempool.space:3001",
"http://node203.fmt.mempool.space:3001",
"http://node204.fmt.mempool.space:3001",
"http://node205.fmt.mempool.space:3001",
"http://node206.fmt.mempool.space:3001",
"http://node201.hnl.mempool.space:3001",
"http://node202.hnl.mempool.space:3001",
"http://node203.hnl.mempool.space:3001",
"http://node204.hnl.mempool.space:3001",
"http://node201.sg1.mempool.space:3001",
"http://node202.sg1.mempool.space:3001",
"http://node203.sg1.mempool.space:3001",
"http://node204.sg1.mempool.space:3001",
"http://node201.va1.mempool.space:3001",
"http://node202.va1.mempool.space:3001",
"http://node203.va1.mempool.space:3001",

View File

@ -26,12 +26,14 @@
"ESPLORA": {
"UNIX_SOCKET_PATH": "/elements/socket/esplora-elements-liquidtestnet",
"FALLBACK": [
"http://node201.fmt.mempool.space:3004",
"http://node202.fmt.mempool.space:3004",
"http://node203.fmt.mempool.space:3004",
"http://node204.fmt.mempool.space:3004",
"http://node205.fmt.mempool.space:3004",
"http://node206.fmt.mempool.space:3004",
"http://node201.hnl.mempool.space:3004",
"http://node202.hnl.mempool.space:3004",
"http://node203.hnl.mempool.space:3004",
"http://node204.hnl.mempool.space:3004",
"http://node201.sg1.mempool.space:3004",
"http://node202.sg1.mempool.space:3004",
"http://node203.sg1.mempool.space:3004",
"http://node204.sg1.mempool.space:3004",
"http://node201.va1.mempool.space:3004",
"http://node202.va1.mempool.space:3004",
"http://node203.va1.mempool.space:3004",

View File

@ -19,12 +19,14 @@
"ESPLORA": {
"UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-mainnet",
"FALLBACK": [
"http://node201.fmt.mempool.space:3000",
"http://node202.fmt.mempool.space:3000",
"http://node203.fmt.mempool.space:3000",
"http://node204.fmt.mempool.space:3000",
"http://node205.fmt.mempool.space:3000",
"http://node206.fmt.mempool.space:3000",
"http://node201.hnl.mempool.space:3000",
"http://node202.hnl.mempool.space:3000",
"http://node203.hnl.mempool.space:3000",
"http://node204.hnl.mempool.space:3000",
"http://node201.sg1.mempool.space:3000",
"http://node202.sg1.mempool.space:3000",
"http://node203.sg1.mempool.space:3000",
"http://node204.sg1.mempool.space:3000",
"http://node201.va1.mempool.space:3000",
"http://node202.va1.mempool.space:3000",
"http://node203.va1.mempool.space:3000",

View File

@ -30,7 +30,8 @@
"CORE_RPC": {
"PORT": 8332,
"USERNAME": "__BITCOIN_RPC_USER__",
"PASSWORD": "__BITCOIN_RPC_PASS__"
"PASSWORD": "__BITCOIN_RPC_PASS__",
"DEBUG_LOG_PATH": "/bitcoin/debug.log"
},
"SECOND_CORE_RPC": {
"PORT": 8302,
@ -40,12 +41,14 @@
"ESPLORA": {
"UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-mainnet",
"FALLBACK": [
"http://node201.fmt.mempool.space:3000",
"http://node202.fmt.mempool.space:3000",
"http://node203.fmt.mempool.space:3000",
"http://node204.fmt.mempool.space:3000",
"http://node205.fmt.mempool.space:3000",
"http://node206.fmt.mempool.space:3000",
"http://node201.hnl.mempool.space:3000",
"http://node202.hnl.mempool.space:3000",
"http://node203.hnl.mempool.space:3000",
"http://node204.hnl.mempool.space:3000",
"http://node201.sg1.mempool.space:3000",
"http://node202.sg1.mempool.space:3000",
"http://node203.sg1.mempool.space:3000",
"http://node204.sg1.mempool.space:3000",
"http://node201.va1.mempool.space:3000",
"http://node202.va1.mempool.space:3000",
"http://node203.va1.mempool.space:3000",
@ -101,12 +104,14 @@
"STATISTICS": true,
"STATISTICS_START_TIME": "24h",
"SERVERS": [
"node201.fmt.mempool.space",
"node202.fmt.mempool.space",
"node203.fmt.mempool.space",
"node204.fmt.mempool.space",
"node205.fmt.mempool.space",
"node206.fmt.mempool.space",
"node201.hnl.mempool.space",
"node202.hnl.mempool.space",
"node203.hnl.mempool.space",
"node204.hnl.mempool.space",
"node201.sg1.mempool.space",
"node202.sg1.mempool.space",
"node203.sg1.mempool.space",
"node204.sg1.mempool.space",
"node201.va1.mempool.space",
"node202.va1.mempool.space",
"node203.va1.mempool.space",
@ -154,5 +159,9 @@
"WALLETS": {
"ENABLED": true,
"WALLETS": ["BITB", "3350"]
},
"STRATUM": {
"ENABLED": true,
"API": "http://127.0.0.1:81/api/v1/stratum/ws"
}
}

View File

@ -19,12 +19,14 @@
"ESPLORA": {
"UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-signet",
"FALLBACK": [
"http://node201.fmt.mempool.space:3003",
"http://node202.fmt.mempool.space:3003",
"http://node203.fmt.mempool.space:3003",
"http://node204.fmt.mempool.space:3003",
"http://node205.fmt.mempool.space:3003",
"http://node206.fmt.mempool.space:3003",
"http://node201.hnl.mempool.space:3003",
"http://node202.hnl.mempool.space:3003",
"http://node203.hnl.mempool.space:3003",
"http://node204.hnl.mempool.space:3003",
"http://node201.sg1.mempool.space:3003",
"http://node202.sg1.mempool.space:3003",
"http://node203.sg1.mempool.space:3003",
"http://node204.sg1.mempool.space:3003",
"http://node201.va1.mempool.space:3003",
"http://node202.va1.mempool.space:3003",
"http://node203.va1.mempool.space:3003",

View File

@ -28,12 +28,14 @@
"ESPLORA": {
"UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-signet",
"FALLBACK": [
"http://node201.fmt.mempool.space:3003",
"http://node202.fmt.mempool.space:3003",
"http://node203.fmt.mempool.space:3003",
"http://node204.fmt.mempool.space:3003",
"http://node205.fmt.mempool.space:3003",
"http://node206.fmt.mempool.space:3003",
"http://node201.hnl.mempool.space:3003",
"http://node202.hnl.mempool.space:3003",
"http://node203.hnl.mempool.space:3003",
"http://node204.hnl.mempool.space:3003",
"http://node201.sg1.mempool.space:3003",
"http://node202.sg1.mempool.space:3003",
"http://node203.sg1.mempool.space:3003",
"http://node204.sg1.mempool.space:3003",
"http://node201.va1.mempool.space:3003",
"http://node202.va1.mempool.space:3003",
"http://node203.va1.mempool.space:3003",

View File

@ -19,12 +19,14 @@
"ESPLORA": {
"UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-testnet",
"FALLBACK": [
"http://node201.fmt.mempool.space:3002",
"http://node202.fmt.mempool.space:3002",
"http://node203.fmt.mempool.space:3002",
"http://node204.fmt.mempool.space:3002",
"http://node205.fmt.mempool.space:3002",
"http://node206.fmt.mempool.space:3002",
"http://node201.hnl.mempool.space:3002",
"http://node202.hnl.mempool.space:3002",
"http://node203.hnl.mempool.space:3002",
"http://node204.hnl.mempool.space:3002",
"http://node201.sg1.mempool.space:3002",
"http://node202.sg1.mempool.space:3002",
"http://node203.sg1.mempool.space:3002",
"http://node204.sg1.mempool.space:3002",
"http://node201.va1.mempool.space:3002",
"http://node202.va1.mempool.space:3002",
"http://node203.va1.mempool.space:3002",

View File

@ -28,12 +28,14 @@
"ESPLORA": {
"UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-testnet",
"FALLBACK": [
"http://node201.fmt.mempool.space:3002",
"http://node202.fmt.mempool.space:3002",
"http://node203.fmt.mempool.space:3002",
"http://node204.fmt.mempool.space:3002",
"http://node205.fmt.mempool.space:3002",
"http://node206.fmt.mempool.space:3002",
"http://node201.hnl.mempool.space:3002",
"http://node202.hnl.mempool.space:3002",
"http://node203.hnl.mempool.space:3002",
"http://node204.hnl.mempool.space:3002",
"http://node201.sg1.mempool.space:3002",
"http://node202.sg1.mempool.space:3002",
"http://node203.sg1.mempool.space:3002",
"http://node204.sg1.mempool.space:3002",
"http://node201.va1.mempool.space:3002",
"http://node202.va1.mempool.space:3002",
"http://node203.va1.mempool.space:3002",

View File

@ -28,12 +28,14 @@
"ESPLORA": {
"UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-testnet4",
"FALLBACK": [
"http://node201.fmt.mempool.space:3005",
"http://node202.fmt.mempool.space:3005",
"http://node203.fmt.mempool.space:3005",
"http://node204.fmt.mempool.space:3005",
"http://node205.fmt.mempool.space:3005",
"http://node206.fmt.mempool.space:3005",
"http://node201.hnl.mempool.space:3005",
"http://node202.hnl.mempool.space:3005",
"http://node203.hnl.mempool.space:3005",
"http://node204.hnl.mempool.space:3005",
"http://node201.sg1.mempool.space:3005",
"http://node202.sg1.mempool.space:3005",
"http://node203.sg1.mempool.space:3005",
"http://node204.sg1.mempool.space:3005",
"http://node201.va1.mempool.space:3005",
"http://node202.va1.mempool.space:3005",
"http://node203.va1.mempool.space:3005",

View File

@ -4,8 +4,7 @@
"TESTNET4_ENABLED": true,
"LIQUID_ENABLED": false,
"LIQUID_TESTNET_ENABLED": false,
"BISQ_ENABLED": true,
"BISQ_SEPARATE_BACKEND": true,
"STRATUM_ENABLED": true,
"SIGNET_ENABLED": true,
"MEMPOOL_WEBSITE_URL": "https://mempool.space",
"LIQUID_WEBSITE_URL": "https://liquid.network",

View File

@ -2,7 +2,7 @@
proxy_cache_path /var/cache/nginx/services keys_zone=services:200m levels=1:2 inactive=30d max_size=200m;
proxy_cache_path /var/cache/nginx/apihot keys_zone=apihot:200m levels=1:2 inactive=60m max_size=20m;
proxy_cache_path /var/cache/nginx/apiwarm keys_zone=apiwarm:200m levels=1:2 inactive=24h max_size=200m;
proxy_cache_path /var/cache/nginx/apinormal keys_zone=apinormal:200m levels=1:2 inactive=30d max_size=2000m;
proxy_cache_path /var/cache/nginx/apinormal keys_zone=apinormal:500m levels=1:2 inactive=24h max_size=2000m;
proxy_cache_path /var/cache/nginx/apicold keys_zone=apicold:200m levels=1:2 inactive=60d max_size=2000m;
proxy_cache_path /var/cache/nginx/unfurler keys_zone=unfurler:200m levels=1:2 inactive=30d max_size=2000m;

View File

@ -140,7 +140,8 @@ location @mempool-api-v1-cache-normal {
proxy_cache_valid 200 2s;
proxy_redirect off;
expires 2s;
# cache for 2 seconds on server, but send expires -1 so browser doesn't cache
expires -1;
}
location @mempool-api-v1-cache-disabled {

189
rust/gbt/Cargo.lock generated
View File

@ -1,21 +1,21 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "addr2line"
version = "0.22.0"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "1.0.2"
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "aho-corasick"
@ -28,48 +28,42 @@ dependencies = [
[[package]]
name = "autocfg"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "backtrace"
version = "0.3.73"
version = "0.3.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
dependencies = [
"addr2line",
"cc",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets",
]
[[package]]
name = "bitflags"
version = "2.6.0"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
[[package]]
name = "bytemuck"
version = "1.16.1"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e"
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
[[package]]
name = "bytes"
version = "1.6.1"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952"
[[package]]
name = "cc"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "324c74f2155653c90b04f25b2a47a8a631360cb908f92a772695f430c7e31052"
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
[[package]]
name = "cfg-if"
@ -88,9 +82,9 @@ dependencies = [
[[package]]
name = "ctor"
version = "0.2.8"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f"
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
dependencies = [
"quote",
"syn",
@ -119,27 +113,21 @@ dependencies = [
[[package]]
name = "gimli"
version = "0.29.0"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "hashbrown"
version = "0.14.5"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hermit-abi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]]
name = "indexmap"
version = "2.2.6"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
dependencies = [
"equivalent",
"hashbrown",
@ -153,15 +141,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.155"
version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "libloading"
version = "0.8.4"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets",
@ -169,9 +157,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.22"
version = "0.4.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
[[package]]
name = "matchers"
@ -190,18 +178,18 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "miniz_oxide"
version = "0.7.4"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
dependencies = [
"adler",
"adler2",
]
[[package]]
name = "napi"
version = "2.16.8"
version = "2.16.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1bd081bbaef43600fd2c5dd4c525b8ecea7dfdacf40ebc674e87851dce6559e"
checksum = "214f07a80874bb96a8433b3cdfc84980d56c7b02e1a0d7ba4ba0db5cef785e2b"
dependencies = [
"bitflags",
"ctor",
@ -213,15 +201,15 @@ dependencies = [
[[package]]
name = "napi-build"
version = "2.1.3"
version = "2.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a"
checksum = "db836caddef23662b94e16bf1f26c40eceb09d6aee5d5b06a7ac199320b69b19"
[[package]]
name = "napi-derive"
version = "2.16.9"
version = "2.16.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87c3b5d4ab13e20a4bb9d3a1e2f3d4e77eee4a205d0f810abfd226b971dc6ce5"
checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c"
dependencies = [
"cfg-if",
"convert_case",
@ -233,9 +221,9 @@ dependencies = [
[[package]]
name = "napi-derive-backend"
version = "1.0.71"
version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96de436a6ab93265beef838f8333c8345438f059df6081fe0ad0b8648ee0c524"
checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf"
dependencies = [
"convert_case",
"once_cell",
@ -265,30 +253,20 @@ dependencies = [
"winapi",
]
[[package]]
name = "num_cpus"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "object"
version = "0.36.1"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.19.0"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "overload"
@ -298,15 +276,15 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "pin-project-lite"
version = "0.2.14"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "priority-queue"
version = "2.0.3"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70c501afe3a2e25c9bd219aa56ec1e04cdb3fcdd763055be268778c13fa82c1f"
checksum = "714c75db297bc88a63783ffc6ab9f830698a6705aa0201416931759ef4c8183d"
dependencies = [
"autocfg",
"equivalent",
@ -315,32 +293,32 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.86"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.10.5"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.7",
"regex-syntax 0.8.4",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
@ -354,13 +332,13 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.7"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.8.4",
"regex-syntax 0.8.5",
]
[[package]]
@ -371,9 +349,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.4"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rustc-demangle"
@ -383,9 +361,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "semver"
version = "1.0.23"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03"
[[package]]
name = "sharded-slab"
@ -404,9 +382,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "syn"
version = "2.0.71"
version = "2.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462"
checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
dependencies = [
"proc-macro2",
"quote",
@ -425,20 +403,19 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.38.1"
version = "1.43.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df"
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
dependencies = [
"backtrace",
"num_cpus",
"pin-project-lite",
]
[[package]]
name = "tracing"
version = "0.1.40"
version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"pin-project-lite",
"tracing-attributes",
@ -447,9 +424,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
version = "0.1.27"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
@ -458,9 +435,9 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.32"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [
"once_cell",
"valuable",
@ -479,9 +456,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.18"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"matchers",
"nu-ansi-term",
@ -497,21 +474,21 @@ dependencies = [
[[package]]
name = "unicode-ident"
version = "1.0.12"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243"
[[package]]
name = "unicode-segmentation"
version = "1.11.0"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "valuable"
version = "0.1.0"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "winapi"

Some files were not shown because too many files have changed in this diff Show More