mixin_bot 1.0.0 → 1.2.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: 2ebb5e10a678c90007560b0f84676d0545ea510de1cefc5534091e0b1338c13b
4
- data.tar.gz: afe4ffc00de92429959bc310a6082112f3dc54d8c48e687165a08e0499a3b1a5
3
+ metadata.gz: 2162191a7068afafb913aa9b898c7915c133b732c25b785c1ec8cbf672fd3edd
4
+ data.tar.gz: 31303c5e3c9dfb3759c943c63d74a67f7efb7a9cbdfbee9a823ba6f7dc7c6ab2
5
5
  SHA512:
6
- metadata.gz: 334913613e7a88d94e3783fbf028545ecfa0cecdb0a46c7189a63563be17830d33a1de098714e7c38da97094e041f16805cb5782b1101587629dde8d91d4abe6
7
- data.tar.gz: b2ae3479345b421c7e96983c32c0e85c42a028022f74428c90467407eeea93f323b52126d5ab305ee317ae44db69a8e284ec024fdd738fbe095c6a7cbe11e1f4
6
+ metadata.gz: 9cef773f5bde48f7d1697b5be99251ec6b4d2dcd5dd15c1d9b7aa60d90089a3476f8b696b5f9e7c93acbbf464d35583519b51d7fdff70cd842c9a97d7a4b2100
7
+ data.tar.gz: 2a16d609f841f1f2c0b85b217254e5368e042e0e2dc78817f7bee89774e90b12f674c8b3c67c4c73644ef02ff8f2a616109cd0659f63e8bed128cae50431152d
@@ -52,8 +52,8 @@ module MixinBot
52
52
  @_scope = scope.join(' ')
53
53
  EM.run do
54
54
  start_blaze_connect do
