Skip to content

Instantly share code, notes, and snippets.

@jeroenvandijk
Last active December 3, 2025 16:27
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)]
;; npm init -y
;; npm install squint-cljs@latest
;; 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->string [bytes]
#?(:clj (String. (byte-array bytes) "UTF-8")
:cljs (apply js/String.fromCharCode bytes)))
(def integer-tag 0x10)
(def big-integer-tag 0x19)
(def double-tag 0x1d)
(defn read-value
([[tag & encoding]]
(read-value 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-dense-record [_tag encoding]
(let [n (first encoding)]
(first (reduce (fn [[acc [t & enc]] _]
(let [[v enc0] (read-value t enc)]
[(conj acc v) enc0]))
[[]
(rest encoding)]
(range 0 n)))))
(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 encoding]
(case tag
0xEA
(let [[addr enc] (read-vlq-count (first encoding) (rest encoding))]
[{:address addr} enc])))
(read-data-structure [tag encoding]
(case tag
0x81 ;; List
(let [n (first encoding)]
(reduce (fn [[acc [t & enc]] _]
(let [[v enc0] (read-value t enc)]
[(conj acc v) enc0]))
[()
(rest encoding)]
(range 0 n)))))
(read-basic-object [tag encoding]
(case tag
0x30 (throw (ex-info "TODO String" {}))
0x32 (let [len (bit-and 0xff (first encoding))]
[(#?(:clj symbol :cljs identity) (bytes->string (take len (drop 1 encoding))))
(drop (inc len) 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) (throw (ex-info ":read-big-integer" {}))
(== tag double-tag) (throw (ex-info ":read-double" {}))))]
(case (bit-shift-right tag 4)
1 (read-numeric tag encoding)
3 (read-basic-object 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 "0x%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?
:-)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment