Merge pull request #4940 from urbit/release/bitcoin-wallet

Bitcoin Wallet
This commit is contained in:
ixv 2021-05-27 10:28:18 -07:00 committed by GitHub
commit 13807303b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
92 changed files with 18873 additions and 120 deletions

View File

@ -1,4 +1,4 @@
FROM jaredtobin/janeway:v0.13.4
FROM jaredtobin/janeway:v0.15.0
COPY entrypoint.sh /entrypoint.sh
EXPOSE 22/tcp
ENTRYPOINT ["/entrypoint.sh"]

View File

@ -10,7 +10,7 @@ chmod 600 service-account
chmod 600 id_ssh
chmod 600 id_ssh.pub
janeway release glob --dev --no-pill \
janeway release glob-all --dev --no-pill \
--credentials service-account \
--ssh-key id_ssh \
--do-it-live \

2
.gitignore vendored
View File

@ -76,4 +76,4 @@ pkg/interface/link-webext/web-ext-artifacts
*.xz
# Logs
*.log
*.log

View File

@ -0,0 +1,355 @@
:: btc-provider.hoon
:: Proxy that serves a BTC full node and ElectRS address indexer
::
:: Subscriptions: none
:: To Subscribers: /clients
:: current connection state
:: results/errors of RPC calls
::
:: Scrys
:: x/is-whitelisted/SHIP: bool, whether ship is whitelisted
::
/- *bitcoin, json-rpc, *btc-provider
/+ dbug, default-agent, bl=btc, groupl=group, resource
|%
+$ versioned-state
$% state-0
==
::
+$ state-0 [%0 =host-info =whitelist]
::
+$ card card:agent:gall
::
--
%- agent:dbug
=| state-0
=* state -
^- agent:gall
=<
|_ =bowl:gall
+* this .
def ~(. (default-agent this %|) bowl)
hc ~(. +> bowl)
::
++ on-init
^- (quip card _this)
~& > '%btc-provider initialized successfully'
=| wl=^whitelist
:- ~
%_ this
host-info
['' connected=%.n %main block=0 clients=*(set ship)]
whitelist wl(public %.n, kids %.n)
==
::
++ on-save
^- vase
!>(state)
::
++ on-load
|= old-state=vase
^- (quip card _this)
~& > '%btc-provider recompiled successfully '
`this(state !<(versioned-state old-state))
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?> ?|((team:title our.bowl src.bowl) (is-client:hc src.bowl))
=^ cards state
?+ mark (on-poke:def mark vase)
%btc-provider-command
?> (team:title our.bowl src.bowl)
(handle-command:hc !<(command vase))
%btc-provider-action
(handle-action:hc !<(action vase))
==
[cards this]
::
++ on-watch
|= pax=path
^- (quip card _this)
:: checking provider permissions before trying to subscribe
:: terrible hack until we have cross-ship scries
::
?: ?=([%permitted @ ~] pax)
:_ this
=/ jon=json
%+ frond:enjs:format
%'providerStatus'
%- pairs:enjs:format
:~ provider+s+(scot %p our.bowl)
permitted+b+(is-whitelisted:hc src.bowl)
==
[%give %fact ~ %json !>(jon)]~
::
?> ?=([%clients *] pax)
?. (is-whitelisted:hc src.bowl)
~& >>> "btc-provider: blocked client {<src.bowl>}"
[~[[%give %kick ~ ~]] this]
~& > "btc-provider: accepted client {<src.bowl>}"
:- [do-ping:hc]~
this(clients.host-info (~(put in clients.host-info) src.bowl))
::
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card _this)
:: check for connectivity every 30 seconds
::
?: ?=([%ping-timer *] wire)
:_ this
:~ do-ping:hc
(start-ping-timer:hc ~s30)
==
=^ cards state
?+ +<.sign-arvo (on-arvo:def wire sign-arvo)
%http-response
(handle-rpc-response:hc wire client-response.sign-arvo)
==
[cards this]
::
++ on-peek
|= pax=path
^- (unit (unit cage))
?+ pax (on-peek:def pax)
[%x %is-whitelisted @t ~]
``noun+!>((is-whitelisted:hc (ship (slav %p +>-.pax))))
::
[%x %is-client @t ~]
``noun+!>((is-client (ship (slav %p +>-.pax))))
==
::
++ on-leave on-leave:def
++ on-agent on-agent:def
++ on-fail on-fail:def
--
:: helper core
|_ =bowl:gall
++ handle-command
|= comm=command
^- (quip card _state)
?- -.comm
%set-credentials
:- :~ do-ping
(start-ping-timer ~s30)
==
%= state
host-info
[api-url.comm connected=%.n network.comm block=0 clients=*(set ship)]
==
::
%add-whitelist
?- -.wt.comm
%public
`state(public.whitelist %.y)
::
%kids
`state(kids.whitelist %.y)
::
%users
`state(users.whitelist (~(uni in users.whitelist) users.wt.comm))
::
%groups
`state(groups.whitelist (~(uni in groups.whitelist) groups.wt.comm))
==
::
%remove-whitelist
=. state
?- -.wt.comm
%public
state(public.whitelist %.n)
::
%kids
state(kids.whitelist %.n)
::
%users
state(users.whitelist (~(dif in users.whitelist) users.wt.comm))
::
%groups
state(groups.whitelist (~(dif in groups.whitelist) groups.wt.comm))
==
clean-client-list
==
:: if not connected, only %ping action is allowed
::
++ handle-action
|= act=action
^- (quip card _state)
?. ?|(connected.host-info ?=(%ping -.act))
~& >>> "Not connected to RPC"
[~[(send-update [%| %not-connected 500])] state]
::
=/ ract=action:rpc-types
?- -.act :: ~|("Invalid action" !!)
%address-info
[%get-address-info address.act]
::
%tx-info
[%get-tx-vals txid.act]
::
%raw-tx
[%get-raw-tx txid.act]
::
%broadcast-tx
[%broadcast-tx rawtx.act]
::
%ping
[%get-block-info ~]
==
[~[(req-card act ract)] state]
::
++ req-card
|= [act=action ract=action:rpc-types]
=| out=outbound-config:iris
=/ req=request:http
(gen-request:bl host-info ract)
[%pass (rpc-wire act) %arvo %i %request req out]
:: wire structure: /action-tas/now
::
++ rpc-wire
|= act=action ^- wire
/[-.act]/[(scot %ux (cut 3 [0 20] eny.bowl))]
::
++ kick-client
|= client=ship
^- (quip card _state)
~& >>> "dropping client {<client>}"
:- ~[[%give %kick ~[/clients] `client]]
state(clients.host-info (~(dif in clients.host-info) (silt ~[client])))
::
:: Handles HTTP responses from RPC servers. Parses for errors, then handles response.
:: For actions that require collating multiple RPC calls, uses req-card to call out
:: to RPC again if more information is required.
::
++ handle-rpc-response
|= [=wire response=client-response:iris]
^- (quip card _state)
?. ?=(%finished -.response) `state
=* status status-code.response-header.response
:: handle error types: connection errors, RPC errors (in order)
::
=^ conn-err state
(connection-error status)
?^ conn-err
:_ state(connected.host-info %.n)
~[(send-status [%disconnected ~]) (send-update [%| u.conn-err])]
::
%+ handle-rpc-result wire
%- parse-result:rpc:bl
(get-rpc-response:bl response)
::
++ connection-error
|= status=@ud
^- [(unit error) _state]
?+ status [`[%rpc-error ~] state]
%200
[~ state]
%400
[`[%bad-request status] state]
%401
[`[%no-auth status] state(connected.host-info %.n)]
%502
[`[%not-connected status] state(connected.host-info %.n)]
%504
[`[%not-connected status] state(connected.host-info %.n)]
==
::
++ handle-rpc-result
|= [=wire r=result:rpc-types]
^- (quip card _state)
?+ -.wire ~|("Unexpected HTTP response" !!)
%address-info
?> ?=([%get-address-info *] r)
:_ state
~[(send-update [%.y %address-info +.r])]
::
%tx-info
?> ?=([%get-tx-vals *] r)
:_ state
~[(send-update [%.y %tx-info +.r])]
::
%raw-tx
?> ?=([%get-raw-tx *] r)
:_ state
~[(send-update [%.y %raw-tx +.r])]
::
%broadcast-tx
?> ?=([%broadcast-tx *] r)
:_ state
~[(send-update [%.y %broadcast-tx +.r])]
::
%ping
?> ?=([%get-block-info *] r)
:_ state(connected.host-info %.y, block.host-info block.r)
?: =(block.host-info block.r)
~[(send-status [%connected network.host-info block.r fee.r])]
~[(send-status [%new-block network.host-info block.r fee.r blockhash.r blockfilter.r])]
==
::
++ send-status
|= =status ^- card
%- ?: ?=(%new-block -.status)
~&(>> "%new-block: {<block.status>}" same)
same
[%give %fact ~[/clients] %btc-provider-status !>(status)]
::
++ send-update
|= =update
^- card
=+ c=[%give %fact ~[/clients] %btc-provider-update !>(update)]
?: ?=(%.y -.update)
:: ~& >> "prov. update: {<p.update>}"
c
~& >> "prov. err: {<p.update>}"
c
::
++ is-whitelisted
|= user=ship ^- ?
|^
?| public.whitelist
=(our.bowl user)
?&(kids.whitelist is-kid)
(~(has in users.whitelist) user)
in-group
==
++ is-kid
=(our.bowl (sein:title our.bowl now.bowl user))
++ in-group
=/ gs ~(tap in groups.whitelist)
|-
?~ gs %.n
?: (~(is-member groupl bowl) user i.gs)
%.y
$(gs t.gs)
:: .^((unit group:g) %gx ;:(weld /=group-store=/groups p /noun))
--
:: +clean-client-list: remove clients who are no longer whitelisted
:: called after a whitelist change
::
++ clean-client-list
^- (quip card _state)
=/ to-kick=(set ship)
%- silt
%+ murn ~(tap in clients.host-info)
|= c=ship ^- (unit ship)
?:((is-whitelisted c) ~ `c)
:_ state(clients.host-info (~(dif in clients.host-info) to-kick))
%+ turn ~(tap in to-kick)
|=(c=ship [%give %kick ~[/clients] `c])
::
++ is-client
|= user=ship ^- ?
(~(has in clients.host-info) user)
::
++ start-ping-timer
|= interval=@dr ^- card
[%pass /ping-timer %arvo %b %wait (add now.bowl interval)]
::
++ do-ping
^- card
=/ act=action [%ping ~]
:* %pass /ping/[(scot %da now.bowl)] %agent
[our.bowl %btc-provider] %poke
%btc-provider-action !>(act)
==
--

1137
pkg/arvo/app/btc-wallet.hoon Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="128" height="128" rx="4" fill="#F98E40"/>
<path d="M75.9373 60.7714C76.4367 57.4333 73.8951 55.6389 70.4198 54.4418L71.5471 49.9199L68.7947 49.2339L67.6971 53.6366C66.9735 53.4563 66.2303 53.2862 65.4919 53.1177L66.5972 48.686L63.8463 48L62.7182 52.5203C62.1193 52.3839 61.5313 52.249 60.9606 52.1071L60.9637 52.093L57.1678 51.1452L56.4356 54.0851C56.4356 54.0851 58.4778 54.5531 58.4347 54.5821C59.5495 54.8604 59.751 55.5981 59.7172 56.1829L58.4331 61.3343C58.51 61.3539 58.6095 61.3821 58.7193 61.426C58.6276 61.4033 58.5296 61.3782 58.4284 61.3539L56.6285 68.5702C56.4921 68.9089 56.1463 69.4169 55.3671 69.224C55.3945 69.264 53.3664 68.7246 53.3664 68.7246L52 71.8754L55.5819 72.7683C56.2483 72.9352 56.9013 73.1101 57.5441 73.2747L56.4051 77.8483L59.1544 78.5343L60.2825 74.0093C61.0335 74.2131 61.7626 74.4013 62.476 74.5784L61.3518 79.0822L64.1043 79.7682L65.2434 75.2032C69.9369 76.0915 73.4663 75.7332 74.9519 71.4881C76.149 68.07 74.8923 66.0984 72.4228 64.8127C74.2212 64.398 75.5759 63.215 75.9373 60.7714V60.7714ZM69.6484 69.5901C68.7978 73.0082 63.0428 71.1604 61.177 70.6971L62.6884 64.6379C64.5543 65.1035 70.5374 66.0255 69.6484 69.5901ZM70.4998 60.722C69.7236 63.8312 64.9337 62.2515 63.3799 61.8642L64.7502 56.3687C66.304 56.756 71.308 57.4788 70.4998 60.722Z" fill="white" fill-opacity="0.7"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,31 @@
<!doctype html>
<html>
<head>
<title>Wallet</title>
<meta charset="utf-8" />
<meta name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no,maximum-scale=1"/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-touch-fullscreen" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<!--
<link rel="apple-touch-icon" href="/~btc/img/touch_icon.png">
<link rel="icon" type="image/png" href="/~btc/img/Favicon.png">
-->
<link rel="manifest"
href='data:application/manifest+json,{
"name": "Wallet",
"short_name": "Wallet",
"description": "A%20bitcoin%20wallet%20for%20urbit",
"display": "standalone",
"background_color": "%23FFFFFF",
"theme_color": "%23000000"}' />
</head>
<body>
<div id="root"></div>
<div id="portal-root"></div>
<script src="/~landscape/js/channel.js"></script>
<script src="/~landscape/js/session.js"></script>
<script src="/~btc/js/bundle/index.js"></script>
</body>
</html>

View File

@ -225,6 +225,7 @@
[~ %js] (js-response:gen file)
[~ %css] (css-response:gen file)
[~ %png] (png-response:gen file)
[~ %svg] (svg-response:gen file)
[~ %ico] (ico-response:gen file)
::
[~ %html]
@ -273,7 +274,10 @@
++ match-content-path
|= [pax=path =^serving is-file=?]
^- (unit [content path ?])
%- ~(rep by serving)
%+ roll
%+ sort ~(tap by serving)
|= [[a=path *] [b=path *]]
(gth (lent a) (lent b))
|= $: [url-base=path =content public=? spa=?]
out=(unit [content path ?])
==

View File

