mirror of
https://github.com/mempool/mempool.git
synced 2024-11-20 10:21:52 +01:00
Merge branch 'master' into natsoni/address-history-chart-usd
This commit is contained in:
commit
43f35837da
@ -181,7 +181,7 @@ Create a new wallet, if needed:
|
||||
bitcoin-cli -regtest createwallet test
|
||||
```
|
||||
|
||||
Load wallet (this command may take a while if you have lot of UTXOs):
|
||||
Load wallet (this command may take a while if you have a lot of UTXOs):
|
||||
```
|
||||
bitcoin-cli -regtest loadwallet test
|
||||
```
|
||||
@ -233,9 +233,9 @@ By default, mining pools will be not automatically updated regularly (`config.ME
|
||||
|
||||
To manually update your mining pools, you can use the `--update-pools` command line flag when you run the nodejs backend. For example `npm run start --update-pools`. This will trigger the mining pools update and automatically re-index appropriate blocks.
|
||||
|
||||
You can enabled the automatic mining pools update by settings `config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING` to `true` in your `mempool-config.json`.
|
||||
You can enable the automatic mining pools update by settings `config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING` to `true` in your `mempool-config.json`.
|
||||
|
||||
When a `coinbase tag` or `coinbase address` change is detected, all blocks tagged to the `unknown` mining pools (starting from height 130635) will be deleted from the `blocks` table. Additionaly, all blocks which were tagged to the pool which has been updated will also be deleted from the `blocks` table. Of course, those blocks will be automatically reindexed.
|
||||
When a `coinbase tag` or `coinbase address` change is detected, all blocks tagged to the `unknown` mining pools (starting from height 130635) will be deleted from the `blocks` table. Additionally, all blocks which were tagged to the pool which has been updated will also be deleted from the `blocks` table. Of course, those blocks will be automatically reindexed.
|
||||
|
||||
### Re-index tables
|
||||
|
||||
|
14
backend/package-lock.json
generated
14
backend/package-lock.json
generated
@ -23,7 +23,7 @@
|
||||
"rust-gbt": "file:./rust-gbt",
|
||||
"socks-proxy-agent": "~7.0.0",
|
||||
"typescript": "~4.9.3",
|
||||
"ws": "~8.17.0"
|
||||
"ws": "~8.17.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
@ -7690,9 +7690,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
|
||||
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
@ -13424,9 +13424,9 @@
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.17.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
|
||||
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"y18n": {
|
||||
|
@ -52,7 +52,7 @@
|
||||
"redis": "^4.6.6",
|
||||
"socks-proxy-agent": "~7.0.0",
|
||||
"typescript": "~4.9.3",
|
||||
"ws": "~8.17.0"
|
||||
"ws": "~8.17.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
|
@ -4,21 +4,37 @@ import { MempoolTransactionExtended } from '../../mempool.interfaces';
|
||||
const randomTransactions = require('./test-data/transactions-random.json');
|
||||
const replacedTransactions = require('./test-data/transactions-replaced.json');
|
||||
const rbfTransactions = require('./test-data/transactions-rbfs.json');
|
||||
const nonStandardTransactions = require('./test-data/non-standard-txs.json');
|
||||
|
||||
describe('Mempool Utils', () => {
|
||||
test('should detect RBF transactions with fast method', () => {
|
||||
describe('Common', () => {
|
||||
describe('RBF', () => {
|
||||
const newTransactions = rbfTransactions.concat(randomTransactions);
|
||||
const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions);
|
||||
expect(Object.values(result).length).toEqual(2);
|
||||
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
|
||||
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
|
||||
test('should detect RBF transactions with fast method', () => {
|
||||
const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions);
|
||||
expect(Object.values(result).length).toEqual(2);
|
||||
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
|
||||
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
|
||||
});
|
||||
|
||||
test('should detect RBF transactions with scalable method', () => {
|
||||
const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions, true);
|
||||
expect(Object.values(result).length).toEqual(2);
|
||||
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
|
||||
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
|
||||
});
|
||||
});
|
||||
|
||||
test.only('should detect RBF transactions with scalable method', () => {
|
||||
const newTransactions = rbfTransactions.concat(randomTransactions);
|
||||
const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions, true);
|
||||
expect(Object.values(result).length).toEqual(2);
|
||||
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
|
||||
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
|
||||
describe('Mempool Goggles', () => {
|
||||
test('should detect nonstandard transactions', () => {
|
||||
nonStandardTransactions.forEach((tx) => {
|
||||
expect(Common.isNonStandard(tx)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
test('should not misclassify as nonstandard transactions', () => {
|
||||
randomTransactions.forEach((tx) => {
|
||||
expect(Common.isNonStandard(tx)).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
52
backend/src/__tests__/api/test-data/non-standard-txs.json
Normal file
52
backend/src/__tests__/api/test-data/non-standard-txs.json
Normal file
@ -0,0 +1,52 @@
|
||||
[
|
||||
{
|
||||
"txid": "50136231cb7eeeffb17fc41d1cca213426abe5bf3760e3d6421cad0c0edad367",
|
||||
"version": 1,
|
||||
"locktime": 0,
|
||||
"vin": [
|
||||
{
|
||||
"txid": "c7f86fb7b830124057475b282809f3474ef3565daa3de0b599980fb9e84ab019",
|
||||
"vout": 4217,
|
||||
"prevout": {
|
||||
"scriptpubkey": "001466197b5eadd8067ec194a457e1044b6d1fbdd3b3",
|
||||
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 66197b5eadd8067ec194a457e1044b6d1fbdd3b3",
|
||||
"scriptpubkey_type": "v0_p2wpkh",
|
||||
"scriptpubkey_address": "bc1qvcvhkh4dmqr8asv553t7zpztd50mm5ang4na33",
|
||||
"value": 106
|
||||
},
|
||||
"scriptsig": "",
|
||||
"scriptsig_asm": "",
|
||||
"witness": [
|
||||
"3043021f2af6060a142c6cfd7428adad6a50745d2424813d7ced5c0bbcca85e70de1be022021440ca1c8c3ed49ecd1b64dca6911adcd430c5d3dd60d77ffe0072953999f5b01",
|
||||
"02ead5c34e3d2c506574b562f857576e11380b6ba15d9f0ad7b7303fdaa9c1513d"
|
||||
],
|
||||
"is_coinbase": false,
|
||||
"sequence": 4294967295
|
||||
}
|
||||
],
|
||||
"vout": [
|
||||
{
|
||||
"scriptpubkey": "6a023a29",
|
||||
"scriptpubkey_asm": "OP_RETURN OP_PUSHBYTES_2 3a29",
|
||||
"scriptpubkey_type": "op_return",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"scriptpubkey": "6a036d7648",
|
||||
"scriptpubkey_asm": "OP_RETURN OP_PUSHBYTES_3 6d7648",
|
||||
"scriptpubkey_type": "op_return",
|
||||
"value": 0
|
||||
}
|
||||
],
|
||||
"size": 186,
|
||||
"weight": 420,
|
||||
"sigops": 1,
|
||||
"fee": 106,
|
||||
"status": {
|
||||
"confirmed": true,
|
||||
"block_height": 836361,
|
||||
"block_hash": "0000000000000000000341cc26cda4af82cd25f7063c448772228cbf2836915b",
|
||||
"block_time": 1711448028
|
||||
}
|
||||
}
|
||||
]
|
@ -273,5 +273,63 @@
|
||||
},
|
||||
"bestDescendant": null,
|
||||
"cpfpChecked": true
|
||||
},
|
||||
{
|
||||
"txid": "20b984492b5264162a4c92c9a34bc7fa08b67d669de7b4c5982ad3cb28aaecf6",
|
||||
"version": 2,
|
||||
"locktime": 0,
|
||||
"vin": [
|
||||
{
|
||||
"txid": "3adda6afd547193793c248e667c2b7dbf26d705003de65e3a25e5be698286aef",
|
||||
"vout": 2,
|
||||
"prevout": {
|
||||
"scriptpubkey": "0014989cf12774fc705609610c7b9419f2d1c4807644",
|
||||
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 989cf12774fc705609610c7b9419f2d1c4807644",
|
||||
"scriptpubkey_type": "v0_p2wpkh",
|
||||
"scriptpubkey_address": "bc1qnzw0zfm5l3c9vztpp3aegx0j68zgqajyffr2r6",
|
||||
"value": 27619
|
||||
},
|
||||
"scriptsig": "",
|
||||
"scriptsig_asm": "",
|
||||
"witness": [
|
||||
"304402205d7f1e0d928982645c2bcc4c730c4545c382d6520c2a14eebc71594702cd06b302200511d452ce51c79017536f50acb115eefe7c04506ad12b9307d2b5d56b999beb01",
|
||||
"03716cb4f0430fe69c596a12c6680c55803150645989b406772838d548cde7cca5"
|
||||
],
|
||||
"is_coinbase": false,
|
||||
"sequence": 4294967295
|
||||
}
|
||||
],
|
||||
"vout": [
|
||||
{
|
||||
"scriptpubkey": "6a5d0614c0a2331441",
|
||||
"scriptpubkey_asm": "OP_RETURN OP_PUSHNUM_13 OP_PUSHBYTES_6 14c0a2331441",
|
||||
"scriptpubkey_type": "op_return",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"scriptpubkey": "5114d71c6c3ea7ba7e6ee477a0bfd82c20c78997882c",
|
||||
"scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_20 d71c6c3ea7ba7e6ee477a0bfd82c20c78997882c",
|
||||
"scriptpubkey_type": "unknown",
|
||||
"scriptpubkey_address": "bc1p6uwxc048hflxaerh5zlastpqc7ye0zpvq7gq2a",
|
||||
"value": 546
|
||||
},
|
||||
{
|
||||
"scriptpubkey": "0014989cf12774fc705609610c7b9419f2d1c4807644",
|
||||
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 989cf12774fc705609610c7b9419f2d1c4807644",
|
||||
"scriptpubkey_type": "v0_p2wpkh",
|
||||
"scriptpubkey_address": "bc1qnzw0zfm5l3c9vztpp3aegx0j68zgqajyffr2r6",
|
||||
"value": 23073
|
||||
}
|
||||
],
|
||||
"size": 240,
|
||||
"weight": 633,
|
||||
"sigops": 1,
|
||||
"fee": 4000,
|
||||
"status": {
|
||||
"confirmed": true,
|
||||
"block_height": 848136,
|
||||
"block_hash": "00000000000000000002c69c7a3010fcd596c0c7451c23e7cd1f5e19ebf8ee6d",
|
||||
"block_time": 1718517071
|
||||
}
|
||||
}
|
||||
]
|
@ -13,7 +13,7 @@ const vectorBuffer: Buffer = fs.readFileSync(path.join(__dirname, './', './test-
|
||||
|
||||
describe('Rust GBT', () => {
|
||||
test('should produce the same template as getBlockTemplate from Bitcoin Core', async () => {
|
||||
const rustGbt = new GbtGenerator();
|
||||
const rustGbt = new GbtGenerator(4_000_000, 8);
|
||||
const { mempool, maxUid } = mempoolFromArrayBuffer(vectorBuffer.buffer);
|
||||
const result = await rustGbt.make(mempool, [], maxUid);
|
||||
|
||||
|
@ -107,8 +107,14 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
|
||||
}
|
||||
|
||||
$getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> {
|
||||
throw new Error('Method getTxsForBlock not supported by the Bitcoin RPC API.');
|
||||
async $getTxsForBlock(hash: string): Promise<IEsploraApi.Transaction[]> {
|
||||
const verboseBlock: IBitcoinApi.VerboseBlock = await this.bitcoindClient.getBlock(hash, 2);
|
||||
const transactions: IEsploraApi.Transaction[] = [];
|
||||
for (const tx of verboseBlock.tx) {
|
||||
const converted = await this.$convertTransaction(tx, true);
|
||||
transactions.push(converted);
|
||||
}
|
||||
return transactions;
|
||||
}
|
||||
|
||||
$getRawBlock(hash: string): Promise<Buffer> {
|
||||
|
@ -258,9 +258,15 @@ export class Common {
|
||||
let opreturnCount = 0;
|
||||
for (const vout of tx.vout) {
|
||||
// scriptpubkey
|
||||
if (['unknown', 'provably_unspendable', 'empty'].includes(vout.scriptpubkey_type)) {
|
||||
if (['nonstandard', 'provably_unspendable', 'empty'].includes(vout.scriptpubkey_type)) {
|
||||
// (non-standard output type)
|
||||
return true;
|
||||
} else if (vout.scriptpubkey_type === 'unknown') {
|
||||
// undefined segwit version/length combinations are actually standard in outputs
|
||||
// https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/interpreter.cpp#L1950-L1951
|
||||
if (vout.scriptpubkey.startsWith('00') || !this.isWitnessProgram(vout.scriptpubkey)) {
|
||||
return true;
|
||||
}
|
||||
} else if (vout.scriptpubkey_type === 'multisig') {
|
||||
if (!DEFAULT_PERMIT_BAREMULTISIG) {
|
||||
// bare-multisig
|
||||
@ -308,6 +314,27 @@ export class Common {
|
||||
return false;
|
||||
}
|
||||
|
||||
// A witness program is any valid scriptpubkey that consists of a 1-byte push opcode
|
||||
// followed by a data push between 2 and 40 bytes.
|
||||
// https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/script.cpp#L224-L240
|
||||
static isWitnessProgram(scriptpubkey: string): false | { version: number, program: string } {
|
||||
if (scriptpubkey.length < 8 || scriptpubkey.length > 84) {
|
||||
return false;
|
||||
}
|
||||
const version = parseInt(scriptpubkey.slice(0,2), 16);
|
||||
if (version !== 0 && version < 0x51 || version > 0x60) {
|
||||
return false;
|
||||
}
|
||||
const push = parseInt(scriptpubkey.slice(2,4), 16);
|
||||
if (push + 2 === (scriptpubkey.length / 2)) {
|
||||
return {
|
||||
version: version ? version - 0x50 : 0,
|
||||
program: scriptpubkey.slice(4),
|
||||
};
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static getNonWitnessSize(tx: TransactionExtended): number {
|
||||
let weight = tx.weight;
|
||||
let hasWitness = false;
|
||||
@ -877,9 +904,10 @@ export class Common {
|
||||
let medianFee = 0;
|
||||
let medianWeight = 0;
|
||||
|
||||
// calculate the "medianFee" as the average fee rate of the middle 10000 weight units of transactions
|
||||
const leftBound = 1995000;
|
||||
const rightBound = 2005000;
|
||||
// calculate the "medianFee" as the average fee rate of the middle 0.25% weight units of transactions
|
||||
const halfWidth = config.MEMPOOL.BLOCK_WEIGHT_UNITS / 800;
|
||||
const leftBound = Math.floor((config.MEMPOOL.BLOCK_WEIGHT_UNITS / 2) - halfWidth);
|
||||
const rightBound = Math.ceil((config.MEMPOOL.BLOCK_WEIGHT_UNITS / 2) + halfWidth);
|
||||
for (let i = 0; i < sortedTxs.length && weightCount < rightBound; i++) {
|
||||
const left = weightCount;
|
||||
const right = weightCount + sortedTxs[i].weight;
|
||||
|
@ -16,7 +16,7 @@ class MempoolBlocks {
|
||||
private mempoolBlockDeltas: MempoolBlockDelta[] = [];
|
||||
private txSelectionWorker: Worker | null = null;
|
||||
private rustInitialized: boolean = false;
|
||||
private rustGbtGenerator: GbtGenerator = new GbtGenerator();
|
||||
private rustGbtGenerator: GbtGenerator = new GbtGenerator(config.MEMPOOL.BLOCK_WEIGHT_UNITS, config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT);
|
||||
|
||||
private nextUid: number = 1;
|
||||
private uidMap: Map<number, string> = new Map(); // map short numerical uids to full txids
|
||||
@ -230,7 +230,7 @@ class MempoolBlocks {
|
||||
|
||||
private resetRustGbt(): void {
|
||||
this.rustInitialized = false;
|
||||
this.rustGbtGenerator = new GbtGenerator();
|
||||
this.rustGbtGenerator = new GbtGenerator(config.MEMPOOL.BLOCK_WEIGHT_UNITS, config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT);
|
||||
}
|
||||
|
||||
public async $rustMakeBlockTemplates(txids: string[], newMempool: { [txid: string]: MempoolTransactionExtended }, candidates: GbtCandidates | undefined, saveResults: boolean = false, useAccelerations: boolean = false, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
|
||||
@ -262,7 +262,7 @@ class MempoolBlocks {
|
||||
});
|
||||
|
||||
// run the block construction algorithm in a separate thread, and wait for a result
|
||||
const rustGbt = saveResults ? this.rustGbtGenerator : new GbtGenerator();
|
||||
const rustGbt = saveResults ? this.rustGbtGenerator : new GbtGenerator(config.MEMPOOL.BLOCK_WEIGHT_UNITS, config.MEMPOOL.MEMPOOL_BLOCKS_AMOUNT);
|
||||
try {
|
||||
const { blocks, blockWeights, rates, clusters, overflow } = this.convertNapiResultTxids(
|
||||
await rustGbt.make(transactions as RustThreadTransaction[], convertedAccelerations as RustThreadAcceleration[], this.nextUid),
|
||||
|
@ -31,10 +31,7 @@ export interface AccelerationHistory {
|
||||
feeDelta: number,
|
||||
blockHash: string,
|
||||
blockHeight: number,
|
||||
pools: {
|
||||
pool_unique_id: number,
|
||||
username: string,
|
||||
}[],
|
||||
pools: number[];
|
||||
};
|
||||
|
||||
class AccelerationApi {
|
||||
|
@ -1300,7 +1300,7 @@ class WebsocketHandler {
|
||||
// and zips it together into a valid JSON object
|
||||
private serializeResponse(response): string {
|
||||
return '{'
|
||||
+ Object.keys(response).map(key => `"${key}": ${response[key]}`).join(', ')
|
||||
+ Object.keys(response).filter(key => response[key] != null).map(key => `"${key}": ${response[key]}`).join(', ')
|
||||
+ '}';
|
||||
}
|
||||
|
||||
|
@ -308,10 +308,10 @@ class AccelerationRepository {
|
||||
}
|
||||
const accelerationSummaries = accelerations.map(acc => ({
|
||||
...acc,
|
||||
pools: acc.pools.map(pool => pool.pool_unique_id),
|
||||
pools: acc.pools,
|
||||
}))
|
||||
for (const acc of accelerations) {
|
||||
if (blockTxs[acc.txid] && acc.pools.some(pool => pool.pool_unique_id === block.extras.pool.id)) {
|
||||
if (blockTxs[acc.txid] && acc.pools.includes(block.extras.pool.id)) {
|
||||
const tx = blockTxs[acc.txid];
|
||||
const accelerationInfo = accelerationCosts.getAccelerationInfo(tx, boostRate, transactions);
|
||||
accelerationInfo.cost = Math.max(0, Math.min(acc.feeDelta, accelerationInfo.cost));
|
||||
|
@ -76,7 +76,7 @@ class FreeCurrencyApi implements ConversionFeed {
|
||||
}
|
||||
|
||||
public async $fetchConversionRates(date: string): Promise<ConversionRates> {
|
||||
const response = await query(`${this.API_URL_PREFIX}historical?date=${date}&apikey=${this.API_KEY}`);
|
||||
const response = await query(`${this.API_URL_PREFIX}historical?date=${date}&apikey=${this.API_KEY}`, true);
|
||||
if (response && response['data'] && (response['data'][date] || this.PAID)) {
|
||||
if (this.PAID) {
|
||||
response['data'] = this.convertData(response['data']);
|
||||
|
@ -59,7 +59,7 @@ class PriceUpdater {
|
||||
private currencyConversionFeed: ConversionFeed | undefined;
|
||||
private newCurrencies: string[] = ['BGN', 'BRL', 'CNY', 'CZK', 'DKK', 'HKD', 'HRK', 'HUF', 'IDR', 'ILS', 'INR', 'ISK', 'KRW', 'MXN', 'MYR', 'NOK', 'NZD', 'PHP', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'THB', 'TRY', 'ZAR'];
|
||||
private lastTimeConversionsRatesFetched: number = 0;
|
||||
private latestConversionsRatesFromFeed: ConversionRates = {};
|
||||
private latestConversionsRatesFromFeed: ConversionRates = { USD: -1 };
|
||||
private ratesChangedCallback: ((rates: ApiPrice) => void) | undefined;
|
||||
|
||||
constructor() {
|
||||
@ -157,9 +157,9 @@ class PriceUpdater {
|
||||
try {
|
||||
this.latestConversionsRatesFromFeed = await this.currencyConversionFeed.$fetchLatestConversionRates();
|
||||
this.lastTimeConversionsRatesFetched = Math.round(new Date().getTime() / 1000);
|
||||
logger.debug(`Fetched currencies conversion rates from external API: ${JSON.stringify(this.latestConversionsRatesFromFeed)}`);
|
||||
logger.debug(`Fetched currencies conversion rates from conversions API: ${JSON.stringify(this.latestConversionsRatesFromFeed)}`);
|
||||
} catch (e) {
|
||||
logger.err(`Cannot fetch conversion rates from the API. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||
logger.err(`Cannot fetch conversion rates from conversions API. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -408,17 +408,17 @@ class PriceUpdater {
|
||||
try {
|
||||
const remainingQuota = await this.currencyConversionFeed?.$getQuota();
|
||||
if (remainingQuota['month']['remaining'] < 500) { // We need some calls left for the daily updates
|
||||
logger.debug(`Not enough currency API credit to insert missing prices in ${priceTimesToFill.length} rows (${remainingQuota['month']['remaining']} calls left).`, logger.tags.mining);
|
||||
logger.debug(`Not enough conversions API credit to insert missing prices in ${priceTimesToFill.length} rows (${remainingQuota['month']['remaining']} calls left).`, logger.tags.mining);
|
||||
this.additionalCurrenciesHistoryInserted = true; // Do not try again until next day
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err(`Cannot fetch currency API credit, insertion of missing prices aborted. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||
logger.err(`Cannot fetch conversions API credit, insertion of missing prices aborted. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.additionalCurrenciesHistoryRunning = true;
|
||||
logger.debug(`Fetching missing conversion rates from external API to fill ${priceTimesToFill.length} rows`, logger.tags.mining);
|
||||
logger.debug(`Inserting missing historical conversion rates using conversions API to fill ${priceTimesToFill.length} rows`, logger.tags.mining);
|
||||
|
||||
let conversionRates: { [timestamp: number]: ConversionRates } = {};
|
||||
let totalInserted = 0;
|
||||
@ -430,10 +430,23 @@ class PriceUpdater {
|
||||
const month = new Date(priceTime.time * 1000).getMonth();
|
||||
const yearMonthTimestamp = new Date(year, month, 1).getTime() / 1000;
|
||||
if (conversionRates[yearMonthTimestamp] === undefined) {
|
||||
conversionRates[yearMonthTimestamp] = await this.currencyConversionFeed?.$fetchConversionRates(`${year}-${month + 1 < 10 ? `0${month + 1}` : `${month + 1}`}-01`) || { USD: -1 };
|
||||
if (conversionRates[yearMonthTimestamp]['USD'] < 0) {
|
||||
logger.err(`Cannot fetch conversion rates from the API for ${year}-${month + 1 < 10 ? `0${month + 1}` : `${month + 1}`}-01. Aborting insertion of missing prices.`, logger.tags.mining);
|
||||
this.lastFailedHistoricalRun = Math.round(new Date().getTime() / 1000);
|
||||
try {
|
||||
if (year === new Date().getFullYear() && month === new Date().getMonth()) { // For rows in the current month, we use the latest conversion rates
|
||||
conversionRates[yearMonthTimestamp] = this.latestConversionsRatesFromFeed;
|
||||
} else {
|
||||
conversionRates[yearMonthTimestamp] = await this.currencyConversionFeed?.$fetchConversionRates(`${year}-${month + 1 < 10 ? `0${month + 1}` : `${month + 1}`}-15`) || { USD: -1 };
|
||||
}
|
||||
|
||||
if (conversionRates[yearMonthTimestamp]['USD'] < 0) {
|
||||
throw new Error('Incorrect USD conversion rate');
|
||||
}
|
||||
} catch (e) {
|
||||
if ((e instanceof Error ? e.message : '').includes('429')) { // Continue 60 seconds later if and only if error is 429
|
||||
this.lastFailedHistoricalRun = Math.round(new Date().getTime() / 1000);
|
||||
logger.info(`Got a 429 error from conversions API. This is expected to happen a few times during the initial historical price insertion, process will resume in 60 seconds.`, logger.tags.mining);
|
||||
} else {
|
||||
logger.err(`Cannot fetch conversion rates from conversions API for ${year}-${month + 1 < 10 ? `0${month + 1}` : `${month + 1}`}-01, trying again next day. Error: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import config from '../config';
|
||||
import logger from '../logger';
|
||||
import * as https from 'https';
|
||||
|
||||
export async function query(path): Promise<object | undefined> {
|
||||
export async function query(path, throwOnFail: boolean = false): Promise<object | undefined> {
|
||||
type axiosOptions = {
|
||||
headers: {
|
||||
'User-Agent': string
|
||||
@ -21,6 +21,7 @@ export async function query(path): Promise<object | undefined> {
|
||||
timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000
|
||||
};
|
||||
let retry = 0;
|
||||
let lastError: any = null;
|
||||
|
||||
while (retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) {
|
||||
try {
|
||||
@ -50,6 +51,7 @@ export async function query(path): Promise<object | undefined> {
|
||||
}
|
||||
return data.data;
|
||||
} catch (e) {
|
||||
lastError = e;
|
||||
logger.warn(`Could not connect to ${path} (Attempt ${retry + 1}/${config.MEMPOOL.EXTERNAL_MAX_RETRY}). Reason: ` + (e instanceof Error ? e.message : e));
|
||||
retry++;
|
||||
}
|
||||
@ -59,5 +61,10 @@ export async function query(path): Promise<object | undefined> {
|
||||
}
|
||||
|
||||
logger.err(`Could not connect to ${path}. All ${config.MEMPOOL.EXTERNAL_MAX_RETRY} attempts failed`);
|
||||
|
||||
if (throwOnFail && lastError) {
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
3
contributors/mackalex.txt
Normal file
3
contributors/mackalex.txt
Normal file
@ -0,0 +1,3 @@
|
||||
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of June 18th, 2024.
|
||||
|
||||
Signed: mackalex
|
@ -16,6 +16,7 @@ fi
|
||||
|
||||
# Runtime overrides - read env vars defined in docker compose
|
||||
|
||||
__MAINNET_ENABLED__=${MAINNET_ENABLED:=true}
|
||||
__TESTNET_ENABLED__=${TESTNET_ENABLED:=false}
|
||||
__SIGNET_ENABLED__=${SIGNET_ENABLED:=false}
|
||||
__LIQUID_ENABLED__=${LIQUID_ENABLED:=false}
|
||||
@ -28,6 +29,7 @@ __NGINX_PORT__=${NGINX_PORT:=8999}
|
||||
__BLOCK_WEIGHT_UNITS__=${BLOCK_WEIGHT_UNITS:=4000000}
|
||||
__MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_BLOCKS_AMOUNT:=8}
|
||||
__BASE_MODULE__=${BASE_MODULE:=mempool}
|
||||
__ROOT_NETWORK__=${ROOT_NETWORK:=}
|
||||
__MEMPOOL_WEBSITE_URL__=${MEMPOOL_WEBSITE_URL:=https://mempool.space}
|
||||
__LIQUID_WEBSITE_URL__=${LIQUID_WEBSITE_URL:=https://liquid.network}
|
||||
__MINING_DASHBOARD__=${MINING_DASHBOARD:=true}
|
||||
@ -42,6 +44,7 @@ __HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
|
||||
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
|
||||
|
||||
# Export as environment variables to be used by envsubst
|
||||
export __MAINNET_ENABLED__
|
||||
export __TESTNET_ENABLED__
|
||||
export __SIGNET_ENABLED__
|
||||
export __LIQUID_ENABLED__
|
||||
@ -54,6 +57,7 @@ export __NGINX_PORT__
|
||||
export __BLOCK_WEIGHT_UNITS__
|
||||
export __MEMPOOL_BLOCKS_AMOUNT__
|
||||
export __BASE_MODULE__
|
||||
export __ROOT_NETWORK__
|
||||
export __MEMPOOL_WEBSITE_URL__
|
||||
export __LIQUID_WEBSITE_URL__
|
||||
export __MINING_DASHBOARD__
|
||||
|
@ -72,20 +72,6 @@ describe('Liquid', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('renders unconfidential addresses correctly on mobile', () => {
|
||||
cy.viewport('iphone-6');
|
||||
cy.visit(`${basePath}/address/ex1qqmmjdwrlg59c8q4l75sj6wedjx57tj5grt8pat`);
|
||||
cy.waitForSkeletonGone();
|
||||
//TODO: Add proper IDs for these selectors
|
||||
const firstRowSelector = '.container-xl > :nth-child(3) > div > :nth-child(1) > .table > tbody';
|
||||
const thirdRowSelector = '.container-xl > :nth-child(3) > div > :nth-child(3)';
|
||||
cy.get(firstRowSelector).invoke('css', 'width').then(firstRowWidth => {
|
||||
cy.get(thirdRowSelector).invoke('css', 'width').then(thirdRowWidth => {
|
||||
expect(parseInt(firstRowWidth)).to.be.lessThan(parseInt(thirdRowWidth));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('peg in/peg out', () => {
|
||||
it('loads peg in addresses', () => {
|
||||
cy.visit(`${basePath}/tx/fe764f7bedfc2a37b29d9c8aef67d64a57d253a6b11c5a55555cfd5826483a58`);
|
||||
|
@ -4,6 +4,7 @@
|
||||
"SIGNET_ENABLED": false,
|
||||
"LIQUID_ENABLED": false,
|
||||
"LIQUID_TESTNET_ENABLED": false,
|
||||
"MAINNET_ENABLED": true,
|
||||
"ITEMS_PER_PAGE": 10,
|
||||
"KEEP_BLOCKS_AMOUNT": 8,
|
||||
"NGINX_PROTOCOL": "http",
|
||||
@ -12,6 +13,7 @@
|
||||
"BLOCK_WEIGHT_UNITS": 4000000,
|
||||
"MEMPOOL_BLOCKS_AMOUNT": 8,
|
||||
"BASE_MODULE": "mempool",
|
||||
"ROOT_NETWORK": "",
|
||||
"MEMPOOL_WEBSITE_URL": "https://mempool.space",
|
||||
"LIQUID_WEBSITE_URL": "https://liquid.network",
|
||||
"MINING_DASHBOARD": true,
|
||||
|
43
frontend/package-lock.json
generated
43
frontend/package-lock.json
generated
@ -32,7 +32,6 @@
|
||||
"bootstrap": "~4.6.2",
|
||||
"browserify": "^17.0.0",
|
||||
"clipboard": "^2.0.11",
|
||||
"cypress": "^13.11.0",
|
||||
"domino": "^2.1.6",
|
||||
"echarts": "~5.5.0",
|
||||
"esbuild": "^0.21.1",
|
||||
@ -63,7 +62,7 @@
|
||||
"optionalDependencies": {
|
||||
"@cypress/schematic": "^2.5.0",
|
||||
"@types/cypress": "^1.1.3",
|
||||
"cypress": "^13.11.0",
|
||||
"cypress": "^13.12.0",
|
||||
"cypress-fail-on-console-error": "~5.1.0",
|
||||
"cypress-wait-until": "^2.0.1",
|
||||
"mock-socket": "~9.3.1",
|
||||
@ -6106,11 +6105,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@ -8029,9 +8028,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/cypress": {
|
||||
"version": "13.11.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.11.0.tgz",
|
||||
"integrity": "sha512-NXXogbAxVlVje4XHX+Cx5eMFZv4Dho/2rIcdBHg9CNPFUGZdM4cRdgIgM7USmNYsC12XY0bZENEQ+KBk72fl+A==",
|
||||
"version": "13.12.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.12.0.tgz",
|
||||
"integrity": "sha512-udzS2JilmI9ApO/UuqurEwOvThclin5ntz7K0BtnHBs+tg2Bl9QShLISXpSEMDv/u8b6mqdoAdyKeZiSqKWL8g==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@ -10152,9 +10151,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
@ -22636,11 +22635,11 @@
|
||||
}
|
||||
},
|
||||
"braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"requires": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
}
|
||||
},
|
||||
"brorand": {
|
||||
@ -24112,9 +24111,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"cypress": {
|
||||
"version": "13.11.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.11.0.tgz",
|
||||
"integrity": "sha512-NXXogbAxVlVje4XHX+Cx5eMFZv4Dho/2rIcdBHg9CNPFUGZdM4cRdgIgM7USmNYsC12XY0bZENEQ+KBk72fl+A==",
|
||||
"version": "13.12.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.12.0.tgz",
|
||||
"integrity": "sha512-udzS2JilmI9ApO/UuqurEwOvThclin5ntz7K0BtnHBs+tg2Bl9QShLISXpSEMDv/u8b6mqdoAdyKeZiSqKWL8g==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@cypress/request": "^3.0.0",
|
||||
@ -25757,9 +25756,9 @@
|
||||
}
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"requires": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
}
|
||||
|
@ -115,7 +115,7 @@
|
||||
"optionalDependencies": {
|
||||
"@cypress/schematic": "^2.5.0",
|
||||
"@types/cypress": "^1.1.3",
|
||||
"cypress": "^13.11.0",
|
||||
"cypress": "^13.12.0",
|
||||
"cypress-fail-on-console-error": "~5.1.0",
|
||||
"cypress-wait-until": "^2.0.1",
|
||||
"mock-socket": "~9.3.1",
|
||||
|
@ -78,6 +78,18 @@ PROXY_CONFIG.push(...[
|
||||
"^/testnet": ""
|
||||
},
|
||||
},
|
||||
/* Optional proxy to route dev to official acceleration services
|
||||
{
|
||||
context: ['/api/v1/services/accelerator/**'],
|
||||
target: `https://mempool.space/api/v1/services/accelerator/`,
|
||||
secure: false,
|
||||
changeOrigin: true,
|
||||
proxyTimeout: 30000,
|
||||
pathRewrite: {
|
||||
"^/api/v1/services/accelerator": ""
|
||||
},
|
||||
},
|
||||
*/
|
||||
{
|
||||
context: ['/api/v1/services/**'],
|
||||
target: `http://localhost:9000`,
|
||||
|
@ -189,7 +189,7 @@
|
||||
<ng-container>
|
||||
<ng-template ngFor let-sponsor [ngForOf]="profiles.whales">
|
||||
<a [href]="'https://twitter.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/unknown.svg'; this.className = 'image unknown'"/>
|
||||
<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'"/>
|
||||
</a>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
@ -201,7 +201,7 @@
|
||||
<div class="wrapper">
|
||||
<ng-template ngFor let-sponsor [ngForOf]="profiles.chads">
|
||||
<a [href]="'https://twitter.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/unknown.svg'; this.className = 'image unknown'"/>
|
||||
<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'"/>
|
||||
</a>
|
||||
</ng-template>
|
||||
</div>
|
||||
@ -214,7 +214,7 @@
|
||||
<div class="wrapper">
|
||||
<ng-container *ngIf="ogs$ | async as ogs; else loadingSponsors">
|
||||
<a *ngFor="let ogSponsor of ogs" [href]="'https://twitter.com/' + ogSponsor.handle" target="_blank" rel="sponsored" [title]="ogSponsor.handle">
|
||||
<img class="image" [src]="'/api/v1/donations/images/' + ogSponsor.handle" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||
<img class="image" [src]="'/api/v1/donations/images/' + ogSponsor.handle" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
|
||||
</a>
|
||||
</ng-container>
|
||||
</div>
|
||||
@ -355,7 +355,7 @@
|
||||
<div class="wrapper">
|
||||
<ng-template ngFor let-translator [ngForOf]="translators">
|
||||
<a [href]="'https://twitter.com/' + translator.value" target="_blank" [title]="translator.key">
|
||||
<img class="image" [src]="'/api/v1/translators/images/' + translator.value" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||
<img class="image" [src]="'/api/v1/translators/images/' + translator.value" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
|
||||
</a>
|
||||
</ng-template>
|
||||
</div>
|
||||
@ -369,7 +369,7 @@
|
||||
<div class="wrapper">
|
||||
<ng-template ngFor let-contributor [ngForOf]="contributors.regular">
|
||||
<a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name">
|
||||
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
|
||||
<span>{{ contributor.name }}</span>
|
||||
</a>
|
||||
</ng-template>
|
||||
@ -381,7 +381,7 @@
|
||||
<div class="wrapper">
|
||||
<ng-template ngFor let-contributor [ngForOf]="contributors.core">
|
||||
<a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name" [class]="'project-member-avatar'">
|
||||
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
|
||||
<span>{{ contributor.name }}</span>
|
||||
</a>
|
||||
</ng-template>
|
||||
|
@ -10,9 +10,6 @@
|
||||
margin: 25px;
|
||||
line-height: 32px;
|
||||
}
|
||||
.unknown {
|
||||
border: 1px solid #b4b4b4;
|
||||
}
|
||||
|
||||
.image.not-rounded {
|
||||
border-radius: 0;
|
||||
|
@ -96,10 +96,16 @@ export class AcceleratorDashboardComponent implements OnInit, OnDestroy {
|
||||
share(),
|
||||
);
|
||||
|
||||
this.minedAccelerations$ = this.accelerations$.pipe(
|
||||
map(accelerations => {
|
||||
return accelerations.filter(acc => ['completed_provisional', 'completed'].includes(acc.status));
|
||||
})
|
||||
this.minedAccelerations$ = this.stateService.chainTip$.pipe(
|
||||
distinctUntilChanged(),
|
||||
switchMap(() => {
|
||||
return this.serviceApiServices.getAccelerationHistory$({ status: 'completed', pageLength: 6 }).pipe(
|
||||
catchError(() => {
|
||||
return of([]);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
share(),
|
||||
);
|
||||
|
||||
this.blocks$ = combineLatest([
|
||||
|
@ -4,7 +4,7 @@
|
||||
<a [routerLink]="['/lightning/channel' | relativeUrl, channel.id]">
|
||||
<span
|
||||
*ngIf="label"
|
||||
class="badge badge-pill badge-warning"
|
||||
class="badge badge-pill badge-warning {{ class }}"
|
||||
>{{ label }}</span>
|
||||
</a>
|
||||
</div>
|
||||
@ -15,6 +15,6 @@
|
||||
<ng-template #default>
|
||||
<span
|
||||
*ngIf="label"
|
||||
class="badge badge-pill badge-warning"
|
||||
class="badge badge-pill badge-warning {{ class }}"
|
||||
>{{ label }}</span>
|
||||
</ng-template>
|
@ -1,7 +1,7 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
|
||||
import { Vin, Vout } from '../../interfaces/electrs.interface';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { parseMultisigScript } from '../../bitcoin.utils';
|
||||
import { AddressType, AddressTypeInfo } from '../../shared/address-utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-address-labels',
|
||||
@ -12,9 +12,11 @@ import { parseMultisigScript } from '../../bitcoin.utils';
|
||||
export class AddressLabelsComponent implements OnChanges {
|
||||
network = '';
|
||||
|
||||
@Input() address: AddressTypeInfo;
|
||||
@Input() vin: Vin;
|
||||
@Input() vout: Vout;
|
||||
@Input() channel: any;
|
||||
@Input() class: string = '';
|
||||
|
||||
label?: string;
|
||||
|
||||
@ -27,10 +29,10 @@ export class AddressLabelsComponent implements OnChanges {
|
||||
ngOnChanges() {
|
||||
if (this.channel) {
|
||||
this.handleChannel();
|
||||
} else if (this.address) {
|
||||
this.handleAddress();
|
||||
} else if (this.vin) {
|
||||
this.handleVin();
|
||||
} else if (this.vout) {
|
||||
this.handleVout();
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,74 +43,22 @@ export class AddressLabelsComponent implements OnChanges {
|
||||
this.label = `Channel ${type}: ${leftNodeName} <> ${rightNodeName}`;
|
||||
}
|
||||
|
||||
handleAddress() {
|
||||
if (this.address?.scripts.size) {
|
||||
const script = this.address?.scripts.values().next().value;
|
||||
if (script.template?.label) {
|
||||
this.label = script.template.label;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleVin() {
|
||||
if (this.vin.inner_witnessscript_asm) {
|
||||
if (this.vin.inner_witnessscript_asm.indexOf('OP_DEPTH OP_PUSHNUM_12 OP_EQUAL OP_IF OP_PUSHNUM_11') === 0 || this.vin.inner_witnessscript_asm.indexOf('OP_PUSHNUM_15 OP_CHECKMULTISIG OP_IFDUP OP_NOTIF OP_PUSHBYTES_2') === 1259) {
|
||||
if (this.vin.witness.length > 11) {
|
||||
this.label = 'Liquid Peg Out';
|
||||
} else {
|
||||
this.label = 'Emergency Liquid Peg Out';
|
||||
}
|
||||
return;
|
||||
const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin])
|
||||
if (address?.scripts.size) {
|
||||
const script = address?.scripts.values().next().value;
|
||||
if (script.template?.label) {
|
||||
this.label = script.template.label;
|
||||
}
|
||||
|
||||
const topElement = this.vin.witness[this.vin.witness.length - 2];
|
||||
if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSH(NUM_\d+|BYTES_(1 \w{2}|2 \w{4})) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(this.vin.inner_witnessscript_asm)) {
|
||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs
|
||||
if (topElement === '01') {
|
||||
// top element is '01' to get in the revocation path
|
||||
this.label = 'Revoked Lightning Force Close';
|
||||
} else {
|
||||
// top element is '', this is a delayed to_local output
|
||||
this.label = 'Lightning Force Close';
|
||||
}
|
||||
return;
|
||||
} else if (
|
||||
/^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_NOTIF OP_DROP OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(this.vin.inner_witnessscript_asm) ||
|
||||
/^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_IF OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_DROP OP_PUSHBYTES_3 \w{6} OP_CLTV OP_DROP OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(this.vin.inner_witnessscript_asm)
|
||||
) {
|
||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs
|
||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#received-htlc-outputs
|
||||
if (topElement.length === 66) {
|
||||
// top element is a public key
|
||||
this.label = 'Revoked Lightning HTLC';
|
||||
} else if (topElement) {
|
||||
// top element is a preimage
|
||||
this.label = 'Lightning HTLC';
|
||||
} else {
|
||||
// top element is '' to get in the expiry of the script
|
||||
this.label = 'Expired Lightning HTLC';
|
||||
}
|
||||
return;
|
||||
} else if (/^OP_PUSHBYTES_33 \w{66} OP_CHECKSIG OP_IFDUP OP_NOTIF OP_PUSHNUM_16 OP_CSV OP_ENDIF$/.test(this.vin.inner_witnessscript_asm)) {
|
||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#to_local_anchor-and-to_remote_anchor-output-option_anchors
|
||||
if (topElement) {
|
||||
// top element is a signature
|
||||
this.label = 'Lightning Anchor';
|
||||
} else {
|
||||
// top element is '', it has been swept after 16 blocks
|
||||
this.label = 'Swept Lightning Anchor';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.detectMultisig(this.vin.inner_witnessscript_asm);
|
||||
}
|
||||
|
||||
this.detectMultisig(this.vin.inner_redeemscript_asm);
|
||||
|
||||
this.detectMultisig(this.vin.prevout.scriptpubkey_asm);
|
||||
}
|
||||
|
||||
detectMultisig(script: string) {
|
||||
const ms = parseMultisigScript(script);
|
||||
|
||||
if (ms) {
|
||||
this.label = $localize`:@@address-label.multisig:Multisig ${ms.m}:multisigM: of ${ms.n}:multisigN:`;
|
||||
}
|
||||
}
|
||||
|
||||
handleVout() {
|
||||
this.detectMultisig(this.vout.scriptpubkey_asm);
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,13 @@
|
||||
<h1 i18n="shared.address">Address</h1>
|
||||
<div class="tx-link">
|
||||
<app-truncate [text]="addressString" [lastChars]="8" [link]="['/address/' | relativeUrl, addressString]">
|
||||
<app-clipboard [text]="addressString"></app-clipboard>
|
||||
<app-clipboard [text]="addressString" [size]="isMobile ? 'large' : 'normal'"></app-clipboard>
|
||||
<span style="position: relative; cursor: pointer" (mouseover)="showQR = true" (mouseout)="showQR = false" (pointerdown)="showQR = true">
|
||||
<fa-icon [icon]="['fas', 'qrcode']" [fixedWidth]="true" [style.font-size]="isMobile ? '18px' : '12px'"></fa-icon>
|
||||
<div class="qr-wrapper" [hidden]="!showQR">
|
||||
<app-qrcode [size]="200" [data]="addressString"></app-qrcode>
|
||||
</div>
|
||||
</span>
|
||||
</app-truncate>
|
||||
</div>
|
||||
</div>
|
||||
@ -14,40 +20,47 @@
|
||||
<div class="box">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<table class="table table-borderless table-striped address-table">
|
||||
<tbody>
|
||||
<tr *ngIf="addressInfo && addressInfo.unconfidential">
|
||||
<td i18n="address.unconfidential">Unconfidential</td>
|
||||
<td>
|
||||
<app-truncate [text]="addressInfo.unconfidential" [lastChars]="8" [link]="['/address/' | relativeUrl, addressInfo.unconfidential]">
|
||||
<app-clipboard [text]="addressInfo.unconfidential"></app-clipboard>
|
||||
</app-truncate>
|
||||
</td>
|
||||
</tr>
|
||||
<ng-template [ngIf]="!address.electrum">
|
||||
<tr>
|
||||
<td i18n="address.total-received">Total received</td>
|
||||
<td *ngIf="address.chain_stats.funded_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="received" [noFiat]="true"></app-amount></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="address.total-sent">Total sent</td>
|
||||
<td *ngIf="address.chain_stats.spent_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="sent" [noFiat]="true"></app-amount></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<tr>
|
||||
<td i18n="address.balance">Balance</td>
|
||||
<td *ngIf="address.chain_stats.funded_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="received - sent" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="received - sent"></app-fiat></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="w-100 d-block d-md-none"></div>
|
||||
<div class="col-md qrcode-col">
|
||||
<div class="qr-wrapper">
|
||||
<app-qrcode [data]="address.address"></app-qrcode>
|
||||
@if (isMobile) {
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped address-table">
|
||||
<tbody>
|
||||
<ng-container *ngTemplateOutlet="balanceRow"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="pendingBalanceRow"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="utxoRow"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="pendingUtxoRow"></ng-container>
|
||||
@if (network === 'liquid' || network === 'liquidtestnet') {
|
||||
<ng-container *ngTemplateOutlet="liquidRow"></ng-container>
|
||||
} @else {
|
||||
<ng-container *ngTemplateOutlet="volumeRow"></ng-container>
|
||||
}
|
||||
<ng-container *ngTemplateOutlet="typeRow"></ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped table-fixed address-table">
|
||||
<tbody>
|
||||
<ng-container *ngTemplateOutlet="balanceRow"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="utxoRow"></ng-container>
|
||||
@if (network === 'liquid' || network === 'liquidtestnet') {
|
||||
<ng-container *ngTemplateOutlet="liquidRow"></ng-container>
|
||||
} @else {
|
||||
<ng-container *ngTemplateOutlet="volumeRow"></ng-container>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped table-fixed address-table">
|
||||
<tbody>
|
||||
<ng-container *ngTemplateOutlet="pendingBalanceRow"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="pendingUtxoRow"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="typeRow"></ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -76,8 +89,8 @@
|
||||
<div class="title-tx">
|
||||
<h2 class="text-left">
|
||||
<ng-template [ngIf]="!transactions?.length"> </ng-template>
|
||||
<ng-template i18n="X of X Address Transaction" [ngIf]="transactions?.length === 1">{{ (transactions?.length | number) || '?' }} of {{ txCount | number }} transaction</ng-template>
|
||||
<ng-template i18n="X of X Address Transactions (Plural)" [ngIf]="transactions?.length > 1">{{ (transactions?.length | number) || '?' }} of {{ txCount | number }} transactions</ng-template>
|
||||
<ng-template i18n="X of X Address Transaction" [ngIf]="transactions?.length === 1">{{ (transactions?.length | number) || '?' }} of {{ mempoolStats.tx_count + chainStats.tx_count | number }} transaction</ng-template>
|
||||
<ng-template i18n="X of X Address Transactions (Plural)" [ngIf]="transactions?.length > 1">{{ (transactions?.length | number) || '?' }} of {{ mempoolStats.tx_count + chainStats.tx_count | number }} transactions</ng-template>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@ -182,3 +195,57 @@
|
||||
<span class="skeleton-loader"></span>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
|
||||
<ng-template #balanceRow>
|
||||
<tr>
|
||||
<td i18n="address.balance">Balance</td>
|
||||
<td *ngIf="chainStats.funded_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="chainStats.balance" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="chainStats.balance"></app-fiat></span></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #pendingBalanceRow>
|
||||
<tr>
|
||||
<td i18n="address.unconfirmed-balance" class="font-italic">unconfirmed balance</td>
|
||||
<td *ngIf="mempoolStats.funded_txo_sum !== undefined; else confidentialTd" class="font-italic"><app-amount [satoshis]="mempoolStats.balance" [noFiat]="true" [addPlus]="true"></app-amount> <span class="fiat"><app-fiat [value]="mempoolStats.balance"></app-fiat></span></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #utxoRow>
|
||||
<tr>
|
||||
<td i18n="address.utxos" i18n-ngbTooltip="unspent-transaction-outputs" ngbTooltip="unspent transaction outputs">UTXOs</td>
|
||||
<td>{{ chainStats.utxos }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #pendingUtxoRow>
|
||||
<tr>
|
||||
<td i18n="address.unconfirmed-utxos" class="font-italic">unconfirmed UTXOs</td>
|
||||
<td class="font-italic">{{ mempoolStats.utxos > 0 ? '+' : ''}}{{ mempoolStats.utxos }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #volumeRow>
|
||||
<tr>
|
||||
<td i18n="address.volume">Volume</td>
|
||||
<td *ngIf="chainStats.funded_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="chainStats.volume + mempoolStats.volume"></app-amount></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #typeRow>
|
||||
<tr>
|
||||
<td i18n="address.type">Type</td>
|
||||
<td class="wrap-cell"><app-address-type [address]="addressTypeInfo"></app-address-type><app-address-labels [channel]="exampleChannel" [address]="addressTypeInfo" class="ml-1"></app-address-labels></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #liquidRow>
|
||||
<tr *ngIf="addressInfo && addressInfo.unconfidential">
|
||||
<td i18n="address.unconfidential">Unconfidential</td>
|
||||
<td>
|
||||
<app-truncate [text]="addressInfo.unconfidential" [lastChars]="8" [textAlign]="isMobile ? 'end' : 'start'" [link]="['/address/' | relativeUrl, addressInfo.unconfidential]">
|
||||
<app-clipboard [text]="addressInfo.unconfidential"></app-clipboard>
|
||||
</app-truncate>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
@ -1,16 +1,14 @@
|
||||
.qr-wrapper {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
right: 0px;
|
||||
border: solid 10px var(--active-bg);
|
||||
border-radius: 5px;
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
padding-bottom: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.qrcode-col {
|
||||
margin: 20px auto 10px;
|
||||
text-align: center;
|
||||
@media (min-width: 992px){
|
||||
margin: 0px auto 0px;
|
||||
}
|
||||
display: block;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.fiat {
|
||||
@ -25,10 +23,14 @@
|
||||
tr td {
|
||||
&:last-child {
|
||||
text-align: right;
|
||||
@media (min-width: 576px) {
|
||||
@media (min-width: 768px) {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
&.wrap-cell {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,10 +80,10 @@ h1 {
|
||||
top: 9px;
|
||||
position: relative;
|
||||
@media (min-width: 576px) {
|
||||
max-width: calc(100% - 180px);
|
||||
top: 11px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
max-width: calc(100% - 180px);
|
||||
top: 17px;
|
||||
}
|
||||
}
|
||||
@ -96,17 +98,6 @@ h1 {
|
||||
.liquid-address {
|
||||
.address-table {
|
||||
table-layout: fixed;
|
||||
|
||||
tr td:first-child {
|
||||
width: 170px;
|
||||
}
|
||||
tr td:last-child {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.qrcode-col {
|
||||
flex-grow: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
|
||||
import { Address, ScriptHash, Transaction } from '../../interfaces/electrs.interface';
|
||||
import { Address, ChainStats, Transaction, Vin } from '../../interfaces/electrs.interface';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { AudioService } from '../../services/audio.service';
|
||||
@ -11,6 +11,83 @@ import { of, merge, Subscription, Observable } from 'rxjs';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
import { AddressInformation } from '../../interfaces/node-api.interface';
|
||||
import { AddressTypeInfo } from '../../shared/address-utils';
|
||||
|
||||
class AddressStats implements ChainStats {
|
||||
address: string;
|
||||
scriptpubkey?: string;
|
||||
funded_txo_count: number;
|
||||
funded_txo_sum: number;
|
||||
spent_txo_count: number;
|
||||
spent_txo_sum: number;
|
||||
tx_count: number;
|
||||
|
||||
constructor (stats: ChainStats, address: string, scriptpubkey?: string) {
|
||||
Object.assign(this, stats);
|
||||
this.address = address;
|
||||
this.scriptpubkey = scriptpubkey;
|
||||
}
|
||||
|
||||
public addTx(tx: Transaction): void {
|
||||
for (const vin of tx.vin) {
|
||||
if (vin.prevout?.scriptpubkey_address === this.address || (this.scriptpubkey === vin.prevout?.scriptpubkey)) {
|
||||
this.spendTxo(vin.prevout.value);
|
||||
}
|
||||
}
|
||||
for (const vout of tx.vout) {
|
||||
if (vout.scriptpubkey_address === this.address || (this.scriptpubkey === vout.scriptpubkey)) {
|
||||
this.fundTxo(vout.value);
|
||||
}
|
||||
}
|
||||
this.tx_count++;
|
||||
}
|
||||
|
||||
public removeTx(tx: Transaction): void {
|
||||
for (const vin of tx.vin) {
|
||||
if (vin.prevout?.scriptpubkey_address === this.address || (this.scriptpubkey === vin.prevout?.scriptpubkey)) {
|
||||
this.unspendTxo(vin.prevout.value);
|
||||
}
|
||||
}
|
||||
for (const vout of tx.vout) {
|
||||
if (vout.scriptpubkey_address === this.address || (this.scriptpubkey === vout.scriptpubkey)) {
|
||||
this.unfundTxo(vout.value);
|
||||
}
|
||||
}
|
||||
this.tx_count--;
|
||||
}
|
||||
|
||||
private fundTxo(value: number): void {
|
||||
this.funded_txo_sum += value;
|
||||
this.funded_txo_count++;
|
||||
}
|
||||
|
||||
private unfundTxo(value: number): void {
|
||||
this.funded_txo_sum -= value;
|
||||
this.funded_txo_count--;
|
||||
}
|
||||
|
||||
private spendTxo(value: number): void {
|
||||
this.spent_txo_sum += value;
|
||||
this.spent_txo_count++;
|
||||
}
|
||||
|
||||
private unspendTxo(value: number): void {
|
||||
this.spent_txo_sum -= value;
|
||||
this.spent_txo_count--;
|
||||
}
|
||||
|
||||
get balance(): number {
|
||||
return this.funded_txo_sum - this.spent_txo_sum;
|
||||
}
|
||||
|
||||
get volume(): number {
|
||||
return this.funded_txo_sum + this.spent_txo_sum;
|
||||
}
|
||||
|
||||
get utxos(): number {
|
||||
return this.funded_txo_count - this.spent_txo_count;
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-address',
|
||||
@ -20,6 +97,9 @@ import { AddressInformation } from '../../interfaces/node-api.interface';
|
||||
export class AddressComponent implements OnInit, OnDestroy {
|
||||
network = '';
|
||||
|
||||
isMobile: boolean;
|
||||
showQR: boolean = false;
|
||||
|
||||
address: Address;
|
||||
addressString: string;
|
||||
isLoadingAddress = true;
|
||||
@ -33,11 +113,14 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
blockTxSubscription: Subscription;
|
||||
addressLoadingStatus$: Observable<number>;
|
||||
addressInfo: null | AddressInformation = null;
|
||||
addressTypeInfo: null | AddressTypeInfo;
|
||||
|
||||
fullyLoaded = false;
|
||||
txCount = 0;
|
||||
received = 0;
|
||||
sent = 0;
|
||||
chainStats: AddressStats;
|
||||
mempoolStats: AddressStats;
|
||||
|
||||
exampleChannel?: any;
|
||||
|
||||
now = Date.now() / 1000;
|
||||
balancePeriod: 'all' | '1m' = 'all';
|
||||
|
||||
@ -55,10 +138,12 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
private seoService: SeoService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
ngOnInit(): void {
|
||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.onResize();
|
||||
|
||||
this.addressLoadingStatus$ = this.route.paramMap
|
||||
.pipe(
|
||||
switchMap(() => this.stateService.loadingIndicators$),
|
||||
@ -75,6 +160,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.isLoadingTransactions = true;
|
||||
this.transactions = null;
|
||||
this.addressInfo = null;
|
||||
this.exampleChannel = null;
|
||||
document.body.scrollTo(0, 0);
|
||||
this.addressString = params.get('id') || '';
|
||||
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(this.addressString)) {
|
||||
@ -83,6 +169,8 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} address ${this.addressString}:INTERPOLATION:.`);
|
||||
|
||||
this.addressTypeInfo = new AddressTypeInfo(this.stateService.network || 'mainnet', this.addressString);
|
||||
|
||||
return merge(
|
||||
of(true),
|
||||
this.stateService.connectionState$
|
||||
@ -175,11 +263,19 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
this.transactions = this.tempTransactions;
|
||||
if (this.transactions.length === this.txCount) {
|
||||
if (this.transactions.length === (this.mempoolStats.tx_count + this.chainStats.tx_count)) {
|
||||
this.fullyLoaded = true;
|
||||
}
|
||||
this.isLoadingTransactions = false;
|
||||
|
||||
let addressVin: Vin[] = [];
|
||||
for (const tx of this.transactions) {
|
||||
addressVin = addressVin.concat(tx.vin.filter(v => v.prevout?.scriptpubkey_address === this.address.address));
|
||||
}
|
||||
this.addressTypeInfo.processInputs(addressVin);
|
||||
// hack to trigger change detection
|
||||
this.addressTypeInfo = this.addressTypeInfo.clone();
|
||||
|
||||
if (!this.showBalancePeriod()) {
|
||||
this.setBalancePeriod('all');
|
||||
} else {
|
||||
@ -196,11 +292,13 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.mempoolTxSubscription = this.stateService.mempoolTransactions$
|
||||
.subscribe(tx => {
|
||||
this.addTransaction(tx);
|
||||
this.mempoolStats.addTx(tx);
|
||||
});
|
||||
|
||||
this.mempoolRemovedTxSubscription = this.stateService.mempoolRemovedTransactions$
|
||||
.subscribe(tx => {
|
||||
this.removeTransaction(tx);
|
||||
this.mempoolStats.removeTx(tx);
|
||||
});
|
||||
|
||||
this.blockTxSubscription = this.stateService.blockTransactions$
|
||||
@ -209,12 +307,14 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
if (tx) {
|
||||
tx.status = transaction.status;
|
||||
this.transactions = this.transactions.slice();
|
||||
this.mempoolStats.removeTx(transaction);
|
||||
this.audioService.playSound('magic');
|
||||
} else {
|
||||
if (this.addTransaction(transaction, false)) {
|
||||
this.audioService.playSound('magic');
|
||||
}
|
||||
}
|
||||
this.chainStats.addTx(transaction);
|
||||
});
|
||||
}
|
||||
|
||||
@ -225,7 +325,6 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.transactions.unshift(transaction);
|
||||
this.transactions = this.transactions.slice();
|
||||
this.txCount++;
|
||||
|
||||
if (playSound) {
|
||||
if (transaction.vout.some((vout) => vout?.scriptpubkey_address === this.address.address)) {
|
||||
@ -235,17 +334,6 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
transaction.vin.forEach((vin) => {
|
||||
if (vin?.prevout?.scriptpubkey_address === this.address.address) {
|
||||
this.sent += vin.prevout.value;
|
||||
}
|
||||
});
|
||||
transaction.vout.forEach((vout) => {
|
||||
if (vout?.scriptpubkey_address === this.address.address) {
|
||||
this.received += vout.value;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -257,23 +345,11 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.transactions.splice(index, 1);
|
||||
this.transactions = this.transactions.slice();
|
||||
this.txCount--;
|
||||
|
||||
transaction.vin.forEach((vin) => {
|
||||
if (vin?.prevout?.scriptpubkey_address === this.address.address) {
|
||||
this.sent -= vin.prevout.value;
|
||||
}
|
||||
});
|
||||
transaction.vout.forEach((vout) => {
|
||||
if (vout?.scriptpubkey_address === this.address.address) {
|
||||
this.received -= vout.value;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
loadMore(): void {
|
||||
if (this.isLoadingTransactions || this.fullyLoaded) {
|
||||
return;
|
||||
}
|
||||
@ -301,10 +377,9 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
updateChainStats() {
|
||||
this.received = this.address.chain_stats.funded_txo_sum + this.address.mempool_stats.funded_txo_sum;
|
||||
this.sent = this.address.chain_stats.spent_txo_sum + this.address.mempool_stats.spent_txo_sum;
|
||||
this.txCount = this.address.chain_stats.tx_count + this.address.mempool_stats.tx_count;
|
||||
updateChainStats(): void {
|
||||
this.chainStats = new AddressStats(this.address.chain_stats, this.address.address);
|
||||
this.mempoolStats = new AddressStats(this.address.mempool_stats, this.address.address);
|
||||
}
|
||||
|
||||
setBalancePeriod(period: 'all' | '1m'): boolean {
|
||||
@ -319,7 +394,12 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(): void {
|
||||
this.isMobile = window.innerWidth < 768;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.mainSubscription.unsubscribe();
|
||||
this.mempoolTxSubscription.unsubscribe();
|
||||
this.mempoolRemovedTxSubscription.unsubscribe();
|
||||
|
@ -411,7 +411,7 @@
|
||||
<td class="text-wrap">
|
||||
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
||||
<span *ngIf="oobFees" class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band">
|
||||
+<app-amount [satoshis]="oobFees" digitsInfo="1.2-8" [noFiat]="true"></app-amount>
|
||||
<app-amount [satoshis]="oobFees" digitsInfo="1.2-8" [noFiat]="true" [addPlus]="true"></app-amount>
|
||||
</span>
|
||||
<span *ngIf="blockAudit.feeDelta" class="difference" [class.positive]="blockAudit.feeDelta <= 0" [class.negative]="blockAudit.feeDelta > 0">
|
||||
{{ blockAudit.feeDelta < 0 ? '+' : '' }}{{ (-blockAudit.feeDelta * 100) | amountShortener: 2 }}%
|
||||
|
@ -8,7 +8,7 @@ import { StateService } from '../../services/state.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
import { BlockAudit, BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
|
||||
import { Acceleration, BlockAudit, BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
|
||||
import { detectWebGL } from '../../shared/graphs.utils';
|
||||
@ -44,6 +44,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
latestBlocks: BlockExtended[] = [];
|
||||
oobFees: number = 0;
|
||||
strippedTransactions: TransactionStripped[];
|
||||
accelerations: Acceleration[];
|
||||
overviewTransitionDirection: string;
|
||||
isLoadingOverview = true;
|
||||
error: any;
|
||||
@ -68,6 +69,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
mode: 'projected' | 'actual' = 'projected';
|
||||
|
||||
overviewSubscription: Subscription;
|
||||
accelerationsSubscription: Subscription;
|
||||
keyNavigationSubscription: Subscription;
|
||||
blocksSubscription: Subscription;
|
||||
cacheBlocksSubscription: Subscription;
|
||||
@ -183,6 +185,9 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
} else {
|
||||
this.isLoadingBlock = true;
|
||||
this.isLoadingOverview = true;
|
||||
this.strippedTransactions = undefined;
|
||||
this.blockAudit = undefined;
|
||||
this.accelerations = undefined;
|
||||
|
||||
let blockInCache: BlockExtended;
|
||||
if (isBlockHeight) {
|
||||
@ -294,158 +299,36 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
this.overviewError = err;
|
||||
return of(null);
|
||||
})
|
||||
),
|
||||
this.stateService.env.ACCELERATOR === true && block.height > 819500
|
||||
? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height })
|
||||
.pipe(catchError(() => {
|
||||
return of([]);
|
||||
}))
|
||||
: of([])
|
||||
)
|
||||
]);
|
||||
})
|
||||
)
|
||||
.subscribe(([transactions, blockAudit, accelerations]) => {
|
||||
.subscribe(([transactions, blockAudit]) => {
|
||||
if (transactions) {
|
||||
this.strippedTransactions = transactions;
|
||||
} else {
|
||||
this.strippedTransactions = [];
|
||||
}
|
||||
this.blockAudit = blockAudit;
|
||||
|
||||
const acceleratedInBlock = {};
|
||||
for (const acc of accelerations) {
|
||||
if (acc.pools?.some(pool => pool === this.block?.extras?.pool.id || pool?.['pool_unique_id'] === this.block?.extras?.pool.id)) {
|
||||
acceleratedInBlock[acc.txid] = acc;
|
||||
}
|
||||
}
|
||||
for (const tx of transactions) {
|
||||
if (acceleratedInBlock[tx.txid]) {
|
||||
tx.acc = true;
|
||||
const acceleration = acceleratedInBlock[tx.txid];
|
||||
const boostCost = acceleration.boostCost || acceleration.bidBoost;
|
||||
const acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
|
||||
if (acceleratedFeeRate > tx.rate) {
|
||||
tx.rate = acceleratedFeeRate;
|
||||
}
|
||||
} else {
|
||||
tx.acc = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.blockAudit = null;
|
||||
if (transactions && blockAudit) {
|
||||
const inTemplate = {};
|
||||
const inBlock = {};
|
||||
const isAdded = {};
|
||||
const isPrioritized = {};
|
||||
const isCensored = {};
|
||||
const isMissing = {};
|
||||
const isSelected = {};
|
||||
const isFresh = {};
|
||||
const isSigop = {};
|
||||
const isRbf = {};
|
||||
const isAccelerated = {};
|
||||
this.numMissing = 0;
|
||||
this.numUnexpected = 0;
|
||||
|
||||
if (blockAudit?.template) {
|
||||
for (const tx of blockAudit.template) {
|
||||
inTemplate[tx.txid] = true;
|
||||
if (tx.acc) {
|
||||
isAccelerated[tx.txid] = true;
|
||||
}
|
||||
}
|
||||
for (const tx of transactions) {
|
||||
inBlock[tx.txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.addedTxs) {
|
||||
isAdded[txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.prioritizedTxs || []) {
|
||||
isPrioritized[txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.missingTxs) {
|
||||
isCensored[txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.freshTxs || []) {
|
||||
isFresh[txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.sigopTxs || []) {
|
||||
isSigop[txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.fullrbfTxs || []) {
|
||||
isRbf[txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.acceleratedTxs || []) {
|
||||
isAccelerated[txid] = true;
|
||||
}
|
||||
// set transaction statuses
|
||||
for (const tx of blockAudit.template) {
|
||||
tx.context = 'projected';
|
||||
if (isCensored[tx.txid]) {
|
||||
tx.status = 'censored';
|
||||
} else if (inBlock[tx.txid]) {
|
||||
tx.status = 'found';
|
||||
} else {
|
||||
if (isFresh[tx.txid]) {
|
||||
if (tx.rate - (tx.fee / tx.vsize) >= 0.1) {
|
||||
tx.status = 'freshcpfp';
|
||||
} else {
|
||||
tx.status = 'fresh';
|
||||
}
|
||||
} else if (isSigop[tx.txid]) {
|
||||
tx.status = 'sigop';
|
||||
} else if (isRbf[tx.txid]) {
|
||||
tx.status = 'rbf';
|
||||
} else {
|
||||
tx.status = 'missing';
|
||||
}
|
||||
isMissing[tx.txid] = true;
|
||||
this.numMissing++;
|
||||
}
|
||||
if (isAccelerated[tx.txid]) {
|
||||
tx.status = 'accelerated';
|
||||
}
|
||||
}
|
||||
for (const [index, tx] of transactions.entries()) {
|
||||
tx.context = 'actual';
|
||||
if (index === 0) {
|
||||
tx.status = null;
|
||||
} else if (isAdded[tx.txid]) {
|
||||
tx.status = 'added';
|
||||
} else if (isPrioritized[tx.txid]) {
|
||||
tx.status = 'prioritized';
|
||||
} else if (inTemplate[tx.txid]) {
|
||||
tx.status = 'found';
|
||||
} else if (isRbf[tx.txid]) {
|
||||
tx.status = 'rbf';
|
||||
} else {
|
||||
tx.status = 'selected';
|
||||
isSelected[tx.txid] = true;
|
||||
this.numUnexpected++;
|
||||
}
|
||||
if (isAccelerated[tx.txid]) {
|
||||
tx.status = 'accelerated';
|
||||
}
|
||||
}
|
||||
for (const tx of transactions) {
|
||||
inBlock[tx.txid] = true;
|
||||
}
|
||||
|
||||
blockAudit.feeDelta = blockAudit.expectedFees > 0 ? (blockAudit.expectedFees - (this.block?.extras.totalFees + this.oobFees)) / blockAudit.expectedFees : 0;
|
||||
blockAudit.weightDelta = blockAudit.expectedWeight > 0 ? (blockAudit.expectedWeight - this.block?.weight) / blockAudit.expectedWeight : 0;
|
||||
blockAudit.txDelta = blockAudit.template.length > 0 ? (blockAudit.template.length - this.block?.tx_count) / blockAudit.template.length : 0;
|
||||
this.blockAudit = blockAudit;
|
||||
this.setAuditAvailable(true);
|
||||
} else {
|
||||
this.setAuditAvailable(false);
|
||||
}
|
||||
} else {
|
||||
this.setAuditAvailable(false);
|
||||
}
|
||||
|
||||
this.setupBlockAudit();
|
||||
this.isLoadingOverview = false;
|
||||
this.setupBlockGraphs();
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
|
||||
this.accelerationsSubscription = this.block$.pipe(
|
||||
switchMap((block) => {
|
||||
return this.stateService.env.ACCELERATOR === true && block.height > 819500
|
||||
? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height })
|
||||
.pipe(catchError(() => {
|
||||
return of([]);
|
||||
}))
|
||||
: of([]);
|
||||
})
|
||||
).subscribe((accelerations) => {
|
||||
this.accelerations = accelerations;
|
||||
if (accelerations.length) {
|
||||
this.setupBlockAudit();
|
||||
}
|
||||
});
|
||||
|
||||
this.oobSubscription = this.block$.pipe(
|
||||
@ -609,6 +492,147 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
setupBlockAudit(): void {
|
||||
const transactions = this.strippedTransactions || [];
|
||||
const blockAudit = this.blockAudit;
|
||||
const accelerations = this.accelerations || [];
|
||||
|
||||
const acceleratedInBlock = {};
|
||||
for (const acc of accelerations) {
|
||||
if (acc.pools?.some(pool => pool === this.block?.extras?.pool.id)) {
|
||||
acceleratedInBlock[acc.txid] = acc;
|
||||
}
|
||||
}
|
||||
|
||||
for (const tx of transactions) {
|
||||
if (acceleratedInBlock[tx.txid]) {
|
||||
tx.acc = true;
|
||||
const acceleration = acceleratedInBlock[tx.txid];
|
||||
const boostCost = acceleration.boostCost || acceleration.bidBoost;
|
||||
const acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
|
||||
if (acceleratedFeeRate > tx.rate) {
|
||||
tx.rate = acceleratedFeeRate;
|
||||
}
|
||||
} else {
|
||||
tx.acc = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (transactions && blockAudit) {
|
||||
const inTemplate = {};
|
||||
const inBlock = {};
|
||||
const isAdded = {};
|
||||
const isPrioritized = {};
|
||||
const isCensored = {};
|
||||
const isMissing = {};
|
||||
const isSelected = {};
|
||||
const isFresh = {};
|
||||
const isSigop = {};
|
||||
const isRbf = {};
|
||||
const isAccelerated = {};
|
||||
this.numMissing = 0;
|
||||
this.numUnexpected = 0;
|
||||
|
||||
if (blockAudit?.template) {
|
||||
for (const tx of blockAudit.template) {
|
||||
inTemplate[tx.txid] = true;
|
||||
if (tx.acc) {
|
||||
isAccelerated[tx.txid] = true;
|
||||
}
|
||||
}
|
||||
for (const tx of transactions) {
|
||||
inBlock[tx.txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.addedTxs) {
|
||||
isAdded[txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.prioritizedTxs || []) {
|
||||
isPrioritized[txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.missingTxs) {
|
||||
isCensored[txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.freshTxs || []) {
|
||||
isFresh[txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.sigopTxs || []) {
|
||||
isSigop[txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.fullrbfTxs || []) {
|
||||
isRbf[txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.acceleratedTxs || []) {
|
||||
isAccelerated[txid] = true;
|
||||
}
|
||||
// set transaction statuses
|
||||
for (const tx of blockAudit.template) {
|
||||
tx.context = 'projected';
|
||||
if (isCensored[tx.txid]) {
|
||||
tx.status = 'censored';
|
||||
} else if (inBlock[tx.txid]) {
|
||||
tx.status = 'found';
|
||||
} else {
|
||||
if (isFresh[tx.txid]) {
|
||||
if (tx.rate - (tx.fee / tx.vsize) >= 0.1) {
|
||||
tx.status = 'freshcpfp';
|
||||
} else {
|
||||
tx.status = 'fresh';
|
||||
}
|
||||
} else if (isSigop[tx.txid]) {
|
||||
tx.status = 'sigop';
|
||||
} else if (isRbf[tx.txid]) {
|
||||
tx.status = 'rbf';
|
||||
} else {
|
||||
tx.status = 'missing';
|
||||
}
|
||||
isMissing[tx.txid] = true;
|
||||
this.numMissing++;
|
||||
}
|
||||
if (isAccelerated[tx.txid]) {
|
||||
tx.status = 'accelerated';
|
||||
}
|
||||
}
|
||||
for (const [index, tx] of transactions.entries()) {
|
||||
tx.context = 'actual';
|
||||
if (index === 0) {
|
||||
tx.status = null;
|
||||
} else if (isAdded[tx.txid]) {
|
||||
tx.status = 'added';
|
||||
} else if (isPrioritized[tx.txid]) {
|
||||
tx.status = 'prioritized';
|
||||
} else if (inTemplate[tx.txid]) {
|
||||
tx.status = 'found';
|
||||
} else if (isRbf[tx.txid]) {
|
||||
tx.status = 'rbf';
|
||||
} else {
|
||||
tx.status = 'selected';
|
||||
isSelected[tx.txid] = true;
|
||||
this.numUnexpected++;
|
||||
}
|
||||
if (isAccelerated[tx.txid]) {
|
||||
tx.status = 'accelerated';
|
||||
}
|
||||
}
|
||||
for (const tx of transactions) {
|
||||
inBlock[tx.txid] = true;
|
||||
}
|
||||
|
||||
blockAudit.feeDelta = blockAudit.expectedFees > 0 ? (blockAudit.expectedFees - (this.block?.extras.totalFees + this.oobFees)) / blockAudit.expectedFees : 0;
|
||||
blockAudit.weightDelta = blockAudit.expectedWeight > 0 ? (blockAudit.expectedWeight - this.block?.weight) / blockAudit.expectedWeight : 0;
|
||||
blockAudit.txDelta = blockAudit.template.length > 0 ? (blockAudit.template.length - this.block?.tx_count) / blockAudit.template.length : 0;
|
||||
this.blockAudit = blockAudit;
|
||||
this.setAuditAvailable(true);
|
||||
} else {
|
||||
this.setAuditAvailable(false);
|
||||
}
|
||||
} else {
|
||||
this.setAuditAvailable(false);
|
||||
}
|
||||
|
||||
this.setupBlockGraphs();
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
setupBlockGraphs(): void {
|
||||
if (this.blockAudit || this.strippedTransactions) {
|
||||
this.blockGraphProjected.forEach(graph => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
<ng-template [ngIf]="button" [ngIfElse]="btnLink">
|
||||
<button #btn [attr.data-clipboard-text]="text" [class]="class" type="button" [disabled]="text === ''">
|
||||
<span #buttonWrapper [attr.data-tlite]="copiedMessage" style="position: relative;top: -2px;left: 1px;">
|
||||
<app-svg-images name="clippy" [width]="size === 'small' ? '10' : '13'" viewBox="0 0 1000 1000"></app-svg-images>
|
||||
<app-svg-images name="clippy" [width]="widths[size]" viewBox="0 0 1000 1000"></app-svg-images>
|
||||
</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
@ -9,7 +9,7 @@
|
||||
<ng-template #btnLink>
|
||||
<span #buttonWrapper [attr.data-tlite]="copiedMessage" style="position: relative;">
|
||||
<button #btn class="btn btn-sm btn-link pt-0 {{ leftPadding ? 'padding' : '' }}" [attr.data-clipboard-text]="text">
|
||||
<app-svg-images name="clippy" [width]="size === 'small' ? '10' : '13'" viewBox="0 0 1000 1000"></app-svg-images>
|
||||
<app-svg-images name="clippy" [width]="widths[size]" viewBox="0 0 1000 1000"></app-svg-images>
|
||||
</button>
|
||||
</span>
|
||||
</ng-template>
|
||||
|
@ -13,11 +13,17 @@ export class ClipboardComponent implements AfterViewInit {
|
||||
@ViewChild('buttonWrapper') buttonWrapper: ElementRef;
|
||||
@Input() button = false;
|
||||
@Input() class = 'btn btn-secondary ml-1';
|
||||
@Input() size: 'small' | 'normal' = 'normal';
|
||||
@Input() size: 'small' | 'normal' | 'large' = 'normal';
|
||||
@Input() text: string;
|
||||
@Input() leftPadding = true;
|
||||
copiedMessage: string = $localize`:@@clipboard.copied-message:Copied!`;
|
||||
|
||||
widths = {
|
||||
small: '10',
|
||||
normal: '13',
|
||||
large: '18',
|
||||
};
|
||||
|
||||
clipboard: any;
|
||||
|
||||
constructor() { }
|
||||
|
@ -62,12 +62,12 @@
|
||||
}
|
||||
</a>
|
||||
|
||||
<div (window:resize)="onResize()" ngbDropdown class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.TESTNET4_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.LIQUID_TESTNET_ENABLED">
|
||||
<div (window:resize)="onResize()" ngbDropdown class="dropdown-container" *ngIf="isDropdownVisible">
|
||||
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split d-flex justify-content-center align-items-center" aria-haspopup="true">
|
||||
<app-svg-images class="d-flex justify-content-center align-items-center current-network-svg" [name]="network.val === '' ? 'bitcoin' : network.val" width="20" height="20" viewBox="0 0 65 65"></app-svg-images>
|
||||
</button>
|
||||
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
|
||||
<a ngbDropdownItem class="mainnet" [routerLink]="networkPaths['mainnet'] || '/'"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
|
||||
<a ngbDropdownItem *ngIf="env.MAINNET_ENABLED" class="mainnet" [routerLink]="networkPaths['mainnet'] || '/'"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
|
||||
<a ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" [routerLink]="networkPaths['signet'] || '/signet'"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
|
||||
<a ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" [routerLink]="networkPaths['testnet'] || '/testnet'"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet3</a>
|
||||
<a ngbDropdownItem *ngIf="env.TESTNET4_ENABLED" class="testnet4" [class.active]="network.val === 'testnet4'" [routerLink]="networkPaths['testnet4'] || '/testnet4'"><app-svg-images name="testnet4" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet4 <span class="badge badge-pill badge-warning beta-network" i18n="beta">beta</span></a>
|
||||
|
@ -31,6 +31,7 @@ export class MasterPageComponent implements OnInit, OnDestroy {
|
||||
user: any = undefined;
|
||||
servicesEnabled = false;
|
||||
menuOpen = false;
|
||||
isDropdownVisible: boolean;
|
||||
|
||||
enterpriseInfo: any;
|
||||
enterpriseInfo$: Subscription;
|
||||
@ -74,19 +75,27 @@ export class MasterPageComponent implements OnInit, OnDestroy {
|
||||
|
||||
const isServicesPage = this.router.url.includes('/services/');
|
||||
this.menuOpen = isServicesPage && !this.isSmallScreen();
|
||||
this.setDropdownVisibility();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.enterpriseInfo$) {
|
||||
this.enterpriseInfo$.unsubscribe();
|
||||
}
|
||||
setDropdownVisibility(): void {
|
||||
const networks = [
|
||||
this.env.TESTNET_ENABLED,
|
||||
this.env.TESTNET4_ENABLED,
|
||||
this.env.SIGNET_ENABLED,
|
||||
this.env.LIQUID_ENABLED,
|
||||
this.env.LIQUID_TESTNET_ENABLED,
|
||||
this.env.MAINNET_ENABLED,
|
||||
];
|
||||
const enabledNetworksCount = networks.filter((networkEnabled) => networkEnabled).length;
|
||||
this.isDropdownVisible = enabledNetworksCount > 1;
|
||||
}
|
||||
|
||||
collapse(): void {
|
||||
this.navCollapsed = !this.navCollapsed;
|
||||
}
|
||||
|
||||
isSmallScreen() {
|
||||
isSmallScreen(): boolean {
|
||||
return window.innerWidth <= 767.98;
|
||||
}
|
||||
|
||||
@ -117,4 +126,11 @@ export class MasterPageComponent implements OnInit, OnDestroy {
|
||||
menuToggled(isOpen: boolean): void {
|
||||
this.menuOpen = isOpen;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.enterpriseInfo$) {
|
||||
this.enterpriseInfo$.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -9,13 +9,13 @@
|
||||
|
||||
<ng-container *ngIf="(hosts$ | async) as hosts">
|
||||
<div class="status-panel">
|
||||
<table class="status-table table table-fixed table-borderless table-striped" *ngIf="(tip$ | async) as tip">
|
||||
<table class="status-table table table-borderless table-striped" *ngIf="(tip$ | async) as tip">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th class="rank"></th>
|
||||
<th class="flag"></th>
|
||||
<th class="host">Host</th>
|
||||
<th class="updated">Last checked</th>
|
||||
<th class="updated">Updated</th>
|
||||
<th class="rtt only-small">RTT</th>
|
||||
<th class="rtt only-large">RTT</th>
|
||||
<th class="height">Height</th>
|
||||
|
@ -20,26 +20,21 @@
|
||||
|
||||
td, th {
|
||||
padding: 0.25em;
|
||||
width: 0%;
|
||||
|
||||
&.rank, &.flag {
|
||||
width: 28px;
|
||||
text-align: right;
|
||||
}
|
||||
&.updated {
|
||||
display: none;
|
||||
width: 130px;
|
||||
text-align: right;
|
||||
white-space: pre-wrap;
|
||||
white-space: nowrap;
|
||||
}
|
||||
&.rtt, &.height {
|
||||
width: 92px;
|
||||
text-align: right;
|
||||
}
|
||||
&.only-small {
|
||||
display: table-cell;
|
||||
&.rtt {
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
&.only-large {
|
||||
display: none;
|
||||
@ -48,21 +43,17 @@
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
&.host {
|
||||
width: auto;
|
||||
width: 100%;
|
||||
max-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
&.rank, &.flag {
|
||||
width: 32px;
|
||||
}
|
||||
&.updated {
|
||||
display: table-cell;
|
||||
}
|
||||
&.rtt, &.height {
|
||||
width: 96px;
|
||||
}
|
||||
&.only-small {
|
||||
display: none;
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ export class ServerHealthComponent implements OnInit {
|
||||
getLastUpdateSeconds(host: HealthCheckHost): string {
|
||||
if (host.lastChecked) {
|
||||
const seconds = Math.ceil((this.now - host.lastChecked) / 1000);
|
||||
return `${seconds} second${seconds > 1 ? 's' : ' '} ago`;
|
||||
return `${seconds} s`;
|
||||
} else {
|
||||
return '~';
|
||||
}
|
||||
|
@ -665,7 +665,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
setIsAccelerated(initialState: boolean = false) {
|
||||
this.isAcceleration = (this.tx.acceleration || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id || pool?.['pool_unique_id'] === this.pool.id))));
|
||||
this.isAcceleration = (this.tx.acceleration || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id))));
|
||||
}
|
||||
|
||||
dismissAccelAlert(): void {
|
||||
|
@ -726,7 +726,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
setIsAccelerated(initialState: boolean = false) {
|
||||
this.isAcceleration = (this.tx.acceleration || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id || pool?.['pool_unique_id'] === this.pool.id))));
|
||||
this.isAcceleration = (this.tx.acceleration || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id))));
|
||||
if (this.isAcceleration && initialState) {
|
||||
this.showAccelerationSummary = false;
|
||||
}
|
||||
|
@ -75,7 +75,7 @@
|
||||
{{ vin.prevout.scriptpubkey_type?.toUpperCase() }}
|
||||
</ng-template>
|
||||
<div>
|
||||
<app-address-labels [vin]="vin" [channel]="tx._channels && tx._channels.inputs[vindex] ? tx._channels.inputs[vindex] : null"></app-address-labels>
|
||||
<app-address-labels [vin]="vin" [channel]="tx._channels && tx._channels.inputs[vindex] ? tx._channels.inputs[vindex] : null"></app-address-labels>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
@ -9126,11 +9126,7 @@ export const restApiDocsData = [
|
||||
"blockHash": "00000000000000000000482f0746d62141694b9210a813b97eb8445780a32003",
|
||||
"blockHeight": 829559,
|
||||
"bidBoost": 6102,
|
||||
"pools": [
|
||||
{
|
||||
"pool_unique_id": 111
|
||||
}
|
||||
]
|
||||
"pools": [111]
|
||||
}
|
||||
]`,
|
||||
},
|
||||
|
@ -23,7 +23,7 @@ export class LightningApiService {
|
||||
}
|
||||
this.apiBasePath = ''; // assume mainnet by default
|
||||
this.stateService.networkChanged$.subscribe((network) => {
|
||||
this.apiBasePath = network ? '/' + network : '';
|
||||
this.apiBasePath = network && network !== this.stateService.env.ROOT_NETWORK ? '/' + network : '';
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ export class ApiService {
|
||||
}
|
||||
this.apiBasePath = ''; // assume mainnet by default
|
||||
this.stateService.networkChanged$.subscribe((network) => {
|
||||
this.apiBasePath = network ? '/' + network : '';
|
||||
this.apiBasePath = network && network !== this.stateService.env.ROOT_NETWORK ? '/' + network : '';
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ export class ElectrsApiService {
|
||||
}
|
||||
this.apiBasePath = ''; // assume mainnet by default
|
||||
this.stateService.networkChanged$.subscribe((network) => {
|
||||
this.apiBasePath = network ? '/' + network : '';
|
||||
this.apiBasePath = network && network !== this.stateService.env.ROOT_NETWORK ? '/' + network : '';
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,32 +1,31 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Router, ActivatedRoute, NavigationEnd, ActivatedRouteSnapshot } from '@angular/router';
|
||||
import { Router, NavigationEnd, ActivatedRouteSnapshot } from '@angular/router';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { StateService } from './state.service';
|
||||
|
||||
const networkModules = {
|
||||
bitcoin: {
|
||||
subnets: [
|
||||
{ name: 'mainnet', path: '' },
|
||||
{ name: 'testnet', path: '/testnet' },
|
||||
{ name: 'testnet4', path: '/testnet4' },
|
||||
{ name: 'signet', path: '/signet' },
|
||||
],
|
||||
},
|
||||
liquid: {
|
||||
subnets: [
|
||||
{ name: 'liquid', path: '' },
|
||||
{ name: 'liquidtestnet', path: '/testnet' },
|
||||
],
|
||||
}
|
||||
};
|
||||
const networks = Object.keys(networkModules);
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class NavigationService {
|
||||
subnetPaths = new BehaviorSubject<Record<string,string>>({});
|
||||
networkModules = {
|
||||
bitcoin: {
|
||||
subnets: [
|
||||
{ name: 'mainnet', path: '' },
|
||||
{ name: 'testnet', path: this.stateService.env.ROOT_NETWORK === 'testnet' ? '/' : '/testnet' },
|
||||
{ name: 'testnet4', path: this.stateService.env.ROOT_NETWORK === 'testnet4' ? '/' : '/testnet4' },
|
||||
{ name: 'signet', path: this.stateService.env.ROOT_NETWORK === 'signet' ? '/' : '/signet' },
|
||||
],
|
||||
},
|
||||
liquid: {
|
||||
subnets: [
|
||||
{ name: 'liquid', path: '' },
|
||||
{ name: 'liquidtestnet', path: '/testnet' },
|
||||
],
|
||||
}
|
||||
};
|
||||
networks = Object.keys(this.networkModules);
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
@ -46,11 +45,11 @@ export class NavigationService {
|
||||
const networkPaths = {};
|
||||
let route = root;
|
||||
// traverse the router state tree until all network paths are set, or we reach the end of the tree
|
||||
while (!networks.reduce((acc, network) => acc && !!networkPaths[network], true) && route) {
|
||||
while (!this.networks.reduce((acc, network) => acc && !!networkPaths[network], true) && route) {
|
||||
// 'networkSpecific' paths may correspond to valid routes on other networks, but aren't directly compatible
|
||||
// (e.g. we shouldn't link a mainnet transaction page to the same txid on testnet or liquid)
|
||||
if (route.data?.networkSpecific) {
|
||||
networks.forEach(network => {
|
||||
this.networks.forEach(network => {
|
||||
if (networkPaths[network] == null) {
|
||||
networkPaths[network] = path;
|
||||
}
|
||||
@ -59,7 +58,7 @@ export class NavigationService {
|
||||
// null or empty networks list is shorthand for "compatible with every network"
|
||||
if (route.data?.networks?.length) {
|
||||
// if the list is non-empty, only those networks are compatible
|
||||
networks.forEach(network => {
|
||||
this.networks.forEach(network => {
|
||||
if (!route.data.networks.includes(network)) {
|
||||
if (networkPaths[network] == null) {
|
||||
networkPaths[network] = path;
|
||||
@ -76,7 +75,7 @@ export class NavigationService {
|
||||
}
|
||||
|
||||
const subnetPaths = {};
|
||||
Object.entries(networkModules).forEach(([key, network]) => {
|
||||
Object.entries(this.networkModules).forEach(([key, network]) => {
|
||||
network.subnets.forEach(subnet => {
|
||||
subnetPaths[subnet.name] = subnet.path + (networkPaths[key] != null ? networkPaths[key] : path);
|
||||
});
|
||||
|
@ -43,6 +43,7 @@ export interface Customization {
|
||||
}
|
||||
|
||||
export interface Env {
|
||||
MAINNET_ENABLED: boolean;
|
||||
TESTNET_ENABLED: boolean;
|
||||
TESTNET4_ENABLED: boolean;
|
||||
SIGNET_ENABLED: boolean;
|
||||
@ -52,6 +53,7 @@ export interface Env {
|
||||
KEEP_BLOCKS_AMOUNT: number;
|
||||
OFFICIAL_MEMPOOL_SPACE: boolean;
|
||||
BASE_MODULE: string;
|
||||
ROOT_NETWORK: string;
|
||||
NGINX_PROTOCOL?: string;
|
||||
NGINX_HOSTNAME?: string;
|
||||
NGINX_PORT?: string;
|
||||
@ -77,12 +79,14 @@ export interface Env {
|
||||
}
|
||||
|
||||
const defaultEnv: Env = {
|
||||
'MAINNET_ENABLED': true,
|
||||
'TESTNET_ENABLED': false,
|
||||
'TESTNET4_ENABLED': false,
|
||||
'SIGNET_ENABLED': false,
|
||||
'LIQUID_ENABLED': false,
|
||||
'LIQUID_TESTNET_ENABLED': false,
|
||||
'BASE_MODULE': 'mempool',
|
||||
'ROOT_NETWORK': '',
|
||||
'ITEMS_PER_PAGE': 10,
|
||||
'KEEP_BLOCKS_AMOUNT': 8,
|
||||
'OFFICIAL_MEMPOOL_SPACE': false,
|
||||
@ -325,7 +329,12 @@ export class StateService {
|
||||
// (?:preview\/)? optional "preview" prefix (non-capturing)
|
||||
// (testnet|signet)/ network string (captured as networkMatches[1])
|
||||
// ($|\/) network string must end or end with a slash
|
||||
const networkMatches = url.match(/^\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?(?:preview\/)?(testnet4?|signet)($|\/)/);
|
||||
let networkMatches: object = url.match(/^\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?(?:preview\/)?(testnet4?|signet)($|\/)/);
|
||||
|
||||
if (!networkMatches && this.env.ROOT_NETWORK) {
|
||||
networkMatches = { 1: this.env.ROOT_NETWORK };
|
||||
}
|
||||
|
||||
switch (networkMatches && networkMatches[1]) {
|
||||
case 'signet':
|
||||
if (this.network !== 'signet') {
|
||||
|
@ -55,7 +55,7 @@ export class WebsocketService {
|
||||
.pipe(take(1))
|
||||
.subscribe((response) => this.handleResponse(response));
|
||||
} else {
|
||||
this.network = this.stateService.network;
|
||||
this.network = this.stateService.network === this.stateService.env.ROOT_NETWORK ? '' : this.stateService.network;
|
||||
this.websocketSubject = webSocket<WebsocketResponse>(this.webSocketUrl.replace('{network}', this.network ? '/' + this.network : ''));
|
||||
|
||||
const { response: theInitData } = this.transferState.get<any>(initData, null) || {};
|
||||
@ -75,7 +75,7 @@ export class WebsocketService {
|
||||
if (network === this.network) {
|
||||
return;
|
||||
}
|
||||
this.network = network;
|
||||
this.network = network === this.stateService.env.ROOT_NETWORK ? '' : network;
|
||||
clearTimeout(this.onlineCheckTimeout);
|
||||
clearTimeout(this.onlineCheckTimeoutTwo);
|
||||
|
||||
|
193
frontend/src/app/shared/address-utils.ts
Normal file
193
frontend/src/app/shared/address-utils.ts
Normal file
@ -0,0 +1,193 @@
|
||||
import '@angular/localize/init';
|
||||
import { ScriptInfo } from './script.utils';
|
||||
import { Vin } from '../interfaces/electrs.interface';
|
||||
import { BECH32_CHARS_LW, BASE58_CHARS, HEX_CHARS } from './regex.utils';
|
||||
|
||||
export type AddressType = 'fee'
|
||||
| 'empty'
|
||||
| 'provably_unspendable'
|
||||
| 'op_return'
|
||||
| 'multisig'
|
||||
| 'p2pk'
|
||||
| 'p2pkh'
|
||||
| 'p2sh'
|
||||
| 'p2sh-p2wpkh'
|
||||
| 'p2sh-p2wsh'
|
||||
| 'v0_p2wpkh'
|
||||
| 'v0_p2wsh'
|
||||
| 'v1_p2tr'
|
||||
| 'confidential'
|
||||
| 'unknown'
|
||||
|
||||
const ADDRESS_PREFIXES = {
|
||||
mainnet: {
|
||||
base58: {
|
||||
pubkey: ['1'],
|
||||
script: ['3'],
|
||||
},
|
||||
bech32: 'bc1',
|
||||
},
|
||||
testnet: {
|
||||
base58: {
|
||||
pubkey: ['m', 'n'],
|
||||
script: '2',
|
||||
},
|
||||
bech32: 'tb1',
|
||||
},
|
||||
testnet4: {
|
||||
base58: {
|
||||
pubkey: ['m', 'n'],
|
||||
script: '2',
|
||||
},
|
||||
bech32: 'tb1',
|
||||
},
|
||||
signet: {
|
||||
base58: {
|
||||
pubkey: ['m', 'n'],
|
||||
script: '2',
|
||||
},
|
||||
bech32: 'tb1',
|
||||
},
|
||||
liquid: {
|
||||
base58: {
|
||||
pubkey: ['P','Q'],
|
||||
script: ['G','H'],
|
||||
confidential: ['V'],
|
||||
},
|
||||
bech32: 'ex1',
|
||||
confidential: 'lq1',
|
||||
},
|
||||
liquidtestnet: {
|
||||
base58: {
|
||||
pubkey: ['F'],
|
||||
script: ['8','9'],
|
||||
confidential: ['V'], // TODO: check if this is actually correct
|
||||
},
|
||||
bech32: 'tex1',
|
||||
confidential: 'tlq1',
|
||||
},
|
||||
};
|
||||
|
||||
// precompiled regexes for common address types (excluding prefixes)
|
||||
const base58Regex = RegExp('^' + BASE58_CHARS + '{26,34}$');
|
||||
const confidentialb58Regex = RegExp('^[TJ]' + BASE58_CHARS + '{78}$');
|
||||
const p2wpkhRegex = RegExp('^q' + BECH32_CHARS_LW + '{38}$');
|
||||
const p2wshRegex = RegExp('^q' + BECH32_CHARS_LW + '{58}$');
|
||||
const p2trRegex = RegExp('^p' + BECH32_CHARS_LW + '{58}$');
|
||||
const pubkeyRegex = RegExp('^' + `(04${HEX_CHARS}{128})|(0[23]${HEX_CHARS}{64})$`);
|
||||
|
||||
export function detectAddressType(address: string, network: string): AddressType {
|
||||
// normal address types
|
||||
const firstChar = address.substring(0, 1);
|
||||
if (ADDRESS_PREFIXES[network].base58.pubkey.includes(firstChar) && base58Regex.test(address.slice(1))) {
|
||||
return 'p2pkh';
|
||||
} else if (ADDRESS_PREFIXES[network].base58.script.includes(firstChar) && base58Regex.test(address.slice(1))) {
|
||||
return 'p2sh';
|
||||
} else if (address.startsWith(ADDRESS_PREFIXES[network].bech32)) {
|
||||
const suffix = address.slice(ADDRESS_PREFIXES[network].bech32.length);
|
||||
if (p2wpkhRegex.test(suffix)) {
|
||||
return 'v0_p2wpkh';
|
||||
} else if (p2wshRegex.test(suffix)) {
|
||||
return 'v0_p2wsh';
|
||||
} else if (p2trRegex.test(suffix)) {
|
||||
return 'v1_p2tr';
|
||||
}
|
||||
}
|
||||
|
||||
// p2pk
|
||||
if (pubkeyRegex.test(address)) {
|
||||
return 'p2pk';
|
||||
}
|
||||
|
||||
// liquid-specific types
|
||||
if (network.startsWith('liquid')) {
|
||||
if (ADDRESS_PREFIXES[network].base58.confidential.includes(firstChar) && confidentialb58Regex.test(address.slice(1))) {
|
||||
return 'confidential';
|
||||
} else if (address.startsWith(ADDRESS_PREFIXES[network].confidential)) {
|
||||
return 'confidential';
|
||||
}
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses & classifies address types + properties from address strings
|
||||
*
|
||||
* can optionally augment this data with examples of spends from the address,
|
||||
* e.g. to classify revealed scripts for scripthash-type addresses.
|
||||
*/
|
||||
export class AddressTypeInfo {
|
||||
network: string;
|
||||
address: string;
|
||||
type: AddressType;
|
||||
// script data
|
||||
scripts: Map<string, ScriptInfo>; // raw script
|
||||
// flags
|
||||
isMultisig?: { m: number, n: number };
|
||||
tapscript?: boolean;
|
||||
|
||||
constructor (network: string, address: string, type?: AddressType, vin?: Vin[]) {
|
||||
this.network = network;
|
||||
this.address = address;
|
||||
this.scripts = new Map();
|
||||
if (type) {
|
||||
this.type = type;
|
||||
} else {
|
||||
this.type = detectAddressType(address, network);
|
||||
}
|
||||
this.processInputs(vin);
|
||||
}
|
||||
|
||||
public clone(): AddressTypeInfo {
|
||||
const cloned = new AddressTypeInfo(this.network, this.address, this.type);
|
||||
cloned.scripts = new Map(Array.from(this.scripts, ([key, value]) => [key, value?.clone()]));
|
||||
cloned.isMultisig = this.isMultisig;
|
||||
cloned.tapscript = this.tapscript;
|
||||
return cloned;
|
||||
}
|
||||
|
||||
public processInputs(vin: Vin[] = []): void {
|
||||
// taproot can have multiple script paths
|
||||
if (this.type === 'v1_p2tr') {
|
||||
for (const v of vin) {
|
||||
if (v.inner_witnessscript_asm) {
|
||||
this.tapscript = true;
|
||||
const controlBlock = v.witness[v.witness.length - 1].startsWith('50') ? v.witness[v.witness.length - 2] : v.witness[v.witness.length - 1];
|
||||
this.processScript(new ScriptInfo('inner_witnessscript', undefined, v.inner_witnessscript_asm, v.witness, controlBlock));
|
||||
}
|
||||
}
|
||||
// for single-script types, if we've seen one input we've seen them all
|
||||
} else if (['p2sh', 'v0_p2wsh'].includes(this.type)) {
|
||||
if (!this.scripts.size && vin.length) {
|
||||
const v = vin[0];
|
||||
// wrapped segwit
|
||||
if (this.type === 'p2sh' && v.witness?.length) {
|
||||
if (v.scriptsig.startsWith('160014')) {
|
||||
this.type = 'p2sh-p2wpkh';
|
||||
} else if (v.scriptsig.startsWith('220020')) {
|
||||
this.type = 'p2sh-p2wsh';
|
||||
}
|
||||
}
|
||||
// real script
|
||||
if (this.type !== 'p2sh-p2wpkh') {
|
||||
if (v.inner_witnessscript_asm) {
|
||||
this.processScript(new ScriptInfo('inner_witnessscript', undefined, v.inner_witnessscript_asm, v.witness));
|
||||
} else if (v.inner_redeemscript_asm) {
|
||||
this.processScript(new ScriptInfo('inner_redeemscript', undefined, v.inner_redeemscript_asm, v.witness));
|
||||
} else if (v.scriptsig || v.scriptsig_asm) {
|
||||
this.processScript(new ScriptInfo('scriptsig', v.scriptsig, v.scriptsig_asm, v.witness));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// and there's nothing more to learn from processing inputs for non-scripthash types
|
||||
}
|
||||
|
||||
private processScript(script: ScriptInfo): void {
|
||||
this.scripts.set(script.key, script);
|
||||
if (script.template?.type === 'multisig') {
|
||||
this.isMultisig = { m: script.template['m'], n: script.template['n'] };
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
@switch (address.type || null) {
|
||||
@case ('fee') {
|
||||
<span i18n="address.fee">fee</span>
|
||||
}
|
||||
@case ('empty') {
|
||||
<span i18n="address.empty">empty</span>
|
||||
}
|
||||
@case ('v0_p2wpkh') {
|
||||
<span>P2WPKH</span>
|
||||
}
|
||||
@case ('v0_p2wsh') {
|
||||
<span>P2WSH</span>
|
||||
}
|
||||
@case ('v1_p2tr') {
|
||||
<span>P2TR</span>
|
||||
}
|
||||
@case ('provably_unspendable') {
|
||||
<span i18n="address.provably-unspendable">provably unspendable</span>
|
||||
}
|
||||
@case ('multisig') {
|
||||
<span i18n="address.bare-multisig">bare multisig</span>
|
||||
}
|
||||
@case (null) {
|
||||
<span>unknown</span>
|
||||
}
|
||||
@default {
|
||||
<span>{{ address.type.toUpperCase() }}</span>
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { AddressTypeInfo } from '../../address-utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-address-type',
|
||||
templateUrl: './address-type.component.html',
|
||||
styleUrls: []
|
||||
})
|
||||
export class AddressTypeComponent {
|
||||
@Input() address: AddressTypeInfo;
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
text-overflow: unset;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
align-items: start;
|
||||
position: relative;
|
||||
|
||||
.truncate-link {
|
||||
|
@ -12,7 +12,9 @@ export class RelativeUrlPipe implements PipeTransform {
|
||||
|
||||
transform(value: string, swapNetwork?: string): string {
|
||||
let network = swapNetwork || this.stateService.network;
|
||||
if (network === 'mainnet') network = '';
|
||||
if (network === 'mainnet' || network === this.stateService.env.ROOT_NETWORK) {
|
||||
network = '';
|
||||
}
|
||||
if (this.stateService.env.BASE_MODULE === 'liquid' && network === 'liquidtestnet') {
|
||||
network = 'testnet';
|
||||
} else if (this.stateService.env.BASE_MODULE !== 'mempool') {
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { Env } from '../services/state.service';
|
||||
|
||||
// all base58 characters
|
||||
const BASE58_CHARS = `[a-km-zA-HJ-NP-Z1-9]`;
|
||||
export const BASE58_CHARS = `[a-km-zA-HJ-NP-Z1-9]`;
|
||||
|
||||
// all bech32 characters (after the separator)
|
||||
const BECH32_CHARS_LW = `[ac-hj-np-z02-9]`;
|
||||
export const BECH32_CHARS_LW = `[ac-hj-np-z02-9]`;
|
||||
const BECH32_CHARS_UP = `[AC-HJ-NP-Z02-9]`;
|
||||
|
||||
// Hex characters
|
||||
const HEX_CHARS = `[a-fA-F0-9]`;
|
||||
export const HEX_CHARS = `[a-fA-F0-9]`;
|
||||
|
||||
// A regex to say "A single 0 OR any number with no leading zeroes"
|
||||
// Capped at 9 digits so as to not be confused with lightning channel IDs (which are around 17 digits)
|
||||
|
@ -145,8 +145,116 @@ for (let i = 187; i <= 255; i++) {
|
||||
|
||||
export { opcodes };
|
||||
|
||||
export type ScriptType = 'scriptpubkey'
|
||||
| 'scriptsig'
|
||||
| 'inner_witnessscript'
|
||||
| 'inner_redeemscript'
|
||||
|
||||
export interface ScriptTemplate {
|
||||
type: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const ScriptTemplates: { [type: string]: (...args: any) => ScriptTemplate } = {
|
||||
liquid_peg_out: () => ({ type: 'liquid_peg_out', label: 'Liquid Peg Out' }),
|
||||
liquid_peg_out_emergency: () => ({ type: 'liquid_peg_out_emergency', label: 'Emergency Liquid Peg Out' }),
|
||||
ln_force_close: () => ({ type: 'ln_force_close', label: 'Lightning Force Close' }),
|
||||
ln_force_close_revoked: () => ({ type: 'ln_force_close_revoked', label: 'Revoked Lightning Force Close' }),
|
||||
ln_htlc: () => ({ type: 'ln_htlc', label: 'Lightning HTLC' }),
|
||||
ln_htlc_revoked: () => ({ type: 'ln_htlc_revoked', label: 'Revoked Lightning HTLC' }),
|
||||
ln_htlc_expired: () => ({ type: 'ln_htlc_expired', label: 'Expired Lightning HTLC' }),
|
||||
ln_anchor: () => ({ type: 'ln_anchor', label: 'Lightning Anchor' }),
|
||||
ln_anchor_swept: () => ({ type: 'ln_anchor_swept', label: 'Swept Lightning Anchor' }),
|
||||
multisig: (m: number, n: number) => ({ type: 'multisig', m, n, label: $localize`:@@address-label.multisig:Multisig ${m}:multisigM: of ${n}:multisigN:` }),
|
||||
};
|
||||
|
||||
export class ScriptInfo {
|
||||
type: ScriptType;
|
||||
scriptPath?: string;
|
||||
hex?: string;
|
||||
asm?: string;
|
||||
template: ScriptTemplate;
|
||||
|
||||
constructor(type: ScriptType, hex?: string, asm?: string, witness?: string[], scriptPath?: string) {
|
||||
this.type = type;
|
||||
this.hex = hex;
|
||||
this.asm = asm;
|
||||
if (scriptPath) {
|
||||
this.scriptPath = scriptPath;
|
||||
}
|
||||
if (this.asm) {
|
||||
this.template = detectScriptTemplate(this.type, this.asm, witness);
|
||||
}
|
||||
}
|
||||
|
||||
public clone(): ScriptInfo {
|
||||
return { ...this };
|
||||
}
|
||||
|
||||
get key(): string {
|
||||
return this.type + (this.scriptPath || '');
|
||||
}
|
||||
}
|
||||
|
||||
/** parses an inner_witnessscript + witness stack, and detects named script types */
|
||||
export function detectScriptTemplate(type: ScriptType, script_asm: string, witness?: string[]): ScriptTemplate | undefined {
|
||||
if (type === 'inner_witnessscript' && witness?.length) {
|
||||
if (script_asm.indexOf('OP_DEPTH OP_PUSHNUM_12 OP_EQUAL OP_IF OP_PUSHNUM_11') === 0 || script_asm.indexOf('OP_PUSHNUM_15 OP_CHECKMULTISIG OP_IFDUP OP_NOTIF OP_PUSHBYTES_2') === 1259) {
|
||||
if (witness.length > 11) {
|
||||
return ScriptTemplates.liquid_peg_out();
|
||||
} else {
|
||||
return ScriptTemplates.liquid_peg_out_emergency();
|
||||
}
|
||||
}
|
||||
|
||||
const topElement = witness[witness.length - 2];
|
||||
if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSH(NUM_\d+|BYTES_(1 \w{2}|2 \w{4})) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(script_asm)) {
|
||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs
|
||||
if (topElement === '01') {
|
||||
// top element is '01' to get in the revocation path
|
||||
return ScriptTemplates.ln_force_close_revoked();
|
||||
} else {
|
||||
// top element is '', this is a delayed to_local output
|
||||
return ScriptTemplates.ln_force_close();
|
||||
}
|
||||
} else if (
|
||||
/^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_NOTIF OP_DROP OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(script_asm) ||
|
||||
/^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_IF OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_DROP OP_PUSHBYTES_3 \w{6} OP_CLTV OP_DROP OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(script_asm)
|
||||
) {
|
||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs
|
||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#received-htlc-outputs
|
||||
if (topElement.length === 66) {
|
||||
// top element is a public key
|
||||
return ScriptTemplates.ln_htlc_revoked();
|
||||
} else if (topElement) {
|
||||
// top element is a preimage
|
||||
return ScriptTemplates.ln_htlc();
|
||||
} else {
|
||||
// top element is '' to get in the expiry of the script
|
||||
return ScriptTemplates.ln_htlc_expired();
|
||||
}
|
||||
} else if (/^OP_PUSHBYTES_33 \w{66} OP_CHECKSIG OP_IFDUP OP_NOTIF OP_PUSHNUM_16 OP_CSV OP_ENDIF$/.test(script_asm)) {
|
||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#to_local_anchor-and-to_remote_anchor-output-option_anchors
|
||||
if (topElement) {
|
||||
// top element is a signature
|
||||
return ScriptTemplates.ln_anchor();
|
||||
} else {
|
||||
// top element is '', it has been swept after 16 blocks
|
||||
return ScriptTemplates.ln_anchor_swept();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const multisig = parseMultisigScript(script_asm);
|
||||
if (multisig) {
|
||||
return ScriptTemplates.multisig(multisig.m, multisig.n);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/** extracts m and n from a multisig script (asm), returns nothing if it is not a multisig script */
|
||||
export function parseMultisigScript(script: string): void | { m: number, n: number } {
|
||||
export function parseMultisigScript(script: string): undefined | { m: number, n: number } {
|
||||
if (!script) {
|
||||
return;
|
||||
}
|
||||
|
@ -87,6 +87,7 @@ import { ChangeComponent } from '../components/change/change.component';
|
||||
import { SatsComponent } from './components/sats/sats.component';
|
||||
import { BtcComponent } from './components/btc/btc.component';
|
||||
import { FeeRateComponent } from './components/fee-rate/fee-rate.component';
|
||||
import { AddressTypeComponent } from './components/address-type/address-type.component';
|
||||
import { TruncateComponent } from './components/truncate/truncate.component';
|
||||
import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component';
|
||||
import { TimestampComponent } from './components/timestamp/timestamp.component';
|
||||
@ -202,6 +203,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
||||
SatsComponent,
|
||||
BtcComponent,
|
||||
FeeRateComponent,
|
||||
AddressTypeComponent,
|
||||
TruncateComponent,
|
||||
SearchResultsComponent,
|
||||
TimestampComponent,
|
||||
@ -343,6 +345,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
||||
SatsComponent,
|
||||
BtcComponent,
|
||||
FeeRateComponent,
|
||||
AddressTypeComponent,
|
||||
TruncateComponent,
|
||||
SearchResultsComponent,
|
||||
TimestampComponent,
|
||||
|
@ -147,9 +147,15 @@ export function isNonStandard(tx: Transaction): boolean {
|
||||
let opreturnCount = 0;
|
||||
for (const vout of tx.vout) {
|
||||
// scriptpubkey
|
||||
if (['unknown', 'provably_unspendable', 'empty'].includes(vout.scriptpubkey_type)) {
|
||||
if (['nonstandard', 'provably_unspendable', 'empty'].includes(vout.scriptpubkey_type)) {
|
||||
// (non-standard output type)
|
||||
return true;
|
||||
} else if (vout.scriptpubkey_type === 'unknown') {
|
||||
// undefined segwit version/length combinations are actually standard in outputs
|
||||
// https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/interpreter.cpp#L1950-L1951
|
||||
if (vout.scriptpubkey.startsWith('00') || !isWitnessProgram(vout.scriptpubkey)) {
|
||||
return true;
|
||||
}
|
||||
} else if (vout.scriptpubkey_type === 'multisig') {
|
||||
if (!DEFAULT_PERMIT_BAREMULTISIG) {
|
||||
// bare-multisig
|
||||
@ -197,6 +203,27 @@ export function isNonStandard(tx: Transaction): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// A witness program is any valid scriptpubkey that consists of a 1-byte push opcode
|
||||
// followed by a data push between 2 and 40 bytes.
|
||||
// https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/script.cpp#L224-L240
|
||||
function isWitnessProgram(scriptpubkey: string): false | { version: number, program: string } {
|
||||
if (scriptpubkey.length < 8 || scriptpubkey.length > 84) {
|
||||
return false;
|
||||
}
|
||||
const version = parseInt(scriptpubkey.slice(0,2), 16);
|
||||
if (version !== 0 && version < 0x51 || version > 0x60) {
|
||||
return false;
|
||||
}
|
||||
const push = parseInt(scriptpubkey.slice(2,4), 16);
|
||||
if (push + 2 === (scriptpubkey.length / 2)) {
|
||||
return {
|
||||
version: version ? version - 0x50 : 0,
|
||||
program: scriptpubkey.slice(4),
|
||||
};
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getNonWitnessSize(tx: Transaction): number {
|
||||
let weight = tx.weight;
|
||||
let hasWitness = false;
|
||||
|
22
frontend/src/resources/profile/grumpy.svg
Normal file
22
frontend/src/resources/profile/grumpy.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 323 KiB |
@ -8,11 +8,9 @@ use crate::{
|
||||
GbtResult, ThreadTransactionsMap, thread_acceleration::ThreadAcceleration,
|
||||
};
|
||||
|
||||
const MAX_BLOCK_WEIGHT_UNITS: u32 = 4_000_000 - 4_000;
|
||||
const BLOCK_SIGOPS: u32 = 80_000;
|
||||
const BLOCK_RESERVED_WEIGHT: u32 = 4_000;
|
||||
const BLOCK_RESERVED_SIGOPS: u32 = 400;
|
||||
const MAX_BLOCKS: usize = 8;
|
||||
|
||||
type AuditPool = Vec<Option<ManuallyDrop<AuditTransaction>>>;
|
||||
type ModifiedQueue = PriorityQueue<u32, TxPriority, U32HasherState>;
|
||||
@ -53,7 +51,13 @@ impl Ord for TxPriority {
|
||||
// TODO: Make gbt smaller to fix these lints.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
pub fn gbt(mempool: &mut ThreadTransactionsMap, accelerations: &[ThreadAcceleration], max_uid: usize) -> GbtResult {
|
||||
pub fn gbt(
|
||||
mempool: &mut ThreadTransactionsMap,
|
||||
accelerations: &[ThreadAcceleration],
|
||||
max_uid: usize,
|
||||
max_block_weight: u32,
|
||||
max_blocks: usize,
|
||||
) -> GbtResult {
|
||||
let mut indexed_accelerations = Vec::with_capacity(max_uid + 1);
|
||||
indexed_accelerations.resize(max_uid + 1, None);
|
||||
for acceleration in accelerations {
|
||||
@ -146,9 +150,9 @@ pub fn gbt(mempool: &mut ThreadTransactionsMap, accelerations: &[ThreadAccelerat
|
||||
modified.pop();
|
||||
}
|
||||
|
||||
if blocks.len() < (MAX_BLOCKS - 1)
|
||||
if blocks.len() < (max_blocks - 1)
|
||||
&& ((block_weight + (4 * next_tx.ancestor_sigop_adjusted_vsize())
|
||||
>= MAX_BLOCK_WEIGHT_UNITS)
|
||||
>= max_block_weight - 4_000)
|
||||
|| (block_sigops + next_tx.ancestor_sigops() > BLOCK_SIGOPS))
|
||||
{
|
||||
// hold this package in an overflow list while we check for smaller options
|
||||
@ -201,9 +205,9 @@ pub fn gbt(mempool: &mut ThreadTransactionsMap, accelerations: &[ThreadAccelerat
|
||||
|
||||
// this block is full
|
||||
let exceeded_package_tries =
|
||||
failures > 1000 && block_weight > (MAX_BLOCK_WEIGHT_UNITS - BLOCK_RESERVED_WEIGHT);
|
||||
failures > 1000 && block_weight > (max_block_weight - 4_000 - BLOCK_RESERVED_WEIGHT);
|
||||
let queue_is_empty = mempool_stack.is_empty() && modified.is_empty();
|
||||
if (exceeded_package_tries || queue_is_empty) && blocks.len() < (MAX_BLOCKS - 1) {
|
||||
if (exceeded_package_tries || queue_is_empty) && blocks.len() < (max_blocks - 1) {
|
||||
// finalize this block
|
||||
if transactions.is_empty() {
|
||||
info!("trying to push an empty block! breaking loop! mempool {:#?} | modified {:#?} | overflow {:#?}", mempool_stack.len(), modified.len(), overflow.len());
|
||||
|
@ -35,6 +35,8 @@ type ThreadTransactionsMap = HashMap<u32, ThreadTransaction, U32HasherState>;
|
||||
#[napi]
|
||||
pub struct GbtGenerator {
|
||||
thread_transactions: Arc<Mutex<ThreadTransactionsMap>>,
|
||||
max_block_weight: u32,
|
||||
max_blocks: usize,
|
||||
}
|
||||
|
||||
#[napi::module_init]
|
||||
@ -65,10 +67,12 @@ impl GbtGenerator {
|
||||
#[napi(constructor)]
|
||||
#[allow(clippy::new_without_default)]
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
pub fn new(max_block_weight: u32, max_blocks: u32) -> Self {
|
||||
debug!("Created new GbtGenerator");
|
||||
Self {
|
||||
thread_transactions: Arc::new(Mutex::new(u32hashmap_with_capacity(STARTING_CAPACITY))),
|
||||
max_block_weight,
|
||||
max_blocks: max_blocks as usize,
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,12 +80,19 @@ impl GbtGenerator {
|
||||
///
|
||||
/// Rejects if the thread panics or if the Mutex is poisoned.
|
||||
#[napi]
|
||||
pub async fn make(&self, mempool: Vec<ThreadTransaction>, accelerations: Vec<ThreadAcceleration>, max_uid: u32) -> Result<GbtResult> {
|
||||
pub async fn make(
|
||||
&self,
|
||||
mempool: Vec<ThreadTransaction>,
|
||||
accelerations: Vec<ThreadAcceleration>,
|
||||
max_uid: u32,
|
||||
) -> Result<GbtResult> {
|
||||
trace!("make: Current State {:#?}", self.thread_transactions);
|
||||
run_task(
|
||||
Arc::clone(&self.thread_transactions),
|
||||
accelerations,
|
||||
max_uid as usize,
|
||||
self.max_block_weight,
|
||||
self.max_blocks,
|
||||
move |map| {
|
||||
for tx in mempool {
|
||||
map.insert(tx.uid, tx);
|
||||
@ -107,6 +118,8 @@ impl GbtGenerator {
|
||||
Arc::clone(&self.thread_transactions),
|
||||
accelerations,
|
||||
max_uid as usize,
|
||||
self.max_block_weight,
|
||||
self.max_blocks,
|
||||
move |map| {
|
||||
for tx in new_txs {
|
||||
map.insert(tx.uid, tx);
|
||||
@ -149,6 +162,8 @@ async fn run_task<F>(
|
||||
thread_transactions: Arc<Mutex<ThreadTransactionsMap>>,
|
||||
accelerations: Vec<ThreadAcceleration>,
|
||||
max_uid: usize,
|
||||
max_block_weight: u32,
|
||||
max_blocks: usize,
|
||||
callback: F,
|
||||
) -> Result<GbtResult>
|
||||
where
|
||||
@ -166,7 +181,13 @@ where
|
||||
callback(&mut map);
|
||||
|
||||
info!("Starting gbt algorithm for {} elements...", map.len());
|
||||
let result = gbt::gbt(&mut map, &accelerations, max_uid);
|
||||
let result = gbt::gbt(
|
||||
&mut map,
|
||||
&accelerations,
|
||||
max_uid,
|
||||
max_block_weight,
|
||||
max_blocks as usize,
|
||||
);
|
||||
info!("Finished gbt algorithm for {} elements...", map.len());
|
||||
|
||||
debug!(
|
||||
|
Loading…
Reference in New Issue
Block a user