Compare commits

..

49 Commits

Author SHA1 Message Date
Felipe Knorr Kuhn
ad6b146aa0 Add example nested network config 2024-06-28 00:48:57 -07:00
wiz
98e9d1a6c3 Merge pull request #5227 from mempool/hunicus/about-juggling
Juggle community integration listings
2024-06-28 15:45:39 +09:00
hunicus
c4577b8c09 Merge branch 'master' into hunicus/about-juggling 2024-06-28 15:36:00 +09:00
hunicus
95c4da51ed Juggle community integration listings
Also add back bitcoin-s and remove mercury.

Signed-off-by: hunicus <93150691+hunicus@users.noreply.github.com>
2024-06-28 15:30:43 +09:00
softsimon
2e336d7ad1 Merge pull request #5231 from mempool/hunicus/about-foundry-logo
Update foundry logo on about page
2024-06-28 14:07:48 +09:00
hunicus
903ff1ea66 Update foundry logo on about page 2024-06-28 13:49:13 +09:00
softsimon
f02d8e0626 Merge pull request #5230 from mempool/simon/docs-root-network-support
Docs root network support
2024-06-28 11:43:05 +09:00
softsimon
ea04ea0048 Docs root network support 2024-06-28 11:28:41 +09:00
softsimon
d91c6bceed Merge pull request #5226 from mempool/natsoni/fix-accel-pie-chart
Fix logic for pool pie chart position
2024-06-27 21:48:04 +09:00
softsimon
aa5355e93d Merge pull request #5229 from rishkwal/rishkwal/fix-tx-base-route
Redirect user to `/` when user goes to `/tx` without any transaction `id`
2024-06-27 21:44:27 +09:00
softsimon
9672928da9 Adding missing TESTNET4_ENABLED to docker build 2024-06-27 19:19:28 +09:00
Rishabh
d189e70817 fix: redirect /tx/ routes to / 2024-06-27 15:11:00 +05:30
natsoni
bb91f9142e Fix accel pool pie chart placement 2024-06-27 17:50:45 +09:00
softsimon
66f90cb0bd Merge pull request #5225 from mempool/natsoni/fix-accel-preview-displaying
Fix acceleration preview showing with fragment on accel txs
2024-06-27 16:50:31 +09:00
softsimon
f1572f0038 Merge pull request #5222 from mempool/mononaut/partition-pool-pie
Show more detail in acceleration pools pie chart
2024-06-27 16:37:28 +09:00
natsoni
c3963d6a0d Fix acceleration preview showing with fragment on accel txs 2024-06-27 16:32:20 +09:00
softsimon
1dd86df3e0 Merge pull request #5224 from mempool/hunicus/about-coldcard
Replace bitcoin-s with coldcard on about page
2024-06-27 16:02:47 +09:00
softsimon
c8d443bea7 Merge pull request #5216 from mempool/natsoni/align-acceleration-pie-chart
Align "Accelerated to / by" fields on mobile
2024-06-27 15:44:03 +09:00
hunicus
575fc737ca Replace bitcoin-s with coldcard on about page 2024-06-27 15:34:53 +09:00
Mononaut
ebd4408b8d Adjust acceleration pool pie labels 2024-06-27 06:19:43 +00:00
Mononaut
d6d8c85419 Show more detail in acceleration pools pie chart 2024-06-27 03:40:03 +00:00
softsimon
fbb409e17b Merge pull request #5219 from mempool/simon/local-accelerator-estimates
Show accelerator estimates on local instances
2024-06-27 11:19:07 +09:00
softsimon
b6d03953b9 Show accelerator estimates on local instances 2024-06-26 21:42:30 +09:00
natsoni
d45104f7c9 Align acceleration pie chart 2024-06-26 18:03:14 +09:00
softsimon
d175c34e5b Merge pull request #5211 from mempool/simon/simpler-advanced-acceleration
Simplify advanced acceleration
2024-06-26 17:42:11 +09:00
softsimon
2bf2440e3a Merge pull request #5215 from mempool/mononaut/acceleration-preview-layout-tweaks
Minor style & layout tweaks for the acceleration preview
2024-06-26 17:41:33 +09:00
Mononaut
124c0acbe1 Minor layout tweaks for the acceleration preview 2024-06-26 08:18:39 +00:00
softsimon
69c5a2fb5a Merge pull request #5214 from mempool/natsoni/rbf-list-loading
Show loading indicator on toggle in RBF list
2024-06-26 17:10:14 +09:00
softsimon
c4f08e0d41 Merge pull request #5213 from mempool/natsoni/accelerations-table-fixes
Add page in URL to accelerations table
2024-06-26 17:08:49 +09:00
natsoni
87ee14f189 Show loading indicator on toggle in RBF list 2024-06-26 16:12:21 +09:00
natsoni
122b4b05c4 Add pagination in URL to accelerations table 2024-06-26 15:37:39 +09:00
natsoni
09f7dddf14 Use url parameter instead of query parameter 2024-06-26 15:25:03 +09:00
softsimon
f7ad45939c Hide surcharge row if zero 2024-06-26 15:08:00 +09:00
natsoni
7b6246a035 Fix loading state in blocks table issue 2024-06-26 14:20:49 +09:00
softsimon
a8d2138404 Simplify advanced acceleration 2024-06-26 12:30:34 +09:00
softsimon
0b608c96dd Merge pull request #5212 from mempool/simon/fix-eta-loading-error
Fix ETA loading error
2024-06-26 12:29:22 +09:00
softsimon
a0402b92f9 Fix ETA loading error 2024-06-26 12:12:49 +09:00
softsimon
14e05b43c7 Merge pull request #5210 from mempool/natsoni/btc-unit-on-pool-page
Force amounts to BTC unit in pool page
2024-06-26 11:46:32 +09:00
natsoni
fc8f8abc7e Add SEO title to Accelerations page 2024-06-26 11:42:22 +09:00
natsoni
a1f1b09c55 Fix loading indicator when changing page 2024-06-26 11:13:32 +09:00
natsoni
ad2d7af084 Force amounts to BTC unit in pool page 2024-06-26 10:48:17 +09:00
softsimon
9d3044efae Merge pull request #5199 from mempool/mononaut/tracker-acceleration-eta
Add projected acceleration ETA to tracker page
2024-06-25 17:15:41 +09:00
wiz
bac21afa54 Merge pull request #5203 from mempool/simon/address-page-romanz-support
Romanz support for address page
2024-06-25 16:54:37 +09:00
softsimon
f98bb675e7 Merge pull request #5209 from mempool/mononaut/fix-type-error
Fix coinbase address type error
2024-06-25 11:25:29 +09:00
Mononaut
3e057f2db1 Fix coinbase address type error 2024-06-25 02:20:44 +00:00
softsimon
4fbdf92f0c Merge pull request #5206 from mempool/simon/address-prefix-fixes
Fix address prefix for non esplora backend
2024-06-25 10:21:50 +09:00
softsimon
563def45d8 Fix address prefix for non esplora backend 2024-06-24 18:27:30 +09:00
softsimon
a9a1ff68ab Romanz support for address page 2024-06-24 16:25:29 +09:00
Mononaut
517e82ec8b Add projected acceleration ETA to tracker page 2024-06-24 02:06:22 +00:00
45 changed files with 981 additions and 1706 deletions

View File

