openreceive 0.2.1

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.
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require_relative "keccak256"
5
+
6
+ module OpenReceive
7
+ # Ruby port of packages/js/core/src/swap/address.ts: address shape checks for
8
+ # swap deposit/refund networks, shared by the settlement engine so both
9
+ # engines apply one rule set.
10
+ #
11
+ # These are checksum checks, not shape guards: a refund goes to whatever
12
+ # address the payer typed, so a transposed character must be refused here
13
+ # rather than sent somewhere unrecoverable. Tron is Base58Check
14
+ # (double-SHA-256 tail over the 0x41-prefixed payload), Ethereum is verified
15
+ # against EIP-55 whenever the address carries mixed case (an all-lower or
16
+ # all-upper address has no checksum bits to verify, and wallets accept it),
17
+ # and Solana must decode to exactly a 32-byte ed25519 public key.
18
+ module SwapAddress
19
+ BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
20
+ BASE58_MAP = BASE58_ALPHABET.each_char.with_index.to_h.freeze
21
+
22
+ ETH_ADDRESS_PATTERN = /\A0x[0-9a-fA-F]{40}\z/
23
+ TRON_ADDRESS_PREFIX = 0x41
24
+ BASE58CHECK_CHECKSUM_BYTES = 4
25
+ TRON_ADDRESS_PATTERN = /\AT[1-9A-HJ-NP-Za-km-z]{33}\z/
26
+ SOLANA_ADDRESS_PATTERN = /\A[1-9A-HJ-NP-Za-km-z]{32,44}\z/
27
+
28
+ module_function
29
+
30
+ # Bitcoin/Solana base58 decode. Returns nil on invalid characters, else an
31
+ # array of byte values. Leading "1" chars are treated as leading zero
32
+ # bytes (matches JS decodeBase58, including the all-'1' zero-value input).
33
+ def decode_base58(value)
34
+ text = value.to_s
35
+ return nil if text.empty?
36
+ bytes = []
37
+ text.each_char do |char|
38
+ digit = BASE58_MAP[char]
39
+ return nil if digit.nil?
40
+ carry = digit
41
+ index = 0
42
+ while index < bytes.length
43
+ carry += bytes[index] * 58
44
+ bytes[index] = carry & 0xff
45
+ carry >>= 8
46
+ index += 1
47
+ end
48
+ while carry.positive?
49
+ bytes << (carry & 0xff)
50
+ carry >>= 8
51
+ end
52
+ end
53
+ text.each_char do |char|
54
+ break unless char == "1"
55
+ bytes << 0
56
+ end
57
+ bytes.reverse
58
+ end
59
+
60
+ def valid_solana_address?(address)
61
+ # Typical encoded length is 32–44; still require a 32-byte pubkey decode.
62
+ return false unless SOLANA_ADDRESS_PATTERN.match?(address)
63
+ decoded = decode_base58(address)
64
+ !decoded.nil? && decoded.length == 32
65
+ end
66
+
67
+ def valid_tron_address?(address)
68
+ return false unless TRON_ADDRESS_PATTERN.match?(address)
69
+ decoded = decode_base58(address)
70
+ return false if decoded.nil? || decoded.length != 21 + BASE58CHECK_CHECKSUM_BYTES
71
+ return false unless decoded[0] == TRON_ADDRESS_PREFIX
72
+
73
+ payload = decoded[0, 21].pack("C*")
74
+ expected = Digest::SHA256.digest(Digest::SHA256.digest(payload)).bytes
75
+ decoded[21, BASE58CHECK_CHECKSUM_BYTES] == expected[0, BASE58CHECK_CHECKSUM_BYTES]
76
+ end
77
+
78
+ def valid_ethereum_address?(address)
79
+ return false unless ETH_ADDRESS_PATTERN.match?(address)
80
+
81
+ body = address[2..]
82
+ lowercase = body.downcase
83
+ # No mixed case means no EIP-55 bits to verify.
84
+ return true if body == lowercase || body == body.upcase
85
+
86
+ digest = Keccak256.digest(lowercase).bytes
87
+ lowercase.each_char.with_index.all? do |character, index|
88
+ next true unless character.between?("a", "f")
89
+
90
+ nibble = index.even? ? (digest[index / 2] >> 4) : (digest[index / 2] & 0x0f)
91
+ (nibble >= 8) == (body[index] == character.upcase)
92
+ end
93
+ end
94
+
95
+ def valid_for_network?(network, address)
96
+ return false if address.length > 200 || address.match?(/\s/)
97
+ return valid_ethereum_address?(address) if network == "ETH"
98
+ return valid_solana_address?(address) if network == "SOL"
99
+ return valid_tron_address?(address) if network == "TRX" || network == "TRON"
100
+
101
+ # An unknown network has no rule to apply, so nothing may be accepted for
102
+ # it (the old `length >= 5` fallback validated almost anything).
103
+ false
104
+ end
105
+
106
+ # Resolve the address network from an OpenReceive pay_in_asset code
107
+ # (USDT_ETH → "ETH", USDT_TRON → "TRX", SOL_SOL → "SOL"), or nil.
108
+ def network_for_pay_in_asset(pay_in_asset)
109
+ suffix = pay_in_asset.to_s.split("_").last&.upcase
110
+ return "ETH" if suffix == "ETH"
111
+ return "SOL" if suffix == "SOL"
112
+ return "TRX" if suffix == "TRON" || suffix == "TRX"
113
+ nil
114
+ end
115
+
116
+ def valid_for_pay_in_asset?(pay_in_asset, address)
117
+ network = network_for_pay_in_asset(pay_in_asset)
118
+ if network.nil?
119
+ return address.length >= 5 && address.length <= 200 && !address.match?(/\s/)
120
+ end
121
+ valid_for_network?(network, address)
122
+ end
123
+
124
+ # User-facing refund address error, or nil when the address is empty
125
+ # (callers keep required/empty-field handling) or valid. Copy mirrors the
126
+ # JS getSwapRefundAddressError strings exactly.
127
+ def refund_address_error(pay_in_asset, address, network_label)
128
+ trimmed = address.to_s.strip
129
+ return nil if trimmed.empty?
130
+ return nil if valid_for_pay_in_asset?(pay_in_asset, trimmed)
131
+ network = network_for_pay_in_asset(pay_in_asset)
132
+ if network == "ETH"
133
+ if ETH_ADDRESS_PATTERN.match?(trimmed)
134
+ return "That #{network_label} address failed its checksum. Copy it again from your wallet."
135
+ end
136
+ return "That doesn't look like an #{network_label} address. Use a 0x address."
137
+ end
138
+ if network == "TRX"
139
+ if TRON_ADDRESS_PATTERN.match?(trimmed)
140
+ return "That #{network_label} address failed its checksum. Copy it again from your wallet."
141
+ end
142
+ return "That doesn't look like a #{network_label} address. Use an address starting with T."
143
+ end
144
+ "That doesn't look like a #{network_label} address. Check you pasted the full address."
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenReceive
4
+ VERSION = "0.2.1"
5
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "openreceive/version"
4
+ require_relative "openreceive/core"
5
+ require_relative "openreceive/nwc_ruby"
6
+ require_relative "openreceive/rates"
7
+ require_relative "openreceive/swap_address"
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: openreceive
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - OpenReceive
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: bigdecimal
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ description: Vector-backed Ruby helpers for OpenReceive receive-checkout contracts.
27
+ email:
28
+ - info@openreceive.org
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - CHANGELOG.md
34
+ - LICENSE
35
+ - README.md
36
+ - lib/openreceive.rb
37
+ - lib/openreceive/core.rb
38
+ - lib/openreceive/keccak256.rb
39
+ - lib/openreceive/nwc_ruby.rb
40
+ - lib/openreceive/rates.rb
41
+ - lib/openreceive/swap_address.rb
42
+ - lib/openreceive/version.rb
43
+ homepage: https://openreceive.org
44
+ licenses:
45
+ - MIT
46
+ metadata:
47
+ homepage_uri: https://openreceive.org
48
+ source_code_uri: https://github.com/openreceive/openreceive
49
+ changelog_uri: https://github.com/openreceive/openreceive/blob/master/packages/ruby/openreceive/CHANGELOG.md
50
+ bug_tracker_uri: https://github.com/openreceive/openreceive/issues
51
+ documentation_uri: https://rubydoc.info/gems/openreceive
52
+ rubygems_mfa_required: 'true'
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '3.2'
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubygems_version: 3.6.8
68
+ specification_version: 4
69
+ summary: OpenReceive Ruby core helpers
70
+ test_files: []