@ -2,13 +2,16 @@
::
:: prompts content delivery and Gall state storage for Landscape JS blob
::
/- glob
/- glob, *resource
/+ default-agent, verb, dbug
|%
++ hash 0v2.3mphd.voocg.covr7.iv5la.kkk8h
++ landscape-hash 0v3.l561u.f9a2b.cn958.ddf81.mc707
++ btc-wallet-hash 0v2.k8l6a.tqs6n.8cg1q.7t1e4.fis67
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
+$ state-1 [%1 =globs:glob]
+$ all-states
$% state-0
state-1
==
+$ card card:agent:gall
--
@ -19,12 +22,12 @@
[%pass [%timer path] %arvo %b %wait (add now ~m30)]
::
++ wait-start
|= now=@da
|= [now=@da =path]
^- card
[%pass /start %arvo %b %wait now]
[%pass [%start path] %arvo %b %wait now]
::
++ poke-file-server
|= [our=@p =cage]
|= [our=@p hash=@uv =cage]
^- card
[%pass /serving/(scot %uv hash) %agent [our %file-server] %poke cage]
::
@ -43,9 +46,12 @@
^- card
[%pass [%running path] %agent [our %spider] %leave ~]
--
=| state=state-0
=. hash.state hash
=/ serve-path=path /'~landscape'/js/bundle
=| state=state-1
=. globs.state
(~(put by globs.state) /'~landscape'/js/bundle landscape-hash ~)
=. globs.state
(~(put by globs.state) /'~btc'/js/bundle btc-wallet-hash ~)
::
^- agent:gall
%+ verb |
%- agent:dbug
@ -56,77 +62,122 @@
++ on-init
^- (quip card _this)
:: delay through timer to make sure %spider has started
[[(wait-start now.bowl) ~] this]
:_ this
%+ turn ~(tap by ~(key by globs.state))
|=(=path (wait-start now.bowl path))
::
++ on-save !>(state)
++ on-load
|= old-state=vase
^- (quip card _this)
=+ !<(old=all-states old-state)
?> ?=(%0 -.old)
?~ glob.old
on-init
?: ?=(%& -.u.glob.old)
?: =(hash.old hash.state)
`this(state old)
on-init
=/ cancel-cards
=/ args [tid.p.u.glob.old &]
:~ (leave-spider /(scot %uv hash.old) our.bowl)
(poke-spider /(scot %uv hash.old) our.bowl %spider-stop !>(args))
=| cards=(list card)
=/ upgrading=? %.n
|-
?- -.old
%1
=/ [cards-1=(list card) =globs:glob]
%- ~(rep by globs.old)
|= $: [=serve=path =glob-details:glob]
cards=(list card)
globs=_globs.state
==
^- [(list card) globs:glob]
=/ new-glob-details (~(get by globs) serve-path)
?~ new-glob-details
[cards globs]
?~ glob.glob-details
:_ globs
[(wait-start now.bowl serve-path) cards]
?: ?=(%& -.u.glob.glob-details)
?: =(hash.u.new-glob-details hash.glob-details)
[cards (~(put by globs) serve-path glob-details)]
:_ globs
[(wait-start now.bowl serve-path) cards]
?: upgrading
:_ globs
[(wait-start now.bowl serve-path) cards]
=/ args [tid.p.u.glob.glob-details &]
=/ spider-wire [(scot %uv hash.glob-details) serve-path]
:_ globs
:* (leave-spider spider-wire our.bowl)
(poke-spider spider-wire our.bowl %spider-stop !>(args))
(wait-start now.bowl serve-path)
cards
==
:- (weld cards cards-1)
this(globs.state globs)
::
%0
=/ globs
%- ~(gas by *globs:glob)
[/'~landscape'/js/bundle hash.old glob.old]~
%= $
old [%1 globs]
::
cards
?~ glob.old ~
?: =(%& -.u.glob.old) ~
?> ?=(%| -.u.glob.old)
=/ args [tid.p.u.glob.old &]
:~ (leave-spider /(scot %uv hash.old) our.bowl)
(poke-spider /(scot %uv hash.old) our.bowl %spider-stop !>(args))
==
::
upgrading %.y
==
=^ init-cards this on-init
[(weld cancel-cards init-cards) this]
==
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?+ mark (on-poke:def mark vase)
%glob-make
=+ !<(dir=path vase)
:_ this
=/ home=path /(scot %p our.bowl)/home/(scot %da now.bowl)
=+ .^(paths=(list path) %ct (weld home dir))
=+ .^(=js=tube:clay %cc (weld home /js/mime))
=+ .^(=map=tube:clay %cc (weld home /map/mime))
=+ .^(arch %cy (weld home /app/landscape/js/bundle))
=/ bundle-hash=@t
%- need
^- (unit @t)
%- ~(rep by dir)
|= [[file=@t ~] out=(unit @t)]
?^ out out
?. ?& =((end [3 6] file) 'index.')
!=('sj.' (end [3 3] (swp 3 file)))
==
out
``@t`(rsh [3 6] file)
=/ js-name
(cat 3 'index.' bundle-hash)
=/ map-name
(cat 3 js-name '.js')
=+ .^(js=@t %cx :(weld home /app/landscape/js/bundle /[js-name]/js))
=+ .^(map=@t %cx :(weld home /app/landscape/js/bundle /[map-name]/map))
=+ .^(sw=@t %cx :(weld home /app/landscape/js/bundle /serviceworker/js))
=+ !<(=js=mime (js-tube !>(js)))
=+ !<(=sw=mime (js-tube !>(sw)))
=+ !<(=map=mime (map-tube !>(map)))
=/ =glob:glob
%- ~(gas by *glob:glob)
:~ /[js-name]/js^js-mime
/[map-name]/map^map-mime
/serviceworker/js^sw-mime
%+ turn paths
|= pax=path
^- [path mime]
=+ .^(file=@t %cx (weld home pax))
=/ mar (snag 0 (flop pax))
:- (slag (lent dir) pax)
?+ mar ~|(unsupported-glob-type+mar !!)
%js !<(mime (js-tube !>(file)))
%map !<(mime (map-tube !>(file)))
==
=/ =path /(cat 3 'glob-' (scot %uv (sham glob)))/glob
~& globbed+`(set ^path)`~(key by glob)
[%pass /make %agent [our.bowl %hood] %poke %drum-put !>([path (jam glob)])]~
::
%noun
?: =(%kick q.vase)
(on-load !>(state(hash *@uv)))
?: =(%kick -.q.vase)
=+ !<([%kick =path] vase)
=/ glob-details (~(get by globs.state) path)
?~ glob-details
~& no-such-glob+path
`this
=/ new-state
state(globs (~(put by globs.state) path *@uv glob.u.glob-details))
(on-load !>(new-state))
(on-poke:def mark vase)
==
::
++ on-watch on-watch:def
++ on-leave on-leave:def
++ on-peek on-peek:def
::
++ on-peek
|= =path
^- (unit (unit cage))
?+ path (on-peek:def path)
[%x %btc-wallet ~] ``noun+!>(btc-wallet-hash)
==
::
++ on-agent
|= [=wire =sign:agent:gall]
^- (quip card _this)
@ -134,83 +185,109 @@
(on-agent:def wire sign)
?: ?=([%make ~] wire)
(on-agent:def wire sign)
?. ?=([%running @ ~] wire)
?. ?=([%running @ *] wire)
%- (slog leaf+"glob: strange on-agent! {<wire -.sign>}" ~)
(on-agent:def wire sign)
::
=/ produced-hash (slav %uv i.t.wire)
=* serve-path t.t.wire
=/ glob-details (~(get by globs.state) serve-path)
?~ glob-details
[~ this]
?. =(hash.u.glob-details produced-hash)
[~ this]
?- -.sign
%poke-ack
?~ p.sign
[~ this]
%- (slog leaf+"glob: couldn't start thread; will retry" u.p.sign)
:_ this(glob.state ~) :_ ~
(leave-spider t.wire our.bowl)
:_ this(globs.state (~(put by globs.state) serve-path produced-hash ~))
[(leave-spider t.wire our.bowl)]~
::
%watch-ack
?~ p.sign
[~ this]
%- (slog leaf+"glob: couldn't listen to thread; will retry" u.p.sign)
[~ this(glob.state ~)]
[~ this(globs.state (~(put by globs.state) serve-path produced-hash ~))]
::
%kick
=? glob.state ?=([~ %| *] glob.state)
~
`this
?. ?=([~ %| *] glob.u.glob-details)
`this
[~ this(globs.state (~(put by globs.state) serve-path produced-hash ~))]
::
%fact
=/ produced-hash (slav %uv i.t.wire)
?. =(hash.state produced-hash)
[~ this]
?+ p.cage.sign (on-agent:def wire sign)
%thread-fail
=+ !<([=term =tang] q.cage.sign)
%- (slog leaf+"glob: thread failed; will retry" leaf+<term> tang)
[~ this(glob.state ~)]
:- ~
this(globs.state (~(put by globs.state) serve-path produced-hash ~))
::
%thread-done
=+ !<(=glob:glob q.cage.sign)
?. =(hash.state (sham glob))
?. =(hash.u.glob-details (sham glob))
%: mean
leaf+"glob: hash doesn't match!"
>expected=hash.state<
>expected=hash.u.glob-details<
>got=(sham glob)<
~
==
:_ this(glob.state `[%& glob]) :_ ~
%+ poke-file-server our.bowl
[%file-server-action !>([%serve-glob serve-path glob %&])]
=. globs.state
(~(put by globs.state) serve-path produced-hash `[%& glob])
:_ this :_ ~
%: poke-file-server
our.bowl
produced-hash
%file-server-action
!>([%serve-glob serve-path glob %&])
==
==
==
::
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card _this)
?: ?=([%start ~] wire)
=/ new-tid=@ta (cat 3 'glob--' (scot %uv eny.bowl))
=/ args [~ `new-tid %glob !>([~ hash.state])]
=/ action !>([%unserve-dir serve-path])
:_ this(glob.state `[%| new-tid])
:~ (poke-file-server our.bowl %file-server-action action)
(wait-timeout /[new-tid] now.bowl)
(watch-spider /(scot %uv hash.state) our.bowl /thread-result/[new-tid])
(poke-spider /(scot %uv hash.state) our.bowl %spider-start !>(args))
?: ?=([%start *] wire)
=* serve-path t.wire
=/ glob-details (~(get by globs.state) serve-path)
?~ glob-details
[~ this]
=/ new-tid=@ta (cat 3 'glob--' (scot %uv (sham eny.bowl serve-path)))
=/ args [~ `new-tid %glob !>([~ hash.u.glob-details])]
=/ action=cage [%file-server-action !>([%unserve-dir serve-path])]
=/ spider-wire [(scot %uv hash.u.glob-details) serve-path]
=. globs.state
(~(put by globs.state) serve-path hash.u.glob-details `[%| new-tid])
:_ this
:~ (poke-file-server our.bowl hash.u.glob-details action)
(wait-timeout [new-tid serve-path] now.bowl)
(watch-spider spider-wire our.bowl /thread-result/[new-tid])
(poke-spider spider-wire our.bowl %spider-start !>(args))
==
?. ?=([%timer @ ~] wire)
::
?. ?=([%timer @ *] wire)
%- (slog leaf+"glob: strange on-arvo wire: {<wire [- +<]:sign-arvo>}" ~)
`this
?. ?=(%wake +<.sign-arvo)
%- (slog leaf+"glob: strange on-arvo sign: {<wire [- +<]:sign-arvo>}" ~)
`this
?: ?=([~ %& *] glob.state)
=* serve-path t.wire
=/ glob-details (~(get by globs.state) serve-path)
?~ glob-details
`this
?. ?| ?=(~ glob.state)
=(i.t.wire tid.p.u.glob.state)
?: ?=([~ %& *] glob.u.glob-details)
`this
?. ?| ?=(~ glob.u.glob-details)
=(i.t.wire tid.p.u.glob.u.glob-details)
==
`this
?^ error.sign-arvo
%- (slog leaf+"glob: timer handling failed; will retry" ~)
[[(wait-timeout t.wire now.bowl)]~ this]
%- (slog leaf+"glob: timed out; retrying" ~)
(on-load !>(state(hash *@uv)))
=/ new-details u.glob-details(hash *@uv)
=/ new-state state(globs (~(put by globs.state) serve-path new-details))
(on-load !>(new-state))
::
++ on-fail on-fail:def
--

View File

@ -24,6 +24,6 @@
<div id="portal-root"></div>
<script src="/~landscape/js/channel.js"></script>
<script src="/~landscape/js/session.js"></script>
<script src="/~landscape/js/bundle/index.31fd4ffe12da870b6215.js"></script>
<script src="/~landscape/js/bundle/index.120e51d99d178b9ff591.js"></script>
</body>
</html>

View File

@ -13,16 +13,23 @@
[%4 state-zero]
[%5 state-zero]
[%6 state-zero]
[%7 state-7]
==
::
+$ state-zero
$: tiles=tiles-0:store
=tile-ordering:store
first-time=?
==
::
+$ state-7
$: =tiles:store
=tile-ordering:store
first-time=?
==
--
::
=| [%6 state-zero]
=| [%7 state-7]
=* state -
%- agent:dbug
^- agent:gall
@ -32,7 +39,7 @@
::
++ on-init
^- (quip card _this)
=/ new-state *state-zero
=/ new-state *state-7
=. new-state
%_ new-state
tiles
@ -41,12 +48,12 @@
|= =term
:- term
^- tile:store
?+ term [[%custom ~] %.y]
?+ term [[%custom ~ ~] %.y]
%term [[%basic 'Terminal' '/~landscape/img/term.png' '/~term'] %.y]
==
tile-ordering [%weather %clock %term ~]
==
[~ this(state [%6 new-state])]
[~ this(state [%7 new-state])]
::
++ on-save !>(state)
++ on-load
@ -55,8 +62,22 @@
=/ old-state !<(versioned-state old)
=| cards=(list card)
|- ^- (quip card _this)
?: ?=(%6 -.old-state)
?: ?=(%7 -.old-state)
[cards this(state old-state)]
::
?: ?=(%6 -.old-state)
=/ new-tiles=tiles:store
%- ~(gas by *tiles:store)
%+ turn ~(tap by tiles.old-state)
|= [=term =tile-0:store]
:- term
:_ is-shown.tile-0
?- -.type.tile-0
%basic type.tile-0
%custom [%custom ~ ~]
==
$(old-state [%7 new-tiles tile-ordering.old-state first-time.old-state])
::
?: ?=(%5 -.old-state)
:: replace %dojo with %term
::
@ -86,11 +107,11 @@
=. new-state
%_ new-state
tiles
%- ~(gas by *tiles:store)
%- ~(gas by *tiles-0:store)
%+ turn `(list term)`[%weather %clock %dojo ~]
|= =term
:- term
^- tile:store
^- tile-0:store
?+ term [[%custom ~] %.y]
%dojo [[%basic 'Dojo' '/~landscape/img/Dojo.png' '/~dojo'] %.y]
==

View File

@ -0,0 +1,13 @@
:: Sends a raw RPC action to the BTC Provider
::
:: Commands:
::
::
::
/- *btc-provider
::
:- %say
|= $: [now=@da eny=@uvJ =beak]
[[act=action ~] ~]
==
[%btc-provider-action act]

View File

@ -0,0 +1,13 @@
:: Sends a command to the BTC Provider
::
:: Commands:
::
::
::
/- *btc-provider
::
:- %say
|= $: [now=@da eny=@uvJ =beak]
[[comm=command ~] ~]
==
[%btc-provider-command comm]

View File

@ -0,0 +1,5 @@
:- %say
|= [[now=time * bec=beak] ~ ~]
:- %noun
:- %btc-wallet-hash
.^(@uv %gx (en-beam bec(q %glob) /btc-wallet/noun))

View File

@ -0,0 +1,9 @@
:: Sends an action to btc-wallet
::
/- *btc-wallet
::
:- %say
|= $: [now=@da eny=@uvJ =beak]
[[act=action ~] ~]
==
[%btc-wallet-action act]

View File

@ -0,0 +1,9 @@
:: Sends a command to btc-wallet
::
/- *btc-wallet
::
:- %say
|= $: [now=@da eny=@uvJ =beak]
[[comm=command ~] ~]
==
[%btc-wallet-command comm]

View File

@ -1,3 +1,3 @@
:- %say
|= *
[%glob-make ~]
|= [^ [=path ~] ~]
[%glob-make path]

247
pkg/arvo/lib/bip/b158.hoon Normal file
View File

@ -0,0 +1,247 @@
/- bc=bitcoin
/+ bcu=bitcoin-utils
|%
++ params
|%
++ p 19
++ m 784.931
--
::
++ siphash
|= [k=byts m=byts]
^- byts
|^
?> =(wid.k 16)
?> (lte (met 3 dat.k) wid.k)
?> (lte (met 3 dat.m) wid.m)
=. k (flim:sha k)
=. m (flim:sha m)
(flim:sha (fin (comp m (init dat.k))))
:: Initialise internal state
::
++ init
|= k=@
^- [@ @ @ @]
=/ k0=@ (end [6 1] k)
=/ k1=@ (cut 6 [1 1] k)
:^ (mix k0 0x736f.6d65.7073.6575)
(mix k1 0x646f.7261.6e64.6f6d)
(mix k0 0x6c79.6765.6e65.7261)
(mix k1 0x7465.6462.7974.6573)
::
:: Compression rounds
++ comp
|= [m=byts v=[v0=@ v1=@ v2=@ v3=@]]
^- [@ @ @ @]
=/ len=@ud (div wid.m 8)
=/ last=@ (lsh [3 7] (mod wid.m 256))
=| i=@ud
=| w=@
|-
=. w (cut 6 [i 1] dat.m)
?: =(i len)
=. v3.v (mix v3.v (mix last w))
=. v (rnd (rnd v))
=. v0.v (mix v0.v (mix last w))
v
%= $
v =. v3.v (mix v3.v w)
=. v (rnd (rnd v))
=. v0.v (mix v0.v w)
v
i (add i 1)
==
::
:: Finalisation rounds
++ fin
|= v=[v0=@ v1=@ v2=@ v3=@]
^- byts
=. v2.v (mix v2.v 0xff)
=. v (rnd (rnd (rnd (rnd v))))
:- 8
:(mix v0.v v1.v v2.v v3.v)
::
:: Sipround
++ rnd
|= [v0=@ v1=@ v2=@ v3=@]
^- [@ @ @ @]
=. v0 (~(sum fe 6) v0 v1)
=. v2 (~(sum fe 6) v2 v3)
=. v1 (~(rol fe 6) 0 13 v1)
=. v3 (~(rol fe 6) 0 16 v3)
=. v1 (mix v1 v0)
=. v3 (mix v3 v2)
=. v0 (~(rol fe 6) 0 32 v0)
=. v2 (~(sum fe 6) v2 v1)
=. v0 (~(sum fe 6) v0 v3)
=. v1 (~(rol fe 6) 0 17 v1)
=. v3 (~(rol fe 6) 0 21 v3)
=. v1 (mix v1 v2)
=. v3 (mix v3 v0)
=. v2 (~(rol fe 6) 0 32 v2)
[v0 v1 v2 v3]
--
:: +str: bit streams
:: read is from the front
:: write appends to the back
::
++ str
|%
++ read-bit
|= s=bits:bc
^- [bit=@ub rest=bits:bc]
?> (gth wid.s 0)
:* ?:((gth wid.s (met 0 dat.s)) 0b0 0b1)
[(dec wid.s) (end [0 (dec wid.s)] dat.s)]
==
::
++ read-bits
|= [n=@ s=bits:bc]
^- [bits:bc rest=bits:bc]
=| bs=bits:bc
|-
?: =(n 0) [bs s]
=^ b s (read-bit s)
$(n (dec n), bs (write-bits bs [1 b]))
::
++ write-bits
|= [s1=bits:bc s2=bits:bc]
^- bits:bc
[(add wid.s1 wid.s2) (can 0 ~[s2 s1])]
--
:: +gol: Golomb-Rice encoding/decoding
::
++ gol
|%
:: +en: encode x and append to end of s
:: - s: bits stream
:: - x: number to add to the stream
:: - p: golomb-rice p param
::
++ en
|= [s=bits:bc x=@ p=@]
^- bits:bc
=+ q=(rsh [0 p] x)
=+ unary=[+(q) (lsh [0 1] (dec (bex q)))]
=+ r=[p (end [0 p] x)]
%+ write-bits:str s
(write-bits:str unary r)
::
++ de
|= [s=bits:bc p=@]
^- [delta=@ rest=bits:bc]
|^ ?> (gth wid.s 0)
=^ q s (get-q s)
=^ r s (read-bits:str p s)
[(add dat.r (lsh [0 p] q)) s]
::
++ get-q
|= s=bits:bc
=| q=@
=^ first-bit s (read-bit:str s)
|-
?: =(0 first-bit) [q s]
=^ b s (read-bit:str s)
$(first-bit b, q +(q))
--
--
:: +hsh
::
++ hsh
|%
:: +to-range
:: - item: scriptpubkey to hash
:: - f: N*M
:: - k: key for siphash (end of blockhash, reversed)
::
++ to-range
|= [item=byts f=@ k=byts]
^- @
(rsh [0 64] (mul f (swp 3 dat:(siphash k item))))
:: +set-construct: return sorted hashes of scriptpubkeys
::
++ set-construct
|= [items=(list byts) k=byts f=@]
^- (list @)
%+ sort
%+ turn items
|= item=byts
(to-range item f k)
lth
--
::
++ parse-filter
|= filter=hexb:bc
^- [n=@ux gcs-set=bits:bc]
=/ n n:(de:csiz:bcu filter)
=/ lead=@ ?:(=(1 wid.n) 1 +(wid.n))
:- dat.n
[(mul 8 (sub wid.filter lead)) `@ub`dat:(drop:byt:bcu lead filter)]
:: +to-key: blockhash (little endian) to key for siphash
::
++ to-key
|= blockhash=tape
^- byts
%+ take:byt:bcu 16
%- flip:byt:bcu
(from-cord:hxb:bcu (crip blockhash))
:: +match: whether block filter matches *any* target scriptpubkeys
:: - filter: full block filter, with leading N
:: - k: key for siphash (end of blockhash, reversed)
:: - targets: scriptpubkeys to match
::
++ match
|= [filter=hexb:bc k=byts targets=(list byts)]
^- ?
=/ [p=@ m=@] [p:params m:params]
=/ [n=@ux gcs-set=bits:bc] (parse-filter filter)
=+ target-hs=(set-construct:hsh targets k (mul n m))
=+ last-val=0
|-
?~ target-hs %.n
?: =(last-val i.target-hs)
%.y
?: (gth last-val i.target-hs)
$(target-hs t.target-hs)
:: last-val is less than target: check next val in GCS, if any
::
?: (lth wid.gcs-set p) %.n
=^ delta gcs-set
(de:gol gcs-set p)
$(last-val (add delta last-val))
:: +all-match: returns all target byts that match
:: - filter: full block filter, with leading N
:: - k: key for siphash (end of blockhash, reversed)
:: - targets: scriptpubkeys to match
::
++ all-match
|= [filter=hexb:bc k=byts targets=(list byts)]
^- (set hexb:bc)
%- ~(gas in *(set hexb:bc))
=/ [p=@ m=@] [p:params m:params]
=/ [n=@ux gcs-set=bits:bc] (parse-filter filter)
=/ target-map=(map @ hexb:bc)
%- ~(gas by *(map @ hexb:bc))
%+ turn targets
|=(t=hexb:bc [(to-range:hsh t (mul n m) k) t])
=+ target-hs=(sort ~(tap in ~(key by target-map)) lth)
=+ last-val=0
=| matches=(list @)
|-
?~ target-hs
(murn matches ~(get by target-map))
?: =(last-val i.target-hs)
%= $
target-hs t.target-hs
matches [last-val matches]
==
?: (gth last-val i.target-hs)
$(target-hs t.target-hs)
:: last-val is less than target: get next val in GCS, if any
::
?: (lth wid.gcs-set p)
(murn matches ~(get by target-map))
=^ delta gcs-set
(de:gol gcs-set p)
$(last-val (add delta last-val))
--

144
pkg/arvo/lib/bip/b173.hoon Normal file
View File

@ -0,0 +1,144 @@
:: BIP173: Bech32 Addresses
:: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki
::
:: Heavily copies:
:: https://github.com/bitcoinjs/bech32/blob/master/index.js
::
/- sur=bitcoin
/+ bcu=bitcoin-utils
=, sur
=, bcu
|%
++ prefixes
^- (map network tape)
(my [[%main "bc"] [%testnet "tb"] ~])
++ charset "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
+$ raw-decoded [hrp=tape data=(list @) checksum=(list @)]
:: below is a port of: https://github.com/bitcoinjs/bech32/blob/master/index.js
::
++ polymod
|= values=(list @)
|^ ^- @
=/ gen=(list @ux)
~[0x3b6a.57b2 0x2650.8e6d 0x1ea1.19fa 0x3d42.33dd 0x2a14.62b3]
=/ chk=@ 1
|- ?~ values chk
=/ top (rsh [0 25] chk)
=. chk
(mix i.values (lsh [0 5] (dis chk 0x1ff.ffff)))
$(values t.values, chk (update-chk chk top gen))
::
++ update-chk
|= [chk=@ top=@ gen=(list @ux)]
=/ is (gulf 0 4)
|- ?~ is chk
?: =(1 (dis 1 (rsh [0 i.is] top)))
$(is t.is, chk (mix chk (snag i.is gen)))
$(is t.is)
--
::
++ expand-hrp
|= hrp=tape
^- (list @)
=/ front (turn hrp |=(p=@tD (rsh [0 5] p)))
=/ back (turn hrp |=(p=@tD (dis 31 p)))
(zing ~[front ~[0] back])
::
++ verify-checksum
|= [hrp=tape data-and-checksum=(list @)]
^- ?
%- |=(a=@ =(1 a))
%- polymod
(weld (expand-hrp hrp) data-and-checksum)
::
++ checksum
|= [hrp=tape data=(list @)]
^- (list @)
:: xor 1 with the polymod
::
=/ pmod=@
%+ mix 1
%- polymod
(zing ~[(expand-hrp hrp) data (reap 6 0)])
%+ turn (gulf 0 5)
|=(i=@ (dis 31 (rsh [0 (mul 5 (sub 5 i))] pmod)))
::
++ charset-to-value
|= c=@tD
^- (unit @)
(find ~[c] charset)
++ value-to-charset
|= value=@
^- (unit @tD)
?: (gth value 31) ~
`(snag value charset)
::
++ is-valid
|= [bech=tape last-1-pos=@] ^- ?
?& ?|(=((cass bech) bech) =((cuss bech) bech)) :: to upper or to lower is same as bech
(gte last-1-pos 1)
(lte (add last-1-pos 7) (lent bech))
(lte (lent bech) 90)
(levy bech |=(c=@tD (gte c 33)))
(levy bech |=(c=@tD (lte c 126)))
==
:: data should be 5bit words
::
++ encode-raw
|= [hrp=tape data=(list @)]
^- cord
=/ combined=(list @)
(weld data (checksum hrp data))
%- crip
(zing ~[hrp "1" (tape (murn combined value-to-charset))])
++ decode-raw
|= body=cord
^- (unit raw-decoded)
=/ bech (cass (trip body)) :: to lowercase
=/ pos (flop (fand "1" bech))
?~ pos ~
=/ last-1=@ i.pos
?. (is-valid bech last-1) :: check bech32 validity (not segwit validity or checksum)
~
=/ hrp (scag last-1 bech)
=/ encoded-data-and-checksum=(list @)
(slag +(last-1) bech)
=/ data-and-checksum=(list @)
%+ murn encoded-data-and-checksum
charset-to-value
?. =((lent encoded-data-and-checksum) (lent data-and-checksum)) :: ensure all were in CHARSET
~
?. (verify-checksum hrp data-and-checksum)
~
=/ checksum-pos (sub (lent data-and-checksum) 6)
`[hrp (scag checksum-pos data-and-checksum) (slag checksum-pos data-and-checksum)]
:: +from-address: BIP173 bech32 address encoding to hex
:: https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki
:: expects to drop a leading 5-bit 0 (the witness version)
::
++ from-address
|= body=cord
^- hexb
~| "Invalid bech32 address"
=/ d=(unit raw-decoded) (decode-raw body)
?> ?=(^ d)
=/ bs=bits (from-atoms:bit 5 data.u.d)
=/ byt-len=@ (div (sub wid.bs 5) 8)
?> =(5^0b0 (take:bit 5 bs))
?> ?| =(20 byt-len)
=(32 byt-len)
==
[byt-len `@ux`dat:(take:bit (mul 8 byt-len) (drop:bit 5 bs))]
:: pubkey is the 33 byte ECC compressed public key
::
++ encode-pubkey
|= [=network pubkey=byts]
^- (unit cord)
?. =(33 wid.pubkey)
~|('pubkey must be a 33 byte ECC compressed public key' !!)
=/ prefix (~(get by prefixes) network)
?~ prefix ~
:- ~
%+ encode-raw u.prefix
[0v0 (to-atoms:bit 5 [160 `@ub`dat:(hash-160 pubkey)])]
--

182
pkg/arvo/lib/bip/b174.hoon Normal file
View File

@ -0,0 +1,182 @@
:: BIP174: PSBTs
:: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
::
/- sur=bitcoin
/+ bcu=bitcoin-utils
=, sur
=, bcu
|%
++ en
|%
++ globals
|= rawtx=hexb
^- map:psbt
:~ [[1 0x0] rawtx]
==
::
++ input
|= [only-witness=? i=in:psbt]
^- map:psbt
%+ weld
?: only-witness ~
~[[1^0x0 rawtx.i]]
:~ (witness-tx i)
(hdkey %input hdkey.i)
==
::
++ output
|= =out:psbt
^- map:psbt
?~ hk.out ~
:~ (hdkey %output u.hk.out)
==
::
++ witness-tx
|= i=in:psbt
^- keyval:psbt
:- [1 0x1]
%- cat:byt
:~ (flip:byt 8^value.utxo.i)
1^0x16
2^0x14
(hash-160 pubkey.hdkey.i)
==
::
++ hdkey
|= [=target:psbt h=^hdkey]
^- keyval:psbt
=/ typ=@ux
?- target
%input 0x6
%output 0x2
==
=/ coin-type=hexb
?- network.h
%main
1^0x0
%testnet
1^0x1
==
:- (cat:byt ~[1^typ pubkey.h])
%- cat:byt
:~ fprint.h
1^`@ux`bipt.h 3^0x80
coin-type 3^0x80
4^0x80
1^`@ux`chyg.h 3^0x0
(flip:byt 4^idx.h)
==
::
++ keyval-byts
|= kv=keyval:psbt
^- hexb
%- cat:byt
:~ 1^wid.key.kv
key.kv
1^wid.val.kv
val.kv
==
::
++ map-byts
|= m=map:psbt
^- (unit hexb)
?~ m ~
:- ~
%- cat:byt
(turn m keyval-byts)
--
++ base64
|= b=hexb
^- base64:psbt
%- en:base64:mimes:html
(flip:byt b)
:: +encode: make base64 cord of PSBT
:: - only-witness: don't include non-witness UTXO
::
++ encode
|= $: only-witness=?
rawtx=hexb
txid=hexb
inputs=(list in:psbt)
outputs=(list out:psbt)
==
^- base64:psbt
=/ sep=(unit hexb) `1^0x0
=/ final=(list (unit hexb))
%+ join sep
%+ turn
%- zing
:~ ~[(globals:en rawtx)]
(turn inputs (cury input:en only-witness))
(turn outputs output:en)
==
map-byts:en
%- base64:en
^- byts
%- cat:byt
%+ weld ~[[5 0x70.7362.74ff]]
(murn (snoc final sep) same)
::
++ parse
|= psbt-base64=cord
^- (list map:psbt)
=/ todo=hexb
(drop:byt 5 (to-byts psbt-base64))
=| acc=(list map:psbt)
=| m=map:psbt
|-
?: =(wid.todo 0)
(snoc acc m)
:: 0x0: map separator
::
?: =(1^0x0 (take:byt 1 todo))
$(acc (snoc acc m), m *map:psbt, todo (drop:byt 1 todo))
=^ kv todo (next-keyval todo)
$(m (snoc m kv))
:: +get-txid: extract txid from a valid PSBT
::
++ get-txid
|= psbt-base64=cord
^- hexb
=/ tx=hexb
%- raw-tx
%+ drop:byt 5
(to-byts psbt-base64)
%- flip:byt
(dsha256 tx)
:: +raw-tx: extract hex transaction
:: looks for key 0x0 in global map
:: crashes if tx not in hex
::
++ raw-tx
|= b=hexb
^- hexb
|-
?: =(wid.b 0) !!
?: =(1^0x0 (take:byt 1 b)) !!
=/ nk (next-keyval b)
?: =(0x0 dat.key.kv.nk)
val.kv.nk
$(b rest.nk)
:: +next-keyval: returns next key-val in a PSBT map
:: input first byte must be a map key length
::
++ next-keyval
|= b=hexb
^- [kv=keyval:psbt rest=hexb]
=/ klen dat:(take:byt 1 b)
=/ k (take:byt klen (drop:byt 1 b))
=/ vlen dat:(take:byt 1 (drop:byt (add 1 klen) b))
=/ v (take:byt vlen (drop:byt (add 2 klen) b))
?> ?&((gth wid.k 0) (gth wid.v 0))
:- [k v]
(drop:byt ;:(add 2 klen vlen) b)
::
++ to-byts
|= psbt-base64=cord
^- hexb
~| "Invalid PSBT"
=+ p=(de:base64:mimes:html psbt-base64)
?~ p !!
(flip:byt u.p)
--

View File

@ -0,0 +1,234 @@
/- btc-wallet, btc-provider, bitcoin
/+ bl=bitcoin
|%
++ dejs
=, dejs:format
|%
++ command
|= jon=json
^- command:btc-wallet
%. jon
%- of
:~ set-provider+(mu ship)
check-provider+ship
check-payee+ship
set-current-wallet+so
add-wallet+add-wallet
delete-wallet+so
init-payment-external+init-payment-external
init-payment+init-payment
broadcast-tx+so
gen-new-address+|=(json ~)
==
::
++ ship (su ;~(pfix sig fed:ag))
::
++ add-wallet
%- ot
:~ xpub+so
fprint+(at [ni ni ~])
scan-to+(mu (at [ni ni ~]))
max-gap+(mu ni)
confs+(mu ni)
==
::
++ init-payment-external
%- ot
:~ address+address
value+ni
feyb+ni
note+(mu so)
==
::
++ init-payment
%- ot
:~ payee+ship
value+ni
feyb+ni
note+(mu so)
==
::
++ address
|= jon=json
?> ?=([%s @t] jon)
^- address:bitcoin
(from-cord:adr:bl +.jon)
--
::
++ enjs
=, enjs:format
|%
++ status
|= sta=status:btc-provider
^- json
%+ frond -.sta
?- -.sta
%connected (connected sta)
%new-block (new-block sta)
%disconnected ~
==
::
++ connected
|= sta=status:btc-provider
?> ?=(%connected -.sta)
%- pairs
:~ network+s+network.sta
block+(numb block.sta)
fee+?~(fee.sta ~ (numb u.fee.sta))
==
::
++ new-block
|= sta=status:btc-provider
?> ?=(%new-block -.sta)
%- pairs
:~ network+s+network.sta
block+(numb block.sta)
fee+?~(fee.sta ~ (numb u.fee.sta))
blockhash+(hexb blockhash.sta)
blockfilter+(hexb blockfilter.sta)
==
::
++ hexb
|= h=hexb:bitcoin
^- json
%- pairs
:~ wid+(numb:enjs wid.h)
dat+s+(scot %ux dat.h)
==
::
++ update
|= upd=update:btc-wallet
^- json
%+ frond -.upd
?- -.upd
%initial (initial upd)
%change-provider (change-provider upd)
%change-wallet (change-wallet upd)
%psbt (psbt upd)
%btc-state (btc-state btc-state.upd)
%new-tx (hest hest.upd)
%cancel-tx (hexb txid.upd)
%new-address (address address.upd)
%balance (balance balance.upd)
%error s+error.upd
%broadcast-success ~
==
::
++ initial
|= upd=update:btc-wallet
?> ?=(%initial -.upd)
^- json
%- pairs
:~ provider+(provider provider.upd)
wallet+?~(wallet.upd ~ [%s u.wallet.upd])
balance+(balance balance.upd)
history+(history history.upd)
btc-state+(btc-state btc-state.upd)
address+?~(address.upd ~ (address u.address.upd))
==
::
++ change-provider
|= upd=update:btc-wallet
?> ?=(%change-provider -.upd)
^- json
(provider provider.upd)
::
++ change-wallet
|= upd=update:btc-wallet
?> ?=(%change-wallet -.upd)
^- json
%- pairs
:~ wallet+?~(wallet.upd ~ [%s u.wallet.upd])
balance+(balance balance.upd)
history+(history history.upd)
==
::
++ psbt
|= upd=update:btc-wallet
?> ?=(%psbt -.upd)
^- json
%- pairs
:~ pb+s+pb.upd
fee+(numb fee.upd)
==
::
++ balance
|= b=(unit [p=@ q=@])
^- json
?~ b ~
%- pairs
:~ confirmed+(numb p.u.b)
unconfirmed+(numb q.u.b)
==
::
++ btc-state
|= bs=btc-state:btc-wallet
^- json
%- pairs
:~ block+(numb block.bs)
fee+?~(fee.bs ~ (numb u.fee.bs))
date+(sect t.bs)
==
::
++ provider
|= p=(unit provider:btc-wallet)
^- json
?~ p ~
%- pairs
:~ host+(ship host.u.p)
connected+b+connected.u.p
==
::
++ history
|= hy=history:btc-wallet
^- json
:- %o
^- (map @t json)
%- ~(rep by hy)
|= [[=txid:btc-wallet h=hest:btc-wallet] out=(map @t json)]
^- (map @t json)
(~(put by out) (scot %ux dat.txid) (hest h))
::
++ hest
|= h=hest:btc-wallet
^- json
%- pairs
:~ xpub+s+xpub.h
txid+(hexb txid.h)
confs+(numb confs.h)
recvd+?~(recvd.h ~ (sect u.recvd.h))
inputs+(vals inputs.h)
outputs+(vals outputs.h)
note+?~(note.h ~ [%s u.note.h])
==
::
++ vals
|= vl=(list [=val:tx:bitcoin s=(unit @p)])
^- json
:- %a
%+ turn vl
|= [v=val:tx:bitcoin s=(unit @p)]
%- pairs
:~ val+(val v)
ship+?~(s ~ (ship u.s))
==
::
++ val
|= v=val:tx:bitcoin
^- json
%- pairs
:~ txid+(hexb txid.v)
pos+(numb pos.v)
address+(address address.v)
value+(numb value.v)
==
::
++ address
|= a=address:bitcoin
^- json
?- -.a
%base58 [%s (rsh [3 2] (scot %uc +.a))]
%bech32 [%s +.a]
==
--
--

View File

@ -0,0 +1,166 @@
:: lib/bitcoin-utils.hoon
:: Utilities for working with BTC data types and transactions
::
/- sur=bitcoin
=, sur
|%
::
:: TODO: move this bit/byt stuff to zuse
:: bit/byte utilities
::
::
:: +blop: munge bit and byt sequences (cat, flip, take, drop)
::
++ blop
|_ =bloq
+$ biyts [wid=@ud dat=@]
++ cat
|= bs=(list biyts)
^- biyts
:- (roll (turn bs |=(b=biyts -.b)) add)
(can bloq (flop bs))
:: +flip: flip endianness while preserving lead/trail zeroes
::
++ flip
|= b=biyts
^- biyts
[wid.b (rev bloq b)]
:: +take: take n bloqs from front
:: pads front with extra zeroes if n is longer than input
::
++ take
|= [n=@ b=biyts]
^- biyts
?: (gth n wid.b)
[n dat.b]
[n (rsh [bloq (sub wid.b n)] dat.b)]
:: +drop: drop n bloqs from front
:: returns 0^0 if n >= width
::
++ drop
|= [n=@ b=biyts]
^- biyts
?: (gte n wid.b)
0^0x0
=+ n-take=(sub wid.b n)
[n-take (end [bloq n-take] dat.b)]
--
++ byt ~(. blop 3)
::
++ bit
=/ bl ~(. blop 0)
|%
++ cat cat:bl:bit
++ flip flip:bl:bit
++ take take:bl:bit
++ drop drop:bl:bit
++ from-atoms
|= [bitwidth=@ digits=(list @)]
^- bits
%- cat:bit
%+ turn digits
|= a=@
?> (lte (met 0 a) bitwidth)
[bitwidth `@ub`a]
:: +to-atoms: convert bits to atoms of bitwidth
::
++ to-atoms
|= [bitwidth=@ bs=bits]
^- (list @)
=| res=(list @)
?> =(0 (mod wid.bs bitwidth))
|-
?: =(0 wid.bs) res
%= $
res (snoc res dat:(take:bit bitwidth bs))
bs (drop:bit bitwidth bs)
==
--
:: big endian sha256: input and output are both MSB first (big endian)
::
++ sha256
|= =byts
^- hexb
%- flip:byt
[32 (shay (flip:byt byts))]
::
++ dsha256
|= =byts
(sha256 (sha256 byts))
::
++ hash-160
|= val=byts
^- hexb
=, ripemd:crypto
:- 20
%- ripemd-160
(sha256 val)
::
:: hxb: hex parsing utilities
::
++ hxb
|%
++ from-cord
|= h=@t
^- hexb
?: =('' h) 1^0x0
:: Add leading 00
::
=+ (lsh [3 2] h)
:: Group by 4-size block
::
=+ (rsh [3 2] -)
:: Parse hex to atom
::
:- (div (lent (trip h)) 2)
`@ux`(rash - hex)
::
++ to-cord
|= =hexb
^- cord
(en:base16:mimes:html hexb)
--
::
:: +csiz: CompactSize integers (a Bitcoin-specific datatype)
:: https://btcinformation.org/en/developer-reference#compactsize-unsigned-integers
:: - encode: big endian to little endian
:: - decode: little endian to big endian
::
++ csiz
|%
++ en
|= a=@
^- hexb
=/ l=@ (met 3 a)
?: =(l 1) 1^a
?: =(l 2) (cat:byt ~[1^0xfd (flip:byt 2^a)])
?: (lte l 4) (cat:byt ~[1^0xfe (flip:byt 4^a)])
?: (lte l 8) (cat:byt ~[1^0xff (flip:byt 8^a)])
~|("Cannot encode CompactSize longer than 8 bytes" !!)
::
++ de
|= h=hexb
^- [n=hexb rest=hexb]
=/ s=@ux dat:(take:byt 1 h)
?: (lth s 0xfd) [1^s (drop:byt 1 h)]
~| "Invalid compact-size at start of {<h>}"
=/ len=bloq
?+ s !!
%0xfd 1
%0xfe 2
%0xff 3
==
:_ (drop:byt (add 1 len) h)
%- flip:byt
(take:byt (bex len) (drop:byt 1 h))
:: +dea: atom instead of hexb for parsed CompactSize
::
++ dea
|= h=hexb
^- [a=@ rest=hexb]
=> (de h)
[dat.n rest]
--
::
--

286
pkg/arvo/lib/bitcoin.hoon Normal file
View File

@ -0,0 +1,286 @@
:: bitcoin.hoon
:: top-level Bitcoin constants
:: expose BIP libraries
::
/- sur=bitcoin
/+ bech32=bip-b173, pbt=bip-b174, bcu=bitcoin-utils
=, sur
=, bcu
|%
++ overhead-weight ^-(vbytes 11)
++ input-weight
|= =bipt
^- vbytes
?- bipt
%44 148
%49 91
%84 68
==
++ output-weight
|= =bipt
^- vbytes
?- bipt
%44 34
%49 32
%84 31
==
::
++ xpub-type
|= =xpub
^- [=bipt =network]
=/ prefix=tape (scag 4 (trip xpub))
?: =("tpub" prefix) [%44 %testnet]
?: =("upub" prefix) [%49 %testnet]
?: =("vpub" prefix) [%84 %testnet]
?: =("xpub" prefix) [%44 %main]
?: =("ypub" prefix) [%49 %main]
?: =("zpub" prefix) [%84 %main]
~|("invalid xpub: {<xpub>}" !!)
::
:: adr: address manipulation
::
++ adr
|%
++ get-bipt
|= a=address
^- bipt
=/ spk=hexb (to-script-pubkey:adr a)
?: =(25 wid.spk) %44
?: =(23 wid.spk) %49
?: =(22 wid.spk) %84
?: =(34 wid.spk) %84
~|("Invalid address" !!)
::
++ to-cord
|= a=address ^- cord
?: ?=([%base58 *] a)
(scot %uc +.a)
+.a
::
++ from-pubkey
|= [=bipt =network pubkey=hexb]
^- address
?- bipt
%44
:- %base58
=< ^-(@uc dat)
%- cat:byt
:- ?- network
%main 1^0x0
%testnet 1^0x6f
==
~[(hash-160 pubkey)]
::
%49
:- %base58
=< ^-(@uc dat)
%- cat:byt
:~ ?- network
%main 1^0x5
%testnet 1^0xc4
==
%- hash-160
(cat:byt ~[2^0x14 (hash-160 pubkey)])
==
::
%84
:- %bech32
(need (encode-pubkey:bech32 network pubkey))
==
::
++ from-cord
|= addrc=@t
|^
=/ addrt=tape (trip addrc)
^- address
?: (is-base58 addrt)
[%base58 `@uc`(scan addrt fim:ag)]
?: (is-bech32 addrt)
[%bech32 addrc]
~|("Invalid address: {<addrc>}" !!)
::
++ is-base58
|= at=tape
^- ?
?| =("m" (scag 1 at))
=("1" (scag 1 at))
=("3" (scag 1 at))
=("2" (scag 1 at))
==
::
++ is-bech32
|= at=tape
^- ?
?| =("bc1" (scag 3 at))
=("tb1" (scag 3 at))
==
--
::
++ to-script-pubkey
|= =address
^- hexb
?- -.address
%bech32
=+ h=(from-address:bech32 +.address)
%- cat:byt
:~ 1^0x0
1^wid.h
h
==
::
%base58
=/ h=hexb [21 `@ux`+.address]
=+ lead-byt=dat:(take:byt 1 h)
=/ version-network=[bipt network]
?: =(0x0 lead-byt) [%44 %main]
?: =(0x6f lead-byt) [%44 %testnet]
?: =(0x5 lead-byt) [%49 %main]
?: =(0xc4 lead-byt) [%49 %testnet]
~|("Invalid base58 address: {<+.address>}" !!)
%- cat:byt
?: ?=(%44 -.version-network)
:~ 3^0x76.a914
(drop:byt 1 h)
2^0x88ac
==
:~ 2^0xa914
(drop:byt 1 h)
1^0x87
==
==
--
::
:: +txu: transaction utility core
:: - primarily used for calculating txids
:: - ignores signatures in inputs
::
++ txu
|%
++ en
|%
++ input
|= i=input:tx
^- hexb
%- cat:byt
:~ (flip:byt txid.i)
(flip:byt 4^pos.i)
?~ script-sig.i 1^0x0
%- cat:byt
~[(en:csiz wid.u.script-sig.i) u.script-sig.i]
(flip:byt sequence.i)
==
::
++ output
|= o=output:tx
^- hexb
%- cat:byt
:~ (flip:byt 8^value.o)
1^wid.script-pubkey.o
script-pubkey.o
==
--
::
++ de
|%
++ nversion
|= b=hexb
^- [nversion=@ud rest=hexb]
:- dat:(flip:byt (take:byt 4 b))
(drop:byt 4 b)
::
++ segwit
|= b=hexb
^- [segwit=(unit @ud) rest=hexb]
?. =(1^0x0 (take:byt 1 b))
[~ b]
:- [~ dat:(take:byt 2 b)]
(drop:byt 2 b)
::
++ script-sig
|= b=hexb
^- [sig=hexb rest=hexb]
=^ siglen=hexb b (de:csiz b)
:- (take:byt dat.siglen b)
(drop:byt dat.siglen b)
::
++ sequence
|= b=hexb
^- [seq=hexb rest=hexb]
[(flip:byt (take:byt 4 b)) (drop:byt 4 b)]
::
++ inputs
|= b=hexb
^- [is=(list input:tx) rest=hexb]
|^
=| acc=(list input:tx)
=^ count b (dea:csiz b)
|-
?: =(0 count) [acc b]
=^ i b (input b)
$(acc (snoc acc i), count (dec count))
::
++ input
|= b=hexb
^- [i=input:tx rest=hexb]
=/ txid (flip:byt (take:byt 32 b))
=/ pos dat:(flip:byt (take:byt 4 (drop:byt 32 b)))
=^ sig=hexb b (script-sig (drop:byt 36 b))
=^ seq=hexb b (sequence b)
:_ b
[txid pos seq ?:((gth wid.sig 0) `sig ~) ~ 0]
--
::
++ outputs
|= b=hexb
^- [os=(list output:tx) rest=hexb]
=| acc=(list output:tx)
=^ count b (dea:csiz b)
|-
?: =(0 count) [acc b]
=/ value (flip:byt (take:byt 8 b))
=^ scriptlen b (dea:csiz (drop:byt 8 b))
%= $
acc %+ snoc acc
:- (take:byt scriptlen b)
dat.value
b (drop:byt scriptlen b)
count (dec count)
==
--
:: +basic-encode: encodes data in a format suitable for hashing
::
++ basic-encode
|= =data:tx
^- hexb
%- cat:byt
%- zing
:~ ~[(flip:byt 4^nversion.data)]
~[(en:csiz (lent is.data))]
(turn is.data input:en)
~[(en:csiz (lent os.data))]
(turn os.data output:en)
~[(flip:byt 4^locktime.data)]
==
++ get-id
|= =data:tx
^- hexb
%- flip:byt
%- dsha256
(basic-encode data)
::
++ decode
|= b=hexb
^- data:tx
=^ nversion b
(nversion:de b)
=^ segwit b
(segwit:de b)
=^ inputs b
(inputs:de b)
=^ outputs b
(outputs:de b)
=/ locktime=@ud
dat:(take:byt 4 (flip:byt b))
[inputs outputs locktime nversion segwit]
--
--

