Skip to content

Commit

Permalink
Move to Typescript + other changes (#9)
Browse files Browse the repository at this point in the history
* Initial move to typescript

* Convert ledger.js to ts + fix dust fees

* Update react version

* Convert all JS pages to TSX

* Amount formatting utils and fixes

* lints

* Set page types

* Components from JS to TSX

* Tests to TS

* TS target es2015

* Pass device type to message form

* lint and config

* Attempt to use 2 UTXOs when change is needed
  • Loading branch information
coderofstuff authored Jan 2, 2024
1 parent f1ac593 commit f189b47
Show file tree
Hide file tree
Showing 24 changed files with 293 additions and 147 deletions.
3 changes: 1 addition & 2 deletions app/layout.js → app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,8 @@ export default function RootLayout({ children }) {
</head>
<body className={inter.className}>
<MantineProvider
withGlobalStyles
defaultColorScheme='dark'
theme={{
colorScheme: 'dark',
fontFamily: 'Lato',
fontFamilyMonospace: 'Roboto Mono,Courier New,Courier,monospace',
colors: {
Expand All @@ -39,6 +37,7 @@ export default function RootLayout({ children }) {
'#49EACB',
'#49EACB',
'#49EACB',
'#49EACB',
],
},
primaryColor: 'brand',
Expand Down
4 changes: 2 additions & 2 deletions app/page.js → app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import styles from './page.module.css';
import { initTransport, getAppAndVersion } from '../lib/ledger.js';
import { initTransport, getAppAndVersion } from '../lib/ledger';
import { useRouter } from 'next/navigation';
import { notifications } from '@mantine/notifications';

Expand Down Expand Up @@ -102,7 +102,7 @@ export default function Home() {
>
<h2>
<Group style={smallStyles}>
<IconBluetooth styles={smallStyles} /> Go to Demo Mode <span>-&gt;</span>
<IconBluetooth style={smallStyles} /> Go to Demo Mode <span>-&gt;</span>
</Group>
</h2>
<Text>(Replaced with bluetooth in the future)</Text>
Expand Down
File renamed without changes.
7 changes: 5 additions & 2 deletions app/wallet/overview-tab.js → app/wallet/overview-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ export default function OverviewTab(props) {
console.error(e);
notifications.show({
title: 'Address not verified',
message: 'Failed to verify address',
message: 'Failed to verify address on the device',
color: 'red',
});
}

Expand Down Expand Up @@ -167,7 +168,9 @@ export default function OverviewTab(props) {
);
break;
case 'Message':
signSection = <MessageForm selectedAddress={selectedAddress} />;
signSection = (
<MessageForm selectedAddress={selectedAddress} deviceType={props.deviceType} />
);
break;
default:
break;
Expand Down
66 changes: 11 additions & 55 deletions app/wallet/page.js → app/wallet/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
'use client';

import styles from './page.module.css';
import { getAddress, fetchAddressDetails, initTransport } from '../../lib/ledger.js';
import { getAddress, fetchAddressDetails, initTransport } from '@/lib/ledger';
import { useState, useEffect } from 'react';
import { Box, Stack, Tabs, Breadcrumbs, Anchor, Button, Center } from '@mantine/core';
import { Stack, Tabs, Breadcrumbs, Anchor, Button, Center } from '@mantine/core';
import Header from '../../components/header';
import AddressesTab from './addresses-tab';
import OverviewTab from './overview-tab';
Expand All @@ -17,29 +17,11 @@ import { delay } from '@/lib/util';

import { useElementSize } from '@mantine/hooks';
import { notifications } from '@mantine/notifications';
import { eslint } from '@/next.config';
import SettingsStore from '@/lib/settings-store';

let loadingAddressBatch = false;
let addressInitialized = false;

function loadAddresses(bip32, addressType = 0, from = 0, to = from + 10) {
const addresses = [];

for (let addressIndex = from; addressIndex < to; addressIndex++) {
const derivationPath = `44'/111111'/0'/${addressType}/${addressIndex}`;
const address = bip32.getAddress(addressType, addressIndex);

addresses.push({
derivationPath,
address,
addressIndex,
addressType,
});
}

return addresses;
}

const addressFilter = (lastReceiveIndex) => {
return (addressData, index) => {
return (
Expand Down Expand Up @@ -174,43 +156,14 @@ function getDemoXPub() {
};
}

class SettingsStore {
constructor(storageKey) {
this.storageKey = `kasvault:${storageKey}`;
this.settings = localStorage.getItem(this.storageKey);

if (this.settings) {
this.settings = JSON.parse(this.settings);
} else {
this.settings = {
receiveAddresses: {},
lastReceiveIndex: 0,
changeAddresses: {},
lastChangeIndex: -1,
version: 0,
};
localStorage.setItem(this.storageKey, JSON.stringify(this.settings));
}
}

setSetting(property, value) {
this.settings[property] = value;
localStorage.setItem(this.storageKey, JSON.stringify(this.settings));
}

getSetting(property) {
return this.settings[property];
}
}

export default function Dashboard(props) {
export default function Dashboard() {
const [addresses, setAddresses] = useState([]);
const [rawAddresses, setRawAddresses] = useState([]);
const [selectedAddress, setSelectedAddress] = useState(null);
const [activeTab, setActiveTab] = useState('addresses');
const [isTransportInitialized, setTransportInitialized] = useState(false);
const [bip32base, setBIP32Base] = useState();
const [userSettings, setUserSettings] = useState();
const [bip32base, setBIP32Base] = useState<KaspaBIP32>();
const [userSettings, setUserSettings] = useState<SettingsStore>();
const [enableGenerate, setEnableGenerate] = useState(false);

const { ref: containerRef, width: containerWidth, height: containerHeight } = useElementSize();
Expand Down Expand Up @@ -296,14 +249,14 @@ export default function Dashboard(props) {

useEffect(() => {
if (isTransportInitialized) {
return;
return () => {};
}

if (deviceType === 'demo') {
setTransportInitialized(true);
const xpub = getDemoXPub();
setBIP32Base(new KaspaBIP32(xpub.compressedPublicKey, xpub.chainCode));
return;
return () => {};
}

let unloaded = false;
Expand All @@ -317,6 +270,8 @@ export default function Dashboard(props) {
setBIP32Base(new KaspaBIP32(xpub.compressedPublicKey, xpub.chainCode)),
);
}

return null;
})
.catch((e) => {
notifications.show({
Expand Down Expand Up @@ -431,6 +386,7 @@ export default function Dashboard(props) {
setAddresses={setAddresses}
containerWidth={containerWidth}
containerHeight={containerHeight}
deviceType={deviceType}
/>
</Tabs.Panel>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
Box,
Loader,
} from '@mantine/core';
import { fetchTransactions, fetchTransactionCount } from '../../lib/ledger.js';
import { fetchTransactions, fetchTransactionCount } from '@/lib/ledger';
import { useEffect, useState } from 'react';
import { format } from 'date-fns';

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
9 changes: 7 additions & 2 deletions components/message-form.js → components/message-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import MessageModal from './message-modal';
import { notifications } from '@mantine/notifications';

export default function MessageForm(props) {
const [signature, setSignature] = useState();
const [signature, setSignature] = useState('');
const [opened, { open, close }] = useDisclosure(false);

const form = useForm({
Expand Down Expand Up @@ -41,7 +41,12 @@ export default function MessageForm(props) {

try {
const path = props.selectedAddress.derivationPath.split('/');
const result = await signMessage(form.values.message, Number(path[3]), Number(path[4]));
const result = await signMessage(
form.values.message,
Number(path[3]),
Number(path[4]),
props.deviceType,
);
setSignature(result.signature);

open();
Expand Down
File renamed without changes.
71 changes: 41 additions & 30 deletions components/send-form.js → components/send-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ import { useSearchParams } from 'next/navigation';

import { useState, useEffect } from 'react';
import { createTransaction, sendAmount, selectUtxos } from '@/lib/ledger';
import styles from './send-form.module.css';
import AddressText from '@/components/address-text';
import { useForm } from '@mantine/form';
import { kasToSompi, sompiToKas } from '@/lib/kaspa-util';

export default function SendForm(props) {
const [confirming, setConfirming] = useState(false);
const [fee, setFee] = useState('-');
const [amountDescription, setAmountDescription] = useState();
const [fee, setFee] = useState<string | number>('-');
const [amountDescription, setAmountDescription] = useState<string>();

const [canSendAmount, setCanSendAmount] = useState(false);

Expand All @@ -37,7 +37,7 @@ export default function SendForm(props) {

const form = useForm({
initialValues: {
amount: '',
amount: undefined,
sendTo: '',
includeFeeInAmount: false,
sentAmount: '',
Expand All @@ -55,20 +55,18 @@ export default function SendForm(props) {
// Reset setup
setConfirming(false);
setFee('-');
const baseValues = { amount: '', sendTo: '', includeFeeInAmount: false };
let baseValues = { amount: '', sendTo: '', includeFeeInAmount: false };

if (resetAllValues) {
baseValues.sentTo = '';
baseValues.sentTxId = '';
baseValues.sentAmount = '';
form.setValues({ sentTo: '', sentTxId: '', sentAmount: '', ...baseValues });
} else {
form.setValues(baseValues);
}

form.setValues(baseValues);
};

const cleanupOnSuccess = (transactionId) => {
const targetAmount = form.values.includeFeeInAmount
? Number((form.values.amount - fee).toFixed(8))
? (Number(form.values.amount) - Number(fee)).toFixed(8)
: form.values.amount;

form.setValues({
Expand Down Expand Up @@ -117,7 +115,7 @@ export default function SendForm(props) {
} else if (deviceType == 'usb') {
try {
const { tx } = createTransaction(
Math.round(form.values.amount * 100000000),
kasToSompi(Number(form.values.amount)),
form.values.sendTo,
props.addressContext.utxos,
props.addressContext.derivationPath,
Expand Down Expand Up @@ -147,26 +145,41 @@ export default function SendForm(props) {
setAmountDescription('');

if (amount && sendTo) {
let calculatedFee = '-';
let calculatedFee: string | number = '-';
if (deviceType === 'demo') {
calculatedFee =
fee === '-' ? Math.round(Math.random() * 10000) / 100000000 : Number(fee);
fee === '-' ? sompiToKas(Math.round(Math.random() * 10000)) : Number(fee);
setCanSendAmount(Number(amount) <= props.addressContext.balance - calculatedFee);
if (includeFeeInAmount) {
setAmountDescription(`Amount after fee: ${amount - calculatedFee}`);
const afterFeeDisplay = sompiToKas(kasToSompi(amount) - calculatedFee);
setAmountDescription(`Amount after fee: ${afterFeeDisplay}`);
}
} else if (deviceType === 'usb') {
const [hasEnough, selectedUtxos, feeCalcResult] = selectUtxos(
amount * 100000000,
props.addressContext.utxos,
includeFeeInAmount,
);
const {
hasEnough,
fee: feeCalcResult,
total: utxoTotalAmount,
} = selectUtxos(kasToSompi(amount), props.addressContext.utxos, includeFeeInAmount);

if (hasEnough) {
calculatedFee = feeCalcResult / 100000000;
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;
}

calculatedFee = sompiToKas(expectedFee);
const afterFeeDisplay = sompiToKas(kasToSompi(amount) - expectedFee);
setCanSendAmount(true);
if (includeFeeInAmount) {
setAmountDescription(`Amount after fee: ${amount - calculatedFee}`);
setAmountDescription(`Amount after fee: ${afterFeeDisplay}`);
}
} else {
setCanSendAmount(false);
Expand All @@ -189,7 +202,7 @@ export default function SendForm(props) {
}, 0);

form.setValues({
amount: Number((total / 100000000).toFixed(8)),
amount: sompiToKas(total),
includeFeeInAmount: true,
});
};
Expand All @@ -201,7 +214,7 @@ export default function SendForm(props) {
label='Send to Address'
placeholder='Address'
{...form.getInputProps('sendTo')}
disabled={form.getInputProps('sendTo').disabled || confirming}
disabled={confirming}
required
/>

Expand All @@ -228,7 +241,7 @@ export default function SendForm(props) {
<Checkbox
{...form.getInputProps('includeFeeInAmount', { type: 'checkbox' })}
label='Include fee in amount'
disabled={confirming || form.getInputProps('includeFeeInAmount').disabled}
disabled={confirming}
/>

<Group justify='space-between'>
Expand All @@ -252,7 +265,7 @@ export default function SendForm(props) {
size={viewportWidth > 700 ? 'auto' : 'md'}
>
<Stack align='center'>
<Text size='lg' align='center' c='brand'>
<Text size='lg' c='brand'>
Sent!
</Text>

Expand All @@ -262,23 +275,21 @@ export default function SendForm(props) {
href={`https://explorer.kaspa.org/txs/${form.values.sentTxId}`}
target='_blank'
c='brand'
align='center'
w={'calc(var(--modal-size) - 6rem)'}
style={{ overflowWrap: 'break-word' }}
>
{form.values.sentTxId}
</Anchor>

<Text component='h2' align='center' fw={600}>
<Text component='h2' fw={600}>
{form.values.sentAmount} KAS
</Text>

<Text align='center'>sent to</Text>
<Text>sent to</Text>

<Text
w={'calc(var(--modal-size) - 6rem)'}
style={{ overflowWrap: 'break-word' }}
align='center'
>
<AddressText address={form.values.sentTo} />
</Text>
Expand Down
Loading

0 comments on commit f189b47

Please sign in to comment.