Skip to content

Instantly share code, notes, and snippets.

@jeroenvandijk
Last active December 10, 2025 12:42
Show Gist options
  • Select an option

  • Save jeroenvandijk/fc693447e645a0c8672a8ad5eacae448 to your computer and use it in GitHub Desktop.

Select an option

Save jeroenvandijk/fc693447e645a0c8672a8ad5eacae448 to your computer and use it in GitHub Desktop.
Partial CAD03 decoding in Clojure and Javascript (via Squint)
(ns dev.jeroenvandijk.convex.encoding
(:require [clojure.string :as str]))
;; Run with clojure or squint (cljs)
;; clj encoding.cljc
;; => [{:address 12} 51 (transfer {:address 13} 1000)]
;; npx squint run encoding.cljc
;; => [{:address 12} 51 ("transfer", {:address 13}, 1000)]
(defn bytes->long [bytes]
#?(:clj (.longValue (BigInteger. (byte-array bytes)))
:cljs (reduce (fn [acc b] (+ (* acc 256) b)) bytes)))
(defn bytes->double [bytes]
#?(:clj (.getDouble (java.nio.ByteBuffer/wrap (byte-array bytes)))
:cljs (throw (ex-info "todo double"))))
(defn bytes->bigint [bytes]
;; FIXME inccorect
(BigInteger. (byte-array bytes)))
(defn bytes->string [bytes]
#?(:clj (String. (byte-array bytes) "UTF-8")
:cljs (apply js/String.fromCharCode bytes)))
(defmacro case'
"case variant that interpolates symbols"
[value & branches]
(let [branches' (cond-> (mapcat (fn [[sym value]]
(letfn [(eval-sym [s]
(if (symbol? s)
(eval s)
s))]
[(if (seq? sym)
(map eval-sym sym)
(eval-sym sym))
value]))
(partition 2 branches))
(odd? (count branches))
(concat [(last branches)]))]
`(case ~value ~@branches')))
(comment
(case' invoke-tag
invoke-tag :invoke)
(case' invoke-tag
(invoke-tag 2) :invoke)
)
(def invoke-tag 0xD0)
(def transfer-tag 0xD1)
(def call-tag 0xD2)
(def address-tag 0xEA)
(def list-tag 0x81)
(def map-tag 0x82)
(def string-tag 0x30)
(def symbol-tag 0x32)
(def keyword-tag 0x33)
(def integer-tag 0x10)
(def big-integer-tag 0x19)
(def double-tag 0x1d)
(defn read-value
([[tag & encoding]]
(first (read-value tag encoding)))
([tag encoding]
[tag encoding]
;; Mostly ported from https://github.com/Convex-Dev/convex/blob/develop/convex-core/src/main/java/convex/core/data/CAD3Encoder.java#L39
(letfn [(read-sequential
([encoding]
(read-sequential [] encoding))
([init [n & t]]
(reduce (fn [[acc [t0 & enc]] _]
(let [[v enc0] (read-value t0 enc)]
[(conj acc v) enc0]))
[init t]
(range n))))
(read-dense-record' [columns encoding]
(let [[coll encoding0] (read-sequential [] encoding)]
[(zipmap columns coll)
encoding0]))
(read-dense-record [tag encoding]
(case' tag
invoke-tag (read-dense-record'
[:origin :sequence :command]
encoding)
transfer-tag (read-dense-record'
[:origin :sequence :target :amount]
encoding)
call-tag (read-dense-record'
[:origin :sequence :target :offer :call :args]
encoding)))
(vlq-continues-from? [octet]
(not (zero? (bit-and (bit-and octet 0xFF) 0x80))))
(read-vlq-count [octet encoding]
(let [init-result (bit-and octet 0x7f)]
(loop [[octet & tail :as enc0] encoding
result init-result
bits 7]
(if (vlq-continues-from? octet)
;; continue while high bit of byte set
(recur tail
(bit-or (bit-shift-left result 7)
(bit-and octet 0x7f))
(+ bits 7))
[result enc0]))))
(read-extension [tag [h & t]]
(case' tag
address-tag
(let [[addr enc] (read-vlq-count h t)]
[{:address addr} enc])))
(read-map [[n & t]]
(reduce
(fn [[acc [t & enc]] _]
(let [[k [t0 & enc0]] (read-value t enc)
[v enc1] (read-value t0 enc0)]
[(assoc acc k v) enc1]))
;; REVIEW sorted-map for consistent encodings?
[{} t] (range n)))
(read-data-structure [tag encoding]
(case' tag
list-tag (read-sequential '() encoding)
map-tag (read-map encoding)
; (throw (ex-info "TODO map" {:encoding encoding}))
#_(read-sequential '() encoding)))
(read-string-encoding [f encoding]
(let [len (bit-and 0xff (first encoding))]
[(f (bytes->string (take len (drop 1 encoding))))
(drop (inc len) encoding)]))
(read-basic-object [tag encoding]
(case' tag
string-tag (read-string-encoding identity encoding)
keyword-tag (read-string-encoding keyword encoding)
symbol-tag (read-string-encoding symbol encoding)))
(read-numeric [tag encoding]
(cond
(< tag big-integer-tag) (let [nbytes (- tag integer-tag)]
[(bytes->long (take nbytes encoding))
(drop nbytes encoding)])
(== tag big-integer-tag) (let [nbytes (- tag integer-tag)]
[(bytes->bigint (take nbytes encoding))
(drop nbytes encoding)])
(== tag double-tag) (let [nbytes 8]
[(bytes->double (take nbytes encoding))
(drop nbytes encoding)])))]
(case (bit-shift-right tag 4)
1 (read-numeric tag encoding)
3 (read-basic-object tag encoding)
(4 5 6 7) ;; RESERVED
(throw (ex-info "reserved" {}))
; (read-vlq-count tag encoding)
8 (read-data-structure tag encoding)
13 (read-dense-record tag encoding)
14 (read-extension tag encoding)))))
(defn hex->bytes
"Convert a hex string into a Java byte[]."
[hex]
#?(:clj (let [hex (str/replace hex #"^0x" "") ; optional
len (/ (count hex) 2)
out (byte-array len)]
(doseq [i (range len)]
(let [byte-str (.substring hex (* 2 i) (+ (* 2 i) 2))
b (unchecked-byte (Integer/parseInt byte-str 16))]
(aset out i b)))
out)
:cljs (let [len (/ (count hex) 2)]
(mapv (fn [i]
(js/parseInt (.substr hex (* i 2) 2) 16))
(range 0 len)))))
(defn bytes->u8-vec
"Convert a Java byte[] to a vector of 0–255 ints."
[^bytes ba]
(mapv #(bit-and % 0xFF) ba))
(defn byte->hex [b]
(format "0%x" b))
(do #_comment
(def hex-hash "d003ea0c113381031203e8ea0d32087472616e73666572")
(def js-bytes
(->> (hex->bytes hex-hash)
bytes->u8-vec))
(prn (read-value js-bytes)) ;=> [{:address 12} 51 (transfer {:address 13} 1000)]
;; TODO collect more encodings, maybe from CVM code?
:-)
(read-value js-bytes) ;=> {:origin {:address 12}, :sequence 51, :command (transfer {:address 13} 1000)}
(read-value (first js-bytes) (rest js-bytes))
(defn read-hex [hex]
(read-value (->> (hex->bytes hex)
bytes->u8-vec)))
(byte->hex 130)
(read-hex "8202330761646472657373110233066f726967696e1101")
(read-hex "820233076164647265737330013233066f726967696e1101")
(read-hex "820333046261727281023203666f6f320571756f746533076164647265737330013233066f726967696e1101")
(read-hex "82033304626172723203666f6f33076164647265737330013233066f726967696e1101")
(read-hex "82033304626172721d3fcdf3b645a1cac133076164647265737330013233066f726967696e1101")
(read-hex "82043304626172721d3fcdf3b645a1cac133076164647265737330013233066f726967696e1101330862696767676767671915446c3b15f9926687d2c40534fdb564000000000000")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment