adding taproot script path

This commit is contained in:
sairaj mote 2023-10-30 01:46:30 +05:30
parent 807f343077
commit 1b5b44c7a1
6 changed files with 414 additions and 52 deletions

View File

@ -233,6 +233,10 @@ details[open] > summary .down-arrow {
transform: rotate(180deg); transform: rotate(180deg);
} }
fieldset {
border: none;
}
sm-input { sm-input {
--border-radius: 0.5rem; --border-radius: 0.5rem;
--background-color: rgba(var(--foreground-color), 1); --background-color: rgba(var(--foreground-color), 1);
@ -834,7 +838,9 @@ theme-toggle {
height: 0.3rem; height: 0.3rem;
background: var(--accent-color); background: var(--accent-color);
border-radius: 1rem 1rem 0 0; border-radius: 1rem 1rem 0 0;
z-index: 1; }
body.loaded .nav-item__indicator {
view-transition-name: indicator; view-transition-name: indicator;
} }
@ -1046,7 +1052,7 @@ theme-toggle {
} }
.primary-action { .primary-action {
padding: 0.5rem 0.7rem 0.5rem 0.5rem; padding: 0.6rem 0.8rem 0.6rem 0.6rem;
gap: 0.5rem; gap: 0.5rem;
white-space: normal; white-space: normal;
font-size: 0.9rem; font-size: 0.9rem;
@ -1216,6 +1222,56 @@ theme-toggle {
color: rgba(var(--text-color), 0.9); color: rgba(var(--text-color), 0.9);
} }
.steps {
display: flex;
counter-reset: step;
width: min(36rem, 100%);
}
.step {
counter-increment: step;
display: flex;
align-items: center;
position: relative;
height: 2rem;
flex: 1;
}
.step::before {
content: counter(step);
display: flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
background-color: rgba(var(--text-color), 0.03);
color: rgba(var(--text-color), 0.8);
font-weight: 500;
margin-right: 0.5rem;
aspect-ratio: 1/1;
font-size: 0.9rem;
}
.step:not(:last-of-type) {
margin-right: 0.5rem;
}
.step:not(:last-of-type)::after {
content: "";
width: 100%;
height: 0.1rem;
background-color: rgba(var(--text-color), 0.3);
}
.step.active::before {
background-color: var(--accent-color);
color: rgba(var(--background-color), 1);
}
.step.done::before {
background-color: var(--accent-color);
color: rgba(var(--background-color), 1);
}
.step.done::after {
background-color: var(--accent-color);
}
@media only screen and (max-width: 640px) { @media only screen and (max-width: 640px) {
.hide-on-small { .hide-on-small {
display: none; display: none;
@ -1283,15 +1339,18 @@ theme-toggle {
#convert_to_taproot_popup { #convert_to_taproot_popup {
--width: min(36rem, 100%); --width: min(36rem, 100%);
} }
#send_tx_form::part(form) { #send_tx_form::part(form),
#sender_receiver_form::part(form) {
gap: 1rem; gap: 1rem;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} }
#send_tx_form > * { #send_tx_form > fieldset,
#sender_receiver_form > fieldset {
align-self: flex-start; align-self: flex-start;
border-radius: 0.8rem; border-radius: 0.8rem;
padding: 1.5rem; padding: 1.5rem;
background-color: rgba(var(--text-color), 0.03); background-color: rgba(var(--text-color), 0.03);
border: none;
} }
#convert { #convert {
width: min(72rem, 100%); width: min(72rem, 100%);
@ -1336,4 +1395,12 @@ theme-toggle {
.button:not([disabled]):hover { .button:not([disabled]):hover {
filter: contrast(2); filter: contrast(2);
} }
}
@media (prefers-reduced-motion) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
-webkit-animation: none !important;
animation: none !important;
}
} }

2
css/main.min.css vendored

File diff suppressed because one or more lines are too long

View File

