From 29d1f93ef9fc2dd71a227271a67da8fe45b9f5f5 Mon Sep 17 00:00:00 2001 From: coderofstuff <114628839+coderofstuff@users.noreply.github.com> Date: Tue, 9 Jan 2024 22:47:17 -0700 Subject: [PATCH] Many utxos (#10) * Basic handling for many UTXOS * Handle Nano S limit --- app/wallet/overview-tab.tsx | 3 +- app/wallet/page.tsx | 20 ++++++- components/send-form.tsx | 106 ++++++++++++++++++++++-------------- lib/kaspa-util.ts | 2 + 4 files changed, 87 insertions(+), 44 deletions(-) diff --git a/app/wallet/overview-tab.tsx b/app/wallet/overview-tab.tsx index be2d399..f7a45f0 100644 --- a/app/wallet/overview-tab.tsx +++ b/app/wallet/overview-tab.tsx @@ -22,6 +22,7 @@ import { fetchAddressDetails, fetchTransaction, getAddress } from '@/lib/ledger' import { delay } from '@/lib/util'; import styles from './overview-tab.module.css'; +import { sompiToKas } from '@/lib/kaspa-util'; export default function OverviewTab(props) { const groupRef = useRef(null); @@ -127,7 +128,7 @@ export default function OverviewTab(props) { selectedAddress.derivationPath, ); - selectedAddress.balance = addressDetails.balance / 100000000; + selectedAddress.balance = sompiToKas(addressDetails.balance); selectedAddress.utxos = addressDetails.utxos; selectedAddress.newTransactions++; // selectedAddress.txCount = addressDetails.txCount; diff --git a/app/wallet/page.tsx b/app/wallet/page.tsx index 0c32aa3..ce242e5 100644 --- a/app/wallet/page.tsx +++ b/app/wallet/page.tsx @@ -18,6 +18,7 @@ import { delay } from '@/lib/util'; import { useElementSize } from '@mantine/hooks'; import { notifications } from '@mantine/notifications'; import SettingsStore from '@/lib/settings-store'; +import { kasToSompi, sompiToKas, NETWORK_UTXO_LIMIT } from '@/lib/kaspa-util'; let loadingAddressBatch = false; let addressInitialized = false; @@ -36,7 +37,7 @@ function loadAddressDetails(rawAddress) { const fetchAddressPromise = fetchAddressDetails(rawAddress.address, rawAddress.derivationPath); return fetchAddressPromise.then((addressDetails) => { - rawAddress.balance = addressDetails.balance / 100000000; + rawAddress.balance = sompiToKas(addressDetails.balance); rawAddress.utxos = addressDetails.utxos; // rawAddress.txCount = addressDetails.txCount; rawAddress.loading = false; @@ -111,15 +112,30 @@ async function demoLoadAddress(bip32, setAddresses, setRawAddresses, lastReceive const demoAddresses = []; for (let i = 0; i <= lastReceiveIndex; i++) { + const balance = Math.round(Math.random() * 10000); const currAddress = { key: i, address: bip32.getAddress(0, i), - balance: Math.round(Math.random() * 10000), + balance, derivationPath: `44'/111111'/0'/0/${i}`, utxos: [], loading: true, }; + currAddress.utxos.push({ + prevTxId: 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', + outpointIndex: 0, + amount: kasToSompi(balance - (NETWORK_UTXO_LIMIT - 1)), + }); + + for (let j = 0; j < NETWORK_UTXO_LIMIT - 1; j++) { + currAddress.utxos.push({ + prevTxId: 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', + outpointIndex: 0, + amount: kasToSompi(1), + }); + } + demoAddresses.push(currAddress); setRawAddresses([...demoAddresses]); diff --git a/components/send-form.tsx b/components/send-form.tsx index 4fcef81..109a69e 100644 --- a/components/send-form.tsx +++ b/components/send-form.tsx @@ -20,7 +20,7 @@ import { useState, useEffect } from 'react'; import { createTransaction, sendAmount, selectUtxos } from '@/lib/ledger'; import AddressText from '@/components/address-text'; import { useForm } from '@mantine/form'; -import { kasToSompi, sompiToKas } from '@/lib/kaspa-util'; +import { kasToSompi, sompiToKas, NETWORK_UTXO_LIMIT } from '@/lib/kaspa-util'; export default function SendForm(props) { const [confirming, setConfirming] = useState(false); @@ -128,12 +128,30 @@ export default function SendForm(props) { cleanupOnSuccess(transactionId); } catch (e) { console.error(e); - notifications.show({ - title: 'Error', - color: 'red', - message: e.message, - loading: false, - }); + + if (e.statusCode == 0xb005 && props.addressContext.utxos.length > 15) { + // This is probably a Nano S + const maxCompoundableAmount = sompiToKas( + props.addressContext.utxos.slice(0, 15).reduce((acc, utxo) => { + return acc + utxo.amount; + }, 0), + ); + notifications.show({ + title: 'Error', + color: 'red', + message: `You have too many UTXOs to send this amount. Please compound first by sending KAS to your address. Maximum sendable without compounding (including fee): ${maxCompoundableAmount}`, + autoClose: false, + loading: false, + }); + } else { + notifications.show({ + title: 'Error', + color: 'red', + message: e.message, + loading: false, + }); + } + setConfirming(false); } finally { notifications.hide(notifId); @@ -146,44 +164,50 @@ export default function SendForm(props) { if (amount && sendTo) { let calculatedFee: string | number = '-'; - if (deviceType === 'demo') { - calculatedFee = - fee === '-' ? sompiToKas(Math.round(Math.random() * 10000)) : Number(fee); - setCanSendAmount(Number(amount) <= props.addressContext.balance - calculatedFee); - if (includeFeeInAmount) { - const afterFeeDisplay = sompiToKas(kasToSompi(amount) - calculatedFee); - setAmountDescription(`Amount after fee: ${afterFeeDisplay}`); + + const { + hasEnough, + utxos, + fee: feeCalcResult, + total: utxoTotalAmount, + } = selectUtxos(kasToSompi(amount), props.addressContext.utxos, includeFeeInAmount); + + if (utxos.length > NETWORK_UTXO_LIMIT) { + const maxCompoundableAmount = sompiToKas( + utxos.slice(0, NETWORK_UTXO_LIMIT).reduce((acc, utxo) => { + return acc + utxo.amount; + }, 0), + ); + notifications.show({ + title: 'Error', + color: 'red', + message: `You have too many UTXOs to send this amount. Please compound first by sending KAS to your address. Maximum sendable without compounding (including fee): ${maxCompoundableAmount}`, + autoClose: false, + loading: false, + }); + setCanSendAmount(false); + } else if (hasEnough) { + let changeAmount = utxoTotalAmount - kasToSompi(amount); + if (!includeFeeInAmount) { + changeAmount -= feeCalcResult; } - } else if (deviceType === 'usb') { - const { - hasEnough, - fee: feeCalcResult, - total: utxoTotalAmount, - } = selectUtxos(kasToSompi(amount), props.addressContext.utxos, includeFeeInAmount); - - if (hasEnough) { - let changeAmount = utxoTotalAmount - kasToSompi(amount); - if (!includeFeeInAmount) { - changeAmount -= feeCalcResult; - } - let expectedFee = feeCalcResult; - // The change is added to the fee if it's less than 0.0001 KAS - console.info('changeAmount', changeAmount); - if (changeAmount < 10000) { - console.info(`Adding dust change ${changeAmount} sompi to fee`); - expectedFee += changeAmount; - } + let expectedFee = feeCalcResult; + // The change is added to the fee if it's less than 0.0001 KAS + console.info('changeAmount', changeAmount); + if (changeAmount < 10000) { + console.info(`Adding dust change ${changeAmount} sompi to fee`); + expectedFee += changeAmount; + } - calculatedFee = sompiToKas(expectedFee); - const afterFeeDisplay = sompiToKas(kasToSompi(amount) - expectedFee); - setCanSendAmount(true); - if (includeFeeInAmount) { - setAmountDescription(`Amount after fee: ${afterFeeDisplay}`); - } - } else { - setCanSendAmount(false); + calculatedFee = sompiToKas(expectedFee); + const afterFeeDisplay = sompiToKas(kasToSompi(amount) - expectedFee); + setCanSendAmount(true); + if (includeFeeInAmount) { + setAmountDescription(`Amount after fee: ${afterFeeDisplay}`); } + } else { + setCanSendAmount(false); } if (fee === '-' || fee !== calculatedFee) { diff --git a/lib/kaspa-util.ts b/lib/kaspa-util.ts index 8ac734d..30b2636 100644 --- a/lib/kaspa-util.ts +++ b/lib/kaspa-util.ts @@ -181,3 +181,5 @@ export function kasToSompi(amount: number) { throw new Error('Invalid amount'); } } + +export const NETWORK_UTXO_LIMIT = 84;