@@ -1,4 +1,14 @@
{
"NETWORKS": {
"TESTNET": {
"ENABLED": true,
"WEIGHT": "__NETWORKS_TESTNET_WEIGHT__"
},
"MAINNET": {
"ENABLED": true,
"WEIGHT": "__NETWORKS_MAINNET_WEIGHT__"
}
},
"MEMPOOL": {
"ENABLED": true,
"OFFICIAL": false,

View File

@@ -157,6 +157,17 @@ describe('Mempool Backend Config', () => {
PAID: false,
API_KEY: '',
});
expect(config.NETWORKS).toStrictEqual({
MAINNET: {
ENABLED: true,
WEIGHT: 'foo'
},
TESTNET: {
ENABLED: false,
WEIGHT: 'bar'
}
})
});
});
@@ -190,6 +201,8 @@ describe('Mempool Backend Config', () => {
expect(config.MEMPOOL_SERVICES).toStrictEqual(fixture.MEMPOOL_SERVICES);
expect(config.REDIS).toStrictEqual(fixture.REDIS);
expect(config.NETWORKS).toStrictEqual(fixture.NETWORKS);
});
});
@@ -197,6 +210,22 @@ describe('Mempool Backend Config', () => {
jest.isolateModules(() => {
const startSh = fs.readFileSync(`${__dirname}/../../../docker/backend/start.sh`, 'utf-8');
const fixture = JSON.parse(fs.readFileSync(`${__dirname}/../__fixtures__/mempool-config.template.json`, 'utf8'));
function generateKeys(obj, prefix = '') {
let keys: string[] = [];
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const newPrefix = prefix ? `${prefix}_${key}` : key;
if (typeof obj[key] === 'object' && obj[key] !== null) {
keys = keys.concat(generateKeys(obj[key], newPrefix));
} else {
keys.push(`${newPrefix}`);
}
}
}
return keys;
}
function parseJson(jsonObj, root?) {
for (const [key, value] of Object.entries(jsonObj)) {
@@ -208,6 +237,31 @@ describe('Mempool Backend Config', () => {
continue;
}
if (root === 'NETWORKS') {
const keys = generateKeys(value);
keys.forEach(item => {
const replaceStr = `__${root}_${key}_${item}__`;
const envVarStr = `${root}_${key}_${item}`;
let defaultEntry = replaceStr + '=' + '\\${' + envVarStr + ':=(.*)' + '}';
if (process.env.CI) {
console.log(`looking for ${defaultEntry} in the start.sh script`);
}
const re = new RegExp(defaultEntry);
expect(startSh).toMatch(re);
const sedStr = 'sed -i "s!' + replaceStr + '!${' + replaceStr + '}!g" mempool-config.json';
if (process.env.CI) {
console.log(`looking for ${sedStr} in the start.sh script`);
}
expect(startSh).toContain(sedStr);
});
continue;
}
if (root) {
//The flattened string, i.e, __MEMPOOL_ENABLED__
const replaceStr = `${root ? '__' + root + '_' : '__'}${key}__`;
@@ -264,6 +318,8 @@ describe('Mempool Backend Config', () => {
const replaceStr = `${root ? '__' + root + '_' : '__'}${key}__`;
expect(dockerJson).toContain(`"${key}": ${replaceStr}`);
break;
} else if (typeof value === 'object') {
break;
} else {
//Check for top level config keys
expect(dockerJson).toContain(`"${key}"`);

View File

@@ -14,6 +14,7 @@ class AccelerationRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history', this.$getAcceleratorAccelerationsHistory.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history/aggregated', this.$getAcceleratorAccelerationsHistoryAggregated.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/stats', this.$getAcceleratorAccelerationsStats.bind(this))
.post(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/estimate', this.$getAcceleratorEstimate.bind(this))
;
}
@@ -64,6 +65,20 @@ class AccelerationRoutes {
res.status(500).end();
}
}
private async $getAcceleratorEstimate(req: Request, res: Response): Promise<void> {
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
try {
const response = await axios.post(url, req.body, { responseType: 'stream', timeout: 10000 });
for (const key in response.headers) {
res.setHeader(key, response.headers[key]);
}
response.data.pipe(res);
} catch (e) {
logger.err(`Unable to get acceleration estimate from ${url} in $getAcceleratorEstimate(), ${e}`, this.tag);
res.status(500).end();
}
}
}
export default new AccelerationRoutes();

View File

@@ -165,13 +165,21 @@ class BitcoinApi implements AbstractBitcoinApi {
const mp = mempool.getMempool();
for (const tx in mp) {
for (const vout of mp[tx].vout) {
if (vout.scriptpubkey_address.indexOf(prefix) === 0) {
if (vout.scriptpubkey_address?.indexOf(prefix) === 0) {
found[vout.scriptpubkey_address] = '';
if (Object.keys(found).length >= 10) {
return Object.keys(found);
}
}
}
for (const vin of mp[tx].vin) {
if (vin.prevout?.scriptpubkey_address?.indexOf(prefix) === 0) {
found[vin.prevout?.scriptpubkey_address] = '';
if (Object.keys(found).length >= 10) {
return Object.keys(found);
}
}
}
}
return Object.keys(found);
}

View File

@@ -54,7 +54,7 @@ export namespace IEsploraApi {
scriptpubkey: string;
scriptpubkey_asm: string;
scriptpubkey_type: string;
scriptpubkey_address: string;
scriptpubkey_address?: string;
value: number;
// Elements
valuecommitment?: number;

View File

@@ -724,7 +724,7 @@ class Blocks {
}
const coinbaseTx = await bitcoinApi.$getCoinbaseTx(hash);
const addresses = new Set<string>(coinbaseTx.vout.map(v => v.scriptpubkey_address).filter(a => a));
const addresses = new Set<string>(coinbaseTx.vout.map(v => v.scriptpubkey_address).filter(a => a) as string[]);
await blocksRepository.$saveCoinbaseAddresses(hash, [...addresses]);
// Logging

View File

@@ -160,6 +160,16 @@ interface IConfig {
PAID: boolean;
API_KEY: string;
},
NETWORKS: {
MAINNET: {
ENABLED: boolean;
WEIGHT: 'foo';
},
TESTNET: {
ENABLED: boolean;
WEIGHT: 'bar'
}
};
}
const defaults: IConfig = {
@@ -320,6 +330,16 @@ const defaults: IConfig = {
'PAID': false,
'API_KEY': '',
},
'NETWORKS': {
'MAINNET': {
'ENABLED': true,
'WEIGHT': 'foo',
},
'TESTNET': {
'ENABLED': false,
'WEIGHT': 'bar'
}
}
};
class Config implements IConfig {
@@ -341,6 +361,7 @@ class Config implements IConfig {
MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES'];
REDIS: IConfig['REDIS'];
FIAT_PRICE: IConfig['FIAT_PRICE'];
NETWORKS: IConfig['NETWORKS'];
constructor() {
const configs = this.merge(configFromFile, defaults);
@@ -362,6 +383,7 @@ class Config implements IConfig {
this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES;
this.REDIS = configs.REDIS;
this.FIAT_PRICE = configs.FIAT_PRICE;
this.NETWORKS = configs.NETWORKS;
}
merge = (...objects: object[]): IConfig => {

View File

@@ -155,5 +155,15 @@
"ENABLED": __FIAT_PRICE_ENABLED__,
"PAID": __FIAT_PRICE_PAID__,
"API_KEY": "__FIAT_PRICE_API_KEY__"
},
"NETWORKS": {
"MAINNET": {
"ENABLED": __NETWORKS_MAINNET_ENABLED__,
"WEIGHT": "__NETWORKS_MAINNET_WEIGHT__,"
},
"TESTNET": {
"ENABLED": __NETWORKS_TESTNET_ENABLED__,
"WEIGHT": "__NETWORKS_TESTNET_WEIGHT__"
}
}
}

View File

@@ -157,6 +157,12 @@ __FIAT_PRICE_ENABLED__=${FIAT_PRICE_ENABLED:=true}
__FIAT_PRICE_PAID__=${FIAT_PRICE_PAID:=false}
__FIAT_PRICE_API_KEY__=${FIAT_PRICE_API_KEY:=""}
# NETWORKS
__NETWORKS_TESTNET_ENABLED__=${NETWORKS_TESTNET_ENABLED:=true}
__NETWORKS_TESTNET_WEIGHT__=${NETWORKS_TESTNET_WEIGHT:="0.1"}
__NETWORKS_MAINNET_ENABLED__=${NETWORKS_MAINNET_ENABLED:=true}
__NETWORKS_MAINNET_WEIGHT__=${NETWORKS_MAINNET_WEIGHT:="0.2"}
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
sed -i "s!__MEMPOOL_NETWORK__!${__MEMPOOL_NETWORK__}!g" mempool-config.json
@@ -306,4 +312,10 @@ sed -i "s!__FIAT_PRICE_ENABLED__!${__FIAT_PRICE_ENABLED__}!g" mempool-config.jso
sed -i "s!__FIAT_PRICE_PAID__!${__FIAT_PRICE_PAID__}!g" mempool-config.json
sed -i "s!__FIAT_PRICE_API_KEY__!${__FIAT_PRICE_API_KEY__}!g" mempool-config.json
# NETWORKS
sed -i "s!__NETWORKS_TESTNET_ENABLED__!${__NETWORKS_TESTNET_ENABLED__}!g" mempool-config.json
sed -i "s!__NETWORKS_TESTNET_WEIGHT__!${__NETWORKS_TESTNET_WEIGHT__}!g" mempool-config.json
sed -i "s!__NETWORKS_MAINNET_ENABLED__!${__NETWORKS_MAINNET_ENABLED__}!g" mempool-config.json
sed -i "s!__NETWORKS_MAINNET_WEIGHT__!${__NETWORKS_MAINNET_WEIGHT__}!g" mempool-config.json
node /backend/package/index.js

View File

@@ -18,6 +18,7 @@ fi
__MAINNET_ENABLED__=${MAINNET_ENABLED:=true}
__TESTNET_ENABLED__=${TESTNET_ENABLED:=false}
__TESTNET4_ENABLED__=${TESTNET_ENABLED:=false}
__SIGNET_ENABLED__=${SIGNET_ENABLED:=false}
__LIQUID_ENABLED__=${LIQUID_ENABLED:=false}
__LIQUID_TESTNET_ENABLED__=${LIQUID_TESTNET_ENABLED:=false}
@@ -46,6 +47,7 @@ __ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
# Export as environment variables to be used by envsubst
export __MAINNET_ENABLED__
export __TESTNET_ENABLED__
export __TESTNET4_ENABLED__
export __SIGNET_ENABLED__
export __LIQUID_ENABLED__
export __LIQUID_TESTNET_ENABLED__

View File

@@ -53,13 +53,26 @@
<span>Spiral</span>
</a>
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="-10 -10 100 100" class="image">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(-186.000000, -2316.000000)">
<g transform="translate(186.000000, 2316.000000)">
<rect id="" fill="#023D32" x="-10" y="-10" width="100" height="100" rx="8"></rect>
<path d="M61.6666667,9.16666667 L61.6666667,17.0041667 L46.2625,17.0041667 C46.2625,17.0041667 44.1666667,16.6666667 44.1666667,18.3333333 L44.1666667,25.8025 L61.6666667,25.8025 L61.6666667,34.7391667 L44.1666667,34.7391667 L44.1666667,70.5575 L31.7825,70.5575 L31.7825,35 L19.1666667,35 L19.1666667,25.595 L31.6666667,25.595 L31.6666667,17.5 C31.6666667,17.5 32.5,9.16666667 40.4166667,9.16666667 L61.6666667,9.16666667 Z" id="Fill-1" fill="#86E2A0"></path>
</g>
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="76" viewBox="0 0 32 76">
<defs>
<style>
.d {
fill: #fff;
}
.e {
fill: #ff8200;
}
</style>
</defs>
<g id="c" data-name="b">
<circle class="e" cx="24" cy="32" r="8" />
<circle class="e" cx="24" cy="56" r="8" />
<circle class="e" cx="8" cy="68" r="8" />
<g>
<circle class="d" cx="24" cy="8" r="8" />
<circle class="d" cx="8" cy="20" r="8" />
<circle class="d" cx="8" cy="44" r="8" />
</g>
</g>
</svg>
@@ -259,22 +272,10 @@
<img class="image" src="/resources/profile/bisq_network.png" />
<span>Bisq</span>
</a>
<a href="https://github.com/BlueWallet/BlueWallet" target="_blank" title="BlueWallet">
<img class="image" src="/resources/profile/bluewallet.png" />
<span>BlueWallet</span>
</a>
<a href="https://github.com/muun/apollo" target="_blank" title="Muun Wallet">
<img class="image" src="/resources/profile/muun.png" />
<span>Muun</span>
</a>
<a href="https://github.com/spesmilo/electrum" target="_blank" title="Electrum Wallet">
<img class="image" src="/resources/profile/electrum.png" />
<span>Electrum</span>
</a>
<a href="https://github.com/cryptoadvance/specter-desktop" target="_blank" title="Specter Wallet">
<img class="image" src="/resources/profile/specter.png" />
<span>Specter</span>
</a>
<a href="https://github.com/sparrowwallet/sparrow" target="_blank" title="Sparrow Wallet">
<img class="image" src="/resources/profile/sparrow.png" />
<span>Sparrow</span>
@@ -283,21 +284,37 @@
<img class="image not-rounded" src="/resources/profile/phoenix.svg" />
<span>Phoenix</span>
</a>
<a href="https://github.com/lnbits/lnbits-legend" target="_blank" title="LNbits">
<img class="image" src="/resources/profile/lnbits.svg" />
<span>LNBits</span>
<a href="http://github.com/COLDCARD" target="_blank" title="COLDCARD">
<img class="image coldcard" src="/resources/profile/coldcard.png" />
<span>COLDCARD</span>
</a>
<a href="https://github.com/layer2tech/mercury-wallet" target="_blank" title="Mercury Wallet">
<img class="image" src="/resources/profile/mercury.svg" />
<span>Mercury</span>
<a href="https://github.com/ZeusLN/zeus" target="_blank" title="ZEUS">
<img class="image" src="/resources/profile/zeus.png" />
<span>ZEUS</span>
</a>
<a href="https://github.com/MutinyWallet" target="_blank" title="Mutiny">
<img class="image not-rounded" src="/resources/profile/mutiny.svg" />
<span>Mutiny</span>
</a>
<a href="https://github.com/hsjoberg/blixt-wallet" target="_blank" title="Blixt Wallet">
<img class="image" src="/resources/profile/blixt.png" />
<span>Blixt</span>
</a>
<a href="https://github.com/ZeusLN/zeus" target="_blank" title="ZEUS">
<img class="image" src="/resources/profile/zeus.png" />
<span>ZEUS</span>
<a href="https://github.com/nunchuk-io" target="_blank" title="Nunchuck">
<img class="image" src="/resources/profile/nunchuk.svg" />
<span>Nunchuk</span>
</a>
<a href="https://github.com/BlueWallet/BlueWallet" target="_blank" title="BlueWallet">
<img class="image" src="/resources/profile/bluewallet.png" />
<span>BlueWallet</span>
</a>
<a href="https://github.com/BoltzExchange" target="_blank" title="Boltz">
<img class="image" src="/resources/profile/boltz.svg" />
<span>Boltz</span>
</a>
<a href="https://github.com/lnbits/lnbits-legend" target="_blank" title="LNbits">
<img class="image" src="/resources/profile/lnbits.svg" />
<span>LNBits</span>
</a>
<a href="https://github.com/vulpemventures/marina" target="_blank" title="Marina Wallet">
<img class="image" src="/resources/profile/marina.svg" />
@@ -307,13 +324,9 @@
<img class="image" src="/resources/profile/schildbach.svg" />
<span>Schildbach</span>
</a>
<a href="https://github.com/nunchuk-io" target="_blank" title="Nunchuck">
<img class="image" src="/resources/profile/nunchuk.svg" />
<span>Nunchuk</span>
</a>
<a href="https://github.com/bitcoin-s/bitcoin-s" target="_blank" title="bitcoin-s">
<img class="image" src="/resources/profile/bitcoin-s.svg" />
<span>bitcoin-s</span>
<a href="https://github.com/cryptoadvance/specter-desktop" target="_blank" title="Specter Wallet">
<img class="image" src="/resources/profile/specter.png" />
<span>Specter</span>
</a>
<a href="https://github.com/EdgeApp" target="_blank" title="Edge">
<img class="image not-rounded" src="/resources/profile/edge.svg" />
@@ -323,13 +336,13 @@
<img class="image" src="/resources/profile/galoy.svg" />
<span>Galoy</span>
</a>
<a href="https://github.com/BoltzExchange" target="_blank" title="Boltz">
<img class="image" src="/resources/profile/boltz.svg" />
<span>Boltz</span>
<a href="https://github.com/muun/apollo" target="_blank" title="Muun Wallet">
<img class="image" src="/resources/profile/muun.png" />
<span>Muun</span>
</a>
<a href="https://github.com/MutinyWallet" target="_blank" title="Mutiny">
<img class="image not-rounded" src="/resources/profile/mutiny.svg" />
<span>Mutiny</span>
<a href="https://github.com/bitcoin-s/bitcoin-s" target="_blank" title="bitcoin-s">
<img class="image" src="/resources/profile/bitcoin-s.svg" />
<span>bitcoin-s</span>
</a>
</div>
</div>

View File

@@ -156,6 +156,12 @@
}
img, svg {
margin: 40px 29px 10px;
&.image.coldcard {
border-radius: 0;
width: auto;
max-height: 50px;
margin: 40px 29px 14px 29px;
}
}
}
}

View File

@@ -21,7 +21,7 @@
<input type="radio" class="form-check-input" id="accelerate" name="accelerate" (change)="selectedOptionChanged($event)">
<label class="form-check-label d-flex flex-column" for="accelerate">
<span class="font-weight-bold">Accelerate</span>
<span style="color: rgb(186, 186, 186); font-size: 14px;">Confirmation expected within ~30 minutes<br>
<span style="color: rgb(186, 186, 186); font-size: 14px;" *ngIf="(etaInfo$ | async) as etaInfo">Confirmation expected <app-time kind="within" [time]="etaInfo.acceleratedETA" [fastRender]="false" [fixedRender]="true"></app-time><br>
@if (!calculating) {
<app-fiat [value]="cost"></app-fiat>fee (<span><small style="font-family: monospace;">{{ cost | number }}</small>&nbsp;<span class="symbol" i18n="shared.sats">sats</span></span>)
} @else {

View File

@@ -1,9 +1,11 @@
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges } from '@angular/core';
import { Subscription, tap, of, catchError } from 'rxjs';
import { Subscription, tap, of, catchError, Observable } from 'rxjs';
import { ServicesApiServices } from '../../services/services-api.service';
import { nextRoundNumber } from '../../shared/common.utils';
import { StateService } from '../../services/state.service';
import { AudioService } from '../../services/audio.service';
import { AccelerationEstimate } from '../accelerate-preview/accelerate-preview.component';
import { EtaService } from '../../services/eta.service';
@Component({
selector: 'app-accelerate-checkout',
@@ -24,8 +26,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
square: { appId: string, locationId: string};
accelerationUUID: string;
estimateSubscription: Subscription;
estimate: AccelerationEstimate;
maxBidBoost: number; // sats
cost: number; // sats
etaInfo$: Observable<{ hashratePercentage: number, ETA: number, acceleratedETA: number }>;
// square
loadingCashapp = false;
@@ -39,6 +43,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
constructor(
private servicesApiService: ServicesApiServices,
private stateService: StateService,
private etaService: EtaService,
private audioService: AudioService,
private cd: ChangeDetectorRef
) {
@@ -59,7 +64,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
locationId: ids.squareLocationId
};
if (this.step === 'cta') {
this.estimate();
this.fetchEstimate();
}
});
}
@@ -99,7 +104,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
/**
* Accelerator
*/
estimate() {
fetchEstimate() {
if (this.estimateSubscription) {
this.estimateSubscription.unsubscribe();
}
@@ -110,16 +115,17 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (response.status === 204) {
this.error = `cannot_accelerate_tx`;
} else {
const estimation = response.body;
if (!estimation) {
this.estimate = response.body;
if (!this.estimate) {
this.error = `cannot_accelerate_tx`;
return;
}
// Make min extra fee at least 50% of the current tx fee
const minExtraBoost = nextRoundNumber(Math.max(estimation.cost * 2, estimation.txSummary.effectiveFee));
const minExtraBoost = nextRoundNumber(Math.max(this.estimate.cost * 2, this.estimate.txSummary.effectiveFee));
const DEFAULT_BID_RATIO = 1.5;
this.maxBidBoost = minExtraBoost * DEFAULT_BID_RATIO;
this.cost = this.maxBidBoost + estimation.mempoolBaseFee + estimation.vsizeFee;
this.cost = this.maxBidBoost + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
this.etaInfo$ = this.etaService.getProjectedEtaObservable(this.estimate);
}
}),

View File

@@ -4,7 +4,6 @@
width: 120px;
margin-left: 4em;
margin-right: 1.5em;
padding-bottom: 63px;
.column {
width: 100%;

View File

@@ -32,54 +32,56 @@
<div class="alert alert-mempool">You are currently on the waitlist</div>
</div>
<h5 i18n="accelerator.your-transaction">Your transaction</h5>
<div class="row">
<div class="col">
<small *ngIf="hasAncestors" class="form-text text-muted mb-2">
<ng-container i18n="accelerator.plus-unconfirmed-ancestors">Plus {{ estimate.txSummary.ancestorCount - 1 }} unconfirmed ancestor(s)</ng-container>
</small>
<table class="table table-borderless table-border table-dark table-background table-accelerator">
<tbody>
<tr class="group-first">
<td class="item" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
<td style="text-align: end;" [innerHTML]="'&lrm;' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td>
</tr>
<tr class="info">
<td class="info" colspan=3>
<i><small i18n="accelerator.transaction-vbytes-size-description">Size in vbytes of this transaction (including unconfirmed ancestors)</small></i>
</td>
</tr>
<tr>
<td class="item" i18n="accelerator.in-band-fees">In-band fees</td>
<td style="text-align: end;">
{{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats">sats</span>
</td>
</tr>
<tr class="info group-last">
<td class="info" colspan=3>
<i><small i18n="accelerator.fees-already-paid-description">Fees already paid by this transaction (including unconfirmed ancestors)</small></i>
</td>
</tr>
</tbody>
</table>
<ng-template [ngIf]="showDetails">
<h5 i18n="accelerator.your-transaction">Your transaction</h5>
<div class="row">
<div class="col">
<small *ngIf="hasAncestors" class="form-text text-muted mb-2">
<ng-container i18n="accelerator.plus-unconfirmed-ancestors">Plus {{ estimate.txSummary.ancestorCount - 1 }} unconfirmed ancestor(s)</ng-container>
</small>
<table class="table table-borderless table-border table-dark table-background table-accelerator">
<tbody>
<tr class="group-first">
<td class="item" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
<td style="text-align: end;" [innerHTML]="'&lrm;' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td>
</tr>
<tr class="info">
<td class="info" colspan=3>
<i><small i18n="accelerator.transaction-vbytes-size-description">Size in vbytes of this transaction (including unconfirmed ancestors)</small></i>
</td>
</tr>
<tr>
<td class="item" i18n="accelerator.in-band-fees">In-band fees</td>
<td style="text-align: end;">
{{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats">sats</span>
</td>
</tr>
<tr class="info group-last">
<td class="info" colspan=3>
<i><small i18n="accelerator.fees-already-paid-description">Fees already paid by this transaction (including unconfirmed ancestors)</small></i>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<br>
<br>
</ng-template>
<h5 *ngIf="estimate?.pools?.length" i18n="accelerator.how-much-faster">How much faster?</h5>
<div class="row">
<div class="col">
<small class="form-text text-muted mb-2" i18n="accelerator.hashrate-percentage-description">Your transaction will be prioritized by up to {{ hashratePercentage | number : '1.1-1' }}% of miners.</small>
<small class="form-text text-muted mb-2" i18n="accelerator.time-estimate-description">This will reduce your expected waiting time until the first confirmation to <app-time kind="within" [time]="acceleratedETA" [fastRender]="false" [fixedRender]="true"></app-time></small>
<ng-container *ngIf="(etaInfo$ | async) as etaInfo; else loadingEstimate">
<small class="form-text text-muted mb-2" i18n="accelerator.hashrate-percentage-description">Your transaction will be prioritized by up to <strong>{{ etaInfo.hashratePercentage | number : '1.1-1' }}%</strong> of miners.</small>
<small class="form-text text-muted mb-2" i18n="accelerator.time-estimate-description">This will reduce your expected waiting time until the first confirmation to <strong><app-time kind="within" [time]="etaInfo.acceleratedETA" [fastRender]="false" [fixedRender]="true"></app-time></strong></small>
</ng-container>
</div>
<div class="col pie">
<app-active-acceleration-box [miningStats]="miningStats" [pools]="estimate.pools" [chartOnly]="true"></app-active-acceleration-box>
</div>
</div>
<br>
<h5 i18n="accelerator.pay-how-much">How much more are you willing to pay?</h5>
<div class="row">
<div class="col">
<small class="form-text text-muted mb-2" i18n="accelerator.transaction-fee-description">Choose the maximum extra transaction fee you're willing to pay.</small>
<div class="form-group">
<div class="fee-card">
<div class="d-flex mb-0">
@@ -95,13 +97,13 @@
</div>
</div>
<h5>Acceleration summary</h5>
<div class="row mb-3">
<h5>Summary</h5>
<div class="row">
<div class="col">
<table class="table table-borderless table-border table-dark table-background table-accelerator">
<tbody>
<!-- ESTIMATED FEE -->
<ng-container>
<ng-template [ngIf]="showDetails">
<tr class="group-first">
<td class="item" i18n="accelerator.next-block-rate">Next block market rate</td>
<td class="amt" style="font-size: 16px">
@@ -121,25 +123,24 @@
<span class="fiat ml-1"><app-fiat [value]="math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee)"></app-fiat></span>
</td>
</tr>
</ng-container>
<!-- MEMPOOL BASE FEE -->
<tr>
<td class="item" i18n="accelerator.mempool-accelerator-fees">Mempool Accelerator™ fees</td>
</tr>
<tr class="info">
<td class="info">
<i><small i18n="accelerator.service-fee">Accelerator Service Fee</small></i>
</td>
<td class="amt">
+{{ estimate.mempoolBaseFee | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span>
</td>
</tr>
<tr class="info group-last">
<!-- MEMPOOL BASE FEE -->
<tr>
<td class="item" i18n="accelerator.mempool-accelerator-fees">Mempool Accelerator™ fees</td>
</tr>
<tr class="info" [class.group-last]="!estimate.vsizeFee" [class.dashed-bottom]="!estimate.vsizeFee">
<td class="info">
<i><small i18n="accelerator.service-fee">Accelerator Service Fee</small></i>
</td>
<td class="amt">
+{{ estimate.mempoolBaseFee | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span>
</td>
</tr>
<tr class="info group-last dashed-bottom" *ngIf="estimate.vsizeFee">
<td class="info">
<i><small i18n="accelerator.tx-size-surcharge">Transaction Size Surcharge</small></i>
</td>
@@ -151,13 +152,13 @@
<span class="fiat ml-1"><app-fiat [value]="estimate.vsizeFee"></app-fiat></span>
</td>
</tr>
</ng-template>
<!-- NEXT BLOCK ESTIMATE -->
<ng-container>
<tr class="group-first" style="border-top: 1px dashed grey; border-collapse: collapse;">
<tr class="group-first">
<td class="item">
<b style="background-color: #5E35B1" class="p-1 pl-0" i18n="accelerator.estimated-cost">Estimated acceleration cost</b>
<b style="background-color: #5E35B1" class="p-1 pl-0" i18n="accelerator.estimated-cost">Estimated acceleration cost</b> ~{{ estimate.targetFeeRate | number : '1.0-0' }} sat/vB
</td>
<td class="amt">
<span style="background-color: #5E35B1" class="p-1 pl-0">
@@ -169,18 +170,13 @@
<span class="fiat ml-1"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span>
</td>
</tr>
<tr class="info group-last" style="border-bottom: 1px solid lightgrey">
<td class="info" colspan=3>
<i><small><ng-container *ngTemplateOutlet="acceleratedTo; context: {$implicit: estimate.targetFeeRate }"></ng-container></small></i>
</td>
</tr>
</ng-container>
<!-- MAX COST -->
<ng-container>
<tr class="group-first">
<tr class="group-first" [class.group-last]="!isLoggedIn() || estimate.userBalance >= maxCost">
<td class="item">
<b style="background-color: var(--primary);" class="p-1 pl-0" i18n="accelerator.maximum-cost">Maximum acceleration cost</b>
<b style="background-color: var(--primary);" class="p-1 pl-0" i18n="accelerator.maximum-cost">Maximum acceleration cost</b>
</td>
<td class="amt">
<span style="background-color: var(--primary)" class="p-1 pl-0">
@@ -190,20 +186,15 @@
<td class="units">
<span class="symbol" i18n="shared.sats">sats</span>
<span class="fiat ml-1">
<app-fiat [value]="maxCost" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat>
<app-fiat [value]="maxCost" [colorClass]="isLoggedIn() && estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat>
</span>
</td>
</tr>
<tr class="info group-last">
<td class="info" colspan=3>
<i><small><ng-container *ngTemplateOutlet="acceleratedTo; context: {$implicit: (estimate.txSummary.effectiveFee + userBid) / estimate.txSummary.effectiveVsize }"></ng-container></small></i>
</td>
</tr>
</ng-container>
<!-- USER BALANCE -->
<ng-container *ngIf="isLoggedIn() && estimate.userBalance < maxCost">
<tr class="group-first group-last" style="border-top: 1px dashed grey">
<tr class="group-first group-last dashed-top">
<td class="item" i18n="accelerator.available-balance">Available balance</td>
<td class="amt">
{{ estimate.userBalance | number }}
@@ -216,23 +207,21 @@
</td>
</tr>
</ng-container>
<!-- LOGIN CTA -->
<ng-container *ngIf="stateService.isMempoolSpaceBuild && !isLoggedIn()">
<ng-container *ngIf="user && estimate?.hasAccess || !isLoggedIn()">
<tr class="group-first group-last" style="border-top: 1px dashed grey">
<td class="item"></td>
<td class="amt"></td>
<td class="units d-flex">
<a [routerLink]="['/login']" [queryParams]="{redirectTo: '/tx/' + tx.txid + '#accelerate'}" class="btn btn-purple flex-grow-1" i18n="shared.sign-in">Sign In</a>
</td>
</tr>
</ng-container>
<ng-container *ngIf="!stateService.isMempoolSpaceBuild">
<tr class="group-first group-last" style="border-top: 1px dashed grey">
<td class="item"></td>
<td class="amt"></td>
<td class="units d-flex">
<a [href]="'https://mempool.space/tx/' + tx.txid + '#accelerate'" class="btn btn-purple flex-grow-1" i18n="accelerator.accelerate-on-mempoolspace">Accelerate on mempool.space</a>
<td colspan="2">
<div class="d-flex">
@if (isLoggedIn()) {
@if (user && estimate.hasAccess) {
<button class="btn btn-sm btn-primary btn-success flex-grow-1" style="width: 150px" (click)="accelerate()" i18n="transaction.accelerate|Accelerate button label">Accelerate</button>
}
} @else if (stateService.isMempoolSpaceBuild) {
<a [routerLink]="['/login']" [queryParams]="{redirectTo: '/tx/' + tx.txid + '#accelerate'}" class="btn btn-purple flex-grow-1" i18n="shared.sign-in">Sign In</a>
} @else {
<a [href]="'https://mempool.space/tx/' + tx.txid + '#accelerate'" class="btn btn-purple flex-grow-1" i18n="accelerator.accelerate-on-mempoolspace">Accelerate on mempool.space</a>
}
</div>
</td>
</tr>
</ng-container>
@@ -240,15 +229,6 @@
</table>
</div>
</div>
<div class="row mb-3" *ngIf="isLoggedIn()">
<div class="col">
<div class="d-flex justify-content-end" *ngIf="user && estimate.hasAccess">
<button class="btn btn-sm btn-primary btn-success" style="width: 150px" (click)="accelerate()" i18n="transaction.accelerate|Accelerate button label">Accelerate</button>
</div>
</div>
</div>
</div>
</ng-container>
</div>
@@ -256,6 +236,4 @@
<ng-template #loadingEstimate>
<div class="skeleton-loader"></div>
<br>
</ng-template>
<ng-template #acceleratedTo let-i i18n="accelerator.accelerated-to-description">If your tx is accelerated to ~{{ i | number : '1.0-0' }} sat/vB</ng-template>
</ng-template>

View File

@@ -72,11 +72,17 @@
padding-top: 0.75rem;
}
}
&.group-last {
&.group-last, &:last-child {
td {
padding-bottom: 0.75rem;
}
}
&.dashed-top {
border-top: 1px dashed grey;
}
&.dashed-bottom {
border-bottom: 1px dashed grey
}
}
td {
&:first-child {
@@ -118,4 +124,9 @@
.table-background {
background-color: var(--bg);
}
.col.pie {
position: relative;
top: -15px;
}

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener, ChangeDetectorRef } from '@angular/core';
import { Subscription, catchError, of, tap } from 'rxjs';
import { Observable, Subscription, catchError, of, tap } from 'rxjs';
import { StorageService } from '../../services/storage.service';
import { Transaction } from '../../interfaces/electrs.interface';
import { nextRoundNumber } from '../../shared/common.utils';
@@ -8,7 +8,6 @@ import { AudioService } from '../../services/audio.service';
import { StateService } from '../../services/state.service';
import { MiningStats } from '../../services/mining.service';
import { EtaService } from '../../services/eta.service';
import { DifficultyAdjustment, MempoolPosition, SinglePoolStats } from '../../interfaces/node-api.interface';
export type AccelerationEstimate = {
txSummary: TxSummary;
@@ -19,6 +18,7 @@ export type AccelerationEstimate = {
cost: number;
mempoolBaseFee: number;
vsizeFee: number;
pools: number[]
}
export type TxSummary = {
txid: string; // txid of the current transaction
@@ -44,9 +44,9 @@ export const MAX_BID_RATIO = 4;
})
export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges {
@Input() tx: Transaction;
@Input() mempoolPosition: MempoolPosition;
@Input() miningStats: MiningStats;
@Input() scrollEvent: boolean;
@Input() showDetails: boolean;
math = Math;
error = '';
@@ -54,11 +54,8 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
estimateSubscription: Subscription;
accelerationSubscription: Subscription;
difficultySubscription: Subscription;
da: DifficultyAdjustment;
estimate: any;
hashratePercentage?: number;
ETA?: number;
acceleratedETA?: number;
etaInfo$: Observable<{ hashratePercentage: number, ETA: number, acceleratedETA: number }>;
hasAncestors: boolean = false;
minExtraCost = 0;
minBidAllowed = 0;
@@ -87,27 +84,19 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
if (this.estimateSubscription) {
this.estimateSubscription.unsubscribe();
}
this.difficultySubscription.unsubscribe();
}
ngOnInit() {
ngOnInit(): void {
this.accelerationUUID = window.crypto.randomUUID();
this.difficultySubscription = this.stateService.difficultyAdjustment$.subscribe(da => {
this.da = da;
this.updateETA();
})
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.scrollEvent) {
this.scrollToPreview('acceleratePreviewAnchor', 'start');
}
if (changes.miningStats || changes.mempoolPosition) {
this.updateETA();
}
}
ngAfterViewInit() {
ngAfterViewInit(): void {
this.user = this.storageService.getAuth()?.user ?? null;
this.estimateSubscription = this.servicesApiService.estimate$(this.tx.txid).pipe(
@@ -132,7 +121,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
}
}
this.updateETA();
this.etaInfo$ = this.etaService.getProjectedEtaObservable(this.estimate, this.miningStats);
this.hasAncestors = this.estimate.txSummary.ancestorCount > 1;
@@ -178,40 +167,10 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
).subscribe();
}
updateETA(): void {
if (!this.mempoolPosition || !this.estimate?.pools?.length || !this.miningStats || !this.da) {
this.hashratePercentage = undefined;
this.ETA = undefined;
this.acceleratedETA = undefined;
return;
}
const pools: { [id: number]: SinglePoolStats } = {};
for (const pool of this.miningStats.pools) {
pools[pool.poolUniqueId] = pool;
}
let totalAcceleratedHashrate = 0;
for (const poolId of this.estimate.pools) {
const pool = pools[poolId];
if (!pool) {
continue;
}
totalAcceleratedHashrate += pool.lastEstimatedHashrate;
}
const acceleratingHashrateFraction = (totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate)
this.hashratePercentage = acceleratingHashrateFraction * 100;
this.ETA = Date.now() + this.da.timeAvg * this.mempoolPosition.block;
this.acceleratedETA = this.etaService.calculateETAFromShares([
{ block: this.mempoolPosition.block, hashrateShare: (1 - acceleratingHashrateFraction) },
{ block: 0, hashrateShare: acceleratingHashrateFraction },
], this.da).time;
}
/**
* User changed his bid
*/
setUserBid({ fee, index }: { fee: number, index: number}) {
setUserBid({ fee, index }: { fee: number, index: number}): void {
if (this.estimate) {
this.selectFeeRateIndex = index;
this.userBid = Math.max(0, fee);
@@ -222,12 +181,12 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
/**
* Scroll to element id with or without setTimeout
*/
scrollToPreviewWithTimeout(id: string, position: ScrollLogicalPosition) {
scrollToPreviewWithTimeout(id: string, position: ScrollLogicalPosition): void {
setTimeout(() => {
this.scrollToPreview(id, position);
}, 100);
}
scrollToPreview(id: string, position: ScrollLogicalPosition) {
scrollToPreview(id: string, position: ScrollLogicalPosition): void {
const acceleratePreviewAnchor = document.getElementById(id);
if (acceleratePreviewAnchor) {
this.cd.markForCheck();
@@ -242,7 +201,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
/**
* Send acceleration request
*/
accelerate() {
accelerate(): void {
if (this.accelerationSubscription) {
this.accelerationSubscription.unsubscribe();
}
@@ -268,7 +227,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
});
}
isLoggedIn() {
isLoggedIn(): boolean {
const auth = this.storageService.getAuth();
return auth !== null;
}
@@ -280,7 +239,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
@HostListener('window:scroll', ['$event']) // for window scroll events
onScroll() {
onScroll(): void {
if (this.estimate) {
setTimeout(() => {
this.onScroll();

View File

@@ -1,9 +1,11 @@
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { BehaviorSubject, Observable, catchError, of, switchMap, tap } from 'rxjs';
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy, Inject, LOCALE_ID } from '@angular/core';
import { BehaviorSubject, Observable, Subscription, catchError, of, switchMap, tap, throttleTime } from 'rxjs';
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
import { StateService } from '../../../services/state.service';
import { WebsocketService } from '../../../services/websocket.service';
import { ServicesApiServices } from '../../../services/services-api.service';
import { SeoService } from '../../../services/seo.service';
import { ActivatedRoute, Router } from '@angular/router';
@Component({
selector: 'app-accelerations-list',
@@ -25,25 +27,44 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
maxSize = window.innerWidth <= 767.98 ? 3 : 5;
skeletonLines: number[] = [];
pageSubject: BehaviorSubject<number> = new BehaviorSubject(this.page);
keyNavigationSubscription: Subscription;
dir: 'rtl' | 'ltr' = 'ltr';
paramSubscription: Subscription;
constructor(
private servicesApiService: ServicesApiServices,
private websocketService: WebsocketService,
public stateService: StateService,
private cd: ChangeDetectorRef,
private seoService: SeoService,
private route: ActivatedRoute,
private router: Router,
@Inject(LOCALE_ID) private locale: string,
) {
if (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')) {
this.dir = 'rtl';
}
}
ngOnInit(): void {
if (!this.widget) {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@02573b6980a2d611b4361a2595a4447e390058cd:Accelerations`);
}
this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()];
this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
this.paramSubscription = this.route.params.pipe(
tap(params => {
this.page = +params['page'] || 1;
this.pageSubject.next(this.page);
})
).subscribe();
this.accelerationList$ = this.pageSubject.pipe(
switchMap((page) => {
this.isLoading = true;
const accelerationObservable$ = this.accelerations$ || (this.pending ? this.stateService.liveAccelerations$ : this.servicesApiService.getAccelerationHistoryObserveResponse$({ page: page }));
if (!this.accelerations$ && this.pending) {
this.websocketService.ensureTrackAccelerations();
@@ -79,10 +100,30 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
);
})
);
this.keyNavigationSubscription = this.stateService.keyNavigation$.pipe(
tap((event) => {
const prevKey = this.dir === 'ltr' ? 'ArrowLeft' : 'ArrowRight';
const nextKey = this.dir === 'ltr' ? 'ArrowRight' : 'ArrowLeft';
if (event.key === prevKey && this.page > 1) {
this.page--;
this.isLoading = true;
this.cd.markForCheck();
}
if (event.key === nextKey && this.page * 15 < this.accelerationCount) {
this.page++;
this.isLoading = true;
this.cd.markForCheck();
}
}),
throttleTime(1000, undefined, { leading: true, trailing: true }),
).subscribe(() => {
this.pageChange(this.page);
});
}
pageChange(page: number): void {
this.pageSubject.next(page);
this.router.navigate(['acceleration', 'list', page]);
}
trackByBlock(index: number, block: BlockExtended): number {
@@ -91,5 +132,7 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
ngOnDestroy(): void {
this.websocketService.stopTrackAccelerations();
this.paramSubscription?.unsubscribe();
this.keyNavigationSubscription?.unsubscribe();
}
}

View File

@@ -4,8 +4,11 @@
<table>
<tbody>
<tr>
<td class="td-width field-label" i18n="transaction.accelerated-to-feerate|Accelerated to feerate">Accelerated to</td>
<td class="field-value">
<td class="td-width field-label" [class]="chartPositionLeft ? 'chart-left' : ''" i18n="transaction.accelerated-to-feerate|Accelerated to feerate">Accelerated to</td>
<td class="pie-chart" rowspan="2" *ngIf="chartPositionLeft">
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
</td>
<td class="field-value" [class]="chartPositionLeft ? 'chart-left' : ''">
<div class="effective-fee-container">
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) {
<app-fee-rate [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
@@ -14,7 +17,7 @@
}
</div>
</td>
<td class="pie-chart" rowspan="2">
<td class="pie-chart" rowspan="2" *ngIf="!chartPositionLeft">
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
</td>
</tr>

View File

@@ -16,6 +16,9 @@
width: auto;
min-width: auto;
}
&.chart-left {
width: 100%;
}
}
.field-value {
@@ -23,6 +26,10 @@
width: 100%;
}
&.chart-left {
width: auto;
}
.hashrate-label {
@media (max-width: 420px) {
display: none;
@@ -47,4 +54,11 @@
@media (max-width: 420px) {
padding-left: 0;
}
}
::ng-deep .chart {
overflow: visible;
& > div, & > div > svg {
overflow: visible !important;
}
}

View File

@@ -4,6 +4,17 @@ import { Acceleration, SinglePoolStats } from '../../../interfaces/node-api.inte
import { EChartsOption, PieSeriesOption } from '../../../graphs/echarts';
import { MiningStats } from '../../../services/mining.service';
function lighten(color, p): { r, g, b } {
return {
r: color.r + ((255 - color.r) * p),
g: color.g + ((255 - color.g) * p),
b: color.b + ((255 - color.b) * p),
};
}
function toRGB({r,g,b}): string {
return `rgb(${r},${g},${b})`;
}
@Component({
selector: 'app-active-acceleration-box',
@@ -17,6 +28,7 @@ export class ActiveAccelerationBox implements OnChanges {
@Input() miningStats: MiningStats;
@Input() pools: number[];
@Input() chartOnly: boolean = false;
@Input() chartPositionLeft: boolean = false;
acceleratedByPercentage: string = '';
@@ -43,57 +55,33 @@ export class ActiveAccelerationBox implements OnChanges {
pools[pool.poolUniqueId] = pool;
}
const getDataItem = (value, color, tooltip) => ({
const getDataItem = (value, color, tooltip, emphasis) => ({
value,
name: tooltip,
itemStyle: {
color,
borderColor: 'rgba(0,0,0,0)',
borderWidth: 1,
},
avoidLabelOverlap: false,
label: {
show: false,
},
labelLine: {
show: false
},
emphasis: {
disabled: true,
},
tooltip: {
show: true,
backgroundColor: 'rgba(17, 19, 31, 1)',
borderRadius: 4,
shadowColor: 'rgba(0, 0, 0, 0.5)',
textStyle: {
color: 'var(--tooltip-grey)',
},
borderColor: '#000',
formatter: () => {
return tooltip;
}
}
});
let totalAcceleratedHashrate = 0;
for (const poolId of poolList || []) {
const acceleratingPools = (poolList || []).filter(id => pools[id]).sort((a,b) => pools[a].lastEstimatedHashrate - pools[b].lastEstimatedHashrate);
const totalAcceleratedHashrate = acceleratingPools.reduce((total, pool) => total + pools[pool].lastEstimatedHashrate, 0);
acceleratingPools.forEach((poolId, index) => {
const pool = pools[poolId];
if (!pool) {
continue;
}
totalAcceleratedHashrate += pool.lastEstimatedHashrate;
}
const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1);
data.push(getDataItem(
pool.lastEstimatedHashrate,
toRGB(lighten({ r: 147, g: 57, b: 244 }, index * .08)),
`<b style="color: white">${pool.name} (${poolShare}%)</b>`,
true,
) as PieSeriesOption);
})
this.acceleratedByPercentage = ((totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1) + '%';
data.push(getDataItem(
totalAcceleratedHashrate,
'var(--mainnet-alt)',
`${this.acceleratedByPercentage} accelerating`,
) as PieSeriesOption);
const notAcceleratedByPercentage = ((1 - (totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate)) * 100).toFixed(1) + '%';
data.push(getDataItem(
(this.miningStats.lastEstimatedHashrate - totalAcceleratedHashrate),
'rgba(127, 127, 127, 0.3)',
`${notAcceleratedByPercentage} not accelerating`,
`not accelerating (${notAcceleratedByPercentage})`,
false,
) as PieSeriesOption);
return data;
@@ -111,11 +99,28 @@ export class ActiveAccelerationBox implements OnChanges {
tooltip: {
show: true,
trigger: 'item',
backgroundColor: 'rgba(17, 19, 31, 1)',
borderRadius: 4,
shadowColor: 'rgba(0, 0, 0, 0.5)',
textStyle: {
color: 'var(--tooltip-grey)',
},
borderColor: '#000',
formatter: (item) => {
return item.name;
}
},
series: [
{
type: 'pie',
radius: '100%',
label: {
show: false
},
labelLine: {
show: false
},
animationDuration: 0,
data: this.getChartData(pools),
}
]

View File

@@ -26,11 +26,13 @@
<tbody>
<tr><ng-container *ngTemplateOutlet="balanceRow"></ng-container></tr>
<tr><ng-container *ngTemplateOutlet="pendingBalanceRow"></ng-container></tr>
<tr><ng-container *ngTemplateOutlet="utxoRow"></ng-container></tr>
<tr><ng-container *ngTemplateOutlet="pendingUtxoRow"></ng-container></tr>
@if (!address.electrum) {
<tr><ng-container *ngTemplateOutlet="utxoRow"></ng-container></tr>
<tr><ng-container *ngTemplateOutlet="pendingUtxoRow"></ng-container></tr>
}
@if (network === 'liquid' || network === 'liquidtestnet') {
<tr><ng-container *ngTemplateOutlet="liquidRow"></ng-container></tr>
} @else {
} @else if (!address.electrum) {
<tr><ng-container *ngTemplateOutlet="volumeRow"></ng-container></tr>
}
<tr><ng-container *ngTemplateOutlet="typeRow"></ng-container></tr>
@@ -46,17 +48,21 @@
<ng-container *ngTemplateOutlet="spacerCell"></ng-container>
<ng-container *ngTemplateOutlet="pendingBalanceRow"></ng-container>
</tr>
@if (!address.electrum) {
<tr>
<ng-container *ngTemplateOutlet="utxoRow"></ng-container>
<ng-container *ngTemplateOutlet="spacerCell"></ng-container>
<ng-container *ngTemplateOutlet="pendingUtxoRow"></ng-container>
</tr>
}
<tr>
@if (network === 'liquid' || network === 'liquidtestnet') {
<ng-container *ngTemplateOutlet="liquidRow"></ng-container>
} @else {
} @else if (!address.electrum) {
<ng-container *ngTemplateOutlet="volumeRow"></ng-container>
}
} @else {
<ng-container *ngTemplateOutlet="emptyTd"></ng-container>
}
<ng-container *ngTemplateOutlet="spacerCell"></ng-container>
<ng-container *ngTemplateOutlet="typeRow"></ng-container>
</tr>
@@ -232,6 +238,11 @@
<td class="spacer"></td>
</ng-template>
<ng-template #emptyTd>
<td class="spacer"></td>
<td class="spacer"></td>
</ng-template>
<ng-template #balanceRow>
<td i18n="address.confirmed-balance">Confirmed balance</td>
<td *ngIf="chainStats.funded_txo_sum !== undefined; else confidentialTd" class="wrap-cell"><app-amount [satoshis]="chainStats.balance" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="chainStats.balance"></app-fiat></span></td>

View File

@@ -1,4 +1,4 @@
<ng-container *ngIf="!noFiat && (viewAmountMode$ | async) === 'fiat' && (conversions$ | async) as conversions; else viewFiatVin">
<ng-container *ngIf="!noFiat && ignoreViewMode === false && (viewAmountMode$ | async) === 'fiat' && (conversions$ | async) as conversions; else viewFiatVin">
<span class="fiat" *ngIf="blockConversion; else noblockconversion">
{{ addPlus && satoshis >= 0 ? '+' : '' }}{{
(
@@ -21,7 +21,7 @@
<span i18n="shared.confidential">Confidential</span>
</ng-template>
<ng-template #default>
@if ((viewAmountMode$ | async) === 'btc' || (viewAmountMode$ | async) === 'fiat') {
@if ((viewAmountMode$ | async) === 'btc' || (viewAmountMode$ | async) === 'fiat' || ignoreViewMode === true) {
&lrm;{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis / 100000000 | number : digitsInfo }}
<span class="symbol">
<ng-container *ngTemplateOutlet="prefix"></ng-container>BTC

View File

@@ -24,6 +24,7 @@ export class AmountComponent implements OnInit, OnDestroy {
@Input() addPlus = false;
@Input() blockConversion: Price;
@Input() forceBtc: boolean = false;
@Input() ignoreViewMode: boolean = false;
@Input() forceBlockConversion: boolean = false; // true = displays fiat price as 0 if blockConversion is undefined instead of falling back to conversions
constructor(

View File

@@ -73,27 +73,27 @@ export class BlocksList implements OnInit {
this.seoService.setDescription($localize`:@@meta.description.bitcoin.blocks:See the most recent Bitcoin${seoDescriptionNetwork(this.stateService.network)} blocks along with basic stats such as block height, block reward, block size, and more.`);
}
this.blocksCountInitializedSubscription = combineLatest([this.blocksCountInitialized$, this.route.queryParams]).pipe(
this.blocksCountInitializedSubscription = combineLatest([this.blocksCountInitialized$, this.route.params]).pipe(
filter(([blocksCountInitialized, _]) => blocksCountInitialized),
tap(([_, params]) => {
this.page = +params['page'] || 1;
this.page === 1 ? this.fromHeightSubject.next(undefined) : this.fromHeightSubject.next((this.blocksCount - 1) - (this.page - 1) * 15);
this.cd.markForCheck();
})
).subscribe();
this.keyNavigationSubscription = this.stateService.keyNavigation$
.pipe(
tap((event) => {
this.isLoading = true;
const prevKey = this.dir === 'ltr' ? 'ArrowLeft' : 'ArrowRight';
const nextKey = this.dir === 'ltr' ? 'ArrowRight' : 'ArrowLeft';
if (event.key === prevKey && this.page > 1) {
this.page--;
this.isLoading = true;
this.cd.markForCheck();
}
if (event.key === nextKey && this.page * 15 < this.blocksCount) {
this.page++;
this.isLoading = true;
this.cd.markForCheck();
}
}),
@@ -118,6 +118,7 @@ export class BlocksList implements OnInit {
if (this.blocksCount === undefined) {
this.blocksCount = blocks[0].height + 1;
this.blocksCountInitialized$.next(true);
this.blocksCountInitialized$.complete();
}
this.isLoading = false;
this.lastBlockHeight = Math.max(...blocks.map(o => o.height));
@@ -179,7 +180,7 @@ export class BlocksList implements OnInit {
}
pageChange(page: number): void {
this.router.navigate([], { queryParams: { page: page } });
this.router.navigate(['blocks', page]);
}
trackByBlock(index: number, block: BlockExtended): number {

View File

@@ -36,7 +36,7 @@ export class RecentPegsListComponent implements OnInit {
lastPegBlockUpdate: number = 0;
lastPegAmount: string = '';
isLoad: boolean = true;
queryParamSubscription: Subscription;
paramSubscription: Subscription;
keyNavigationSubscription: Subscription;
dir: 'rtl' | 'ltr' = 'ltr';
@@ -66,7 +66,7 @@ export class RecentPegsListComponent implements OnInit {
this.seoService.setTitle($localize`:@@a8b0889ea1b41888f1e247f2731cc9322198ca04:Recent Peg-In / Out's`);
this.websocketService.want(['blocks']);
this.queryParamSubscription = this.route.queryParams.pipe(
this.paramSubscription = this.route.params.pipe(
tap((params) => {
this.page = +params['page'] || 1;
this.startingIndexSubject.next((this.page - 1) * 15);
@@ -76,15 +76,16 @@ export class RecentPegsListComponent implements OnInit {
this.keyNavigationSubscription = this.stateService.keyNavigation$
.pipe(
tap((event) => {
this.isLoading = true;
const prevKey = this.dir === 'ltr' ? 'ArrowLeft' : 'ArrowRight';
const nextKey = this.dir === 'ltr' ? 'ArrowRight' : 'ArrowLeft';
if (event.key === prevKey && this.page > 1) {
this.page--;
this.isLoading = true;
this.cd.markForCheck();
}
if (event.key === nextKey && this.page < this.pegsCount / this.pageSize) {
this.page++;
this.isLoading = true;
this.cd.markForCheck();
}
}),
@@ -172,12 +173,12 @@ export class RecentPegsListComponent implements OnInit {
ngOnDestroy(): void {
this.destroy$.next(1);
this.destroy$.complete();
this.queryParamSubscription?.unsubscribe();
this.paramSubscription?.unsubscribe();
this.keyNavigationSubscription?.unsubscribe();
}
pageChange(page: number): void {
this.router.navigate([], { queryParams: { page: page } });
this.router.navigate(['audit', 'pegs', page]);
}
}

View File

@@ -97,7 +97,7 @@
</tr>
</thead>
<tbody>
<td class="text-center"><app-amount [satoshis]="poolStats.totalReward" digitsInfo="1.0-0" [noFiat]="true"></app-amount></td>
<td class="text-center"><app-amount [satoshis]="poolStats.totalReward" digitsInfo="1.0-0" [noFiat]="true" [ignoreViewMode]="true"></app-amount></td>
<td class="text-center">{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}</td>
<td class="text-center" *ngIf="auditAvailable; else emptyTd"><span class="health-badge badge" [class.badge-success]="poolStats.avgBlockHealth >= 99"
[class.badge-warning]="poolStats.avgBlockHealth >= 75 && poolStats.avgBlockHealth < 99" [class.badge-danger]="poolStats.avgBlockHealth < 75"
@@ -146,7 +146,7 @@
</tr>
</thead>
<tbody>
<td *ngFor="let total of oob" class="text-center clip"><app-amount [satoshis]="total.cost" [digitsInfo]="isMobile() ? '1.2-4' : '1.8-8'"></app-amount></td>
<td *ngFor="let total of oob" class="text-center clip"><app-amount [satoshis]="total.cost" [digitsInfo]="isMobile() ? '1.2-4' : '1.8-8'" [ignoreViewMode]="true"></app-amount></td>
</tbody>
</table>
</td>
@@ -219,10 +219,10 @@
</ng-template>
</td>
<td class="reward text-right">
<app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
<app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-2" [noFiat]="true" [ignoreViewMode]="true"></app-amount>
</td>
<td *ngIf="!auditAvailable" class="fees text-right">
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-2" [noFiat]="true" [ignoreViewMode]="true"></app-amount>
</td>
<td class="txs text-right">
{{ block.tx_count | number }}

View File

@@ -15,7 +15,7 @@
<div class="clearfix"></div>
<div class="rbf-trees" style="min-height: 295px">
<div class="rbf-trees" [ngStyle]="{ 'min-height': '295px', 'opacity': isLoading ? '0.75' : '1' }">
<ng-container *ngIf="rbfTrees$ | async as trees">
<div *ngFor="let tree of trees" class="tree">
<p class="info">

View File

@@ -38,6 +38,7 @@ export class RbfList implements OnInit, OnDestroy {
this.fullRbf = (fragment === 'fullrbf');
this.websocketService.startTrackRbf(this.fullRbf ? 'fullRbf' : 'all');
this.nextRbfSubject.next(null);
this.isLoading = true;
});
this.rbfTrees$ = merge(

View File

@@ -78,6 +78,10 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
}
calculate() {
if (!this.time) {
return;
}
let seconds: number;
switch (this.kind) {
case 'since':

View File

@@ -116,7 +116,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
hasEffectiveFeeRate: boolean;
accelerateCtaType: 'alert' | 'button' = 'button';
acceleratorAvailable: boolean = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
acceleratorAvailable: boolean = this.stateService.env.ACCELERATOR && this.stateService.network === '';
accelerationEligible: boolean = false;
showAccelerationSummary = false;
accelerationFlowCompleted = false;

View File

@@ -80,10 +80,12 @@
<div class="title float-left">
<h2 i18n="transaction.accelerate|Accelerate button label">Accelerate</h2>
</div>
<button type="button" class="btn btn-outline-info flow-toggle btn-sm float-right" (click)="showAccelerationDetails = !showAccelerationDetails" i18n="transaction.details|Transaction Details">Details</button>
<div class="clearfix"></div>
<div class="box">
<app-accelerate-preview [tx]="tx" [miningStats]="miningStats" [mempoolPosition]="mempoolPosition" [scrollEvent]="scrollIntoAccelPreview"></app-accelerate-preview>
<app-accelerate-preview [tx]="tx" [miningStats]="miningStats" [scrollEvent]="scrollIntoAccelPreview" [showDetails]="showAccelerationDetails"></app-accelerate-preview>
</div>
</ng-container>
@@ -648,7 +650,7 @@
<ng-template #acceleratingRow>
<tr>
<td rowspan="2" colspan="2" style="padding: 0;">
<app-active-acceleration-box [tx]="tx" [accelerationInfo]="accelerationInfo" [miningStats]="miningStats"></app-active-acceleration-box>
<app-active-acceleration-box [tx]="tx" [accelerationInfo]="accelerationInfo" [miningStats]="miningStats" [chartPositionLeft]="isMobile"></app-active-acceleration-box>
</td>
</tr>
<tr></tr>

View File

@@ -136,8 +136,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
taprootEnabled: boolean;
hasEffectiveFeeRate: boolean;
accelerateCtaType: 'alert' | 'button' = 'button';
acceleratorAvailable: boolean = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
acceleratorAvailable: boolean = this.stateService.env.ACCELERATOR && this.stateService.network === '';
showAccelerationSummary = false;
showAccelerationDetails = false;
scrollIntoAccelPreview = false;
auditEnabled: boolean = this.stateService.env.AUDIT && this.stateService.env.BASE_MODULE === 'mempool' && this.stateService.env.MINING_DASHBOARD === true;
@@ -166,15 +167,13 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
) {}
ngOnInit() {
this.acceleratorAvailable = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
this.enterpriseService.page();
this.websocketService.want(['blocks', 'mempool-blocks']);
this.stateService.networkChanged$.subscribe(
(network) => {
this.network = network;
this.acceleratorAvailable = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
this.acceleratorAvailable = this.stateService.env.ACCELERATOR && this.stateService.network === '';
}
);
@@ -748,6 +747,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.tx.acceleratedBy = cpfpInfo.acceleratedBy;
this.setIsAccelerated(firstCpfp);
}
if (!this.isAcceleration && this.fragmentParams.has('accelerate')) {
this.onAccelerateClicked();
}
this.txChanged$.next(true);
this.cpfpInfo = cpfpInfo;
@@ -885,14 +889,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
// simulate normal anchor fragment behavior
applyFragment(): void {
const anchor = Array.from(this.fragmentParams.entries()).find(([frag, value]) => value === '');
if (anchor?.length) {
if (anchor[0] === 'accelerate') {
setTimeout(this.onAccelerateClicked.bind(this), 100);
} else {
const anchorElement = document.getElementById(anchor[0]);
if (anchorElement) {
anchorElement.scrollIntoView();
}
if (anchor?.length && anchor[0] !== 'accelerate') {
const anchorElement = document.getElementById(anchor[0]);
if (anchorElement) {
anchorElement.scrollIntoView();
}
}
}

View File

@@ -9,6 +9,11 @@ import { AcceleratePreviewComponent } from '../accelerate-preview/accelerate-pre
import { AccelerateFeeGraphComponent } from '../accelerate-preview/accelerate-fee-graph.component';
const routes: Routes = [
{
path: '',
redirectTo: '/',
pathMatch: 'full',
},
{
path: ':id',
component: TransactionComponent,

View File

@@ -72,7 +72,7 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
this.auditEnabled = this.env.AUDIT;
this.network$ = merge(of(''), this.stateService.networkChanged$).pipe(
tap((network: string) => {
if (this.env.BASE_MODULE === 'mempool' && network !== '') {
if (this.env.BASE_MODULE === 'mempool' && network !== '' && this.env.ROOT_NETWORK === '') {
this.baseNetworkUrl = `/${network}`;
} else if (this.env.BASE_MODULE === 'liquid') {
if (!['', 'liquid'].includes(network)) {
@@ -195,6 +195,10 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
}
}
if (network === this.env.ROOT_NETWORK) {
curlNetwork = '';
}
let text = code.codeTemplate.curl;
for (let index = 0; index < curlResponse.length; index++) {
const curlText = curlResponse[index];

View File

@@ -284,7 +284,7 @@ yarn add @mempool/liquid.js`;
const headersString = code.headers ? ` -H "${code.headers}"` : ``;
if (this.env.BASE_MODULE === 'mempool') {
if (this.network === 'main' || this.network === '') {
if (this.network === 'main' || this.network === '' || this.network === this.env.ROOT_NETWORK) {
if (this.method === 'POST') {
return `curl${headersString} -X POST -sSLd "${text}"`;
}
@@ -296,7 +296,7 @@ yarn add @mempool/liquid.js`;
return `curl${headersString} -sSL "${this.hostname}/${this.network}${text}"`;
} else if (this.env.BASE_MODULE === 'liquid') {
if (this.method === 'POST') {
if (this.network !== 'liquid') {
if (this.network !== 'liquid' || this.network === this.env.ROOT_NETWORK) {
text = text.replace('/api', `/${this.network}/api`);
}
return `curl${headersString} -X POST -sSLd "${text}"`;

View File

@@ -60,10 +60,14 @@ const routes: Routes = [
]
},
{
path: 'acceleration/list',
path: 'acceleration/list/:page',
data: { networks: ['bitcoin'] },
component: AccelerationsListComponent,
},
{
path: 'acceleration/list',
redirectTo: 'acceleration/list/1',
},
{
path: 'mempool-block/:id',
data: { networks: ['bitcoin', 'liquid'] },

View File

@@ -84,10 +84,14 @@ const routes: Routes = [
]
},
{
path: 'audit/pegs',
path: 'audit/pegs/:page',
data: { networks: ['liquid'] },
component: RecentPegsListComponent,
},
{
path: 'audit/pegs',
redirectTo: 'audit/pegs/1'
},
{
path: 'assets',
data: { networks: ['liquid'] },

View File

@@ -45,9 +45,13 @@ const routes: Routes = [
loadChildren: () => import('./components/about/about.module').then(m => m.AboutModule),
},
{
path: 'blocks',
path: 'blocks/:page',
component: BlocksList,
},
{
path: 'blocks',
redirectTo: 'blocks/1',
},
{
path: 'rbf',
component: RbfList,

View File

@@ -3,8 +3,10 @@ import { AccelerationPosition, CpfpInfo, DifficultyAdjustment, MempoolPosition,
import { StateService } from './state.service';
import { MempoolBlock } from '../interfaces/websocket.interface';
import { Transaction } from '../interfaces/electrs.interface';
import { MiningStats } from './mining.service';
import { MiningService, MiningStats } from './mining.service';
import { getUnacceleratedFeeRate } from '../shared/transaction.utils';
import { AccelerationEstimate } from '../components/accelerate-preview/accelerate-preview.component';
import { Observable, combineLatest, map, of } from 'rxjs';
export interface ETA {
now: number, // time at which calculation performed
@@ -19,8 +21,50 @@ export interface ETA {
export class EtaService {
constructor(
private stateService: StateService,
private miningService: MiningService,
) { }
getProjectedEtaObservable(estimate: AccelerationEstimate, miningStats?: MiningStats): Observable<{ hashratePercentage: number, ETA: number, acceleratedETA: number }> {
return combineLatest([
this.stateService.mempoolTxPosition$.pipe(map(p => p?.position)),
this.stateService.difficultyAdjustment$,
miningStats ? of(miningStats) : this.miningService.getMiningStats('1w'),
]).pipe(
map(([mempoolPosition, da, miningStats]) => {
if (!mempoolPosition || !estimate?.pools?.length || !miningStats || !da) {
return {
hashratePercentage: undefined,
ETA: undefined,
acceleratedETA: undefined,
};
}
const pools: { [id: number]: SinglePoolStats } = {};
for (const pool of miningStats.pools) {
pools[pool.poolUniqueId] = pool;
}
let totalAcceleratedHashrate = 0;
for (const poolId of estimate.pools) {
const pool = pools[poolId];
if (!pool) {
continue;
}
totalAcceleratedHashrate += pool.lastEstimatedHashrate;
}
const acceleratingHashrateFraction = (totalAcceleratedHashrate / miningStats.lastEstimatedHashrate);
return {
hashratePercentage: acceleratingHashrateFraction * 100,
ETA: Date.now() + da.timeAvg * mempoolPosition.block,
acceleratedETA: this.calculateETAFromShares([
{ block: mempoolPosition.block, hashrateShare: (1 - acceleratingHashrateFraction) },
{ block: 0, hashrateShare: acceleratingHashrateFraction },
], da).time,
};
})
);
}
mempoolPositionFromFees(feerate: number, mempoolBlocks: MempoolBlock[]): MempoolPosition {
for (let txInBlockIndex = 0; txInBlockIndex < mempoolBlocks.length; txInBlockIndex++) {
const block = mempoolBlocks[txInBlockIndex];
@@ -41,7 +85,7 @@ export class EtaService {
return {
block: txInBlockIndex,
vsize: (1 - feePosition) * blockedFilledPercentage * this.stateService.blockVSize,
}
};
}
}
if (feerate >= block.feeRange[block.feeRange.length - 1]) {
@@ -49,14 +93,14 @@ export class EtaService {
return {
block: txInBlockIndex,
vsize: 0,
}
};
}
}
// at the very back of the last block
return {
block: mempoolBlocks.length - 1,
vsize: mempoolBlocks[mempoolBlocks.length - 1].blockVSize,
}
};
}
calculateETA(
@@ -88,7 +132,7 @@ export class EtaService {
time: now + (60_000 * (mempoolPosition.block + 1)),
wait: (60_000 * (mempoolPosition.block + 1)),
blocks: mempoolPosition.block + 1,
}
};
}
// difficulty adjustment estimate is required to know avg block time on non-Liquid networks
@@ -104,7 +148,7 @@ export class EtaService {
time: wait + now + da.timeOffset,
wait,
blocks,
}
};
} else {
// accelerated transactions
@@ -121,7 +165,7 @@ export class EtaService {
pools[pool.poolUniqueId] = pool;
}
const unacceleratedPosition = this.mempoolPositionFromFees(getUnacceleratedFeeRate(tx, true), mempoolBlocks);
let totalAcceleratedHashrate = accelerationPositions.reduce((total, pos) => total + (pools[pos.poolId].lastEstimatedHashrate), 0);
const totalAcceleratedHashrate = accelerationPositions.reduce((total, pos) => total + (pools[pos.poolId].lastEstimatedHashrate), 0);
const shares = [
{
block: unacceleratedPosition.block,
@@ -163,7 +207,7 @@ export class EtaService {
// find H_i
const H = shares.reduce((total, share) => total + (share.block <= i ? share.hashrateShare : 0), 0);
// find S_i
let S = H * (1 - tailProb);
const S = H * (1 - tailProb);
// accumulate sum (S_i x i)
Q += (S * (i + 1));
// accumulate sum (S_j)
@@ -178,6 +222,6 @@ export class EtaService {
time: eta + now + da.timeOffset,
wait: eta,
blocks: Math.ceil(eta / da.adjustedTimeAvg),
}
};
}
}

View File

@@ -150,7 +150,7 @@ export class StateService {
utxoSpent$ = new Subject<object>();
difficultyAdjustment$ = new ReplaySubject<DifficultyAdjustment>(1);
mempoolTransactions$ = new Subject<Transaction>();
mempoolTxPosition$ = new Subject<{ txid: string, position: MempoolPosition, cpfp: CpfpInfo | null, accelerationPositions?: AccelerationPosition[] }>();
mempoolTxPosition$ = new BehaviorSubject<{ txid: string, position: MempoolPosition, cpfp: CpfpInfo | null, accelerationPositions?: AccelerationPosition[] }>(null);
mempoolRemovedTransactions$ = new Subject<Transaction>();
multiAddressTransactions$ = new Subject<{ [address: string]: { mempool: Transaction[], confirmed: Transaction[], removed: Transaction[] }}>();
blockTransactions$ = new Subject<Transaction>();

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,7 @@
"express": "^4.19.2",
"ejs": "^3.1.10",
"node-fetch-commonjs": "^3.3.1",
"puppeteer": "^22.12.0",
"puppeteer": "^15.3.2",
"puppeteer-cluster": "^0.23.0",
"typescript": "~4.7.4"
},