View File

@ -0,0 +1,209 @@
/- bp=btc-provider, json-rpc
/+ bc=bitcoin
^?
::=< [sur .]
::=, sur
|%
:: +from-epoch: time since Jan 1, 1970 in seconds.
::
++ from-epoch
|= secs=@ud
^- (unit @da)
?: =(0 secs) ~
[~ (add ~1970.1.1 `@dr`(mul secs ~s1))]
::
++ get-request
|= url=@t
^- request:http
[%'GET' url ~ ~]
::
++ post-request
|= [url=@t body=json]
^- request:http
:* %'POST'
url
~[['Content-Type' 'application/json']]
=, html
%- some
%- as-octt:mimes
(en-json body)
==
::
++ gen-request
|= [=host-info:bp ract=action:rpc-types:bp]
^- request:http
%+ rpc-action-to-http
api-url.host-info ract
::
++ rpc
=, dejs:format
|%
++ parse-result
|= res=response:json-rpc
|^ ^- result:rpc-types:bp
~| -.res
?> ?=(%result -.res)
?+ id.res ~|([%unsupported-result id.res] !!)
%get-address-info
[id.res (address-info res.res)]
::
%get-tx-vals
[id.res (tx-vals res.res)]
::
%get-raw-tx
[id.res (raw-tx res.res)]
::
%broadcast-tx
[%broadcast-tx (broadcast-tx res.res)]
::
%get-block-count
[id.res (ni res.res)]
::
%get-block-info
[id.res (block-info res.res)]
==
++ address-info
%- ot
:~ [%address (cu from-cord:adr:bc so)]
[%utxos (as utxo)]
[%used bo]
[%block ni]
==
++ utxo
%- ot
:~ ['tx_pos' ni]
['tx_hash' (cu from-cord:hxb:bc so)]
[%height ni]
[%value ni]
[%recvd (cu from-epoch ni)]
==
++ tx-vals
%- ot
:~ [%included bo]
[%txid (cu from-cord:hxb:bc so)]
[%confs ni]
[%recvd (cu from-epoch ni)]
[%inputs (ar tx-val)]
[%outputs (ar tx-val)]
==
++ tx-val
%- ot
:~ [%txid (cu from-cord:hxb:bc so)]
[%pos ni]
[%address (cu from-cord:adr:bc so)]
[%value ni]
==
++ raw-tx
%- ot
:~ [%txid (cu from-cord:hxb:bc so)]
[%rawtx (cu from-cord:hxb:bc so)]
==
++ broadcast-tx
%- ot
:~ [%txid (cu from-cord:hxb:bc so)]
[%broadcast bo]
[%included bo]
==
++ block-info
%- ot
:~ [%block ni]
[%fee (mu ni)]
[%blockhash (cu from-cord:hxb:bc so)]
[%blockfilter (cu from-cord:hxb:bc so)]
==
--
--
::
++ rpc-action-to-http
|= [endpoint=@t ract=action:rpc-types:bp]
|^ ^- request:http
?- -.ract
%get-address-info
%- get-request
%+ mk-url '/addresses/info/'
(to-cord:adr:bc address.ract)
::
%get-tx-vals
%- get-request
%+ mk-url '/gettxvals/'
(to-cord:hxb:bc txid.ract)
::
%get-raw-tx
%- get-request
%+ mk-url '/getrawtx/'
(to-cord:hxb:bc txid.ract)
::
%broadcast-tx
%- get-request
%+ mk-url '/broadcasttx/'
(to-cord:hxb:bc rawtx.ract)
::
%get-block-count
%- get-request
(mk-url '/getblockcount' '')
::
%get-block-info
%- get-request
(mk-url '/getblockinfo' '')
==
++ mk-url
|= [base=@t params=@t]
%^ cat 3
(cat 3 endpoint base) params
--
:: RPC/HTTP Utilities
::
++ httr-to-rpc-response
|= hit=httr:eyre
^- response:json-rpc
~| hit
=/ jon=json (need (de-json:html q:(need r.hit)))
?. =(%2 (div p.hit 100))
(parse-rpc-error jon)
=, dejs-soft:format
^- response:json-rpc
=; dere
=+ res=((ar dere) jon)
?~ res (need (dere jon))
[%batch u.res]
|= jon=json
^- (unit response:json-rpc)
=/ res=[id=(unit @t) res=(unit json) err=(unit json)]
%. jon
=, dejs:format
=- (ou -)
:~ ['id' (uf ~ (mu so))]
['result' (uf ~ (mu same))]
['error' (uf ~ (mu same))]
==
?: ?=([^ * ~] res)
`[%result [u.id.res ?~(res.res ~ u.res.res)]]
~| jon
`(parse-rpc-error jon)
::
++ get-rpc-response
|= response=client-response:iris
^- response:json-rpc
?> ?=(%finished -.response)
%- httr-to-rpc-response
%+ to-httr:iris
response-header.response
full-file.response
::
++ parse-rpc-error
|= =json
^- response:json-rpc
:- %error
?~ json ['' '' '']
%. json
=, dejs:format
=- (ou -)
:~ =- ['id' (uf '' (cu - (mu so)))]
|*(a=(unit) ?~(a '' u.a))
:- 'error'
=- (uf ['' ''] -)
=- (cu |*(a=(unit) ?~(a ['' ''] u.a)) (mu (ou -)))
:~ ['code' (uf '' no)]
['message' (uf '' so)]
== ==
--

571
pkg/arvo/lib/btc.hoon Normal file
View File

@ -0,0 +1,571 @@
:: lib/btc.hoon
::
/- *btc-wallet, json-rpc, bp=btc-provider
/+ bip32, bc=bitcoin
=, secp:crypto
=+ ecc=secp256k1
|%
::
:: Formerly lib/btc-wallet.hoon
::
::
++ defaults
|%
++ max-gap 20
++ confs 6
--
:: +fam: planet parent if s is a moon
::
++ fam
|= [our=ship now=@da s=ship]
^- ship
?. =(%earl (clan:title s)) s
(sein:title our now s)
::
++ num-confs
|= [last-block=@ud =utxo:bc]
?: =(0 height.utxo) 0
(add 1 (sub last-block height.utxo))
::
++ from-xpub
|= $: =xpub:bc
=fprint:bc
scan-to=(unit scon)
max-gap=(unit @ud)
confs=(unit @ud)
==
^- walt
=/ [=bipt =network] (xpub-type:bc xpub)
:* xpub
network
fprint
(from-extended:bip32 (trip xpub))
bipt
*wach
[0 0]
%.n
(fall scan-to *scon)
(fall max-gap max-gap:defaults)
(fall confs confs:defaults)
==
:: +address-coords: find wallet info for the address, if any
::
++ address-coords
|= [a=address ws=(list walt)]
^- (unit [w=walt =chyg =idx])
|^
|- ?~ ws ~
=/ res=(unit [=chyg =idx])
(lookup i.ws)
?^ res `[i.ws chyg.u.res idx.u.res]
$(ws t.ws)
::
++ lookup
|= w=walt
^- (unit [=chyg =idx])
=/ ad=(unit addi) (~(get by wach.w) a)
?~(ad ~ `[chyg.u.ad idx.u.ad])
--
::
++ new-txbu
|= $: w=walt
payee=(unit ship)
=vbytes:bc
is=(list insel)
txos=(list txo)
==
^- txbu
:* xpub.w
payee
vbytes
%+ turn is
|= i=insel
[utxo.i ~ (~(hdkey wad w chyg.i) idx.i)]
txos
~
==
:: txb: transaction builder helpers
::
++ txb
|_ t=txbu
++ value
^- [in=sats out=sats]
:- %+ roll
%+ turn txis.t
|=(=txi value.utxo.txi)
add
(roll (turn txos.t |=(=txo value.txo)) add)
::
++ fee
^- sats:bc
=/ [in=sats out=sats] value
(sub in out)
::
++ vbytes
^- vbytes:bc
%+ add overhead-weight:bc
%+ add
%+ roll
(turn txis.t |=(t=txi (input-weight:bc bipt.hdkey.t)))
add
%+ roll
(turn txos.t |=(t=txo (output-weight:bc (get-bipt:adr:bc address.t))))
add
++ tx-data
|^
^- data:tx:bc
:* (turn txis.t txi-data)
(turn txos.t txo-data)
0 1 `1
==
::
++ txi-data
|= =txi
:* txid.utxo.txi pos.utxo.txi
4^0xffff.ffff ~ ~ value.utxo.txi
==
++ txo-data
|= =txo
:- (to-script-pubkey:adr:bc address.txo)
value.txo
--
::
++ get-txid
^- txid
(get-id:txu:bc tx-data)
::
++ get-rawtx
(basic-encode:txu:bc tx-data)
:: +add-output: append output (usually change) to txos
::
++ add-output
|= =txo
^- txbu
:: todo update vbytes
t(txos (snoc [txos.t] txo))
:: +to-psbt: returns a based 64 PSBT if
:: - all inputs have an associated rawtx
::
++ to-psbt
^- (unit base64:psbt:bc)
=/ ins=(list in:psbt:bc)
%+ murn txis.t
|= =txi
?~ rawtx.txi ~
`[utxo.txi u.rawtx.txi hdkey.txi]
?: (lth (lent ins) (lent txis.t))
~
=/ outs=(list out:psbt:bc)
%+ turn txos.t
|=(=txo [address.txo hk.txo])
`(encode:pbt:bc %.y get-rawtx get-txid ins outs)
--
:: wad: door for processing walts (wallets)
:: parameterized on a walt and it's chyg account
::
++ wad
|_ [w=walt =chyg]
++ pubkey
|= =idx:bc
^- hexb:bc
=/ pk=@ux
%- compress-point:ecc
pub:(derive-public:(derive-public:wilt.w (@ chyg)) idx)
[(met 3 pk) pk]
::
++ hdkey
|= =idx:bc
^- hdkey:bc
[fprint.w (~(pubkey wad w chyg) idx) network.w bipt.w chyg idx]
::
++ mk-address
|= =idx:bc
^- address:bc
(from-pubkey:adr:bc bipt.w network.w (pubkey idx))
:: +nixt-address: used to get change addresses
:: - gets the current next available address
:: - doesn't bump nixt-address if it's unused
:: - if used, fall back to gen-address and make a new one
::
++ nixt-address
^- (trel address:bc idx:bc walt)
=/ addr (mk-address nixt-idx)
~| "lib/btc-wallet-store: get-next-address: nixt shouldn't be blank"
=/ =addi (~(got by wach.w) addr)
?. used.addi
[addr nixt-idx w]
gen-address
::
:: +gen-address:
:: - generates the next available address
:: - watches it (using update address)
::
++ gen-address
^- (trel address:bc idx:bc walt)
=/ addr (mk-address nixt-idx)
:* addr
nixt-idx
%+ update-address addr
[%.n chyg nixt-idx *(set utxo:bc)]
==
:: +update-address
:: - insert a new address
:: - if it's used, move "nixt" to the next free address
:: - watch address
::
++ update-address
|= [a=address:bc =addi]
^- walt
?> =(chyg chyg.addi)
?> =(a (mk-address idx.addi))
=? w ?&(used.addi (is-nixt addi))
bump-nixt
w(wach (~(put by wach.w) a addi))
::
++ is-nixt
|= =addi ^- ?
?: ?=(%0 chyg.addi)
=(idx.addi p.nixt.w)
=(idx.addi q.nixt.w)
++ nixt-idx
?:(?=(%0 chyg) p.nixt.w q.nixt.w)
:: +bump-nixt: return wallet with bumped nixt
:: - find next unused address
:: - watches that address
:: - crashes if max-index is passed
::
++ bump-nixt
|^ ^- walt
=/ new-idx=idx:bc +(nixt-idx)
|- ?> (lte new-idx max-index)
=+ addr=(mk-address new-idx)
=/ =addi
%+ ~(gut by wach.w) addr
[%.n chyg new-idx *(set utxo:bc)]
?. used.addi
%= w
nixt (set-nixt new-idx)
wach (~(put by wach.w) addr addi)
==
$(new-idx +(new-idx))
::
++ set-nixt
|= =idx:bc ^- nixt
?:(?=(%0 chyg) [idx q.nixt.w] [p.nixt.w idx])
--
--
:: sut: select utxos
::
++ sut
|_ [w=walt eny=@uvJ last-block=@ud payee=(unit ship) =feyb txos=(list txo)]
++ dust-sats 3
++ dust-threshold
|= output-bipt=bipt:bc
^- vbytes
(mul dust-sats (input-weight:bc output-bipt))
::
++ target-value
^- sats
%+ roll (turn txos |=(=txo value.txo))
|=([a=sats b=sats] (add a b))
::
++ base-weight
^- vbytes
%+ add overhead-weight:bc
%+ roll
%+ turn txos
|=(=txo (output-weight:bc (get-bipt:adr:bc address.txo)))
add
::
++ total-vbytes
|= selected=(list insel)
^- vbytes
%+ add base-weight
(mul (input-weight:bc bipt.w) (lent selected))
:: value of an input after fee
:: 0 if net is <= 0
::
++ net-value
|= val=sats
^- sats
=/ cost (mul (input-weight:bc bipt.w) feyb)
?: (lte val cost) 0
(sub val cost)
::
:: +spendable: whether utxo has enough confs to spend
::
++ spendable
|= =utxo:bc ^- ?
(gte (num-confs last-block utxo) confs.w)
:: +with-change:
:: - choose UTXOs, if there are enough
:: - return txbu and amount of change (if any)
::
++ with-change
^- [tb=(unit txbu) chng=(unit sats)]
=/ tb=(unit txbu) select-utxos
?~ tb [~ ~]
=+ excess=~(fee txb u.tb) :: (inputs - outputs)
=/ new-fee=sats :: cost of this tx + one more output
(mul feyb (add (output-weight:bc bipt.w) vbytes.u.tb))
?. (gth excess new-fee)
[tb ~]
?. (gth (sub excess new-fee) (dust-threshold bipt.w))
[tb ~]
:- tb
`(sub excess new-fee)
:: Uses naive random selection. Should switch to branch-and-bound later.
::
++ select-utxos
|^ ^- (unit txbu)
?. %+ levy txos
|= =txo
%+ gth value.txo
(dust-threshold (get-bipt:adr:bc address.txo))
~|("One or more suggested outputs is dust." !!)
=/ is=(unit (list insel))
%- single-random-draw
%- zing
(turn ~(val by wach.w) to-insels)
?~ is ~
`(new-txbu w payee (total-vbytes u.is) u.is txos)
::
++ to-insels
|= =addi
^- (list insel)
%+ turn ~(tap in utxos.addi)
|=(=utxo:bc [utxo chyg.addi idx.addi])
--
:: single-random-draw
:: randomly choose utxos until target is hit
:: only use an insel if its net-value > 0
::
++ single-random-draw
|= is=(list insel)
^- (unit (list insel))
=/ rng ~(. og eny)
=/ target (add target-value (mul feyb base-weight)) :: add base fees to target
=| [select=(list insel) total=sats:bc]
|-
?: =(~ is) ~
=^ n rng (rads:rng (lent is))
=/ i=insel (snag n is)
?. (spendable utxo.i)
$(is (oust [n 1] is))
=/ net-val (net-value value.utxo.i)
=? select (gth net-val 0)
[i select]
=/ new-total (add total net-val)
?: (gte new-total target) `select
%= $
is (oust [n 1] is)
total new-total
==
::
--
::
::
:: Formerly lib/btc-provider
::
::
++ from-epoch
|= secs=@ud
^- (unit @da)
?: =(0 secs) ~
[~ (add ~1970.1.1 `@dr`(mul secs ~s1))]
::
++ get-request
|= url=@t
^- request:http
[%'GET' url ~ ~]
::
++ post-request
|= [url=@t body=json]
^- request:http
:* %'POST'
url
~[['Content-Type' 'application/json']]
=, html
%- some
%- as-octt:mimes
(en-json body)
==
::
++ gen-request
|= [=host-info:bp ract=action:rpc-types:bp]
^- request:http
%+ rpc-action-to-http
api-url.host-info ract
::
++ rpc
=, dejs:format
|%
++ parse-result
|= res=response:json-rpc
|^ ^- result:rpc-types:bp
~| -.res
?> ?=(%result -.res)
?+ id.res ~|([%unsupported-result id.res] !!)
%get-address-info
[id.res (address-info res.res)]
::
%get-tx-vals
[id.res (tx-vals res.res)]
::
%get-raw-tx
[id.res (raw-tx res.res)]
::
%broadcast-tx
[%broadcast-tx (broadcast-tx res.res)]
::
%get-block-count
[id.res (ni res.res)]
::
%get-block-info
[id.res (block-info res.res)]
==
++ address-info
%- ot
:~ [%address (cu from-cord:adr:bc so)]
[%utxos (as utxo)]
[%used bo]
[%block ni]
==
++ utxo
%- ot
:~ ['tx_pos' ni]
['tx_hash' (cu from-cord:hxb:bc so)]
[%height ni]
[%value ni]
[%recvd (cu from-epoch ni)]
==
++ tx-vals
%- ot
:~ [%included bo]
[%txid (cu from-cord:hxb:bc so)]
[%confs ni]
[%recvd (cu from-epoch ni)]
[%inputs (ar tx-val)]
[%outputs (ar tx-val)]
==
++ tx-val
%- ot
:~ [%txid (cu from-cord:hxb:bc so)]
[%pos ni]
[%address (cu from-cord:adr:bc so)]
[%value ni]
==
++ raw-tx
%- ot
:~ [%txid (cu from-cord:hxb:bc so)]
[%rawtx (cu from-cord:hxb:bc so)]
==
++ broadcast-tx
%- ot
:~ [%txid (cu from-cord:hxb:bc so)]
[%broadcast bo]
[%included bo]
==
++ block-info
%- ot
:~ [%block ni]
[%fee (mu ni)]
[%blockhash (cu from-cord:hxb:bc so)]
[%blockfilter (cu from-cord:hxb:bc so)]
==
--
--
::
++ rpc-action-to-http
|= [endpoint=@t ract=action:rpc-types:bp]
|^ ^- request:http
?- -.ract
%get-address-info
%- get-request
%+ mk-url '/addresses/info/'
(to-cord:adr:bc address.ract)
::
%get-tx-vals
%- get-request
%+ mk-url '/gettxvals/'
(to-cord:hxb:bc txid.ract)
::
%get-raw-tx
%- get-request
%+ mk-url '/getrawtx/'
(to-cord:hxb:bc txid.ract)
::
%broadcast-tx
%- get-request
%+ mk-url '/broadcasttx/'
(to-cord:hxb:bc rawtx.ract)
::
%get-block-count
%- get-request
(mk-url '/getblockcount' '')
::
%get-block-info
%- get-request
(mk-url '/getblockinfo' '')
==
++ mk-url
|= [base=@t params=@t]
%^ cat 3
(cat 3 endpoint base) params
--
:: RPC/HTTP Utilities
::
++ httr-to-rpc-response
|= hit=httr:eyre
^- response:json-rpc
~| hit
=/ jon=json (need (de-json:html q:(need r.hit)))
?. =(%2 (div p.hit 100))
(parse-rpc-error jon)
=, dejs-soft:format
^- response:json-rpc
=; dere
=+ res=((ar dere) jon)
?~ res (need (dere jon))
[%batch u.res]
|= jon=json
^- (unit response:json-rpc)
=/ res=[id=(unit @t) res=(unit json) err=(unit json)]
%. jon
=, dejs:format
=- (ou -)
:~ ['id' (uf ~ (mu so))]
['result' (uf ~ (mu same))]
['error' (uf ~ (mu same))]
==
?: ?=([^ * ~] res)
`[%result [u.id.res ?~(res.res ~ u.res.res)]]
~| jon
`(parse-rpc-error jon)
::
++ get-rpc-response
|= response=client-response:iris
^- response:json-rpc
?> ?=(%finished -.response)
%- httr-to-rpc-response
%+ to-httr:iris
response-header.response
full-file.response
::
++ parse-rpc-error
|= =json
^- response:json-rpc
:- %error
?~ json ['' '' '']
%. json
=, dejs:format
=- (ou -)
:~ =- ['id' (uf '' (cu - (mu so)))]
|*(a=(unit) ?~(a '' u.a))
:- 'error'
=- (uf ['' ''] -)
=- (cu |*(a=(unit) ?~(a ['' ''] u.a)) (mu (ou -)))
:~ ['code' (uf '' no)]
['message' (uf '' so)]
== ==
--

