diff --git a/frontend/src/app/bitcoin.utils.ts b/frontend/src/app/bitcoin.utils.ts new file mode 100644 index 000000000..b023ac2b1 --- /dev/null +++ b/frontend/src/app/bitcoin.utils.ts @@ -0,0 +1,68 @@ +import { Transaction, Vin } from './interfaces/electrs.interface'; + +const P2SH_P2WPKH_COST = 21 * 4; // the WU cost for the non-witness part of P2SH-P2WPKH +const P2SH_P2WSH_COST = 35 * 4; // the WU cost for the non-witness part of P2SH-P2WSH + +export function calcSegwitFeeGains(tx: Transaction) { + // calculated in weight units + let realizedGains = 0; + let potentialBech32Gains = 0; + let potentialP2shGains = 0; + + for (const vin of tx.vin) { + if (!vin.prevout) { continue; } + + const isP2pkh = vin.prevout.scriptpubkey_type === 'p2pkh'; + const isP2sh = vin.prevout.scriptpubkey_type === 'p2sh'; + const isP2wsh = vin.prevout.scriptpubkey_type === 'v0_p2wsh'; + const isP2wpkh = vin.prevout.scriptpubkey_type === 'v0_p2wpkh'; + + const op = vin.scriptsig ? vin.scriptsig_asm.split(' ')[0] : null; + const isP2sh2Wpkh = isP2sh && !!vin.witness && op === 'OP_PUSHBYTES_22'; + const isP2sh2Wsh = isP2sh && !!vin.witness && op === 'OP_PUSHBYTES_34'; + + switch (true) { + // Native Segwit - P2WPKH/P2WSH (Bech32) + case isP2wpkh: + case isP2wsh: + // maximal gains: the scriptSig is moved entirely to the witness part + realizedGains += witnessSize(vin) * 3; + // XXX P2WSH output creation is more expensive, should we take this into consideration? + break; + + // Backward compatible Segwit - P2SH-P2WPKH + case isP2sh2Wpkh: + // the scriptSig is moved to the witness, but we have extra 21 extra non-witness bytes (48 WU) + realizedGains += witnessSize(vin) * 3 - P2SH_P2WPKH_COST; + potentialBech32Gains += P2SH_P2WPKH_COST; + break; + + // Backward compatible Segwit - P2SH-P2WSH + case isP2sh2Wsh: + // the scriptSig is moved to the witness, but we have extra 35 extra non-witness bytes + realizedGains += witnessSize(vin) * 3 - P2SH_P2WSH_COST; + potentialBech32Gains += P2SH_P2WSH_COST; + break; + + // Non-segwit P2PKH/P2SH + case isP2pkh: + case isP2sh: + const fullGains = scriptSigSize(vin) * 3; + potentialBech32Gains += fullGains; + potentialP2shGains += fullGains - (isP2pkh ? P2SH_P2WPKH_COST : P2SH_P2WSH_COST); + break; + + // TODO: should we also consider P2PK and pay-to-bare-script (non-p2sh-wrapped) as upgradable to P2WPKH and P2WSH? + } + } + + // returned as percentage of the total tx weight + return { realizedGains: realizedGains / (tx.weight + realizedGains) // percent of the pre-segwit tx size + , potentialBech32Gains: potentialBech32Gains / tx.weight + , potentialP2shGains: potentialP2shGains / tx.weight + }; +} + +// Utilities for segwitFeeGains +const witnessSize = (vin: Vin) => vin.witness.reduce((S, w) => S + (w.length / 2), 0); +const scriptSigSize = (vin: Vin) => vin.scriptsig ? vin.scriptsig.length / 2 : 0; diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 06d858fd1..17846964d 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -43,6 +43,7 @@ After + @@ -115,6 +116,7 @@ + @@ -250,3 +252,15 @@
+ + + + Features + + SegWit + SegWit + SegWit + RBF + + + \ No newline at end of file diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index c50d85f65..7b0bfd2b6 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -9,6 +9,7 @@ import { WebsocketService } from '../../services/websocket.service'; import { AudioService } from 'src/app/services/audio.service'; import { ApiService } from 'src/app/services/api.service'; import { SeoService } from 'src/app/services/seo.service'; +import { calcSegwitFeeGains } from 'src/app/bitcoin.utils'; @Component({ selector: 'app-transaction', @@ -29,6 +30,12 @@ export class TransactionComponent implements OnInit, OnDestroy { latestBlock: Block; transactionTime = -1; subscription: Subscription; + segwitGains = { + realizedGains: 0, + potentialBech32Gains: 0, + potentialP2shGains: 0, + }; + isRbfTransaction: boolean; constructor( private route: ActivatedRoute, @@ -79,6 +86,8 @@ export class TransactionComponent implements OnInit, OnDestroy { this.error = undefined; this.waitingForTransaction = false; this.setMempoolBlocksSubscription(); + this.segwitGains = calcSegwitFeeGains(tx); + this.isRbfTransaction = tx.vin.some((v) => v.sequence < 0xfffffffe); if (!tx.status.confirmed) { this.websocketService.startTrackTransaction(tx.txid); diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index ee36f2a39..a72bafa8a 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -9,6 +9,7 @@ $nav-tabs-link-active-bg: #11131f; $primary: #105fb0; $secondary: #2d3348; +$success: #1a9436; $link-color: #1bd8f4; $link-decoration: none !default;