cardano-bech32 0.3.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +159 -48
- data/lib/cardano/bech32/address/abstract_address.rb +32 -0
- data/lib/cardano/bech32/address/payment.rb +11 -0
- data/lib/cardano/bech32/address/stake.rb +11 -0
- data/lib/cardano/bech32/address.rb +93 -0
- data/lib/cardano/bech32/gov_credentials/abstract_credential.rb +83 -0
- data/lib/cardano/bech32/gov_credentials/cc.rb +15 -0
- data/lib/cardano/bech32/gov_credentials/decode.rb +64 -0
- data/lib/cardano/bech32/gov_credentials/drep.rb +12 -0
- data/lib/cardano/bech32/gov_credentials/encode.rb +74 -0
- data/lib/cardano/bech32/gov_credentials/gov_action.rb +81 -0
- data/lib/cardano/bech32/gov_credentials/header.rb +123 -0
- data/lib/cardano/bech32/gov_credentials.rb +9 -0
- data/lib/cardano/bech32/stake_pool.rb +68 -0
- data/lib/cardano/bech32/version.rb +1 -1
- data/lib/cardano/bech32.rb +43 -4
- metadata +14 -2
- data/lib/cardano/bech32/gov_action.rb +0 -77
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c13d50b51db3aad428e4dc97ed0036c8bdd2bc04340a13af4d5634f372015300
|
|
4
|
+
data.tar.gz: 9ec070ab4fd53ad9b6cfc0e64d7bb9de66cee2bb6644b235f4557eebc96feabc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ebbf68a3fb7bbcd5b07530d435cc78901fdc1c64e8135a87d12bb053b4fb70088da9fa2ea540ec10f85863efad2e750c4510d475e87059bd62a778b5b98c7b7b
|
|
7
|
+
data.tar.gz: 34b20e1373a2d0953f04d2fca08e465b4dc2549b4df53c3bcd08207dccc7aad7b945a955918ce6c748752d005927595053394668097c62b04ba7c02bd410598f
|
data/CHANGELOG.md
CHANGED
|
@@ -8,3 +8,14 @@
|
|
|
8
8
|
- Decoding of `gov_action` Bech32 identifiers into transaction ID and index
|
|
9
9
|
- Validation helpers for governance action identifiers
|
|
10
10
|
- RSpec test suite with normative CIP-0129 test vectors
|
|
11
|
+
|
|
12
|
+
## [0.1.0] - 2026-01-05
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- Encoding of DRep IDs `drep` as defined in CIP-0129
|
|
16
|
+
- Decoding of `drep` Bech32 identifiers into hex and byte data
|
|
17
|
+
- Encoding of CC (cold / hot) IDs `cc_cold` `cc_hot` as defined in CIP-0129
|
|
18
|
+
- Decoding of `cc_cold` `cc_hot` into hex and byte data
|
|
19
|
+
- Encoding of Stake Pool identifier `pool`
|
|
20
|
+
- Decoding of Stake Pool identifiers into hex and byte data
|
|
21
|
+
- Decoding of Addresses such as Base, Stake, Pointer, Enterprise into network info, byte data
|
data/README.md
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
# cardano-bech32
|
|
2
2
|
|
|
3
3
|
A small, focused Ruby library for encoding and decoding Cardano Bech32 identifiers.
|
|
4
|
-
This gem deliberately avoids higher-level ledger concerns and focuses solely on
|
|
4
|
+
This gem deliberately avoids higher-level ledger concerns and focuses solely on **specification-compliant Bech32 handling**.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
## Supported Identifiers
|
|
7
7
|
|
|
8
|
-
- [x] Governance
|
|
9
|
-
- [
|
|
10
|
-
- [
|
|
11
|
-
- [
|
|
8
|
+
- [x] Governance [CIP-0129](https://cips.cardano.org/cip/CIP-0129)
|
|
9
|
+
- [x] GovAction IDs
|
|
10
|
+
- [x] DRep IDs
|
|
11
|
+
- [x] CC hot/cold
|
|
12
|
+
- [x] Addresses
|
|
13
|
+
- [x] Base Address
|
|
14
|
+
- [x] Enterprise Address
|
|
15
|
+
- [x] Pointer Address
|
|
16
|
+
- [x] Stake Address
|
|
17
|
+
- [x] Stake Pool IDs
|
|
12
18
|
|
|
13
19
|
The API is designed to grow conservatively as additional Cardano Improvement Proposals (CIPs) are implemented.
|
|
14
20
|
|
|
15
21
|
## Installation
|
|
16
22
|
|
|
17
|
-
Add
|
|
23
|
+
Add to your Gemfile:
|
|
18
24
|
|
|
19
|
-
```
|
|
25
|
+
```ruby
|
|
20
26
|
gem "cardano-bech32"
|
|
21
27
|
```
|
|
22
28
|
|
|
@@ -34,79 +40,184 @@ gem install cardano-bech32
|
|
|
34
40
|
|
|
35
41
|
## Usage
|
|
36
42
|
|
|
37
|
-
### Governance
|
|
43
|
+
### Governance Credentials (CIP-0129)
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
tx_id (32 bytes) || index (1 byte)
|
|
41
|
-
→ Bech32 encode with HRP "gov_action"
|
|
45
|
+
Cardano governance identifiers defined in CIP-0129 are Bech32-encoded binary payloads used throughout on-chain governance.
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
This library supports all CIP-0129 governance identifiers, including:
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
* Constitutional Committee credentials (hot & cold)
|
|
50
|
+
* DRep credentials
|
|
51
|
+
* Governance Action IDs
|
|
47
52
|
|
|
48
|
-
|
|
53
|
+
#### Constitutional Committee & DRep Credentials
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
Governance credentials consist of:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
header (1 byte) || credential hash (N bytes)
|
|
52
59
|
```
|
|
53
60
|
|
|
54
|
-
|
|
61
|
+
The header byte is authoritative and encodes:
|
|
62
|
+
* Credential type (CC Hot, CC Cold, DRep)
|
|
63
|
+
* Key type (key hash or script hash)
|
|
55
64
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
The HRP is derived from the header and validated during decoding.
|
|
66
|
+
|
|
67
|
+
Decode a governance credential
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
require "cardano/bech32"
|
|
71
|
+
|
|
72
|
+
cred = Cardano::Bech32::GovCredentials.decode(
|
|
73
|
+
"cc_cold1zvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6kflvs"
|
|
59
74
|
)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
75
|
+
|
|
76
|
+
cred.credential # => :cc_cold
|
|
77
|
+
cred.hash_bytes # => [0, 0, 0, ...]
|
|
78
|
+
cred.header_byte # => 19
|
|
79
|
+
cred.hrp # => "cc_cold"
|
|
80
|
+
cred.key_type # => :script
|
|
81
|
+
cred.payload_bytes # => [19, 0, 0, ...]
|
|
82
|
+
cred.payload_hex # => "1300000000..."
|
|
64
83
|
```
|
|
65
84
|
|
|
66
|
-
|
|
85
|
+
The returned object is one of:
|
|
86
|
+
* `Cardano::Bech32::GovCredentials::Cc`
|
|
87
|
+
* `Cardano::Bech32::GovCredentials::Drep`
|
|
67
88
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
89
|
+
Both inherit from `AbstractCredential` and expose a uniform interface.
|
|
90
|
+
|
|
91
|
+
Encode a governance credential
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
payload_hex = "130000000000000000000000000000000000000000000000000000000000"
|
|
95
|
+
|
|
96
|
+
bech32 = Cardano::Bech32::GovCredentials.encode(payload_hex)
|
|
97
|
+
# => "cc_cold1zvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6kflvs"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The encoder:
|
|
101
|
+
* Accepts raw bytes or hex
|
|
102
|
+
* Infers HRP from the header byte
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
#### Governance Action IDs
|
|
106
|
+
|
|
107
|
+
Governance Action IDs identify a governance action on-chain and consist of:
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
tx_id (32 bytes) || index (1 byte)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
They are encoded as Bech32 using HRP `gov_action`.
|
|
72
115
|
|
|
73
116
|
```ruby
|
|
74
|
-
|
|
117
|
+
require "cardano/bech32"
|
|
118
|
+
|
|
119
|
+
txref = "b2a591ac219ce6dcca5847e0248015209c7cb0436aa6bd6863d0c1f152a60bc5#0"
|
|
120
|
+
bech32 = Cardano::Bech32::GovAction.encode(txref)
|
|
121
|
+
# => "gov_action1k2jertppnnndejjcglszfqq4yzw8evzrd2nt66rr6rqlz54xp0zsq05ecsn"
|
|
122
|
+
|
|
123
|
+
gov_action = Cardano::Bech32::GovAction.decode(bech32)
|
|
124
|
+
gov_action.tx_id # => "b2a591ac219ce6dcca5847e0248015209c7cb0436aa6bd6863d0c1f152a60bc5"
|
|
125
|
+
gov_action.index # => 0
|
|
126
|
+
gov_action.payload_bytes # => [178, 165, 145, 172, ...]
|
|
127
|
+
gov_action.hrp # => "gov_action"
|
|
128
|
+
|
|
129
|
+
Cardano::Bech32::GovAction.valid?(bech32)
|
|
75
130
|
# => true
|
|
76
131
|
```
|
|
77
132
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
The library normalizes all Bech32 decoding and validation failures into
|
|
81
|
-
explicit, typed errors:
|
|
133
|
+
All decoding/validation failures for `GovAction` raise explicit, typed errors:
|
|
82
134
|
|
|
83
|
-
* InvalidFormat — malformed input, wrong HRP, invalid Bech32
|
|
84
|
-
* InvalidPayload — incorrect
|
|
135
|
+
* `Cardano::Bech32::GovAction::InvalidFormat` — malformed input, wrong HRP, invalid Bech32
|
|
136
|
+
* `Cardano::Bech32::GovAction::InvalidPayload` — incorrect payload length, invalid index
|
|
85
137
|
|
|
86
138
|
```ruby
|
|
87
|
-
# Example
|
|
88
139
|
begin
|
|
89
|
-
Cardano::Bech32::GovAction.
|
|
90
|
-
rescue Cardano::Bech32::GovAction::
|
|
140
|
+
Cardano::Bech32::GovAction.decode("invalid")
|
|
141
|
+
rescue Cardano::Bech32::GovAction::InvalidFormat,
|
|
142
|
+
Cardano::Bech32::GovAction::InvalidPayload => e
|
|
91
143
|
puts e.message
|
|
92
144
|
end
|
|
145
|
+
```
|
|
146
|
+
|
|
93
147
|
|
|
148
|
+
|
|
149
|
+
### Stake Pool IDs
|
|
150
|
+
|
|
151
|
+
Stake Pool IDs are Bech32-encoded 28-byte hashes identifying a registered stake pool.
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
pool_hash = "6e90911fdb579e203f556f3f24aca5b8714be049ccf716008ab849fd"
|
|
155
|
+
Cardano::Bech32::StakePool.encode(pool_hash)
|
|
156
|
+
# => pool1d6gfz87m270zq064duljft99hpc5hczfenm3vqy2hpyl67tteq9
|
|
157
|
+
|
|
158
|
+
pool_id = "pool1d6gfz87m270zq064duljft99hpc5hczfenm3vqy2hpyl67tteq9"
|
|
159
|
+
Cardano::Bech32::StakePool.decode(pool_id)
|
|
160
|
+
# => { bytes: "\x8C\xB8\...", hex: "6e90911..." }
|
|
161
|
+
|
|
162
|
+
Cardano::Bech32::StakePool.valid?(pool_id)
|
|
163
|
+
# => true
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Addresses
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
# Decode a Cardano Address
|
|
170
|
+
addr_bech32 = "addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgse35a3x"
|
|
171
|
+
address = CardanoBech32::Address.decode(addr_bech32)
|
|
172
|
+
|
|
173
|
+
address.address_type # => :base, :stake, :pointer, or :enterprise
|
|
174
|
+
address.payment_credential # => :key or :script (nil for stake/pointer-only addresses)
|
|
175
|
+
address.stake_credential # => :key or :script (nil if not applicable)
|
|
176
|
+
address.network # => network string derived from HRP
|
|
94
177
|
```
|
|
95
178
|
|
|
179
|
+
#### Bech32 and Cardano Addresses
|
|
180
|
+
|
|
181
|
+
Some Bech32 libraries may report the variant incorrectly (Bech32 / Bech32m), but for Cardano this does **not affect validity**.
|
|
182
|
+
|
|
183
|
+
This library validates addresses based on:
|
|
184
|
+
|
|
185
|
+
* The HRP (`addr`, `addr_test`, `stake`, etc.)
|
|
186
|
+
* The header byte
|
|
187
|
+
* The network id encoded in the payload
|
|
188
|
+
|
|
189
|
+
Checksum and payload parsing are fully specification-compliant.
|
|
190
|
+
|
|
96
191
|
## Development
|
|
97
192
|
|
|
98
|
-
After checking out the repo
|
|
193
|
+
After checking out the repo:
|
|
99
194
|
|
|
100
|
-
|
|
195
|
+
```
|
|
196
|
+
bin/setup # install dependencies
|
|
197
|
+
rake spec # run tests
|
|
198
|
+
bin/console # interactive console
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
To install locally:
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
bundle exec rake install
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
To release a new version:
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
# update version.rb
|
|
211
|
+
bundle exec rake release
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
This creates a git tag, pushes commits, and uploads the gem to [rubygems.org](https://rubygems.org).
|
|
101
215
|
|
|
102
216
|
## Contributing
|
|
103
217
|
|
|
104
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
|
218
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/lacepool/cardano-bech32.
|
|
219
|
+
Please follow the [code of conduct](https://github.com/lacepool/cardano-bech32/blob/main/CODE_OF_CONDUCT.md).
|
|
105
220
|
|
|
106
221
|
## License
|
|
107
222
|
|
|
108
223
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
109
|
-
|
|
110
|
-
## Code of Conduct
|
|
111
|
-
|
|
112
|
-
Everyone interacting in the Cardano::Bech32 project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/cardano_bech32/blob/main/CODE_OF_CONDUCT.md).
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cardano
|
|
4
|
+
module Bech32
|
|
5
|
+
module Address
|
|
6
|
+
# Abstract class for different types of Cardano Bech32 addresses
|
|
7
|
+
class AbstractAddress
|
|
8
|
+
attr_reader :hrp, :network, :payload_bytes, :header, :address_type, :payment_credential, :stake_credential
|
|
9
|
+
|
|
10
|
+
def initialize(hrp:, network:, payload_bytes:, header:)
|
|
11
|
+
@hrp = hrp
|
|
12
|
+
@network = network
|
|
13
|
+
@payload_bytes = payload_bytes
|
|
14
|
+
@header = header
|
|
15
|
+
|
|
16
|
+
@address_type = Address.address_type(header)
|
|
17
|
+
|
|
18
|
+
@payment_credential = Address.payment_credential_kind(@header, @address_type)
|
|
19
|
+
@stake_credential = Address.stake_credential_kind(@header, @address_type)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def base? = @address_type == :base
|
|
23
|
+
def pointer? = @address_type == :pointer
|
|
24
|
+
def enterprise? = @address_type == :enterprise
|
|
25
|
+
def stake? = @address_type == :stake
|
|
26
|
+
|
|
27
|
+
def script_payment? = @payment_credential == Address::Credential::SCRIPT
|
|
28
|
+
def script_stake? = @stake_credential == Address::Credential::SCRIPT
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bech32"
|
|
4
|
+
require_relative "address/payment"
|
|
5
|
+
require_relative "address/stake"
|
|
6
|
+
|
|
7
|
+
module Cardano
|
|
8
|
+
module Bech32
|
|
9
|
+
# Module for Cardano Bech32 addresses
|
|
10
|
+
module Address
|
|
11
|
+
module Credential
|
|
12
|
+
KEY = :key
|
|
13
|
+
SCRIPT = :script
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module Type
|
|
17
|
+
BASE = :base
|
|
18
|
+
POINTER = :pointer
|
|
19
|
+
ENTERPRISE = :enterprise
|
|
20
|
+
STAKE = :stake
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
ADDRESS_TYPE_BY_HEADER = {
|
|
24
|
+
0b0000 => Type::BASE,
|
|
25
|
+
0b0001 => Type::BASE,
|
|
26
|
+
0b0010 => Type::BASE,
|
|
27
|
+
0b0011 => Type::BASE,
|
|
28
|
+
0b0100 => Type::POINTER,
|
|
29
|
+
0b0101 => Type::POINTER,
|
|
30
|
+
0b0110 => Type::ENTERPRISE,
|
|
31
|
+
0b0111 => Type::ENTERPRISE,
|
|
32
|
+
0b1110 => Type::STAKE,
|
|
33
|
+
0b1111 => Type::STAKE
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
def self.address_type(header)
|
|
37
|
+
high_nibble = high_nibble_from_header(header)
|
|
38
|
+
|
|
39
|
+
ADDRESS_TYPE_BY_HEADER.fetch(high_nibble) do
|
|
40
|
+
raise InvalidFormat, "Unknown header type: #{header.to_s(2)}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Payment credential: only exists for Base, Pointer, Enterprise
|
|
45
|
+
# Last bit of high nibble (0 = key, 1 = script)
|
|
46
|
+
def self.payment_credential_kind(header, address_type)
|
|
47
|
+
return nil if address_type == Type::STAKE
|
|
48
|
+
|
|
49
|
+
(high_nibble_from_header(header) & 0b0001).zero? ? Credential::KEY : Credential::SCRIPT
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Stake credential: varies by address type
|
|
53
|
+
def self.stake_credential_kind(header, address_type)
|
|
54
|
+
case address_type
|
|
55
|
+
when Type::STAKE
|
|
56
|
+
# last bit of high nibble: 0 = key, 1 = script
|
|
57
|
+
(high_nibble_from_header(header) & 0b0001).zero? ? Credential::KEY : Credential::SCRIPT
|
|
58
|
+
when Type::BASE
|
|
59
|
+
# third bit of high nibble: 1 = script, 0 = key
|
|
60
|
+
(high_nibble_from_header(header) & 0b0010).zero? ? Credential::KEY : Credential::SCRIPT
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.network_symbol(header)
|
|
65
|
+
header & 0x0F == 1 ? :mainnet : :testnet
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.high_nibble_from_header(header)
|
|
69
|
+
(header & 0b11110000) >> 4
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.decode(bech32)
|
|
73
|
+
hrp, data = Cardano::Bech32.decode(bech32)
|
|
74
|
+
raise InvalidFormat, "invalid bech32 string" if hrp.nil? || data.nil?
|
|
75
|
+
|
|
76
|
+
# Convert 5-bit array to bytes
|
|
77
|
+
payload_bytes = Cardano::Bech32.convert_bits(data, from_bits: 5, to_bits: 8, pad: true)
|
|
78
|
+
raise InvalidPayload, "invalid payload length" unless payload_bytes.length.positive?
|
|
79
|
+
|
|
80
|
+
header = payload_bytes.first
|
|
81
|
+
type = address_type(header)
|
|
82
|
+
klass = type == :stake ? Stake : Payment
|
|
83
|
+
|
|
84
|
+
klass.new(
|
|
85
|
+
hrp: hrp,
|
|
86
|
+
network: network_symbol(header),
|
|
87
|
+
payload_bytes: payload_bytes,
|
|
88
|
+
header: header
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "header"
|
|
4
|
+
|
|
5
|
+
module Cardano
|
|
6
|
+
module Bech32
|
|
7
|
+
module GovCredentials
|
|
8
|
+
# Abstract base class for CIP-0129 governance credentials
|
|
9
|
+
#
|
|
10
|
+
# Header byte is authoritative and is interpreted exclusively
|
|
11
|
+
# via GovCredentials::Header.
|
|
12
|
+
class AbstractCredential
|
|
13
|
+
attr_reader :credential,
|
|
14
|
+
:hash_bytes,
|
|
15
|
+
:header_byte,
|
|
16
|
+
:hrp,
|
|
17
|
+
:key_type,
|
|
18
|
+
:payload_bytes,
|
|
19
|
+
:payload_hex
|
|
20
|
+
|
|
21
|
+
HASH_SIZE = 28
|
|
22
|
+
|
|
23
|
+
def initialize(hrp:, payload_bytes:)
|
|
24
|
+
@payload_bytes = payload_bytes
|
|
25
|
+
@header_byte = payload_bytes.first
|
|
26
|
+
@credential = Header.credential_type(@header_byte)
|
|
27
|
+
@hash_bytes = payload_bytes[1..]
|
|
28
|
+
@hrp = hrp
|
|
29
|
+
@key_type = Header.key_type(@header_byte)
|
|
30
|
+
@payload_hex = @payload_bytes.pack("C*").unpack1("H*").encode(Encoding::UTF_8)
|
|
31
|
+
|
|
32
|
+
validate!
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# -----------------------------
|
|
36
|
+
# KeyType helpers
|
|
37
|
+
# -----------------------------
|
|
38
|
+
|
|
39
|
+
def key? = @key_type == Header::KeyType::KEY
|
|
40
|
+
def script? = @key_type == Header::KeyType::SCRIPT
|
|
41
|
+
|
|
42
|
+
# Returns a JSON-safe hash representation
|
|
43
|
+
#
|
|
44
|
+
# @return [Hash]
|
|
45
|
+
def to_h
|
|
46
|
+
{
|
|
47
|
+
credential: @credential,
|
|
48
|
+
hash_bytes: @hash_bytes,
|
|
49
|
+
header_byte: @header_byte,
|
|
50
|
+
hrp: @hrp,
|
|
51
|
+
key_type: @key_type,
|
|
52
|
+
payload_bytes: @payload_bytes,
|
|
53
|
+
payload_hex: @payload_hex
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# -----------------------------
|
|
60
|
+
# Validation
|
|
61
|
+
# -----------------------------
|
|
62
|
+
|
|
63
|
+
def validate!
|
|
64
|
+
unless @hash_bytes.length == HASH_SIZE
|
|
65
|
+
raise InvalidPayload,
|
|
66
|
+
"invalid hash length: expected #{HASH_SIZE} bytes, got #{@hash_bytes.length}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
expected_hrp = Header.hrp(@header_byte)
|
|
70
|
+
unless @hrp == expected_hrp
|
|
71
|
+
raise InvalidFormat,
|
|
72
|
+
"HRP mismatch: expected #{expected_hrp}, got #{@hrp}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
return if @payload_bytes.length > 1
|
|
76
|
+
|
|
77
|
+
raise InvalidPayload,
|
|
78
|
+
"payload must include header + hash, got #{@payload_bytes.length} bytes"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "abstract_credential"
|
|
4
|
+
|
|
5
|
+
module Cardano
|
|
6
|
+
module Bech32
|
|
7
|
+
module GovCredentials
|
|
8
|
+
# Class for Cold and Hot CC credentials
|
|
9
|
+
class Cc < AbstractCredential
|
|
10
|
+
def hot? = @key_type == Header::KeyType::CC_HOT
|
|
11
|
+
def cold? = @key_type == Header::KeyType::CC_COLD
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bech32"
|
|
4
|
+
require_relative "cc"
|
|
5
|
+
require_relative "drep"
|
|
6
|
+
require_relative "header"
|
|
7
|
+
require_relative "abstract_credential"
|
|
8
|
+
|
|
9
|
+
module Cardano
|
|
10
|
+
module Bech32
|
|
11
|
+
# CIP-0129 Governance Credentials
|
|
12
|
+
module GovCredentials
|
|
13
|
+
class << self
|
|
14
|
+
# Decode a governance credential
|
|
15
|
+
#
|
|
16
|
+
# @param bech32 [String] Bech32 encoded governance credential
|
|
17
|
+
# @return [AbstractCredential, GovAction]
|
|
18
|
+
#
|
|
19
|
+
# Raises:
|
|
20
|
+
# - Cardano::Bech32::InvalidFormat
|
|
21
|
+
# - Cardano::Bech32::InvalidPayload
|
|
22
|
+
#
|
|
23
|
+
def decode(bech32)
|
|
24
|
+
hrp, data = Cardano::Bech32.decode(bech32)
|
|
25
|
+
raise InvalidFormat, "invalid bech32 string" if hrp.nil? || data.nil?
|
|
26
|
+
|
|
27
|
+
case hrp
|
|
28
|
+
when "gov_action"
|
|
29
|
+
GovAction.decode_from_data(data)
|
|
30
|
+
when "cc_hot", "cc_cold", "drep"
|
|
31
|
+
# when Header::HRP_BY_CREDENTIAL_TYPE.values
|
|
32
|
+
decode_credential(hrp, data)
|
|
33
|
+
else
|
|
34
|
+
raise InvalidFormat, "unknown governance HRP: #{hrp}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def decode_credential(hrp, data)
|
|
41
|
+
payload_bytes =
|
|
42
|
+
Cardano::Bech32.convert_bits(data, from_bits: 5, to_bits: 8, pad: false)
|
|
43
|
+
|
|
44
|
+
raise InvalidPayload, "empty payload" if payload_bytes.empty?
|
|
45
|
+
|
|
46
|
+
header_byte = payload_bytes.first
|
|
47
|
+
credential_type = Header.credential_type(header_byte)
|
|
48
|
+
|
|
49
|
+
klass =
|
|
50
|
+
case credential_type
|
|
51
|
+
when Header::CredentialType::CC_HOT, Header::CredentialType::CC_COLD
|
|
52
|
+
Cc
|
|
53
|
+
when Header::CredentialType::DREP
|
|
54
|
+
Drep
|
|
55
|
+
else
|
|
56
|
+
raise InvalidFormat, "unknown governance credential header: #{header_byte}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
klass.new(hrp: hrp, payload_bytes: payload_bytes)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bech32"
|
|
4
|
+
require_relative "header"
|
|
5
|
+
require_relative "abstract_credential"
|
|
6
|
+
|
|
7
|
+
module Cardano
|
|
8
|
+
module Bech32
|
|
9
|
+
# CIP-0129 Governance Credentials
|
|
10
|
+
module GovCredentials
|
|
11
|
+
class << self
|
|
12
|
+
#
|
|
13
|
+
# Encode a governance-related Bech32 object
|
|
14
|
+
#
|
|
15
|
+
# Supports:
|
|
16
|
+
# - Governance credentials (header + hash payload)
|
|
17
|
+
# - Governance actions (txid#index)
|
|
18
|
+
#
|
|
19
|
+
# @param payload [String, Array<Integer>] Hex string or byte array
|
|
20
|
+
# @return [String] Bech32 encoded governance credential
|
|
21
|
+
#
|
|
22
|
+
# Raises:
|
|
23
|
+
# - ArgumentError
|
|
24
|
+
#
|
|
25
|
+
def encode(input)
|
|
26
|
+
case input
|
|
27
|
+
when String
|
|
28
|
+
if gov_action_ref?(input)
|
|
29
|
+
GovAction.encode(input)
|
|
30
|
+
else
|
|
31
|
+
encode_credential(input)
|
|
32
|
+
end
|
|
33
|
+
when Array
|
|
34
|
+
encode_credential(input)
|
|
35
|
+
else
|
|
36
|
+
raise ArgumentError, "invalid input type: #{input.class}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def encode_credential(payload)
|
|
43
|
+
bytes = bytes_from_payload(payload)
|
|
44
|
+
raise ArgumentError, "empty payload" if bytes.empty?
|
|
45
|
+
|
|
46
|
+
header_byte = bytes.first
|
|
47
|
+
hrp = Header.hrp(header_byte)
|
|
48
|
+
data = Cardano::Bech32.convert_bits(bytes, from_bits: 8, to_bits: 5, pad: true)
|
|
49
|
+
|
|
50
|
+
Cardano::Bech32.encode(hrp, data)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def gov_action_ref?(value)
|
|
54
|
+
value.include?("#")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def bytes_from_payload(payload)
|
|
58
|
+
case payload
|
|
59
|
+
when String
|
|
60
|
+
if payload.match?(/\A[0-9a-fA-F]+\z/) && payload.length.even?
|
|
61
|
+
[payload].pack("H*").bytes
|
|
62
|
+
else
|
|
63
|
+
payload.bytes
|
|
64
|
+
end
|
|
65
|
+
when Array
|
|
66
|
+
payload
|
|
67
|
+
else
|
|
68
|
+
raise ArgumentError, "invalid payload type: #{payload.class}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bech32"
|
|
4
|
+
|
|
5
|
+
module Cardano
|
|
6
|
+
module Bech32
|
|
7
|
+
module GovCredentials
|
|
8
|
+
# Bech32 encoding for governance action references.
|
|
9
|
+
# A governance action reference is of the form "txid#index"
|
|
10
|
+
# where txid is a 32-byte hex string and index is a 0..255 integer
|
|
11
|
+
# The Bech32 encoding has HRP "gov_action" and a payload of 33 bytes:
|
|
12
|
+
# - 32 bytes: txid (binary)
|
|
13
|
+
# - 1 byte: index (binary)
|
|
14
|
+
#
|
|
15
|
+
class GovAction
|
|
16
|
+
HRP = "gov_action"
|
|
17
|
+
TX_ID_BYTES = 32
|
|
18
|
+
INDEX_BYTES = 1
|
|
19
|
+
TOTAL_BYTES = TX_ID_BYTES + INDEX_BYTES
|
|
20
|
+
|
|
21
|
+
attr_reader :tx_id, :index, :payload_bytes, :hrp
|
|
22
|
+
|
|
23
|
+
def initialize(hrp:, payload_bytes:)
|
|
24
|
+
@payload_bytes = payload_bytes
|
|
25
|
+
@index = payload_bytes.last
|
|
26
|
+
@tx_id = payload_bytes[0, TX_ID_BYTES].pack("C*").unpack1("H*").encode(Encoding::UTF_8)
|
|
27
|
+
@hrp = hrp
|
|
28
|
+
|
|
29
|
+
validate!
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.encode(tx_ref)
|
|
33
|
+
txid_hex, index_str = tx_ref.split("#", 2)
|
|
34
|
+
index = index_str.to_i if index_str
|
|
35
|
+
|
|
36
|
+
raise InvalidFormat, "expected txid#index" unless txid_hex && index
|
|
37
|
+
raise InvalidFormat, "invalid hex" unless txid_hex.match?(/\A[0-9a-fA-F]{64}\z/)
|
|
38
|
+
|
|
39
|
+
payload_bytes = [txid_hex].pack("H*").bytes + [index]
|
|
40
|
+
|
|
41
|
+
raise InvalidPayload, "invalid payload length" unless payload_bytes.length == TOTAL_BYTES
|
|
42
|
+
raise InvalidPayload, "index must be 0..255" unless index.between?(0, 255)
|
|
43
|
+
|
|
44
|
+
data = Cardano::Bech32.convert_bits(payload_bytes, from_bits: 8, to_bits: 5, pad: true)
|
|
45
|
+
|
|
46
|
+
Cardano::Bech32.encode(HRP, data)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.decode(bech32)
|
|
50
|
+
hrp, data = Cardano::Bech32.decode(bech32)
|
|
51
|
+
raise InvalidFormat, "invalid HRP" unless hrp == HRP
|
|
52
|
+
|
|
53
|
+
decode_from_data(data)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.decode_from_data(data)
|
|
57
|
+
bytes = Cardano::Bech32.convert_bits(data, from_bits: 5, to_bits: 8, pad: false)
|
|
58
|
+
raise InvalidPayload, "invalid payload length" unless bytes.length == TOTAL_BYTES
|
|
59
|
+
|
|
60
|
+
new(hrp: HRP, payload_bytes: bytes)
|
|
61
|
+
rescue ArgumentError
|
|
62
|
+
raise InvalidFormat, "invalid bech32 encoding"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.valid?(bech32)
|
|
66
|
+
decode(bech32)
|
|
67
|
+
true
|
|
68
|
+
rescue Error
|
|
69
|
+
false
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def validate!
|
|
73
|
+
return if @payload_bytes.length == TOTAL_BYTES
|
|
74
|
+
|
|
75
|
+
raise InvalidPayload,
|
|
76
|
+
"invalid payload length: expected #{TOTAL_BYTES} bytes, got #{@payload_bytes.length}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cardano
|
|
4
|
+
module Bech32
|
|
5
|
+
module GovCredentials
|
|
6
|
+
# Header byte utilities for Governance credentials
|
|
7
|
+
module Header
|
|
8
|
+
#
|
|
9
|
+
# CIP-0129 Header byte layout:
|
|
10
|
+
#
|
|
11
|
+
# 7 6 5 4 | 3 2 1 0
|
|
12
|
+
# --------+--------
|
|
13
|
+
# credential type | key type
|
|
14
|
+
#
|
|
15
|
+
# High nibble (credential type):
|
|
16
|
+
# 0000 = CC Hot
|
|
17
|
+
# 0001 = CC Cold
|
|
18
|
+
# 0010 = DRep
|
|
19
|
+
#
|
|
20
|
+
# Lower nibble (key type):
|
|
21
|
+
# 0010 = Key hash
|
|
22
|
+
# 0011 = Script hash
|
|
23
|
+
#
|
|
24
|
+
|
|
25
|
+
# -----------------------------
|
|
26
|
+
# Key Types (upper nibble)
|
|
27
|
+
# -----------------------------
|
|
28
|
+
|
|
29
|
+
module CredentialType
|
|
30
|
+
CC_HOT = :cc_hot
|
|
31
|
+
CC_COLD = :cc_cold
|
|
32
|
+
DREP = :drep
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
CREDENTIAL_TYPE_BY_NIBBLE = {
|
|
36
|
+
0b0000 => CredentialType::CC_HOT,
|
|
37
|
+
0b0001 => CredentialType::CC_COLD,
|
|
38
|
+
0b0010 => CredentialType::DREP
|
|
39
|
+
}.freeze
|
|
40
|
+
|
|
41
|
+
NIBBLE_BY_CREDENTIAL_TYPE = CREDENTIAL_TYPE_BY_NIBBLE.invert.freeze
|
|
42
|
+
|
|
43
|
+
# -----------------------------
|
|
44
|
+
# Key Types (lower nibble)
|
|
45
|
+
# -----------------------------
|
|
46
|
+
|
|
47
|
+
module KeyType
|
|
48
|
+
KEY = :key
|
|
49
|
+
SCRIPT = :script
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
KEY_TYPE_BY_NIBBLE = {
|
|
53
|
+
0b0010 => KeyType::KEY,
|
|
54
|
+
0b0011 => KeyType::SCRIPT
|
|
55
|
+
}.freeze
|
|
56
|
+
|
|
57
|
+
NIBBLE_BY_KEY_TYPE = KEY_TYPE_BY_NIBBLE.invert.freeze
|
|
58
|
+
|
|
59
|
+
# -----------------------------
|
|
60
|
+
# HRP Mapping (by key type)
|
|
61
|
+
# -----------------------------
|
|
62
|
+
|
|
63
|
+
HRP_BY_CREDENTIAL_TYPE = {
|
|
64
|
+
CredentialType::CC_HOT => "cc_hot",
|
|
65
|
+
CredentialType::CC_COLD => "cc_cold",
|
|
66
|
+
CredentialType::DREP => "drep"
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
69
|
+
# -----------------------------
|
|
70
|
+
# Public API
|
|
71
|
+
# -----------------------------
|
|
72
|
+
|
|
73
|
+
def self.credential_type(header)
|
|
74
|
+
nibble = high_nibble(header)
|
|
75
|
+
|
|
76
|
+
CREDENTIAL_TYPE_BY_NIBBLE.fetch(nibble) do
|
|
77
|
+
raise InvalidFormat, "Unknown governance credential type: #{nibble.to_s(2)}"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.key_type(header)
|
|
82
|
+
nibble = low_nibble(header)
|
|
83
|
+
|
|
84
|
+
KEY_TYPE_BY_NIBBLE.fetch(nibble) do
|
|
85
|
+
raise InvalidFormat, "Unknown governance key type: #{nibble.to_s(2)}"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.hrp(header)
|
|
90
|
+
HRP_BY_CREDENTIAL_TYPE.fetch(credential_type(header))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# -----------------------------
|
|
94
|
+
# Header Construction
|
|
95
|
+
# -----------------------------
|
|
96
|
+
|
|
97
|
+
def self.build(key_type:, credential:)
|
|
98
|
+
cred_nibble = NIBBLE_BY_CREDENTIAL_TYPE.fetch(credential) do
|
|
99
|
+
raise ArgumentError, "Invalid credential type: #{credential}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
key_nibble = NIBBLE_BY_KEY_TYPE.fetch(key_type) do
|
|
103
|
+
raise ArgumentError, "Invalid key type: #{key_type}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
((cred_nibble << 4) | key_nibble) & 0xFF
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# -----------------------------
|
|
110
|
+
# Bit Helpers
|
|
111
|
+
# -----------------------------
|
|
112
|
+
|
|
113
|
+
def self.high_nibble(header)
|
|
114
|
+
(header & 0b1111_0000) >> 4
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.low_nibble(header)
|
|
118
|
+
header & 0b0000_1111
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "gov_credentials/header"
|
|
4
|
+
require_relative "gov_credentials/abstract_credential"
|
|
5
|
+
require_relative "gov_credentials/cc"
|
|
6
|
+
require_relative "gov_credentials/drep"
|
|
7
|
+
require_relative "gov_credentials/gov_action"
|
|
8
|
+
require_relative "gov_credentials/decode"
|
|
9
|
+
require_relative "gov_credentials/encode"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bech32"
|
|
4
|
+
|
|
5
|
+
module Cardano
|
|
6
|
+
module Bech32
|
|
7
|
+
# Module for encoding and decoding Cardano stake pool identifiers.
|
|
8
|
+
module StakePool
|
|
9
|
+
HRP = "pool"
|
|
10
|
+
POOL_HASH_LENGTH = 28 # bytes (blake2b-224)
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# Encode a stake pool hash into Bech32.
|
|
15
|
+
#
|
|
16
|
+
# @param pool_hash [String] 56-char hex string or 28-byte binary string
|
|
17
|
+
# @return [String] Bech32 stake pool id
|
|
18
|
+
def encode(pool_hash)
|
|
19
|
+
raise InvalidPayload, "pool_hash must be a String" unless pool_hash.is_a?(String)
|
|
20
|
+
|
|
21
|
+
payload =
|
|
22
|
+
if pool_hash.encoding == Encoding::BINARY
|
|
23
|
+
pool_hash
|
|
24
|
+
elsif pool_hash.match?(/\A[0-9a-fA-F]{56}\z/)
|
|
25
|
+
[pool_hash].pack("H*")
|
|
26
|
+
else
|
|
27
|
+
raise InvalidPayload, "pool_hash must be 28 bytes or 56 hex chars"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
raise InvalidPayload, "invalid pool hash length" unless payload.bytesize == POOL_HASH_LENGTH
|
|
31
|
+
|
|
32
|
+
data = Cardano::Bech32.convert_bits(payload.bytes, from_bits: 8, to_bits: 5, pad: true)
|
|
33
|
+
Cardano::Bech32.encode(HRP, data)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Decode a Bech32 stake pool id.
|
|
37
|
+
#
|
|
38
|
+
# @param bech32 [String]
|
|
39
|
+
# @return [Hash] with keys :bytes (28-byte binary string) and :hex (56-char hex string)
|
|
40
|
+
def decode(bech32)
|
|
41
|
+
hrp, data = Cardano::Bech32.decode(bech32)
|
|
42
|
+
|
|
43
|
+
raise InvalidFormat, "invalid bech32 string" unless hrp && data
|
|
44
|
+
raise InvalidFormat, "invalid stake pool HRP: #{hrp}" unless hrp == HRP
|
|
45
|
+
|
|
46
|
+
bytes = Cardano::Bech32.convert_bits(data, from_bits: 5, to_bits: 8, pad: false)
|
|
47
|
+
raise InvalidPayload, "invalid payload" unless bytes
|
|
48
|
+
raise InvalidPayload, "invalid pool hash length" unless bytes.length == POOL_HASH_LENGTH
|
|
49
|
+
|
|
50
|
+
{
|
|
51
|
+
bytes: bytes.pack("C*"),
|
|
52
|
+
hex: bytes.pack("C*").unpack1("H*")
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Check whether a Bech32 string is a valid stake pool id.
|
|
57
|
+
#
|
|
58
|
+
# @param bech32 [String]
|
|
59
|
+
# @return [Boolean]
|
|
60
|
+
def valid?(bech32)
|
|
61
|
+
decode(bech32)
|
|
62
|
+
true
|
|
63
|
+
rescue Error
|
|
64
|
+
false
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
data/lib/cardano/bech32.rb
CHANGED
|
@@ -1,11 +1,50 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "bech32/version"
|
|
4
|
-
require_relative "bech32/gov_action"
|
|
5
|
-
|
|
6
3
|
module Cardano
|
|
4
|
+
# Bech32 module for encoding and decoding Cardano Bech32 identifiers.
|
|
7
5
|
module Bech32
|
|
6
|
+
# Maximum length for a Bech32-encoded Cardano identifier.
|
|
7
|
+
# Chosen to be very high (2048) to allow all current and foreseeable
|
|
8
|
+
# identifiers while protecting against excessively long/malformed input.
|
|
9
|
+
MAX_BECH32_LENGTH = 2048
|
|
10
|
+
|
|
11
|
+
# Wrapper around bech32rb encode function.
|
|
12
|
+
# @param hrp [String] human-readable part
|
|
13
|
+
# @param data [Array<Integer>] data as array of integers
|
|
14
|
+
# @return [String] Bech32-encoded string
|
|
15
|
+
def self.encode(hrp, data)
|
|
16
|
+
::Bech32.encode(hrp, data, ::Bech32::Encoding::BECH32)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Wrapper around bech32rb decode function.
|
|
20
|
+
# @param bech32 [String]
|
|
21
|
+
# @return [Array(String, Array<Integer>), nil] human-readable part and data
|
|
22
|
+
def self.decode(bech32)
|
|
23
|
+
hrp, data, _spec = ::Bech32.decode(bech32, MAX_BECH32_LENGTH)
|
|
24
|
+
[hrp, data]
|
|
25
|
+
rescue StandardError
|
|
26
|
+
raise InvalidFormat, "invalid bech32 string"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Convert bits between different bases.
|
|
30
|
+
# @param data [Array<Integer>] input data as array of integers
|
|
31
|
+
# @param from_bits [Integer] number of bits per input value
|
|
32
|
+
# @param to_bits [Integer] number of bits per output value
|
|
33
|
+
# @param pad [Boolean] whether to pad the output
|
|
34
|
+
# @return [Array<Integer>, nil] converted data as array of integers, or nil on failure
|
|
35
|
+
def self.convert_bits(data, from_bits: 5, to_bits: 8, pad: true)
|
|
36
|
+
::Bech32.convert_bits(data, from_bits, to_bits, pad)
|
|
37
|
+
rescue StandardError
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
8
41
|
class Error < StandardError; end
|
|
9
|
-
|
|
42
|
+
class InvalidFormat < Error; end
|
|
43
|
+
class InvalidPayload < Error; end
|
|
10
44
|
end
|
|
11
45
|
end
|
|
46
|
+
|
|
47
|
+
require_relative "bech32/version"
|
|
48
|
+
require_relative "bech32/address"
|
|
49
|
+
require_relative "bech32/stake_pool"
|
|
50
|
+
require_relative "bech32/gov_credentials"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cardano-bech32
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Robin Böning
|
|
@@ -53,7 +53,19 @@ files:
|
|
|
53
53
|
- README.md
|
|
54
54
|
- Rakefile
|
|
55
55
|
- lib/cardano/bech32.rb
|
|
56
|
-
- lib/cardano/bech32/
|
|
56
|
+
- lib/cardano/bech32/address.rb
|
|
57
|
+
- lib/cardano/bech32/address/abstract_address.rb
|
|
58
|
+
- lib/cardano/bech32/address/payment.rb
|
|
59
|
+
- lib/cardano/bech32/address/stake.rb
|
|
60
|
+
- lib/cardano/bech32/gov_credentials.rb
|
|
61
|
+
- lib/cardano/bech32/gov_credentials/abstract_credential.rb
|
|
62
|
+
- lib/cardano/bech32/gov_credentials/cc.rb
|
|
63
|
+
- lib/cardano/bech32/gov_credentials/decode.rb
|
|
64
|
+
- lib/cardano/bech32/gov_credentials/drep.rb
|
|
65
|
+
- lib/cardano/bech32/gov_credentials/encode.rb
|
|
66
|
+
- lib/cardano/bech32/gov_credentials/gov_action.rb
|
|
67
|
+
- lib/cardano/bech32/gov_credentials/header.rb
|
|
68
|
+
- lib/cardano/bech32/stake_pool.rb
|
|
57
69
|
- lib/cardano/bech32/version.rb
|
|
58
70
|
- sig/cardano/bech32.rbs
|
|
59
71
|
homepage: https://github.com/lacepool/cardano-bech32
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "bech32"
|
|
4
|
-
|
|
5
|
-
module Cardano
|
|
6
|
-
module Bech32
|
|
7
|
-
# Bech32 encoding for governance action references.
|
|
8
|
-
# A governance action reference is of the form "txid#index"
|
|
9
|
-
# where txid is a 32-byte hex string and index is a 0..255 integer
|
|
10
|
-
# The Bech32 encoding has HRP "gov_action" and a payload of 33 bytes:
|
|
11
|
-
# - 32 bytes: txid (binary)
|
|
12
|
-
# - 1 byte: index (binary)
|
|
13
|
-
#
|
|
14
|
-
module GovAction
|
|
15
|
-
HRP = "gov_action"
|
|
16
|
-
TX_ID_BYTES = 32
|
|
17
|
-
INDEX_BYTES = 1
|
|
18
|
-
TOTAL_BYTES = TX_ID_BYTES + INDEX_BYTES
|
|
19
|
-
|
|
20
|
-
class Error < StandardError; end
|
|
21
|
-
class InvalidFormat < Error; end
|
|
22
|
-
class InvalidPayload < Error; end
|
|
23
|
-
|
|
24
|
-
def self.encode(tx_ref)
|
|
25
|
-
txid_hex, index_str = tx_ref.split("#", 2)
|
|
26
|
-
raise InvalidFormat, "expected txid#index" unless txid_hex && index_str
|
|
27
|
-
|
|
28
|
-
txid_bytes = hex_to_bytes(txid_hex)
|
|
29
|
-
index = Integer(index_str)
|
|
30
|
-
|
|
31
|
-
raise InvalidPayload, "txid must be 32 bytes" unless txid_bytes.bytesize == TX_ID_BYTES
|
|
32
|
-
raise InvalidPayload, "index must be 0..255" unless index.between?(0, 255)
|
|
33
|
-
|
|
34
|
-
payload = txid_bytes + [index].pack("C")
|
|
35
|
-
|
|
36
|
-
# Convert to 5-bit array
|
|
37
|
-
# args: data (array of integers), from_bits, to_bits, pad (boolean)
|
|
38
|
-
data_5bit = ::Bech32.convert_bits(payload.bytes, 8, 5, true)
|
|
39
|
-
::Bech32.encode(HRP, data_5bit, ::Bech32::Encoding::BECH32)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def self.decode(bech32)
|
|
43
|
-
hrp, data = ::Bech32.decode(bech32)
|
|
44
|
-
raise InvalidFormat, "invalid HRP" unless hrp == HRP
|
|
45
|
-
|
|
46
|
-
# Convert to 5-bit array
|
|
47
|
-
# args: data (array of integers), from_bits, to_bits, pad (boolean)
|
|
48
|
-
bytes = ::Bech32.convert_bits(data, 5, 8, false)
|
|
49
|
-
raise InvalidPayload, "invalid payload length" unless bytes.length == TOTAL_BYTES
|
|
50
|
-
|
|
51
|
-
{
|
|
52
|
-
tx_id: bytes_to_hex(bytes[0, TX_ID_BYTES]),
|
|
53
|
-
index: bytes.last
|
|
54
|
-
}
|
|
55
|
-
rescue ArgumentError
|
|
56
|
-
raise InvalidFormat, "invalid bech32 encoding"
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def self.valid?(bech32)
|
|
60
|
-
decode(bech32)
|
|
61
|
-
true
|
|
62
|
-
rescue Error
|
|
63
|
-
false
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def self.hex_to_bytes(hex)
|
|
67
|
-
raise InvalidFormat, "invalid hex" unless hex.match?(/\A[0-9a-fA-F]{64}\z/)
|
|
68
|
-
|
|
69
|
-
[hex].pack("H*")
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def self.bytes_to_hex(bytes)
|
|
73
|
-
bytes.pack("C*").unpack1("H*")
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|