55
- def on_open(ws, _event)
56
- ws.send write_ws_message(
55
+ def on_open(websocket, _event) # rubocop:disable Lint/NestedMethodDefinition
56
+ websocket.send write_ws_message(
57
57
  action: 'REFRESH_OAUTH_CODE',
58
58
  params: {
59
59
  client_id: @_app_id,
@@ -64,13 +64,13 @@ module MixinBot
64
64
  )
65
65
  end
66
66
 
67
- def on_message(ws, event)
67
+ def on_message(websocket, event) # rubocop:disable Lint/NestedMethodDefinition
68
68
  raw = JSON.parse ws_message(event.data)
69
69
  @_data = raw
70
- ws.close
70
+ websocket.close
71
71
  end
72
72
 
73
- def on_close(_ws, _event)
73
+ def on_close(_websocket, _event) # rubocop:disable Lint/NestedMethodDefinition
74
74
  EM.stop_event_loop
75
75
  end
76
76
  end
@@ -110,7 +110,7 @@ module MixinBot
110
110
  client.post path, *payload
111
111
  end
112
112
 
113
- def encrypt_message(data, sessions = [], sk: nil, pk: nil)
113
+ def encrypt_message(data, sessions = [], sk: nil, pk: nil) # rubocop:disable Naming/MethodParameterName
114
114
  raise ArgumentError, 'Wrong sessions format!' unless sessions.all?(&->(s) { s.key?('session_id') && s.key?('public_key') })
115
115
 
116
116
  sk ||= config.session_private_key[0...32]
@@ -153,7 +153,7 @@ module MixinBot
153
153
  Base64.urlsafe_encode64 bytes.pack('C*'), padding: false
154
154
  end
155
155
 
156
- def decrypt_message(data, sk: nil, si: nil)
156
+ def decrypt_message(data, sk: nil, si: nil) # rubocop:disable Naming/MethodParameterName
157
157
  bytes = Base64.urlsafe_decode64(data).bytes
158
158
 
159
159
  si ||= config.session_id
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MixinBot
4
+ class API
5
+ module Inscription
6
+ def collection(hash)
7
+ path = "/safe/inscriptions/collections/#{hash}"
8
+
9
+ client.get path
10
+ end
11
+
12
+ def collectible(hash)
13
+ path = "/safe/inscriptions/items/#{hash}"
14
+
15
+ client.get path
16
+ end
17
+
18
+ def collection_collectibles(hash, offset: 0)
19
+ path = "/safe/inscriptions/collections/#{hash}/items"
20
+
21
+ client.get path, offset:
22
+ end
23
+
24
+ def collectibles(members: [], access_token: nil)
25
+ unspent_outputs = safe_outputs(state: :unspent, members:, access_token:)['data']
26
+ unspent_outputs.select { |output| output['inscription_hash'].present? }
27
+ end
28
+
29
+ def create_collectible_transfer(utxo, **kwargs)
30
+ # verify collectible
31
+ utxo = utxo.with_indifferent_access
32
+ raise MixinBot::ArgumentError, 'not a valid collectible' unless utxo['inscription_hash'].present?
33
+
34
+ # verify members
35
+ members = [kwargs[:members]].flatten.compact
36
+ raise ArgumentError, 'members required' if members.blank?
37
+
38
+ threshold = kwargs[:threshold] || members.length
39
+ request_id = kwargs[:request_id] || kwargs[:trace_id] || SecureRandom.uuid
40
+
41
+ memo = kwargs[:memo] || ''
42
+
43
+ # build transaction
44
+ tx = build_safe_transaction(
45
+ utxos: [utxo],
46
+ receivers: [{
47
+ members:,
48
+ threshold:,
49
+ amount: utxo['amount']
50
+ }],
51
+ extra: memo
52
+ )
53
+
54
+ # encode transaction
55
+ raw = MixinBot.utils.encode_raw_transaction tx
56
+
57
+ # verify transaction
58
+ request = create_safe_transaction_request(request_id, raw)['data']
59
+
60
+ # sign transaction
61
+ spend_key = MixinBot.utils.decode_key(kwargs[:spend_key]) || config.spend_key
62
+ signed_raw = MixinBot.api.sign_safe_transaction(
63
+ raw:,
64
+ utxos: [utxo],
65
+ request: request[0],
66
+ spend_key:
67
+ )
68
+
69
+ # submit transaction
70
+ send_safe_transaction(
71
+ request_id,
72
+ signed_raw
73
+ )
74
+ end
75
+ end
76
+ end
77
+ end
@@ -2,20 +2,20 @@
2
2
 
3
3
  module MixinBot
4
4
  class API
5
- module Collectible
5
+ module LegacyCollectible
6
6
  NFT_ASSET_MIXIN_ID = '1700941284a95f31b25ec8c546008f208f88eee4419ccdcdbe6e3195e60128ca'
7
7
 
8
- def collectible(id, access_token: nil)
8
+ def legacy_collectible(id, access_token: nil)
9
9
  path = "/collectibles/tokens/#{id}"
10
10
  client.get path, access_token:
11
11
  end
12
12
 
13
- def collection(id, access_token: nil)
13
+ def legacy_collection(id, access_token: nil)
14
14
  path = "/collectibles/collections/#{id}"
15
15
  client.get path, access_token:
16
16
  end
17
17
 
18
- def collectibles(**kwargs)
18
+ def legacy_collectibles(**kwargs)
19
19
  limit = kwargs[:limit] || 100
20
20
  offset = kwargs[:offset] || ''
21
21
  state = kwargs[:state] || ''
@@ -40,7 +40,7 @@ module MixinBot
40
40
  }
41
41
  end
42
42
 
43
- client.post path, **payload, access_token:
43
+ client.post path, **payload
44
44
  end
45
45
 
46
46
  def unlock_multisig_request(request_id, pin = nil)
@@ -3,7 +3,7 @@
3
3
  module MixinBot
4
4
  class API
5
5
  module LegacyOutput
6
- def outputs(**kwargs)
6
+ def legacy_outputs(**kwargs)
7
7
  limit = kwargs[:limit] || 100
8
8
  offset = kwargs[:offset] || ''
9
9
  state = kwargs[:state] || ''
@@ -23,8 +23,8 @@ module MixinBot
23
23
 
24
24
  client.get path, **params, access_token:
25
25
  end
26
- alias multisigs outputs
27
- alias multisig_outputs outputs
26
+ alias multisigs legacy_outputs
27
+ alias multisig_outputs legacy_outputs
28
28
 
29
29
  def create_output(receivers:, index:, hint: nil, access_token: nil)
30
30
  path = '/outputs'
@@ -4,7 +4,7 @@ module MixinBot
4
4
  class API
5
5
  module LegacyTransfer
6
6
  TRANSFER_ARGUMENTS = %i[asset_id opponent_id amount].freeze
7
- def create_transfer(pin, **kwargs)
7
+ def create_legacy_transfer(pin, **kwargs)
8
8
  raise ArgumentError, "#{TRANSFER_ARGUMENTS.join(', ')} are needed for create transfer" unless TRANSFER_ARGUMENTS.all? { |param| kwargs.keys.include? param }
9
9
 
10
10
  asset_id = kwargs[:asset_id]
@@ -33,7 +33,7 @@ module MixinBot
33
33
  client.post path, **payload
34
34
  end
35
35
 
36
- def transfer(trace_id, access_token: nil)
36
+ def legacy_transfer(trace_id, access_token: nil)
37
37
  path = format('/transfers/trace/%<trace_id>s', trace_id:)
38
38
  client.get path, access_token:
39
39
  end
@@ -6,11 +6,11 @@ module MixinBot
6
6
  def create_safe_multisig_request(request_id, raw, access_token: nil)
7
7
  path = '/safe/multisigs'
8
8
  payload = [{
9
- request_id: request_id,
10
- raw: raw
9
+ request_id:,
10
+ raw:
11
11
  }]
12
12
 
13
- client.post path, *payload
13
+ client.post path, *payload, access_token:
14
14
  end
15
15
 
16
16
  def sign_safe_multisig_request(request_id, raw, access_token: nil)
@@ -20,13 +20,13 @@ module MixinBot
20
20
  raw:
21
21
  }