View File

@ -71,7 +71,12 @@
[%'linkedUrl' s+linked-url.type]
==
::
%custom (frond %custom ~)
%custom
%+ frond %custom
%- pairs
:~ [%'linkedUrl' ?~(linked-url.type ~ s+u.linked-url.type)]
[%'image' ?~(image.type ~ s+u.image.type)]
==
==
::
++ terms
@ -105,10 +110,10 @@
[%'isShown' bo]
==
::
++ tile-type
++ tile-type
%- of
:~ [%basic basic]
[%custom ul]
[%custom (ot [%'linkedUrl' (mu so)] [%'image' (mu so)] ~)]
==
::
++ basic

View File

@ -106,6 +106,13 @@
^- simple-payload:http
:_ `octs
[200 [['content-type' 'image/png'] ?:(cache [max-1-wk ~] ~)]]
::
++ svg-response
=| cache=?
|= =octs
^- simple-payload:http
:_ `octs
[200 [['content-type' 'image/svg+xml'] ?:(cache [max-1-wk ~] ~)]]
::
++ ico-response
|= =octs

View File

@ -0,0 +1,12 @@
/- *btc-provider
|_ act=action
++ grad %noun
++ grow
|%
++ noun act
--
++ grab
|%
++ noun action
--
--

View File

@ -0,0 +1,14 @@
/- *btc-provider
/+ bitcoin-json
|_ sta=status
++ grad %noun
++ grow
|%
++ noun sta
++ json (status:enjs:bitcoin-json sta)
--
++ grab
|%
++ noun status
--
--

View File

@ -0,0 +1,12 @@
/- *btc-provider
|_ upd=update
++ grad %noun
++ grow
|%
++ noun upd
--
++ grab
|%
++ noun update
--
--

View File

@ -0,0 +1,12 @@
/- *btc-wallet
|_ act=action
++ grad %noun
++ grow
|%
++ noun act
--
++ grab
|%
++ noun action
--
--

View File

@ -0,0 +1,14 @@
/- *btc-wallet
/+ bitcoin-json
|_ com=command
++ grad %noun
++ grow
|%
++ noun com
--
++ grab
|%
++ noun command
++ json command:dejs:bitcoin-json
--
--

View File

@ -0,0 +1,12 @@
/- *btc-wallet
|_ intr=internal
++ grad %noun
++ grow
|%
++ noun intr
--
++ grab
|%
++ noun internal
--
--

View File

@ -0,0 +1,14 @@
/- *btc-wallet
/+ bitcoin-json
|_ upd=update
++ grad %noun
++ grow
|%
++ noun upd
++ json (update:enjs:bitcoin-json upd)
--
++ grab
|%
++ noun update
--
--

12
pkg/arvo/mar/svg.hoon Normal file
View File

@ -0,0 +1,12 @@
|_ dat=@
++ grow
|%
++ mime [/image/'svg+xml' (as-octs:mimes:html dat)]
--
++ grab
|%
++ mime |=([p=mite q=octs] q.q)
++ noun @
--
++ grad %mime
--

84
pkg/arvo/sur/bitcoin.hoon Normal file
View File

@ -0,0 +1,84 @@
:: sur/btc.hoon
:: Utilities for working with BTC data types and transactions
::
:: chyg: whether account is (non-)change. 0 or 1
:: bytc: "btc-byts" with dat cast to @ux
|%
+$ network ?(%main %testnet)
+$ hexb [wid=@ dat=@ux] :: hex byts
+$ bits [wid=@ dat=@ub]
+$ xpub @ta
+$ address
$% [%base58 @uc]
[%bech32 cord]
==
+$ fprint hexb
+$ bipt $?(%44 %49 %84)
+$ chyg $?(%0 %1)
+$ idx @ud
+$ hdkey [=fprint pubkey=hexb =network =bipt =chyg =idx]
+$ sats @ud
+$ vbytes @ud
+$ txid hexb
+$ utxo [pos=@ =txid height=@ value=sats recvd=(unit @da)]
++ address-info
$: =address
confirmed-value=sats
unconfirmed-value=sats
utxos=(set utxo)
==
++ tx
|%
+$ data
$: is=(list input)
os=(list output)
locktime=@ud
nversion=@ud
segwit=(unit @ud)
==
+$ val
$: =txid
pos=@ud
=address
value=sats
==
:: included: whether tx is in the mempool or blockchain
::
+$ info
$: included=?
=txid
confs=@ud
recvd=(unit @da)
inputs=(list val)
outputs=(list val)
==
+$ input
$: =txid
pos=@ud
sequence=hexb
script-sig=(unit hexb)
pubkey=(unit hexb)
value=sats
==
+$ output
$: script-pubkey=hexb
value=sats
==
--
++ psbt
|%
+$ base64 cord
+$ in [=utxo rawtx=hexb =hdkey]
+$ out [=address hk=(unit hdkey)]
+$ target $?(%input %output)
+$ keyval [key=hexb val=hexb]
+$ map (list keyval)
--
++ ops
|%
++ op-dup 118
++ op-equalverify 136
++ op-hash160 169
++ op-checksig 172
--
--

View File

@ -0,0 +1,78 @@
/- *bitcoin, resource
|%
+$ host-info
$: api-url=@t
connected=?
=network
block=@ud
clients=(set ship)
==
+$ whitelist
$: public=?
kids=?
users=(set ship)
groups=(set resource:resource)
==
::
+$ whitelist-target
$% [%public ~]
[%kids ~]
[%users users=(set ship)]
[%groups groups=(set resource:resource)]
==
+$ command
$% [%set-credentials api-url=@t =network]
[%add-whitelist wt=whitelist-target]
[%remove-whitelist wt=whitelist-target]
==
+$ action
$% [%address-info =address]
[%tx-info txid=hexb]
[%raw-tx txid=hexb]
[%broadcast-tx rawtx=hexb]
[%ping ~]
==
::
+$ result
$% [%address-info =address utxos=(set utxo) used=? block=@ud]
[%tx-info =info:tx]
[%raw-tx txid=hexb rawtx=hexb]
[%broadcast-tx txid=hexb broadcast=? included=?]
==
+$ error
$% [%not-connected status=@ud]
[%bad-request status=@ud]
[%no-auth status=@ud]
[%rpc-error ~]
==
+$ update (each result error)
+$ status
$% [%connected =network block=@ud fee=(unit sats)]
[%new-block =network block=@ud fee=(unit sats) blockhash=hexb blockfilter=hexb]
[%disconnected ~]
==
::
++ rpc-types
|%
+$ action
$% [%get-address-info =address]
[%get-tx-vals txid=hexb]
[%get-raw-tx txid=hexb]
[%broadcast-tx rawtx=hexb]
[%get-block-count ~]
[%get-block-info ~]
==
::
+$ result
$% [%get-address-info =address utxos=(set utxo) used=? block=@ud]
[%get-tx-vals =info:tx]
[%get-raw-tx txid=hexb rawtx=hexb]
[%create-raw-tx rawtx=hexb]
[%broadcast-tx txid=hexb broadcast=? included=?]
[%get-block-count block=@ud]
[%get-block-info block=@ud fee=(unit sats) blockhash=hexb blockfilter=hexb]
==
--
--
::

View File

@ -0,0 +1,152 @@
/- *bitcoin, bp=btc-provider
/+ bip32
|%
+$ params [batch-size=@ud fam-limit=@ud piym-limit=@ud]
+$ provider [host=ship connected=?]
+$ block @ud
+$ btc-state [=block fee=(unit sats) t=@da]
+$ payment [pend=(unit txid) =xpub =address payer=ship value=sats note=(unit @t)]
+$ piym
$: ps=(map ship payment)
pend=(map txid payment)
num-fam=(map ship @ud)
==
+$ poym [txbu=(unit txbu) note=(unit @t)]
::
:: command: run from the CLI or as API calls by our ship
::
+$ command
$% [%set-provider provider=(unit ship)]
[%check-provider provider=ship]
[%check-payee payee=ship]
[%set-current-wallet =xpub]
[%add-wallet =xpub =fprint scan-to=(unit scon) max-gap=(unit @ud) confs=(unit @ud)]
[%delete-wallet =xpub]
[%init-payment-external =address value=sats feyb=sats note=(unit @t)]
[%init-payment payee=ship value=sats feyb=sats note=(unit @t)]
[%broadcast-tx txhex=cord]
[%gen-new-address ~]
==
:: action: how peers poke us
::
+$ action
$% [%gen-pay-address value=sats note=(unit @t)]
[%give-pay-address =address value=sats]
[%expect-payment =txid value=sats]
==
:: internal: actions that simply make the state machine more explicit
::
+$ internal
$% [%add-poym-raw-txi =txid rawtx=hexb]
[%close-pym ti=info:tx]
[%fail-broadcast-tx =txid]
[%succeed-broadcast-tx =txid]
==
::
:: Wallet Types
::
:: nixt: next indices to generate addresses from (non-change/change)
:: addi: HD path along with UTXOs
:: wach: map for watched addresses.
:: Membership implies the address is known by outside parties or had prior activity
:: scon: indices to initially scan to in (non-)change accounts
:: defaults to 2^32-1 (i.e. all the addresses, ~4B)
:: wilt: copulates with thousands of indices to form addresses
::
++ max-index (dec (pow 2 32))
+$ nixt (pair idx idx)
+$ addi [used=? =chyg =idx utxos=(set utxo)]
+$ wach (map address addi)
+$ scon $~([max-index max-index] (pair idx idx))
+$ wilt _bip32
::
:: walt: wallet datastructure
:: scanned: whether the wallet's addresses have been checked for prior activity
:: scan-to
:: max-gap: maximum number of consec blank addresses before wallet stops scanning
:: confs: confirmations required (after this is hit for an address, wallet stops refreshing it)
::
+$ walt
$: =xpub
=network
=fprint
=wilt
=bipt
=wach
=nixt
scanned=?
scan-to=scon
max-gap=@ud
confs=@ud
==
:: batch: indexes to scan for a given chyg
:: scans: all scans underway (batches)
::
+$ batch [todo=(set idx) endpoint=idx has-used=?]
+$ scans (map [xpub chyg] batch)
::
:: insel: a selected utxo for input to a transaction
:: pmet: optional payment metadata
:: feyb: fee per byte in sats
:: txi/txo: input/output for a transaction being built
:: - txo has an hdkey if it's a change account
:: - by convention, first output of txo is to the payee, if one is present
:: txbu: tx builder -- all information needed to make a transaction for signing
::
+$ insel [=utxo =chyg =idx]
+$ feyb sats
+$ txi [=utxo rawtx=(unit hexb) =hdkey]
+$ txo [=address value=sats hk=(unit hdkey)]
+$ txbu
$: =xpub
payee=(unit ship)
=vbytes
txis=(list txi)
txos=(list txo)
signed-tx=(unit hexb)
==
:: hest: an entry in the history log
::
+$ hest
$: =xpub
=txid
confs=@ud
recvd=(unit @da)
inputs=(list [=val:tx s=(unit ship)])
outputs=(list [=val:tx s=(unit ship)])
note=(unit @t)
==
+$ history (map txid hest)
::
+$ error
$? %cant-pay-ourselves
%no-comets
%no-dust
%tx-being-signed
%insufficient-balance
%broadcast-fail
==
:: data to send to the frontend
::
+$ update
$% $: %initial
provider=(unit provider)
wallet=(unit xpub)
balance=(unit [confirmed=sats unconfirmed=sats])
=history
=btc-state
address=(unit address)
==
[%broadcast-success ~]
[%change-provider provider=(unit provider)]
[%change-wallet wallet=(unit xpub) balance=(unit [p=sats q=sats]) =history]
[%psbt pb=@t fee=sats]
[%btc-state =btc-state]
[%new-tx =hest]
[%cancel-tx =txid]
[%new-address =address]
[%balance balance=(unit [confirmed=sats unconfirmed=sats])]
[%error =error]
==
::
--

View File

@ -1,3 +1,5 @@
|%
+$ glob (map path mime)
+$ glob (map path mime)
+$ glob-details [hash=@uv glob=(unit (each glob tid=@ta))]
+$ globs (map serve-path=path glob-details)
--

View File

@ -1,4 +1,14 @@
|%
+$ tiles-0 (map term tile-0)
+$ tile-0
$: type=tile-type-0
is-shown=?
==
+$ tile-type-0
$% [%basic title=cord icon-url=cord linked-url=cord]
[%custom ~]
==
::
+$ tiles (map term tile)
+$ tile-ordering (list term)
::
@ -9,7 +19,7 @@
::
+$ tile-type
$% [%basic title=cord icon-url=cord linked-url=cord]
[%custom ~]
[%custom linked-url=(unit cord) image=(unit cord)]
==
::
+$ action

95
pkg/arvo/ted/btc-rpc.hoon Normal file
View File

