From f189b471a2ec816fa18bc69f1375acc21e761712 Mon Sep 17 00:00:00 2001
From: coderofstuff <114628839+coderofstuff@users.noreply.github.com>
Date: Mon, 1 Jan 2024 23:12:36 -0700
Subject: [PATCH] Move to Typescript + other changes (#9)
* 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
---
app/{layout.js => layout.tsx} | 3 +-
app/{page.js => page.tsx} | 4 +-
.../{addresses-tab.js => addresses-tab.tsx} | 0
.../{overview-tab.js => overview-tab.tsx} | 7 +-
app/wallet/{page.js => page.tsx} | 66 +++--------------
...ansactions-tab.js => transactions-tab.tsx} | 2 +-
.../{address-text.js => address-text.tsx} | 0
components/{header.js => header.tsx} | 0
.../{kaspa-qrcode.js => kaspa-qrcode.tsx} | 0
.../{message-form.js => message-form.tsx} | 9 ++-
.../{message-modal.js => message-modal.tsx} | 0
components/{send-form.js => send-form.tsx} | 71 ++++++++++--------
lib/{base32.js => base32.ts} | 16 ++--
lib/{bip32.js => bip32.ts} | 10 ++-
lib/{kaspa-util.js => kaspa-util.ts} | 46 +++++++++---
lib/{ledger.js => ledger.ts} | 74 ++++++++++++++-----
lib/settings-store.ts | 33 +++++++++
lib/util.js | 5 --
lib/util.ts | 5 ++
package-lock.json | 34 +++++++--
package.json | 4 +-
tests/{bip32.test.js => bip32.test.ts} | 0
...{kaspa-util.test.js => kaspa-util.test.ts} | 0
tsconfig.json | 51 +++++++++++++
24 files changed, 293 insertions(+), 147 deletions(-)
rename app/{layout.js => layout.tsx} (95%)
rename app/{page.js => page.tsx} (96%)
rename app/wallet/{addresses-tab.js => addresses-tab.tsx} (100%)
rename app/wallet/{overview-tab.js => overview-tab.tsx} (97%)
rename app/wallet/{page.js => page.tsx} (89%)
rename app/wallet/{transactions-tab.js => transactions-tab.tsx} (98%)
rename components/{address-text.js => address-text.tsx} (100%)
rename components/{header.js => header.tsx} (100%)
rename components/{kaspa-qrcode.js => kaspa-qrcode.tsx} (100%)
rename components/{message-form.js => message-form.tsx} (92%)
rename components/{message-modal.js => message-modal.tsx} (100%)
rename components/{send-form.js => send-form.tsx} (78%)
rename lib/{base32.js => base32.ts} (89%)
rename lib/{bip32.js => bip32.ts} (61%)
rename lib/{kaspa-util.js => kaspa-util.ts} (73%)
rename lib/{ledger.js => ledger.ts} (83%)
create mode 100644 lib/settings-store.ts
delete mode 100644 lib/util.js
create mode 100644 lib/util.ts
rename tests/{bip32.test.js => bip32.test.ts} (100%)
rename tests/{kaspa-util.test.js => kaspa-util.test.ts} (100%)
create mode 100644 tsconfig.json
diff --git a/app/layout.js b/app/layout.tsx
similarity index 95%
rename from app/layout.js
rename to app/layout.tsx
index d3c3d1f..826de42 100644
--- a/app/layout.js
+++ b/app/layout.tsx
@@ -22,10 +22,8 @@ export default function RootLayout({ children }) {
- Go to Demo Mode ->
+ Go to Demo Mode ->
(Replaced with bluetooth in the future)
diff --git a/app/wallet/addresses-tab.js b/app/wallet/addresses-tab.tsx
similarity index 100%
rename from app/wallet/addresses-tab.js
rename to app/wallet/addresses-tab.tsx
diff --git a/app/wallet/overview-tab.js b/app/wallet/overview-tab.tsx
similarity index 97%
rename from app/wallet/overview-tab.js
rename to app/wallet/overview-tab.tsx
index c896783..be2d399 100644
--- a/app/wallet/overview-tab.js
+++ b/app/wallet/overview-tab.tsx
@@ -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',
});
}
@@ -167,7 +168,9 @@ export default function OverviewTab(props) {
);
break;
case 'Message':
- signSection = ;
+ signSection = (
+
+ );
break;
default:
break;
diff --git a/app/wallet/page.js b/app/wallet/page.tsx
similarity index 89%
rename from app/wallet/page.js
rename to app/wallet/page.tsx
index 6e0a6dd..0c32aa3 100644
--- a/app/wallet/page.js
+++ b/app/wallet/page.tsx
@@ -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';
@@ -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 (
@@ -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();
+ const [userSettings, setUserSettings] = useState();
const [enableGenerate, setEnableGenerate] = useState(false);
const { ref: containerRef, width: containerWidth, height: containerHeight } = useElementSize();
@@ -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;
@@ -317,6 +270,8 @@ export default function Dashboard(props) {
setBIP32Base(new KaspaBIP32(xpub.compressedPublicKey, xpub.chainCode)),
);
}
+
+ return null;
})
.catch((e) => {
notifications.show({
@@ -431,6 +386,7 @@ export default function Dashboard(props) {
setAddresses={setAddresses}
containerWidth={containerWidth}
containerHeight={containerHeight}
+ deviceType={deviceType}
/>
diff --git a/app/wallet/transactions-tab.js b/app/wallet/transactions-tab.tsx
similarity index 98%
rename from app/wallet/transactions-tab.js
rename to app/wallet/transactions-tab.tsx
index e30daa3..74e8dc6 100644
--- a/app/wallet/transactions-tab.js
+++ b/app/wallet/transactions-tab.tsx
@@ -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';
diff --git a/components/address-text.js b/components/address-text.tsx
similarity index 100%
rename from components/address-text.js
rename to components/address-text.tsx
diff --git a/components/header.js b/components/header.tsx
similarity index 100%
rename from components/header.js
rename to components/header.tsx
diff --git a/components/kaspa-qrcode.js b/components/kaspa-qrcode.tsx
similarity index 100%
rename from components/kaspa-qrcode.js
rename to components/kaspa-qrcode.tsx
diff --git a/components/message-form.js b/components/message-form.tsx
similarity index 92%
rename from components/message-form.js
rename to components/message-form.tsx
index 1c46998..0d313b0 100644
--- a/components/message-form.js
+++ b/components/message-form.tsx
@@ -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({
@@ -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();
diff --git a/components/message-modal.js b/components/message-modal.tsx
similarity index 100%
rename from components/message-modal.js
rename to components/message-modal.tsx
diff --git a/components/send-form.js b/components/send-form.tsx
similarity index 78%
rename from components/send-form.js
rename to components/send-form.tsx
index 74df23c..4fcef81 100644
--- a/components/send-form.js
+++ b/components/send-form.tsx
@@ -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('-');
+ const [amountDescription, setAmountDescription] = useState();
const [canSendAmount, setCanSendAmount] = useState(false);
@@ -37,7 +37,7 @@ export default function SendForm(props) {
const form = useForm({
initialValues: {
- amount: '',
+ amount: undefined,
sendTo: '',
includeFeeInAmount: false,
sentAmount: '',
@@ -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({
@@ -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,
@@ -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);
@@ -189,7 +202,7 @@ export default function SendForm(props) {
}, 0);
form.setValues({
- amount: Number((total / 100000000).toFixed(8)),
+ amount: sompiToKas(total),
includeFeeInAmount: true,
});
};
@@ -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
/>
@@ -228,7 +241,7 @@ export default function SendForm(props) {
@@ -252,7 +265,7 @@ export default function SendForm(props) {
size={viewportWidth > 700 ? 'auto' : 'md'}
>
-
+
Sent!
@@ -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}
-
+
{form.values.sentAmount} KAS
- sent to
+ sent to
diff --git a/lib/base32.js b/lib/base32.ts
similarity index 89%
rename from lib/base32.js
rename to lib/base32.ts
index e72b996..7893e4b 100644
--- a/lib/base32.js
+++ b/lib/base32.ts
@@ -10,12 +10,12 @@
/***
* Charset containing the 32 symbols used in the base32 encoding.
*/
-var CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
+const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
/***
* Inverted index mapping each symbol into its index within the charset.
*/
-var CHARSET_INVERSE_INDEX = {
+const CHARSET_INVERSE_INDEX = {
q: 0,
p: 1,
z: 2,
@@ -55,7 +55,7 @@ var CHARSET_INVERSE_INDEX = {
*
* @param {Array} data Array of integers between 0 and 31 inclusive.
*/
-function encode(data) {
+export function encode(data) {
if (!(data instanceof Array)) {
throw new Error('Must be Array');
}
@@ -75,7 +75,7 @@ function encode(data) {
*
* @param {string} base32
*/
-function decode(base32) {
+export function decode(base32) {
if (typeof base32 !== 'string') {
throw new Error('Must be base32-encoded string');
}
@@ -90,7 +90,9 @@ function decode(base32) {
return data;
}
-module.exports = {
- encode: encode,
- decode: decode,
+const base32 = {
+ encode,
+ decode,
};
+
+export default base32;
diff --git a/lib/bip32.js b/lib/bip32.ts
similarity index 61%
rename from lib/bip32.js
rename to lib/bip32.ts
index b98ff38..2b847dd 100644
--- a/lib/bip32.js
+++ b/lib/bip32.ts
@@ -1,15 +1,17 @@
import ecc from '@bitcoinerlab/secp256k1';
-import BIP32Factory from 'bip32';
+import BIP32Factory, { BIP32API, BIP32Interface } from 'bip32';
import { publicKeyToAddress } from './kaspa-util';
-const bip32 = BIP32Factory(ecc);
+const bip32: BIP32API = BIP32Factory(ecc);
export default class KaspaBIP32 {
- constructor(compressedPublicKey, chainCode) {
+ rootNode: BIP32Interface;
+
+ constructor(compressedPublicKey: Buffer, chainCode: Buffer) {
this.rootNode = bip32.fromPublicKey(compressedPublicKey, chainCode);
}
- getAddress(type = 0, index = 0) {
+ getAddress(type: number = 0, index: number = 0) {
const child = this.rootNode.derivePath(`${type}/${index}`);
// child.publicKey is a compressed public key
diff --git a/lib/kaspa-util.js b/lib/kaspa-util.ts
similarity index 73%
rename from lib/kaspa-util.js
rename to lib/kaspa-util.ts
index 4913f78..8ac734d 100644
--- a/lib/kaspa-util.js
+++ b/lib/kaspa-util.ts
@@ -1,6 +1,6 @@
-import base32 from './base32';
+import base32 from '@/lib/base32';
-function convertBits(data, from, to, strict) {
+function convertBits(data: number[], from: number, to: number, strict: boolean = false): number[] {
strict = strict || false;
var accumulator = 0;
var bits = 0;
@@ -89,7 +89,11 @@ function checksumToArray(checksum) {
return result.reverse();
}
-export function publicKeyToAddress(hashBuffer, stripPrefix, type = 'schnorr') {
+export function publicKeyToAddress(
+ hashBuffer: Buffer,
+ stripPrefix: boolean,
+ type: string = 'schnorr',
+): string {
function getTypeBits(type) {
switch (type) {
case 'schnorr':
@@ -103,12 +107,12 @@ export function publicKeyToAddress(hashBuffer, stripPrefix, type = 'schnorr') {
}
}
- var eight0 = [0, 0, 0, 0, 0, 0, 0, 0];
+ var eight0: number[] = [0, 0, 0, 0, 0, 0, 0, 0];
var prefixData = prefixToArray('kaspa').concat([0]);
- var versionByte = getTypeBits(type);
- var arr = Array.prototype.slice.call(hashBuffer, 0);
- var payloadData = convertBits([versionByte].concat(arr), 8, 5);
- var checksumData = prefixData.concat(payloadData).concat(eight0);
+ var versionByte: number = getTypeBits(type);
+ var arr: number[] = Array.prototype.slice.call(hashBuffer, 0);
+ var payloadData: number[] = convertBits([versionByte].concat(arr), 8, 5);
+ var checksumData: number[] = prefixData.concat(payloadData).concat(eight0);
var payload = payloadData.concat(checksumToArray(polymod(checksumData)));
if (stripPrefix === true) {
return base32.encode(payload);
@@ -117,7 +121,7 @@ export function publicKeyToAddress(hashBuffer, stripPrefix, type = 'schnorr') {
}
}
-export function addressToPublicKey(address) {
+export function addressToPublicKey(address: string): { version: number; publicKey: number[] } {
const addrPart = address.split(':')[1];
const payload = convertBits(base32.decode(addrPart), 5, 8);
@@ -133,7 +137,7 @@ export function addressToPublicKey(address) {
}
}
-function numArrayToHexString(numArray = []) {
+function numArrayToHexString(numArray = []): string {
const hexArr = [];
for (const num of numArray) {
@@ -143,7 +147,7 @@ function numArrayToHexString(numArray = []) {
return hexArr.join('');
}
-export function addressToScriptPublicKey(address) {
+export function addressToScriptPublicKey(address: string): string {
const { version, publicKey } = addressToPublicKey(address);
switch (version) {
@@ -157,3 +161,23 @@ export function addressToScriptPublicKey(address) {
throw new Error('Address could not be translated to script public key');
}
}
+
+export function sompiToKas(amount: number) {
+ const amountStr = '00000000' + amount;
+ return Number(amountStr.slice(0, -8) + '.' + amountStr.slice(-8));
+}
+
+export function kasToSompi(amount: number) {
+ const amountStr = String(amount);
+ const parts = amountStr.split('.');
+
+ if (parts.length === 1) {
+ return Number(amountStr + '00000000');
+ } else if (parts.length === 2) {
+ const [left, right] = parts;
+ const rightStr = right + '00000000';
+ return Number(left + rightStr.slice(0, 8));
+ } else {
+ throw new Error('Invalid amount');
+ }
+}
diff --git a/lib/ledger.js b/lib/ledger.ts
similarity index 83%
rename from lib/ledger.js
rename to lib/ledger.ts
index cec6203..7ad0349 100644
--- a/lib/ledger.js
+++ b/lib/ledger.ts
@@ -1,4 +1,3 @@
-import Transport from '@ledgerhq/hw-transport';
import TransportWebHID from '@ledgerhq/hw-transport-webhid';
import axios from 'axios';
import axiosRetry from 'axios-retry';
@@ -19,13 +18,32 @@ let transportState = {
type: null,
};
-export async function fetchTransaction(transactionId) {
+export async function fetchTransaction(transactionId: string) {
const { data: txData } = await axios.get(`https://api.kaspa.org/transactions/${transactionId}`);
return txData;
}
-export function selectUtxos(amount, utxosInput, feeIncluded = false) {
+export type UtxoSelectionResult = {
+ hasEnough: boolean;
+ utxos: Array;
+ fee: number;
+ total: number;
+};
+
+/**
+ * Selects the UTXOs to fulfill the amount requested
+ *
+ * @param amount - the amount to select for, in SOMPI
+ * @param utxosInput - the utxos array to select from
+ * @param feeIncluded - whether or not fees are included in the amount passed
+ * @returns [has_enough, utxos, fee, total]
+ */
+export function selectUtxos(
+ amount: number,
+ utxosInput: UtxoInfo[],
+ feeIncluded: boolean = false,
+): UtxoSelectionResult {
// Fee does not have to be accurate. It just has to be over the absolute minimum.
// https://kaspa-mdbook.aspectron.com/transactions/constraints/fees.html
// Fee = (total mass) x (min_relay_tx_fee) / 1000
@@ -60,7 +78,7 @@ export function selectUtxos(amount, utxosInput, feeIncluded = false) {
selected.push(utxo);
- const targetAmount = feeIncluded ? Number((amount - fee).toFixed(8)) : amount;
+ const targetAmount = feeIncluded ? amount - fee : amount;
console.info({
targetAmount,
amount,
@@ -68,15 +86,17 @@ export function selectUtxos(amount, utxosInput, feeIncluded = false) {
total,
});
- if (total >= targetAmount + fee) {
+ const totalSpend = targetAmount + fee;
+ // If we have change, we want to try to use at least 2 UTXOs
+ if (total == totalSpend || (total > totalSpend && selected.length > 1)) {
// We have enough
break;
}
}
// [has_enough, utxos, fee, total]
- const targetAmount = feeIncluded ? Number((amount - fee).toFixed(8)) : amount;
- return [total >= targetAmount + fee, selected, fee, total];
+ const targetAmount = feeIncluded ? amount - fee : amount;
+ return { hasEnough: total >= targetAmount + fee, utxos: selected, fee, total };
}
export async function initTransport(type = 'usb') {
@@ -103,6 +123,12 @@ export async function fetchTransactionCount(address) {
return txCount.total || 0;
}
+export type UtxoInfo = {
+ prevTxId: string;
+ outpointIndex: number;
+ amount: number;
+};
+
export async function fetchAddressDetails(address, derivationPath) {
const { data: balanceData } = await axios.get(
`https://api.kaspa.org/addresses/${address}/balance`,
@@ -111,7 +137,7 @@ export async function fetchAddressDetails(address, derivationPath) {
// UTXOs sorted by decreasing amount. Using the biggest UTXOs first minimizes number of utxos needed
// in a transaction
- const utxos = utxoData
+ const utxos: UtxoInfo[] = utxoData
.map((utxo) => {
return {
prevTxId: utxo.outpoint.transactionId,
@@ -119,7 +145,7 @@ export async function fetchAddressDetails(address, derivationPath) {
amount: Number(utxo.utxoEntry.amount),
};
})
- .sort((a, b) => b.amount - a.amount);
+ .sort((a: UtxoInfo, b: UtxoInfo) => b.amount - a.amount);
const path = derivationPath.split('/');
@@ -217,19 +243,24 @@ export const sendTransaction = async (signedTx) => {
};
export function createTransaction(
- amount,
- sendTo,
- utxosInput,
- derivationPath,
- address,
- feeIncluded,
+ amount: number,
+ sendTo: string,
+ utxosInput: any,
+ derivationPath: string,
+ changeAddress: string,
+ feeIncluded: boolean = false,
) {
console.info('Amount:', amount);
console.info('Send to:', sendTo);
console.info('UTXOs:', utxosInput);
console.info('Derivation Path:', derivationPath);
- const [hasEnough, utxos, fee, totalUtxoAmount] = selectUtxos(amount, utxosInput, feeIncluded);
+ const {
+ hasEnough,
+ utxos,
+ fee,
+ total: totalUtxoAmount,
+ } = selectUtxos(amount, utxosInput, feeIncluded);
console.info('hasEnough', hasEnough);
console.info(utxos);
@@ -242,7 +273,7 @@ export function createTransaction(
const path = derivationPath.split('/');
console.info('Split Path:', path);
- const inputs = utxos.map(
+ const inputs: TransactionInput[] = utxos.map(
(utxo) =>
new TransactionInput({
value: utxo.amount,
@@ -253,7 +284,7 @@ export function createTransaction(
}),
);
- const outputs = [];
+ const outputs: TransactionOutput[] = [];
const targetAmount = feeIncluded ? Number((amount - fee).toFixed(8)) : amount;
@@ -266,14 +297,17 @@ export function createTransaction(
const changeAmount = totalUtxoAmount - targetAmount - fee;
- if (changeAmount > 0) {
+ // Any change smaller than 0.0001 is contributed to the fee to avoid dust
+ if (changeAmount >= 10000) {
// Send remainder back to self:
outputs.push(
new TransactionOutput({
value: Math.round(changeAmount),
- scriptPublicKey: addressToScriptPublicKey(address),
+ scriptPublicKey: addressToScriptPublicKey(changeAddress),
}),
);
+ } else {
+ console.info(`Adding dust change ${changeAmount} sompi to fee`);
}
const tx = new Transaction({
diff --git a/lib/settings-store.ts b/lib/settings-store.ts
new file mode 100644
index 0000000..1fb4ff4
--- /dev/null
+++ b/lib/settings-store.ts
@@ -0,0 +1,33 @@
+class SettingsStore {
+ storageKey: string;
+ settings: Object;
+ constructor(storageKey: string) {
+ this.storageKey = `kasvault:${storageKey}`;
+
+ const storedSettings: string = localStorage.getItem(this.storageKey);
+
+ if (storedSettings) {
+ this.settings = JSON.parse(storedSettings);
+ } 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 SettingsStore;
diff --git a/lib/util.js b/lib/util.js
deleted file mode 100644
index fdbc8f9..0000000
--- a/lib/util.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export function delay(ms = 0) {
- return new Promise((resolve) => {
- setTimeout(resolve, ms);
- });
-}
diff --git a/lib/util.ts b/lib/util.ts
new file mode 100644
index 0000000..5234ffa
--- /dev/null
+++ b/lib/util.ts
@@ -0,0 +1,5 @@
+export function delay(ms: number = 0) {
+ return new Promise((resolve: (args: void) => void) => {
+ setTimeout(resolve, ms);
+ });
+}
diff --git a/package-lock.json b/package-lock.json
index 7634244..0116e78 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -32,12 +32,14 @@
"react-qrcode-logo": "^2.9.0"
},
"devDependencies": {
+ "@types/react": "18.2.46",
"eslint": "^8.54.0",
"eslint-config-next": "13.4.7",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-prettier": "^4.2.1",
"jest": "^29.7.0",
- "prettier": "^2.8.8"
+ "prettier": "^2.8.8",
+ "typescript": "^5.3.3"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -1947,6 +1949,29 @@
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="
},
+ "node_modules/@types/prop-types": {
+ "version": "15.7.11",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
+ "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==",
+ "devOptional": true
+ },
+ "node_modules/@types/react": {
+ "version": "18.2.46",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.46.tgz",
+ "integrity": "sha512-nNCvVBcZlvX4NU1nRRNV/mFl1nNRuTuslAJglQsq+8ldXe5Xv0Wd2f7WTE3jOxhLH2BFfiZGC6GCp+kHQbgG+w==",
+ "devOptional": true,
+ "dependencies": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/scheduler": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
+ "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
+ "devOptional": true
+ },
"node_modules/@types/stack-utils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
@@ -7657,11 +7682,10 @@
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
},
"node_modules/typescript": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz",
- "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==",
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
+ "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"dev": true,
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
diff --git a/package.json b/package.json
index 0dab4c3..d9a0f48 100644
--- a/package.json
+++ b/package.json
@@ -35,12 +35,14 @@
"react-qrcode-logo": "^2.9.0"
},
"devDependencies": {
+ "@types/react": "18.2.46",
"eslint": "^8.54.0",
"eslint-config-next": "13.4.7",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-prettier": "^4.2.1",
"jest": "^29.7.0",
- "prettier": "^2.8.8"
+ "prettier": "^2.8.8",
+ "typescript": "^5.3.3"
},
"overrides": {
"eslint-plugin-import": {
diff --git a/tests/bip32.test.js b/tests/bip32.test.ts
similarity index 100%
rename from tests/bip32.test.js
rename to tests/bip32.test.ts
diff --git a/tests/kaspa-util.test.js b/tests/kaspa-util.test.ts
similarity index 100%
rename from tests/kaspa-util.test.js
rename to tests/kaspa-util.test.ts
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..df7993e
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,51 @@
+{
+ "compilerOptions": {
+ "paths": {
+ "@/*": [
+ "./*"
+ ]
+ },
+ "target": "esnext",
+ "lib": [
+ "es6",
+ "es7",
+ "esnext",
+ "dom"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": false, /* Enable all strict type-checking options. */
+ // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
+ // "strictNullChecks": true, /* Enable strict null checks. */
+ "strictFunctionTypes": true, /* Enable strict checking of function types. */
+ // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
+ "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
+ "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
+ "noUnusedLocals": true, /* Report errors on unused locals. */
+ "noUnusedParameters": true, /* Report errors on unused parameters. */
+ "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+ "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
+ "incremental": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "noEmit": true
+ },
+ "include": [
+ "next-env.d.ts",
+ ".next/types/**/*.ts",
+ "**/*.ts",
+ "**/*.tsx"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}