22
22
 
23
- client.post path, **payload
23
+ client.post path, **payload, access_token:
24
24
  end
25
25
 
26
26
  def unlock_safe_multisig_request(request_id, access_token: nil)
27
27
  path = format('/safe/multisigs/%<request_id>s/unlock', request_id:)
28
28
 
29
- client.post path, access_token: access_token
29
+ client.post path, access_token:
30
30
  end
31
31
 
32
32
  def safe_multisig_request(request_id, access_token: nil)
@@ -18,7 +18,7 @@ module MixinBot
18
18
  state = kwargs[:state] || ''
19
19
  access_token = kwargs[:access_token]
20
20
  order = kwargs[:order] || 'ASC'
21
- members = kwargs[:members] || [config.app_id]
21
+ members = kwargs[:members].presence || [config.app_id]
22
22
  threshold = kwargs[:threshold] || members.length
23
23
 
24
24
  members_hash = SHA3::Digest::SHA256.hexdigest(members&.sort&.join)
@@ -36,11 +36,13 @@ module MixinBot
36
36
 
37
37
  client.get path, **params, access_token:
38
38
  end
39
+ alias outputs safe_outputs
39
40
 
40
41
  def safe_output(id, access_token: nil)
41
42
  path = format('/safe/outputs/%<id>s', id:)
42
43
  client.get path, access_token:
43
44
  end
45
+ alias output safe_output
44
46
  end
45
47
  end
46
48
  end
@@ -11,7 +11,7 @@ module MixinBot
11
11
  memo = kwargs[:memo] || ''
12
12
  trace_id = kwargs[:trace_id] || SecureRandom.uuid
13
13
 
14
- mix_address = MixinBot.utils.build_mix_address(members, threshold)
14
+ mix_address = MixinBot.utils.build_mix_address(members:, threshold:)
15
15
 
16
16
  "https://mixin.one/pay/#{mix_address}?amount=#{amount}&asset=#{asset_id}&memo=#{memo}&trace=#{trace_id}"
17
17
  end
@@ -30,7 +30,7 @@ module MixinBot
30
30
 
31
31
  # https://developers.mixin.one/api/alpha-mixin-network/create-pin/
32
32
  def update_pin(pin:, old_pin: nil)
33
- old_pin ||= MixinBot.config.pin
33
+ # old_pin ||= MixinBot.config.pin
34
34
  raise ArgumentError, 'invalid old pin' if old_pin.present? && old_pin.length != 6
35
35
 
36
36
  path = '/pin/update'
@@ -49,7 +49,7 @@ module MixinBot
49
49
  ed25519_key = JOSE::JWA::Ed25519.keypair
50
50
 
51
51
  private_key = ed25519_key[1].unpack1('H*')
52
- public_key = (ed25519_key[0].bytes + MixinBot::Utils.encode_uint_64(counter + 1)).pack('c*').unpack1('H*')
52
+ public_key = (ed25519_key[0].bytes + MixinBot::Utils.encode_uint64(counter + 1)).pack('c*').unpack1('H*')
53
53
 