@ -212,7 +212,9 @@ details[open] {
transform: rotate(180deg); transform: rotate(180deg);
} }
} }
fieldset {
border: none;
}
sm-input { sm-input {
--border-radius: 0.5rem; --border-radius: 0.5rem;
--background-color: rgba(var(--foreground-color), 1); --background-color: rgba(var(--foreground-color), 1);
@ -774,7 +776,10 @@ theme-toggle {
height: 0.3rem; height: 0.3rem;
background: var(--accent-color); background: var(--accent-color);
border-radius: 1rem 1rem 0 0; border-radius: 1rem 1rem 0 0;
z-index: 1; }
}
body.loaded .nav-item {
&__indicator {
view-transition-name: indicator; view-transition-name: indicator;
} }
} }
@ -980,7 +985,7 @@ theme-toggle {
} }
.primary-action { .primary-action {
padding: 0.5rem 0.7rem 0.5rem 0.5rem; padding: 0.6rem 0.8rem 0.6rem 0.6rem;
gap: 0.5rem; gap: 0.5rem;
white-space: normal; white-space: normal;
font-size: 0.9rem; font-size: 0.9rem;
@ -1122,6 +1127,58 @@ theme-toggle {
color: rgba(var(--text-color), 0.9); color: rgba(var(--text-color), 0.9);
} }
} }
.steps {
display: flex;
counter-reset: step;
width: min(36rem, 100%);
}
.step {
counter-increment: step;
display: flex;
align-items: center;
position: relative;
height: 2rem;
flex: 1;
&::before {
content: counter(step);
display: flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
background-color: rgba(var(--text-color), 0.03);
color: rgba(var(--text-color), 0.8);
font-weight: 500;
margin-right: 0.5rem;
aspect-ratio: 1/1;
font-size: 0.9rem;
}
&:not(:last-of-type) {
margin-right: 0.5rem;
&::after {
content: "";
width: 100%;
height: 0.1rem;
background-color: rgba(var(--text-color), 0.3);
}
}
&.active {
&::before {
background-color: var(--accent-color);
color: rgba(var(--background-color), 1);
}
}
&.done {
&::before {
background-color: var(--accent-color);
color: rgba(var(--background-color), 1);
}
&::after {
background-color: var(--accent-color);
}
}
}
@media only screen and (max-width: 640px) { @media only screen and (max-width: 640px) {
.hide-on-small { .hide-on-small {
display: none; display: none;
@ -1192,16 +1249,18 @@ theme-toggle {
#convert_to_taproot_popup { #convert_to_taproot_popup {
--width: min(36rem, 100%); --width: min(36rem, 100%);
} }
#send_tx_form { #send_tx_form,
#sender_receiver_form {
&::part(form) { &::part(form) {
gap: 1rem; gap: 1rem;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} }
& > * { & > fieldset {
align-self: flex-start; align-self: flex-start;
border-radius: 0.8rem; border-radius: 0.8rem;
padding: 1.5rem; padding: 1.5rem;
background-color: rgba(var(--text-color), 0.03); background-color: rgba(var(--text-color), 0.03);
border: none;
} }
} }
#convert { #convert {
@ -1250,3 +1309,10 @@ theme-toggle {
} }
} }
} }
@media (prefers-reduced-motion) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}

View File

