solrengine-realtime 0.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 +7 -0
- data/README.md +43 -0
- data/app/assets/javascripts/solrengine/realtime/auto_refresh_controller.js +46 -0
- data/lib/solrengine/realtime/account_monitor.rb +148 -0
- data/lib/solrengine/realtime/engine.rb +11 -0
- data/lib/solrengine/realtime/version.rb +7 -0
- data/lib/solrengine/realtime.rb +15 -0
- metadata +95 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e24339e63d6e1bea90aa6dd8bac526f5ef32659451ea97866b4b2d90f927b171
|
|
4
|
+
data.tar.gz: c64312b31bd91b27311431729677109b22a9dee0de8e38a3b8784e3cbff2352d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4bb5d4ecb5d4bdf28d9d90c7308506bbd17cda2ab4bed4f0f7534b9fe72f68aeaff195c87b329bb7c6af3d0b46ae7bb560c58d5405f1429da0db68699461b25b
|
|
7
|
+
data.tar.gz: 2b610121748e95b28ce407e363bdb7221708f8f8b1ba87ac4558f0a4133e2f2e283cc428f5d30585c4450397d5be6b262409d703056385e3dcd212fefb09fd93
|
data/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# SolRengine Realtime
|
|
2
|
+
|
|
3
|
+
Real-time Solana account monitoring via WebSocket + Turbo Streams. Subscribes to account changes and triggers callbacks when balances update.
|
|
4
|
+
|
|
5
|
+
Part of the [SolRengine](https://github.com/solrengine) framework.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "solrengine-realtime"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
Configure what happens when an account changes:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
# config/initializers/solrengine_realtime.rb
|
|
19
|
+
Solrengine::Realtime.on_account_change = ->(wallet_address) {
|
|
20
|
+
Rails.cache.delete("wallet/#{wallet_address}/tokens")
|
|
21
|
+
|
|
22
|
+
portfolio = Solrengine::Tokens::Portfolio.new(wallet_address)
|
|
23
|
+
|
|
24
|
+
Turbo::StreamsChannel.broadcast_replace_to(
|
|
25
|
+
"wallet_#{wallet_address}",
|
|
26
|
+
target: "token_list",
|
|
27
|
+
partial: "dashboard/token_list",
|
|
28
|
+
locals: { tokens: portfolio.tokens }
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Run the monitor as a separate process:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
# bin/solana_monitor
|
|
37
|
+
monitor = Solrengine::Realtime::AccountMonitor.new("Abc...xyz")
|
|
38
|
+
monitor.start
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
MIT
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { morphChildren } from "@hotwired/turbo"
|
|
3
|
+
|
|
4
|
+
// Periodically fetches fresh HTML and morphs only the changed DOM elements.
|
|
5
|
+
// No Turbo visit, no loading bar, no scroll reset — completely invisible.
|
|
6
|
+
export default class extends Controller {
|
|
7
|
+
static values = {
|
|
8
|
+
interval: { type: Number, default: 60 }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
connect() {
|
|
12
|
+
this.timer = setInterval(() => {
|
|
13
|
+
if (document.visibilityState === "visible") {
|
|
14
|
+
this.morph()
|
|
15
|
+
}
|
|
16
|
+
}, this.intervalValue * 1000)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
disconnect() {
|
|
20
|
+
clearInterval(this.timer)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async morph() {
|
|
24
|
+
if (this._morphing) return
|
|
25
|
+
this._morphing = true
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const response = await fetch(window.location.href, {
|
|
29
|
+
headers: { "Accept": "text/html" }
|
|
30
|
+
})
|
|
31
|
+
if (!response.ok) return
|
|
32
|
+
|
|
33
|
+
const html = await response.text()
|
|
34
|
+
const doc = new DOMParser().parseFromString(html, "text/html")
|
|
35
|
+
const newContent = doc.querySelector(`[data-controller~="auto-refresh"]`)
|
|
36
|
+
|
|
37
|
+
if (newContent) {
|
|
38
|
+
morphChildren(this.element, newContent)
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// Silently fail — next interval will retry
|
|
42
|
+
} finally {
|
|
43
|
+
this._morphing = false
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "logger"
|
|
3
|
+
require "websocket-client-simple"
|
|
4
|
+
|
|
5
|
+
module Solrengine
|
|
6
|
+
module Realtime
|
|
7
|
+
# Subscribes to Solana WebSocket RPC for real-time account changes.
|
|
8
|
+
# When an account changes, calls Solrengine::Realtime.on_account_change.
|
|
9
|
+
class AccountMonitor
|
|
10
|
+
SOLANA_ADDRESS_REGEX = /\A[1-9A-HJ-NP-Za-km-z]{32,44}\z/
|
|
11
|
+
RECONNECT_DELAY = 5
|
|
12
|
+
BROADCAST_DELAY = 3
|
|
13
|
+
|
|
14
|
+
attr_reader :wallet_address
|
|
15
|
+
|
|
16
|
+
def initialize(wallet_address)
|
|
17
|
+
unless wallet_address.is_a?(String) && wallet_address.match?(SOLANA_ADDRESS_REGEX)
|
|
18
|
+
raise ArgumentError, "Invalid Solana wallet address"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
@wallet_address = wallet_address.freeze
|
|
22
|
+
@ws_url = Solrengine::Rpc.configuration.ws_url
|
|
23
|
+
|
|
24
|
+
unless @ws_url&.start_with?("wss://")
|
|
25
|
+
logger.warn("[SolanaWS] WebSocket URL does not use wss:// (TLS) — connection is unencrypted")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
@running = false
|
|
29
|
+
@subscription_id = nil
|
|
30
|
+
@account_changed = false
|
|
31
|
+
@mutex = Mutex.new
|
|
32
|
+
@cv = ConditionVariable.new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def start
|
|
36
|
+
return if @running
|
|
37
|
+
@running = true
|
|
38
|
+
|
|
39
|
+
@ws_thread = Thread.new { websocket_loop }
|
|
40
|
+
@broadcast_thread = Thread.new { broadcast_loop }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def stop
|
|
44
|
+
@running = false
|
|
45
|
+
@cv.signal
|
|
46
|
+
@ws&.close rescue nil
|
|
47
|
+
@ws_thread&.join(5)
|
|
48
|
+
@broadcast_thread&.join(5)
|
|
49
|
+
@ws_thread = nil
|
|
50
|
+
@broadcast_thread = nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def running?
|
|
54
|
+
@running
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def flag_changed!
|
|
58
|
+
@mutex.synchronize do
|
|
59
|
+
@account_changed = true
|
|
60
|
+
@cv.signal
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def logger
|
|
65
|
+
@logger ||= defined?(Rails) ? Rails.logger : Logger.new($stdout, progname: "SolanaWS")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def websocket_loop
|
|
71
|
+
while @running
|
|
72
|
+
begin
|
|
73
|
+
connect_and_listen
|
|
74
|
+
rescue => e
|
|
75
|
+
logger.error("[SolanaWS] Connection error: #{e.message}")
|
|
76
|
+
sleep RECONNECT_DELAY if @running
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def connect_and_listen
|
|
82
|
+
@ws = WebSocket::Client::Simple.connect(@ws_url)
|
|
83
|
+
ws = @ws
|
|
84
|
+
monitor = self
|
|
85
|
+
|
|
86
|
+
ws.on :open do
|
|
87
|
+
monitor.logger.info("[SolanaWS] Connected for #{monitor.wallet_address} on #{Solrengine::Rpc.configuration.network}")
|
|
88
|
+
ws.send({
|
|
89
|
+
jsonrpc: "2.0",
|
|
90
|
+
id: 1,
|
|
91
|
+
method: "accountSubscribe",
|
|
92
|
+
params: [
|
|
93
|
+
monitor.wallet_address,
|
|
94
|
+
{ encoding: "jsonParsed", commitment: "confirmed" }
|
|
95
|
+
]
|
|
96
|
+
}.to_json)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
ws.on :message do |msg|
|
|
100
|
+
begin
|
|
101
|
+
parsed = JSON.parse(msg.data)
|
|
102
|
+
if parsed["id"] == 1 && parsed["result"]
|
|
103
|
+
monitor.instance_variable_set(:@subscription_id, parsed["result"])
|
|
104
|
+
monitor.logger.info("[SolanaWS] Subscribed with ID #{parsed['result']}")
|
|
105
|
+
elsif parsed["method"] == "accountNotification" &&
|
|
106
|
+
parsed.dig("params", "subscription") == monitor.instance_variable_get(:@subscription_id)
|
|
107
|
+
monitor.logger.info("[SolanaWS] Account changed for #{monitor.wallet_address}")
|
|
108
|
+
monitor.flag_changed!
|
|
109
|
+
end
|
|
110
|
+
rescue => e
|
|
111
|
+
monitor.logger.error("[SolanaWS] Message parse error: #{e.message}")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
ws.on :error do |e|
|
|
116
|
+
monitor.logger.error("[SolanaWS] Error: #{e.message}")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
ws.on :close do
|
|
120
|
+
monitor.logger.info("[SolanaWS] Disconnected")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
sleep 1 while @running && !ws.closed?
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def broadcast_loop
|
|
127
|
+
while @running
|
|
128
|
+
changed = @mutex.synchronize do
|
|
129
|
+
@cv.wait(@mutex, 1) until @account_changed || !@running
|
|
130
|
+
val = @account_changed
|
|
131
|
+
@account_changed = false
|
|
132
|
+
val
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
if changed && @running
|
|
136
|
+
sleep BROADCAST_DELAY
|
|
137
|
+
@mutex.synchronize { @account_changed = false }
|
|
138
|
+
begin
|
|
139
|
+
Solrengine::Realtime.on_account_change.call(@wallet_address)
|
|
140
|
+
rescue => e
|
|
141
|
+
logger.error("[SolanaWS] Callback error: #{e.message}")
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require "active_support/core_ext/module/attribute_accessors"
|
|
2
|
+
require "solrengine/rpc"
|
|
3
|
+
require_relative "realtime/version"
|
|
4
|
+
require_relative "realtime/account_monitor"
|
|
5
|
+
require_relative "realtime/engine" if defined?(Rails::Engine)
|
|
6
|
+
|
|
7
|
+
module Solrengine
|
|
8
|
+
module Realtime
|
|
9
|
+
mattr_accessor :on_account_change
|
|
10
|
+
|
|
11
|
+
self.on_account_change = ->(wallet_address) {
|
|
12
|
+
puts "[SolRengine] Account changed: #{wallet_address}"
|
|
13
|
+
}
|
|
14
|
+
end
|
|
15
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: solrengine-realtime
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Jose Ferrer
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-21 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activesupport
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.1'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '7.1'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: solrengine-rpc
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0.1'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0.1'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: websocket-client-simple
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0.9'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0.9'
|
|
55
|
+
description: Subscribes to Solana WebSocket RPC for account changes and broadcasts
|
|
56
|
+
updates via ActionCable/Turbo Streams. Includes Idiomorph auto-refresh Stimulus
|
|
57
|
+
controller.
|
|
58
|
+
email:
|
|
59
|
+
- estoy@moviendo.me
|
|
60
|
+
executables: []
|
|
61
|
+
extensions: []
|
|
62
|
+
extra_rdoc_files: []
|
|
63
|
+
files:
|
|
64
|
+
- README.md
|
|
65
|
+
- app/assets/javascripts/solrengine/realtime/auto_refresh_controller.js
|
|
66
|
+
- lib/solrengine/realtime.rb
|
|
67
|
+
- lib/solrengine/realtime/account_monitor.rb
|
|
68
|
+
- lib/solrengine/realtime/engine.rb
|
|
69
|
+
- lib/solrengine/realtime/version.rb
|
|
70
|
+
homepage: https://github.com/solrengine/realtime
|
|
71
|
+
licenses:
|
|
72
|
+
- MIT
|
|
73
|
+
metadata:
|
|
74
|
+
homepage_uri: https://github.com/solrengine/realtime
|
|
75
|
+
source_code_uri: https://github.com/solrengine/realtime
|
|
76
|
+
post_install_message:
|
|
77
|
+
rdoc_options: []
|
|
78
|
+
require_paths:
|
|
79
|
+
- lib
|
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - ">="
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: 3.2.0
|
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
requirements: []
|
|
91
|
+
rubygems_version: 3.5.22
|
|
92
|
+
signing_key:
|
|
93
|
+
specification_version: 4
|
|
94
|
+
summary: Real-time Solana account monitoring via WebSocket + Turbo Streams
|
|
95
|
+
test_files: []
|