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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +30 -0
- data/LICENSE +21 -0
- data/README.md +62 -0
- data/lib/openreceive/core.rb +611 -0
- data/lib/openreceive/keccak256.rb +94 -0
- data/lib/openreceive/nwc_ruby.rb +72 -0
- data/lib/openreceive/rates.rb +498 -0
- data/lib/openreceive/swap_address.rb +147 -0
- data/lib/openreceive/version.rb +5 -0
- data/lib/openreceive.rb +7 -0
- metadata +70 -0
|
@@ -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
|
data/lib/openreceive.rb
ADDED
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: []
|