Convert string to principal

Parse string addresses into principal types using c32 decoding in Clarity


(define-read-only (string-to-principal? (input (string-ascii 82)))
(let (
;; Find the dot separator for contract addresses
(dot (default-to (len input) (index-of? input ".")))
;; Extract address part (skip first char which is version)
(addr (unwrap! (slice? input u1 dot) ERR_INVALID_LENGTH))
;; Decode c32 characters to numbers
(addressc32 (map unwrap-panic-uint (filter is-some-uint (map c32-index addr))))
;; Validate all characters are valid c32
(isValidChars (asserts! (is-eq (len addr) (len addressc32)) ERR_INVALID_CHAR))
;; Extract version and decode address data
(version (unwrap-panic (element-at? addressc32 u0)))
(decoded (decode-address addressc32))
;; Verify checksum
(checksum (verify-checksum decoded version))
)
;; Construct principal with or without contract name
(match (slice? input (+ u1 dot) (len input)) contract
(principal-construct? (to-byte version) (get-address-bytes decoded) contract)
(principal-construct? (to-byte version) (get-address-bytes decoded))
)
)
)
;; Example usage
(string-to-principal? "SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7")
;; Returns (some SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7)
(string-to-principal? "SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7.my-contract")
;; Returns (some SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7.my-contract)

Use cases

  • Parsing user input addresses in contracts
  • Converting stored string addresses to principals
  • Validating address formats before use
  • Building dynamic contract calls with string inputs

Key concepts

Stacks addresses use c32 encoding:

  • c32 alphabet: 0123456789ABCDEFGHJKMNPQRSTVWXYZ (no I, L, O, U)
  • Checksum: Last 4 bytes verify address integrity
  • Version byte: First character indicates address type
  • Contract addresses: Include .contract-name suffix

Complete implementation

;; Constants
(define-constant C32_CHARS "0123456789ABCDEFGHJKMNPQRSTVWXYZ")
(define-constant ALL_HEX 0x00010203...FF) ;; All hex bytes
;; Error codes
(define-constant ERR_INVALID_CHAR (err u100))
(define-constant ERR_INVALID_LENGTH (err u101))
(define-constant ERR_INVALID_CHECKSUM (err u105))
;; Helper to get c32 character index
(define-read-only (c32-index (c (string-ascii 1)))
(index-of? C32_CHARS c))
;; Decode c32 to bytes
(define-read-only (decode (i uint) (out (list 26 uint)))
(let (
(cb (unwrap-panic (element-at? out u1)))
(carry (+ (unwrap-panic (element-at? out u0)) (bit-shift-left i cb)))
(carryBits (+ u5 cb))
)
(unwrap-panic (as-max-len? (concat
(if (>= carryBits u8)
(list (bit-shift-right carry u8) (- carryBits u8) (bit-and carry (- (bit-shift-left u1 u8) u1)))
(list carry carryBits)
)
(default-to (list) (slice? out u2 (len out)))) u26))
)
)

Address version types

VersionTypeExample
SStandard (mainnet)SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7
TStandard (testnet)ST2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7
MMultisig (mainnet)SM2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7
NMultisig (testnet)SN2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7

Validation example

;; Validate address format before conversion
(define-read-only (validate-address-string (address (string-ascii 82)))
(let (
;; Check minimum length
(len-check (asserts! (>= (len address) u28) (err u1)))
;; Check starts with valid version
(version-check (asserts!
(is-some (index-of? "STMNP" (unwrap-panic (slice? address u0 u1))))
(err u2)
))
;; Try to convert
(converted (string-to-principal? address))
)
(match converted
principal (ok principal)
(err u3)
)
)
)

Batch conversion

;; Convert list of string addresses
(define-read-only (convert-address-list (addresses (list 10 (string-ascii 82))))
(map string-to-principal? addresses)
)
;; Filter valid addresses
(define-read-only (get-valid-addresses (addresses (list 10 (string-ascii 82))))
(filter is-some (map string-to-principal? addresses))
)
Security note

Always validate string addresses before converting to principals. Invalid addresses will cause the conversion to fail, potentially blocking contract execution.