solana_ruby_wallet_adapter 0.1.1
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/LICENSE +21 -0
- data/README.md +608 -0
- data/lib/solana_ruby_wallet_adapter.rb +39 -0
- data/lib/solana_wallet_adapter/adapter.rb +112 -0
- data/lib/solana_wallet_adapter/controller_helpers.rb +69 -0
- data/lib/solana_wallet_adapter/errors.rb +70 -0
- data/lib/solana_wallet_adapter/event_emitter.rb +82 -0
- data/lib/solana_wallet_adapter/message_signer_adapter.rb +123 -0
- data/lib/solana_wallet_adapter/network.rb +30 -0
- data/lib/solana_wallet_adapter/public_key.rb +72 -0
- data/lib/solana_wallet_adapter/railtie.rb +22 -0
- data/lib/solana_wallet_adapter/signature_verifier.rb +86 -0
- data/lib/solana_wallet_adapter/signer_adapter.rb +114 -0
- data/lib/solana_wallet_adapter/transaction.rb +66 -0
- data/lib/solana_wallet_adapter/version.rb +6 -0
- data/lib/solana_wallet_adapter/view_helpers.rb +22 -0
- data/lib/solana_wallet_adapter/wallet_ready_state.rb +19 -0
- data/lib/solana_wallet_adapter/wallet_registry.rb +65 -0
- data/lib/solana_wallet_adapter/wallets/coinbase.rb +77 -0
- data/lib/solana_wallet_adapter/wallets/ledger.rb +74 -0
- data/lib/solana_wallet_adapter/wallets/phantom.rb +118 -0
- data/lib/solana_wallet_adapter/wallets/solflare.rb +85 -0
- data/lib/solana_wallet_adapter/wallets/walletconnect.rb +88 -0
- data/sig/solana_wallet_adapter/adapter.rbi +47 -0
- data/sig/solana_wallet_adapter/errors.rbi +29 -0
- data/sig/solana_wallet_adapter/public_key.rbi +25 -0
- data/sorbet/config +6 -0
- metadata +183 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7a0e9caf19e2a18ea8c6c910d69b21a97391c7f52ad77997a069ef8dd8665c4c
|
|
4
|
+
data.tar.gz: cf3ec248114950685f094a702eb0a9b4a89eea60086f8d5547a78d71c4a24514
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 76a89389d97d4708aa3257bce24933e8d439651efc6aa7be58a02f39276e10baadcec6e1c5a69faecae3fea1428aae106bf48b6231fa4fa4dc5311cf2a77e520
|
|
7
|
+
data.tar.gz: 2e963b0f4e6c5aa70033d1563cccecfefca59bd6dbffc576d07bb16be1bf16cbfa0a8183fde81ee66be3bdce5d10223163d8b02b2f7bad2874ccc8346c394705
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Paul Zupan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
# solana-ruby-wallet-adapter
|
|
2
|
+
|
|
3
|
+
A Ruby on Rails gem port of [@solana/wallet-adapter](https://github.com/pzupan/wallet-adapter), with [Sorbet](https://sorbet.org) static types throughout.
|
|
4
|
+
|
|
5
|
+
Provides:
|
|
6
|
+
- Wallet metadata (name, icon, URL) for seeding a frontend wallet picker
|
|
7
|
+
- Server-side Ed25519 signature verification
|
|
8
|
+
- [Sign-In-With-Solana (SIWS)](https://login.xyz/solana) challenge/verify helpers
|
|
9
|
+
- A typed adapter hierarchy that mirrors the original TypeScript classes
|
|
10
|
+
|
|
11
|
+
> **How it differs from the TypeScript original.** Browser-only features — DOM wallet detection, `window.phantom`, popup flows — have no server-side equivalent. The Ruby adapters serve as **metadata providers** for the frontend and **verifiers** for signatures returned by the browser wallet.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Table of contents
|
|
16
|
+
|
|
17
|
+
1. [Installation](#installation)
|
|
18
|
+
2. [Setup](#setup)
|
|
19
|
+
3. [Usage](#usage)
|
|
20
|
+
- [Seed wallets to the frontend](#1-seed-wallets-to-the-frontend)
|
|
21
|
+
- [Simple message signing](#2-simple-message-signing-authentication)
|
|
22
|
+
- [Sign-In-With-Solana (SIWS)](#3-sign-in-with-solana-siws)
|
|
23
|
+
- [Send a pre-signed transaction](#4-send-a-pre-signed-transaction)
|
|
24
|
+
- [Working with public keys](#5-working-with-public-keys)
|
|
25
|
+
- [Networks](#6-networks)
|
|
26
|
+
4. [Writing a custom adapter](#writing-a-custom-adapter)
|
|
27
|
+
5. [Error reference](#error-reference)
|
|
28
|
+
6. [API reference](#api-reference)
|
|
29
|
+
7. [Development](#development)
|
|
30
|
+
8. [License](#license)
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
Add to your `Gemfile`:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
gem "solana_ruby_wallet_adapter"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Then run:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bundle install
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Requirements
|
|
49
|
+
|
|
50
|
+
| Requirement | Version |
|
|
51
|
+
|---|---|
|
|
52
|
+
| Ruby | >= 3.2 |
|
|
53
|
+
| Rails | >= 7.0 (optional – auto-detected) |
|
|
54
|
+
| `sorbet-runtime` | >= 0.5 |
|
|
55
|
+
| `ed25519` | >= 1.2 |
|
|
56
|
+
| `base58` | >= 0.2 |
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Setup
|
|
61
|
+
|
|
62
|
+
Generate the initializer:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# create config/initializers/solana_wallet_adapter.rb manually, or copy the example below
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
# config/initializers/solana_wallet_adapter.rb
|
|
70
|
+
SolanaWalletAdapter::WalletRegistry.register(
|
|
71
|
+
SolanaWalletAdapter::Wallets::PhantomWalletAdapter,
|
|
72
|
+
SolanaWalletAdapter::Wallets::SolflareWalletAdapter,
|
|
73
|
+
SolanaWalletAdapter::Wallets::LedgerWalletAdapter,
|
|
74
|
+
SolanaWalletAdapter::Wallets::CoinbaseWalletAdapter,
|
|
75
|
+
SolanaWalletAdapter::Wallets::WalletConnectWalletAdapter,
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
That's it. The gem auto-loads its Railtie when Rails is present, so
|
|
80
|
+
`ControllerHelpers` and `ViewHelpers` are mixed in automatically.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Usage
|
|
85
|
+
|
|
86
|
+
### 1. Seed wallets to the frontend
|
|
87
|
+
|
|
88
|
+
Render wallet metadata into your layout so the JavaScript wallet-picker knows
|
|
89
|
+
which wallets to offer and what icons to show:
|
|
90
|
+
|
|
91
|
+
```erb
|
|
92
|
+
<%# app/views/layouts/application.html.erb %>
|
|
93
|
+
<head>
|
|
94
|
+
<%# ... %>
|
|
95
|
+
<script>
|
|
96
|
+
window.__SOLANA_WALLETS__ = <%= solana_wallets_json %>;
|
|
97
|
+
</script>
|
|
98
|
+
</head>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`solana_wallets_json` returns a JSON array like:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
[
|
|
105
|
+
{
|
|
106
|
+
"name": "Phantom",
|
|
107
|
+
"url": "https://phantom.app",
|
|
108
|
+
"icon": "data:image/svg+xml;base64,...",
|
|
109
|
+
"readyState": "Unsupported",
|
|
110
|
+
"connected": false,
|
|
111
|
+
"publicKey": null
|
|
112
|
+
},
|
|
113
|
+
{ "name": "Solflare", ... },
|
|
114
|
+
...
|
|
115
|
+
]
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Or fetch it from a dedicated endpoint:
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
# config/routes.rb
|
|
122
|
+
get "/wallets", to: "wallets#index"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
# app/controllers/wallets_controller.rb
|
|
127
|
+
class WalletsController < ApplicationController
|
|
128
|
+
def index
|
|
129
|
+
render json: SolanaWalletAdapter::WalletRegistry.to_json_array
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
### 2. Simple message signing (authentication)
|
|
137
|
+
|
|
138
|
+
**Flow:**
|
|
139
|
+
1. Server generates a nonce and stores it in the session.
|
|
140
|
+
2. Browser wallet signs `"Sign in to MyApp\nNonce: <nonce>"`.
|
|
141
|
+
3. Browser POSTs `{ public_key, message, signature }` to the server.
|
|
142
|
+
4. Server verifies the signature, then creates the session.
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
# app/controllers/wallet_sessions_controller.rb
|
|
146
|
+
class WalletSessionsController < ApplicationController
|
|
147
|
+
# GET /wallet_sessions/new
|
|
148
|
+
# Returns a nonce for the client to include in the message it signs.
|
|
149
|
+
def new
|
|
150
|
+
session[:wallet_nonce] = SecureRandom.hex(16)
|
|
151
|
+
render json: { nonce: session[:wallet_nonce] }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# POST /wallet_sessions
|
|
155
|
+
# Body: { public_key: "...", message: "...", signature: "..." }
|
|
156
|
+
def create
|
|
157
|
+
expected_message = "Sign in to MyApp\nNonce: #{session[:wallet_nonce]}"
|
|
158
|
+
|
|
159
|
+
unless params[:message] == expected_message
|
|
160
|
+
return render json: { error: "Message mismatch" }, status: :unprocessable_entity
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
verify_wallet_signature!(
|
|
164
|
+
public_key_b58: params.require(:public_key),
|
|
165
|
+
message: params.require(:message),
|
|
166
|
+
signature_b64: params.require(:signature), # Base64-encoded 64-byte signature
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
session[:wallet_public_key] = params[:public_key]
|
|
170
|
+
session.delete(:wallet_nonce)
|
|
171
|
+
|
|
172
|
+
render json: { ok: true, public_key: params[:public_key] }
|
|
173
|
+
|
|
174
|
+
rescue SolanaWalletAdapter::WalletSignMessageError => e
|
|
175
|
+
render json: { error: "Invalid signature: #{e.message}" }, status: :unauthorized
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# DELETE /wallet_sessions
|
|
179
|
+
def destroy
|
|
180
|
+
session.delete(:wallet_public_key)
|
|
181
|
+
head :no_content
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Matching JavaScript (browser side, using any wallet adapter):
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
// Sign the expected message with the connected wallet
|
|
190
|
+
const message = new TextEncoder().encode(`Sign in to MyApp\nNonce: ${nonce}`);
|
|
191
|
+
const { signature } = await wallet.signMessage(message);
|
|
192
|
+
|
|
193
|
+
await fetch("/wallet_sessions", {
|
|
194
|
+
method: "POST",
|
|
195
|
+
headers: { "Content-Type": "application/json" },
|
|
196
|
+
body: JSON.stringify({
|
|
197
|
+
public_key: wallet.publicKey.toBase58(),
|
|
198
|
+
message: `Sign in to MyApp\nNonce: ${nonce}`,
|
|
199
|
+
signature: btoa(String.fromCharCode(...signature)), // Base64
|
|
200
|
+
}),
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
### 3. Sign-In-With-Solana (SIWS)
|
|
207
|
+
|
|
208
|
+
SIWS is the Solana equivalent of [EIP-4361](https://eips.ethereum.org/EIPS/eip-4361).
|
|
209
|
+
It produces a structured, human-readable message that the wallet displays before
|
|
210
|
+
signing.
|
|
211
|
+
|
|
212
|
+
#### 3a. Issue a challenge
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
# GET /siws/challenge
|
|
216
|
+
# Returns the SIWS message string and the input fields needed to verify later.
|
|
217
|
+
def challenge
|
|
218
|
+
input = SolanaWalletAdapter::SignInInput.new(
|
|
219
|
+
domain: request.host,
|
|
220
|
+
address: params.require(:address), # wallet public key (Base58)
|
|
221
|
+
statement: "Sign in to #{Rails.application.class.module_parent_name}",
|
|
222
|
+
uri: root_url,
|
|
223
|
+
version: "1",
|
|
224
|
+
nonce: SecureRandom.hex(16),
|
|
225
|
+
issued_at: Time.now.utc.iso8601,
|
|
226
|
+
expiration_time: 10.minutes.from_now.utc.iso8601,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Store the input in the session so the verify step can reconstruct it.
|
|
230
|
+
session[:siws_input] = {
|
|
231
|
+
domain: input.domain,
|
|
232
|
+
address: input.address,
|
|
233
|
+
statement: input.statement,
|
|
234
|
+
uri: input.uri,
|
|
235
|
+
version: input.version,
|
|
236
|
+
nonce: input.nonce,
|
|
237
|
+
issued_at: input.issued_at,
|
|
238
|
+
expiration_time: input.expiration_time,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
render json: { message: input.to_message, input: session[:siws_input] }
|
|
242
|
+
end
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`input.to_message` produces:
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
example.com wants you to sign in with your Solana account:
|
|
249
|
+
9noXzpXnLTia8oBHFMza9tqPuFvPpRCtAfJmZeoBc7x2
|
|
250
|
+
|
|
251
|
+
Sign in to MyApp
|
|
252
|
+
|
|
253
|
+
URI: https://example.com/
|
|
254
|
+
Version: 1
|
|
255
|
+
Nonce: a3f1b2c8d4e5f6a7
|
|
256
|
+
Issued At: 2024-06-01T12:00:00Z
|
|
257
|
+
Expiration Time: 2024-06-01T12:10:00Z
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
#### 3b. Verify the signed response
|
|
261
|
+
|
|
262
|
+
```ruby
|
|
263
|
+
# POST /siws/verify
|
|
264
|
+
# Body: { output: { address:, signed_message:, signature: } }
|
|
265
|
+
def verify
|
|
266
|
+
stored_input = session[:siws_input]
|
|
267
|
+
return render json: { error: "No active SIWS challenge" }, status: :bad_request if stored_input.nil?
|
|
268
|
+
|
|
269
|
+
verify_sign_in!(
|
|
270
|
+
input_params: stored_input,
|
|
271
|
+
output_params: params.require(:output).to_unsafe_h,
|
|
272
|
+
# output must contain:
|
|
273
|
+
# address: the signer's Base58 public key
|
|
274
|
+
# signed_message: the exact SIWS message that was signed (UTF-8 string)
|
|
275
|
+
# signature: Base64-encoded 64-byte Ed25519 signature
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
session[:wallet_public_key] = params.dig(:output, :address)
|
|
279
|
+
session.delete(:siws_input)
|
|
280
|
+
|
|
281
|
+
render json: { ok: true }
|
|
282
|
+
|
|
283
|
+
rescue SolanaWalletAdapter::WalletSignInError => e
|
|
284
|
+
render json: { error: e.message }, status: :unauthorized
|
|
285
|
+
end
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Matching JavaScript:
|
|
289
|
+
|
|
290
|
+
```js
|
|
291
|
+
// 1. Fetch the challenge
|
|
292
|
+
const { message, input } = await fetch(`/siws/challenge?address=${publicKey}`).then(r => r.json());
|
|
293
|
+
|
|
294
|
+
// 2. Sign with wallet
|
|
295
|
+
const encoded = new TextEncoder().encode(message);
|
|
296
|
+
const { signature } = await wallet.signMessage(encoded);
|
|
297
|
+
|
|
298
|
+
// 3. POST the output
|
|
299
|
+
await fetch("/siws/verify", {
|
|
300
|
+
method: "POST",
|
|
301
|
+
headers: { "Content-Type": "application/json" },
|
|
302
|
+
body: JSON.stringify({
|
|
303
|
+
output: {
|
|
304
|
+
address: publicKey.toBase58(),
|
|
305
|
+
signed_message: message,
|
|
306
|
+
signature: btoa(String.fromCharCode(...signature)),
|
|
307
|
+
},
|
|
308
|
+
}),
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
### 4. Send a pre-signed transaction
|
|
315
|
+
|
|
316
|
+
For use cases where the browser signs a transaction and the server forwards it
|
|
317
|
+
to the RPC (e.g., sponsored transactions, relayers):
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
# POST /transactions
|
|
321
|
+
# Body: { signed_tx: "<Base64-encoded serialised transaction>" }
|
|
322
|
+
def submit
|
|
323
|
+
raw_bytes = Base64.strict_decode64(params.require(:signed_tx))
|
|
324
|
+
tx = SolanaWalletAdapter::Transaction.new(raw_bytes)
|
|
325
|
+
|
|
326
|
+
rpc_url = SolanaWalletAdapter::Network::MainnetBeta.rpc_url
|
|
327
|
+
options = SolanaWalletAdapter::SendTransactionOptions.new(
|
|
328
|
+
skip_preflight: false,
|
|
329
|
+
preflight_commitment: "confirmed",
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# BaseSignerWalletAdapter#send_raw_transaction posts the bytes to the RPC.
|
|
333
|
+
# Since the tx is already signed, instantiate a pass-through adapter helper:
|
|
334
|
+
signature = SolanaWalletAdapter::RpcClient.new(rpc_url).send_raw_transaction(raw_bytes, options)
|
|
335
|
+
|
|
336
|
+
render json: { signature: signature }
|
|
337
|
+
rescue SolanaWalletAdapter::WalletSendTransactionError => e
|
|
338
|
+
render json: { error: e.message }, status: :unprocessable_entity
|
|
339
|
+
end
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Or call the RPC directly from any adapter instance using the protected helper:
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
adapter = SolanaWalletAdapter::Wallets::PhantomWalletAdapter.new
|
|
346
|
+
# (internal method, exposed here for illustration)
|
|
347
|
+
signature = adapter.send(:send_raw_transaction, raw_bytes, rpc_url, options)
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
### 5. Working with public keys
|
|
353
|
+
|
|
354
|
+
```ruby
|
|
355
|
+
# From a Base58 string (most common – what wallets return)
|
|
356
|
+
pk = SolanaWalletAdapter::PublicKey.new("9noXzpXnLTia8oBHFMza9tqPuFvPpRCtAfJmZeoBc7x2")
|
|
357
|
+
pk.to_base58 # => "9noXzpXnLTia8oBHFMza9tqPuFvPpRCtAfJmZeoBc7x2"
|
|
358
|
+
pk.to_bytes # => [138, 12, ...] Array of 32 integers
|
|
359
|
+
pk.bytes # => "\x8a\x0c..." raw 32-byte binary String
|
|
360
|
+
|
|
361
|
+
# From a raw binary string (e.g. decoded from a transaction)
|
|
362
|
+
pk = SolanaWalletAdapter::PublicKey.new("\x00" * 32)
|
|
363
|
+
|
|
364
|
+
# From a Uint8Array-style Integer array
|
|
365
|
+
pk = SolanaWalletAdapter::PublicKey.new([0] * 32)
|
|
366
|
+
|
|
367
|
+
# Equality
|
|
368
|
+
pk1 = SolanaWalletAdapter::PublicKey.new("11111111111111111111111111111111")
|
|
369
|
+
pk2 = SolanaWalletAdapter::PublicKey.new([0] * 32)
|
|
370
|
+
pk1 == pk2 # => true (System Program address)
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
### 6. Networks
|
|
376
|
+
|
|
377
|
+
```ruby
|
|
378
|
+
net = SolanaWalletAdapter::Network::MainnetBeta
|
|
379
|
+
net.serialize # => "mainnet-beta"
|
|
380
|
+
net.rpc_url # => "https://api.mainnet-beta.solana.com"
|
|
381
|
+
|
|
382
|
+
SolanaWalletAdapter::Network::Devnet.rpc_url
|
|
383
|
+
# => "https://api.devnet.solana.com"
|
|
384
|
+
|
|
385
|
+
SolanaWalletAdapter::Network::Testnet.rpc_url
|
|
386
|
+
# => "https://api.testnet.solana.com"
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## Writing a custom adapter
|
|
392
|
+
|
|
393
|
+
Subclass `BaseMessageSignerWalletAdapter` (for sign + message), or
|
|
394
|
+
`BaseSignerWalletAdapter` (sign only), or `BaseWalletAdapter` (metadata only):
|
|
395
|
+
|
|
396
|
+
```ruby
|
|
397
|
+
# typed: strict
|
|
398
|
+
# app/solana/my_custom_wallet_adapter.rb
|
|
399
|
+
|
|
400
|
+
class MyCustomWalletAdapter < SolanaWalletAdapter::BaseMessageSignerWalletAdapter
|
|
401
|
+
extend T::Sig
|
|
402
|
+
|
|
403
|
+
sig { override.returns(String) }
|
|
404
|
+
def name = "MyWallet"
|
|
405
|
+
|
|
406
|
+
sig { override.returns(String) }
|
|
407
|
+
def url = "https://mywallet.example.com"
|
|
408
|
+
|
|
409
|
+
sig { override.returns(String) }
|
|
410
|
+
def icon = "data:image/svg+xml;base64,PHN2Zy..."
|
|
411
|
+
|
|
412
|
+
sig { override.returns(SolanaWalletAdapter::WalletReadyState) }
|
|
413
|
+
def ready_state = SolanaWalletAdapter::WalletReadyState::Unsupported
|
|
414
|
+
|
|
415
|
+
sig { override.returns(T.nilable(SolanaWalletAdapter::PublicKey)) }
|
|
416
|
+
def public_key = nil
|
|
417
|
+
|
|
418
|
+
sig { override.returns(T::Boolean) }
|
|
419
|
+
def connecting? = false
|
|
420
|
+
|
|
421
|
+
sig { override.returns(SolanaWalletAdapter::SupportedTransactionVersions) }
|
|
422
|
+
def supported_transaction_versions
|
|
423
|
+
Set[
|
|
424
|
+
SolanaWalletAdapter::TransactionVersion::Legacy,
|
|
425
|
+
SolanaWalletAdapter::TransactionVersion::Version0,
|
|
426
|
+
].freeze
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
sig { override.void }
|
|
430
|
+
def connect
|
|
431
|
+
raise SolanaWalletAdapter::WalletNotReadyError,
|
|
432
|
+
"#{name} connection is initiated in the browser"
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
sig { override.void }
|
|
436
|
+
def disconnect; end
|
|
437
|
+
|
|
438
|
+
sig do
|
|
439
|
+
override
|
|
440
|
+
.params(
|
|
441
|
+
transaction: SolanaWalletAdapter::Transaction,
|
|
442
|
+
rpc_url: String,
|
|
443
|
+
options: SolanaWalletAdapter::SendTransactionOptions,
|
|
444
|
+
)
|
|
445
|
+
.returns(String)
|
|
446
|
+
end
|
|
447
|
+
def send_transaction(transaction, rpc_url, options = SolanaWalletAdapter::SendTransactionOptions.new)
|
|
448
|
+
raise SolanaWalletAdapter::WalletNotConnectedError
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
sig { override.params(transaction: SolanaWalletAdapter::Transaction).returns(SolanaWalletAdapter::Transaction) }
|
|
452
|
+
def sign_transaction(transaction)
|
|
453
|
+
raise SolanaWalletAdapter::WalletNotConnectedError
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
sig { override.params(transactions: T::Array[SolanaWalletAdapter::Transaction]).returns(T::Array[SolanaWalletAdapter::Transaction]) }
|
|
457
|
+
def sign_all_transactions(transactions)
|
|
458
|
+
raise SolanaWalletAdapter::WalletNotConnectedError
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
sig { override.params(message: String).returns(String) }
|
|
462
|
+
def sign_message(message)
|
|
463
|
+
raise SolanaWalletAdapter::WalletNotConnectedError
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
Register it:
|
|
469
|
+
|
|
470
|
+
```ruby
|
|
471
|
+
# config/initializers/solana_wallet_adapter.rb
|
|
472
|
+
SolanaWalletAdapter::WalletRegistry.register(MyCustomWalletAdapter)
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## Error reference
|
|
478
|
+
|
|
479
|
+
All errors inherit from `SolanaWalletAdapter::WalletError < StandardError`.
|
|
480
|
+
|
|
481
|
+
| Class | When raised |
|
|
482
|
+
|---|---|
|
|
483
|
+
| `WalletNotReadyError` | Wallet not installed / not in a connectable state |
|
|
484
|
+
| `WalletLoadError` | Wallet extension failed to load |
|
|
485
|
+
| `WalletConfigError` | Adapter misconfiguration |
|
|
486
|
+
| `WalletConnectionError` | Connection attempt failed |
|
|
487
|
+
| `WalletDisconnectedError` | Wallet disconnected unexpectedly |
|
|
488
|
+
| `WalletDisconnectionError` | Explicit disconnect request failed |
|
|
489
|
+
| `WalletAccountError` | Wallet returned an unexpected account |
|
|
490
|
+
| `WalletPublicKeyError` | Wallet returned an invalid public key |
|
|
491
|
+
| `WalletKeypairError` | Keypair-level error |
|
|
492
|
+
| `WalletNotConnectedError` | Operation requires a connected wallet |
|
|
493
|
+
| `WalletSendTransactionError` | Sending a transaction failed |
|
|
494
|
+
| `WalletSignTransactionError` | Signing a transaction failed |
|
|
495
|
+
| `WalletSignMessageError` | Signing a message failed / signature invalid |
|
|
496
|
+
| `WalletSignInError` | SIWS flow failed (mismatch or bad signature) |
|
|
497
|
+
| `WalletTimeoutError` | Operation timed out |
|
|
498
|
+
| `WalletWindowBlockedError` | Popup window was blocked |
|
|
499
|
+
| `WalletWindowClosedError` | User closed the wallet popup |
|
|
500
|
+
|
|
501
|
+
All errors accept an optional second argument `cause_error`:
|
|
502
|
+
|
|
503
|
+
```ruby
|
|
504
|
+
raise SolanaWalletAdapter::WalletConnectionError.new("Could not connect", original_exception)
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## API reference
|
|
510
|
+
|
|
511
|
+
### `WalletRegistry`
|
|
512
|
+
|
|
513
|
+
```ruby
|
|
514
|
+
WalletRegistry.register(*adapter_classes) # register one or more adapter classes
|
|
515
|
+
WalletRegistry.all # => Array of adapter classes
|
|
516
|
+
WalletRegistry.find("Phantom") # => PhantomWalletAdapter class or nil
|
|
517
|
+
WalletRegistry.to_json_array # => Array<Hash> ready for JSON serialisation
|
|
518
|
+
WalletRegistry.reset! # clear all (useful in tests)
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### `SignatureVerifier`
|
|
522
|
+
|
|
523
|
+
```ruby
|
|
524
|
+
verifier = SolanaWalletAdapter::SignatureVerifier.new
|
|
525
|
+
|
|
526
|
+
# Returns true/false
|
|
527
|
+
verifier.verify(public_key: pk, message: msg, signature: sig)
|
|
528
|
+
|
|
529
|
+
# Raises WalletSignMessageError on failure
|
|
530
|
+
verifier.verify!(public_key: pk, message: msg, signature: sig)
|
|
531
|
+
|
|
532
|
+
# Raises WalletSignInError on failure
|
|
533
|
+
verifier.verify_sign_in!(input: sign_in_input, output: sign_in_output)
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### `SignInInput`
|
|
537
|
+
|
|
538
|
+
```ruby
|
|
539
|
+
input = SolanaWalletAdapter::SignInInput.new(
|
|
540
|
+
domain: "example.com", # required
|
|
541
|
+
address: "9noX...", # required – Base58 public key
|
|
542
|
+
statement: "Sign in to MyApp", # optional
|
|
543
|
+
uri: "https://example.com/", # optional
|
|
544
|
+
version: "1", # default "1"
|
|
545
|
+
nonce: "abc123", # optional but recommended
|
|
546
|
+
issued_at: Time.now.utc.iso8601, # optional
|
|
547
|
+
expiration_time: 10.minutes.from_now.utc.iso8601, # optional
|
|
548
|
+
not_before: nil, # optional
|
|
549
|
+
request_id: nil, # optional
|
|
550
|
+
resources: [], # optional Array<String>
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
input.to_message # => canonical SIWS message string to sign
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
### `PublicKey`
|
|
557
|
+
|
|
558
|
+
```ruby
|
|
559
|
+
SolanaWalletAdapter::PublicKey.new(base58_string)
|
|
560
|
+
SolanaWalletAdapter::PublicKey.new(binary_string) # 32-byte binary
|
|
561
|
+
SolanaWalletAdapter::PublicKey.new(integer_array) # 32-element Array<Integer>
|
|
562
|
+
|
|
563
|
+
pk.to_base58 # => String
|
|
564
|
+
pk.to_bytes # => Array<Integer>
|
|
565
|
+
pk.bytes # => String (binary)
|
|
566
|
+
pk == other_pk # => Boolean
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### `Transaction`
|
|
570
|
+
|
|
571
|
+
```ruby
|
|
572
|
+
SolanaWalletAdapter::Transaction.new(wire_bytes, version: nil)
|
|
573
|
+
|
|
574
|
+
tx.versioned? # => true for Version0, false for Legacy/nil
|
|
575
|
+
tx.serialize # => wire_bytes
|
|
576
|
+
tx.version # => TransactionVersion or nil
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
### `SendTransactionOptions`
|
|
580
|
+
|
|
581
|
+
```ruby
|
|
582
|
+
SolanaWalletAdapter::SendTransactionOptions.new(
|
|
583
|
+
signers: [], # Array<String> additional signer keypairs
|
|
584
|
+
preflight_commitment: "confirmed", # String or nil
|
|
585
|
+
skip_preflight: false,
|
|
586
|
+
max_retries: nil,
|
|
587
|
+
min_context_slot: nil,
|
|
588
|
+
)
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
## Development
|
|
594
|
+
|
|
595
|
+
```bash
|
|
596
|
+
bundle install
|
|
597
|
+
bundle exec rspec # run tests
|
|
598
|
+
bundle exec srb tc # Sorbet type-check
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
To add more wallet adapters, copy `lib/solana_wallet_adapter/wallets/phantom.rb`
|
|
602
|
+
as a template and register the new class in the initializer.
|
|
603
|
+
|
|
604
|
+
---
|
|
605
|
+
|
|
606
|
+
## License
|
|
607
|
+
|
|
608
|
+
Apache-2.0 — same as the original TypeScript library.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
require "set"
|
|
6
|
+
|
|
7
|
+
# Core infrastructure
|
|
8
|
+
require_relative "solana_wallet_adapter/version"
|
|
9
|
+
require_relative "solana_wallet_adapter/errors"
|
|
10
|
+
require_relative "solana_wallet_adapter/network"
|
|
11
|
+
require_relative "solana_wallet_adapter/public_key"
|
|
12
|
+
require_relative "solana_wallet_adapter/transaction"
|
|
13
|
+
require_relative "solana_wallet_adapter/wallet_ready_state"
|
|
14
|
+
require_relative "solana_wallet_adapter/event_emitter"
|
|
15
|
+
|
|
16
|
+
# Adapter hierarchy
|
|
17
|
+
require_relative "solana_wallet_adapter/adapter"
|
|
18
|
+
require_relative "solana_wallet_adapter/signer_adapter"
|
|
19
|
+
require_relative "solana_wallet_adapter/message_signer_adapter"
|
|
20
|
+
|
|
21
|
+
# Server-side signature verification
|
|
22
|
+
require_relative "solana_wallet_adapter/signature_verifier"
|
|
23
|
+
|
|
24
|
+
# Wallet registry
|
|
25
|
+
require_relative "solana_wallet_adapter/wallet_registry"
|
|
26
|
+
|
|
27
|
+
# Wallet adapter implementations
|
|
28
|
+
require_relative "solana_wallet_adapter/wallets/phantom"
|
|
29
|
+
require_relative "solana_wallet_adapter/wallets/solflare"
|
|
30
|
+
require_relative "solana_wallet_adapter/wallets/ledger"
|
|
31
|
+
require_relative "solana_wallet_adapter/wallets/coinbase"
|
|
32
|
+
require_relative "solana_wallet_adapter/wallets/walletconnect"
|
|
33
|
+
|
|
34
|
+
# Rails integration (only when Rails is available)
|
|
35
|
+
if defined?(Rails)
|
|
36
|
+
require_relative "solana_wallet_adapter/controller_helpers"
|
|
37
|
+
require_relative "solana_wallet_adapter/view_helpers"
|
|
38
|
+
require_relative "solana_wallet_adapter/railtie"
|
|
39
|
+
end
|