Added auto fee calculation
This commit is contained in:
parent
062c48af1b
commit
354abe7cd6
@ -237,7 +237,7 @@ sm-input {
|
||||
}
|
||||
|
||||
sm-spinner {
|
||||
--size: 1.5rem;
|
||||
--size: 1.3rem;
|
||||
--stroke-width: 0.1rem;
|
||||
}
|
||||
|
||||
@ -617,6 +617,7 @@ ul {
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
isolation: isolate;
|
||||
}
|
||||
.multi-state-button > * {
|
||||
grid-area: 1/1/2/2;
|
||||
|
||||
2
css/main.min.css
vendored
2
css/main.min.css
vendored
File diff suppressed because one or more lines are too long
@ -217,7 +217,7 @@ sm-input {
|
||||
}
|
||||
|
||||
sm-spinner {
|
||||
--size: 1.5rem;
|
||||
--size: 1.3rem;
|
||||
--stroke-width: 0.1rem;
|
||||
}
|
||||
|
||||
@ -576,6 +576,7 @@ ul {
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
isolation: isolate;
|
||||
& > * {
|
||||
grid-area: 1/1/2/2;
|
||||
}
|
||||
|
||||
439
index.html
439
index.html
@ -53,7 +53,7 @@
|
||||
<div class="nav-item__indicator"></div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<li class="hidden">
|
||||
<a href="#/convert" class="nav-item interactive">
|
||||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
||||
width="24px" fill="#000000">
|
||||
@ -62,7 +62,8 @@
|
||||
</svg>
|
||||
<span class="nav-item__title">
|
||||
Convert
|
||||
</span></a>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
@ -569,6 +570,60 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
let currentSubscriber = null;
|
||||
/**
|
||||
* @param {any} initialValue - initial value for the signal
|
||||
* @param {function} [Optional] callback - function to be called when the signal changes
|
||||
* @returns {array} - array containing getter and setter for the signal
|
||||
* @example
|
||||
* const [getCount, setCount] = $signal(0);
|
||||
*/
|
||||
function $signal(initialValue, callback) {
|
||||
let value = initialValue;
|
||||
const subscribers = new Set();
|
||||
let hasCustomSubscriber = false;
|
||||
function getter(subscriber) {
|
||||
if (currentSubscriber) {
|
||||
subscribers.add(currentSubscriber);
|
||||
}
|
||||
if (!hasCustomSubscriber && subscriber) {
|
||||
subscribers.add(subscriber)
|
||||
hasCustomSubscriber = true
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function setter(newValue) {
|
||||
if (newValue === value) return;
|
||||
value = newValue;
|
||||
for (const subscriber of subscribers) {
|
||||
subscriber();
|
||||
}
|
||||
}
|
||||
return [getter, setter];
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {function} fn - function that will run if any of its dependent signals change
|
||||
* @example
|
||||
* $effect(() => {
|
||||
* console.log(count());
|
||||
* }
|
||||
* @returns {void}
|
||||
*/
|
||||
async function $effect(fn) {
|
||||
currentSubscriber = fn;
|
||||
const result = fn();
|
||||
try {
|
||||
if (result instanceof Promise) {
|
||||
await result;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
currentSubscriber = null;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
window.smCompConfig = {
|
||||
@ -576,7 +631,7 @@
|
||||
{
|
||||
selector: '[data-btc-address]',
|
||||
customValidation: (value) => {
|
||||
if (!value) return { isValid: false, errorText: 'Please enter a FLO address' }
|
||||
if (!value) return { isValid: false, errorText: 'Please enter a BTC address' }
|
||||
return {
|
||||
isValid: btcOperator.validateAddress(value),
|
||||
errorText: `Invalid address.<br> It usually starts with "1", "3" or "bc1"`
|
||||
@ -631,70 +686,129 @@
|
||||
renderHome(state)
|
||||
})
|
||||
router.addRoute('home', renderHome)
|
||||
const [suggestedFee, setSuggestedFee] = $signal(0);
|
||||
const [suggestedFeeStatus, setSuggestedFeeStatus] = $signal(['idle'])
|
||||
const [feeType, setFeeType] = $signal('suggested')
|
||||
function renderHome(state) {
|
||||
console.log(state)
|
||||
renderElem(getRef('page_container'), html`
|
||||
<div class="flex flex-direction-column gap-1-5">
|
||||
<menu class="flex gap-0-5">
|
||||
<li>
|
||||
<button class="button button--colored primary-action" onclick="openPopup('generate_address_popup')">
|
||||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <path d="M0 0h24v24H0V0z" fill="none" /> <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z" /> </svg>
|
||||
Create new address
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="button button--colored primary-action" onclick="openPopup('convert_to_taproot_popup')">
|
||||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <path d="M0 0h24v24H0V0z" fill="none" /> <path d="M6.99 11L3 15l3.99 4v-3H14v-2H6.99v-3zM21 9l-3.99-4v3H10v2h7.01v3L21 9z" /> </svg>
|
||||
Retrieve Taproot address
|
||||
</button>
|
||||
</li>
|
||||
</menu>
|
||||
<h3>
|
||||
Perform Transaction
|
||||
</h3>
|
||||
<sm-form>
|
||||
<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
|
||||
$effect(() => {
|
||||
let feeSection = ''
|
||||
if (feeType() === 'suggested') {
|
||||
const [status, message] = suggestedFeeStatus()
|
||||
if (status === 'idle') {
|
||||
feeSection = html` <p>*Fee will be calculated after you enter all the details</p> `
|
||||
} else if (status === 'calculating') {
|
||||
feeSection = html`<div class="flex align-center"> Calculating fee...<sm-spinner></sm-spinner> </div>`
|
||||
} else if (status === 'error') {
|
||||
feeSection = html`<p class="error">${message}</p>`
|
||||
} else if (status === 'success') {
|
||||
feeSection = html`
|
||||
<sm-input id="fee_input" placeholder="Suggested fee" type="number" value=${suggestedFee()} step="0.00000001" min="0.0000001" readonly animate required></sm-input>
|
||||
`
|
||||
}
|
||||
} else {
|
||||
feeSection = html`
|
||||
<sm-input id="fee_input" placeholder="Custom fee" type="number" step="0.00000001" min="0.0000001" error-text="Minimum fee is 0.0000001BTC" animate required>
|
||||
</sm-input>
|
||||
`
|
||||
}
|
||||
renderElem(getRef('page_container'), html`
|
||||
<div class="flex flex-direction-column gap-1-5">
|
||||
<menu class="flex gap-0-5">
|
||||
<li>
|
||||
<button class="button button--colored primary-action" onclick="openPopup('generate_address_popup')">
|
||||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <path d="M0 0h24v24H0V0z" fill="none" /> <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z" /> </svg>
|
||||
Create new address
|
||||
</button>
|
||||
</div>
|
||||
<sm-input id="private_key_input" placeholder="Sender's private key" data-private-key class="password-field" type="password" oninput=${handlePrivateKeyInput} 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 id="sender_balance_container" class="flex align-center gap-0-3 hidden"></div>
|
||||
</div>
|
||||
<div class="flex flex-direction-column gap-1">
|
||||
</li>
|
||||
<li>
|
||||
<button class="button button--colored primary-action" onclick="openPopup('convert_to_taproot_popup')">
|
||||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <path d="M0 0h24v24H0V0z" fill="none" /> <path d="M6.99 11L3 15l3.99 4v-3H14v-2H6.99v-3zM21 9l-3.99-4v3H10v2h7.01v3L21 9z" /> </svg>
|
||||
Retrieve Taproot address
|
||||
</button>
|
||||
</li>
|
||||
</menu>
|
||||
<h3>
|
||||
Perform Transaction
|
||||
</h3>
|
||||
<sm-form id="send_tx_form" onvalid=${calculateSuggestedFee} oninvalid=${handleInvalidForm} ?skip-submit=${feeType() === 'suggested'}>
|
||||
<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 required></sm-input>
|
||||
<sm-input class="receiver-amount" placeholder="Amount" type="number" step="0.00000001" min="0.0000001" error-text="Amount should be grater than 0.0000001 BTC" 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 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="private_key_input" placeholder="Sender's private key" data-private-key class="password-field" type="password" oninput=${handlePrivateKeyInput} 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 id="sender_balance_container" class="flex align-center gap-0-3 hidden"></div>
|
||||
</div>
|
||||
<sm-input id="fee_input" placeholder="Fee" required></sm-input>
|
||||
<div class="multi-state-button">
|
||||
<button id="send_tx_button" class="button button--primary" type="submit" disabled onclick="sendTx()">Send</button>
|
||||
<div class="flex flex-direction-column gap-1">
|
||||
<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" 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>
|
||||
<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(); }}>
|
||||
<sm-chip value="suggested" selected>Suggested</sm-chip>
|
||||
<sm-chip value="custom">Custom</sm-chip>
|
||||
</sm-chips>
|
||||
</div>
|
||||
${feeSection}
|
||||
</div>
|
||||
<div class="multi-state-button">
|
||||
<button id="send_tx_button" class="button button--primary" type="submit" disabled onclick="sendTx()">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</sm-form>
|
||||
</div>
|
||||
`)
|
||||
</sm-form>
|
||||
</div>
|
||||
`)
|
||||
})
|
||||
}
|
||||
async function calculateSuggestedFee() {
|
||||
try {
|
||||
getRef('send_tx_button').disabled = true
|
||||
if (feeType() === 'custom') {
|
||||
|
||||
} else {
|
||||
if (!getRef('send_tx_form').isFormValid) return
|
||||
const { senderPrivateKey, senderAddress, receivers, receiverAddresses, receiverAmounts } = getTransactionDetails()
|
||||
if (!senderPrivateKey || !senderAddress || !receiverAddresses.length || !receiverAmounts.length)
|
||||
return
|
||||
setSuggestedFeeStatus(['calculating'])
|
||||
const { fee } = await createTx(senderPrivateKey, receiverAddresses, receiverAmounts)
|
||||
setSuggestedFee(fee)
|
||||
setSuggestedFeeStatus(['success'])
|
||||
getRef('send_tx_button').disabled = false
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
setSuggestedFeeStatus(['error', e])
|
||||
}
|
||||
}
|
||||
function handleInvalidForm() {
|
||||
setSuggestedFee(0);
|
||||
setSuggestedFeeStatus(['idle']);
|
||||
getRef('send_tx_button').disabled = true
|
||||
}
|
||||
router.addRoute('convert', (state) => {
|
||||
renderElem(getRef('page_container'), html`
|
||||
@ -718,20 +832,25 @@
|
||||
`)
|
||||
const { tr: { address } } = getTaprootAddress(wif)
|
||||
btcOperator.getBalance(address).then(balance => {
|
||||
console.log(balance)
|
||||
renderElem(getRef('sender_balance_container'), html`
|
||||
Balance: <b>${formatAmount(balance)}</b>
|
||||
<div class="grid gap-1" style="padding: 1rem; border-radius: 0.5rem; border: solid thin rgba(var(--text-color),0.3)">
|
||||
<div class="grid">
|
||||
<p class="label">Sender address</p>
|
||||
<sm-copy value=${address}><p>${address}<p></sm-copy>
|
||||
</div>
|
||||
<p>
|
||||
Balance: <b>${formatAmount(balance)}</b>
|
||||
</p>
|
||||
</div>
|
||||
`)
|
||||
}).catch(err => {
|
||||
notify(e, 'error')
|
||||
})
|
||||
}
|
||||
function handlePrivateKeyInput(e) {
|
||||
getRef('check_balance_button').disabled = !e.target.isValid
|
||||
if (!e.target.isValid) {
|
||||
getRef('sender_balance_container').classList.add('hidden')
|
||||
getRef('check_balance_button').disabled = true
|
||||
} else {
|
||||
getRef('check_balance_button').disabled = false
|
||||
}
|
||||
}
|
||||
getRef('convert_to_taproot_form').addEventListener('invalid', () => {
|
||||
@ -758,7 +877,7 @@
|
||||
})
|
||||
}
|
||||
function addReceiver() {
|
||||
getRef('receivers_container').append(html.node`
|
||||
getRef('receivers_container').append(html.node/*html*/`
|
||||
<div class="grid gap-0-5 receiver-wrapper">
|
||||
<sm-input class="receiver-address" placeholder="Receiver's address" data-btc-address
|
||||
required></sm-input>
|
||||
@ -776,8 +895,11 @@
|
||||
function removeReceiver(button) {
|
||||
button.closest('.receiver-wrapper').remove()
|
||||
}
|
||||
async function sendTx() {
|
||||
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 = [...getRef('receivers_container').children].reduce((receivers, receiver) => {
|
||||
const receiverAddress = receiver.querySelector('.receiver-address').value.trim()
|
||||
const amount = parseFloat(receiver.querySelector('.receiver-amount').value.trim())
|
||||
@ -786,54 +908,56 @@
|
||||
receivers[receiverAddress] += amount
|
||||
return receivers
|
||||
}, {})
|
||||
console.log(receivers, Object.keys(receivers), Object.values(receivers))
|
||||
const fee = parseFloat(getRef('fee_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 confirmation = await getConfirmation('Confirm transaction', {
|
||||
message: html`
|
||||
<div class="grid gap-1-5">
|
||||
<div class="grid gap-0-5">
|
||||
<span class="label">Sender address</span>
|
||||
<sm-copy value=${senderAddress}></sm-copy>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<span class="label">Receivers</span>
|
||||
<div class="grid gap-0-3">
|
||||
${Object.entries(receivers).map(([address, amount]) => html.node`
|
||||
<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>${formatAmount(amount)}</b>
|
||||
</div>
|
||||
`)}
|
||||
const receiverAddresses = Object.keys(receivers)
|
||||
const receiverAmounts = Object.values(receivers)
|
||||
const fee = parseFloat(getRef('fee_input')?.value.trim()) || 0
|
||||
return { senderPrivateKey, senderAddress, receivers, receiverAddresses, receiverAmounts, fee }
|
||||
}
|
||||
async function sendTx() {
|
||||
try {
|
||||
const { senderPrivateKey, senderAddress, receivers, receiverAddresses, receiverAmounts, fee } = getTransactionDetails()
|
||||
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">
|
||||
${Object.entries(receivers).map(([address, amount]) => html.node`
|
||||
<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>${formatAmount(amount)}</b>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<span class="label">Fee</span>
|
||||
<b>${formatAmount(fee)}</b>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<span class="label">Fee</span>
|
||||
<b>${formatAmount(fee)}</b>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
confirmText: 'Send',
|
||||
})
|
||||
if (!confirmation)
|
||||
return;
|
||||
buttonLoader('send_tx_button', true)
|
||||
createTx(senderPrivateKey, Object.keys(receivers), Object.values(receivers), fee).then(txHex => {
|
||||
`,
|
||||
confirmText: 'Send',
|
||||
})
|
||||
if (!confirmation)
|
||||
return;
|
||||
buttonLoader('send_tx_button', true)
|
||||
const { txHex } = await createTx(senderPrivateKey, receiverAddresses, receiverAmounts, fee)
|
||||
console.log(txHex)
|
||||
btcOperator.broadcastTx(txHex).then(txid => {
|
||||
notify(`Transaction sent successfully. Txid: ${txid}`, 'success')
|
||||
showTransactionResult(true, txid)
|
||||
}).catch(err => {
|
||||
notify(err, 'error')
|
||||
showTransactionResult(false, err)
|
||||
}).finally(() => {
|
||||
buttonLoader('send_tx_button', false)
|
||||
})
|
||||
}).catch(err => {
|
||||
notify(err, 'error')
|
||||
} catch (e) {
|
||||
notify(e, 'error')
|
||||
buttonLoader('send_tx_button', false)
|
||||
})
|
||||
}
|
||||
}
|
||||
function showTransactionResult(success, result, options = {}) {
|
||||
let { title, description } = options
|
||||
@ -889,8 +1013,20 @@
|
||||
|
||||
const util = {};
|
||||
|
||||
util.Sat_to_BTC = value => BigInt(parseFloat((value / SATOSHI_IN_BTC).toFixed(8)));
|
||||
util.BTC_to_Sat = value => BigInt(parseInt(value * SATOSHI_IN_BTC));
|
||||
util.Sat_to_BTC = value => parseFloat((value / SATOSHI_IN_BTC).toFixed(8));
|
||||
util.BTC_to_Sat = value => parseInt(value * SATOSHI_IN_BTC);
|
||||
function get_fee_rate() {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch('https://api.blockchain.info/mempool/fees').then(response => {
|
||||
if (response.ok)
|
||||
response.json()
|
||||
.then(result => resolve(util.Sat_to_BTC(result.regular)))
|
||||
.catch(error => reject(error));
|
||||
else
|
||||
reject(response);
|
||||
}).catch(error => reject(error))
|
||||
})
|
||||
}
|
||||
|
||||
const fetch_api = function (api, json_res = true) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -951,39 +1087,48 @@
|
||||
* @param {array} array of amounts in BTC
|
||||
* @param {number} fee in BTC
|
||||
*/
|
||||
async function createTx(senderPrivateKey, receivers = [], amounts = [], fee) {
|
||||
try {
|
||||
const { tr: { address, script } } = getTaprootAddress(senderPrivateKey)
|
||||
const opts = {};
|
||||
const tx = new taproot.Transaction(opts);
|
||||
const totalAmount = amounts.reduce((total, amount) => total + amount, 0)
|
||||
const amountInSat = util.BTC_to_Sat(totalAmount)
|
||||
// check if sender has enough balance
|
||||
btcOperator.getBalance(address).then(balance => {
|
||||
if (balance < totalAmount + fee)
|
||||
throw new Error(`Insufficient balance. Balance: ${balance}, Required: ${totalAmount + fee}`)
|
||||
}).catch(err => {
|
||||
throw new Error(err)
|
||||
})
|
||||
await addUTXOs(tx, tr, [address], util.BTC_to_Sat(amount))
|
||||
// add receivers
|
||||
receivers.forEach((receiver, i) => {
|
||||
tx.addOutputAddress(receiver, util.BTC_to_Sat(amounts[i]))
|
||||
})
|
||||
// add change address
|
||||
tx.addOutputAddress(address, tx.inputAmount - amountInSat - util.BTC_to_Sat(fee));
|
||||
|
||||
tx.sign(privKey_arrayform, undefined, new Uint8Array(32));
|
||||
tx.finalize()
|
||||
return tx.hex
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
async function createTx(senderPrivateKey, receivers = [], amounts = [], fee = 0) {
|
||||
console.log(amounts)
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const { tr } = getTaprootAddress(senderPrivateKey)
|
||||
const { address, script } = tr
|
||||
const opts = {};
|
||||
const tx = new taproot.Transaction(opts);
|
||||
const totalAmount = amounts.reduce((total, amount) => total + amount, 0)
|
||||
const amountInSat = util.BTC_to_Sat(totalAmount)
|
||||
// check if sender has enough balance
|
||||
const senderBalance = await btcOperator.getBalance(address)
|
||||
const feeRate = await get_fee_rate();
|
||||
let calculatedFee = 0
|
||||
const { input_size } = await addUTXOs(tx, tr, [address], util.BTC_to_Sat(totalAmount), feeRate)
|
||||
calculatedFee += input_size
|
||||
// add receivers
|
||||
receivers.forEach((receiver, i) => {
|
||||
tx.addOutputAddress(receiver, BigInt(util.BTC_to_Sat(amounts[i])))
|
||||
calculatedFee += _sizePerOutput(receiver)
|
||||
})
|
||||
calculatedFee += _sizePerOutput(address)
|
||||
calculatedFee = parseFloat((calculatedFee * feeRate).toFixed(8)) // convert to sat
|
||||
fee = fee || calculatedFee; // if fee is not provided, pass calculated fee
|
||||
// add change address
|
||||
const changeAmount = senderBalance - (totalAmount + fee)
|
||||
if (changeAmount < 0)
|
||||
return reject(`Insufficient balance. Required: ${totalAmount + fee} BTC, Available: ${util.Sat_to_BTC(senderBalance)} BTC`)
|
||||
tx.addOutputAddress(address, BigInt(util.BTC_to_Sat(changeAmount)));
|
||||
const privKey = coinjs.wif2privkey(senderPrivateKey).privkey;
|
||||
const privKey_arrayForm = hex.decode(privKey);
|
||||
tx.sign(privKey_arrayForm, undefined, new Uint8Array(32));
|
||||
tx.finalize()
|
||||
resolve({ txHex: tx.hex, fee })
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
const testTaproot = 'bc1p05whkacavgmh77pgsr7v5k4tyg28x3pqyjanfnjkzzdngqur4yks39w8zk' //remove this later
|
||||
function addUTXOs(tx, tr, senders = [testTaproot], required_amount, fee_rate, rec_args = {}) {
|
||||
function addUTXOs(tx, tr, senders = [], required_amount, fee_rate, rec_args = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
required_amount = parseFloat(required_amount.toFixed(8));
|
||||
required_amount = parseFloat(required_amount);
|
||||
if (typeof rec_args.n === "undefined") {
|
||||
rec_args.n = 0;
|
||||
rec_args.input_size = 0;
|
||||
@ -996,25 +1141,25 @@
|
||||
change_amount: required_amount * -1 //required_amount will be -ve of change_amount
|
||||
});
|
||||
else if (rec_args.n >= senders.length)
|
||||
return reject("Insufficient Balance");
|
||||
return reject(`Insufficient Balance.`);
|
||||
let addr = senders[rec_args.n];
|
||||
let size_per_input = _sizePerInput(addr);
|
||||
fetch_api(`unspent?active=${addr}`).then(result => {
|
||||
let utxos = result.unspent_outputs;
|
||||
// console.debug("add-utxo", addr, required_amount, utxos);
|
||||
for (let i = 0; i < utxos.length && required_amount > 0; i++) {
|
||||
if (!utxos[i].confirmations) //ignore unconfirmed utxo
|
||||
const { tx_output_n, value, confirmations, tx_hash_big_endian } = utxos[i];
|
||||
if (!confirmations) //ignore unconfirmed utxo
|
||||
continue;
|
||||
const { tx_hash, tx_index, value } = utxos[i];
|
||||
// tx.addinput(utxos[i].tx_hash_big_endian, utxos[i].tx_output_n, script, 0xfffffffd /*sequence*/); //0xfffffffd for Replace-by-fee
|
||||
// changes for taproot
|
||||
const input = { txid: tx_hash, index: tx_index, script: tr.script, amount: 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 }, });
|
||||
|
||||
|
||||
//update track values
|
||||
rec_args.input_size += size_per_input; // Adjust input size calculation
|
||||
rec_args.input_amount += util.Sat_to_BTC(utxos[i].value);
|
||||
required_amount -= util.Sat_to_BTC(utxos[i].value);
|
||||
rec_args.input_amount += value;
|
||||
required_amount -= value;
|
||||
if (fee_rate) //automatic fee calculation (dynamic)
|
||||
required_amount += size_per_input * fee_rate;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
2
scripts/components.min.js
vendored
2
scripts/components.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
9
scripts/tap_combined.min.js
vendored
Normal file
9
scripts/tap_combined.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user