@ -0,0 +1,95 @@
:: Note: these are for BTC testnet
::
/- spider, rpc=json-rpc
/+ strandio, bc=bitcoin
=, strand=strand:spider
=>
|%
++ url1 "http://localhost:50002"
++ addr ^-(address:bc [%bech32 'bc1q39wus23jwe7m2j7xmrfr2svhrtejmsn262x3j2'])
++ btc-req
^- request:http
=, enjs:format
:* method=%'POST'
url=`@ta`(crip (weld url1 "/btc-rpc"))
header-list=['Content-Type'^'application/json' ~]
^= body
%- some
%- as-octt:mimes:html
%- en-json:html
%- pairs
:~ jsonrpc+s+'2.0'
id+s+'block-info'
method+s+'getblockchaininfo'
==
==
++ electrs-req
^- request:http
=, enjs:format
:* method=%'POST'
url=`@ta`(crip (weld url1 "/electrs-rpc"))
header-list=['Content-Type'^'application/json' ~]
^= body
%- some
%- as-octt:mimes:html
%- en-json:html
%- pairs
:~ jsonrpc+s+'2.0'
id+s+'list-unspent'
method+s+'blockchain.scripthash.listunspent'
params+a+~[[%s '34aae877286aa09828803af27ce2315e72c4888efdf74d7d067c975b7c558789']]
==
==
::
:: convert address to Electrs ScriptHash that it uses to index
:: big-endian sha256 of the output script
::
++ electrs-script-hash
|= a=address:bc
^- hexb:bc
%- flip:byt:bc
%- sha256:bc
(script-pubkey:bc a)
::
++ parse-json-rpc
|= =json
^- (unit response:rpc)
=/ res=(unit [@t ^json])
%. json
=, dejs-soft:format
(ot id+so result+some ~)
?^ res `[%result u.res]
~| parse-one-response=json
:+ ~ %error %- need
%. json
=, dejs-soft:format
(ot id+so error+(ot code+no message+so ~) ~)
::
++ parse-response
|= =client-response:iris
=/ m (strand:strandio ,(unit response:rpc))
^- form:m
?> ?=(%finished -.client-response)
?~ full-file.client-response
(pure:m ~)
=/ body=@t q.data.u.full-file.client-response
=/ jon=(unit json) (de-json:html body)
?~ jon (pure:m ~)
(pure:m (parse-json-rpc u.jon))
::
++ attempt-request
|= =request:http
=/ m (strand:strandio ,~)
^- form:m
(send-request:strandio request)
--
^- thread:spider
|= arg=vase
:: =+ !<([~ a=@ud] arg)
=/ m (strand ,vase)
^- form:m
;< ~ bind:m (attempt-request electrs-req)
;< rep=client-response:iris bind:m
take-client-response:strandio
;< rpc-resp=(unit response:rpc) bind:m (parse-response rep)
(pure:m !>(rpc-resp))

View File

@ -8,5 +8,6 @@
=+ !<([~ hash=@uv] arg)
=/ url "https://bootstrap.urbit.org/glob-{(scow %uv hash)}.glob"
;< =cord bind:m (fetch-cord:strandio url)
~| failed-glob+hash
=+ ;;(=glob:glob (cue cord))
(pure:m !>(glob))

13
pkg/arvo/ted/rpc.hoon Normal file
View File

@ -0,0 +1,13 @@
/- spider, rpc=json-rpc
/+ strandio, bcu=bitcoin-utils
=, strand=strand:spider
=>
|%
++ blah 2
--
^- thread:spider
|= arg=vase
:: =+ !<([~ a=@ud] arg)
=/ m (strand ,vase)
^- form:m
(pure:m !>(blah))

View File

@ -0,0 +1,159 @@
/+ *test, *bip-b158, *bitcoin-utils
|%
+$ filter-vector
$: filter=hexb
expect=[parse=[n=@ux gcs-set=bits] decode=[delta=@ rest=bits]]
==
+$ siphash-vector
$: blockhash=tape
filter=hexb
item=hexb
expect=@
==
+$ match-vector
$: blockhash=tape
filter=hexb
inc-spks=(list hexb)
exc-spks=(list hexb)
expect=(list @)
==
::
++ filter-vectors
^- (list filter-vector)
:~
:: testnet genesis block
::
:* 4^0x19d.fca8
:* 0x1
24^0b1001.1101.1111.1100.1010.1000
==
[769.941 [3 0b0]]
==
:: testnet block 926485
::
:* 25^0x9.027a.cea6.1b6c.c3fb.33f5.d52f.7d08.8a6b.2f75.d234.e89c.a800
:* 0x9
192^0b10.0111.1010.1100.1110.1010.0110.0001.1011.0110.1100.1100.0011.1111.1011.0011.0011.1111.0101.1101.0101.0010.1111.0111.1101.0000.1000.1000.1010.0110.1011.0010.1111.0111.0101.1101.0010.0011.0100.1110.1000.1001.1100.1010.1000.0000.0000
==
[10.156 172^0b1110.1010.0110.0001.1011.0110.1100.1100.0011.1111.1011.0011.0011.1111.0101.1101.0101.0010.1111.0111.1101.0000.1000.1000.1010.0110.1011.0010.1111.0111.0101.1101.0010.0011.0100.1110.1000.1001.1100.1010.1000.0000.0000]
==
:: 3 vectors with large Ns (i.e. CompactSize starting with 0xfd/fe/ff)
::
:* 6^0xfd88.279d.fca8
:* 0x2788
24^0b1001.1101.1111.1100.1010.1000
==
[769.941 [3 0b0]]
==
::
:* 8^0xfe11.2233.449d.fca8
:* 0x4433.2211
24^0b1001.1101.1111.1100.1010.1000
==
[769.941 [3 0b0]]
==
::
:* 12^0xff11.2233.4455.6677.889d.fca8
:* 0x8877.6655.4433.2211
24^0b1001.1101.1111.1100.1010.1000
==
[769.941 [3 0b0]]
==
==
::
++ siphash-vectors
^- (list siphash-vector)
:: testnet genesis block
:~ :* "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"
4^0x19d.fca8
67^0x41.0467.8afd.b0fe.5548.2719.67f1.a671.30b7.105c.d6a8.28e0.3909.a679.62e0.ea1f.61de.b649.f6bc.3f4c.ef38.c4f3.5504.e51e.c112.de5c.384d.f7ba.0b8d.578a.4c70.2b6b.f11d.5fac
769.941
==
==
::
++ match-vectors
^- (list match-vector)
:: testnet genesis block
:~ :* "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"
4^0x19d.fca8
~[67^0x41.0467.8afd.b0fe.5548.2719.67f1.a671.30b7.105c.d6a8.28e0.3909.a679.62e0.ea1f.61de.b649.f6bc.3f4c.ef38.c4f3.5504.e51e.c112.de5c.384d.f7ba.0b8d.578a.4c70.2b6b.f11d.5fac]
~[25^0x76.a914.3ebc.40e4.11ed.3c76.f867.1150.7ab9.5230.0890.3972.88ac]
~[271.501 769.941]
==
:: testnet block 926485
::
:* "000000000000015d6077a411a8f5cc95caf775ccf11c54e27df75ce58d187313"
25^0x9.027a.cea6.1b6c.c3fb.33f5.d52f.7d08.8a6b.2f75.d234.e89c.a800
:~ 25^0x76.a914.3ebc.40e4.11ed.3c76.f867.1150.7ab9.5230.0890.3972.88ac
25^0x76.a914.5033.3046.115e.aa0a.c9e0.2165.65f9.4507.0e44.5739.88ac
==
:~ 21^0x14.7e69.a44c.1a94.2139.c8ab.4127.8325.5e1e.46d0.f0da
==
~[176.536 2.341.508 3.078.625]
==
==
::
++ test-all-vectors
=/ [p=@ m=@] [p:params m:params]
^- tang
|^ ;: weld
%+ category "parse filters"
(zing (turn filter-vectors check-filter-parse))
%+ category "decode GCS"
(zing (turn filter-vectors check-gcs-decode))
%+ category "siphash"
(zing (turn siphash-vectors check-siphash))
%+ category "hash script-pubkeys"
(zing (turn match-vectors check-hashing))
%+ category "whether filter matches any script-pubkey"
(zing (turn match-vectors check-match))
%+ category "get all script-pubkey matches for a block filter"
(zing (turn match-vectors check-all-match))
==
::
++ check-filter-parse
|= v=filter-vector
%+ expect-eq
!>(parse.expect.v)
!>((parse-filter filter.v))
::
++ check-gcs-decode
|= v=filter-vector
%+ expect-eq
!>(decode.expect.v)
!>((de:gol gcs-set:(parse-filter filter.v) p))
::
++ check-siphash
|= v=siphash-vector
=+ f=(mul n:(parse-filter filter.v) m)
%+ expect-eq
!>(expect.v)
!>((to-range:hsh item.v f (to-key blockhash.v)))
::
++ check-hashing
|= v=match-vector
=/ [n=@ux gcs-set=bits] (parse-filter filter.v)
=+ k=(to-key blockhash.v)
%+ expect-eq
!>(expect.v)
!>((set-construct:hsh (weld inc-spks.v exc-spks.v) k (mul n m)))
::
++ check-match
|= v=match-vector
=+ k=(to-key blockhash.v)
%+ weld
%+ expect-eq
!>(%.y)
!>((match filter.v k inc-spks.v))
%+ expect-eq
!>(%.n)
!>((match filter.v k exc-spks.v))
::
++ check-all-match
|= v=match-vector
=+ k=(to-key blockhash.v)
%+ expect-eq
!>(`(set hexb)`(sy inc-spks.v))
!>(`(set hexb)`(all-match filter.v k (weld inc-spks.v exc-spks.v)))
--
--

View File

@ -0,0 +1,49 @@
/- *bitcoin
/+ *test, *bip-b158, bcu=bitcoin-utils, pbt=bip-b174
|%
+$ psbt-vector
$: =hdkey
hdkey-hex=hexb
==
++ fprint 4^0xdead.beef
++ psbt-vectors
^- (list psbt-vector)
:~ :* [fprint 33^0x1 %testnet %44 %0 1]
20^0x2c00.0080.0100.0080.0000.0080.0000.0000.0100.0000
==
::
:* [fprint 33^0x1 %testnet %49 %0 1]
20^0x3100.0080.0100.0080.0000.0080.0000.0000.0100.0000
==
::
:* [fprint 33^0x1 %testnet %84 %0 1]
20^0x5400.0080.0100.0080.0000.0080.0000.0000.0100.0000
==
::
:* [fprint 33^0x1 %main %44 %0 1]
20^0x2c00.0080.0000.0080.0000.0080.0000.0000.0100.0000
==
::
:* [fprint 33^0x1 %main %49 %0 1]
20^0x3100.0080.0000.0080.0000.0080.0000.0000.0100.0000
==
::
:* [fprint 33^0x1 %main %84 %0 1]
20^0x5400.0080.0000.0080.0000.0080.0000.0000.0100.0000
==
==
++ test-all-vectors
^- tang
|^ ;: weld
%+ category "check PSBT"
(zing (turn psbt-vectors check-psbt))
==
++ check-psbt
|= v=psbt-vector
=/ key=hexb
(cat:byt:bcu ~[1^0x6 pubkey.hdkey.v]) :: %input target
%+ expect-eq
!>([key (cat:byt:bcu ~[fprint.hdkey.v hdkey-hex.v])])
!>((hdkey:en:pbt %input hdkey.v))
--
--

View File

@ -0,0 +1,207 @@
/+ *test, *bitcoin, bip32
=, secp:crypto
=+ ecc=secp256k1
|%
+$ chyg ?(%0 %1)
+$ bits-vector [bitwidth=@ atoms=(list @) =bits]
+$ compact-size-vector @ux
+$ tx-vector [hex-cord=@t txid=hexb]
+$ xpub-vector
$: =xpub
=network
hdpath=[=bipt =chyg =idx]
pubkey=hexb
=address
==
+$ script-pubkey-vector [=address spk=hexb]
::
++ bits-vectors
^- (list bits-vector)
:~ :* 5
~[0 31 31 0 31 0]
[30 0b1.1111.1111.1000.0011.1110.0000]
==
==
::
++ compact-size-vectors
^- (list compact-size-vector)
:~ 0x98
0x302
0xaa.bbcc
0xaabb.ccdd
0xaa.bbcc.ddee
0xaabb.ccdd.eeff.1122
==
::
++ tx-vectors
^- (list tx-vector)
:~ :* '0200000002ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40000000000feffffffab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff02603bea0b000000001976a914768a40bbd740cbe81d988e71de2a4d5c71396b1d88ac8e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac00000000'
32^0xfed6.cd1f.de4d.b4e1.3e7e.8003.17e3.7f9c.bd75.ec36.4389.670e.eff8.0da9.93c7.e560
==
::
:* '01000000000102267b34b058a44e678ca0609825fe37c5bb893462337334ad5a2e887b8ebef65c000000002322002049f80613b30fe3063d4e6ce75c53a7bc573ea17fe49dcb805aca416a8af5d991ffffffff666f99bf914bb18e28ec39af93d99a1938767fbec892408acced7c80663f0df40000000023220020096def7756afb4dba661ec58602cf6af1f7881e48e4b8a8c12be9985073f5adeffffffff06602d59010000000017a914572290324c72e6842e8a77c2cbb9882a3b9c2a9f87195c0e00000000001976a914abfdf3698ceef95986b31b763e6764cfe3ce584e88ac68642400000000001976a91456cf5fcc3654c5646b930e8773a95dce98c49e0588acfbb34b00000000001976a914518ee0d1b48f3d99f76e6e8283006610e39aeeba88acf492560000000000160014d1930fff9862af879ee14ecd3e1b9dc1099524ce0374c802000000001976a9148a6727bc345abeae523b6af7828053f95332918688ac03483045022100fcc8336b7c81e67cc7b53587fb06c3f950a6d3349e658594a444618c75988e45022065b3b957e0f7d2def98565a34c1ee7843d60525c4337730fa5d6ac8ff7aacb89012102ac604909ed86488338ec6255b0bdc0162562b299e66b860480a8bd2b99c7f3291976a9140e60a2ad39efd10ad61e2a3e6e5c1baa73190ee088ac0347304402202ec01f623cd48ba990caea70463de86b37ffb2363640b510ce9d80c750a54eec02204915d11649d636e5e8503b226d7a6a45da0163f3f0ca4b07bdaaa9af4d6f850f012102a52b3f9958c0f4b57b99f287832ea75775ddf7c83fa0648b6b1545ec4881ef2d1976a91401121fe150c9b05f9146bac57ad9947ea5c1478e88ac00000000'
32^0x2b9c.60c4.dfcd.0aa2.b1b8.83a5.0a4a.2a96.197b.07d8.cdd1.e749.f0a1.f296.0f43.b339
==
==
:: below use mnemonic:
:: abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about
::
++ xpub-vectors
^- (list xpub-vector)
:~ :* 'tpubDC5FSnBiZDMmhiuCmWAYsLwgLYrrT9rAqvTySfuCCrgsWz8wxMXUS9Tb9iVMvcRbvFcAHGkMD5Kx8koh4GquNGNTfohfk7pgjhaPCdXpoba'
%testnet
[%44 %0 0]
33^0x2.a745.1395.7353.69f2.ecdf.c829.c0f7.74e8.8ef1.303d.fe5b.2f04.dbaa.b30a.535d.fdd6
[%base58 0cmkpZhYtJu2r87Js3pDiWJDmPte2NRZ8bJV]
==
::
:* 'upub5EFU65HtV5TeiSHmZZm7FUffBGy8UKeqp7vw43jYbvZPpoVsgU93oac7Wk3u6moKegAEWtGNF8DehrnHtv21XXEMYRUocHqguyjknFHYfgY'
%testnet
[%49 %0 0]
33^0x3.a1af.804a.c108.a8a5.1782.198c.2d03.4b28.bf90.c880.3f5a.53f7.6276.fa69.a4ea.e77f
[%base58 0c2Mww8dCYPUpKHofjgcXcBCEGmniw9CoaiD2]
==
::
:* 'vpub5Y6cjg78GGuNLsaPhmYsiw4gYX3HoQiRBiSwDaBXKUafCt9bNwWQiitDk5VZ5BVxYnQdwoTyXSs2JHRPAgjAvtbBrf8ZhDYe2jWAqvZVnsc'
%testnet
[%84 %0 0]
33^0x2.e7ab.2537.b5d4.9e97.0309.aae0.6e9e.49f3.6ce1.c9fe.bbd4.4ec8.e0d1.cca0.b4f9.c319
[%bech32 'tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl']
==
::
:* 'xpub6BosfCnifzxcFwrSzQiqu2DBVTshkCXacvNsWGYJVVhhawA7d4R5WSWGFNbi8Aw6ZRc1brxMyWMzG3DSSSSoekkudhUd9yLb6qx39T9nMdj'
%main
[%44 %0 0]
33^0x3.aaeb.52dd.7494.c361.049d.e67c.c680.e83e.bcbb.bdbe.b136.37d9.2cd8.45f7.0308.af5e
[%base58 0c1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA]
==
::
:* 'ypub6Ww3ibxVfGzLrAH1PNcjyAWenMTbbAosGNB6VvmSEgytSER9azLDWCxoJwW7Ke7icmizBMXrzBx9979FfaHxHcrArf3zbeJJJUZPf663zsP'
%main
[%49 %0 0]
33^0x3.9b3b.694b.8fc5.b5e0.7fb0.69c7.83ca.c754.f5d3.8c3e.08be.d196.0e31.fdb1.dda3.5c24
[%base58 0c37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf]
==
::
:* 'zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs'
%main
[%84 %0 0]
33^0x3.30d5.4fd0.dd42.0a6e.5f8d.3624.f5f3.482c.ae35.0f79.d5f0.753b.f5be.ef9c.2d91.af3c
[%bech32 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu']
==
==
::
++ script-pubkey-vectors
^- (list script-pubkey-vector)
:~ :* [%bech32 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu']
[wid=22 dat=0x14.c0ce.bcd6.c3d3.ca8c.75dc.5ec6.2ebe.5533.0ef9.10e2]
==
::
:* [%bech32 'bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3']
[wid=34 dat=0x20.1863.143c.14c5.1668.04bd.1920.3356.da13.6c98.5678.cd4d.27a1.b8c6.3296.0490.3262]
==
::
:* [%bech32 'tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl']
[wid=22 dat=0x14.d0c4.a3ef.09e9.97b6.e99e.397e.518f.e3e4.1a11.8ca1]
==
::
:* [%base58 0c1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA]
[wid=25 dat=0x76.a914.d986.ed01.b7a2.2225.a70e.dbf2.ba7c.fb63.a15c.b3aa.88ac]
==
::
:* [%base58 0cmxVFsFW5N4mu1HPkxPttorvocvzeZ7KZyk]
[wid=25 dat=0x76.a914.ba27.f99e.007c.7f60.5a83.05e3.18c1.abde.3cd2.20ac.88ac]
==
::
:* [%base58 0cmfWxJ45yp2SFn7UciZyNpvDKrzbhyfKrY8]
[wid=25 dat=0x76.a914.0000.0000.0000.0000.0000.0000.0000.0000.0000.0000.88ac]
==
::
:* [%base58 0c37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf]
[wid=23 dat=0xa9.143f.b6e9.5812.e57b.b469.1f9a.4a62.8862.a61a.4f76.9b87]
==
::
:* [%base58 0c2MvLWCyKPQQ6oqJKSJ9ic8hYVmLyNry6yuF]
[wid=23 dat=0xa9.1421.e7fe.f309.cf6f.6cfc.fe94.c572.e541.d74f.d848.5487]
==
==
::
++ mk-pubkey
|= [=xpub =chyg =idx]
^- hexb
=/ pk=@ux
%- compress-point:ecc
pub:(derive-public:(derive-public:(from-extended:bip32 (trip xpub)) (@ chyg)) idx)
[(met 3 pk) pk]
::
++ test-all-vectors
^- tang
|^ ;: weld
%+ category "bit manipulation"
(zing (turn bits-vectors check-bits))
%+ category "compact-size en/decoding"
(zing (turn compact-size-vectors check-compact-size))
%+ category "check TX en/decoding"
(zing (turn tx-vectors check-tx))
%+ category "xpub parsing"
(zing (turn xpub-vectors check-xpub-parsing))
%+ category "pubkey derivation"
(zing (turn xpub-vectors check-pubkey-derivation))
%+ category "address derivation"
(zing (turn xpub-vectors check-address-derivation))
%+ category "script-pubkey derivation"
(zing (turn script-pubkey-vectors check-script-pubkey-derivation))
==
::
++ check-bits
|= v=bits-vector
;: weld
:: TODO: from-atoms works, but to-atoms doesn't
%+ expect-eq
!>(bits.v)
!>((from-atoms:bit bitwidth.v atoms.v))
%+ expect-eq
!>(atoms.v)
!>((to-atoms:bit bitwidth.v bits.v))
==
::
++ check-compact-size
|= v=compact-size-vector
%+ expect-eq
!>(v)
!>(dat:n:(de:csiz (en:csiz v)))
::
++ check-tx
|= v=tx-vector
%+ expect-eq
!>(txid.v)
!>((get-id:txu (decode:txu (from-cord:hxb hex-cord.v))))
::
++ check-xpub-parsing
|= v=xpub-vector
=/ [b=bipt n=network] (xpub-type xpub.v)
%+ expect-eq
!>([b n])
!>([bipt.hdpath.v network.v])
::
++ check-pubkey-derivation
|= v=xpub-vector
%+ expect-eq
!>(pubkey.v)
!>((mk-pubkey xpub.v chyg.hdpath.v idx.hdpath.v))
::
++ check-address-derivation
|= v=xpub-vector
=/ [b=bipt n=network] (xpub-type xpub.v)
%+ expect-eq
!>(address.v)
!>((from-pubkey:adr b n pubkey.v))
::
++ check-script-pubkey-derivation
|= v=script-pubkey-vector
%+ expect-eq
!>(spk.v)
!>((to-script-pubkey:adr address.v))
--
::
--

190
pkg/arvo/tests/lib/btc.hoon Normal file
View File

@ -0,0 +1,190 @@
/- bc=bitcoin
/+ *test, *btc
|%
+$ wallet-vector
$: =xpub:bc
=chyg
=idx:bc
=address:bc
==
+$ vector
$: =xpub:bc
eny=@uv
block=@ud
feyb=sats
ins=(list insel)
outs=(list txo)
expect=[selected=(unit (list insel)) chng=(unit sats:bc)]
==
++ mk-utxo
|= value=sats:bc
^- utxo:bc
:* pos=0
[wid=32 dat=0xc493.f6f1.4668.5f76.b44f.0c77.ca88.120c.b8bc.89f5.34fe.69b6.8288.27b9.74e6.8849]
height=3
value
recvd=~
==
::
++ fprint 4^0xdead.beef
::
++ wallet-vectors
^- (list wallet-vector)
:~ :* 'zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs'
%0
0
[%bech32 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu']
==
==
::
++ vectors
=| w=walt
^- (list vector)
:~ :* 'zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs'
0v3uc.iuebi.5qilc.l8d87.c1k6n.7iksq.nkobs.8s5he.raq40.9ff0b.5tj3u.kjtg7.aq59e.hatv7.oioam.mlsr4.pqqcd.cnbjn.pnpi2.1m5rt.k4scg
999
10
:~ [(mk-utxo 200.000) %0 1]
[(mk-utxo 500.000) %0 2]
[(mk-utxo 204) %0 3]
[(mk-utxo 235.000) %1 2]
==
:~ [[%bech32 'bc1q59u5epktervh6fxqay2dlph0wxu9hjnx6v8n66'] 200.100 ~]
[[%bech32 'bc1qlwd7mw33uea5m8r2lsnsrkc7gp2qynrxsfxpfm'] 200.000 ~]
==
:* `~[[(mk-utxo 500.000) %0 2]]
`332.500
==
==
::
:* 'zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs'
0v1gt.mc4ca.lfs0m.q1dal.lqobu.mmlbd.2umnp.lj9dr.4pf4s.pvclr.dps96.4a6i8.rt6n9.krp0r.11kqu.ckqe4.1tmat.gr754.463aj.a4b41.jj7qg
999
10
:~ [(mk-utxo 200.000) %0 1]
[(mk-utxo 500.000) %0 2]
[(mk-utxo 204) %0 3]
[(mk-utxo 235.000) %1 2]
==
:~ [[%bech32 'bc1q59u5epktervh6fxqay2dlph0wxu9hjnx6v8n66'] 200.100 ~]
[[%bech32 'bc1qlwd7mw33uea5m8r2lsnsrkc7gp2qynrxsfxpfm'] 200.000 ~]
==
:* `~[[(mk-utxo 235.000) %1 2] [(mk-utxo 200.000) %0 1]]
`297.500
==
==
::
:* 'zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs'
0v1gt.mc4ca.lfs0m.q1dal.lqobu.mmlbd.2umnp.lj9dr.4pf4s.pvclr.dps96.4a6i8.rt6n9.krp0r.11kqu.ckqe4.1tmat.gr754.463aj.a4b41.jj7qg
999
10
~[[(mk-utxo 500.000) %0 2]]
:~ [[%bech32 'bc1q59u5epktervh6fxqay2dlph0wxu9hjnx6v8n66'] 299.797 ~]
[[%bech32 'bc1qlwd7mw33uea5m8r2lsnsrkc7gp2qynrxsfxpfm'] 200.000 ~]
==
:* *(unit (list insel))
*(unit sats:bc)
==
==
:* 'zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs'
0v1gt.mc4ca.lfs0m.q1dal.lqobu.mmlbd.2umnp.lj9dr.4pf4s.pvclr.dps96.4a6i8.rt6n9.krp0r.11kqu.ckqe4.1tmat.gr754.463aj.a4b41.jj7qg
999
10
~[[(mk-utxo 500.000) %0 2]]
:~ [[%bech32 'bc1q59u5epktervh6fxqay2dlph0wxu9hjnx6v8n66'] 298.500 ~]
[[%bech32 'bc1qlwd7mw33uea5m8r2lsnsrkc7gp2qynrxsfxpfm'] 200.000 ~]
==
:* `~[[(mk-utxo 500.000) %0 2]]
*(unit sats:bc)
==
==
==
::
++ dust-output-vectors
=| w=walt
^- (list vector)
:~
:* 'zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs'
0v1gt.mc4ca.lfs0m.q1dal.lqobu.mmlbd.2umnp.lj9dr.4pf4s.pvclr.dps96.4a6i8.rt6n9.krp0r.11kqu.ckqe4.1tmat.gr754.463aj.a4b41.jj7qg
999
10
~[[(mk-utxo 500.000) %0 2]]
:~ [[%bech32 'bc1q59u5epktervh6fxqay2dlph0wxu9hjnx6v8n66'] 298.580 ~]
[[%bech32 'bc1qlwd7mw33uea5m8r2lsnsrkc7gp2qynrxsfxpfm'] 204 ~]
==
:* `~[[(mk-utxo 500.000) %0 2]]
*(unit sats:bc)
==
==
==
::
++ test-all-vectors
^- tang
|^ ;: weld
%+ category "address generation/lookup"
(zing (turn wallet-vectors address-gen-lookup))
%+ category "single-random-draw"
(zing (turn vectors check-single-random-draw))
::
%+ category "select with change"
(zing (turn vectors check-change))
::
%+ category "don't allow dust outputs"
(zing (turn dust-output-vectors check-dust-output))
==
::
++ address-gen-lookup
|= v=wallet-vector
=/ w=walt (from-xpub xpub.v fprint ~ ~ ~)
=/ =address (~(mk-address wad w chyg.v) idx.v)
=. w (~(update-address wad w chyg.v) address [%.n %0 0 *(set utxo:bc)])
=/ [w2=walt c=chyg i=idx] (need (address-coords address ~[w]))
;: weld
%+ expect-eq
!>(address)
!>(address.v)
%+ expect-eq
!>([w2 c i])
!>([w chyg.v idx.v])
==
::
++ check-single-random-draw
|= v=vector
=/ w=walt (from-xpub xpub.v fprint ~ ~ ~)
%+ expect-eq
!>(selected.expect.v)
!>((~(single-random-draw sut [w eny.v block.v ~ feyb.v outs.v]) ins.v))
::
++ check-change
|= v=vector
=/ w=walt (from-xpub xpub.v fprint ~ ~ ~)
=. wach.w
%- ~(gas by *(map address:bc addi))
%+ turn ins.v
|= i=insel
:- (~(mk-address wad w chyg.i) idx.i)
[%.y %0 0 (sy ~[utxo.i])]
%+ expect-eq
!>(chng.expect.v)
!>(chng:~(with-change sut [w eny.v block.v ~ feyb.v outs.v]))
::
++ check-dust-output
|= v=vector
=/ w=walt (from-xpub xpub.v fprint ~ ~ ~)
=. wach.w (insels-to-wach w ins.v)
%- expect-fail
|.(~(with-change sut [w eny.v block.v ~ feyb.v outs.v]))
::
++ insels-to-wach
|= [w=walt is=(list insel)]
^- wach
%- ~(gas by *(map address:bc addi))
%+ turn is
|= i=insel
:- (~(mk-address wad w chyg.i) idx.i)
[%.y %0 0 (sy ~[utxo.i])]
--
:: if a non-change output is dust, error
:: change shouldn't be returned when change is dust
::
--

8
pkg/btc-wallet/README.md Normal file
View File

@ -0,0 +1,8 @@
To verify your version of the bitcoin wallet, run the following command in the
dojo:
`> +btc-wallet-check`
it should return with the following hash:
`0v9t022.n8kv1.5emkt.s2p9i.hvsa9`

View File

@ -0,0 +1,6 @@
module.exports = {
URBIT_PIERS: [
"%URBITPIER%",
],
URL: 'http://localhost:80'
};

View File

@ -0,0 +1,128 @@
const path = require('path');
const webpack = require('webpack');
// const HtmlWebpackPlugin = require('html-webpack-plugin');
// const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const urbitrc = require('./urbitrc');
const fs = require('fs-extra');
const _ = require('lodash');
function copy(src,dest) {
return new Promise((res,rej) =>
fs.copy(src,dest, err => err ? rej(err) : res()));
}
class UrbitShipPlugin {
constructor(urbitrc) {
this.piers = urbitrc.URBIT_PIERS;
}
apply(compiler) {
compiler.hooks.afterEmit.tapPromise(
'UrbitShipPlugin',
async (compilation) => {
const src = path.resolve(compiler.options.output.path, 'index.js');
}
);
}
}
let devServer = {
contentBase: path.join(__dirname, '../dist'),
hot: true,
port: 9000,
host: '0.0.0.0',
disableHostCheck: true,
historyApiFallback: true,
};
const router = _.mapKeys(urbitrc.FLEET || {}, (value, key) => `${key}.localhost:9000`);
if(urbitrc.URL) {
devServer = {
...devServer,
index: '',
proxy: {
'/~btc/js/bundle/index.js': {
target: 'http://localhost:9000',
pathRewrite: (req, path) => {
return '/index.js'
}
},
'**': {
changeOrigin: true,
target: urbitrc.URL,
router,
// ensure proxy doesn't timeout channels
proxyTimeout: 0
}
}
};
}
module.exports = {
node: { fs: 'empty' },
mode: 'development',
entry: {
app: './src/index.js'
},
module: {
rules: [
{
test: /\.(j|t)sx?$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', ['@babel/preset-react', {
runtime: 'automatic',
development: 'true',
importSource: '@welldone-software/why-did-you-render',
}]],
plugins: [
'@babel/transform-runtime',
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-class-properties',
'react-hot-loader/babel'
]
}
},
exclude: /node_modules/
},
{
test: /\.css$/i,
use: [
// Creates `style` nodes from JS strings
'style-loader',
// Translates CSS into CommonJS
'css-loader',
// Compiles Sass to CSS
'sass-loader'
]
}
]
},
resolve: {
extensions: ['.js', '.ts', '.tsx']
},
devtool: 'inline-source-map',
devServer: devServer,
plugins: [
new UrbitShipPlugin(urbitrc)
],
watch: true,
watchOptions: {
poll: true,
ignored: '/node_modules/'
},
output: {
filename: 'index.js',
chunkFilename: 'index.js',
path: path.resolve(__dirname, '../dist'),
publicPath: '/',
globalObject: 'this'
},
optimization: {
minimize: false,
usedExports: true
}
};

View File

@ -0,0 +1,60 @@
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const urbitrc = require('./urbitrc');
module.exports = {
node: { fs: 'empty' },
mode: 'production',
entry: {
app: './src/index.js'
},
module: {
rules: [
{
test: /\.jsx?$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: [
'@babel/transform-runtime',
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-class-properties'
]
}
},
exclude: /node_modules/
},
{
test: /\.css$/i,
use: [
// Creates `style` nodes from JS strings
'style-loader',
// Translates CSS into CommonJS
'css-loader',
// Compiles Sass to CSS
'sass-loader'
]
}
]
},
resolve: {
extensions: ['.js', '.ts', '.tsx']
},
devtool: 'source-map',
plugins: [
new CleanWebpackPlugin()
],
output: {
filename: (pathData) => {
return pathData.chunk.name === 'app' ? 'index.[contenthash].js' : '[name].js';
},
path: path.resolve(__dirname, `../../arvo/app/btc-wallet/js/bundle`),
publicPath: '/',
},
optimization: {
minimize: true,
usedExports: true
}
};

9912
pkg/btc-wallet/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,75 @@
{
"name": "urbit-bitcoin-wallet",
"version": "0.1.0",
"main": "node install.js",
"scripts": {
"start": "webpack-dev-server --config config/webpack.dev.js",
"build:dev": "cross-env NODE_ENV=production webpack --config config/webpack.dev.js",
"build:prod": "cross-env NODE_ENV=production webpack --config config/webpack.prod.js"
},
"author": "Tlon Corp",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.9.0",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-object-rest-spread": "^7.9.5",
"@babel/plugin-proposal-optional-chaining": "^7.9.0",
"@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.10.5",
"@babel/preset-env": "^7.9.5",
"@babel/preset-react": "^7.9.4",
"@babel/preset-typescript": "^7.13.0",
"@welldone-software/why-did-you-render": "^6.1.1",
"babel-loader": "^8.1.0",
"babel-plugin-root-import": "^6.5.0",
"clean-webpack-plugin": "^3.0.0",
"cross-env": "^7.0.2",
"file-loader": "^6.0.0",
"html-webpack-plugin": "^4.2.0",
"react-hot-loader": "^4.12.21",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"typescript": "^4.2.3",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3"
},
"dependencies": {
"@babel/runtime": "^7.10.5",
"@reach/disclosure": "^0.10.5",
"@reach/menu-button": "^0.10.5",
"@reach/tabs": "^0.10.5",
"@tlon/indigo-light": "^1.0.7",
"@tlon/indigo-react": "^1.2.22",
"@tlon/sigil-js": "^1.4.3",
"bip39": "^2.5.0",
"bitcoin-address-validation": "^2.0.1",
"bitcoinjs-lib": "^5.2.0",
"bs58check": "^2.1.2",
"buffer": "^6.0.3",
"classnames": "^2.2.6",
"css-loader": "^3.5.3",
"formik": "^2.2.0",
"fs-extra": "^8.1.0",
"lodash": "^4.17.11",
"markdown-to-jsx": "^7.1.2",
"moment": "^2.20.1",
"mousetrap": "^1.6.3",
"mv": "^2.1.1",
"promise": "^8.0.3",
"prompt": "^1.0.0",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-router-dom": "^5.0.0",
"replace-in-file": "^4.1.1",
"style-loader": "^1.2.1",
"styled-components": "^5.2.3",
"styled-system": "^5.1.5",
"urbit-key-generation": "^0.19.0",
"urbit-ob": "^5.0.0",
"urbit-sigil-js": "^1.3.13"
},
"resolutions": {
"natives": "1.1.3"
}
}

View File

@ -0,0 +1,158 @@
p, h1, h2, h3, h4, h5, h6, a, input, textarea, button {
margin-block-end: unset;
margin-block-start: unset;
-webkit-margin-before: unset;
-webkit-margin-after: unset;
font-family: Inter, sans-serif;
}
a {
color: #000;
text-decoration: none;
}
textarea, select, input, button {
outline: none;
-webkit-appearance: none;
border: none;
background-color: #fff;
}
.body-regular {
font-size: 16px;
line-height: 24px;
font-weight: 600;
}
.body-large {
font-size: 20px;
line-height: 24px;
}
.label-regular {
font-size: 14px;
line-height: 24px;
}
.label-small-mono {
font-size: 12px;
line-height: 24px;
font-family: "Source Code Pro", monospace;
}
.body-regular-400 {
font-size: 16px;
line-height: 24px;
font-weight: 400;
}
.plus-font {
font-size: 48px;
line-height: 24px;
}
.btn-font {
font-size: 14px;
line-height: 16px;
font-weight: 600;
}
.mono {
font-family: "Source Code Pro", monospace;
}
.inter {
font-family: Inter, sans-serif;
}
.mix-blend-diff {
mix-blend-mode: difference;
}
/* dark */
@media (prefers-color-scheme: dark) {
body {
background-color: #333;
}
.bg-black-d {
background-color: black;
}
.white-d {
color: white;
}
.gray1-d {
color: #4d4d4d;
}
.gray2-d {
color: #7f7f7f;
}
.gray3-d {
color: #b1b2b3;
}
.gray4-d {
color: #e6e6e6;
}
.bg-gray0-d {
background-color: #333;
}
.bg-gray1-d {
background-color: #4d4d4d;
}
.b--gray0-d {
border-color: #333;
}
.b--gray1-d {
border-color: #4d4d4d;
}
.b--gray2-d {
border-color: #7f7f7f;
}
.b--white-d {
border-color: #fff;
}
.bb-d {
border-bottom-width: 1px;
border-bottom-style: solid;
}
.invert-d {
filter: invert(1);
}
.o-80-d {
opacity: .8;
}
.focus-b--white-d:focus {
border-color: #fff;
}
a {
color: #fff;
}
.hover-bg-gray1-d:hover {
color: #4d4d4d;
}
}
/* responsive */
@media all and (max-width: 34.375em) {
.h-100-minus-40-s {
height: calc(100% - 40px);
}
}
@media all and (min-width: 34.375em) and (max-width: 46.875em) {
.h-100-minus-40-m {
height: calc(100% - 40px);
}
}
@media all and (min-width: 46.875em) and (max-width: 60em) {
.h-100-minus-40-l {
height: calc(100% - 40px);
}
}
@media all and (min-width: 60em) {
.h-100-minus-40-xl {
height: calc(100% - 40px);
}
}

View File

@ -0,0 +1,63 @@
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
src: url("https://media.urbit.org/fonts/Inter-Regular.woff2") format("woff2");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 400;
src: url("https://media.urbit.org/fonts/Inter-Italic.woff2") format("woff2");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
src: url("https://media.urbit.org/fonts/Inter-Bold.woff2") format("woff2");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 700;
src: url("https://media.urbit.org/fonts/Inter-BoldItalic.woff2") format("woff2");
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-extralight.woff");
font-weight: 200;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-light.woff");
font-weight: 300;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-regular.woff");
font-weight: 400;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-medium.woff");
font-weight: 500;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-semibold.woff");
font-weight: 600;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-bold.woff");
font-weight: 700;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,23 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Root } from './js/components/root.js';
import { api } from './js/api.js';
import { subscription } from "./js/subscription.js";
import './css/indigo-static.css';
import './css/fonts.css';
import './css/custom.css';
window.NETWORK = 'testnet'; // 'bitcoin'
const channel = new window.channel();
api.setChannel(window.ship, channel);
if (module.hot) {
module.hot.accept()
}
ReactDOM.render((
<Root channel={channel}/>
), document.querySelectorAll("#root")[0]);

View File

@ -0,0 +1,52 @@
import _ from 'lodash';
class UrbitApi {
setChannel(ship, channel) {
this.ship = ship;
this.channel = channel;
this.bindPaths = [];
}
bind(path, method, ship = this.ship, appl = "btc-wallet", success, fail) {
this.bindPaths = _.uniq([...this.bindPaths, path]);
window.subscriptionId = this.channel.subscribe(ship, appl, path,
(err) => {
fail(err);
},
(event) => {
success({
data: event,
from: {
ship,
path
}
});
},
(err) => {
fail(err);
});
}
btcWalletCommand(data) {
return this.action("btc-wallet", "btc-wallet-command", data);
}
settingsEvent(data) {
return this.action("settings-store", "settings-event", data);
}
action(appl, mark, data) {
return new Promise((resolve, reject) => {
this.channel.poke(ship, appl, mark, data,
(json) => {
resolve(json);
},
(err) => {
reject(err);
});
});
}
}
export let api = new UrbitApi();
window.api = api;

View File

@ -0,0 +1,148 @@
import React, { Component } from 'react';
import {
Box,
Icon,
Row,
Text,
Button,
Col,
} from '@tlon/indigo-react';
import Send from './send.js'
import CurrencyPicker from './currencyPicker.js'
import { currencyToSats, satsToCurrency } from '../../lib/util.js';
export default class Balance extends Component {
constructor(props) {
super(props);
this.state = {
sending: false,
copiedButton: false,
copiedString: false,
}
this.copyAddress = this.copyAddress.bind(this);
}
copyAddress(arg) {
let address = this.props.state.address;
function listener(e) {
e.clipboardData.setData('text/plain', address);
e.preventDefault();
}
document.addEventListener('copy', listener);
document.execCommand('copy');
document.removeEventListener('copy', listener);
this.props.api.btcWalletCommand({'gen-new-address': null});
if (arg === 'button'){
this.setState({copiedButton: true});
setTimeout(() => { this.setState({copiedButton: false}); }, 2000);
} else if (arg === 'string') {
this.setState({copiedString: true});
setTimeout(() => { this.setState({copiedString: false}); }, 2000);
}
}
render() {
const sats = (this.props.state.confirmedBalance || 0);
const unconfirmedSats = this.props.state.unconfirmedBalance;
const unconfirmedString = unconfirmedSats ? ` (${unconfirmedSats}) ` : '';
const denomination = this.props.state.denomination;
const value = satsToCurrency(sats, denomination, this.props.state.currencyRates);
const sendDisabled = (sats === 0);
const addressText = (this.props.state.address === null) ? '' :
this.props.state.address.slice(0, 6) + '...' +
this.props.state.address.slice(-6);
const conversion = this.props.state.currencyRates[denomination].last;
return (
<>
{this.state.sending ?
<Send
state={this.props.state}
api={api}
psbt={this.props.state.psbt}
currencyRates={this.props.state.currencyRates}
shipWallets={this.props.state.shipWallets}
value={value}
denomination={denomination}
sats={sats}
conversion={conversion}
network={this.props.network}
error={this.props.state.error}
stopSending={() => {
this.setState({sending: false});
store.handleEvent({data: {psbt: '', fee: 0, error: '', "broadcast-fail": null}});
}}
/> :
<Col
height="400px"
width='100%'
backgroundColor="white"
borderRadius="48px"
justifyContent="space-between"
mb={5}
p={5}
>
<Row justifyContent="space-between">
<Text color="orange" fontSize={1}>Balance</Text>
<Text color="lightGray" fontSize="14px" mono style={{cursor: "pointer"}}
onClick={() => {this.copyAddress('string')}}>
{this.state.copiedString ? "copied" : addressText}
</Text>
<CurrencyPicker
api={this.props.api}
denomination={denomination}
currencies={this.props.state.currencyRates}
/>
</Row>
<Col justifyContent="center" alignItems="center">
<Text fontSize="40px" color="orange" style={{whiteSpace: "nowrap"}} >
{value}
</Text>
<Text fontSize={1} color="orange">{`${sats}${unconfirmedString} sats`}</Text>
</Col>
<Row flexDirection="row-reverse">
<Button children="Send"
disabled={sendDisabled}
fontSize={1}
fontWeight="bold"
color={sendDisabled ? "lighterGray" : "white"}
backgroundColor={sendDisabled ? "veryLightGray" : "orange"}
style={{cursor: sendDisabled ? "default" : "pointer" }}
borderColor="none"
borderRadius="24px"
py="24px"
px="24px"
onClick={() => this.setState({sending: true})}
/>
<Button children={(this.state.copiedButton) ? "Address Copied!" : "Copy Address"}
mr={3}
disabled={this.state.copiedButton}
fontSize={1}
fontWeight="bold"
color={(this.state.copiedButton) ? "green" : "orange"}
backgroundColor={(this.state.copiedButton) ? "veryLightGreen" : "midOrange" }
style={{cursor: (this.state.copiedButton) ? "default" : "pointer"}}
borderColor="none"
borderRadius="24px"
py="24px"
px="24px"
onClick={() => {this.copyAddress('button')}}
/>
</Row>
</Col>
}
</>
);
}
}

View File

@ -0,0 +1,69 @@
import React, { Component } from 'react';
import {
Box,
Icon,
Row,
Text,
LoadingSpinner,
Col,
} from '@tlon/indigo-react';
import {
Switch,
Route,
} from 'react-router-dom';
import Balance from './balance.js';
import Transactions from './transactions.js';
import Warning from './warning.js';
import Header from './header.js';
import Settings from './settings.js';
export default class Body extends Component {
constructor(props) {
super(props);
}
render() {
if (!this.props.loaded) {
return (
<Box display="flex" width="100%" height="100%" alignItems="center" justifyContent="center">
<LoadingSpinner
width={7}
height={7}
background="midOrange"
foreground="orange"
/>
</Box>
);
} else {
return (
<Switch>
<Route path="/~btc/settings">
<Col
display='flex'
flexDirection='column'
width='400px'
>
<Header settings={true} state={this.props.state}/>
<Settings state={this.props.state}
api={this.props.api}
network={this.props.network}
/>
</Col>
</Route>
<Route path="/~btc">
<Col
display='flex'
flexDirection='column'
width='400px'
>
<Header settings={false} state={this.props.state}/>
{ (!this.props.warning) ? null : <Warning api={this.props.api}/>}
<Balance api={this.props.api} state={this.props.state} network={this.props.network}/>
<Transactions state={this.props.state} network={this.props.network}/>
</Col>
</Route>
</Switch>
);
}
}
}

View File

@ -0,0 +1,214 @@
import React, { Component } from 'react';
import {
Box,
Icon,
StatelessTextInput as Input,
Row,
Text,
Button,
Col,
LoadingSpinner,
} from '@tlon/indigo-react';
import * as bitcoin from 'bitcoinjs-lib';
import * as kg from 'urbit-key-generation';
import Sent from './sent.js'
import Error from './error.js'
import { satsToCurrency } from '../../lib/util.js';
export default class BridgeInvoice extends Component {
constructor(props) {
super(props);
this.state = {
txHex: '',
ready: false,
error: this.props.state.error,
broadcasting: false,
};
this.checkTxHex = this.checkTxHex.bind(this);
this.broadCastTx = this.broadCastTx.bind(this);
this.sendBitcoin = this.sendBitcoin.bind(this);
}
broadCastTx(hex) {
let command = {
'broadcast-tx': hex
}
return this.props.api.btcWalletCommand(command)
}
componentDidMount() {
window.open('https://bridge.urbit.org/?kind=btc&utx=' + this.props.psbt);
}
componentDidUpdate(prevProps){
if (this.state.broadcasting) {
if (this.state.error !== '') {
this.setState({broadcasting: false});
}
}
if (prevProps.state.error !== this.props.state.error) {
this.setState({error: this.props.state.error});
}
}
sendBitcoin(hex) {
try {
bitcoin.Transaction.fromHex(hex)
this.broadCastTx(hex)
this.setState({broadcasting: true});
}
catch(e) {
this.setState({error: 'invalid-signed', broadcasting: false});
}
}
checkTxHex(e){
let txHex = e.target.value;
let ready = (txHex.length > 0);
let error = '';
this.setState({txHex, ready, error});
}
render() {
const { stopSending, payee, denomination, satsAmount, psbt, currencyRates } = this.props;
const { error, txHex } = this.state;
let inputColor = 'black';
let inputBg = 'white';
let inputBorder = 'lightGray';
if (error !== '') {
inputColor = 'red';
inputBg = 'veryLightRed';
inputBorder = 'red';
}
console.log('bridge invoice', error);
return (
<>
{ this.props.state.broadcastSuccess ?
<Sent
payee={payee}
stopSending={stopSending}
denomination={denomination}
currencyRates={currencyRates}
satsAmount={satsAmount}
/> :
<Col
height='400px'
width='100%'
backgroundColor='white'
borderRadius='48px'
mb={5}
p={5}
>
<Row
justifyContent='space-between'
alignItems='center'
>
<Text bold fontSize={1}>Invoice</Text>
<Icon
icon='X'
cursor='pointer'
onClick={() => stopSending()}
/>
</Row>
<Box
mt={4}
backgroundColor='rgba(0, 159, 101, 0.05)'
borderRadius='12px'
>
<Box
padding={4}
>
<Row>
<Text fontSize='14px' fontWeight='500'>You are sending</Text>
</Row>
<Row
mt={2}
>
<Text
color='green'
fontSize='14px'
>{satsToCurrency(satsAmount, denomination, currencyRates)}</Text>
<Text
ml={2}
fontSize='14px'
color='gray'
>{`${satsAmount} sats`}</Text>
</Row>
<Row
mt={2}
>
<Text fontSize='14px'>To:</Text>
<Text
ml={2}
fontSize='14px'
style={{'display': 'block', 'overflow-wrap': 'anywhere'}}
>{payee}</Text>
</Row>
</Box>
</Box>
<Box mt={3}>
<Text fontSize='14px' fontWeight='500'>
Bridge signed transaction
</Text>
</Box>
<Box mt={1} mb={2}>
<Text gray fontSize='14px'>
Copy the signed transaction from Bridge
</Text>
</Box>
<Input
value={this.state.txHex}
fontSize='14px'
placeholder='010000000001019e478cc370323ac539097...'
autoCapitalize='none'
autoCorrect='off'
color={inputColor}
backgroundColor={inputBg}
borderColor={inputBorder}
style={{'line-height': '4'}}
onChange={this.checkTxHex}
/>
{ (error !== '') &&
<Row>
<Error
error={error}
fontSize='14px'
mt={2}/>
</Row>
}
<Row
flexDirection='row-reverse'
mt={4}
alignItems="center"
>
<Button
primary
children='Send BTC'
mr={3}
fontSize={1}
borderRadius='24px'
py='24px'
px='24px'
onClick={() => this.sendBitcoin(txHex)}
disabled={!this.state.ready || error}
style={{cursor: (this.state.ready && !error) ? "pointer" : "default"}}
/>
{this.state.broadcasting ? <LoadingSpinner mr={3}/> : null}
</Row>
</Col>
}
</>
);
}
}

View File

@ -0,0 +1,50 @@
import React, { Component } from 'react';
import {
Box,
Icon,
Row,
Text,
Button,
Col,
LoadingSpinner,
} from '@tlon/indigo-react';
import _ from 'lodash';
import { satsToCurrency } from '../../lib/util.js'
import { store } from '../../store';
export default class CurrencyPicker extends Component {
constructor(props) {
super(props);
this.switchCurrency = this.switchCurrency.bind(this);
}
switchCurrency(){
let newCurrency;
if (this.props.denomination === 'BTC') {
if (this.props.currencies['USD']) {
newCurrency = "USD";
}
} else if (this.props.denomination === 'USD') {
newCurrency = "BTC";
}
let setCurrency = {
"put-entry": {
value: newCurrency,
"entry-key": "currency",
"bucket-key": "btc-wallet",
}
}
this.props.api.settingsEvent(setCurrency);
}
render() {
return (
<Row style={{cursor: "pointer"}} onClick={this.switchCurrency}>
<Icon icon="ChevronDouble" color="orange" pt="2px" pr={1} />
<Text color="orange" fontSize={1}>{this.props.denomination}</Text>
</Row>
);
}
}

View File

@ -0,0 +1,41 @@
import React, { Component } from 'react';
import { Text } from '@tlon/indigo-react';
const errorToString = (error) => {
if (error === 'cant-pay-ourselves') {
return 'Cannot pay ourselves';
}
if (error === 'no-comets') {
return 'Cannot pay comets';
}
if (error === 'no-dust') {
return 'Cannot send dust';
}
if (error === 'tx-being-signed') {
return 'Cannot pay when transaction is being signed';
}
if (error === 'insufficient-balance') {
return 'Insufficient confirmed balance';
}
if (error === 'broadcast-fail') {
return 'Transaction broadcast failed';
}
if (error === 'invalid-master-ticket') {
return 'Invalid master ticket';
}
if (error === 'invalid-signed') {
return 'Invalid signed bitcoin transaction';
}
}
export default function Error(props) {
const error = errorToString(props.error);
return(
<Text
color='red'
{...props}>
{error}
</Text>
);
}

View File

@ -0,0 +1,99 @@
import React, { Component } from 'react';
import {
Box,
Icon,
Row,
Text,
Button,
Col,
StatelessRadioButtonField as RadioButton,
Label,
} from '@tlon/indigo-react';
export default class FeePicker extends Component {
constructor(props) {
super(props);
this.state = {
selected: 'mid'
}
this.select = this.select.bind(this);
this.clickDismiss = this.clickDismiss.bind(this);
this.setModalRef = this.setModalRef.bind(this);
}
componentDidMount() {
document.addEventListener("click", this.clickDismiss);
}
componentWillUnount() {
document.removeEventListener("click", this.clickDismiss);
}
setModalRef(n) {
this.modalRef = n;
}
clickDismiss(e) {
if (this.modalRef && !(this.modalRef.contains(e.target))){
this.props.feeDismiss();
}
}
select(which) {
this.setState({selected: which});
this.props.feeSelect(which);
}
render() {
return (
<Box
ref={this.setModalRef}
position="absolute" p={4}
border="1px solid green" zIndex={10}
backgroundColor="white" borderRadius={3}
>
<Text fontSize={1} color="black" fontWeight="bold" mb={4}>
Transaction Speed
</Text>
<Col mt={4}>
<RadioButton
name="feeRadio"
selected={this.state.selected === 'low'}
p="2"
onChange={() => {
this.select('low');
}}
>
<Label fontSize="14px">Slow: {this.props.feeChoices.low[1]} sats/vbyte ~{this.props.feeChoices.low[0]}m</Label>
</RadioButton>
<RadioButton
name="feeRadio"
selected={this.state.selected === 'mid'}
p="2"
onChange={() => {
this.select('mid');
}}
>
<Label fontSize="14px">Normal: {this.props.feeChoices.mid[1]} sats/vbyte ~{this.props.feeChoices.mid[0]}m</Label>
</RadioButton>
<RadioButton
name="feeRadio"
selected={this.state.selected === 'high'}
p="2"
onChange={() => {
this.select('high');
}}
>
<Label fontSize="14px">Fast: {this.props.feeChoices.high[1]} sats/vbyte ~{this.props.feeChoices.high[0]}m</Label>
</RadioButton>
</Col>
</Box>
);
}
}

View File

@ -0,0 +1,78 @@
import React, { Component } from 'react';
import {
Box,
Icon,
Row,
Text,
} from '@tlon/indigo-react';
import { Link } from 'react-router-dom';
export default class Header extends Component {
constructor(props) {
super(props);
}
render() {
let icon = this.props.settings ? "X" : "Adjust";
let iconColor = this.props.settings ? "black" : "orange";
let iconLink = this.props.settings ? "/~btc" : "/~btc/settings";
let connection = null;
let badge = null;
if (!(this.props.state.provider && this.props.state.provider.connected)) {
connection =
<Text fontSize={1} color="red" fontWeight="bold" mr={3}>
Provider Offline
</Text>
if (!this.props.settings) {
badge = <Box borderRadius="50%" width="8px" height="8px" backgroundColor="red" position="absolute" top="0px" right="0px"></Box>
}
}
return (
<Row
height={8}
width='100%'
justifyContent="space-between"
alignItems="center"
pt={5}
pb={5}
>
<Row alignItems="center" justifyContent="center">
<Box backgroundColor="orange"
borderRadius={4} mr="12px"
width={5}
height={5}
alignItems="center"
justifyContent="center"
>
<Icon icon="Bitcoin" width={4} p={1} height={4} color="white"/>
</Box>
<Text fontSize={2} fontWeight="bold" color="orange">
Bitcoin
</Text>
</Row>
<Row alignItems="center">
{connection}
<Link to={iconLink}>
<Box backgroundColor="white"
borderRadius={4}
width={5}
height={5}
p={2}
position="relative"
>
{badge}
<Icon icon={icon} color={iconColor} />
</Box>
</Link>
</Row>
</Row>
);
}
}

View File

@ -0,0 +1,262 @@
import React, { Component } from 'react';
import {
Box,
Icon,
StatelessTextInput as Input,
Row,
Text,
Button,
Col,
LoadingSpinner,
} from '@tlon/indigo-react';
import * as bitcoin from 'bitcoinjs-lib';
import * as kg from 'urbit-key-generation';
import * as bip39 from 'bip39';
import Sent from './sent.js'
import { patp2dec, isValidPatq } from 'urbit-ob';
import { satsToCurrency } from '../../lib/util.js';
import Error from './error.js';
const BITCOIN_MAINNET_INFO = {
messagePrefix: '\x18Bitcoin Signed Message:\n',
bech32: 'bc',
bip32: {
public: 0x04b24746,
private: 0x04b2430c,
},
pubKeyHash: 0x00,
scriptHash: 0x05,
wif: 0x80,
};
const BITCOIN_TESTNET_INFO = {
messagePrefix: '\x18Bitcoin Signed Message:\n',
bech32: 'tb',
bip32: {
public: 0x045f1cf6,
private: 0x045f18bc,
},
pubKeyHash: 0x6f,
scriptHash: 0xc4,
wif: 0xef,
};
export default class Invoice extends Component {
constructor(props) {
super(props);
this.state = {
masterTicket: '',
ready: false,
error: this.props.state.error,
sent: false,
broadcasting: false,
};
this.checkTicket = this.checkTicket.bind(this);
this.broadCastTx = this.broadCastTx.bind(this);
this.sendBitcoin = this.sendBitcoin.bind(this);
}
componentDidUpdate(prevProps, prevState) {
if (this.state.broadcasting) {
if (this.state.error !== '') {
this.setState({broadcasting: false});
}
}
}
broadCastTx(psbtHex) {
let command = {
'broadcast-tx': psbtHex
}
return this.props.api.btcWalletCommand(command)
}
sendBitcoin(ticket, psbt) {
const newPsbt = bitcoin.Psbt.fromBase64(psbt);
this.setState({broadcasting: true});
kg.generateWallet({ ticket, ship: parseInt(patp2dec('~' + window.ship)) })
.then(urbitWallet => {
const { xpub } = this.props.network === 'testnet'
? urbitWallet.bitcoinTestnet.keys
: urbitWallet.bitcoinMainnet.keys;
const { xprv: zprv } = urbitWallet.bitcoinMainnet.keys;
const { xprv: vprv } = urbitWallet.bitcoinTestnet.keys;
const isTestnet = (this.props.network === 'testnet');
const derivationPrefix = isTestnet ? "m/84'/1'/0'/" : "m/84'/0'/0'/";
const btcWallet = (isTestnet)
? bitcoin.bip32.fromBase58(vprv, BITCOIN_TESTNET_INFO)
: bitcoin.bip32.fromBase58(zprv, BITCOIN_MAINNET_INFO);
try {
const hex = newPsbt.data.inputs
.reduce((psbt, input, idx) => {
// removing already derived part, eg m/84'/0'/0'/0/0 becomes 0/0
const path = input.bip32Derivation[0].path
.split(derivationPrefix)
.join('');
const prv = btcWallet.derivePath(path).privateKey;
return psbt.signInput(idx, bitcoin.ECPair.fromPrivateKey(prv));
}, newPsbt)
.finalizeAllInputs()
.extractTransaction()
.toHex();
this.broadCastTx(hex);
}
catch(e) {
this.setState({error: 'invalid-master-ticket', broadcasting: false});
}
});
}
checkTicket(e){
// TODO: port over bridge ticket validation logic
let masterTicket = e.target.value;
let ready = isValidPatq(masterTicket);
let error = (ready) ? '' : 'invalid-master-ticket';
this.setState({masterTicket, ready, error});
}
render() {
const broadcastSuccess = this.props.state.broadcastSuccess;
const { stopSending, payee, denomination, satsAmount, psbt, currencyRates } = this.props;
const { sent, error } = this.state;
let inputColor = 'black';
let inputBg = 'white';
let inputBorder = 'lightGray';
if (error !== '') {
inputColor = 'red';
inputBg = 'veryLightRed';
inputBorder = 'red';
}
return (
<>
{ broadcastSuccess ?
<Sent
payee={payee}
stopSending={stopSending}
denomination={denomination}
currencyRates={currencyRates}
satsAmount={satsAmount}
/> :
<Col
height='400px'
width='100%'
backgroundColor='white'
borderRadius='48px'
mb={5}
p={5}
>
<Row
justifyContent='space-between'
alignItems='center'
>
<Text bold fontSize={1}>Invoice</Text>
<Icon
icon='X'
cursor='pointer'
onClick={() => stopSending()}
/>
</Row>
<Box
mt={4}
backgroundColor='rgba(0, 159, 101, 0.05)'
borderRadius='12px'
>
<Box
padding={4}
>
<Row>
<Text fontSize='14px' fontWeight='500'>You are sending</Text>
</Row>
<Row
mt={2}
>
<Text
color='green'
fontSize='14px'
>{satsToCurrency(satsAmount, denomination, currencyRates)}</Text>
<Text
ml={2}
fontSize='14px'
color='gray'
>{`${satsAmount} sats`}</Text>
</Row>
<Row
mt={2}
>
<Text fontSize='14px'>To:</Text>
<Text
ml={2}
fontSize='14px'
style={{'display': 'block', 'overflow-wrap': 'anywhere'}}
>{payee}</Text>
</Row>
</Box>
</Box>
<Box mt={3} mb={2}>
<Text gray fontSize={1} fontWeight='600'>
Master Key
</Text>
</Box>
<Input
value={this.state.masterTicket}
fontSize="14px"
type="password"
name="masterTicket"
obscure={value => value.replace(/[^~-]+/g, '••••••')}
placeholder="••••••-••••••-••••••-••••••"
autoCapitalize="none"
autoCorrect="off"
color={inputColor}
backgroundColor={inputBg}
borderColor={inputBorder}
onChange={this.checkTicket}
/>
{(error !== '') &&
<Row>
<Error
fontSize='14px'
color='red'
error={error}
mt={2}/>
</Row>
}
<Row
flexDirection='row-reverse'
mt={4}
alignItems="center"
>
<Button
primary
children='Send BTC'
mr={3}
fontSize={1}
borderRadius='24px'
py='24px'
px='24px'
onClick={() => this.sendBitcoin(this.state.masterTicket, psbt)}
disabled={!this.state.ready || error || this.state.broadcasting}
style={{cursor: (this.state.ready && !error && !this.state.broadcasting) ? "pointer" : "default"}}
/>
{ (this.state.broadcasting) ? <LoadingSpinner mr={3}/> : null}
</Row>
</Col>
}
</>
);
}
}

View File

@ -0,0 +1,153 @@
import React, { Component } from 'react';
import {
Box,
Text,
Button,
StatelessTextInput,
Icon,
Row,
Input,
LoadingSpinner,
} from '@tlon/indigo-react';
import { isValidPatp } from 'urbit-ob';
export default class ProviderModal extends Component {
constructor(props) {
super(props);
this.state = {
potentialProvider: null,
checkingProvider: false,
providerFailed: false,
ready: false,
provider: null,
connecting: false,
}
this.checkProvider = this.checkProvider.bind(this);
this.submitProvider = this.submitProvider.bind(this);
}
checkProvider(e) {
// TODO: loading states
let provider = e.target.value;
let ready = false;
let checkingProvider = false;
let potentialProvider = this.state.potentialProvider;
if (isValidPatp(provider)) {
let command = {
"check-provider": provider
}
potentialProvider = provider;
checkingProvider = true;
this.props.api.btcWalletCommand(command);
setTimeout(() => {
this.setState({providerFailed: true, checkingProvider: false});
}, 5000);
}
this.setState({provider, ready, checkingProvider, potentialProvider});
}
componentDidUpdate(prevProps, prevState){
if (!this.state.ready){
if (this.props.providerPerms[this.state.provider]) {
this.setState({ready: true, checkingProvider: false, providerFailed: false});
}
}
}
submitProvider(e){
if (this.state.ready){
let command = {
"set-provider": this.state.provider
}
this.props.api.btcWalletCommand(command);
this.setState({connecting: true});
}
}
render() {
let workingNode = null;
let workingColor = null;
let workingBg = null;
if (this.state.ready) {
workingColor = "green";
workingBg = "veryLightGreen"
workingNode =
<Box mt={3}>
<Text fontSize="14px" color="green">
{this.state.provider} is a working provider node
</Text>
</Box>
} else if (this.state.providerFailed) {
workingColor = "red";
workingBg = "veryLightRed"
workingNode =
<Box mt={3}>
<Text fontSize="14px" color="red">
{this.state.potentialProvider} is not a working provider node
</Text>
</Box>
}
return (
<Box
width="100%"
height="100%"
padding={3}
>
<Row>
<Icon icon="Bitcoin" mr={2}/>
<Text fontSize="14px" fontWeight="bold">
Step 1 of 2: Set up Bitcoin Provider Node
</Text>
</Row>
<Box mt={3}>
<Text fontSize="14px" fontWeight="regular" color="gray">
In order to perform Bitcoin transaction in Landscape, you'll need to set a provider node. A provider node is an urbit which maintains a synced Bitcoin ledger.
<a fontSize="14px" target="_blank" href="https://urbit.org/bitcoin-wallet"> Learn More</a>
</Text>
</Box>
<Box mt={3} mb={2}>
<Text fontSize="14px" fontWeight="500">
Provider Node
</Text>
</Box>
<Row alignItems="center">
<StatelessTextInput
mr={2}
width="256px"
fontSize="14px"
type="text"
name="masterTicket"
placeholder="e.g. ~zod"
autoCapitalize="none"
autoCorrect="off"
mono
backgroundColor={workingBg}
color={workingColor}
borderColor={workingColor}
onChange={this.checkProvider}
/>
{(this.state.checkingProvider) ? <LoadingSpinner/> : null}
</Row>
{workingNode}
<Row alignItems="center" mt={3}>
<Button
mr={2}
primary
disabled={!this.state.ready}
children="Set Peer Node"
fontSize="14px"
style={{cursor: this.state.ready ? "pointer" : "default"}}
onClick={() => {this.submitProvider(this.state.provider)}}
/>
{(this.state.connecting) ? <LoadingSpinner/> : null}
</Row>
</Box>
);
}
}

View File

@ -0,0 +1,445 @@
import React, { Component } from 'react';
import {
Box,
Icon,
StatelessTextInput as Input,
Row,
Text,
Button,
Col,
LoadingSpinner,
StatelessRadioButtonField as RadioButton,
} from '@tlon/indigo-react';
import Invoice from './invoice.js'
import BridgeInvoice from './bridgeInvoice.js'
import FeePicker from './feePicker.js'
import Error from './error.js'
import Signer from './signer.js'
import { validate } from 'bitcoin-address-validation';
import * as ob from 'urbit-ob';
export default class Send extends Component {
constructor(props) {
super(props);
this.state = {
signing: false,
denomAmount: '0.00',
satsAmount: '0',
payee: '',
checkingPatp: false,
payeeType: '',
ready: false,
validPayee: false,
focusPayee: true,
focusCurrency: false,
focusSats: false,
focusNote: false,
submitting: false,
feeChoices: {
low: [10, 1],
mid: [10, 1],
high: [10, 1],
},
feeValue: "mid",
showModal: false,
note: '',
choosingSignMethod: false,
signMethod: 'Sign Transaction',
};
this.initPayment = this.initPayment.bind(this);
this.checkPayee = this.checkPayee.bind(this);
this.feeSelect = this.feeSelect.bind(this);
this.feeDismiss = this.feeDismiss.bind(this);
this.toggleSignMethod = this.toggleSignMethod.bind(this);
this.setSignMethod = this.setSignMethod.bind(this);
}
feeDismiss() {
this.setState({showModal: false});
}
feeSelect(which) {
this.setState({feeValue: which});
}
componentDidMount(){
if (this.props.network === 'bitcoin'){
let url = "https://bitcoiner.live/api/fees/estimates/latest";
fetch(url).then(res => res.json()).then(n => {
let estimates = Object.keys(n.estimates);
let mid = Math.floor(estimates.length/2)
let high = estimates.length - 1;
this.setState({
feeChoices: {
low: [30, n.estimates[30]["sat_per_vbyte"]],
mid: [180, n.estimates[180]["sat_per_vbyte"]],
high: [360, n.estimates[360]["sat_per_vbyte"]],
}
});
})
}
}
setSignMethod(signMethod) {
this.setState({signMethod});
}
checkPayee(e){
store.handleEvent({data: {error: ''}});
let payee = e.target.value;
let isPatp = ob.isValidPatp(payee);
let isAddress = validate(payee);
if (isPatp) {
let command = {'check-payee': payee}
this.props.api.btcWalletCommand(command)
setTimeout(() => {
this.setState({checkingPatp: false});
}, 5000);
this.setState({
checkingPatp: true,
payeeType: 'ship',
payee,
});
} else if (isAddress) {
this.setState({
payee,
ready: true,
checkingPatp: false,
payeeType: 'address',
validPayee: true,
});
} else {
this.setState({
payee,
ready: false,
checkingPatp: false,
payeeType: '',
validPayee: false,
});
}
}
componentDidUpdate(prevProps, prevState) {
if ((prevProps.error !== this.props.error) &&
(this.props.error !== '') && (this.props.error !== 'broadcast-fail')) {
this.setState({signing: false});
}
if (!this.state.ready && this.state.checkingPatp) {
if (this.props.shipWallets[this.state.payee.slice(1)]) {
this.setState({ready: true, checkingPatp: false, validPayee: true});
}
}
}
toggleSignMethod(toggle) {
this.setState({choosingSignMethod: !toggle});
}
initPayment() {
if (this.state.payeeType === 'ship') {
let command = {
'init-payment': {
'payee': this.state.payee,
'value': parseInt(this.state.satsAmount),
'feyb': this.state.feeChoices[this.state.feeValue][1],
'note': (this.state.note || null),
}
}
this.props.api.btcWalletCommand(command).then(res => this.setState({signing: true}));
} else if (this.state.payeeType === 'address') {
let command = {
'init-payment-external': {
'address': this.state.payee,
'value': parseInt(this.state.satsAmount),
'feyb': 1,
'note': (this.state.note || null),
}
}
this.props.api.btcWalletCommand(command).then(res => this.setState({signing: true}));
}
}
render() {
let payeeColor = "black";
let payeeBg = "white";
let payeeBorder = "lightGray";
if (this.props.error) {
payeeColor="red";
payeeBorder = "red";
payeeBg="veryLightRed";
} else if (this.state.focusPayee && this.state.validPayee) {
payeeColor = "green";
payeeBorder = "green";
payeeBg = "veryLightGreen";
} else if (!this.state.focusPayee && this.state.validPayee){
payeeColor="blue";
payeeBorder = "white";
payeeBg = "white";
} else if (!this.state.focusPayee && !this.state.validPayee) {
payeeColor="red";
payeeBorder = "red";
payeeBg="veryLightRed";
} else if (this.state.focusPayee &&
!this.state.validPayee &&
!this.state.checkingPatp &&
this.state.payeeType === 'ship'){
payeeColor="red";
payeeBorder = "red";
payeeBg="veryLightRed";
}
const { api, value, conversion, stopSending, denomination, psbt, currencyRates, error, network } = this.props;
const { denomAmount, satsAmount, signing, payee, choosingSignMethod, signMethod } = this.state;
const signReady = (this.state.ready && (parseInt(this.state.satsAmount) > 0)) && !signing;
let invoice = null;
if (signMethod === 'Sign Transaction') {
invoice =
<Invoice
network={network}
api={api}
psbt={psbt}
currencyRates={currencyRates}
stopSending={stopSending}
payee={payee}
denomination={denomination}
satsAmount={satsAmount}
state={this.props.state}
/>
} else if (signMethod === 'Sign with Bridge') {
invoice =
<BridgeInvoice
state={this.props.state}
api={api}
psbt={psbt}
currencyRates={currencyRates}
stopSending={stopSending}
payee={payee}
denomination={denomination}
satsAmount={satsAmount}
/>
}
return (
<>
{ (signing && psbt) ? invoice :
<Col
width='100%'
backgroundColor='white'
borderRadius='48px'
mb={5}
p={5}
>
<Col width="100%">
<Row
justifyContent='space-between'
alignItems='center'
>
<Text highlight color='blue' fontSize={1}>Send BTC</Text>
<Text highlight color='blue' fontSize={1}>{value}</Text>
<Icon
icon='X'
cursor='pointer'
onClick={() => stopSending()}
/>
</Row>
<Row
alignItems='center'
mt={6}
justifyContent='space-between'>
<Row justifyContent="space-between" width='calc(40% - 30px)' alignItems="center">
<Text gray fontSize={1} fontWeight='600'>To</Text>
{this.state.checkingPatp ?
<LoadingSpinner background="midOrange" foreground="orange"/> : null
}
</Row>
<Input
autoFocus
onFocus={() => {this.setState({focusPayee: true})}}
onBlur={() => {this.setState({focusPayee: false})}}
color={payeeColor}
backgroundColor={payeeBg}
borderColor={payeeBorder}
ml={2}
flexGrow="1"
fontSize='14px'
placeholder='~sampel-palnet or BTC address'
value={payee}
fontFamily="mono"
disabled={signing}
onChange={this.checkPayee}
/>
</Row>
{error &&
<Row
alignItems='center'
justifyContent='space-between'>
{/* yes this is a hack */}
<Box width='calc(40% - 30px)'/>
<Error
error={error}
fontSize='14px'
ml={2}
mt={2}
width='100%' />
</Row>
}
<Row
alignItems='center'
mt={4}
justifyContent='space-between'>
<Text
gray
fontSize={1}
fontWeight='600'
width="40%"
>Amount</Text>
<Input
onFocus={() => {this.setState({focusCurrency: true})}}
onBlur={() => {this.setState({focusCurrency: false})}}
fontSize='14px'
width='100%'
type='number'
borderColor={this.state.focusCurrency ? "lightGray" : "none"}
disabled={signing}
value={denomAmount}
onChange={e => {
this.setState({
denomAmount: e.target.value,
satsAmount: Math.round(parseFloat(e.target.value) / conversion * 100000000)
});
}}
/>
<Text color="lighterGray" fontSize={1} ml={3}>{denomination}</Text>
</Row>
<Row
alignItems='center'
mt={2}
justifyContent='space-between'>
{/* yes this is a hack */}
<Box width='40%'/>
<Input
onFocus={() => {this.setState({focusSats: true})}}
onBlur={() => {this.setState({focusSats: false})}}
fontSize='14px'
width='100%'
type='number'
borderColor={this.state.focusSats ? "lightGray" : "none"}
disabled={signing}
value={satsAmount}
onChange={e => {
this.setState({
denomAmount: parseFloat(e.target.value) * (conversion / 100000000),
satsAmount: e.target.value
});
}}
/>
<Text color="lightGray" fontSize={1} ml={3}>sats</Text>
</Row>
<Row mt={4} width="100%" justifyContent="space-between">
<Text
gray
fontSize={1}
fontWeight='600'
width="40%"
>Fee</Text>
<Row alignItems="center">
<Text mr={2} color="lightGray" fontSize="14px">
{this.state.feeChoices[this.state.feeValue][1]} sats/vbyte
</Text>
<Icon icon="ChevronSouth"
fontSize="14px"
color="lightGray"
onClick={() => {if (!this.state.showModal) this.setState({showModal: true}); }}
cursor="pointer"/>
</Row>
</Row>
<Col alignItems="center">
{!this.state.showModal ? null :
<FeePicker
feeChoices={this.state.feeChoices}
feeSelect={this.feeSelect}
feeDismiss={this.feeDismiss}
/>
}
</Col>
<Row mt={4} width="100%"
justifyContent="space-between"
alignItems='center'
>
<Text
gray
fontSize={1}
fontWeight='600'
width="40%"
>Note</Text>
<Input
onFocus={() => {this.setState({focusNote: true})}}
onBlur={() => {this.setState({focusNote: false})}}
fontSize='14px'
width='100%'
placeholder="What's this for?"
type='text'
borderColor={this.state.focusNote ? "lightGray" : "none"}
disabled={signing}
value={this.state.note}
onChange={e => {
this.setState({
note: e.target.value,
});
}}
/>
</Row>
</Col>
<Row
flexDirection='row-reverse'
alignItems="center"
mt={4}
>
<Signer
signReady={signReady}
choosingSignMethod={choosingSignMethod}
signMethod={signMethod}
setSignMethod={this.setSignMethod}
initPayment={this.initPayment} />
{ (!(signing && !error)) ? null :
<LoadingSpinner mr={2} background="midOrange" foreground="orange"/>
}
<Button
width='48px'
children={
<Icon
icon={choosingSignMethod ? 'X' : 'Ellipsis'}
color={signReady ? 'blue' : 'lighterGray'}
/>
}
fontSize={1}
fontWeight='bold'
borderRadius='24px'
mr={2}
py='24px'
px='24px'
onClick={() => this.toggleSignMethod(choosingSignMethod)}
color={signReady ? 'white' : 'lighterGray'}
backgroundColor={signReady ? 'rgba(33, 157, 255, 0.2)' : 'veryLightGray'}
disabled={!signReady}
border='none'
style={{cursor: signReady ? 'pointer' : 'default'}} />
</Row>
</Col>
}
</>
);
}
}

View File

@ -0,0 +1,59 @@
import React, { Component } from 'react';
import {
Box,
Icon,
StatelessTextInput as Input,
Row,
Center,
Text,
Button,
Col,
} from '@tlon/indigo-react';
import { satsToCurrency } from '../../lib/util.js';
export default function Sent(props) {
const { payee, denomination, satsAmount, stopSending, currencyRates } = props;
return (
<Col
height='400px'
width='100%'
backgroundColor='orange'
borderRadius='48px'
mb={5}
p={5}
>
<Row
flexDirection='row-reverse'
>
<Icon
color='white'
icon='X'
cursor='pointer'
onClick={stopSending}
/>
</Row>
<Center>
<Text
style={{'display': 'block', 'overflow-wrap': 'anywhere'}}
color='white'>{`You sent BTC to ${payee}`}</Text>
</Center>
<Center
flexDirection='column'
flex='1 1 auto'
>
<Text
color='white'
fontSize='40px'
>
{satsToCurrency(satsAmount, denomination, currencyRates)}
</Text>
<Text
color='white'
>
{`${satsAmount} sats`}
</Text>
</Center>
</Col>
);
}

View File

@ -0,0 +1,121 @@
import React, { Component } from 'react';
import {
Box,
Icon,
Row,
Text,
Button,
Col,
} from '@tlon/indigo-react';
export default class Settings extends Component {
constructor(props) {
super(props);
this.changeProvider = this.changeProvider.bind(this);
this.replaceWallet = this.replaceWallet.bind(this);
}
changeProvider(){
this.props.api.btcWalletCommand({'set-provider': null});
}
replaceWallet(){
this.props.api.btcWalletCommand({
'delete-wallet': this.props.state.wallet,
});
}
render() {
let connColor = "red";
let connBackground = "veryLightRed";
let conn = 'Offline'
let host = '';
if (this.props.state.provider){
if (this.props.state.provider.connected) conn = 'Connected';
if (this.props.state.provider.host) host = this.props.state.provider.host;
if (this.props.state.provider.connected && this.props.state.provider.host) {
connColor = "orange";
connBackground = "lightOrange";
}
}
return (
<Col
display="flex"
width="100%"
p={5}
mb={5}
borderRadius="48px"
backgroundColor="white"
>
<Row mb="12px">
<Text fontSize={1} fontWeight="bold" color="black">
XPub Derivation
</Text>
</Row>
<Row borderRadius="12px"
backgroundColor="veryLightGray"
py={5}
px="36px"
mb="12px"
alignItems="center"
justifyContent="space-between"
>
<Text mono
fontSize={1}
style={{wordBreak: "break-all"}}
color="gray"
>
{this.props.state.wallet}
</Text>
</Row>
<Row width="100%" mb={5}>
<Button children="Replace Wallet"
width="100%"
fontSize={1}
fontWeight="bold"
backgroundColor="gray"
color="white"
borderColor="none"
borderRadius="12px"
p={4}
onClick={this.replaceWallet}
/>
</Row>
<Row mb="12px">
<Text fontSize={1} fontWeight="bold" color="black">
BTC Node Provider
</Text>
</Row>
<Col mb="12px"
py={5}
px="36px"
borderRadius="12px"
backgroundColor={connBackground}
alignItems="center"
justifyContent="space-between"
>
<Text fontSize={1} color={connColor} mono>
~{host}
</Text>
<Text fontSize={0} color={connColor}>
{conn}
</Text>
</Col>
<Row width="100%">
<Button children="Change Provider"
width="100%"
fontSize={1}
fontWeight="bold"
backgroundColor="orange"
color="white"
borderColor="none"
borderRadius="12px"
p={4}
onClick={this.changeProvider}
/>
</Row>
</Col>
);
}
}

View File

@ -0,0 +1,67 @@
import React, { memo } from 'react';
import { sigil, reactRenderer } from '@tlon/sigil-js';
import { Box } from '@tlon/indigo-react';
export const foregroundFromBackground = (background) => {
const rgb = {
r: parseInt(background.slice(1, 3), 16),
g: parseInt(background.slice(3, 5), 16),
b: parseInt(background.slice(5, 7), 16)
};
const brightness = (299 * rgb.r + 587 * rgb.g + 114 * rgb.b) / 1000;
const whiteBrightness = 255;
return whiteBrightness - brightness < 50 ? 'black' : 'white';
};
export const Sigil = memo(
({
classes = '',
color,
foreground = '',
ship,
size,
svgClass = '',
icon = false,
padding = 0,
display = 'inline-block'
}) => {
const innerSize = Number(size) - 2 * padding;
const paddingPx = `${padding}px`;
const foregroundColor = foreground
? foreground
: foregroundFromBackground(color);
return ship.length > 14 ? (
<Box
backgroundColor={color}
borderRadius={icon ? '1' : '0'}
display={display}
height={size}
width={size}
className={classes}
/>
) : (
<Box
display={display}
borderRadius={icon ? '1' : '0'}
flexBasis={size}
backgroundColor={color}
padding={paddingPx}
className={classes}
>
{sigil({
patp: ship,
renderer: reactRenderer,
size: innerSize,
icon,
colors: [color, foregroundColor],
class: svgClass
})}
</Box>
);
}
);
Sigil.displayName = 'Sigil';
export default Sigil;

View File

@ -0,0 +1,55 @@
import React, { Component } from 'react';
import {
Box,
Button,
} from '@tlon/indigo-react';
export default function Signer(props) {
const { signReady, initPayment, choosingSignMethod, signMethod, setSignMethod } = props;
return (
choosingSignMethod ?
<Box
borderRadius='24px'
backgroundColor='rgba(33, 157, 255, 0.2)'
>
<Button
border='none'
backgroundColor='transparent'
fontWeight='bold'
cursor='pointer'
color={(signMethod === 'Sign Transaction') ? 'blue' : 'lightBlue'}
py='24px'
px='24px'
onClick={() => setSignMethod('Sign Transaction')}
children='Sign Transaction' />
<Button
border='none'
backgroundColor='transparent'
fontWeight='bold'
cursor='pointer'
color={(signMethod === 'Sign with Bridge') ? 'blue' : 'lightBlue'}
py='24px'
px='24px'
onClick={() => setSignMethod('Sign with Bridge')}
children='Sign with Bridge' />
</Box>
:
<Button
primary
children={signMethod}
fontSize={1}
fontWeight='bold'
borderRadius='24px'
py='24px'
px='24px'
onClick={initPayment}
color={signReady ? 'white' : 'lighterGray'}
backgroundColor={signReady ? 'blue' : 'veryLightGray'}
disabled={!signReady}
border='none'
style={{cursor: signReady ? 'pointer' : 'default'}}
/>
)
}

View File

@ -0,0 +1,52 @@
import React, { Component } from 'react';
import { Box } from '@tlon/indigo-react';
import WalletModal from './walletModal.js'
import ProviderModal from './providerModal.js'
export default class StartupModal extends Component {
constructor(props) {
super(props);
}
render() {
let modal = null;
if (this.props.state.wallet && this.props.state.provider) {
return null;
} else if (!this.props.state.provider){
modal =
<ProviderModal
api={this.props.api}
providerPerms={this.props.state.providerPerms}
/>
} else if (!this.props.state.wallet){
modal = <WalletModal api={this.props.api} network={this.props.network}/>
}
return (
<Box
backgroundColor="scales.black20"
left="0px"
top="0px"
width="100%"
height="100%"
position="fixed"
display="flex"
zIndex={10}
justifyContent="center"
alignItems="center"
>
<Box display="flex"
flexDirection="column"
width='400px'
backgroundColor="white"
borderRadius={3}
>
{modal}
</Box>
</Box>
);
}
}

View File

@ -0,0 +1,105 @@
import React, { Component } from 'react';
import {
Box,
Icon,
Row,
Text,
Button,
Col,
LoadingSpinner,
} from '@tlon/indigo-react';
import _ from 'lodash';
import { Sigil } from './sigil.js'
import TxAction from './tx-action.js'
import TxCounterparty from './tx-counterparty.js'
import { satsToCurrency } from '../../lib/util.js'
export default class Transaction extends Component {
constructor(props) {
super(props);
}
render() {
const pending = (!this.props.tx.recvd);
let weSent = _.find(this.props.tx.inputs, (input) => {
return (input.ship === window.ship);
});
let weRecv = this.props.tx.outputs.every((output) => {
return (output.ship === window.ship)
});
let action =
(weRecv) ? "recv" :
(weSent) ? "sent" : "recv";
let counterShip = null;
let counterAddress = null;
let value;
let sign;
if (action === "sent") {
let counter = _.find(this.props.tx.outputs, (output) => {
return (output.ship !== window.ship);
});
counterShip = _.get(counter, 'ship', null);
counterAddress = _.get(counter, 'val.address', null);
value = _.get(counter, 'val.value', null);
sign = '-'
}
else if (action === "recv") {
value = _.reduce(this.props.tx.outputs, (sum, output) => {
if (output.ship === window.ship) {
return sum + output.val.value;
} else {
return sum;
}
}, 0);
if (weSent && weRecv) {
counterAddress = _.get(_.find(this.props.tx.inputs, (input) => {
return (input.ship === window.ship);
}), 'val.address', null);
} else {
let counter = _.find(this.props.tx.inputs, (input) => {
return (input.ship !== window.ship);
});
counterShip = _.get(counter, 'ship', null);
counterAddress = _.get(counter, 'val.address', null);
}
sign = '';
}
let currencyValue = sign + satsToCurrency(value, this.props.denom, this.props.rates);
const failure = Boolean(this.props.tx.failure);
if (failure) action = "fail";
const txid = this.props.tx.txid.dat.slice(2).replaceAll('.','');
return (
<Col
width='100%'
backgroundColor="white"
justifyContent="space-between"
mb="16px"
>
<Row justifyContent="space-between" alignItems="center">
<TxAction action={action} pending={pending} txid={txid} network={this.props.network}/>
<Text fontSize="14px" alignItems="center" color="gray">
{sign}{value} sats
</Text>
</Row>
<Box ml="11px" borderLeft="2px solid black" height="4px">
</Box>
<Row justifyContent="space-between" alignItems="center">
<TxCounterparty address={counterAddress} ship={counterShip}/>
<Text fontSize="14px">{currencyValue}</Text>
</Row>
</Col>
);
}
}

View File

@ -0,0 +1,62 @@
import React, { Component } from 'react';
import {
Box,
Icon,
Row,
Text,
Button,
Col,
} from '@tlon/indigo-react';
import Transaction from './transaction.js';
export default class Transactions extends Component {
constructor(props) {
super(props);
}
render() {
if (!this.props.state.history || this.props.state.history.length <= 0) {
return (
<Box alignItems="center"
display="flex"
justifyContent="center"
height="340px"
width="100%"
p={5}
mb={5}
borderRadius="48px"
backgroundColor="white"
>
<Text color="gray" fontSize={2} fontWeight="bold">No Transactions Yet</Text>
</Box>
);
} else {
return (
<Col
width='100%'
backgroundColor="white"
borderRadius="48px"
mb={5}
p={5}
>
{
this.props.state.history.map((tx, i) => {
return(
<Transaction
tx={tx}
key={i}
denom={this.props.state.denomination}
rates={this.props.state.currencyRates}
network={this.props.network}
/>
);
})
}
</Col>
);
}
}
}

View File

@ -0,0 +1,73 @@
import React, { Component } from 'react';
import {
Box,
Icon,
Row,
Text,
Button,
Col,
LoadingSpinner,
} from '@tlon/indigo-react';
import { Sigil } from './sigil.js'
export default class TxAction extends Component {
constructor(props) {
super(props);
}
render() {
const leftIcon =
(this.props.action === "sent") ? "ArrowSouth" :
(this.props.action === "recv") ? "ArrowNorth" :
(this.props.action === "fail") ? "X" :
"NullIcon";
const actionColor =
(this.props.action === "sent") ? "sentBlue" :
(this.props.action === "recv") ? "recvGreen" :
(this.props.action === "fail") ? "gray" :
"red";
const actionText =
(this.props.action === "sent" && !this.props.pending) ? "Sent BTC" :
(this.props.action === "sent" && this.props.pending) ? "Sending BTC" :
(this.props.action === "recv" && !this.props.pending) ? "Received BTC" :
(this.props.action === "recv" && this.props.pending) ? "Receiving BTC" :
(this.props.action === "fail") ? "Failed" :
"error";
const pending = (!this.props.pending) ? null :
<LoadingSpinner
background="midOrange"
foreground="orange"
/>
const url = (this.props.network === 'testnet')
? `http://blockstream.info/testnet/tx/${this.props.txid}`
: `http://blockstream.info/tx/${this.props.txid}`;
return (
<Row alignItems="center">
<Box backgroundColor={actionColor}
width="24px"
height="24px"
textAlign="center"
alignItems="center"
borderRadius="2px"
mr={2}
p={1}
>
<Icon icon={leftIcon} color="white"/>
</Box>
<Text color={actionColor} fontSize="14px">{actionText}</Text>
<a href={url} target="_blank">
<Icon color={actionColor} icon="ArrowNorthEast" ml={1} mr={2}/>
</a>
{pending}
</Row>
);
}
}

