From b822cefa4291d1c6195b18f909b4c2ee17f22341 Mon Sep 17 00:00:00 2001 From: Brandon Lucas <38222767+thebrandonlucas@users.noreply.github.com> Date: Thu, 18 Jul 2024 07:48:20 -0400 Subject: [PATCH 1/8] add regular cost calculations --- package.json | 1 + src/pages/savings-calculator.tsx | 113 +++++++++++++++++ src/utils/tx.js | 200 +++++++++++++++++++++++++++++++ yarn.lock | 20 ++++ 4 files changed, 334 insertions(+) create mode 100644 src/pages/savings-calculator.tsx create mode 100644 src/utils/tx.js diff --git a/package.json b/package.json index 30a0ec8..d770713 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@docusaurus/preset-classic": "^3.4.0", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", + "echarts": "^5.5.1", "prism-react-renderer": "^2.3.0", "react": "^18.0.0", "react-dom": "^18.0.0", diff --git a/src/pages/savings-calculator.tsx b/src/pages/savings-calculator.tsx new file mode 100644 index 0000000..00c89c2 --- /dev/null +++ b/src/pages/savings-calculator.tsx @@ -0,0 +1,113 @@ +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; +import Layout from "@theme/Layout"; +import { processForm, } from "../utils/tx"; + +export default function SavingsCalculator(): JSX.Element { + const { siteConfig } = useDocusaurusContext(); + return ( + +
+ Payjoin provides a unique opportunity for receiver-side savings. +
+
+
+
+
+

This calculator will give you the upper bound of the size of a transaction with specific characteristics. The size could be a few bytes smaller due to signature randomness, but it's unpredictable and you should err on the side of caution to avoid creating transactions that don't get relayed due to paying fees below the standard minimum rate.

+

If you want to understand how the calculation gets broken down, check out the explanation on the Bitcoin Optech Calculator

+
+
+
+ +
+
+

What are the attributes of the bitcoin transaction?

+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+


+ Transaction size in raw bytes:
+ Transaction size in virtual bytes:
+ Transaction size in weight units:

+

Which size should you use for calculating fee estimates?
+ Estimates should be in satoshis per virtual byte.

+
+
+
+
+
+ ); +} diff --git a/src/utils/tx.js b/src/utils/tx.js new file mode 100644 index 0000000..5ec8c0e --- /dev/null +++ b/src/utils/tx.js @@ -0,0 +1,200 @@ +const P2PKH_IN_SIZE = 148; +const P2PKH_OUT_SIZE = 34; + +const P2SH_OUT_SIZE = 32; +const P2SH_P2WPKH_OUT_SIZE = 32; +const P2SH_P2WSH_OUT_SIZE = 32; + +// All segwit input sizes are reduced by 1 WU to account for the witness item counts being added for every input per the transaction header +const P2SH_P2WPKH_IN_SIZE = 90.75; + +const P2WPKH_IN_SIZE = 67.75; +const P2WPKH_OUT_SIZE = 31; + +const P2WSH_OUT_SIZE = 43 +const P2TR_OUT_SIZE = 43; + +const P2TR_IN_SIZE = 57.25; + +const PUBKEY_SIZE = 33; +const SIGNATURE_SIZE = 72; + +function getSizeOfScriptLengthElement(length) { + if (length < 75) { + return 1; + } else if (length <= 255) { + return 2; + } else if (length <= 65535) { + return 3; + } else if (length <= 4294967295) { + return 5; + } else { + alert('Size of redeem script is too large'); + } +} + +function getSizeOfVarInt(length) { + if (length < 253) { + return 1; + } else if (length < 65535) { + return 3; + } else if (length < 4294967295) { + return 5; + } else if (length < 18446744073709551615) { + return 9; + } else { + alert("Invalid var int"); + } +} + +function getWitnessVbytes(input_script, input_count) { + if (input_script == "P2PKH" || input_script == "P2SH") { + var witness_vbytes = 0; + } else { // Transactions with segwit inputs have extra overhead + var witness_vbytes = 0.25 // segwit marker + + 0.25 // segwit flag + + input_count / 4; // witness element count per input + } + + return witness_vbytes; +} + +function getTxOverheadVBytes(input_script, input_count, output_count) { + return 4 // nVersion + + getSizeOfVarInt(input_count) // number of inputs + + getSizeOfVarInt(output_count) // number of outputs + + 4 // nLockTime + + getWitnessVbytes(input_script, input_count); +} + +function getTxOverheadExtraRawBytes(input_script, input_count) { + // Returns the remaining 3/4 bytes per witness bytes + return getWitnessVbytes(input_script, input_count) * 3; +} + +function getRedeemScriptSize(input_n) { + return 1 + // OP_M + input_n*(1 + PUBKEY_SIZE) + // OP_PUSH33 + 1 + // OP_N + 1; // OP_CHECKMULTISIG +} + +function getScriptSignatureSize(input_m, redeemScriptSize) { + return 1 + // size(0) + input_m * (1 + SIGNATURE_SIZE) + // size(SIGNATURE_SIZE) + signature + getSizeOfScriptLengthElement(redeemScriptSize) + redeemScriptSize; +} + +export function processForm() { + // Validate transaction input attributes + var input_count = parseInt(document.getElementById('input_count').value); + if (!Number.isInteger(input_count) || input_count < 0) { + alert('expecting positive input count, got: ' + input_count); + return; + } + var input_script = document.getElementById('input_script').value; + var input_m = parseInt(document.getElementById('input_m').value); + if (!Number.isInteger(input_m) || input_m < 0) { + alert('expecting positive signature count'); + return; + } + var input_n = parseInt(document.getElementById('input_n').value); + if (!Number.isInteger(input_n) || input_n < 0) { + alert('expecting positive pubkey count'); + return; + } + + // Validate transaction output attributes + var p2pkh_output_count = parseInt(document.getElementById('p2pkh_output_count').value); + if (!Number.isInteger(p2pkh_output_count) || p2pkh_output_count < 0) { + alert('expecting positive p2pkh output count'); + return; + } + var p2sh_output_count = parseInt(document.getElementById('p2sh_output_count').value); + if (!Number.isInteger(p2sh_output_count) || p2sh_output_count < 0) { + alert('expecting positive p2sh output count'); + return; + } + var p2sh_p2wpkh_output_count = parseInt(document.getElementById('p2sh_p2wpkh_output_count').value); + if (!Number.isInteger(p2sh_p2wpkh_output_count) || p2sh_p2wpkh_output_count < 0) { + alert('expecting positive p2sh-p2wpkh output count'); + return; + } + var p2sh_p2wsh_output_count = parseInt(document.getElementById('p2sh_p2wsh_output_count').value); + if (!Number.isInteger(p2sh_p2wsh_output_count) || p2sh_p2wsh_output_count < 0) { + alert('expecting positive p2sh-p2wsh output count'); + return; + } + var p2wpkh_output_count = parseInt(document.getElementById('p2wpkh_output_count').value); + if (!Number.isInteger(p2wpkh_output_count) || p2wpkh_output_count < 0) { + alert('expecting positive p2wpkh output count'); + return; + } + var p2wsh_output_count = parseInt(document.getElementById('p2wsh_output_count').value); + if (!Number.isInteger(p2wsh_output_count) || p2wsh_output_count < 0) { + alert('expecting positive p2wsh output count'); + return; + } + var p2tr_output_count = parseInt(document.getElementById('p2tr_output_count').value); + if (!Number.isInteger(p2tr_output_count) || p2tr_output_count < 0) { + alert('expecting positive p2tr output count'); + return; + } + + const output_count = p2pkh_output_count + p2sh_output_count + p2sh_p2wpkh_output_count + + p2sh_p2wsh_output_count + p2wpkh_output_count + p2wsh_output_count + p2tr_output_count; + + // In most cases the input size is predictable. For multisig inputs we need to perform a detailed calculation + var inputSize = 0; // in virtual bytes + var inputWitnessSize = 0; + switch (input_script) { + case "P2PKH": + inputSize = P2PKH_IN_SIZE; + break; + case "P2SH-P2WPKH": + inputSize = P2SH_P2WPKH_IN_SIZE; + inputWitnessSize = 107; // size(signature) + signature + size(pubkey) + pubkey + break; + case "P2WPKH": + inputSize = P2WPKH_IN_SIZE; + inputWitnessSize = 107; // size(signature) + signature + size(pubkey) + pubkey + break; + case "P2TR": // Only consider the cooperative taproot signing path; assume multisig is done via aggregate signatures + inputSize = P2TR_IN_SIZE; + inputWitnessSize = 65; // getSizeOfVarInt(schnorrSignature) + schnorrSignature; + break; + case "P2SH": + var redeemScriptSize = getRedeemScriptSize(input_n); + var scriptSigSize = getScriptSignatureSize(input_m, redeemScriptSize); + inputSize = 32 + 4 + getSizeOfVarInt(scriptSigSize) + scriptSigSize + 4; + break; + case "P2SH-P2WSH": + case "P2WSH": + var redeemScriptSize = getRedeemScriptSize(input_n); + inputWitnessSize = getScriptSignatureSize(input_m, redeemScriptSize); + inputSize = 36 + // outpoint (spent UTXO ID) + inputWitnessSize / 4 + // witness program + 4; // nSequence + if (input_script == "P2SH-P2WSH") { + inputSize += 32 + 3; // P2SH wrapper (redeemscript hash) + overhead? + } + } + + var txVBytes = getTxOverheadVBytes(input_script, input_count, output_count) + + inputSize * input_count + + P2PKH_OUT_SIZE * p2pkh_output_count + + P2SH_OUT_SIZE * p2sh_output_count + + P2SH_P2WPKH_OUT_SIZE * p2sh_p2wpkh_output_count + + P2SH_P2WSH_OUT_SIZE * p2sh_p2wsh_output_count + + P2WPKH_OUT_SIZE * p2wpkh_output_count + + P2WSH_OUT_SIZE * p2wsh_output_count + + P2TR_OUT_SIZE * p2tr_output_count; + txVBytes = Math.ceil(txVBytes); + + var txBytes = getTxOverheadExtraRawBytes(input_script, input_count) + txVBytes + (inputWitnessSize * input_count) * 3 / 4; + var txWeight = txVBytes * 4; + + document.getElementById('txBytes').innerHTML = txBytes; + document.getElementById('txVBytes').innerHTML = txVBytes; + document.getElementById('txWeight').innerHTML = txWeight; +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 8705bf3..6c43d05 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4632,6 +4632,14 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +echarts@^5.5.1: + version "5.5.1" + resolved "https://registry.yarnpkg.com/echarts/-/echarts-5.5.1.tgz#8dc9c68d0c548934bedcb5f633db07ed1dd2101c" + integrity sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA== + dependencies: + tslib "2.3.0" + zrender "5.6.0" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -9072,6 +9080,11 @@ ts-interface-checker@^0.1.9: resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== +tslib@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" + integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== + tslib@^2.0.3, tslib@^2.6.0: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" @@ -9592,6 +9605,13 @@ yocto-queue@^1.0.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== +zrender@5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/zrender/-/zrender-5.6.0.tgz#01325b0bb38332dd5e87a8dbee7336cafc0f4a5b" + integrity sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg== + dependencies: + tslib "2.3.0" + zwitch@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" From 19765ba61e8ec0cc37fac1c23b6ebb845f899b7c Mon Sep 17 00:00:00 2001 From: Brandon Lucas <38222767+thebrandonlucas@users.noreply.github.com> Date: Thu, 18 Jul 2024 20:15:34 -0400 Subject: [PATCH 2/8] modify tx file to ts --- src/utils/tx.js | 200 ------------------------------------------------ src/utils/tx.ts | 102 ++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 200 deletions(-) delete mode 100644 src/utils/tx.js create mode 100644 src/utils/tx.ts diff --git a/src/utils/tx.js b/src/utils/tx.js deleted file mode 100644 index 5ec8c0e..0000000 --- a/src/utils/tx.js +++ /dev/null @@ -1,200 +0,0 @@ -const P2PKH_IN_SIZE = 148; -const P2PKH_OUT_SIZE = 34; - -const P2SH_OUT_SIZE = 32; -const P2SH_P2WPKH_OUT_SIZE = 32; -const P2SH_P2WSH_OUT_SIZE = 32; - -// All segwit input sizes are reduced by 1 WU to account for the witness item counts being added for every input per the transaction header -const P2SH_P2WPKH_IN_SIZE = 90.75; - -const P2WPKH_IN_SIZE = 67.75; -const P2WPKH_OUT_SIZE = 31; - -const P2WSH_OUT_SIZE = 43 -const P2TR_OUT_SIZE = 43; - -const P2TR_IN_SIZE = 57.25; - -const PUBKEY_SIZE = 33; -const SIGNATURE_SIZE = 72; - -function getSizeOfScriptLengthElement(length) { - if (length < 75) { - return 1; - } else if (length <= 255) { - return 2; - } else if (length <= 65535) { - return 3; - } else if (length <= 4294967295) { - return 5; - } else { - alert('Size of redeem script is too large'); - } -} - -function getSizeOfVarInt(length) { - if (length < 253) { - return 1; - } else if (length < 65535) { - return 3; - } else if (length < 4294967295) { - return 5; - } else if (length < 18446744073709551615) { - return 9; - } else { - alert("Invalid var int"); - } -} - -function getWitnessVbytes(input_script, input_count) { - if (input_script == "P2PKH" || input_script == "P2SH") { - var witness_vbytes = 0; - } else { // Transactions with segwit inputs have extra overhead - var witness_vbytes = 0.25 // segwit marker - + 0.25 // segwit flag - + input_count / 4; // witness element count per input - } - - return witness_vbytes; -} - -function getTxOverheadVBytes(input_script, input_count, output_count) { - return 4 // nVersion - + getSizeOfVarInt(input_count) // number of inputs - + getSizeOfVarInt(output_count) // number of outputs - + 4 // nLockTime - + getWitnessVbytes(input_script, input_count); -} - -function getTxOverheadExtraRawBytes(input_script, input_count) { - // Returns the remaining 3/4 bytes per witness bytes - return getWitnessVbytes(input_script, input_count) * 3; -} - -function getRedeemScriptSize(input_n) { - return 1 + // OP_M - input_n*(1 + PUBKEY_SIZE) + // OP_PUSH33 - 1 + // OP_N - 1; // OP_CHECKMULTISIG -} - -function getScriptSignatureSize(input_m, redeemScriptSize) { - return 1 + // size(0) - input_m * (1 + SIGNATURE_SIZE) + // size(SIGNATURE_SIZE) + signature - getSizeOfScriptLengthElement(redeemScriptSize) + redeemScriptSize; -} - -export function processForm() { - // Validate transaction input attributes - var input_count = parseInt(document.getElementById('input_count').value); - if (!Number.isInteger(input_count) || input_count < 0) { - alert('expecting positive input count, got: ' + input_count); - return; - } - var input_script = document.getElementById('input_script').value; - var input_m = parseInt(document.getElementById('input_m').value); - if (!Number.isInteger(input_m) || input_m < 0) { - alert('expecting positive signature count'); - return; - } - var input_n = parseInt(document.getElementById('input_n').value); - if (!Number.isInteger(input_n) || input_n < 0) { - alert('expecting positive pubkey count'); - return; - } - - // Validate transaction output attributes - var p2pkh_output_count = parseInt(document.getElementById('p2pkh_output_count').value); - if (!Number.isInteger(p2pkh_output_count) || p2pkh_output_count < 0) { - alert('expecting positive p2pkh output count'); - return; - } - var p2sh_output_count = parseInt(document.getElementById('p2sh_output_count').value); - if (!Number.isInteger(p2sh_output_count) || p2sh_output_count < 0) { - alert('expecting positive p2sh output count'); - return; - } - var p2sh_p2wpkh_output_count = parseInt(document.getElementById('p2sh_p2wpkh_output_count').value); - if (!Number.isInteger(p2sh_p2wpkh_output_count) || p2sh_p2wpkh_output_count < 0) { - alert('expecting positive p2sh-p2wpkh output count'); - return; - } - var p2sh_p2wsh_output_count = parseInt(document.getElementById('p2sh_p2wsh_output_count').value); - if (!Number.isInteger(p2sh_p2wsh_output_count) || p2sh_p2wsh_output_count < 0) { - alert('expecting positive p2sh-p2wsh output count'); - return; - } - var p2wpkh_output_count = parseInt(document.getElementById('p2wpkh_output_count').value); - if (!Number.isInteger(p2wpkh_output_count) || p2wpkh_output_count < 0) { - alert('expecting positive p2wpkh output count'); - return; - } - var p2wsh_output_count = parseInt(document.getElementById('p2wsh_output_count').value); - if (!Number.isInteger(p2wsh_output_count) || p2wsh_output_count < 0) { - alert('expecting positive p2wsh output count'); - return; - } - var p2tr_output_count = parseInt(document.getElementById('p2tr_output_count').value); - if (!Number.isInteger(p2tr_output_count) || p2tr_output_count < 0) { - alert('expecting positive p2tr output count'); - return; - } - - const output_count = p2pkh_output_count + p2sh_output_count + p2sh_p2wpkh_output_count - + p2sh_p2wsh_output_count + p2wpkh_output_count + p2wsh_output_count + p2tr_output_count; - - // In most cases the input size is predictable. For multisig inputs we need to perform a detailed calculation - var inputSize = 0; // in virtual bytes - var inputWitnessSize = 0; - switch (input_script) { - case "P2PKH": - inputSize = P2PKH_IN_SIZE; - break; - case "P2SH-P2WPKH": - inputSize = P2SH_P2WPKH_IN_SIZE; - inputWitnessSize = 107; // size(signature) + signature + size(pubkey) + pubkey - break; - case "P2WPKH": - inputSize = P2WPKH_IN_SIZE; - inputWitnessSize = 107; // size(signature) + signature + size(pubkey) + pubkey - break; - case "P2TR": // Only consider the cooperative taproot signing path; assume multisig is done via aggregate signatures - inputSize = P2TR_IN_SIZE; - inputWitnessSize = 65; // getSizeOfVarInt(schnorrSignature) + schnorrSignature; - break; - case "P2SH": - var redeemScriptSize = getRedeemScriptSize(input_n); - var scriptSigSize = getScriptSignatureSize(input_m, redeemScriptSize); - inputSize = 32 + 4 + getSizeOfVarInt(scriptSigSize) + scriptSigSize + 4; - break; - case "P2SH-P2WSH": - case "P2WSH": - var redeemScriptSize = getRedeemScriptSize(input_n); - inputWitnessSize = getScriptSignatureSize(input_m, redeemScriptSize); - inputSize = 36 + // outpoint (spent UTXO ID) - inputWitnessSize / 4 + // witness program - 4; // nSequence - if (input_script == "P2SH-P2WSH") { - inputSize += 32 + 3; // P2SH wrapper (redeemscript hash) + overhead? - } - } - - var txVBytes = getTxOverheadVBytes(input_script, input_count, output_count) + - inputSize * input_count + - P2PKH_OUT_SIZE * p2pkh_output_count + - P2SH_OUT_SIZE * p2sh_output_count + - P2SH_P2WPKH_OUT_SIZE * p2sh_p2wpkh_output_count + - P2SH_P2WSH_OUT_SIZE * p2sh_p2wsh_output_count + - P2WPKH_OUT_SIZE * p2wpkh_output_count + - P2WSH_OUT_SIZE * p2wsh_output_count + - P2TR_OUT_SIZE * p2tr_output_count; - txVBytes = Math.ceil(txVBytes); - - var txBytes = getTxOverheadExtraRawBytes(input_script, input_count) + txVBytes + (inputWitnessSize * input_count) * 3 / 4; - var txWeight = txVBytes * 4; - - document.getElementById('txBytes').innerHTML = txBytes; - document.getElementById('txVBytes').innerHTML = txVBytes; - document.getElementById('txWeight').innerHTML = txWeight; -} \ No newline at end of file diff --git a/src/utils/tx.ts b/src/utils/tx.ts new file mode 100644 index 0000000..e6949d0 --- /dev/null +++ b/src/utils/tx.ts @@ -0,0 +1,102 @@ +const P2PKH_IN_SIZE = 148; +const P2PKH_OUT_SIZE = 34; +const P2WPKH_IN_SIZE = 67.75; +const P2WPKH_OUT_SIZE = 31; +const P2TR_OUT_SIZE = 43; +const P2TR_IN_SIZE = 57.25; + +export enum ScriptType { + P2PKH = "P2PKH", + P2WPKH = "P2WPKH", + P2TR = "P2TR", +} + +function getSizeOfVarInt(length: number) { + if (length < 253) { + return 1; + } else if (length < 65535) { + return 3; + } else if (length < 4294967295) { + return 5; + } else if (length < 18446744073709551615) { + return 9; + } else { + alert("Invalid var int"); + } +} + +function getWitnessVbytes(inputScript: ScriptType, inputCount: number) { + let witnessVBytes = 0; + if (inputScript !== ScriptType.P2PKH) { + // Transactions with segwit inputs have extra overhead + witnessVBytes = 0.25 // segwit marker + + 0.25 // segwit flag + + inputCount / 4; // witness element count per input + } + return witnessVBytes; +} + +function getTxOverheadVBytes(inputScript: ScriptType, inputCount: number, outputCount: number, recipientCount: number) { + return 4 // nVersion + + getSizeOfVarInt(inputCount) // number of inputs + + getSizeOfVarInt(outputCount) // number of outputs + + 4 // nLockTime + + getWitnessVbytes(inputScript, inputCount); +} + +// function getTxOverheadExtraRawBytes(inputScript, inputCount) { +// // Returns the remaining 3/4 bytes per witness bytes +// return getWitnessVbytes(inputScript, inputCount) * 3; +// } + +function calculateTxVBytesNoBatching(inputScript: ScriptType, inputCount: number, outputCount: number, recipientCount: number, inputSize: number, inputWitnessSize: number) { + // per tx cost without batching: r𝑏 + r𝑖 + 2r𝑜 + 1 + const b = getTxOverheadVBytes(inputScript, inputCount, outputCount, recipientCount); + const i = inputCount * getTxOverheadVBytes(inputScript, 1, 0, 1); + const o = outputCount * getTxOverheadVBytes(inputScript, 0, 1, 1); + const r = recipientCount; + const txVBytes = r * b + r * i + 2 * r * o + 1; + return Math.ceil(txVBytes); +} + +function calculateTxVBytesBatching(inputScript: ScriptType, inputCount: number, outputCount: number, recipientCount: number, inputSize: number, inputWitnessSize: number) { + // total tx cost with batching: 𝑏 + 𝑖 + r𝑜 + 1 + const b = getTxOverheadVBytes(inputScript, inputCount, outputCount, recipientCount); + const i = inputCount * getTxOverheadVBytes(inputScript, inputCount, 0, 1) + inputSize + inputWitnessSize; + const o = outputCount * getTxOverheadVBytes(inputScript, 0, 1, 1); + const r = recipientCount; + const txVBytes = b + i + r * o + 1; + return Math.ceil(txVBytes); +} + +export function processForm() { + const scriptType = (document.getElementById('input_script') as HTMLInputElement).value as ScriptType; // assuming both inputs and outputs are of same script type + const inputCount = parseInt((document.getElementById('input_count') as HTMLInputElement).value); + const outputCount = parseInt((document.getElementById('output_count') as HTMLInputElement).value); + const recipientCount = parseInt((document.getElementById('recipient_count') as HTMLInputElement).value); + + // In most cases the input size is predictable. For multisig inputs we need to perform a detailed calculation + let inputSize = 0; // in virtual bytes + let inputWitnessSize = 0; + switch (scriptType) { + case ScriptType.P2PKH: + inputSize = P2PKH_IN_SIZE; + break; + case ScriptType.P2WPKH: + inputSize = P2WPKH_IN_SIZE; + inputWitnessSize = 107; // size(signature) + signature + size(pubkey) + pubkey + break; + case ScriptType.P2TR: // Only consider the cooperative taproot signing path; assume multisig is done via aggregate signatures + inputSize = P2TR_IN_SIZE; + inputWitnessSize = 65; // getSizeOfVarInt(schnorrSignature) + schnorrSignature; + break; + } + + const txVBytes = calculateTxVBytesNoBatching(scriptType, inputCount, outputCount, recipientCount, inputSize, inputWitnessSize); + + // const txBytes = getTxOverheadExtraRawBytes(inputScript, inputCount) + txVBytes + (inputWitnessSize * inputCount) * 3 / 4; + // const txWeight = txVBytes * 4; + // document.getElementById('txBytes').innerHTML = txBytes; + document.getElementById('txVBytes').innerHTML = String(txVBytes); + // document.getElementById('txWeight').innerHTML = txWeight; +} \ No newline at end of file From 4c44528d1ab866a1f77fbdf8e8f61f9e471899e8 Mon Sep 17 00:00:00 2001 From: Brandon Lucas <38222767+thebrandonlucas@users.noreply.github.com> Date: Thu, 18 Jul 2024 20:15:59 -0400 Subject: [PATCH 3/8] use proper react-style forms --- src/pages/savings-calculator.tsx | 120 ++++++++++--------------------- 1 file changed, 38 insertions(+), 82 deletions(-) diff --git a/src/pages/savings-calculator.tsx b/src/pages/savings-calculator.tsx index 00c89c2..478aa49 100644 --- a/src/pages/savings-calculator.tsx +++ b/src/pages/savings-calculator.tsx @@ -1,9 +1,21 @@ import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; import Layout from "@theme/Layout"; -import { processForm, } from "../utils/tx"; +import { processForm, ScriptType, } from "../utils/tx"; +import { useState } from "react"; export default function SavingsCalculator(): JSX.Element { const { siteConfig } = useDocusaurusContext(); + const [inputScript, setInputScript] = useState(ScriptType.P2WPKH); + const [inputCount, setInputCount] = useState(1); + const [outputCount, setOutputCount] = useState(1); + const [recipientCount, setRecipientCount] = useState(1); + + const scriptTypes = [ + { value: ScriptType.P2PKH, label: "P2PKH" }, + { value: ScriptType.P2WPKH, label: "P2WPKH" }, + { value: ScriptType.P2TR, label: "P2TR"} + ] + return ( Payjoin provides a unique opportunity for receiver-side savings.
-
-
-
-

