Source Code

;; 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
        }
    )
)

Functions (29)

FunctionAccessArgs
hex-to-uintread-onlyx: (buff 1
is-zeroread-onlyi: uint
is-some-uintread-onlyi: (optional uint
unwrap-panic-uintread-onlyi: (optional uint
base58read-onlyx: uint
convert-stx-to-btcread-onlyaddr: principal
convert-btc-to-stxread-onlyaddr: (string-ascii 44
decode-base58-addressread-onlyaddr: (string-ascii 44
is-some-stringread-onlys: (optional (string-ascii 1
string-to-charsread-onlys: (string-ascii 44
char-to-base58-valueread-onlyc: (string-ascii 1
decode-base58-valuesread-onlyvalues: (list 44 uint
decode-base58-loopread-onlyvalue: uint, acc: (buff 25
base58-multiply-bytesread-onlybytes: (buff 25
base58-multiply-byteread-onlybyte-val: uint, acc: (buff 25
base58-add-valueread-onlybytes: (buff 25
process-carriesread-onlyvalues: (list 39 uint
process-carryread-onlyvalue: uint, acc: (list 39 uint
uint-to-byteread-onlyn: uint
outer-loopread-onlyx: uint, out: (list 44 uint
update-outread-onlyx: uint, out: (list 35 uint
carry-pushread-onlyx: (buff 1
calculate-checksumread-onlydata: (buff 21
convert-to-base58-stringread-onlyx: uint, out: (string-ascii 44
get-base58-charread-onlyindex: uint
test-hex-to-uintread-onlyhex: (buff 1
map-stx-to-btc-versionread-onlystx-version: (buff 1
test-checksumread-onlydata: (buff 21
test-helper-functionsread-only