solrengine-programs 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 +130 -0
- data/lib/generators/solrengine/program/program_generator.rb +101 -0
- data/lib/generators/solrengine/program/templates/account.rb.erb +15 -0
- data/lib/generators/solrengine/program/templates/instruction.rb.erb +12 -0
- data/lib/generators/solrengine/program/templates/stimulus_controller.js.erb +171 -0
- data/lib/solrengine/programs/account.rb +157 -0
- data/lib/solrengine/programs/borsh_types.rb +160 -0
- data/lib/solrengine/programs/configuration.rb +44 -0
- data/lib/solrengine/programs/engine.rb +11 -0
- data/lib/solrengine/programs/error_mapper.rb +50 -0
- data/lib/solrengine/programs/idl_parser.rb +151 -0
- data/lib/solrengine/programs/instruction.rb +132 -0
- data/lib/solrengine/programs/pda.rb +99 -0
- data/lib/solrengine/programs/transaction_builder.rb +207 -0
- data/lib/solrengine/programs/version.rb +7 -0
- data/lib/solrengine/programs.rb +41 -0
- metadata +132 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
require "ed25519"
|
|
2
|
+
require "base58"
|
|
3
|
+
require "base64"
|
|
4
|
+
|
|
5
|
+
module Solrengine
|
|
6
|
+
module Programs
|
|
7
|
+
class TransactionBuilder
|
|
8
|
+
def initialize
|
|
9
|
+
@instructions = []
|
|
10
|
+
@signers = [] # Array of { secret_key:, public_key: } hashes
|
|
11
|
+
@fee_payer = nil
|
|
12
|
+
@recent_blockhash = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def add_instruction(instruction)
|
|
16
|
+
if instruction.is_a?(Instruction)
|
|
17
|
+
@instructions << instruction.to_instruction
|
|
18
|
+
elsif instruction.is_a?(Hash)
|
|
19
|
+
@instructions << instruction
|
|
20
|
+
else
|
|
21
|
+
raise Error, "Instruction must be a Solrengine::Programs::Instruction or Hash"
|
|
22
|
+
end
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def add_signer(keypair)
|
|
27
|
+
@signers << keypair
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def set_fee_payer(pubkey)
|
|
32
|
+
@fee_payer = pubkey
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def set_recent_blockhash(blockhash)
|
|
37
|
+
@recent_blockhash = blockhash
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Build the serialized transaction (signatures + message)
|
|
42
|
+
def build
|
|
43
|
+
resolve_blockhash! unless @recent_blockhash
|
|
44
|
+
resolve_fee_payer! unless @fee_payer
|
|
45
|
+
|
|
46
|
+
message = build_message
|
|
47
|
+
signatures = sign_message(message)
|
|
48
|
+
|
|
49
|
+
# Serialize: compact-u16 signature count + signatures + message
|
|
50
|
+
result = BorshTypes.encode_compact_u16(signatures.size)
|
|
51
|
+
signatures.each { |sig| result += sig }
|
|
52
|
+
result += message
|
|
53
|
+
result
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Build, sign, and send the transaction
|
|
57
|
+
def sign_and_send(commitment: "confirmed")
|
|
58
|
+
ensure_signers!
|
|
59
|
+
|
|
60
|
+
tx_bytes = build
|
|
61
|
+
tx_base64 = Base64.strict_encode64(tx_bytes)
|
|
62
|
+
|
|
63
|
+
result = Solrengine::Rpc.client.request("sendTransaction", [
|
|
64
|
+
tx_base64,
|
|
65
|
+
{
|
|
66
|
+
"encoding" => "base64",
|
|
67
|
+
"skipPreflight" => false,
|
|
68
|
+
"preflightCommitment" => commitment
|
|
69
|
+
}
|
|
70
|
+
])
|
|
71
|
+
|
|
72
|
+
if result["error"]
|
|
73
|
+
raise TransactionError, "Transaction failed: #{result["error"].inspect}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
result["result"] # Returns the transaction signature
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def resolve_blockhash!
|
|
82
|
+
bh = Solrengine::Rpc.client.get_latest_blockhash
|
|
83
|
+
raise TransactionError, "Failed to fetch blockhash" unless bh
|
|
84
|
+
@recent_blockhash = bh[:blockhash]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def resolve_fee_payer!
|
|
88
|
+
if @signers.any?
|
|
89
|
+
@fee_payer = Base58.binary_to_base58(@signers.first[:public_key], :bitcoin)
|
|
90
|
+
else
|
|
91
|
+
raise ConfigurationError, "Fee payer must be set or a signer must be added"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def ensure_signers!
|
|
96
|
+
if @signers.empty?
|
|
97
|
+
keypair = Solrengine::Programs.configuration.server_keypair
|
|
98
|
+
raise ConfigurationError, "Server keypair not configured. Set SOLANA_KEYPAIR environment variable." unless keypair
|
|
99
|
+
@signers << keypair
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def build_message
|
|
104
|
+
# Collect all unique account keys, ordered:
|
|
105
|
+
# 1. Fee payer (always first, signer + writable)
|
|
106
|
+
# 2. Other signers (writable first, then read-only)
|
|
107
|
+
# 3. Non-signers (writable first, then read-only)
|
|
108
|
+
account_metas = collect_account_metas
|
|
109
|
+
ordered_keys = order_account_keys(account_metas)
|
|
110
|
+
|
|
111
|
+
# Build key index lookup
|
|
112
|
+
key_index = ordered_keys.each_with_index.to_h { |k, i| [ k[:pubkey], i ] }
|
|
113
|
+
|
|
114
|
+
# Count categories
|
|
115
|
+
num_required_signatures = ordered_keys.count { |k| k[:is_signer] }
|
|
116
|
+
num_readonly_signed = ordered_keys.count { |k| k[:is_signer] && !k[:is_writable] }
|
|
117
|
+
num_readonly_unsigned = ordered_keys.count { |k| !k[:is_signer] && !k[:is_writable] }
|
|
118
|
+
|
|
119
|
+
# Message header (3 bytes)
|
|
120
|
+
message = [ num_required_signatures, num_readonly_signed, num_readonly_unsigned ].pack("CCC")
|
|
121
|
+
|
|
122
|
+
# Account keys (compact-u16 count + 32 bytes each)
|
|
123
|
+
message += BorshTypes.encode_compact_u16(ordered_keys.size)
|
|
124
|
+
ordered_keys.each do |key|
|
|
125
|
+
message += Base58.base58_to_binary(key[:pubkey], :bitcoin)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Recent blockhash (32 bytes)
|
|
129
|
+
message += Base58.base58_to_binary(@recent_blockhash, :bitcoin)
|
|
130
|
+
|
|
131
|
+
# Instructions (compact-u16 count + each instruction)
|
|
132
|
+
message += BorshTypes.encode_compact_u16(@instructions.size)
|
|
133
|
+
@instructions.each do |ix|
|
|
134
|
+
# Program ID index (1 byte)
|
|
135
|
+
prog_index = key_index[ix[:program_id]]
|
|
136
|
+
message += [ prog_index ].pack("C")
|
|
137
|
+
|
|
138
|
+
# Account indices (compact-u16 count + 1 byte each)
|
|
139
|
+
acct_indices = ix[:accounts].map { |a| key_index[a[:pubkey]] }
|
|
140
|
+
message += BorshTypes.encode_compact_u16(acct_indices.size)
|
|
141
|
+
message += acct_indices.pack("C*")
|
|
142
|
+
|
|
143
|
+
# Instruction data (compact-u16 length + data)
|
|
144
|
+
data = ix[:data] || ""
|
|
145
|
+
message += BorshTypes.encode_compact_u16(data.bytesize)
|
|
146
|
+
message += data
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
message
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def sign_message(message)
|
|
153
|
+
@signers.map do |signer|
|
|
154
|
+
signing_key = Ed25519::SigningKey.new(signer[:secret_key])
|
|
155
|
+
signing_key.sign(message)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def collect_account_metas
|
|
160
|
+
metas = {}
|
|
161
|
+
|
|
162
|
+
# Fee payer is always signer + writable
|
|
163
|
+
metas[@fee_payer] = { pubkey: @fee_payer, is_signer: true, is_writable: true }
|
|
164
|
+
|
|
165
|
+
@instructions.each do |ix|
|
|
166
|
+
# Program ID is a non-signer, read-only account
|
|
167
|
+
unless metas.key?(ix[:program_id])
|
|
168
|
+
metas[ix[:program_id]] = { pubkey: ix[:program_id], is_signer: false, is_writable: false }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
ix[:accounts].each do |acct|
|
|
172
|
+
if metas.key?(acct[:pubkey])
|
|
173
|
+
# Merge: upgrade to signer/writable if needed
|
|
174
|
+
metas[acct[:pubkey]][:is_signer] ||= acct[:is_signer]
|
|
175
|
+
metas[acct[:pubkey]][:is_writable] ||= acct[:is_writable]
|
|
176
|
+
else
|
|
177
|
+
metas[acct[:pubkey]] = acct.dup
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
metas.values
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def order_account_keys(metas)
|
|
186
|
+
# Sort order:
|
|
187
|
+
# 1. Signer + writable
|
|
188
|
+
# 2. Signer + read-only
|
|
189
|
+
# 3. Non-signer + writable
|
|
190
|
+
# 4. Non-signer + read-only
|
|
191
|
+
# Fee payer is always first
|
|
192
|
+
fee_payer_meta = metas.find { |m| m[:pubkey] == @fee_payer }
|
|
193
|
+
others = metas.reject { |m| m[:pubkey] == @fee_payer }
|
|
194
|
+
|
|
195
|
+
sorted = others.sort_by do |m|
|
|
196
|
+
[
|
|
197
|
+
m[:is_signer] ? 0 : 1,
|
|
198
|
+
m[:is_writable] ? 0 : 1,
|
|
199
|
+
m[:pubkey] # deterministic ordering within category
|
|
200
|
+
]
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
[ fee_payer_meta ] + sorted
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
require "solrengine/rpc"
|
|
2
|
+
require "borsh"
|
|
3
|
+
|
|
4
|
+
module Solrengine
|
|
5
|
+
module Programs
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
class ConfigurationError < Error; end
|
|
8
|
+
class DeserializationError < Error; end
|
|
9
|
+
class TransactionError < Error; end
|
|
10
|
+
class ProgramError < Error
|
|
11
|
+
attr_reader :code, :error_name
|
|
12
|
+
|
|
13
|
+
def initialize(code:, error_name:, message:)
|
|
14
|
+
@code = code
|
|
15
|
+
@error_name = error_name
|
|
16
|
+
super("#{error_name} (#{code}): #{message}")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
def configuration
|
|
22
|
+
@configuration ||= Configuration.new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def configure
|
|
26
|
+
yield(configuration)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
require_relative "programs/version"
|
|
33
|
+
require_relative "programs/configuration"
|
|
34
|
+
require_relative "programs/borsh_types"
|
|
35
|
+
require_relative "programs/idl_parser"
|
|
36
|
+
require_relative "programs/pda"
|
|
37
|
+
require_relative "programs/error_mapper"
|
|
38
|
+
require_relative "programs/account"
|
|
39
|
+
require_relative "programs/instruction"
|
|
40
|
+
require_relative "programs/transaction_builder"
|
|
41
|
+
require_relative "programs/engine" if defined?(Rails::Engine)
|
metadata
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: solrengine-programs
|
|
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: rails
|
|
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: borsh
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0.2'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0.2'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: ed25519
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '1.3'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '1.3'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: base58
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0.2'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0.2'
|
|
83
|
+
description: Parse Anchor IDL files to scaffold Ruby account models, instruction builders,
|
|
84
|
+
and Stimulus controllers for interacting with custom Solana programs.
|
|
85
|
+
email:
|
|
86
|
+
- estoy@moviendo.me
|
|
87
|
+
executables: []
|
|
88
|
+
extensions: []
|
|
89
|
+
extra_rdoc_files: []
|
|
90
|
+
files:
|
|
91
|
+
- README.md
|
|
92
|
+
- lib/generators/solrengine/program/program_generator.rb
|
|
93
|
+
- lib/generators/solrengine/program/templates/account.rb.erb
|
|
94
|
+
- lib/generators/solrengine/program/templates/instruction.rb.erb
|
|
95
|
+
- lib/generators/solrengine/program/templates/stimulus_controller.js.erb
|
|
96
|
+
- lib/solrengine/programs.rb
|
|
97
|
+
- lib/solrengine/programs/account.rb
|
|
98
|
+
- lib/solrengine/programs/borsh_types.rb
|
|
99
|
+
- lib/solrengine/programs/configuration.rb
|
|
100
|
+
- lib/solrengine/programs/engine.rb
|
|
101
|
+
- lib/solrengine/programs/error_mapper.rb
|
|
102
|
+
- lib/solrengine/programs/idl_parser.rb
|
|
103
|
+
- lib/solrengine/programs/instruction.rb
|
|
104
|
+
- lib/solrengine/programs/pda.rb
|
|
105
|
+
- lib/solrengine/programs/transaction_builder.rb
|
|
106
|
+
- lib/solrengine/programs/version.rb
|
|
107
|
+
homepage: https://github.com/solrengine/programs
|
|
108
|
+
licenses:
|
|
109
|
+
- MIT
|
|
110
|
+
metadata:
|
|
111
|
+
homepage_uri: https://github.com/solrengine/programs
|
|
112
|
+
source_code_uri: https://github.com/solrengine/programs
|
|
113
|
+
post_install_message:
|
|
114
|
+
rdoc_options: []
|
|
115
|
+
require_paths:
|
|
116
|
+
- lib
|
|
117
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
118
|
+
requirements:
|
|
119
|
+
- - ">="
|
|
120
|
+
- !ruby/object:Gem::Version
|
|
121
|
+
version: 3.2.0
|
|
122
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
123
|
+
requirements:
|
|
124
|
+
- - ">="
|
|
125
|
+
- !ruby/object:Gem::Version
|
|
126
|
+
version: '0'
|
|
127
|
+
requirements: []
|
|
128
|
+
rubygems_version: 3.5.22
|
|
129
|
+
signing_key:
|
|
130
|
+
specification_version: 4
|
|
131
|
+
summary: Solana program interaction for Rails via Anchor IDL parsing
|
|
132
|
+
test_files: []
|