Skip to content

Commit

Permalink
Refactorization of account classes -- w pending cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
KaffinPX committed Jul 22, 2024
1 parent eb9745c commit 693709e
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 108 deletions.
5 changes: 3 additions & 2 deletions src/pages/Wallet/Send/Submit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ export default function Submit ({ transactions, onSubmitted }: {
<Button className={"gap-2"} onClick={({ currentTarget }) => {
currentTarget.disabled = true

kaspa.request('node:submit', [ transactions ]).then((ids) => {
kaspa.request('account:submitContextful', [ transactions ]).then((ids) => {
setIds(ids)
onSubmitted()
}).catch(() => {
}).catch((err) => {
console.log(err)
currentTarget.disabled = false
})
}}>
Expand Down
3 changes: 1 addition & 2 deletions src/pages/Wallet/UTXOs.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useEffect, useRef, useState } from "react"
import { Button } from "@/components/ui/button"
import useKaspa from "@/hooks/useKaspa"
import { OutputStatus } from "@/wallet/kaspa/account"

export default function UTXOs () {
const { kaspa } = useKaspa()
Expand Down Expand Up @@ -35,7 +34,7 @@ export default function UTXOs () {
<div className={"grid grid-cols-3 mx-4 gap-2"}>
{kaspa.utxos.slice(0, index).map((utxo, id) => {
return (
<div key={id} className={"flex flex-col items-center py-2 border-2 rounded-xl w-full h-24 " + (utxo.status === OutputStatus.mature ? "hover:border-dashed" : (utxo.status === OutputStatus.incoming ? "border-yellow-600" : "border-red-600"))}>
<div key={id} className={"flex flex-col items-center py-2 border-2 rounded-xl w-full h-24 " + (utxo.mature ? "hover:border-dashed" : "border-yellow-600")}>
<p className={"text-lg font-bold"}>{utxo.amount.toFixed(4)}</p>
<Button variant="link" className={"text-inherit font-extrabold"} onClick={() => {
window.open(`https://explorer.kaspa.org/txs/${utxo.transaction}`)
Expand Down
50 changes: 40 additions & 10 deletions src/wallet/kaspa/account/addresses.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,35 @@
import { PublicKeyGenerator } from "@/../wasm"
import { PublicKeyGenerator, UtxoContext } from "@/../wasm"
import LocalStorage from "@/storage/LocalStorage"
import { EventEmitter } from "events"

export default class Addresses {
export default class Addresses extends EventEmitter {
context: UtxoContext
publicKey: PublicKeyGenerator | undefined
networkId: string
accountId: number | undefined
networkId: string // TODO: can be derived from context
receiveAddresses: string[] = []
changeAddresses: string[] = []

constructor (networkId: string) {
constructor (context: UtxoContext, networkId: string) {
super()

this.context = context
this.networkId = networkId
}

get allAddresses () {
return [ ...this.receiveAddresses, ...this.changeAddresses ]
}

async import (publicKey: PublicKeyGenerator, accountId: number) {
this.publicKey = publicKey
this.accountId = accountId

const account = (await LocalStorage.get('wallet', undefined))!.accounts[accountId]

await this.increment(account.receiveCount, account.changeCount, false)
}

async derive (isReceive: boolean, start: number, end: number) {
if (!this.publicKey) throw Error('No active account')

Expand All @@ -24,28 +40,42 @@ export default class Addresses {
}
}

async increment (receiveCount: number, changeCount: number) {
const [ receiveAddresses, changeAddresses ] = await Promise.all([
async increment (receiveCount: number, changeCount: number, commit = true) {
const addresses = await Promise.all([
this.derive(true, this.receiveAddresses.length, this.receiveAddresses.length + receiveCount),
this.derive(false, this.changeAddresses.length, this.changeAddresses.length + changeCount)
])

this.receiveAddresses.push(...receiveAddresses)
this.changeAddresses.push(...changeAddresses)
this.receiveAddresses.push(...addresses[0])
this.changeAddresses.push(...addresses[1])

return [ receiveAddresses, changeAddresses ]
if (this.context.isActive) await this.context.trackAddresses(addresses.flat())
if (commit) await this.commit()

this.emit('addresses', addresses)
}

async setNetworkId (networkId: string) {
async changeNetwork (networkId: string) {
this.networkId = networkId
this.receiveAddresses = await this.derive(true, 0, this.receiveAddresses.length)
this.changeAddresses = await this.derive(false, 0, this.changeAddresses.length)
}

reset () {
delete this.publicKey
delete this.accountId

this.receiveAddresses = []
this.changeAddresses = []
}

private async commit () {
const wallet = (await LocalStorage.get('wallet', undefined))!
const account = wallet.accounts[this.accountId!]

account.receiveCount = this.receiveAddresses.length
account.changeCount = this.changeAddresses.length

await LocalStorage.set('wallet', wallet)
}
}
109 changes: 19 additions & 90 deletions src/wallet/kaspa/account/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,28 @@ import { EventEmitter } from "events"
import { UtxoContext, UtxoProcessor, PublicKeyGenerator, PrivateKeyGenerator, createTransactions, Transaction, decryptXChaCha20Poly1305, kaspaToSompi, signTransaction, type UtxoEntryReference } from "@/../wasm"
import type Node from "../node"
import Addresses from "./addresses"
import LocalStorage from "@/storage/LocalStorage"
import SessionStorage, { ISession } from "@/storage/SessionStorage"

export enum OutputStatus {
mature,
incoming,
outgoing
}
import SessionStorage from "@/storage/SessionStorage"
import Transactions from "./transactions"

export interface UTXO {
amount: number
transaction: string,
status: OutputStatus
mature: boolean
}

export default class Account extends EventEmitter {
processor: UtxoProcessor
addresses: Addresses
session: ISession | undefined
context: UtxoContext
transactions: Transactions

constructor (node: Node) {
super()

this.addresses = new Addresses(node.networkId)
this.processor = new UtxoProcessor({ rpc: node.kaspa, networkId: node.networkId })
this.context = new UtxoContext({ processor: this.processor })
this.addresses = new Addresses(this.context, node.networkId)
this.transactions = new Transactions(node.kaspa, this.context, this.addresses)

node.on('network', async (networkId: string) => {
if (this.processor.isActive) {
Expand All @@ -39,7 +34,7 @@ export default class Account extends EventEmitter {
this.processor.setNetworkId(networkId)
}

await this.addresses.setNetworkId(networkId)
await this.addresses.changeNetwork(networkId)
})

this.registerProcessor()
Expand All @@ -51,60 +46,16 @@ export default class Account extends EventEmitter {
}

get UTXOs () {
const mapUTXO = (utxo: UtxoEntryReference, status: OutputStatus) => ({
const mapUTXO = (utxo: UtxoEntryReference, mature: boolean) => ({
amount: Number(utxo.amount) / 1e8,
transaction: utxo.getTransactionId(),
status
mature
})

const outgoingUTXOs = this.context.getPendingOutgoingTransactions().map(transaction => transaction.transaction.inputs.map((input) => mapUTXO(input.utxo!, OutputStatus.outgoing)))
const pendingUTXOs = this.context.getPending().map(utxo => mapUTXO(utxo, OutputStatus.incoming))
const matureUTXOs = this.context.getMatureRange(0, this.context.matureLength).map(utxo => mapUTXO(utxo, OutputStatus.mature))
const pendingUTXOs = this.context.getPending().map(utxo => mapUTXO(utxo, false))
const matureUTXOs = this.context.getMatureRange(0, this.context.matureLength).map(utxo => mapUTXO(utxo, true))

return [ ...outgoingUTXOs, ...pendingUTXOs, ...matureUTXOs ]
}

async createSend (recipient: string, amount: string, fee: string) {
const { transactions } = await createTransactions({
entries: this.context,
outputs: [{
address: recipient,
amount: kaspaToSompi(amount)!
}],
changeAddress: this.addresses.changeAddresses[this.addresses.changeAddresses.length - 1],
priorityFee: kaspaToSompi(fee)!,
})

await this.incrementAddresses(0, 1)
return transactions.map((transaction) => transaction.serializeToSafeJSON())
}

async sign (transactions: string[], password: string) {this.context.getPendingOutgoingTransactions()
const keyGenerator = new PrivateKeyGenerator(decryptXChaCha20Poly1305(this.session!.encryptedKey, password), false, BigInt(this.session!.activeAccount))
const signedTransactions: Transaction[] = []

for (const transaction of transactions) {
const parsedTransaction = Transaction.deserializeFromSafeJSON(transaction)
const privateKeys = []

for (const address of parsedTransaction.addresses(this.addresses.networkId)) {
const receiveIndex = this.addresses.receiveAddresses.indexOf(address.toString())
const changeIndex = this.addresses.changeAddresses.indexOf(address.toString())

if (receiveIndex !== -1) {
privateKeys.push(keyGenerator.receiveKey(receiveIndex))
} else if (changeIndex !== -1) {
privateKeys.push(keyGenerator.changeKey(changeIndex))
} else throw Error('UTXO is not owned by active wallet')
}

signedTransactions.push(signTransaction(parsedTransaction, privateKeys, false))
}

const parsedTransactions = signedTransactions.map(transaction => transaction.serializeToSafeJSON())
this.emit('transactions', parsedTransactions)

return parsedTransactions
return [ ...pendingUTXOs, ...matureUTXOs ]
}

async scan (steps = 50, count = 10) {
Expand All @@ -123,11 +74,11 @@ export default class Account extends EventEmitter {
if (entries.length > 0) { foundIndex = startIndex }
}

await this.incrementAddresses(isReceive ? foundIndex : 0, isReceive ? 0 : foundIndex)
await this.addresses.increment(isReceive ? foundIndex : 0, isReceive ? 0 : foundIndex)
}

await scanAddresses(true, this.addresses.receiveAddresses.length - 1)
await scanAddresses(false, this.addresses.changeAddresses.length - 1)
await scanAddresses(true, this.addresses.receiveAddresses.length)
await scanAddresses(false, this.addresses.changeAddresses.length)
}

private registerProcessor () {
Expand All @@ -140,7 +91,7 @@ export default class Account extends EventEmitter {
const utxos = event.data.data.utxoEntries

if (utxos.some(utxo => utxo.address?.toString() === this.addresses.receiveAddresses[this.addresses.receiveAddresses.length - 1])) {
await this.incrementAddresses(1, 0)
await this.addresses.increment(1, 0)
}
})

Expand All @@ -149,39 +100,17 @@ export default class Account extends EventEmitter {
})
}

private async incrementAddresses (receiveCount: number, changeCount: number, initial = false) {
const addresses = await this.addresses.increment(receiveCount, changeCount)
if (this.processor.isActive) await this.context.trackAddresses(addresses.flat())

if (!initial) {
const wallet = (await LocalStorage.get('wallet', undefined))!
const account = wallet.accounts[this.session!.activeAccount]

account.receiveCount = this.addresses.receiveAddresses.length
account.changeCount = this.addresses.changeAddresses.length

await LocalStorage.set('wallet', wallet)
}

this.emit('addresses', addresses)
}

private listenSession () {
SessionStorage.subscribeChanges(async (key, newValue) => {
if (key !== 'session') return

if (newValue) {
this.session = newValue // TODO: Possibly get rid of this w account set optimizations
this.addresses.publicKey = await PublicKeyGenerator.fromXPub(this.session.publicKey)

const account = (await LocalStorage.get('wallet', undefined))!.accounts[this.session.activeAccount]

await this.incrementAddresses(account.receiveCount, account.changeCount, true)
await this.addresses.import(PublicKeyGenerator.fromXPub(newValue.publicKey), newValue.activeAccount)
await this.transactions.import(newValue.encryptedKey, newValue.activeAccount)
await this.processor.start()
} else {
delete this.session

this.addresses.reset()
this.transactions.reset()
await this.processor.stop()
await this.context.clear()
}
Expand Down
Loading

0 comments on commit 693709e

Please sign in to comment.