Skip to content

Commit f52d634

Browse files
authored
refactor(ledger): move address functions to its own file (#647)
Signed-off-by: Chris Gianelloni <[email protected]>
1 parent cb5ccad commit f52d634

File tree

4 files changed

+495
-452
lines changed

4 files changed

+495
-452
lines changed

ledger/address.go

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
// Copyright 2024 Blink Labs Software
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package ledger
16+
17+
import (
18+
"fmt"
19+
20+
"github.com/blinklabs-io/gouroboros/base58"
21+
"github.com/blinklabs-io/gouroboros/bech32"
22+
"github.com/blinklabs-io/gouroboros/cbor"
23+
)
24+
25+
const (
26+
AddressHeaderTypeMask = 0xF0
27+
AddressHeaderNetworkMask = 0x0F
28+
AddressHashSize = 28
29+
30+
AddressNetworkTestnet = 0
31+
AddressNetworkMainnet = 1
32+
33+
AddressTypeKeyKey = 0b0000
34+
AddressTypeScriptKey = 0b0001
35+
AddressTypeKeyScript = 0b0010
36+
AddressTypeScriptScript = 0b0011
37+
AddressTypeKeyPointer = 0b0100
38+
AddressTypeScriptPointer = 0b0101
39+
AddressTypeKeyNone = 0b0110
40+
AddressTypeScriptNone = 0b0111
41+
AddressTypeByron = 0b1000
42+
AddressTypeNoneKey = 0b1110
43+
AddressTypeNoneScript = 0b1111
44+
)
45+
46+
type Address struct {
47+
addressType uint8
48+
networkId uint8
49+
paymentAddress []byte
50+
stakingAddress []byte
51+
extraData []byte
52+
}
53+
54+
// NewAddress returns an Address based on the provided bech32 address string
55+
func NewAddress(addr string) (Address, error) {
56+
_, data, err := bech32.DecodeNoLimit(addr)
57+
if err != nil {
58+
return Address{}, err
59+
}
60+
decoded, err := bech32.ConvertBits(data, 5, 8, false)
61+
if err != nil {
62+
return Address{}, err
63+
}
64+
a := Address{}
65+
err = a.populateFromBytes(decoded)
66+
if err != nil {
67+
return Address{}, err
68+
}
69+
return a, nil
70+
}
71+
72+
// NewAddressFromParts returns an Address based on the individual parts of the address that are provided
73+
func NewAddressFromParts(
74+
addrType uint8,
75+
networkId uint8,
76+
paymentAddr []byte,
77+
stakingAddr []byte,
78+
) (Address, error) {
79+
if len(paymentAddr) != AddressHashSize {
80+
return Address{}, fmt.Errorf(
81+
"invalid payment address hash length: %d",
82+
len(paymentAddr),
83+
)
84+
}
85+
if len(stakingAddr) > 0 && len(stakingAddr) != AddressHashSize {
86+
return Address{}, fmt.Errorf(
87+
"invalid staking address hash length: %d",
88+
len(stakingAddr),
89+
)
90+
}
91+
return Address{
92+
addressType: addrType,
93+
networkId: networkId,
94+
paymentAddress: paymentAddr[:],
95+
stakingAddress: stakingAddr[:],
96+
}, nil
97+
}
98+
99+
func (a *Address) populateFromBytes(data []byte) error {
100+
// Extract header info
101+
header := data[0]
102+
a.addressType = (header & AddressHeaderTypeMask) >> 4
103+
a.networkId = header & AddressHeaderNetworkMask
104+
// Check length
105+
// We exclude a few address types without fixed sizes that we don't properly support yet
106+
if a.addressType != AddressTypeByron &&
107+
a.addressType != AddressTypeKeyPointer &&
108+
a.addressType != AddressTypeScriptPointer {
109+
dataLen := len(data)
110+
// Addresses must be at least the address hash size plus header byte
111+
if dataLen < (AddressHashSize + 1) {
112+
return fmt.Errorf("invalid address length: %d", dataLen)
113+
}
114+
// Check bounds of second part if the address type is supposed to have one
115+
if a.addressType != AddressTypeKeyNone &&
116+
a.addressType != AddressTypeScriptNone {
117+
if dataLen > (AddressHashSize + 1) {
118+
if dataLen < (AddressHashSize + AddressHashSize + 1) {
119+
return fmt.Errorf("invalid address length: %d", dataLen)
120+
}
121+
}
122+
}
123+
}
124+
// Extract payload
125+
// NOTE: this is definitely incorrect for Byron
126+
payload := data[1:]
127+
a.paymentAddress = payload[:AddressHashSize]
128+
payload = payload[AddressHashSize:]
129+
if a.addressType != AddressTypeKeyNone &&
130+
a.addressType != AddressTypeScriptNone {
131+
if len(payload) >= AddressHashSize {
132+
a.stakingAddress = payload[:AddressHashSize]
133+
payload = payload[AddressHashSize:]
134+
}
135+
}
136+
// Store any extra address data
137+
// This is needed to handle the case describe in:
138+
// https://github.com/IntersectMBO/cardano-ledger/issues/2729
139+
if len(payload) > 0 {
140+
a.extraData = payload[:]
141+
}
142+
// Adjust stake addresses
143+
if a.addressType == AddressTypeNoneKey ||
144+
a.addressType == AddressTypeNoneScript {
145+
a.stakingAddress = a.paymentAddress[:]
146+
a.paymentAddress = make([]byte, 0)
147+
}
148+
return nil
149+
}
150+
151+
func (a *Address) UnmarshalCBOR(data []byte) error {
152+
// Try to unwrap as bytestring (Shelley and forward)
153+
tmpData := []byte{}
154+
if _, err := cbor.Decode(data, &tmpData); err == nil {
155+
err := a.populateFromBytes(tmpData)
156+
if err != nil {
157+
return err
158+
}
159+
} else {
160+
// Probably a Byron address
161+
if err := a.populateFromBytes(data); err != nil {
162+
return err
163+
}
164+
}
165+
return nil
166+
}
167+
168+
func (a *Address) MarshalCBOR() ([]byte, error) {
169+
return cbor.Encode(a.Bytes())
170+
}
171+
172+
// PaymentAddress returns a new Address with only the payment address portion. This will return nil for anything other than payment and script addresses
173+
func (a Address) PaymentAddress() *Address {
174+
var addrType uint8
175+
if a.addressType == AddressTypeKeyKey ||
176+
a.addressType == AddressTypeKeyNone {
177+
addrType = AddressTypeKeyNone
178+
} else if a.addressType == AddressTypeScriptKey ||
179+
a.addressType == AddressTypeScriptNone ||
180+
a.addressType == AddressTypeScriptScript {
181+
addrType = AddressTypeScriptNone
182+
} else {
183+
// Unsupported address type
184+
return nil
185+
}
186+
newAddr := &Address{
187+
addressType: addrType,
188+
networkId: a.networkId,
189+
paymentAddress: a.paymentAddress[:],
190+
}
191+
return newAddr
192+
}
193+
194+
// StakeAddress returns a new Address with only the stake key portion. This will return nil if the address is not a payment/staking key pair
195+
func (a Address) StakeAddress() *Address {
196+
var addrType uint8
197+
if a.addressType == AddressTypeKeyKey ||
198+
a.addressType == AddressTypeScriptKey {
199+
addrType = AddressTypeNoneKey
200+
} else if a.addressType == AddressTypeScriptScript ||
201+
a.addressType == AddressTypeNoneScript {
202+
addrType = AddressTypeNoneScript
203+
} else {
204+
// Unsupported address type
205+
return nil
206+
}
207+
newAddr := &Address{
208+
addressType: addrType,
209+
networkId: a.networkId,
210+
stakingAddress: a.stakingAddress[:],
211+
}
212+
return newAddr
213+
}
214+
215+
func (a Address) generateHRP() string {
216+
var ret string
217+
if a.addressType == AddressTypeNoneKey ||
218+
a.addressType == AddressTypeNoneScript {
219+
ret = "stake"
220+
} else {
221+
ret = "addr"
222+
}
223+
// Add test_ suffix if not mainnet
224+
if a.networkId != AddressNetworkMainnet {
225+
ret += "_test"
226+
}
227+
return ret
228+
}
229+
230+
// Bytes returns the underlying bytes for the address
231+
func (a Address) Bytes() []byte {
232+
ret := []byte{}
233+
ret = append(
234+
ret,
235+
(byte(a.addressType)<<4)|(byte(a.networkId)&AddressHeaderNetworkMask),
236+
)
237+
ret = append(ret, a.paymentAddress...)
238+
ret = append(ret, a.stakingAddress...)
239+
ret = append(ret, a.extraData...)
240+
return ret
241+
}
242+
243+
// String returns the bech32-encoded version of the address
244+
func (a Address) String() string {
245+
data := a.Bytes()
246+
if a.addressType == AddressTypeByron {
247+
// Encode data to base58
248+
encoded := base58.Encode(data)
249+
return encoded
250+
} else {
251+
// Convert data to base32 and encode as bech32
252+
convData, err := bech32.ConvertBits(data, 8, 5, true)
253+
if err != nil {
254+
panic(fmt.Sprintf("unexpected error converting data to base32: %s", err))
255+
}
256+
// Generate human readable part of address for output
257+
hrp := a.generateHRP()
258+
encoded, err := bech32.Encode(hrp, convData)
259+
if err != nil {
260+
panic(fmt.Sprintf("unexpected error encoding data as bech32: %s", err))
261+
}
262+
return encoded
263+
}
264+
}
265+
266+
func (a Address) MarshalJSON() ([]byte, error) {
267+
return []byte(`"` + a.String() + `"`), nil
268+
}

0 commit comments

Comments
 (0)