mixin_bot 1.2.4 → 1.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/lib/mixin_bot/address.rb +173 -0
- data/lib/mixin_bot/invoice.rb +183 -0
- data/lib/mixin_bot/utils/decoder.rb +2 -5
- data/lib/mixin_bot/utils/encoder.rb +8 -4
- data/lib/mixin_bot/uuid.rb +2 -0
- data/lib/mixin_bot/version.rb +1 -1
- data/lib/mixin_bot.rb +3 -0
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d19c3b346fb19c84e8045235246add31bc765d808695fe57f7db54b0f72b1106
|
4
|
+
data.tar.gz: f60ed81681d68b0ef8a56d2b9f746a1a0c3ef1faf3026a6668fc7ad5342e2d3a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5189f995c82669d2a5f005dad509f78ca16d588d0991fcb4efd9ef26abb12811c4a2c4fdc51ffaace80bd94090491992bcde5babce3dd2cc9bff7611a222c92d
|
7
|
+
data.tar.gz: 97e934c3b91abb3da8a3a3a1c0b3a16f45c5d0abca360b443586bba5f737df8330db9ac93deb53700b9d0af24f0d990c7bb6c2b83f9184a09582e4ae2d35d6e2
|
@@ -0,0 +1,173 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MixinBot
|
4
|
+
MAIN_ADDRESS_PREFIX = 'XIN'
|
5
|
+
MIX_ADDRESS_PREFIX = 'MIX'
|
6
|
+
MIX_ADDRESS_VERSION = 2
|
7
|
+
UUID_ADDRESS_LENGTH = 16
|
8
|
+
MAIN_ADDRESS_LENGTH = 64
|
9
|
+
|
10
|
+
class MixAddress
|
11
|
+
attr_accessor :version, :uuid_members, :xin_members, :threshold, :address, :payload
|
12
|
+
|
13
|
+
def initialize(**args)
|
14
|
+
args = args.with_indifferent_access
|
15
|
+
|
16
|
+
if args[:address]
|
17
|
+
@address = args[:address]
|
18
|
+
decode
|
19
|
+
elsif args[:payload]
|
20
|
+
@payload = args[:payload]
|
21
|
+
decode
|
22
|
+
else
|
23
|
+
@version = args[:version] || MIX_ADDRESS_VERSION
|
24
|
+
|
25
|
+
if args[:members].present?
|
26
|
+
@uuid_members = args[:members].reject { |member| member.start_with?(MAIN_ADDRESS_PREFIX) }
|
27
|
+
@xin_members = args[:members].select { |member| member.start_with? MAIN_ADDRESS_PREFIX }
|
28
|
+
else
|
29
|
+
@uuid_members = args[:uuid_members] || []
|
30
|
+
@xin_members = args[:xin_members] || []
|
31
|
+
end
|
32
|
+
|
33
|
+
@uuid_members = @uuid_members.sort
|
34
|
+
@xin_members = @xin_members.sort
|
35
|
+
|
36
|
+
@threshold = args[:threshold]
|
37
|
+
encode
|
38
|
+
end
|
39
|
+
|
40
|
+
raise ArgumentError, 'invalid address' unless valid?
|
41
|
+
end
|
42
|
+
|
43
|
+
def valid?
|
44
|
+
address.present? && (uuid_members.present? || xin_members.present?) && threshold.present?
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_safe_recipient
|
48
|
+
{
|
49
|
+
members: uuid_members + xin_members,
|
50
|
+
threshold:,
|
51
|
+
amount:,
|
52
|
+
mix_address: address
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
def encode
|
57
|
+
raise ArgumentError, 'members should be an array' unless uuid_members.is_a?(Array) || xin_members.is_a?(Array)
|
58
|
+
raise ArgumentError, 'members should not be empty' if uuid_members.empty? && xin_members.empty?
|
59
|
+
raise ArgumentError, 'members length should less than 256' if uuid_members.length + xin_members.length > 255
|
60
|
+
raise ArgumentError, "invalid threshold: #{threshold}" if threshold > (uuid_members.length + xin_members.length)
|
61
|
+
|
62
|
+
prefix =
|
63
|
+
[version].pack('C*') +
|
64
|
+
[threshold].pack('C*') +
|
65
|
+
[uuid_members.length + xin_members.length].pack('C*')
|
66
|
+
msg =
|
67
|
+
uuid_members&.map(&->(member) { MixinBot::UUID.new(hex: member).packed })&.join.to_s +
|
68
|
+
xin_members&.map(&->(member) { MainAddress.new(address: member).public_key })&.join.to_s
|
69
|
+
|
70
|
+
self.payload = prefix + msg
|
71
|
+
|
72
|
+
checksum = SHA3::Digest::SHA256.digest(MIX_ADDRESS_PREFIX + payload)
|
73
|
+
data = payload + checksum[0...4]
|
74
|
+
data = Base58.binary_to_base58 data, :bitcoin
|
75
|
+
self.address = "#{MIX_ADDRESS_PREFIX}#{data}"
|
76
|
+
|
77
|
+
address
|
78
|
+
end
|
79
|
+
|
80
|
+
def decode
|
81
|
+
if address.present?
|
82
|
+
raise ArgumentError, 'invalid address' unless address&.start_with? MIX_ADDRESS_PREFIX
|
83
|
+
|
84
|
+
data = address[MIX_ADDRESS_PREFIX.length..]
|
85
|
+
data = Base58.base58_to_binary data, :bitcoin
|
86
|
+
raise ArgumentError, 'invalid address, length invalid' if data.length < 3 + 16 + 4
|
87
|
+
|
88
|
+
self.payload = data[...-4]
|
89
|
+
checksum = SHA3::Digest::SHA256.digest(MIX_ADDRESS_PREFIX + payload)[0...4]
|
90
|
+
raise ArgumentError, 'invalid address, checksum invalid' unless checksum == data[-4..]
|
91
|
+
else
|
92
|
+
checksum = SHA3::Digest::SHA256.digest(MIX_ADDRESS_PREFIX + payload)[0...4]
|
93
|
+
data = payload + checksum
|
94
|
+
data = Base58.binary_to_base58 data, :bitcoin
|
95
|
+
self.address = "#{MIX_ADDRESS_PREFIX}#{data}"
|
96
|
+
end
|
97
|
+
|
98
|
+
self.version = payload[0].ord
|
99
|
+
raise ArgumentError, 'invalid address, version invalid' unless version.is_a?(Integer)
|
100
|
+
|
101
|
+
self.threshold = payload[1].ord
|
102
|
+
raise ArgumentError, 'invalid address, threshold invalid' unless threshold.is_a?(Integer)
|
103
|
+
|
104
|
+
members_count = payload[2].ord
|
105
|
+
raise ArgumentError, 'invalid address, members count invalid' unless members_count.is_a?(Integer)
|
106
|
+
|
107
|
+
if payload[3...].length == members_count * UUID_ADDRESS_LENGTH
|
108
|
+
uuid_members = payload[3...].chars.each_slice(UUID_ADDRESS_LENGTH).map(&:join)
|
109
|
+
self.uuid_members = uuid_members.map(&->(member) { MixinBot::UUID.new(raw: member).unpacked })
|
110
|
+
self.xin_members = []
|
111
|
+
else
|
112
|
+
xin_members = payload[3...].chars.each_slice(MAIN_ADDRESS_LENGTH).map(&:join)
|
113
|
+
self.xin_members = xin_members.map(&->(member) { MainAddress.new(public_key: member).address })
|
114
|
+
self.uuid_members = []
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
class MainAddress
|
120
|
+
attr_accessor :public_key, :address
|
121
|
+
|
122
|
+
def initialize(**args)
|
123
|
+
if args[:address]
|
124
|
+
@address = args[:address]
|
125
|
+
decode
|
126
|
+
else
|
127
|
+
@public_key = args[:public_key]
|
128
|
+
encode
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def encode
|
133
|
+
msg = MAIN_ADDRESS_PREFIX + public_key
|
134
|
+
checksum = SHA3::Digest::SHA256.digest msg
|
135
|
+
data = public_key + checksum[0...4]
|
136
|
+
base58 = Base58.binary_to_base58 data, :bitcoin
|
137
|
+
self.address = "#{MAIN_ADDRESS_PREFIX}#{base58}"
|
138
|
+
|
139
|
+
address
|
140
|
+
end
|
141
|
+
|
142
|
+
def decode
|
143
|
+
raise ArgumentError, 'invalid address' unless address.start_with? MAIN_ADDRESS_PREFIX
|
144
|
+
|
145
|
+
data = address[MAIN_ADDRESS_PREFIX.length..]
|
146
|
+
data = Base58.base58_to_binary data, :bitcoin
|
147
|
+
|
148
|
+
payload = data[...-4]
|
149
|
+
|
150
|
+
msg = MAIN_ADDRESS_PREFIX + payload
|
151
|
+
checksum = SHA3::Digest::SHA256.digest msg
|
152
|
+
|
153
|
+
raise ArgumentError, 'invalid address' unless checksum[0...4] == data[-4..]
|
154
|
+
|
155
|
+
self.public_key = payload
|
156
|
+
|
157
|
+
public_key
|
158
|
+
end
|
159
|
+
|
160
|
+
def self.burning_address
|
161
|
+
seed = "\0" * 64
|
162
|
+
|
163
|
+
digest1 = SHA3::Digest::SHA256.digest seed
|
164
|
+
digest2 = SHA3::Digest::SHA256.digest digest1
|
165
|
+
src = digest1 + digest2
|
166
|
+
|
167
|
+
spend_key = MixinBot::Utils.shared_public_key(seed)
|
168
|
+
view_key = MixinBot::Utils.shared_public_key(src)
|
169
|
+
|
170
|
+
MainAddress.new(public_key: spend_key + view_key)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MixinBot
|
4
|
+
class Invoice
|
5
|
+
INVOICE_PREFIX = 'MIN'
|
6
|
+
INVOICE_VERSION = 0x00
|
7
|
+
|
8
|
+
attr_accessor :version, :recipient, :entries, :address
|
9
|
+
|
10
|
+
def initialize(**args)
|
11
|
+
args = args.with_indifferent_access
|
12
|
+
|
13
|
+
if args[:address]
|
14
|
+
@address = args[:address]
|
15
|
+
decode
|
16
|
+
else
|
17
|
+
@version = args[:version] || INVOICE_VERSION
|
18
|
+
@recipient = args[:recipient]
|
19
|
+
@entries = args[:entries] || []
|
20
|
+
encode
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_hash
|
25
|
+
{
|
26
|
+
version:,
|
27
|
+
address:,
|
28
|
+
recipient: recipient.address,
|
29
|
+
entries: entries.map(&:to_hash)
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def encode
|
34
|
+
# Start with empty payload
|
35
|
+
payload = []
|
36
|
+
|
37
|
+
# Add version
|
38
|
+
payload += MixinBot.utils.encode_int(version)
|
39
|
+
|
40
|
+
# Add recipient - ensure we're using the raw payload bytes
|
41
|
+
recipient_payload = recipient.payload
|
42
|
+
recipient_bytes = recipient_payload.is_a?(String) ? recipient_payload.bytes : recipient_payload
|
43
|
+
payload += MixinBot.utils.encode_uint16(recipient_bytes.size)
|
44
|
+
|
45
|
+
payload += recipient_bytes
|
46
|
+
|
47
|
+
# Add entries
|
48
|
+
payload += MixinBot.utils.encode_int(entries.size)
|
49
|
+
entries.each do |entry|
|
50
|
+
payload += entry.encode
|
51
|
+
end
|
52
|
+
|
53
|
+
# Convert payload to binary string
|
54
|
+
payload = payload.pack('C*')
|
55
|
+
|
56
|
+
# Calculate checksum
|
57
|
+
checksum = SHA3::Digest::SHA256.digest(INVOICE_PREFIX + payload)[0...4]
|
58
|
+
|
59
|
+
# Combine everything and encode to base64
|
60
|
+
self.address = INVOICE_PREFIX + Base64.urlsafe_encode64(payload + checksum, padding: false)
|
61
|
+
end
|
62
|
+
|
63
|
+
def decode
|
64
|
+
prefix = address[0..2]
|
65
|
+
raise MixinBot::InvalidInvoiceFormatError, 'invalid invoice prefix' unless prefix == INVOICE_PREFIX
|
66
|
+
|
67
|
+
data = Base64.urlsafe_decode64(address[3..])
|
68
|
+
raise MixinBot::InvalidInvoiceFormatError, 'invalid invoice payload size' if data.size < 3 + 23 + 1
|
69
|
+
|
70
|
+
payload = data[...-4]
|
71
|
+
checksum = SHA3::Digest::SHA256.digest(INVOICE_PREFIX + payload)[0...4]
|
72
|
+
raise MixinBot::InvalidInvoiceFormatError, 'invalid invoice checksum' unless checksum == data[-4..]
|
73
|
+
|
74
|
+
payload = payload.bytes
|
75
|
+
|
76
|
+
# Read version
|
77
|
+
self.version = MixinBot.utils.decode_int payload.shift(1)
|
78
|
+
raise MixinBot::InvalidInvoiceFormatError, 'invalid invoice version' unless version == INVOICE_VERSION
|
79
|
+
|
80
|
+
# Read recipient with proper size handling
|
81
|
+
recipient_size = MixinBot.utils.decode_uint16 payload.shift(2)
|
82
|
+
recipient_bytes = payload.shift(recipient_size)
|
83
|
+
self.recipient = MixinBot::MixAddress.new(payload: recipient_bytes.pack('C*'))
|
84
|
+
|
85
|
+
# decode entries
|
86
|
+
entries_size = MixinBot.utils.decode_int payload.shift(1)
|
87
|
+
entries = []
|
88
|
+
entries_size.times do
|
89
|
+
next if payload.empty?
|
90
|
+
|
91
|
+
trace_id_bytes = payload.shift(16)
|
92
|
+
trace_id = MixinBot::UUID.new(raw: trace_id_bytes.pack('C*')).unpacked
|
93
|
+
|
94
|
+
asset_id_bytes = payload.shift(16)
|
95
|
+
asset_id = MixinBot::UUID.new(raw: asset_id_bytes.pack('C*')).unpacked
|
96
|
+
|
97
|
+
amount_size = MixinBot.utils.decode_int payload.shift(1)
|
98
|
+
amount_bytes = payload.shift(amount_size)
|
99
|
+
amount = amount_bytes.pack('C*').to_d
|
100
|
+
|
101
|
+
extra_size = MixinBot.utils.decode_int payload.shift(2)
|
102
|
+
extra = payload.shift(extra_size).pack('C*')
|
103
|
+
|
104
|
+
references_count = MixinBot.utils.decode_int payload.shift(1)
|
105
|
+
hash_references = []
|
106
|
+
index_references = []
|
107
|
+
|
108
|
+
references_count.times do
|
109
|
+
rv = MixinBot.utils.decode_int payload.shift(1)
|
110
|
+
case rv
|
111
|
+
when 0
|
112
|
+
hash_references << payload.shift(32).pack('C*').unpack1('H*')
|
113
|
+
when 1
|
114
|
+
index_references << MixinBot.utils.decode_int(payload.shift(1))
|
115
|
+
else
|
116
|
+
raise MixinBot::InvalidInvoiceFormatError, "invalid invoice reference type: #{rv}"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
entries << InvoiceEntry.new(trace_id:, asset_id:, amount:, extra:, index_references:, hash_references:)
|
121
|
+
end
|
122
|
+
|
123
|
+
self.entries = entries
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
class InvoiceEntry
|
128
|
+
attr_accessor :trace_id, :asset_id, :amount, :extra, :index_references, :hash_references
|
129
|
+
|
130
|
+
def initialize(**args)
|
131
|
+
args = args.with_indifferent_access
|
132
|
+
|
133
|
+
@trace_id = args[:trace_id]
|
134
|
+
@asset_id = args[:asset_id]
|
135
|
+
@amount = args[:amount].to_d
|
136
|
+
@extra = args[:extra]
|
137
|
+
@index_references = args[:index_references]
|
138
|
+
@hash_references = args[:hash_references]
|
139
|
+
end
|
140
|
+
|
141
|
+
def encode
|
142
|
+
bytes = []
|
143
|
+
|
144
|
+
bytes += MixinBot::UUID.new(hex: trace_id).packed.bytes
|
145
|
+
bytes += MixinBot::UUID.new(hex: asset_id).packed.bytes
|
146
|
+
|
147
|
+
amount_string = amount.to_d.to_s('F')
|
148
|
+
amount_bytes = amount_string.bytes
|
149
|
+
bytes += MixinBot.utils.encode_int(amount_bytes.size)
|
150
|
+
bytes += amount_bytes
|
151
|
+
|
152
|
+
extra_bytes = extra.bytes
|
153
|
+
bytes += MixinBot.utils.encode_uint16(extra_bytes.size)
|
154
|
+
bytes += extra_bytes
|
155
|
+
|
156
|
+
references_count = (index_references || []).size + (hash_references || []).size
|
157
|
+
bytes += MixinBot.utils.encode_int(references_count)
|
158
|
+
|
159
|
+
index_references&.each do |index|
|
160
|
+
bytes += MixinBot.utils.encode_int(1)
|
161
|
+
bytes += MixinBot.utils.encode_int(index)
|
162
|
+
end
|
163
|
+
|
164
|
+
hash_references&.each do |hash|
|
165
|
+
bytes += MixinBot.utils.encode_int(0)
|
166
|
+
bytes += [hash].pack('H*').bytes
|
167
|
+
end
|
168
|
+
|
169
|
+
bytes
|
170
|
+
end
|
171
|
+
|
172
|
+
def to_hash
|
173
|
+
{
|
174
|
+
trace_id:,
|
175
|
+
asset_id:,
|
176
|
+
amount:,
|
177
|
+
extra:,
|
178
|
+
index_references:,
|
179
|
+
hash_references:
|
180
|
+
}
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -40,12 +40,9 @@ module MixinBot
|
|
40
40
|
end
|
41
41
|
|
42
42
|
def decode_int(bytes)
|
43
|
-
|
44
|
-
bytes.each do |byte|
|
45
|
-
int = (int * (2**8)) + byte
|
46
|
-
end
|
43
|
+
raise ArgumentError, "only support bytes #{bytes}" unless bytes.is_a?(Array)
|
47
44
|
|
48
|
-
|
45
|
+
bytes.reduce(0) { |sum, byte| (sum << 8) + byte }
|
49
46
|
end
|
50
47
|
|
51
48
|
def hex_to_uuid(hex)
|
@@ -41,11 +41,15 @@ module MixinBot
|
|
41
41
|
raise ArgumentError, 'not integer' unless int.is_a?(Integer)
|
42
42
|
|
43
43
|
bytes = []
|
44
|
-
|
45
|
-
|
44
|
+
if int.zero?
|
45
|
+
bytes.push(0)
|
46
|
+
else
|
47
|
+
loop do
|
48
|
+
break if int.zero?
|
46
49
|
|
47
|
-
|
48
|
-
|
50
|
+
bytes.push int & 255
|
51
|
+
int = (int / (2**8)) | 0
|
52
|
+
end
|
49
53
|
end
|
50
54
|
|
51
55
|
bytes.reverse
|
data/lib/mixin_bot/uuid.rb
CHANGED
data/lib/mixin_bot/version.rb
CHANGED
data/lib/mixin_bot.rb
CHANGED
@@ -20,8 +20,10 @@ require 'openssl'
|
|
20
20
|
require 'rbnacl'
|
21
21
|
require 'sha3'
|
22
22
|
|
23
|
+
require_relative 'mixin_bot/address'
|
23
24
|
require_relative 'mixin_bot/api'
|
24
25
|
require_relative 'mixin_bot/cli'
|
26
|
+
require_relative 'mixin_bot/invoice'
|
25
27
|
require_relative 'mixin_bot/utils'
|
26
28
|
require_relative 'mixin_bot/nfo'
|
27
29
|
require_relative 'mixin_bot/uuid'
|
@@ -70,4 +72,5 @@ module MixinBot
|
|
70
72
|
class InvalidUuidFormatError < Error; end
|
71
73
|
class InvalidTransactionFormatError < Error; end
|
72
74
|
class ConfigurationNotValidError < Error; end
|
75
|
+
class InvalidInvoiceFormatError < Error; end
|
73
76
|
end
|
metadata
CHANGED
@@ -1,27 +1,27 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mixin_bot
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- an-lee
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-02-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '7'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '7'
|
27
27
|
- !ruby/object:Gem::Dependency
|
@@ -245,6 +245,7 @@ files:
|
|
245
245
|
- MIT-LICENSE
|
246
246
|
- bin/mixinbot
|
247
247
|
- lib/mixin_bot.rb
|
248
|
+
- lib/mixin_bot/address.rb
|
248
249
|
- lib/mixin_bot/api.rb
|
249
250
|
- lib/mixin_bot/api/address.rb
|
250
251
|
- lib/mixin_bot/api/app.rb
|
@@ -281,6 +282,7 @@ files:
|
|
281
282
|
- lib/mixin_bot/cli/utils.rb
|
282
283
|
- lib/mixin_bot/client.rb
|
283
284
|
- lib/mixin_bot/configuration.rb
|
285
|
+
- lib/mixin_bot/invoice.rb
|
284
286
|
- lib/mixin_bot/nfo.rb
|
285
287
|
- lib/mixin_bot/transaction.rb
|
286
288
|
- lib/mixin_bot/utils.rb
|