@ -389,18 +389,6 @@
notify('Browser is not fully compatible, some features may not work. for best experience please use Chrome, Edge, Firefox or Safari', 'error') notify('Browser is not fully compatible, some features may not work. for best experience please use Chrome, Edge, Firefox or Safari', 'error')
} }
document.body.classList.remove('hidden') document.body.classList.remove('hidden')
document.querySelectorAll('sm-input[data-btc-address]').forEach(input => input.customValidation = (value) => {
return {
isValid: btcOperator.validateAddress(value),
errorText: `Invalid address. It's a long string of random characters usually starting with '1','3' or 'bc1'.`
}
})
document.querySelectorAll('sm-input[data-private-key]').forEach(input => input.customValidation = (value) => {
return {
isValid: floCrypto.getPubKeyHex(value),
errorText: `Invalid private key. It's a long string of random characters usually starting with 'L' or 'R'.`
}
})
document.addEventListener('keyup', (e) => { document.addEventListener('keyup', (e) => {
if (e.key === 'Escape') { if (e.key === 'Escape') {
closePopup() closePopup()
@ -437,6 +425,7 @@
easing: 'ease' easing: 'ease'
}).onfinish = () => { }).onfinish = () => {
getRef('loading_page').remove() getRef('loading_page').remove()
document.body.classList.add('loaded')
} }
}, 500); }, 500);
document.querySelectorAll('.currency-symbol').forEach(el => el.innerHTML = currencyIcons[selectedCurrency]) document.querySelectorAll('.currency-symbol').forEach(el => el.innerHTML = currencyIcons[selectedCurrency])
@ -819,7 +808,17 @@
if (!value) return { isValid: false, errorText: 'Please enter a BTC address' } if (!value) return { isValid: false, errorText: 'Please enter a BTC address' }
return { return {
isValid: btcOperator.validateAddress(value), isValid: btcOperator.validateAddress(value),
errorText: `Invalid address.<br> It usually starts with "1", "3" or "bc1"` errorText: `Invalid address.<br> It usually starts with "1", "3" or "bc1."`
}
}
},
{
selector: '[data-taproot-address]',
customValidation: (value) => {
if (!value) return { isValid: false, errorText: 'Please enter a Taproot address' }
return {
isValid: btcOperator.validateAddress(value) === 'bech32m',
errorText: `Invalid Taproot address.<br> It starts with "bc1p."`
} }
} }
}, },
@ -829,7 +828,7 @@
if (!value) return { isValid: false, errorText: 'Please enter a private key' } if (!value) return { isValid: false, errorText: 'Please enter a private key' }
return { return {
isValid: floCrypto.getPubKeyHex(value), isValid: floCrypto.getPubKeyHex(value),
errorText: `Invalid private key.<br> It usually starts with "L" or "R"` errorText: `Invalid private key.<br> It usually starts with "L".`
} }
} }
} }
@ -1180,6 +1179,7 @@
}) })
router.addRoute('', (state) => { router.addRoute('', (state) => {
history.replaceState({}, '', '#/search') history.replaceState({}, '', '#/search')
renderSearch(state)
}) })
router.addRoute('search', renderSearch) router.addRoute('search', renderSearch)
function renderSearch(state) { function renderSearch(state) {
@ -1243,7 +1243,9 @@
const [suggestedFee, setSuggestedFee] = $signal(0); const [suggestedFee, setSuggestedFee] = $signal(0);
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);
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') {
@ -1265,20 +1267,89 @@
</sm-input> </sm-input>
` `
} }
renderElem(getRef('page_container'), html` let form = '';
<div class="flex flex-direction-column gap-1-5"> if (isScriptMode()) {
<h3> switch (atTaprootStep()) {
Perform Transaction case 1:
</h3> form = html`
<sm-form id="sender_receiver_form">
<fieldset class="flex flex-direction-column gap-1">
<div class="flex flex-direction-column gap-0-5">
<div class="flex space-between align-center">
<h4>Sender</h4>
<button id="check_balance_button" class="button button--small button--colored" disabled onclick="checkBalance()">
Check balance
</button>
</div>
<sm-input id="taproot_sender_input" placeholder="Sender's Taproot address" oninput=${handleSenderInput} data-taproot-address animate required> </sm-input>
<div id="sender_balance_container" class="flex align-center gap-0-3 hidden"></div>
</div>
<div class="flex flex-direction-column gap-0-5">
<h4>Receiver</h4>
<ul id="receivers_container" class="grid gap-1">
<li class="grid gap-0-5 receiver-wrapper">
<sm-input class="receiver-address" placeholder="Receiver's address" data-btc-address animate required></sm-input>
<sm-input class="receiver-amount amount-shown" placeholder="Amount" type="number" step="0.00000001" min="0.0000001" error-text="Amount should be grater than 0.0000001 BTC" animate required>
<div class="currency-symbol flex" slot="icon">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <g> <rect fill="none" height="24" width="24"></rect> </g> <g> <path d="M17.06,11.57C17.65,10.88,18,9.98,18,9c0-1.86-1.27-3.43-3-3.87L15,3h-2v2h-2V3H9v2H6v2h2v10H6v2h3v2h2v-2h2v2h2v-2 c2.21,0,4-1.79,4-4C19,13.55,18.22,12.27,17.06,11.57z M10,7h4c1.1,0,2,0.9,2,2s-0.9,2-2,2h-4V7z M15,17h-5v-4h5c1.1,0,2,0.9,2,2 S16.1,17,15,17z"> </path> </g> </svg>
</div>
</sm-input>
</li>
</ul>
<button class="button button--colored button--small margin-right-auto" onclick="addReceiver()">
Add receiver
</button>
</div>
</fieldset>
<fieldset class="flex flex-direction-column gap-1">
<sm-input id="script_input" placeholder="Script (hex)" pattern="^[0-9A-Fa-f]+$" error-text="Only hexadecimal values are allowed" animate required></sm-input>
<sm-input id="control_block_input" placeholder="Control block (hex)" pattern="^[0-9A-Fa-f]+$" error-text="Only hexadecimal values are allowed" animate required></sm-input>
<sm-input id="solutions_input" placeholder="No. of solutions" type="number" min="1" animate required></sm-input>
<sm-checkbox id="sign_transaction_checkbox">
<p style="margin-left: 0.5rem;">I want to sign the transaction</p>
</sm-checkbox>
<button class="button button--primary" type="submit" disabled onclick=${() => proceedTo(2)}>Proceed</button>
</fieldset>
</sm-form>
`;
break;
case 2:
form = html`
<sm-form id="script_details_form" style="max-width: 36rem">
<div class="flex flex-direction-column gap-1">
${[...Array(taprootScriptTxDetails.solutions).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>
`)}
</div>
${taprootScriptTxDetails.signTransaction ? html`
<sm-input id="signer_input" placeholder="Signer's private key" data-private-key class="password-field" type="password" animate required>
<svg class="icon" slot="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <g> <rect fill="none" height="24" width="24"></rect> </g> <g> <path d="M21,10h-8.35C11.83,7.67,9.61,6,7,6c-3.31,0-6,2.69-6,6s2.69,6,6,6c2.61,0,4.83-1.67,5.65-4H13l2,2l2-2l2,2l4-4.04L21,10z M7,15c-1.65,0-3-1.35-3-3c0-1.65,1.35-3,3-3s3,1.35,3,3C10,13.65,8.65,15,7,15z"> </path> </g> </svg>
<label slot="right" class="interact">
<input type="checkbox" class="hidden" autocomplete="off" readonly="" onchange="togglePrivateKeyVisibility(this)">
<svg class="icon invisible" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <title>Hide password</title> <path d="M0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0z" fill="none"></path> <path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z"> </path> </svg>
<svg class="icon visible" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <title>Show password</title> <path d="M0 0h24v24H0z" fill="none"></path> <path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"> </path> </svg>
</label>
</sm-input>
`: ''}
<div class="flex gap-0-5">
<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>
</sm-form>
`
break;
}
} else {
form = html`
<sm-form id="send_tx_form" onvalid=${calculateSuggestedFee} oninvalid=${handleInvalidForm} ?skip-submit=${feeType() === 'suggested'}> <sm-form id="send_tx_form" onvalid=${calculateSuggestedFee} oninvalid=${handleInvalidForm} ?skip-submit=${feeType() === 'suggested'}>
<div 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>
<sm-input id="private_key_input" placeholder="Sender's private key" data-private-key class="password-field" type="password" oninput=${handlePrivateKeyInput} animate required> <sm-input id="private_key_input" placeholder="Sender's private key" data-private-key class="password-field" type="password" oninput=${handleSenderInput} animate required>
<svg class="icon" slot="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <g> <rect fill="none" height="24" width="24"></rect> </g> <g> <path d="M21,10h-8.35C11.83,7.67,9.61,6,7,6c-3.31,0-6,2.69-6,6s2.69,6,6,6c2.61,0,4.83-1.67,5.65-4H13l2,2l2-2l2,2l4-4.04L21,10z M7,15c-1.65,0-3-1.35-3-3c0-1.65,1.35-3,3-3s3,1.35,3,3C10,13.65,8.65,15,7,15z"> </path> </g> </svg> <svg class="icon" slot="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <g> <rect fill="none" height="24" width="24"></rect> </g> <g> <path d="M21,10h-8.35C11.83,7.67,9.61,6,7,6c-3.31,0-6,2.69-6,6s2.69,6,6,6c2.61,0,4.83-1.67,5.65-4H13l2,2l2-2l2,2l4-4.04L21,10z M7,15c-1.65,0-3-1.35-3-3c0-1.65,1.35-3,3-3s3,1.35,3,3C10,13.65,8.65,15,7,15z"> </path> </g> </svg>
<label slot="right" class="interact"> <label slot="right" class="interact">
<input type="checkbox" class="hidden" autocomplete="off" readonly="" onchange="togglePrivateKeyVisibility(this)"> <input type="checkbox" class="hidden" autocomplete="off" readonly="" onchange="togglePrivateKeyVisibility(this)">
@ -1287,8 +1358,8 @@
</label> </label>
</sm-input> </sm-input>
<div id="sender_balance_container" class="flex align-center gap-0-3 hidden"></div> <div id="sender_balance_container" class="flex align-center gap-0-3 hidden"></div>
</div> </fieldset>
<div class="flex flex-direction-column gap-1"> <fieldset class="flex flex-direction-column gap-1">
<div class="flex flex-direction-column gap-0-5"> <div class="flex flex-direction-column gap-0-5">
<h4>Receiver</h4> <h4>Receiver</h4>
<ul id="receivers_container" class="grid gap-1"> <ul id="receivers_container" class="grid gap-1">
@ -1318,8 +1389,29 @@
<div class="multi-state-button"> <div class="multi-state-button">
<button id="send_tx_button" class="button button--primary" type="submit" disabled onclick="sendTx()">Send</button> <button id="send_tx_button" class="button button--primary" type="submit" disabled onclick="sendTx()">Send</button>
</div> </div>
</div> </fieldset>
</sm-form> </sm-form>
`;
}
renderElem(getRef('page_container'), html`
<div class="flex flex-direction-column gap-1-5">
<div class="flex flex-direction-column gap-0-5">
<h3>
Perform Transaction
</h3>
<sm-switch style="width: fit-content" onchange=${e => setIsScriptMode(!isScriptMode())}>
<p slot="left" style="margin-right: 0.5rem">Toggle script mode</p>
</sm-switch>
</div>
${isScriptMode() ? html`
<ul id="taproot_steps" class="steps">
${[1, 2].map(step => html`
<li class=${`step ${atTaprootStep() === step ? 'active' : ''} ${atTaprootStep() > step ? 'done' : ''}`}></li>
`)}
</ul>
`: ''}
${form}
</div> </div>
`) `)
}) })
@ -1331,11 +1423,11 @@
} else { } else {
if (!getRef('send_tx_form').isFormValid) return if (!getRef('send_tx_form').isFormValid) return
const { senderPrivateKey, senderAddress, receivers, receiverAddresses, receiverAmounts } = getTransactionDetails() const { senderPrivateKey, senderAddress, receiverAddresses, receiverAmounts } = getTransactionDetails()
if (!senderPrivateKey || !senderAddress || !receiverAddresses.length || !receiverAmounts.length) if (!senderPrivateKey || !senderAddress || !receiverAddresses.length || !receiverAmounts.length)
return return
setSuggestedFeeStatus(['calculating']) setSuggestedFeeStatus(['calculating'])
const { fee } = await createTx(senderPrivateKey, receiverAddresses, receiverAmounts) const { fee } = await createTx({ senderPrivateKey, receivers: receiverAddresses, amounts: receiverAmounts })
setSuggestedFee(fee) setSuggestedFee(fee)
setSuggestedFeeStatus(['success']) setSuggestedFeeStatus(['success'])
getRef('send_tx_button').disabled = false getRef('send_tx_button').disabled = false
@ -1350,6 +1442,117 @@
setSuggestedFeeStatus(['idle']); setSuggestedFeeStatus(['idle']);
getRef('send_tx_button').disabled = true getRef('send_tx_button').disabled = true
} }
let taprootScriptTxDetails = {};
function proceedTo(step) {
if (step) {
switch (atTaprootStep()) {
case 1:
if (!getRef('sender_receiver_form').isFormValid) {
notify('Please fill all the details', 'error')
return
}
const { receiverAddresses, receiverAmounts } = getTransactionReceivers()
const senderAddress = getRef('taproot_sender_input').value.trim()
taprootScriptTxDetails = {
senderAddress, receiverAddresses, receiverAmounts,
script: getRef('script_input').value.trim(),
controlBlock: getRef('control_block_input').value.trim(),
solutions: parseInt(getRef('solutions_input').value.trim()),
signTransaction: getRef('sign_transaction_checkbox').checked
}
break;
case 2:
break;
}
setAtTaprootStep(step)
} else {
setAtTaprootStep(atTaprootStep() + 1)
}
}
async function sendScriptPathTx() {
try {
const scriptWitness = []
const solutions = document.querySelectorAll('.solution-input')
solutions.forEach(solution => {
const decodedSolution = hex.decode(solution.value.trim())
scriptWitness.push(decodedSolution)
})
const { senderAddress, receiverAddresses, receiverAmounts, script, controlBlock, signTransaction } = taprootScriptTxDetails
scriptWitness.push(hex.decode(script), hex.decode(controlBlock))
scriptWitness = scriptWitness.map(witness => hex.encode(witness))
let signerSecretKey
if (signTransaction) {
const signerPrivateKey = getRef('signer_input').value.trim()
if (!signerPrivateKey) {
notify(`Please enter the signer's private key`, 'error')
return
}
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', {
message: html`
<div class="grid gap-1-5">
<div class="grid">
<span class="label">Sender address</span>
<sm-copy value=${senderAddress}></sm-copy>
</div>
<div class="grid">
<span class="label">Receivers</span>
<div class="grid gap-0-3">
${receiverAddresses.map((address, index) => html.node/*html*/`
<div class="grid gap-0-5" style="padding:0.5rem;border:solid thin rgba(var(--text-color),0.3);border-radius: 0.3rem;">
<b>${address}</b>
<b class="amount-shown" data-btc-amount=${receiverAmounts[index]}>${getConvertedAmount(receiverAmounts[index], true)}</b>
</div>
`)}
</div>
</div>
<div class="grid">
<span class="label">Fee</span>
<b class="amount-shown" data-btc-amount=${fee}>${getConvertedAmount(fee, true)}</b>
</div>
</div>
`,
confirmText: 'Send',
})
if (!confirmation)
return;
buttonLoader('taproot_script_path_send_button', true)
const { txHex } = await createTx({
senderAddress,
receivers: receiverAddresses,
amounts: receiverAmounts,
scriptWitness,
tr: taprootCommitment,
signerSecretKey
})
console.log(txHex)
btcOperator.broadcastTx(txHex).then(txid => {
showTransactionResult(true, txid)
}).catch(err => {
showTransactionResult(false, err)
}).finally(() => {
buttonLoader('taproot_script_path_send_button', false)
})
} catch (e) {
notify(e, 'error')
buttonLoader('taproot_script_path_send_button', false)
}
}
router.addRoute('convert', (state) => { router.addRoute('convert', (state) => {
const convertingFrom = state.params.from || 'flo'; const convertingFrom = state.params.from || 'flo';
let privateKeyConversionDescription = ''; let privateKeyConversionDescription = '';
@ -1556,7 +1759,7 @@
notify(e, 'error') notify(e, 'error')
}) })
} }
function handlePrivateKeyInput(e) { function handleSenderInput(e) {
getRef('check_balance_button').disabled = !e.target.isValid getRef('check_balance_button').disabled = !e.target.isValid
if (!e.target.isValid) { if (!e.target.isValid) {
getRef('sender_balance_container').classList.add('hidden') getRef('sender_balance_container').classList.add('hidden')
@ -1605,11 +1808,7 @@
function removeReceiver(button) { function removeReceiver(button) {
button.closest('.receiver-wrapper').remove() button.closest('.receiver-wrapper').remove()
} }
function getTransactionDetails() { function getTransactionReceivers() {
const senderPrivateKey = getRef('private_key_input').value.trim();
const senderAddress = getTaprootAddress(senderPrivateKey).tr.address;
if (btcOperator.validateAddress(senderAddress) !== 'bech32m')
return notify('Sender address is not a Taproot address', 'error')
const receivers = [...getRef('receivers_container').children].reduce((receivers, receiver) => { const receivers = [...getRef('receivers_container').children].reduce((receivers, receiver) => {
const receiverAddress = receiver.querySelector('.receiver-address').value.trim() const receiverAddress = receiver.querySelector('.receiver-address').value.trim()
let amount = parseFloat(receiver.querySelector('.receiver-amount').value.trim()) let amount = parseFloat(receiver.querySelector('.receiver-amount').value.trim())
@ -1621,6 +1820,14 @@
}, {}) }, {})
const receiverAddresses = Object.keys(receivers) const receiverAddresses = Object.keys(receivers)
const receiverAmounts = Object.values(receivers) const receiverAmounts = Object.values(receivers)
return { receivers, receiverAddresses, receiverAmounts }
}
function getTransactionDetails() {
const senderPrivateKey = getRef('private_key_input').value.trim();
const senderAddress = getTaprootAddress(senderPrivateKey).tr.address;
if (btcOperator.validateAddress(senderAddress) !== 'bech32m')
return notify('Sender address is not a Taproot address', 'error')
const { receivers, receiverAddresses, receiverAmounts } = getTransactionReceivers()
let fee = parseFloat(getRef('fee_input')?.value.trim()) || 0 let fee = parseFloat(getRef('fee_input')?.value.trim()) || 0
fee = fee / (globalExchangeRate[selectedCurrency] || 1) fee = fee / (globalExchangeRate[selectedCurrency] || 1)
return { senderPrivateKey, senderAddress, receivers, receiverAddresses, receiverAmounts, fee } return { senderPrivateKey, senderAddress, receivers, receiverAddresses, receiverAmounts, fee }
@ -1657,7 +1864,7 @@
if (!confirmation) if (!confirmation)
return; return;
buttonLoader('send_tx_button', true) buttonLoader('send_tx_button', true)
const { txHex } = await createTx(senderPrivateKey, receiverAddresses, receiverAmounts, fee) const { txHex } = await createTx({ senderPrivateKey, receivers: receiverAddresses, amounts: receiverAmounts, fee })
console.log(txHex) console.log(txHex)
btcOperator.broadcastTx(txHex).then(txid => { btcOperator.broadcastTx(txHex).then(txid => {
showTransactionResult(true, txid) showTransactionResult(true, txid)
@ -1906,11 +2113,12 @@
* @param {array} array of amounts in BTC * @param {array} array of amounts in BTC
* @param {number} fee in BTC * @param {number} fee in BTC
*/ */
async function createTx(senderPrivateKey, receivers = [], amounts = [], fee = 0) { async function createTx(params = {}) {
console.log(amounts) let { senderPrivateKey, receivers = [], amounts = [], fee = 0, isTaprootScriptPath = false, tr, witness, signerSecretKey } = params
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
const { tr } = getTaprootAddress(senderPrivateKey) if (!tr)
tr = getTaprootAddress(senderPrivateKey).tr
const { address, script } = tr const { address, script } = tr
const opts = {}; const opts = {};
const tx = new taproot.Transaction(opts); const tx = new taproot.Transaction(opts);
@ -1935,9 +2143,15 @@
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 reject(`Insufficient balance. Required: ${getConvertedAmount(totalAmount + fee, true)}, Available: ${getConvertedAmount(btcOperator.util.Sat_to_BTC(senderBalance), true)}`)
tx.addOutputAddress(address, BigInt(btcOperator.util.BTC_to_Sat(changeAmount))); tx.addOutputAddress(address, BigInt(btcOperator.util.BTC_to_Sat(changeAmount)));
const privKey = coinjs.wif2privkey(senderPrivateKey).privkey; if (isTaprootScriptPath) {
const privKey_arrayForm = hex.decode(privKey); // TODO: check this
tx.sign(privKey_arrayForm, undefined, new Uint8Array(32)); tx.witness = witness;
tx.sign(signerSecretKey, undefined, new Uint8Array(32));
} else {
const privKey = coinjs.wif2privkey(senderPrivateKey).privkey;
const privKey_arrayForm = hex.decode(privKey);
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) {
@ -1972,8 +2186,10 @@
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 input = { txid: tx_hash_big_endian, index: tx_output_n, script: tr.script, amount: BigInt(value) }
tx.addInput({ ...input, ...tr, witnessUtxo: { script: input.script, amount: input.amount }, }); tx.addInput({ ...input, ...tr, witnessUtxo: { script: input.script, amount: input.amount } });
//TODO: verify this
// tx.addInput({ ...input, ...taprootCommitment, witnessUtxo: { script: taprootCommitment.script, amount: input.amount }});
//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
@ -2057,6 +2273,17 @@
}) })
} }
</script> </script>
<script>
keyPairFromSecret = (hexSecretKey) => {
const secretKey = hex.decode(hexSecretKey);
const schnorrPublicKey = secp256k1_schnorr.getPublicKey(secretKey);
return {
schnorrPublicKey,
secretKey,
};
};
</script>
</body> </body>
</html> </html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long