Merge branch 'release/next-vere' into jb/next-gen-term

* release/next-vere: (67 commits)
  pill: all
  kh: improve code style
  glob: update to 0v758lj.uf0s5.0nh3m.gunn6.942gj
  Fix feepicker issues
  Add exit buttons to invoices
  Fix issue with change provider button not triggering modal
  Fix scanning text issues
  Fix enum, was breaking signer button
  Fix imports in ExternalInvoice
  glob: update to 0v4.e52ik.udm4j.6aus5.02b25.vomaj
  btc-wallet: fix imports
  aqua: assert pill type
  Port BTC wallet to Typescript
  Match edouard's designs
  Add external (psbt) invoice
  Fix copy from non-secure context issue
  Use deSig for isPatp
  Use deSig rather than concat
  Add sig to valid patp in send component
  Just show total main/change addresses scanned
  ...
This commit is contained in:
Joe Bryan 2021-09-09 11:48:43 -04:00
commit 148779f4d4
130 changed files with 5043 additions and 3921 deletions

View File

@ -30,7 +30,7 @@
==
+$ state-0
$: %0
pil=pill
pil=$>(%pill pill)
assembled=*
tym=@da
fleet-snaps=(map term fleet)
@ -38,11 +38,7 @@
==
:: XX temporarily shadowed, fix and remove
::
+$ pill
$: boot-ova=*
kernel-ova=(list unix-event)
userspace-ova=(list unix-event)
==
+$ pill pill:pill-lib
::
+$ fleet [ships=(map ship pier) azi=az-state]
+$ pier
@ -86,7 +82,7 @@
=^ cards state
?+ mark ~|([%aqua-bad-mark mark] !!)
%aqua-events (poke-aqua-events:ac !<((list aqua-event) vase))
%pill (poke-pill:ac !<(pill vase))
%pill (poke-pill:ac !<(pill vase))
%noun (poke-noun:ac !<(* vase))
%azimuth-action (poke-azimuth-action:ac !<(azimuth-action vase))
==
@ -183,7 +179,7 @@
?. processing-events
..abet-pe
=^ ue next-events ~(get to next-events)
=/ poke-arm (mox +47.snap)
=/ poke-arm (mox +23.snap)
?> ?=(%0 -.poke-arm)
=/ poke p.poke-arm
=. tym (max +(tym) now.hid)
@ -202,20 +198,21 @@
::
++ peek
|= p=*
=/ res (mox +46.snap)
=/ res (mox +22.snap)
?> ?=(%0 -.res)
=/ peek p.res
=/ pax (path p)
?> ?=([@ @ @ @ *] pax)
=. i.t.t.t.pax (scot %da tym)
=/ pek (slum peek [tym pax])
pek
::
=/ pek (slum peek [[~ ~] & pax])
=+ ;;(res=(unit (cask)) pek)
(bind res tail)
::
:: Wish
::
++ wish
|= txt=@t
=/ res (mox +22.snap)
=/ res (mox +10.snap)
?> ?=(%0 -.res)
=/ wish p.res
~& [who=who %wished (slum wish txt)]
@ -373,6 +370,7 @@
++ poke-pill
|= p=pill
^- (quip card:agent:gall _state)
?< ?=(%ivory -.p)
=. this apex-aqua =< abet-aqua
=. pil p
~& lent=(met 3 (jam boot-ova.pil))
@ -411,10 +409,11 @@
::
?+ val ~|(%bad-noun-arg !!)
[%swap-vanes vs=*]
?> ?=([[%7 * %1 installed=*] ~] boot-ova.pil)
=. installed.boot-ova.pil
?> ?=(^ boot-ova.pil)
?> ?=([%7 * %1 installed=*] i.boot-ova.pil)
=. installed.i.boot-ova.pil
%+ roll (,(list term) vs.val)
|= [v=term =_installed.boot-ova.pil]
|= [v=term =_installed.i.boot-ova.pil]
%^ slum installed now.hid
=/ vane
?+ v ~|([%unknown-vane v] !!)
@ -507,28 +506,42 @@
?- -.ae
::
%init-ship
:: XX Note that the keys that get passed in are unused. The keys field
:: should be deleted now that aqua is capable of managing azimuth state
:: internally. Its been left this way for now until all the ph tests
:: can be rewritten
=/ keys=dawn-event:jael (dawn who.ae)
=. this abet-pe:(publish-effect:(pe who.ae) [/ %sleep ~])
=/ initted
=< plow
%- push-events:apex:(pe who.ae)
^- (list unix-event)
:~ [/ %wack 0] :: eny
[/ %whom who.ae] :: eny
[//newt/0v1n.2m9vh %born ~]
[//behn/0v1n.2m9vh %born ~]
:^ //term/1 %boot &
?~ keys.ae
[%fake who.ae]
[%dawn keys]
-.userspace-ova.pil
[//http-client/0v1n.2m9vh %born ~]
[//http-server/0v1n.2m9vh %born ~]
[//http-server/0v1n.2m9vh %live 8.080 `8.445]
%- zing
:~
:~ [/ %wack 0] :: eny
:: [/ %verb `|] :: possible verb
:^ / %wyrd [~.nonce /aqua] :: dummy runtime version + nonce
^- (list (pair term @))
:~ zuse+zuse
lull+lull
arvo+arvo
hoon+hoon-version
nock+4
==
[/ %whom who.ae] :: who
==
::
kernel-ova.pil :: load compiler
::
:_ ~
:^ /d/term/1 %boot &
?: fake.ae
[%fake who.ae]
[%dawn (dawn who.ae)]
::
userspace-ova.pil :: load os
::
:~ [/b/behn/0v1n.2m9vh %born ~]
[/i/http-client/0v1n.2m9vh %born ~]
[/e/http-server/0v1n.2m9vh %born ~]
[/e/http-server/0v1n.2m9vh %live 8.080 `8.445]
[/a/newt/0v1n.2m9vh %born ~]
==
==
=. this abet-pe:initted
(pe who.ae)

View File

@ -26,6 +26,6 @@
<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.3e8bcc150ebd820dd3b2.js"></script>
<script src="/~btc/js/bundle/index.050889bac51cbd935dd9.js"></script>
</body>
</html>

View File

@ -5,8 +5,8 @@
/- glob, *resource
/+ default-agent, verb, dbug
|%
++ landscape-hash 0v4.qs4qf.3bam7.736sg.0poq8.c6vhq
++ btc-wallet-hash 0v7.v4dng.o33qi.kc497.5jc02.ke5es
++ landscape-hash 0v3.sdoer.mnnfi.opjrg.npmcj.utr8l
++ btc-wallet-hash 0v758lj.uf0s5.0nh3m.gunn6.942gj
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
+$ state-1 [%1 =globs:glob]
+$ all-states

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.c1a890b306daa1aee34b.js"></script>
<script src="/~landscape/js/bundle/index.60c063d34a42a08ab440.js"></script>
</body>
</html>

View File

@ -5,10 +5,10 @@
:- %aqua-events
%+ turn
^- (list unix-event)
:~ [//term/1 %belt %mod %ctl `@c`%e]
[//term/1 %belt %mod %ctl `@c`%u]
[//term/1 %belt %txt ((list @c) command)]
[//term/1 %belt %ret ~]
:~ [/d/term/1 %belt %mod %ctl `@c`%e]
[/d/term/1 %belt %mod %ctl `@c`%u]
[/d/term/1 %belt %txt ((list @c) command)]
[/d/term/1 %belt %ret ~]
==
|= ue=unix-event
[%event her ue]

View File

@ -7,5 +7,5 @@
:+ %event her
?> ?=([@ @ @ *] pax)
=/ file [/text/plain (as-octs:mimes:html .^(@ %cx pax))]
:- //sync/0v1n.2m9vh
:- /c/sync/0v1n.2m9vh
[%into `desk`i.t.pax | `mode:clay`[t.t.t.pax `file]~]

View File

@ -1,6 +1,8 @@
:: Start an aqua ship
::
/- aquarium
=, aquarium
:- %say
|= [* [her=ship ~] ~]
|= [* [her=ship fake=? ~] ~]
:- %aqua-events
[%init-ship her `*dawn-event:jael]~
[%init-ship her fake]~

View File

@ -12,7 +12,7 @@
arg=$@(~ [top=path ~])
~
==
:- %noun
:- %boot-pill
^- pill:pill
::
:: sys: root path to boot system, `/~me/[desk]/now/sys`

View File

@ -0,0 +1,30 @@
/- ms=metadata-store
/+ crunch
:- %say
|= [[now=@da * bec=beak] [csv-path=path from=@da ~] [to=@da groups=(list path) content=(unit ?) ~]]
=/ our=@p p.bec
:: check given path has `csv` mark
::
?> =(%csv (snag (dec (lent csv-path)) csv-path))
:: get all graph associations ship is a part of
::
=/ associations=associations:ms
(~(scry-graph-associations crunch [our now]))
:: filter by input groups, if any (default: all from scry)
::
=/ filtered-associations=associations:ms
?~ groups
associations
%+ filter-associations-by-group-resources.crunch
associations
(paths-to-resources.crunch groups)
:: walk graphs to extract content
::
=/ file-content=wain
%: ~(walk-graph-associations crunch [our now])
filtered-associations
?~ content %.n u.content
from
?: =(*@da to) now to
==
[%helm-pass (note-write-csv-to-clay.crunch csv-path file-content)]

View File

@ -15,7 +15,7 @@
arg=$@(~ [top=path ~])
dub=_|
==
:- %pill
:- %boot-pill
^- pill:pill
:: sys: root path to boot system, `/~me/[desk]/now/sys`
::

View File

@ -122,7 +122,7 @@
:_ ~
:* %event
her
//http-client/0v1n.2m9vh
/i/http-client/0v1n.2m9vh
%receive
num.u.ask
[%start [200 ~] `(as-octs:mimes:html resp) &]

356
pkg/arvo/lib/crunch.hoon Normal file
View File

@ -0,0 +1,356 @@
/- c=crunch, gs=graph-store, ms=metadata-store, p=post, r=resource
::
=<
|_ [our=ship now=@da]
++ walk-graph-associations
|= [=associations:ms content=? from=@da to=@da]
^- wain
:: graph resources in `our`; used to avoid scrying, e.g.,
:: a graph `our` has left and can no longer access
::
=/ accessible-graphs=(set resource:r) (scry-graph-resources)
%- ~(rep by associations)
|= [[=md-resource:ms =association:ms] out=wain]
^- wain
?. ?=(%graph app-name.md-resource)
out
?. ?=(%graph -.config.metadatum.association)
out
:: ensure graph, given by association, exists in `our`
::
?. (~(has in accessible-graphs) resource.md-resource)
out
:: scry the graph
::
=/ graph=(unit graph:gs) (scry-graph resource.md-resource)
?~ graph
out
:: prepare channel-info argument
::
=/ channel-info=channel-info:c
:* group.association
resource.md-resource
module.config.metadatum.association
==
:: walk the graph
::
?+ module.config.metadatum.association
:: non-chat (e.g. links & notes)
::
%+ weld out
%: walk-nested-graph-for-most-recent-entries
u.graph
content
channel-info
from
to
==
::
%chat
%+ weld out
%: walk-chat-graph
u.graph
content
channel-info
from
to
==
==
::
++ scry-graph
|= graph-resource=resource:r
^- (unit graph:gs)
=/ scry-response=update:gs
.^ update:gs
%gx
(scot %p our)
%graph-store
(scot %da now)
%graph
(scot %p entity.graph-resource)
name.graph-resource
/noun
==
?. ?=(%add-graph -.q.scry-response)
~
?~ graph.q.scry-response
~
[~ graph.q.scry-response]
::
++ scry-graph-resources
|= ~
^- (set resource:r)
=/ scry-response=update:gs
.^ update:gs
%gx
(scot %p our)
%graph-store
(scot %da now)
/keys/noun
==
?. ?=(%keys -.q.scry-response)
~
resources.q.scry-response
:: helper arm for callers to get graph associations
:: to pass to `walk-graph-associations`
::
++ scry-graph-associations
|= ~
^- associations:ms
.^ associations:ms
%gx
(scot %p our)
%metadata-store
(scot %da now)
/app-name/graph/noun
==
--
::
|%
::
:: parsing and formatting
::
++ resource-to-cord
|= =resource:r
^- @t
(rap 3 (scot %p entity.resource) '/' (scot %tas name.resource) ~)
::
++ paths-to-resources
|= paxs=(list path)
^- (set resource:r)
%- ~(gas in *(set resource:r))
(turn paxs path-to-resource)
::
++ path-to-resource
|= pax=path
^- resource:r
=/ entity=@p (slav %p -.pax)
=/ name=@tas -.+.pax
[entity name]
::
++ escape-characters-in-cord
|= =cord
^- @t
%- crip
%- mesc
:: specific to CSVs: make sure content does not
:: contain commas (only allowed as delimiters)
::
%- replace-tape-commas-with-semicolons
%- trip
cord
::
++ replace-tape-commas-with-semicolons
|= string=tape
^- tape
=/ comma-indices=(list @ud) (fand "," string)
|-
^- tape
?~ comma-indices
string
$(string (snap string i.comma-indices ';'), comma-indices t.comma-indices)
::
++ contents-to-cord
|= contents=(list content:p)
^- @t
?~ contents
''
%+ join-cords
' '
(turn contents content-to-cord)
::
++ content-to-cord
|= =content:p
^- @t
?- -.content
%text (escape-characters-in-cord text.content)
%mention (scot %p ship.content)
%url url.content
%code expression.content :: TODO: also print output?
%reference (reference-content-to-cord reference.content)
==
::
++ reference-content-to-cord
|= =reference:p
^- @t
?- -.reference
%group (resource-to-cord group.reference)
%graph (rap 3 (resource-to-cord group.reference) ': ' (resource-to-cord resource.uid.reference) ~)
==
::
++ format-post-to-comma-separated-cord
|= [=post:gs =channel-info:c]
^- @t
%+ join-cords
','
:~ (scot %da time-sent.post)
(scot %p author.post)
(resource-to-cord group.channel-info)
(resource-to-cord channel.channel-info)
(scot %tas channel-type.channel-info)
:: exclude content; optionally add later
::
==
::
++ join-cords
|= [delimiter=@t cords=(list @t)]
^- @t
%+ roll cords
|= [cord=@t out=@t]
^- @t
?: =('' out)
:: don't put delimiter before first element
::
cord
(rap 3 out delimiter cord ~)
::
:: walking graphs
::
++ walk-chat-graph
|= [=graph:gs content=? =channel-info:c from=@da to=@da]
^- wain
%- flop
%+ roll
:: filter by time
::
%+ only-nodes-older-than to
%+ only-nodes-newer-than from
~(val by graph)
|= [=node:gs out=wain]
^- wain
?- -.post.node
%|
:: do not output deleted posts
::
out
%&
?~ contents.p.post.node
:: do not output structural nodes
::
out
:_ out
=/ post-no-content=@t (format-post-to-comma-separated-cord p.post.node channel-info)
?- content
%| post-no-content
%&
%+ join-cords ','
~[post-no-content (contents-to-cord contents.p.post.node)]
==
==
::
++ walk-nested-graph-for-most-recent-entries
|= [=graph:gs content=? =channel-info:c from=@da to=@da]
^- wain
=| out=wain
=| most-recent-post-content=@t
=/ nodes
:: filter by time
::
%+ only-nodes-older-than to
%+ only-nodes-newer-than from
~(val by graph)
%- flop
|-
^- wain
?~ nodes
?: =('' most-recent-post-content)
:: don't return a cell: `['' ~]`
:: we want either an empty list `~`
:: or a list populated with actual entries
::
out
[most-recent-post-content out]
::
=? out ?=(%graph -.children.i.nodes)
%+ weld out
%: walk-nested-graph-for-most-recent-entries
p.children.i.nodes
content
channel-info
from
to
==
::
?- -.post.i.nodes
%|
:: do not keep deleted posts
::
$(nodes t.nodes)
%&
?~ contents.p.post.i.nodes
:: do not keep structural nodes
::
$(nodes t.nodes)
=/ post-no-content=@t (format-post-to-comma-separated-cord p.post.i.nodes channel-info)
%= $
nodes t.nodes
most-recent-post-content
?- content
%| post-no-content
%&
%+ join-cords ','
~[post-no-content (contents-to-cord contents.p.post.i.nodes)]
==
==
==
::
:: filters
::
++ filter-associations-by-group-resources
|= [=associations:ms group-resources=(set resource:r)]
^- associations:ms
%- ~(rep by associations)
|= [[=md-resource:ms =association:ms] out=associations:ms]
^- associations:ms
?. (~(has in group-resources) group.association)
out
(~(put by out) md-resource association)
:: wrappers for intuitive use of `filter-nodes-by-timestamp`:
:: pass `nodes` as given by the `graph-store` scry and no
:: need to worry about comparators
::
++ only-nodes-older-than
|= [time=@da nodes=(list node:gs)]
(filter-nodes-by-timestamp nodes lte time)
::
++ only-nodes-newer-than
|= [time=@da nodes=(list node:gs)]
%- flop
(filter-nodes-by-timestamp (flop nodes) gte time)
::
++ filter-nodes-by-timestamp
|= [nodes=(list node:gs) comparator=$-([@ @] ?) time=@da]
=| out=(list node:gs)
:: return `out` in same time-order as `nodes`
::
%- flop
|-
^- (list node:gs)
?~ nodes
out
?- -.post.i.nodes
%|
:: skip deleted posts
::
$(nodes t.nodes)
%&
?. (comparator time-sent.p.post.i.nodes time)
:: assume:
:: * time is monotonic
:: * first `%.n` we hit indicates nodes further on are `%.n`
:: (i.e. `nodes` must be ordered st. they start `%.y`,
:: e.g. if want all `nodes` older than given time,
:: `nodes` must start with oldest and comparator is `lth`)
::
out
$(nodes t.nodes, out [i.nodes out])
==
::
:: io
::
++ note-write-csv-to-clay
|= [pax=path file-content=wain]
?> =(%csv (snag (dec (lent pax)) pax))
[%c [%info %home %& [pax %ins %csv !>(file-content)]~]]
::
--

View File

@ -23,17 +23,14 @@
::
++ start-simple
(start-test %aqua-ames %aqua-behn %aqua-dill %aqua-eyre ~)
++ end-simple
(end-test %aqua-ames %aqua-behn %aqua-dill %aqua-eyre ~)
::
++ start-azimuth
=/ m (strand ,tid:spider)
=/ m (strand ,~)
^- form:m
;< ~ bind:m (start-test %aqua-ames %aqua-behn %aqua-dill ~)
(start-thread %aqua-eyre-azimuth)
;<(~ bind:m start-simple init)
::
++ end-azimuth
(end-test %aqua-ames %aqua-behn %aqua-dill %aqua-eyre-azimuth ~)
++ end
(end-test %aqua-ames %aqua-behn %aqua-dill %aqua-eyre ~)
::
++ start-test
|= vane-threads=(list term)
@ -91,77 +88,32 @@
^- form:m
(pure:m ~)
::
:: XX +spawn-aqua and +breach-aqua mean do these actions using aqua's internal
:: azimuth management system, eventually these should just replace +spawn
:: +breach
::
++ init-azimuth
++ init
=/ m (strand ,~)
^- form:m
(send-azimuth-action %init-azimuth ~)
::
++ spawn-aqua
++ spawn
|= =ship
~& > "spawning {<ship>}"
=/ m (strand ,~)
^- form:m
(send-azimuth-action %spawn ship)
::
++ breach-aqua
++ breach
|= =ship
~& > "breaching {<ship>}"
=/ m (strand ,~)
^- form:m
(send-azimuth-action %breach ship)
::
++ spawn
|= [=tid:spider =ship]
~& > "spawning {<ship>}"
=/ m (strand ,~)
=/ =vase !>(`input:spider`[tid %azimuth-command !>([%spawn ship])])
(poke-our %spider %spider-input vase)
::
++ breach
|= [=tid:spider who=ship]
=/ m (strand ,~)
~& > "breaching {<who>}"
=/ =vase
!>([tid %azimuth-command !>([%breach who])])
(poke-our %spider %spider-input vase)
::
:: who: breachee
:: her: wait until hears about breach
::
++ breach-and-hear
|= [=tid:spider who=ship her=ship]
=/ m (strand ,~)
~& > "breaching {<who>} for {<her>}"
;< =bowl:spider bind:m get-bowl
=/ aqua-pax
:- %i
/(scot %p her)/j/(scot %p her)/rift/(scot %da now.bowl)/(scot %p who)/noun
=/ old-rut ;;((unit @) (scry-aqua:util noun our.bowl now.bowl aqua-pax))
=/ new-rut
?~ old-rut
1
+(+.old-rut)
=/ =vase
!>([tid %azimuth-command !>([%breach who])])
;< ~ bind:m (poke-our %spider %spider-input vase)
|- ^- form:m
=* loop $
;< [him=ship =unix-effect] bind:m take-unix-effect
;< =bowl:spider bind:m get-bowl
=/ aqua-pax
:- %i
/(scot %p her)/j/(scot %p her)/rift/(scot %da now.bowl)/(scot %p who)/noun
=/ rut (scry-aqua:util noun our.bowl now.bowl aqua-pax)
?: =([~ new-rut] rut)
(pure:m ~)
loop
::
++ breach-and-hear-aqua
|= [who=ship her=ship]
~& > "breaching {<who>} for {<her>}"
=/ m (strand ,~)
;< =bowl:spider bind:m get-bowl
=/ aqua-pax
@ -186,27 +138,11 @@
loop
::
++ init-ship
|= =ship
|= [=ship fake=?]
=/ m (strand ,~)
^- form:m
~& > "starting {<ship>}"
;< ~ bind:m (send-events (init:util ship `*dawn-event:jael))
(check-ship-booted ship)
::
++ real-ship
|= [=tid:spider =ship]
~& > "booting real {<ship>}"
=/ m (strand ,~)
=/ =vase !>([tid %azimuth-command !>([%create-ship ship])])
;< ~ bind:m (poke-our %spider %spider-input vase)
(check-ship-booted ship)
::
++ raw-ship
|= [=ship keys=(unit dawn-event:jael)]
=/ m (strand ,~)
^- form:m
~& > "starting {<ship>}"
;< ~ bind:m (send-events (init:util ship keys))
;< ~ bind:m (send-events (init:util ship fake))
(check-ship-booted ship)
::
++ check-ship-booted
@ -258,6 +194,7 @@
::
++ send-hi-not-responding
|= [from=@p to=@p]
~& > 'sending hi not responding'
=/ m (strand ,~)
;< ~ bind:m (dojo from "|hi {(scow %p to)}")
(wait-for-output from "{(scow %p to)} not responding still trying")

View File

@ -16,9 +16,9 @@
:: Start a ship (low-level; prefer +raw-ship)
::
++ init
|= [who=ship keys=(unit dawn-event:jael)]
|= [who=ship fake=?]
^- (list aqua-event)
[%init-ship who keys]~
[%init-ship who fake]~
::
:: Send dojo command
::
@ -28,10 +28,10 @@
%+ send-events-to who
^- (list unix-event)
:~
[//term/1 %belt %mod %ctl `@c`%e]
[//term/1 %belt %mod %ctl `@c`%u]
[//term/1 %belt %txt ((list @c) what)]
[//term/1 %belt %ret ~]
[/d/term/1 %belt %mod %ctl `@c`%e]
[/d/term/1 %belt %mod %ctl `@c`%u]
[/d/term/1 %belt %txt ((list @c) what)]
[/d/term/1 %belt %ret ~]
==
::
:: Control character
@ -40,7 +40,7 @@
|= [who=ship what=term]
^- (list ph-event)
%+ send-events-to who
:~ [//term/1 %belt %mod %ctl (,@c what)]
:~ [/d/term/1 %belt %mod %ctl (,@c what)]
==
::
:: Inject a file into a ship
@ -54,7 +54,7 @@
[path ~ /text/plain (as-octs:mimes:html txt)]
%+ send-events-to who
:~
[//sync/0v1n.2m9vh %into des | input]
[/c/sync/0v1n.2m9vh %into des | input]
==
::
:: Checks whether the given event is a dojo output blit containing the

View File

@ -5,12 +5,13 @@
::
+$ pill
$% [%ivory p=(list)]
$: %pill
nam=term
boot-ova=(list)
kernel-ova=(list unix-event)
userspace-ova=(list unix-event)
== ==
$: %pill
nam=term
boot-ova=(list)
kernel-ova=(list unix-event)
userspace-ova=(list unix-event)
==
==
::
+$ unix-event
%+ pair wire
@ -18,6 +19,8 @@
[%what p=(list (pair path (cask)))]
[%whom p=ship]
[%boot ? $%($>(%fake task:jael) $>(%dawn task:jael))]
[%wyrd p=vere]
[%verb p=(unit ?)]
unix-task
==
:: +boot-ovum: boostrap kernel filesystem load

15
pkg/arvo/mar/csv.hoon Normal file
View File

@ -0,0 +1,15 @@
=, format
=, mimes:html
|_ csv=wain
::
++ grab :: convert from
|%
++ mime |=((pair mite octs) (to-wain q.q))
++ noun wain :: clam from %noun
--
++ grow
|%
++ mime [/text/csv (as-octs (of-wain csv))]
--
++ grad %mime
--

View File

@ -28,7 +28,7 @@
+$ pill pill:pill-lib
::
+$ aqua-event
$% [%init-ship who=ship keys=(unit dawn-event:jael)]
$% [%init-ship who=ship fake=?]
[%pause-events who=ship]
[%snap-ships lab=term hers=(list ship)]
[%restore-snap lab=term]

9
pkg/arvo/sur/crunch.hoon Normal file
View File

@ -0,0 +1,9 @@
/- resource
::
|%
+$ channel-info
$: group=resource:resource
channel=resource:resource
channel-type=term
==
--

View File

@ -18,7 +18,7 @@
|= [our=ship who=@p]
^- (list card:agent:gall)
%+ emit-aqua-events our
[%event who [//newt/0v1n.2m9vh %born ~]]~
[%event who [/a/newt/0v1n.2m9vh %born ~]]~
::
++ handle-send
|= [our=ship now=@da sndr=@p way=wire %send lan=lane:ames pac=@]
@ -26,7 +26,7 @@
=/ rcvr=ship (lane-to-ship lan)
=/ hear-lane (ship-to-lane sndr)
%+ emit-aqua-events our
[%event rcvr //newt/0v1n.2m9vh %hear hear-lane pac]~
[%event rcvr /a/newt/0v1n.2m9vh %hear hear-lane pac]~
:: +lane-to-ship: decode a ship from an aqua lane
::
:: Special-case one comet, since its address doesn't fit into a lane.

View File

@ -40,7 +40,7 @@
^+ ..abet-pe
=. this
%- emit-aqua-events
[%event who [//behn/0v1n.2m9vh %born ~]]~
[%event who [/b/behn/0v1n.2m9vh %born ~]]~
..abet-pe
::
++ handle-doze
@ -82,7 +82,7 @@
:_ ~
^- aqua-event
:+ %event who
[//behn/0v1n.2m9vh [%wake ~]]
[/b/behn/0v1n.2m9vh [%wake ~]]
..abet-pe
--
--

View File

@ -179,7 +179,7 @@
:_ ~
:* %event
her
//http-client/0v1n.2m9vh
/i/http-client/0v1n.2m9vh
%receive
num.u.ask
[%start [200 ~] `(as-octs:mimes:html resp) &]
@ -274,7 +274,7 @@
=/ clan (clan:title who)
?- clan
?(%czar %king %duke)
;< ~ bind:m (raw-ship:ph-io who `(dawn who ~))
;< ~ bind:m (init-ship:ph-io who |)
(pure:m state)
::
?(%earl %pawn)
@ -294,7 +294,7 @@
=/ rank ?:(=(%earl clan) "moon" "comet")
"|{rank} {(scow %p who)}, =public-key {(scow %uw pass)}"
;< ~ bind:m (dojo:ph-io spon com)
;< ~ bind:m (raw-ship:ph-io who `(dawn who `seed))
;< ~ bind:m (init-ship:ph-io who |)
(pure:m state)
==
::

View File

@ -39,7 +39,7 @@
^+ ..abet-pe
=. this
%- emit-aqua-events
[%event who [//http/0v1n.2m9vh %born ~]]~
[%event who [/i/http/0v1n.2m9vh %born ~]]~
..abet-pe
::
++ handle-thus
@ -81,7 +81,7 @@
..abet-pe
=. http-requests (~(del in http-requests) num)
=. this
(emit-aqua-events [%event who [//http/0v1n.2m9vh %receive num [%start [p.res q.res] r.res &]]]~)
(emit-aqua-events [%event who [/i/http/0v1n.2m9vh %receive num [%start [p.res q.res] r.res &]]]~)
..abet-pe
::
:: Got error in HTTP response

View File

@ -5,8 +5,8 @@
|= args=vase
=/ m (strand ,vase)
;< ~ bind:m start-simple
;< ~ bind:m (raw-ship ~bud ~)
;< ~ bind:m (init-ship ~bud &)
;< ~ bind:m (dojo ~bud "[%test-result (add 2 3)]")
;< ~ bind:m (wait-for-output ~bud "[%test-result 5]")
;< ~ bind:m end-simple
;< ~ bind:m end
(pure:m *vase)

View File

@ -4,8 +4,8 @@
^- thread:spider
|= vase
=/ m (strand ,vase)
;< az=tid:spider bind:m start-azimuth
;< ~ bind:m (spawn az ~bud)
;< ~ bind:m (real-ship az ~bud)
;< ~ bind:m end-azimuth
;< ~ bind:m start-azimuth
;< ~ bind:m (spawn ~bud)
;< ~ bind:m (init-ship ~bud |)
;< ~ bind:m end
(pure:m *vase)

View File

@ -5,8 +5,8 @@
|= vase
=/ m (strand ,vase)
;< ~ bind:m start-simple
;< ~ bind:m (raw-ship ~bud ~)
;< ~ bind:m (raw-ship ~marbud ~)
;< ~ bind:m (raw-ship ~linnup-torsyx ~)
;< ~ bind:m end-simple
;< ~ bind:m (init-ship ~bud &)
;< ~ bind:m (init-ship ~marbud &)
;< ~ bind:m (init-ship ~linnup-torsyx &)
;< ~ bind:m end
(pure:m *vase)

View File

@ -1,19 +1,19 @@
/- spider
/+ *ph-io, *ph-util
/+ *ph-io, *ph-util, strandio
=, strand=strand:spider
^- thread:spider
|= vase
=/ m (strand ,vase)
;< =bowl:spider bind:m get-bowl
;< ~ bind:m start-simple
;< ~ bind:m init-azimuth
;< ~ bind:m (spawn-aqua ~bud)
;< ~ bind:m (spawn-aqua ~dev)
;< ~ bind:m (init-ship ~bud)
;< ~ bind:m (init-ship ~dev)
;< ~ bind:m start-azimuth
;< ~ bind:m (spawn ~bud)
;< ~ bind:m (spawn ~dev)
;< ~ bind:m (init-ship ~bud |)
;< ~ bind:m (init-ship ~dev |)
;< ~ bind:m (sleep:strandio ~s10)
;< ~ bind:m (send-hi ~bud ~dev)
;< ~ bind:m (breach-and-hear-aqua ~dev ~bud)
;< ~ bind:m (breach-and-hear ~dev ~bud)
;< ~ bind:m (send-hi-not-responding ~bud ~dev)
;< ~ bind:m (init-ship ~dev)
;< ~ bind:m (init-ship ~dev |)
;< ~ bind:m (wait-for-output ~bud "hi ~dev successful")
(pure:m *vase)

View File

@ -8,20 +8,19 @@
^- thread:spider
|= vase
=/ m (strand ,vase)
;< az=tid:spider bind:m
start-azimuth
;< ~ bind:m (spawn az ~bud)
;< ~ bind:m (spawn az ~dev)
;< ~ bind:m (spawn az ~marbud)
;< ~ bind:m (spawn az ~mardev)
;< ~ bind:m (real-ship az ~bud)
;< ~ bind:m (real-ship az ~dev)
;< ~ bind:m (real-ship az ~marbud)
;< ~ bind:m (real-ship az ~mardev)
;< ~ bind:m start-azimuth
;< ~ bind:m (spawn ~bud)
;< ~ bind:m (spawn ~dev)
;< ~ bind:m (spawn ~marbud)
;< ~ bind:m (spawn ~mardev)
;< ~ bind:m (init-ship ~bud |)
;< ~ bind:m (init-ship ~dev |)
;< ~ bind:m (init-ship ~marbud |)
;< ~ bind:m (init-ship ~mardev |)
;< ~ bind:m (send-hi ~marbud ~mardev)
;< ~ bind:m (breach-and-hear az ~mardev ~marbud)
;< ~ bind:m (breach-and-hear ~mardev ~marbud)
;< ~ bind:m (send-hi-not-responding ~marbud ~mardev)
;< ~ bind:m (real-ship az ~mardev)
;< ~ bind:m (init-ship ~mardev |)
;< ~ bind:m (wait-for-output ~marbud "hi ~mardev successful")
;< ~ bind:m end-azimuth
;< ~ bind:m end
(pure:m *vase)

View File

@ -4,16 +4,15 @@
^- thread:spider
|= vase
=/ m (strand ,vase)
;< az=tid:spider bind:m
start-azimuth
;< ~ bind:m (spawn az ~bud)
;< ~ bind:m (spawn az ~dev)
;< ~ bind:m (real-ship az ~bud)
;< ~ bind:m (real-ship az ~dev)
;< ~ bind:m start-azimuth
;< ~ bind:m (spawn ~bud)
;< ~ bind:m (spawn ~dev)
;< ~ bind:m (init-ship ~bud |)
;< ~ bind:m (init-ship ~dev |)
;< ~ bind:m (send-hi ~bud ~dev)
;< ~ bind:m (breach-and-hear az ~dev ~bud)
;< ~ bind:m (breach-and-hear ~dev ~bud)
;< ~ bind:m (send-hi-not-responding ~bud ~dev)
;< ~ bind:m (real-ship az ~dev)
;< ~ bind:m (init-ship ~dev |)
;< ~ bind:m (wait-for-output ~bud "hi ~dev successful")
;< ~ bind:m end-azimuth
;< ~ bind:m end
(pure:m *vase)

View File

@ -6,19 +6,19 @@
^- thread:spider
|= vase
=/ m (strand ,vase)
;< az=tid:spider bind:m start-azimuth
;< ~ bind:m (spawn az ~bud)
;< ~ bind:m (spawn az ~marbud)
;< ~ bind:m (real-ship az ~bud)
;< ~ bind:m (real-ship az ~marbud)
;< file=@t bind:m (touch-file ~bud %kids %foo)
;< ~ bind:m (check-file-touched ~marbud %home file)
;< ~ bind:m (breach-and-hear az ~bud ~marbud)
;< ~ bind:m (real-ship az ~bud)
;< ~ bind:m (breach-and-hear az ~marbud ~bud)
;< ~ bind:m (real-ship az ~marbud)
;< file=@t bind:m (touch-file ~bud %kids %bar)
;< file=@t bind:m (touch-file ~bud %kids %baz)
;< ~ bind:m (check-file-touched ~marbud %home file)
;< ~ bind:m end-azimuth
;< ~ bind:m start-azimuth
;< ~ bind:m (spawn ~bud)
;< ~ bind:m (spawn ~marbud)
;< ~ bind:m (init-ship ~bud |)
;< ~ bind:m (init-ship ~marbud |)
;< file=@t bind:m (touch-file ~bud %kids %foo)
;< ~ bind:m (check-file-touched ~marbud %home file)
;< ~ bind:m (breach-and-hear ~bud ~marbud)
;< ~ bind:m (init-ship ~bud |)
;< ~ bind:m (breach-and-hear ~marbud ~bud)
;< ~ bind:m (init-ship ~marbud |)
;< file=@t bind:m (touch-file ~bud %kids %bar)
;< file=@t bind:m (touch-file ~bud %kids %baz)
;< ~ bind:m (check-file-touched ~marbud %home file)
;< ~ bind:m end
(pure:m *vase)

View File

@ -8,19 +8,19 @@
^- thread:spider
|= vase
=/ m (strand ,vase)
;< az=tid:spider bind:m start-azimuth
;< ~ bind:m (spawn az ~bud)
;< ~ bind:m (spawn az ~marbud)
;< ~ bind:m (real-ship az ~bud)
;< ~ bind:m (real-ship az ~marbud)
;< file=@t bind:m (touch-file ~bud %kids %foo)
;< ~ bind:m (check-file-touched ~marbud %home file)
;< ~ bind:m (breach az ~bud)
;< ~ bind:m (real-ship az ~bud)
;< ~ bind:m
;< ~ bind:m start-azimuth
;< ~ bind:m (spawn ~bud)
;< ~ bind:m (spawn ~marbud)
;< ~ bind:m (init-ship ~bud |)
;< ~ bind:m (init-ship ~marbud |)
;< file=@t bind:m (touch-file ~bud %kids %foo)
;< ~ bind:m (check-file-touched ~marbud %home file)
;< ~ bind:m (breach ~bud)
;< ~ bind:m (init-ship ~bud |)
;< ~ bind:m
(dojo ~bud "|merge %home ~marbud %kids, =gem %only-this")
;< file=@t bind:m (touch-file ~bud %kids %bar)
;< file=@t bind:m (touch-file ~bud %kids %baz)
;< ~ bind:m (check-file-touched ~marbud %home file)
;< ~ bind:m end-azimuth
;< file=@t bind:m (touch-file ~bud %kids %bar)
;< file=@t bind:m (touch-file ~bud %kids %baz)
;< ~ bind:m (check-file-touched ~marbud %home file)
;< ~ bind:m end
(pure:m *vase)

View File

@ -6,23 +6,23 @@
^- thread:spider
|= vase
=/ m (strand ,vase)
;< az=tid:spider bind:m start-azimuth
;< ~ bind:m (spawn az ~bud)
;< ~ bind:m (spawn az ~marbud)
;< ~ bind:m (real-ship az ~bud)
;< ~ bind:m (real-ship az ~marbud)
;< ~ bind:m start-azimuth
;< ~ bind:m (spawn ~bud)
;< ~ bind:m (spawn ~marbud)
;< ~ bind:m (init-ship ~bud |)
;< ~ bind:m (init-ship ~marbud |)
;< file=@t bind:m (touch-file ~bud %kids %foo)
;< ~ bind:m (check-file-touched ~marbud %home file)
:: Merge so that when we unify history with the %only-this merge later, we
:: don't get a spurious conflict in %home
::
;< ~ bind:m (dojo ~marbud "|merge %kids our %home")
;< ~ bind:m (breach-and-hear az ~bud ~marbud)
;< ~ bind:m (real-ship az ~bud)
;< ~ bind:m (breach-and-hear ~bud ~marbud)
;< ~ bind:m (init-ship ~bud |)
;< ~ bind:m
(dojo ~bud "|merge %kids ~marbud %kids, =gem %only-this")
;< file=@t bind:m (touch-file ~bud %kids %bar)
;< file=@t bind:m (touch-file ~bud %kids %baz)
;< ~ bind:m (check-file-touched ~marbud %home file)
;< ~ bind:m end-azimuth
;< ~ bind:m end
(pure:m *vase)

View File

@ -5,8 +5,8 @@
|= vase
=/ m (strand ,vase)
;< ~ bind:m start-simple
;< ~ bind:m (raw-ship ~bud ~)
;< ~ bind:m (init-ship ~bud &)
;< file=@t bind:m (touch-file ~bud %home %foo)
;< ~ bind:m (check-file-touched ~bud %home file)
;< ~ bind:m end-simple
;< ~ bind:m end
(pure:m *vase)

View File

@ -5,10 +5,10 @@
|= vase
=/ m (strand ,vase)
;< ~ bind:m start-simple
;< ~ bind:m (raw-ship ~bud ~)
;< ~ bind:m (raw-ship ~marbud ~)
;< ~ bind:m (init-ship ~bud &)
;< ~ bind:m (init-ship ~marbud &)
;< file=@t bind:m (touch-file ~bud %home %foo)
;< ~ bind:m (dojo ~bud "|merge %kids our %home")
;< ~ bind:m (check-file-touched ~marbud %home file)
;< ~ bind:m end-simple
;< ~ bind:m end
(pure:m *vase)

View File

@ -6,12 +6,12 @@
|^
=/ m (strand ,vase)
;< ~ bind:m start-simple
;< ~ bind:m (raw-ship ~bud ~)
;< ~ bind:m (raw-ship ~marbud ~)
;< ~ bind:m (init-ship ~bud |)
;< ~ bind:m (init-ship ~marbud |)
;< [path @t] bind:m (modify ~bud %home)
;< [=path file=@t] bind:m (modify ~bud %kids)
;< ~ bind:m (check-touched ~marbud %kids path file)
;< ~ bind:m end-simple
;< ~ bind:m end
(pure:m *vase)
::
++ modify

View File

@ -42,27 +42,26 @@
^- thread:spider
|= args=vase
=/ m (strand ,vase)
;< az=tid:spider
bind:m start-azimuth
;< ~ bind:m (spawn az ~bud)
;< ~ bind:m (spawn az ~marbud)
;< ~ bind:m (spawn az ~zod)
;< ~ bind:m (spawn az ~marzod)
;< ~ bind:m (real-ship az ~bud)
;< ~ bind:m (real-ship az ~marbud)
;< ~ bind:m start-azimuth
;< ~ bind:m (spawn ~bud)
;< ~ bind:m (spawn ~marbud)
;< ~ bind:m (spawn ~zod)
;< ~ bind:m (spawn ~marzod)
;< ~ bind:m (init-ship ~bud |)
;< ~ bind:m (init-ship ~marbud |)
;< ~ bind:m (wait-for-goad ~marbud)
;< ~ bind:m (real-ship az ~zod)
;< ~ bind:m (real-ship az ~marzod)
;< ~ bind:m (init-ship ~zod |)
;< ~ bind:m (init-ship ~marzod |)
;< ~ bind:m (wait-for-goad ~marzod)
;< ~ bind:m (start-group-agents ~marbud)
;< ~ bind:m (start-group-agents ~marzod)
;< ~ bind:m (dojo ~marbud ":group-store|create 'test-group'")
;< ~ bind:m (wait-for-output ~marbud ">=")
;< ~ bind:m (sleep ~s1)
;< ~ bind:m (breach-and-hear az ~marzod ~marbud)
;< ~ bind:m (real-ship az ~marzod)
;< ~ bind:m (breach-and-hear ~marzod ~marbud)
;< ~ bind:m (init-ship ~marzod |)
;< ~ bind:m (wait-for-goad ~marzod)
;< ~ bind:m (start-group-agents ~marzod)
;< ~ bind:m (sleep ~s3)
;< ~ bind:m end-azimuth
;< ~ bind:m end
(pure:m *vase)

View File

@ -4,12 +4,11 @@
^- thread:spider
|= vase
=/ m (strand ,vase)
;< az=tid:spider
bind:m start-azimuth
;< ~ bind:m (spawn az ~bud)
;< ~ bind:m (spawn az ~dev)
;< ~ bind:m (real-ship az ~bud)
;< ~ bind:m (real-ship az ~dev)
;< ~ bind:m start-azimuth
;< ~ bind:m (spawn ~bud)
;< ~ bind:m (spawn ~dev)
;< ~ bind:m (init-ship ~bud |)
;< ~ bind:m (init-ship ~dev |)
;< ~ bind:m (send-hi ~bud ~dev)
;< ~ bind:m end-azimuth
;< ~ bind:m end
(pure:m *vase)

View File

@ -5,22 +5,21 @@
|= vase
=/ m (strand ,vase)
=/ comet ~bosrym-podwyl-magnes-dacrys--pander-hablep-masrym-marbud
;< az=tid:spider
bind:m start-azimuth
;< ~ bind:m (spawn az ~bud)
;< ~ bind:m (real-ship az ~bud)
;< ~ bind:m start-azimuth
;< ~ bind:m (spawn ~bud)
;< ~ bind:m (init-ship ~bud |)
::
;< ~ bind:m (spawn az ~marbud)
;< ~ bind:m (real-ship az ~marbud)
;< ~ bind:m (spawn ~marbud)
;< ~ bind:m (init-ship ~marbud |)
::
;< ~ bind:m (real-ship az comet)
;< ~ bind:m (send-hi comet ~bud)
;< ~ bind:m (init-ship comet |)
;< ~ bind:m (send-hi comet ~bud)
::
;< ~ bind:m (spawn az ~linnup-torsyx)
;< ~ bind:m (real-ship az ~linnup-torsyx)
;< ~ bind:m (spawn ~linnup-torsyx)
;< ~ bind:m (init-ship ~linnup-torsyx |)
::
;< ~ bind:m (send-hi comet ~linnup-torsyx)
;< ~ bind:m (send-hi ~linnup-torsyx comet)
::
;< ~ bind:m end-azimuth
;< ~ bind:m end
(pure:m *vase)

View File

@ -4,16 +4,15 @@
^- thread:spider
|= vase
=/ m (strand ,vase)
;< az=tid:spider
bind:m start-azimuth
;< ~ bind:m (spawn az ~bud)
;< ~ bind:m (spawn az ~marbud)
;< ~ bind:m (spawn az ~dev)
;< ~ bind:m (spawn az ~mardev)
;< ~ bind:m (real-ship az ~bud)
;< ~ bind:m (real-ship az ~marbud)
;< ~ bind:m (real-ship az ~dev)
;< ~ bind:m (real-ship az ~mardev)
;< ~ bind:m start-azimuth
;< ~ bind:m (spawn ~bud)
;< ~ bind:m (spawn ~marbud)
;< ~ bind:m (spawn ~dev)
;< ~ bind:m (spawn ~mardev)
;< ~ bind:m (init-ship ~bud |)
;< ~ bind:m (init-ship ~marbud |)
;< ~ bind:m (init-ship ~dev |)
;< ~ bind:m (init-ship ~mardev |)
;< ~ bind:m (send-hi ~mardev ~marbud)
;< ~ bind:m end-azimuth
;< ~ bind:m end
(pure:m *vase)

View File

@ -4,14 +4,13 @@
^- thread:spider
|= vase
=/ m (strand ,vase)
;< az=tid:spider
bind:m start-azimuth
;< ~ bind:m (spawn az ~bud)
;< ~ bind:m (spawn az ~marbud)
;< ~ bind:m (spawn az ~linnup-torsyx)
;< ~ bind:m (real-ship az ~linnup-torsyx)
;< ~ bind:m (real-ship az ~marbud)
;< ~ bind:m (real-ship az ~bud)
;< ~ bind:m start-azimuth
;< ~ bind:m (spawn ~bud)
;< ~ bind:m (spawn ~marbud)
;< ~ bind:m (spawn ~linnup-torsyx)
;< ~ bind:m (init-ship ~linnup-torsyx |)
;< ~ bind:m (init-ship ~marbud |)
;< ~ bind:m (init-ship ~bud |)
;< ~ bind:m (send-hi ~linnup-torsyx ~marbud)
;< ~ bind:m end-azimuth
;< ~ bind:m end
(pure:m *vase)

View File

@ -4,14 +4,13 @@
^- thread:spider
|= vase
=/ m (strand ,vase)
;< az=tid:spider
bind:m start-azimuth
;< ~ bind:m (spawn az ~bud)
;< ~ bind:m (spawn az ~marbud)
;< ~ bind:m (spawn az ~linnup-torsyx)
;< ~ bind:m (real-ship az ~bud)
;< ~ bind:m (real-ship az ~marbud)
;< ~ bind:m (real-ship az ~linnup-torsyx)
;< ~ bind:m start-azimuth
;< ~ bind:m (spawn ~bud)
;< ~ bind:m (spawn ~marbud)
;< ~ bind:m (spawn ~linnup-torsyx)
;< ~ bind:m (init-ship ~bud |)
;< ~ bind:m (init-ship ~marbud |)
;< ~ bind:m (init-ship ~linnup-torsyx |)
;< ~ bind:m (send-hi ~linnup-torsyx ~marbud)
;< ~ bind:m end-azimuth
;< ~ bind:m end
(pure:m *vase)

View File

@ -4,12 +4,11 @@
^- thread:spider
|= vase
=/ m (strand ,vase)
;< az=tid:spider
bind:m start-azimuth
;< ~ bind:m (spawn az ~bud)
;< ~ bind:m (spawn az ~marbud)
;< ~ bind:m (real-ship az ~bud)
;< ~ bind:m (real-ship az ~marbud)
;< ~ bind:m start-azimuth
;< ~ bind:m (spawn ~bud)
;< ~ bind:m (spawn ~marbud)
;< ~ bind:m (init-ship ~bud |)
;< ~ bind:m (init-ship ~marbud |)
;< ~ bind:m (send-hi ~bud ~marbud)
;< ~ bind:m end-azimuth
;< ~ bind:m end
(pure:m *vase)

View File

@ -4,14 +4,13 @@
^- thread:spider
|= vase
=/ m (strand ,vase)
;< az=tid:spider
bind:m start-azimuth
;< ~ bind:m (spawn az ~bud)
;< ~ bind:m (spawn az ~marbud)
;< ~ bind:m (spawn az ~dev)
;< ~ bind:m (real-ship az ~bud)
;< ~ bind:m (real-ship az ~marbud)
;< ~ bind:m (real-ship az ~dev)
;< ~ bind:m start-azimuth
;< ~ bind:m (spawn ~bud)
;< ~ bind:m (spawn ~marbud)
;< ~ bind:m (spawn ~dev)
;< ~ bind:m (init-ship ~bud |)
;< ~ bind:m (init-ship ~marbud |)
;< ~ bind:m (init-ship ~dev |)
;< ~ bind:m (send-hi ~dev ~marbud)
;< ~ bind:m end-azimuth
;< ~ bind:m end
(pure:m *vase)

View File

@ -4,14 +4,13 @@
^- thread:spider
|= vase
=/ m (strand ,vase)
;< az=tid:spider
bind:m start-azimuth
;< ~ bind:m (spawn az ~bud)
;< ~ bind:m (spawn az ~marbud)
;< ~ bind:m (spawn az ~dev)
;< ~ bind:m (real-ship az ~bud)
;< ~ bind:m (real-ship az ~marbud)
;< ~ bind:m (real-ship az ~dev)
;< ~ bind:m start-azimuth
;< ~ bind:m (spawn ~bud)
;< ~ bind:m (spawn ~marbud)
;< ~ bind:m (spawn ~dev)
;< ~ bind:m (init-ship ~bud |)
;< ~ bind:m (init-ship ~marbud |)
;< ~ bind:m (init-ship ~dev |)
;< ~ bind:m (send-hi ~marbud ~dev)
;< ~ bind:m end-azimuth
;< ~ bind:m end
(pure:m *vase)

View File

@ -5,9 +5,9 @@
|= vase
=/ m (strand ,vase)
;< ~ bind:m start-simple
;< ~ bind:m (raw-ship ~bud ~)
;< ~ bind:m (raw-ship ~dev ~)
;< ~ bind:m (raw-ship ~dev ~)
;< ~ bind:m (init-ship ~bud &)
;< ~ bind:m (init-ship ~dev &)
;< ~ bind:m (init-ship ~dev &)
;< ~ bind:m (send-hi ~bud ~dev)
;< ~ bind:m end-simple
;< ~ bind:m end
(pure:m *vase)

View File

@ -1,6 +1,7 @@
/- spider
/+ *ph-io, *strandio
/+ io=ph-io, *strandio
=>
=, io
|%
++ strand strand:spider
++ start-agents
@ -27,12 +28,11 @@
^- thread:spider
|= vase
=/ m (strand ,vase)
;< az=tid:spider
bind:m start-azimuth
;< ~ bind:m (spawn az ~bud)
;< ~ bind:m (spawn az ~dev)
;< ~ bind:m (real-ship az ~bud)
;< ~ bind:m (real-ship az ~dev)
;< ~ bind:m start-azimuth
;< ~ bind:m (spawn ~bud)
;< ~ bind:m (spawn ~dev)
;< ~ bind:m (init-ship ~bud |)
;< ~ bind:m (init-ship ~dev |)
;< ~ bind:m (start-agents ~bud)
;< ~ bind:m (start-agents ~dev)
;< ~ bind:m (send-hi ~bud ~dev)
@ -61,5 +61,6 @@
;< ~ bind:m (dojo ~dev ":graph-store +dbug")
;< ~ bind:m (dojo ~bud ":graph-push-hook +dbug %bowl")
;< ~ bind:m (dojo ~bud ":graph-store +dbug")
;< ~ bind:m end-azimuth
;< ~ bind:m end
(pure:m *vase)
::(pure:m *vase)

View File

@ -17,6 +17,6 @@
:: ;< ~ bind:m (send-hi ~web ~zod)
:: ;< ~ bind:m (send-hi ~web ~bus)
(pure:m *vase)
;< ~ bind:m (breach-aqua i.who)
;< ~ bind:m (init-ship i.who)
;< ~ bind:m (breach i.who)
;< ~ bind:m (init-ship i.who |)
loop(who t.who)

View File

View File

@ -4,15 +4,14 @@
^- thread:spider
|= vase
=/ m (strand ,vase)
;< ~ bind:m start-simple
;< ~ bind:m init-azimuth
;< ~ bind:m (spawn-aqua ~zod)
;< ~ bind:m (spawn-aqua ~bus)
;< ~ bind:m (spawn-aqua ~web)
;< ~ bind:m start-azimuth
;< ~ bind:m (spawn ~zod)
;< ~ bind:m (spawn ~bus)
;< ~ bind:m (spawn ~web)
::
;< ~ bind:m (init-ship ~zod)
;< ~ bind:m (init-ship ~bus)
;< ~ bind:m (init-ship ~web)
;< ~ bind:m (init-ship ~zod |)
;< ~ bind:m (init-ship ~bus |)
;< ~ bind:m (init-ship ~web |)
::
;< ~ bind:m (send-hi ~zod ~web)
;< ~ bind:m (send-hi ~zod ~bus)

View File

@ -32,7 +32,7 @@
[~zod %graph-1]
'graph 1'
'desc 1'
~
`%graph-validator-chat
[%group group-rid]
'fake'
==
@ -42,7 +42,7 @@
[~bus %graph-2]
'graph 2'
'desc 2'
~
`%graph-validator-chat
[%group group-rid]
'fake'
==
@ -52,11 +52,19 @@
[~web %graph-3]
'graph 3'
'desc 3'
~
`%graph-validator-chat
[%policy %invite (sy ~zod ~bus ~)]
'fake'
==
::
;< ~ bind:m (poke-app ~zod %group-store %group-update-0 [%add-tag group-rid %admin (sy ~bus ~)])
;< ~ bind:m (send-hi ~zod ~bus)
;< ~ bind:m (send-hi ~zod ~web)
;< ~ bind:m (send-hi ~bus ~zod)
;< ~ bind:m (send-hi ~bus ~web)
;< ~ bind:m (send-hi ~web ~zod)
;< ~ bind:m (send-hi ~web ~bus)
;< ~ bind:m (dojo-thread ~zod %graph-create %graph-view-action create-1)
;< ~ bind:m (dojo-thread ~bus %graph-create %graph-view-action create-2)
;< ~ bind:m (dojo-thread ~web %graph-create %graph-view-action create-3)
@ -71,11 +79,12 @@
=/ join-3=action:graph-view
[%join [~web %graph-3] ~web]
::
;< ~ bind:m (dojo-thread ~zod %graph-join %graph-view-action join-2)
;< ~ bind:m (dojo-thread ~zod %graph-join %graph-view-action join-3)
;< ~ bind:m (dojo-thread ~bus %graph-join %graph-view-action join-1)
;< ~ bind:m (dojo-thread ~bus %graph-join %graph-view-action join-3)
;< ~ bind:m (sleep ~s10)
;< ~ bind:m (poke-app ~zod %group-view %group-view-action join-3)
;< ~ bind:m (poke-app ~bus %group-view %group-view-action join-3)
;< ~ bind:m (dojo-thread ~web %graph-join %graph-view-action join-1)
;< ~ bind:m (dojo-thread ~bus %graph-join %graph-view-action join-1)
;< ~ bind:m (dojo-thread ~zod %graph-join %graph-view-action join-2)
;< ~ bind:m (dojo-thread ~web %graph-join %graph-view-action join-2)
;< ~ bind:m (sleep ~s30)
::

View File

@ -0,0 +1,49 @@
/- spider,
graph-store,
graph-view,
post,
*resource,
*group
/+ *ph-io, strandio
=, strand=strand:spider
=>
|%
::
++ create-group
|= our=@p
%^ dojo-thread our %group-create
:- %group-view-action
:* %create
%group-1
[%open ~ ~]
'Test Group'
'A description'
==
::
++ join-group
|= our=@p
%^ poke-app our %group-view
:- %group-view-action
:* %join
[~zod %group-1]
~zod
==
--
::
^- thread:spider
|= vase
=/ m (strand ,vase)
;< ~ bind:m start-simple
;< ~ bind:m (create-group ~zod)
;< ~ bind:m (join-group ~bus)
;< ~ bind:m (join-group ~web)
;< ~ bind:m (send-hi ~zod ~bus)
;< ~ bind:m (send-hi ~zod ~web)
;< ~ bind:m (send-hi ~bus ~zod)
;< ~ bind:m (send-hi ~bus ~web)
;< ~ bind:m (send-hi ~web ~zod)
;< ~ bind:m (send-hi ~web ~bus)
(pure:m *vase)

View File

@ -0,0 +1,38 @@
/- spider,
graph-store,
graph-view,
post,
*resource,
*group
/+ *ph-io, strandio
=, strand=strand:spider
=>
|%
::
++ create-group
|= our=@p
%^ dojo-thread our %group-create
:- %group-view-action
:* %create
%group-1
[%open ~ ~]
'Test Group'
'A description'
==
::
++ hang
=/ m (strand ,~)
^- form:m
|= tin=strand-input:strand
`[%wait ~]
--
::
^- thread:spider
|= vase
=/ m (strand ,vase)
;< ~ bind:m start-simple
;< ~ bind:m hang
(pure:m *vase)

View File

@ -31,20 +31,24 @@
;< ~ bind:m (start-agent ship %group-store)
;< ~ bind:m (start-agent ship %group-pull-hook)
;< ~ bind:m (start-agent ship %group-push-hook)
;< ~ bind:m (start-agent ship %group-view)
;< ~ bind:m (start-agent ship %dm-hook)
::
;< ~ bind:m (start-agent ship %hark-store)
;< ~ bind:m (start-agent ship %hark-graph-hook)
;< ~ bind:m (start-agent ship %hark-group-hook)
::
;< ~ bind:m (start-agent ship %metadata-store)
;< ~ bind:m (start-agent ship %metadata-hook)
;< ~ bind:m (start-agent ship %metadata-pull-hook)
;< ~ bind:m (start-agent ship %metadata-push-hook)
::
;< ~ bind:m (start-agent ship %invite-store)
;< ~ bind:m (start-agent ship %invite-hook)
::
;< ~ bind:m (start-agent ship %chat-store)
;< ~ bind:m (start-agent ship %chat-hook)
;< ~ bind:m (start-agent ship %chat-view)
::
;< ~ bind:m (start-agent ship %contact-store)
;< ~ bind:m (start-agent ship %contact-hook)
;< ~ bind:m (start-agent ship %contact-view)
;< ~ bind:m (start-agent ship %contact-push-hook)
;< ~ bind:m (start-agent ship %contact-pull-hook)
::
;< ~ bind:m (start-agent ship %graph-store)
;< ~ bind:m (start-agent ship %graph-push-hook)

View File

@ -0,0 +1,42 @@
/- spider,
graph-store,
graph-view,
post,
*resource,
*group
/+ *ph-io, strandio
=, strand=strand:spider
=>
|%
::
++ create-group
|= our=@p
%^ dojo-thread our %group-create
:- %group-view-action
:* %create
%group-1
[%open ~ ~]
'Test Group'
'A description'
==
::
++ join-group
|= our=@p
%^ poke-app our %group-view
:- %group-view-action
:* %join
[~zod %group-1]
~zod
==
--
::
^- thread:spider
|= vase
=/ m (strand ,vase)
;< ~ bind:m start-simple
;< ~ bind:m (sleep ~s10)
;< ~ bind:m end
(pure:m *vase)

View File

@ -4,20 +4,19 @@
^- thread:spider
|= vase
=/ m (strand ,vase)
;< az=tid:spider bind:m
start-azimuth
;< ~ bind:m (spawn az ~bud)
;< ~ bind:m (spawn az ~marbud)
;< ~ bind:m (spawn az ~linnup-torsyx)
;< ~ bind:m (spawn az ~dev)
;< ~ bind:m (real-ship az ~bud)
;< ~ bind:m (real-ship az ~marbud)
;< ~ bind:m (real-ship az ~linnup-torsyx)
;< ~ bind:m (real-ship az ~linnup-torsyx-linnup-torsyx)
;< ~ bind:m start-azimuth
;< ~ bind:m (spawn ~bud)
;< ~ bind:m (spawn ~marbud)
;< ~ bind:m (spawn ~linnup-torsyx)
;< ~ bind:m (spawn ~dev)
;< ~ bind:m (init-ship ~bud |)
;< ~ bind:m (init-ship ~marbud |)
;< ~ bind:m (init-ship ~linnup-torsyx |)
;< ~ bind:m (init-ship ~linnup-torsyx-linnup-torsyx |)
;< ~ bind:m (send-hi ~bud ~linnup-torsyx-linnup-torsyx)
;< ~ bind:m (send-hi ~linnup-torsyx-linnup-torsyx ~marbud)
;< ~ bind:m (real-ship az ~dev)
;< ~ bind:m (init-ship ~dev |)
;< ~ bind:m (send-hi ~linnup-torsyx-linnup-torsyx ~dev)
;< ~ bind:m (send-hi ~dev ~linnup-torsyx-linnup-torsyx)
;< ~ bind:m end-azimuth
;< ~ bind:m end
(pure:m *vase)

View File

@ -5,12 +5,12 @@
|= vase
=/ m (strand ,vase)
;< ~ bind:m start-simple
;< ~ bind:m (raw-ship ~bud ~)
;< ~ bind:m (raw-ship ~marbud ~)
;< ~ bind:m (raw-ship ~linnup-torsyx ~)
;< ~ bind:m (raw-ship ~dev ~)
;< ~ bind:m (raw-ship ~mardev ~)
;< ~ bind:m (raw-ship ~mitnep-todsut ~)
;< ~ bind:m (init-ship ~bud &)
;< ~ bind:m (init-ship ~marbud &)
;< ~ bind:m (init-ship ~linnup-torsyx &)
;< ~ bind:m (init-ship ~dev &)
;< ~ bind:m (init-ship ~mardev &)
;< ~ bind:m (init-ship ~mitnep-todsut &)
;< ~ bind:m (send-hi ~linnup-torsyx ~mitnep-todsut)
;< ~ bind:m end-simple
;< ~ bind:m end
(pure:m *vase)

View File

@ -0,0 +1,2 @@
config/webpack.dev.js
config/webpack.prod.js

View File

@ -5,4 +5,4 @@ dojo:
it should return with the following hash:
`0v7.v4dng.o33qi.kc497.5jc02.ke5es`
`0v758lj.uf0s5.0nh3m.gunn6.942gj`

View File

@ -6,9 +6,10 @@ 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()));
function copy(src, dest) {
return new Promise((res, rej) =>
fs.copy(src, dest, (err) => (err ? rej(err) : res()))
);
}
class UrbitShipPlugin {
@ -35,9 +36,12 @@ let devServer = {
historyApiFallback: true,
};
const router = _.mapKeys(urbitrc.FLEET || {}, (value, key) => `${key}.localhost:9000`);
const router = _.mapKeys(
urbitrc.FLEET || {},
(value, key) => `${key}.localhost:9000`
);
if(urbitrc.URL) {
if (urbitrc.URL) {
devServer = {
...devServer,
index: '',
@ -45,17 +49,17 @@ if(urbitrc.URL) {
'/~btc/js/bundle/index.*.js': {
target: 'http://localhost:9000',
pathRewrite: (req, path) => {
return '/index.js'
}
return '/index.js';
},
},
'**': {
changeOrigin: true,
target: urbitrc.URL,
router,
// ensure proxy doesn't timeout channels
proxyTimeout: 0
}
}
proxyTimeout: 0,
},
},
};
}
@ -63,30 +67,16 @@ module.exports = {
node: { fs: 'empty' },
mode: 'development',
entry: {
app: './src/index.js'
app: './src/index.tsx',
},
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'
]
}
loader: 'ts-loader',
},
exclude: /node_modules/
exclude: /node_modules/,
},
{
test: /\.css$/i,
@ -96,33 +86,31 @@ module.exports = {
// Translates CSS into CommonJS
'css-loader',
// Compiles Sass to CSS
'sass-loader'
]
}
]
'sass-loader',
],
},
],
},
resolve: {
extensions: ['.js', '.ts', '.tsx']
extensions: ['.js', '.ts', '.tsx'],
},
devtool: 'inline-source-map',
devServer: devServer,
plugins: [
new UrbitShipPlugin(urbitrc)
],
plugins: [new UrbitShipPlugin(urbitrc)],
watch: true,
watchOptions: {
poll: true,
ignored: '/node_modules/'
ignored: '/node_modules/',
},
output: {
filename: 'index.js',
chunkFilename: 'index.js',
path: path.resolve(__dirname, '../dist'),
publicPath: '/',
globalObject: 'this'
globalObject: 'this',
},
optimization: {
minimize: false,
usedExports: true
}
usedExports: true,
},
};

View File

@ -6,55 +6,46 @@ module.exports = {
node: { fs: 'empty' },
mode: 'production',
entry: {
app: './src/index.js'
app: './src/index.tsx',
},
module: {
rules: [
{
test: /\.jsx?$/,
test: /\.(j|t)sx?$/,
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'
]
}
loader: 'ts-loader',
},
exclude: /node_modules/
exclude: /node_modules/,
},
{
test: /\.css$/i,
test: /\.css$/i,
use: [
// Creates `style` nodes from JS strings
'style-loader',
// Translates CSS into CommonJS
'css-loader',
// Compiles Sass to CSS
'sass-loader'
]
}
]
'sass-loader',
],
},
],
},
resolve: {
extensions: ['.js', '.ts', '.tsx']
extensions: ['.js', '.ts', '.tsx'],
},
devtool: 'source-map',
plugins: [
new CleanWebpackPlugin()
],
plugins: [new CleanWebpackPlugin()],
output: {
filename: (pathData) => {
return pathData.chunk.name === 'app' ? 'index.[contenthash].js' : '[name].js';
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
}
usedExports: true,
},
};

View File

@ -1430,6 +1430,22 @@
"@types/node": "*"
}
},
"@types/history": {
"version": "4.7.9",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz",
"integrity": "sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==",
"dev": true
},
"@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
"dev": true,
"requires": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
}
},
"@types/html-minifier-terser": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz",
@ -1441,6 +1457,12 @@
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz",
"integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA=="
},
"@types/lodash": {
"version": "4.14.171",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.171.tgz",
"integrity": "sha512-7eQ2xYLLI/LsicL2nejW9Wyko3lcpN6O/z0ZLHrEQsg280zIdCv1t/0m6UtBjUHokCGBQ3gYTbHzDkZ1xOBwwg==",
"dev": true
},
"@types/minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz",
@ -1459,12 +1481,76 @@
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==",
"dev": true
},
"@types/prop-types": {
"version": "15.7.4",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==",
"dev": true
},
"@types/react": {
"version": "17.0.15",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.15.tgz",
"integrity": "sha512-uTKHDK9STXFHLaKv6IMnwp52fm0hwU+N89w/p9grdUqcFA6WuqDyPhaWopbNyE1k/VhgzmHl8pu1L4wITtmlLw==",
"dev": true,
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
"@types/react-dom": {
"version": "17.0.9",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.9.tgz",
"integrity": "sha512-wIvGxLfgpVDSAMH5utdL9Ngm5Owu0VsGmldro3ORLXV8CShrL8awVj06NuEXFQ5xyaYfdca7Sgbk/50Ri1GdPg==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/react-router": {
"version": "5.1.16",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.16.tgz",
"integrity": "sha512-8d7nR/fNSqlTFGHti0R3F9WwIertOaaA1UEB8/jr5l5mDMOs4CidEgvvYMw4ivqrBK+vtVLxyTj2P+Pr/dtgzg==",
"dev": true,
"requires": {
"@types/history": "*",
"@types/react": "*"
}
},
"@types/react-router-dom": {
"version": "5.1.8",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.8.tgz",
"integrity": "sha512-03xHyncBzG0PmDmf8pf3rehtjY0NpUj7TIN46FrT5n1ZWHPZvXz32gUyNboJ+xsL8cpg8bQVLcllptcQHvocrw==",
"dev": true,
"requires": {
"@types/history": "*",
"@types/react": "*",
"@types/react-router": "*"
}
},
"@types/scheduler": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
"dev": true
},
"@types/source-list-map": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz",
"integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==",
"dev": true
},
"@types/styled-components": {
"version": "5.1.11",
"resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.11.tgz",
"integrity": "sha512-u8g3bSw9KUiZY+S++gh+LlURGraqBe3MC5I5dygrNjGDHWWQfsmZZRTJ9K9oHU2CqWtxChWmJkDI/gp+TZPQMw==",
"dev": true,
"requires": {
"@types/hoist-non-react-statics": "*",
"@types/react": "*",
"csstype": "^3.0.2"
}
},
"@types/tapable": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.7.tgz",
@ -1499,6 +1585,12 @@
"source-map": "^0.6.0"
}
},
"@types/webpack-env": {
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.16.2.tgz",
"integrity": "sha512-vKx7WNQNZDyJveYcHAm9ZxhqSGLYwoyLhrHjLBOkw3a7cT76sTdjgtwyijhk1MaHyRIuSztcVwrUOO/NEu68Dw==",
"dev": true
},
"@types/webpack-sources": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-2.1.0.tgz",
@ -3472,6 +3564,12 @@
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="
},
"csstype": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz",
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==",
"dev": true
},
"cycle": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz",
@ -10240,6 +10338,130 @@
"resolved": "https://registry.npmjs.org/transformation-matrix/-/transformation-matrix-2.1.1.tgz",
"integrity": "sha512-74MoNHhwLVuzwaPDcAecFjSkOA9vwWqyOdkeB0Be8Jc/IWSS5SNZKapFllqzkTliqZptkvqX5CZnVeDvfhN8cw=="
},
"ts-loader": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.2.0.tgz",
"integrity": "sha512-ebXBFrNyMSmbWgjnb3WBloUBK+VSx1xckaXsMXxlZRDqce/OPdYBVN5efB0W3V0defq0Gcy4YuzvPGqRgjj85A==",
"dev": true,
"requires": {
"chalk": "^4.1.0",
"enhanced-resolve": "^4.0.0",
"loader-utils": "^2.0.0",
"micromatch": "^4.0.0",
"semver": "^7.3.4"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"json5": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
"integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
},
"loader-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"requires": {
"yallist": "^4.0.0"
}
},
"micromatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
"integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
"dev": true,
"requires": {
"braces": "^3.0.1",
"picomatch": "^2.2.3"
}
},
"picomatch": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
"integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==",
"dev": true
},
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"requires": {
"has-flag": "^4.0.0"
}
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
}
}
},
"tslib": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
@ -10293,9 +10515,9 @@
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
},
"typescript": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz",
"integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==",
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
"integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==",
"dev": true
},
"unbox-primitive": {

View File

@ -20,6 +20,11 @@
"@babel/preset-env": "^7.9.5",
"@babel/preset-react": "^7.9.4",
"@babel/preset-typescript": "^7.13.0",
"@types/lodash": "^4.14.171",
"@types/react-dom": "^17.0.9",
"@types/react-router-dom": "^5.1.8",
"@types/styled-components": "^5.1.11",
"@types/webpack-env": "^1.16.2",
"@welldone-software/why-did-you-render": "^6.1.1",
"babel-loader": "^8.1.0",
"babel-plugin-root-import": "^6.5.0",
@ -36,7 +41,8 @@
"react-hot-loader": "^4.12.21",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"typescript": "^4.2.3",
"ts-loader": "8.2.0",
"typescript": "^4.3.5",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3"

View File

@ -0,0 +1,40 @@
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import { ThemeProvider } from 'styled-components';
import light from './themes/light';
import { Box, Reset } from '@tlon/indigo-react';
import StartupModal from './components/StartupModal';
import { useSettings } from './hooks/useSettings';
import Body from './components/Body';
const App: React.FC = () => {
const { loaded, wallet, provider, scanProgress } = useSettings();
const scanning = scanProgress?.main !== null || scanProgress?.change !== null;
const blur = !loaded || scanning ? false : !(wallet && provider);
return (
<BrowserRouter>
<ThemeProvider theme={light}>
<Reset />
{loaded && !scanning ? <StartupModal /> : null}
<Box
display="flex"
flexDirection="column"
position="absolute"
alignItems="center"
backgroundColor="lightOrange"
width="100%"
minHeight={loaded && !scanning ? '100%' : 'none'}
height={loaded && !scanning ? 'none' : '100%'}
style={{ filter: blur ? 'blur(8px)' : 'none' }}
px={[0, 4]}
pb={[0, 4]}
>
<Body />
</Box>
</ThemeProvider>
</BrowserRouter>
);
};
export default App;

View File

@ -0,0 +1,162 @@
import React, { useState } from 'react';
import { Row, Text, Button, Col } from '@tlon/indigo-react';
import Send from './Send/Send';
import CurrencyPicker from './CurrencyPicker';
import { copyToClipboard, satsToCurrency } from '../lib/util';
import { useSettings } from '../hooks/useSettings';
import { api } from '../lib/api';
const Balance = () => {
const {
address,
confirmedBalance: sats,
unconfirmedBalance: unconfirmedSats,
denomination,
currencyRates,
setPsbt,
setFee,
setError,
scanProgress,
} = useSettings();
const [sending, setSending] = useState(false);
const [copiedButton, setCopiedButton] = useState(false);
const [copiedString, setCopiedString] = useState(false);
const scanning = scanProgress?.main !== null || scanProgress?.change !== null;
const copyAddress = async (arg: 'string' | 'button') => {
await copyToClipboard(address);
api.btcWalletCommand({ 'gen-new-address': null });
if (arg === 'button') {
setCopiedButton(true);
setTimeout(() => {
setCopiedButton(false);
}, 2000);
} else if (arg === 'string') {
setCopiedString(true);
setTimeout(() => {
setCopiedString(false);
}, 2000);
}
};
const unconfirmedString = unconfirmedSats ? ` (${unconfirmedSats}) ` : '';
const value = satsToCurrency(sats, denomination, currencyRates);
const sendDisabled = sats === 0;
const addressText =
address === null ? '' : address.slice(0, 6) + '...' + address.slice(-6);
const conversion = currencyRates[denomination]?.last;
return (
<>
{sending ? (
<Send
value={value}
conversion={conversion}
stopSending={() => {
setSending(false);
setPsbt('');
setFee(0);
setError('');
}}
/>
) : (
<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={() => copyAddress('string')}
>
{copiedString ? 'copied' : addressText}
</Text>
<CurrencyPicker />
</Row>
<Col justifyContent="center" alignItems="center">
<Text
fontSize="40px"
color="orange"
style={{ whiteSpace: 'nowrap' }}
>
{value}
</Text>
{scanning ? (
<Col alignItems="center">
<Row>
<Text fontSize={1} color="orange">
Balance will be updated shortly:
</Text>
</Row>
<Row>
<Text fontSize={1} color="orange">
{scanProgress.main === null ? 0 : scanProgress.main} main
wallet addresses scanned
</Text>
</Row>
<Text fontSize={1} color="orange">
{scanProgress.change === null ? 0 : scanProgress.change}{' '}
change wallet addresses scanned
</Text>
</Col>
) : (
<Text
fontSize={1}
color="orange"
>{`${sats}${unconfirmedString} sats`}</Text>
)}
</Col>
<Row flexDirection="row-reverse">
<Button
disabled={sendDisabled}
fontSize={1}
fontWeight="bold"
color={sendDisabled ? 'lighterGray' : 'white'}
backgroundColor={sendDisabled ? 'veryLightGray' : 'orange'}
style={{ cursor: sendDisabled ? 'default' : 'pointer' }}
borderColor="none"
borderRadius="24px"
height="48px"
onClick={() => setSending(true)}
>
Send
</Button>
<Button
mr={3}
disabled={copiedButton}
fontSize={1}
fontWeight="bold"
color={copiedButton ? 'green' : 'orange'}
backgroundColor={copiedButton ? 'veryLightGreen' : 'midOrange'}
style={{
cursor: copiedButton ? 'default' : 'pointer',
}}
borderColor="none"
borderRadius="24px"
height="48px"
onClick={() => copyAddress('button')}
>
{copiedButton ? 'Address Copied!' : 'Copy Address'}
</Button>
</Row>
</Col>
)}
</>
);
};
export default Balance;

View File

@ -0,0 +1,44 @@
import React from 'react';
import { Box, LoadingSpinner, Col } from '@tlon/indigo-react';
import { Switch, Route } from 'react-router-dom';
import Balance from './Balance';
import Transactions from './Transactions/Transactions';
import Warning from './Warning';
import Header from './Header';
import Settings from './Settings';
import { useSettings } from '../hooks/useSettings';
const Body: React.FC = () => {
const { loaded, showWarning: warning } = useSettings();
const cardWidth = window.innerWidth <= 475 ? '350px' : '400px';
return !loaded ? (
<Box
display="flex"
width="100%"
height="100%"
alignItems="center"
justifyContent="center"
>
<LoadingSpinner background="midOrange" foreground="orange" />
</Box>
) : (
<Switch>
<Route path="/~btc/settings">
<Col display="flex" flexDirection="column" width={cardWidth}>
<Header settings={true} />
<Settings />
</Col>
</Route>
<Route path="/~btc">
<Col display="flex" flexDirection="column" width={cardWidth}>
<Header settings={false} />
{!warning ? null : <Warning />}
<Balance />
<Transactions />
</Col>
</Route>
</Switch>
);
};
export default Body;

View File

@ -0,0 +1,38 @@
import React from 'react';
import { Icon, Row, Text } from '@tlon/indigo-react';
import { api } from '../lib/api';
import { useSettings } from '../hooks/useSettings';
const CurrencyPicker = () => {
const { denomination, currencyRates } = useSettings();
const switchCurrency = () => {
let newCurrency;
if (denomination === 'BTC') {
if ((currencyRates as any)['USD']) {
newCurrency = 'USD';
}
} else if (denomination === 'USD') {
newCurrency = 'BTC';
}
console.log({ newCurrency, denomination });
let setCurrency = {
'put-entry': {
value: newCurrency,
'entry-key': 'currency',
'bucket-key': 'btc-wallet',
},
};
api.settingsEvent(setCurrency);
};
return (
<Row style={{ cursor: 'pointer' }} onClick={() => switchCurrency()}>
<Icon icon="ChevronDouble" color="orange" pt="2px" pr={1} />
<Text color="orange" fontSize={1}>
{denomination}
</Text>
</Row>
);
};
export default CurrencyPicker;

View File

@ -0,0 +1,32 @@
import React from 'react';
import { Text } from '@tlon/indigo-react';
enum ErrorTypes {
'cant-pay-ourselves' = 'Cannot pay ourselves',
'no-comets' = 'Cannot pay comets',
'no-dust' = 'Cannot send dust',
'tx-being-signed' = 'Cannot pay when transaction is being signed',
'insufficient-balance' = 'Insufficient confirmed balance',
'broadcast-fail' = 'Transaction broadcast failed',
'invalid-master-ticker' = 'Invalid master ticket',
'invalid-signed' = 'Invalid signed bitcoin transaction',
}
const Error = ({
error,
fontSize,
...rest
}: {
error: string;
fontSize?: string;
}) => (
<Text color="red" style={{ fontSize }} {...rest}>
{
(ErrorTypes as any)[
Object.keys(ErrorTypes).filter((et) => et === error)[0]
]
}
</Text>
);
export default Error;

View File

@ -0,0 +1,81 @@
import React from 'react';
import { Box, Icon, Row, Text } from '@tlon/indigo-react';
import { Link } from 'react-router-dom';
import { useSettings } from '../hooks/useSettings';
const Header = ({ settings }: { settings: boolean }) => {
const { provider } = useSettings();
let icon = settings ? 'X' : 'Adjust';
let iconColor = settings ? 'black' : 'orange';
let iconLink = settings ? '/~btc' : '/~btc/settings';
let connection = null;
let badge = null;
if (!(provider && provider.connected)) {
connection = (
<Text fontSize={1} color="red" fontWeight="bold" mr={3}>
Provider Offline
</Text>
);
if (!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>
);
};
export default Header;

View File

@ -0,0 +1,166 @@
import React, { useEffect, useState } from 'react';
import {
Box,
Text,
Button,
StatelessTextInput,
Icon,
Row,
LoadingSpinner,
} from '@tlon/indigo-react';
import { isValidPatp } from 'urbit-ob';
import { api } from '../lib/api';
import { useSettings } from '../hooks/useSettings';
enum providerStatuses {
checking,
failed,
ready,
initial = '',
}
const ProviderModal = () => {
const { providerPerms } = useSettings();
const [providerStatus, setProviderStatus] = useState(
providerStatuses.initial
);
const [potentialProvider, setPotentialProvider] = useState(null);
const [provider, setProvider] = useState(null);
const [connecting, setConnecting] = useState(false);
const checkProvider = (e: React.ChangeEvent<HTMLInputElement>) => {
// TODO: loading states
setProviderStatus(providerStatuses.initial);
let givenProvider = e.target.value;
if (isValidPatp(givenProvider)) {
let command = {
'check-provider': givenProvider,
};
setPotentialProvider(givenProvider);
setProviderStatus(providerStatuses.checking);
api.btcWalletCommand(command);
setTimeout(() => {
setProviderStatus(providerStatuses.failed);
}, 5000);
}
setProvider(givenProvider);
};
const submitProvider = () => {
if (providerStatus === providerStatuses.ready) {
let command = {
'set-provider': provider,
};
api.btcWalletCommand(command);
setConnecting(true);
}
};
useEffect(() => {
if (providerStatus !== providerStatuses.ready) {
if (providerPerms.provider === provider && providerPerms.permitted) {
setProviderStatus(providerStatuses.ready);
}
}
}, [providerStatus, providerPerms, provider, setProviderStatus]);
let workingNode = null;
let workingColor = null;
let workingBg = null;
if (providerStatus === providerStatuses.ready) {
workingColor = 'green';
workingBg = 'veryLightGreen';
workingNode = (
<Box mt={3}>
<Text fontSize="14px" color="green">
{provider} is a working provider node
</Text>
</Box>
);
} else if (providerStatus === providerStatuses.failed) {
workingColor = 'red';
workingBg = 'veryLightRed';
workingNode = (
<Box mt={3}>
<Text fontSize="14px" color="red">
{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&apos;ll need
to set a provider node. A provider node is an urbit which maintains a
synced Bitcoin ledger.
<a
style={{ fontSize: '14px' }}
target="_blank"
href="https://urbit.org/bitcoin-wallet"
rel="noreferrer"
>
{' '}
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={(e: React.ChangeEvent<HTMLInputElement>) =>
checkProvider(e)
}
/>
{providerStatus === providerStatuses.checking ? (
<LoadingSpinner />
) : null}
</Row>
{workingNode}
<Row alignItems="center" mt={3}>
<Button
mr={2}
primary
disabled={providerStatus !== providerStatuses.ready}
fontSize="14px"
style={{
cursor:
providerStatus === providerStatuses.ready ? 'pointer' : 'default',
}}
onClick={() => {
submitProvider();
}}
>
Set Peer Node
</Button>
{connecting ? <LoadingSpinner /> : null}
</Row>
</Box>
);
};
export default ProviderModal;

View File

@ -0,0 +1,223 @@
import React, { useEffect, useRef, useState } from 'react';
import {
Box,
Icon,
StatelessTextInput as Input,
Row,
Text,
Button,
Col,
LoadingSpinner,
} from '@tlon/indigo-react';
import Sigil from '../Sigil';
import * as bitcoin from 'bitcoinjs-lib';
import { isValidPatp } from 'urbit-ob';
import Sent from './Sent';
import Error from '../Error';
import { satsToCurrency } from '../../lib/util';
import { useSettings } from '../../hooks/useSettings';
import { api } from '../../lib/api';
type Props = {
payee: string;
stopSending: () => void;
satsAmount: number;
};
const BridgeInvoice: React.FC<Props> = ({ payee, stopSending, satsAmount }) => {
const { error, currencyRates, fee, broadcastSuccess, denomination, psbt } =
useSettings();
const [txHex, setTxHex] = useState('');
const [ready, setReady] = useState(false);
const [localError, setLocalError] = useState('');
const [broadcasting, setBroadcasting] = useState(false);
const invoiceRef = useRef();
useEffect(() => {
if (broadcasting && localError !== '') {
setBroadcasting(false);
}
if (error !== '') {
setLocalError(error);
}
}, [error, broadcasting, setBroadcasting]);
useEffect(() => {
window.open('https://bridge.urbit.org/?kind=btc&utx=' + psbt);
});
const broadCastTx = (hex: string) => {
let command = {
'broadcast-tx': hex,
};
return api.btcWalletCommand(command);
};
const sendBitcoin = (hex: string) => {
try {
bitcoin.Transaction.fromHex(hex);
broadCastTx(hex);
setBroadcasting(true);
} catch (e) {
setLocalError('invalid-signed');
setBroadcasting(false);
}
};
const checkTxHex = (e: React.ChangeEvent<HTMLInputElement>) => {
setTxHex(e.target.value);
setReady(txHex.length > 0);
setLocalError('');
};
let inputColor = 'black';
let inputBg = 'white';
let inputBorder = 'lightGray';
if (localError !== '') {
inputColor = 'red';
inputBg = 'veryLightRed';
inputBorder = 'red';
}
const isShip = isValidPatp(payee);
const icon = isShip ? (
<Sigil ship={payee} 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>
);
return (
<>
{broadcastSuccess ? (
<Sent payee={payee} stopSending={stopSending} satsAmount={satsAmount} />
) : (
<Col
ref={invoiceRef}
width="100%"
backgroundColor="white"
borderRadius="48px"
mb={5}
p={5}
>
<Row flexDirection="row-reverse">
<Icon icon="X" cursor="pointer" onClick={() => stopSending()} />
</Row>
<Col
p={5}
mt={4}
backgroundColor="veryLightGreen"
borderRadius="24px"
alignItems="center"
>
<Row>
<Text color="green" fontSize="40px">
{satsToCurrency(satsAmount, denomination, currencyRates)}
</Text>
</Row>
<Row>
<Text
fontWeight="bold"
fontSize="16px"
color="midGreen"
>{`${satsAmount} sats`}</Text>
</Row>
<Row mt={2}>
<Text fontSize="14px" color="midGreen">{`Fee: ${satsToCurrency(
fee,
denomination,
currencyRates
)} (${fee} sats)`}</Text>
</Row>
<Row mt={4}>
<Text fontSize="16px" fontWeight="bold" color="gray">
You are paying
</Text>
</Row>
<Row mt={2} alignItems="center">
{icon}
<Text
ml={2}
mono
color="gray"
fontSize="14px"
style={{ display: 'block', overflowWrap: 'anywhere' }}
>
{payee}
</Text>
</Row>
</Col>
<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={txHex}
fontSize="14px"
placeholder="010000000001019e478cc370323ac539097..."
autoCapitalize="none"
autoCorrect="off"
color={inputColor}
backgroundColor={inputBg}
borderColor={inputBorder}
style={{ lineHeight: '4' }}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => checkTxHex(e)}
/>
{localError !== '' && (
<Row>
<Error error={localError} fontSize="14px" />
</Row>
)}
<Row flexDirection="row-reverse" mt={4} alignItems="center">
<Button
primary
mr={3}
fontSize={1}
borderRadius="24px"
border="none"
height="48px"
onClick={() => sendBitcoin(txHex)}
disabled={!ready || localError || broadcasting}
color={
ready && !localError && !broadcasting ? 'white' : 'lighterGray'
}
backgroundColor={
ready && !localError && !broadcasting
? 'green'
: 'veryLightGray'
}
style={{
cursor: ready && !localError ? 'pointer' : 'default',
}}
>
Send BTC
</Button>
{
// @ts-ignore
broadcasting ? <LoadingSpinner mr={3} /> : null
}
</Row>
</Col>
)}
</>
);
};
export default BridgeInvoice;

View File

@ -0,0 +1,275 @@
import React, { useEffect, useRef, useState } from 'react';
import {
Box,
Icon,
StatelessTextInput as Input,
Row,
Text,
Button,
Col,
LoadingSpinner,
} from '@tlon/indigo-react';
import Sigil from '../Sigil';
import * as bitcoin from 'bitcoinjs-lib';
import { isValidPatp } from 'urbit-ob';
import Sent from './Sent';
import Error from '../Error';
import { copyToClipboard, satsToCurrency } from '../../lib/util';
import { useSettings } from '../../hooks/useSettings';
import { api } from '../../lib/api';
type Props = {
payee: string;
stopSending: () => void;
satsAmount: number;
};
const ExternalInvoice: React.FC<Props> = ({
payee,
stopSending,
satsAmount,
}) => {
const { error, currencyRates, fee, broadcastSuccess, denomination, psbt } =
useSettings();
const [txHex, setTxHex] = useState('');
const [ready, setReady] = useState(false);
const [localError, setLocalError] = useState('');
const [broadcasting, setBroadcasting] = useState(false);
const invoiceRef = useRef();
useEffect(() => {
if (broadcasting && localError !== '') {
setBroadcasting(false);
}
if (error !== '') {
setLocalError(error);
}
}, [error, broadcasting, setBroadcasting]);
const broadCastTx = (hex: string) => {
let command = {
'broadcast-tx': hex,
};
return api.btcWalletCommand(command);
};
const sendBitcoin = (hex: string) => {
try {
bitcoin.Transaction.fromHex(hex);
broadCastTx(hex);
setBroadcasting(true);
} catch (e) {
setLocalError('invalid-signed');
setBroadcasting(false);
}
};
const checkTxHex = (e: React.ChangeEvent<HTMLInputElement>) => {
setTxHex(e.target.value);
setReady(txHex.length > 0);
setLocalError('');
};
const copyPsbt = () => {
copyToClipboard(psbt);
};
const downloadPsbtFile = () => {
const blob = new Blob([psbt]);
const downloadURL = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = downloadURL;
link.setAttribute('download', 'urbit.psbt');
document.body.appendChild(link);
link.click();
link.parentNode.removeChild(link);
};
let inputColor = 'black';
let inputBg = 'white';
let inputBorder = 'lightGray';
if (localError !== '') {
inputColor = 'red';
inputBg = 'veryLightRed';
inputBorder = 'red';
}
const isShip = isValidPatp(payee);
const icon = isShip ? (
<Sigil ship={payee} 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>
);
return (
<>
{broadcastSuccess ? (
<Sent payee={payee} stopSending={stopSending} satsAmount={satsAmount} />
) : (
<Col
ref={invoiceRef}
width="100%"
backgroundColor="white"
borderRadius="48px"
mb={5}
p={5}
>
<Row flexDirection="row-reverse">
<Icon icon="X" cursor="pointer" onClick={() => stopSending()} />
</Row>
<Col
p={5}
mt={4}
backgroundColor="veryLightGreen"
borderRadius="24px"
alignItems="center"
>
<Row>
<Text color="green" fontSize="40px">
{satsToCurrency(satsAmount, denomination, currencyRates)}
</Text>
</Row>
<Row>
<Text
fontWeight="bold"
fontSize="16px"
color="midGreen"
>{`${satsAmount} sats`}</Text>
</Row>
<Row mt={2}>
<Text fontSize="14px" color="midGreen">{`Fee: ${satsToCurrency(
fee,
denomination,
currencyRates
)} (${fee} sats)`}</Text>
</Row>
<Row mt={4}>
<Text fontSize="16px" fontWeight="bold" color="gray">
You are paying
</Text>
</Row>
<Row mt={2} alignItems="center">
{icon}
<Text
ml={2}
mono
color="gray"
fontSize="14px"
style={{ display: 'block', overflowWrap: 'anywhere' }}
>
{payee}
</Text>
</Row>
</Col>
<Box mt={3}>
<Text fontSize="14px" fontWeight="500">
Partially-signed Bitcoin Transaction (PSBT)
</Text>
</Box>
<Box mt={3}>
<Row flexDirection="row">
<Button
borderRadius="24px"
border="none"
width="24px"
height="24px"
padding="0px"
backgroundColor="veryLightGray"
onClick={() => downloadPsbtFile()}
mr={2}
>
<Icon icon="Download" width="12px" />
</Button>
<Button
borderRadius="24px"
border="none"
width="24px"
height="24px"
padding="0px"
backgroundColor="veryLightGray"
onClick={() => copyPsbt()}
>
<Icon icon="Copy" />
</Button>
</Row>
</Box>
<Row
justifyContent="flex-end"
alignItems="center"
flexDirection="row"
>
<Text gray bold fontSize="16px" mr={2}>
Signed Tx
</Text>
<Input
value={txHex}
fontSize="14px"
placeholder="7f3..."
autoCapitalize="none"
autoCorrect="off"
color={inputColor}
width="70%"
backgroundColor={inputBg}
borderColor={inputBorder}
style={{ lineHeight: '4' }}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
checkTxHex(e)
}
/>
{localError !== '' && (
<Row>
<Error error={localError} fontSize="14px" />
</Row>
)}
</Row>
<Row
flexDirection="row"
mt={4}
alignItems="center"
justifyContent="center"
>
<Button
primary
mr={3}
fontSize={1}
borderRadius="24px"
border="none"
height="48px"
width="100%"
onClick={() => sendBitcoin(txHex)}
disabled={!ready || localError || broadcasting}
color={
ready && !localError && !broadcasting ? 'white' : 'lighterGray'
}
backgroundColor={
ready && !localError && !broadcasting
? 'green'
: 'veryLightGray'
}
style={{
cursor: ready && !localError ? 'pointer' : 'default',
}}
>
Send BTC
</Button>
{broadcasting ? <LoadingSpinner /> : null}
</Row>
</Col>
)}
</>
);
};
export default ExternalInvoice;

View File

@ -0,0 +1,100 @@
import React, { useEffect, useRef } from 'react';
import {
Box,
Text,
Col,
StatelessRadioButtonField as RadioButton,
Label,
} from '@tlon/indigo-react';
import { FeeChoices, feeLevels } from './Send';
type Props = {
feeChoices: FeeChoices;
feeValue: number;
setFeeValue: React.Dispatch<feeLevels>;
feeDismiss: () => void;
};
const FeePicker: React.FC<Props> = ({
feeChoices,
feeValue,
setFeeValue,
feeDismiss,
}) => {
const modalRef = useRef(null);
const clickDismiss = (e: any) => {
if (modalRef && !modalRef.current.contains(e.target)) {
feeDismiss();
}
};
useEffect(() => {
document.addEventListener('click', clickDismiss);
return () => {
document.removeEventListener('click', clickDismiss);
};
}, []);
return (
<Box
position="absolute"
p={4}
border="1px solid green"
zIndex={10}
backgroundColor="white"
borderRadius={3}
ref={modalRef}
>
<Text fontSize={1} color="black" fontWeight="bold" mb={4}>
Transaction Speed
</Text>
<Col mt={4}>
<RadioButton
name="feeRadio"
selected={feeValue === feeLevels.low}
p="2"
onChange={() => {
setFeeValue(feeLevels.low);
feeDismiss();
}}
>
<Label fontSize="14px">
Slow: {feeChoices[feeLevels.low][1]} sats/vbyte ~
{feeChoices[feeLevels.low][0]}m
</Label>
</RadioButton>
<RadioButton
name="feeRadio"
selected={feeValue === feeLevels.mid}
p="2"
onChange={() => {
setFeeValue(feeLevels.mid);
feeDismiss();
}}
>
<Label fontSize="14px">
Normal: {feeChoices[feeLevels.mid][1]} sats/vbyte ~
{feeChoices[feeLevels.mid][0]}m
</Label>
</RadioButton>
<RadioButton
name="feeRadio"
selected={feeValue === feeLevels.high}
p="2"
onChange={() => {
setFeeValue(feeLevels.high);
feeDismiss();
}}
>
<Label fontSize="14px">
Fast: {feeChoices[feeLevels.high][1]} sats/vbyte ~
{feeChoices[feeLevels.high][0]}m
</Label>
</RadioButton>
</Col>
</Box>
);
};
export default FeePicker;

View File

@ -0,0 +1,280 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Icon,
StatelessTextInput as Input,
Row,
Text,
Button,
Col,
LoadingSpinner,
} from '@tlon/indigo-react';
import * as kg from 'urbit-key-generation';
import { patp2dec, isValidPatq, isValidPatp } from 'urbit-ob';
import * as bitcoin from 'bitcoinjs-lib';
import Sigil from '../Sigil';
import Sent from './Sent';
import { satsToCurrency } from '../../lib/util';
import Error from '../Error';
import { useSettings } from '../../hooks/useSettings';
import { api } from '../../lib/api';
import { UrbitWallet } from '../../types';
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,
};
type Props = {
stopSending: () => void;
payee: string;
satsAmount: number;
};
const Invoice: React.FC<Props> = ({ stopSending, payee, satsAmount }) => {
const {
error,
currencyRates,
psbt,
fee,
broadcastSuccess,
network,
denomination,
} = useSettings();
const [masterTicket, setMasterTicket] = useState('');
const [ready, setReady] = useState(false);
const [localError, setLocalError] = useState(error);
const [broadcasting, setBroadcasting] = useState(false);
useEffect(() => {
if (broadcasting && localError !== '') {
setBroadcasting(false);
}
}, [error, broadcasting, setBroadcasting]);
const broadCastTx = (psbtHex: string) => {
let command = {
'broadcast-tx': psbtHex,
};
return api.btcWalletCommand(command);
};
const sendBitcoin = (ticket: string, psbt: string) => {
const newPsbt = bitcoin.Psbt.fromBase64(psbt);
setBroadcasting(true);
kg.generateWallet({
ticket,
ship: parseInt(patp2dec('~' + (window as any).ship)),
}).then((urbitWallet: UrbitWallet) => {
// this wasn't being used, not clear why it was pulled out.
// const { xpub } =
// network === 'testnet'
// ? urbitWallet.bitcoinTestnet.keys
// : urbitWallet.bitcoinMainnet.keys;
const { xprv: zprv } = urbitWallet.bitcoinMainnet.keys;
const { xprv: vprv } = urbitWallet.bitcoinTestnet.keys;
const isTestnet = 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();
broadCastTx(hex);
} catch (e) {
setLocalError('invalid-master-ticket');
setBroadcasting(false);
}
});
};
const checkTicket = ({
target: { value },
}: React.ChangeEvent<HTMLInputElement>) => {
// TODO: port over bridge ticket validation logic
setMasterTicket(value);
setReady(isValidPatq(value));
setLocalError(isValidPatq(value) ? '' : 'invalid-master-ticket');
};
let inputColor = 'black';
let inputBg = 'white';
let inputBorder = 'lightGray';
if (error !== '') {
inputColor = 'red';
inputBg = 'veryLightRed';
inputBorder = 'red';
}
const isShip = isValidPatp(payee);
const icon = isShip ? (
<Sigil ship={payee} 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>
);
return (
<>
{broadcastSuccess ? (
<Sent payee={payee} stopSending={stopSending} satsAmount={satsAmount} />
) : (
<Col
width="100%"
backgroundColor="white"
borderRadius="48px"
mb={5}
p={5}
>
<Row flexDirection="row-reverse">
<Icon icon="X" cursor="pointer" onClick={() => stopSending()} />
</Row>
<Col
p={5}
mt={4}
backgroundColor="veryLightGreen"
borderRadius="24px"
alignItems="center"
>
<Row>
<Text color="green" fontSize="40px">
{satsToCurrency(satsAmount, denomination, currencyRates)}
</Text>
</Row>
<Row>
<Text
fontWeight="bold"
fontSize="16px"
color="midGreen"
>{`${satsAmount} sats`}</Text>
</Row>
<Row mt={2}>
<Text fontSize="14px" color="midGreen">{`Fee: ${satsToCurrency(
fee,
denomination,
currencyRates
)} (${fee} sats)`}</Text>
</Row>
<Row mt={4}>
<Text fontSize="16px" fontWeight="bold" color="gray">
You are paying
</Text>
</Row>
<Row mt={2} alignItems="center">
{icon}
<Text
ml={2}
mono
color="gray"
fontSize="14px"
style={{ display: 'block', overflowWrap: 'anywhere' }}
>
{payee}
</Text>
</Row>
</Col>
<Row mt={3} mb={2} alignItems="center">
<Text gray fontSize={1} fontWeight="600" mr={4}>
Ticket
</Text>
<Input
value={masterTicket}
fontSize="14px"
type="password"
name="masterTicket"
obscure={(value: string) => value.replace(/[^~-]+/g, '••••••')}
placeholder="••••••-••••••-••••••-••••••"
autoCapitalize="none"
autoCorrect="off"
color={inputColor}
backgroundColor={inputBg}
borderColor={inputBorder}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
checkTicket(e)
}
/>
</Row>
{error !== '' && (
<Row>
<Error fontSize="14px" error={error} />
</Row>
)}
<Row flexDirection="row-reverse" mt={4} alignItems="center">
<Button
primary
mr={3}
fontSize={1}
border="none"
borderRadius="24px"
color={ready && !error && !broadcasting ? 'white' : 'lighterGray'}
backgroundColor={
ready && !error && !broadcasting ? 'green' : 'veryLightGray'
}
height="48px"
onClick={() => sendBitcoin(masterTicket, psbt)}
disabled={!ready || error || broadcasting}
style={{
cursor:
ready && !error && !broadcasting ? 'pointer' : 'default',
}}
>
Send BTC
</Button>
{
// @ts-ignore
broadcasting ? <LoadingSpinner mr={3} /> : null
}
</Row>
</Col>
)}
</>
);
};
export default Invoice;

