adequate_crypto_address 0.1.11 → 0.3.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 +86 -0
- data/README.md +145 -30
- data/bin/console +0 -1
- data/lib/adequate_crypto_address/ada.rb +127 -0
- data/lib/adequate_crypto_address/altcoin.rb +30 -14
- data/lib/adequate_crypto_address/atom.rb +53 -0
- data/lib/adequate_crypto_address/bch.rb +84 -30
- data/lib/adequate_crypto_address/btc.rb +86 -59
- data/lib/adequate_crypto_address/doge.rb +12 -0
- data/lib/adequate_crypto_address/eth.rb +17 -7
- data/lib/adequate_crypto_address/ltc.rb +45 -37
- data/lib/adequate_crypto_address/sol.rb +36 -0
- data/lib/adequate_crypto_address/ton.rb +54 -4
- data/lib/adequate_crypto_address/trx.rb +12 -0
- data/lib/adequate_crypto_address/utils/bch.rb +8 -28
- data/lib/adequate_crypto_address/utils/bech32.rb +136 -107
- data/lib/adequate_crypto_address/utils/monero_base58.rb +60 -0
- data/lib/adequate_crypto_address/utils/xlm.rb +25 -0
- data/lib/adequate_crypto_address/version.rb +1 -1
- data/lib/adequate_crypto_address/xlm.rb +75 -0
- data/lib/adequate_crypto_address/xmr.rb +63 -15
- data/lib/adequate_crypto_address/xtz.rb +18 -0
- data/lib/adequate_crypto_address.rb +30 -4
- metadata +14 -43
- data/.coveralls.yml +0 -2
- data/.gitignore +0 -16
- data/.rspec +0 -3
- data/.rubocop.yml +0 -36
- data/.travis.yml +0 -13
- data/Gemfile +0 -12
- data/Makefile +0 -6
- data/Rakefile +0 -8
- data/adequate_crypto_address.gemspec +0 -25
- data/spec/adequate_crypto_address/bch_spec.rb +0 -77
- data/spec/adequate_crypto_address_spec.rb +0 -235
- data/spec/spec_helper.rb +0 -17
|
@@ -5,6 +5,7 @@ require 'digest'
|
|
|
5
5
|
|
|
6
6
|
module AdequateCryptoAddress
|
|
7
7
|
class InvalidAddress < StandardError; end
|
|
8
|
+
|
|
8
9
|
class Bch
|
|
9
10
|
class InvalidLegacyAddress < ::AdequateCryptoAddress::InvalidAddress; end
|
|
10
11
|
class InvalidCashAddress < ::AdequateCryptoAddress::InvalidAddress; end
|
|
@@ -25,6 +26,11 @@ module AdequateCryptoAddress
|
|
|
25
26
|
]
|
|
26
27
|
}.freeze
|
|
27
28
|
DEFAULT_PREFIX = :bitcoincash
|
|
29
|
+
CASH_PREFIXES = %w[bitcoincash bchtest bchreg].freeze
|
|
30
|
+
# CashAddr size bits (version byte bits 2..0) mapped to the hash length in bytes.
|
|
31
|
+
CASH_HASH_SIZES = { 0 => 20, 1 => 24, 2 => 28, 3 => 32, 4 => 40, 5 => 48, 6 => 56, 7 => 64 }.freeze
|
|
32
|
+
LEGACY_LENGTH = 25 # 1 version byte + 20 hash bytes + 4 checksum bytes
|
|
33
|
+
MAX_LENGTH = 120
|
|
28
34
|
|
|
29
35
|
attr_reader :raw_address, :type, :payload, :prefix, :digest
|
|
30
36
|
|
|
@@ -41,23 +47,20 @@ module AdequateCryptoAddress
|
|
|
41
47
|
end
|
|
42
48
|
end
|
|
43
49
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
raise(AdequateCryptoAddress::InvalidAddress, 'Could not determine address type')
|
|
50
|
+
# Public contract: the detected legacy/cash type Symbol, or nil when invalid.
|
|
51
|
+
def address_type
|
|
52
|
+
type
|
|
50
53
|
end
|
|
51
54
|
|
|
52
55
|
def legacy_address
|
|
53
|
-
type_int =
|
|
56
|
+
type_int = type_mapping(:legacy, type)[1]
|
|
54
57
|
input = code_list_to_string([type_int] + payload + Array(digest))
|
|
55
58
|
input += Digest::SHA256.digest(Digest::SHA256.digest(input))[0..3] unless digest
|
|
56
59
|
Base58.binary_to_base58(input, :bitcoin)
|
|
57
60
|
end
|
|
58
61
|
|
|
59
62
|
def cash_address
|
|
60
|
-
type_int =
|
|
63
|
+
type_int = type_mapping(:cash, type)[1]
|
|
61
64
|
p = [type_int] + payload
|
|
62
65
|
p = convertbits(p, 8, 5)
|
|
63
66
|
checksum = calculate_cash_checksum(p)
|
|
@@ -68,7 +71,18 @@ module AdequateCryptoAddress
|
|
|
68
71
|
|
|
69
72
|
private
|
|
70
73
|
|
|
74
|
+
def type_mapping(address_code, address_type)
|
|
75
|
+
TYPE_MAP[address_code].each do |mapping|
|
|
76
|
+
return mapping if mapping.include?(address_type)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
raise(AdequateCryptoAddress::InvalidAddress, 'Could not determine address type')
|
|
80
|
+
end
|
|
81
|
+
|
|
71
82
|
def normalize
|
|
83
|
+
return unless raw_address.is_a?(String)
|
|
84
|
+
return if raw_address.length > MAX_LENGTH
|
|
85
|
+
|
|
72
86
|
begin
|
|
73
87
|
from_cash_string
|
|
74
88
|
rescue InvalidCashAddress
|
|
@@ -79,43 +93,83 @@ module AdequateCryptoAddress
|
|
|
79
93
|
end
|
|
80
94
|
|
|
81
95
|
def from_cash_string
|
|
82
|
-
|
|
83
|
-
raise(InvalidCashAddress, 'Cash address contains uppercase and lowercase characters')
|
|
84
|
-
end
|
|
96
|
+
validate_cash_address_case!
|
|
85
97
|
|
|
86
98
|
@raw_address = raw_address.downcase
|
|
87
|
-
@raw_address = "#{DEFAULT_PREFIX}:#{raw_address}"
|
|
99
|
+
@raw_address = "#{DEFAULT_PREFIX}:#{raw_address}" unless raw_address.include?(':')
|
|
88
100
|
|
|
89
101
|
@prefix, base32string = raw_address.split(':')
|
|
90
|
-
|
|
102
|
+
raise(InvalidCashAddress, 'Unsupported cash address prefix') unless CASH_PREFIXES.include?(prefix)
|
|
103
|
+
|
|
104
|
+
assign_cash_payload(decode_cash_payload(base32string))
|
|
105
|
+
end
|
|
91
106
|
|
|
92
|
-
|
|
107
|
+
def assign_cash_payload(payload_bytes)
|
|
108
|
+
version = payload_bytes.first
|
|
109
|
+
raise(InvalidCashAddress, 'Invalid cash address version byte') unless valid_cash_version?(version, payload_bytes)
|
|
110
|
+
|
|
111
|
+
@payload = payload_bytes[1..]
|
|
112
|
+
@type = cash_type(version)
|
|
113
|
+
@type = testnet_type(type) if prefix == 'bchtest'
|
|
114
|
+
end
|
|
93
115
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
116
|
+
# Version byte layout: bit 7 reserved (must be 0), bits 6..3 type, bits 2..0 size.
|
|
117
|
+
def valid_cash_version?(version, payload_bytes)
|
|
118
|
+
return false unless version&.nobits?(0x80)
|
|
119
|
+
return false unless cash_type(version)
|
|
97
120
|
|
|
98
|
-
|
|
99
|
-
|
|
121
|
+
expected = CASH_HASH_SIZES[version & 0x07]
|
|
122
|
+
(payload_bytes.length - 1) == expected
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def cash_type(version)
|
|
126
|
+
{ 0 => :p2pkh, 1 => :p2sh }[(version >> 3) & 0x0f]
|
|
100
127
|
end
|
|
101
128
|
|
|
102
129
|
def from_legacy_string
|
|
103
|
-
decoded =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
rescue StandardError
|
|
107
|
-
raise(InvalidLegacyAddress, 'Could not decode legacy address')
|
|
108
|
-
end
|
|
130
|
+
decoded = decode_legacy_address
|
|
131
|
+
raise(InvalidLegacyAddress, 'Invalid legacy address length') unless decoded.length == LEGACY_LENGTH
|
|
132
|
+
raise(InvalidLegacyAddress, 'Bad legacy address checksum') unless valid_legacy_checksum?(decoded)
|
|
109
133
|
|
|
110
|
-
@type =
|
|
134
|
+
@type = type_mapping(:legacy, decoded[0].to_i)[0]
|
|
111
135
|
@payload = decoded[1..-5]
|
|
112
|
-
@digest = decoded[-4
|
|
136
|
+
@digest = decoded[-4..]
|
|
113
137
|
@prefix = DEFAULT_PREFIX
|
|
114
|
-
|
|
115
|
-
@type = :p2shtest if prefix == 'bchtest' && type == :p2sh
|
|
116
|
-
@type = :p2pkhtest if prefix == 'bchtest' && type == :p2pkh
|
|
117
138
|
@prefix = 'bchtest' if [:p2shtest, :p2pkhtest].include?(type)
|
|
118
139
|
end
|
|
140
|
+
|
|
141
|
+
def valid_legacy_checksum?(decoded)
|
|
142
|
+
body = code_list_to_string(decoded[0...-4])
|
|
143
|
+
Digest::SHA256.digest(Digest::SHA256.digest(body))[0, 4] == code_list_to_string(decoded[-4..])
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def validate_cash_address_case!
|
|
147
|
+
return if raw_address.upcase == raw_address || raw_address.downcase == raw_address
|
|
148
|
+
|
|
149
|
+
raise(InvalidCashAddress, 'Cash address contains uppercase and lowercase characters')
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def decode_cash_payload(base32string)
|
|
153
|
+
decoded = b32decode(base32string)
|
|
154
|
+
raise(InvalidCashAddress, 'Invalid cash address encoding') if decoded.empty? || decoded.include?(nil)
|
|
155
|
+
raise(InvalidCashAddress, 'Bad cash address checksum') unless verify_cash_checksum(decoded)
|
|
156
|
+
|
|
157
|
+
# Drop the eight 5-bit checksum symbols, then map the payload back to bytes.
|
|
158
|
+
payload = convertbits(decoded[0...-8], 5, 8, pad: false)
|
|
159
|
+
raise(InvalidCashAddress, 'Invalid cash address payload') unless payload
|
|
160
|
+
|
|
161
|
+
payload
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def decode_legacy_address
|
|
165
|
+
Base58.base58_to_binary(raw_address, :bitcoin).bytes
|
|
166
|
+
rescue StandardError
|
|
167
|
+
raise(InvalidLegacyAddress, 'Could not decode legacy address')
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def testnet_type(address_type)
|
|
171
|
+
{ p2sh: :p2shtest, p2pkh: :p2pkhtest }.fetch(address_type)
|
|
172
|
+
end
|
|
119
173
|
end
|
|
120
174
|
|
|
121
175
|
Bitcoincash = Bch
|
|
@@ -2,97 +2,124 @@
|
|
|
2
2
|
|
|
3
3
|
module AdequateCryptoAddress
|
|
4
4
|
class Btc
|
|
5
|
-
|
|
5
|
+
BASE58_TYPES = {
|
|
6
|
+
'00' => :hash160,
|
|
7
|
+
'05' => :p2sh,
|
|
8
|
+
'6f' => :hash160test,
|
|
9
|
+
'c4' => :p2shtest
|
|
10
|
+
}.freeze
|
|
11
|
+
BASE58_NETWORKS = { '00' => :mainnet, '05' => :mainnet, '6f' => :testnet, 'c4' => :testnet }.freeze
|
|
12
|
+
SEGWIT_NETWORKS = { 'bc' => :mainnet, 'tb' => :testnet }.freeze
|
|
13
|
+
MAX_LENGTH = 100
|
|
14
|
+
|
|
15
|
+
attr_reader :address, :type, :network
|
|
6
16
|
alias raw_address address
|
|
7
17
|
|
|
8
18
|
def initialize(address)
|
|
9
19
|
@address = address
|
|
10
|
-
@type =
|
|
20
|
+
@type = detect_type
|
|
11
21
|
end
|
|
12
22
|
|
|
13
|
-
def valid?(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
def valid?(validated_type = nil)
|
|
24
|
+
return !type.nil? unless validated_type
|
|
25
|
+
|
|
26
|
+
type == validated_type.to_sym
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Public contract: the detected type Symbol, or nil when invalid. The
|
|
30
|
+
# SegWit type is network-agnostic; use #network to enforce mainnet/testnet.
|
|
31
|
+
def address_type
|
|
32
|
+
type
|
|
19
33
|
end
|
|
20
34
|
|
|
21
35
|
private
|
|
22
36
|
|
|
23
|
-
def
|
|
24
|
-
|
|
25
|
-
decode_segwit_address
|
|
26
|
-
rescue StandardError
|
|
27
|
-
nil
|
|
28
|
-
end
|
|
29
|
-
if segwit_decoded
|
|
30
|
-
witness_version, witness_program_hex = segwit_decoded
|
|
31
|
-
witness_program = [witness_program_hex].pack('H*')
|
|
32
|
-
|
|
33
|
-
return :segwit_v0_keyhash if witness_version == 0 && witness_program.bytesize == 20
|
|
34
|
-
|
|
35
|
-
return :segwit_v0_scripthash if witness_version == 0 && witness_program.bytesize == 32
|
|
36
|
-
|
|
37
|
-
return :taproot if witness_version == 1 && witness_program.bytesize == 32
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
base58_decoded = begin
|
|
41
|
-
decode_base58(address)
|
|
42
|
-
rescue StandardError
|
|
43
|
-
nil
|
|
44
|
-
end
|
|
45
|
-
if base58_decoded && base58_decoded.bytesize == 50 && valid_base58_address_checksum?(base58_decoded)
|
|
46
|
-
case base58_decoded[0...2]
|
|
47
|
-
when '00'
|
|
48
|
-
return :hash160
|
|
49
|
-
when '05'
|
|
50
|
-
return :p2sh
|
|
51
|
-
when '6f'
|
|
52
|
-
return :hash160test
|
|
53
|
-
when 'c4'
|
|
54
|
-
return :p2shtest
|
|
55
|
-
end
|
|
56
|
-
end
|
|
37
|
+
def detect_type
|
|
38
|
+
return nil if address.to_s.length > MAX_LENGTH
|
|
57
39
|
|
|
58
|
-
|
|
40
|
+
segwit_address_type || base58_address_type
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def segwit_address_type
|
|
44
|
+
hrp, witness_version, witness_program_hex = safely_decode_segwit_address
|
|
45
|
+
return unless witness_version
|
|
46
|
+
|
|
47
|
+
witness_program_size = witness_program_hex.length / 2
|
|
48
|
+
type = segwit_type(witness_version, witness_program_size)
|
|
49
|
+
@network = SEGWIT_NETWORKS[hrp] if type
|
|
50
|
+
type
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def segwit_type(witness_version, witness_program_size)
|
|
54
|
+
return { 20 => :segwit_v0_keyhash, 32 => :segwit_v0_scripthash }[witness_program_size] if witness_version.zero?
|
|
55
|
+
|
|
56
|
+
:taproot if witness_version == 1 && witness_program_size == 32
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def base58_address_type
|
|
60
|
+
decoded = safely_decode_base58
|
|
61
|
+
return unless decoded&.bytesize == 50
|
|
62
|
+
return unless valid_base58_address_checksum?(decoded)
|
|
63
|
+
|
|
64
|
+
version = decoded[0...2]
|
|
65
|
+
@network = BASE58_NETWORKS[version]
|
|
66
|
+
BASE58_TYPES[version]
|
|
59
67
|
end
|
|
60
68
|
|
|
61
69
|
def decode_segwit_address
|
|
62
|
-
actual_hrp, data = Utils::Bech32.decode(address)
|
|
70
|
+
actual_hrp, data, encoding = Utils::Bech32.decode(address, include_encoding: true)
|
|
71
|
+
return nil unless %w[bc tb].include?(actual_hrp)
|
|
72
|
+
return nil if data.empty? || data.size > 65
|
|
73
|
+
|
|
74
|
+
witness_version = data.first
|
|
75
|
+
return nil unless valid_witness_encoding?(witness_version, encoding)
|
|
76
|
+
|
|
77
|
+
program = Utils::Bech32.convert_bits(data[1..], from_bits: 5, to_bits: 8, pad: false)
|
|
78
|
+
return nil unless valid_witness_program?(witness_version, program)
|
|
79
|
+
|
|
80
|
+
[actual_hrp, witness_version, program.pack('C*').unpack1('H*')]
|
|
81
|
+
end
|
|
63
82
|
|
|
64
|
-
|
|
83
|
+
def valid_witness_encoding?(witness_version, encoding)
|
|
84
|
+
return false unless witness_version&.between?(0, 16)
|
|
65
85
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
86
|
+
encoding == (witness_version.zero? ? :bech32 : :bech32m)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def valid_witness_program?(witness_version, program)
|
|
90
|
+
return false unless program&.size&.between?(2, 40)
|
|
91
|
+
return [20, 32].include?(program.size) if witness_version.zero?
|
|
92
|
+
return program.size == 32 if witness_version == 1
|
|
70
93
|
|
|
71
|
-
|
|
72
|
-
|
|
94
|
+
true
|
|
95
|
+
end
|
|
73
96
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
97
|
+
def safely_decode_segwit_address
|
|
98
|
+
decode_segwit_address
|
|
99
|
+
rescue StandardError
|
|
100
|
+
[]
|
|
101
|
+
end
|
|
78
102
|
|
|
79
|
-
|
|
80
|
-
|
|
103
|
+
def safely_decode_base58
|
|
104
|
+
decode_base58
|
|
105
|
+
rescue StandardError
|
|
106
|
+
nil
|
|
81
107
|
end
|
|
82
108
|
|
|
83
|
-
def decode_base58
|
|
109
|
+
def decode_base58
|
|
84
110
|
Base58.base58_to_binary(address, :bitcoin).each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
|
|
85
111
|
end
|
|
86
112
|
|
|
87
113
|
def valid_base58_address_checksum?(base58_decoded)
|
|
88
114
|
return false unless base58_decoded
|
|
89
115
|
|
|
90
|
-
checksum(base58_decoded[0...-8]) == base58_decoded[-8
|
|
116
|
+
checksum(base58_decoded[0...-8]) == base58_decoded[-8..]
|
|
91
117
|
end
|
|
92
118
|
|
|
93
119
|
def checksum(hex)
|
|
94
120
|
Digest::SHA256.hexdigest(Digest::SHA256.digest([hex].pack('H*')))[0...8]
|
|
95
121
|
end
|
|
96
122
|
end
|
|
123
|
+
|
|
97
124
|
Bitcoin = Btc
|
|
98
125
|
end
|
|
@@ -23,7 +23,10 @@ module AdequateCryptoAddress
|
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
# :eth when the address is a valid (optionally checksummed) address, else nil.
|
|
27
|
+
def address_type
|
|
28
|
+
valid? ? :eth : nil
|
|
29
|
+
end
|
|
27
30
|
|
|
28
31
|
private
|
|
29
32
|
|
|
@@ -37,8 +40,6 @@ module AdequateCryptoAddress
|
|
|
37
40
|
normalize(cased.join)
|
|
38
41
|
end
|
|
39
42
|
|
|
40
|
-
private
|
|
41
|
-
|
|
42
43
|
def checksum_matches?
|
|
43
44
|
address == checksummed
|
|
44
45
|
end
|
|
@@ -67,17 +68,26 @@ module AdequateCryptoAddress
|
|
|
67
68
|
remove_hex_prefix address
|
|
68
69
|
end
|
|
69
70
|
|
|
70
|
-
def remove_hex_prefix(
|
|
71
|
-
|
|
71
|
+
def remove_hex_prefix(string)
|
|
72
|
+
string[0, 2] == '0x' ? string[2..] : string
|
|
72
73
|
end
|
|
73
74
|
|
|
74
75
|
def bin_to_hex(string)
|
|
75
76
|
string.unpack1('H*')
|
|
76
77
|
end
|
|
77
78
|
|
|
78
|
-
def keccak256(
|
|
79
|
-
Digest::Keccak.new(256).digest(
|
|
79
|
+
def keccak256(input)
|
|
80
|
+
Digest::Keccak.new(256).digest(input)
|
|
80
81
|
end
|
|
81
82
|
end
|
|
82
83
|
Ethereum = Eth
|
|
84
|
+
|
|
85
|
+
# BNB Smart Chain uses the same address format and EIP-55 checksum as Ethereum.
|
|
86
|
+
class Bsc < Eth
|
|
87
|
+
def address_type
|
|
88
|
+
valid? ? :bsc : nil
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
Binancesmartchain = Bsc
|
|
83
93
|
end
|
|
@@ -6,51 +6,59 @@ module AdequateCryptoAddress
|
|
|
6
6
|
|
|
7
7
|
private
|
|
8
8
|
|
|
9
|
-
def
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
nil
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
if segwit_decoded
|
|
17
|
-
witness_version, witness_program_hex, hrp = segwit_decoded
|
|
18
|
-
witness_program = [witness_program_hex].pack('H*')
|
|
19
|
-
|
|
20
|
-
if witness_version == 0
|
|
21
|
-
return :prod if hrp == 'ltc' && [20, 32].include?(witness_program.bytesize)
|
|
22
|
-
return :test if hrp == 'tltc' && [20, 32].include?(witness_program.bytesize)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
if witness_version == 1 && witness_program.bytesize == 32
|
|
26
|
-
return :prod if hrp == 'ltc'
|
|
27
|
-
return :test if hrp == 'tltc'
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
super
|
|
9
|
+
def detect_type
|
|
10
|
+
return nil if too_long?
|
|
11
|
+
|
|
12
|
+
segwit_address_type || super
|
|
32
13
|
end
|
|
33
14
|
|
|
34
|
-
def
|
|
35
|
-
|
|
15
|
+
def segwit_address_type
|
|
16
|
+
witness_version, witness_program_hex, hrp = safely_decode_segwit_address
|
|
17
|
+
return unless witness_version
|
|
36
18
|
|
|
37
|
-
|
|
19
|
+
witness_program_size = witness_program_hex.length / 2
|
|
20
|
+
return unless valid_known_witness_program?(witness_version, witness_program_size)
|
|
21
|
+
|
|
22
|
+
{ 'ltc' => :prod, 'tltc' => :test }[hrp]
|
|
23
|
+
end
|
|
38
24
|
|
|
39
|
-
|
|
40
|
-
|
|
25
|
+
def decode_segwit_address
|
|
26
|
+
actual_hrp, data, encoding = Utils::Bech32.decode(address, include_encoding: true)
|
|
41
27
|
return nil unless %w[ltc tltc].include?(actual_hrp)
|
|
42
|
-
return nil if data
|
|
28
|
+
return nil if data.empty? || data.size > 65
|
|
29
|
+
|
|
30
|
+
witness_version = data.first
|
|
31
|
+
return nil unless valid_witness_encoding?(witness_version, encoding)
|
|
43
32
|
|
|
44
|
-
program = Utils::Bech32.convert_bits(data[1
|
|
45
|
-
return nil
|
|
33
|
+
program = Utils::Bech32.convert_bits(data[1..], from_bits: 5, to_bits: 8, pad: false)
|
|
34
|
+
return nil unless valid_witness_program?(witness_version, program)
|
|
35
|
+
|
|
36
|
+
program_hex = program.pack('C*').unpack1('H*')
|
|
37
|
+
[witness_version, program_hex, actual_hrp]
|
|
38
|
+
end
|
|
46
39
|
|
|
47
|
-
|
|
48
|
-
return
|
|
49
|
-
|
|
50
|
-
|
|
40
|
+
def valid_witness_encoding?(witness_version, encoding)
|
|
41
|
+
return false unless witness_version&.between?(0, 16)
|
|
42
|
+
|
|
43
|
+
encoding == (witness_version.zero? ? :bech32 : :bech32m)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def valid_witness_program?(witness_version, program)
|
|
47
|
+
return false unless program&.size&.between?(2, 40)
|
|
48
|
+
return [20, 32].include?(program.size) if witness_version.zero?
|
|
49
|
+
return program.size == 32 if witness_version == 1
|
|
50
|
+
|
|
51
|
+
true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def valid_known_witness_program?(witness_version, program_size)
|
|
55
|
+
(witness_version.zero? && [20, 32].include?(program_size)) || (witness_version == 1 && program_size == 32)
|
|
56
|
+
end
|
|
51
57
|
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
def safely_decode_segwit_address
|
|
59
|
+
decode_segwit_address
|
|
60
|
+
rescue StandardError
|
|
61
|
+
[]
|
|
54
62
|
end
|
|
55
63
|
end
|
|
56
64
|
Litecoin = Ltc
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AdequateCryptoAddress
|
|
4
|
+
class Sol
|
|
5
|
+
ALPHABET_TYPE = :bitcoin
|
|
6
|
+
MAX_LENGTH = 64 # a 32-byte key encodes to at most 44 Base58 characters
|
|
7
|
+
|
|
8
|
+
attr_reader :address
|
|
9
|
+
|
|
10
|
+
def initialize(address_string)
|
|
11
|
+
@address = address_string
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def valid?(_type = nil)
|
|
15
|
+
valid_format?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# :solana when the address decodes to a 32-byte key, otherwise nil.
|
|
19
|
+
def address_type
|
|
20
|
+
valid_format? ? :solana : nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def valid_format?
|
|
26
|
+
return false if address.to_s.length > MAX_LENGTH
|
|
27
|
+
|
|
28
|
+
decoded = Base58.base58_to_binary(address, ALPHABET_TYPE)
|
|
29
|
+
decoded.bytesize == 32
|
|
30
|
+
rescue StandardError
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
Solana = Sol
|
|
36
|
+
end
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module AdequateCryptoAddress
|
|
4
|
+
# Validates TON user-friendly addresses (TEP-2). The 48-character Base64URL
|
|
5
|
+
# form decodes to 36 bytes: tag(1) + workchain(1) + account_id(32) + crc16(2).
|
|
4
6
|
class Ton
|
|
7
|
+
ADDRESS_LENGTH = 48
|
|
8
|
+
DECODED_LENGTH = 36
|
|
9
|
+
BOUNCEABLE_TAG = 0x11
|
|
10
|
+
NON_BOUNCEABLE_TAG = 0x51
|
|
11
|
+
TEST_FLAG = 0x80
|
|
12
|
+
VALID_WORKCHAINS = [0x00, 0xff].freeze
|
|
13
|
+
|
|
5
14
|
attr_reader :address, :type
|
|
6
15
|
|
|
7
16
|
def initialize(address)
|
|
@@ -17,14 +26,55 @@ module AdequateCryptoAddress
|
|
|
17
26
|
end
|
|
18
27
|
end
|
|
19
28
|
|
|
29
|
+
# :ton_mainnet or :ton_testnet when valid, otherwise nil.
|
|
30
|
+
def address_type
|
|
31
|
+
bytes = decode
|
|
32
|
+
return nil unless bytes
|
|
33
|
+
|
|
34
|
+
tag = bytes[0]
|
|
35
|
+
return nil unless valid_tag?(tag)
|
|
36
|
+
return nil unless VALID_WORKCHAINS.include?(bytes[1])
|
|
37
|
+
return nil unless checksum_valid?(bytes)
|
|
38
|
+
|
|
39
|
+
tag.anybits?(TEST_FLAG) ? :ton_testnet : :ton_mainnet
|
|
40
|
+
end
|
|
41
|
+
|
|
20
42
|
private
|
|
21
43
|
|
|
22
|
-
def
|
|
23
|
-
return
|
|
44
|
+
def decode
|
|
45
|
+
return nil unless valid_encoding?
|
|
46
|
+
|
|
47
|
+
# Base64URL decode via core unpack (the base64 gem is only a wrapper).
|
|
48
|
+
bytes = address.tr('-_', '+/').unpack1('m0')&.bytes
|
|
49
|
+
bytes if bytes && bytes.length == DECODED_LENGTH
|
|
50
|
+
rescue ArgumentError
|
|
51
|
+
nil
|
|
24
52
|
end
|
|
25
53
|
|
|
26
|
-
def
|
|
27
|
-
address.
|
|
54
|
+
def valid_encoding?
|
|
55
|
+
address.is_a?(String) &&
|
|
56
|
+
address.length == ADDRESS_LENGTH &&
|
|
57
|
+
address.match?(/\A[A-Za-z0-9_-]{#{ADDRESS_LENGTH}}\z/)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def valid_tag?(tag)
|
|
61
|
+
[BOUNCEABLE_TAG, NON_BOUNCEABLE_TAG].include?(tag & ~TEST_FLAG)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def checksum_valid?(bytes)
|
|
65
|
+
crc = crc16(bytes[0...34])
|
|
66
|
+
[(crc >> 8) & 0xff, crc & 0xff] == bytes[34..35]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# CRC16-CCITT (XMODEM): polynomial 0x1021, initial value 0x0000.
|
|
70
|
+
def crc16(bytes)
|
|
71
|
+
bytes.reduce(0) do |crc, byte|
|
|
72
|
+
crc ^= byte << 8
|
|
73
|
+
8.times { crc = crc.anybits?(0x8000) ? ((crc << 1) ^ 0x1021) & 0xffff : (crc << 1) & 0xffff }
|
|
74
|
+
crc
|
|
75
|
+
end
|
|
28
76
|
end
|
|
29
77
|
end
|
|
78
|
+
|
|
79
|
+
Toncoin = Ton
|
|
30
80
|
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AdequateCryptoAddress
|
|
4
|
+
# TRON mainnet addresses are Base58Check with the 0x41 version byte and a
|
|
5
|
+
# four-byte double-SHA256 checksum (25 bytes total, rendered as a 34-character
|
|
6
|
+
# string starting with "T"). https://developers.tron.network/docs/account
|
|
7
|
+
class Trx < Altcoin
|
|
8
|
+
ADDRESS_TYPES = { prod: %w[41] }.freeze
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
Tron = Trx
|
|
12
|
+
end
|