mixin_bot 1.2.5 → 1.3.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 +4 -4
- data/lib/mixin_bot/address.rb +173 -0
- data/lib/mixin_bot/api/message.rb +4 -1
- 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 +5 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 436b9a0d785744f840ab00dbb6643a372fcba52a1a2091c8c86f9fd1e0c08b30
|
4
|
+
data.tar.gz: 895cd928e05c763ef9452ab564ceb357a1c512f52ce7b57ca69b7067380d7233
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c9d2183496bde43efaed75052508ad9791bcf5c3842709a764bac499815d817a8b3222a602ba0f6140b624855aaf3c88f08e945cb19e6ede0f290b2c308edb57
|
7
|
+
data.tar.gz: c085749d2fc91b1aa9104d48e46eb1cfd4a765b18c8c7095e71f11b714d8572dff05f03e69f51e5a67d3f196ca6cc8b0c49ee63db76452940a2d1f9ccd9f4c0c
|
@@ -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
|
@@ -95,7 +95,10 @@ module MixinBot
|
|
95
95
|
|
96
96
|
# read the gzipped message form websocket
|
97
97
|
def ws_message(data)
|
98
|
-
|
98
|
+
data = data.pack('c*') if data.is_a?(Array)
|
99
|
+
raise MixinBot::ArgumentError, 'data should be String or Array of integer' unless data.is_a?(String)
|
100
|
+
|
101
|
+
io = StringIO.new(data, 'rb')
|
99
102
|
gzip = Zlib::GzipReader.new io
|
100
103
|
msg = gzip.read
|
101
104
|
gzip.close
|
@@ -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,14 +1,13 @@
|
|
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.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- an-lee
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: activesupport
|
@@ -245,6 +244,7 @@ files:
|
|
245
244
|
- MIT-LICENSE
|
246
245
|
- bin/mixinbot
|
247
246
|
- lib/mixin_bot.rb
|
247
|
+
- lib/mixin_bot/address.rb
|
248
248
|
- lib/mixin_bot/api.rb
|
249
249
|
- lib/mixin_bot/api/address.rb
|
250
250
|
- lib/mixin_bot/api/app.rb
|
@@ -281,6 +281,7 @@ files:
|
|
281
281
|
- lib/mixin_bot/cli/utils.rb
|
282
282
|
- lib/mixin_bot/client.rb
|
283
283
|
- lib/mixin_bot/configuration.rb
|
284
|
+
- lib/mixin_bot/invoice.rb
|
284
285
|
- lib/mixin_bot/nfo.rb
|
285
286
|
- lib/mixin_bot/transaction.rb
|
286
287
|
- lib/mixin_bot/utils.rb
|
@@ -306,7 +307,6 @@ licenses:
|
|
306
307
|
- MIT
|
307
308
|
metadata:
|
308
309
|
rubygems_mfa_required: 'true'
|
309
|
-
post_install_message:
|
310
310
|
rdoc_options: []
|
311
311
|
require_paths:
|
312
312
|
- lib
|
@@ -321,8 +321,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
321
321
|
- !ruby/object:Gem::Version
|
322
322
|
version: '0'
|
323
323
|
requirements: []
|
324
|
-
rubygems_version: 3.
|
325
|
-
signing_key:
|
324
|
+
rubygems_version: 3.6.9
|
326
325
|
specification_version: 4
|
327
326
|
summary: A Ruby SDK for Mixin Nexwork
|
328
327
|
test_files: []
|