mixin_bot 0.12.1 → 1.1.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 +34 -56
- data/lib/mixin_bot/api/blaze.rb +4 -3
- data/lib/mixin_bot/api/conversation.rb +29 -49
- data/lib/mixin_bot/api/encrypted_message.rb +19 -19
- data/lib/mixin_bot/api/inscription.rb +71 -0
- data/lib/mixin_bot/api/legacy_collectible.rb +140 -0
- 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 +17 -222
- data/lib/mixin_bot/api/output.rb +48 -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 +12 -14
- 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 +295 -60
- data/lib/mixin_bot/api/transfer.rb +69 -31
- data/lib/mixin_bot/api/user.rb +88 -53
- data/lib/mixin_bot/api/withdraw.rb +52 -53
- data/lib/mixin_bot/api.rb +81 -46
- data/lib/mixin_bot/cli/api.rb +149 -5
- data/lib/mixin_bot/cli/utils.rb +14 -4
- data/lib/mixin_bot/cli.rb +13 -10
- data/lib/mixin_bot/client.rb +74 -127
- data/lib/mixin_bot/configuration.rb +98 -0
- data/lib/mixin_bot/nfo.rb +174 -0
- data/lib/mixin_bot/transaction.rb +524 -0
- data/lib/mixin_bot/utils/address.rb +121 -0
- data/lib/mixin_bot/utils/crypto.rb +218 -0
- data/lib/mixin_bot/utils/decoder.rb +56 -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 +77 -103
- data/lib/mixin_bot/api/collectible.rb +0 -138
- 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
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MixinBot
|
4
|
+
class API
|
5
|
+
module Tip
|
6
|
+
TIP_ACTIONS = %w[
|
7
|
+
TIP:VERIFY:
|
8
|
+
TIP:ADDRESS:ADD:
|
9
|
+
TIP:ADDRESS:REMOVE:
|
10
|
+
TIP:USER:DEACTIVATE:
|
11
|
+
TIP:EMERGENCY:CONTACT:CREATE:
|
12
|
+
TIP:EMERGENCY:CONTACT:READ:
|
13
|
+
TIP:EMERGENCY:CONTACT:REMOVE:
|
14
|
+
TIP:PHONE:NUMBER:UPDATE:
|
15
|
+
TIP:MULTISIG:REQUEST:SIGN:
|
16
|
+
TIP:MULTISIG:REQUEST:UNLOCK:
|
17
|
+
TIP:COLLECTIBLE:REQUEST:SIGN:
|
18
|
+
TIP:COLLECTIBLE:REQUEST:UNLOCK:
|
19
|
+
TIP:TRANSFER:CREATE:
|
20
|
+
TIP:WITHDRAWAL:CREATE:
|
21
|
+
TIP:TRANSACTION:CREATE:
|
22
|
+
TIP:OAUTH:APPROVE:
|
23
|
+
TIP:PROVISIONING:UPDATE:
|
24
|
+
TIP:APP:OWNERSHIP:TRANSFER:
|
25
|
+
SEQUENCER:REGISTER:
|
26
|
+
].freeze
|
27
|
+
|
28
|
+
def encrypt_tip_pin(pin, action, *params)
|
29
|
+
raise ArgumentError, 'invalid action' unless TIP_ACTIONS.include? action
|
30
|
+
|
31
|
+
pin_key = MixinBot.utils.decode_key pin
|
32
|
+
|
33
|
+
msg = action + params.flatten.map(&:to_s).join
|
34
|
+
|
35
|
+
msg = Digest::SHA256.digest(msg) unless action == 'TIP:VERIFY:'
|
36
|
+
|
37
|
+
signature = JOSE::JWA::Ed25519.sign msg, pin_key
|
38
|
+
|
39
|
+
encrypt_pin signature
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -3,75 +3,310 @@
|
|
3
3
|
module MixinBot
|
4
4
|
class API
|
5
5
|
module Transaction
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
6
|
+
SAFE_TX_VERSION = 0x05
|
7
|
+
OUTPUT_TYPE_SCRIPT = 0x00
|
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
|
12
|
+
|
13
|
+
def create_safe_keys(*payload, access_token: nil)
|
14
|
+
raise ArgumentError, 'payload should be an array' unless payload.is_a? Array
|
15
|
+
raise ArgumentError, 'payload should not be empty' unless payload.size.positive?
|
16
|
+
raise ArgumentError, 'invalid payload' unless payload.all?(&lambda { |param|
|
17
|
+
param.key?(:receivers) && param.key?(:index)
|
18
|
+
})
|
19
|
+
|
20
|
+
payload.each do |param|
|
21
|
+
param[:hint] ||= SecureRandom.uuid
|
22
|
+
end
|
23
|
+
|
24
|
+
path = '/safe/keys'
|
25
|
+
|
26
|
+
client.post path, *payload, access_token:
|
27
|
+
end
|
28
|
+
alias create_ghost_keys create_safe_keys
|
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 = []
|
30
35
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
35
76
|
end
|
36
77
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
78
|
+
# kwargs:
|
79
|
+
# {
|
80
|
+
# utxos: [ utxo ],
|
81
|
+
# receivers: [ {
|
82
|
+
# members: [ uuid ],
|
83
|
+
# threshold: integer,
|
84
|
+
# amount: string,
|
85
|
+
# } ],
|
86
|
+
# ghosts: [ ghost ],
|
87
|
+
# extra: string,
|
88
|
+
# }
|
89
|
+
SAFE_RAW_TRANSACTION_ARGUMENTS = %i[utxos receivers].freeze
|
90
|
+
def build_safe_transaction(**kwargs)
|
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
|
94
|
+
|
95
|
+
utxos = kwargs[:utxos].map(&:with_indifferent_access)
|
96
|
+
receivers = kwargs[:receivers].map(&:with_indifferent_access)
|
97
|
+
|
98
|
+
senders = utxos.map { |utxo| utxo['receivers'] }.uniq
|
99
|
+
raise ArgumentError, 'utxos should have same senders' if senders.size > 1
|
100
|
+
|
101
|
+
senders_threshold = utxos.map { |utxo| utxo['receivers_threshold'] }.uniq
|
102
|
+
raise ArgumentError, 'utxos should have same senders_threshold' if senders_threshold.size > 1
|
103
|
+
|
104
|
+
raise ArgumentError, 'utxos should not be empty' if utxos.empty?
|
105
|
+
raise ArgumentError, 'utxos too many' if utxos.size > 256
|
106
|
+
|
107
|
+
recipients = receivers.map do |receiver|
|
108
|
+
MixinBot.utils.build_safe_recipient(
|
109
|
+
members: receiver[:members],
|
110
|
+
threshold: receiver[:threshold],
|
111
|
+
amount: receiver[:amount]
|
112
|
+
).with_indifferent_access
|
113
|
+
end
|
114
|
+
|
115
|
+
inputs_sum = utxos.sum(&->(utxo) { utxo['amount'].to_d })
|
116
|
+
outputs_sum = recipients.sum(&->(recipient) { recipient['amount'].to_d })
|
117
|
+
change = inputs_sum - outputs_sum
|
118
|
+
raise InsufficientBalanceError, "inputs sum #{inputs_sum} < outputs sum #{outputs_sum}" if change.negative?
|
119
|
+
|
120
|
+
if change.positive?
|
121
|
+
recipients << MixinBot.utils.build_safe_recipient(
|
122
|
+
members: utxos.first['receivers'],
|
123
|
+
threshold: utxos.first['receivers_threshold'],
|
124
|
+
amount: change
|
125
|
+
).with_indifferent_access
|
126
|
+
end
|
127
|
+
raise ArgumentError, 'recipients too many' if recipients.size > 256
|
128
|
+
|
129
|
+
asset = utxos[0]['asset']
|
130
|
+
inputs = []
|
131
|
+
utxos.each do |utxo|
|
132
|
+
raise ArgumentError, 'utxo asset not match' unless utxo['asset'] == asset
|
133
|
+
|
134
|
+
inputs << {
|
135
|
+
hash: utxo['transaction_hash'],
|
136
|
+
index: utxo['output_index']
|
137
|
+
}
|
138
|
+
end
|
139
|
+
|
140
|
+
ghosts = generate_safe_keys(recipients)
|
141
|
+
|
142
|
+
outputs = []
|
143
|
+
recipients.each_with_index do |recipient, index|
|
144
|
+
outputs << if recipient['destination']
|
145
|
+
{
|
146
|
+
type: OUTPUT_TYPE_WITHDRAW_SUBMIT,
|
147
|
+
amount: recipient['amount'],
|
148
|
+
withdrawal: {
|
149
|
+
address: recipient['destination'],
|
150
|
+
tag: recipient['tag'] || ''
|
151
|
+
}
|
152
|
+
}
|
153
|
+
else
|
154
|
+
{
|
155
|
+
type: OUTPUT_TYPE_SCRIPT,
|
156
|
+
amount: recipient['amount'],
|
157
|
+
keys: ghosts[index]['keys'],
|
158
|
+
mask: ghosts[index]['mask'],
|
159
|
+
script: build_threshold_script(recipient['threshold'])
|
160
|
+
}
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
{
|
165
|
+
version: SAFE_TX_VERSION,
|
166
|
+
asset:,
|
167
|
+
inputs:,
|
168
|
+
outputs:,
|
169
|
+
extra: kwargs[:extra] || '',
|
170
|
+
references: kwargs[:references] || []
|
56
171
|
}
|
172
|
+
end
|
173
|
+
|
174
|
+
def create_safe_transaction_request(request_id, raw)
|
175
|
+
path = '/safe/transaction/requests'
|
176
|
+
payload = [{
|
177
|
+
request_id:,
|
178
|
+
raw:
|
179
|
+
}]
|
180
|
+
|
181
|
+
client.post path, *payload
|
182
|
+
end
|
57
183
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
184
|
+
def send_safe_transaction(request_id, raw)
|
185
|
+
path = '/safe/transactions'
|
186
|
+
payload = [{
|
187
|
+
request_id:,
|
188
|
+
raw:
|
189
|
+
}]
|
190
|
+
|
191
|
+
client.post path, *payload
|
192
|
+
end
|
193
|
+
|
194
|
+
def safe_transaction(request_id, access_token: nil)
|
195
|
+
path = format('/safe/transactions/%<request_id>s', request_id:)
|
196
|
+
|
197
|
+
client.get path, access_token:
|
62
198
|
end
|
63
199
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
200
|
+
SIGN_SAFE_TRANSACTION_ARGUMENTS = %i[raw utxos request spend_key].freeze
|
201
|
+
def sign_safe_transaction(**kwargs)
|
202
|
+
raise ArgumentError, "#{SIGN_SAFE_TRANSACTION_ARGUMENTS.join(', ')} are needed for sign safe transaction" unless SIGN_SAFE_TRANSACTION_ARGUMENTS.all? { |param| kwargs.keys.include? param }
|
203
|
+
|
204
|
+
raw = kwargs[:raw]
|
205
|
+
tx = MixinBot.utils.decode_raw_transaction raw
|
206
|
+
utxos = kwargs[:utxos]
|
207
|
+
request = kwargs[:request]
|
208
|
+
spend_key = MixinBot.utils.decode_key(kwargs[:spend_key]) || config.spend_key
|
209
|
+
spend_key = Digest::SHA512.digest spend_key[...32]
|
210
|
+
|
211
|
+
msg = [raw].pack('H*')
|
212
|
+
|
213
|
+
y_scalar = JOSE::JWA::FieldElement.new(
|
214
|
+
JOSE::JWA::X25519.clamp_scalar(spend_key[...32]).x,
|
215
|
+
JOSE::JWA::Edwards25519Point::L
|
72
216
|
)
|
73
217
|
|
74
|
-
|
218
|
+
tx[:signatures] = []
|
219
|
+
tx[:inputs].each_with_index do |input, index|
|
220
|
+
utxo = utxos[index]
|
221
|
+
raise ArgumentError, 'utxo not match' unless input['hash'] == utxo['transaction_hash'] && input['index'] == utxo['output_index']
|
222
|
+
|
223
|
+
view = [request['views'][index]].pack('H*')
|
224
|
+
x_scalar = MixinBot.utils.scalar_from_bytes(view)
|
225
|
+
|
226
|
+
t_scalar = x_scalar + y_scalar
|
227
|
+
key = t_scalar.to_bytes(JOSE::JWA::Edwards25519Point::B)
|
228
|
+
|
229
|
+
pub = MixinBot.utils.shared_public_key key
|
230
|
+
key_index = utxo['keys'].index pub.unpack1('H*')
|
231
|
+
raise ArgumentError, 'cannot find valid key' unless key_index.is_a? Integer
|
232
|
+
|
233
|
+
signature = MixinBot.utils.sign(msg, key:)
|
234
|
+
signature = signature.unpack1('H*')
|
235
|
+
sig = {}
|
236
|
+
sig[key_index] = signature
|
237
|
+
tx[:signatures] << sig
|
238
|
+
end
|
239
|
+
|
240
|
+
MixinBot.utils.encode_raw_transaction tx
|
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(receivers, 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])
|
75
310
|
end
|
76
311
|
end
|
77
312
|
end
|
@@ -3,40 +3,78 @@
|
|
3
3
|
module MixinBot
|
4
4
|
class API
|
5
5
|
module Transfer
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
6
|
+
# kwargs:
|
7
|
+
# {
|
8
|
+
# members: uuid | [ uuid ],
|
9
|
+
# threshold: integer / nil,
|
10
|
+
# asset_id: uuid,
|
11
|
+
# amount: string / float,
|
12
|
+
# trace_id: uuid / nil,
|
13
|
+
# request_id: uuid / nil,
|
14
|
+
# memo: string,
|
15
|
+
# spend_key: string / nil,
|
16
|
+
# }
|
17
|
+
def create_safe_transfer(**kwargs)
|
18
|
+
asset_id = kwargs[:asset_id]
|
19
|
+
raise ArgumentError, 'asset_id required' if asset_id.blank?
|
20
|
+
|
21
|
+
amount = kwargs[:amount]&.to_d
|
22
|
+
raise ArgumentError, 'amount required' if amount.blank?
|
23
|
+
|
24
|
+
members = [kwargs[:members]].flatten.compact
|
25
|
+
raise ArgumentError, 'members required' if members.blank?
|
26
|
+
|
27
|
+
threshold = kwargs[:threshold] || members.length
|
28
|
+
request_id = kwargs[:request_id] || kwargs[:trace_id] || SecureRandom.uuid
|
29
|
+
memo = kwargs[:memo] || ''
|
30
|
+
|
31
|
+
# step 1: select inputs
|
32
|
+
utxos = build_utxos(asset_id:, amount:)
|
33
|
+
|
34
|
+
# step 2: build transaction
|
35
|
+
tx = build_safe_transaction(
|
36
|
+
utxos:,
|
37
|
+
receivers: [{
|
38
|
+
members:,
|
39
|
+
threshold:,
|
40
|
+
amount:
|
41
|
+
}],
|
42
|
+
extra: memo
|
43
|
+
)
|
44
|
+
raw = MixinBot.utils.encode_raw_transaction tx
|
45
|
+
|
46
|
+
# step 3: verify transaction
|
47
|
+
request = create_safe_transaction_request(request_id, raw)['data']
|
48
|
+
|
49
|
+
# step 4: sign transaction
|
50
|
+
spend_key = MixinBot.utils.decode_key(kwargs[:spend_key]) || config.spend_key
|
51
|
+
signed_raw = MixinBot.api.sign_safe_transaction(
|
52
|
+
raw:,
|
53
|
+
utxos:,
|
54
|
+
request: request[0],
|
55
|
+
spend_key:
|
56
|
+
)
|
57
|
+
|
58
|
+
# step 5: submit transaction
|
59
|
+
send_safe_transaction(
|
60
|
+
request_id,
|
61
|
+
signed_raw
|
62
|
+
)
|
31
63
|
end
|
32
64
|
|
33
|
-
def
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
65
|
+
def build_utxos(asset_id:, amount:)
|
66
|
+
outputs = safe_outputs(state: 'unspent', asset: asset_id, limit: 500)['data'].sort_by { |o| o['amount'].to_d }
|
67
|
+
|
68
|
+
utxos = []
|
69
|
+
outputs.each do |output|
|
70
|
+
break if utxos.sum { |o| o['amount'].to_d } >= amount
|
71
|
+
|
72
|
+
utxos.shift if utxos.size >= 256
|
73
|
+
utxos << output
|
74
|
+
end
|
75
|
+
|
76
|
+
utxos
|
38
77
|
end
|
39
|
-
alias read_transfer transfer
|
40
78
|
end
|
41
79
|
end
|
42
80
|
end
|