View File

@ -0,0 +1,53 @@
import React, { Component } from 'react';
import {
Box,
Icon,
Row,
Text,
Button,
Col,
} from '@tlon/indigo-react';
import { Sigil } from './sigil.js'
import TxAction from './tx-action.js'
export default class TxCounterparty extends Component {
constructor(props) {
super(props);
}
render() {
const icon = (this.props.ship)
? <Sigil
ship={this.props.ship}
size={24}
color="black"
classes={''}
icon
padding={5}
/>
: <Box backgroundColor="lighterGray"
width="24px"
height="24px"
textAlign="center"
alignItems="center"
borderRadius="2px"
p={1}
>
<Icon icon="Bitcoin" color="gray"/>
</Box>
const addressText = (!this.props.address) ? '' :
this.props.address.slice(0, 6) + '...' +
this.props.address.slice(-6);
const text = (this.props.ship) ?
`~${this.props.ship}` : addressText;
return (
<Row alignItems="center">
{icon}
<Text ml={2} mono fontSize="14px" color="gray">{text}</Text>
</Row>
);
}
}

View File

@ -0,0 +1,249 @@
import React, { Component } from 'react';
import {
Box,
Text,
Button,
StatelessTextInput,
Icon,
Row,
Input,
LoadingSpinner,
} from '@tlon/indigo-react';
import { patp2dec, isValidPatq } from 'urbit-ob';
const kg = require('urbit-key-generation');
const bitcoin = require('bitcoinjs-lib');
const bs58check = require('bs58check')
import { Buffer } from 'buffer';
export default class WalletModal extends Component {
constructor(props) {
super(props);
this.state = {
mode: 'masterTicket',
masterTicket: '',
confirmedMasterTicket: '',
xpub: '',
readyToSubmit: false,
processingSubmission: false,
confirmingMasterTicket: false,
error: false,
}
this.checkTicket = this.checkTicket.bind(this);
this.checkXPub = this.checkXPub.bind(this);
this.submitMasterTicket = this.submitMasterTicket.bind(this);
this.submitXPub = this.submitXPub.bind(this);
}
checkTicket(e){
// TODO: port over bridge ticket validation logic
if (this.state.confirmingMasterTicket) {
let confirmedMasterTicket = e.target.value;
let readyToSubmit = isValidPatq(confirmedMasterTicket);
this.setState({confirmedMasterTicket, readyToSubmit});
} else {
let masterTicket = e.target.value;
let readyToSubmit = isValidPatq(masterTicket);
this.setState({masterTicket, readyToSubmit});
}
}
checkXPub(e){
let xpub = e.target.value;
let readyToSubmit = (xpub.length > 0);
this.setState({xpub, readyToSubmit});
}
submitMasterTicket(ticket){
this.setState({processingSubmission: true});
kg.generateWallet({ ticket, ship: parseInt(patp2dec('~' + window.ship)) })
.then(urbitWallet => {
const { xpub } = this.props.network === 'testnet'
? urbitWallet.bitcoinTestnet.keys :
urbitWallet.bitcoinMainnet.keys
this.submitXPub(xpub);
});
}
submitXPub(xpub){
const command = {
"add-wallet": {
"xpub": xpub,
"fprint": [4, 0],
"scan-to": null,
"max-gap": 8,
"confs": 1
}
}
api.btcWalletCommand(command);
this.setState({processingSubmission: true});
}
render() {
const buttonDisabled = (!this.state.readyToSubmit || this.state.processingSubmission );
const inputDisabled = this.state.processingSubmission;
const processingSpinner = (!this.state.processingSubmission) ? null :
<LoadingSpinner/>
if (this.state.mode === 'masterTicket'){
return (
<Box
width="100%"
height="100%"
padding={3}
>
<Row>
<Icon icon="Bitcoin" mr={2}/>
<Text fontSize="14px" fontWeight="bold">
Step 2 of 2: Import your extended public key
</Text>
</Row>
<Box mt={3}>
<Text fontSize="14px" fontWeight="regular" color="gray">
To begin, you'll need to derive an XPub Bitcoin address using your
master ticket.
<a fontSize="14px" target="_blank" href="https://urbit.org/bitcoin-wallet"> Learn More</a>
</Text>
</Box>
<Box
display='flex'
alignItems='center'
mt={3}
mb={2}>
{this.state.confirmingMasterTicket &&
<Icon
icon='ArrowWest'
cursor='pointer'
onClick={() => this.setState({
confirmingMasterTicket: false,
masterTicket: '',
confirmedMasterTicket: '',
error: false
})}/>}
<Text fontSize="14px" fontWeight="500">
{this.state.confirmingMasterTicket ? 'Confirm Master Key' : 'Master Key'}
</Text>
</Box>
<Row alignItems="center">
<StatelessTextInput
mr={2}
width="256px"
value={this.state.confirmingMasterTicket ? this.state.confirmedMasterTicket : this.state.masterTicket}
disabled={inputDisabled}
fontSize="14px"
type="password"
name="masterTicket"
obscure={value => value.replace(/[^~-]+/g, '••••••')}
placeholder="••••••-••••••-••••••-••••••"
autoCapitalize="none"
autoCorrect="off"
onChange={this.checkTicket}
/>
{(!inputDisabled) ? null : <LoadingSpinner/>}
</Row>
{this.state.error &&
<Row mt={2}>
<Text
fontSize='14px'
color='red'>
Master tickets do not match
</Text>
</Row>
}
<Box mt={3} mb={3}>
<Text fontSize="14px" fontWeight="regular"
color={(inputDisabled) ? "lighterGray" : "gray"}
style={{cursor: (inputDisabled) ? "default" : "pointer"}}
onClick={() => {
if (inputDisabled) return;
this.setState({mode: 'xpub', xpub: '', masterTicket: '', readyToSubmit: false})
}}
>
Manually import your extended public key ->
</Text>
</Box>
<Button
primary
disabled={buttonDisabled}
children="Next Step"
fontSize="14px"
style={{cursor: buttonDisabled ? "default" : "pointer"}}
onClick={() => {
if (!this.state.confirmingMasterTicket) {
this.setState({confirmingMasterTicket: true});
} else {
if (this.state.masterTicket === this.state.confirmedMasterTicket) {
this.setState({error: false});
this.submitMasterTicket(this.state.masterTicket);
} else {
this.setState({error: true});
}
}
}}
/>
</Box>
);
} else if (this.state.mode === 'xpub') {
return (
<Box
width="100%"
height="100%"
padding={3}
>
<Row>
<Icon icon="Bitcoin" mr={2}/>
<Text fontSize="14px" fontWeight="bold">
Step 2 of 2: Import your extended public key
</Text>
</Row>
<Box mt={3}>
<Text fontSize="14px" fontWeight="regular" color="gray">
Visit bridge.urbit.org to obtain your key
</Text>
</Box>
<Box mt={3} mb={2}>
<Text fontSize="14px" fontWeight="500">
Extended Public Key (XPub)
</Text>
</Box>
<StatelessTextInput
value={this.state.xpub}
disabled={inputDisabled}
fontSize="14px"
type="password"
name="xpub"
autoCapitalize="none"
autoCorrect="off"
onChange={this.checkXPub}
/>
<Row mt={3}>
<Button
primary
color="black"
backgroundColor="veryLightGray"
borderColor="veryLightGray"
children="Cancel"
fontSize="14px"
mr={2}
style={{cursor: "pointer"}}
onClick={() => {this.setState({mode: 'masterTicket', masterTicket: '', xpub: '', readyToSubmit: false})}}
/>
<Button
primary
disabled={buttonDisabled}
children="Next Step"
fontSize="14px"
style={{cursor: this.state.ready ? "pointer" : "default"}}
onClick={() => { this.submitXPub(this.state.xpub) }}
/>
</Row>
</Box>
);
}
}
}