54
54
  {
55
55
  private_key:,
@@ -4,7 +4,7 @@ module MixinBot
4
4
  class API
5
5
  module Rpc
6
6
  def rpc_proxy(method, params = [], access_token: nil)
7
- path = '/external/proxy'
7
+ path = '/external/kernel'
8
8
  payload = {
9
9
  method:,
10
10
  params:
@@ -19,7 +19,7 @@ module MixinBot
19
19
  end
20
20
 
21
21
  def get_transaction(hash, access_token: nil)
22
- rpc_proxy('gettransaction', [hash], access_token: nil)
22
+ rpc_proxy('gettransaction', [hash], access_token:)
23
23
  end
24
24
 
25
25
  def get_utxo(hash, index = 0, access_token: nil)
@@ -30,16 +30,16 @@ module MixinBot
30
30
  rpc_proxy 'getsnapshot', [hash], access_token:
31
31
  end
32
32
 
33
- def list_snapshots(offset = 0, count = 10, sig = false, tx = false, access_token: nil)
34
- rpc_proxy 'listsnapshots', [offset, count, sig, tx], access_token:
33
+ def list_snapshots(offset = 0, count = 10, sig: false, txn: false, access_token: nil)
34
+ rpc_proxy 'listsnapshots', [offset, count, sig, txn], access_token:
35
35
  end
36
36
 
37
37
  def list_mint_works(offset = 0, access_token: nil)
38
38
  rpc_proxy 'listmintworks', [offset], access_token:
39
39
  end
40
40
 
41
- def list_mint_distributions(offset = 0, count = 10, tx = false, access_token: nil)
42
- rpc_proxy 'listmintdistributions', [offset, count, tx], access_token:
41
+ def list_mint_distributions(offset = 0, count = 10, txn: false, access_token: nil)
42
+ rpc_proxy 'listmintdistributions', [offset, count, txn], access_token:
43
43
  end
44
44
  end
45
45
  end
@@ -6,8 +6,10 @@ module MixinBot
6
6
  SAFE_TX_VERSION = 0x05
7
7
  OUTPUT_TYPE_SCRIPT = 0x00
8
8
  OUTPUT_TYPE_WITHDRAW_SUBMIT = 0xa1
9
+ XIN_ASSET_ID = 'c94ac88f-4671-3976-b60a-09064f1811e8'
10
+ EXTRA_SIZE_STORAGE_CAPACITY = 1024 * 1024 * 4
11
+ EXTRA_STORAGE_PRICE_STEP = 0.0001
9
12
 
10
- # ghost keys
11
13
  def create_safe_keys(*payload, access_token: nil)
12
14
  raise ArgumentError, 'payload should be an array' unless payload.is_a? Array
13
15
  raise ArgumentError, 'payload should not be empty' unless payload.size.positive?
@@ -21,10 +23,58 @@ module MixinBot
21
23
 
22
24
  path = '/safe/keys'
23
25
 
24
- client.post path, *payload
26
+ client.post path, *payload, access_token:
25
27
  end
26
28
  alias create_ghost_keys create_safe_keys
27
29
 
30
+ def generate_safe_keys(recipients)
31
+ raise ArgumentError, 'recipients should be an array' unless recipients.is_a? Array
32
+
33
+ ghost_keys = []
34
+ uuid_recipients = []
35
+
36
+ recipients.each_with_index do |recipient, index|
37
+ next if recipient[:mix_address].blank?
38
+
39
+ if recipient[:members].all?(&->(m) { m.start_with? MixinBot::Utils::Address::MAIN_ADDRESS_PREFIX })
40
+ key = JOSE::JWA::Ed25519.keypair
41
+ gk = {
42
+ mask: key[0].unpack1('H*'),
43
+ keys: []
44
+ }
45
+ recipient[:members].each do |member|
46
+ payload = MixinBot.utils.parse_main_address member
47
+ spend_key = payload[0...32]
48
+ view_key = payload[-32..]
49
+
50
+ ghost_public_key = MixinBot.utils.derive_ghost_public_key(key[1], view_key, spend_key, index)
51
+
52
+ gk[:keys] << ghost_public_key.unpack1('H*')
53
+ end
54
+
55
+ ghost_keys[index] = gk.with_indifferent_access
56
+
57
+ elsif recipient[:members].none?(&->(m) { m.start_with? MixinBot::Utils::Address::MAIN_ADDRESS_PREFIX })
58
+ uuid_recipients.push(
59
+ {
60
+ receivers: recipient[:members],
61
+ index:,
62
+ hint: SecureRandom.uuid
63
+ }.with_indifferent_access
64
+ )
65
+ end
66
+ end
67
+
68
+ if uuid_recipients.present?
69
+ keys = create_safe_keys(*uuid_recipients)['data']
70
+ keys.each_with_index do |key, index|
71
+ ghost_keys[uuid_recipients[index][:index]] = key
72
+ end
73
+ end
74
+
75
+ ghost_keys
76
+ end
77
+
28
78
  # kwargs:
29
79
  # {
30
80
  # utxos: [ utxo ],
@@ -39,6 +89,8 @@ module MixinBot
39
89
  SAFE_RAW_TRANSACTION_ARGUMENTS = %i[utxos receivers].freeze
40
90
  def build_safe_transaction(**kwargs)
41
91
  raise ArgumentError, "#{SAFE_RAW_TRANSACTION_ARGUMENTS.join(', ')} are needed for build safe transaction" unless SAFE_RAW_TRANSACTION_ARGUMENTS.all? { |param| kwargs.keys.include? param }
92
+ raise ArgumentError, 'receivers should be an array' unless kwargs[:receivers].is_a? Array
93
+ raise ArgumentError, 'utxos should be an array' unless kwargs[:utxos].is_a? Array
42
94
 
43
95
  utxos = kwargs[:utxos].map(&:with_indifferent_access)
44
96
  receivers = kwargs[:receivers].map(&:with_indifferent_access)
@@ -63,12 +115,12 @@ module MixinBot
63
115
  inputs_sum = utxos.sum(&->(utxo) { utxo['amount'].to_d })
64
116
  outputs_sum = recipients.sum(&->(recipient) { recipient['amount'].to_d })
65
117
  change = inputs_sum - outputs_sum
66
- raise InsufficientBalanceError, "inputs sum: #{inputs_sum}" if change.negative?
118
+ raise InsufficientBalanceError, "inputs sum #{inputs_sum} < outputs sum #{outputs_sum}" if change.negative?
67
119
 
68
120
  if change.positive?
69
121
  recipients << MixinBot.utils.build_safe_recipient(
70
- members: utxos[0]['receivers'],
71
- threshold: utxos[0]['receivers_threshold'],
122
+ members: utxos.first['receivers'],
123
+ threshold: utxos.first['receivers_threshold'],
72
124
  amount: change
73
125
  ).with_indifferent_access
74
126
  end
@@ -85,14 +137,7 @@ module MixinBot
85
137
  }
86
138
  end
87
139
 
88
- ghost_payload = recipients.map.with_index do |r, index|
89
- {
90
- receivers: r[:members],
91
- index:,
92
- hint: SecureRandom.uuid
93
- }
94
- end
95
- ghosts = create_safe_keys(*ghost_payload)['data']
140
+ ghosts = generate_safe_keys(recipients)
96
141
 
97
142
  outputs = []
98
143
  recipients.each_with_index do |recipient, index|
@@ -121,7 +166,8 @@ module MixinBot
121
166
  asset:,
122
167
  inputs:,
123
168
  outputs:,
124
- extra: kwargs[:extra] || ''
169
+ extra: kwargs[:extra] || '',
170
+ references: kwargs[:references] || []
125
171
  }
