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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 06d868e6c483f8a82d6c21772e4d896851d15e12890b0a20417550aa1845976b
4
- data.tar.gz: d194883faa311375b65c162bccb8fbdb90b69885c902fb7cab4a7a2c036286ff
3
+ metadata.gz: d19c3b346fb19c84e8045235246add31bc765d808695fe57f7db54b0f72b1106
4
+ data.tar.gz: f60ed81681d68b0ef8a56d2b9f746a1a0c3ef1faf3026a6668fc7ad5342e2d3a
5
5
  SHA512:
6
- metadata.gz: e0365bcde4f72bcd7ed3d035fc96a92d9efd8b98574d042f6b4790456295bc4555267c6f295209a69079a19258cda8350191a37c140df7fd76f9dc3273c11758
7
- data.tar.gz: c258b992609d0d5b09adc3305e10d07eb8fa727ac49458c413706ad42bd6c52143ba69dab0532080ffa9940e5876f98d917d83b07cb7366ffe831f034e18ac74
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
- int = 0
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
- int
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
- loop do
45
- break if int.zero?
44
+ if int.zero?
45
+ bytes.push(0)
46
+ else
47
+ loop do
48
+ break if int.zero?
46
49
 
47
- bytes.push int & 255
48
- int = (int / (2**8)) | 0
50
+ bytes.push int & 255
51
+ int = (int / (2**8)) | 0
52
+ end
49
53
  end
50
54
 
51
55
  bytes.reverse
@@ -5,6 +5,8 @@ module MixinBot
5
5
  attr_accessor :hex, :raw
6
6
 
7
7
  def initialize(**args)
8
+ args = args.with_indifferent_access
9
+
8
10
  @hex = args[:hex]
9
11
  @raw = args[:raw]
10
12
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MixinBot
4
- VERSION = '1.2.4'
4
+ VERSION = '1.3.0'
5
5
  end
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.2.4
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: 2024-10-21 00:00:00.000000000 Z
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