View File

@ -0,0 +1,78 @@
import React, { Component } from 'react';
import {
Box,
Icon,
Row,
Text,
Button,
Col,
Anchor,
} from '@tlon/indigo-react';
import { store } from '../../store'
export default class Warning extends Component {
constructor(props) {
super(props);
this.understand = this.understand.bind(this);
}
understand(){
store.handleEvent({ data: { bucket: { warning: false}}});
let removeWarning = {
"put-entry": {
value: false,
"entry-key": "warning",
"bucket-key": "btc-wallet",
}
}
this.props.api.settingsEvent(removeWarning);
}
render() {
return (
<Box
backgroundColor="red"
color="white"
borderRadius="32px"
justifyContent="space-between"
width='100%'
p={5}
mb={5}
>
<Col>
<Text color="white" fontWeight='bold' fontSize={1}>
Warning!
</Text>
<br/>
<Text color="white" fontWeight='bold' fontSize={1}>
Be safe while using this wallet, and be sure to store responsible amounts
of BTC.
</Text>
<Text color="white" fontWeight='bold' fontSize={1}>
Always ensure that the checksum of the wallet matches that of the wallet's repo.
</Text>
<br/>
<Anchor color="white" fontWeight="bold" fontSize={1} style={{textDecoration: 'underline'}}
href="https://urbit.org/bitcoin-wallet" target="_blank">
Learn more on urbit.org
</Anchor>
</Col>
<Button children="I Understand"
backgroundColor="white"
fontSize={1}
mt={5}
color="red"
fontWeight="bold"
borderRadius="24px"
p="24px"
borderColor="none"
onClick={this.understand}
/>
</Box>
);
}
}

