Skip to content

Commit 15adbb8

Browse files
committed
feat(c32): implement C32 encoding and address
- Add C32Encode and C32Decode functions for Crockford base32 encoding - Implement DecodeC32Address for parsing human-readable Stacks addresses - Add SerializeAddress and DeserializeAddress for binary address representation - Include unit tests for C32 decoding and both address formats
1 parent b347e53 commit 15adbb8

File tree

2 files changed

+202
-0
lines changed

2 files changed

+202
-0
lines changed

pkg/c32/c32.go

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package c32
2+
3+
import (
4+
"encoding/base32"
5+
"errors"
6+
"fmt"
7+
"math/big"
8+
"strings"
9+
10+
"github.com/icon-project/stacks-go-sdk/pkg/stacks"
11+
)
12+
13+
var crockfordAlphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
14+
15+
func C32Encode(input []byte) string {
16+
encoder := base32.NewEncoding(crockfordAlphabet).WithPadding(base32.NoPadding)
17+
return encoder.EncodeToString(input)
18+
}
19+
20+
func C32Decode(input string) ([]byte, error) {
21+
input = strings.ToUpper(strings.ReplaceAll(input, "-", ""))
22+
input = strings.ReplaceAll(input, "O", "0")
23+
input = strings.ReplaceAll(input, "I", "1")
24+
input = strings.ReplaceAll(input, "L", "1")
25+
26+
bi := big.NewInt(0)
27+
for _, char := range input {
28+
bi.Mul(bi, big.NewInt(32))
29+
index := strings.IndexRune(crockfordAlphabet, char)
30+
if index == -1 {
31+
return nil, fmt.Errorf("invalid character: %c", char)
32+
}
33+
bi.Add(bi, big.NewInt(int64(index)))
34+
}
35+
36+
bytes := bi.Bytes()
37+
38+
for len(bytes) > 0 && bytes[0] == 0 {
39+
bytes = bytes[1:]
40+
}
41+
42+
return bytes, nil
43+
}
44+
45+
func DecodeC32Address(address string) (version byte, hash160 [20]byte, err error) {
46+
if len(address) < 5 || address[0] != 'S' {
47+
return 0, [20]byte{}, fmt.Errorf("invalid C32 address: must start with 'S' and be at least 5 characters long")
48+
}
49+
50+
versionChar := address[1]
51+
version = byte(strings.IndexRune(crockfordAlphabet, rune(versionChar)))
52+
53+
decoded, err := C32Decode(address[2:])
54+
if err != nil {
55+
return 0, [20]byte{}, fmt.Errorf("failed to decode address: %v", err)
56+
}
57+
58+
if len(decoded) != 24 { // 20 bytes hash160 + 4 bytes checksum
59+
return 0, [20]byte{}, fmt.Errorf("invalid decoded length: expected 24, got %d", len(decoded))
60+
}
61+
62+
copy(hash160[:], decoded[:20])
63+
64+
return version, hash160, nil
65+
}
66+
67+
func SerializeAddress(address string) ([]byte, error) {
68+
if len(address) != 1+20*2 { // 'S' + version + 40 hex chars
69+
return nil, fmt.Errorf("invalid address length: %d", len(address))
70+
}
71+
72+
var version byte
73+
switch address[0] {
74+
case 'S':
75+
version = 22 // Mainnet single-sig
76+
case 'T':
77+
version = 26 // Testnet single-sig
78+
default:
79+
return nil, fmt.Errorf("invalid address version: %c", address[0])
80+
}
81+
82+
hashBytes, err := C32Decode(address[1:])
83+
if err != nil {
84+
return nil, fmt.Errorf("invalid address hash: %v", err)
85+
}
86+
87+
result := make([]byte, 1+len(hashBytes))
88+
result[0] = version
89+
copy(result[1:], hashBytes)
90+
91+
return result, nil
92+
}
93+
94+
func DeserializeAddress(data []byte) (string, int, error) {
95+
if len(data) < 1+stacks.AddressHashLength {
96+
return "", 0, errors.New("insufficient data for address")
97+
}
98+
99+
version := stacks.AddressVersion(data[0])
100+
var prefix string
101+
switch version {
102+
case stacks.AddressVersionMainnetSingleSig:
103+
prefix = "S"
104+
case stacks.AddressVersionTestnetSingleSig:
105+
prefix = "T"
106+
default:
107+
return "", 0, fmt.Errorf("invalid address version: %d", version)
108+
}
109+
110+
c32hash := C32Encode(data[1 : 1+stacks.AddressHashLength+5])
111+
address := fmt.Sprintf("%s%s", prefix, c32hash)
112+
113+
return address, 1 + stacks.AddressHashLength + 5, nil
114+
}

pkg/c32/c32_test.go

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package c32
2+
3+
import (
4+
"encoding/hex"
5+
"reflect"
6+
"testing"
7+
)
8+
9+
func TestCrockfordDecode(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
input string
13+
expected string
14+
wantErr bool
15+
}{
16+
{
17+
name: "Decode 'hello world'",
18+
input: "38CNP6RVS0EXQQ4V34",
19+
expected: "68656c6c6f20776f726c64",
20+
wantErr: false,
21+
},
22+
{
23+
name: "Decode 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM'",
24+
input: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM",
25+
expected: "19d06d78de7b0625dfbfc16c3a8a5735f6dc3dc3f2ce35687e14",
26+
wantErr: false,
27+
},
28+
{
29+
name: "Decode 'SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7'",
30+
input: "SP2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKNRV9EJ7",
31+
expected: "19b0a46ff88886c2ef9762d970b4d2c63678835bd39d71b4ba47",
32+
wantErr: false,
33+
},
34+
}
35+
36+
for _, tt := range tests {
37+
t.Run(tt.name, func(t *testing.T) {
38+
decoded, err := C32Decode(tt.input)
39+
if (err != nil) != tt.wantErr {
40+
t.Errorf("CrockfordDecode() error = %v, wantErr %v", err, tt.wantErr)
41+
return
42+
}
43+
44+
gotHex := hex.EncodeToString(decoded)
45+
46+
if !reflect.DeepEqual(gotHex, tt.expected) {
47+
t.Errorf("CrockfordDecode() = %v, want %v", gotHex, tt.expected)
48+
}
49+
})
50+
}
51+
}
52+
53+
func TestDecodeC32Address(t *testing.T) {
54+
tests := []struct {
55+
name string
56+
input string
57+
expectedVer byte
58+
expectedHash string
59+
wantErr bool
60+
}{
61+
{
62+
name: "Decode Stacks address",
63+
input: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM",
64+
expectedVer: 26,
65+
expectedHash: "6d78de7b0625dfbfc16c3a8a5735f6dc3dc3f2ce",
66+
wantErr: false,
67+
},
68+
}
69+
70+
for _, tt := range tests {
71+
t.Run(tt.name, func(t *testing.T) {
72+
version, hash160, err := DecodeC32Address(tt.input)
73+
if (err != nil) != tt.wantErr {
74+
t.Errorf("DecodeC32Address() error = %v, wantErr %v", err, tt.wantErr)
75+
return
76+
}
77+
78+
if version != tt.expectedVer {
79+
t.Errorf("DecodeC32Address() version = %v, want %v", version, tt.expectedVer)
80+
}
81+
82+
gotHash := hex.EncodeToString(hash160[:])
83+
if gotHash != tt.expectedHash {
84+
t.Errorf("DecodeC32Address() hash160 = %v, want %v", gotHash, tt.expectedHash)
85+
}
86+
})
87+
}
88+
}

0 commit comments

Comments
 (0)