mixin_bot 2.6.0 → 3.0.2
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 +3 -3
- data/API_COVERAGE.md +1 -0
- data/CHANGELOG.md +22 -1
- data/README.md +116 -5
- data/docs/agent/cookbook.md +49 -0
- data/examples/blaze_async.rb +6 -4
- data/lib/generators/mixin_bot/authentication/USAGE +29 -0
- data/lib/generators/mixin_bot/authentication/authentication_generator.rb +82 -0
- data/lib/generators/mixin_bot/authentication/templates/app/controllers/concerns/authentication.rb.tt +52 -0
- data/lib/generators/mixin_bot/authentication/templates/app/controllers/sessions_controller.rb.tt +24 -0
- data/lib/generators/mixin_bot/authentication/templates/app/models/current.rb.tt +4 -0
- data/lib/generators/mixin_bot/authentication/templates/app/models/session.rb.tt +3 -0
- data/lib/generators/mixin_bot/authentication/templates/app/models/user.rb.tt +12 -0
- data/lib/generators/mixin_bot/authentication/templates/app/views/sessions/new.html.erb.tt +3 -0
- data/lib/generators/mixin_bot/authentication/templates/config/initializers/omniauth.rb.tt +21 -0
- data/lib/generators/mixin_bot/blaze/USAGE +19 -0
- data/lib/generators/mixin_bot/blaze/blaze_generator.rb +43 -0
- data/lib/generators/mixin_bot/blaze/templates/app/mixin/handlers/text_handler.rb +14 -0
- data/lib/generators/mixin_bot/blaze/templates/config/initializers/mixin_bot_blaze.rb +23 -0
- data/lib/generators/mixin_bot/install/USAGE +24 -0
- data/lib/generators/mixin_bot/install/install_generator.rb +50 -0
- data/lib/generators/mixin_bot/notifications/USAGE +13 -0
- data/lib/generators/mixin_bot/notifications/notifications_generator.rb +49 -0
- data/lib/generators/mixin_bot/notifications/templates/app/notifications/payment_received_notification.rb +24 -0
- data/lib/generators/mixin_bot/outputs/USAGE +29 -0
- data/lib/generators/mixin_bot/outputs/outputs_generator.rb +50 -0
- data/lib/generators/mixin_bot/outputs/templates/app/mixin/processors/deposit_processor.rb +21 -0
- data/lib/generators/mixin_bot/outputs/templates/app/models/mixin_output.rb +8 -0
- data/lib/generators/mixin_bot/outputs/templates/db/migrate/create_mixin_outputs.rb.tt +26 -0
- data/lib/generators/mixin_bot/transfers/USAGE +37 -0
- data/lib/generators/mixin_bot/transfers/templates/app/jobs/mixin_transfers/perform_job.rb +13 -0
- data/lib/generators/mixin_bot/transfers/templates/app/jobs/mixin_transfers/reconcile_job.rb +14 -0
- data/lib/generators/mixin_bot/transfers/templates/app/models/mixin_transfer.rb +11 -0
- data/lib/generators/mixin_bot/transfers/templates/db/migrate/create_mixin_transfers.rb.tt +23 -0
- data/lib/generators/mixin_bot/transfers/transfers_generator.rb +67 -0
- data/lib/generators/mixin_bot.rb +10 -0
- data/lib/mixin_bot/api/app.rb +20 -0
- data/lib/mixin_bot/api/blaze_async.rb +5 -1
- data/lib/mixin_bot/api/snapshot.rb +5 -0
- data/lib/mixin_bot/blaze/reactor.rb +249 -0
- data/lib/mixin_bot/blaze/router.rb +333 -0
- data/lib/mixin_bot/cache_store_adapter.rb +3 -1
- data/lib/mixin_bot/client.rb +2 -1
- data/lib/mixin_bot/computer.rb +3 -1
- data/lib/mixin_bot/configuration.rb +52 -0
- data/lib/mixin_bot/json_decoder.rb +14 -0
- data/lib/mixin_bot/notifications/mixin_channel.rb +80 -0
- data/lib/mixin_bot/outputs/cursor_model.rb +33 -0
- data/lib/mixin_bot/outputs/envelope.rb +160 -0
- data/lib/mixin_bot/outputs/memory_stores.rb +97 -0
- data/lib/mixin_bot/outputs/poller.rb +178 -0
- data/lib/mixin_bot/outputs/processing_job.rb +54 -0
- data/lib/mixin_bot/outputs/processors.rb +124 -0
- data/lib/mixin_bot/outputs/receipt_model.rb +86 -0
- data/lib/mixin_bot/outputs.rb +42 -0
- data/lib/mixin_bot/rails/railtie.rb +44 -0
- data/lib/mixin_bot/rails/tasks/poller.rake +33 -0
- data/lib/mixin_bot/rails.rb +34 -0
- data/lib/mixin_bot/registry.rb +79 -0
- data/lib/mixin_bot/transfers/model.rb +112 -0
- data/lib/mixin_bot/transfers/performer.rb +64 -0
- data/lib/mixin_bot/transfers/reconcile.rb +53 -0
- data/lib/mixin_bot/transfers.rb +23 -0
- data/lib/mixin_bot/version.rb +1 -1
- data/lib/mixin_bot.rb +4 -0
- data/lib/puma/plugin/mixin_blaze.rb +263 -0
- data/llms.txt +1 -1
- metadata +69 -7
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Sets up a Mixin Messenger/Network Rails application in one command:
|
|
3
|
+
"Sign in with Mixin" (OmniAuth), Blaze message routing with the Puma
|
|
4
|
+
plugin, the outputs poller with an example processor, the outbound
|
|
5
|
+
transfer ledger, and Noticed-driven Mixin notifications.
|
|
6
|
+
|
|
7
|
+
Skip any component:
|
|
8
|
+
|
|
9
|
+
rails generate mixin_bot:install --skip-notifications --skip-transfers
|
|
10
|
+
rails generate mixin_bot:install --skip notifications,transfers
|
|
11
|
+
rails generate mixin_bot:install --skip authentication
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
rails generate mixin_bot:install
|
|
15
|
+
|
|
16
|
+
This will run, in order:
|
|
17
|
+
rails generate mixin_bot:authentication
|
|
18
|
+
rails generate mixin_bot:blaze
|
|
19
|
+
rails generate mixin_bot:outputs
|
|
20
|
+
rails generate mixin_bot:transfers
|
|
21
|
+
rails generate mixin_bot:notifications
|
|
22
|
+
|
|
23
|
+
Afterwards: set your bot credentials, run `rails db:migrate`, and start
|
|
24
|
+
the poller (`rails mixin_bot:poller`) and your job backend.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
|
|
5
|
+
module MixinBot
|
|
6
|
+
module Generators
|
|
7
|
+
# `rails generate mixin_bot:install` — one-command setup: runs every
|
|
8
|
+
# component generator in dependency order. Skip any of them:
|
|
9
|
+
#
|
|
10
|
+
# rails g mixin_bot:install --skip-notifications --skip-transfers
|
|
11
|
+
# rails g mixin_bot:install --skip notifications transfers
|
|
12
|
+
# rails g mixin_bot:install --skip=notifications,transfers
|
|
13
|
+
#
|
|
14
|
+
# Idempotent: each component generator (and this one) is safe to re-run.
|
|
15
|
+
class InstallGenerator < Rails::Generators::Base
|
|
16
|
+
COMPONENTS = %w[authentication blaze outputs transfers notifications].freeze
|
|
17
|
+
|
|
18
|
+
class_option :skip,
|
|
19
|
+
type: :array, default: [],
|
|
20
|
+
banner: 'component,component2',
|
|
21
|
+
desc: "Skip components (comma-separated; any of: #{COMPONENTS.join(' ')})"
|
|
22
|
+
|
|
23
|
+
COMPONENTS.each do |component|
|
|
24
|
+
class_option :"skip_#{component}", type: :boolean, default: false,
|
|
25
|
+
desc: "Skip the #{component} component"
|
|
26
|
+
|
|
27
|
+
define_method(:"install_#{component}") do
|
|
28
|
+
invoke "mixin_bot:#{component}" unless skip?(component)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def show_plan
|
|
33
|
+
skipped = COMPONENTS.select { |component| skip?(component) }
|
|
34
|
+
say_status :install, if skipped.empty?
|
|
35
|
+
'all components'
|
|
36
|
+
else
|
|
37
|
+
"all components except #{skipped.join(', ')}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def skip?(component)
|
|
44
|
+
return true if options[:"skip_#{component}"]
|
|
45
|
+
|
|
46
|
+
Array(options[:skip]).flat_map { |value| value.to_s.split(',') }.include?(component.to_s)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Adds Noticed-driven Mixin message notifications: ensures the `noticed`
|
|
3
|
+
gem is in the Gemfile and scaffolds one example notification delivered
|
|
4
|
+
through MixinBot's Mixin channel (`deliver_by :mixin`) over the HTTP
|
|
5
|
+
message API.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
rails generate mixin_bot:notifications
|
|
9
|
+
|
|
10
|
+
This will create:
|
|
11
|
+
app/notifications/payment_received_notification.rb
|
|
12
|
+
|
|
13
|
+
It will also ensure `noticed` is present in the Gemfile.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
|
|
5
|
+
module MixinBot
|
|
6
|
+
module Generators
|
|
7
|
+
# `rails generate mixin_bot:notifications` — adds the Noticed gem and
|
|
8
|
+
# scaffolds one example notification delivered as a Mixin message through
|
|
9
|
+
# the gem's MixinChannel (`deliver_by :mixin`).
|
|
10
|
+
class NotificationsGenerator < Rails::Generators::Base
|
|
11
|
+
# Rails 8.1 moved bundle_command out of Actions into BundleHelper.
|
|
12
|
+
unless method_defined?(:bundle_command) || private_method_defined?(:bundle_command)
|
|
13
|
+
begin
|
|
14
|
+
require 'rails/generators/bundle_helper'
|
|
15
|
+
include Rails::Generators::BundleHelper
|
|
16
|
+
rescue LoadError
|
|
17
|
+
# Older Rails ships bundle_command in Rails::Generators::Actions.
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
source_root File.expand_path('templates', __dir__)
|
|
22
|
+
|
|
23
|
+
NOTICED_GEM = 'noticed'
|
|
24
|
+
|
|
25
|
+
def add_noticed_gem
|
|
26
|
+
gemfile = File.read(gemfile_path)
|
|
27
|
+
pattern = /gem\s+["']#{NOTICED_GEM}["']/
|
|
28
|
+
return if gemfile.match?(/^\s*#{pattern}/)
|
|
29
|
+
|
|
30
|
+
if gemfile.match?(/^\s*#\s*#{pattern}/)
|
|
31
|
+
uncomment_lines 'Gemfile', pattern
|
|
32
|
+
bundle_command('install --quiet')
|
|
33
|
+
else
|
|
34
|
+
bundle_command("add #{NOTICED_GEM} --quiet")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def create_example_notification
|
|
39
|
+
template 'app/notifications/payment_received_notification.rb'
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def gemfile_path
|
|
45
|
+
File.expand_path('Gemfile', destination_root)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Example notification delivered as a Mixin message through the mixin channel:
|
|
4
|
+
#
|
|
5
|
+
# PaymentReceivedNotification.with(amount: '1.5').deliver(user)
|
|
6
|
+
#
|
|
7
|
+
# (`user` must answer #mixin_user_id — the mixin_bot:authentication
|
|
8
|
+
# generator's users table has that column.)
|
|
9
|
+
#
|
|
10
|
+
# The recipient and message contract lives here; MixinBot's channel sends
|
|
11
|
+
# over the HTTP message API with the configured bot's credentials.
|
|
12
|
+
class PaymentReceivedNotification < Noticed::Notification
|
|
13
|
+
deliver_by :mixin
|
|
14
|
+
|
|
15
|
+
param :amount
|
|
16
|
+
|
|
17
|
+
def mixin_recipient
|
|
18
|
+
recipient.mixin_user_id
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def mixin_message
|
|
22
|
+
{ category: 'PLAIN_TEXT', content: "Payment received: #{params[:amount]}" }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Scaffolds the Mixin output-polling foundation: the receipt table and
|
|
3
|
+
model, and an example processor under app/mixin/processors. Receipts
|
|
4
|
+
deduplicate every observed output (inbound or outbound); the poller's
|
|
5
|
+
resume cursor is derived from the receipts themselves; processors are
|
|
6
|
+
selected by predicate and run in the job queue.
|
|
7
|
+
|
|
8
|
+
Start polling with the rake task (bots default to MixinBot's global
|
|
9
|
+
credentials; pass a registered bot name for multi-bot apps):
|
|
10
|
+
|
|
11
|
+
rails mixin_bot:poller # default bot
|
|
12
|
+
rails mixin_bot:poller[shop] # registered bot :shop
|
|
13
|
+
|
|
14
|
+
Optional filters via environment variables:
|
|
15
|
+
|
|
16
|
+
MIXIN_BOT_POLL_ASSET restrict to one asset id
|
|
17
|
+
MIXIN_BOT_POLL_MEMBERS comma-separated member user ids
|
|
18
|
+
MIXIN_BOT_POLL_THRESHOLD members threshold
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
rails generate mixin_bot:outputs
|
|
22
|
+
|
|
23
|
+
This will create:
|
|
24
|
+
db/migrate/xxx_create_mixin_outputs.rb
|
|
25
|
+
app/models/mixin_output.rb
|
|
26
|
+
app/mixin/processors/deposit_processor.rb
|
|
27
|
+
|
|
28
|
+
Run `rails db:migrate` afterwards, and make sure a job backend
|
|
29
|
+
(e.g. Solid Queue) is running for processor jobs.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
|
|
5
|
+
module MixinBot
|
|
6
|
+
module Generators
|
|
7
|
+
# `rails generate mixin_bot:outputs` — scaffolds the output-polling
|
|
8
|
+
# foundation: the receipt table and model, and an example processor under
|
|
9
|
+
# app/mixin/processors. Pair with the `mixin_bot:poller` rake task and a
|
|
10
|
+
# job backend (e.g. Solid Queue). The poller's resume cursor is derived
|
|
11
|
+
# from the receipts themselves — there is no separate cursor table.
|
|
12
|
+
class OutputsGenerator < Rails::Generators::Base
|
|
13
|
+
include Rails::Generators::Migration
|
|
14
|
+
|
|
15
|
+
source_root File.expand_path('templates', __dir__)
|
|
16
|
+
|
|
17
|
+
def self.next_migration_number(dirname)
|
|
18
|
+
current = current_migration_number(dirname)
|
|
19
|
+
return Time.now.utc.strftime('%Y%m%d%H%M%S%6N') if current.zero?
|
|
20
|
+
|
|
21
|
+
current.succ.to_s
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def create_migrations
|
|
25
|
+
migration 'create_mixin_outputs'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def create_model
|
|
29
|
+
template 'app/models/mixin_output.rb'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def create_example_processor
|
|
33
|
+
template 'app/mixin/processors/deposit_processor.rb'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# Skips the migration when a same-named one already exists (re-run).
|
|
39
|
+
def migration(name)
|
|
40
|
+
return if self.class.migration_exists?('db/migrate', name)
|
|
41
|
+
|
|
42
|
+
migration_template "db/migrate/#{name}.rb.tt", "db/migrate/#{name}.rb"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def migration_version
|
|
46
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mixin
|
|
4
|
+
module Processors
|
|
5
|
+
# Example processor: matches every inbound (unspent) output. Replace the
|
|
6
|
+
# predicate and body with real business rules — processors run in the job
|
|
7
|
+
# queue and enqueue is at-least-once, so key side effects on
|
|
8
|
+
# envelope.output_id to stay idempotent.
|
|
9
|
+
class DepositProcessor < MixinBot::Outputs::Processor
|
|
10
|
+
def self.matches?(envelope)
|
|
11
|
+
!envelope.spent?
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def process
|
|
15
|
+
Rails.logger.info(
|
|
16
|
+
"received #{envelope.amount} of #{envelope.asset_id} (output #{envelope.output_id})"
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Receipt row for every output the poller observes (inbound or outbound).
|
|
4
|
+
# The (bot_app_id, output_id) unique index is the dedup gate; snapshot-derived
|
|
5
|
+
# fields (memo, opponent_id, trace_id) are cached here by the envelope bridge.
|
|
6
|
+
class MixinOutput < ApplicationRecord
|
|
7
|
+
include MixinBot::Outputs::ReceiptModel
|
|
8
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class CreateMixinOutputs < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :mixin_outputs do |t|
|
|
4
|
+
t.string :bot_app_id, null: false
|
|
5
|
+
t.string :output_id, null: false
|
|
6
|
+
t.string :amount
|
|
7
|
+
t.string :asset_id
|
|
8
|
+
t.string :state
|
|
9
|
+
t.string :transaction_hash
|
|
10
|
+
t.integer :output_index
|
|
11
|
+
t.bigint :sequence
|
|
12
|
+
t.datetime :output_created_at
|
|
13
|
+
t.text :memo
|
|
14
|
+
t.string :opponent_id
|
|
15
|
+
t.string :trace_id
|
|
16
|
+
t.boolean :snapshot_bridged, null: false, default: false
|
|
17
|
+
t.datetime :enqueued_at
|
|
18
|
+
t.timestamps
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
add_index :mixin_outputs, %i[bot_app_id output_id], unique: true
|
|
22
|
+
add_index :mixin_outputs, :bot_app_id
|
|
23
|
+
# cursor lookups: MAX(sequence) per bot
|
|
24
|
+
add_index :mixin_outputs, %i[bot_app_id sequence]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Scaffolds the outbound transfer ledger: a migration and model for
|
|
3
|
+
MixinTransfer (with the runtime state machine), perform and reconcile
|
|
4
|
+
jobs under app/jobs/mixin_transfers, and a Solid Queue recurring
|
|
5
|
+
schedule for reconciliation.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
|
|
9
|
+
transfer = MixinTransfer.create!(
|
|
10
|
+
recipient_id: user_id, # Mixin user id of the recipient
|
|
11
|
+
asset_id: ASSET_ID,
|
|
12
|
+
amount: '1.5',
|
|
13
|
+
memo: 'payout',
|
|
14
|
+
trace_id: SecureRandom.uuid # idempotency: re-sends are deduped
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
The record is enqueued on create and ends `confirmed` (with its
|
|
18
|
+
transaction hash), `failed` (definitive rejection), or `reconciling`
|
|
19
|
+
(timeout — resolved by the recurring job).
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
rails generate mixin_bot:transfers
|
|
23
|
+
|
|
24
|
+
This will create:
|
|
25
|
+
db/migrate/xxx_create_mixin_transfers.rb
|
|
26
|
+
app/models/mixin_transfer.rb
|
|
27
|
+
app/jobs/mixin_transfers/perform_job.rb
|
|
28
|
+
app/jobs/mixin_transfers/reconcile_job.rb
|
|
29
|
+
config/recurring.yml
|
|
30
|
+
|
|
31
|
+
Run `rails db:migrate` afterwards. If config/recurring.yml already
|
|
32
|
+
exists, add the reconcile entry manually:
|
|
33
|
+
|
|
34
|
+
production:
|
|
35
|
+
mixin_transfers_reconcile:
|
|
36
|
+
class: MixinTransfers.ReconcileJob
|
|
37
|
+
schedule: every 5 minutes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MixinTransfers
|
|
4
|
+
# Executes the Safe pipeline for one MixinTransfer. Retries are safe: the
|
|
5
|
+
# transfer's trace id deduplicates network-side.
|
|
6
|
+
class PerformJob < ApplicationJob
|
|
7
|
+
queue_as :mixin_transfers
|
|
8
|
+
|
|
9
|
+
def perform(transfer_id)
|
|
10
|
+
MixinTransfer.find(transfer_id).perform!
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MixinTransfers
|
|
4
|
+
# Resolves transfers whose send attempt ended indeterminately (timeout
|
|
5
|
+
# after submission): confirms from the trace snapshot or re-sends with the
|
|
6
|
+
# same trace id. Wired by the recurring schedule in config/recurring.yml.
|
|
7
|
+
class ReconcileJob < ApplicationJob
|
|
8
|
+
queue_as :mixin_transfers
|
|
9
|
+
|
|
10
|
+
def perform
|
|
11
|
+
MixinTransfer.reconcile_pending!
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Outbound transfer ledger. Creating a record enqueues the perform job; the
|
|
4
|
+
# runtime concern (MixinBot::Transfers::Model) drives the Safe pipeline and
|
|
5
|
+
# records the outcome in #state. bot_app_id selects a registered bot
|
|
6
|
+
# (MixinBot.register_bot); nil uses the default bot.
|
|
7
|
+
class MixinTransfer < ApplicationRecord
|
|
8
|
+
include MixinBot::Transfers::Model
|
|
9
|
+
|
|
10
|
+
after_create_commit -> { MixinTransfers::PerformJob.perform_later(id) }
|
|
11
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class CreateMixinTransfers < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :mixin_transfers do |t|
|
|
4
|
+
t.string :recipient_id, null: false
|
|
5
|
+
t.string :asset_id, null: false
|
|
6
|
+
t.decimal :amount, precision: 32, scale: 18, null: false
|
|
7
|
+
t.text :memo
|
|
8
|
+
t.string :trace_id, null: false
|
|
9
|
+
t.string :state, null: false, default: 'pending'
|
|
10
|
+
t.string :previous_state
|
|
11
|
+
t.string :transaction_hash
|
|
12
|
+
t.text :error
|
|
13
|
+
t.datetime :transitioned_at
|
|
14
|
+
t.string :bot_app_id
|
|
15
|
+
|
|
16
|
+
t.timestamps
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
add_index :mixin_transfers, :trace_id, unique: true
|
|
20
|
+
add_index :mixin_transfers, :state
|
|
21
|
+
add_index :mixin_transfers, :bot_app_id
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
|
|
5
|
+
module MixinBot
|
|
6
|
+
module Generators
|
|
7
|
+
# `rails generate mixin_bot:transfers` — scaffolds the outbound transfer
|
|
8
|
+
# ledger: migration + model (state machine concern), perform/reconcile
|
|
9
|
+
# jobs, and a Solid Queue recurring entry for reconciliation.
|
|
10
|
+
class TransfersGenerator < Rails::Generators::Base
|
|
11
|
+
include Rails::Generators::Migration
|
|
12
|
+
|
|
13
|
+
source_root File.expand_path('templates', __dir__)
|
|
14
|
+
|
|
15
|
+
RECURRING_ENTRY = <<~YAML
|
|
16
|
+
production:
|
|
17
|
+
mixin_transfers_reconcile:
|
|
18
|
+
class: MixinTransfers::ReconcileJob
|
|
19
|
+
schedule: every 5 minutes
|
|
20
|
+
YAML
|
|
21
|
+
|
|
22
|
+
def self.next_migration_number(dirname)
|
|
23
|
+
current = current_migration_number(dirname)
|
|
24
|
+
return Time.now.utc.strftime('%Y%m%d%H%M%S%6N') if current.zero?
|
|
25
|
+
|
|
26
|
+
current.succ.to_s
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def create_ledger_migration
|
|
30
|
+
migration 'create_mixin_transfers'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def create_model
|
|
34
|
+
template 'app/models/mixin_transfer.rb'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def create_jobs
|
|
38
|
+
template 'app/jobs/mixin_transfers/perform_job.rb'
|
|
39
|
+
template 'app/jobs/mixin_transfers/reconcile_job.rb'
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def create_recurring_schedule
|
|
43
|
+
path = File.expand_path('config/recurring.yml', destination_root)
|
|
44
|
+
if File.exist?(path)
|
|
45
|
+
say_status :todo, <<~MSG.chomp
|
|
46
|
+
add the MixinTransfers reconcile entry to config/recurring.yml (see USAGE)
|
|
47
|
+
MSG
|
|
48
|
+
else
|
|
49
|
+
create_file 'config/recurring.yml', RECURRING_ENTRY
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Skips the migration when a same-named one already exists (re-run).
|
|
56
|
+
def migration(name)
|
|
57
|
+
return if self.class.migration_exists?('db/migrate', name)
|
|
58
|
+
|
|
59
|
+
migration_template "db/migrate/#{name}.rb.tt", "db/migrate/#{name}.rb"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def migration_version
|
|
63
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MixinBot
|
|
4
|
+
module Generators
|
|
5
|
+
# Generator namespace discovered by railties: every bundler-managed gem's
|
|
6
|
+
# `lib/generators` directory is on the generator load path, so apps can run
|
|
7
|
+
# `rails generate mixin_bot:<name>` without any configuration. Individual
|
|
8
|
+
# generators live in sibling directories (e.g. mixin_bot/authentication).
|
|
9
|
+
end
|
|
10
|
+
end
|
data/lib/mixin_bot/api/app.rb
CHANGED
|
@@ -85,6 +85,26 @@ module MixinBot
|
|
|
85
85
|
client.post path, **payload, access_token: kwargs[:access_token]
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
+
##
|
|
89
|
+
# Updates the app security profile — the allowed IP addresses and the
|
|
90
|
+
# resource URL patterns.
|
|
91
|
+
#
|
|
92
|
+
# +POST /apps/:id+ does not persist these two fields; the security
|
|
93
|
+
# endpoint is the only one that stores them. Both lists are submitted on
|
|
94
|
+
# every call, so fetch the live values first when only one list should
|
|
95
|
+
# change.
|
|
96
|
+
#
|
|
97
|
+
# @param allowed_ips [Array<String>] IPv4 / CIDR entries; +[]+ removes
|
|
98
|
+
# the restriction
|
|
99
|
+
# @param resource_patterns [Array<String>] URL patterns; +[]+ removes the
|
|
100
|
+
# restriction
|
|
101
|
+
# @return [MixinBot::Models::ApiEnvelope]
|
|
102
|
+
#
|
|
103
|
+
def update_app_security(app_id, allowed_ips: [], resource_patterns: [], access_token: nil)
|
|
104
|
+
path = format('/apps/%<id>s/security', id: app_id)
|
|
105
|
+
client.post path, allowed_ips:, resource_patterns:, access_token:
|
|
106
|
+
end
|
|
107
|
+
|
|
88
108
|
def rotate_app_secret(app_id, access_token: nil)
|
|
89
109
|
path = format('/apps/%<id>s/secret', id: app_id)
|
|
90
110
|
client.post path, access_token:
|
|
@@ -33,7 +33,11 @@ module MixinBot
|
|
|
33
33
|
# +handler:+ is yield-through so the caller can supply an
|
|
34
34
|
# +Async::WebSocket::Connection+ subclass (e.g. one that correlates
|
|
35
35
|
# PONG frames). +endpoint_options:+ are forwarded to
|
|
36
|
-
# +Async::HTTP::Endpoint.parse
|
|
36
|
+
# +Async::HTTP::Endpoint.parse+. Prefer not passing +timeout:+ there: it
|
|
37
|
+
# becomes the socket's per-read timeout, so a quiet-but-healthy
|
|
38
|
+
# connection dies when no frame arrives within that window. Bound the
|
|
39
|
+
# connect phase at the call site instead (see
|
|
40
|
+
# MixinBot::Blaze::Reactor#connect!).
|
|
37
41
|
def blaze_async(handler: Async::WebSocket::Connection, endpoint_options: {})
|
|
38
42
|
access_token = access_token('GET', '/', '')
|
|
39
43
|
authorization = format('Bearer %<access_token>s', access_token:)
|
|
@@ -23,6 +23,11 @@ module MixinBot
|
|
|
23
23
|
end
|
|
24
24
|
alias safe_snapshot_by_id safe_snapshot
|
|
25
25
|
|
|
26
|
+
def safe_snapshot_by_trace(trace_id, access_token: nil)
|
|
27
|
+
path = format('/safe/snapshots/trace/%<trace_id>s', trace_id:)
|
|
28
|
+
client.get path, access_token:
|
|
29
|
+
end
|
|
30
|
+
|
|
26
31
|
def create_safe_snapshot_notification(**kwargs)
|
|
27
32
|
path = '/safe/snapshots/notifications'
|
|
28
33
|
|