126
172
  end
127
173
 
@@ -164,7 +210,7 @@ module MixinBot
164
210
 
165
211
  msg = [raw].pack('H*')
166
212
 
167
- y_point = JOSE::JWA::FieldElement.new(
213
+ y_scalar = JOSE::JWA::FieldElement.new(
168
214
  JOSE::JWA::X25519.clamp_scalar(spend_key[...32]).x,
169
215
  JOSE::JWA::Edwards25519Point::L
170
216
  )
@@ -175,16 +221,12 @@ module MixinBot
175
221
  raise ArgumentError, 'utxo not match' unless input['hash'] == utxo['transaction_hash'] && input['index'] == utxo['output_index']
176
222
 
177
223
  view = [request['views'][index]].pack('H*')
178
- x_point = JOSE::JWA::FieldElement.new(
179
- # https://github.com/potatosalad/ruby-jose/blob/e1be589b889f1e59ac233a5d19a3fa13f1e4b8a0/lib/jose/jwa/x25519.rb#L122C14-L122C48
180
- OpenSSL::BN.new(view.reverse, 2),
181
- JOSE::JWA::Edwards25519Point::L
182
- )
224
+ x_scalar = MixinBot.utils.scalar_from_bytes(view)
183
225
 
184
- t_point = x_point + y_point
185
- key = t_point.to_bytes(JOSE::JWA::Edwards25519Point::B)
226
+ t_scalar = x_scalar + y_scalar
227
+ key = t_scalar.to_bytes(JOSE::JWA::Edwards25519Point::B)
186
228
 
187
- pub = MixinBot.utils.generate_public_key key
229
+ pub = MixinBot.utils.shared_public_key key
188
230
  key_index = utxo['keys'].index pub.unpack1('H*')
189
231
  raise ArgumentError, 'cannot find valid key' unless key_index.is_a? Integer
190
232
 
@@ -197,6 +239,75 @@ module MixinBot
197
239
 
198
240
  MixinBot.utils.encode_raw_transaction tx
199
241
  end
242
+
243
+ def build_object_transaction(extra, **)
244
+ extra = extra.to_s
245
+ raise ArgumentError, 'Extra too large' if extra.bytesize > EXTRA_SIZE_STORAGE_CAPACITY
246
+
247
+ # calculate fee base on extra length
248
+ amount = EXTRA_STORAGE_PRICE_STEP * ((extra.bytesize / 1024) + 1)
249
+
250
+ # burning address
251
+ receivers = [
252
+ {
253
+ members: [MixinBot.utils.burning_address],
254
+ threshold: 64,
255
+ amount:
256
+ }
257
+ ]
258
+
259
+ # find XIN utxos
260
+ utxos = build_utxos(asset_id: XIN_ASSET_ID, amount:)
261
+
262
+ # build transaction
263
+ build_safe_transaction utxos:, receivers:, extra:, **
264
+ end
265
+
266
+ INSCRIBE_TRANSACTION_ARGUMENTS = %i[content collection_hash].freeze
267
+ def build_inscribe_transaction(**kwargs)
268
+ raise ArgumentError, "#{INSCRIBE_TRANSACTION_ARGUMENTS.join(', ')} are needed for inscribe transaction" unless INSCRIBE_TRANSACTION_ARGUMENTS.all? { |param| kwargs.keys.include? param }
269
+
270
+ receivers = kwargs[:receivers].presence || [config.app_id]
271
+ receivers_threshold = kwargs[:receivers_threshold] || receivers.length
272
+ recipient = MixinBot.utils.build_mix_address(members: receivers, threshold: receivers_threshold)
273
+
274
+ content = kwargs[:content]
275
+ collection_hash = kwargs[:collection_hash]
276
+
277
+ data = {
278
+ operation: 'inscribe',
279
+ recipient:,
280
+ content:
281
+ }
282
+
283
+ MixinBot.api.build_object_transaction data.to_json, references: [collection_hash]
284
+ end
285
+
286
+ OCCUPY_INSCRIPTION_TRANSACTION_ARGUMENTS = %i[amount inscription_hash utxos].freeze
287
+ def build_occupy_transaction(**kwargs)
288
+ raise ArgumentError, "#{OCCUPY_INSCRIPTION_TRANSACTION_ARGUMENTS.join(', ')} are needed for occupy NFT transaction" unless OCCUPY_INSCRIPTION_TRANSACTION_ARGUMENTS.all? { |param| kwargs.keys.include? param }
289
+
290
+ members = kwargs[:members].presence || [config.app_id]
291
+ threshold = kwargs[:threshold] || members.length
292
+ amount = kwargs[:amount]
293
+ inscription_hash = kwargs[:inscription_hash]
294
+
295
+ receivers = [
296
+ {
297
+ members:,
298
+ threshold:,
299
+ amount:
300
+ }
301
+ ]
302
+
303
+ extra = {
304
+ operation: 'occupy',
305
+ recipient:,
306
+ content:
307
+ }.to_json
308
+
309
+ MixinBot.api.build_safe_transaction(utxos:, receivers:, extra:, references: [inscription_hash])
310
+ end
200
311
  end
201
312
  end
202
313
  end
@@ -15,8 +15,17 @@ module MixinBot
15
15
  # spend_key: string / nil,
16
16
  # }
17
17
  def create_safe_transfer(**kwargs)
18
- asset_id = kwargs[:asset_id]
19
- raise ArgumentError, 'asset_id required' if asset_id.blank?
18
+ utxos = kwargs[:utxos]
19
+ raise ArgumentError, 'utxos must be array' if utxos.present? && !utxos.is_a?(Array)
20
+
21
+ asset_id =
22
+ if utxos.present?
23
+ utxos.first['asset_id']
24
+ else
25
+ kwargs[:asset_id]
26
+ end
27
+
28
+ raise ArgumentError, 'utxos or asset_id required' if utxos.blank? && asset_id.blank?
20
29
 
21
30
  amount = kwargs[:amount]&.to_d
22
31
  raise ArgumentError, 'amount required' if amount.blank?
@@ -29,15 +38,7 @@ module MixinBot
29
38
  memo = kwargs[:memo] || ''
30
39
 
31
40
  # step 1: select inputs
32
- outputs = safe_outputs(state: 'unspent', asset: asset_id, limit: 500)['data'].sort_by { |o| o['amount'].to_d }
33
-
34
- utxos = []
35
- outputs.each do |output|
36
- break if utxos.sum { |o| o['amount'].to_d } >= amount
37
-
38
- utxos.shift if utxos.size >= 256
39
- utxos << output
40
- end
41
+ utxos ||= build_utxos(asset_id:, amount:)
41
42
 
42
43
  # step 2: build transaction
43
44
  tx = build_safe_transaction(
@@ -56,7 +57,7 @@ module MixinBot
56
57
 
57
58
  # step 4: sign transaction
58
59
  spend_key = MixinBot.utils.decode_key(kwargs[:spend_key]) || config.spend_key
59
- signed_raw = sign_safe_transaction(
60
+ signed_raw = MixinBot.api.sign_safe_transaction(
60
61
  raw:,
61
62
  utxos:,
62
63
  request: request[0],
@@ -69,6 +70,21 @@ module MixinBot
69
70
  signed_raw
70
71
  )
71
72
  end
73
+ alias create_transfer create_safe_transfer
74
+
75
+ def build_utxos(asset_id:, amount:)
76
+ outputs = safe_outputs(state: 'unspent', asset: asset_id, limit: 500)['data'].sort_by { |o| o['amount'].to_d }
77
+
78
+ utxos = []
79
+ outputs.each do |output|
80
+ break if utxos.sum { |o| o['amount'].to_d } >= amount
81
+
82
+ utxos.shift if utxos.size >= 256
83
+ utxos << output
84
+ end
85
+
86
+ utxos
87
+ end
72
88
  end
73
89
  end
74
90
  end
@@ -49,9 +49,9 @@ module MixinBot
49
49
  keystore = {
50
50
  app_id: user['data']['user_id'],
51
51
  session_id: user['data']['session_id'],
52
- private_key:,
53
- pin_token: user['data']['pin_token_base64'],
54
- spend_key: spend_keypair[1].unpack1('H*')
52
+ session_private_key: private_key,
53
+ server_public_key: user['data']['pin_token_base64'],
54
+ spend_key:
55
55
  }
56
56
  user_api = MixinBot::API.new(**keystore)
57
57
 
@@ -60,7 +60,7 @@ module MixinBot
60
60
  # wait for tip pin update in server
61
61
  sleep 1
62
62
 
63
- user_api.safe_register spend_key
63
+ user_api.safe_register keystore[:spend_key]
64
64
 
65
65
  keystore
66
66
  end
@@ -86,19 +86,24 @@ module MixinBot
86
86
  client.post path, **payload
87
87
  end
88
88
 
89
- def migrate_to_safe(spend_key:, old_pin: nil)
89
+ def migrate_to_safe(spend_key:, pin: nil)
90
90
  profile = me['data']
91
91
  return true if profile['has_safe']
92
92
 
93
93
  spend_keypair = JOSE::JWA::Ed25519.keypair spend_key
94
94
  spend_key = spend_keypair[1].unpack1('H*')
95
95
 
96
- update_pin pin: MixinBot.utils.tip_public_key(spend_keypair[0], counter: profile['tip_counter']) if profile['tip_key_base64'].blank?
96
+ if profile['tip_key_base64'].blank?
97
+ new_pin = MixinBot.utils.tip_public_key(spend_keypair[0], counter: profile['tip_counter'])
98
+ update_pin(pin: new_pin, old_pin: pin)
99
+
100
+ pin = new_pin
101
+ end
97
102
 
98
103
  # wait for tip pin update in server
99
104
  sleep 1
100
105
 
101
- safe_register spend_key
106
+ safe_register pin, spend_key
102
107
 
103
108
  {
104
109
  spend_key: