From c79d42572e1db586be2505c1356ba56c535bb7be Mon Sep 17 00:00:00 2001 From: tomholford Date: Sat, 28 Jan 2023 21:36:01 -0800 Subject: [PATCH 1/7] data: add Account & Wallet sur, data model doc --- desk/sur/account.hoon | 37 ++++++++++++++++++++++++++ desk/sur/wallet.hoon | 34 ++++++++++++++++++++++++ notes/data.md | 60 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 desk/sur/account.hoon create mode 100644 desk/sur/wallet.hoon create mode 100644 notes/data.md diff --git a/desk/sur/account.hoon b/desk/sur/account.hoon new file mode 100644 index 0000000..407d7bb --- /dev/null +++ b/desk/sur/account.hoon @@ -0,0 +1,37 @@ +:: /sur/account.hoon +|% +:: Account ID - uuid string used for edit, del, query (not an ETH 0x...) ++$ id @t +:: Wallet ID - foreign key to parent wallet ++$ wallet-id @t +:: Account Name ++$ name @t +:: Note - optional description of the account ++$ note @t +:: TODO: tags, types ++$ account + $: id=id + wallet-id=wallet-id + name=name + note=note + == +:: Accounts - map of account-id to account ++$ accounts (map id account) +++ action + =< + |% + +$ action + $% [%add add] + [%del del] + [%edit edit] + == + +$ add [=id =wallet-id =name =note] + +$ del [=id] + +$ edit [=id =wallet-id =name =note] + -- ++$ update + $% [%add =account] + [%del =id] + [%edit =account] + == +-- diff --git a/desk/sur/wallet.hoon b/desk/sur/wallet.hoon new file mode 100644 index 0000000..dba3ca2 --- /dev/null +++ b/desk/sur/wallet.hoon @@ -0,0 +1,34 @@ +:: /sur/wallet.hoon +|% +:: Wallet ID - uuid string used for edit, del, query (not an ETH 0x...) ++$ id @t +:: Wallet Name ++$ name @t +:: Note - optional description of the wallet ++$ note @t +:: TODO: tags, types ++$ wallet + $: id=id + name=name + note=note + == +:: Wallets - map of wallet ids to wallets ++$ wallets (map id wallet) +++ action + =< action + |% + +$ action + $% [%add add] + [%del del] + [%edit edit] + == + +$ add [=id =name =note] + +$ del [=id] + +$ edit [=id =name =note] + -- ++$ update + $% [%add =wallet] + [%del =id] + [%edit =wallet] + == +-- diff --git a/notes/data.md b/notes/data.md new file mode 100644 index 0000000..7bec58b --- /dev/null +++ b/notes/data.md @@ -0,0 +1,60 @@ +# Data Model + +## Wallets + +A wallet is a collection of accounts. + +```hoon +:: Wallet ID - uuid string used for edit, del, query (not an ETH 0x...) ++$ id @t +:: Wallet Name ++$ name @t +:: Note - optional description of the wallet ++$ note @t +:: TODO: tags, types +``` + +## Accounts + +An account is a collection of transactions. + +```hoon +:: Account ID - uuid string used for edit, del, query (not an ETH 0x...) ++$ id @t +:: Wallet ID - foreign key to parent wallet ++$ wallet-id @t +:: Account Name ++$ name @t +:: Note - optional description of the account ++$ note @t +:: TODO: tags, types +``` + +## Transactions + +A transaction is a credit or debit to an account. + +```hoon +:: Transaction ID - uuid string used for edit, del, query (not an ETH 0x...) ++$ id @t +:: CoinGecko Coin ID - external ID that maps to a CG entity (e.g. BTC is 'bitcoin'). +:: Used for querying CG API ++$ coin-id @t +:: Timestamp of transaction ++$ date @da +:: Optional description of the TX ++$ note @t +:: TX Amount in currency +:: Note usage of a string for storage; since no maths will be done in the app +:: (for now!), it is simpler for a noob like me to work with. Eventually this +:: will be migrated to using an aura with numerical precision ++$ amount @t +:: optional: cost per unit at the time of the TX (can be roughly auto-populated +:: with CoinGecko History API async, or manually entered by user... or if +:: imported from a CEX, use that value). see note re: string usage for amount, +:: same goes for this ++$ cost-basis @t +:: %buy, %sell ++$ type @ta +:: TODO: tags +``` From 88a8a77ba4b236c80737bcd280384d82fec0c398 Mon Sep 17 00:00:00 2001 From: tomholford Date: Sat, 11 Feb 2023 00:35:37 -0800 Subject: [PATCH 2/7] data: account-id.transaction + state migration --- desk/app/hodl.hoon | 83 +++++++++++++++++++++++-------------- desk/lib/transaction.hoon | 3 ++ desk/sur/transaction-0.hoon | 53 +++++++++++++++++++++++ desk/sur/transaction.hoon | 12 +++++- 4 files changed, 119 insertions(+), 32 deletions(-) create mode 100644 desk/sur/transaction-0.hoon diff --git a/desk/app/hodl.hoon b/desk/app/hodl.hoon index d7cb187..540819d 100644 --- a/desk/app/hodl.hoon +++ b/desk/app/hodl.hoon @@ -6,18 +6,26 @@ :: mainly for orchestration. But, first focus on getting a working TX CRUD :: agent before optimizing. So, one day :) :: -/+ *transaction -/+ default-agent, dbug, agentio +/+ *account, *transaction, *wallet +/+ default-agent, dbug, agentio, verb |% ++$ card card:agent:gall +$ versioned-state $% state-0 + state-1 == -+$ state-0 [%0 txns=transactions] -+$ card card:agent:gall +:: +:: acts: map of account-id --> account +:: txns: map of transaction-id --> transaction +:: wlts: map of wallet-id --> wallets ++$ state-0 [%0 txns=transactions:zero:past] ++$ state-1 [%1 acts=accounts txns=transactions wlts=wallets] -- -%- agent:dbug -=| state-0 +=| state-1 =* state - +:: +%- agent:dbug +%+ verb & :: TODO: disable before production ^- agent:gall |_ =bowl:gall +* this . @@ -31,10 +39,44 @@ !>(state) :: ++ on-load - |= old-vase=vase + |= old-state=vase ^- (quip card _this) - `this(state !<(versioned-state old-vase)) + =/ old !<(versioned-state old-state) + ?- -.old + %1 + `this(state old) + %0 + `this(state-0-to-1 old) + == + ++ state-0-to-1 + |= zer=state-0 + ^- state-1 + :* %1 + acts *accounts + txns (transactions-0-to-1 txns.zer) + wlts *wallets + == + ++ transactions-0-to-1 + |= txns=transactions:zero:past + ^- transactions + %- ~(run by txns) + |= [id=@t txn=transaction:zero:past] + ^- [id=@t txn=transaction] + [id (transaction-0-to-1 txn)] + ++ transaction-0-to-1 + |= txn=transaction:zero:past + ^- ^transaction :: TODO: which transaction to point to? should use the ket to traverse up the tree? + :* id.txn + coin-id.txn + date.txn + note.txn + amount.txn + cost-basis.txn + type.txn + ~ + == :: +:: TODO: add pokes for accounts and wallets ++ on-poke |= [=mark =vase] ^- (quip card _this) @@ -60,6 +102,7 @@ amount=amount.act cost-basis=cost-basis.act type=type.act + account-id=account-id.act == state(txns (~(put by txns) id.act txn)) :: @@ -73,6 +116,7 @@ amount=amount.act cost-basis=cost-basis.act type=type.act + account-id=account-id.act == state(txns (~(put by txns) id.act txn)) :: TODO: should all fields be editable? probably not id :: @@ -90,40 +134,19 @@ [%updates ~] `this == :: -:: ++ on-peek on-peek:def +:: TODO: add scries for accounts and wallets ++ on-peek |= =path ^- (unit (unit cage)) ?> (team:title our.bowl src.bowl) - =/ now=@ (unm:chrono:userlib now.bowl) ?+ path (on-peek:def path) [%x %transactions *] - :: ~& t.path - :: (on-peek:def path) ?+ t.t.path (on-peek:def path) [%all ~] :^ ~ ~ %hodl-update !> ^- update [%txns txns] - :: [%txns (tap:t-orm txns)] - :: [now %txns (tap:t-orm txns)] == - :: [%before @ @ ~] - :: =/ before=@ (rash i.t.t.t.path dem) - :: =/ max=@ (rash i.t.t.t.t.path dem) - :: :^ ~ ~ %hodl-update - :: !> ^- update - :: [now %txns (tab:t-orm txns `before max)] - :: - :: [%between @ @ ~] - :: =/ start=@ - :: =+ (rash i.t.t.t.path dem) - :: ?:(=(0 -) - (sub - 1)) - :: =/ end=@ (add 1 (rash i.t.t.t.t.path dem)) - :: :^ ~ ~ %hodl-update - :: !> ^- update - :: [now %txns (tap:t-orm (lot:t-orm txns `end `start))] - :: == == :: ++ on-leave on-leave:def diff --git a/desk/lib/transaction.hoon b/desk/lib/transaction.hoon index 5096168..8133374 100644 --- a/desk/lib/transaction.hoon +++ b/desk/lib/transaction.hoon @@ -20,6 +20,7 @@ amount+so cost-basis+so type+so + account-id+so == ++ edit ^- $-(json edit:^action) @@ -31,6 +32,7 @@ amount+so cost-basis+so type+so + account-id+so == ++ del ^- $-(json del:^action) @@ -75,6 +77,7 @@ amount/s+amount cost-basis/s+cost-basis type/s+type + account-id/s+account-id == -- -- diff --git a/desk/sur/transaction-0.hoon b/desk/sur/transaction-0.hoon new file mode 100644 index 0000000..4ab458a --- /dev/null +++ b/desk/sur/transaction-0.hoon @@ -0,0 +1,53 @@ +:: /sur/transaction-0.hoon +|% +:: Transaction ID - uuid string used for edit, del, query (not an ETH 0x...) ++$ id @t +:: CoinGecko Coin ID - external ID that maps to a CG entity (e.g. BTC is 'bitcoin'). +:: Used for querying CG API ++$ coin-id @t +:: Timestamp of transaction ++$ date @da +:: Optional description of the TX ++$ note @t +:: TX Amount in currency +:: Note usage of a string for storage; since no maths will be done in the app +:: (for now!), it is simpler for a noob like me to work with. Eventually this +:: will be migrated to using an aura with numerical precision ++$ amount @t +:: optional: cost per unit at the time of the TX (can be roughly auto-populated +:: with CoinGecko History API async, or manually entered by user... or if +:: imported from a CEX, use that value). see note re: string usage for amount, +:: same goes for this ++$ cost-basis @t +:: %buy, %sell ++$ type @ta ++$ txn + $: =id + =coin-id + =date + =note + =amount + =cost-basis + =type + == +++ action + =< action + |% + +$ action + $% [%add add] + [%edit edit] + [%del del] + == + +$ add [=id =coin-id =date =note =amount =cost-basis =type] + +$ edit [=id =coin-id =date =note =amount =cost-basis =type] + +$ del [=id] + -- ++$ update + $% action + [%txns txns=transactions] + [%add =txn] + [%edit =txn] + [%del =id] + == ++$ transactions (map id txn) +-- diff --git a/desk/sur/transaction.hoon b/desk/sur/transaction.hoon index 8fbf27f..69463ed 100644 --- a/desk/sur/transaction.hoon +++ b/desk/sur/transaction.hoon @@ -1,5 +1,10 @@ :: /sur/transaction.hoon +/- zer=transaction-0 |% +++ past + |% + ++ zero zer + -- :: Transaction ID - uuid string used for edit, del, query (not an ETH 0x...) +$ id @t :: CoinGecko Coin ID - external ID that maps to a CG entity (e.g. BTC is 'bitcoin'). @@ -21,6 +26,8 @@ +$ cost-basis @t :: %buy, %sell +$ type @ta +:: Account ID - foreign key to parent account ++$ account-id @t +$ txn $: =id =coin-id @@ -29,6 +36,7 @@ =amount =cost-basis =type + =account-id == ++ action =< action @@ -38,8 +46,8 @@ [%edit edit] [%del del] == - +$ add [=id =coin-id =date =note =amount =cost-basis =type] - +$ edit [=id =coin-id =date =note =amount =cost-basis =type] + +$ add [=id =coin-id =date =note =amount =cost-basis =type =account-id] + +$ edit [=id =coin-id =date =note =amount =cost-basis =type =account-id] +$ del [=id] -- +$ update From 366b5a818bb89210955fe624104951bfdd636c3d Mon Sep 17 00:00:00 2001 From: tomholford Date: Sun, 23 Apr 2023 02:28:14 -0700 Subject: [PATCH 3/7] wip --- desk/app/hodl.hoon | 204 ++++++++++++++------- desk/lib/account.hoon | 39 ++++ desk/lib/transaction.hoon | 52 +++--- desk/lib/wallet.hoon | 59 ++++++ desk/mar/account/action.hoon | 14 ++ desk/mar/account/update.hoon | 14 ++ desk/mar/{hodl => transaction}/action.hoon | 1 + desk/mar/{hodl => transaction}/update.hoon | 0 desk/mar/wallet/action.hoon | 13 ++ desk/mar/wallet/update.hoon | 14 ++ desk/sur/account.hoon | 35 ++-- desk/sur/transaction.hoon | 26 +-- desk/sur/wallet.hoon | 36 ++-- 13 files changed, 347 insertions(+), 160 deletions(-) create mode 100644 desk/lib/account.hoon create mode 100644 desk/lib/wallet.hoon create mode 100644 desk/mar/account/action.hoon create mode 100644 desk/mar/account/update.hoon rename desk/mar/{hodl => transaction}/action.hoon (89%) rename desk/mar/{hodl => transaction}/update.hoon (100%) create mode 100644 desk/mar/wallet/action.hoon create mode 100644 desk/mar/wallet/update.hoon diff --git a/desk/app/hodl.hoon b/desk/app/hodl.hoon index 540819d..557e80d 100644 --- a/desk/app/hodl.hoon +++ b/desk/app/hodl.hoon @@ -6,8 +6,8 @@ :: mainly for orchestration. But, first focus on getting a working TX CRUD :: agent before optimizing. So, one day :) :: -/+ *account, *transaction, *wallet -/+ default-agent, dbug, agentio, verb +/+ a=account, t=transaction, w=wallet +/+ default-agent, dbug, verb |% +$ card card:agent:gall +$ versioned-state @@ -18,8 +18,8 @@ :: acts: map of account-id --> account :: txns: map of transaction-id --> transaction :: wlts: map of wallet-id --> wallets -+$ state-0 [%0 txns=transactions:zero:past] -+$ state-1 [%1 acts=accounts txns=transactions wlts=wallets] ++$ state-0 [%0 txns=transactions:zero:past:t] ++$ state-1 [%1 acts=accounts:a txns=transactions:t wlts=wallets:w] -- =| state-1 =* state - @@ -27,10 +27,12 @@ %- agent:dbug %+ verb & :: TODO: disable before production ^- agent:gall +:: +=< :: app core |_ =bowl:gall +* this . + do ~(. +> bowl) def ~(. (default-agent this %.n) bowl) - io ~(. agentio bowl) :: ++ on-init on-init:def :: @@ -42,30 +44,29 @@ |= old-state=vase ^- (quip card _this) =/ old !<(versioned-state old-state) + |^ ?- -.old - %1 - `this(state old) - %0 - `this(state-0-to-1 old) + %1 `this(state old) + %0 `this(state (state-0-to-1 old)) == ++ state-0-to-1 |= zer=state-0 ^- state-1 :* %1 - acts *accounts + acts *accounts:a txns (transactions-0-to-1 txns.zer) - wlts *wallets + wlts *wallets:w == ++ transactions-0-to-1 - |= txns=transactions:zero:past - ^- transactions + |= txns=transactions:zero:past:t + ^- transactions:t %- ~(run by txns) - |= [id=@t txn=transaction:zero:past] - ^- [id=@t txn=transaction] + |= [id=@t txn=transaction:zero:past:t] + ^- [id=@t txn=transaction:t] [id (transaction-0-to-1 txn)] ++ transaction-0-to-1 - |= txn=transaction:zero:past - ^- ^transaction :: TODO: which transaction to point to? should use the ket to traverse up the tree? + |= txn=transaction:zero:past:t + ^- transaction:t :* id.txn coin-id.txn date.txn @@ -75,78 +76,44 @@ type.txn ~ == + -- :: -:: TODO: add pokes for accounts and wallets ++ on-poke |= [=mark =vase] ^- (quip card _this) - |^ ?> (team:title our.bowl src.bowl) - ?. ?=(%hodl-action mark) (on-poke:def mark vase) - =/ act !<(action vase) - =. state (poke-action act) - :_ this - ~[(fact:io hodl-update+!>(`update`[act]) ~[/updates])] - :: - ++ poke-action - |= act=action - ^- _state - ?- -.act - %add - ?< (~(has by txns) id.act) - =/ txn=txn - :* id=id.act - coin-id=coin-id.act - date=date.act - note=note.act - amount=amount.act - cost-basis=cost-basis.act - type=type.act - account-id=account-id.act - == - state(txns (~(put by txns) id.act txn)) - :: - %edit - ?> (~(has by txns) id.act) - =/ txn=txn - :* id=id.act - coin-id=coin-id.act - date=date.act - note=note.act - amount=amount.act - cost-basis=cost-basis.act - type=type.act - account-id=account-id.act - == - state(txns (~(put by txns) id.act txn)) :: TODO: should all fields be editable? probably not id - :: - %del - ?> (~(has by txns) id.act) - state(txns (~(del by txns) id.act)) + =^ cards state + ?+ mark (on-poke:def mark vase) + %account-action (poke-account-action:do !<(action:a vase)) :: TODO do these action need to be namesspaced? + %transaction-action (poke-transaction-action:do !<(action:t vase)) + %wallet-action (poke-wallet-action:do !<(action:w vase)) == - -- + [cards this] :: ++ on-watch |= =path ^- (quip card _this) ?> (team:title our.bowl src.bowl) ?+ path (on-watch:def path) - [%updates ~] `this + [%updates ~] `this + :: TODO: should the updates be scoped to each model? + :: ?: ?=([%updates ~] path) + :: :_ this + :: [%give %fact ~ %hodl-update !>(update)]~ + :: (on-watch:def path) == :: -:: TODO: add scries for accounts and wallets ++ on-peek |= =path ^- (unit (unit cage)) ?> (team:title our.bowl src.bowl) - ?+ path (on-peek:def path) - [%x %transactions *] - ?+ t.t.path (on-peek:def path) - [%all ~] - :^ ~ ~ %hodl-update - !> ^- update - [%txns txns] - == + ?+ path (on-peek:def path) + [%x %accounts @t ~] ``noun+!>((~(get by acts) i.t.t.path)) + [%x %accounts ~] ``noun+!>(acts) + [%x %transactions @t ~] ``noun+!>((~(get by txns) i.t.t.path)) + [%x %transactions ~] ``noun+!>(txns) + [%x %wallets @t ~] ``noun+!>((~(get by wlts) i.t.t.path)) + [%x %wallets ~] ``noun+!>(wlts) == :: ++ on-leave on-leave:def @@ -154,3 +121,98 @@ ++ on-arvo on-arvo:def ++ on-fail on-fail:def -- +:: helper core +|_ =bowl:gall +++ poke-account-action + |= act=action:a + ^- _state + ?- -.act + %add + ?< (~(has by acts) id.act) + =/ =acct:a + :* id=id.act + wallet-id=wallet-id.act + name=name.act + note=note.act + == + state(acts (~(put by acts) id.act acct)) + :: + %edit + ?> (~(has by acts) id.act) + =/ =acct:a + :* id=id.act :: TODO: should all fields be editable? probably not id + wallet-id=wallet-id.act + name=name.act + note=note.act + == + state(acts (~(put by acts) id.act acct)) + :: + %del + ?> (~(has by acts) id.act) + state(acts (~(del by acts) id.act)) + == +:: +++ poke-transaction-action + |= act=action:t + ^- _state + ?- -.act + %add + ?< (~(has by txns) id.act) + =/ =txn:t + :* id=id.act + coin-id=coin-id.act + date=date.act + note=note.act + amount=amount.act + cost-basis=cost-basis.act + type=type.act + account-id=account-id.act + == + state(txns (~(put by txns) id.act txn)) + :: + %edit + ?> (~(has by txns) id.act) + =/ =txn:t + :* id=id.act :: TODO: should all fields be editable? probably not id + coin-id=coin-id.act + date=date.act + note=note.act + amount=amount.act + cost-basis=cost-basis.act + type=type.act + account-id=account-id.act + == + state(txns (~(put by txns) id.act txn)) + :: + %del + ?> (~(has by txns) id.act) + state(txns (~(del by txns) id.act)) + == +:: +++ poke-wallet-action + |= act=action:w + ^- _state + ?- -.act + %add + ?< (~(has by wlts) id.act) + =/ =wllt:w + :* id=id.act + name=name.act + note=note.act + == + state(wlts (~(put by wlts) id.act wllt)) + :: + %edit + ?> (~(has by wlts) id.act) + =/ =wllt:w + :* id=id.act :: TODO: should all fields be editable? probably not id + name=name.act + note=note.act + == + state(wlts (~(put by wlts) id.act wllt)) + :: + %del + ?> (~(has by wlts) id.act) + state(wlts (~(del by wlts) id.act)) + == +-- diff --git a/desk/lib/account.hoon b/desk/lib/account.hoon new file mode 100644 index 0000000..50e52d1 --- /dev/null +++ b/desk/lib/account.hoon @@ -0,0 +1,39 @@ +/- *account +|% +++ dejs-action + =, dejs:format + |= jon=json + ^- action + %. jon + %- of + :~ [%add (ot ~[id+so wallet-id+so name+so note+so])] + [%edit (ot ~[id+so wallet-id+so name+so note+so])] + [%del (ot ~[id+so])] + == +++ enjs-update + =, enjs:format + |= upd=update + ^- json + |^ + ?- -.upd + %accts (acts +.upd) + == + ++ acts + |= ud=[=accounts] + ^- json + %- pairs + %+ turn ~(tap by accounts.ud) + |= act=[=id =acct] + ^- (pair @t json) + [id.act (acjs acct.act)] + ++ acjs + |= [id=@tas wallet-id=@tas name=@tas note=@tas] + ^- json + %- pairs + :~ id/s+id + wallet-id/s+wallet-id + name/s+name + note/s+note + == + -- +-- diff --git a/desk/lib/transaction.hoon b/desk/lib/transaction.hoon index 8133374..0c5e2bd 100644 --- a/desk/lib/transaction.hoon +++ b/desk/lib/transaction.hoon @@ -11,7 +11,6 @@ [%del del] == ++ add - ^- $-(json add:^action) %- ot :~ id+so coin-id+so @@ -23,7 +22,6 @@ account-id+so == ++ edit - ^- $-(json edit:^action) %- ot :~ id+so coin-id+so @@ -35,7 +33,7 @@ account-id+so == ++ del - ^- $-(json del:^action) + ^- json %- ot :~ id+so == @@ -46,38 +44,32 @@ ++ update |= upd=^update ^- json + %+ frond -.upd ?- -.upd - %txns - %- pairs - %+ turn ~(tap by txns.upd) - |= [=id =txn] - ^- (pair @t json) - [id (txjs txn)] - %add - %- pairs - :~ add/(txjs +.upd) - == - %edit - %- pairs - :~ edit/(txjs +.upd) - == - %del - %- pairs - :~ del/s+id.upd + %txns (txns txns.upd) == - == + :: + ++ txns + |= txns=transactions + ^- json + %- pairs + %+ turn ~(tap by txns) + |= t=[=id =txn] + ^- (pair @t json) + [id.t (txjs txn.t)] + :: ++ txjs - |= txn + |= t=txn ^- json %- pairs - :~ id/s+id - coin-id/s+coin-id - date/(time date) - note/s+note - amount/s+amount - cost-basis/s+cost-basis - type/s+type - account-id/s+account-id + :~ id/s+id.t + coin-id/s+coin-id.t + date/(time date.t) + note/s+note.t + amount/s+amount.t + cost-basis/s+cost-basis.t + type/s+type.t + account-id/s+account-id.t == -- -- diff --git a/desk/lib/wallet.hoon b/desk/lib/wallet.hoon new file mode 100644 index 0000000..cfff11e --- /dev/null +++ b/desk/lib/wallet.hoon @@ -0,0 +1,59 @@ +/- *wallet +|% +++ dejs + =, dejs:format + |% + ++ action + ^- $-(json ^action) + %- of + :~ add+add + edit+edit + del+del + == + ++ add + ^- json + %- ot + :~ id+so + name+so + note+so + == + ++ edit + ^- json + %- ot + :~ id+so + name+so + note+so + == + ++ del + ^- json + %- ot + :~ id+so + == + -- +++ enjs + =, enjs:format + |% + ++ update + |= upd=^update + ^- json + ?- -.upd + %wllts (wllts upd) + == + ++ wllts + |= wllts=(map id wllt) + ^- json + %- pairs + %+ turn ~(tap by wllts) + |= [=id =wllt] + ^- (pair @t json) + [id (wljs wllt)] + ++ wljs + |= wllt + ^- json + %- pairs + :~ id/s+id + name/s+name + note/s+note + == + -- +-- diff --git a/desk/mar/account/action.hoon b/desk/mar/account/action.hoon new file mode 100644 index 0000000..1f64aa3 --- /dev/null +++ b/desk/mar/account/action.hoon @@ -0,0 +1,14 @@ +/- *account +/+ *account +|_ act=action +++ grow + |% + ++ noun act + -- +++ grab + |% + ++ noun action + ++ json dejs-action + -- +++ grad %noun +-- diff --git a/desk/mar/account/update.hoon b/desk/mar/account/update.hoon new file mode 100644 index 0000000..77b8507 --- /dev/null +++ b/desk/mar/account/update.hoon @@ -0,0 +1,14 @@ +/- *account +/+ *account +|_ upd=update +++ grow + |% + ++ noun upd + ++ json (enjs-update upd) + -- +++ grab + |% + ++ noun update + -- +++ grad %noun +-- diff --git a/desk/mar/hodl/action.hoon b/desk/mar/transaction/action.hoon similarity index 89% rename from desk/mar/hodl/action.hoon rename to desk/mar/transaction/action.hoon index 6791d31..21b717c 100644 --- a/desk/mar/hodl/action.hoon +++ b/desk/mar/transaction/action.hoon @@ -1,3 +1,4 @@ +/- *transaction /+ *transaction |_ act=action ++ grow diff --git a/desk/mar/hodl/update.hoon b/desk/mar/transaction/update.hoon similarity index 100% rename from desk/mar/hodl/update.hoon rename to desk/mar/transaction/update.hoon diff --git a/desk/mar/wallet/action.hoon b/desk/mar/wallet/action.hoon new file mode 100644 index 0000000..b81d069 --- /dev/null +++ b/desk/mar/wallet/action.hoon @@ -0,0 +1,13 @@ +/+ *wallet +|_ act=action +++ grow + |% + ++ noun act + -- +++ grab + |% + ++ noun action + ++ json action:dejs + -- +++ grad %noun +-- diff --git a/desk/mar/wallet/update.hoon b/desk/mar/wallet/update.hoon new file mode 100644 index 0000000..c7e478d --- /dev/null +++ b/desk/mar/wallet/update.hoon @@ -0,0 +1,14 @@ +/- *wallet +/+ *wallet +|_ upd=update +++ grow + |% + ++ noun upd + ++ json (update:enjs upd) + -- +++ grab + |% + ++ noun update + -- +++ grad %noun +-- diff --git a/desk/sur/account.hoon b/desk/sur/account.hoon index 407d7bb..b377515 100644 --- a/desk/sur/account.hoon +++ b/desk/sur/account.hoon @@ -9,29 +9,22 @@ :: Note - optional description of the account +$ note @t :: TODO: tags, types -+$ account - $: id=id - wallet-id=wallet-id - name=name - note=note +:: Account struct, named uniquely ++$ acct + $: =id + =wallet-id + =name + =note == :: Accounts - map of account-id to account -+$ accounts (map id account) -++ action - =< - |% - +$ action - $% [%add add] - [%del del] - [%edit edit] - == - +$ add [=id =wallet-id =name =note] - +$ del [=id] - +$ edit [=id =wallet-id =name =note] - -- -+$ update - $% [%add =account] ++$ accounts (map id acct) ++$ action + $% [%add =id =wallet-id =name =note] + [%edit =id =wallet-id =name =note] [%del =id] - [%edit =account] + == +:: ++$ update + $% [%accts =accounts] == -- diff --git a/desk/sur/transaction.hoon b/desk/sur/transaction.hoon index 69463ed..1de9c27 100644 --- a/desk/sur/transaction.hoon +++ b/desk/sur/transaction.hoon @@ -28,6 +28,7 @@ +$ type @ta :: Account ID - foreign key to parent account +$ account-id @t +:: Transaction struct, named uniquely +$ txn $: =id =coin-id @@ -38,24 +39,15 @@ =type =account-id == -++ action - =< action - |% - +$ action - $% [%add add] - [%edit edit] - [%del del] - == - +$ add [=id =coin-id =date =note =amount =cost-basis =type =account-id] - +$ edit [=id =coin-id =date =note =amount =cost-basis =type =account-id] - +$ del [=id] - -- -+$ update - $% action - [%txns txns=transactions] - [%add =txn] +:: Transactions are stored in a map keyed by ID ++$ transactions (map id txn) ++$ action + $% [%add =txn] [%edit =txn] [%del =id] == -+$ transactions (map id txn) +:: ++$ update + $% [%txns txns=transactions] + == -- diff --git a/desk/sur/wallet.hoon b/desk/sur/wallet.hoon index dba3ca2..eefb7f4 100644 --- a/desk/sur/wallet.hoon +++ b/desk/sur/wallet.hoon @@ -7,28 +7,22 @@ :: Note - optional description of the wallet +$ note @t :: TODO: tags, types -+$ wallet - $: id=id - name=name - note=note +:: Wallet struct, named uniquely ++$ wllt + $: =id + =name + =note == -:: Wallets - map of wallet ids to wallets -+$ wallets (map id wallet) -++ action - =< action - |% - +$ action - $% [%add add] - [%del del] - [%edit edit] - == - +$ add [=id =name =note] - +$ del [=id] - +$ edit [=id =name =note] - -- -+$ update - $% [%add =wallet] +:: ++$ action + $% [%add =wllt] + [%edit =wllt] [%del =id] - [%edit =wallet] == +:: ++$ update + $% [%wllts wllts=wallets] + == +:: Wallets - map of wallet ids to wallets ++$ wallets (map id wllt) -- From 60517ad18ee42bb888687fbbf884403228d0c66b Mon Sep 17 00:00:00 2001 From: tomholford Date: Sat, 4 Apr 2026 22:32:15 -0700 Subject: [PATCH 4/7] fix: lib serialization type errors - Remove incorrect `^- json` casts from dejs arms in wallet.hoon and transaction.hoon (ot returns a gate, not json) - Fix enjs wallet update to pass `+.upd` instead of full tagged update - Fix account.hoon acjs aura from @tas to @t to match sur definitions --- desk/lib/account.hoon | 2 +- desk/lib/transaction.hoon | 1 - desk/lib/wallet.hoon | 5 +---- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/desk/lib/account.hoon b/desk/lib/account.hoon index 50e52d1..af42c52 100644 --- a/desk/lib/account.hoon +++ b/desk/lib/account.hoon @@ -27,7 +27,7 @@ ^- (pair @t json) [id.act (acjs acct.act)] ++ acjs - |= [id=@tas wallet-id=@tas name=@tas note=@tas] + |= [id=@t wallet-id=@t name=@t note=@t] ^- json %- pairs :~ id/s+id diff --git a/desk/lib/transaction.hoon b/desk/lib/transaction.hoon index 0c5e2bd..8f0a73a 100644 --- a/desk/lib/transaction.hoon +++ b/desk/lib/transaction.hoon @@ -33,7 +33,6 @@ account-id+so == ++ del - ^- json %- ot :~ id+so == diff --git a/desk/lib/wallet.hoon b/desk/lib/wallet.hoon index cfff11e..9590378 100644 --- a/desk/lib/wallet.hoon +++ b/desk/lib/wallet.hoon @@ -11,21 +11,18 @@ del+del == ++ add - ^- json %- ot :~ id+so name+so note+so == ++ edit - ^- json %- ot :~ id+so name+so note+so == ++ del - ^- json %- ot :~ id+so == @@ -37,7 +34,7 @@ |= upd=^update ^- json ?- -.upd - %wllts (wllts upd) + %wllts (wllts +.upd) == ++ wllts |= wllts=(map id wllt) From ee4770f63514ce5e49b5b9df524d640342d8f01a Mon Sep 17 00:00:00 2001 From: tomholford Date: Sat, 4 Apr 2026 23:47:18 -0700 Subject: [PATCH 5/7] fix: agent type errors (migration + helper core) Migration (on-load): - Inline old transaction type as txn-0 to avoid unresolvable type path through transaction-0 sur import chain - Fix ~(run by) gate to accept value only, not [key value] - Fix :* tuple construction (bare faces parsed as separate elements) - Use '' (empty cord) instead of ~ for default account-id Helper core (poke handlers): - Return (quip card _state) instead of _state for =^ compat - Extract nested struct from action before accessing fields (txn.act/wllt.act) since face resolution doesn't descend through named faces like =txn or =wllt --- desk/app/hodl.hoon | 102 +++++++++++++-------------------------------- 1 file changed, 30 insertions(+), 72 deletions(-) diff --git a/desk/app/hodl.hoon b/desk/app/hodl.hoon index 557e80d..f14243a 100644 --- a/desk/app/hodl.hoon +++ b/desk/app/hodl.hoon @@ -10,15 +10,14 @@ /+ default-agent, dbug, verb |% +$ card card:agent:gall +:: old transaction type (state-0): no account-id field ++$ txn-0 [@t @t @da @t @t @t @ta] ++$ txns-0 (map @t txn-0) +$ versioned-state $% state-0 state-1 == -:: -:: acts: map of account-id --> account -:: txns: map of transaction-id --> transaction -:: wlts: map of wallet-id --> wallets -+$ state-0 [%0 txns=transactions:zero:past:t] ++$ state-0 [%0 txns=txns-0] +$ state-1 [%1 acts=accounts:a txns=transactions:t wlts=wallets:w] -- =| state-1 @@ -52,30 +51,15 @@ ++ state-0-to-1 |= zer=state-0 ^- state-1 - :* %1 - acts *accounts:a - txns (transactions-0-to-1 txns.zer) - wlts *wallets:w - == + [%1 *accounts:a (transactions-0-to-1 txns.zer) *wallets:w] ++ transactions-0-to-1 - |= txns=transactions:zero:past:t + |= txns=txns-0 ^- transactions:t %- ~(run by txns) - |= [id=@t txn=transaction:zero:past:t] - ^- [id=@t txn=transaction:t] - [id (transaction-0-to-1 txn)] - ++ transaction-0-to-1 - |= txn=transaction:zero:past:t - ^- transaction:t - :* id.txn - coin-id.txn - date.txn - note.txn - amount.txn - cost-basis.txn - type.txn - ~ - == + |= old=txn-0 + ^- txn:t + =+ [id cid dat not amt cb typ]=old + [id cid dat not amt cb typ ''] -- :: ++ on-poke @@ -125,7 +109,7 @@ |_ =bowl:gall ++ poke-account-action |= act=action:a - ^- _state + ^- (quip card _state) ?- -.act %add ?< (~(has by acts) id.act) @@ -135,7 +119,7 @@ name=name.act note=note.act == - state(acts (~(put by acts) id.act acct)) + `state(acts (~(put by acts) id.act acct)) :: %edit ?> (~(has by acts) id.act) @@ -145,74 +129,48 @@ name=name.act note=note.act == - state(acts (~(put by acts) id.act acct)) + `state(acts (~(put by acts) id.act acct)) :: %del ?> (~(has by acts) id.act) - state(acts (~(del by acts) id.act)) + `state(acts (~(del by acts) id.act)) == :: ++ poke-transaction-action |= act=action:t - ^- _state + ^- (quip card _state) ?- -.act %add - ?< (~(has by txns) id.act) - =/ =txn:t - :* id=id.act - coin-id=coin-id.act - date=date.act - note=note.act - amount=amount.act - cost-basis=cost-basis.act - type=type.act - account-id=account-id.act - == - state(txns (~(put by txns) id.act txn)) + =/ =txn:t txn.act + ?< (~(has by txns) id.txn) + `state(txns (~(put by txns) id.txn txn)) :: %edit - ?> (~(has by txns) id.act) - =/ =txn:t - :* id=id.act :: TODO: should all fields be editable? probably not id - coin-id=coin-id.act - date=date.act - note=note.act - amount=amount.act - cost-basis=cost-basis.act - type=type.act - account-id=account-id.act - == - state(txns (~(put by txns) id.act txn)) + =/ =txn:t txn.act + ?> (~(has by txns) id.txn) + `state(txns (~(put by txns) id.txn txn)) :: %del ?> (~(has by txns) id.act) - state(txns (~(del by txns) id.act)) + `state(txns (~(del by txns) id.act)) == :: ++ poke-wallet-action |= act=action:w - ^- _state + ^- (quip card _state) ?- -.act %add - ?< (~(has by wlts) id.act) - =/ =wllt:w - :* id=id.act - name=name.act - note=note.act - == - state(wlts (~(put by wlts) id.act wllt)) + =/ =wllt:w wllt.act + ?< (~(has by wlts) id.wllt) + `state(wlts (~(put by wlts) id.wllt wllt)) :: %edit - ?> (~(has by wlts) id.act) - =/ =wllt:w - :* id=id.act :: TODO: should all fields be editable? probably not id - name=name.act - note=note.act - == - state(wlts (~(put by wlts) id.act wllt)) + =/ =wllt:w wllt.act + ?> (~(has by wlts) id.wllt) + `state(wlts (~(put by wlts) id.wllt wllt)) :: %del ?> (~(has by wlts) id.act) - state(wlts (~(del by wlts) id.act)) + `state(wlts (~(del by wlts) id.act)) == -- From 20704b32898705732d045f439cc77978d899e9e9 Mon Sep 17 00:00:00 2001 From: tomholford Date: Sun, 5 Apr 2026 00:24:57 -0700 Subject: [PATCH 6/7] feat: emit subscription facts on poke - Add per-operation update variants to sur types ([%add ...], [%edit ...], [%del =id]) - Add JSON encoders for new update variants in lib enjs cores - Emit %give %fact cards on /updates path from all poke handlers - Matches existing frontend TransactionUpdate interface shape --- desk/app/hodl.hoon | 29 +++++++++++++++++++---------- desk/lib/account.hoon | 7 +++++-- desk/lib/transaction.hoon | 7 +++++-- desk/lib/wallet.hoon | 4 ++++ desk/sur/account.hoon | 3 +++ desk/sur/transaction.hoon | 3 +++ desk/sur/wallet.hoon | 3 +++ 7 files changed, 42 insertions(+), 14 deletions(-) diff --git a/desk/app/hodl.hoon b/desk/app/hodl.hoon index f14243a..37a8ae3 100644 --- a/desk/app/hodl.hoon +++ b/desk/app/hodl.hoon @@ -119,21 +119,24 @@ name=name.act note=note.act == - `state(acts (~(put by acts) id.act acct)) + :_ state(acts (~(put by acts) id.act acct)) + ~[[%give %fact ~[/updates] %account-update !>(`update:a`[%add acct])]] :: %edit ?> (~(has by acts) id.act) =/ =acct:a - :* id=id.act :: TODO: should all fields be editable? probably not id + :* id=id.act wallet-id=wallet-id.act name=name.act note=note.act == - `state(acts (~(put by acts) id.act acct)) + :_ state(acts (~(put by acts) id.act acct)) + ~[[%give %fact ~[/updates] %account-update !>(`update:a`[%edit acct])]] :: %del ?> (~(has by acts) id.act) - `state(acts (~(del by acts) id.act)) + :_ state(acts (~(del by acts) id.act)) + ~[[%give %fact ~[/updates] %account-update !>(`update:a`[%del id.act])]] == :: ++ poke-transaction-action @@ -143,16 +146,19 @@ %add =/ =txn:t txn.act ?< (~(has by txns) id.txn) - `state(txns (~(put by txns) id.txn txn)) + :_ state(txns (~(put by txns) id.txn txn)) + ~[[%give %fact ~[/updates] %transaction-update !>(`update:t`[%add txn])]] :: %edit =/ =txn:t txn.act ?> (~(has by txns) id.txn) - `state(txns (~(put by txns) id.txn txn)) + :_ state(txns (~(put by txns) id.txn txn)) + ~[[%give %fact ~[/updates] %transaction-update !>(`update:t`[%edit txn])]] :: %del ?> (~(has by txns) id.act) - `state(txns (~(del by txns) id.act)) + :_ state(txns (~(del by txns) id.act)) + ~[[%give %fact ~[/updates] %transaction-update !>(`update:t`[%del id.act])]] == :: ++ poke-wallet-action @@ -162,15 +168,18 @@ %add =/ =wllt:w wllt.act ?< (~(has by wlts) id.wllt) - `state(wlts (~(put by wlts) id.wllt wllt)) + :_ state(wlts (~(put by wlts) id.wllt wllt)) + ~[[%give %fact ~[/updates] %wallet-update !>(`update:w`[%add wllt])]] :: %edit =/ =wllt:w wllt.act ?> (~(has by wlts) id.wllt) - `state(wlts (~(put by wlts) id.wllt wllt)) + :_ state(wlts (~(put by wlts) id.wllt wllt)) + ~[[%give %fact ~[/updates] %wallet-update !>(`update:w`[%edit wllt])]] :: %del ?> (~(has by wlts) id.act) - `state(wlts (~(del by wlts) id.act)) + :_ state(wlts (~(del by wlts) id.act)) + ~[[%give %fact ~[/updates] %wallet-update !>(`update:w`[%del id.act])]] == -- diff --git a/desk/lib/account.hoon b/desk/lib/account.hoon index af42c52..f1844ae 100644 --- a/desk/lib/account.hoon +++ b/desk/lib/account.hoon @@ -16,8 +16,11 @@ ^- json |^ ?- -.upd - %accts (acts +.upd) - == + %accts (acts +.upd) + %add (acjs +.upd) + %edit (acjs +.upd) + %del s+id.upd + == ++ acts |= ud=[=accounts] ^- json diff --git a/desk/lib/transaction.hoon b/desk/lib/transaction.hoon index 8f0a73a..786f361 100644 --- a/desk/lib/transaction.hoon +++ b/desk/lib/transaction.hoon @@ -45,8 +45,11 @@ ^- json %+ frond -.upd ?- -.upd - %txns (txns txns.upd) - == + %txns (txns txns.upd) + %add (txjs txn.upd) + %edit (txjs txn.upd) + %del s+id.upd + == :: ++ txns |= txns=transactions diff --git a/desk/lib/wallet.hoon b/desk/lib/wallet.hoon index 9590378..0a7230e 100644 --- a/desk/lib/wallet.hoon +++ b/desk/lib/wallet.hoon @@ -33,8 +33,12 @@ ++ update |= upd=^update ^- json + %+ frond -.upd ?- -.upd %wllts (wllts +.upd) + %add (wljs wllt.upd) + %edit (wljs wllt.upd) + %del s+id.upd == ++ wllts |= wllts=(map id wllt) diff --git a/desk/sur/account.hoon b/desk/sur/account.hoon index b377515..e12be1a 100644 --- a/desk/sur/account.hoon +++ b/desk/sur/account.hoon @@ -26,5 +26,8 @@ :: +$ update $% [%accts =accounts] + [%add =acct] + [%edit =acct] + [%del =id] == -- diff --git a/desk/sur/transaction.hoon b/desk/sur/transaction.hoon index 1de9c27..631142f 100644 --- a/desk/sur/transaction.hoon +++ b/desk/sur/transaction.hoon @@ -49,5 +49,8 @@ :: +$ update $% [%txns txns=transactions] + [%add =txn] + [%edit =txn] + [%del =id] == -- diff --git a/desk/sur/wallet.hoon b/desk/sur/wallet.hoon index eefb7f4..1a0b273 100644 --- a/desk/sur/wallet.hoon +++ b/desk/sur/wallet.hoon @@ -22,6 +22,9 @@ :: +$ update $% [%wllts wllts=wallets] + [%add =wllt] + [%edit =wllt] + [%del =id] == :: Wallets - map of wallet ids to wallets +$ wallets (map id wllt) From 47561599422e350b0f22f4ce7d4ea16313bbb4c3 Mon Sep 17 00:00:00 2001 From: tomholford Date: Mon, 6 Apr 2026 20:10:15 -0700 Subject: [PATCH 7/7] feat: wire frontend to wallet/account/transaction backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Align types with backend sur (Wallet, Account, Transaction + account-id) - Add Zustand stores for wallets and accounts (poke/scry/handleUpdate) - Refactor transaction store: hodl-action → transaction-action, /transactions/all → /transactions - Centralize subscription in bootstrap.ts (single /updates sub, dispatch by mark) - Update views: account-id selector on tx form, parseFloat for string amounts - Replace old React Context stores with Zustand, delete store/Wallets + store/Accounts - Add /wallets and /accounts routes --- ui/src/App.tsx | 12 +- ui/src/state/accounts.tsx | 100 ++++++++++++++++ ui/src/state/bootstrap.ts | 32 +++++ ui/src/state/transactions.tsx | 112 ++++++------------ ui/src/state/wallets.tsx | 100 ++++++++++++++++ ui/src/store/Accounts.tsx | 26 ---- ui/src/store/Wallets.tsx | 25 ---- ui/src/store/useAccountsStore.ts | 43 ------- ui/src/store/useWalletsStore.ts | 16 --- ui/src/types/Account.type.ts | 21 ++-- ui/src/types/Transaction.type.ts | 5 +- ui/src/types/Wallet.type.ts | 17 +-- ui/src/views/Accounts/AccountRow.tsx | 41 ++----- ui/src/views/Accounts/Accounts.tsx | 16 ++- ui/src/views/Accounts/AccountsForm.tsx | 75 ++++-------- ui/src/views/Transactions/TransactionRow.tsx | 4 +- .../Transactions/TransactionTableRow.tsx | 2 +- .../views/Transactions/TransactionTotals.tsx | 4 +- .../views/Transactions/TransactionsForm.tsx | 26 +++- ui/src/views/Wallets/WalletForm.tsx | 32 +++-- ui/src/views/Wallets/Wallets.tsx | 27 ++++- 21 files changed, 401 insertions(+), 335 deletions(-) create mode 100644 ui/src/state/accounts.tsx create mode 100644 ui/src/state/bootstrap.ts create mode 100644 ui/src/state/wallets.tsx delete mode 100644 ui/src/store/Accounts.tsx delete mode 100644 ui/src/store/Wallets.tsx delete mode 100644 ui/src/store/useAccountsStore.ts delete mode 100644 ui/src/store/useWalletsStore.ts diff --git a/ui/src/App.tsx b/ui/src/App.tsx index e18012d..22d86df 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -15,8 +15,10 @@ import { About } from './views/About/About'; import { Footer } from './views/App/Footer'; import { Header } from './views/App/Header'; import { Settings } from './views/Settings/Settings'; -import { useTransactionsState } from './state/transactions'; +import { bootstrapStores } from './state/bootstrap'; import { Transactions } from './views/Transactions/Transactions'; +import Wallets from './views/Wallets/Wallets'; +import Accounts from './views/Accounts/Accounts'; import ErrorAlert from './components/ErrorAlert'; const AppContainer = ({ children }: { children: React.ReactNode }) => { @@ -50,13 +52,9 @@ const RoutedApp = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const subscribeTransactions = useCallback(async () => { - await useTransactionsState.getState().start(); - }, []); - useEffect(() => { handleError(() => { - subscribeTransactions(); + bootstrapStores(); }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []) @@ -74,6 +72,8 @@ const RoutedApp = () => {
} /> + } /> + } /> } /> } /> } /> diff --git a/ui/src/state/accounts.tsx b/ui/src/state/accounts.tsx new file mode 100644 index 0000000..49c933c --- /dev/null +++ b/ui/src/state/accounts.tsx @@ -0,0 +1,100 @@ +import { unstable_batchedUpdates as batchUpdates } from 'react-dom'; +import produce from 'immer'; +import create from 'zustand'; +import { Account } from '../types/Account.type'; +import api from '../services/API'; + +type AccountUpdate = + | { add: Account } + | { edit: Account } + | { del: string }; + +function accountAction(diff: Record) { + return { + app: 'hodl', + mark: 'account-action', + json: diff, + }; +} + +export interface AccountsState { + set: (fn: (sta: AccountsState) => void) => void; + batchSet: (fn: (sta: AccountsState) => void) => void; + initialized: boolean; + accounts: Account[]; + add: (account: Account) => Promise; + edit: (account: Account) => Promise; + del: (id: string) => Promise; + init: () => Promise; + handleUpdate: (update: AccountUpdate) => void; +} + +export const useAccountsState = create((set, get) => ({ + set: (fn) => { + set(produce(get(), fn)); + }, + batchSet: (fn) => { + batchUpdates(() => { + get().set(fn); + }); + }, + initialized: false, + accounts: [], + add: async (account) => { + await api.poke( + accountAction({ add: account }) + ); + }, + edit: async (account) => { + await api.poke( + accountAction({ edit: account }) + ); + }, + del: async (id) => { + await api.poke( + accountAction({ del: { id } }) + ); + }, + init: async () => { + const accounts = await api.scry>({ + app: 'hodl', + path: '/accounts', + }); + + set((s) => ({ + ...s, + accounts: Object.values(accounts), + initialized: true, + })); + }, + handleUpdate: (update: AccountUpdate) => { + if ('add' in update) { + get().batchSet((draft) => { + draft.accounts = [...draft.accounts, update.add]; + }); + } + if ('edit' in update) { + get().batchSet((draft) => { + draft.accounts = [ + ...draft.accounts.filter((a) => a.id !== update.edit.id), + update.edit, + ]; + }); + } + if ('del' in update) { + get().batchSet((draft) => { + draft.accounts = draft.accounts.filter((a) => a.id !== update.del); + }); + } + }, +})); + +const selAccountsInitialized = (s: AccountsState) => s.initialized; +export function useAccountsInitialized() { + return useAccountsState(selAccountsInitialized); +} + +const selAccounts = (s: AccountsState) => s.accounts; +export function useAccounts() { + return useAccountsState(selAccounts); +} diff --git a/ui/src/state/bootstrap.ts b/ui/src/state/bootstrap.ts new file mode 100644 index 0000000..11947ab --- /dev/null +++ b/ui/src/state/bootstrap.ts @@ -0,0 +1,32 @@ +import api from '../services/API'; +import { useWalletsState } from './wallets'; +import { useAccountsState } from './accounts'; +import { useTransactionsState } from './transactions'; + +export async function bootstrapStores() { + await Promise.all([ + useWalletsState.getState().init(), + useAccountsState.getState().init(), + useTransactionsState.getState().init(), + ]); + + await api.subscribe({ + app: 'hodl', + path: '/updates', + event: (data: any, mark: string) => { + switch (mark) { + case 'wallet-update': + useWalletsState.getState().handleUpdate(data); + break; + case 'account-update': + useAccountsState.getState().handleUpdate(data); + break; + case 'transaction-update': + useTransactionsState.getState().handleUpdate(data); + break; + default: + console.warn('Unknown subscription mark:', mark); + } + }, + }); +} diff --git a/ui/src/state/transactions.tsx b/ui/src/state/transactions.tsx index 191decb..01c014c 100644 --- a/ui/src/state/transactions.tsx +++ b/ui/src/state/transactions.tsx @@ -5,44 +5,15 @@ import { Transaction } from '../types/Transaction.type'; import api from '../services/API'; import { groupBy } from 'lodash'; -interface TransactionAddDiff { - add: Transaction; -} - -interface TransactionEditDiff { - edit: Transaction; -} - -interface TransactionDelDiff { - del: { - id: string; - } -} - -type TransactionDiff = - | TransactionAddDiff - | TransactionEditDiff - | TransactionDelDiff; - -interface TransactionAddUpdate { - add: Transaction; -} -interface TransactionEditUpdate { - edit: Transaction; -} -interface TransactionDelUpdate { - del: string; -} - -type TransactionUpdate = - | TransactionAddUpdate - | TransactionEditUpdate - | TransactionDelUpdate; +export type TransactionUpdate = + | { add: Transaction } + | { edit: Transaction } + | { del: string }; -function txAction(diff: TransactionDiff) { +function txAction(diff: Record) { return { app: 'hodl', - mark: 'hodl-action', + mark: 'transaction-action', json: diff, }; } @@ -55,7 +26,8 @@ export interface TransactionsState { add: (transaction: Transaction) => Promise; edit: (transaction: Transaction) => Promise; del: (id: string) => Promise; - start: () => Promise; + init: () => Promise; + handleUpdate: (update: TransactionUpdate) => void; } export const useTransactionsState = create((set, get) => ({ @@ -71,63 +43,51 @@ export const useTransactionsState = create((set, get) => ({ transactions: [], add: async (transaction) => { await api.poke( - txAction({ - add: transaction - }) + txAction({ add: transaction }) ); }, edit: async (transaction) => { await api.poke( - txAction({ - edit: transaction - }) + txAction({ edit: transaction }) ); }, del: async (id) => { await api.poke( - txAction({ - del: { id } - }) + txAction({ del: { id } }) ); }, - start: async () => { - const transactions = await api.scry({ + init: async () => { + const transactions = await api.scry>({ app: 'hodl', - path: '/transactions/all', + path: '/transactions', }); set((s) => ({ ...s, - transactions: Object.values(transactions), // TODO: scry returns list? - })); - - await api.subscribe({ - app: 'hodl', - path: '/updates', - event: (update: TransactionUpdate) => { - if('add' in update) { - get().batchSet((draft) => { - draft.transactions = [...draft.transactions, update.add] - }) - } - if('edit' in update) { - get().batchSet((draft) => { - draft.transactions = [...draft.transactions.slice().filter(t => t.id !== update.edit.id), update.edit] - }) - } - if('del' in update) { - get().batchSet((draft) => { - draft.transactions = draft.transactions.slice().filter(t => t.id !== update.del) - }) - } - }, - }); - - set((s) => ({ - ...s, - initialized: true + transactions: Object.values(transactions), + initialized: true, })); }, + handleUpdate: (update: TransactionUpdate) => { + if ('add' in update) { + get().batchSet((draft) => { + draft.transactions = [...draft.transactions, update.add]; + }); + } + if ('edit' in update) { + get().batchSet((draft) => { + draft.transactions = [ + ...draft.transactions.filter((t) => t.id !== update.edit.id), + update.edit, + ]; + }); + } + if ('del' in update) { + get().batchSet((draft) => { + draft.transactions = draft.transactions.filter((t) => t.id !== update.del); + }); + } + }, })); const selTransactionsInitialized = (s: TransactionsState) => s.initialized; diff --git a/ui/src/state/wallets.tsx b/ui/src/state/wallets.tsx new file mode 100644 index 0000000..73ad9b4 --- /dev/null +++ b/ui/src/state/wallets.tsx @@ -0,0 +1,100 @@ +import { unstable_batchedUpdates as batchUpdates } from 'react-dom'; +import produce from 'immer'; +import create from 'zustand'; +import { Wallet } from '../types/Wallet.type'; +import api from '../services/API'; + +type WalletUpdate = + | { add: Wallet } + | { edit: Wallet } + | { del: string }; + +function walletAction(diff: Record) { + return { + app: 'hodl', + mark: 'wallet-action', + json: diff, + }; +} + +export interface WalletsState { + set: (fn: (sta: WalletsState) => void) => void; + batchSet: (fn: (sta: WalletsState) => void) => void; + initialized: boolean; + wallets: Wallet[]; + add: (wallet: Wallet) => Promise; + edit: (wallet: Wallet) => Promise; + del: (id: string) => Promise; + init: () => Promise; + handleUpdate: (update: WalletUpdate) => void; +} + +export const useWalletsState = create((set, get) => ({ + set: (fn) => { + set(produce(get(), fn)); + }, + batchSet: (fn) => { + batchUpdates(() => { + get().set(fn); + }); + }, + initialized: false, + wallets: [], + add: async (wallet) => { + await api.poke( + walletAction({ add: wallet }) + ); + }, + edit: async (wallet) => { + await api.poke( + walletAction({ edit: wallet }) + ); + }, + del: async (id) => { + await api.poke( + walletAction({ del: { id } }) + ); + }, + init: async () => { + const wallets = await api.scry>({ + app: 'hodl', + path: '/wallets', + }); + + set((s) => ({ + ...s, + wallets: Object.values(wallets), + initialized: true, + })); + }, + handleUpdate: (update: WalletUpdate) => { + if ('add' in update) { + get().batchSet((draft) => { + draft.wallets = [...draft.wallets, update.add]; + }); + } + if ('edit' in update) { + get().batchSet((draft) => { + draft.wallets = [ + ...draft.wallets.filter((w) => w.id !== update.edit.id), + update.edit, + ]; + }); + } + if ('del' in update) { + get().batchSet((draft) => { + draft.wallets = draft.wallets.filter((w) => w.id !== update.del); + }); + } + }, +})); + +const selWalletsInitialized = (s: WalletsState) => s.initialized; +export function useWalletsInitialized() { + return useWalletsState(selWalletsInitialized); +} + +const selWallets = (s: WalletsState) => s.wallets; +export function useWallets() { + return useWalletsState(selWallets); +} diff --git a/ui/src/store/Accounts.tsx b/ui/src/store/Accounts.tsx deleted file mode 100644 index e369ec6..0000000 --- a/ui/src/store/Accounts.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React, { createContext, useContext } from 'react'; -import { useAccountsStore } from './useAccountsStore'; - -const initialContext = { - accounts: null, - addAccount: () => { console.log('add') }, - removeAccount: () => {}, - setAccounts: () => {}, -} - -export const AccountsContext = createContext>(initialContext); - -export const AccountsProvider = ({ children }: { children: React.ReactNode }) => { - - const accountsStore = useAccountsStore(); - - return ( - - {children} - - ); -} - -export const useAccounts = () => { - return useContext(AccountsContext); -} diff --git a/ui/src/store/Wallets.tsx b/ui/src/store/Wallets.tsx deleted file mode 100644 index eccf6e8..0000000 --- a/ui/src/store/Wallets.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, { createContext, useContext } from 'react'; -import { useWalletsStore } from './useWalletsStore'; - -const initialContext = { - wallets: null, - addWallet: () => {}, - setWallets: () => {} -} - -export const WalletsContext = createContext>(initialContext); - -export const WalletsProvider = ({ children }: { children: React.ReactNode }) => { - - const walletsStore = useWalletsStore(); - - return ( - - {children} - - ); -} - -export const useWallets = () => { - return useContext(WalletsContext); -} diff --git a/ui/src/store/useAccountsStore.ts b/ui/src/store/useAccountsStore.ts deleted file mode 100644 index 3113d8c..0000000 --- a/ui/src/store/useAccountsStore.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { useEffect, useState } from "react"; -import { useLocalStorage } from "usehooks-ts"; -import { Account } from "../types/Account.type"; - -export const useAccountsStore = () => { - const [localAccounts, setLocalAccounts] = useLocalStorage('hodl:accounts', [] as Account[]); - const [accounts, setAccounts] = useState(null); - - const addAccount = (account: Account) => { - accounts ? setAccounts([...accounts, account]) : setAccounts([account]); - } - - const removeAccount = (uuid: string) => { - if(!accounts) { - return; - } - - setAccounts([...accounts.filter(a => a.uuid !== uuid)]); - } - - // Restore from localStorage if available - useEffect(() => { - if(localAccounts && localAccounts.length > 0) { - setAccounts(localAccounts); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // Keep it up to date - useEffect(() => { - if(accounts) { - setLocalAccounts(accounts); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [accounts]); - - return { - accounts, - addAccount, - removeAccount, - setAccounts, - }; -} diff --git a/ui/src/store/useWalletsStore.ts b/ui/src/store/useWalletsStore.ts deleted file mode 100644 index 4c7a36a..0000000 --- a/ui/src/store/useWalletsStore.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useState } from "react"; -import Wallet from "../types/Wallet.type"; - -export const useWalletsStore = () => { - const [wallets, setWallets] = useState(null); - - const addWallet = (wallet: Wallet) => { - wallets ? setWallets([...wallets, wallet]) : setWallets([wallet]); - } - - return { - wallets, - addWallet, - setWallets - }; -} diff --git a/ui/src/types/Account.type.ts b/ui/src/types/Account.type.ts index 4f4c7d9..a161fca 100644 --- a/ui/src/types/Account.type.ts +++ b/ui/src/types/Account.type.ts @@ -1,14 +1,11 @@ -// An Account has one more Assets - -import BN from "bn.js"; -import { Currency } from "./Currency.type"; - +/** + * Schema for the backend's `account` record. + * + * See desk/sur/account.hoon + */ export interface Account { - address?: string; - currency?: Currency; - balance?: number; - balanceBN?: BN; - // wallet?: Wallet; - uuid?: string; - custodial?: boolean; + id: string; + "wallet-id": string; + name: string; + note: string; } diff --git a/ui/src/types/Transaction.type.ts b/ui/src/types/Transaction.type.ts index f83edc0..cb4be27 100644 --- a/ui/src/types/Transaction.type.ts +++ b/ui/src/types/Transaction.type.ts @@ -8,7 +8,8 @@ export interface Transaction { "coin-id": string; date: number; note: string; - amount: number; - "cost-basis": number; + amount: string; + "cost-basis": string; type: string; + "account-id": string; } diff --git a/ui/src/types/Wallet.type.ts b/ui/src/types/Wallet.type.ts index 29b7bcc..406d313 100644 --- a/ui/src/types/Wallet.type.ts +++ b/ui/src/types/Wallet.type.ts @@ -1,9 +1,10 @@ -// A Wallet can be used to automatically import Accounts; they are optional to import / create - -interface Wallet { - uuid?: string; - mnemonic?: string; - passphrase?: string; +/** + * Schema for the backend's `wallet` record. + * + * See desk/sur/wallet.hoon + */ +export interface Wallet { + id: string; + name: string; + note: string; } - -export default Wallet; diff --git a/ui/src/views/Accounts/AccountRow.tsx b/ui/src/views/Accounts/AccountRow.tsx index d0f7c03..2772a86 100644 --- a/ui/src/views/Accounts/AccountRow.tsx +++ b/ui/src/views/Accounts/AccountRow.tsx @@ -1,43 +1,20 @@ -import { BN } from "bn.js"; -import { useCallback, useEffect } from "react"; -import { abbreviatedAddress } from "../../lib/presenters"; -import { getBalance } from "../../services/Avalanche.service"; -import { useAccounts } from "../../store/Accounts"; +import { useCallback } from "react"; +import { useAccountsState } from "../../state/accounts"; import { Account } from "../../types/Account.type"; import './AccountRow.scss'; export const AccountRow = ({ account }: { account: Account }) => { - const { removeAccount } = useAccounts(); + const handleRemoveClick = useCallback(async () => { + await useAccountsState.getState().del(account.id); + }, [account.id]); - // const fetchBalance = useCallback(async () => { - // const response = await getBalance(account.address!); - // const foundBalance = response.balances.find(b => b.asset === 'AVAX'); - // if(foundBalance) { - // account.balance = new BN(foundBalance.balance); - // } else { - // account.balance = new BN(0); - // } - // }, [account]); - - // useEffect(() => { - // if(account.balance) { return }; - - // fetchBalance(); - // }, [account.balance, fetchBalance]); - - const handleRemoveClick = useCallback(() => { - removeAccount(account.uuid!); - }, [account.uuid, removeAccount]); - - return (<> + return (
-
{account.address ? abbreviatedAddress(account.address) : 'custodian'}
-
{account.currency}
- {/*
{account.balance ? balanceWithDecimal(account.balance) : 0}
*/} -
{account.balance ? account.balance : 0}
+
{account.name}
+
{account.note}
- ); + ); } diff --git a/ui/src/views/Accounts/Accounts.tsx b/ui/src/views/Accounts/Accounts.tsx index 25ff97a..4472bb1 100644 --- a/ui/src/views/Accounts/Accounts.tsx +++ b/ui/src/views/Accounts/Accounts.tsx @@ -1,9 +1,14 @@ -import { useAccounts } from "../../store/Accounts"; +import { useAccounts, useAccountsInitialized } from "../../state/accounts"; import { AccountRow } from "./AccountRow"; import AccountsForm from "./AccountsForm"; const Accounts = () => { - const { accounts } = useAccounts(); + const accounts = useAccounts(); + const initialized = useAccountsInitialized(); + + if (!initialized) { + return

Loading...

; + } return ( <> @@ -11,10 +16,9 @@ const Accounts = () => {

- { - accounts ? - accounts.map(a => ) - : 'No accounts' + {accounts.length > 0 + ? accounts.map(a => ) + :

No accounts

} ); diff --git a/ui/src/views/Accounts/AccountsForm.tsx b/ui/src/views/Accounts/AccountsForm.tsx index 25a263c..8c52ec6 100644 --- a/ui/src/views/Accounts/AccountsForm.tsx +++ b/ui/src/views/Accounts/AccountsForm.tsx @@ -1,66 +1,43 @@ import * as React from "react"; import { useForm } from "react-hook-form"; -import { useAccounts } from "../../store/Accounts"; +import { useAccountsState } from "../../state/accounts"; +import { useWallets } from "../../state/wallets"; import { v4 as uuidv4 } from 'uuid'; -import { Currency } from "../../types/Currency.type"; -import { CURRENCIES } from "../../constants"; -import { isValidAddress } from "../../lib/validators"; type FormData = { - custodial: boolean; - address: string; - currency: Currency; + "wallet-id": string; + name: string; + note: string; }; export default function AccountForm() { - const { addAccount } = useAccounts(); - const { register, handleSubmit, setError, reset, watch, formState: { errors } } = useForm(); - const onSubmit = handleSubmit(data => { - if (!isValidAddress(data.address, data.currency)) { - setError('address', { message: `Invalid ${data.currency} address` }) - return; - } - - reset(); - - addAccount({ - custodial: data.custodial, - address: data.address, - currency: data.currency, - uuid: uuidv4() + const wallets = useWallets(); + const { register, handleSubmit, reset, formState: { errors } } = useForm(); + const onSubmit = handleSubmit(async (data) => { + await useAccountsState.getState().add({ + id: uuidv4(), + "wallet-id": data["wallet-id"], + name: data.name, + note: data.note ?? '', }); + reset(); }); - const isCustodial = watch("custodial"); - return (
- - - { - isCustodial ? - (<> - custodial - ) : ( - <> - - - ) - } - { - errors.address &&

{errors.address.message}

- } - - + + {wallets.map(w => ( + + ))} + {errors["wallet-id"] &&

Wallet is required

} + + + {errors.name &&

Name is required

} + +
); diff --git a/ui/src/views/Transactions/TransactionRow.tsx b/ui/src/views/Transactions/TransactionRow.tsx index 1737a24..d93e075 100644 --- a/ui/src/views/Transactions/TransactionRow.tsx +++ b/ui/src/views/Transactions/TransactionRow.tsx @@ -17,13 +17,13 @@ export const TransactionRow = ({ transaction }: { transaction: Transaction }) => const currentValue = useMemo(() => { if(!exchangeRate) { return 0 }; - return exchangeRate * transaction.amount; + return exchangeRate * parseFloat(transaction.amount); }, [transaction.amount, exchangeRate]); const initialValue = useMemo(() => { if(!(transaction.amount && transaction['cost-basis'])) { return null }; - return transaction.amount * transaction['cost-basis']; + return parseFloat(transaction.amount) * parseFloat(transaction['cost-basis']); }, [transaction]); const pnl = useMemo(() => { diff --git a/ui/src/views/Transactions/TransactionTableRow.tsx b/ui/src/views/Transactions/TransactionTableRow.tsx index 3bb3f55..0a65a8e 100644 --- a/ui/src/views/Transactions/TransactionTableRow.tsx +++ b/ui/src/views/Transactions/TransactionTableRow.tsx @@ -36,7 +36,7 @@ export const TransactionTableRow = ({ transactions, coinId }: { transactions: Tr const initialValue = useMemo(() => { if (!transactions) { return 0 }; - return transactions.reduce((memo, a) => memo + Number(a["cost-basis"]) * a.amount, 0) + return transactions.reduce((memo, a) => memo + Number(a["cost-basis"]) * Number(a.amount), 0) }, [transactions]); const pnl = useMemo(() => { diff --git a/ui/src/views/Transactions/TransactionTotals.tsx b/ui/src/views/Transactions/TransactionTotals.tsx index 3dd4f21..9f16add 100644 --- a/ui/src/views/Transactions/TransactionTotals.tsx +++ b/ui/src/views/Transactions/TransactionTotals.tsx @@ -34,7 +34,7 @@ export const TransactionTotals = () => { return transactionCoins.reduce((total, c) => { const transactions = groupedTransactions[c]; - return transactions.reduce((transactionTotal, a) => transactionTotal + (a.amount * priceMap[c]), total) + return transactions.reduce((transactionTotal, a) => transactionTotal + (parseFloat(a.amount) * priceMap[c]), total) }, 0); }, [transactionCoins, groupedTransactions, priceMap]); @@ -44,7 +44,7 @@ export const TransactionTotals = () => { return transactionCoins.reduce((total, c) => { const transactions = groupedTransactions[c]; - return transactions.reduce((transactionTotal, t) => transactionTotal + (t.amount * (t['cost-basis'] || 1)), total) + return transactions.reduce((transactionTotal, t) => transactionTotal + (parseFloat(t.amount) * (parseFloat(t['cost-basis']) || 1)), total) }, 0); }, [transactionCoins, groupedTransactions]); diff --git a/ui/src/views/Transactions/TransactionsForm.tsx b/ui/src/views/Transactions/TransactionsForm.tsx index 8753d85..b7546fa 100644 --- a/ui/src/views/Transactions/TransactionsForm.tsx +++ b/ui/src/views/Transactions/TransactionsForm.tsx @@ -7,6 +7,7 @@ import { useCallback, useEffect } from "react"; import { useTransactionsState } from "../../state/transactions"; import { format, getTime } from 'date-fns' import { CURRENCIES } from "../../constants"; +import { useAccounts } from "../../state/accounts"; import AsyncSelect from 'react-select/async'; import { coinSearch, getCoin } from "../../services/CoinGecko.service"; import debounce from 'debounce-promise'; @@ -24,8 +25,9 @@ interface ThumbOption extends Option { type FormData = { type: string; "coin-id": Option; - amount: number; - "cost-basis": number; + amount: string; + "cost-basis": string; + "account-id": string; note: string; date: string; }; @@ -33,6 +35,7 @@ type FormData = { export default function TransactionsForm({ transaction }: { transaction?: Transaction }) { const isEditing = transaction !== undefined; const navigate = useNavigate() + const accounts = useAccounts(); const { control, register, handleSubmit, setValue, formState: { isValid } } = useForm(); const onSubmit = async (data: FormData) => { @@ -45,9 +48,10 @@ export default function TransactionsForm({ transaction }: { transaction?: Transa "coin-id": data["coin-id"].value, date: msDate, note: data.note, - amount: data.amount, - "cost-basis": data['cost-basis'], + amount: String(data.amount), + "cost-basis": String(data['cost-basis']), type: 'buy', + "account-id": data['account-id'], }) } else { useTransactionsState.getState().add({ @@ -55,9 +59,10 @@ export default function TransactionsForm({ transaction }: { transaction?: Transa "coin-id": data["coin-id"].value, date: msDate, note: data.note, - amount: data.amount, - "cost-basis": data['cost-basis'], + amount: String(data.amount), + "cost-basis": String(data['cost-basis']), type: 'buy', + "account-id": data['account-id'], }); } @@ -207,6 +212,15 @@ export default function TransactionsForm({ transaction }: { transaction?: Transa +
+ + +
diff --git a/ui/src/views/Wallets/WalletForm.tsx b/ui/src/views/Wallets/WalletForm.tsx index 47cb327..91d0076 100644 --- a/ui/src/views/Wallets/WalletForm.tsx +++ b/ui/src/views/Wallets/WalletForm.tsx @@ -1,35 +1,33 @@ import * as React from "react"; import { useForm } from "react-hook-form"; -import { useWallets } from "../../store/Wallets"; +import { useWalletsState } from "../../state/wallets"; import { v4 as uuidv4 } from 'uuid'; - type FormData = { - mnemonic: string; - passphrase?: string; + name: string; + note: string; }; export default function WalletForm() { - const { addWallet } = useWallets(); - const { register, handleSubmit, formState: { errors } } = useForm(); - const onSubmit = handleSubmit(data => { - console.log(data); - addWallet({ - mnemonic: data.mnemonic, - passphrase: data.passphrase, - uuid: uuidv4() + const { register, handleSubmit, reset, formState: { errors } } = useForm(); + const onSubmit = handleSubmit(async (data) => { + await useWalletsState.getState().add({ + id: uuidv4(), + name: data.name, + note: data.note ?? '', }); + reset(); }); return (
- - - - + + + + { - errors ?

There's an error

: null + errors.name ?

Name is required

: null }
); diff --git a/ui/src/views/Wallets/Wallets.tsx b/ui/src/views/Wallets/Wallets.tsx index f4d3cc5..9a413eb 100644 --- a/ui/src/views/Wallets/Wallets.tsx +++ b/ui/src/views/Wallets/Wallets.tsx @@ -1,17 +1,32 @@ -import { useWallets } from "../../store/Wallets"; +import { useWallets, useWalletsInitialized } from "../../state/wallets"; +import { useWalletsState } from "../../state/wallets"; import WalletForm from "./WalletForm"; +import { useCallback } from "react"; const Wallets = () => { - const { wallets } = useWallets(); + const wallets = useWallets(); + const initialized = useWalletsInitialized(); + + const handleDelete = useCallback(async (id: string) => { + await useWalletsState.getState().del(id); + }, []); + + if (!initialized) { + return

Loading...

; + } return ( <>

wallets


- { - wallets ? - wallets.map(w =>

{w.uuid}

) - : 'No wallets' + {wallets.length > 0 + ? wallets.map(w => ( +
+ {w.name} {w.note && — {w.note}} + +
+ )) + :

No wallets

}