mixin_bot 0.12.0 → 1.0.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/api/address.rb +21 -0
- data/lib/mixin_bot/api/app.rb +5 -11
- data/lib/mixin_bot/api/asset.rb +9 -16
- data/lib/mixin_bot/api/attachment.rb +27 -22
- data/lib/mixin_bot/api/auth.rb +31 -53
- data/lib/mixin_bot/api/blaze.rb +4 -3
- data/lib/mixin_bot/api/collectible.rb +60 -58
- data/lib/mixin_bot/api/conversation.rb +29 -49
- data/lib/mixin_bot/api/encrypted_message.rb +17 -17
- data/lib/mixin_bot/api/legacy_multisig.rb +87 -0
- data/lib/mixin_bot/api/legacy_output.rb +50 -0
- data/lib/mixin_bot/api/legacy_payment.rb +31 -0
- data/lib/mixin_bot/api/legacy_snapshot.rb +39 -0
- data/lib/mixin_bot/api/legacy_transaction.rb +173 -0
- data/lib/mixin_bot/api/legacy_transfer.rb +42 -0
- data/lib/mixin_bot/api/me.rb +13 -17
- data/lib/mixin_bot/api/message.rb +13 -10
- data/lib/mixin_bot/api/multisig.rb +16 -221
- data/lib/mixin_bot/api/output.rb +46 -0
- data/lib/mixin_bot/api/payment.rb +9 -20
- data/lib/mixin_bot/api/pin.rb +57 -65
- data/lib/mixin_bot/api/rpc.rb +9 -11
- data/lib/mixin_bot/api/snapshot.rb +15 -29
- data/lib/mixin_bot/api/tip.rb +43 -0
- data/lib/mixin_bot/api/transaction.rb +184 -60
- data/lib/mixin_bot/api/transfer.rb +64 -32
- data/lib/mixin_bot/api/user.rb +83 -53
- data/lib/mixin_bot/api/withdraw.rb +52 -53
- data/lib/mixin_bot/api.rb +78 -45
- data/lib/mixin_bot/cli/api.rb +151 -8
- data/lib/mixin_bot/cli/utils.rb +14 -4
- data/lib/mixin_bot/cli.rb +13 -10
- data/lib/mixin_bot/client.rb +76 -127
- data/lib/mixin_bot/configuration.rb +98 -0
- data/lib/mixin_bot/nfo.rb +174 -0
- data/lib/mixin_bot/transaction.rb +505 -0
- data/lib/mixin_bot/utils/address.rb +108 -0
- data/lib/mixin_bot/utils/crypto.rb +182 -0
- data/lib/mixin_bot/utils/decoder.rb +58 -0
- data/lib/mixin_bot/utils/encoder.rb +63 -0
- data/lib/mixin_bot/utils.rb +8 -109
- data/lib/mixin_bot/uuid.rb +41 -0
- data/lib/mixin_bot/version.rb +1 -1
- data/lib/mixin_bot.rb +39 -14
- data/lib/mvm/bridge.rb +2 -19
- data/lib/mvm/client.rb +11 -33
- data/lib/mvm/nft.rb +4 -4
- data/lib/mvm/registry.rb +9 -9
- data/lib/mvm/scan.rb +3 -5
- data/lib/mvm.rb +5 -6
- metadata +101 -44
- data/lib/mixin_bot/utils/nfo.rb +0 -176
- data/lib/mixin_bot/utils/transaction.rb +0 -478
- data/lib/mixin_bot/utils/uuid.rb +0 -43
data/lib/mixin_bot/client.rb
CHANGED
@@ -4,153 +4,102 @@ module MixinBot
|
|
4
4
|
class Client
|
5
5
|
SERVER_SCHEME = 'https'
|
6
6
|
|
7
|
-
attr_reader :
|
8
|
-
|
9
|
-
def initialize(
|
10
|
-
@
|
7
|
+
attr_reader :config, :conn
|
8
|
+
|
9
|
+
def initialize(config)
|
10
|
+
@config = config || MixinBot.config
|
11
|
+
@conn = Faraday.new(
|
12
|
+
url: "#{SERVER_SCHEME}://#{config.api_host}",
|
13
|
+
headers: {
|
14
|
+
'Content-Type' => 'application/json',
|
15
|
+
'User-Agent' => "mixin_bot/#{MixinBot::VERSION}"
|
16
|
+
}
|
17
|
+
) do |f|
|
18
|
+
f.request :json
|
19
|
+
f.request :retry
|
20
|
+
f.response :json
|
21
|
+
f.response :logger if config.debug
|
22
|
+
end
|
11
23
|
end
|
12
24
|
|
13
|
-
def get(path,
|
14
|
-
request(:get, path,
|
25
|
+
def get(path, *, **)
|
26
|
+
request(:get, path, *, **)
|
15
27
|
end
|
16
28
|
|
17
|
-
def post(path,
|
18
|
-
request(:post, path,
|
29
|
+
def post(path, *, **)
|
30
|
+
request(:post, path, *, **)
|
19
31
|
end
|
20
32
|
|
21
33
|
private
|
22
34
|
|
23
|
-
def request(verb, path,
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
options[:headers]['Content-Type'] ||= 'application/json'
|
28
|
-
|
29
|
-
begin
|
30
|
-
response = HTTP.timeout(connect: 5, write: 5, read: 5).request(verb, uri, options)
|
31
|
-
rescue HTTP::Error => e
|
32
|
-
raise HttpError, e.message
|
33
|
-
end
|
34
|
-
|
35
|
-
raise RequestError, response.to_s unless response.status.success?
|
36
|
-
|
37
|
-
parse_response(response) do |parse_as, result|
|
38
|
-
case parse_as
|
39
|
-
when :json
|
40
|
-
if result['error'].nil?
|
41
|
-
result.merge! result['data'] if result['data'].is_a? Hash
|
42
|
-
break result
|
43
|
-
end
|
35
|
+
def request(verb, path, *args, **kwargs)
|
36
|
+
access_token = kwargs.delete :access_token
|
37
|
+
exp_in = kwargs.delete(:exp_in) || 600
|
38
|
+
scp = kwargs.delete(:scp) || 'FULL'
|
44
39
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
# 202 403 Forbidden.
|
51
|
-
# 202 404 The endpoint is not found.
|
52
|
-
# 202 429 Too Many Requests.
|
53
|
-
# 202 10006 App update required.
|
54
|
-
# 202 20116 The group chat is full.
|
55
|
-
# 500 500 Internal Server Error.
|
56
|
-
# 500 7000 Blaze server error.
|
57
|
-
# 500 7001 The blaze operation timeout.
|
58
|
-
# 202 10002 Illegal request paramters.
|
59
|
-
# 202 20117 Insufficient balance。
|
60
|
-
# 202 20118 PIN format error.
|
61
|
-
# 202 20119 PIN error.
|
62
|
-
# 202 20120 Transfer amount is too small.
|
63
|
-
# 202 20121 Authorization code has expired.
|
64
|
-
# 202 20124 Insufficient withdrawal fee.
|
65
|
-
# 202 20125 The transfer has been paid by someone else.
|
66
|
-
# 202 20127 The withdrawal amount is too small.
|
67
|
-
# 202 20131 Withdrawal Memo format error.
|
68
|
-
# 500 30100 The current asset's public chain synchronization error.
|
69
|
-
# 500 30101 Wrong private key.
|
70
|
-
# 500 30102 Wrong withdrawal address.
|
71
|
-
# 500 30103 Insufficient pool.
|
72
|
-
# 500 7000 WebSocket server error.
|
73
|
-
# 500 7001 WebSocket operation timeout.
|
74
|
-
case result['error']['code']
|
75
|
-
when 401, 20121
|
76
|
-
raise UnauthorizedError, errmsg
|
77
|
-
when 403, 20116, 10002, 429
|
78
|
-
raise ForbiddenError, errmsg
|
79
|
-
when 404
|
80
|
-
raise NotFoundError, errmsg
|
81
|
-
when 400, 10006, 20133, 500, 7000, 7001
|
82
|
-
raise ResponseError, errmsg
|
83
|
-
when 20117
|
84
|
-
raise InsufficientBalanceError, errmsg
|
85
|
-
when 20118, 20119
|
86
|
-
raise PinError, errmsg
|
87
|
-
when 30103
|
88
|
-
raise InsufficientPoolError, errmsg
|
40
|
+
kwargs.compact!
|
41
|
+
body =
|
42
|
+
if verb == :post
|
43
|
+
if args.present?
|
44
|
+
args.to_json
|
89
45
|
else
|
90
|
-
|
46
|
+
kwargs.to_json
|
91
47
|
end
|
92
48
|
else
|
93
|
-
|
49
|
+
''
|
94
50
|
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
def uri_for(path)
|
99
|
-
uri_options = {
|
100
|
-
scheme: SERVER_SCHEME,
|
101
|
-
host: host,
|
102
|
-
path: path
|
103
|
-
}
|
104
|
-
Addressable::URI.new(uri_options)
|
105
|
-
end
|
106
51
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
52
|
+
path = "#{path}?#{URI.encode_www_form(kwargs.sort_by { |k, _v| k })}" if verb == :get && kwargs.present?
|
53
|
+
access_token ||=
|
54
|
+
MixinBot.utils.access_token(
|
55
|
+
verb.to_s.upcase,
|
56
|
+
path,
|
57
|
+
body,
|
58
|
+
exp_in:,
|
59
|
+
scp:,
|
60
|
+
app_id: config.app_id,
|
61
|
+
session_id: config.session_id,
|
62
|
+
private_key: config.session_private_key
|
63
|
+
)
|
64
|
+
authorization = format('Bearer %<access_token>s', access_token:)
|
65
|
+
|
66
|
+
response =
|
67
|
+
case verb
|
68
|
+
when :get
|
69
|
+
@conn.get path, nil, { Authorization: authorization }
|
70
|
+
when :post
|
71
|
+
@conn.post path, body, { Authorization: authorization }
|
72
|
+
end
|
115
73
|
|
116
|
-
|
117
|
-
result = JSON.parse(response&.body&.to_s)
|
118
|
-
result && yield(:json, result)
|
74
|
+
result = response.body
|
119
75
|
|
120
|
-
|
76
|
+
if result['error'].blank?
|
77
|
+
result.merge! result['data'] if result['data'].is_a? Hash
|
78
|
+
return result
|
121
79
|
end
|
122
80
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
when
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
ensure
|
143
|
-
file&.close
|
144
|
-
end
|
145
|
-
|
146
|
-
result = file
|
147
|
-
when :xml
|
148
|
-
result = Hash.from_xml(response.body.to_s)
|
81
|
+
errmsg = "#{verb.upcase}|#{path}|#{body}, errcode: #{result['error']['code']}, errmsg: #{result['error']['description']}, request_id: #{response&.[]('X-Request-Id')}, server_time: #{response&.[]('X-Server-Time')}'"
|
82
|
+
|
83
|
+
case result['error']['code']
|
84
|
+
when 401, 20121
|
85
|
+
raise UnauthorizedError, errmsg
|
86
|
+
when 403, 20116, 10002, 429
|
87
|
+
raise ForbiddenError, errmsg
|
88
|
+
when 404
|
89
|
+
raise NotFoundError, errmsg
|
90
|
+
when 400, 10006, 20133, 500, 7000, 7001
|
91
|
+
raise ResponseError, errmsg
|
92
|
+
when 20117
|
93
|
+
raise InsufficientBalanceError, errmsg
|
94
|
+
when 20118, 20119
|
95
|
+
raise PinError, errmsg
|
96
|
+
when 30103
|
97
|
+
raise InsufficientPoolError, errmsg
|
98
|
+
when 10404
|
99
|
+
raise UserNotFoundError, errmsg
|
149
100
|
else
|
150
|
-
|
101
|
+
raise ResponseError, errmsg
|
151
102
|
end
|
152
|
-
|
153
|
-
yield(parse_as, result)
|
154
103
|
end
|
155
104
|
end
|
156
105
|
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MixinBot
|
4
|
+
class Configuration
|
5
|
+
CONFIGURABLE_ATTRS = %i[
|
6
|
+
app_id
|
7
|
+
client_secret
|
8
|
+
session_id
|
9
|
+
session_private_key
|
10
|
+
server_public_key
|
11
|
+
spend_key
|
12
|
+
pin
|
13
|
+
api_host
|
14
|
+
blaze_host
|
15
|
+
session_private_key_curve25519
|
16
|
+
server_public_key_curve25519
|
17
|
+
debug
|
18
|
+
].freeze
|
19
|
+
attr_accessor(*CONFIGURABLE_ATTRS)
|
20
|
+
|
21
|
+
def initialize(**kwargs)
|
22
|
+
@app_id = kwargs[:app_id] || kwargs[:client_id]
|
23
|
+
@client_secret = kwargs[:client_secret]
|
24
|
+
@session_id = kwargs[:session_id]
|
25
|
+
@api_host = kwargs[:api_host] || 'api.mixin.one'
|
26
|
+
@blaze_host = kwargs[:blaze_host] || 'blaze.mixin.one'
|
27
|
+
@debug = kwargs[:debug] || false
|
28
|
+
|
29
|
+
self.session_private_key = kwargs[:session_private_key] || kwargs[:private_key]
|
30
|
+
self.server_public_key = kwargs[:server_public_key] || kwargs[:pin_token]
|
31
|
+
self.spend_key = kwargs[:spend_key]
|
32
|
+
self.pin = kwargs[:pin] || spend_key
|
33
|
+
end
|
34
|
+
|
35
|
+
def valid?
|
36
|
+
%i[app_id session_id session_private_key server_public_key].all? do |attr|
|
37
|
+
send(attr).present?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def session_private_key=(key)
|
42
|
+
return if key.blank?
|
43
|
+
|
44
|
+
_private_key = decode_key key
|
45
|
+
@session_private_key =
|
46
|
+
if _private_key.size == 32
|
47
|
+
JOSE::JWA::Ed25519.keypair(_private_key).last
|
48
|
+
else
|
49
|
+
_private_key
|
50
|
+
end
|
51
|
+
|
52
|
+
@session_private_key_curve25519 = JOSE::JWA::Ed25519.sk_to_curve25519(@session_private_key) if @session_private_key.size == 64
|
53
|
+
end
|
54
|
+
|
55
|
+
def server_public_key=(key)
|
56
|
+
return if key.blank?
|
57
|
+
|
58
|
+
@server_public_key = decode_key key
|
59
|
+
# HEX encoded
|
60
|
+
@server_public_key_curve25519 =
|
61
|
+
if key.match?(/\A[\h]{32,}\z/i)
|
62
|
+
JOSE::JWA::Ed25519.pk_to_curve25519 @server_public_key
|
63
|
+
else
|
64
|
+
server_public_key
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def spend_key=(key)
|
69
|
+
return if key.blank?
|
70
|
+
|
71
|
+
_private_key = decode_key key
|
72
|
+
@spend_key =
|
73
|
+
if _private_key.size == 32
|
74
|
+
JOSE::JWA::Ed25519.keypair(_private_key).last
|
75
|
+
else
|
76
|
+
_private_key
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def pin=(key)
|
81
|
+
return if key.blank?
|
82
|
+
|
83
|
+
_private_key = decode_key key
|
84
|
+
@pin =
|
85
|
+
if _private_key.size == 32
|
86
|
+
JOSE::JWA::Ed25519.keypair(_private_key).last
|
87
|
+
else
|
88
|
+
_private_key
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def decode_key(key)
|
95
|
+
MixinBot.utils.decode_key key
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MixinBot
|
4
|
+
class Nfo
|
5
|
+
NFT_MEMO_PREFIX = 'NFO'
|
6
|
+
NFT_MEMO_VERSION = 0x00
|
7
|
+
NFT_MEMO_DEFAULT_CHAIN = '43d61dcd-e413-450d-80b8-101d5e903357'
|
8
|
+
NFT_MEMO_DEFAULT_CLASS = '3c8c161a18ae2c8b14fda1216fff7da88c419b5d'
|
9
|
+
NULL_UUID = '00000000-0000-0000-0000-000000000000'
|
10
|
+
|
11
|
+
attr_reader :prefix, :version, :raw
|
12
|
+
attr_accessor :mask, :chain, :nm_class, :collection, :token, :extra, :memo, :hex
|
13
|
+
|
14
|
+
def initialize(**kwargs)
|
15
|
+
@prefix = NFT_MEMO_PREFIX
|
16
|
+
@version = NFT_MEMO_VERSION
|
17
|
+
@mask = kwargs[:mask] || 0
|
18
|
+
@chain = kwargs[:chain] || NFT_MEMO_DEFAULT_CHAIN
|
19
|
+
@nm_class = kwargs[:nm_class] || NFT_MEMO_DEFAULT_CLASS
|
20
|
+
@collection = kwargs[:collection] || NULL_UUID
|
21
|
+
@token = kwargs[:token].presence&.to_i
|
22
|
+
@extra = kwargs[:extra]
|
23
|
+
@memo = kwargs[:memo]
|
24
|
+
@hex = kwargs[:hex]
|
25
|
+
end
|
26
|
+
|
27
|
+
def mint_memo
|
28
|
+
raise MixinBot::InvalidNfoFormatError, 'token is required' if token.blank?
|
29
|
+
raise MixinBot::InvalidNfoFormatError, 'extra must be 256-bit string' if extra.blank? || extra.size != 64
|
30
|
+
|
31
|
+
@collection = NULL_UUID if collection.blank?
|
32
|
+
@chain = NFT_MEMO_DEFAULT_CHAIN
|
33
|
+
@nm_class = NFT_MEMO_DEFAULT_CLASS
|
34
|
+
mark 0
|
35
|
+
encode
|
36
|
+
|
37
|
+
memo
|
38
|
+
end
|
39
|
+
|
40
|
+
def unique_token_id
|
41
|
+
bytes = []
|
42
|
+
bytes += MixinBot::UUID.new(hex: chain).packed.bytes
|
43
|
+
bytes += [nm_class].pack('H*').bytes
|
44
|
+
bytes += MixinBot::UUID.new(hex: collection).packed.bytes
|
45
|
+
bytes += MixinBot.utils.encode_int token
|
46
|
+
|
47
|
+
md5 = Digest::MD5.new
|
48
|
+
md5.update bytes.pack('c*')
|
49
|
+
digest = [md5.hexdigest].pack('H*').bytes
|
50
|
+
|
51
|
+
digest[6] = (digest[6] & 0x0f) | 0x30
|
52
|
+
digest[8] = (digest[8] & 0x3f) | 0x80
|
53
|
+
|
54
|
+
hex = digest.pack('c*').unpack1('H*')
|
55
|
+
|
56
|
+
MixinBot::UUID.new(hex:).unpacked
|
57
|
+
end
|
58
|
+
|
59
|
+
def mark(*indexes)
|
60
|
+
indexes.map do |index|
|
61
|
+
raise ArgumentError, "invalid NFO memo index #{index}" if index >= 64 || index.negative?
|
62
|
+
|
63
|
+
@mask = mask ^ (1 << index)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def encode
|
68
|
+
bytes = []
|
69
|
+
|
70
|
+
bytes += prefix.bytes
|
71
|
+
bytes += [version]
|
72
|
+
|
73
|
+
if mask.zero?
|
74
|
+
bytes += [0]
|
75
|
+
else
|
76
|
+
bytes += [1]
|
77
|
+
bytes += MixinBot.utils.encode_uint_64 mask
|
78
|
+
bytes += MixinBot::UUID.new(hex: chain).packed.bytes
|
79
|
+
|
80
|
+
class_bytes = [nm_class].pack('H*').bytes
|
81
|
+
bytes += MixinBot.utils.encode_int class_bytes.size
|
82
|
+
bytes += class_bytes
|
83
|
+
|
84
|
+
collection_bytes = collection.split('-').pack('H* H* H* H* H*').bytes
|
85
|
+
bytes += MixinBot.utils.encode_int collection_bytes.size
|
86
|
+
bytes += collection_bytes
|
87
|
+
|
88
|
+
# token_bytes = memo[:token].split('-').pack('H* H* H* H* H*').bytes
|
89
|
+
token_bytes = MixinBot.utils.encode_int token
|
90
|
+
bytes += MixinBot.utils.encode_int token_bytes.size
|
91
|
+
bytes += token_bytes
|
92
|
+
end
|
93
|
+
|
94
|
+
extra_bytes = [extra].pack('H*').bytes
|
95
|
+
bytes += MixinBot.utils.encode_int extra_bytes.size
|
96
|
+
bytes += extra_bytes
|
97
|
+
|
98
|
+
@raw = bytes.pack('C*')
|
99
|
+
@hex = raw.unpack1('H*')
|
100
|
+
@memo = Base64.urlsafe_encode64 raw, padding: false
|
101
|
+
|
102
|
+
self
|
103
|
+
end
|
104
|
+
|
105
|
+
def decode
|
106
|
+
@raw =
|
107
|
+
if memo.present?
|
108
|
+
Base64.urlsafe_decode64 memo
|
109
|
+
elsif hex.present?
|
110
|
+
[hex].pack('H*')
|
111
|
+
else
|
112
|
+
raise InvalidNfoFormatError, 'memo or hex is required'
|
113
|
+
end
|
114
|
+
|
115
|
+
@hex = raw.unpack1('H*') if hex.blank?
|
116
|
+
@memo = Base64.urlsafe_encode64 raw, padding: false if memo.blank?
|
117
|
+
|
118
|
+
decode_bytes
|
119
|
+
self
|
120
|
+
end
|
121
|
+
|
122
|
+
def decode_bytes
|
123
|
+
bytes = raw.bytes
|
124
|
+
|
125
|
+
_prefix = bytes.shift(3).pack('C*')
|
126
|
+
raise MixinBot::InvalidNfoFormatError, "NFO prefix #{_prefix}" if _prefix != prefix
|
127
|
+
|
128
|
+
_version = bytes.shift
|
129
|
+
raise MixinBot::InvalidNfoFormatError, "NFO version #{prefix}" if _version != version
|
130
|
+
|
131
|
+
hint = bytes.shift
|
132
|
+
if hint == 1
|
133
|
+
@mask = bytes.shift(8).reverse.pack('C*').unpack1('Q*')
|
134
|
+
|
135
|
+
@chain = MixinBot::UUID.new(hex: bytes.shift(16).pack('C*').unpack1('H*')).unpacked
|
136
|
+
|
137
|
+
class_length = bytes.shift
|
138
|
+
@nm_class = bytes.shift(class_length).pack('C*').unpack1('H*')
|
139
|
+
|
140
|
+
collection_length = bytes.shift
|
141
|
+
@collection = MixinBot::UUID.new(hex: bytes.shift(collection_length).pack('C*').unpack1('H*')).unpacked
|
142
|
+
|
143
|
+
token_length = bytes.shift
|
144
|
+
@token = MixinBot.utils.decode_int bytes.shift(token_length)
|
145
|
+
end
|
146
|
+
|
147
|
+
extra_length = bytes.shift
|
148
|
+
@extra = bytes.shift(extra_length).pack('C*').unpack1('H*')
|
149
|
+
|
150
|
+
self
|
151
|
+
end
|
152
|
+
|
153
|
+
def to_h
|
154
|
+
hash = {
|
155
|
+
prefix:,
|
156
|
+
version:,
|
157
|
+
mask:,
|
158
|
+
chain:,
|
159
|
+
class: nm_class,
|
160
|
+
collection:,
|
161
|
+
token:,
|
162
|
+
extra:,
|
163
|
+
memo:,
|
164
|
+
hex:
|
165
|
+
}
|
166
|
+
|
167
|
+
hash.each do |key, value|
|
168
|
+
hash.delete key if value.blank?
|
169
|
+
end
|
170
|
+
|
171
|
+
hash
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|