:: /ker/wallet/wallet: nockchain wallet /= bip39 /common/bip39 /= slip10 /common/slip10 /= m /common/markdown/types /= md /common/markdown/markdown /= transact /common/tx-engine /= z /common/zeke /= zo /common/zoon /= dumb /apps/dumbnet/lib/types /= * /common/zose /= * /common/wrapper :: => =| bug=_& |% :: $key: public or private key :: :: a public key is generated from a private key and +cc (chain code). :: pub contains the base58 encoded version of the cheetah curve point :: +$ key $~ [%pub p=*@ux] $% [%pub p=@ux] [%prv p=@ux] == :: $coil: key and chaincode :: :: a wallet consists of a collection of +coil (address and entropy pair). the :: $cc (called a chain code elsewhere) allows for the deterministic :: generation of child keys from a parent key without compromising other :: branches of the hierarchy. :: :: .key: public or private key :: .cc: associated entropy (chain code) :: +$ coil [%coil =key =cc] :: :: $meta: stored metadata for a key +$ meta $% coil [%label @t] [%seed @t] == :: :: $keys: path indexed map for keys :: :: path format for keys state: :: :: /keys :: root path (holds nothing in its fil) :: /keys/[t/master]/[key-type]/m/[coil/key] :: master key path :: /keys/[t/master]/[key-type]/[ud/index]/[coil/key] :: derived key path :: /keys/[t/master]/[key-type]/[ud/index]/[coil/key] :: specific key path :: /keys/[t/master]/[key-type]/[ud/index]/label/[label/label] :: key label path for derived key :: /keys/[t/master]/[key-type]/m/label/[label/label] :: key label path for master key :: /keys/[t/master]/seed/[seed/seed-phrase] :: seed-phrase path :: :: Note the terminal entry of the path holds that value, this value is the :: non-unit `fil` in the $axal definition :: :: where: :: - [t/master] is the base58 encoded master public key as @t :: - m denotes the master key :: - [ud/index] is the derivation index as @ud :: - [key-type] is either %pub or %prv :: - [coil/key] is the key and chaincode pair. key is in serialized :: format as a @ux, NOT base58. :: - [seed/seed-phrase] is the seed phrase as a tape :: - [label/label] is a label value :: :: master key is stored under 'm'. :: derived keys use incrementing indices starting from 0 under their master-key and key-type :: labels are stored as children of their associated keys. :: seed is a seed phrase and is only stored as a child of [t/master] :: +$ keys $+(keys-axal (axal meta)) :: :: $draft-tree: structured tree of draft, input, and seed data :: :: we use the axal structure to track the relationship between drafts, :: inputs, and seeds. this allows us to navigate the tree and maintain :: all the relationships without duplicating data. :: :: paths in the draft-tree follow these conventions: :: :: /draft/[draft-name] :: draft node :: /draft/[draft-name]/input/[input-name] :: input in a draft :: /input/[input-name] :: input node :: /input/[input-name]/seed/[seed-name] :: seed in an input :: /seed/[seed-name] :: seed node :: +$ draft-tree $+ wallet-draft-tree (axal draft-entity) :: $draft-entity: entities stored in the draft tree :: +$ draft-entity $% [%draft =draft-name =draft] [%input =input-name =preinput] [%seed =seed-name =preseed] == :: :: +master: master key pair ++ master =< form |% +$ form (unit coil) ++ public |= =form ~| "master public key not found" ?< ?=(~ form) u.form :: ++ to-b58 |= =form ^- @t (crip (en:base58:wrap p.key:(public form))) -- :: $cc: chaincode :: +$ cc @ux :: $balance: wallet balance +$ balance $+ wallet-balance (z-map:zo nname:transact nnote:transact) :: $state: wallet state :: +$ state $: %0 =balance hash-to-name=(z-map:zo hash:transact nname:transact) :: hash of note -> name of note name-to-hash=(z-map:zo nname:transact hash:transact) :: name of note -> hash of note receive-address=lock:transact =master =keys transactions=$+(transactions (map * transaction)) last-block=(unit block-id:transact) peek-requests=$+(peek-requests (map @ud ?(%balance %block))) active-draft=(unit draft-name) active-input=(unit input-name) active-seed=(unit seed-name) :: currently selected seed draft-tree=draft-tree :: structured tree of drafts, inputs, and seeds pending-commands=(z-map:zo @ud [phase=?(%block %balance %ready) wrapped=cause]) :: commands waiting for sync == +$ seed-name $~('default-seed' @t) :: +$ draft-name $~('default-draft' @t) :: +$ input-name $~('default-input' @t) :: :: $transaction: TODO :: +$ transaction $: recipient=@ux amount=@ud status=?(%unsigned %signed %sent) == :: +$ cause $% [%keygen entropy=byts salt=byts] [%derive-child key-type=?(%pub %prv) i=@ label=(unit @t)] [%import-keys keys=(list (pair trek coil))] [%import-master-pubkey key=@t cc=@t] :: base58-encoded pubkey + chain code [%make-tx dat=draft] [%list-notes-by-pubkey pubkey=@t] :: base58-encoded pubkey $: %simple-spend names=(list [first=@t last=@t]) :: base58-encoded name hashes recipients=(list [m=@ pks=(list @t)]) :: base58-encoded locks gifts=(list coins:transact) :: number of coins to spend fee=coins:transact :: fee == [%sign-tx dat=draft index=(unit @ud) entropy=@] [%list-pubkeys ~] [%list-notes ~] [%show-balance block=@ux] [%show =path] [%gen-master-privkey seedphrase=@t] [%gen-master-pubkey master-privkey=keyc:slip10] [%update-balance ~] [%update-block ~] [%sync-run wrapped=cause] :: run command after sync completes $: %scan master-pubkey=@t :: base58 encoded master public key to scan for search-depth=$~(100 @ud) :: how many addresses to scan (default 100) include-timelocks=$~(%.n ?) :: include timelocked notes (default false) include-multisig=$~(%.n ?) :: include notes with multisigs (default false) == [%advanced-spend advanced-spend] [%file %write path=@t contents=@t success=?] npc-cause == :: +$ advanced-spend $% [%seed advanced-spend-seed] [%input advanced-spend-input] [%draft advanced-spend-draft] == :: +$ advanced-spend-seed $% [%new name=@t] :: new empty seed in draft $: %set-name seed-name=@t new-name=@t == $: %set-source :: set .output-source seed-name=@t source=(unit [hash=@t is-coinbase=?]) == $: %set-recipient :: set .recipient seed-name=@t recipient=[m=@ pks=(list @t)] == $: %set-timelock :: set .timelock-intent seed-name=@t absolute=timelock-range:transact relative=timelock-range:transact == $: %set-gift seed-name=@t gift=coins:transact == $: %set-parent-hash seed-name=@t parent-hash=@t == $: %set-parent-hash-from-name seed-name=@t name=[@t @t] == $: %print-status :: do the needful seed-name=@t == == :: $seed-mask: tracks which fields of a $seed:transact have been set :: :: this might have been better as a "unitized seed" but would have been :: much more annoying to read the code +$ seed-mask $~ [%.n %.n %.n %.n %.n] $: output-source=? recipient=? timelock-intent=? gift=? parent-hash=? == :: $preseed: a $seed:transact in process of being built +$ preseed [name=@t (pair seed:transact seed-mask)] :: :: $spend-mask: tracks which field of a $spend:transact have been set +$ spend-mask $~ [%.n %.n %.n] $: signature=? seeds=? fee=? == :: +$ advanced-spend-input :: there is only one right way to create an $input from a $spend, so we don't need :: the mask or other commands. $% [%new name=@t] :: new empty input $: %set-name input-name=@t new-name=@t == $: %add-seed input-name=@t seed-name=@t == $: %set-fee input-name=@t fee=coins:transact == $: %set-note-from-name :: set .note using .name input-name=@t name=[@t @t] == $: %set-note-from-hash :: set .note using hash input-name=@t hash=@t == $: %derive-note-from-seeds :: derive note from seeds input-name=@t == $: %remove-seed input-name=@t seed-name=@t == $: %remove-seed-by-hash input-name=@t hash=@t == $: %print-status input-name=@t == == :: +$ input-mask $~ [%.n *spend-mask] $: note=? spend=spend-mask == :: +$ preinput [name=@t (pair input:transact input-mask)] :: +$ draft [name=@t p=inputs:transact] :: +$ advanced-spend-draft $% [%new name=@t] :: new input draft $: %set-name draft-name=@t new-name=@t == $: %add-input draft-name=@t input-name=@t == $: %remove-input draft-name=@t input-name=@t == $: %remove-input-by-name draft-name=@t name=[first=@t last=@t] == [%print-status =draft-name] :: print draft status == :: +$ npc-cause $% [%npc-bind pid=@ result=*] == :: +$ effect $~ [%npc 0 %poke %fact *fact:dumb] $% file-effect [%markdown @t] [%raw *] [%npc pid=@ npc-effect] [%exit code=@] == :: +$ file-effect $% [%file %read path=@t] [%file %write path=@t contents=@] == :: +$ npc-effect $% [%poke $>(%fact cause:dumb)] [%peek path] == :: ::TODO this probably shouldnt live in here :: ++ print |= nodes=markdown:m ^- (list effect) ~[(make-markdown-effect nodes)] :: ++ warn |* meg=tape |* * ?. bug +< ~> %slog.[1 (crip "WARN: !! {meg} !!")] +< :: ++ debug |* meg=tape |* * ?. bug +< ~> %slog.[0 (crip "wallet: {meg}")] +< :: ++ moat (keep state) :: :: :: +edit: modify inputs ++ edit |_ =state :: +* inp ^- preinput ?~ active-input.state %- (debug "no active input set!") *preinput =/ input-result (~(get-input plan draft-tree.state) u.active-input.state) ?~ input-result ~| "active input not found in draft-tree" !! u.input-result :: +add-seed: add a seed to the input :: ++ add-seed |= =seed:transact ^- [(list effect) ^state] ?: (~(has z-in:zo seeds.spend.p.inp) seed) :_ state %- print %- need %- de:md %- crip """ ## add-seed **seed already exists in .spend** """ =/ pre=preinput inp =/ =preinput %= pre seeds.spend.p %. seed ~(put z-in:zo seeds.spend.p.pre) :: seeds.spend.q %.y == =. active-input.state (some name.pre) :: =/ input-name=input-name (need active-input.state) =. draft-tree.state (~(add-input plan draft-tree.state) input-name preinput) :: if active-seed is set, link it to this input =. draft-tree.state ?: ?=(^ active-seed.state) (~(link-seed-to-input plan draft-tree.state) input-name u.active-seed.state) draft-tree.state `state :: ++ remove-seed |= =seed:transact ^- [(list effect) ^state] ?. (~(has z-in:zo seeds.spend.p.inp) seed) :_ state %- print %- need %- de:md %- crip """ ## remove-seed **seed not found in .spend** """ =/ pre=preinput inp =. seeds.spend.p.pre %. seed ~(del z-in:zo seeds.spend.p.pre) =. draft-tree.state =/ input-name=input-name (need active-input.state) (~(add-input plan draft-tree.state) input-name pre) `state -- :: :: +draw: modify drafts ++ draw |_ =state +* df ^- draft ?> ?=(^ active-draft.state) =/ draft-result (~(get-draft plan draft-tree.state) u.active-draft.state) ?~ draft-result *draft u.draft-result :: +add-input: add an input to the draft :: ++ add-input |= =input:transact ^- [(list effect) ^state] =/ =draft df =/ =input-name =+ (to-b58:nname:transact name.note.input) %- crip "{}-{}" ?: (~(has z-by:zo p.df) name.note.input) :_ state %- print %- need %- de:md %- crip """ ## add-input **input already exists in .draft** draft already has input with note name: {} """ =/ active-draft=draft-name (need active-draft.state) =. p.draft %- ~(put z-by:zo p.draft) :- name.note.input input =. draft-tree.state %. [active-draft draft] ~(add-draft plan draft-tree.state) =. draft-tree.state %. [active-draft input-name] ~(link-input-to-draft plan draft-tree.state) write-draft :: ++ write-draft ^- [(list effect) ^state] =? active-draft.state ?=(~ active-draft.state) (some *draft-name) ?> ?=(^ active-draft.state) =/ =draft df =. draft-tree.state (~(add-draft plan draft-tree.state) u.active-draft.state draft) =/ dat-jam (jam draft) =/ path=@t (crip "drafts/{(trip u.active-draft.state)}.draft") =/ effect [%file %write path dat-jam] :_ state ~[effect [%exit 0]] -- :: :: Convenience wrapper door for slip10 library :: ** Never use slip10 directly in the wallet ** ++ s10 |_ bas=base:slip10 ++ gen-master-key |= [entropy=byts salt=byts] =/ argon-byts=byts :- 32 %+ argon2-nockchain:argon2:crypto entropy salt =/ memo=tape (from-entropy:bip39 argon-byts) %- (debug "memo: {memo}") :- (crip memo) (from-seed:slip10 [64 (to-seed:bip39 memo "")]) :: ++ from-seed |= =byts (from-seed:slip10 byts) :: ++ from-private |= =keyc:slip10 (from-private:slip10 keyc) :: ++ from-public |= =keyc:slip10 (from-public:slip10 keyc) :: :: Derives public key from parent public key :: index i is expected to be a bip32 style index :: meaning that for the n-th child key, i=n. :: ++ derive-public |= [parent=coil i=@u] ?> &(?=(%pub -.key.parent) (lte i (dec (bex 31)))) => [cor=(from-public [p.key cc]:parent) i=i] (derive-public:cor i) :: :: Derives private key from parent private key :: index i is expected to be a bip32 style index :: meaning that for n-th child key: i = (n + 2^31) :: ++ derive-private |= [parent=coil i=@u] ?> &(?=(%prv -.key.parent) (gte i (bex 31))) => [cor=(from-private [p.key cc]:parent) i=i] (derive-private:cor i) -- :: ++ vault |_ =state ++ base-path ^- trek ?~ master.state ~|("base path not accessible because master not set" !!) /keys/[t/(to-b58:master master.state)] :: ++ seed-path ^- trek (welp base-path /seed) :: ++ get |_ key-type=?(%pub %prv) :: ++ key-path ^- trek (welp base-path ~[key-type]) :: ++ seed-path ^- trek (welp base-path /seed) :: ++ master ^- coil =/ =trek (welp key-path /m) =/ =meta (~(got of keys.state) trek) ?> ?=(%coil -.meta) meta :: ++ by-index |= index=@ud ^- coil ~| "key not found at index {}" =/ =trek (welp key-path /[ud/index]) =/ =meta (~(got of keys.state) trek) ?> ?=(%coil -.meta) meta :: ++ seed ^- meta ~| "key not found at {}" (~(got of keys.state) seed-path) :: ++ by-label |= label=@t ~| "key not found with label {