View File

@ -0,0 +1,512 @@
import React, { useEffect, useState } from 'react';
import {
Box,
Icon,
StatelessTextInput as Input,
Row,
Text,
Button,
Col,
LoadingSpinner,
} from '@tlon/indigo-react';
import Invoice from './Invoice';
import BridgeInvoice from './BridgeInvoice';
import ExternalInvoice from './ExternalInvoice';
import FeePicker from './FeePicker';
import Error from '../Error';
import Signer from './Signer';
import { validate } from 'bitcoin-address-validation';
import * as ob from 'urbit-ob';
import { useSettings } from '../../hooks/useSettings';
import { api } from '../../lib/api';
import { deSig } from '../../lib/util';
enum focusFields {
payee,
currency,
sats,
note,
empty = '',
}
export enum feeLevels {
low,
mid,
high,
}
export enum signMethods {
bridge = 'bridge',
masterTicket = 'masterTicket',
external = 'external',
}
enum payeeTypes {
ship,
address,
initial = '',
}
export type FeeChoices = {
[feeLevels.low]: [number, number];
[feeLevels.mid]: [number, number];
[feeLevels.high]: [number, number];
};
type Props = {
stopSending: () => void;
value: string;
conversion: number;
};
const Send: React.FC<Props> = ({ stopSending, value, conversion }) => {
const { error, setError, network, psbt, denomination, shipWallets } =
useSettings();
const [signing, setSigning] = useState(false);
const [denomAmount, setDenomAmount] = useState(0.0);
const [satsAmount, setSatsAmount] = useState(0);
const [payee, setPayee] = useState('');
const [checkingPatp, setCheckingPatp] = useState(false);
const [payeeType, setPayeeType] = useState<payeeTypes>(payeeTypes.initial);
const [ready, setReady] = useState(false);
const [validPayee, setValidPayee] = useState(false);
const [focusedField, setFocusedField] = useState(focusFields.empty);
const [feeChoices, setFeeChoices] = useState<FeeChoices>({
[feeLevels.low]: [10, 1],
[feeLevels.mid]: [10, 1],
[feeLevels.high]: [10, 1],
});
const [feeValue, setFeeValue] = useState(feeLevels.mid);
const [showFeePicker, setShowFeePicker] = useState(false);
const [note, setNote] = useState('');
const [choosingSignMethod, setChoosingSignMethod] = useState(false);
const [signMethod, setSignMethod] = useState<signMethods>(signMethods.bridge);
const feeDismiss = () => {
setShowFeePicker(false);
};
const handleSetSignMethod = (signMethod: signMethods) => {
setSignMethod(signMethod);
setChoosingSignMethod(false);
};
const checkPayee = (e: React.ChangeEvent<HTMLInputElement>) => {
setError('');
const validPatPCommand = (validPatP: string) => {
let command = { 'check-payee': validPatP };
api.btcWalletCommand(command);
setTimeout(() => {
setCheckingPatp(false);
}, 5000);
setCheckingPatp(true);
setPayeeType(payeeTypes.ship);
setPayee(validPatP);
};
let payeeReceived = e.target.value;
let isPatp = ob.isValidPatp(`~${deSig(payeeReceived)}`);
let isAddress = validate(payeeReceived);
if (isPatp) {
validPatPCommand(`~${deSig(payeeReceived)}`);
} else if (isAddress) {
setPayee(payeeReceived);
setReady(true);
setCheckingPatp(false);
setPayeeType(payeeTypes.address);
setValidPayee(true);
} else {
setPayee(payeeReceived);
setReady(false);
setCheckingPatp(false);
setPayeeType(payeeTypes.initial);
setValidPayee(false);
}
};
const toggleSignMethod = () => {
setChoosingSignMethod(!choosingSignMethod);
};
const initPayment = () => {
if (payeeType === payeeTypes.ship) {
let command = {
'init-payment': {
payee,
value: satsAmount,
feyb: feeChoices[feeValue][1],
note: note || null,
},
};
api.btcWalletCommand(command).then(() => setSigning(true));
} else if (payeeType === payeeTypes.address) {
let command = {
'init-payment-external': {
address: payee,
value: satsAmount,
feyb: 1,
note: note || null,
},
};
api.btcWalletCommand(command).then(() => setSigning(true));
}
};
useEffect(() => {
if (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;
setFeeChoices({
[feeLevels.high]: [30, n.estimates[30]['sat_per_vbyte']],
[feeLevels.mid]: [180, n.estimates[180]['sat_per_vbyte']],
[feeLevels.low]: [360, n.estimates[360]['sat_per_vbyte']],
});
});
}
}, []);
useEffect(() => {
if (!ready && !checkingPatp) {
if (shipWallets.payee === payee.slice(1) && shipWallets.hasWallet) {
setReady(true);
setCheckingPatp(false);
setValidPayee(true);
}
}
}, [ready, checkingPatp, shipWallets, payee]);
let payeeColor = 'black';
let payeeBg = 'white';
let payeeBorder = 'lightGray';
if (error) {
payeeColor = 'red';
payeeBorder = 'red';
payeeBg = 'veryLightRed';
} else if (focusedField === focusFields.payee && validPayee) {
payeeColor = 'green';
payeeBorder = 'green';
payeeBg = 'veryLightGreen';
} else if (focusedField !== focusFields.payee && validPayee) {
payeeColor = 'blue';
payeeBorder = 'white';
payeeBg = 'white';
} else if (focusedField !== focusFields.payee && !validPayee) {
payeeColor = 'red';
payeeBorder = 'red';
payeeBg = 'veryLightRed';
} else if (
focusedField === focusFields.payee &&
!validPayee &&
!checkingPatp &&
payeeType === payeeTypes.ship
) {
payeeColor = 'red';
payeeBorder = 'red';
payeeBg = 'veryLightRed';
}
const signReady = ready && satsAmount > 0 && !signing;
let invoice = null;
switch (signMethod) {
case signMethods.masterTicket: {
invoice = (
<Invoice
stopSending={stopSending}
payee={payee}
satsAmount={satsAmount}
/>
);
break;
}
case signMethods.bridge: {
invoice = (
<BridgeInvoice
stopSending={stopSending}
payee={payee}
satsAmount={satsAmount}
/>
);
break;
}
case signMethods.external: {
invoice = (
<ExternalInvoice
stopSending={stopSending}
payee={payee}
satsAmount={satsAmount}
/>
);
break;
}
default:
break;
}
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>
{checkingPatp ? (
<LoadingSpinner background="midOrange" foreground="orange" />
) : null}
</Row>
<Input
// autoFocus
onFocus={() => {
setFocusedField(focusFields.payee);
}}
onBlur={() => {
setFocusedField(focusFields.empty);
}}
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={(e: React.ChangeEvent<HTMLInputElement>) =>
checkPayee(e)
}
/>
</Row>
{error && (
<Row alignItems="center" justifyContent="space-between">
{/* yes this is a hack */}
<Box width="calc(40% - 30px)" />
<Error error={error} fontSize="14px" />
</Row>
)}
<Row alignItems="center" mt={4} justifyContent="space-between">
<Text gray fontSize={1} fontWeight="600" width="40%">
Amount
</Text>
<Input
onFocus={() => {
setFocusedField(focusFields.currency);
}}
onBlur={() => {
setFocusedField(focusFields.empty);
}}
fontSize="14px"
width="100%"
type="number"
borderColor={
focusedField === focusFields.currency ? 'lightGray' : 'none'
}
disabled={signing}
value={denomAmount}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setDenomAmount(parseFloat(e.target.value));
setSatsAmount(
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={() => {
setFocusedField(focusFields.sats);
}}
onBlur={() => {
setFocusedField(focusFields.empty);
}}
fontSize="14px"
width="100%"
type="number"
borderColor={
focusedField === focusFields.sats ? 'lightGray' : 'none'
}
disabled={signing}
value={satsAmount}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setDenomAmount(
parseFloat(e.target.value) * (conversion / 100000000)
);
setSatsAmount(parseInt(e.target.value, 10));
}}
/>
<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"
backgroundColor="blue"
borderRadius="24px"
paddingX="12px"
paddingY="8px"
>
<Text mr={2} color="white" fontSize="14px">
{feeChoices[feeValue][1]} sats/vbyte
</Text>
<Button
borderRadius="24px"
height="24px"
width="24px"
border="none"
backgroundColor="rgba(33, 157, 255)"
onClick={() => {
setShowFeePicker((prev) => {
if (prev) {
return false;
}
return true;
});
}}
>
<Icon
icon="ChevronSouth"
width="12px"
color="white"
cursor="pointer"
/>
</Button>
</Row>
</Row>
<Col alignItems="center">
{!showFeePicker ? null : (
<FeePicker
feeChoices={feeChoices}
feeValue={feeValue}
setFeeValue={setFeeValue}
feeDismiss={feeDismiss}
/>
)}
</Col>
<Row
mt={4}
width="100%"
justifyContent="space-between"
alignItems="center"
>
<Text gray fontSize={1} fontWeight="600" width="40%">
Note
</Text>
<Input
onFocus={() => {
setFocusedField(focusFields.note);
}}
onBlur={() => {
setFocusedField(focusFields.empty);
}}
fontSize="14px"
width="100%"
placeholder="What's this for?"
type="text"
borderColor={
focusedField === focusFields.note ? 'lightGray' : 'none'
}
disabled={signing}
value={note}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setNote(e.target.value);
}}
/>
</Row>
</Col>
<Row
flexDirection="row"
alignItems="center"
mt={4}
justifyContent="flex-end"
>
{!(signing && !error) ? null : (
<LoadingSpinner background="midOrange" foreground="orange" />
)}
<Signer
signReady={signReady}
choosingSignMethod={choosingSignMethod}
signMethod={signMethod}
setSignMethod={handleSetSignMethod}
initPayment={initPayment}
/>
<Button
ml={2}
width="48px"
fontSize={1}
fontWeight="bold"
borderRadius="24px"
height="48px"
onClick={() => toggleSignMethod()}
color={signReady ? 'white' : 'lighterGray'}
backgroundColor={
signReady ? 'rgba(33, 157, 255, 0.2)' : 'veryLightGray'
}
disabled={!signReady}
border="none"
style={{ cursor: signReady ? 'pointer' : 'default' }}
>
<Icon
icon="ChevronSouth"
color={signReady ? 'blue' : 'lighterGray'}
/>
</Button>
</Row>
{signMethod === signMethods.masterTicket && (
<Row mt={4} alignItems="center">
<Icon icon="Info" color="yellow" height={4} width={4} />
<Text fontSize="14px" fontWeight="regular" color="gray" ml={2}>
We recommend that you sign transactions using Bridge to protect
your master ticket.
</Text>
</Row>
)}
</Col>
)}
</>
);
};
export default Send;