View File

@ -0,0 +1,76 @@
import React, { Component } from 'react';
import { BrowserRouter, Route } from "react-router-dom";
import _ from 'lodash';
import { api } from '../api.js';
import { store } from '../store.js';
import styled, { ThemeProvider, createGlobalStyle } from 'styled-components';
import light from './themes/light';
import dark from './themes/dark';
import {
Text,
Box,
Reset,
Col,
LoadingSpinner,
} from '@tlon/indigo-react';
import StartupModal from './lib/startupModal.js';
import Header from './lib/header.js'
import Balance from './lib/balance.js'
import Transactions from './lib/transactions.js'
import Warning from './lib/warning.js'
import Body from './lib/body.js'
import { subscription } from '../subscription.js'
const network = "bitcoin";
export class Root extends Component {
constructor(props) {
super(props);
this.ship = window.ship;
this.state = store.state;
store.setStateHandler(this.setState.bind(this));
console.log('state', this.state);
}
componentDidMount(){
this.props.channel.setOnChannelError((e) => {
subscription.start();
});
subscription.start();
}
render() {
const loaded = this.state.loaded;
const warning = this.state.showWarning;
const blur = (!loaded) ? false :
!(this.state.wallet && this.state.provider);
return (
<BrowserRouter>
<ThemeProvider theme={light}>
<Reset/>
{ (loaded) ? <StartupModal api={api} state={this.state} network={network}/> : null }
<Box display="flex"
flexDirection='column'
position='absolute'
alignItems='center'
backgroundColor='lightOrange'
width='100%'
minHeight={loaded ? '100%' : 'none'}
height={loaded ? 'none' : '100%'}
style={{filter: (blur ? 'blur(8px)' : 'none')}}
px={[0,4]}
pb={[0,4]}
>
<Body loaded={loaded}
state={this.state}
api={api} network={network}
warning={warning}
/>
</Box>
</ThemeProvider>
</BrowserRouter>
)
}
}

View File

@ -0,0 +1,183 @@
import baseStyled from "styled-components";
const base = {
white: "rgba(255,255,255,1)",
black: "rgba(0,0,0,1)",
red: "rgba(255,65,54,1)",
yellow: "rgba(255,199,0,1)",
green: "rgba(0,159,101,1)",
blue: "rgba(0,142,255,1)",
};
const scales = {
white05: "rgba(255,255,255,0.05)",
white10: "rgba(255,255,255,0.1)",
white20: "rgba(255,255,255,0.2)",
white30: "rgba(255,255,255,0.3)",
white40: "rgba(255,255,255,0.4)",
white50: "rgba(255,255,255,0.5)",
white60: "rgba(255,255,255,0.6)",
white70: "rgba(255,255,255,0.7)",
white80: "rgba(255,255,255,0.8)",
white90: "rgba(255,255,255,0.9)",
white100: "rgba(255,255,255,1)",
black05: "rgba(0,0,0,0.05)",
black10: "rgba(0,0,0,0.1)",
black20: "rgba(0,0,0,0.2)",
black30: "rgba(0,0,0,0.3)",
black40: "rgba(0,0,0,0.4)",
black50: "rgba(0,0,0,0.5)",
black60: "rgba(0,0,0,0.6)",
black70: "rgba(0,0,0,0.7)",
black80: "rgba(0,0,0,0.8)",
black90: "rgba(0,0,0,0.9)",
black100: "rgba(0,0,0,1)",
red05: "rgba(255,65,54,0.05)",
red10: "rgba(255,65,54,0.1)",
red20: "rgba(255,65,54,0.2)",
red30: "rgba(255,65,54,0.3)",
red40: "rgba(255,65,54,0.4)",
red50: "rgba(255,65,54,0.5)",
red60: "rgba(255,65,54,0.6)",
red70: "rgba(255,65,54,0.7)",
red80: "rgba(255,65,54,0.8)",
red90: "rgba(255,65,54,0.9)",
red100: "rgba(255,65,54,1)",
yellow05: "rgba(255,199,0,0.05)",
yellow10: "rgba(255,199,0,0.1)",
yellow20: "rgba(255,199,0,0.2)",
yellow30: "rgba(255,199,0,0.3)",
yellow40: "rgba(255,199,0,0.4)",
yellow50: "rgba(255,199,0,0.5)",
yellow60: "rgba(255,199,0,0.6)",
yellow70: "rgba(255,199,0,0.7)",
yellow80: "rgba(255,199,0,0.8)",
yellow90: "rgba(255,199,0,0.9)",
yellow100: "rgba(255,199,0,1)",
green05: "rgba(0,159,101,0.05)",
green10: "rgba(0,159,101,0.1)",
green20: "rgba(0,159,101,0.2)",
green30: "rgba(0,159,101,0.3)",
green40: "rgba(0,159,101,0.4)",
green50: "rgba(0,159,101,0.5)",
green60: "rgba(0,159,101,0.6)",
green70: "rgba(0,159,101,0.7)",
green80: "rgba(0,159,101,0.8)",
green90: "rgba(0,159,101,0.9)",
green100: "rgba(0,159,101,1)",
blue05: "rgba(0,142,255,0.05)",
blue10: "rgba(0,142,255,0.1)",
blue20: "rgba(0,142,255,0.2)",
blue30: "rgba(0,142,255,0.3)",
blue40: "rgba(0,142,255,0.4)",
blue50: "rgba(0,142,255,0.5)",
blue60: "rgba(0,142,255,0.6)",
blue70: "rgba(0,142,255,0.7)",
blue80: "rgba(0,142,255,0.8)",
blue90: "rgba(0,142,255,0.9)",
blue100: "rgba(0,142,255,1)",
};
const util = {
cyan: "#00FFFF",
magenta: "#FF00FF",
yellow: "#FFFF00",
black: "#000000",
gray0: "#333333"
};
const theme = {
colors: {
white: util.gray0,
black: base.white,
gray: scales.white60,
lightGray: scales.white30,
washedGray: scales.white05,
red: base.red,
lightRed: scales.red30,
washedRed: scales.red05,
yellow: base.yellow,
lightYellow: scales.yellow30,
washedYellow: scales.yellow10,
green: base.green,
lightGreen: scales.green30,
washedGreen: scales.green10,
blue: base.blue,
lightBlue: scales.blue30,
washedBlue: scales.blue10,
none: "rgba(0,0,0,0)",
scales: scales,
util: util,
},
fonts: {
sans: `"Inter", "Inter UI", -apple-system, BlinkMacSystemFont, 'San Francisco', 'Helvetica Neue', Arial, sans-serif`,
mono: `"Source Code Pro", "Roboto mono", "Courier New", monospace`,
},
// font-size
fontSizes: [
12, // 0
16, // 1
24, // 2
32, // 3
48, // 4
64, // 5
],
// font-weight
fontWeights: {
thin: 300,
regular: 400,
bold: 600,
},
// line-height
lineHeights: {
min: 1.2,
short: 1.333333,
regular: 1.5,
tall: 1.666666,
},
// border, border-top, border-right, border-bottom, border-left
borders: ["none", "1px solid"],
// margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, grid-gap, grid-column-gap, grid-row-gap
space: [
0, // 0
4, // 1
8, // 2
16, // 3
24, // 4
32, // 5
48, // 6
64, // 7
96, // 8
],
// border-radius
radii: [
0, // 0
2, // 1
4, // 2
8, // 3
],
// width, height, min-width, max-width, min-height, max-height
sizes: [
0, // 0
4, // 1
8, // 2
16, // 3
24, // 4
32, // 5
48, // 6
64, // 7
96, // 8
],
// z-index
zIndices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
breakpoints: ["550px", "750px", "960px"],
};
export const styled = baseStyled;
export default theme;

View File

@ -0,0 +1,183 @@
import baseStyled from "styled-components";
const base = {
white: "rgba(255,255,255,1)",
black: "rgba(0,0,0,1)",
red: "rgba(255,65,54,1)",
yellow: "rgba(255,199,0,1)",
green: "rgba(0,159,101,1)",
blue: "rgba(0,142,255,1)",
none: "rgba(0,0,0,0)",
};
const scales = {
white10: "rgba(255,255,255,0.1)",
white20: "rgba(255,255,255,0.2)",
white30: "rgba(255,255,255,0.3)",
white40: "rgba(255,255,255,0.4)",
white50: "rgba(255,255,255,0.5)",
white60: "rgba(255,255,255,0.6)",
white70: "rgba(255,255,255,0.7)",
white80: "rgba(255,255,255,0.8)",
white90: "rgba(255,255,255,0.9)",
white100: "rgba(255,255,255,1)",
black04: "rgba(0,0,0,0.04)",
black10: "rgba(0,0,0,0.1)",
black20: "rgba(0,0,0,0.2)",
black30: "rgba(0,0,0,0.3)",
black40: "rgba(0,0,0,0.4)",
black50: "rgba(0,0,0,0.5)",
black60: "rgba(0,0,0,0.6)",
black70: "rgba(0,0,0,0.7)",
black80: "rgba(0,0,0,0.8)",
black90: "rgba(0,0,0,0.9)",
black100: "rgba(0,0,0,1)",
red05: "rgba(255,65,54,0.05)",
red10: "rgba(255,65,54,0.1)",
red20: "rgba(255,65,54,0.2)",
red30: "rgba(255,65,54,0.3)",
red40: "rgba(255,65,54,0.4)",
red50: "rgba(255,65,54,0.5)",
red60: "rgba(255,65,54,0.6)",
red70: "rgba(255,65,54,0.7)",
red80: "rgba(255,65,54,0.8)",
red90: "rgba(255,65,54,0.9)",
red100: "rgba(255,65,54,1)",
yellow10: "rgba(255,199,0,0.1)",
yellow20: "rgba(255,199,0,0.2)",
yellow30: "rgba(255,199,0,0.3)",
yellow40: "rgba(255,199,0,0.4)",
yellow50: "rgba(255,199,0,0.5)",
yellow60: "rgba(255,199,0,0.6)",
yellow70: "rgba(255,199,0,0.7)",
yellow80: "rgba(255,199,0,0.8)",
yellow90: "rgba(255,199,0,0.9)",
yellow100: "rgba(255,199,0,1)",
green05: "rgba(0,159,101,0.05)",
green10: "rgba(0,159,101,0.1)",
green20: "rgba(0,159,101,0.2)",
green30: "rgba(0,159,101,0.3)",
green40: "rgba(0,159,101,0.4)",
green50: "rgba(0,159,101,0.5)",
green60: "rgba(0,159,101,0.6)",
green70: "rgba(0,159,101,0.7)",
green80: "rgba(0,159,101,0.8)",
green90: "rgba(0,159,101,0.9)",
green100: "rgba(0,159,101,1)",
blue10: "rgba(0,142,255,0.1)",
blue20: "rgba(0,142,255,0.2)",
blue30: "rgba(0,142,255,0.3)",
blue40: "rgba(0,142,255,0.4)",
blue50: "rgba(0,142,255,0.5)",
blue60: "rgba(0,142,255,0.6)",
blue70: "rgba(0,142,255,0.7)",
blue80: "rgba(0,142,255,0.8)",
blue90: "rgba(0,142,255,0.9)",
blue100: "rgba(0,142,255,1)",
};
const theme = {
colors: {
white: base.white,
black: base.black,
gray: scales.black60,
lighterGray: scales.black20,
lightGray: scales.black30,
washedGray: scales.black10,
veryLightGray: scales.black04,
red: base.red,
lightRed: scales.red30,
washedRed: scales.red10,
veryLightRed: scales.red05,
yellow: base.yellow,
lightYellow: scales.yellow30,
washedYellow: scales.yellow10,
green: base.green,
lightGreen: scales.green30,
washedGreen: scales.green10,
veryLightGreen: scales.green05,
blue: base.blue,
lightBlue: scales.blue30,
washedBlue: scales.blue10,
none: "rgba(0,0,0,0)",
scales: scales,
orange: "rgba(255, 153, 0, 1)",
midOrange: "rgba(255, 153, 0, 0.2)",
lightOrange: "rgba(255, 153, 0, 0.08)",
sentBlue: "rgba(33,157,255,1)",
recvGreen: "rgba(0,159,101,1)",
},
fonts: {
sans: `"Inter", "Inter UI", -apple-system, BlinkMacSystemFont, 'San Francisco', 'Helvetica Neue', Arial, sans-serif`,
mono: `"Source Code Pro", "Roboto mono", "Courier New", monospace`,
},
// font-size
fontSizes: [
12, // 0
16, // 1
24, // 2
32, // 3
48, // 4
64, // 5
],
// font-weight
fontWeights: {
thin: 300,
regular: 400,
bold: 600,
},
// line-height
lineHeights: {
min: 1.2,
short: 1.333333,
regular: 1.5,
tall: 1.666666,
},
// border, border-top, border-right, border-bottom, border-left
borders: ["none", "1px solid"],
// margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, grid-gap, grid-column-gap, grid-row-gap
space: [
0, // 0
4, // 1
8, // 2
16, // 3
24, // 4
32, // 5
48, // 6
64, // 7
96, // 8
],
// border-radius
radii: [
0, // 0
2, // 1
4, // 2
8, // 3
16, // 4
],
// width, height, min-width, max-width, min-height, max-height
sizes: [
0, // 0
4, // 1
8, // 2
16, // 3
24, // 4
32, // 5
48, // 6
64, // 7
96, // 8
],
// z-index
zIndices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
breakpoints: ["550px", "750px", "960px"],
};
export const styled = baseStyled;
export default theme;

View File

@ -0,0 +1,112 @@
import _ from 'lodash';
import classnames from 'classnames';
export function uuid() {
let str = "0v"
str += Math.ceil(Math.random()*8)+"."
for (var i = 0; i < 5; i++) {
let _str = Math.ceil(Math.random()*10000000).toString(32);
_str = ("00000"+_str).substr(-5,5);
str += _str+".";
}
return str.slice(0,-1);
}
export function isPatTa(str) {
const r = /^[a-z,0-9,\-,\.,_,~]+$/.exec(str)
return !!r;
}
/*
Goes from:
~2018.7.17..23.15.09..5be5 // urbit @da
To:
(javascript Date object)
*/
export function daToDate(st) {
var dub = function(n) {
return parseInt(n) < 10 ? "0" + parseInt(n) : n.toString();
};
var da = st.split('..');
var bigEnd = da[0].split('.');
var lilEnd = da[1].split('.');
var ds = `${bigEnd[0].slice(1)}-${dub(bigEnd[1])}-${dub(bigEnd[2])}T${dub(lilEnd[0])}:${dub(lilEnd[1])}:${dub(lilEnd[2])}Z`;
return new Date(ds);
}
/*
Goes from:
(javascript Date object)
To:
~2018.7.17..23.15.09..5be5 // urbit @da
*/
export function dateToDa(d, mil) {
  var fil = function(n) {
    return n >= 10 ? n : "0" + n;
  };
  return (
    `~${d.getUTCFullYear()}.` +
    `${(d.getUTCMonth() + 1)}.` +
    `${fil(d.getUTCDate())}..` +
    `${fil(d.getUTCHours())}.` +
    `${fil(d.getUTCMinutes())}.` +
    `${fil(d.getUTCSeconds())}` +
`${mil ? "..0000" : ""}`
  );
}
export function deSig(ship) {
return ship.replace('~', '');
}
// trim patps to match dojo, chat-cli
export function cite(ship) {
let patp = ship, shortened = "";
if (patp.startsWith("~")) {
patp = patp.substr(1);
}
// comet
if (patp.length === 56) {
shortened = "~" + patp.slice(0, 6) + "_" + patp.slice(50, 56);
return shortened;
}
// moon
if (patp.length === 27) {
shortened = "~" + patp.slice(14, 20) + "^" + patp.slice(21, 27);
return shortened;
}
return `~${patp}`;
}
export function satsToCurrency(sats, denomination, rates){
if (!rates) {
throw "nonexistent currency table"
}
if (!rates[denomination]){
denomination = "BTC";
}
let rate = rates[denomination];
let val = parseFloat(((sats * rate.last) * 0.00000001).toFixed(8));
let text;
if (denomination === 'BTC'){
text = val + ' ' + rate.symbol
} else {
text = rate.symbol + val.toFixed(2).replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,')
}
return text
}
export function currencyToSats(val, denomination, rates){
if (!rates) {
throw "nonexistent currency table"
}
if (!rates[denomination]){
throw 'currency not in table'
}
let rate = rates[denomination];
let sats = (parseFloat(val) / rate.last) * 100000000;
return sats;
}

View File

@ -0,0 +1,20 @@
import _ from 'lodash';
export class CurrencyReducer {
reduce(json, state) {
if (!json) {
return;
}
if (json.currencyRates) {
for (var c in json.currencyRates) {
state.currencyRates[c] = json.currencyRates[c];
}
}
if (json.denomination) {
if (state.currencyRates[json.denomination]) {
state.denomination = json.denomination
}
}
}
}

View File

@ -0,0 +1,29 @@
import _ from 'lodash';
export class InitialReducer {
reduce(json, state) {
let data = _.get(json, 'initial', false);
if (data) {
state.provider = data.provider;
state.wallet = data.wallet;
state.confirmedBalance = _.get(data.balance, 'confirmed', null);
state.unconfirmedBalance = _.get(data.balance, 'unconfirmed', null);
state.btcState = data['btc-state'];
state.history = this.reduceHistory(data.history);
state.address = data.address;
state.loadedBtc = true;
if (state.loadedSettings) {
state.loaded = true;
}
}
}
reduceHistory(history) {
return Object.values(history).sort((hest1, hest2) => {
if (hest1.recvd === null) return -1;
if (hest2.recvd === null) return +1;
return (hest2.recvd - hest1.recvd)
})
}
}

View File

@ -0,0 +1,30 @@
import _ from 'lodash';
export class SettingsReducer {
reduce(json, state) {
let data = _.get(json, 'bucket', false);
if (data) {
let warning = _.get(json, 'bucket.warning', -1);
if (warning !== -1) {
state.showWarning = warning
}
let currency = _.get(json, 'bucket.currency', -1);
if (currency !== -1) {
state.denomination = currency;
}
state.loadedSettings = true;
if (state.loadedBtc) {
state.loaded = true;
}
}
let entry = _.get(json, 'settings-event.put-entry.entry-key', false);
if (entry === 'currency') {
let value = _.get(json, 'settings-event.put-entry.value', false);
state.denomination = value;
} else if (entry === 'warning') {
let value = _.get(json, 'settings-event.put-entry.value', false);
state.showWarning = value;
}
}
}

View File

@ -0,0 +1,117 @@
import _ from 'lodash';
export class UpdateReducer {
reduce(json, state) {
if (!json) {
return;
}
console.log('reduce', json);
if (json.providerStatus) {
this.reduceProviderStatus(json.providerStatus, state);
}
if (json.checkPayee) {
this.reduceCheckPayee(json.checkPayee, state);
}
if ("change-provider" in json) {
this.reduceChangeProvider(json["change-provider"], state);
}
if (json["change-wallet"]) {
this.changeWallet(json["change-wallet"], state);
}
if (json.hasOwnProperty('psbt')) {
this.reducePsbt(json.psbt, state);
}
if (json["btc-state"]) {
this.reduceBtcState(json["btc-state"], state);
}
if (json["new-tx"]) {
this.reduceNewTx(json["new-tx"], state);
}
if (json["cancel-tx"]) {
this.reduceCancelTx(json["cancel-tx"], state);
}
if (json.address) {
this.reduceAddress(json.address, state);
}
if (json.balance) {
this.reduceBalance(json.balance, state);
}
if (json.hasOwnProperty('error')) {
this.reduceError(json.error, state);
}
if (json.hasOwnProperty('broadcast-success')){
state.broadcastSuccess = true;
}
if (json.hasOwnProperty('broadcast-fail')){
state.broadcastSuccess = false;
}
}
reduceProviderStatus(json, state) {
state.providerPerms[json.provider] = json.permitted;
}
reduceCheckPayee(json, state) {
state.shipWallets[json.payee] = json.hasWallet;
}
reduceChangeProvider(json, state) {
state.provider = json;
}
reduceChangeWallet(json, state) {
state.wallet = json;
}
reducePsbt(json, state) {
state.psbt = json.pb;
state.fee = json.fee;
}
reduceBtcState(json, state) {
state.btcState = json;
}
reduceNewTx(json, state) {
let old = _.findIndex(state.history, (h) => {
return ( h.txid.dat === json.txid.dat &&
h.txid.wid === json.txid.wid );
});
if (old !== -1) {
delete state.history.splice(old, 1);
}
if (json.recvd === null) {
state.history.unshift(json);
} else {
// we expect history to have null recvd values first, and the rest in
// descending order
let insertionIndex = _.findIndex(state.history, (h) => {
return ((h.recvd < json.recvd) && (h.recvd !== null));
});
state.history.splice(insertionIndex, 0, json);
}
}
reduceCancelTx(json, state) {
let entryIndex = _.findIndex(state.history, (h) => {
return ((json.wid === h.txid.wid) && (json.dat === h.txid.dat));
});
if (entryIndex > -1) {
state.history[entryIndex].failure = true;
}
}
reduceAddress(json, state) {
state.address = json;
}
reduceBalance(json, state) {
state.unconfirmedBalance = json.unconfirmed;
state.confirmedBalance = json.confirmed;
}
reduceError(json, state) {
state.error = json;
}
}

View File

@ -0,0 +1,54 @@
import { InitialReducer } from './reducers/initial';
import { UpdateReducer } from './reducers/update';
import { CurrencyReducer } from './reducers/currency';
import { SettingsReducer } from './reducers/settings';
class Store {
constructor() {
this.state = {
loadedBtc: false,
loadedSettings: false,
loaded: false,
providerPerms: {},
shipWallets: {},
provider: null,
wallet: null,
confirmedBalance: null,
unconfirmedBalance: null,
btcState: null,
history: [],
psbt: '',
address: null,
currencyRates: {
BTC: { last: 1, symbol: 'BTC' }
},
denomination: 'BTC',
showWarning: true,
error: '',
broadcastSuccess: false,
};
this.initialReducer = new InitialReducer();
this.updateReducer = new UpdateReducer();
this.currencyReducer = new CurrencyReducer();
this.settingsReducer = new SettingsReducer();
this.setState = () => { };
}
setStateHandler(setState) {
this.setState = setState;
}
handleEvent(data) {
let json = data.data;
this.initialReducer.reduce(json, this.state);
this.updateReducer.reduce(json, this.state);
this.currencyReducer.reduce(json, this.state);
this.settingsReducer.reduce(json, this.state);
this.setState(this.state);
}
}
export let store = new Store();
window.store = store;

View File

@ -0,0 +1,55 @@
import { api } from './api';
import { store } from './store';
export class Subscription {
start() {
if (api.ship) {
this.initializeBtcWallet();
this.initializeSettings();
this.initializeCurrencyPoll();
} else {
console.error("~~~ ERROR: Must set api.authTokens before operation ~~~");
}
}
initializeBtcWallet() {
api.bind('/all', 'PUT', api.ship, 'btc-wallet',
this.handleEvent.bind(this),
this.handleError.bind(this));
}
initializeSettings() {
let app = 'settings-store';
let path = '/bucket/btc-wallet';
fetch(`/~/scry/${app}${path}.json`).then(res => res.json())
.then(n => {
this.handleEvent({data: n});
});
api.bind(path, 'PUT', api.ship, app,
this.handleEvent.bind(this),
this.handleError.bind(this));
}
initializeCurrencyPoll() {
fetch('https://blockchain.info/ticker')
.then(res => res.json())
.then(n => {
store.handleEvent({data: {currencyRates: n}})
setTimeout(this.initializeCurrencyPoll, 1000 * 60 * 15);
});
}
handleEvent(diff) {
store.handleEvent(diff);
}
handleError(err) {
console.error(err);
this.initializeBtcWallet();
this.initializeSettings();
}
}
export let subscription = new Subscription();

View File

@ -47,7 +47,7 @@ export interface TileTypeBasic {
}
interface TileTypeCustom {
custom: null;
custom: any;
}
interface WeatherDay {

View File

@ -15,6 +15,8 @@ const Tiles = (props: TileProps): ReactElement => {
const weather = useLaunchState(state => state.weather) as WeatherState;
const tileOrdering = useLaunchState(state => state.tileOrdering);
const tileState = useLaunchState(state => state.tiles);
console.log('tileOrdering', tileOrdering);
console.log('tileState', tileState);
const tiles = tileOrdering.filter((key) => {
const tile = tileState[key];
@ -44,9 +46,15 @@ const Tiles = (props: TileProps): ReactElement => {
return (
<ClockTile key={key} location={location} />
);
} else {
return (
<CustomTile
key={key}
tileImage={tile.type.custom.image}
linkedUrl={tile.type.custom.linkedUrl}
/>
);
}
} else {
return <CustomTile key={key} />;
}
return null;
});

View File

@ -1,28 +1,24 @@
import { BaseImage, Box } from '@tlon/indigo-react';
import { BaseImage } from '@tlon/indigo-react';
import React from 'react';
import Tile from './tile';
export default class CustomTile extends React.PureComponent {
interface CustomTileProps {
linkedUrl: string;
tileImage: string;
}
export default class CustomTile extends React.PureComponent<CustomTileProps> {
render() {
const { props } = this;
return (
<Tile>
<Box
<Tile to={props.linkedUrl} p="0px">
<BaseImage
position='absolute'
src={props.tileImage}
width='100%'
height='100%'
position='relative'
backgroundColor='white'
border='1px solid'
borderColor='lightGray'
borderRadius={2}
>
<BaseImage
position='absolute'
style={{ left: 38, top: 38 }}
src='/~launch/img/UnknownCustomTile.png'
width='48px'
height='48px'
/>
</Box>
/>
</Tile>
);
}