2021-05-13 18:21:35 +03:00
|
|
|
:: aggregator: Azimuth L2 roll aggregator
|
|
|
|
::
|
|
|
|
:: general flow is as described below, to ensure transactions actually go
|
|
|
|
:: through once we start sending it out, in the dumbest reasonable way.
|
|
|
|
::
|
|
|
|
:: periodic timer fires:
|
|
|
|
:: if there are no pending l2 txs, do nothing.
|
|
|
|
:: else kick off tx submission flow:
|
|
|
|
:: "freeze" pending txs, store alongside nonce, then increment nonce,
|
|
|
|
:: kick off thread for sending the corresponding l1 tx:
|
|
|
|
:: if nonce doesn't match on-chain expected nonce, bail.
|
|
|
|
:: if we can't afford the tx fee, bail.
|
|
|
|
:: construct, sign, submit the l1 tx.
|
|
|
|
:: if thread bailed, retry in five minutes.
|
|
|
|
:: if thread succeeded, retry in five minutes with higher gas price.
|
|
|
|
:: when retrying, only do so if l2 txs remain in the "frozen" txs group.
|
|
|
|
:: on %tx diff from naive, remove the matching tx from the frozen group.
|
|
|
|
::
|
|
|
|
::TODO remaining general work:
|
|
|
|
::
|
|
|
|
::TODO questions:
|
|
|
|
:: - it's a bit weird how we just assume the raw and tx in raw-tx to match...
|
|
|
|
::
|
2021-05-21 16:07:08 +03:00
|
|
|
/- *aggregator
|
2021-06-14 16:00:15 +03:00
|
|
|
/+ azimuth,
|
|
|
|
naive,
|
|
|
|
lib=naive-transactions,
|
|
|
|
default-agent,
|
|
|
|
ethereum,
|
|
|
|
dbug,
|
|
|
|
verb
|
2021-05-13 18:21:35 +03:00
|
|
|
::
|
|
|
|
|%
|
|
|
|
+$ state-0
|
|
|
|
$: %0
|
|
|
|
:: pending: the next l2 txs to be sent
|
|
|
|
:: sending: the l2 txs currently sending/awaiting l2 confirmation
|
|
|
|
:: finding: raw-tx-hash reverse lookup for sending map
|
2021-06-16 18:03:21 +03:00
|
|
|
:: history: status of l2 txs by ethereum address
|
2021-05-13 18:21:35 +03:00
|
|
|
:: next-nonce: next l1 nonce to use
|
2021-06-09 14:05:17 +03:00
|
|
|
:: next-batch: when then next l2 batch will be sent
|
2021-06-09 14:30:36 +03:00
|
|
|
:: pre: predicted l2 state
|
2021-06-11 13:27:05 +03:00
|
|
|
:: flush: flag for deriving predicted state
|
2021-05-13 18:21:35 +03:00
|
|
|
::
|
|
|
|
pending=(list pend-tx)
|
2021-06-01 17:18:32 +03:00
|
|
|
::
|
|
|
|
$= sending
|
|
|
|
%+ map l1-tx-pointer
|
|
|
|
[next-gas-price=@ud txs=(list raw-tx:naive)]
|
|
|
|
::
|
2021-05-13 18:21:35 +03:00
|
|
|
finding=(map keccak $?(%confirmed %failed l1-tx-pointer))
|
2021-06-16 18:03:21 +03:00
|
|
|
history=(jug address:ethereum roller-tx)
|
2021-06-09 16:37:37 +03:00
|
|
|
next-nonce=(unit @ud)
|
2021-06-09 14:05:17 +03:00
|
|
|
next-batch=time
|
2021-06-09 14:30:36 +03:00
|
|
|
pre=^state:naive
|
2021-06-11 13:27:05 +03:00
|
|
|
flush=?
|
2021-05-13 18:21:35 +03:00
|
|
|
::
|
|
|
|
:: pk: private key to send the roll
|
|
|
|
:: frequency: time to wait between sending batches (TODO fancier)
|
|
|
|
:: endpoint: ethereum rpc endpoint to use
|
2021-05-29 15:43:13 +03:00
|
|
|
:: contract: ethereum contract address
|
|
|
|
:: chain-id: mainnet, ropsten, local (https://chainid.network/)
|
2021-05-13 18:21:35 +03:00
|
|
|
::
|
|
|
|
pk=@
|
|
|
|
frequency=@dr
|
2021-06-09 16:37:37 +03:00
|
|
|
endpoint=(unit @t)
|
2021-05-29 15:43:13 +03:00
|
|
|
contract=@ux
|
|
|
|
chain-id=@
|
2021-05-13 18:21:35 +03:00
|
|
|
==
|
|
|
|
::
|
2021-06-03 10:11:33 +03:00
|
|
|
+$ config
|
|
|
|
$% [%frequency frequency=@dr]
|
2021-05-13 18:21:35 +03:00
|
|
|
[%setkey pk=@]
|
2021-05-21 13:16:57 +03:00
|
|
|
[%endpoint endpoint=@t]
|
2021-05-29 15:43:13 +03:00
|
|
|
[%network net=?(%mainnet %ropsten %local)]
|
2021-06-03 10:11:33 +03:00
|
|
|
==
|
|
|
|
::
|
|
|
|
+$ action
|
|
|
|
$% [%submit force=? sig=@ tx=part-tx]
|
2021-06-21 12:27:36 +03:00
|
|
|
[%cancel sig=@ keccak=@ =l2-tx]
|
2021-06-03 10:11:33 +03:00
|
|
|
[%commit ~] ::TODO maybe pk=(unit @) later
|
|
|
|
[%config config]
|
2021-05-13 18:21:35 +03:00
|
|
|
==
|
|
|
|
::
|
|
|
|
+$ card card:agent:gall
|
|
|
|
::
|
|
|
|
++ resend-time ~m5
|
|
|
|
::
|
|
|
|
++ lverb &
|
|
|
|
--
|
|
|
|
::
|
2021-05-17 18:48:49 +03:00
|
|
|
=| state-0
|
|
|
|
=* state -
|
|
|
|
::
|
2021-05-13 18:21:35 +03:00
|
|
|
%- agent:dbug
|
|
|
|
%+ verb |
|
|
|
|
^- agent:gall
|
|
|
|
::
|
|
|
|
=<
|
|
|
|
|_ =bowl:gall
|
|
|
|
+* this .
|
2021-05-17 18:48:49 +03:00
|
|
|
do ~(. +> bowl)
|
2021-05-13 18:21:35 +03:00
|
|
|
def ~(. (default-agent this %|) bowl)
|
|
|
|
::
|
|
|
|
++ on-init
|
|
|
|
^- (quip card _this)
|
|
|
|
=. frequency ~h1
|
2021-05-29 15:43:13 +03:00
|
|
|
=. contract naive:local-contracts:azimuth
|
|
|
|
=. chain-id chain-id:local-contracts:azimuth
|
2021-06-09 14:05:17 +03:00
|
|
|
=^ card next-batch set-timer
|
2021-05-23 16:49:55 +03:00
|
|
|
:_ this
|
2021-06-09 14:05:17 +03:00
|
|
|
:~ card
|
2021-06-07 14:07:35 +03:00
|
|
|
[%pass /azimuth-txs %agent [our.bowl %azimuth] %watch /txs]
|
|
|
|
==
|
2021-05-13 18:21:35 +03:00
|
|
|
::
|
|
|
|
++ on-save !>(state)
|
|
|
|
++ on-load
|
|
|
|
|= old=vase
|
|
|
|
^- (quip card _this)
|
|
|
|
[~ this(state !<(state-0 old))]
|
|
|
|
::
|
|
|
|
++ on-poke
|
|
|
|
|= [=mark =vase]
|
|
|
|
^- (quip card _this)
|
2021-05-17 18:48:49 +03:00
|
|
|
=^ cards state
|
|
|
|
?+ mark (on-poke:def mark vase)
|
2021-05-21 13:16:57 +03:00
|
|
|
%aggregator-action
|
2021-05-17 18:48:49 +03:00
|
|
|
=+ !<(poke=action vase)
|
|
|
|
(on-action:do poke)
|
|
|
|
==
|
|
|
|
[cards this]
|
2021-05-13 18:21:35 +03:00
|
|
|
:: +on-peek: scry paths
|
|
|
|
::
|
|
|
|
:: /x/pending -> %noun (list pend-tx)
|
|
|
|
:: /x/pending/[~ship] -> %noun (list pend-tx)
|
|
|
|
:: /x/pending/[0xadd.ress] -> %noun (list pend-tx)
|
|
|
|
:: /x/tx/[0xke.ccak]/status -> %noun tx-status
|
2021-06-14 15:18:57 +03:00
|
|
|
:: /x/nonce/[~ship]/[proxy] -> %noun (unit @)
|
2021-06-21 12:27:36 +03:00
|
|
|
:: /x/spawned/[~ship] -> %noun (list [ship address])
|
2021-06-09 14:05:17 +03:00
|
|
|
:: /x/next-batch -> %atom time
|
2021-05-13 18:21:35 +03:00
|
|
|
::
|
|
|
|
++ on-peek
|
|
|
|
|= =path
|
|
|
|
^- (unit (unit cage))
|
2021-06-04 12:41:59 +03:00
|
|
|
|^
|
2021-05-13 18:21:35 +03:00
|
|
|
?+ path ~
|
2021-06-09 14:05:17 +03:00
|
|
|
[%x %pending ~] ``noun+!>(pending)
|
2021-06-04 12:41:59 +03:00
|
|
|
[%x %pending @ ~] (pending-by i.t.t.path)
|
|
|
|
[%x %tx @ %status ~] (status i.t.t.path)
|
2021-06-16 18:03:21 +03:00
|
|
|
[%x %history @ ~] (history i.t.t.path)
|
2021-06-04 12:41:59 +03:00
|
|
|
[%x %nonce @ @ ~] (nonce i.t.t.path i.t.t.t.path)
|
|
|
|
[%x %spawned @ ~] (spawned i.t.t.path)
|
2021-06-09 14:05:17 +03:00
|
|
|
[%x %next-batch ~] ``noun+!>(next-batch)
|
2021-06-22 16:00:24 +03:00
|
|
|
[%x %point @ ~] (point i.t.t.path)
|
2021-06-04 12:41:59 +03:00
|
|
|
==
|
|
|
|
::
|
|
|
|
++ pending-by
|
|
|
|
|= wat=@t
|
2021-05-13 18:21:35 +03:00
|
|
|
?~ who=(slaw %p wat)
|
2021-05-17 18:48:49 +03:00
|
|
|
:: by-address
|
|
|
|
::
|
2021-05-13 18:21:35 +03:00
|
|
|
?~ wer=(slaw %ux wat)
|
|
|
|
[~ ~]
|
2021-05-17 18:48:49 +03:00
|
|
|
=; pending=(list pend-tx)
|
|
|
|
``noun+!>(pending)
|
2021-06-09 14:05:17 +03:00
|
|
|
%+ skim pending
|
2021-05-13 18:21:35 +03:00
|
|
|
|= pend-tx
|
2021-06-14 16:00:15 +03:00
|
|
|
=(u.wer (get-l1-address tx.raw-tx pre))
|
2021-05-17 18:48:49 +03:00
|
|
|
:: by-ship
|
|
|
|
::
|
|
|
|
=; pending=(list pend-tx)
|
|
|
|
``noun+!>(pending)
|
2021-06-09 14:05:17 +03:00
|
|
|
%+ skim pending
|
2021-05-13 18:21:35 +03:00
|
|
|
|= pend-tx
|
|
|
|
=(u.who ship.from.tx.raw-tx)
|
|
|
|
::
|
2021-06-04 12:41:59 +03:00
|
|
|
++ status
|
|
|
|
|= wat=@t
|
|
|
|
?~ keccak=(slaw %ux wat)
|
2021-05-13 18:21:35 +03:00
|
|
|
[~ ~]
|
|
|
|
:+ ~ ~
|
|
|
|
:- %noun
|
|
|
|
!> ^- tx-status
|
|
|
|
?^ status=(~(get by finding) u.keccak)
|
|
|
|
?@ u.status [u.status ~]
|
2021-05-17 18:48:49 +03:00
|
|
|
[%sending status]
|
2021-05-13 18:21:35 +03:00
|
|
|
::TODO potentially slow!
|
|
|
|
=; known=?
|
|
|
|
[?:(known %pending %unknown) ~]
|
2021-06-09 14:05:17 +03:00
|
|
|
%+ lien pending
|
2021-06-16 18:03:21 +03:00
|
|
|
|= pend-tx
|
|
|
|
=(u.keccak (hash-tx raw.raw-tx))
|
|
|
|
::
|
|
|
|
++ history
|
|
|
|
|= wat=@t
|
|
|
|
:+ ~ ~
|
|
|
|
:- %noun
|
|
|
|
!> ^- (list roller-tx)
|
|
|
|
?~ addr=(slaw %ux wat) ~
|
|
|
|
%~ tap in
|
|
|
|
(~(get ju ^history) u.addr)
|
2021-05-13 18:21:35 +03:00
|
|
|
::
|
2021-06-04 12:41:59 +03:00
|
|
|
++ nonce
|
|
|
|
|= [who=@t proxy=@t]
|
|
|
|
?~ who=(slaw %p who)
|
2021-05-13 18:21:35 +03:00
|
|
|
[~ ~]
|
|
|
|
?. ?=(proxy:naive proxy)
|
|
|
|
[~ ~]
|
2021-06-14 15:18:57 +03:00
|
|
|
:+ ~ ~
|
|
|
|
:- %noun
|
|
|
|
!> ^- (unit @)
|
|
|
|
?~ point=(get:orm:naive points.pre u.who)
|
|
|
|
~
|
|
|
|
=< `nonce
|
|
|
|
(proxy-from-point:naive proxy u.point)
|
2021-06-04 12:41:59 +03:00
|
|
|
::
|
|
|
|
++ spawned
|
|
|
|
|= wat=@t
|
|
|
|
:+ ~ ~
|
|
|
|
:- %noun
|
|
|
|
!> ^- (list [=^ship =address:ethereum])
|
|
|
|
?~ star=(slaw %p wat) ~
|
2021-06-09 15:20:58 +03:00
|
|
|
=/ range
|
2021-06-10 14:39:03 +03:00
|
|
|
%+ lot:orm:naive points.pre
|
2021-06-09 15:20:58 +03:00
|
|
|
:: range exclusive [star first-planet-next-star]
|
|
|
|
:: TODO: make range inclusive? [first-planet last-planet]
|
|
|
|
::
|
|
|
|
[`u.star `(cat 3 +(u.star) 0x1)]
|
2021-06-09 14:34:04 +03:00
|
|
|
%+ turn (tap:orm:naive range)
|
2021-06-04 12:41:59 +03:00
|
|
|
|= [=ship =point:naive]
|
2021-06-09 15:20:58 +03:00
|
|
|
^- [=^ship =address:ethereum]
|
2021-06-04 12:41:59 +03:00
|
|
|
:- ship
|
|
|
|
address:(proxy-from-point:naive %own point)
|
2021-06-22 16:00:24 +03:00
|
|
|
::
|
|
|
|
++ point
|
|
|
|
|= wat=@t
|
|
|
|
?~ ship=(rush wat ;~(pfix sig fed:ag))
|
|
|
|
``noun+!>(*(unit point:naive))
|
|
|
|
``noun+!>((get:orm:naive points.pre u.ship))
|
2021-06-04 12:41:59 +03:00
|
|
|
--
|
2021-05-13 18:21:35 +03:00
|
|
|
::
|
|
|
|
++ on-arvo
|
|
|
|
|= [=wire =sign-arvo]
|
|
|
|
^- (quip card _this)
|
2021-06-07 14:09:16 +03:00
|
|
|
?+ wire (on-arvo:def wire sign-arvo)
|
|
|
|
[%timer ~]
|
|
|
|
?+ +<.sign-arvo (on-arvo:def wire sign-arvo)
|
|
|
|
%wake =^(cards state on-timer:do [cards this])
|
|
|
|
==
|
2021-06-11 13:27:05 +03:00
|
|
|
::
|
|
|
|
[%predict ~]
|
|
|
|
?+ +<.sign-arvo (on-arvo:def wire sign-arvo)
|
|
|
|
%wake
|
|
|
|
=^ pending pre (predicted-state:do canonical-state:do)
|
|
|
|
[~ this(pending pending, flush &)]
|
|
|
|
==
|
2021-06-07 14:09:16 +03:00
|
|
|
::
|
|
|
|
[%resend @ @ ~]
|
|
|
|
=/ [address=@ux nonce=@ud]
|
|
|
|
[(slav %ux i.t.wire) (rash i.t.t.wire dem)]
|
|
|
|
?+ +<.sign-arvo (on-arvo:def wire sign-arvo)
|
|
|
|
%wake [(send-roll:do address nonce) this]
|
|
|
|
==
|
2021-05-13 18:21:35 +03:00
|
|
|
==
|
|
|
|
::
|
|
|
|
++ on-fail
|
|
|
|
|= [=term =tang]
|
|
|
|
::TODO if crashed during timer, set new timer? how to detect?
|
|
|
|
(on-fail:def term tang)
|
|
|
|
::
|
|
|
|
++ on-watch on-watch:def
|
|
|
|
++ on-leave on-leave:def
|
2021-05-21 13:16:57 +03:00
|
|
|
++ on-agent
|
|
|
|
|= [=wire =sign:agent:gall]
|
|
|
|
^- (quip card _this)
|
2021-05-23 16:49:55 +03:00
|
|
|
|^
|
2021-05-29 15:40:13 +03:00
|
|
|
?+ wire (on-agent:def wire sign)
|
2021-06-05 14:58:34 +03:00
|
|
|
[%send @ @ *] (send-batch i.t.wire i.t.t.wire sign)
|
|
|
|
[%azimuth-txs ~] (azimuth-update sign)
|
|
|
|
[%nonce ~] (nonce sign)
|
2021-05-29 15:40:13 +03:00
|
|
|
==
|
2021-05-21 13:16:57 +03:00
|
|
|
::
|
2021-06-03 10:24:28 +03:00
|
|
|
++ send-batch
|
2021-06-01 17:18:32 +03:00
|
|
|
|= [address=@t nonce=@t =sign:agent:gall]
|
2021-05-23 16:49:55 +03:00
|
|
|
^- (quip card _this)
|
2021-06-03 10:26:13 +03:00
|
|
|
=/ [address=@ux nonce=@ud]
|
|
|
|
[(slav %ux address) (rash nonce dem)]
|
2021-05-23 16:49:55 +03:00
|
|
|
?- -.sign
|
|
|
|
%poke-ack
|
|
|
|
?~ p.sign
|
2021-05-29 15:40:13 +03:00
|
|
|
%- (slog leaf+"Send batch thread started successfully" ~)
|
2021-05-23 16:49:55 +03:00
|
|
|
[~ this]
|
|
|
|
%- (slog leaf+"{(trip dap.bowl)} couldn't start thread" u.p.sign)
|
|
|
|
:_ this
|
|
|
|
[(leave:spider:do wire)]~
|
|
|
|
::
|
|
|
|
%watch-ack
|
|
|
|
?~ p.sign
|
|
|
|
[~ this]
|
|
|
|
=/ =tank leaf+"{(trip dap.bowl)} couldn't start listen to thread"
|
|
|
|
%- (slog tank u.p.sign)
|
2021-05-21 13:16:57 +03:00
|
|
|
[~ this]
|
2021-05-23 16:49:55 +03:00
|
|
|
::
|
|
|
|
%kick
|
|
|
|
[~ this]
|
|
|
|
::
|
|
|
|
%fact
|
|
|
|
?+ p.cage.sign (on-agent:def wire sign)
|
|
|
|
%thread-fail
|
|
|
|
=+ !<([=term =tang] q.cage.sign)
|
|
|
|
%- (slog leaf+"{(trip dap.bowl)} failed" leaf+<term> tang)
|
|
|
|
=^ cards state
|
2021-06-03 10:26:13 +03:00
|
|
|
(on-batch-result:do address nonce %.n^'thread failed')
|
2021-05-23 16:49:55 +03:00
|
|
|
[cards this]
|
|
|
|
::
|
|
|
|
%thread-done
|
|
|
|
=+ !<(result=(each @ud @t) q.cage.sign)
|
|
|
|
=^ cards state
|
2021-06-03 10:26:13 +03:00
|
|
|
(on-batch-result:do address nonce result)
|
2021-05-23 16:49:55 +03:00
|
|
|
[cards this]
|
|
|
|
==
|
|
|
|
==
|
2021-05-21 13:16:57 +03:00
|
|
|
::
|
2021-06-03 10:24:28 +03:00
|
|
|
++ azimuth-update
|
2021-05-29 15:40:13 +03:00
|
|
|
|= =sign:agent:gall
|
2021-05-23 16:49:55 +03:00
|
|
|
^- (quip card _this)
|
|
|
|
?+ -.sign [~ this]
|
|
|
|
%watch-ack
|
|
|
|
?~ p.sign [~ this]
|
|
|
|
=/ =tank leaf+"{(trip dap.bowl)} couldn't start listen to %azimuth"
|
|
|
|
%- (slog tank u.p.sign)
|
|
|
|
[~ this]
|
2021-05-21 13:16:57 +03:00
|
|
|
::
|
2021-05-23 16:49:55 +03:00
|
|
|
%fact
|
|
|
|
?+ p.cage.sign (on-agent:def wire sign)
|
2021-05-29 15:40:13 +03:00
|
|
|
%naive-diffs
|
2021-05-23 16:49:55 +03:00
|
|
|
=+ !<(=diff:naive q.cage.sign)
|
|
|
|
=^ cards state
|
|
|
|
(on-naive-diff:do diff)
|
|
|
|
[cards this]
|
2021-06-01 14:56:56 +03:00
|
|
|
::
|
|
|
|
%naive-state
|
|
|
|
:: cache naive state, received upon innitializing subscription
|
|
|
|
::
|
|
|
|
~& > %get-naive-state
|
2021-06-03 10:24:28 +03:00
|
|
|
:: this assumes that %azimuth has already processed eth data
|
2021-06-01 14:56:56 +03:00
|
|
|
::
|
2021-06-09 14:30:36 +03:00
|
|
|
=^ pending pre
|
2021-06-11 13:27:05 +03:00
|
|
|
(predicted-state:do !<(^state:naive q.cage.sign))
|
2021-06-03 10:24:28 +03:00
|
|
|
[~ this(pending pending)]
|
2021-05-23 16:49:55 +03:00
|
|
|
==
|
2021-05-21 13:16:57 +03:00
|
|
|
==
|
2021-05-29 15:40:13 +03:00
|
|
|
::
|
2021-06-03 10:24:28 +03:00
|
|
|
++ nonce
|
2021-05-29 15:40:13 +03:00
|
|
|
|= =sign:agent:gall
|
|
|
|
^- (quip card _this)
|
|
|
|
?- -.sign
|
|
|
|
%poke-ack
|
|
|
|
?~ p.sign
|
|
|
|
%- (slog leaf+"Nonce thread started successfully" ~)
|
|
|
|
[~ this]
|
|
|
|
%- (slog leaf+"{(trip dap.bowl)} couldn't start thread" u.p.sign)
|
|
|
|
:_ this
|
|
|
|
[(leave:spider:do wire)]~
|
|
|
|
::
|
|
|
|
%watch-ack
|
|
|
|
?~ p.sign
|
|
|
|
[~ this]
|
|
|
|
=/ =tank leaf+"{(trip dap.bowl)} couldn't start listen to thread"
|
|
|
|
%- (slog tank u.p.sign)
|
|
|
|
[~ this]
|
|
|
|
::
|
|
|
|
%kick
|
|
|
|
[~ this]
|
|
|
|
::
|
|
|
|
%fact
|
|
|
|
?+ p.cage.sign (on-agent:def wire sign)
|
|
|
|
%thread-fail
|
|
|
|
=+ !<([=term =tang] q.cage.sign)
|
|
|
|
%- (slog leaf+"{(trip dap.bowl)} failed" leaf+<term> tang)
|
|
|
|
[~ this]
|
|
|
|
::
|
|
|
|
%thread-done
|
|
|
|
=+ !<(nonce=@ud q.cage.sign)
|
2021-06-09 16:37:37 +03:00
|
|
|
[~ this(next-nonce `nonce)]
|
2021-05-29 15:40:13 +03:00
|
|
|
==
|
|
|
|
==
|
2021-05-23 16:49:55 +03:00
|
|
|
--
|
2021-05-13 18:21:35 +03:00
|
|
|
--
|
|
|
|
::
|
|
|
|
|_ =bowl:gall
|
|
|
|
::TODO /lib/sys.hoon?
|
|
|
|
++ sys
|
|
|
|
|%
|
|
|
|
++ b
|
|
|
|
|%
|
|
|
|
++ wait
|
|
|
|
|= [=wire =time]
|
|
|
|
^- card
|
|
|
|
[%pass wire %arvo %b %wait time]
|
|
|
|
--
|
|
|
|
--
|
|
|
|
::TODO /lib/spider.hoon?
|
|
|
|
++ spider
|
|
|
|
|%
|
|
|
|
++ start-thread
|
|
|
|
|= [=wire thread=term arg=vase]
|
|
|
|
^- (list card)
|
|
|
|
=/ tid=@ta (rap 3 thread '--' (scot %uv eny.bowl) ~)
|
2021-05-23 16:49:55 +03:00
|
|
|
=/ args [~ `tid thread arg]
|
2021-05-21 13:16:57 +03:00
|
|
|
:~ [%pass wire %agent [our.bowl %spider] %watch /thread-result/[tid]]
|
2021-05-23 16:49:55 +03:00
|
|
|
[%pass wire %agent [our.bowl %spider] %poke %spider-start !>(args)]
|
2021-05-13 18:21:35 +03:00
|
|
|
==
|
2021-05-17 18:48:49 +03:00
|
|
|
::
|
|
|
|
++ leave
|
|
|
|
|= =path
|
|
|
|
^- card
|
|
|
|
[%pass path %agent [our.bowl %spider] %leave ~]
|
2021-05-13 18:21:35 +03:00
|
|
|
--
|
|
|
|
::
|
|
|
|
++ hash-tx keccak-256:keccak:crypto
|
|
|
|
::
|
|
|
|
++ hash-raw-tx
|
2021-05-17 18:48:49 +03:00
|
|
|
|= =raw-tx:naive
|
2021-05-29 15:46:19 +03:00
|
|
|
^- @ux
|
2021-05-17 18:48:49 +03:00
|
|
|
(hash-tx raw.raw-tx)
|
2021-05-13 18:21:35 +03:00
|
|
|
::
|
|
|
|
++ part-tx-to-full
|
|
|
|
|= =part-tx
|
|
|
|
^- [octs tx:naive]
|
2021-05-29 15:46:19 +03:00
|
|
|
?- -.part-tx
|
2021-05-23 15:59:29 +03:00
|
|
|
%raw
|
|
|
|
?~ batch=(parse-raw-tx:naive q.raw.part-tx)
|
|
|
|
~& %parse-failed
|
|
|
|
:: TODO: maybe return a unit if parsing fails?
|
|
|
|
::
|
|
|
|
!!
|
|
|
|
[raw tx]:-.u.batch
|
2021-05-29 15:44:16 +03:00
|
|
|
::
|
|
|
|
%don [(gen-tx-octs:lib +.part-tx) +.part-tx]
|
2021-05-13 18:21:35 +03:00
|
|
|
%ful +.part-tx
|
|
|
|
==
|
2021-06-21 12:27:36 +03:00
|
|
|
:: +canonical-state: load current l2 state instead
|
2021-06-03 10:24:28 +03:00
|
|
|
::
|
|
|
|
++ canonical-state
|
|
|
|
.^ ^state:naive
|
|
|
|
%gx
|
|
|
|
(scot %p our.bowl)
|
|
|
|
%azimuth
|
|
|
|
(scot %da now.bowl)
|
|
|
|
/nas/nas
|
|
|
|
==
|
2021-06-07 12:20:31 +03:00
|
|
|
:: +predicted-state
|
2021-05-13 18:21:35 +03:00
|
|
|
::
|
2021-06-07 12:20:31 +03:00
|
|
|
:: derives predicted state from pending/sending txs and
|
2021-06-05 15:24:37 +03:00
|
|
|
:: canonical state, discarding invalid txs in the process.
|
2021-05-13 18:21:35 +03:00
|
|
|
::
|
2021-06-07 12:20:31 +03:00
|
|
|
++ predicted-state
|
2021-06-03 10:24:28 +03:00
|
|
|
|= nas=^state:naive
|
|
|
|
^- [_pending _nas]
|
2021-06-05 15:24:37 +03:00
|
|
|
|^
|
|
|
|
=^ new-sending nas apply-sending
|
|
|
|
=. sending new-sending
|
2021-06-22 16:15:33 +03:00
|
|
|
(update-txs pending %pending)
|
2021-05-13 18:21:35 +03:00
|
|
|
::
|
2021-06-05 15:24:37 +03:00
|
|
|
++ apply-sending
|
|
|
|
=| valid=_sending
|
|
|
|
=+ sending=~(tap by sending)
|
|
|
|
|- ^+ [valid nas]
|
|
|
|
?~ sending [valid nas]
|
|
|
|
::
|
|
|
|
=* key p.i.sending
|
|
|
|
=* val q.i.sending
|
|
|
|
=^ new-valid nas
|
2021-06-22 16:15:33 +03:00
|
|
|
%+ update-txs
|
|
|
|
(turn txs.val |=(=raw-tx:naive [| 0x0 raw-tx]))
|
|
|
|
%sending
|
2021-06-05 15:24:37 +03:00
|
|
|
=. valid
|
|
|
|
%+ ~(put by valid) key
|
2021-06-16 18:03:21 +03:00
|
|
|
val(txs (turn new-valid (cork tail tail)))
|
2021-06-05 15:24:37 +03:00
|
|
|
$(sending t.sending)
|
|
|
|
::
|
|
|
|
++ update-txs
|
2021-06-22 16:15:33 +03:00
|
|
|
|= [txs=(list pend-tx) type=?(%pending %sending)]
|
2021-06-05 15:24:37 +03:00
|
|
|
=/ valid=_txs ~
|
|
|
|
|- ^+ [valid nas]
|
|
|
|
?~ txs [valid nas]
|
|
|
|
=* tx i.txs
|
2021-06-16 18:03:21 +03:00
|
|
|
=^ gud=? nas (try-apply nas [force raw-tx]:tx)
|
2021-06-05 15:24:37 +03:00
|
|
|
=? valid gud (snoc valid tx)
|
|
|
|
=? finding =(gud %.n)
|
|
|
|
%- ~(put by finding)
|
|
|
|
[(hash-raw-tx raw-tx.tx) %failed]
|
2021-06-22 16:15:33 +03:00
|
|
|
=? history =(gud %.n)
|
|
|
|
=/ =roller-tx
|
|
|
|
:+ [type ~]
|
|
|
|
(hash-raw-tx raw-tx.tx)
|
|
|
|
(l2-tx +<.tx.raw-tx.tx)
|
|
|
|
%+ ~(put ju (~(del ju history) address.tx roller-tx))
|
|
|
|
address.tx
|
|
|
|
roller-tx(status [%failed ~])
|
2021-06-05 15:24:37 +03:00
|
|
|
$(txs t.txs)
|
2021-05-13 18:21:35 +03:00
|
|
|
::
|
2021-06-11 13:27:05 +03:00
|
|
|
++ try-apply
|
|
|
|
|= [nas=^state:naive force=? =raw-tx:naive]
|
|
|
|
^- [success=? _nas]
|
|
|
|
=/ chain-t=@t (ud-to-ascii:naive chain-id)
|
|
|
|
?. (verify-sig-and-nonce:naive verifier:lib chain-t nas raw-tx)
|
|
|
|
~& [%verify-sig-and-nonce %failed]
|
|
|
|
[force nas]
|
|
|
|
::
|
|
|
|
=^ * points.nas
|
|
|
|
(increment-nonce:naive nas from.tx.raw-tx)
|
|
|
|
::
|
|
|
|
?~ nex=(receive-tx:naive nas tx.raw-tx)
|
|
|
|
[force nas]
|
|
|
|
[& +.u.nex]
|
|
|
|
--
|
2021-05-13 18:21:35 +03:00
|
|
|
::
|
2021-06-09 16:37:37 +03:00
|
|
|
++ get-l1-address
|
2021-05-29 15:46:19 +03:00
|
|
|
|= [=tx:naive nas=^state:naive]
|
2021-06-09 16:37:37 +03:00
|
|
|
^- address:ethereum
|
2021-06-09 14:34:04 +03:00
|
|
|
?~ point=(get:orm:naive points.nas ship.from.tx)
|
2021-05-29 15:46:19 +03:00
|
|
|
!!
|
|
|
|
=< address
|
2021-06-01 14:56:56 +03:00
|
|
|
(proxy-from-point:naive proxy.from.tx u.point)
|
2021-05-29 15:46:19 +03:00
|
|
|
::
|
2021-05-13 18:21:35 +03:00
|
|
|
++ on-action
|
|
|
|
|= =action
|
2021-05-17 18:48:49 +03:00
|
|
|
^- (quip card _state)
|
2021-05-13 18:21:35 +03:00
|
|
|
?- -.action
|
2021-06-03 10:11:33 +03:00
|
|
|
%commit on-timer
|
|
|
|
%config (on-config +.action)
|
2021-06-21 12:27:36 +03:00
|
|
|
%cancel (cancel-tx +.action)
|
|
|
|
%submit (take-tx force.action sig.action (part-tx-to-full tx.action))
|
2021-06-03 10:11:33 +03:00
|
|
|
==
|
|
|
|
::
|
|
|
|
++ on-config
|
|
|
|
|= =config
|
|
|
|
^- (quip card _state)
|
|
|
|
?- -.config
|
|
|
|
%frequency [~ state(frequency frequency.config)]
|
2021-06-09 16:37:37 +03:00
|
|
|
%endpoint [~ state(endpoint `endpoint.config)]
|
2021-05-29 15:43:13 +03:00
|
|
|
::
|
|
|
|
%network
|
|
|
|
:- ~
|
|
|
|
=/ [contract=@ux chain-id=@]
|
|
|
|
=< [naive chain-id]
|
|
|
|
=, azimuth
|
2021-06-03 10:11:33 +03:00
|
|
|
?- net.config
|
2021-05-29 15:43:13 +03:00
|
|
|
%mainnet mainnet-contracts
|
|
|
|
%ropsten ropsten-contracts
|
|
|
|
%local local-contracts
|
|
|
|
==
|
|
|
|
state(contract contract, chain-id chain-id)
|
2021-05-21 13:16:57 +03:00
|
|
|
::
|
|
|
|
%setkey
|
2021-06-03 10:11:33 +03:00
|
|
|
?~ pk=(de:base16:mimes:html pk.config)
|
2021-06-01 14:56:56 +03:00
|
|
|
`state
|
|
|
|
[(get-nonce q.u.pk) state(pk q.u.pk)]
|
2021-05-13 18:21:35 +03:00
|
|
|
==
|
2021-06-01 17:18:32 +03:00
|
|
|
:: TODO: move address to state?
|
|
|
|
::
|
|
|
|
++ get-address
|
|
|
|
^- address:ethereum
|
|
|
|
(address-from-prv:key:ethereum pk)
|
2021-06-21 12:27:36 +03:00
|
|
|
:: +cancel-tx: cancel a pending transaction
|
|
|
|
::
|
|
|
|
++ cancel-tx
|
|
|
|
|= [sig=@ =keccak =l2-tx]
|
|
|
|
^- (quip card _state)
|
|
|
|
?^ status=(~(get by finding) keccak)
|
|
|
|
~? lverb [dap.bowl %tx-not-pending status+u.status]
|
|
|
|
[~ state]
|
|
|
|
:: "cancel: 0x1234abcd"
|
|
|
|
::
|
|
|
|
=/ message=octs
|
|
|
|
%: cad:naive 3
|
|
|
|
8^'cancel: '
|
|
|
|
::
|
|
|
|
=; hash=@t
|
|
|
|
(met 3 hash)^hash
|
|
|
|
(crip "0x{((x-co:co 20) keccak)}")
|
|
|
|
::
|
|
|
|
~
|
|
|
|
==
|
|
|
|
?~ addr=(verify-sig sig message)
|
|
|
|
~? lverb [dap.bowl %cancel-sig-fail]
|
|
|
|
[~ state]
|
|
|
|
=. history
|
|
|
|
%+ ~(del ju history) u.addr
|
|
|
|
[[%pending ~] keccak l2-tx]
|
|
|
|
=. pending
|
|
|
|
%+ skip pending
|
|
|
|
|= pend-tx
|
|
|
|
=(keccak (hash-raw-tx raw-tx))
|
|
|
|
[~ state]
|
|
|
|
:: TODO: move to /lib/naive-transactions
|
|
|
|
::
|
|
|
|
++ verify-sig
|
|
|
|
|= [sig=@ txdata=octs]
|
|
|
|
^- (unit address:naive)
|
|
|
|
|^
|
|
|
|
:: Reversed of the usual r-s-v order because Ethereum integers are
|
|
|
|
:: big-endian
|
|
|
|
::
|
|
|
|
=^ v sig (take 3)
|
|
|
|
=^ s sig (take 3 32)
|
|
|
|
=^ r sig (take 3 32)
|
|
|
|
:: In Ethereum, v is generally 27 + recid, and verifier expects a
|
|
|
|
:: recid. Old versions of geth used 0 + recid, so most software
|
|
|
|
:: now supports either format. See:
|
|
|
|
::
|
|
|
|
:: https://github.com/ethereum/go-ethereum/issues/2053
|
|
|
|
::
|
|
|
|
=? v (gte v 27) (sub v 27)
|
|
|
|
(verifier:lib txdata v r s)
|
|
|
|
::
|
|
|
|
++ take
|
|
|
|
|= =bite
|
|
|
|
[(end bite sig) (rsh bite sig)]
|
|
|
|
--
|
2021-05-13 18:21:35 +03:00
|
|
|
:: +take-tx: accept submitted l2 tx into the :pending list
|
|
|
|
::
|
|
|
|
++ take-tx
|
|
|
|
|= [force=? =raw-tx:naive]
|
2021-06-11 13:27:05 +03:00
|
|
|
^- (quip card _state)
|
2021-06-16 18:03:21 +03:00
|
|
|
=/ =address:ethereum
|
|
|
|
(get-l1-address tx.raw-tx pre)
|
|
|
|
=. pending (snoc pending [force address raw-tx])
|
|
|
|
=. history
|
|
|
|
%+ ~(put ju history) address
|
|
|
|
:+ [%pending ~]
|
|
|
|
(hash-raw-tx raw-tx)
|
|
|
|
(l2-tx +<.tx.raw-tx)
|
2021-06-11 13:27:05 +03:00
|
|
|
:: toggle flush flag
|
|
|
|
::
|
|
|
|
:_ state(flush ?:(flush | &))
|
|
|
|
?. flush ~
|
|
|
|
:: derive predicted state in 5m.
|
|
|
|
::
|
|
|
|
[(wait:b:sys /predict (add ~m5 now.bowl))]~
|
2021-05-13 18:21:35 +03:00
|
|
|
:: +set-timer: %wait until next whole :frequency
|
|
|
|
::
|
|
|
|
++ set-timer
|
2021-06-09 14:05:17 +03:00
|
|
|
^- [=card =time]
|
|
|
|
=+ time=(mul +((div now.bowl frequency)) frequency)
|
|
|
|
[(wait:b:sys /timer time) time]
|
2021-05-13 18:21:35 +03:00
|
|
|
:: +on-timer: every :frequency, freeze :pending txs roll and start sending it
|
|
|
|
::
|
|
|
|
++ on-timer
|
2021-05-17 18:48:49 +03:00
|
|
|
^- (quip card _state)
|
2021-06-11 13:27:05 +03:00
|
|
|
=^ new-pending pre (predicted-state canonical-state)
|
|
|
|
=. pending new-pending
|
2021-05-13 18:21:35 +03:00
|
|
|
=^ cards state
|
2021-05-21 13:16:57 +03:00
|
|
|
?: =(~ pending) [~ state]
|
2021-06-09 16:37:37 +03:00
|
|
|
?~ next-nonce
|
|
|
|
~&([dap.bowl %no-nonce] [~ state])
|
|
|
|
=/ nonce=@ud u.next-nonce
|
2021-05-21 13:16:57 +03:00
|
|
|
=: pending ~
|
2021-06-11 13:27:05 +03:00
|
|
|
flush &
|
2021-06-09 16:37:37 +03:00
|
|
|
next-nonce `+(u.next-nonce)
|
2021-05-13 18:21:35 +03:00
|
|
|
::
|
|
|
|
sending
|
2021-06-01 17:18:32 +03:00
|
|
|
%+ ~(put by sending)
|
|
|
|
[get-address nonce]
|
2021-06-16 18:03:21 +03:00
|
|
|
[0 (turn pending (cork tail tail))]
|
2021-06-01 14:56:56 +03:00
|
|
|
::
|
|
|
|
finding
|
|
|
|
%- ~(gas by finding)
|
|
|
|
%+ turn pending
|
2021-06-16 18:03:21 +03:00
|
|
|
|= pend-tx
|
|
|
|
(hash-raw-tx raw-tx)^[address nonce]
|
|
|
|
::
|
|
|
|
history
|
|
|
|
%+ roll pending
|
|
|
|
|= [pend-tx hist=_history]
|
|
|
|
=/ tx=roller-tx
|
|
|
|
:+ [%pending ~]
|
|
|
|
(hash-raw-tx raw-tx)
|
|
|
|
(l2-tx +<.tx.raw-tx)
|
|
|
|
%+ ~(put ju (~(del ju hist) address tx))
|
|
|
|
address
|
|
|
|
tx(status [%sending ~])
|
2021-05-13 18:21:35 +03:00
|
|
|
==
|
2021-06-01 17:18:32 +03:00
|
|
|
[(send-roll get-address nonce) state]
|
2021-06-09 14:05:17 +03:00
|
|
|
=^ card next-batch set-timer
|
|
|
|
[[card cards] state]
|
2021-05-29 15:40:13 +03:00
|
|
|
:: +get-nonce: retrieves the latest nonce
|
|
|
|
::
|
|
|
|
++ get-nonce
|
2021-06-01 14:56:56 +03:00
|
|
|
|= pk=@
|
2021-05-29 15:40:13 +03:00
|
|
|
^- (list card)
|
2021-06-09 16:37:37 +03:00
|
|
|
?~ endpoint ~&([dap.bowl %no-endpoint] ~)
|
|
|
|
(start-thread:spider /nonce [%aggregator-nonce !>([u.endpoint pk])])
|
2021-05-29 15:40:13 +03:00
|
|
|
::
|
2021-05-13 18:21:35 +03:00
|
|
|
:: +send-roll: start thread to submit roll from :sending to l1
|
|
|
|
::
|
|
|
|
++ send-roll
|
2021-06-01 17:18:32 +03:00
|
|
|
|= [=address:ethereum nonce=@ud]
|
2021-05-13 18:21:35 +03:00
|
|
|
^- (list card)
|
|
|
|
:: if this nonce isn't in the sending queue anymore, it's done
|
|
|
|
::
|
2021-06-01 17:18:32 +03:00
|
|
|
?. (~(has by sending) [address nonce])
|
|
|
|
~? lverb [dap.bowl %done-sending [address nonce]]
|
2021-05-13 18:21:35 +03:00
|
|
|
~
|
|
|
|
:: start the thread, passing in the l2 txs to use
|
|
|
|
::
|
2021-06-09 16:37:37 +03:00
|
|
|
?~ endpoint ~&([dap.bowl %no-endpoint] ~)
|
2021-05-13 18:21:35 +03:00
|
|
|
::TODO should go ahead and set resend timer in case thread hangs, or nah?
|
|
|
|
%+ start-thread:spider
|
2021-06-03 10:26:13 +03:00
|
|
|
/send/(scot %ux address)/(scot %ud nonce)
|
2021-05-13 18:21:35 +03:00
|
|
|
:- %aggregator-send
|
2021-05-21 16:07:08 +03:00
|
|
|
!> ^- rpc-send-roll
|
2021-06-09 16:37:37 +03:00
|
|
|
:* u.endpoint
|
2021-05-17 18:48:49 +03:00
|
|
|
contract
|
|
|
|
chain-id
|
2021-05-21 13:16:57 +03:00
|
|
|
pk
|
2021-05-17 18:48:49 +03:00
|
|
|
nonce
|
2021-06-01 17:18:32 +03:00
|
|
|
(~(got by sending) [address nonce])
|
2021-05-17 18:48:49 +03:00
|
|
|
==
|
2021-05-29 15:40:13 +03:00
|
|
|
:: +on-batch-result: await resend after thread success or failure
|
2021-05-13 18:21:35 +03:00
|
|
|
::
|
2021-05-29 15:40:13 +03:00
|
|
|
++ on-batch-result
|
2021-06-01 17:18:32 +03:00
|
|
|
|= [=address:ethereum nonce=@ud result=(each @ud @t)]
|
2021-05-13 18:21:35 +03:00
|
|
|
^- (quip card _state)
|
|
|
|
:: update gas price for this tx in state
|
|
|
|
::
|
|
|
|
=? sending ?=(%& -.result)
|
2021-06-01 17:18:32 +03:00
|
|
|
%+ ~(jab by sending) [address nonce]
|
2021-05-13 18:21:35 +03:00
|
|
|
(cork tail (lead p.result))
|
|
|
|
:: print error if there was one
|
|
|
|
::
|
|
|
|
~? ?=(%| -.result) [dap.bowl %send-error p.result]
|
|
|
|
:: resend the l1 tx in five minutes
|
|
|
|
::
|
|
|
|
:_ state
|
2021-06-01 17:18:32 +03:00
|
|
|
:_ ~
|
|
|
|
%+ wait:b:sys
|
2021-06-03 10:26:13 +03:00
|
|
|
/resend/(scot %ux address)/(scot %ud nonce)
|
2021-06-01 17:18:32 +03:00
|
|
|
(add resend-time now.bowl)
|
2021-05-13 18:21:35 +03:00
|
|
|
:: +on-naive-diff: process l2 tx confirmations
|
|
|
|
::
|
|
|
|
++ on-naive-diff
|
|
|
|
|= =diff:naive
|
|
|
|
^- (quip card _state)
|
|
|
|
?. ?=(%tx -.diff)
|
|
|
|
[~ state]
|
|
|
|
=/ =keccak (hash-raw-tx raw-tx.diff)
|
|
|
|
?~ wer=(~(get by finding) keccak)
|
|
|
|
[~ state]
|
|
|
|
:: if we had already seen the tx, no-op
|
|
|
|
::
|
|
|
|
?@ u.wer
|
|
|
|
~? &(?=(%confirmed u.wer) ?=(~ err.diff))
|
2021-05-17 18:48:49 +03:00
|
|
|
[dap.bowl %weird-double-confirm from.tx.raw-tx.diff]
|
2021-05-13 18:21:35 +03:00
|
|
|
[~ state]
|
|
|
|
=* nonce nonce.u.wer
|
|
|
|
:: remove the tx from the sending map
|
|
|
|
::
|
|
|
|
=. sending
|
2021-06-01 17:18:32 +03:00
|
|
|
?~ sen=(~(get by sending) [get-address nonce])
|
2021-05-13 18:21:35 +03:00
|
|
|
~& [dap.bowl %weird-double-remove]
|
|
|
|
sending
|
|
|
|
?~ nin=(find [raw-tx.diff]~ txs.u.sen)
|
|
|
|
~& [dap.bowl %weird-unknown]
|
2021-05-17 18:48:49 +03:00
|
|
|
sending
|
2021-05-13 18:21:35 +03:00
|
|
|
=. txs.u.sen (oust [u.nin 1] txs.u.sen)
|
|
|
|
?~ txs.u.sen
|
2021-06-01 17:18:32 +03:00
|
|
|
~? lverb [dap.bowl %done-with-nonce [get-address nonce]]
|
|
|
|
(~(del by sending) [get-address nonce])
|
|
|
|
(~(put by sending) [get-address nonce] u.sen)
|
2021-05-13 18:21:35 +03:00
|
|
|
:: update the finding map with the new status
|
|
|
|
::
|
|
|
|
=. finding
|
|
|
|
%+ ~(put by finding) keccak
|
|
|
|
?~ err.diff %confirmed
|
|
|
|
:: if we kept the forced flag around for longer, we could notify of
|
|
|
|
:: unexpected tx failures here. would that be useful? probably not?
|
|
|
|
:: ~? !forced [dap.bowl %aggregated-tx-failed-anyway err.diff]
|
|
|
|
%failed
|
2021-06-21 12:27:36 +03:00
|
|
|
::
|
2021-06-16 18:03:21 +03:00
|
|
|
=. history
|
|
|
|
=/ tx=roller-tx
|
|
|
|
:+ [%sending ~]
|
|
|
|
keccak
|
|
|
|
(l2-tx +<.tx.raw-tx.diff)
|
|
|
|
=/ =address:ethereum
|
|
|
|
(get-l1-address tx.raw-tx.diff pre)
|
|
|
|
%+ ~(put ju (~(del ju history) address tx))
|
|
|
|
address
|
|
|
|
%_ tx
|
|
|
|
status ?~(err.diff [%confirmed ~] [%failed ~])
|
|
|
|
==
|
2021-06-11 13:27:05 +03:00
|
|
|
:_ state(flush ?:(flush | &))
|
|
|
|
?. flush ~
|
|
|
|
:: derive predicted state in 5m.
|
2021-06-01 14:56:56 +03:00
|
|
|
::
|
2021-06-11 13:27:05 +03:00
|
|
|
[(wait:b:sys /predict (add ~m5 now.bowl))]~
|
2021-05-13 18:21:35 +03:00
|
|
|
::
|
|
|
|
--
|