crypto_address_validator 0.2

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a5acb742dfaf129cf78b31bd02f819a1f004e8e6b6243f3f4b440a4902175987
4
+ data.tar.gz: 566d0f3c14dbcbdb22474bdfe1964e53d073a241ad5c9d843934cd8104c6dab7
5
+ SHA512:
6
+ metadata.gz: 32e065985182686f76333525bba80e2277978a2577c074ad0904c5a36bdbf58a515993e521a4b89a124e70e9ed657d152ccb39f5421c0389910a02b2346d44f9
7
+ data.tar.gz: ca0739bc1a05275bbe620dd704116b9ec85cf273795e0f6796df37fdc1136bd6e4ab5be9f0caa5a1a02ab06532c206c2430441bf545bff28486d034928bbe84e
data/.coveralls.yml ADDED
@@ -0,0 +1,2 @@
1
+ repo_token: ATgUXu3s0jZaYdzAdUr8P8ytuMMbR9ueh
2
+
data/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ Gemfile.lock
11
+
12
+ # rspec failure tracking
13
+ .rspec_status
14
+
15
+ # Spec artifacts
16
+ /coverage
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,36 @@
1
+ require: rubocop-rspec
2
+
3
+ AllCops:
4
+ DisplayCopNames: true
5
+
6
+ Naming/AccessorMethodName:
7
+ Description: Check the naming of accessor methods for get_/set_.
8
+ Enabled: false
9
+
10
+ Style/DocumentationMethod:
11
+ RequireForNonPublicMethods: false
12
+
13
+ Documentation:
14
+ Enabled: false
15
+
16
+ Style/ClassAndModuleChildren:
17
+ Enabled: false
18
+
19
+ Style/NegatedIf:
20
+ Enabled: false
21
+
22
+ Metrics/LineLength:
23
+ Max: 120
24
+
25
+ Metrics/MethodLength:
26
+ Max: 30
27
+
28
+ Style/SymbolArray:
29
+ Enabled: true
30
+ EnforcedStyle: brackets
31
+
32
+ Style/StringLiterals:
33
+ EnforcedStyle: single_quotes
34
+ SupportedStyles:
35
+ - single_quotes
36
+
data/.travis.yml ADDED
@@ -0,0 +1,13 @@
1
+ ---
2
+ language: ruby
3
+ sudo: false
4
+ cache: bundler
5
+ rvm:
6
+ - 2.5.1
7
+ before_install: gem install bundler -v 1.16.3
8
+ script: bundle exec rspec spec
9
+ notifications:
10
+ email:
11
+ - justin@sequoir.com
12
+ after_success:
13
+ - CI=true TRAVIS=true coveralls --verbose
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ gem 'coveralls', '>= 0.8.17', require: false
8
+ gem 'pry', require: false
9
+
10
+ gem 'rubocop-rspec'
11
+ # Specify your gem's dependencies in crypto_address_validator.gemspec
12
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 vtm
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/Makefile ADDED
@@ -0,0 +1,6 @@
1
+
2
+ build:
3
+ gem build crypto_address_validator.gemspec
4
+
5
+ push:
6
+ gem push crypto_address_validator-0.1.5.gem
data/README.md ADDED
@@ -0,0 +1,150 @@
1
+
2
+ CryptoAddressValidator
3
+ ===============
4
+
5
+ [![Gem Version][gem-version-svg]][gem-version-link]
6
+ [![Build Status][build-status-svg]][build-status-link]
7
+ [![Code Climate][codeclimate-status-svg]][codeclimate-status-link]
8
+ [![Coverage Status][coverage-status-svg]][coverage-status-link]
9
+ [![Downloads][downloads-svg]][downloads-link]
10
+ [![Docs][docs-rubydoc-svg]][docs-rubydoc-link]
11
+ [![License][license-svg]][license-link]
12
+
13
+ Simple wallet address validator and normalizer for cryptocurrencies addresses in Ruby.
14
+
15
+ Inspired by [vtm9/adequate_crypto_address](https://github.com/vtm9/adequate_crypto_address).
16
+
17
+
18
+ ## Installation
19
+
20
+ Add this line to your application's Gemfile:
21
+
22
+ ```ruby
23
+ gem 'crypto_address_validator'
24
+ ```
25
+
26
+ Or install it yourself as:
27
+
28
+ ```bash
29
+ gem install crypto_address_validator
30
+ ```
31
+
32
+ ## Main API
33
+
34
+ ##### .valid? (address, currency [, type = :prod, type = [])
35
+
36
+ ###### Parameters
37
+ * address - Wallet address to validate.
38
+ * currency - Currency name string or symbol in any case, `:bitcoin` or `'BTC'` or `:btc` or `'BitCoin'`
39
+ * type - Optional. You can enforce validation with specific type or an array of types. Not all currencies support types.
40
+
41
+ > Returns true if the address (string) is a valid wallet address for the crypto currency specified, see below for supported currencies.
42
+
43
+ ### Supported crypto currencies
44
+
45
+ * Bitcoin/BTC, `'bitcoin'` or `'BTC'` types: `:segwit_v0_keyhash :segwit_v0_scripthash :hash160 :p2sh`
46
+ * BitcoinCash/BCH, `'bitcoincash'` or `'BCH'` types: `:p2sh :p2pkh :p2pkhtest :p2shtest`
47
+ * Dash, `'dash'` or `'DASH'` types: `:prod :test`
48
+ * Zcash/ZEC, `'zcash'` or `'ZEC'` types: `:prod :test`
49
+ * Ethereum/ETH, `'ethereum'` or `'ETH'`
50
+ * Ripple/XRP, `'ripple'` or `'XRP'`
51
+
52
+ ## Usage
53
+
54
+ ### Validation
55
+ ``` ruby
56
+ require 'crypto_address_validator'
57
+ # BTC
58
+ CryptoAddressValidator.valid?('12QeMLzSrB8XH8FvEzPMVoRxVAzTr5XM2y', 'BTC') #=> true
59
+ CryptoAddressValidator.valid?('3NJZLcZEEYBpxYEUGewU4knsQRn1WM5Fkt', :bitcoin, :p2sh) #=> true
60
+
61
+ # BTC(non SegWit address validation)
62
+ CryptoAddressValidator.valid?('3NJZLcZEEYBpxYEUGewU4knsQRn1WM5Fkt', :bitcoin, [:p2sh, :hash160]) #=> true
63
+
64
+ # BCH
65
+ CryptoAddressValidator.valid?('bitcoincash:qrtj3rd8524cndt2eew3s6wljqggmne00sgh4kfypk', :bch) #=> true
66
+ CryptoAddressValidator.valid?('mmRH4e9WW4ekZUP5HvBScfUyaSUjfQRyvD', :BCH, :p2pkhtest) #=> true
67
+
68
+ # ETH
69
+ CryptoAddressValidator.valid?('0xde709f2102306220921060314715629080e2fb77', :ETH) #=> true
70
+ CryptoAddressValidator.valid?('de709f2102306220921060314715629080e2fb77', :ethereum) #=> true
71
+ ```
72
+
73
+ ### Normalization
74
+ ###### *Not all currencies support this feature.
75
+ ``` ruby
76
+ require 'crypto_address_validator'
77
+
78
+ # BCH
79
+ CryptoAddressValidator.address('mmRH4e9WW4ekZUP5HvBScfUyaSUjfQRyvD', 'bch').cash_address #=> "bchtest:qpqtmmfpw79thzq5z7s0spcd87uhn6d34uqqem83hf"
80
+ CryptoAddressValidator.address('bitcoincash:qrtj3rd8524cndt2eew3s6wljqggmne00sgh4kfypk', 'bch').legacy_address #=> "1LcerwTc1oPsMtByDCNUXFxReZpN1EXHoe"
81
+
82
+ address_string = 'qrtj3rd8524cndt2eew3s6wljqggmne00sgh4kfypk'
83
+ addr = CryptoAddressValidator.address(address_string, 'bch')
84
+ addr.prefix #=> "bitcoincash"
85
+ addr.type #=> :p2pkh
86
+ addr.address #=> "bitcoincash:qrtj3rd8524cndt2eew3s6wljqggmne00sgh4kfypk"
87
+
88
+ # ETH
89
+ CryptoAddressValidator.address('D1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb', 'eth').address #=> "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb"
90
+ ```
91
+
92
+
93
+ ### ActiveRecord validation example
94
+ ``` ruby
95
+ class Model < ActiveRecord::Base
96
+ attribute :address, :string
97
+ attribute :dest_tag, :string
98
+ attribute :currency, :string
99
+
100
+ validate :validate_address_type
101
+ validate :validate_destination_tag
102
+
103
+ def validate_address_type
104
+ errors.add(:address, 'invalid address') unless CryptoAddressValidator.valid?(address, currency)
105
+ end
106
+
107
+ # for Ripple
108
+ def validate_destination_tag
109
+ errors.add(:dest_tag, 'invalid destination tag') if dest_tag.present? && !(dest_tag =~ /\A\d{1,10}\z/)
110
+ end
111
+ end
112
+
113
+ ```
114
+ ### Add your currnecy
115
+ ``` ruby
116
+ # frozen_string_literal: true
117
+ # for Rails /config/initializers/crypto_address_validator.rb
118
+ module CryptoAddressValidator
119
+ class Coin
120
+ attr_reader :address
121
+
122
+ def initialize(address_sring)
123
+ @address = address_sring
124
+ end
125
+
126
+ def valid?(_type)
127
+ address.present?
128
+ end
129
+ end
130
+ end
131
+
132
+ CryptoAddressValidator.valid?('addr', :coin) #=> true
133
+ ```
134
+
135
+ ## Development
136
+
137
+ Run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
138
+
139
+ ## Contributing
140
+
141
+ 1. Fork [the repo](https://github.com/sequoir/crypto_address_validator)
142
+ 2. Grab dependencies: `bundle install`
143
+ 3. Make sure everything is working: `bundle exec rake spec`
144
+ 4. Make your changes
145
+ 5. Test your changes
146
+ 5. Create a Pull Request
147
+
148
+ ## Notes
149
+
150
+ Bug reports and pull requests are welcome on GitHub at https://github.com/sequoir/crypto_address_validator/issues
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
5
+ require 'crypto_address_validator'
6
+
7
+ ACA = CryptoAddressValidator
8
+ include CryptoAddressValidator
9
+
10
+ require 'pry'
11
+ Pry.start
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'crypto_address_validator/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'crypto_address_validator'
9
+ spec.version = CryptoAddressValidator::VERSION
10
+ spec.authors = ['sequoir']
11
+ spec.email = ['engineering@sequoir.com']
12
+
13
+ spec.summary = 'A Ruby Library for dealing with validation cryptocurrency adresses.'
14
+ spec.description = 'A Ruby Library for dealing with validation cryptocurrency adresses.'
15
+ spec.homepage = 'https://github.com/sequoir/crypto_address_validator'
16
+
17
+ spec.license = 'MIT'
18
+ spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'base58', '~> 0.2'
22
+ spec.add_dependency 'keccak', '1.2'
23
+ spec.add_development_dependency 'rake', '~> 13.0'
24
+ spec.add_development_dependency 'rspec', '~> 3.10'
25
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CryptoAddressValidator
4
+ # simple class for common altcoin addresses like legacy btc, ripple, dash, zec, etc.
5
+ class Altcoin
6
+ EXPECTED_LENGTH = 50
7
+ ADDRESS_TYPES = {}.freeze
8
+ ALPHABET_TYPE = :bitcoin
9
+
10
+ attr_reader :address, :type
11
+ alias raw_address address
12
+
13
+ def initialize(address)
14
+ @address = address
15
+ @type = address_type
16
+ end
17
+
18
+ def valid?(validated_type = nil)
19
+ if validated_type
20
+ type == validated_type.to_sym
21
+ else
22
+ !type.nil?
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :decoded
29
+
30
+ def address_type
31
+ @decoded = begin
32
+ decode_base58
33
+ rescue StandardError
34
+ nil
35
+ end
36
+ if decoded && decoded.bytesize == self.class::EXPECTED_LENGTH && valid_address_checksum?
37
+ self.class::ADDRESS_TYPES.each do |net_type, net_prefixes|
38
+ net_prefixes.each do |net_prefix|
39
+ return net_type if decoded.start_with?(net_prefix)
40
+ end
41
+ end
42
+ end
43
+ nil
44
+ end
45
+
46
+ def decode_base58
47
+ Base58.base58_to_binary(address, self.class::ALPHABET_TYPE).each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
48
+ end
49
+
50
+ def valid_address_checksum?
51
+ return false unless decoded
52
+
53
+ checksum(decoded[0...-8]) == decoded[-8..-1]
54
+ end
55
+
56
+ def checksum(bytes)
57
+ Digest::SHA256.hexdigest(Digest::SHA256.digest([bytes].pack('H*')))[0...8]
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base58'
4
+ require 'digest'
5
+
6
+ module CryptoAddressValidator
7
+ class InvalidAddress < StandardError; end
8
+ class Bch
9
+ class InvalidLegacyAddress < ::CryptoAddressValidator::InvalidAddress; end
10
+ class InvalidCashAddress < ::CryptoAddressValidator::InvalidAddress; end
11
+ include ::CryptoAddressValidator::Utils::Bch
12
+
13
+ TYPE_MAP = {
14
+ legacy: [
15
+ [:p2sh, 5],
16
+ [:p2pkh, 0],
17
+ [:p2shtest, 196],
18
+ [:p2pkhtest, 111]
19
+ ],
20
+ cash: [
21
+ [:p2sh, 8],
22
+ [:p2pkh, 0],
23
+ [:p2shtest, 8],
24
+ [:p2pkhtest, 0]
25
+ ]
26
+ }.freeze
27
+ DEFAULT_PREFIX = :bitcoincash
28
+
29
+ attr_reader :raw_address, :type, :payload, :prefix, :digest
30
+
31
+ def initialize(address)
32
+ @raw_address = address
33
+ normalize
34
+ end
35
+
36
+ def valid?(validated_type = nil)
37
+ if validated_type
38
+ type == validated_type.to_sym
39
+ else
40
+ !type.nil?
41
+ end
42
+ end
43
+
44
+ def address_type(address_code, address_type)
45
+ TYPE_MAP[address_code].each do |mapping|
46
+ return mapping if mapping.include?(address_type)
47
+ end
48
+
49
+ raise(CryptoAddressValidator::InvalidAddress, 'Could not determine address type')
50
+ end
51
+
52
+ def legacy_address
53
+ type_int = address_type(:legacy, type)[1]
54
+ input = code_list_to_string([type_int] + payload + Array(digest))
55
+ input += Digest::SHA256.digest(Digest::SHA256.digest(input))[0..3] unless digest
56
+ Base58.binary_to_base58(input, :bitcoin)
57
+ end
58
+
59
+ def cash_address
60
+ type_int = address_type(:cash, type)[1]
61
+ p = [type_int] + payload
62
+ p = convertbits(p, 8, 5)
63
+ checksum = calculate_cash_checksum(p)
64
+ "#{prefix}:#{b32encode(p + checksum)}"
65
+ end
66
+
67
+ alias address cash_address
68
+
69
+ private
70
+
71
+ def normalize
72
+ begin
73
+ from_cash_string
74
+ rescue InvalidCashAddress
75
+ from_legacy_string
76
+ end
77
+ rescue CryptoAddressValidator::InvalidAddress
78
+ nil
79
+ end
80
+
81
+ def from_cash_string
82
+ if (raw_address.upcase != raw_address) && (raw_address.downcase != raw_address)
83
+ raise(InvalidCashAddress, 'Cash address contains uppercase and lowercase characters')
84
+ end
85
+
86
+ @raw_address = raw_address.downcase
87
+ @raw_address = "#{DEFAULT_PREFIX}:#{raw_address}" if !raw_address.include?(':')
88
+
89
+ @prefix, base32string = raw_address.split(':')
90
+ decoded = b32decode(base32string)
91
+
92
+ raise(InvalidCashAddress, 'Bad cash address checksum') if !verify_cash_checksum(decoded)
93
+
94
+ converted = convertbits(decoded, 5, 8)
95
+ @type = address_type(:cash, converted[0].to_i)[0]
96
+ @payload = converted[1..-7]
97
+
98
+ @type = :p2shtest if prefix == 'bchtest' && type == :p2sh
99
+ @type = :p2pkhtest if prefix == 'bchtest' && type == :p2pkh
100
+ end
101
+
102
+ def from_legacy_string
103
+ decoded = nil
104
+ begin
105
+ decoded = Base58.base58_to_binary(raw_address, :bitcoin).bytes
106
+ rescue StandardError
107
+ raise(InvalidLegacyAddress, 'Could not decode legacy address')
108
+ end
109
+
110
+ @type = address_type(:legacy, decoded[0].to_i)[0]
111
+ @payload = decoded[1..-5]
112
+ @digest = decoded[-4..-1]
113
+ @prefix = DEFAULT_PREFIX
114
+
115
+ @type = :p2shtest if prefix == 'bchtest' && type == :p2sh
116
+ @type = :p2pkhtest if prefix == 'bchtest' && type == :p2pkh
117
+ @prefix = 'bchtest' if [:p2shtest, :p2pkhtest].include?(type)
118
+ end
119
+ end
120
+
121
+ Bitcoincash = Bch
122
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CryptoAddressValidator
4
+ class Btc
5
+ attr_reader :address, :type
6
+ alias raw_address address
7
+
8
+ def initialize(address)
9
+ @address = address
10
+ @type = address_type
11
+ end
12
+
13
+ def valid?(types = nil)
14
+ if types
15
+ types = [types].flatten.map(&:to_sym)
16
+ types.include?(address_type)
17
+ else
18
+ !address_type.nil?
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def address_type
25
+ segwit_decoded = begin
26
+ decode_segwit_address
27
+ rescue StandardError
28
+ nil
29
+ end
30
+ if segwit_decoded
31
+ witness_version, witness_program_hex = segwit_decoded
32
+ witness_program = [witness_program_hex].pack('H*')
33
+
34
+ return :segwit_v0_keyhash if witness_version == 0 && witness_program.bytesize == 20
35
+
36
+ return :segwit_v0_scripthash if witness_version == 0 && witness_program.bytesize == 32
37
+ end
38
+
39
+ base58_decoded = begin
40
+ decode_base58(address)
41
+ rescue StandardError
42
+ nil
43
+ end
44
+ if base58_decoded && base58_decoded.bytesize == 50 && valid_base58_address_checksum?(base58_decoded)
45
+ case base58_decoded[0...2]
46
+ when '00'
47
+ return :hash160
48
+ when '05'
49
+ return :p2sh
50
+ when '6f'
51
+ return :hash160test
52
+ when 'c4'
53
+ return :p2shtest
54
+ end
55
+ end
56
+
57
+ nil
58
+ end
59
+
60
+ def decode_segwit_address
61
+ actual_hrp, data = Utils::Bech32.decode(address)
62
+
63
+ return nil if actual_hrp.nil?
64
+
65
+ length = data.size
66
+ return nil if length == 0 || length > 65
67
+ return nil if actual_hrp != 'bc'
68
+ return nil if data[0] > 16
69
+
70
+ program = Utils::Bech32.convert_bits(data[1..-1], from_bits: 5, to_bits: 8, pad: false)
71
+ return nil if program.nil?
72
+
73
+ length = program.size
74
+ return nil if length < 2 || length > 40
75
+ return nil if data[0] == 0 && length != 20 && length != 32
76
+
77
+ program_hex = program.pack('C*').unpack('H*').first
78
+ [data[0], program_hex]
79
+ end
80
+
81
+ def decode_base58(_base58_val)
82
+ Base58.base58_to_binary(address, :bitcoin).each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
83
+ end
84
+
85
+ def valid_base58_address_checksum?(base58_decoded)
86
+ return false unless base58_decoded
87
+
88
+ checksum(base58_decoded[0...-8]) == base58_decoded[-8..-1]
89
+ end
90
+
91
+ def checksum(hex)
92
+ Digest::SHA256.hexdigest(Digest::SHA256.digest([hex].pack('H*')))[0...8]
93
+ end
94
+ end
95
+ Bitcoin = Btc
96
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CryptoAddressValidator
4
+ class Dash < Altcoin
5
+ ADDRESS_TYPES = { prod: %w[4c 10], test: %w[8c 13] }.freeze
6
+ end
7
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CryptoAddressValidator
4
+ class Eth
5
+ attr_reader :address, :raw_address
6
+
7
+ def initialize(address_sring)
8
+ @address = normalize(address_sring)
9
+ @raw_address = address_sring
10
+ end
11
+
12
+ def normalize(address_sring)
13
+ /\A0x/.match?(address_sring) ? address_sring : "0x#{address_sring}"
14
+ end
15
+
16
+ def valid?(_type = nil)
17
+ if !valid_format?
18
+ false
19
+ elsif not_checksummed?
20
+ true
21
+ else
22
+ checksum_matches?
23
+ end
24
+ end
25
+
26
+ def address_type; end
27
+
28
+ private
29
+
30
+ def checksummed
31
+ raise "Invalid address: #{address}" unless valid_format?
32
+
33
+ cased = unprefixed.chars.zip(checksum.chars).map do |char, check|
34
+ /[0-7]/.match?(check) ? char.downcase : char.upcase
35
+ end
36
+
37
+ normalize(cased.join)
38
+ end
39
+
40
+ private
41
+
42
+ def checksum_matches?
43
+ address == checksummed
44
+ end
45
+
46
+ def not_checksummed?
47
+ all_uppercase? || all_lowercase?
48
+ end
49
+
50
+ def all_uppercase?
51
+ address.match(/(?:0[xX])[A-F0-9]{40}/)
52
+ end
53
+
54
+ def all_lowercase?
55
+ address.match(/(?:0[xX])[a-f0-9]{40}/)
56
+ end
57
+
58
+ def valid_format?
59
+ address.match(/\A(?:0[xX])[a-fA-F0-9]{40}\z/)
60
+ end
61
+
62
+ def checksum
63
+ bin_to_hex(keccak256(unprefixed.downcase))
64
+ end
65
+
66
+ def unprefixed
67
+ remove_hex_prefix address
68
+ end
69
+
70
+ def remove_hex_prefix(s)
71
+ s[0, 2] == '0x' ? s[2..-1] : s
72
+ end
73
+
74
+ def bin_to_hex(string)
75
+ string.unpack1('H*')
76
+ end
77
+
78
+ def keccak256(x)
79
+ Digest::Keccak.new(256).digest(x)
80
+ end
81
+ end
82
+ Ethereum = Eth
83
+ end