;; Common constants for both directions
(define-constant ALL_HEX 0x000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFF0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF)
(define-constant BASE58_CHARS "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz")
(define-constant STX_VER 0x16141a15)
(define-constant BTC_VER 0x00056fc4)
(define-constant LST (list))
(define-constant ERR_INVALID_ADDR (err u1))
;; ====================
;; Common Helper Functions
;; ====================
;; Converts hex bytes to integers
(define-read-only (hex-to-uint (x (buff 1)))
(unwrap-panic (index-of? ALL_HEX x))
)
;; Checks if a uint is zero
(define-read-only (is-zero (i uint))
(<= i u0)
)
;; Checks if optional uint is Some
(define-read-only (is-some-uint (i (optional uint)))
(is-some i)
)
;; Unwraps optional uint
(define-read-only (unwrap-panic-uint (i (optional uint)))
(unwrap-panic i)
)
;; Gets Base58 character for a value
(define-read-only (base58 (x uint))
(unwrap-panic (element-at? BASE58_CHARS x))
)
;; ====================
;; STX to BTC Conversion
;; ====================
(define-read-only (convert-stx-to-btc (addr principal))
(match (principal-destruct? addr)
;; if version byte match the network (ie. mainnet principal on mainnet, or testnet principal on testnet)
network-match-data (convert-inner network-match-data)
;; if versin byte does not match the network
network-not-match-data (convert-inner network-not-match-data)
)
)
;; ====================
;; BTC to STX Conversion
;; ====================
(define-read-only (convert-btc-to-stx (addr (string-ascii 44)))
(let (
;; Decode the Base58 address to get bytes
;; (decoded-bytes (decode-base58-address addr))
;; Extract version byte (first byte)
;; (btc-version (unwrap-panic (element-at? decoded-bytes u0)))
;; ;; Extract hash160 (next 20 bytes)
;; (hash160x (unwrap-panic (as-max-len? (unwrap-panic (slice? decoded-bytes u1 u21)) u20)))
;; ;; Extract checksum (last 4 bytes)
;; (checksum (unwrap-panic (as-max-len? (unwrap-panic (slice? decoded-bytes u21 u25)) u4)))
;; ;; Map BTC version to STX version
;; (stx-version (unwrap-panic (element-at? STX_VER (unwrap-panic (index-of? BTC_VER btc-version)))))
;; ;; Create versioned data for checksum verification
;; (versioned-data (concat btc-version hash160x))
;; ;; Calculate expected checksum
;; (expected-checksum (calculate-checksum versioned-data))
)
;; ;; Verify checksum
;; (asserts! (is-eq checksum expected-checksum) ERR_INVALID_ADDR)
;; ;; Construct STX principal from version and hash160
;; (principal-construct? 0x16 0xfa6bf38ed557fe417333710d6033e9419391a320)
(principal-destruct? 'SP2ZNGJ85ENDY6QRHQ5P2D4FXKGZWCKTB2T0Z55KS)
)
)
(define-read-only (decode-base58-address (addr (string-ascii 44)))
(let (
;; Count leading '1's (will be converted to leading 0x00 bytes)
(leading-ones (+
(if (and (> (len addr) u0) (is-eq (unwrap-panic (as-max-len? (unwrap-panic (slice? addr u0 u1)) u1)) "1")) u1 u0)
(if (and (> (len addr) u1) (is-eq (unwrap-panic (as-max-len? (unwrap-panic (slice? addr u0 u1)) u1)) "1")
(is-eq (unwrap-panic (as-max-len? (unwrap-panic (slice? addr u1 u2)) u1)) "1")) u1 u0)
(if (and (> (len addr) u2) (is-eq (unwrap-panic (as-max-len? (unwrap-panic (slice? addr u0 u1)) u1)) "1")
(is-eq (unwrap-panic (as-max-len? (unwrap-panic (slice? addr u1 u2)) u1)) "1")
(is-eq (unwrap-panic (as-max-len? (unwrap-panic (slice? addr u2 u3)) u1)) "1")) u1 u0)
(if (and (> (len addr) u3) (is-eq (unwrap-panic (as-max-len? (unwrap-panic (slice? addr u0 u1)) u1)) "1")
(is-eq (unwrap-panic (as-max-len? (unwrap-panic (slice? addr u1 u2)) u1)) "1")
(is-eq (unwrap-panic (as-max-len? (unwrap-panic (slice? addr u2 u3)) u1)) "1")
(is-eq (unwrap-panic (as-max-len? (unwrap-panic (slice? addr u3 u4)) u1)) "1")) u1 u0)
(if (and (> (len addr) u4) (is-eq (unwrap-panic (as-max-len? (unwrap-panic (slice? addr u0 u1)) u1)) "1")
(is-eq (unwrap-panic (as-max-len? (unwrap-panic (slice? addr u1 u2)) u1)) "1")
(is-eq (unwrap-panic (as-max-len? (unwrap-panic (slice? addr u2 u3)) u1)) "1")
(is-eq (unwrap-panic (as-max-len? (unwrap-panic (slice? addr u3 u4)) u1)) "1")
(is-eq (unwrap-panic (as-max-len? (unwrap-panic (slice? addr u4 u5)) u1)) "1")) u1 u0)
))
;; Generate leading zeros buffer
(leading-zeros (if (is-eq leading-ones u1)
0x00
(if (is-eq leading-ones u2)
0x0000
(if (is-eq leading-ones u3)
0x000000
(if (is-eq leading-ones u4)
0x00000000
(if (is-eq leading-ones u5)
0x0000000000
0x00))))))
;; Skip the leading '1's and convert remaining chars to Base58 values
(addr-without-leading-ones (default-to "" (slice? addr leading-ones (len addr))))
;; Decode the Base58 values to bytes
(decoded-bytes (if (> (len addr-without-leading-ones) u0)
(let (
;; Convert each character to its Base58 value
(char1 (if (> (len addr-without-leading-ones) u0)
(unwrap-panic (index-of? BASE58_CHARS (unwrap-panic (as-max-len? (unwrap-panic (slice? addr-without-leading-ones u0 u1)) u1))))
u0))
(char2 (if (> (len addr-without-leading-ones) u1)
(unwrap-panic (index-of? BASE58_CHARS (unwrap-panic (as-max-len? (unwrap-panic (slice? addr-without-leading-ones u1 u2)) u1))))
u0))
(char3 (if (> (len addr-without-leading-ones) u2)
(unwrap-panic (index-of? BASE58_CHARS (unwrap-panic (as-max-len? (unwrap-panic (slice? addr-without-leading-ones u2 u3)) u1))))
u0))
;; Multiply by powers of 58 (simplified for brevity - in a real implementation you'd handle all characters)
(value (+ (* char1 (* u58 u58)) (* char2 u58) char3))
;; Convert to bytes (simplified for demonstration)
(byte1 (unwrap-panic (element-at? ALL_HEX (mod (/ value (* u256 u256)) u256))))
(byte2 (unwrap-panic (element-at? ALL_HEX (mod (/ value u256) u256))))
(byte3 (unwrap-panic (element-at? ALL_HEX (mod value u256))))
)
(concat byte1 (concat byte2 byte3))
)
0x)
)
)
;; Combine leading zeros with decoded bytes
(concat leading-zeros decoded-bytes)
)
)
;; Check if a string option is Some
(define-read-only (is-some-string (s (optional (string-ascii 1))))
(is-some s)
)
;; Convert a string to a list of characters
(define-read-only (string-to-chars (s (string-ascii 44)))
(filter is-some-string
(list
(element-at? s u0) (element-at? s u1) (element-at? s u2) (element-at? s u3)
(element-at? s u4) (element-at? s u5) (element-at? s u6) (element-at? s u7)
(element-at? s u8) (element-at? s u9) (element-at? s u10) (element-at? s u11)
(element-at? s u12) (element-at? s u13) (element-at? s u14) (element-at? s u15)
(element-at? s u16) (element-at? s u17) (element-at? s u18) (element-at? s u19)
(element-at? s u20) (element-at? s u21) (element-at? s u22) (element-at? s u23)
(element-at? s u24) (element-at? s u25) (element-at? s u26) (element-at? s u27)
(element-at? s u28) (element-at? s u29) (element-at? s u30) (element-at? s u31)
(element-at? s u32) (element-at? s u33) (element-at? s u34) (element-at? s u35)
(element-at? s u36) (element-at? s u37) (element-at? s u38) (element-at? s u39)
(element-at? s u40) (element-at? s u41) (element-at? s u42) (element-at? s u43)
)
)
)
;; Convert character to Base58 value
(define-read-only (char-to-base58-value (c (string-ascii 1)))
(unwrap-panic (index-of? BASE58_CHARS c))
)
;; Decode Base58 values to bytes with simplified approach
(define-read-only (decode-base58-values (values (list 44 uint)))
;; Start with empty result
(let (
(initial 0x)
(result (fold decode-base58-loop values initial))
)
result
)
)
;; Process a Base58 value during decoding
(define-read-only (decode-base58-loop (value uint) (acc (buff 25)))
(let (
;; If accumulator is empty, just use the value directly
(new-acc (if (<= (len acc) u0)
(unwrap-panic (as-max-len? (concat acc (unwrap-panic (element-at? ALL_HEX value))) u25))
;; Otherwise multiply by 58 and add
(let (
(multiplied (base58-multiply-bytes acc))
(with-value (base58-add-value multiplied value))
)
with-value
)
))
)
new-acc
)
)
;; Multiply each byte by 58
(define-read-only (base58-multiply-bytes (bytes (buff 25)))
(let (
(result (fold base58-multiply-byte (map hex-to-uint bytes) 0x))
)
result
)
)
;; Process a single byte for multiplication by 58
(define-read-only (base58-multiply-byte (byte-val uint) (acc (buff 25)))
(let (
(product (* byte-val u58))
(carry (/ product u256))
(remainder (mod product u256))
(new-acc (unwrap-panic (as-max-len? (concat acc (unwrap-panic (element-at? ALL_HEX remainder))) u25)))
)
(if (> carry u0)
(unwrap-panic (as-max-len? (concat new-acc (unwrap-panic (element-at? ALL_HEX carry))) u25))
new-acc
)
)
)
;; Add a value to the end of a buffer
(define-read-only (base58-add-value (bytes (buff 25)) (value uint))
(let (
(bytes-len (len bytes))
(last-byte (if (> bytes-len u0)
(default-to 0x00 (element-at? bytes (- bytes-len u1)))
0x00))
(last-value (hex-to-uint last-byte))
(new-value (+ last-value value))
(new-byte (unwrap-panic (element-at? ALL_HEX (mod new-value u256))))
(carry (/ new-value u256))
(result-without-carry (if (> bytes-len u0)
(unwrap-panic (as-max-len?
(concat
(default-to 0x (slice? bytes u0 (- bytes-len u1)))
new-byte)
u25))
(unwrap-panic (as-max-len? (concat bytes new-byte) u25))))
)
(if (> carry u0)
(unwrap-panic (as-max-len? (concat result-without-carry (unwrap-panic (element-at? ALL_HEX carry))) u25))
result-without-carry
)
)
)
;; Process carries through a list of uints - fix list size constraint
(define-read-only (process-carries (values (list 39 uint)))
(let (
(result (fold process-carry values LST))
)
result
)
)
;; Process a single carry - fix list size constraint
(define-read-only (process-carry (value uint) (acc (list 39 uint)))
(let (
(carry (/ value u256))
(remainder (mod value u256))
(new-acc (concat acc (list remainder)))
)
(if (> carry u0)
(unwrap-panic (as-max-len? (concat new-acc (list carry)) u39))
(unwrap-panic (as-max-len? new-acc u39))
)
)
)
;; Convert uint to byte using ALL_HEX lookup
(define-read-only (uint-to-byte (n uint))
(unwrap-panic (element-at? ALL_HEX n))
)
(define-read-only (convert-inner (data {hash-bytes: (buff 20), name: (optional (string-ascii 40)), version:(buff 1)}))
(let (
;; exit early if contract principal
(t1 (asserts! (is-none (get name data)) ERR_INVALID_ADDR))
;; convert STX version byte to BTC version
(version (unwrap-panic (element-at? BTC_VER (unwrap-panic (index-of? STX_VER (get version data))))))
;; concat BTC version & hash160
(versioned-hash-bytes (concat version (get hash-bytes data)))
;; concat hash-bytes & 4 bytes checksum, and convert hext to uint
(to-encode (map hex-to-uint (concat
versioned-hash-bytes
;; checksum = encode versionded-hash-bytes 2x with sha256, and then extract first 4 bytes
;; we can use unwrap-panic twice, because sha256 of empty buff will alwasy return value
(unwrap-panic (as-max-len? (unwrap-panic (slice? (sha256 (sha256 versioned-hash-bytes)) u0 u4)) u4))
)))
;; "cut" leading zeros leveraging index-of? property
;; first convert list of uint's to list of booleans that tells if value was 0 or not
;; (list u0 u0 u2 u23 u0 u3 u53 u22) -> (list true true false false true false false false)
;; since index-of? always returns first index we use it to find the position of first non-zero value
;; and we default it to u0 - in case it won't find anything
;; in our example, it will return (some u2)
;; the reason why we default to u0 is that (slice? (list u0 u0 u2 u23 u0 u3 u53 u22) u0 u0) will return (some (list))
;; it guarantees that our slice? will never return (none) so we can safely use unwrap-panic here
(leading-zeros (unwrap-panic (slice? to-encode u0 (default-to u0 (index-of? (map is-zero to-encode) false)))))
)
(ok
(fold
convert-to-base58-string
;; run through "outer-loop" everything except leading zeros
;; and concatenate results with leading zeros if any
;; we use u25, because hash-bytes (aka. hash160) = 20 bytes, version = 1 byte, and checksum = 4 bytes
(concat (fold outer-loop (unwrap-panic (slice? to-encode (len leading-zeros) u25)) LST) leading-zeros)
""
)
)
)
)
;; ====================
;; Common Base58 Encoding Functions
;; ====================
;; Core algorithm for Base58 encoding
(define-read-only (outer-loop (x uint) (out (list 44 uint)))
(let (
(new-out (fold update-out out (list x)))
(push (fold carry-push 0x0000 (list (unwrap-panic (element-at? new-out u0)))))
)
(concat
(default-to LST (slice? new-out u1 (len new-out)))
(default-to LST (slice? push u1 (len push)))
)
)
)
;; Division and modulus operations for base conversion
(define-read-only (update-out (x uint) (out (list 35 uint)))
(let (
;; First byte of out is always a carry from previous iteration
(carry (+ (unwrap-panic (element-at? out u0)) (* x u256)))
)
(unwrap-panic (as-max-len? (concat
(list (/ carry u58)) ;; new carry
(concat
(default-to LST (slice? out u1 (len out))) ;; existing list
(list (mod carry u58)) ;; new value we want to append
)
) u35))
)
)
;; Handle remainder propagation
(define-read-only (carry-push (x (buff 1)) (out (list 9 uint)))
(let (
;; First byte of out is always a carry from previous iteration
(carry (unwrap-panic (element-at? out u0)))
)
(if (> carry u0)
;; We only change out if carry is > u0
(unwrap-panic (as-max-len? (concat
(list (/ carry u58)) ;; new carry
(concat
(default-to LST (slice? out u1 (len out))) ;; existing list
(list (mod carry u58)) ;; new value we want to append
)
) u9))
;; Do nothing
out
)
)
)
;; ====================
;; Common Checksum Functions
;; ====================
;; Calculate checksum (double SHA-256 and take first 4 bytes)
(define-read-only (calculate-checksum (data (buff 21)))
(unwrap-panic (as-max-len? (unwrap-panic (slice? (sha256 (sha256 data)) u0 u4)) u4))
)
;; ====================
;; Common String/Encoding Functions
;; ====================
;; Converts uint to base58 character and concatenates in reverse order
(define-read-only (convert-to-base58-string (x uint) (out (string-ascii 44)))
(unwrap-panic (as-max-len? (concat (base58 x) out) u44))
)
;; Helper function to test Base58 character mapping
(define-read-only (get-base58-char (index uint))
(base58 index)
)
;; Helper function to test hex-to-uint conversion
(define-read-only (test-hex-to-uint (hex (buff 1)))
(hex-to-uint hex)
)
;; Helper to demonstrate version mapping
(define-read-only (map-stx-to-btc-version (stx-version (buff 1)))
(let (
(index (unwrap-panic (index-of? STX_VER stx-version)))
(btc-version (unwrap-panic (element-at? BTC_VER index)))
)
{
stx-version: stx-version,
btc-version: btc-version,
index: index
}
)
)
;; Helper to demonstrate checksum calculation
(define-read-only (test-checksum (data (buff 21)))
(calculate-checksum data)
)
;; ====================
;; Test Helper Functions
;; ====================
(define-read-only (test-helper-functions)
(let (
;; Test hex-to-uint conversion
(a (test-hex-to-uint 0x00)) ;; Should return 0
(b (test-hex-to-uint 0x0A)) ;; Should return 10
(c (test-hex-to-uint 0xFF)) ;; Should return 255
;; Test Base58 character mapping
(d (get-base58-char u0)) ;; Should return "1"
(e (get-base58-char u25)) ;; Should return "Q"
(f (get-base58-char u57)) ;; Should return "z"
;; Test version mapping
(g (map-stx-to-btc-version 0x16)) ;; STX P2PKH Mainnet -> BTC P2PKH Mainnet
(h (map-stx-to-btc-version 0x14)) ;; STX P2SH Mainnet -> BTC P2SH Mainnet
(i (map-stx-to-btc-version 0x1a)) ;; STX P2PKH Testnet -> BTC P2PKH Testnet
(j (map-stx-to-btc-version 0x15)) ;; STX P2SH Testnet -> BTC P2SH Testnet
;; Test checksum calculation with sample data
(k (test-checksum 0x0000000000000000000000000000000000000000)) ;; Checksum of all zeros
(l (test-checksum 0x16000000000000000000000000000000000000FF)) ;; STX mainnet with a single byte
;; Test conversion functions
(m (convert-stx-to-btc 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE))
(n (convert-btc-to-stx "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"))
(o (string-to-chars "SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE"))
)
{
a: a,
b: b,
c: c,
d: d,
e: e,
f: f,
g: g,
h: h,
i: i,
j: j,
k: k,
l: l,
m: m,
n: n,
o: o
}
)
)