Merge branch 'ea/master-dist-deconflict' into dist

This commit is contained in:
Hunter Miller 2021-09-27 18:56:49 -05:00
commit 72e3aca57c
131 changed files with 4881 additions and 3970 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

@ -5,10 +5,10 @@
:- %aqua-events
%+ turn
^- (list unix-event)
:~ [//term/1 %belt %ctl `@c`%e]
[//term/1 %belt %ctl `@c`%u]
[//term/1 %belt %txt ((list @c) command)]
[//term/1 %belt %ret ~]
:~ [/d/term/1 %belt %ctl `@c`%e]
[/d/term/1 %belt %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

@ -29,7 +29,7 @@
::
dub=_|
==
:- %pill
:- %boot-pill
^- pill:pill
:: sys: root path to boot system, `/~me/[desk]/now/sys`
:: bas: root path to boot system' desk

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

@ -1 +0,0 @@
../../../base-dev/lib/ph/io.hoon

View File

@ -1 +0,0 @@
../../../base-dev/lib/ph/util.hoon

View File

@ -1 +0,0 @@
../../base-dev/lib/pill.hoon

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

@ -1 +0,0 @@
../../base-dev/sur/aquarium.hoon

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

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

@ -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

@ -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 %ctl `@c`%e]
[//term/1 %belt %ctl `@c`%u]
[//term/1 %belt %txt ((list @c) what)]
[//term/1 %belt %ret ~]
[/d/term/1 %belt %ctl `@c`%e]
[/d/term/1 %belt %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 %ctl (,@c what)]
:~ [/d/term/1 %belt %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

@ -18,6 +18,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

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]

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 {
@ -36,24 +37,29 @@ let devServer = {
publicPath: '/apps/bitcoin/',
};
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: 'index.html',
proxy: [{
target: 'http://localhost:9000',
changeOrigin: true,
target: urbitrc.URL,
router,
context: path => {
if(path === '/apps/bitcoin/desk.js') {
return true;
}
return !path.startsWith('/apps/bitcoin')
}
}]
proxy: [
{
target: 'http://localhost:9000',
changeOrigin: true,
target: urbitrc.URL,
router,
context: (path) => {
if (path === '/apps/bitcoin/desk.js') {
return true;
}
return !path.startsWith('/apps/bitcoin');
},
},
],
};
}
@ -61,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,
@ -94,13 +86,13 @@ 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,
@ -108,24 +100,23 @@ module.exports = {
new UrbitShipPlugin(urbitrc),
new HtmlWebpackPlugin({
title: 'Bitcoin Wallet',
template: './public/index.html'
})
template: './public/index.html',
}),
],
watch: true,
watchOptions: {
poll: true,
ignored: '/node_modules/'
ignored: '/node_modules/',
},
output: {
filename: 'index.js',
chunkFilename: 'index.js',
path: path.resolve(__dirname, '../dist'),
publicPath: '/apps/bitcoin/',
globalObject: 'this'
globalObject: 'this',
},
optimization: {
minimize: false,
usedExports: true
}
usedExports: true,
},
};

View File

@ -7,59 +7,52 @@ 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(),
new HtmlWebpackPlugin({
title: 'Bitcoin Wallet',
template: './public/index.html'
})
template: './public/index.html',
}),
],
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, '../dist'),
publicPath: '/apps/bitcoin/',
},
optimization: {
minimize: true,
usedExports: true
}
usedExports: true,
},
};

Binary file not shown.

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 basename="/apps/bitcoin">
<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="/settings">
<Col display="flex" flexDirection="column" width={cardWidth}>
<Header settings={true} />
<Settings />
</Col>
</Route>
<Route path="/">
<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,39 @@
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': {
desk: window.desk,
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 ? '/' : '/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.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.ship;
});
let weRecv = tx.outputs.every((output) => {
return output.ship === window.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.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.ship) {
return sum + output.val.value;
} else {
return sum;
}
},
0
);
if (weSent && weRecv) {
counterAddress = _.get(
_.find(tx.inputs, (input) => {
return input.ship === window.ship;
}),
'val.address',
null
);
} else {
let counter = _.find(tx.inputs, (input) => {
return input.ship !== window.ship;
});
counterShip = _.get(counter, 'ship', null);
counterAddress = _.get(counter, 'val.address', null);
}
sign = '';
}
let currencyValue = sign + satsToCurrency(value, 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.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,73 @@
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,
desk: window.desk,
'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: true,
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: false,
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/${window.desk}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 Channel from './js/channel';
import './css/indigo-static.css';
import './css/fonts.css';
import './css/custom.css';
// rebuild x3
const channel = new 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,24 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { api } from './lib/api';
import Channel from './lib/channel';
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 Channel();
api.setChannel(window.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="/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="/">
<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,51 +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": {
desk: window.desk,
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 ? "/" : "/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'}}
/>
)
}

View File

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

View File

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

View File

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

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