View File

@ -0,0 +1,42 @@
import React from 'react';
import { Icon, Row, Col, Center, Text } from '@tlon/indigo-react';
import { satsToCurrency } from '../../lib/util';
import { useSettings } from '../../hooks/useSettings';
type Props = {
payee: string;
stopSending: () => void;
satsAmount: number;
};
const Sent: React.FC<Props> = ({ payee, stopSending, satsAmount }) => {
const { denomination, currencyRates } = useSettings();
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', overflowWrap: '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>
);
};
export default Sent;

View File

@ -0,0 +1,79 @@
import React from 'react';
import { Box, Button, Icon, Row } from '@tlon/indigo-react';
import { signMethods } from './Send';
const signMethodLabels = {
bridge: 'Sign with Bridge',
masterTicket: 'Sign with Master Ticket',
external: 'Sign Externally (PSBT)',
};
type Props = {
signReady: boolean;
initPayment: () => void;
choosingSignMethod: boolean;
signMethod: signMethods;
setSignMethod: (arg: signMethods) => void;
};
const Signer: React.FC<Props> = ({
signReady,
initPayment,
choosingSignMethod,
signMethod,
setSignMethod,
}) => {
return choosingSignMethod ? (
<Box borderRadius="24px" backgroundColor="rgba(33, 157, 255, 0.2)">
{Object.keys(signMethods).map((method) => (
<Row key={method} flexDirection="row" alignItems="center">
<Button
border="none"
backgroundColor="transparent"
fontWeight="bold"
cursor="pointer"
color={
signMethod === (signMethods as any)[method] ? 'blue' : 'lightBlue'
}
height="48px"
onClick={() => setSignMethod((signMethods as any)[method])}
>
{(signMethodLabels as any)[method]}
</Button>
{signMethod === (signMethods as any)[method] && (
<Button
borderRadius="24px"
width="24px"
height="24px"
backgroundColor="blue"
border="none"
padding="0px"
mr="12px"
>
<Icon width="12px" icon="Checkmark" color="white" />
</Button>
)}
</Row>
))}
</Box>
) : (
<Button
primary
fontSize={1}
fontWeight="bold"
borderRadius="24px"
mr={2}
height="48px"
onClick={initPayment}
color={signReady ? 'white' : 'lighterGray'}
backgroundColor={signReady ? 'blue' : 'veryLightGray'}
disabled={!signReady}
border="none"
style={{ cursor: signReady ? 'pointer' : 'default' }}
>
{(signMethodLabels as any)[signMethod]}
</Button>
);
};
export default Signer;

