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
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
module SolanaWalletAdapter
|
|
5
|
+
# Abstract base class for all wallet adapters.
|
|
6
|
+
# Mirrors BaseWalletAdapter from @solana/wallet-adapter-base.
|
|
7
|
+
#
|
|
8
|
+
# Subclasses MUST implement:
|
|
9
|
+
# - name (String)
|
|
10
|
+
# - url (String)
|
|
11
|
+
# - icon (String)
|
|
12
|
+
# - ready_state (WalletReadyState)
|
|
13
|
+
# - public_key (T.nilable(PublicKey))
|
|
14
|
+
# - connecting? (Boolean)
|
|
15
|
+
# - connect
|
|
16
|
+
# - disconnect
|
|
17
|
+
# - send_transaction(transaction, rpc_url, options)
|
|
18
|
+
#
|
|
19
|
+
# In Rails the adapter is used server-side to:
|
|
20
|
+
# 1. Supply wallet metadata (name/icon/url) for the frontend.
|
|
21
|
+
# 2. Verify signatures returned by the front-end wallet.
|
|
22
|
+
# 3. Facilitate Sign-In-With-Solana flows.
|
|
23
|
+
class BaseWalletAdapter
|
|
24
|
+
extend T::Sig
|
|
25
|
+
extend T::Helpers
|
|
26
|
+
include EventEmitter
|
|
27
|
+
|
|
28
|
+
abstract!
|
|
29
|
+
|
|
30
|
+
# -------------------------------------------------------------------------
|
|
31
|
+
# Abstract interface
|
|
32
|
+
# -------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
# Human-readable wallet name.
|
|
35
|
+
sig { abstract.returns(String) }
|
|
36
|
+
def name; end
|
|
37
|
+
|
|
38
|
+
# URL pointing to the wallet's download / landing page.
|
|
39
|
+
sig { abstract.returns(String) }
|
|
40
|
+
def url; end
|
|
41
|
+
|
|
42
|
+
# Base64-encoded SVG or PNG data URI for the wallet's icon.
|
|
43
|
+
sig { abstract.returns(String) }
|
|
44
|
+
def icon; end
|
|
45
|
+
|
|
46
|
+
# Current ready state of this adapter.
|
|
47
|
+
sig { abstract.returns(WalletReadyState) }
|
|
48
|
+
def ready_state; end
|
|
49
|
+
|
|
50
|
+
# The currently connected public key, or nil when not connected.
|
|
51
|
+
sig { abstract.returns(T.nilable(PublicKey)) }
|
|
52
|
+
def public_key; end
|
|
53
|
+
|
|
54
|
+
# True while a connection is in progress.
|
|
55
|
+
sig { abstract.returns(T::Boolean) }
|
|
56
|
+
def connecting?; end
|
|
57
|
+
|
|
58
|
+
# Set of supported transaction versions. nil means all versions.
|
|
59
|
+
sig { returns(SupportedTransactionVersions) }
|
|
60
|
+
def supported_transaction_versions
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Initiate a wallet connection.
|
|
65
|
+
sig { abstract.void }
|
|
66
|
+
def connect; end
|
|
67
|
+
|
|
68
|
+
# Disconnect the wallet.
|
|
69
|
+
sig { abstract.void }
|
|
70
|
+
def disconnect; end
|
|
71
|
+
|
|
72
|
+
# Send a transaction to the network via the RPC endpoint.
|
|
73
|
+
sig do
|
|
74
|
+
abstract
|
|
75
|
+
.params(
|
|
76
|
+
transaction: Transaction,
|
|
77
|
+
rpc_url: String,
|
|
78
|
+
options: SendTransactionOptions
|
|
79
|
+
)
|
|
80
|
+
.returns(String) # transaction signature (base58)
|
|
81
|
+
end
|
|
82
|
+
def send_transaction(transaction, rpc_url, options); end
|
|
83
|
+
|
|
84
|
+
# -------------------------------------------------------------------------
|
|
85
|
+
# Concrete helpers
|
|
86
|
+
# -------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
# True when a public key is present (i.e. connected).
|
|
89
|
+
sig { returns(T::Boolean) }
|
|
90
|
+
def connected?
|
|
91
|
+
!public_key.nil?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Serialize adapter metadata as a plain Hash (useful for JSON APIs).
|
|
95
|
+
sig { returns(T::Hash[String, T.untyped]) }
|
|
96
|
+
def to_h
|
|
97
|
+
{
|
|
98
|
+
"name" => name,
|
|
99
|
+
"url" => url,
|
|
100
|
+
"icon" => icon,
|
|
101
|
+
"readyState" => ready_state.serialize,
|
|
102
|
+
"connected" => connected?,
|
|
103
|
+
"publicKey" => public_key&.to_base58,
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
sig { returns(String) }
|
|
108
|
+
def inspect
|
|
109
|
+
"#<#{self.class} name=#{name.inspect} ready_state=#{ready_state.serialize}>"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
module SolanaWalletAdapter
|
|
5
|
+
# Helpers mixed into ActionController::Base when the Railtie loads.
|
|
6
|
+
module ControllerHelpers
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
# Verify a Solana message signature submitted from the browser.
|
|
10
|
+
# Raises WalletSignMessageError if verification fails.
|
|
11
|
+
#
|
|
12
|
+
# Params:
|
|
13
|
+
# public_key_b58 – Base58 string of the signer's public key
|
|
14
|
+
# message – the original UTF-8 message string
|
|
15
|
+
# signature_b64 – Base64-encoded 64-byte Ed25519 signature
|
|
16
|
+
sig do
|
|
17
|
+
params(
|
|
18
|
+
public_key_b58: String,
|
|
19
|
+
message: String,
|
|
20
|
+
signature_b64: String
|
|
21
|
+
).void
|
|
22
|
+
end
|
|
23
|
+
def verify_wallet_signature!(public_key_b58:, message:, signature_b64:)
|
|
24
|
+
require "base64"
|
|
25
|
+
public_key = PublicKey.new(public_key_b58)
|
|
26
|
+
signature = Base64.strict_decode64(signature_b64)
|
|
27
|
+
SignatureVerifier.new.verify!(
|
|
28
|
+
public_key: public_key,
|
|
29
|
+
message: message,
|
|
30
|
+
signature: signature
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Verify a Sign-In-With-Solana output POSTed from the browser.
|
|
35
|
+
# Raises WalletSignInError if verification fails.
|
|
36
|
+
sig do
|
|
37
|
+
params(
|
|
38
|
+
input_params: T::Hash[String, T.untyped],
|
|
39
|
+
output_params: T::Hash[String, T.untyped]
|
|
40
|
+
).void
|
|
41
|
+
end
|
|
42
|
+
def verify_sign_in!(input_params:, output_params:)
|
|
43
|
+
require "base64"
|
|
44
|
+
|
|
45
|
+
input = SignInInput.new(
|
|
46
|
+
domain: input_params.fetch("domain"),
|
|
47
|
+
address: input_params.fetch("address"),
|
|
48
|
+
statement: input_params["statement"],
|
|
49
|
+
uri: input_params["uri"],
|
|
50
|
+
version: input_params.fetch("version", "1"),
|
|
51
|
+
nonce: input_params["nonce"],
|
|
52
|
+
issued_at: input_params["issued_at"],
|
|
53
|
+
expiration_time: input_params["expiration_time"],
|
|
54
|
+
not_before: input_params["not_before"],
|
|
55
|
+
request_id: input_params["request_id"],
|
|
56
|
+
resources: Array(input_params["resources"]),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
output = SignInOutput.new(
|
|
60
|
+
address: output_params.fetch("address"),
|
|
61
|
+
signed_message: output_params.fetch("signed_message"),
|
|
62
|
+
signature: Base64.strict_decode64(output_params.fetch("signature")),
|
|
63
|
+
signature_type: output_params.fetch("signature_type", "ed25519"),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
SignatureVerifier.new.verify_sign_in!(input: input, output: output)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
module SolanaWalletAdapter
|
|
5
|
+
# Base error for all wallet-adapter errors.
|
|
6
|
+
# Mirrors WalletError from @solana/wallet-adapter-base.
|
|
7
|
+
class WalletError < StandardError
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
sig { returns(T.nilable(Exception)) }
|
|
11
|
+
attr_reader :cause_error
|
|
12
|
+
|
|
13
|
+
sig { params(message: T.nilable(String), cause_error: T.nilable(Exception)).void }
|
|
14
|
+
def initialize(message = nil, cause_error = nil)
|
|
15
|
+
super(message)
|
|
16
|
+
@cause_error = cause_error
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Raised when a wallet is not in a ready / installed state.
|
|
21
|
+
class WalletNotReadyError < WalletError; end
|
|
22
|
+
|
|
23
|
+
# Raised when the wallet extension / app fails to load.
|
|
24
|
+
class WalletLoadError < WalletError; end
|
|
25
|
+
|
|
26
|
+
# Raised when the wallet adapter is misconfigured.
|
|
27
|
+
class WalletConfigError < WalletError; end
|
|
28
|
+
|
|
29
|
+
# Raised when the wallet connection attempt fails.
|
|
30
|
+
class WalletConnectionError < WalletError; end
|
|
31
|
+
|
|
32
|
+
# Raised when the wallet disconnects unexpectedly.
|
|
33
|
+
class WalletDisconnectedError < WalletError; end
|
|
34
|
+
|
|
35
|
+
# Raised when an explicit disconnect request fails.
|
|
36
|
+
class WalletDisconnectionError < WalletError; end
|
|
37
|
+
|
|
38
|
+
# Raised when the wallet returns an unexpected account.
|
|
39
|
+
class WalletAccountError < WalletError; end
|
|
40
|
+
|
|
41
|
+
# Raised when the wallet returns an invalid public key.
|
|
42
|
+
class WalletPublicKeyError < WalletError; end
|
|
43
|
+
|
|
44
|
+
# Raised for keypair-level errors.
|
|
45
|
+
class WalletKeypairError < WalletError; end
|
|
46
|
+
|
|
47
|
+
# Raised when an operation requires a connected wallet.
|
|
48
|
+
class WalletNotConnectedError < WalletError; end
|
|
49
|
+
|
|
50
|
+
# Raised when sending a transaction fails.
|
|
51
|
+
class WalletSendTransactionError < WalletError; end
|
|
52
|
+
|
|
53
|
+
# Raised when signing a transaction fails.
|
|
54
|
+
class WalletSignTransactionError < WalletError; end
|
|
55
|
+
|
|
56
|
+
# Raised when signing a message fails.
|
|
57
|
+
class WalletSignMessageError < WalletError; end
|
|
58
|
+
|
|
59
|
+
# Raised when a Sign-In-With-Solana flow fails.
|
|
60
|
+
class WalletSignInError < WalletError; end
|
|
61
|
+
|
|
62
|
+
# Raised when a wallet operation times out.
|
|
63
|
+
class WalletTimeoutError < WalletError; end
|
|
64
|
+
|
|
65
|
+
# Raised when a popup window is blocked.
|
|
66
|
+
class WalletWindowBlockedError < WalletError; end
|
|
67
|
+
|
|
68
|
+
# Raised when the user closes the wallet popup.
|
|
69
|
+
class WalletWindowClosedError < WalletError; end
|
|
70
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
module SolanaWalletAdapter
|
|
5
|
+
# A simple synchronous event-emitter mixin.
|
|
6
|
+
# Mirrors the EventEmitter3 interface used by the TypeScript adapters.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# class MyAdapter
|
|
10
|
+
# include EventEmitter
|
|
11
|
+
# # ...
|
|
12
|
+
# def do_something
|
|
13
|
+
# emit(:connect, public_key)
|
|
14
|
+
# end
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# adapter = MyAdapter.new
|
|
18
|
+
# adapter.on(:connect) { |pk| puts "connected: #{pk}" }
|
|
19
|
+
module EventEmitter
|
|
20
|
+
extend T::Sig
|
|
21
|
+
extend T::Helpers
|
|
22
|
+
|
|
23
|
+
# @!visibility private
|
|
24
|
+
def self.included(base)
|
|
25
|
+
base.extend(ClassMethods)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module ClassMethods
|
|
29
|
+
extend T::Sig
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Register a listener for the given event name.
|
|
33
|
+
# Returns self for chaining.
|
|
34
|
+
sig { params(event: Symbol, block: T.proc.params(args: T.untyped).void).returns(T.self_type) }
|
|
35
|
+
def on(event, &block)
|
|
36
|
+
listeners_for(event) << block
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Register a one-shot listener that auto-removes itself after the first call.
|
|
41
|
+
sig { params(event: Symbol, block: T.proc.params(args: T.untyped).void).returns(T.self_type) }
|
|
42
|
+
def once(event, &block)
|
|
43
|
+
wrapper = T.let(nil, T.nilable(Proc))
|
|
44
|
+
wrapper = proc do |*args|
|
|
45
|
+
off(event, T.must(wrapper))
|
|
46
|
+
block.call(*args)
|
|
47
|
+
end
|
|
48
|
+
on(event, &T.must(wrapper))
|
|
49
|
+
self
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Remove a specific listener (or all listeners if block is omitted).
|
|
53
|
+
sig { params(event: Symbol, block: T.nilable(Proc)).returns(T.self_type) }
|
|
54
|
+
def off(event, block = nil)
|
|
55
|
+
if block
|
|
56
|
+
listeners_for(event).delete(block)
|
|
57
|
+
else
|
|
58
|
+
event_listeners.delete(event)
|
|
59
|
+
end
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Emit an event, calling all registered listeners synchronously.
|
|
64
|
+
sig { params(event: Symbol, args: T.untyped).void }
|
|
65
|
+
def emit(event, *args)
|
|
66
|
+
listeners_for(event).dup.each { |listener| listener.call(*args) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
sig { params(event: Symbol).returns(T::Array[Proc]) }
|
|
72
|
+
def listeners_for(event)
|
|
73
|
+
event_listeners[event] ||= []
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
sig { returns(T::Hash[Symbol, T::Array[Proc]]) }
|
|
77
|
+
def event_listeners
|
|
78
|
+
@event_listeners ||= T.let({}, T.nilable(T::Hash[Symbol, T::Array[Proc]]))
|
|
79
|
+
T.must(@event_listeners)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
module SolanaWalletAdapter
|
|
5
|
+
# Mixin for adapters that can sign arbitrary byte messages.
|
|
6
|
+
# Mirrors MessageSignerWalletAdapterProps from @solana/wallet-adapter-base.
|
|
7
|
+
module MessageSignerWalletAdapter
|
|
8
|
+
extend T::Sig
|
|
9
|
+
extend T::Helpers
|
|
10
|
+
|
|
11
|
+
requires_ancestor { BaseWalletAdapter }
|
|
12
|
+
|
|
13
|
+
# Sign an arbitrary message and return the 64-byte Ed25519 signature.
|
|
14
|
+
sig do
|
|
15
|
+
abstract
|
|
16
|
+
.params(message: String) # binary string
|
|
17
|
+
.returns(String) # 64-byte binary signature
|
|
18
|
+
end
|
|
19
|
+
def sign_message(message); end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Abstract base class combining transaction + message signing.
|
|
23
|
+
# Mirrors BaseMessageSignerWalletAdapter from @solana/wallet-adapter-base.
|
|
24
|
+
class BaseMessageSignerWalletAdapter < BaseSignerWalletAdapter
|
|
25
|
+
extend T::Sig
|
|
26
|
+
extend T::Helpers
|
|
27
|
+
include MessageSignerWalletAdapter
|
|
28
|
+
|
|
29
|
+
abstract!
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# -------------------------------------------------------------------------
|
|
33
|
+
# Sign-In-With-Solana (SIWS) support
|
|
34
|
+
# Mirrors SignInMessageSignerWalletAdapterProps from @solana/wallet-adapter-base.
|
|
35
|
+
# -------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
# Input parameters for a SIWS sign-in request.
|
|
38
|
+
class SignInInput < T::Struct
|
|
39
|
+
# The domain presenting the sign-in request (e.g. "example.com").
|
|
40
|
+
const :domain, String
|
|
41
|
+
|
|
42
|
+
# The Solana address being authenticated.
|
|
43
|
+
const :address, String
|
|
44
|
+
|
|
45
|
+
# Human-readable statement.
|
|
46
|
+
const :statement, T.nilable(String), default: nil
|
|
47
|
+
|
|
48
|
+
# URI of the resource that is the subject of the signing.
|
|
49
|
+
const :uri, T.nilable(String), default: nil
|
|
50
|
+
|
|
51
|
+
# Version of the SIWS message specification.
|
|
52
|
+
const :version, String, default: "1"
|
|
53
|
+
|
|
54
|
+
# Unique nonce to prevent replay attacks.
|
|
55
|
+
const :nonce, T.nilable(String), default: nil
|
|
56
|
+
|
|
57
|
+
# ISO 8601 datetime of when the sign-in was issued.
|
|
58
|
+
const :issued_at, T.nilable(String), default: nil
|
|
59
|
+
|
|
60
|
+
# ISO 8601 datetime after which the sign-in expires.
|
|
61
|
+
const :expiration_time, T.nilable(String), default: nil
|
|
62
|
+
|
|
63
|
+
# ISO 8601 datetime after which the sign-in is valid.
|
|
64
|
+
const :not_before, T.nilable(String), default: nil
|
|
65
|
+
|
|
66
|
+
# Randomised value for this request (request ID).
|
|
67
|
+
const :request_id, T.nilable(String), default: nil
|
|
68
|
+
|
|
69
|
+
# List of information or references the dapp requests to be included.
|
|
70
|
+
const :resources, T::Array[String], default: []
|
|
71
|
+
|
|
72
|
+
# Builds the canonical EIP-4361-like SIWS message string.
|
|
73
|
+
sig { returns(String) }
|
|
74
|
+
def to_message
|
|
75
|
+
lines = ["#{domain} wants you to sign in with your Solana account:"]
|
|
76
|
+
lines << address
|
|
77
|
+
lines << "" << (statement || "") << ""
|
|
78
|
+
lines << "URI: #{uri}" if uri
|
|
79
|
+
lines << "Version: #{version}"
|
|
80
|
+
lines << "Nonce: #{nonce}" if nonce
|
|
81
|
+
lines << "Issued At: #{issued_at}" if issued_at
|
|
82
|
+
lines << "Expiration Time: #{expiration_time}" if expiration_time
|
|
83
|
+
lines << "Not Before: #{not_before}" if not_before
|
|
84
|
+
lines << "Request ID: #{request_id}" if request_id
|
|
85
|
+
resources.each_with_index do |r, i|
|
|
86
|
+
lines << "Resources:" if i.zero?
|
|
87
|
+
lines << "- #{r}"
|
|
88
|
+
end
|
|
89
|
+
lines.join("\n")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Output from a completed SIWS sign-in.
|
|
94
|
+
class SignInOutput < T::Struct
|
|
95
|
+
# The account that signed in.
|
|
96
|
+
const :address, String
|
|
97
|
+
|
|
98
|
+
# The signed SIWS message bytes (UTF-8 binary).
|
|
99
|
+
const :signed_message, String
|
|
100
|
+
|
|
101
|
+
# The 64-byte Ed25519 signature.
|
|
102
|
+
const :signature, String
|
|
103
|
+
|
|
104
|
+
# The type of signature (always "ed25519" for Solana).
|
|
105
|
+
const :signature_type, String, default: "ed25519"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Abstract base class that adds sign-in capability.
|
|
109
|
+
class BaseSignInMessageSignerWalletAdapter < BaseMessageSignerWalletAdapter
|
|
110
|
+
extend T::Sig
|
|
111
|
+
extend T::Helpers
|
|
112
|
+
|
|
113
|
+
abstract!
|
|
114
|
+
|
|
115
|
+
# Perform a SIWS sign-in flow. Returns a +SignInOutput+.
|
|
116
|
+
sig do
|
|
117
|
+
abstract
|
|
118
|
+
.params(input: T.nilable(SignInInput))
|
|
119
|
+
.returns(SignInOutput)
|
|
120
|
+
end
|
|
121
|
+
def sign_in(input = nil); end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
module SolanaWalletAdapter
|
|
5
|
+
# Mirrors WalletAdapterNetwork from @solana/wallet-adapter-base.
|
|
6
|
+
class Network < T::Enum
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
enums do
|
|
10
|
+
MainnetBeta = new("mainnet-beta")
|
|
11
|
+
Testnet = new("testnet")
|
|
12
|
+
Devnet = new("devnet")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# The RPC cluster URL commonly associated with each network.
|
|
16
|
+
RPC_URLS = T.let(
|
|
17
|
+
{
|
|
18
|
+
MainnetBeta => "https://api.mainnet-beta.solana.com",
|
|
19
|
+
Testnet => "https://api.testnet.solana.com",
|
|
20
|
+
Devnet => "https://api.devnet.solana.com",
|
|
21
|
+
}.freeze,
|
|
22
|
+
T::Hash[Network, String]
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
sig { returns(String) }
|
|
26
|
+
def rpc_url
|
|
27
|
+
RPC_URLS.fetch(self)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
require "base58"
|
|
5
|
+
|
|
6
|
+
module SolanaWalletAdapter
|
|
7
|
+
# Lightweight representation of a Solana public key.
|
|
8
|
+
# A Solana public key is a 32-byte Ed25519 public key encoded as Base58.
|
|
9
|
+
class PublicKey
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
BYTE_LENGTH = T.let(32, Integer)
|
|
13
|
+
|
|
14
|
+
sig { returns(String) }
|
|
15
|
+
attr_reader :bytes # raw 32-byte binary string
|
|
16
|
+
|
|
17
|
+
# Accepts a Base58-encoded string, a raw 32-byte binary string, or a
|
|
18
|
+
# 32-element Integer array (matching the JS Uint8Array constructor).
|
|
19
|
+
sig { params(value: T.any(String, T::Array[Integer])).void }
|
|
20
|
+
def initialize(value)
|
|
21
|
+
@bytes = T.let(decode(value), String)
|
|
22
|
+
raise ArgumentError, "Public key must be #{BYTE_LENGTH} bytes" unless @bytes.bytesize == BYTE_LENGTH
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
sig { returns(String) }
|
|
26
|
+
def to_base58
|
|
27
|
+
Base58.binary_to_base58(@bytes, :bitcoin)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
alias to_s to_base58
|
|
31
|
+
|
|
32
|
+
sig { returns(T::Array[Integer]) }
|
|
33
|
+
def to_bytes
|
|
34
|
+
@bytes.bytes
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
sig { params(other: PublicKey).returns(T::Boolean) }
|
|
38
|
+
def ==(other)
|
|
39
|
+
other.is_a?(PublicKey) && bytes == other.bytes
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
sig { returns(Integer) }
|
|
43
|
+
def hash
|
|
44
|
+
bytes.hash
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
sig { returns(String) }
|
|
48
|
+
def inspect
|
|
49
|
+
"#<#{self.class} #{to_base58}>"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
sig { params(value: T.any(String, T::Array[Integer])).returns(String) }
|
|
55
|
+
def decode(value)
|
|
56
|
+
case value
|
|
57
|
+
when Array
|
|
58
|
+
value.pack("C*")
|
|
59
|
+
when String
|
|
60
|
+
# Detect binary vs Base58: binary strings are 32 bytes, Base58 strings
|
|
61
|
+
# are 32–44 ASCII characters.
|
|
62
|
+
if value.encoding == Encoding::BINARY || value.bytesize == BYTE_LENGTH
|
|
63
|
+
value.b
|
|
64
|
+
else
|
|
65
|
+
Base58.base58_to_binary(value, :bitcoin)
|
|
66
|
+
end
|
|
67
|
+
else
|
|
68
|
+
T.absurd(value)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
module SolanaWalletAdapter
|
|
5
|
+
# Hooks into Rails to auto-load configuration from
|
|
6
|
+
# config/initializers/solana_wallet_adapter.rb (if present) and to expose
|
|
7
|
+
# a controller helper module.
|
|
8
|
+
class Railtie < Rails::Railtie
|
|
9
|
+
# Expose helper methods in ActionController and ActionView.
|
|
10
|
+
initializer "solana_wallet_adapter.action_controller" do
|
|
11
|
+
ActiveSupport.on_load(:action_controller_base) do
|
|
12
|
+
include SolanaWalletAdapter::ControllerHelpers
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
initializer "solana_wallet_adapter.action_view" do
|
|
17
|
+
ActiveSupport.on_load(:action_view) do
|
|
18
|
+
include SolanaWalletAdapter::ViewHelpers
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
require "ed25519"
|
|
5
|
+
|
|
6
|
+
module SolanaWalletAdapter
|
|
7
|
+
# Server-side Ed25519 signature verification for Solana.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# verifier = SolanaWalletAdapter::SignatureVerifier.new
|
|
11
|
+
#
|
|
12
|
+
# # Verify a raw message + signature
|
|
13
|
+
# verifier.verify!(
|
|
14
|
+
# public_key: PublicKey.new("..."),
|
|
15
|
+
# message: "hello world",
|
|
16
|
+
# signature: Base64.decode64("...") # 64-byte binary
|
|
17
|
+
# )
|
|
18
|
+
#
|
|
19
|
+
# # Verify a SIWS sign-in output
|
|
20
|
+
# verifier.verify_sign_in!(input: sign_in_input, output: sign_in_output)
|
|
21
|
+
class SignatureVerifier
|
|
22
|
+
extend T::Sig
|
|
23
|
+
|
|
24
|
+
# Verify that +signature+ was produced by the holder of +public_key+
|
|
25
|
+
# over +message+. Raises +WalletSignMessageError+ on failure.
|
|
26
|
+
sig do
|
|
27
|
+
params(
|
|
28
|
+
public_key: PublicKey,
|
|
29
|
+
message: String, # arbitrary binary / UTF-8 bytes
|
|
30
|
+
signature: String # 64-byte binary Ed25519 signature
|
|
31
|
+
).returns(T::Boolean)
|
|
32
|
+
end
|
|
33
|
+
def verify(public_key:, message:, signature:)
|
|
34
|
+
vk = Ed25519::VerifyKey.new(public_key.bytes)
|
|
35
|
+
vk.verify(signature, message)
|
|
36
|
+
true
|
|
37
|
+
rescue Ed25519::VerifyError
|
|
38
|
+
false
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Same as +verify+ but raises +WalletSignMessageError+ on failure.
|
|
42
|
+
sig do
|
|
43
|
+
params(
|
|
44
|
+
public_key: PublicKey,
|
|
45
|
+
message: String,
|
|
46
|
+
signature: String
|
|
47
|
+
).void
|
|
48
|
+
end
|
|
49
|
+
def verify!(public_key:, message:, signature:)
|
|
50
|
+
return if verify(public_key: public_key, message: message, signature: signature)
|
|
51
|
+
|
|
52
|
+
raise WalletSignMessageError, "Signature verification failed for public key #{public_key}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Verify a complete SIWS sign-in output against its input.
|
|
56
|
+
# Raises +WalletSignInError+ on failure.
|
|
57
|
+
sig do
|
|
58
|
+
params(
|
|
59
|
+
input: SignInInput,
|
|
60
|
+
output: SignInOutput
|
|
61
|
+
).void
|
|
62
|
+
end
|
|
63
|
+
def verify_sign_in!(input:, output:)
|
|
64
|
+
unless output.address == input.address
|
|
65
|
+
raise WalletSignInError,
|
|
66
|
+
"Sign-in address mismatch: expected #{input.address}, got #{output.address}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
expected_message = input.to_message
|
|
70
|
+
unless output.signed_message == expected_message
|
|
71
|
+
raise WalletSignInError, "Sign-in message does not match the expected SIWS message"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
public_key = PublicKey.new(output.address)
|
|
75
|
+
verify!(
|
|
76
|
+
public_key: public_key,
|
|
77
|
+
message: output.signed_message,
|
|
78
|
+
signature: output.signature
|
|
79
|
+
)
|
|
80
|
+
rescue WalletError
|
|
81
|
+
raise
|
|
82
|
+
rescue => e
|
|
83
|
+
raise WalletSignInError.new(e.message, e)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|