:: ethereum: utilities :: =, ethereum-types |% :: deriving and using ethereum keys :: ++ key |% ++ address-from-pub =, keccak:crypto |= pub=@ %+ end [3 20] %+ keccak-256 64 (rev 3 64 pub) :: ++ address-from-prv (cork pub-from-prv address-from-pub) :: ++ pub-from-prv =, secp256k1:secp:crypto |= prv=@ %- serialize-point (priv-to-pub prv) :: ++ sign-typed-transaction |= [tx=typed-transaction:rpc pk=@] ^- @ux =- (cat 3 - -.tx) ?- -.tx %0x0 (sign-transaction +.tx pk) %0x2 (sign-transaction-1559 +.tx pk) == :: ++ sign-transaction =, crypto |= [tx=transaction:rpc pk=@] |^ ^- @ux :: hash the raw transaction data =/ hash=@ %- keccak-256:keccak =+ dat=(encode chain-id.tx 0 0) =+ wid=(met 3 dat) [wid (rev 3 wid dat)] :: sign transaction hash with private key =+ (ecdsa-raw-sign:secp256k1:secp hash pk) :: complete transaction is raw data, with r and s :: taken from the signature, and v as per eip-155 (encode :(add (mul chain-id.tx 2) 35 v) r s) :: ++ encode |= [v=@ r=@ s=@] %+ encode:rlp %l tx(to b+20^to.tx, chain-id [v r s ~]) -- :: ++ sign-transaction-1559 =, crypto |= [tx=transaction-1559:rpc pk=@] |^ ^- @ux =; hash=@ =+ (ecdsa-raw-sign:secp256k1:secp hash pk) ::NOTE we retrieve y's parity from the v value (encode-1559 ~ (end 0 v) r s) :: hash the raw transaction data including leading 0x2 %- keccak-256:keccak =+ dat=(cat 3 (encode-1559 ~) 0x2) =+ wid=(met 3 dat) [wid (rev 3 wid dat)] :: ++ encode-1559 |= sig=(unit [y=@ r=@ s=@]) %+ encode:rlp %l =, tx :* chain-id nonce max-priority-gas-fee max-gas-fee gas b+20^to value data :: :- %l %+ turn ~(tap by access-list) |= [a=address b=(list @ux)] l+~[b+20^a l+(turn b |=(c=@ux b+32^c))] :: ?~ sig ~ ~[y r s]:u.sig == -- -- :: :: rlp en/decoding ::NOTE https://eth.wiki/en/fundamentals/rlp :: ++ rlp |% ::NOTE rlp encoding doesn't really care about leading zeroes, :: but because we need to disinguish between no-bytes zero :: and one-byte zero (and also empty list) we end up with :: this awful type... +$ item $@ @ $% [%l l=(list item)] [%b b=byts] == :: +encode-atoms: encode list of atoms as a %l of %b items :: ++ encode-atoms ::NOTE deprecated |= l=(list @) ^- @ (encode l+l) :: ++ encode |= in=item |^ ^- @ ?- in @ $(in [%b (met 3 in) in]) :: [%b *] ?: &(=(1 wid.b.in) (lte dat.b.in 0x7f)) dat.b.in =- (can 3 ~[b.in [(met 3 -) -]]) (encode-length wid.b.in 0x80) :: [%l *] :: we +can because b+1^0x0 encodes to 0x00 :: =/ l=(list byts) %+ turn l.in |= ni=item =+ (encode ni) [(max 1 (met 3 -)) -] %+ can 3 %- flop =- [(met 3 -)^- l] (encode-length (roll (turn l head) add) 0xc0) == :: ++ encode-length |= [len=@ off=@] ?: (lth len 56) (add len off) =- (cat 3 len -) :(add (met 3 len) off 55) -- :: +decode-atoms: decode expecting a %l of %b items, producing atoms within :: ++ decode-atoms |= dat=@ ^- (list @) =/ i=item (decode dat) ~| [%unexpected-data i] ?> ?=(%l -.i) %+ turn l.i |= i=item ~| [%unexpected-list i] ?> ?=(%b -.i) dat.b.i :: ++ decode |= dat=@ ^- item =/ bytes=(list @) (flop (rip 3 dat)) =? bytes ?=(~ bytes) ~[0] |^ item:decode-head :: ++ decode-head ^- [done=@ud =item] ?~ bytes ~| %rlp-unexpected-end !! =* byt i.bytes :: byte in 0x00-0x79 range encodes itself :: ?: (lte byt 0x79) :- 1 [%b 1^byt] :: byte in 0x80-0xb7 range encodes string length :: ?: (lte byt 0xb7) =+ len=(sub byt 0x80) :- +(len) :- %b len^(get-value 1 len) :: byte in 0xb8-0xbf range encodes string length length :: ?: (lte byt 0xbf) =+ led=(sub byt 0xb7) =+ len=(get-value 1 led) :- (add +(led) len) :- %b len^(get-value +(led) len) :: byte in 0xc0-f7 range encodes list length :: ?: (lte byt 0xf7) =+ len=(sub byt 0xc0) :- +(len) :- %l %. len decode-list(bytes (slag 1 `(list @)`bytes)) :: byte in 0xf8-ff range encodes list length length :: ?: (lte byt 0xff) =+ led=(sub byt 0xf7) =+ len=(get-value 1 led) :- (add +(led) len) :- %l %. len decode-list(bytes (slag +(led) `(list @)`bytes)) ~| [%rip-not-bloq-3 `@ux`byt] !! :: ++ decode-list |= rem=@ud ^- (list item) ?: =(0 rem) ~ =+ ^- [don=@ud =item] ::TODO =/ decode-head :- item %= $ rem (sub rem don) bytes (slag don bytes) == :: ++ get-value |= [at=@ud to=@ud] ^- @ (rep 3 (flop (swag [at to] bytes))) -- -- :: :: abi en/decoding ::NOTE https://solidity.readthedocs.io/en/develop/abi-spec.html :: ++ abi => |% :: solidity types. integer bitsizes ignored ++ etyp $@ $? :: static %address %bool %int %uint %real %ureal :: dynamic %bytes %string == $% :: static [%bytes-n n=@ud] :: dynamic [%array-n t=etyp n=@ud] [%array t=etyp] == :: :: solidity-style typed data. integer bitsizes ignored ++ data $% [%address p=address] [%string p=tape] [%bool p=?] [%int p=@sd] [%uint p=@ud] [%real p=@rs] [%ureal p=@urs] [%array-n p=(list data)] [%array p=(list data)] [%bytes-n p=octs] ::TODO just @, because context knows length? [%bytes p=octs] == -- =, mimes:html |% :: encoding :: ++ encode-args :: encode list of arguments. :: |= das=(list data) ^- tape (encode-data [%array-n das]) :: ++ encode-data :: encode typed data into ABI bytestring. :: |= dat=data ^- tape ?+ -.dat ~| [%unsupported-type -.dat] !! :: %array-n :: enc(X) = head(X[0]) ... head(X[k-1]) tail(X[0]) ... tail(X[k-1]) :: where head and tail are defined for X[i] being of a static type as :: head(X[i]) = enc(X[i]) and tail(X[i]) = "" (the empty string), or as :: head(X[i]) = enc(len( head(X[0])..head(X[k-1]) :: tail(X[0])..tail(X[i-1]) )) :: and tail(X[i]) = enc(X[i]) otherwise. :: :: so: if it's a static type, data goes in the head. if it's a dynamic :: type, a reference goes into the head and data goes into the tail. :: :: in the head, we first put a placeholder where references need to go. =+ hol=(reap 64 'x') =/ hes=(list tape) %+ turn p.dat |= d=data ?. (is-dynamic-type d) ^$(dat d) hol =/ tas=(list tape) %+ turn p.dat |= d=data ?. (is-dynamic-type d) "" ^$(dat d) :: once we know the head and tail, we can fill in the references in head. =- (weld nes `tape`(zing tas)) ^- [@ud nes=tape] =+ led=(lent (zing hes)) %+ roll hes |= [t=tape i=@ud nes=tape] :- +(i) :: if no reference needed, just put the data. ?. =(t hol) (weld nes t) :: calculate byte offset of data we need to reference. =/ ofs=@ud =- (div - 2) :: two hex digits per byte. %+ add led :: count head, and %- lent %- zing :: count all tail data (scag i tas) :: preceding ours. =+ ref=^$(dat [%uint ofs]) :: shouldn't hit this unless we're sending over 2gb of data? ~| [%weird-ref-lent (lent ref)] ?> =((lent ref) (lent hol)) (weld nes ref) :: %array :: where X has k elements (k is assumed to be of type uint256): :: enc(X) = enc(k) enc([X[1], ..., X[k]]) :: i.e. it is encoded as if it were an array of static size k, prefixed :: with the number of elements. %+ weld $(dat [%uint (lent p.dat)]) $(dat [%array-n p.dat]) :: %bytes-n :: enc(X) is the sequence of bytes in X padded with zero-bytes to a :: length of 32. :: Note that for any X, len(enc(X)) is a multiple of 32. ~| [%bytes-n-too-long max=32 actual=p.p.dat] ?> (lte p.p.dat 32) (pad-to-multiple (render-hex-bytes p.dat) 64 %right) :: %bytes :: of length k (which is assumed to be of type uint256) :: enc(X) = enc(k) pad_right(X), i.e. the number of bytes is encoded as a :: uint256 followed by the actual value of X as a byte sequence, followed :: by the minimum number of zero-bytes such that len(enc(X)) is a :: multiple of 32. %+ weld $(dat [%uint p.p.dat]) (pad-to-multiple (render-hex-bytes p.dat) 64 %right) :: %string :: enc(X) = enc(enc_utf8(X)), i.e. X is utf-8 encoded and this value is :: interpreted as of bytes type and encoded further. Note that the length :: used in this subsequent encoding is the number of bytes of the utf-8 :: encoded string, not its number of characters. $(dat [%bytes (lent p.dat) (swp 3 (crip p.dat))]) :: %uint :: enc(X) is the big-endian encoding of X, padded on the higher-order :: (left) side with zero-bytes such that the length is a multiple of 32 :: bytes. (pad-to-multiple (render-hex-bytes (as-octs p.dat)) 64 %left) :: %bool :: as in the uint8 case, where 1 is used for true and 0 for false $(dat [%uint ?:(p.dat 1 0)]) :: %address :: as in the uint160 case $(dat [%uint `@ud`p.dat]) == :: ++ is-dynamic-type |= a=data ?. ?=(%array-n -.a) ?=(?(%string %bytes %array) -.a) &(!=((lent p.a) 0) (lien p.a is-dynamic-type)) :: :: decoding :: ++ decode-topics decode-arguments :: ++ decode-results :: rex: string of hex bytes with leading 0x. |* [rex=@t tys=(list etyp)] =- (decode-arguments - tys) %^ rut 9 (rsh [3 2] rex) (curr rash hex) :: ++ decode-arguments |* [wos=(list @) tys=(list etyp)] =/ wos=(list @) wos :: get rid of tmi =| win=@ud =< (decode-from 0 tys) |% ++ decode-from |* [win=@ud tys=(list etyp)] ?~ tys !! =- ?~ t.tys dat [dat $(win nin, tys t.tys)] (decode-one win ~[i.tys]) :: ++ decode-one ::NOTE we take (list etyp) even though we only operate on :: a single etyp as a workaround for urbit/arvo#673 |* [win=@ud tys=(list etyp)] =- [nin dat]=- ::NOTE ^= regular form broken ?~ tys !! =* typ i.tys =+ wor=(snag win wos) ?+ typ ~| [%unsupported-type typ] !! :: ?(%address %bool %uint) :: %int %real %ureal :- +(win) ?- typ %address `@ux`wor %uint `@ud`wor %bool =(1 wor) == :: %string =+ $(tys ~[%bytes]) [nin (trip (swp 3 q.dat))] :: %bytes :- +(win) :: find the word index of the actual data. =/ lic=@ud (div wor 32) :: learn the bytelength of the data. =/ len=@ud (snag lic wos) (decode-bytes-n +(lic) len) :: [%bytes-n *] :- (add win +((div (dec n.typ) 32))) (decode-bytes-n win n.typ) :: [%array *] :- +(win) :: find the word index of the actual data. =. win (div wor 32) :: read the elements from their location. %- tail %^ decode-array-n ~[t.typ] +(win) (snag win wos) :: [%array-n *] (decode-array-n ~[t.typ] win n.typ) == :: ++ decode-bytes-n |= [fro=@ud bys=@ud] ^- octs :: parse {bys} bytes from {fro}. :- bys %+ rsh :- 3 =+ (mod bys 32) ?:(=(0 -) - (sub 32 -)) %+ rep 8 %- flop =- (swag [fro -] wos) +((div (dec bys) 32)) :: ++ decode-array-n ::NOTE we take (list etyp) even though we only operate on :: a single etyp as a workaround for urbit/arvo#673 ::NOTE careful! produces lists without type info =| res=(list) |* [tys=(list etyp) fro=@ud len=@ud] ^- [@ud (list)] ?~ tys !! ?: =(len 0) [fro (flop `(list)`res)] =+ (decode-one fro ~[i.tys]) :: [nin=@ud dat=*] $(res ^+(res [dat res]), fro nin, len (dec len)) -- -- :: :: communicating with rpc nodes ::NOTE https://github.com/ethereum/wiki/wiki/JSON-RPC :: ++ rpc :: types :: => =, abi =, format |% :: raw call data ++ call-data $: function=@t arguments=(list data) == :: :: raw transaction data +$ typed-transaction $% [%0x0 transaction] [%0x2 transaction-1559] == :: +$ transaction $: nonce=@ud gas-price=@ud gas=@ud to=address value=@ud data=@ux chain-id=@ux == :: +$ transaction-1559 $: chain-id=@ux nonce=@ud max-priority-gas-fee=@ud max-gas-fee=@ud gas=@ud to=address value=@ud data=@ux access-list=(jar address @ux) == :: :: ethereum json rpc api :: :: supported requests. ++ request $% [%eth-block-number ~] [%eth-call cal=call deb=block] $: %eth-new-filter fro=(unit block) tob=(unit block) adr=(list address) top=(list ?(@ux (list @ux))) == [%eth-get-block-by-number bon=@ud txs=?] [%eth-get-filter-logs fid=@ud] $: %eth-get-logs fro=(unit block) tob=(unit block) adr=(list address) top=(list ?(@ux (list @ux))) == $: %eth-get-logs-by-hash has=@ adr=(list address) top=(list ?(@ux (list @ux))) == [%eth-get-filter-changes fid=@ud] [%eth-get-transaction-by-hash txh=@ux] [%eth-get-transaction-count adr=address =block] [%eth-get-balance adr=address =block] [%eth-get-transaction-receipt txh=@ux] [%eth-send-raw-transaction dat=@ux] == :: ::TODO clean up & actually use ++ response $% ::TODO [%eth-new-filter fid=@ud] [%eth-get-filter-logs los=(list event-log)] [%eth-get-logs los=(list event-log)] [%eth-get-logs-by-hash los=(list event-log)] [%eth-got-filter-changes los=(list event-log)] [%eth-transaction-hash haz=@ux] == :: ++ transaction-result $: block-hash=(unit @ux) block-number=(unit @ud) transaction-index=(unit @ud) from=@ux to=(unit @ux) input=@t == :: ++ event-log $: :: null for pending logs $= mined %- unit $: input=(unit @ux) log-index=@ud transaction-index=@ud transaction-hash=@ux block-number=@ud block-hash=@ux removed=? == :: address=@ux data=@t :: event data :: :: For standard events, the first topic is the event signature :: hash. For anonymous events, the first topic is the first :: indexed argument. :: Note that this does not support the "anonymous event with :: zero topics" case. This has dubious usability, and using :: +lest instead of +list saves a lot of ?~ checks. :: topics=(lest @ux) == :: :: data for eth_call. ++ call $: from=(unit address) to=address gas=(unit @ud) gas-price=(unit @ud) value=(unit @ud) data=tape == :: :: minimum data needed to construct a read call ++ proto-read-request $: id=(unit @t) to=address call-data == :: :: block to operate on. ++ block $% [%number n=@ud] [%label l=?(%earliest %latest %pending)] == -- :: :: logic :: |% ++ encode-call |= call-data ^- tape ::TODO should this check to see if the data matches the function signature? =- :(weld "0x" - (encode-args arguments)) %+ scag 8 %+ render-hex-bytes 32 %- keccak-256:keccak:crypto (as-octs:mimes:html function) :: :: building requests :: ++ json-request =, eyre |= [url=purl jon=json] ^- hiss :^ url %post %- ~(gas in *math) ~['Content-Type'^['application/json']~] (some (as-octs (en:json:html jon))) :: +light-json-request: like json-request, but for %l :: :: TODO: Exorcising +purl from our system is a much longer term effort; :: get the current output types for now. :: ++ light-json-request |= [url=purl:eyre jon=json] ^- request:http :: :* %'POST' (crip (en-purl:html url)) ~[['content-type' 'application/json']] (some (as-octs (en:json:html jon))) == :: ++ batch-read-request |= req=(list proto-read-request) ^- json a+(turn req read-request) :: ++ read-request |= proto-read-request ^- json %+ request-to-json id :+ %eth-call ^- call [~ to ~ ~ ~ `tape`(encode-call function arguments)] [%label %latest] :: ++ request-to-json =, enjs:format |= [riq=(unit @t) req=request] ^- json %- pairs =; r=[met=@t pas=(list json)] ::TODO should use request-to-json:rpc:jstd, :: and probably (fall riq -.req) :* jsonrpc+s+'2.0' method+s+met.r params+a+pas.r ::TODO would just jamming the req noun for id be a bad idea? ?~ riq ~ [id+s+u.riq]~ == ?- -.req %eth-block-number ['eth_blockNumber' ~] :: %eth-call :- 'eth_call' :~ (eth-call-to-json cal.req) (block-to-json deb.req) == :: %eth-new-filter :- 'eth_newFilter' :_ ~ :- %o %- ~(gas by *(map @t json)) =- (murn - same) ^- (list (unit (pair @t json))) :~ ?~ fro.req ~ `['fromBlock' (block-to-json u.fro.req)] :: ?~ tob.req ~ `['toBlock' (block-to-json u.tob.req)] :: ::NOTE tmi ?: =(0 (lent adr.req)) ~ :+ ~ 'address' ?: =(1 (lent adr.req)) (tape (address-to-hex (snag 0 adr.req))) :- %a (turn adr.req (cork address-to-hex tape)) :: ?~ top.req ~ :+ ~ 'topics' (topics-to-json top.req) == :: %eth-get-block-by-number :- 'eth_getBlockByNumber' :~ (tape (num-to-hex-minimal bon.req)) b+txs.req == :: %eth-get-filter-logs ['eth_getFilterLogs' (tape (num-to-hex fid.req)) ~] :: %eth-get-logs :- 'eth_getLogs' :_ ~ :- %o %- ~(gas by *(map @t json)) =- (murn - same) ^- (list (unit (pair @t json))) :~ ?~ fro.req ~ `['fromBlock' (block-to-json u.fro.req)] :: ?~ tob.req ~ `['toBlock' (block-to-json u.tob.req)] :: ?: =(0 (lent adr.req)) ~ :+ ~ 'address' ?: =(1 (lent adr.req)) (tape (address-to-hex (snag 0 adr.req))) :- %a (turn adr.req (cork address-to-hex tape)) :: ?~ top.req ~ :+ ~ 'topics' (topics-to-json top.req) == :: %eth-get-logs-by-hash :- 'eth_getLogs' :_ ~ :- %o %- ~(gas by *(map @t json)) =- (murn - same) ^- (list (unit (pair @t json))) :~ `['blockHash' (tape (transaction-to-hex has.req))] :: ?: =(0 (lent adr.req)) ~ :+ ~ 'address' ?: =(1 (lent adr.req)) (tape (address-to-hex (snag 0 adr.req))) :- %a (turn adr.req (cork address-to-hex tape)) :: ?~ top.req ~ :+ ~ 'topics' (topics-to-json top.req) == :: %eth-get-filter-changes ['eth_getFilterChanges' (tape (num-to-hex fid.req)) ~] :: %eth-get-transaction-count :- 'eth_getTransactionCount' :~ (tape (address-to-hex adr.req)) (block-to-json block.req) == :: %eth-get-balance :- 'eth_getBalance' :~ (tape (address-to-hex adr.req)) (block-to-json block.req) == :: %eth-get-transaction-by-hash ['eth_getTransactionByHash' (tape (transaction-to-hex txh.req)) ~] :: %eth-get-transaction-receipt ['eth_getTransactionReceipt' (tape (transaction-to-hex txh.req)) ~] :: %eth-send-raw-transaction ['eth_sendRawTransaction' (tape (num-to-hex dat.req)) ~] == :: ++ eth-call-to-json =, enjs:format |= cal=call ^- json :- %o %- ~(gas by *(map @t json)) =- (murn - same) ^- (list (unit (pair @t json))) :~ ?~ from.cal ~ `['from' (tape (address-to-hex u.from.cal))] :: `['to' (tape (address-to-hex to.cal))] :: ?~ gas.cal ~ `['gas' (tape (num-to-hex u.gas.cal))] :: ?~ gas-price.cal ~ `['gasPrice' (tape (num-to-hex u.gas-price.cal))] :: ?~ value.cal ~ `['value' (tape (num-to-hex u.value.cal))] :: ?~ data.cal ~ `['data' (tape data.cal)] == :: ++ block-to-json |= dob=block ^- json ?- -.dob %number s+(crip '0' 'x' ((x-co:co 1) n.dob)) %label s+l.dob == :: ++ topics-to-json |= tos=(list ?(@ux (list @ux))) ^- json :- %a =/ ttj ;: cork (cury render-hex-bytes 32) prefix-hex tape:enjs:format == %+ turn tos |= t=?(@ (list @)) ?@ t ?: =(0 t) ~ (ttj `@`t) a+(turn t ttj) :: :: parsing responses :: ::TODO ++ parse-response |= json ^- response :: ++ parse-hex-result |= j=json ^- @ ?> ?=(%s -.j) (hex-to-num p.j) :: ++ parse-eth-new-filter-res parse-hex-result :: ++ parse-eth-block-number parse-hex-result :: ++ parse-transaction-hash parse-hex-result :: ++ parse-eth-get-transaction-count parse-hex-result :: ++ parse-eth-get-balance parse-hex-result :: ++ parse-event-logs (ar:dejs:format parse-event-log) :: ++ parse-event-log =, dejs:format |= log=json ^- event-log =- ((ot -) log) :~ =- ['logIndex'^(cu - (mu so))] |= li=(unit @t) ?~ li ~ =- ``((ou -) log) ::TODO not sure if elegant or hacky. :~ 'logIndex'^(un (cu hex-to-num so)) 'transactionIndex'^(un (cu hex-to-num so)) 'transactionHash'^(un (cu hex-to-num so)) 'blockNumber'^(un (cu hex-to-num so)) 'blockHash'^(un (cu hex-to-num so)) 'removed'^(uf | bo) == :: address+(cu hex-to-num so) data+so :: =- topics+(cu - (ar so)) |= r=(list @t) ^- (lest @ux) ?> ?=([@t *] r) :- (hex-to-num i.r) (turn t.r hex-to-num) == :: ++ parse-transaction-result =, dejs:format |= jon=json ~| jon=jon ^- transaction-result =- ((ot -) jon) :~ 'blockHash'^_~ :: TODO: fails if maybe-num? 'blockNumber'^maybe-num 'transactionIndex'^maybe-num from+(cu hex-to-num so) to+maybe-num input+so == :: ++ maybe-num =, dejs:format =- (cu - (mu so)) |= r=(unit @t) ?~ r ~ `(hex-to-num u.r) -- :: :: utilities ::TODO give them better homes! :: ++ num-to-hex |= n=@ ^- tape %- prefix-hex ?: =(0 n) "0" %- render-hex-bytes (as-octs:mimes:html n) :: ++ num-to-hex-minimal |= n=@ ^- tape %- prefix-hex ((x-co:co 1) n) :: ++ address-to-hex |= a=address ^- tape %- prefix-hex (render-hex-bytes 20 `@`a) :: ++ address-to-checksum |= a=address ^- tape =/ hexed (render-hex-bytes 20 `@`a) =/ hash (keccak-256:keccak:crypto (as-octs:mimes:html (crip hexed))) =| ret=tape =/ pos 63 |- ?~ hexed (prefix-hex (flop ret)) =/ char i.hexed ?: (lth char 58) $(pos (dec pos), ret [char ret], hexed t.hexed) =/ nib (cut 2 [pos 1] hash) ?: (lth 7 nib) $(pos (dec pos), ret [(sub char 32) ret], hexed t.hexed) $(pos (dec pos), ret [char ret], hexed t.hexed) :: ++ transaction-to-hex |= h=@ ^- tape %- prefix-hex (render-hex-bytes 32 h) :: ++ prefix-hex |= a=tape ^- tape ['0' 'x' a] :: ++ render-hex-bytes :: atom to string of hex bytes without 0x prefix and dots. |= a=octs ^- tape ((x-co:co (mul 2 p.a)) q.a) :: ++ pad-to-multiple |= [wat=tape mof=@ud wer=?(%left %right)] ^- tape =+ len=(lent wat) ?: =(0 len) (reap mof '0') =+ mad=(mod len mof) ?: =(0 mad) wat =+ tad=(reap (sub mof mad) '0') %- weld ?:(?=(%left wer) [tad wat] [wat tad]) :: ++ hex-to-num |= a=@t ~| %non-hex-cord ?> =((end [3 2] a) '0x') =< ?<(=(0 p) q) %- need (de:base16:mimes:html (rsh [3 2] a)) --