View File

@ -0,0 +1,115 @@
import React from 'react';
import { Row, Text, Button, Col } from '@tlon/indigo-react';
import { useSettings } from '../hooks/useSettings';
import { api } from '../lib/api';
const Settings = () => {
const { wallet, provider } = useSettings();
const changeProvider = () => {
api.btcWalletCommand({ 'set-provider': null });
window.location.reload();
};
const replaceWallet = () => {
api.btcWalletCommand({
'delete-wallet': wallet,
});
};
let connColor = 'red';
let connBackground = 'veryLightRed';
let conn = 'Offline';
let host = '';
if (provider) {
if (provider.connected) conn = 'Connected';
if (provider.host) host = provider.host;
if (provider.connected && 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">
{wallet}
</Text>
</Row>
<Row width="100%" mb={5}>
<Button
width="100%"
fontSize={1}
fontWeight="bold"
backgroundColor="gray"
color="white"
borderColor="none"
borderRadius="12px"
p={4}
onClick={() => replaceWallet()}
>
Replace Wallet
</Button>
</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
width="100%"
fontSize={1}
fontWeight="bold"
backgroundColor="orange"
color="white"
borderColor="none"
borderRadius="12px"
p={4}
onClick={() => changeProvider()}
>
Change Provider
</Button>
</Row>
</Col>
);
};
export default Settings;

View File

@ -2,11 +2,11 @@ import React, { memo } from 'react';
import { sigil, reactRenderer } from '@tlon/sigil-js';
import { Box } from '@tlon/indigo-react';
export const foregroundFromBackground = (background) => {
export const foregroundFromBackground = (background: string) => {
const rgb = {
r: parseInt(background.slice(1, 3), 16),
g: parseInt(background.slice(3, 5), 16),
b: parseInt(background.slice(5, 7), 16)
b: parseInt(background.slice(5, 7), 16),
};
const brightness = (299 * rgb.r + 587 * rgb.g + 114 * rgb.b) / 1000;
const whiteBrightness = 255;
@ -14,7 +14,19 @@ export const foregroundFromBackground = (background) => {
return whiteBrightness - brightness < 50 ? 'black' : 'white';
};
export const Sigil = memo(
type Props = {
classes?: string;
color: string;
foreground?: string;
ship: string;
size: number;
svgClass?: string;
icon?: boolean;
padding?: number;
display?: string;
};
const Sigil: React.FC<Props> = memo(
({
classes = '',
color,
@ -24,7 +36,7 @@ export const Sigil = memo(
svgClass = '',
icon = false,
padding = 0,
display = 'inline-block'
display = 'inline-block',
}) => {
const innerSize = Number(size) - 2 * padding;
const paddingPx = `${padding}px`;
@ -55,7 +67,7 @@ export const Sigil = memo(
size: innerSize,
icon,
colors: [color, foregroundColor],
class: svgClass
class: svgClass,
})}
</Box>
);

View File

@ -0,0 +1,44 @@
import React from 'react';
import { Box } from '@tlon/indigo-react';
import WalletModal from './WalletModal';
import ProviderModal from './ProviderModal';
import { useSettings } from '../hooks/useSettings';
const StartupModal: React.FC = () => {
const { wallet, provider } = useSettings();
let modal = null;
if (wallet && provider) {
return null;
} else if (!provider) {
modal = <ProviderModal />;
} else if (!wallet) {
modal = <WalletModal />;
}
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>
);
};
export default StartupModal;

View File

@ -0,0 +1,101 @@
import React from 'react';
import { Box, Row, Text, Col } from '@tlon/indigo-react';
import _ from 'lodash';
import TxAction from './TxAction';
import TxCounterparty from './TxCounterparty';
import { satsToCurrency } from '../../lib/util';
import { useSettings } from '../../hooks/useSettings';
import { Transaction as TransactionType } from '../../types';
const Transaction = ({ tx }: { tx: TransactionType }) => {
const { denomination, currencyRates } = useSettings();
const pending = !tx.recvd;
let weSent = _.find(tx.inputs, (input) => {
return input.ship === (window as any).ship;
});
let weRecv = tx.outputs.every((output) => {
return output.ship === (window as any).ship;
});
let action: 'sent' | 'recv' | 'fail' = weRecv
? 'recv'
: weSent
? 'sent'
: 'recv';
let counterShip = null;
let counterAddress = null;
let value;
let sign;
if (action === 'sent') {
let counter = _.find(tx.outputs, (output) => {
return output.ship !== (window as any).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(
tx.outputs,
(sum, output) => {
if (output.ship === (window as any).ship) {
return sum + output.val.value;
} else {
return sum;
}
},
0
);
if (weSent && weRecv) {
counterAddress = _.get(
_.find(tx.inputs, (input) => {
return input.ship === (window as any).ship;
}),
'val.address',
null
);
} else {
let counter = _.find(tx.inputs, (input) => {
return input.ship !== (window as any).ship;
});
counterShip = _.get(counter, 'ship', null);
counterAddress = _.get(counter, 'val.address', null);
}
sign = '';
}
let currencyValue = sign + satsToCurrency(value, denomination, currencyRates);
const failure = Boolean(tx.failure);
if (failure) action = 'fail';
const txid = 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} />
<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>
);
};
export default Transaction;

View File

@ -0,0 +1,43 @@
import React from 'react';
import { Box, Text, Col } from '@tlon/indigo-react';
import Transaction from './Transaction';
import { useSettings } from '../../hooks/useSettings';
const Transactions = () => {
const { history } = useSettings();
if (!history || 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}
>
{history.map((tx, i) => {
return <Transaction tx={tx} key={i} />;
})}
</Col>
);
}
};
export default Transactions;

View File

@ -0,0 +1,78 @@
import React from 'react';
import { Box, Icon, Row, Text, LoadingSpinner } from '@tlon/indigo-react';
import { useSettings } from '../../hooks/useSettings';
type Props = {
action: 'sent' | 'recv' | 'fail';
pending: boolean;
txid: string;
};
const TxAction: React.FC<Props> = ({ action, pending, txid }) => {
const { network } = useSettings();
const leftIcon =
action === 'sent'
? 'ArrowSouth'
: action === 'recv'
? 'ArrowNorth'
: action === 'fail'
? 'X'
: 'NullIcon';
const actionColor =
action === 'sent'
? 'sentBlue'
: action === 'recv'
? 'recvGreen'
: action === 'fail'
? 'gray'
: 'red';
const actionText =
action === 'sent' && !pending
? 'Sent BTC'
: action === 'sent' && pending
? 'Sending BTC'
: action === 'recv' && !pending
? 'Received BTC'
: action === 'recv' && pending
? 'Receiving BTC'
: action === 'fail'
? 'Failed'
: 'error';
const pendingSpinner = !pending ? null : (
<LoadingSpinner background="midOrange" foreground="orange" />
);
const url =
network === 'testnet'
? `http://blockstream.info/testnet/tx/${txid}`
: `http://blockstream.info/tx/${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" rel="noreferrer">
<Icon color={actionColor} icon="ArrowNorthEast" ml={1} mr={2} />
</a>
{pendingSpinner}
</Row>
);
};
export default TxAction;

View File

@ -0,0 +1,41 @@
import React from 'react';
import { Box, Icon, Row, Text } from '@tlon/indigo-react';
import Sigil from '../Sigil';
type Props = {
ship: string;
address: string;
};
const TxCounterparty: React.FC<Props> = ({ ship, address }) => {
const icon = ship ? (
<Sigil ship={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 = !address
? ''
: address.slice(0, 6) + '...' + address.slice(-6);
const text = ship ? `~${ship}` : addressText;
return (
<Row alignItems="center">
{icon}
<Text ml={2} mono fontSize="14px" color="gray">
{text}
</Text>
</Row>
);
};
export default TxCounterparty;

View File

@ -0,0 +1,268 @@
import React, { useState } from 'react';
import {
Box,
Text,
Button,
StatelessTextInput,
Icon,
Row,
LoadingSpinner,
} from '@tlon/indigo-react';
import { patp2dec, isValidPatq } from 'urbit-ob';
import * as kg from 'urbit-key-generation';
import { useSettings } from '../hooks/useSettings';
import { api } from '../lib/api';
import { UrbitWallet } from '../types';
const WalletModal: React.FC = () => {
const { network } = useSettings();
const [mode, setMode] = useState('xpub');
const [masterTicket, setMasterTicket] = useState('');
const [confirmedMasterTicket, setConfirmedMasterTicket] = useState('');
const [xpub, setXpub] = useState('');
const [readyToSubmit, setReadyToSubmit] = useState(false);
const [processingSubmission, setProcessingSubmission] = useState(false);
const [confirmingMasterTicket, setConfirmingMasterTicket] = useState(false);
const [error, setError] = useState(false);
const checkTicket = ({
target: { value },
}: React.ChangeEvent<HTMLInputElement>) => {
// TODO: port over bridge ticket validation logic
if (confirmingMasterTicket) {
setConfirmedMasterTicket(value);
setReadyToSubmit(isValidPatq(value));
} else {
setMasterTicket(value);
setReadyToSubmit(isValidPatq(value));
}
};
const checkXPub = ({
target: { value: xpubGiven },
}: React.ChangeEvent<HTMLInputElement>) => {
setXpub(xpubGiven);
setReadyToSubmit(xpubGiven.length > 0);
};
const submitXPub = (givenXpub: string) => {
type AddWalletCommand = {
'add-wallet': {
xpub: string;
fprint: number[];
'scan-to': number | null;
'max-gap': number;
confs: number;
};
};
const command: AddWalletCommand = {
'add-wallet': {
xpub: givenXpub,
fprint: [4, 0],
'scan-to': null,
'max-gap': 8,
confs: 1,
},
};
api.btcWalletCommand(command);
setProcessingSubmission(true);
};
const submitMasterTicket = (ticket: string) => {
setProcessingSubmission(true);
kg.generateWallet({
ticket,
ship: parseInt(patp2dec('~' + (window as any).ship)),
}).then((urbitWallet: UrbitWallet) => {
const { xpub: xpubFromWallet } =
network === 'testnet'
? urbitWallet.bitcoinTestnet.keys
: urbitWallet.bitcoinMainnet.keys;
submitXPub(xpubFromWallet);
});
};
const buttonDisabled = !readyToSubmit || processingSubmission;
const inputDisabled = processingSubmission;
// const processingSpinner = !processingSubmission ? null : <LoadingSpinner />;
if (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>
<Row mt={3} alignItems="center">
<Icon icon="Info" color="yellow" height={4} width={4} />
<Text fontSize="14px" fontWeight="regular" color="gray" ml={2}>
We recommend that you import your wallet using Bridge to protect
your master ticket.
</Text>
</Row>
<Box display="flex" alignItems="center" mt={3} mb={2}>
{confirmingMasterTicket && (
<Icon
icon="ArrowWest"
cursor="pointer"
onClick={() => {
setConfirmingMasterTicket(false);
setMasterTicket('');
setConfirmedMasterTicket('');
setError(false);
}}
/>
)}
<Text fontSize="14px" fontWeight="500">
{confirmingMasterTicket ? 'Confirm Master Ticket' : 'Master Ticket'}
</Text>
</Box>
<Row alignItems="center">
<StatelessTextInput
mr={2}
width="256px"
value={
confirmingMasterTicket ? confirmedMasterTicket : masterTicket
}
disabled={inputDisabled}
fontSize="14px"
type="password"
name="masterTicket"
obscure={(value: string) => value.replace(/[^~-]+/g, '••••••')}
placeholder="••••••-••••••-••••••-••••••"
autoCapitalize="none"
autoCorrect="off"
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
checkTicket(e)
}
/>
{!inputDisabled ? null : <LoadingSpinner />}
</Row>
{error && (
<Row mt={2}>
<Text fontSize="14px" color="red">
Master tickets do not match
</Text>
</Row>
)}
<Row mt={3}>
<Button
primary
color="black"
backgroundColor="veryLightGray"
borderColor="veryLightGray"
fontSize="14px"
mr={2}
style={{ cursor: 'pointer' }}
onClick={() => {
setMode('xpub');
setMasterTicket('');
setXpub('');
setReadyToSubmit(false);
}}
>
Cancel
</Button>
<Button
primary
disabled={buttonDisabled}
fontSize="14px"
style={{ cursor: buttonDisabled ? 'default' : 'pointer' }}
onClick={() => {
if (!confirmingMasterTicket) {
setConfirmingMasterTicket(true);
} else {
if (masterTicket === confirmedMasterTicket) {
setError(false);
submitMasterTicket(masterTicket);
} else {
setError(true);
}
}
}}
>
Next Step
</Button>
</Row>
</Box>
);
} else if (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{' '}
<a
rel="noreferrer"
href="https://bridge.urbit.org/?kind=xpub"
target="_blank"
style={{ color: 'black' }}
>
bridge.urbit.org
</a>{' '}
to obtain your key
</Text>
</Box>
<Box mt={3} mb={2}>
<Text fontSize="14px" fontWeight="500">
Extended Public Key (XPub)
</Text>
</Box>
<Row alignItems="center">
<StatelessTextInput
value={xpub}
disabled={inputDisabled}
fontSize="14px"
type="password"
name="xpub"
autoCapitalize="none"
autoCorrect="off"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => checkXPub(e)}
mr={1}
/>
{!inputDisabled ? null : <LoadingSpinner />}
</Row>
<Box mt={3} mb={3}>
<Text
fontSize="14px"
fontWeight="regular"
color={inputDisabled ? 'lighterGray' : 'gray'}
style={{ cursor: inputDisabled ? 'default' : 'pointer' }}
onClick={() => {
if (inputDisabled) return;
setMode('masterTicket');
setXpub('');
setMasterTicket('');
setReadyToSubmit(false);
}}
>
Import using master ticket -&gt;
</Text>
</Box>
<Button
primary
mt={3}
disabled={buttonDisabled}
fontSize="14px"
style={{ cursor: readyToSubmit ? 'pointer' : 'default' }}
onClick={() => {
submitXPub(xpub);
}}
>
Next Step
</Button>
</Box>
);
}
};
export default WalletModal;

View File

@ -0,0 +1,72 @@
import React from 'react';
import { Box, Text, Button, Col, Anchor } from '@tlon/indigo-react';
import { api } from '../lib/api';
import { useSettings } from '../hooks/useSettings';
const Warning = () => {
const { setShowWarning } = useSettings();
const understand = () => {
setShowWarning(false);
let removeWarning = {
'put-entry': {
value: false,
'entry-key': 'warning',
'bucket-key': 'btc-wallet',
},
};
api.settingsEvent(removeWarning);
};
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&apos;s repo.
</Text>
<br />
<Anchor href="https://urbit.org/bitcoin-wallet" target="_blank">
<Text
color="white"
fontWeight="bold"
fontSize={1}
style={{ textDecoration: 'underline' }}
>
Learn more on urbit.org
</Text>
</Anchor>
</Col>
<Button
backgroundColor="white"
fontSize={1}
mt={5}
color="red"
fontWeight="bold"
borderRadius="24px"
p="24px"
borderColor="none"
onClick={() => understand()}
>
I understand
</Button>
</Box>
);
};
export default Warning;

View File

@ -0,0 +1,397 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
import _ from 'lodash';
import { api } from '../lib/api';
import { mapDenominationToSymbol, reduceHistory } from '../lib/util';
import {
CurrencyRate,
Denomination,
Network,
Provider,
ProviderPerms,
ScanProgress,
ShipWallets,
Transaction,
TxidType,
} from '../types';
type SettingsContextType = {
network: Network;
setNetwork: React.Dispatch<React.SetStateAction<Network>>;
loadedBtc: boolean;
setLoadedBtc: React.Dispatch<React.SetStateAction<boolean>>;
loadedSettings: boolean;
setLoadedSettings: React.Dispatch<React.SetStateAction<boolean>>;
loaded: boolean;
setLoaded: React.Dispatch<React.SetStateAction<boolean>>;
providerPerms: ProviderPerms;
setProviderPerms: React.Dispatch<React.SetStateAction<ProviderPerms>>;
shipWallets: ShipWallets;
setShipWallets: React.Dispatch<React.SetStateAction<ShipWallets>>;
provider: Provider;
setProvider: React.Dispatch<React.SetStateAction<string | null>>;
wallet: string | null;
setWallet: React.Dispatch<React.SetStateAction<string | null>>;
confirmedBalance: number;
setConfirmedBalance: React.Dispatch<React.SetStateAction<number>>;
unconfirmedBalance: number;
setUnconfirmedBalance: React.Dispatch<React.SetStateAction<number>>;
btcState: any;
setBtcState: React.Dispatch<React.SetStateAction<any>>;
history: Transaction[];
setHistory: React.Dispatch<React.SetStateAction<Transaction[]>>;
fee: number;
setFee: React.Dispatch<React.SetStateAction<number>>;
psbt: string;
setPsbt: React.Dispatch<React.SetStateAction<string>>;
address: string | null;
setAddress: React.Dispatch<React.SetStateAction<string | null>>;
currencyRates: CurrencyRate;
setCurrencyRates: React.Dispatch<React.SetStateAction<{}>>;
denomination: Denomination;
setDenomination: React.Dispatch<React.SetStateAction<Denomination>>;
showWarning: boolean;
setShowWarning: React.Dispatch<React.SetStateAction<boolean>>;
error: string;
setError: React.Dispatch<React.SetStateAction<string>>;
broadcastSuccess: boolean;
setBroadcastSuccess: React.Dispatch<React.SetStateAction<boolean>>;
scanProgress: ScanProgress;
setScanProgress: React.Dispatch<React.SetStateAction<ScanProgress>>;
};
export const SettingsContext = createContext<SettingsContextType>({
network: 'bitcoin',
setNetwork: () => {},
loadedBtc: false,
setLoadedBtc: () => {},
loadedSettings: false,
setLoadedSettings: () => {},
loaded: false,
setLoaded: () => {},
providerPerms: { provider: '', permitted: false },
setProviderPerms: () => {},
shipWallets: { payee: '', hasWallet: false },
setShipWallets: () => {},
provider: null,
setProvider: () => {},
wallet: null,
setWallet: () => {},
confirmedBalance: 0,
setConfirmedBalance: () => {},
unconfirmedBalance: 0,
setUnconfirmedBalance: () => {},
btcState: null,
setBtcState: () => {},
history: [],
setHistory: () => {},
fee: 0,
setFee: () => {},
psbt: '',
setPsbt: () => {},
address: null,
setAddress: () => {},
currencyRates: {
BTC: { last: 1, symbol: 'BTC' },
},
setCurrencyRates: () => {},
denomination: 'BTC',
setDenomination: () => {},
showWarning: true,
setShowWarning: () => {},
error: '',
setError: () => {},
broadcastSuccess: false,
setBroadcastSuccess: () => {},
scanProgress: { main: null, change: null },
setScanProgress: () => {},
});
type Props = {
channel: { setOnChannelError: (arg: () => void) => void };
};
export const SettingsProvider: React.FC<Props> = ({ channel, children }) => {
const [network, setNetwork] = useState<Network>('bitcoin');
const [channelData, setChannelData] = useState(null);
const [loadedBtc, setLoadedBtc] = useState(false);
const [loadedSettings, setLoadedSettings] = useState(false);
const [loaded, setLoaded] = useState(false);
const [providerPerms, setProviderPerms] = useState<ProviderPerms>({
provider: '',
permitted: false,
});
const [shipWallets, setShipWallets] = useState<ShipWallets>({
payee: '',
hasWallet: false,
});
const [provider, setProvider] = useState(null);
const [wallet, setWallet] = useState(null);
const [confirmedBalance, setConfirmedBalance] = useState(0);
const [unconfirmedBalance, setUnconfirmedBalance] = useState(0);
const [btcState, setBtcState] = useState(null);
const [history, setHistory] = useState([]);
const [psbt, setPsbt] = useState('');
const [fee, setFee] = useState(0);
const [address, setAddress] = useState(null);
const [currencyRates, setCurrencyRates] = useState({
BTC: { last: 1, symbol: 'BTC' },
});
const [denomination, setDenomination] = useState<Denomination>('BTC');
const [showWarning, setShowWarning] = useState(false);
const [error, setError] = useState('');
const [broadcastSuccess, setBroadcastSuccess] = useState(false);
const [scanProgress, setScanProgress] = useState({
main: null,
change: null,
});
const { Provider } = SettingsContext;
const success = (event: any) => {
console.log({ event });
setChannelData(event);
};
const fail = (error: any) => console.log({ error });
const initializeBtcWallet = () => {
api.bind('/all', 'PUT', api.ship, 'btc-wallet', success, fail);
};
const initializeSettings = () => {
let app = 'settings-store';
let path = '/bucket/btc-wallet';
fetch(`/~/scry/${app}${path}.json`)
.then((res) => res.json())
.then((n) => {
let data = _.get(n, 'initial', false);
let bucketData = _.get(n, 'bucket', false);
if (data) {
setChannelData(n);
}
if (bucketData) {
let bucketWarning = _.get(n, 'bucket.warning', -1);
if (bucketWarning !== -1) {
setShowWarning(bucketWarning);
}
let bucketCurrency = _.get(n, 'bucket.currency', -1);
if (bucketCurrency !== -1) {
setDenomination(bucketCurrency);
}
setLoadedSettings(true);
if (loadedBtc) {
setLoaded(true);
}
}
});
api.bind(path, 'PUT', api.ship, app, success, fail);
};
const initializeCurrencyPoll = () => {
fetch('https://blockchain.info/ticker')
.then((res) => res.json())
.then((n) => {
const newCurrencyRates: any = currencyRates;
for (let c in n) {
newCurrencyRates[c] = n[c];
newCurrencyRates[c].symbol = mapDenominationToSymbol(c);
}
setCurrencyRates(newCurrencyRates);
setTimeout(() => initializeCurrencyPoll(), 1000 * 60 * 15);
});
};
const start = () => {
if (api.ship) {
initializeBtcWallet();
initializeSettings();
initializeCurrencyPoll();
}
};
const handleNewTx = (newTx: Transaction) => {
const { txid, recvd } = newTx;
let old = _.findIndex(history, (h: Transaction) => {
return h.txid.dat === txid.dat && h.txid.wid === txid.wid;
});
if (old !== -1) {
const newHistory = history.filter((_, i) => i !== old);
setHistory(newHistory);
}
if (recvd === null && old === -1) {
const newHistory = [...history, newTx];
setHistory(newHistory);
} else if (recvd !== null && old === -1) {
// we expect history to have null recvd values first, and the rest in
// descending order
let insertionIndex = _.findIndex(history, (h: Transaction) => {
return h.recvd < recvd && h.recvd !== null;
});
const newHistory = history.map((o, i) =>
i === insertionIndex ? newTx : o
);
setHistory(newHistory);
}
};
const handleCancelTx = ({ wid, dat }: TxidType) => {
let entryIndex = _.findIndex(history, (h: Transaction) => {
return wid === h.txid.wid && dat === h.txid.dat;
});
if (entryIndex > -1) {
history[entryIndex].failure = true;
}
};
useEffect(() => {
const initialData = channelData?.data?.initial;
const putEntryData = channelData?.data?.['settings-event']?.['put-entry'];
const btcStateData = channelData?.data?.['btc-state'];
const changeProvider = channelData?.data?.['change-provider'];
const newTx = channelData?.data?.['new-tx'];
const providerStatus = channelData?.data?.providerStatus;
const checkPayee = channelData?.data?.checkPayee;
const changeWallet = channelData?.data?.changeWallet;
const psbtData = channelData?.data.psbt;
const cancelTx = channelData?.data['cancel-tx'];
const addressData = channelData?.data?.address;
const balanceData = channelData?.data?.balance;
const errorData = channelData?.data?.error;
const broadcastSuccessData = channelData?.data?.['broadcast-success'];
const broadcastFailData = channelData?.data?.['broadcast-fail'];
const scanProgressData = channelData?.data?.['scan-progress'];
if (initialData) {
setProvider(initialData.provider);
setWallet(initialData.wallet);
setConfirmedBalance(_.get(initialData.balance, 'confirmed', null));
setUnconfirmedBalance(_.get(initialData.balance, 'unconfirmed', null));
setBtcState(initialData['btc-state']);
setHistory(reduceHistory(initialData.history));
setAddress(initialData.address);
setLoadedBtc(true);
if (loadedSettings) {
setLoaded(true);
}
}
if (putEntryData && putEntryData?.['entry-key'] === 'currency') {
setDenomination(putEntryData.value);
}
if (putEntryData && putEntryData?.['entry-key'] === 'warning') {
setShowWarning(putEntryData.value);
}
if (btcStateData) {
setBtcState(btcStateData);
}
if (changeProvider) {
setProvider(changeProvider);
}
if (newTx) {
handleNewTx(newTx);
}
if (providerStatus) {
let newProviderPerms: any = providerPerms;
for (let c in providerStatus) {
newProviderPerms[c] = providerStatus[c];
}
setProviderPerms(newProviderPerms);
}
if (checkPayee) {
let newShipWallets: any = shipWallets;
for (let c in checkPayee) {
newShipWallets[c] = checkPayee[c];
}
setShipWallets(newShipWallets);
}
if (changeWallet) {
setWallet(changeWallet);
}
if (psbtData) {
setPsbt(psbtData.pb);
setFee(psbtData.fee);
}
if (cancelTx) {
handleCancelTx(cancelTx);
}
if (addressData) {
setAddress(addressData);
}
if (balanceData) {
setUnconfirmedBalance(balanceData.unconfirmed);
setConfirmedBalance(balanceData.confirmed);
}
if (errorData) {
setError(errorData);
}
if (broadcastSuccessData) {
setBroadcastSuccess(true);
}
if (broadcastFailData) {
setBroadcastSuccess(false);
}
if (scanProgressData) {
setScanProgress(scanProgressData);
}
}, [channelData]);
useEffect(() => {
channel.setOnChannelError(() => {
start();
});
start();
}, []);
return (
<Provider
value={{
network,
setNetwork,
loadedBtc,
setLoadedBtc,
loadedSettings,
setLoadedSettings,
loaded,
setLoaded,
providerPerms,
setProviderPerms,
shipWallets,
setShipWallets,
provider,
setProvider,
wallet,
setWallet,
confirmedBalance,
setConfirmedBalance,
unconfirmedBalance,
setUnconfirmedBalance,
btcState,
setBtcState,
history,
setHistory,
psbt,
setPsbt,
fee,
setFee,
address,
setAddress,
currencyRates,
setCurrencyRates,
denomination,
setDenomination,
showWarning,
setShowWarning,
error,
setError,
broadcastSuccess,
setBroadcastSuccess,
scanProgress,
setScanProgress,
}}
>
{children}
</Provider>
);
};
export const useSettings = () => useContext(SettingsContext);

View File

@ -1,23 +0,0 @@
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';
// rebuild x3
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,23 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { api } from './lib/api';
import { SettingsProvider } from './hooks/useSettings';
import App from './App';
import './css/indigo-static.css';
import './css/fonts.css';
import './css/custom.css';
const channel = new (window as any).channel();
api.setChannel((window as any).ship, channel);
if (module.hot) {
module.hot.accept();
}
ReactDOM.render(
<SettingsProvider channel={channel}>
<App />
</SettingsProvider>,
document.querySelectorAll('#root')[0]
);

View File

@ -1,170 +0,0 @@
import React, { Component } from 'react';
import { Row, Text, Button, Col } from '@tlon/indigo-react';
import Send from './send.js';
import CurrencyPicker from './currencyPicker.js';
import { satsToCurrency } from '../../lib/util.js';
import { store } from '../../store.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;
navigator.clipboard.writeText(address);
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={this.props.api}
psbt={this.props.state.psbt}
fee={this.props.state.fee}
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
disabled={sendDisabled}
fontSize={1}
fontWeight="bold"
color={sendDisabled ? 'lighterGray' : 'white'}
backgroundColor={sendDisabled ? 'veryLightGray' : 'orange'}
style={{ cursor: sendDisabled ? 'default' : 'pointer' }}
borderColor="none"
borderRadius="24px"
height="48px"
onClick={() => this.setState({ sending: true })}
>
Send
</Button>
<Button
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"
height="48px"
onClick={() => {
this.copyAddress('button');
}}
>
{this.state.copiedButton ? 'Address Copied!' : 'Copy Address'}
</Button>
</Row>
</Col>
)}
</>
);
}
}

View File

@ -1,72 +0,0 @@
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() {
const cardWidth = window.innerWidth <= 475 ? '350px' : '400px'
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={cardWidth}
>
<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={cardWidth}
>
<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

@ -1,239 +0,0 @@
import React, { Component } from 'react';
import {
Box,
Icon,
StatelessTextInput as Input,
Row,
Text,
Button,
Col,
LoadingSpinner,
} from '@tlon/indigo-react';
import { Sigil } from './sigil.js';
import * as bitcoin from 'bitcoinjs-lib';
import * as kg from 'urbit-key-generation';
import { isValidPatp } from 'urbit-ob';
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);
this.clickDismiss = this.clickDismiss.bind(this);
this.setInvoiceRef = this.setInvoiceRef.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);
document.addEventListener("click", this.clickDismiss);
}
componentWillUnmount(){
document.removeEventListener("click", this.clickDismiss);
}
setInvoiceRef(n){
this.invoiceRef = n;
}
clickDismiss(e){
if (this.invoiceRef && !(this.invoiceRef.contains(e.target))){
this.props.stopSending();
}
}
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, fee } = this.props;
const { error, txHex } = this.state;
let inputColor = 'black';
let inputBg = 'white';
let inputBorder = 'lightGray';
if (error !== '') {
inputColor = 'red';
inputBg = 'veryLightRed';
inputBorder = 'red';
}
const isShip = isValidPatp(payee);
const icon = (isShip)
? <Sigil ship={payee} 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>;
return (
<>
{ this.props.state.broadcastSuccess ?
<Sent
payee={payee}
stopSending={stopSending}
denomination={denomination}
currencyRates={currencyRates}
satsAmount={satsAmount}
/> :
<Col
ref={this.setInvoiceRef}
width='100%'
backgroundColor='white'
borderRadius='48px'
mb={5}
p={5}
>
<Col
p={5}
mt={4}
backgroundColor='veryLightGreen'
borderRadius='24px'
alignItems="center"
>
<Row>
<Text
color='green'
fontSize='40px'
>{satsToCurrency(satsAmount, denomination, currencyRates)}</Text>
</Row>
<Row>
<Text
fontWeight="bold"
fontSize='16px'
color='midGreen'
>{`${satsAmount} sats`}</Text>
</Row>
<Row mt={2}>
<Text
fontSize='14px'
color='midGreen'
>{`Fee: ${satsToCurrency(fee, denomination, currencyRates)} (${fee} sats)`}</Text>
</Row>
<Row mt={4} >
<Text fontSize='16px' fontWeight="bold" color="gray">You are paying</Text>
</Row>
<Row mt={2} alignItems="center">
{icon}
<Text ml={2}
mono
color="gray"
fontSize='14px'
style={{'display': 'block', 'overflow-wrap': 'anywhere'}}
>{payee}</Text>
</Row>
</Col>
<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'
border='none'
height='48px'
onClick={() => this.sendBitcoin(txHex)}
disabled={!this.state.ready || error || this.state.broadcasting}
color={(this.state.ready && !error && !this.state.broadcasting) ? "white" : "lighterGray"}
backgroundColor={(this.state.ready && !error && !this.state.broadcasting) ? "green" : "veryLightGray"}
style={{cursor: (this.state.ready && !error) ? "pointer" : "default"}}
/>
{this.state.broadcasting ? <LoadingSpinner mr={3}/> : null}
</Row>
</Col>
}
</>
);
}
}

View File

@ -1,50 +0,0 @@
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

@ -1,41 +0,0 @@
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

@ -1,99 +0,0 @@
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

@ -1,78 +0,0 @@
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

@ -1,291 +0,0 @@
import React, { Component } from 'react';
import {
Box,
Icon,
StatelessTextInput as Input,
Row,
Text,
Button,
Col,
LoadingSpinner,
} from '@tlon/indigo-react';
import { Sigil } from './sigil.js'
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, isValidPatp } 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);
this.clickDismiss = this.clickDismiss.bind(this);
this.setInvoiceRef = this.setInvoiceRef.bind(this);
}
componentDidMount(){
document.addEventListener("click", this.clickDismiss);
}
componentWillUnMount(){
document.removeEventListener("click", this.clickDismiss);
}
setInvoiceRef(n){
this.invoiceRef = n;
}
clickDismiss(e){
if (this.invoiceRef && !(this.invoiceRef.contains(e.target))) {
this.props.stopSending();
}
}
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, fee } = this.props;
const { sent, error } = this.state;
let inputColor = 'black';
let inputBg = 'white';
let inputBorder = 'lightGray';
if (error !== '') {
inputColor = 'red';
inputBg = 'veryLightRed';
inputBorder = 'red';
}
const isShip = isValidPatp(payee);
const icon = (isShip)
? <Sigil ship={payee} 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>;
return (
<>
{ broadcastSuccess ?
<Sent
payee={payee}
stopSending={stopSending}
denomination={denomination}
currencyRates={currencyRates}
satsAmount={satsAmount}
/> :
<Col
ref={this.setInvoiceRef}
width='100%'
backgroundColor='white'
borderRadius='48px'
mb={5}
p={5}
>
<Col
p={5}
mt={4}
backgroundColor='veryLightGreen'
borderRadius='24px'
alignItems="center"
>
<Row>
<Text
color='green'
fontSize='40px'
>{satsToCurrency(satsAmount, denomination, currencyRates)}</Text>
</Row>
<Row>
<Text
fontWeight="bold"
fontSize='16px'
color='midGreen'
>{`${satsAmount} sats`}</Text>
</Row>
<Row mt={2}>
<Text
fontSize='14px'
color='midGreen'
>{`Fee: ${satsToCurrency(fee, denomination, currencyRates)} (${fee} sats)`}</Text>
</Row>
<Row mt={4} >
<Text fontSize='16px' fontWeight="bold" color="gray">You are paying</Text>
</Row>
<Row mt={2} alignItems="center">
{icon}
<Text ml={2}
mono
color="gray"
fontSize='14px'
style={{'display': 'block', 'overflow-wrap': 'anywhere'}}
>{payee}</Text>
</Row>
</Col>
<Row mt={3} mb={2} alignItems="center">
<Text gray fontSize={1} fontWeight='600' mr={4}>
Ticket
</Text>
<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}
/>
</Row>
{(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}
border="none"
borderRadius='24px'
color={(this.state.ready && !error && !this.state.broadcasting) ? "white" : "lighterGray"}
backgroundColor={(this.state.ready && !error && !this.state.broadcasting) ? "green" : "veryLightGray"}
height='48px'
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

@ -1,166 +0,0 @@
import React, { Component } from 'react';
import {
Box,
Text,
Button,
StatelessTextInput,
Icon,
Row,
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() {
if (!this.state.ready) {
if (this.props.providerPerms[this.state.provider]) {
this.setState({
ready: true,
checkingProvider: false,
providerFailed: false,
});
}
}
}
submitProvider() {
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&apos;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"
rel="noreferrer"
>
{' '}
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}
fontSize="14px"
style={{ cursor: this.state.ready ? 'pointer' : 'default' }}
onClick={() => {
this.submitProvider(this.state.provider);
}}
>
Set Peer Node
</Button>
{this.state.connecting ? <LoadingSpinner /> : null}
</Row>
</Box>
);
}
}

View File

@ -1,457 +0,0 @@
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: 'bridge',
};
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: {
high: [30, n.estimates[30]["sat_per_vbyte"]],
mid: [180, n.estimates[180]["sat_per_vbyte"]],
low: [360, n.estimates[360]["sat_per_vbyte"]],
}
});
})
}
}
setSignMethod(signMethod) {
this.setState({signMethod, choosingSignMethod: false});
}
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, fee } = 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 === 'masterTicket') {
invoice =
<Invoice
network={network}
api={api}
psbt={psbt}
fee={fee}
currencyRates={currencyRates}
stopSending={stopSending}
payee={payee}
denomination={denomination}
satsAmount={satsAmount}
state={this.props.state}
/>
} else if (signMethod === 'bridge') {
invoice =
<BridgeInvoice
state={this.props.state}
api={api}
psbt={psbt}
fee={fee}
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}
height='48px'
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>
{signMethod === 'masterTicket' &&
<Row
mt={4}
alignItems='center'
>
<Icon icon='Info' color='yellow' height={4} width={4}/>
<Text fontSize="14px" fontWeight="regular" color="gray" ml={2}>
We recommend that you sign transactions using Bridge to protect your master ticket.
</Text>
</Row>
}
</Col>
}
</>
);
}
}

View File

@ -1,59 +0,0 @@
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

@ -1,121 +0,0 @@
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

@ -1,52 +0,0 @@
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 === 'masterTicket') ? 'blue' : 'lightBlue'}
height='48px'
onClick={() => setSignMethod('masterTicket')}
children='Sign with Master Ticket' />
<Button
border='none'
backgroundColor='transparent'
fontWeight='bold'
cursor='pointer'
color={(signMethod === 'bridge') ? 'blue' : 'lightBlue'}
height='48px'
onClick={() => setSignMethod('bridge')}
children='Sign with Bridge' />
</Box>
:
<Button
primary
children={signMethod === 'bridge' ? 'Sign with Bridge' : 'Sign with Master Ticket'}
fontSize={1}
fontWeight='bold'
borderRadius='24px'
height='48px'
onClick={initPayment}
color={signReady ? 'white' : 'lighterGray'}
backgroundColor={signReady ? 'blue' : 'veryLightGray'}
disabled={!signReady}
border='none'
style={{cursor: signReady ? 'pointer' : 'default'}}
/>
)
}

Some files were not shown because too many files have changed in this diff Show More