CAD03 is the binary data format underlying the lattice of Convex. This a first version of a partial decoder in Clojure and Javascript (via Squint)
More information on the format here https://docs.convex.world/docs/cad/encoding
CAD03 is the binary data format underlying the lattice of Convex. This a first version of a partial decoder in Clojure and Javascript (via Squint)
More information on the format here https://docs.convex.world/docs/cad/encoding
| (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") |