This calculator will give you the upper bound of the size of a transaction with specific characteristics. The size could be a few bytes smaller due to signature randomness, but it's unpredictable and you should err on the side of caution to avoid creating transactions that don't get relayed due to paying fees below the standard minimum rate.

-

If you want to understand how the calculation gets broken down, check out the explanation on the Bitcoin Optech Calculator

-
-
-
- -
-
-

What are the attributes of the bitcoin transaction?

- -
- -
-
-
- -
- setInputScript(e.target.value as ScriptType)} value={inputScript}> + {scriptTypes.map((scriptType) => ( + + ))}
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- +
+ +
+ setInputCount(parseInt(e.target.value))} />
-
- -
- +
+ +
+ setOutputCount(parseInt(e.target.value))} />
-
- -
- +
+ +
+ setRecipientCount(parseInt(e.target.value))} />
-


- Transaction size in raw bytes:
+


+ {/* Transaction size in raw bytes:
*/} Transaction size in virtual bytes:
- Transaction size in weight units:

-

Which size should you use for calculating fee estimates?
- Estimates should be in satoshis per virtual byte.

+ {/* Transaction size in weight units:

*/} + {/*

Which size should you use for calculating fee estimates?
+ Estimates should be in satoshis per virtual byte.

*/}
From 8479e1ec779f6303e3ce0fb6a82a7fa1f78e7343 Mon Sep 17 00:00:00 2001 From: Brandon Lucas <38222767+thebrandonlucas@users.noreply.github.com> Date: Thu, 18 Jul 2024 20:38:28 -0400 Subject: [PATCH 4/8] form validation --- src/pages/savings-calculator.tsx | 25 +++++++++++++++++++++---- src/utils/tx.ts | 7 +------ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/pages/savings-calculator.tsx b/src/pages/savings-calculator.tsx index 478aa49..0c93348 100644 --- a/src/pages/savings-calculator.tsx +++ b/src/pages/savings-calculator.tsx @@ -1,14 +1,15 @@ import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; import Layout from "@theme/Layout"; import { processForm, ScriptType, } from "../utils/tx"; -import { useState } from "react"; +import { useEffect, useState } from "react"; export default function SavingsCalculator(): JSX.Element { const { siteConfig } = useDocusaurusContext(); - const [inputScript, setInputScript] = useState(ScriptType.P2WPKH); + const [inputScript, setInputScript] = useState(ScriptType.P2WPKH); // we assume both inputs and outputs are of same script type const [inputCount, setInputCount] = useState(1); const [outputCount, setOutputCount] = useState(1); const [recipientCount, setRecipientCount] = useState(1); + const [isDisabled, setIsDisabled] = useState(true); const scriptTypes = [ { value: ScriptType.P2PKH, label: "P2PKH" }, @@ -16,6 +17,22 @@ export default function SavingsCalculator(): JSX.Element { { value: ScriptType.P2TR, label: "P2TR"} ] + function isInvalid() { + return [inputCount, outputCount, recipientCount].some((value) => isNaN(value) || value < 1); + } + + function handleSubmit() { + if (isInvalid()) { + alert("Please enter a valid number greater than 0."); + return; + } + processForm(inputScript, inputCount, outputCount, recipientCount); + } + + useEffect(() => { + setIsDisabled(isInvalid()); + }, [inputScript, inputCount, outputCount, recipientCount]); + return ( setRecipientCount(parseInt(e.target.value))} />
-


+


{/* Transaction size in raw bytes:
*/} - Transaction size in virtual bytes:
+ Transaction size in virtual bytes: {txVBytes}
{/* Transaction size in weight units:

*/} {/*

Which size should you use for calculating fee estimates?
Estimates should be in satoshis per virtual byte.

*/} diff --git a/src/utils/tx.ts b/src/utils/tx.ts index e6949d0..b161d5a 100644 --- a/src/utils/tx.ts +++ b/src/utils/tx.ts @@ -69,12 +69,7 @@ function calculateTxVBytesBatching(inputScript: ScriptType, inputCount: number, return Math.ceil(txVBytes); } -export function processForm() { - const scriptType = (document.getElementById('input_script') as HTMLInputElement).value as ScriptType; // assuming both inputs and outputs are of same script type - const inputCount = parseInt((document.getElementById('input_count') as HTMLInputElement).value); - const outputCount = parseInt((document.getElementById('output_count') as HTMLInputElement).value); - const recipientCount = parseInt((document.getElementById('recipient_count') as HTMLInputElement).value); - +export function processForm(scriptType: ScriptType, inputCount: number, outputCount: number, recipientCount: number) { // In most cases the input size is predictable. For multisig inputs we need to perform a detailed calculation let inputSize = 0; // in virtual bytes let inputWitnessSize = 0; From df0629d9a0c4d251012505d7690eac6972c52a82 Mon Sep 17 00:00:00 2001 From: Brandon Lucas <38222767+thebrandonlucas@users.noreply.github.com> Date: Thu, 18 Jul 2024 21:05:50 -0400 Subject: [PATCH 5/8] refactor vbytes calculations --- src/pages/savings-calculator.tsx | 3 +- src/utils/tx.ts | 121 ++++++++++++------------------- 2 files changed, 50 insertions(+), 74 deletions(-) diff --git a/src/pages/savings-calculator.tsx b/src/pages/savings-calculator.tsx index 0c93348..ba09e8f 100644 --- a/src/pages/savings-calculator.tsx +++ b/src/pages/savings-calculator.tsx @@ -10,6 +10,7 @@ export default function SavingsCalculator(): JSX.Element { const [outputCount, setOutputCount] = useState(1); const [recipientCount, setRecipientCount] = useState(1); const [isDisabled, setIsDisabled] = useState(true); + const [txVBytes, setTxVBytes] = useState(0); const scriptTypes = [ { value: ScriptType.P2PKH, label: "P2PKH" }, @@ -26,7 +27,7 @@ export default function SavingsCalculator(): JSX.Element { alert("Please enter a valid number greater than 0."); return; } - processForm(inputScript, inputCount, outputCount, recipientCount); + setTxVBytes(processForm(inputScript, inputCount, outputCount, recipientCount)); } useEffect(() => { diff --git a/src/utils/tx.ts b/src/utils/tx.ts index b161d5a..73a9439 100644 --- a/src/utils/tx.ts +++ b/src/utils/tx.ts @@ -1,9 +1,13 @@ +// From: https://bitcoinops.org/en/tools/calc-size/ const P2PKH_IN_SIZE = 148; const P2PKH_OUT_SIZE = 34; +const P2PKH_OVERHEAD = 10; const P2WPKH_IN_SIZE = 67.75; const P2WPKH_OUT_SIZE = 31; +const P2WPKH_OVERHEAD = 10.5; const P2TR_OUT_SIZE = 43; -const P2TR_IN_SIZE = 57.25; +const P2TR_IN_SIZE = 57.5; +const P2TR_OVERHEAD = 10.5; export enum ScriptType { P2PKH = "P2PKH", @@ -11,87 +15,58 @@ export enum ScriptType { P2TR = "P2TR", } -function getSizeOfVarInt(length: number) { - if (length < 253) { - return 1; - } else if (length < 65535) { - return 3; - } else if (length < 4294967295) { - return 5; - } else if (length < 18446744073709551615) { - return 9; - } else { - alert("Invalid var int"); - } -} - -function getWitnessVbytes(inputScript: ScriptType, inputCount: number) { - let witnessVBytes = 0; - if (inputScript !== ScriptType.P2PKH) { - // Transactions with segwit inputs have extra overhead - witnessVBytes = 0.25 // segwit marker - + 0.25 // segwit flag - + inputCount / 4; // witness element count per input - } - return witnessVBytes; -} - -function getTxOverheadVBytes(inputScript: ScriptType, inputCount: number, outputCount: number, recipientCount: number) { - return 4 // nVersion - + getSizeOfVarInt(inputCount) // number of inputs - + getSizeOfVarInt(outputCount) // number of outputs - + 4 // nLockTime - + getWitnessVbytes(inputScript, inputCount); -} +// total tx cost without batching: r𝑏 + r𝑖 + 2r𝑜 + 1 +// total tx cost with batching: 𝑏 + 𝑖 + r𝑜 + 1 +const formula = (b: number, i: number, o: number, r: number, isBatching = false) => + isBatching + ? b + i + r * o + 1 + : r * b + r * i + 2 * r * o + 1; -// function getTxOverheadExtraRawBytes(inputScript, inputCount) { -// // Returns the remaining 3/4 bytes per witness bytes -// return getWitnessVbytes(inputScript, inputCount) * 3; -// } -function calculateTxVBytesNoBatching(inputScript: ScriptType, inputCount: number, outputCount: number, recipientCount: number, inputSize: number, inputWitnessSize: number) { - // per tx cost without batching: r𝑏 + r𝑖 + 2r𝑜 + 1 - const b = getTxOverheadVBytes(inputScript, inputCount, outputCount, recipientCount); - const i = inputCount * getTxOverheadVBytes(inputScript, 1, 0, 1); - const o = outputCount * getTxOverheadVBytes(inputScript, 0, 1, 1); - const r = recipientCount; - const txVBytes = r * b + r * i + 2 * r * o + 1; - return Math.ceil(txVBytes); +function getBaseCost(inputScript: ScriptType) { + switch (inputScript) { + case ScriptType.P2PKH: + return P2PKH_OVERHEAD; + case ScriptType.P2WPKH: + return P2WPKH_OVERHEAD; + case ScriptType.P2TR: + return P2TR_OVERHEAD; + } } -function calculateTxVBytesBatching(inputScript: ScriptType, inputCount: number, outputCount: number, recipientCount: number, inputSize: number, inputWitnessSize: number) { - // total tx cost with batching: 𝑏 + 𝑖 + r𝑜 + 1 - const b = getTxOverheadVBytes(inputScript, inputCount, outputCount, recipientCount); - const i = inputCount * getTxOverheadVBytes(inputScript, inputCount, 0, 1) + inputSize + inputWitnessSize; - const o = outputCount * getTxOverheadVBytes(inputScript, 0, 1, 1); - const r = recipientCount; - const txVBytes = b + i + r * o + 1; - return Math.ceil(txVBytes); +function getPerInputCost(inputScript: ScriptType) { + switch (inputScript) { + case ScriptType.P2PKH: + return P2PKH_IN_SIZE; + case ScriptType.P2WPKH: + return P2WPKH_IN_SIZE; + case ScriptType.P2TR: + return P2TR_IN_SIZE; + } } -export function processForm(scriptType: ScriptType, inputCount: number, outputCount: number, recipientCount: number) { - // In most cases the input size is predictable. For multisig inputs we need to perform a detailed calculation - let inputSize = 0; // in virtual bytes - let inputWitnessSize = 0; - switch (scriptType) { +function getPerOutputCost(inputScript: ScriptType) { + switch (inputScript) { case ScriptType.P2PKH: - inputSize = P2PKH_IN_SIZE; - break; + return P2PKH_OUT_SIZE; case ScriptType.P2WPKH: - inputSize = P2WPKH_IN_SIZE; - inputWitnessSize = 107; // size(signature) + signature + size(pubkey) + pubkey - break; - case ScriptType.P2TR: // Only consider the cooperative taproot signing path; assume multisig is done via aggregate signatures - inputSize = P2TR_IN_SIZE; - inputWitnessSize = 65; // getSizeOfVarInt(schnorrSignature) + schnorrSignature; - break; + return P2WPKH_OUT_SIZE; + case ScriptType.P2TR: + return P2TR_OUT_SIZE; } +} + +function getVbytes(script: ScriptType, inputCount: number, outputCount: number, recipientCount: number, isBatching: boolean) { + const perInputCost = getPerInputCost(script) * inputCount; + const perOutputCost = getPerOutputCost(script) * outputCount; + const baseCost = getBaseCost(script); + const vbytes = formula(baseCost, perInputCost, perOutputCost, recipientCount, isBatching); + return Math.ceil(vbytes); +} - const txVBytes = calculateTxVBytesNoBatching(scriptType, inputCount, outputCount, recipientCount, inputSize, inputWitnessSize); +export function getUnbatchedAndBatchedVbytes(script: ScriptType, inputCount: number, outputCount: number, recipientCount: number) { + const vbytesNonBatched = getVbytes(script, inputCount, outputCount, recipientCount, false); + const vbytesBatched = getVbytes(script, inputCount, outputCount, recipientCount, true); - // const txBytes = getTxOverheadExtraRawBytes(inputScript, inputCount) + txVBytes + (inputWitnessSize * inputCount) * 3 / 4; - // const txWeight = txVBytes * 4; - // document.getElementById('txBytes').innerHTML = txBytes; - document.getElementById('txVBytes').innerHTML = String(txVBytes); - // document.getElementById('txWeight').innerHTML = txWeight; + return {vbytesBatched, vbytesNonBatched}; } \ No newline at end of file From 234e2f37a36af56f8151b9a746a0e683f950a207 Mon Sep 17 00:00:00 2001 From: Brandon Lucas <38222767+thebrandonlucas@users.noreply.github.com> Date: Thu, 18 Jul 2024 21:43:06 -0400 Subject: [PATCH 6/8] batching calculations working correctly --- src/pages/savings-calculator.tsx | 12 ++++++++---- src/utils/tx.ts | 21 ++++++++++++--------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/pages/savings-calculator.tsx b/src/pages/savings-calculator.tsx index ba09e8f..3d43d89 100644 --- a/src/pages/savings-calculator.tsx +++ b/src/pages/savings-calculator.tsx @@ -1,6 +1,6 @@ import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; import Layout from "@theme/Layout"; -import { processForm, ScriptType, } from "../utils/tx"; +import { getUnbatchedAndBatchedVbytes, ScriptType, } from "../utils/tx"; import { useEffect, useState } from "react"; export default function SavingsCalculator(): JSX.Element { @@ -10,7 +10,8 @@ export default function SavingsCalculator(): JSX.Element { const [outputCount, setOutputCount] = useState(1); const [recipientCount, setRecipientCount] = useState(1); const [isDisabled, setIsDisabled] = useState(true); - const [txVBytes, setTxVBytes] = useState(0); + const [unbatchedVbytes, setUnbatchedVbytes] = useState(0); + const [batchedVbytes, setBatchedVbytes] = useState(0); const scriptTypes = [ { value: ScriptType.P2PKH, label: "P2PKH" }, @@ -27,7 +28,9 @@ export default function SavingsCalculator(): JSX.Element { alert("Please enter a valid number greater than 0."); return; } - setTxVBytes(processForm(inputScript, inputCount, outputCount, recipientCount)); + const { vbytesUnbatched, vbytesBatched } = getUnbatchedAndBatchedVbytes(inputScript, inputCount, outputCount, recipientCount); + setUnbatchedVbytes(vbytesUnbatched); + setBatchedVbytes(vbytesBatched); } useEffect(() => { @@ -74,7 +77,8 @@ export default function SavingsCalculator(): JSX.Element {



{/* Transaction size in raw bytes:
*/} - Transaction size in virtual bytes: {txVBytes}
+ Transaction size in virtual bytes without batching: {unbatchedVbytes}
+ Transaction size in virtual bytes with batching: {batchedVbytes}
{/* Transaction size in weight units:

*/} {/*

Which size should you use for calculating fee estimates?
Estimates should be in satoshis per virtual byte.

*/} diff --git a/src/utils/tx.ts b/src/utils/tx.ts index 73a9439..68a1774 100644 --- a/src/utils/tx.ts +++ b/src/utils/tx.ts @@ -2,11 +2,11 @@ const P2PKH_IN_SIZE = 148; const P2PKH_OUT_SIZE = 34; const P2PKH_OVERHEAD = 10; -const P2WPKH_IN_SIZE = 67.75; +const P2WPKH_IN_SIZE = 68; const P2WPKH_OUT_SIZE = 31; const P2WPKH_OVERHEAD = 10.5; -const P2TR_OUT_SIZE = 43; const P2TR_IN_SIZE = 57.5; +const P2TR_OUT_SIZE = 43; const P2TR_OVERHEAD = 10.5; export enum ScriptType { @@ -15,12 +15,12 @@ export enum ScriptType { P2TR = "P2TR", } -// total tx cost without batching: r𝑏 + r𝑖 + 2r𝑜 + 1 -// total tx cost with batching: 𝑏 + 𝑖 + r𝑜 + 1 +// total tx cost without batching: r𝑏 + r𝑖 + r𝑜 +// total tx cost with batching: 𝑏 + 𝑖 + r𝑜 const formula = (b: number, i: number, o: number, r: number, isBatching = false) => isBatching - ? b + i + r * o + 1 - : r * b + r * i + 2 * r * o + 1; + ? b + i + r * o + : r * b + r * i + r * o; function getBaseCost(inputScript: ScriptType) { @@ -61,12 +61,15 @@ function getVbytes(script: ScriptType, inputCount: number, outputCount: number, const perOutputCost = getPerOutputCost(script) * outputCount; const baseCost = getBaseCost(script); const vbytes = formula(baseCost, perInputCost, perOutputCost, recipientCount, isBatching); - return Math.ceil(vbytes); + console.log({ baseCost, perInputCost, perOutputCost, recipientCount, isBatching, vbytes }); + + return vbytes; } export function getUnbatchedAndBatchedVbytes(script: ScriptType, inputCount: number, outputCount: number, recipientCount: number) { - const vbytesNonBatched = getVbytes(script, inputCount, outputCount, recipientCount, false); + const vbytesUnbatched = getVbytes(script, inputCount, outputCount, recipientCount, false); const vbytesBatched = getVbytes(script, inputCount, outputCount, recipientCount, true); - return {vbytesBatched, vbytesNonBatched}; + console.log({vbytesBatched, vbytesUnbatched}) + return { vbytesBatched, vbytesUnbatched }; } \ No newline at end of file From 8f92953910d6878d0ee36d01ede7feca8d951574 Mon Sep 17 00:00:00 2001 From: Brandon Lucas <38222767+thebrandonlucas@users.noreply.github.com> Date: Fri, 19 Jul 2024 09:59:25 -0400 Subject: [PATCH 7/8] accurately displaying charts --- package.json | 1 + src/components/Charts/BatchBar/bar.tsx | 37 ++++++++++++++ src/components/Charts/chart.tsx | 63 +++++++++++++++++++++++ src/pages/savings-calculator.tsx | 69 ++++++++++++++------------ src/utils/tx.ts | 5 +- yarn.lock | 13 +++++ 6 files changed, 155 insertions(+), 33 deletions(-) create mode 100644 src/components/Charts/BatchBar/bar.tsx create mode 100644 src/components/Charts/chart.tsx diff --git a/package.json b/package.json index d770713..5654314 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "echarts": "^5.5.1", + "echarts-for-react": "^3.0.2", "prism-react-renderer": "^2.3.0", "react": "^18.0.0", "react-dom": "^18.0.0", diff --git a/src/components/Charts/BatchBar/bar.tsx b/src/components/Charts/BatchBar/bar.tsx new file mode 100644 index 0000000..6de8e19 --- /dev/null +++ b/src/components/Charts/BatchBar/bar.tsx @@ -0,0 +1,37 @@ + +import * as echarts from 'echarts'; +import { useEffect, useState } from 'react'; +import ReactECharts from 'echarts-for-react'; + +export default function BatchBar({unbatchedVbytes, batchedVbytes}: {unbatchedVbytes: number, batchedVbytes: number}): JSX.Element { + + const [option, setOption] = useState(undefined); + + + useEffect(() => { + + setOption({ + xAxis: { + type: 'category', + data: ['Unbatched', 'Batched'] + }, + yAxis: { + type: 'value' + }, + series: [ + { + data: [unbatchedVbytes, batchedVbytes], + type: 'bar' + } + ] + }); + + + + }, [unbatchedVbytes, batchedVbytes]); + + console.log(unbatchedVbytes, batchedVbytes, {option}); + + return batchedVbytes !== undefined && unbatchedVbytes !== undefined && !!option ? : <>nada; + } + \ No newline at end of file diff --git a/src/components/Charts/chart.tsx b/src/components/Charts/chart.tsx new file mode 100644 index 0000000..7cdac35 --- /dev/null +++ b/src/components/Charts/chart.tsx @@ -0,0 +1,63 @@ + +import React, { useRef, useEffect } from "react"; +import { init, getInstanceByDom } from "echarts"; +import type { CSSProperties } from "react"; +import type { EChartsOption, ECharts, SetOptionOpts } from "echarts"; + +export interface ReactEChartsProps { + option: EChartsOption; + style?: CSSProperties; + settings?: SetOptionOpts; + loading?: boolean; + theme?: "light" | "dark"; +} + +export function Chart({ + option, + style, + settings, + loading, + theme, +}: ReactEChartsProps): JSX.Element { + const chartRef = useRef(null); + + useEffect(() => { + // Initialize chart + let chart: ECharts | undefined; + if (chartRef.current !== null) { + chart = init(chartRef.current, theme); + } + + // Add chart resize listener + // ResizeObserver is leading to a bit janky UX + function resizeChart() { + chart?.resize(); + } + window.addEventListener("resize", resizeChart); + + // Return cleanup function + return () => { + chart?.dispose(); + window.removeEventListener("resize", resizeChart); + }; + }, [theme]); + + useEffect(() => { + // Update chart + if (chartRef.current !== null) { + const chart = getInstanceByDom(chartRef.current); + chart.setOption(option, settings); + } + }, [option, settings, theme]); // Whenever theme changes we need to add option and setting due to it being deleted in cleanup function + + useEffect(() => { + // Update chart + if (chartRef.current !== null) { + const chart = getInstanceByDom(chartRef.current); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + loading === true ? chart.showLoading() : chart.hideLoading(); + } + }, [loading, theme]); + + return
; +} \ No newline at end of file diff --git a/src/pages/savings-calculator.tsx b/src/pages/savings-calculator.tsx index 3d43d89..84cc9c2 100644 --- a/src/pages/savings-calculator.tsx +++ b/src/pages/savings-calculator.tsx @@ -2,6 +2,7 @@ import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; import Layout from "@theme/Layout"; import { getUnbatchedAndBatchedVbytes, ScriptType, } from "../utils/tx"; import { useEffect, useState } from "react"; +import BatchBar from "../components/Charts/BatchBar/bar"; export default function SavingsCalculator(): JSX.Element { const { siteConfig } = useDocusaurusContext(); @@ -44,48 +45,54 @@ export default function SavingsCalculator(): JSX.Element { >
Payjoin provides a unique opportunity for receiver-side savings. -
-
+
+
- -
- -
-
-
-
- setInputCount(parseInt(e.target.value))} /> + +
+ +
+
+
+ +
+ setInputCount(parseInt(e.target.value))} /> +
-
-
-
- setOutputCount(parseInt(e.target.value))} /> + +
+ setOutputCount(parseInt(e.target.value))} /> +
-
-
-
- setRecipientCount(parseInt(e.target.value))} /> + +
+ setRecipientCount(parseInt(e.target.value))} /> +
+


+ {/* Transaction size in raw bytes:
*/} + Transaction size in virtual bytes without batching: {unbatchedVbytes}
+ Transaction size in virtual bytes with batching: {batchedVbytes}
+ {/* Transaction size in weight units:

*/} + {/*

Which size should you use for calculating fee estimates?
+ Estimates should be in satoshis per virtual byte.

*/}
-


- {/* Transaction size in raw bytes:
*/} - Transaction size in virtual bytes without batching: {unbatchedVbytes}
- Transaction size in virtual bytes with batching: {batchedVbytes}
- {/* Transaction size in weight units:

*/} - {/*

Which size should you use for calculating fee estimates?
- Estimates should be in satoshis per virtual byte.

*/}
+ +
+ +
- -
+
+
); } diff --git a/src/utils/tx.ts b/src/utils/tx.ts index 68a1774..459d089 100644 --- a/src/utils/tx.ts +++ b/src/utils/tx.ts @@ -17,11 +17,12 @@ export enum ScriptType { // total tx cost without batching: r𝑏 + r𝑖 + r𝑜 // total tx cost with batching: 𝑏 + 𝑖 + r𝑜 -const formula = (b: number, i: number, o: number, r: number, isBatching = false) => +const totalCost = (b: number, i: number, o: number, r: number, isBatching = false) => isBatching ? b + i + r * o : r * b + r * i + r * o; +// TODO: payjoin recipient/cut-through formula function getBaseCost(inputScript: ScriptType) { switch (inputScript) { @@ -60,7 +61,7 @@ function getVbytes(script: ScriptType, inputCount: number, outputCount: number, const perInputCost = getPerInputCost(script) * inputCount; const perOutputCost = getPerOutputCost(script) * outputCount; const baseCost = getBaseCost(script); - const vbytes = formula(baseCost, perInputCost, perOutputCost, recipientCount, isBatching); + const vbytes = totalCost(baseCost, perInputCost, perOutputCost, recipientCount, isBatching); console.log({ baseCost, perInputCost, perOutputCost, recipientCount, isBatching, vbytes }); return vbytes; diff --git a/yarn.lock b/yarn.lock index 6c43d05..1cfa4e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4632,6 +4632,14 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +echarts-for-react@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/echarts-for-react/-/echarts-for-react-3.0.2.tgz#ac5859157048a1066d4553e34b328abb24f2b7c1" + integrity sha512-DRwIiTzx8JfwPOVgGttDytBqdp5VzCSyMRIxubgU/g2n9y3VLUmF2FK7Icmg/sNVkv4+rktmrLN9w22U2yy3fA== + dependencies: + fast-deep-equal "^3.1.3" + size-sensor "^1.0.1" + echarts@^5.5.1: version "5.5.1" resolved "https://registry.yarnpkg.com/echarts/-/echarts-5.5.1.tgz#8dc9c68d0c548934bedcb5f633db07ed1dd2101c" @@ -8652,6 +8660,11 @@ sitemap@^7.1.1: arg "^5.0.0" sax "^1.2.4" +size-sensor@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/size-sensor/-/size-sensor-1.0.2.tgz#b8f8da029683cf2b4e22f12bf8b8f0a1145e8471" + integrity sha512-2NCmWxY7A9pYKGXNBfteo4hy14gWu47rg5692peVMst6lQLPKrVjhY+UTEsPI5ceFRJSl3gVgMYaUi/hKuaiKw== + skin-tone@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/skin-tone/-/skin-tone-2.0.0.tgz#4e3933ab45c0d4f4f781745d64b9f4c208e41237" From 7e5e12471d0972af18a9c5e53567955bf422a459 Mon Sep 17 00:00:00 2001 From: Brandon Lucas <38222767+thebrandonlucas@users.noreply.github.com> Date: Fri, 19 Jul 2024 15:46:52 -0400 Subject: [PATCH 8/8] wip - add payjoin calculations to charts --- src/components/Charts/BatchBar/bar.tsx | 26 +++++------- src/pages/savings-calculator.tsx | 33 ++++++++++++--- src/utils/tx.ts | 58 +++++++++++++++++++------- 3 files changed, 83 insertions(+), 34 deletions(-) diff --git a/src/components/Charts/BatchBar/bar.tsx b/src/components/Charts/BatchBar/bar.tsx index 6de8e19..0e2a0af 100644 --- a/src/components/Charts/BatchBar/bar.tsx +++ b/src/components/Charts/BatchBar/bar.tsx @@ -3,35 +3,31 @@ import * as echarts from 'echarts'; import { useEffect, useState } from 'react'; import ReactECharts from 'echarts-for-react'; -export default function BatchBar({unbatchedVbytes, batchedVbytes}: {unbatchedVbytes: number, batchedVbytes: number}): JSX.Element { - +export default function BatchBar({unbatchedVbytes, batchedVbytes, payjoinVbytes}: {unbatchedVbytes: number, batchedVbytes: number, payjoinVbytes: number}): JSX.Element { const [option, setOption] = useState(undefined); - useEffect(() => { - setOption({ xAxis: { type: 'category', - data: ['Unbatched', 'Batched'] + data: ['Unbatched', 'Batched', 'Payjoin'] }, yAxis: { type: 'value' }, series: [ { - data: [unbatchedVbytes, batchedVbytes], + data: [ + {value: unbatchedVbytes, itemStyle: {color: '#ffe751'}}, + {value: batchedVbytes, itemStyle: {color: '#81e86a'}}, + {value: payjoinVbytes, itemStyle: {color: '#ff6f6f'}} + ], type: 'bar' } ] - }); - - - - }, [unbatchedVbytes, batchedVbytes]); - - console.log(unbatchedVbytes, batchedVbytes, {option}); + }); + }, [unbatchedVbytes, batchedVbytes, payjoinVbytes]); - return batchedVbytes !== undefined && unbatchedVbytes !== undefined && !!option ? : <>nada; - } + return option && ; +} \ No newline at end of file diff --git a/src/pages/savings-calculator.tsx b/src/pages/savings-calculator.tsx index 84cc9c2..2cdd3c8 100644 --- a/src/pages/savings-calculator.tsx +++ b/src/pages/savings-calculator.tsx @@ -1,6 +1,6 @@ import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; import Layout from "@theme/Layout"; -import { getUnbatchedAndBatchedVbytes, ScriptType, } from "../utils/tx"; +import { getVbytesForEachTxType, ScriptType, } from "../utils/tx"; import { useEffect, useState } from "react"; import BatchBar from "../components/Charts/BatchBar/bar"; @@ -10,9 +10,13 @@ export default function SavingsCalculator(): JSX.Element { const [inputCount, setInputCount] = useState(1); const [outputCount, setOutputCount] = useState(1); const [recipientCount, setRecipientCount] = useState(1); + const [payjoinRecipientInputCount, setPayjoinRecipientInputCount] = useState(1); + const [depositorInputCount, setDepositorInputCount] = useState(1); + const [depositorOutputCount, setDepositorOutputCount] = useState(1); const [isDisabled, setIsDisabled] = useState(true); const [unbatchedVbytes, setUnbatchedVbytes] = useState(0); const [batchedVbytes, setBatchedVbytes] = useState(0); + const [payjoinVbytes, setPayjoinVbytes] = useState(0); const scriptTypes = [ { value: ScriptType.P2PKH, label: "P2PKH" }, @@ -29,9 +33,10 @@ export default function SavingsCalculator(): JSX.Element { alert("Please enter a valid number greater than 0."); return; } - const { vbytesUnbatched, vbytesBatched } = getUnbatchedAndBatchedVbytes(inputScript, inputCount, outputCount, recipientCount); + const { vbytesUnbatched, vbytesBatched, vbytesPayjoined } = getVbytesForEachTxType(inputScript, inputCount, outputCount, recipientCount, payjoinRecipientInputCount, depositorInputCount, depositorOutputCount); setUnbatchedVbytes(vbytesUnbatched); setBatchedVbytes(vbytesBatched); + setPayjoinVbytes(vbytesPayjoined); } useEffect(() => { @@ -77,19 +82,37 @@ export default function SavingsCalculator(): JSX.Element { setRecipientCount(parseInt(e.target.value))} />
+
+ +
+ setPayjoinRecipientInputCount(parseInt(e.target.value))} /> +
+
+
+ +
+ setDepositorInputCount(parseInt(e.target.value))} /> +
+
+
+ +
+ setDepositorOutputCount(parseInt(e.target.value))} /> +
+



