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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +75 -0
  3. data/API_COVERAGE.md +220 -0
  4. data/CHANGELOG.md +108 -0
  5. data/README.md +375 -0
  6. data/docs/agent/cli.md +149 -0
  7. data/docs/agent/cookbook.md +152 -0
  8. data/examples/blaze.rb +43 -0
  9. data/examples/config.yml.example +21 -0
  10. data/lib/mixin_bot/address.rb +43 -3
  11. data/lib/mixin_bot/api/app.rb +75 -0
  12. data/lib/mixin_bot/api/asset.rb +114 -3
  13. data/lib/mixin_bot/api/auth.rb +29 -9
  14. data/lib/mixin_bot/api/blaze.rb +81 -0
  15. data/lib/mixin_bot/api/chain.rb +94 -0
  16. data/lib/mixin_bot/api/circle.rb +57 -0
  17. data/lib/mixin_bot/api/code.rb +21 -0
  18. data/lib/mixin_bot/api/computer_api.rb +60 -0
  19. data/lib/mixin_bot/api/conversation.rb +33 -10
  20. data/lib/mixin_bot/api/deposit.rb +19 -0
  21. data/lib/mixin_bot/api/encrypted_message.rb +1 -1
  22. data/lib/mixin_bot/api/external.rb +12 -0
  23. data/lib/mixin_bot/api/fiat.rb +12 -0
  24. data/lib/mixin_bot/api/inscription.rb +2 -2
  25. data/lib/mixin_bot/api/legacy_collectible.rb +26 -27
  26. data/lib/mixin_bot/api/legacy_multisig.rb +20 -21
  27. data/lib/mixin_bot/api/legacy_output.rb +10 -3
  28. data/lib/mixin_bot/api/legacy_payment.rb +2 -0
  29. data/lib/mixin_bot/api/legacy_snapshot.rb +16 -0
  30. data/lib/mixin_bot/api/legacy_transaction.rb +28 -13
  31. data/lib/mixin_bot/api/legacy_transfer.rb +11 -8
  32. data/lib/mixin_bot/api/legacy_user.rb +51 -0
  33. data/lib/mixin_bot/api/me.rb +120 -3
  34. data/lib/mixin_bot/api/message.rb +66 -28
  35. data/lib/mixin_bot/api/multisig.rb +19 -0
  36. data/lib/mixin_bot/api/network.rb +17 -0
  37. data/lib/mixin_bot/api/network_asset.rb +27 -0
  38. data/lib/mixin_bot/api/output.rb +1 -1
  39. data/lib/mixin_bot/api/pin.rb +16 -3
  40. data/lib/mixin_bot/api/pin_payload.rb +26 -0
  41. data/lib/mixin_bot/api/session.rb +14 -0
  42. data/lib/mixin_bot/api/snapshot.rb +6 -0
  43. data/lib/mixin_bot/api/tip.rb +74 -1
  44. data/lib/mixin_bot/api/transaction.rb +106 -17
  45. data/lib/mixin_bot/api/transfer.rb +141 -14
  46. data/lib/mixin_bot/api/turn.rb +12 -0
  47. data/lib/mixin_bot/api/user.rb +148 -45
  48. data/lib/mixin_bot/api/withdraw.rb +29 -23
  49. data/lib/mixin_bot/api.rb +252 -3
  50. data/lib/mixin_bot/bot_auth.rb +71 -0
  51. data/lib/mixin_bot/cli/api.rb +224 -143
  52. data/lib/mixin_bot/cli/base.rb +77 -0
  53. data/lib/mixin_bot/cli/call.rb +71 -0
  54. data/lib/mixin_bot/cli/errors.rb +56 -0
  55. data/lib/mixin_bot/cli/node.rb +9 -2
  56. data/lib/mixin_bot/cli/output.rb +196 -0
  57. data/lib/mixin_bot/cli/schema.rb +274 -0
  58. data/lib/mixin_bot/cli/schema_command.rb +21 -0
  59. data/lib/mixin_bot/cli/utils.rb +114 -18
  60. data/lib/mixin_bot/cli.rb +124 -48
  61. data/lib/mixin_bot/client/error_mapper.rb +40 -0
  62. data/lib/mixin_bot/client.rb +94 -64
  63. data/lib/mixin_bot/computer.rb +132 -0
  64. data/lib/mixin_bot/configuration.rb +108 -1
  65. data/lib/mixin_bot/errors.rb +102 -0
  66. data/lib/mixin_bot/models/address.rb +11 -0
  67. data/lib/mixin_bot/models/api_envelope.rb +67 -0
  68. data/lib/mixin_bot/models/asset.rb +11 -0
  69. data/lib/mixin_bot/models/ghost_keys.rb +14 -0
  70. data/lib/mixin_bot/models/output.rb +11 -0
  71. data/lib/mixin_bot/models/safe_multisig_request.rb +11 -0
  72. data/lib/mixin_bot/models/sequencer_transaction_request.rb +11 -0
  73. data/lib/mixin_bot/models/user.rb +11 -0
  74. data/lib/mixin_bot/models.rb +10 -0
  75. data/lib/mixin_bot/monitor.rb +77 -0
  76. data/lib/mixin_bot/transaction/buffer.rb +34 -0
  77. data/lib/mixin_bot/transaction/decoder.rb +227 -0
  78. data/lib/mixin_bot/transaction/encoder.rb +255 -0
  79. data/lib/mixin_bot/transaction.rb +6 -475
  80. data/lib/mixin_bot/url_scheme.rb +63 -0
  81. data/lib/mixin_bot/utils/address.rb +17 -80
  82. data/lib/mixin_bot/utils/crypto.rb +173 -1
  83. data/lib/mixin_bot/utils/decoder.rb +1 -1
  84. data/lib/mixin_bot/utils/encoder.rb +13 -0
  85. data/lib/mixin_bot/utils.rb +45 -0
  86. data/lib/mixin_bot/uuid.rb +78 -1
  87. data/lib/mixin_bot/version.rb +11 -1
  88. data/lib/mixin_bot.rb +172 -18
  89. data/lib/mvm/bridge.rb +46 -0
  90. data/lib/mvm/client.rb +60 -0
  91. data/lib/mvm/nft.rb +4 -2
  92. data/lib/mvm/registry.rb +2 -1
  93. data/lib/mvm.rb +93 -0
  94. data/lib/tasks/api_coverage.rake +20 -0
  95. data/llms.txt +30 -0
  96. 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-----
@@ -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].reject { |member| member.start_with?(MAIN_ADDRESS_PREFIX) }
27
- @xin_members = args[:members].select { |member| member.start_with? MAIN_ADDRESS_PREFIX }
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
- raise ArgumentError, "invalid threshold: #{threshold}" if threshold > (uuid_members.length + xin_members.length)
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*') +
@@ -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
@@ -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
- # https://developers.mixin.one/api/alpha-mixin-network/read-assets/
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
- # https://developers.mixin.one/api/alpha-mixin-network/read-asset/
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
- # https://developers.mixin.one/document/wallet/api/ticker
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
@@ -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: encrypt_pin(kwargs[:pin])
53
+ pin_base64: tip[:pin_base64] || tip[:pin]
37
54
  }
38
55
 
39
- raise ArgumentError, 'pin is required' if pin.blank?
56
+ client.post path, **payload, access_token: kwargs[:access_token]
57
+ end
40
58
 
41
- payload[:pin_base64] = if pin.size > 6
42
- encrypt_tip_pin(pin, 'TIP:OAUTH:APPROVE:', data['scopes'], data['authorization_id'])
43
- else
44
- encrypt_pin(pin)
45
- end
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
- client.post path, **payload, access_token: kwargs[:access_token]
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
@@ -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