adequate_crypto_address 0.1.10 → 0.2.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 +70 -0
- data/README.md +143 -29
- 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/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 +8 -7
- data/lib/adequate_crypto_address/ltc.rb +57 -0
- data/lib/adequate_crypto_address/sol.rb +36 -0
- data/lib/adequate_crypto_address/ton.rb +54 -4
- 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.rb +27 -4
- metadata +15 -46
- 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 -24
- data/spec/adequate_crypto_address/bch_spec.rb +0 -77
- data/spec/adequate_crypto_address_spec.rb +0 -231
- data/spec/spec_helper.rb +0 -17
|
@@ -15,154 +15,183 @@ module AdequateCryptoAddress
|
|
|
15
15
|
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
|
|
16
16
|
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1
|
|
17
17
|
].freeze
|
|
18
|
+
GENERATORS = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3].freeze
|
|
18
19
|
|
|
19
20
|
class << self
|
|
20
21
|
def polymod_step(pre)
|
|
21
22
|
b = pre >> 25
|
|
22
|
-
((pre & 0x1FFFFFF) << 5)
|
|
23
|
-
(-((b >>
|
|
24
|
-
|
|
25
|
-
(-((b >> 2) & 1) & 0x1ea119fa) ^ \
|
|
26
|
-
(-((b >> 3) & 1) & 0x3d4233dd) ^ \
|
|
27
|
-
(-((b >> 4) & 1) & 0x2a1462b3)
|
|
23
|
+
GENERATORS.each_with_index.reduce((pre & 0x1FFFFFF) << 5) do |checksum, (generator, index)|
|
|
24
|
+
checksum ^ (-((b >> index) & 1) & generator)
|
|
25
|
+
end
|
|
28
26
|
end
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
def decode(input, ignore_length: false, include_encoding: false)
|
|
29
|
+
hrp_string, data_string = split_input(input, ignore_length: ignore_length)
|
|
30
|
+
return nil unless hrp_string
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
# return nil if ch >= 'A'.ord && ch <= 'Z'.ord
|
|
32
|
+
hrp, checksum, casing = decode_hrp(hrp_string)
|
|
33
|
+
return nil unless hrp
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
data, checksum, casing = decode_data(data_string, checksum, casing)
|
|
36
|
+
return nil unless data
|
|
37
|
+
return nil if casing.values.all?
|
|
40
38
|
|
|
41
|
-
|
|
39
|
+
encoding = checksum_encoding(checksum)
|
|
40
|
+
return nil unless encoding
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
# buf << ch
|
|
47
|
-
# end
|
|
42
|
+
decoded = [hrp.pack('C*'), data]
|
|
43
|
+
include_encoding ? decoded << encoding : decoded
|
|
44
|
+
end
|
|
48
45
|
|
|
49
|
-
#
|
|
46
|
+
# Utility for converting bytes of data between bases. These is used for
|
|
47
|
+
# BIP 173 address encoding/decoding to convert between sequences of bytes
|
|
48
|
+
# representing 8-bit values and groups of 5 bits. Conversions may be padded
|
|
49
|
+
# with trailing 0 bits to the nearest byte boundary. Returns nil if
|
|
50
|
+
# conversion requires padding and pad is false.
|
|
51
|
+
#
|
|
52
|
+
# For example:
|
|
53
|
+
#
|
|
54
|
+
# convert_bits("\xFF\xFF", from_bits: 8, to_bits: 5, pad: true)
|
|
55
|
+
# => "\x1F\x1F\x1F\10"
|
|
56
|
+
#
|
|
57
|
+
# See https://github.com/bitcoin/bitcoin/blob/595a7bab23bc21049526229054ea1fff1a29c0bf/src/utilstrencodings.h#L154
|
|
58
|
+
def convert_bits(chunks, from_bits:, to_bits:, pad:)
|
|
59
|
+
return nil unless valid_chunks?(chunks, from_bits)
|
|
60
|
+
|
|
61
|
+
conversion = {
|
|
62
|
+
from_bits: from_bits,
|
|
63
|
+
to_bits: to_bits,
|
|
64
|
+
output_mask: (1 << to_bits) - 1,
|
|
65
|
+
buffer_mask: (1 << (from_bits + to_bits - 1)) - 1
|
|
66
|
+
}
|
|
67
|
+
buffer = 0
|
|
68
|
+
bits = 0
|
|
69
|
+
output = []
|
|
50
70
|
|
|
51
|
-
|
|
52
|
-
|
|
71
|
+
chunks.each do |chunk|
|
|
72
|
+
buffer, bits, converted = convert_chunk(chunk, buffer, bits, conversion)
|
|
73
|
+
output.concat(converted)
|
|
74
|
+
end
|
|
53
75
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
# end
|
|
76
|
+
remainder = conversion_remainder(buffer, bits, conversion, pad)
|
|
77
|
+
return nil if remainder == :invalid
|
|
57
78
|
|
|
58
|
-
|
|
59
|
-
# chk = polymod_step(chk)
|
|
60
|
-
# end
|
|
79
|
+
output << remainder if remainder
|
|
61
80
|
|
|
62
|
-
|
|
81
|
+
output
|
|
82
|
+
end
|
|
63
83
|
|
|
64
|
-
|
|
65
|
-
# buf << CHARSET[(chk >> ((5 - i) * 5)) & 0x1f]
|
|
66
|
-
# end
|
|
84
|
+
private
|
|
67
85
|
|
|
68
|
-
|
|
69
|
-
|
|
86
|
+
def split_input(input, ignore_length:)
|
|
87
|
+
input_length = input.bytesize
|
|
88
|
+
return if input_length < 8
|
|
89
|
+
return if input_length > 90 && !ignore_length
|
|
70
90
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
input_len = input.bytesize
|
|
75
|
-
have_lower = false
|
|
76
|
-
have_upper = false
|
|
91
|
+
separator = input.rindex('1')
|
|
92
|
+
return unless separator&.positive?
|
|
93
|
+
return if (input_length - separator - 1) < 6
|
|
77
94
|
|
|
78
|
-
|
|
95
|
+
[input[0...separator], input[(separator + 1)..]]
|
|
96
|
+
end
|
|
79
97
|
|
|
80
|
-
|
|
81
|
-
|
|
98
|
+
def decode_hrp(hrp_string)
|
|
99
|
+
hrp, casing = normalize_hrp(hrp_string)
|
|
100
|
+
return unless hrp
|
|
82
101
|
|
|
83
|
-
|
|
84
|
-
|
|
102
|
+
[hrp, hrp_checksum(hrp), casing]
|
|
103
|
+
end
|
|
85
104
|
|
|
105
|
+
def normalize_hrp(hrp_string)
|
|
86
106
|
hrp = []
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
hrp << ch
|
|
99
|
-
chk = polymod_step(chk) ^ (ch >> 5)
|
|
107
|
+
casing = { lower: false, upper: false }
|
|
108
|
+
|
|
109
|
+
index = 0
|
|
110
|
+
while index < hrp_string.bytesize
|
|
111
|
+
character = hrp_string.getbyte(index)
|
|
112
|
+
character, character_case = normalize_hrp_character(character)
|
|
113
|
+
return unless character
|
|
114
|
+
|
|
115
|
+
casing[character_case] = true if character_case
|
|
116
|
+
hrp << character
|
|
117
|
+
index += 1
|
|
100
118
|
end
|
|
101
119
|
|
|
102
|
-
|
|
120
|
+
[hrp, casing]
|
|
121
|
+
end
|
|
103
122
|
|
|
104
|
-
|
|
105
|
-
|
|
123
|
+
def hrp_checksum(hrp)
|
|
124
|
+
checksum = hrp.reduce(1) do |value, character|
|
|
125
|
+
polymod_step(value) ^ (character >> 5)
|
|
106
126
|
end
|
|
127
|
+
checksum = polymod_step(checksum)
|
|
128
|
+
hrp.each { |character| checksum = polymod_step(checksum) ^ (character & 0x1f) }
|
|
129
|
+
checksum
|
|
130
|
+
end
|
|
107
131
|
|
|
132
|
+
def decode_data(data_string, checksum, casing)
|
|
108
133
|
data = []
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
134
|
+
payload_length = data_string.bytesize - 6
|
|
135
|
+
|
|
136
|
+
index = 0
|
|
137
|
+
while index < data_string.bytesize
|
|
138
|
+
character = data_string.getbyte(index)
|
|
139
|
+
value = character.nobits?(0x80) ? CHARSET_REV[character] : -1
|
|
140
|
+
return if value == -1
|
|
141
|
+
|
|
142
|
+
character_case = letter_case(character)
|
|
143
|
+
casing[character_case] = true if character_case
|
|
144
|
+
checksum = polymod_step(checksum) ^ value
|
|
145
|
+
data << value if index < payload_length
|
|
146
|
+
index += 1
|
|
121
147
|
end
|
|
122
148
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
149
|
+
[data, checksum, casing]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def normalize_hrp_character(character)
|
|
153
|
+
return unless character.between?(33, 126)
|
|
154
|
+
return [character, :lower] if character.between?('a'.ord, 'z'.ord)
|
|
155
|
+
return [(character - 'A'.ord) + 'a'.ord, :upper] if character.between?('A'.ord, 'Z'.ord)
|
|
127
156
|
|
|
128
|
-
[
|
|
157
|
+
[character, nil]
|
|
129
158
|
end
|
|
130
|
-
# rubocop:enable CyclomaticComplexity,PerceivedComplexity
|
|
131
159
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
# representing 8-bit values and groups of 5 bits. Conversions may be padded
|
|
135
|
-
# with trailing 0 bits to the nearest byte boundary. Returns nil if
|
|
136
|
-
# conversion requires padding and pad is false.
|
|
137
|
-
#
|
|
138
|
-
# For example:
|
|
139
|
-
#
|
|
140
|
-
# convert_bits("\xFF\xFF", from_bits: 8, to_bits: 5, pad: true)
|
|
141
|
-
# => "\x1F\x1F\x1F\10"
|
|
142
|
-
#
|
|
143
|
-
# See https://github.com/bitcoin/bitcoin/blob/595a7bab23bc21049526229054ea1fff1a29c0bf/src/utilstrencodings.h#L154
|
|
144
|
-
def convert_bits(chunks, from_bits:, to_bits:, pad:)
|
|
145
|
-
output_mask = (1 << to_bits) - 1
|
|
146
|
-
buffer_mask = (1 << (from_bits + to_bits - 1)) - 1
|
|
160
|
+
def letter_case(character)
|
|
161
|
+
return :lower if character.between?('a'.ord, 'z'.ord)
|
|
147
162
|
|
|
148
|
-
|
|
149
|
-
|
|
163
|
+
:upper if character.between?('A'.ord, 'Z'.ord)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def checksum_encoding(checksum)
|
|
167
|
+
return :bech32 if checksum == 1
|
|
168
|
+
|
|
169
|
+
:bech32m if checksum == 0x2bc830a3
|
|
170
|
+
end
|
|
150
171
|
|
|
172
|
+
def valid_chunks?(chunks, from_bits)
|
|
173
|
+
chunks.all? { |chunk| chunk.between?(0, (1 << from_bits) - 1) }
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def convert_chunk(chunk, buffer, bits, conversion)
|
|
177
|
+
buffer = ((buffer << conversion[:from_bits]) | chunk) & conversion[:buffer_mask]
|
|
178
|
+
bits += conversion[:from_bits]
|
|
151
179
|
output = []
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
bits
|
|
155
|
-
|
|
156
|
-
bits -= to_bits
|
|
157
|
-
output << ((buffer >> bits) & output_mask)
|
|
158
|
-
end
|
|
180
|
+
|
|
181
|
+
while bits >= conversion[:to_bits]
|
|
182
|
+
bits -= conversion[:to_bits]
|
|
183
|
+
output << ((buffer >> bits) & conversion[:output_mask])
|
|
159
184
|
end
|
|
160
185
|
|
|
161
|
-
|
|
186
|
+
[buffer, bits, output]
|
|
187
|
+
end
|
|
162
188
|
|
|
163
|
-
|
|
189
|
+
def conversion_remainder(buffer, bits, conversion, pad)
|
|
190
|
+
return (buffer << (conversion[:to_bits] - bits)) & conversion[:output_mask] if pad && bits.positive?
|
|
191
|
+
return if pad
|
|
192
|
+
return :invalid if bits >= conversion[:from_bits]
|
|
164
193
|
|
|
165
|
-
|
|
194
|
+
:invalid if (buffer << (conversion[:to_bits] - bits)).anybits?(conversion[:output_mask])
|
|
166
195
|
end
|
|
167
196
|
end
|
|
168
197
|
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AdequateCryptoAddress
|
|
4
|
+
module Utils
|
|
5
|
+
# Monero's Base58 encodes fixed 8-byte blocks into 11 characters (shorter
|
|
6
|
+
# trailing blocks use fewer characters). Reference:
|
|
7
|
+
# https://github.com/monero-project/monero/blob/master/src/common/base58.cpp
|
|
8
|
+
module MoneroBase58
|
|
9
|
+
ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
|
10
|
+
FULL_BLOCK_SIZE = 8
|
|
11
|
+
FULL_ENCODED_BLOCK_SIZE = 11
|
|
12
|
+
# Decoded byte count produced by an encoded block of each valid length.
|
|
13
|
+
ENCODED_BLOCK_SIZES = [0, 2, 3, 5, 6, 7, 9, 10, 11].freeze
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def decode(string)
|
|
18
|
+
return nil if string.nil? || string.empty?
|
|
19
|
+
|
|
20
|
+
full_blocks = string.length / FULL_ENCODED_BLOCK_SIZE
|
|
21
|
+
last_size = string.length % FULL_ENCODED_BLOCK_SIZE
|
|
22
|
+
return nil unless ENCODED_BLOCK_SIZES.include?(last_size)
|
|
23
|
+
|
|
24
|
+
out = []
|
|
25
|
+
full_blocks.times do |i|
|
|
26
|
+
block = decode_block(string[i * FULL_ENCODED_BLOCK_SIZE, FULL_ENCODED_BLOCK_SIZE], FULL_BLOCK_SIZE)
|
|
27
|
+
return nil unless block
|
|
28
|
+
|
|
29
|
+
out.concat(block)
|
|
30
|
+
end
|
|
31
|
+
decode_tail(string, full_blocks, last_size, out)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def decode_tail(string, full_blocks, last_size, out)
|
|
35
|
+
return out if last_size.zero?
|
|
36
|
+
|
|
37
|
+
tail = string[full_blocks * FULL_ENCODED_BLOCK_SIZE, last_size]
|
|
38
|
+
block = decode_block(tail, ENCODED_BLOCK_SIZES.index(last_size))
|
|
39
|
+
return nil unless block
|
|
40
|
+
|
|
41
|
+
out.concat(block)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Decodes one Base58 block into exactly `size` big-endian bytes, rejecting
|
|
45
|
+
# unknown characters and values that overflow the target byte width.
|
|
46
|
+
def decode_block(chars, size)
|
|
47
|
+
num = 0
|
|
48
|
+
chars.each_char do |char|
|
|
49
|
+
index = ALPHABET.index(char)
|
|
50
|
+
return nil unless index
|
|
51
|
+
|
|
52
|
+
num = (num * 58) + index
|
|
53
|
+
end
|
|
54
|
+
return nil if num >= (1 << (size * 8))
|
|
55
|
+
|
|
56
|
+
Array.new(size) { |i| (num >> (8 * (size - 1 - i))) & 0xff }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AdequateCryptoAddress
|
|
4
|
+
module Utils
|
|
5
|
+
module Xlm
|
|
6
|
+
CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def decode(input)
|
|
11
|
+
values = input.chars.map { |c| CHARSET.index(c) }
|
|
12
|
+
return nil if values.include?(nil)
|
|
13
|
+
|
|
14
|
+
bytes = convertbits(values, 5, 8, pad: false)
|
|
15
|
+
return nil unless bytes
|
|
16
|
+
|
|
17
|
+
bytes.pack('C*')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def convertbits(data, frombits, tobits, pad: true)
|
|
21
|
+
Bech32.convert_bits(data, from_bits: frombits, to_bits: tobits, pad: pad)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AdequateCryptoAddress
|
|
4
|
+
class Xlm
|
|
5
|
+
attr_reader :address
|
|
6
|
+
|
|
7
|
+
ACCOUNT_VERSION_BYTE = 0x30
|
|
8
|
+
MUXED_VERSION_BYTE = 0x60
|
|
9
|
+
|
|
10
|
+
ACCOUNT_SIZE = 35
|
|
11
|
+
MUXED_SIZE = 43
|
|
12
|
+
MAX_LENGTH = 90 # G-addresses are 56 chars, M-addresses 69
|
|
13
|
+
|
|
14
|
+
def initialize(address)
|
|
15
|
+
@address = address
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def valid?(_type = nil)
|
|
19
|
+
!address_type.nil?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# :account (G...) or :muxed (M...) when valid, otherwise nil.
|
|
23
|
+
def address_type
|
|
24
|
+
decoded = decode_strkey
|
|
25
|
+
return nil unless decoded
|
|
26
|
+
|
|
27
|
+
case decoded.getbyte(0)
|
|
28
|
+
when ACCOUNT_VERSION_BYTE
|
|
29
|
+
:account if valid_strkey?(decoded, ACCOUNT_SIZE)
|
|
30
|
+
when MUXED_VERSION_BYTE
|
|
31
|
+
:muxed if valid_strkey?(decoded, MUXED_SIZE)
|
|
32
|
+
end
|
|
33
|
+
rescue StandardError
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def valid_strkey?(decoded, expected_size)
|
|
40
|
+
return false unless decoded.bytesize == expected_size
|
|
41
|
+
|
|
42
|
+
payload = decoded[0...-2]
|
|
43
|
+
checksum = decoded[-2..]
|
|
44
|
+
|
|
45
|
+
crc16(payload) == checksum
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def decode_strkey
|
|
49
|
+
return nil if address.to_s.length > MAX_LENGTH
|
|
50
|
+
|
|
51
|
+
Utils::Xlm.decode(address)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def crc16(data)
|
|
55
|
+
crc = 0
|
|
56
|
+
|
|
57
|
+
data.each_byte do |byte|
|
|
58
|
+
crc ^= byte << 8
|
|
59
|
+
|
|
60
|
+
8.times do
|
|
61
|
+
crc =
|
|
62
|
+
if crc.anybits?(0x8000)
|
|
63
|
+
(crc << 1) ^ 0x1021
|
|
64
|
+
else
|
|
65
|
+
crc << 1
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
[crc & 0xffff].pack('v')
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
Stellar = Xlm
|
|
75
|
+
end
|
|
@@ -1,35 +1,83 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'digest'
|
|
4
|
+
|
|
3
5
|
module AdequateCryptoAddress
|
|
6
|
+
# Validates Monero addresses by decoding the Monero Base58 payload and
|
|
7
|
+
# verifying the network byte, structural length, and Keccak-256 checksum.
|
|
8
|
+
# References: https://docs.getmonero.org/public-address/standard-address/
|
|
9
|
+
# and https://docs.getmonero.org/public-address/integrated-address/
|
|
4
10
|
class Xmr
|
|
5
|
-
|
|
11
|
+
STANDARD_LENGTH = 69 # prefix(1) + spend key(32) + view key(32) + checksum(4)
|
|
12
|
+
INTEGRATED_LENGTH = 77 # standard + payment id(8)
|
|
13
|
+
CHECKSUM_LENGTH = 4
|
|
14
|
+
ENCODED_LENGTHS = [95, 106].freeze
|
|
15
|
+
|
|
16
|
+
NETWORK_PREFIXES = {
|
|
17
|
+
standard: { 18 => :mainnet, 53 => :testnet, 24 => :stagenet },
|
|
18
|
+
integrated: { 19 => :mainnet, 54 => :testnet, 25 => :stagenet },
|
|
19
|
+
subaddress: { 42 => :mainnet, 63 => :testnet, 36 => :stagenet }
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
attr_reader :address, :type, :network
|
|
6
23
|
|
|
7
24
|
def initialize(address)
|
|
8
25
|
@address = address
|
|
9
|
-
@type =
|
|
26
|
+
@type = detect_type
|
|
10
27
|
end
|
|
11
28
|
|
|
12
|
-
def valid?(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
29
|
+
def valid?(validated_type = nil)
|
|
30
|
+
return !type.nil? unless validated_type
|
|
31
|
+
|
|
32
|
+
type == validated_type.to_sym
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# :standard, :integrated, or :subaddress when valid, otherwise nil.
|
|
36
|
+
def address_type
|
|
37
|
+
type
|
|
18
38
|
end
|
|
19
39
|
|
|
20
40
|
private
|
|
21
41
|
|
|
22
|
-
def
|
|
23
|
-
|
|
42
|
+
def detect_type
|
|
43
|
+
bytes = decode
|
|
44
|
+
return nil unless bytes
|
|
24
45
|
|
|
25
|
-
|
|
46
|
+
category, @network = classify(bytes.first)
|
|
47
|
+
return nil unless category
|
|
48
|
+
return nil unless bytes.length == expected_length(category)
|
|
49
|
+
return nil unless valid_checksum?(bytes)
|
|
50
|
+
|
|
51
|
+
category
|
|
26
52
|
end
|
|
27
53
|
|
|
28
|
-
def
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
54
|
+
def decode
|
|
55
|
+
return nil unless ENCODED_LENGTHS.include?(address.to_s.length)
|
|
56
|
+
|
|
57
|
+
Utils::MoneroBase58.decode(address)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def classify(prefix)
|
|
61
|
+
NETWORK_PREFIXES.each do |category, prefixes|
|
|
62
|
+
network = prefixes[prefix]
|
|
63
|
+
return [category, network] if network
|
|
64
|
+
end
|
|
65
|
+
[nil, nil]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def expected_length(category)
|
|
69
|
+
category == :integrated ? INTEGRATED_LENGTH : STANDARD_LENGTH
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def valid_checksum?(bytes)
|
|
73
|
+
body = bytes[0...-CHECKSUM_LENGTH]
|
|
74
|
+
keccak256(body.pack('C*')).bytes[0, CHECKSUM_LENGTH] == bytes[-CHECKSUM_LENGTH..]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def keccak256(data)
|
|
78
|
+
Digest::Keccak.new(256).digest(data)
|
|
32
79
|
end
|
|
33
80
|
end
|
|
81
|
+
|
|
34
82
|
Monero = Xmr
|
|
35
83
|
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require 'adequate_crypto_address/utils/bech32'
|
|
4
4
|
require 'adequate_crypto_address/utils/bch'
|
|
5
|
+
require 'adequate_crypto_address/utils/xlm'
|
|
6
|
+
require 'adequate_crypto_address/utils/monero_base58'
|
|
5
7
|
|
|
6
8
|
require 'adequate_crypto_address/altcoin'
|
|
7
9
|
require 'adequate_crypto_address/bch'
|
|
@@ -13,6 +15,10 @@ require 'adequate_crypto_address/zec'
|
|
|
13
15
|
require 'adequate_crypto_address/ltc'
|
|
14
16
|
require 'adequate_crypto_address/ton'
|
|
15
17
|
require 'adequate_crypto_address/xmr'
|
|
18
|
+
require 'adequate_crypto_address/doge'
|
|
19
|
+
require 'adequate_crypto_address/sol'
|
|
20
|
+
require 'adequate_crypto_address/xlm'
|
|
21
|
+
require 'adequate_crypto_address/ada'
|
|
16
22
|
|
|
17
23
|
module AdequateCryptoAddress
|
|
18
24
|
class UnknownCurrency < StandardError; end
|
|
@@ -23,12 +29,29 @@ module AdequateCryptoAddress
|
|
|
23
29
|
end
|
|
24
30
|
|
|
25
31
|
def address(address, currency)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
klass = currency_class(currency)
|
|
33
|
+
raise UnknownCurrency, "Wrong currency #{currency}" unless klass
|
|
34
|
+
|
|
35
|
+
klass.new(address)
|
|
29
36
|
end
|
|
30
37
|
|
|
38
|
+
# Public contract: returns the detected address type as a Symbol when the
|
|
39
|
+
# address is valid for the currency, or nil when it is not.
|
|
31
40
|
def address_type(address, currency)
|
|
32
|
-
|
|
41
|
+
address(address, currency).address_type
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Resolve a currency name (string or symbol, any case) to its validator class.
|
|
45
|
+
# Only classes defined directly on this module that implement the validator
|
|
46
|
+
# interface are eligible, so malformed input can never be mistaken for a
|
|
47
|
+
# currency and NoMethodError is never swallowed as UnknownCurrency.
|
|
48
|
+
def currency_class(currency)
|
|
49
|
+
const_name = currency.to_s.capitalize
|
|
50
|
+
return nil unless const_defined?(const_name, false)
|
|
51
|
+
|
|
52
|
+
klass = const_get(const_name)
|
|
53
|
+
return nil unless klass.is_a?(Class) && klass.method_defined?(:valid?)
|
|
54
|
+
|
|
55
|
+
klass
|
|
33
56
|
end
|
|
34
57
|
end
|