{/* Transaction size in raw bytes:
*/} Transaction size in virtual bytes without batching: {unbatchedVbytes}
Transaction size in virtual bytes with batching: {batchedVbytes}
+ Transaction size in virtual bytes with payjoin: {payjoinVbytes}
{/* Transaction size in weight units:

*/} {/*

Which size should you use for calculating fee estimates?
Estimates should be in satoshis per virtual byte.

*/}
-
- - +
+
diff --git a/src/utils/tx.ts b/src/utils/tx.ts index 459d089..2bf16ba 100644 --- a/src/utils/tx.ts +++ b/src/utils/tx.ts @@ -15,12 +15,40 @@ export enum ScriptType { P2TR = "P2TR", } -// total tx cost without batching: r𝑏 + r𝑖 + r𝑜 -// total tx cost with batching: 𝑏 + 𝑖 + r𝑜 -const totalCost = (b: number, i: number, o: number, r: number, isBatching = false) => - isBatching - ? b + i + r * o - : r * b + r * i + r * o; +// See definitions here: https://gist.github.com/thebrandonlucas/fb4283bef3df51b88a85ae974488d81f +enum TxType { + Standard = "Standard", + Batch = "Batch", + Payjoin = "Payjoin", +} + +// Variables: +// b = base cost +// i = per input cost +// o = per output cost +// r = recipient count +// p = recipient input count (payjoin only) +// di = depositor input count (payjoin only) +// do = depositor output count (payjoin only) + +// total tx cost without batching: r(b + i) + 2ro +// total tx cost with batching: b + i + ro + o +// total tx cost with payjoin: b + p(i) + di(i) + ro + do(o) + o +const totalCost = (b: number, i: number, o: number, r: number, type: TxType, p?: number, di?: number, _do?: number) => { + switch (type) { + case TxType.Standard: + return r * (b + i) + 2 * r * o; + case TxType.Batch: + return b + i + r * o + o; + case TxType.Payjoin: + if (!p || !di || !_do) { + throw new Error("Payjoin requires recipient input count, depositor input count, and depositor output count"); + } + return b + p * i + di * i + r * o + _do * o + o; + } +} + + // TODO: payjoin recipient/cut-through formula @@ -57,20 +85,22 @@ function getPerOutputCost(inputScript: ScriptType) { } } -function getVbytes(script: ScriptType, inputCount: number, outputCount: number, recipientCount: number, isBatching: boolean) { +function getVbytes(script: ScriptType, inputCount: number, outputCount: number, recipientCount: number, + type: TxType, payjoinRecipientInputCount?: number, depositorInputCount?: number, depositorOutputCount?: number) { const perInputCost = getPerInputCost(script) * inputCount; const perOutputCost = getPerOutputCost(script) * outputCount; const baseCost = getBaseCost(script); - const vbytes = totalCost(baseCost, perInputCost, perOutputCost, recipientCount, isBatching); - console.log({ baseCost, perInputCost, perOutputCost, recipientCount, isBatching, vbytes }); + const vbytes = totalCost(baseCost, perInputCost, perOutputCost, recipientCount, type, payjoinRecipientInputCount, depositorInputCount, depositorOutputCount); + console.log({ baseCost, perInputCost, perOutputCost, recipientCount, type, vbytes }); return vbytes; } -export function getUnbatchedAndBatchedVbytes(script: ScriptType, inputCount: number, outputCount: number, recipientCount: number) { - const vbytesUnbatched = getVbytes(script, inputCount, outputCount, recipientCount, false); - const vbytesBatched = getVbytes(script, inputCount, outputCount, recipientCount, true); +export function getVbytesForEachTxType(script: ScriptType, inputCount: number, outputCount: number, recipientCount: number, payjoinRecipientInputCount: number, depositorInputCount: number, depositorOutputCount: number) { + const vbytesUnbatched = getVbytes(script, inputCount, outputCount, recipientCount, TxType.Standard); + const vbytesBatched = getVbytes(script, inputCount, outputCount, recipientCount, TxType.Batch); + const vbytesPayjoined = getVbytes(script, inputCount, outputCount, recipientCount, TxType.Payjoin, payjoinRecipientInputCount, depositorInputCount, depositorOutputCount); - console.log({vbytesBatched, vbytesUnbatched}) - return { vbytesBatched, vbytesUnbatched }; + // console.log({vbytesBatched, vbytesUnbatched, vbytesPayjoined}) + return { vbytesBatched, vbytesUnbatched, vbytesPayjoined }; } \ No newline at end of file