Script path modifications

This commit is contained in:
sairaj mote 2023-11-01 03:49:23 +05:30
parent 1b5b44c7a1
commit 13fd4bb6e3
3 changed files with 207 additions and 102 deletions

View File

@ -639,25 +639,30 @@
*/ */
function $signal(initialValue, callback) { function $signal(initialValue, callback) {
let value = initialValue; let value = initialValue;
const subscribers = new Set(); const subscribers = new Map();
let hasCustomSubscriber = false; let hasCustomSubscriber = false;
function getter(subscriber) { function getter(subscriber) {
if (currentSubscriber) {
subscribers.add(currentSubscriber);
}
if (!hasCustomSubscriber && subscriber) { if (!hasCustomSubscriber && subscriber) {
subscribers.add(subscriber) currentSubscriber = subscriber;
hasCustomSubscriber = true hasCustomSubscriber = true
} }
if (currentSubscriber) {
let hash = currentSubscriber.toString();
if (Crypto)
hash = Crypto.SHA1(currentSubscriber.toString())
if (!subscribers.has(hash)) {
subscribers.set(hash, currentSubscriber);
}
}
return value; return value;
} }
function setter(newValue) { function setter(newValue) {
if (newValue === value) return; if (newValue === value) return;
value = newValue; value = newValue;
for (const subscriber of subscribers) { subscribers.forEach((subscriber, hash) => {
subscriber(); subscriber();
} });
} }
return [getter, setter]; return [getter, setter];
} }
@ -1244,8 +1249,8 @@
const [suggestedFeeStatus, setSuggestedFeeStatus] = $signal(['idle']) const [suggestedFeeStatus, setSuggestedFeeStatus] = $signal(['idle'])
const [feeType, setFeeType] = $signal('suggested') const [feeType, setFeeType] = $signal('suggested')
const [atTaprootStep, setAtTaprootStep] = $signal(1); const [atTaprootStep, setAtTaprootStep] = $signal(1);
const [isScriptMode, setIsScriptMode] = $signal(false);
router.addRoute('send', (state) => { router.addRoute('send', (state) => {
const [isScriptMode, setIsScriptMode] = $signal(false);
$effect(() => { $effect(() => {
let feeSection = '' let feeSection = ''
if (feeType() === 'suggested') { if (feeType() === 'suggested') {
@ -1317,7 +1322,7 @@
form = html` form = html`
<sm-form id="script_details_form" style="max-width: 36rem"> <sm-form id="script_details_form" style="max-width: 36rem">
<div class="flex flex-direction-column gap-1"> <div class="flex flex-direction-column gap-1">
${[...Array(taprootScriptTxDetails.solutions).keys()].map(index => html` ${[...Array(taprootScriptTxDetails.solutions || 0).keys()].map(index => html`
<sm-input class="solution-input" placeholder=${`Solution ${index + 1} (hex)`} pattern="^[0-9A-Fa-f]+$" error-text="Only hexadecimal values are allowed" animate required></sm-input> <sm-input class="solution-input" placeholder=${`Solution ${index + 1} (hex)`} pattern="^[0-9A-Fa-f]+$" error-text="Only hexadecimal values are allowed" animate required></sm-input>
`)} `)}
</div> </div>
@ -1331,9 +1336,21 @@
</label> </label>
</sm-input> </sm-input>
`: ''} `: ''}
<div class="grid gap-0-5">
<div class="flex align-center space-between gap-1">
<h4>Fee</h4>
<sm-chips onchange=${e => { setFeeType(e.target.value); calculateSuggestedFee('script-path'); }}>
<sm-chip value="suggested" selected>Suggested</sm-chip>
<sm-chip value="custom">Custom</sm-chip>
</sm-chips>
</div>
${feeSection}
</div>
<div class="flex gap-0-5"> <div class="flex gap-0-5">
<button class="button button--colored" onclick="proceedTo(1)">Back</button> <button class="button button--colored" onclick="proceedTo(1)">Back</button>
<button id="taproot_script_path_send_button" class="button button--primary flex-1" type="submit" disabled onclick=${sendScriptPathTx}>Send</button> <div class="multi-state-button flex-1">
<button id="taproot_script_path_send_button" class="button button--primary" type="submit" disabled onclick=${sendScriptPathTx}>Send</button>
</div>
</div> </div>
</sm-form> </sm-form>
` `
@ -1341,11 +1358,11 @@
} }
} else { } else {
form = html` form = html`
<sm-form id="send_tx_form" onvalid=${calculateSuggestedFee} oninvalid=${handleInvalidForm} ?skip-submit=${feeType() === 'suggested'}> <sm-form id="send_tx_form" onvalid=${() => calculateSuggestedFee('key-path')} oninvalid=${handleInvalidForm} ?skip-submit=${feeType() === 'suggested'}>
<fieldset class="flex flex-direction-column gap-0-5"> <fieldset class="flex flex-direction-column gap-0-5">
<div class="flex space-between align-center"> <div class="flex space-between align-center">
<h4>Sender</h4> <h4>Sender</h4>
<button id="check_balance_button" class="button button--small button--colored" disabled onclick="checkBalance()"> <button id="check_balance_button" class="button button--small button--colored" disabled onclick=${checkBalance}>
Check balance Check balance
</button> </button>
</div> </div>
@ -1379,7 +1396,7 @@
<div class="grid gap-0-5"> <div class="grid gap-0-5">
<div class="flex align-center space-between gap-1"> <div class="flex align-center space-between gap-1">
<h4>Fee</h4> <h4>Fee</h4>
<sm-chips onchange=${e => { setFeeType(e.target.value); calculateSuggestedFee(); }}> <sm-chips onchange=${e => { setFeeType(e.target.value); calculateSuggestedFee('key-path'); }}>
<sm-chip value="suggested" selected>Suggested</sm-chip> <sm-chip value="suggested" selected>Suggested</sm-chip>
<sm-chip value="custom">Custom</sm-chip> <sm-chip value="custom">Custom</sm-chip>
</sm-chips> </sm-chips>
@ -1393,15 +1410,14 @@
</sm-form> </sm-form>
`; `;
} }
renderElem(getRef('page_container'), html` renderElem(getRef('page_container'), html`
<div class="flex flex-direction-column gap-1-5"> <div class="flex flex-direction-column gap-1-5">
<div class="flex flex-direction-column gap-0-5"> <div class="flex flex-direction-column gap-0-5">
<h3> <h3>
Perform Transaction Perform Transaction
</h3> </h3>
<sm-switch style="width: fit-content" onchange=${e => setIsScriptMode(!isScriptMode())}> <sm-switch style="width: fit-content" ?checked=${isScriptMode()} onchange=${e => setIsScriptMode(e.target.value)}>
<p slot="left" style="margin-right: 0.5rem">Toggle script mode</p> <p slot="left" style="margin-right: 0.5rem">Toggle script path mode</p>
</sm-switch> </sm-switch>
</div> </div>
${isScriptMode() ? html` ${isScriptMode() ? html`
@ -1416,21 +1432,25 @@
`) `)
}) })
}) })
async function calculateSuggestedFee() { async function calculateSuggestedFee(type = 'key-path') {
try { try {
getRef('send_tx_button').disabled = true const submitButton = getRef('send_tx_button') || getRef('taproot_script_path_send_button')
submitButton.disabled = true
if (feeType() === 'custom') { if (feeType() === 'custom') {
} else { } else {
if (!getRef('send_tx_form').isFormValid) return
const { senderPrivateKey, senderAddress, receiverAddresses, receiverAmounts } = getTransactionDetails()
if (!senderPrivateKey || !senderAddress || !receiverAddresses.length || !receiverAmounts.length)
return
setSuggestedFeeStatus(['calculating']) setSuggestedFeeStatus(['calculating'])
const { fee } = await createTx({ senderPrivateKey, receivers: receiverAddresses, amounts: receiverAmounts }) if (type === 'key-path') {
setSuggestedFee(fee) const { senderPrivateKey, receiverAddresses, receiverAmounts } = getTransactionDetails();
const { fee } = await createTx({ senderPrivateKey, receivers: receiverAddresses, amounts: receiverAmounts })
setSuggestedFee(fee)
} else if (type === 'script-path') {
const { senderAddress, receiverAddresses, receiverAmounts } = taprootScriptTxDetails;
const { fee } = await createTx({ isTaprootScriptPath: true, senderAddress, receivers: receiverAddresses, amounts: receiverAmounts })
setSuggestedFee(fee)
}
setSuggestedFeeStatus(['success']) setSuggestedFeeStatus(['success'])
getRef('send_tx_button').disabled = false submitButton.disabled = false
} }
} catch (e) { } catch (e) {
console.error(e) console.error(e)
@ -1440,33 +1460,35 @@
function handleInvalidForm() { function handleInvalidForm() {
setSuggestedFee(0); setSuggestedFee(0);
setSuggestedFeeStatus(['idle']); setSuggestedFeeStatus(['idle']);
getRef('send_tx_button').disabled = true (getRef('send_tx_button') || getRef('taproot_script_path_send_button')).disabled = true
} }
let taprootScriptTxDetails = {}; let taprootScriptTxDetails = {};
function proceedTo(step) { function proceedTo(step) {
if (step) { if (!step) return
switch (atTaprootStep()) { switch (atTaprootStep()) {
case 1: case 1:
if (!getRef('sender_receiver_form').isFormValid) { // if (!getRef('sender_receiver_form').isFormValid) {
notify('Please fill all the details', 'error') // notify('Please fill all the details', 'error')
return // return
} // }
const { receiverAddresses, receiverAmounts } = getTransactionReceivers() const { receiverAddresses, receiverAmounts } = getTransactionReceivers()
const senderAddress = getRef('taproot_sender_input').value.trim() const senderAddress = getRef('taproot_sender_input').value.trim()
taprootScriptTxDetails = { taprootScriptTxDetails = {
senderAddress, receiverAddresses, receiverAmounts, senderAddress, receiverAddresses, receiverAmounts,
script: getRef('script_input').value.trim(), script: getRef('script_input').value.trim(),
controlBlock: getRef('control_block_input').value.trim(), controlBlockHex: getRef('control_block_input').value.trim(),
solutions: parseInt(getRef('solutions_input').value.trim()), solutions: parseInt(getRef('solutions_input').value.trim()),
signTransaction: getRef('sign_transaction_checkbox').checked signTransaction: getRef('sign_transaction_checkbox').checked
} }
break; break;
case 2: case 2:
break; break;
} }
setAtTaprootStep(step) setAtTaprootStep(step)
} else { switch (step) {
setAtTaprootStep(atTaprootStep() + 1) case 2:
calculateSuggestedFee('script-path')
break;
} }
} }
async function sendScriptPathTx() { async function sendScriptPathTx() {
@ -1477,9 +1499,8 @@
const decodedSolution = hex.decode(solution.value.trim()) const decodedSolution = hex.decode(solution.value.trim())
scriptWitness.push(decodedSolution) scriptWitness.push(decodedSolution)
}) })
const { senderAddress, receiverAddresses, receiverAmounts, script, controlBlock, signTransaction } = taprootScriptTxDetails const { senderAddress, receiverAddresses, receiverAmounts, script, controlBlockHex, signTransaction } = taprootScriptTxDetails
scriptWitness.push(hex.decode(script), hex.decode(controlBlock)) scriptWitness.push(hex.decode(script), hex.decode(controlBlockHex))
scriptWitness = scriptWitness.map(witness => hex.encode(witness))
let signerSecretKey let signerSecretKey
if (signTransaction) { if (signTransaction) {
const signerPrivateKey = getRef('signer_input').value.trim() const signerPrivateKey = getRef('signer_input').value.trim()
@ -1489,20 +1510,6 @@
} }
signerSecretKey = hex.decode(signerPrivateKey) signerSecretKey = hex.decode(signerPrivateKey)
} }
const internalKeyPair = keyPairFromSecret('1229101a0fcf2104e8808dab35661134aa5903867d44deb73ce1c7e4eb925be8');
const taprootTree = taproot.taprootListToTree([
{
script,
leafVersion: 0xc0,
}
]);
const taprootCommitment = taproot.p2tr(
internalKeyPair.schnorrPublicKey,
taprootTree,
undefined,
true
);
const confirmation = await getConfirmation('Confirm transaction', { const confirmation = await getConfirmation('Confirm transaction', {
message: html` message: html`
<div class="grid gap-1-5"> <div class="grid gap-1-5">
@ -1537,8 +1544,9 @@
receivers: receiverAddresses, receivers: receiverAddresses,
amounts: receiverAmounts, amounts: receiverAmounts,
scriptWitness, scriptWitness,
tr: taprootCommitment, signerSecretKey,
signerSecretKey userScript: hex.decode(script),
userControlBlock: taproot.TaprootControlBlock.decode(hex.decode(controlBlockHex))
}) })
console.log(txHex) console.log(txHex)
btcOperator.broadcastTx(txHex).then(txid => { btcOperator.broadcastTx(txHex).then(txid => {
@ -1735,21 +1743,29 @@
}) })
function checkBalance() { function checkBalance() {
const wif = getRef('private_key_input').value.trim() let address;
if (!wif) const hasProvidedPrivateKey = !!getRef('private_key_input')
return notify(`Please enter sender's private key to check balance`) if (hasProvidedPrivateKey) {
const wif = getRef('private_key_input').value.trim()
if (!wif)
return notify(`Please enter sender's private key to check balance`)
address = getTaprootAddress(wif).tr.address
} else {
address = getRef('taproot_sender_input').value.trim()
}
getRef('sender_balance_container').classList.remove('hidden') getRef('sender_balance_container').classList.remove('hidden')
renderElem(getRef('sender_balance_container'), html` renderElem(getRef('sender_balance_container'), html`
Loading balance...<sm-spinner></sm-spinner> Loading balance...<sm-spinner></sm-spinner>
`) `)
const { tr: { address } } = getTaprootAddress(wif)
btcOperator.getBalance(address).then(balance => { btcOperator.getBalance(address).then(balance => {
renderElem(getRef('sender_balance_container'), html` renderElem(getRef('sender_balance_container'), html`
<div class="grid gap-1" style="padding: 1rem; border-radius: 0.5rem; border: solid thin rgba(var(--text-color),0.3)"> <div class="grid gap-1" style="padding: 1rem; border-radius: 0.5rem; border: solid thin rgba(var(--text-color),0.3)">
<div class="grid"> ${hasProvidedPrivateKey ? html`
<p class="label">Sender address</p> <div class="grid">
<sm-copy value=${address}><p>${address}<p></sm-copy> <p class="label">Sender address</p>
</div> <sm-copy value=${address}><p>${address}<p></sm-copy>
</div>
`: ''}
<p> <p>
Balance: <b class="amount-shown" data-btc-amount=${balance}>${getConvertedAmount(balance, true)}</b> Balance: <b class="amount-shown" data-btc-amount=${balance}>${getConvertedAmount(balance, true)}</b>
</p> </p>
@ -2076,7 +2092,7 @@
const { privkey } = coinjs.wif2privkey(wif) const { privkey } = coinjs.wif2privkey(wif)
const privKey_arrayform = hex.decode(privkey) const privKey_arrayform = hex.decode(privkey)
const pubS = secp256k1_schnorr.getPublicKey(privKey_arrayform); const pubS = secp256k1_schnorr.getPublicKey(privKey_arrayform);
const tr = btc.p2tr(pubS); const tr = taproot.p2tr(pubS);
return { return {
tr, tr,
wif wif
@ -2114,45 +2130,66 @@
* @param {number} fee in BTC * @param {number} fee in BTC
*/ */
async function createTx(params = {}) { async function createTx(params = {}) {
let { senderPrivateKey, receivers = [], amounts = [], fee = 0, isTaprootScriptPath = false, tr, witness, signerSecretKey } = params
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
let {
senderAddress,
senderPrivateKey,
receivers = [],
amounts = [],
fee = 0,
isTaprootScriptPath = false,
scriptWitness,
userScript,
signerSecretKey,
userControlBlock,
} = params
try { try {
if (!tr) let tr
let address, script
let opts = {};
if (isTaprootScriptPath) {
address = senderAddress
script = hex.decode(coinjs.addressDecode(senderAddress).outstring)
opts = { allowUnknownInputs: true }
} else {
tr = getTaprootAddress(senderPrivateKey).tr tr = getTaprootAddress(senderPrivateKey).tr
const { address, script } = tr address = tr.address
const opts = {}; script = tr.script
}
const tx = new taproot.Transaction(opts); const tx = new taproot.Transaction(opts);
const totalAmount = amounts.reduce((total, amount) => total + amount, 0) const totalAmount = amounts.reduce((total, amount) => total + amount, 0)
const amountInSat = btcOperator.util.BTC_to_Sat(totalAmount) const amountInSat = btcOperator.util.BTC_to_Sat(totalAmount)
// check if sender has enough balance const [senderBalance, feeRate] = await Promise.all([btcOperator.getBalance(address), btcOperator.getFeeRate()]);
const senderBalance = await btcOperator.getBalance(address)
const feeRate = await btcOperator.getFeeRate();
let calculatedFee = 0; let calculatedFee = 0;
const { input_size } = await addUTXOs(tx, tr, [address], btcOperator.util.BTC_to_Sat(totalAmount), feeRate) const { input_size, consumedUtxoInputs } = await addUTXOs(tx, tr, [address], btcOperator.util.BTC_to_Sat(totalAmount), feeRate)
calculatedFee += input_size calculatedFee += input_size
// add receivers // add receivers
receivers.forEach((receiver, i) => { receivers.forEach((receiver, i) => {
tx.addOutputAddress(receiver, BigInt(btcOperator.util.BTC_to_Sat(amounts[i]))) tx.addOutputAddress(receiver, BigInt(btcOperator.util.BTC_to_Sat(amounts[i])))
calculatedFee += _sizePerOutput(receiver) calculatedFee += _sizePerOutput(receiver)
}) })
calculatedFee += _sizePerOutput(address) calculatedFee += _sizePerOutput(address) // add change output
calculatedFee = parseFloat((calculatedFee * feeRate).toFixed(8)) // convert to sat calculatedFee = parseFloat((calculatedFee * feeRate).toFixed(8)) // convert to sat
fee = fee || calculatedFee; // if fee is not provided, pass calculated fee fee = fee || calculatedFee; // if fee is not provided, pass calculated fee
// add change address // add change address
const changeAmount = senderBalance - (totalAmount + fee) const changeAmount = senderBalance - (totalAmount + fee)
if (changeAmount < 0) // if (changeAmount < 0)
return reject(`Insufficient balance. Required: ${getConvertedAmount(totalAmount + fee, true)}, Available: ${getConvertedAmount(btcOperator.util.Sat_to_BTC(senderBalance), true)}`) // return resolve({
// txHex: null,
// fee: calculatedFee
// })
tx.addOutputAddress(address, BigInt(btcOperator.util.BTC_to_Sat(changeAmount))); tx.addOutputAddress(address, BigInt(btcOperator.util.BTC_to_Sat(changeAmount)));
if (isTaprootScriptPath) { if (isTaprootScriptPath) {
// TODO: check this if (signerSecretKey) {
tx.witness = witness; const hash = tx.preimageWitnessV1(0, [script], 0, consumedUtxoInputs, "", userScript, userControlBlock.version)
tx.sign(signerSecretKey, undefined, new Uint8Array(32)); const sig = secp.schnorr.signSync(hash, signerSecretKey, new Uint8Array(32))
}
tx.inputs.forEach(input => input.finalScriptWitness = scriptWitness)
} else { } else {
const privKey = coinjs.wif2privkey(senderPrivateKey).privkey; const privKey = coinjs.wif2privkey(senderPrivateKey).privkey;
const privKey_arrayForm = hex.decode(privKey); tx.sign(hex.decode(privKey), undefined, new Uint8Array(32));
tx.sign(privKey_arrayForm, undefined, new Uint8Array(32)); tx.finalize()
} }
tx.finalize()
resolve({ txHex: tx.hex, fee }) resolve({ txHex: tx.hex, fee })
} catch (e) { } catch (e) {
reject(e) reject(e)
@ -2166,11 +2203,13 @@
rec_args.n = 0; rec_args.n = 0;
rec_args.input_size = 0; rec_args.input_size = 0;
rec_args.input_amount = 0; rec_args.input_amount = 0;
rec_args.consumedUtxoInputs = [];
} }
if (required_amount <= 0) if (required_amount <= 0)
return resolve({ return resolve({
input_size: rec_args.input_size, input_size: rec_args.input_size,
input_amount: rec_args.input_amount, input_amount: rec_args.input_amount,
consumedUtxoInputs: rec_args.consumedUtxoInputs,
change_amount: required_amount * -1 //required_amount will be -ve of change_amount change_amount: required_amount * -1 //required_amount will be -ve of change_amount
}); });
else if (rec_args.n >= senders.length) else if (rec_args.n >= senders.length)
@ -2185,15 +2224,25 @@
if (!confirmations) //ignore unconfirmed utxo if (!confirmations) //ignore unconfirmed utxo
continue; continue;
// changes for taproot // changes for taproot
const input = { txid: tx_hash_big_endian, index: tx_output_n, script: tr.script, amount: BigInt(value) } const inputScript = tr ? tr.script : hex.decode(coinjs.addressDecode(addr).outstring);
tx.addInput({ ...input, ...tr, witnessUtxo: { script: input.script, amount: input.amount } }); let input = {
txid: tx_hash_big_endian,
//TODO: verify this index: tx_output_n,
// tx.addInput({ ...input, ...taprootCommitment, witnessUtxo: { script: taprootCommitment.script, amount: input.amount }}); script: inputScript,
amount: BigInt(value),
witnessUtxo: {
script: inputScript,
amount: BigInt(value)
}
}
if (tr)
input = { ...input, ...tr }
console.log(input)
tx.addInput(input);
//update track values //update track values
rec_args.input_size += size_per_input; // Adjust input size calculation rec_args.input_size += size_per_input; // Adjust input size calculation
rec_args.input_amount += value; rec_args.input_amount += value;
rec_args.consumedUtxoInputs.push(value);
required_amount -= value; required_amount -= value;
if (fee_rate) //automatic fee calculation (dynamic) if (fee_rate) //automatic fee calculation (dynamic)
required_amount += size_per_input * fee_rate; required_amount += size_per_input * fee_rate;
@ -2283,6 +2332,62 @@
secretKey, secretKey,
}; };
}; };
// generate new private key
internalKeyPair = keyPairFromSecret(
'1229101a0fcf2104e8808dab35661134aa5903867d44deb73ce1c7e4eb925be8'
);
preimage = hashmini.sha256(
hex.decode('107661134f21fc7c02223d50ab9eb3600bc3ffc3712423a1e47bb1f9a9dbf55f')
);
aliceKeyPair = keyPairFromSecret(
'2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90'
);
bobKeyPair = keyPairFromSecret(
'81b637d8fcd2c6da6359e6963113a1170de795e4b725b84d1e0b4cfd9ec58ce9'
);
scriptAlice = new Uint8Array([
0x02,
144,
0x00,
btc.OP.CHECKSEQUENCEVERIFY,
btc.OP.DROP,
0x20,
...aliceKeyPair.schnorrPublicKey,
0xac,
]);
scriptBob = new Uint8Array([
btc.OP.SHA256,
0x20,
...preimage,
btc.OP.EQUALVERIFY,
0x20,
...bobKeyPair.schnorrPublicKey,
0xac,
]);
taprootTree = btc.taprootListToTree([
{
script: scriptAlice,
leafVersion: 0xc0,
},
{
script: scriptBob,
leafVersion: 0xc0,
},
]);
taprootCommitment = btc.p2tr(
internalKeyPair.schnorrPublicKey,
taprootTree,
undefined,
true
);
</script> </script>
</body> </body>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long