mixin_bot 0.12.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mixin_bot/api/address.rb +21 -0
  3. data/lib/mixin_bot/api/app.rb +5 -11
  4. data/lib/mixin_bot/api/asset.rb +9 -16
  5. data/lib/mixin_bot/api/attachment.rb +27 -22
  6. data/lib/mixin_bot/api/auth.rb +31 -53
  7. data/lib/mixin_bot/api/blaze.rb +4 -3
  8. data/lib/mixin_bot/api/collectible.rb +60 -58
  9. data/lib/mixin_bot/api/conversation.rb +29 -49
  10. data/lib/mixin_bot/api/encrypted_message.rb +17 -17
  11. data/lib/mixin_bot/api/legacy_multisig.rb +87 -0
  12. data/lib/mixin_bot/api/legacy_output.rb +50 -0
  13. data/lib/mixin_bot/api/legacy_payment.rb +31 -0
  14. data/lib/mixin_bot/api/legacy_snapshot.rb +39 -0
  15. data/lib/mixin_bot/api/legacy_transaction.rb +173 -0
  16. data/lib/mixin_bot/api/legacy_transfer.rb +42 -0
  17. data/lib/mixin_bot/api/me.rb +13 -17
  18. data/lib/mixin_bot/api/message.rb +13 -10
  19. data/lib/mixin_bot/api/multisig.rb +16 -221
  20. data/lib/mixin_bot/api/output.rb +46 -0
  21. data/lib/mixin_bot/api/payment.rb +9 -20
  22. data/lib/mixin_bot/api/pin.rb +57 -65
  23. data/lib/mixin_bot/api/rpc.rb +9 -11
  24. data/lib/mixin_bot/api/snapshot.rb +15 -29
  25. data/lib/mixin_bot/api/tip.rb +43 -0
  26. data/lib/mixin_bot/api/transaction.rb +184 -60
  27. data/lib/mixin_bot/api/transfer.rb +64 -32
  28. data/lib/mixin_bot/api/user.rb +83 -53
  29. data/lib/mixin_bot/api/withdraw.rb +52 -53
  30. data/lib/mixin_bot/api.rb +78 -45
  31. data/lib/mixin_bot/cli/api.rb +151 -8
  32. data/lib/mixin_bot/cli/utils.rb +14 -4
  33. data/lib/mixin_bot/cli.rb +13 -10
  34. data/lib/mixin_bot/client.rb +76 -127
  35. data/lib/mixin_bot/configuration.rb +98 -0
  36. data/lib/mixin_bot/nfo.rb +174 -0
  37. data/lib/mixin_bot/transaction.rb +505 -0
  38. data/lib/mixin_bot/utils/address.rb +108 -0
  39. data/lib/mixin_bot/utils/crypto.rb +182 -0
  40. data/lib/mixin_bot/utils/decoder.rb +58 -0
  41. data/lib/mixin_bot/utils/encoder.rb +63 -0
  42. data/lib/mixin_bot/utils.rb +8 -109
  43. data/lib/mixin_bot/uuid.rb +41 -0
  44. data/lib/mixin_bot/version.rb +1 -1
  45. data/lib/mixin_bot.rb +39 -14
  46. data/lib/mvm/bridge.rb +2 -19
  47. data/lib/mvm/client.rb +11 -33
  48. data/lib/mvm/nft.rb +4 -4
  49. data/lib/mvm/registry.rb +9 -9
  50. data/lib/mvm/scan.rb +3 -5
  51. data/lib/mvm.rb +5 -6
  52. metadata +101 -44
  53. data/lib/mixin_bot/utils/nfo.rb +0 -176
  54. data/lib/mixin_bot/utils/transaction.rb +0 -478
  55. data/lib/mixin_bot/utils/uuid.rb +0 -43
@@ -4,153 +4,102 @@ module MixinBot
4
4
  class Client
5
5
  SERVER_SCHEME = 'https'
6
6
 
7
- attr_reader :host
8
-
9
- def initialize(host = 'api.mixin.one')
10
- @host = host
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, options = {})
14
- request(:get, path, options)
25
+ def get(path, *, **)
26
+ request(:get, path, *, **)
15
27
  end
16
28
 
17
- def post(path, options = {})
18
- request(:post, path, options)
29
+ def post(path, *, **)
30
+ request(:post, path, *, **)
19
31
  end
20
32
 
21
33
  private
22
34
 
23
- def request(verb, path, options = {})
24
- uri = uri_for path
25
-
26
- options[:headers] ||= {}
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
- errmsg = "errcode: #{result['error']['code']}, errmsg: #{result['error']['description']}, request_id: #{response&.[]('X-Request-Id')}, server_time: #{response&.[]('X-Server-Time')}'"
46
-
47
- # status code description
48
- # 202 400 The request body can’t be pasred as valid data.
49
- # 202 401 Unauthorized.
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
- raise ResponseError, errmsg
46
+ kwargs.to_json
91
47
  end
92
48
  else
93
- result
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
- def parse_response(response)
108
- content_type = response.headers[:content_type]
109
- parse_as = {
110
- %r{^application/json} => :json,
111
- %r{^image/.*} => :file,
112
- %r{^text/html} => :xml,
113
- %r{^text/plain} => :plain
114
- }.each_with_object([]) { |match, memo| memo << match[1] if content_type =~ match[0] }.first || :plain
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
- if parse_as == :plain
117
- result = JSON.parse(response&.body&.to_s)
118
- result && yield(:json, result)
74
+ result = response.body
119
75
 
120
- yield(:plain, response.body)
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
- case parse_as
124
- when :json
125
- result = JSON.parse(response.body.to_s)
126
- when :file
127
- extension =
128
- if response.headers[:content_type] =~ %r{^image/.*}
129
- {
130
- 'image/gif': '.gif',
131
- 'image/jpeg': '.jpg',
132
- 'image/png': '.png'
133
- }[response.headers['content-type']]
134
- else
135
- ''
136
- end
137
-
138
- begin
139
- file = Tempfile.new(['mixin-file-', extension])
140
- file.binmode
141
- file.write(response.body)
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
- result = response.body
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