solana_ruby_wallet_adapter 0.1.1 → 0.1.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a0e9caf19e2a18ea8c6c910d69b21a97391c7f52ad77997a069ef8dd8665c4c
4
- data.tar.gz: cf3ec248114950685f094a702eb0a9b4a89eea60086f8d5547a78d71c4a24514
3
+ metadata.gz: 51811ed20a2a99f1c970eb5fefbaef2d8186355df44758ea919964df3ac561af
4
+ data.tar.gz: 8c8b14961ddcf9838ebfdaea5a73f42cff8a683ee76bd4603a69eeaa630f4f13
5
5
  SHA512:
6
- metadata.gz: 76a89389d97d4708aa3257bce24933e8d439651efc6aa7be58a02f39276e10baadcec6e1c5a69faecae3fea1428aae106bf48b6231fa4fa4dc5311cf2a77e520
7
- data.tar.gz: 2e963b0f4e6c5aa70033d1563cccecfefca59bd6dbffc576d07bb16be1bf16cbfa0a8183fde81ee66be3bdce5d10223163d8b02b2f7bad2874ccc8346c394705
6
+ metadata.gz: 3de9b50d9b861222d103dcaf7f06616a26f8447bed66bf4d433afbce15e2326732f52a29a91c4bbc740e832f56a9d0546c9f44983aa14ef3c3d6a3521e406741
7
+ data.tar.gz: 8d2d223f9be47268939f5c4b99d3d20cca149f22a9ebd771c036191167277e7da466837a6576dda773708a664ee781dd5840a0656363d9bc03707f635ef2df47
data/README.md CHANGED
@@ -21,8 +21,9 @@ Provides:
21
21
  - [Simple message signing](#2-simple-message-signing-authentication)
22
22
  - [Sign-In-With-Solana (SIWS)](#3-sign-in-with-solana-siws)
23
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)
24
+ - [Server-built stake transaction (React-free Rails + Stimulus)](#5-server-built-stake-transaction-react-free-rails--stimulus)
25
+ - [Working with public keys](#6-working-with-public-keys)
26
+ - [Networks](#7-networks)
26
27
  4. [Writing a custom adapter](#writing-a-custom-adapter)
27
28
  5. [Error reference](#error-reference)
28
29
  6. [API reference](#api-reference)
@@ -349,7 +350,313 @@ signature = adapter.send(:send_raw_transaction, raw_bytes, rpc_url, options)
349
350
 
350
351
  ---
351
352
 
352
- ### 5. Working with public keys
353
+ ### 5. Server-built stake transaction (React-free Rails + Stimulus)
354
+
355
+ This example shows how to build a Solana staking UI without React.
356
+ The transaction is constructed and partially signed on the server using
357
+ [solana-ruby-kit](https://github.com/pzupan/solana-ruby-kit), then sent to
358
+ the browser for the user's wallet signature. This gem seeds the wallet
359
+ picker and handles any server-side signature work.
360
+
361
+ **Flow:**
362
+ 1. Rails layout seeds registered wallet metadata into `window.__SOLANA_WALLETS__`.
363
+ 2. A Stimulus controller detects the wallet, connects, and collects the SOL amount.
364
+ 3. The browser POSTs `{ public_key, sol }` to `/staking/prepare`.
365
+ 4. The Rails controller builds a `createAccount + delegate` transaction,
366
+ partially signs it with the generated stake-account keypair, and returns
367
+ the wire bytes as Base64.
368
+ 5. The Stimulus controller passes the bytes to the wallet for the user's signature.
369
+ 6. The signed wire bytes are POSTed to `/staking/submit`.
370
+ 7. Rails verifies every Ed25519 signature via `WalletStandard`, then broadcasts
371
+ to the cluster and returns the transaction signature.
372
+
373
+ #### Environment — RPC credentials
374
+
375
+ Store your RPC API key in `.env` (already gitignored by Rails):
376
+
377
+ ```
378
+ # .env
379
+ HELIUS_API_KEY=your-api-key-here
380
+ ```
381
+
382
+ #### Initializer — configure solana-ruby-kit and register adapters
383
+
384
+ ```ruby
385
+ # config/initializers/solana_ruby_kit.rb
386
+ Solana::Ruby::Kit.configure do |config|
387
+ if Rails.env.production?
388
+ config.rpc_url = "https://mainnet.helius-rpc.com/?api-key=#{ENV.fetch('HELIUS_API_KEY')}"
389
+ config.ws_url = "wss://mainnet.helius-rpc.com/?api-key=#{ENV.fetch('HELIUS_API_KEY')}"
390
+ else
391
+ config.rpc_url = "https://devnet.helius-rpc.com/?api-key=#{ENV.fetch('HELIUS_API_KEY')}"
392
+ config.ws_url = "wss://devnet.helius-rpc.com/?api-key=#{ENV.fetch('HELIUS_API_KEY')}"
393
+ end
394
+ config.commitment = :confirmed
395
+ config.timeout = 30
396
+ end
397
+ ```
398
+
399
+ ```ruby
400
+ # config/initializers/solana_wallet_adapter.rb
401
+ SolanaWalletAdapter::WalletRegistry.register(
402
+ SolanaWalletAdapter::Wallets::PhantomWalletAdapter,
403
+ SolanaWalletAdapter::Wallets::SolflareWalletAdapter,
404
+ SolanaWalletAdapter::Wallets::LedgerWalletAdapter,
405
+ SolanaWalletAdapter::Wallets::CoinbaseWalletAdapter,
406
+ SolanaWalletAdapter::Wallets::WalletConnectWalletAdapter,
407
+ )
408
+ ```
409
+
410
+ #### Layout — seed wallet metadata
411
+
412
+ ```erb
413
+ <%# app/views/layouts/application.html.erb %>
414
+ <head>
415
+ <%# ... %>
416
+ <script>window.__SOLANA_WALLETS__ = <%= solana_wallets_json.html_safe %>;</script>
417
+ </head>
418
+ ```
419
+
420
+ #### Rails controller — build, verify, and broadcast
421
+
422
+ `Solana::Ruby::Kit.rpc_client` picks up the URL configured in the initializer.
423
+ `WalletStandard.verify_signed_transaction!` decodes the wire bytes returned by
424
+ the browser wallet and verifies every Ed25519 signature before broadcasting.
425
+
426
+ ```ruby
427
+ # app/controllers/staking_controller.rb
428
+ require 'base64'
429
+
430
+ class StakingController < ApplicationController
431
+ VOTE_ACCOUNT = '26RGqX3mezgYDxJnGh94gnMM4L2k9grH1eWcTSCHnaxR'
432
+
433
+ # POST /staking/prepare
434
+ # Builds and partially signs a stake transaction server-side.
435
+ # Returns a base64-encoded wire transaction ready for the user's wallet to sign.
436
+ def prepare
437
+ public_key = params.require(:public_key)
438
+ lamports = (params.require(:sol).to_f * 1_000_000_000).to_i
439
+
440
+ rpc = Solana::Ruby::Kit.rpc_client
441
+ blockhash_result = rpc.get_latest_blockhash
442
+ blockhash = blockhash_result.value.blockhash
443
+ last_valid = blockhash_result.value.last_valid_block_height
444
+
445
+ owner = Solana::Ruby::Kit::Addresses::Address.new(public_key)
446
+ stake_kp = Solana::Ruby::Kit::Keys.generate_key_pair
447
+ stake_address = Solana::Ruby::Kit::Addresses::Address.new(
448
+ Solana::Ruby::Kit::Addresses.encode_address(stake_kp.verify_key.to_bytes)
449
+ )
450
+
451
+ create_ixs = Solana::Ruby::Kit::Programs::StakeProgram.create_account_instructions(
452
+ from: owner,
453
+ stake_account: stake_address,
454
+ authorized: owner,
455
+ lamports: lamports
456
+ )
457
+
458
+ delegate_ix = Solana::Ruby::Kit::Programs::StakeProgram.delegate_instruction(
459
+ stake_account: stake_address,
460
+ vote_account: Solana::Ruby::Kit::Addresses::Address.new(VOTE_ACCOUNT),
461
+ authorized: owner
462
+ )
463
+
464
+ message = Solana::Ruby::Kit::TransactionMessages::TransactionMessage.new(
465
+ version: :legacy,
466
+ instructions: create_ixs + [delegate_ix],
467
+ fee_payer: owner,
468
+ lifetime_constraint: Solana::Ruby::Kit::TransactionMessages::BlockhashLifetimeConstraint.new(
469
+ blockhash: blockhash,
470
+ last_valid_block_height: last_valid
471
+ )
472
+ )
473
+
474
+ tx = Solana::Ruby::Kit::Transactions.compile_transaction_message(message)
475
+ tx = Solana::Ruby::Kit::Transactions.partially_sign_transaction([stake_kp.signing_key], tx)
476
+
477
+ wire_bytes = Solana::Ruby::Kit::Transactions.wire_encode_transaction(tx)
478
+ render json: { transaction: Base64.strict_encode64(wire_bytes) }
479
+ rescue => e
480
+ render json: { error: e.message }, status: :unprocessable_entity
481
+ end
482
+
483
+ # POST /staking/submit
484
+ # Receives a wallet-signed transaction (base64 wire bytes), verifies every
485
+ # Ed25519 signature server-side via WalletStandard, then broadcasts to the
486
+ # cluster and returns the transaction signature.
487
+ def submit
488
+ signed_b64 = params.require(:signed_transaction)
489
+ wire_bytes = Base64.strict_decode64(signed_b64)
490
+
491
+ tx = Solana::Ruby::Kit::WalletStandard.verify_signed_transaction!(wire_bytes)
492
+ Solana::Ruby::Kit::Transactions.assert_fully_signed_transaction!(tx)
493
+
494
+ rpc = Solana::Ruby::Kit.rpc_client
495
+ sig = rpc.send_transaction(signed_b64)
496
+
497
+ render json: { signature: sig.value }
498
+ rescue => e
499
+ render json: { error: e.message }, status: :unprocessable_entity
500
+ end
501
+ end
502
+ ```
503
+
504
+ #### Routes
505
+
506
+ ```ruby
507
+ # config/routes.rb
508
+ post '/staking/prepare', to: 'staking#prepare'
509
+ post '/staking/submit', to: 'staking#submit'
510
+ ```
511
+
512
+ #### ViewComponent — Stimulus markup
513
+
514
+ ```erb
515
+ <%# app/components/stake_button/show_component.html.erb %>
516
+ <div data-controller="stake">
517
+ <button
518
+ class="btn btn-outline-primary w-100 mb-3"
519
+ data-action="click->stake#connectWallet"
520
+ data-stake-target="connectButton">
521
+ Select Wallet
522
+ </button>
523
+
524
+ <input
525
+ type="number"
526
+ class="form-control mb-3"
527
+ data-stake-target="input"
528
+ disabled
529
+ value="1"
530
+ min="0.01"
531
+ step="0.01"
532
+ />
533
+
534
+ <button
535
+ class="btn btn-primary w-100"
536
+ data-action="click->stake#stake"
537
+ data-stake-target="stakeButton"
538
+ disabled>
539
+ Stake Now
540
+ </button>
541
+
542
+ <p class="mt-2 small text-muted" data-stake-target="status"></p>
543
+ </div>
544
+ ```
545
+
546
+ #### Stimulus controller — wallet connection and signing
547
+
548
+ The signed transaction is POSTed back to Rails for server-side verification and
549
+ broadcasting. No RPC credentials are needed in the browser.
550
+
551
+ ```js
552
+ // app/javascript/controllers/stake_controller.js
553
+ import { Controller } from "@hotwired/stimulus"
554
+ import { Transaction } from "@solana/web3.js"
555
+
556
+ export default class extends Controller {
557
+ static targets = ["connectButton", "input", "stakeButton", "status"]
558
+
559
+ connect() {
560
+ this.provider = null
561
+ this.publicKey = null
562
+ this.detectWallet()
563
+ }
564
+
565
+ detectWallet() {
566
+ if (window.phantom?.solana?.isPhantom) this.provider = window.phantom.solana
567
+ else if (window.solana?.isPhantom) this.provider = window.solana
568
+ else if (window.solflare?.isSolflare) this.provider = window.solflare
569
+ }
570
+
571
+ async connectWallet() {
572
+ if (!this.provider) this.detectWallet()
573
+ if (!this.provider) {
574
+ this.setStatus("No Solana wallet found. Please install Phantom or Solflare.")
575
+ return
576
+ }
577
+ try {
578
+ this.setStatus("Connecting…")
579
+ const resp = await this.provider.connect()
580
+ this.publicKey = resp.publicKey.toString()
581
+ this.connectButtonTarget.textContent =
582
+ `${this.publicKey.slice(0, 6)}…${this.publicKey.slice(-4)}`
583
+ this.inputTarget.disabled = false
584
+ this.stakeButtonTarget.disabled = false
585
+ this.setStatus("")
586
+ } catch (e) {
587
+ this.setStatus(`Connect failed: ${e.message}`)
588
+ }
589
+ }
590
+
591
+ async stake() {
592
+ if (!this.publicKey) return
593
+
594
+ const sol = parseFloat(this.inputTarget.value || "0")
595
+ if (sol <= 0) { this.setStatus("Enter a valid SOL amount."); return }
596
+
597
+ this.stakeButtonTarget.disabled = true
598
+ this.setStatus("Building transaction…")
599
+
600
+ try {
601
+ const csrfToken = document.querySelector("meta[name='csrf-token']").content
602
+ const resp = await fetch("/staking/prepare", {
603
+ method: "POST",
604
+ headers: { "Content-Type": "application/json", "X-CSRF-Token": csrfToken },
605
+ body: JSON.stringify({ public_key: this.publicKey, sol }),
606
+ })
607
+ const data = await resp.json()
608
+ if (data.error) throw new Error(data.error)
609
+
610
+ this.setStatus("Awaiting wallet signature…")
611
+ const txBytes = Uint8Array.from(atob(data.transaction), c => c.charCodeAt(0))
612
+ const tx = Transaction.from(txBytes)
613
+ const signedTx = await this.provider.signTransaction(tx)
614
+
615
+ this.setStatus("Broadcasting…")
616
+ const signedB64 = btoa(String.fromCharCode(...signedTx.serialize()))
617
+ const submitResp = await fetch("/staking/submit", {
618
+ method: "POST",
619
+ headers: { "Content-Type": "application/json", "X-CSRF-Token": csrfToken },
620
+ body: JSON.stringify({ signed_transaction: signedB64 }),
621
+ })
622
+ const submitData = await submitResp.json()
623
+ if (submitData.error) throw new Error(submitData.error)
624
+
625
+ this.setStatus(`Staked! Tx: ${submitData.signature.slice(0, 20)}…`)
626
+ } catch (e) {
627
+ this.setStatus(`Error: ${e.message}`)
628
+ this.stakeButtonTarget.disabled = false
629
+ }
630
+ }
631
+
632
+ setStatus(msg) {
633
+ if (this.hasStatusTarget) this.statusTarget.textContent = msg
634
+ }
635
+ }
636
+ ```
637
+
638
+ #### esbuild — IIFE format required
639
+
640
+ `@solana/web3.js` uses `import.meta` internally. Bundle as IIFE and define
641
+ `import.meta.url` away so the bundle runs as a plain script (no `type="module"`
642
+ needed):
643
+
644
+ ```json
645
+ // package.json
646
+ "build": "esbuild app/javascript/application.js --bundle --sourcemap --format=iife --define:global=globalThis --define:import.meta.url=undefined --outdir=app/assets/builds --public-path=/assets"
647
+ ```
648
+
649
+ > **Note:** `@solana/web3.js` is still needed in the browser solely to
650
+ > deserialize the server-built transaction (`Transaction.from`) before passing it
651
+ > to the wallet for signing. Broadcasting is handled server-side by
652
+ > `WalletStandard` + `rpc.send_transaction`, so `Connection` and `sendRawTransaction`
653
+ > are not used. All React and `@solana/wallet-adapter-react*` packages can be
654
+ > removed from `package.json`.
655
+
656
+ ---
657
+
658
+ ### 6. Working with public keys
659
+
353
660
 
354
661
  ```ruby
355
662
  # From a Base58 string (most common – what wallets return)
@@ -372,7 +679,7 @@ pk1 == pk2 # => true (System Program address)
372
679
 
373
680
  ---
374
681
 
375
- ### 6. Networks
682
+ ### 7. Networks
376
683
 
377
684
  ```ruby
378
685
  net = SolanaWalletAdapter::Network::MainnetBeta
@@ -36,6 +36,8 @@ module SolanaWalletAdapter
36
36
 
37
37
  # Input parameters for a SIWS sign-in request.
38
38
  class SignInInput < T::Struct
39
+ extend T::Sig
40
+
39
41
  # The domain presenting the sign-in request (e.g. "example.com").
40
42
  const :domain, String
41
43
 
@@ -2,5 +2,5 @@
2
2
  # typed: strict
3
3
 
4
4
  module SolanaWalletAdapter
5
- VERSION = "0.1.1"
5
+ VERSION = "0.1.3"
6
6
  end
@@ -28,7 +28,7 @@ module SolanaWalletAdapter
28
28
  extend T::Sig
29
29
 
30
30
  # Register one or more adapter classes.
31
- sig { params(adapter_classes: T::Array[T.class_of(BaseWalletAdapter)]).void }
31
+ sig { params(adapter_classes: T.class_of(BaseWalletAdapter)).void }
32
32
  def register(*adapter_classes)
33
33
  adapter_classes.each do |klass|
34
34
  instance = klass.new
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solana_ruby_wallet_adapter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - pzupan
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-04-09 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: sorbet-runtime
@@ -177,7 +177,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
177
177
  - !ruby/object:Gem::Version
178
178
  version: '0'
179
179
  requirements: []
180
- rubygems_version: 3.6.2
180
+ rubygems_version: 4.0.10
181
181
  specification_version: 4
182
182
  summary: Modular Solana wallet adapters for Ruby on Rails
183
183
  test_files: []