mixin_bot 1.4.0 → 2.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/AGENTS.md +75 -0
- data/API_COVERAGE.md +143 -0
- data/CHANGELOG.md +112 -0
- data/README.md +375 -0
- data/docs/agent/cli.md +149 -0
- data/docs/agent/cookbook.md +152 -0
- data/examples/blaze.rb +43 -0
- data/examples/config.yml.example +21 -0
- data/lib/mixin_bot/address.rb +43 -3
- data/lib/mixin_bot/api/app.rb +7 -0
- data/lib/mixin_bot/api/asset.rb +114 -3
- data/lib/mixin_bot/api/auth.rb +19 -10
- data/lib/mixin_bot/api/blaze.rb +81 -0
- data/lib/mixin_bot/api/chain.rb +94 -0
- data/lib/mixin_bot/api/code.rb +16 -0
- data/lib/mixin_bot/api/computer_api.rb +60 -0
- data/lib/mixin_bot/api/conversation.rb +7 -1
- data/lib/mixin_bot/api/deposit.rb +12 -0
- data/lib/mixin_bot/api/encrypted_message.rb +1 -1
- data/lib/mixin_bot/api/fiat.rb +12 -0
- data/lib/mixin_bot/api/inscription.rb +2 -2
- data/lib/mixin_bot/api/legacy_collectible.rb +26 -27
- data/lib/mixin_bot/api/legacy_multisig.rb +20 -21
- data/lib/mixin_bot/api/legacy_output.rb +10 -3
- data/lib/mixin_bot/api/legacy_payment.rb +2 -0
- data/lib/mixin_bot/api/legacy_snapshot.rb +16 -0
- data/lib/mixin_bot/api/legacy_transaction.rb +28 -13
- data/lib/mixin_bot/api/legacy_transfer.rb +11 -8
- data/lib/mixin_bot/api/legacy_user.rb +51 -0
- data/lib/mixin_bot/api/me.rb +99 -3
- data/lib/mixin_bot/api/message.rb +18 -27
- data/lib/mixin_bot/api/multisig.rb +19 -0
- data/lib/mixin_bot/api/network.rb +17 -0
- data/lib/mixin_bot/api/network_asset.rb +27 -0
- data/lib/mixin_bot/api/output.rb +1 -1
- data/lib/mixin_bot/api/pin.rb +16 -3
- data/lib/mixin_bot/api/pin_payload.rb +26 -0
- data/lib/mixin_bot/api/session.rb +14 -0
- data/lib/mixin_bot/api/snapshot.rb +6 -0
- data/lib/mixin_bot/api/tip.rb +74 -1
- data/lib/mixin_bot/api/transaction.rb +106 -17
- data/lib/mixin_bot/api/transfer.rb +141 -14
- data/lib/mixin_bot/api/turn.rb +12 -0
- data/lib/mixin_bot/api/user.rb +148 -45
- data/lib/mixin_bot/api/withdraw.rb +24 -23
- data/lib/mixin_bot/api.rb +248 -3
- data/lib/mixin_bot/bot_auth.rb +71 -0
- data/lib/mixin_bot/cli/api.rb +224 -143
- data/lib/mixin_bot/cli/base.rb +77 -0
- data/lib/mixin_bot/cli/call.rb +71 -0
- data/lib/mixin_bot/cli/errors.rb +56 -0
- data/lib/mixin_bot/cli/node.rb +9 -2
- data/lib/mixin_bot/cli/output.rb +196 -0
- data/lib/mixin_bot/cli/schema.rb +274 -0
- data/lib/mixin_bot/cli/schema_command.rb +21 -0
- data/lib/mixin_bot/cli/utils.rb +114 -18
- data/lib/mixin_bot/cli.rb +124 -48
- data/lib/mixin_bot/client/error_mapper.rb +40 -0
- data/lib/mixin_bot/client.rb +94 -64
- data/lib/mixin_bot/computer.rb +132 -0
- data/lib/mixin_bot/configuration.rb +108 -1
- data/lib/mixin_bot/errors.rb +102 -0
- data/lib/mixin_bot/models/address.rb +11 -0
- data/lib/mixin_bot/models/api_envelope.rb +67 -0
- data/lib/mixin_bot/models/asset.rb +11 -0
- data/lib/mixin_bot/models/ghost_keys.rb +14 -0
- data/lib/mixin_bot/models/output.rb +11 -0
- data/lib/mixin_bot/models/safe_multisig_request.rb +11 -0
- data/lib/mixin_bot/models/sequencer_transaction_request.rb +11 -0
- data/lib/mixin_bot/models/user.rb +11 -0
- data/lib/mixin_bot/models.rb +10 -0
- data/lib/mixin_bot/monitor.rb +77 -0
- data/lib/mixin_bot/transaction/buffer.rb +34 -0
- data/lib/mixin_bot/transaction/decoder.rb +227 -0
- data/lib/mixin_bot/transaction/encoder.rb +255 -0
- data/lib/mixin_bot/transaction.rb +6 -475
- data/lib/mixin_bot/url_scheme.rb +63 -0
- data/lib/mixin_bot/utils/address.rb +17 -80
- data/lib/mixin_bot/utils/crypto.rb +173 -1
- data/lib/mixin_bot/utils/decoder.rb +1 -1
- data/lib/mixin_bot/utils/encoder.rb +13 -0
- data/lib/mixin_bot/utils.rb +45 -0
- data/lib/mixin_bot/uuid.rb +78 -1
- data/lib/mixin_bot/version.rb +11 -1
- data/lib/mixin_bot.rb +172 -18
- data/lib/mvm/bridge.rb +46 -0
- data/lib/mvm/client.rb +60 -0
- data/lib/mvm/nft.rb +4 -2
- data/lib/mvm/registry.rb +2 -1
- data/lib/mvm.rb +93 -0
- data/lib/tasks/api_coverage.rake +20 -0
- data/llms.txt +29 -0
- metadata +77 -9
|
@@ -1,7 +1,47 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module MixinBot
|
|
4
|
+
##
|
|
5
|
+
# Configuration class for storing Mixin bot credentials and settings.
|
|
6
|
+
#
|
|
7
|
+
# This class handles the configuration of bot credentials including:
|
|
8
|
+
# - Application ID and secret
|
|
9
|
+
# - Session ID and private key
|
|
10
|
+
# - Server public key
|
|
11
|
+
# - Spend key and PIN
|
|
12
|
+
# - API and Blaze host settings
|
|
13
|
+
#
|
|
14
|
+
# == Usage
|
|
15
|
+
#
|
|
16
|
+
# Configure globally:
|
|
17
|
+
#
|
|
18
|
+
# MixinBot.configure do
|
|
19
|
+
# app_id = 'your-app-id'
|
|
20
|
+
# session_id = 'your-session-id'
|
|
21
|
+
# session_private_key = 'your-private-key'
|
|
22
|
+
# server_public_key = 'server-public-key'
|
|
23
|
+
# spend_key = 'your-spend-key'
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# Or create a specific configuration instance:
|
|
27
|
+
#
|
|
28
|
+
# config = MixinBot::Configuration.new(
|
|
29
|
+
# app_id: 'your-app-id',
|
|
30
|
+
# session_id: 'your-session-id',
|
|
31
|
+
# session_private_key: 'your-private-key',
|
|
32
|
+
# server_public_key: 'server-public-key'
|
|
33
|
+
# )
|
|
34
|
+
#
|
|
35
|
+
# == Key Conversion
|
|
36
|
+
#
|
|
37
|
+
# The configuration automatically handles key format conversions:
|
|
38
|
+
# - Ed25519 keys are converted from seed format (32 bytes) to full format (64 bytes)
|
|
39
|
+
# - Keys are converted to Curve25519 format when needed
|
|
40
|
+
# - Keys can be provided in various encodings (Base64, hex, etc.)
|
|
41
|
+
#
|
|
4
42
|
class Configuration
|
|
43
|
+
##
|
|
44
|
+
# List of configurable attributes.
|
|
5
45
|
CONFIGURABLE_ATTRS = %i[
|
|
6
46
|
app_id
|
|
7
47
|
client_secret
|
|
@@ -18,6 +58,29 @@ module MixinBot
|
|
|
18
58
|
].freeze
|
|
19
59
|
attr_accessor(*CONFIGURABLE_ATTRS)
|
|
20
60
|
|
|
61
|
+
##
|
|
62
|
+
# Initializes a new Configuration instance.
|
|
63
|
+
#
|
|
64
|
+
# @param kwargs [Hash] configuration options
|
|
65
|
+
# @option kwargs [String] :app_id the application ID (or :client_id)
|
|
66
|
+
# @option kwargs [String] :client_secret the client secret
|
|
67
|
+
# @option kwargs [String] :session_id the session ID
|
|
68
|
+
# @option kwargs [String] :session_private_key the session private key (or :private_key)
|
|
69
|
+
# @option kwargs [String] :server_public_key the server public key (or :pin_token)
|
|
70
|
+
# @option kwargs [String] :spend_key the spend private key
|
|
71
|
+
# @option kwargs [String] :pin the PIN (defaults to spend_key if not provided)
|
|
72
|
+
# @option kwargs [String] :api_host ('api.mixin.one') the API host
|
|
73
|
+
# @option kwargs [String] :blaze_host ('blaze.mixin.one') the Blaze WebSocket host
|
|
74
|
+
# @option kwargs [Boolean] :debug (false) enable debug logging
|
|
75
|
+
#
|
|
76
|
+
# @example
|
|
77
|
+
# config = MixinBot::Configuration.new(
|
|
78
|
+
# app_id: '25696f85-b7b4-4509-8c3f-2684a8fc4a2a',
|
|
79
|
+
# session_id: '25696f85-b7b4-4509-8c3f-2684a8fc4a2a',
|
|
80
|
+
# session_private_key: 'base64_encoded_key',
|
|
81
|
+
# server_public_key: 'base64_encoded_key'
|
|
82
|
+
# )
|
|
83
|
+
#
|
|
21
84
|
def initialize(**kwargs)
|
|
22
85
|
@app_id = kwargs[:app_id] || kwargs[:client_id]
|
|
23
86
|
@client_secret = kwargs[:client_secret]
|
|
@@ -32,12 +95,32 @@ module MixinBot
|
|
|
32
95
|
self.pin = kwargs[:pin] || spend_key
|
|
33
96
|
end
|
|
34
97
|
|
|
98
|
+
##
|
|
99
|
+
# Validates if the configuration has all required credentials.
|
|
100
|
+
#
|
|
101
|
+
# Required fields are:
|
|
102
|
+
# - app_id
|
|
103
|
+
# - session_id
|
|
104
|
+
# - session_private_key
|
|
105
|
+
# - server_public_key
|
|
106
|
+
#
|
|
107
|
+
# @return [Boolean] true if all required fields are present
|
|
108
|
+
#
|
|
35
109
|
def valid?
|
|
36
110
|
%i[app_id session_id session_private_key server_public_key].all? do |attr|
|
|
37
111
|
send(attr).present?
|
|
38
112
|
end
|
|
39
113
|
end
|
|
40
114
|
|
|
115
|
+
##
|
|
116
|
+
# Sets the session private key with automatic format conversion.
|
|
117
|
+
#
|
|
118
|
+
# Handles Ed25519 key conversion:
|
|
119
|
+
# - If key is 32 bytes (seed), converts to 64-byte keypair
|
|
120
|
+
# - Automatically converts to Curve25519 format for encryption
|
|
121
|
+
#
|
|
122
|
+
# @param key [String] the session private key in various formats
|
|
123
|
+
#
|
|
41
124
|
def session_private_key=(key)
|
|
42
125
|
return if key.blank?
|
|
43
126
|
|
|
@@ -52,19 +135,35 @@ module MixinBot
|
|
|
52
135
|
@session_private_key_curve25519 = JOSE::JWA::Ed25519.sk_to_curve25519(@session_private_key) if @session_private_key.size == 64
|
|
53
136
|
end
|
|
54
137
|
|
|
138
|
+
##
|
|
139
|
+
# Sets the server public key with automatic format conversion.
|
|
140
|
+
#
|
|
141
|
+
# Converts Ed25519 public key to Curve25519 format when needed.
|
|
142
|
+
# Handles both hex-encoded and Base64-encoded keys.
|
|
143
|
+
#
|
|
144
|
+
# @param key [String] the server public key
|
|
145
|
+
#
|
|
55
146
|
def server_public_key=(key)
|
|
56
147
|
return if key.blank?
|
|
57
148
|
|
|
58
149
|
@server_public_key = decode_key key
|
|
59
150
|
# HEX encoded
|
|
60
151
|
@server_public_key_curve25519 =
|
|
61
|
-
if key.match?(/\A
|
|
152
|
+
if key.match?(/\A\h{32,}\z/i)
|
|
62
153
|
JOSE::JWA::Ed25519.pk_to_curve25519 @server_public_key
|
|
63
154
|
else
|
|
64
155
|
server_public_key
|
|
65
156
|
end
|
|
66
157
|
end
|
|
67
158
|
|
|
159
|
+
##
|
|
160
|
+
# Sets the spend key with automatic format conversion.
|
|
161
|
+
#
|
|
162
|
+
# Used for signing transactions in the Safe API.
|
|
163
|
+
# Converts from seed format to full keypair if needed.
|
|
164
|
+
#
|
|
165
|
+
# @param key [String] the spend private key
|
|
166
|
+
#
|
|
68
167
|
def spend_key=(key)
|
|
69
168
|
return if key.blank?
|
|
70
169
|
|
|
@@ -77,6 +176,14 @@ module MixinBot
|
|
|
77
176
|
end
|
|
78
177
|
end
|
|
79
178
|
|
|
179
|
+
##
|
|
180
|
+
# Sets the PIN with automatic format conversion.
|
|
181
|
+
#
|
|
182
|
+
# The PIN is used for certain operations requiring additional authorization.
|
|
183
|
+
# Defaults to the spend_key if not explicitly set.
|
|
184
|
+
#
|
|
185
|
+
# @param key [String] the PIN key
|
|
186
|
+
#
|
|
80
187
|
def pin=(key)
|
|
81
188
|
return if key.blank?
|
|
82
189
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MixinBot
|
|
4
|
+
##
|
|
5
|
+
# Base error class for all MixinBot errors.
|
|
6
|
+
#
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
|
|
9
|
+
##
|
|
10
|
+
# Raised when invalid arguments are provided.
|
|
11
|
+
#
|
|
12
|
+
class ArgumentError < StandardError; end
|
|
13
|
+
|
|
14
|
+
##
|
|
15
|
+
# Raised when HTTP request fails.
|
|
16
|
+
#
|
|
17
|
+
class HttpError < Error; end
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
# Raised when a request to Mixin API fails.
|
|
21
|
+
#
|
|
22
|
+
class RequestError < Error; end
|
|
23
|
+
|
|
24
|
+
##
|
|
25
|
+
# Raised when Mixin API returns an error response.
|
|
26
|
+
#
|
|
27
|
+
class ResponseError < Error; end
|
|
28
|
+
|
|
29
|
+
##
|
|
30
|
+
# Raised when a requested resource is not found (HTTP 404).
|
|
31
|
+
#
|
|
32
|
+
class NotFoundError < Error; end
|
|
33
|
+
|
|
34
|
+
##
|
|
35
|
+
# Raised when a user is not found (error code 10404).
|
|
36
|
+
#
|
|
37
|
+
class UserNotFoundError < Error; end
|
|
38
|
+
|
|
39
|
+
##
|
|
40
|
+
# Raised when authentication fails (HTTP 401).
|
|
41
|
+
#
|
|
42
|
+
class UnauthorizedError < Error; end
|
|
43
|
+
|
|
44
|
+
##
|
|
45
|
+
# Raised when access is forbidden (HTTP 403).
|
|
46
|
+
#
|
|
47
|
+
class ForbiddenError < Error; end
|
|
48
|
+
|
|
49
|
+
##
|
|
50
|
+
# Raised when there is insufficient balance for a transaction (error code 20117).
|
|
51
|
+
#
|
|
52
|
+
class InsufficientBalanceError < Error; end
|
|
53
|
+
|
|
54
|
+
##
|
|
55
|
+
# Raised when selected UTXOs cannot cover the requested amount (mirrors Go +UtxoInsufficientError+).
|
|
56
|
+
#
|
|
57
|
+
class UtxoInsufficientError < InsufficientBalanceError
|
|
58
|
+
attr_reader :total_input, :total_output, :output_size
|
|
59
|
+
|
|
60
|
+
def initialize(message, total_input: nil, total_output: nil, output_size: nil)
|
|
61
|
+
super(message)
|
|
62
|
+
@total_input = total_input
|
|
63
|
+
@total_output = total_output
|
|
64
|
+
@output_size = output_size
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
##
|
|
69
|
+
# Raised when there is insufficient pool for a transaction (error code 30103).
|
|
70
|
+
#
|
|
71
|
+
class InsufficientPoolError < Error; end
|
|
72
|
+
|
|
73
|
+
##
|
|
74
|
+
# Raised when PIN verification fails (error codes 20118, 20119).
|
|
75
|
+
#
|
|
76
|
+
class PinError < Error; end
|
|
77
|
+
|
|
78
|
+
##
|
|
79
|
+
# Raised when NFO memo format is invalid.
|
|
80
|
+
#
|
|
81
|
+
class InvalidNfoFormatError < Error; end
|
|
82
|
+
|
|
83
|
+
##
|
|
84
|
+
# Raised when UUID format is invalid.
|
|
85
|
+
#
|
|
86
|
+
class InvalidUuidFormatError < Error; end
|
|
87
|
+
|
|
88
|
+
##
|
|
89
|
+
# Raised when transaction format is invalid.
|
|
90
|
+
#
|
|
91
|
+
class InvalidTransactionFormatError < Error; end
|
|
92
|
+
|
|
93
|
+
##
|
|
94
|
+
# Raised when configuration is not valid or incomplete.
|
|
95
|
+
#
|
|
96
|
+
class ConfigurationNotValidError < Error; end
|
|
97
|
+
|
|
98
|
+
##
|
|
99
|
+
# Raised when invoice format is invalid.
|
|
100
|
+
#
|
|
101
|
+
class InvalidInvoiceFormatError < Error; end
|
|
102
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MixinBot
|
|
4
|
+
module Models
|
|
5
|
+
##
|
|
6
|
+
# Wraps a raw Mixin API JSON object so callers can use both:
|
|
7
|
+
# - +response['data']['user_id']+ (envelope shape)
|
|
8
|
+
# - +response['user_id']+ (flattened shape, matching legacy +merge!+ behaviour)
|
|
9
|
+
#
|
|
10
|
+
class ApiEnvelope < SimpleDelegator
|
|
11
|
+
def [](key)
|
|
12
|
+
k = key.is_a?(Symbol) ? key.to_s : key
|
|
13
|
+
inner = __getobj__
|
|
14
|
+
return inner[k] if inner.key?(k)
|
|
15
|
+
|
|
16
|
+
data = inner['data']
|
|
17
|
+
return data[k] if data.is_a?(Hash) && data.key?(k)
|
|
18
|
+
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def dig(*keys)
|
|
23
|
+
return nil if keys.empty?
|
|
24
|
+
|
|
25
|
+
k0 = keys[0]
|
|
26
|
+
k0 = k0.to_s if k0.is_a?(Symbol)
|
|
27
|
+
inner = __getobj__
|
|
28
|
+
|
|
29
|
+
if inner.key?(k0)
|
|
30
|
+
v = inner[k0]
|
|
31
|
+
return v if keys.size == 1
|
|
32
|
+
|
|
33
|
+
return v.dig(*keys[1..]) if v.respond_to?(:dig)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
data = inner['data']
|
|
37
|
+
return nil unless data.is_a?(Hash) && data.key?(k0)
|
|
38
|
+
|
|
39
|
+
v = data[k0]
|
|
40
|
+
return v if keys.size == 1
|
|
41
|
+
|
|
42
|
+
v.respond_to?(:dig) ? v.dig(*keys[1..]) : nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def key?(key)
|
|
46
|
+
k = key.is_a?(Symbol) ? key.to_s : key
|
|
47
|
+
inner = __getobj__
|
|
48
|
+
inner.key?(k) || (inner['data'].is_a?(Hash) && inner['data'].key?(k))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
alias include? key?
|
|
52
|
+
alias has_key? key?
|
|
53
|
+
|
|
54
|
+
def with_indifferent_access
|
|
55
|
+
inner = __getobj__
|
|
56
|
+
base = inner.dup
|
|
57
|
+
d = base['data']
|
|
58
|
+
merged = d.is_a?(Hash) ? base.merge(d) : base
|
|
59
|
+
merged.with_indifferent_access
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def to_h
|
|
63
|
+
__getobj__
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'models/api_envelope'
|
|
4
|
+
require_relative 'models/user'
|
|
5
|
+
require_relative 'models/output'
|
|
6
|
+
require_relative 'models/ghost_keys'
|
|
7
|
+
require_relative 'models/sequencer_transaction_request'
|
|
8
|
+
require_relative 'models/safe_multisig_request'
|
|
9
|
+
require_relative 'models/address'
|
|
10
|
+
require_relative 'models/asset'
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module MixinBot
|
|
6
|
+
# Monitoring helpers (parity with Go monitor package).
|
|
7
|
+
module Monitor
|
|
8
|
+
class AppMessage
|
|
9
|
+
attr_accessor :project, :status, :data
|
|
10
|
+
|
|
11
|
+
def initialize(project: nil, status: 0, data: [])
|
|
12
|
+
@project = project
|
|
13
|
+
@status = status
|
|
14
|
+
@data = data
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.load(yaml_str)
|
|
18
|
+
h = YAML.safe_load(yaml_str, permitted_classes: [Symbol], aliases: true)
|
|
19
|
+
new(
|
|
20
|
+
project: h['project'],
|
|
21
|
+
status: h['status'] || 0,
|
|
22
|
+
data: Array(h['data'])
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def marshal
|
|
27
|
+
YAML.dump(
|
|
28
|
+
'project' => project,
|
|
29
|
+
'status' => status,
|
|
30
|
+
'data' => data
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
def unmarshal_app_message(bytes)
|
|
37
|
+
AppMessage.load(bytes)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def report_to_monitor(api, asset:, amount:, receivers:, threshold:, message:, trace: nil, **transfer_opts)
|
|
41
|
+
memo = message.is_a?(AppMessage) ? message.marshal : message.to_s
|
|
42
|
+
mix = MixinBot::MixAddress.from_members(members: receivers, threshold:)
|
|
43
|
+
trace ||= MixinBot.utils.unique_object_id(mix.address, asset, amount, api.config.app_id, memo,
|
|
44
|
+
(Time.now.to_i / 60).to_s)
|
|
45
|
+
existing = begin
|
|
46
|
+
api.safe_transaction(trace)
|
|
47
|
+
rescue StandardError
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
return existing if existing.present? && existing['data'].present?
|
|
51
|
+
|
|
52
|
+
api.create_safe_transfer(
|
|
53
|
+
members: receivers,
|
|
54
|
+
threshold:,
|
|
55
|
+
asset_id: asset,
|
|
56
|
+
amount:,
|
|
57
|
+
trace_id: trace,
|
|
58
|
+
memo:,
|
|
59
|
+
**transfer_opts
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def check_retryable_error(error)
|
|
64
|
+
return false if error.nil?
|
|
65
|
+
|
|
66
|
+
reason = error.message.to_s.downcase
|
|
67
|
+
return true if reason.include?('timeout')
|
|
68
|
+
return true if reason.include?('internal server')
|
|
69
|
+
return true if reason.include?('insufficient')
|
|
70
|
+
return true if reason.include?('inputs locked by')
|
|
71
|
+
return true if reason.include?('by other transaction')
|
|
72
|
+
|
|
73
|
+
false
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MixinBot
|
|
4
|
+
class Transaction
|
|
5
|
+
##
|
|
6
|
+
# Byte cursor for decoding raw transaction bytes.
|
|
7
|
+
#
|
|
8
|
+
class Buffer
|
|
9
|
+
attr_reader :bytes
|
|
10
|
+
|
|
11
|
+
def initialize(bytes)
|
|
12
|
+
@bytes = bytes
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def shift(byte_count = nil)
|
|
16
|
+
return @bytes.shift if byte_count.nil?
|
|
17
|
+
|
|
18
|
+
@bytes.shift(byte_count)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def peek(byte_count)
|
|
22
|
+
@bytes[0, byte_count]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def size
|
|
26
|
+
@bytes.size
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def empty?
|
|
30
|
+
@bytes.empty?
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|