mixin_bot 1.4.0 → 2.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/AGENTS.md +75 -0
- data/API_COVERAGE.md +220 -0
- data/CHANGELOG.md +108 -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 +75 -0
- data/lib/mixin_bot/api/asset.rb +114 -3
- data/lib/mixin_bot/api/auth.rb +29 -9
- data/lib/mixin_bot/api/blaze.rb +81 -0
- data/lib/mixin_bot/api/chain.rb +94 -0
- data/lib/mixin_bot/api/circle.rb +57 -0
- data/lib/mixin_bot/api/code.rb +21 -0
- data/lib/mixin_bot/api/computer_api.rb +60 -0
- data/lib/mixin_bot/api/conversation.rb +33 -10
- data/lib/mixin_bot/api/deposit.rb +19 -0
- data/lib/mixin_bot/api/encrypted_message.rb +1 -1
- data/lib/mixin_bot/api/external.rb +12 -0
- 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 +120 -3
- data/lib/mixin_bot/api/message.rb +66 -28
- 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 +29 -23
- data/lib/mixin_bot/api.rb +252 -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 +30 -0
- metadata +79 -9
data/examples/blaze.rb
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require './lib/mixin_bot'
|
|
4
|
+
require 'base64'
|
|
5
|
+
require 'yaml'
|
|
6
|
+
|
|
7
|
+
CONFIG = YAML.load_file("#{File.dirname __FILE__}/config.yml")
|
|
8
|
+
MixinBot.configure do
|
|
9
|
+
self.app_id = CONFIG['app_id']
|
|
10
|
+
self.client_secret = CONFIG['client_secret']
|
|
11
|
+
self.session_id = CONFIG['session_id']
|
|
12
|
+
self.server_public_key = CONFIG['server_public_key']
|
|
13
|
+
self.session_private_key = CONFIG['session_private_key']
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# default connect
|
|
17
|
+
# EM.run {
|
|
18
|
+
# MixinBot.api.start_blaze_connect
|
|
19
|
+
# }
|
|
20
|
+
|
|
21
|
+
EM.run do
|
|
22
|
+
MixinBot.api.start_blaze_connect do
|
|
23
|
+
def on_open(blaze, _event)
|
|
24
|
+
p [Time.now.to_s, :on_open]
|
|
25
|
+
blaze.send list_pending_message
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def on_message(blaze, event)
|
|
29
|
+
raw = JSON.parse ws_message(event.data)
|
|
30
|
+
p [Time.now.to_s, :on_message, raw&.[]('action')]
|
|
31
|
+
|
|
32
|
+
blaze.send acknowledge_message_receipt(raw['data']['message_id']) unless raw&.[]('data')&.[]('message_id').nil?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# def on_error(blaze, event)
|
|
36
|
+
# p [Time.now.to_s, :on_error]
|
|
37
|
+
# end
|
|
38
|
+
|
|
39
|
+
# def on_close(blaze, event)
|
|
40
|
+
# p [Time.now.to_s, :on_close, event.code, event.reason]
|
|
41
|
+
# end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
app_id: 0508a116-1239-4e28-b150-85a8e3e6b400
|
|
2
|
+
client_secret: d9dc58107bacde6713c12734663de6bbb2b83e917d5fe62167bda13ca6665b15
|
|
3
|
+
session_id: 25696f85-b7b4-4509-8c3f-2684a8fc4a2a
|
|
4
|
+
pin_code: "431005"
|
|
5
|
+
server_public_key: b0pjBUKI0Vp9K+NspaLCKKeV1IzuO5o2Zgzp2uriXYn6bKcKowhZ7nsjNT1mxtq82dsPzGficOM9H9n1HvNyilrKrpW5ndu7/dMsw/r3JvbexDvLZfAGmuAHcZOy+AwbMSJ/XurLJg1lS9w9PVkDltqWWy2TOunUlbe1lWYy/1M=
|
|
6
|
+
session_private_key: |
|
|
7
|
+
-----BEGIN RSA PRIVATE KEY-----
|
|
8
|
+
MIICXAIBAAKBgQDQYjiR/Te6Bh/1bk8gWRbQkrX0AIGPja1DLUQHu5Uw9M4P53O3
|
|
9
|
+
f4pDCGoN3R5+LYjODtquOwmEjcMhbhp6XarrnJVXH8WGmJcpjVwGtwIjPTeRMu4Z
|
|
10
|
+
8XWo314fO2pLeiqWbJ4knlGb0oiXxBghwF4KglUSElQ/FXjdKaabHCw0zQIDAQAB
|
|
11
|
+
AoGALgqFlTvtZByWUxPcR7lnYQ4JRbAW8DDNZ1pI/axkejyciscIujJjygvB4u5I
|
|
12
|
+
HnjRETYW+wfwQmlQA8Lf9slbSe1FlsajNWl/zc+bwoYTO3yfB81GWwk/e20dTGfs
|
|
13
|
+
2d8ITe+LlYUMlqZgBGr2Y2pHeXDvuzC6ZsKZQKoS3B2RscECQQD0RTXp0tNSbWE7
|
|
14
|
+
TzhXwZu3jkIRhZsajeDv+7VYMZ94TO1PGeQSi25jBT8JcDfbyXO8eZTtjARfTEmE
|
|
15
|
+
Ln6ejveHAkEA2mPb+TyZl0FqIluwo1KbCv3PVlaHDL+HyHoBsq+6yhn3VRMDbyqs
|
|
16
|
+
ZKViJxaLrzUx6+IQOr9T8LqFcaQhSrxeCwJANqMgewuoLwC+RejjXmW08erFBmxP
|
|
17
|
+
FDJ2BNfVaUO4Os1iK1ZMOIWtjEKJJhBOvj+iPp8nW7b852AF9aX8tnSeEwJAacH8
|
|
18
|
+
D7l6A5aJCDRw2NazAGKjGoNyiQjjf4Ed+2NASIjEjq1Td20p6N9yJc20PVe8Yieq
|
|
19
|
+
hliLFMOuxbae7KtFuwJBAI9xMuLBLWixwvezyFCAJpj93ClOgSIlM7qgEfUxLvjZ
|
|
20
|
+
YxyVoIU3haXJPYPgaVE9nEwwiNPRS5DJ2fCGT+oGfrY=
|
|
21
|
+
-----END RSA PRIVATE KEY-----
|
data/lib/mixin_bot/address.rb
CHANGED
|
@@ -10,6 +10,14 @@ module MixinBot
|
|
|
10
10
|
class MixAddress
|
|
11
11
|
attr_accessor :version, :uuid_members, :xin_members, :threshold, :address, :payload
|
|
12
12
|
|
|
13
|
+
def self.parse(string)
|
|
14
|
+
new(address: string)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.from_members(members:, threshold:)
|
|
18
|
+
new(members:, threshold:)
|
|
19
|
+
end
|
|
20
|
+
|
|
13
21
|
def initialize(**args)
|
|
14
22
|
args = args.with_indifferent_access
|
|
15
23
|
|
|
@@ -23,8 +31,9 @@ module MixinBot
|
|
|
23
31
|
@version = args[:version] || MIX_ADDRESS_VERSION
|
|
24
32
|
|
|
25
33
|
if args[:members].present?
|
|
26
|
-
@uuid_members = args[:members].
|
|
27
|
-
|
|
34
|
+
@xin_members, @uuid_members = args[:members].partition do |member|
|
|
35
|
+
member.start_with?(MAIN_ADDRESS_PREFIX)
|
|
36
|
+
end
|
|
28
37
|
else
|
|
29
38
|
@uuid_members = args[:uuid_members] || []
|
|
30
39
|
@xin_members = args[:xin_members] || []
|
|
@@ -53,11 +62,42 @@ module MixinBot
|
|
|
53
62
|
}
|
|
54
63
|
end
|
|
55
64
|
|
|
65
|
+
def request_or_generate_ghost_keys(output_index = 0, api: MixinBot.api)
|
|
66
|
+
if xin_members.present?
|
|
67
|
+
key = JOSE::JWA::Ed25519.keypair
|
|
68
|
+
gk = { 'mask' => key[0].unpack1('H*'), 'keys' => [] }
|
|
69
|
+
xin_members.each do |member|
|
|
70
|
+
payload = MixinBot.utils.parse_main_address(member)
|
|
71
|
+
spend_key = payload[0...32]
|
|
72
|
+
view_key = payload[-32..]
|
|
73
|
+
ghost = MixinBot.utils.derive_ghost_public_key(key[1], view_key, spend_key, output_index)
|
|
74
|
+
gk['keys'] << ghost.unpack1('H*')
|
|
75
|
+
end
|
|
76
|
+
gk
|
|
77
|
+
else
|
|
78
|
+
hint = SecureRandom.uuid
|
|
79
|
+
api.create_safe_keys(
|
|
80
|
+
{ receivers: (uuid_members + xin_members).sort, index: output_index, hint: }
|
|
81
|
+
)['data'].first
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
56
85
|
def encode
|
|
57
86
|
raise ArgumentError, 'members should be an array' unless uuid_members.is_a?(Array) || xin_members.is_a?(Array)
|
|
58
87
|
raise ArgumentError, 'members should not be empty' if uuid_members.empty? && xin_members.empty?
|
|
59
88
|
raise ArgumentError, 'members length should less than 256' if uuid_members.length + xin_members.length > 255
|
|
60
|
-
|
|
89
|
+
|
|
90
|
+
member_count = uuid_members.length + xin_members.length
|
|
91
|
+
# UUID mix (Go NewUUIDMixAddress): threshold must be in 1..len(members).
|
|
92
|
+
# XIN-only mix (Go NewMainnetMixAddress): threshold may exceed member count (sparse), e.g. storage 1-of-64 style.
|
|
93
|
+
if uuid_members.present? && xin_members.empty?
|
|
94
|
+
raise ArgumentError, "invalid threshold: #{threshold}" unless threshold.positive? && threshold <= member_count
|
|
95
|
+
elsif xin_members.present? && uuid_members.empty?
|
|
96
|
+
raise ArgumentError, "invalid threshold: #{threshold}" unless threshold.positive?
|
|
97
|
+
raise ArgumentError, 'too many XIN members' if member_count > 64
|
|
98
|
+
elsif threshold > member_count
|
|
99
|
+
raise ArgumentError, "invalid threshold: #{threshold}"
|
|
100
|
+
end
|
|
61
101
|
|
|
62
102
|
prefix =
|
|
63
103
|
[version].pack('C*') +
|
data/lib/mixin_bot/api/app.rb
CHANGED
|
@@ -3,23 +3,98 @@
|
|
|
3
3
|
module MixinBot
|
|
4
4
|
class API
|
|
5
5
|
module App
|
|
6
|
+
def app(app_id, access_token: nil)
|
|
7
|
+
path = format('/apps/%<id>s', id: app_id)
|
|
8
|
+
client.get path, access_token:
|
|
9
|
+
end
|
|
10
|
+
alias fetch_app app
|
|
11
|
+
|
|
12
|
+
def apps(access_token: nil)
|
|
13
|
+
client.get '/apps', access_token:
|
|
14
|
+
end
|
|
15
|
+
alias fetch_apps apps
|
|
16
|
+
|
|
17
|
+
def app_properties(access_token: nil)
|
|
18
|
+
client.get '/apps/property', access_token:
|
|
19
|
+
end
|
|
20
|
+
alias app_property app_properties
|
|
21
|
+
|
|
22
|
+
def app_billing(app_id, access_token: nil)
|
|
23
|
+
path = format('/safe/apps/%<id>s/billing', id: app_id)
|
|
24
|
+
client.get path, access_token:
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def create_app(**kwargs)
|
|
28
|
+
payload = {
|
|
29
|
+
redirect_uri: kwargs[:redirect_uri],
|
|
30
|
+
home_uri: kwargs[:home_uri],
|
|
31
|
+
name: kwargs[:name],
|
|
32
|
+
description: kwargs[:description],
|
|
33
|
+
icon_base64: kwargs[:icon_base64],
|
|
34
|
+
category: kwargs[:category],
|
|
35
|
+
capabilities: kwargs[:capabilities],
|
|
36
|
+
resource_patterns: kwargs[:resource_patterns]
|
|
37
|
+
}.compact
|
|
38
|
+
client.post '/apps', **payload, access_token: kwargs[:access_token]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def update_app(app_id, **kwargs)
|
|
42
|
+
path = format('/apps/%<id>s', id: app_id)
|
|
43
|
+
payload = {
|
|
44
|
+
redirect_uri: kwargs[:redirect_uri],
|
|
45
|
+
home_uri: kwargs[:home_uri],
|
|
46
|
+
name: kwargs[:name],
|
|
47
|
+
description: kwargs[:description],
|
|
48
|
+
icon_base64: kwargs[:icon_base64],
|
|
49
|
+
category: kwargs[:category],
|
|
50
|
+
capabilities: kwargs[:capabilities],
|
|
51
|
+
resource_patterns: kwargs[:resource_patterns]
|
|
52
|
+
}.compact
|
|
53
|
+
client.post path, **payload, access_token: kwargs[:access_token]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def rotate_app_secret(app_id, access_token: nil)
|
|
57
|
+
path = format('/apps/%<id>s/secret', id: app_id)
|
|
58
|
+
client.post path, access_token:
|
|
59
|
+
end
|
|
60
|
+
alias update_app_secret rotate_app_secret
|
|
61
|
+
|
|
62
|
+
def update_app_safe_session(app_id, session_public_key:, access_token: nil)
|
|
63
|
+
path = format('/safe/apps/%<id>s/session', id: app_id)
|
|
64
|
+
client.post path, session_public_key:, access_token:
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def register_app_safe(app_id, spend_public_key:, signature_base64:, access_token: nil)
|
|
68
|
+
path = format('/safe/apps/%<id>s/register', id: app_id)
|
|
69
|
+
client.post path, spend_public_key:, signature_base64:, access_token:
|
|
70
|
+
end
|
|
71
|
+
|
|
6
72
|
def add_favorite_app(app_id, access_token: nil)
|
|
7
73
|
path = format('/apps/%<id>s/favorite', id: app_id)
|
|
8
74
|
|
|
9
75
|
client.post path, access_token:
|
|
10
76
|
end
|
|
77
|
+
alias favorite_app add_favorite_app
|
|
11
78
|
|
|
12
79
|
def remove_favorite_app(app_id, access_token: nil)
|
|
13
80
|
path = format('/apps/%<id>s/unfavorite', id: app_id)
|
|
14
81
|
|
|
15
82
|
client.post path, access_token:
|
|
16
83
|
end
|
|
84
|
+
alias unfavorite_app remove_favorite_app
|
|
17
85
|
|
|
18
86
|
def favorite_apps(user_id = nil, access_token: nil)
|
|
19
87
|
path = format('/users/%<id>s/apps/favorite', id: user_id || config.app_id)
|
|
20
88
|
|
|
21
89
|
client.get path, access_token:
|
|
22
90
|
end
|
|
91
|
+
|
|
92
|
+
def transfer_app_ownership(receiver_user_id:, pin:, access_token: nil)
|
|
93
|
+
path = format('/apps/%<app_id>s/transfer', app_id: config.app_id)
|
|
94
|
+
tip = tip_or_legacy_pin_payload(pin, 'TIP:APP:OWNERSHIP:TRANSFER:', receiver_user_id)
|
|
95
|
+
client.post path, user_id: receiver_user_id, pin_base64: tip[:pin_base64] || tip[:pin], access_token:
|
|
96
|
+
end
|
|
97
|
+
alias migrate transfer_app_ownership
|
|
23
98
|
end
|
|
24
99
|
end
|
|
25
100
|
end
|
data/lib/mixin_bot/api/asset.rb
CHANGED
|
@@ -2,20 +2,101 @@
|
|
|
2
2
|
|
|
3
3
|
module MixinBot
|
|
4
4
|
class API
|
|
5
|
+
##
|
|
6
|
+
# API methods for asset management.
|
|
7
|
+
#
|
|
8
|
+
# Provides methods to:
|
|
9
|
+
# - List all assets in bot's wallet
|
|
10
|
+
# - Read specific asset information
|
|
11
|
+
# - Get asset ticker/price data
|
|
12
|
+
#
|
|
5
13
|
module Asset
|
|
6
|
-
|
|
14
|
+
##
|
|
15
|
+
# Retrieves all assets in the bot's wallet.
|
|
16
|
+
#
|
|
17
|
+
# Returns an array of asset objects, each containing:
|
|
18
|
+
# - asset_id: the asset UUID
|
|
19
|
+
# - symbol: the asset symbol (e.g., "BTC", "ETH")
|
|
20
|
+
# - name: the full asset name
|
|
21
|
+
# - icon_url: URL to the asset icon
|
|
22
|
+
# - balance: current balance
|
|
23
|
+
# - destination: deposit address
|
|
24
|
+
# - tag: deposit memo/tag (if applicable)
|
|
25
|
+
# - price_btc: price in BTC
|
|
26
|
+
# - price_usd: price in USD
|
|
27
|
+
# - chain_id: the blockchain UUID
|
|
28
|
+
# - change_btc: 24h price change in BTC
|
|
29
|
+
# - change_usd: 24h price change in USD
|
|
30
|
+
# - confirmations: required confirmations
|
|
31
|
+
# - asset_key: the asset key for deposit
|
|
32
|
+
#
|
|
33
|
+
# @param access_token [String, nil] optional access token
|
|
34
|
+
# @return [Array<Hash>] array of asset objects
|
|
35
|
+
#
|
|
36
|
+
# @example
|
|
37
|
+
# assets = api.assets
|
|
38
|
+
# assets.each do |asset|
|
|
39
|
+
# puts "#{asset['symbol']}: #{asset['balance']}"
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# @see https://developers.mixin.one/docs/api/assets/assets
|
|
43
|
+
#
|
|
7
44
|
def assets(access_token: nil)
|
|
8
45
|
path = '/assets'
|
|
9
46
|
client.get path, access_token:
|
|
10
47
|
end
|
|
11
48
|
|
|
12
|
-
|
|
49
|
+
##
|
|
50
|
+
# Retrieves information for a specific asset.
|
|
51
|
+
#
|
|
52
|
+
# Returns detailed information about a single asset including:
|
|
53
|
+
# - Current balance
|
|
54
|
+
# - Price information
|
|
55
|
+
# - Deposit address
|
|
56
|
+
# - Network details
|
|
57
|
+
#
|
|
58
|
+
# @param asset_id [String] the asset UUID
|
|
59
|
+
# @param access_token [String, nil] optional access token
|
|
60
|
+
# @return [Hash] the asset information
|
|
61
|
+
#
|
|
62
|
+
# @example
|
|
63
|
+
# # Get Bitcoin information
|
|
64
|
+
# btc = api.asset('c6d0c728-2624-429b-8e0d-d9d19b6592fa')
|
|
65
|
+
# puts "BTC Balance: #{btc['balance']}"
|
|
66
|
+
# puts "BTC Price: $#{btc['price_usd']}"
|
|
67
|
+
#
|
|
68
|
+
# @see https://developers.mixin.one/docs/api/assets/asset
|
|
69
|
+
#
|
|
13
70
|
def asset(asset_id, access_token: nil)
|
|
14
71
|
path = format('/assets/%<asset_id>s', asset_id:)
|
|
15
72
|
client.get path, access_token:
|
|
16
73
|
end
|
|
17
74
|
|
|
18
|
-
|
|
75
|
+
##
|
|
76
|
+
# Retrieves ticker/price data for an asset.
|
|
77
|
+
#
|
|
78
|
+
# Returns historical price and volume data for an asset,
|
|
79
|
+
# useful for charts and price tracking.
|
|
80
|
+
#
|
|
81
|
+
# @param asset_id [String] the asset UUID
|
|
82
|
+
# @param kwargs [Hash] query options
|
|
83
|
+
# @option kwargs [String, DateTime, Time] :offset the time offset for historical data
|
|
84
|
+
# @option kwargs [String] :access_token optional access token
|
|
85
|
+
# @return [Hash] ticker data including price and volume
|
|
86
|
+
#
|
|
87
|
+
# @example
|
|
88
|
+
# # Get current ticker
|
|
89
|
+
# ticker = api.ticker('c6d0c728-2624-429b-8e0d-d9d19b6592fa')
|
|
90
|
+
# puts "Price: $#{ticker['price_usd']}"
|
|
91
|
+
#
|
|
92
|
+
# # Get historical ticker
|
|
93
|
+
# ticker = api.ticker(
|
|
94
|
+
# 'c6d0c728-2624-429b-8e0d-d9d19b6592fa',
|
|
95
|
+
# offset: 1.day.ago
|
|
96
|
+
# )
|
|
97
|
+
#
|
|
98
|
+
# @see https://developers.mixin.one/docs/api/assets/ticker
|
|
99
|
+
#
|
|
19
100
|
def ticker(asset_id, **kwargs)
|
|
20
101
|
offset = kwargs[:offset]
|
|
21
102
|
offset = DateTime.rfc3339(offset) if offset.is_a? String
|
|
@@ -24,6 +105,36 @@ module MixinBot
|
|
|
24
105
|
path = '/ticker'
|
|
25
106
|
client.get path, asset_id:, offset:, access_token: kwargs[:access_token]
|
|
26
107
|
end
|
|
108
|
+
|
|
109
|
+
def fetch_assets(asset_ids, access_token: nil)
|
|
110
|
+
client.fetch_post_array '/safe/assets/fetch', Array(asset_ids), access_token:
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def asset_fee(asset_id, destination:, access_token: nil)
|
|
114
|
+
path = format('/safe/assets/%<asset_id>s/fees', asset_id:)
|
|
115
|
+
client.get path, destination:, access_token:
|
|
116
|
+
end
|
|
117
|
+
alias read_asset_fee asset_fee
|
|
118
|
+
|
|
119
|
+
def asset_balance(asset_id)
|
|
120
|
+
outputs = safe_outputs(asset: asset_id, state: 'unspent')
|
|
121
|
+
Array(outputs['data']).sum { |o| o['amount'].to_d }
|
|
122
|
+
end
|
|
123
|
+
alias asset_balance_with_safe_user asset_balance
|
|
124
|
+
|
|
125
|
+
def user_asset_balance(user_id, asset_id, access_token: nil)
|
|
126
|
+
members_hash = MixinBot.utils.hash_members([user_id])
|
|
127
|
+
path = '/safe/outputs'
|
|
128
|
+
response = client.get(
|
|
129
|
+
path,
|
|
130
|
+
members: members_hash,
|
|
131
|
+
threshold: 1,
|
|
132
|
+
asset: asset_id,
|
|
133
|
+
state: 'unspent',
|
|
134
|
+
access_token:
|
|
135
|
+
)
|
|
136
|
+
Array(response['data']).sum { |o| o['amount'].to_d }
|
|
137
|
+
end
|
|
27
138
|
end
|
|
28
139
|
end
|
|
29
140
|
end
|
data/lib/mixin_bot/api/auth.rb
CHANGED
|
@@ -3,8 +3,22 @@
|
|
|
3
3
|
module MixinBot
|
|
4
4
|
class API
|
|
5
5
|
module Auth
|
|
6
|
+
def sign_oauth_access_token(_authorization_id:, method:, uri:, body:, scope:, request_id: nil, **kwargs)
|
|
7
|
+
MixinBot.utils.access_token(
|
|
8
|
+
method,
|
|
9
|
+
uri,
|
|
10
|
+
body,
|
|
11
|
+
exp_in: kwargs[:exp_in] || 600,
|
|
12
|
+
scp: scope,
|
|
13
|
+
app_id: kwargs[:app_id] || config.app_id,
|
|
14
|
+
session_id: kwargs[:session_id] || config.session_id,
|
|
15
|
+
private_key: kwargs[:private_key] || config.session_private_key,
|
|
16
|
+
request_id:
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
6
20
|
def oauth_token(code)
|
|
7
|
-
path = 'oauth/token'
|
|
21
|
+
path = '/oauth/token'
|
|
8
22
|
payload = {
|
|
9
23
|
client_id: config.app_id,
|
|
10
24
|
client_secret: config.client_secret,
|
|
@@ -30,22 +44,28 @@ module MixinBot
|
|
|
30
44
|
|
|
31
45
|
path = '/oauth/authorize'
|
|
32
46
|
pin = kwargs[:pin] || config.pin
|
|
47
|
+
raise ArgumentError, 'pin is required' if pin.blank?
|
|
48
|
+
|
|
49
|
+
tip = tip_or_legacy_pin_payload(pin, 'TIP:OAUTH:APPROVE:', data['scopes'], data['authorization_id'])
|
|
33
50
|
payload = {
|
|
34
51
|
authorization_id: data['authorization_id'],
|
|
35
52
|
scopes: data['scopes'],
|
|
36
|
-
pin_base64:
|
|
53
|
+
pin_base64: tip[:pin_base64] || tip[:pin]
|
|
37
54
|
}
|
|
38
55
|
|
|
39
|
-
|
|
56
|
+
client.post path, **payload, access_token: kwargs[:access_token]
|
|
57
|
+
end
|
|
40
58
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
59
|
+
def authorizations(app_id: nil, access_token: nil)
|
|
60
|
+
params = {}
|
|
61
|
+
params[:app] = app_id if app_id
|
|
62
|
+
client.get '/authorizations', **params, access_token:
|
|
63
|
+
end
|
|
46
64
|
|
|
47
|
-
|
|
65
|
+
def revoke_authorization(client_id, access_token: nil)
|
|
66
|
+
client.post '/oauth/cancel', client_id:, access_token:
|
|
48
67
|
end
|
|
68
|
+
alias revoke_authorize revoke_authorization
|
|
49
69
|
|
|
50
70
|
def authorization_data(app_id, scope = ['PROFILE:READ'])
|
|
51
71
|
@_app_id = app_id
|
data/lib/mixin_bot/api/blaze.rb
CHANGED
|
@@ -58,6 +58,87 @@ module MixinBot
|
|
|
58
58
|
start_blaze_connect(&_block) if reconnect
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
|
+
|
|
62
|
+
def blaze_send_plain_text(socket, conversation_id:, recipient_id:, content:)
|
|
63
|
+
socket.send write_ws_message(
|
|
64
|
+
params: {
|
|
65
|
+
conversation_id:,
|
|
66
|
+
recipient_id:,
|
|
67
|
+
message_id: SecureRandom.uuid,
|
|
68
|
+
category: 'PLAIN_TEXT',
|
|
69
|
+
data_base64: Base64.urlsafe_encode64(content.to_s, padding: false)
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def blaze_send_recall_message(socket, conversation_id:, recipient_id:, message_id:)
|
|
75
|
+
data = { message_id: }.to_json
|
|
76
|
+
socket.send write_ws_message(
|
|
77
|
+
params: {
|
|
78
|
+
conversation_id:,
|
|
79
|
+
recipient_id:,
|
|
80
|
+
message_id: SecureRandom.uuid,
|
|
81
|
+
category: 'MESSAGE_RECALL',
|
|
82
|
+
data_base64: Base64.urlsafe_encode64(data, padding: false)
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def blaze_send_post(socket, conversation_id:, recipient_id:, content:)
|
|
88
|
+
blaze_send_plain_text(socket, conversation_id:, recipient_id:, content:)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def blaze_send_contact(socket, conversation_id:, recipient_id:, contact_id:)
|
|
92
|
+
data = { user_id: contact_id }.to_json
|
|
93
|
+
socket.send write_ws_message(
|
|
94
|
+
params: {
|
|
95
|
+
conversation_id:,
|
|
96
|
+
recipient_id:,
|
|
97
|
+
message_id: SecureRandom.uuid,
|
|
98
|
+
category: 'PLAIN_CONTACT',
|
|
99
|
+
data_base64: Base64.urlsafe_encode64(data, padding: false)
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def blaze_send_app_card(socket, conversation_id:, recipient_id:, title:, description:, action:, icon_url:)
|
|
105
|
+
data = { title:, description:, action:, icon_url: }.to_json
|
|
106
|
+
socket.send write_ws_message(
|
|
107
|
+
params: {
|
|
108
|
+
conversation_id:,
|
|
109
|
+
recipient_id:,
|
|
110
|
+
message_id: SecureRandom.uuid,
|
|
111
|
+
category: 'APP_CARD',
|
|
112
|
+
data_base64: Base64.urlsafe_encode64(data, padding: false)
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def blaze_send_app_button(socket, conversation_id:, recipient_id:, label:, action:, color:)
|
|
118
|
+
data = [{ label:, action:, color: }].to_json
|
|
119
|
+
socket.send write_ws_message(
|
|
120
|
+
params: {
|
|
121
|
+
conversation_id:,
|
|
122
|
+
recipient_id:,
|
|
123
|
+
message_id: SecureRandom.uuid,
|
|
124
|
+
category: 'APP_BUTTON_GROUP',
|
|
125
|
+
data_base64: Base64.urlsafe_encode64(data, padding: false)
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def blaze_send_group_app_button(socket, conversation_id:, recipient_id:, buttons:)
|
|
131
|
+
data = buttons.to_json
|
|
132
|
+
socket.send write_ws_message(
|
|
133
|
+
params: {
|
|
134
|
+
conversation_id:,
|
|
135
|
+
recipient_id:,
|
|
136
|
+
message_id: SecureRandom.uuid,
|
|
137
|
+
category: 'APP_BUTTON_GROUP',
|
|
138
|
+
data_base64: Base64.urlsafe_encode64(data, padding: false)
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
end
|
|
61
142
|
end
|
|
62
143
|
end
|
|
63
144
|
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MixinBot
|
|
4
|
+
class API
|
|
5
|
+
module Chain
|
|
6
|
+
CHAIN_NAMES = {
|
|
7
|
+
'59c09123-95cc-3ffd-a659-0f9169074cee' => 'Lightning',
|
|
8
|
+
'c6d0c728-2624-429b-8e0d-d9d19b6592fa' => 'Bitcoin',
|
|
9
|
+
'fd11b6e3-0b87-41f1-a41f-f0e9b49e5bf0' => 'Bitcoin Cash',
|
|
10
|
+
'574388fd-b93f-4034-a682-01c2bc095d17' => 'Bitcoin SV',
|
|
11
|
+
'76c802a2-7c88-447f-a93e-c29c9e5dd9c8' => 'Litecoin',
|
|
12
|
+
'43d61dcd-e413-450d-80b8-101d5e903357' => 'Ethereum',
|
|
13
|
+
'2204c1ee-0ea2-4add-bb9a-b3719cfff93a' => 'Ethereum Classic',
|
|
14
|
+
'1949e683-6a08-49e2-b087-d6b72398588f' => 'BNB Smart Chain',
|
|
15
|
+
'b7938396-3f94-4e0a-9179-d3440718156f' => 'Polygon',
|
|
16
|
+
'3fb612c5-6844-3979-ae4a-5a84e79da870' => 'Base',
|
|
17
|
+
'60360611-370c-3b69-9826-b13db93f6aba' => 'OP Mainnet',
|
|
18
|
+
'8c590110-1abc-3697-84f2-05214e6516aa' => 'Arbitrum One',
|
|
19
|
+
'a0ffd769-5850-4b48-9651-d2ae44a3e64d' => 'Mixin Virtual Machine',
|
|
20
|
+
'8f5caf2a-283d-4c85-832a-91e83bbf290b' => 'Decred',
|
|
21
|
+
'23dfb5a5-5d7b-48b6-905f-3970e3176e27' => 'Ripple',
|
|
22
|
+
'990c4c29-57e9-48f6-9819-7d986ea44985' => 'Siacoin',
|
|
23
|
+
'6cfe566e-4aad-470b-8c9a-2fd35b49c68d' => 'EOS',
|
|
24
|
+
'6770a1e5-6086-44d5-b60f-545f9d9e8ffd' => 'Dogecoin',
|
|
25
|
+
'6472e7e3-75fd-48b6-b1dc-28d294ee1476' => 'Dash',
|
|
26
|
+
'c996abc9-d94e-4494-b1cf-2a3fd3ac5714' => 'Zcash',
|
|
27
|
+
'27921032-f73e-434e-955f-43d55672ee31' => 'NEM',
|
|
28
|
+
'882eb041-64ea-465f-a4da-817bd3020f52' => 'Arweave',
|
|
29
|
+
'a2c5d22b-62a2-4c13-b3f0-013290dbac60' => 'Horizen',
|
|
30
|
+
'25dabac5-056a-48ff-b9f9-f67395dc407c' => 'TRON',
|
|
31
|
+
'56e63c06-b506-4ec5-885a-4a5ac17b83c1' => 'Stellar',
|
|
32
|
+
'b207bce9-c248-4b8e-b6e3-e357146f3f4c' => 'MassGrid',
|
|
33
|
+
'443e1ef5-bc9b-47d3-be77-07f328876c50' => 'Bytom',
|
|
34
|
+
'71a0e8b5-a289-4845-b661-2b70ff9968aa' => 'Bytom',
|
|
35
|
+
'7397e9f1-4e42-4dc8-8a3b-171daaadd436' => 'Cosmos',
|
|
36
|
+
'9c612618-ca59-4583-af34-be9482f5002d' => 'Akash',
|
|
37
|
+
'17f78d7c-ed96-40ff-980c-5dc62fecbc85' => 'BNB Beacon Chain',
|
|
38
|
+
'05c5ac01-31f9-4a69-aa8a-ab796de1d041' => 'Monero',
|
|
39
|
+
'c99a3779-93df-404d-945d-eddc440aa0b2' => 'Starcoin',
|
|
40
|
+
'05891083-63d2-4f3d-bfbe-d14d7fb9b25a' => 'Bitshares',
|
|
41
|
+
'6877d485-6b64-4225-8d7e-7333393cb243' => 'Ravencoin',
|
|
42
|
+
'1351e6bd-66cf-40c1-8105-8a8fe518a222' => 'Grin',
|
|
43
|
+
'c3b9153a-7fab-4138-a3a4-99849cadc073' => 'VCash',
|
|
44
|
+
'13036886-6b83-4ced-8d44-9f69151587bf' => 'Handshake',
|
|
45
|
+
'd243386e-6d84-42e6-be03-175be17bf275' => 'Nervos',
|
|
46
|
+
'5649ca42-eb5f-4c0e-ae28-d9a4e77eded3' => 'Tezos',
|
|
47
|
+
'f8b77dc0-46fd-4ea1-9821-587342475869' => 'Namecoin',
|
|
48
|
+
'64692c23-8971-4cf4-84a7-4dd1271dd887' => 'Solana',
|
|
49
|
+
'd6ac94f7-c932-4e11-97dd-617867f0669e' => 'NEAR',
|
|
50
|
+
'08285081-e1d8-4be6-9edc-e203afa932da' => 'Filecoin',
|
|
51
|
+
'eea900a8-b327-488c-8d8d-1428702fe240' => 'MobileCoin',
|
|
52
|
+
'54c61a72-b982-4034-a556-0d99e3c21e39' => 'Polkadot',
|
|
53
|
+
'9d29e4f6-d67c-4c4b-9525-604b04afbe9f' => 'Kusama',
|
|
54
|
+
'706b6f84-3333-4e55-8e89-275e71ce9803' => 'Algorand',
|
|
55
|
+
'cbc77539-0a20-4666-8c8a-4ded62b36f0a' => 'Avalanche X-Chain',
|
|
56
|
+
'1f67ac58-87ba-3571-9781-e9413c046f34' => 'Avalanche C-Chain',
|
|
57
|
+
'163a2142-398d-3483-aee3-d47db8da4d10' => 'MarsChain',
|
|
58
|
+
'b12bb04a-1cea-401c-a086-0be61f544889' => 'XDC Network',
|
|
59
|
+
'd2c1c7e1-a1a9-4f88-b282-d93b0a08b42b' => 'Aptos',
|
|
60
|
+
'2bd97283-2582-33a8-bcba-f4b8ed189572' => 'Sui',
|
|
61
|
+
'ef660437-d915-4e27-ad3f-632bfb6ba0ee' => 'TON'
|
|
62
|
+
}.freeze
|
|
63
|
+
|
|
64
|
+
XIN_ASSET_ID = 'c94ac88f-4671-3976-b60a-09064f1811e8'
|
|
65
|
+
VAULTA_ASSET_ID = 'ac2b79f3-ec9c-3d87-b4ca-3e825228dda5'
|
|
66
|
+
|
|
67
|
+
def network_chain(chain_id)
|
|
68
|
+
path = format('/network/chains/%<chain_id>s', chain_id:)
|
|
69
|
+
client.get path, access_token: ''
|
|
70
|
+
end
|
|
71
|
+
alias read_network_chain_by_id network_chain
|
|
72
|
+
|
|
73
|
+
def network_chains
|
|
74
|
+
client.get '/network/chains', access_token: ''
|
|
75
|
+
end
|
|
76
|
+
alias read_network_chains network_chains
|
|
77
|
+
|
|
78
|
+
def chain_name(chain_id)
|
|
79
|
+
CHAIN_NAMES[chain_id] || 'Not Supported Chain'
|
|
80
|
+
end
|
|
81
|
+
alias get_chain_name chain_name
|
|
82
|
+
|
|
83
|
+
def chain_id?(chain_id)
|
|
84
|
+
CHAIN_NAMES.key?(chain_id)
|
|
85
|
+
end
|
|
86
|
+
alias is_chain_id chain_id?
|
|
87
|
+
|
|
88
|
+
def full_chains
|
|
89
|
+
CHAIN_NAMES.transform_values { true }.dup
|
|
90
|
+
end
|
|
91
|
+
alias get_full_chains full_chains
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MixinBot
|
|
4
|
+
class API
|
|
5
|
+
module Circle
|
|
6
|
+
def circle(circle_id, access_token: nil)
|
|
7
|
+
path = format('/circles/%<circle_id>s', circle_id:)
|
|
8
|
+
client.get path, access_token:
|
|
9
|
+
end
|
|
10
|
+
alias fetch_circle circle
|
|
11
|
+
|
|
12
|
+
def circles(access_token: nil)
|
|
13
|
+
client.get '/circles', access_token:
|
|
14
|
+
end
|
|
15
|
+
alias fetch_circles circles
|
|
16
|
+
|
|
17
|
+
def circle_conversations(circle_id, **params)
|
|
18
|
+
path = format('/circles/%<circle_id>s/conversations', circle_id:)
|
|
19
|
+
client.get path, **params.compact, access_token: params[:access_token]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def create_circle(name:, access_token: nil)
|
|
23
|
+
client.post '/circles', name:, access_token:
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def update_circle(circle_id, name:, access_token: nil)
|
|
27
|
+
path = format('/circles/%<circle_id>s', circle_id:)
|
|
28
|
+
client.post path, name:, access_token:
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def delete_circle(circle_id, access_token: nil)
|
|
32
|
+
path = format('/circles/%<circle_id>s/delete', circle_id:)
|
|
33
|
+
client.post path, access_token:
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def add_user_to_circle(user_id:, circle_id:, access_token: nil)
|
|
37
|
+
path = format('/users/%<user_id>s/circles', user_id:)
|
|
38
|
+
client.post path, circle_id:, action: 'ADD', access_token:
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def remove_user_from_circle(user_id:, circle_id:, access_token: nil)
|
|
42
|
+
path = format('/users/%<user_id>s/circles', user_id:)
|
|
43
|
+
client.post path, circle_id:, action: 'REMOVE', access_token:
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def add_conversation_to_circle(conversation_id:, circle_id:, access_token: nil)
|
|
47
|
+
path = format('/conversations/%<conversation_id>s/circles', conversation_id:)
|
|
48
|
+
client.post path, circle_id:, action: 'ADD', access_token:
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def remove_conversation_from_circle(conversation_id:, circle_id:, access_token: nil)
|
|
52
|
+
path = format('/conversations/%<conversation_id>s/circles', conversation_id:)
|
|
53
|
+
client.post path, circle_id:, action: 'REMOVE', access_token:
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|