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,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
module SolanaWalletAdapter
|
|
5
|
+
# Mixin for adapters that can sign transactions locally (without sending).
|
|
6
|
+
# Mirrors SignerWalletAdapterProps from @solana/wallet-adapter-base.
|
|
7
|
+
module SignerWalletAdapter
|
|
8
|
+
extend T::Sig
|
|
9
|
+
extend T::Helpers
|
|
10
|
+
|
|
11
|
+
requires_ancestor { BaseWalletAdapter }
|
|
12
|
+
|
|
13
|
+
# Sign a single transaction and return the signed bytes.
|
|
14
|
+
sig do
|
|
15
|
+
abstract
|
|
16
|
+
.params(transaction: Transaction)
|
|
17
|
+
.returns(Transaction)
|
|
18
|
+
end
|
|
19
|
+
def sign_transaction(transaction); end
|
|
20
|
+
|
|
21
|
+
# Sign multiple transactions and return them in the same order.
|
|
22
|
+
sig do
|
|
23
|
+
abstract
|
|
24
|
+
.params(transactions: T::Array[Transaction])
|
|
25
|
+
.returns(T::Array[Transaction])
|
|
26
|
+
end
|
|
27
|
+
def sign_all_transactions(transactions); end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Abstract base class that combines BaseWalletAdapter with signing.
|
|
31
|
+
# Mirrors BaseSignerWalletAdapter from @solana/wallet-adapter-base.
|
|
32
|
+
class BaseSignerWalletAdapter < BaseWalletAdapter
|
|
33
|
+
extend T::Sig
|
|
34
|
+
extend T::Helpers
|
|
35
|
+
include SignerWalletAdapter
|
|
36
|
+
|
|
37
|
+
abstract!
|
|
38
|
+
|
|
39
|
+
# send_transaction default implementation: sign then forward raw bytes
|
|
40
|
+
# to the Solana RPC via Net::HTTP. Subclasses may override.
|
|
41
|
+
sig do
|
|
42
|
+
override
|
|
43
|
+
.params(
|
|
44
|
+
transaction: Transaction,
|
|
45
|
+
rpc_url: String,
|
|
46
|
+
options: SendTransactionOptions
|
|
47
|
+
)
|
|
48
|
+
.returns(String)
|
|
49
|
+
end
|
|
50
|
+
def send_transaction(transaction, rpc_url, options = SendTransactionOptions.new)
|
|
51
|
+
raise WalletNotConnectedError unless connected?
|
|
52
|
+
|
|
53
|
+
if transaction.versioned?
|
|
54
|
+
unless supported_transaction_versions
|
|
55
|
+
raise WalletSendTransactionError,
|
|
56
|
+
"Sending versioned transactions isn't supported by this wallet"
|
|
57
|
+
end
|
|
58
|
+
unless T.must(supported_transaction_versions).include?(T.must(transaction.version))
|
|
59
|
+
raise WalletSendTransactionError,
|
|
60
|
+
"Sending transaction version #{transaction.version&.serialize} isn't supported by this wallet"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
signed = begin
|
|
65
|
+
sign_transaction(transaction)
|
|
66
|
+
rescue WalletSignTransactionError
|
|
67
|
+
raise
|
|
68
|
+
rescue => e
|
|
69
|
+
raise WalletSendTransactionError.new(e.message, e)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
send_raw_transaction(signed.serialize, rpc_url, options)
|
|
73
|
+
rescue WalletError => e
|
|
74
|
+
emit(:error, e)
|
|
75
|
+
raise
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# Submits serialised transaction bytes to the RPC endpoint and returns
|
|
81
|
+
# the transaction signature string.
|
|
82
|
+
sig { params(raw_bytes: String, rpc_url: String, options: SendTransactionOptions).returns(String) }
|
|
83
|
+
def send_raw_transaction(raw_bytes, rpc_url, options)
|
|
84
|
+
require "net/http"
|
|
85
|
+
require "json"
|
|
86
|
+
require "base64"
|
|
87
|
+
|
|
88
|
+
encoded = Base64.strict_encode64(raw_bytes)
|
|
89
|
+
|
|
90
|
+
config = [encoded, { encoding: "base64" }]
|
|
91
|
+
config[1]["skipPreflight"] = options.skip_preflight if options.skip_preflight
|
|
92
|
+
config[1]["preflightCommitment"] = options.preflight_commitment if options.preflight_commitment
|
|
93
|
+
config[1]["maxRetries"] = options.max_retries if options.max_retries
|
|
94
|
+
config[1]["minContextSlot"] = options.min_context_slot if options.min_context_slot
|
|
95
|
+
|
|
96
|
+
body = JSON.generate({
|
|
97
|
+
jsonrpc: "2.0",
|
|
98
|
+
id: 1,
|
|
99
|
+
method: "sendTransaction",
|
|
100
|
+
params: config,
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
uri = URI(rpc_url)
|
|
104
|
+
response = Net::HTTP.post(uri, body, "Content-Type" => "application/json")
|
|
105
|
+
result = JSON.parse(response.body)
|
|
106
|
+
|
|
107
|
+
if (err = result["error"])
|
|
108
|
+
raise WalletSendTransactionError, err["message"] || err.inspect
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
result.dig("result") || raise(WalletSendTransactionError, "Unexpected RPC response: #{result.inspect}")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
module SolanaWalletAdapter
|
|
5
|
+
# Mirrors the TransactionVersion type in @solana/web3.js.
|
|
6
|
+
# Solana supports "legacy" transactions and versioned transactions (0+).
|
|
7
|
+
class TransactionVersion < T::Enum
|
|
8
|
+
enums do
|
|
9
|
+
Legacy = new("legacy")
|
|
10
|
+
Version0 = new(0)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Mirrors SupportedTransactionVersions – a frozen set of accepted versions.
|
|
15
|
+
# nil means "all versions supported" (equivalent to null/undefined in TS).
|
|
16
|
+
SupportedTransactionVersions = T.type_alias { T.nilable(T::Set[TransactionVersion]) }
|
|
17
|
+
|
|
18
|
+
# Lightweight wrapper around raw serialised Solana transaction bytes.
|
|
19
|
+
# The gem does not attempt to parse the transaction wire format; that
|
|
20
|
+
# responsibility belongs to a dedicated Solana RPC client gem.
|
|
21
|
+
class Transaction
|
|
22
|
+
extend T::Sig
|
|
23
|
+
|
|
24
|
+
sig { returns(String) }
|
|
25
|
+
attr_reader :wire_bytes
|
|
26
|
+
|
|
27
|
+
sig { returns(T.nilable(TransactionVersion)) }
|
|
28
|
+
attr_reader :version
|
|
29
|
+
|
|
30
|
+
sig { params(wire_bytes: String, version: T.nilable(TransactionVersion)).void }
|
|
31
|
+
def initialize(wire_bytes, version: nil)
|
|
32
|
+
@wire_bytes = T.let(wire_bytes, String)
|
|
33
|
+
@version = T.let(version, T.nilable(TransactionVersion))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
sig { returns(T::Boolean) }
|
|
37
|
+
def versioned?
|
|
38
|
+
!version.nil? && version != TransactionVersion::Legacy
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sig { returns(String) }
|
|
42
|
+
def serialize
|
|
43
|
+
wire_bytes
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Options forwarded to the RPC sendTransaction call.
|
|
48
|
+
class SendTransactionOptions < T::Struct
|
|
49
|
+
extend T::Sig
|
|
50
|
+
|
|
51
|
+
# Additional signers that should co-sign the transaction before sending.
|
|
52
|
+
const :signers, T::Array[String], default: []
|
|
53
|
+
|
|
54
|
+
# Preflight commitment level. Maps to Solana's Commitment type.
|
|
55
|
+
const :preflight_commitment, T.nilable(String), default: nil
|
|
56
|
+
|
|
57
|
+
# Whether to skip preflight simulation.
|
|
58
|
+
const :skip_preflight, T::Boolean, default: false
|
|
59
|
+
|
|
60
|
+
# Maximum number of retries.
|
|
61
|
+
const :max_retries, T.nilable(Integer), default: nil
|
|
62
|
+
|
|
63
|
+
# Minimum context slot for the transaction.
|
|
64
|
+
const :min_context_slot, T.nilable(Integer), default: nil
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
module SolanaWalletAdapter
|
|
5
|
+
# Helpers mixed into ActionView::Base when the Railtie loads.
|
|
6
|
+
module ViewHelpers
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
# Returns a JSON array of all registered wallet adapter metadata.
|
|
10
|
+
# Useful for seeding a JS wallet picker component.
|
|
11
|
+
#
|
|
12
|
+
# Example (in an ERB layout):
|
|
13
|
+
# <script>
|
|
14
|
+
# window.__SOLANA_WALLETS__ = <%= solana_wallets_json %>;
|
|
15
|
+
# </script>
|
|
16
|
+
sig { returns(String) }
|
|
17
|
+
def solana_wallets_json
|
|
18
|
+
require "json"
|
|
19
|
+
JSON.generate(WalletRegistry.to_json_array)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
module SolanaWalletAdapter
|
|
5
|
+
# Mirrors the WalletReadyState enum from @solana/wallet-adapter-base.
|
|
6
|
+
#
|
|
7
|
+
# +Installed+ – The wallet browser extension / app was detected.
|
|
8
|
+
# +NotDetected+ – No wallet detected in the current environment.
|
|
9
|
+
# +Loadable+ – The wallet is always available (e.g. iOS deep-link redirect).
|
|
10
|
+
# +Unsupported+ – The platform cannot support this wallet (server-side, etc.).
|
|
11
|
+
class WalletReadyState < T::Enum
|
|
12
|
+
enums do
|
|
13
|
+
Installed = new("Installed")
|
|
14
|
+
NotDetected = new("NotDetected")
|
|
15
|
+
Loadable = new("Loadable")
|
|
16
|
+
Unsupported = new("Unsupported")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
module SolanaWalletAdapter
|
|
5
|
+
# A registry that maps wallet names to their adapter classes.
|
|
6
|
+
# Provides a single place to look up all known wallet adapters and to
|
|
7
|
+
# filter them by ready-state for use in frontend JSON serialisation.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# # Register your adapters (typically in an initializer)
|
|
11
|
+
# SolanaWalletAdapter::WalletRegistry.register(
|
|
12
|
+
# SolanaWalletAdapter::Wallets::PhantomWalletAdapter,
|
|
13
|
+
# SolanaWalletAdapter::Wallets::SolflareWalletAdapter,
|
|
14
|
+
# )
|
|
15
|
+
#
|
|
16
|
+
# # Query
|
|
17
|
+
# WalletRegistry.all # => [PhantomWalletAdapter, ...]
|
|
18
|
+
# WalletRegistry.find("Phantom") # => PhantomWalletAdapter
|
|
19
|
+
class WalletRegistry
|
|
20
|
+
extend T::Sig
|
|
21
|
+
|
|
22
|
+
@adapters = T.let(
|
|
23
|
+
{},
|
|
24
|
+
T::Hash[String, T.class_of(BaseWalletAdapter)]
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
extend T::Sig
|
|
29
|
+
|
|
30
|
+
# Register one or more adapter classes.
|
|
31
|
+
sig { params(adapter_classes: T::Array[T.class_of(BaseWalletAdapter)]).void }
|
|
32
|
+
def register(*adapter_classes)
|
|
33
|
+
adapter_classes.each do |klass|
|
|
34
|
+
instance = klass.new
|
|
35
|
+
@adapters[instance.name] = klass
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Return all registered adapter classes in registration order.
|
|
40
|
+
sig { returns(T::Array[T.class_of(BaseWalletAdapter)]) }
|
|
41
|
+
def all
|
|
42
|
+
@adapters.values
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Find an adapter class by wallet name.
|
|
46
|
+
sig { params(name: String).returns(T.nilable(T.class_of(BaseWalletAdapter))) }
|
|
47
|
+
def find(name)
|
|
48
|
+
@adapters[name]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Serialise all adapters to an array of hashes suitable for JSON.
|
|
52
|
+
# Instantiates each adapter once to read metadata.
|
|
53
|
+
sig { returns(T::Array[T::Hash[String, T.untyped]]) }
|
|
54
|
+
def to_json_array
|
|
55
|
+
@adapters.values.map { |klass| klass.new.to_h }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Remove all registered adapters. Useful in tests.
|
|
59
|
+
sig { void }
|
|
60
|
+
def reset!
|
|
61
|
+
@adapters.clear
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
module SolanaWalletAdapter
|
|
5
|
+
module Wallets
|
|
6
|
+
class CoinbaseWalletAdapter < BaseMessageSignerWalletAdapter
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
COINBASE_ICON = T.let(
|
|
10
|
+
"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAyNCIgaGVpZ2h0PSIxMDI0IiB2aWV3Qm94PSIwIDAgMTAyNCAxMDI0IiBm" \
|
|
11
|
+
"aWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8Y2lyY2xlIGN4PSI1MTIiIGN5" \
|
|
12
|
+
"PSI1MTIiIHI9IjUxMiIgZmlsbD0iIzAwNTJGRiIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVs" \
|
|
13
|
+
"ZT0iZXZlbm9kZCIgZD0iTTE1MiA1MTJDMTUyIDcxMC44MjMgMzEzLjE3NyA4NzIgNTEyIDg3MkM3MTAuODIzIDg3" \
|
|
14
|
+
"MiA4NzIgNzEwLjgyMyA4NzIgNTEyQzg3MiAzMTMuMTc3IDcxMC44MjMgMTUyIDUxMiAxNTJDMzEzLjE3NyAxNTIg" \
|
|
15
|
+
"MTUyIDMxMy4xNzcgMTUyIDUxMlpNNDIwIDM5NkM0MDYuNzQ1IDM5NiAzOTYgNDA2Ljc0NSAzOTYgNDIwVjYwNEMz" \
|
|
16
|
+
"OTYgNjE3LjI1NSA0MDYuNzQ1IDYyOCA0MjAgNjI4SDYwNEM2MTcuMjU1IDYyOCA2MjggNjE3LjI1NSA2MjggNjA0" \
|
|
17
|
+
"VjQyMEM2MjggNDA2Ljc0NSA2MTcuMjU1IDM5NiA2MDQgMzk2SDQyMFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=",
|
|
18
|
+
String
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
sig { override.returns(String) }
|
|
22
|
+
def name = "Coinbase Wallet"
|
|
23
|
+
|
|
24
|
+
sig { override.returns(String) }
|
|
25
|
+
def url = "https://chrome.google.com/webstore/detail/coinbase-wallet-extension/hnfanknocfeofbddgcijnmhnfnkdnaad"
|
|
26
|
+
|
|
27
|
+
sig { override.returns(String) }
|
|
28
|
+
def icon = COINBASE_ICON
|
|
29
|
+
|
|
30
|
+
sig { override.returns(WalletReadyState) }
|
|
31
|
+
def ready_state = WalletReadyState::Unsupported
|
|
32
|
+
|
|
33
|
+
sig { override.returns(T.nilable(PublicKey)) }
|
|
34
|
+
def public_key = nil
|
|
35
|
+
|
|
36
|
+
sig { override.returns(T::Boolean) }
|
|
37
|
+
def connecting? = false
|
|
38
|
+
|
|
39
|
+
sig { override.returns(SupportedTransactionVersions) }
|
|
40
|
+
def supported_transaction_versions
|
|
41
|
+
Set[TransactionVersion::Legacy, TransactionVersion::Version0].freeze
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
sig { override.void }
|
|
45
|
+
def connect
|
|
46
|
+
raise WalletNotReadyError, "#{name} connection is initiated in the browser"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
sig { override.void }
|
|
50
|
+
def disconnect; end
|
|
51
|
+
|
|
52
|
+
sig do
|
|
53
|
+
override
|
|
54
|
+
.params(transaction: Transaction, rpc_url: String, options: SendTransactionOptions)
|
|
55
|
+
.returns(String)
|
|
56
|
+
end
|
|
57
|
+
def send_transaction(transaction, rpc_url, options = SendTransactionOptions.new)
|
|
58
|
+
raise WalletNotConnectedError, "send_transaction must use a pre-signed transaction server-side"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
sig { override.params(transaction: Transaction).returns(Transaction) }
|
|
62
|
+
def sign_transaction(transaction)
|
|
63
|
+
raise WalletNotConnectedError, "sign_transaction must be performed client-side"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
sig { override.params(transactions: T::Array[Transaction]).returns(T::Array[Transaction]) }
|
|
67
|
+
def sign_all_transactions(transactions)
|
|
68
|
+
raise WalletNotConnectedError, "sign_all_transactions must be performed client-side"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
sig { override.params(message: String).returns(String) }
|
|
72
|
+
def sign_message(message)
|
|
73
|
+
raise WalletNotConnectedError, "sign_message must be performed client-side"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
module SolanaWalletAdapter
|
|
5
|
+
module Wallets
|
|
6
|
+
class LedgerWalletAdapter < BaseSignerWalletAdapter
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
LEDGER_ICON = T.let(
|
|
10
|
+
"data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMzUgMzUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2Zy" \
|
|
11
|
+
"I+PGcgZmlsbD0iI2ZmZiI+PHBhdGggZD0ibTIzLjU4OCAweC0xNnYyMS41ODNoMjEuNnYtMTZhNS41ODUgNS41OD" \
|
|
12
|
+
"UgMCAwIDAgLTUuNi01LjU4M3oiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDUuNzM5KSIvPjxwYXRoIGQ9Im04LjM0Mi" \
|
|
13
|
+
"AwaC0yLjc1N2E1LjU4NSA1LjU4NSAwIDAgMCAtNS41ODUgNS41ODV2Mi43NTdoOC4zNDJ6Ii8+PHBhdGggZD0ibTAg" \
|
|
14
|
+
"Ny41OWg4LjM0MnY4LjM0MmgtOC4zNDJ6IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDUuNzM5KSIvPjxwYXRoIGQ9" \
|
|
15
|
+
"Im0xNS4xOCAyMy40NTFoMi43NTdhNS41ODUgNS41ODUgMCAwIDAgNS41ODUtNS42di0yLjY3MWgtOC4zNDJ6IiB0" \
|
|
16
|
+
"cmFuc2Zvcm09InRyYW5zbGF0ZSgxMS40NzggMTEuNDc4KSIvPjxwYXRoIGQ9Im03LjU5IDE1LjE4aDguMzQydjgu" \
|
|
17
|
+
"MzQyaC04LjM0MnoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDUuNzM5IDExLjQ3OCkiLz48cGF0aCBkPSJtMCAxNS4x" \
|
|
18
|
+
"OHYyLjc1N2E1LjU4NSA1LjU4NSAwIDAgMCA1LjU4NSA1LjU4NWgyLjc1N3YtOC4zNDJ6IiB0cmFuc2Zvcm09InRy" \
|
|
19
|
+
"YW5zbGF0ZSgwIDExLjQ3OCkiLz48L2c+PC9zdmc+",
|
|
20
|
+
String
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
sig { override.returns(String) }
|
|
24
|
+
def name = "Ledger"
|
|
25
|
+
|
|
26
|
+
sig { override.returns(String) }
|
|
27
|
+
def url = "https://ledger.com"
|
|
28
|
+
|
|
29
|
+
sig { override.returns(String) }
|
|
30
|
+
def icon = LEDGER_ICON
|
|
31
|
+
|
|
32
|
+
sig { override.returns(WalletReadyState) }
|
|
33
|
+
def ready_state = WalletReadyState::Unsupported
|
|
34
|
+
|
|
35
|
+
sig { override.returns(T.nilable(PublicKey)) }
|
|
36
|
+
def public_key = nil
|
|
37
|
+
|
|
38
|
+
sig { override.returns(T::Boolean) }
|
|
39
|
+
def connecting? = false
|
|
40
|
+
|
|
41
|
+
sig { override.returns(SupportedTransactionVersions) }
|
|
42
|
+
def supported_transaction_versions
|
|
43
|
+
Set[TransactionVersion::Legacy, TransactionVersion::Version0].freeze
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
sig { override.void }
|
|
47
|
+
def connect
|
|
48
|
+
raise WalletNotReadyError, "#{name} connection is initiated in the browser via USB/Bluetooth"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
sig { override.void }
|
|
52
|
+
def disconnect; end
|
|
53
|
+
|
|
54
|
+
sig do
|
|
55
|
+
override
|
|
56
|
+
.params(transaction: Transaction, rpc_url: String, options: SendTransactionOptions)
|
|
57
|
+
.returns(String)
|
|
58
|
+
end
|
|
59
|
+
def send_transaction(transaction, rpc_url, options = SendTransactionOptions.new)
|
|
60
|
+
raise WalletNotConnectedError, "send_transaction must use a pre-signed transaction server-side"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
sig { override.params(transaction: Transaction).returns(Transaction) }
|
|
64
|
+
def sign_transaction(transaction)
|
|
65
|
+
raise WalletNotConnectedError, "sign_transaction must be performed client-side by the Ledger device"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
sig { override.params(transactions: T::Array[Transaction]).returns(T::Array[Transaction]) }
|
|
69
|
+
def sign_all_transactions(transactions)
|
|
70
|
+
raise WalletNotConnectedError, "sign_all_transactions must be performed client-side"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
module SolanaWalletAdapter
|
|
5
|
+
module Wallets
|
|
6
|
+
# Server-side metadata adapter for the Phantom wallet.
|
|
7
|
+
#
|
|
8
|
+
# In a Rails app the "connecting" lifecycle is handled in the browser by
|
|
9
|
+
# the Phantom extension / app. This class provides:
|
|
10
|
+
# - Wallet metadata (name, icon, url) for frontend consumption.
|
|
11
|
+
# - A stub implementation of the abstract interface so instances can be
|
|
12
|
+
# registered in WalletRegistry and serialised to JSON.
|
|
13
|
+
# - Optional override points for custom server-side send_transaction /
|
|
14
|
+
# sign_message if you choose to proxy those calls through the server.
|
|
15
|
+
#
|
|
16
|
+
# For a full round-trip flow:
|
|
17
|
+
# 1. Render wallet metadata to the frontend via WalletRegistry.to_json_array.
|
|
18
|
+
# 2. The browser connects to Phantom and obtains a public key + signature.
|
|
19
|
+
# 3. POST both to a Rails controller.
|
|
20
|
+
# 4. Verify server-side with SignatureVerifier.
|
|
21
|
+
class PhantomWalletAdapter < BaseMessageSignerWalletAdapter
|
|
22
|
+
extend T::Sig
|
|
23
|
+
|
|
24
|
+
PHANTOM_ICON = T.let(
|
|
25
|
+
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDgi" \
|
|
26
|
+
"IGhlaWdodD0iMTA4IiB2aWV3Qm94PSIwIDAgMTA4IDEwOCIgZmlsbD0ibm9uZSI+CjxyZWN0IHdpZHRoPSIxMDgi" \
|
|
27
|
+
"IGhlaWdodD0iMTA4IiByeD0iMjYiIGZpbGw9IiNBQjlGRjIiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBj" \
|
|
28
|
+
"bGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik00Ni41MjY3IDY5LjkyMjlDNDIuMDA1NCA3Ni44NTA5IDM0LjQyOTIgODUu" \
|
|
29
|
+
"NjE4MiAyNC4zNDggODUuNjE4MkMxOS41ODI0IDg1LjYxODIgMTUgODMuNjU2MyAxNSA3NS4xMzQyQzE1IDUzLjQz" \
|
|
30
|
+
"MDUgNDQuNjMyNiAxOS44MzI3IDcyLjEyNjggMTkuODMyN0M4Ny43NjggMTkuODMyNyA5NCAzMC42ODQ2IDk0IDQz" \
|
|
31
|
+
"LjAwNzlDOTQgNTguODI1OCA4My43MzU1IDc2LjkxMjIgNzMuNTMyMSA3Ni45MTIyQzcwLjI5MzkgNzYuOTEyMiA2" \
|
|
32
|
+
"OC43MDUzIDc1LjEzNDIgNjguNzA1MyA3Mi4zMTRDNjguNzA1MyA3MS41NzgzIDY4LjgyNzUgNzAuNzgxMiA2OS4w" \
|
|
33
|
+
"NzE5IDY5LjkyMjlDNjUuNTg5MyA3NS44Njk5IDU4Ljg2ODUgODEuMzg3OCA1Mi41NzU0IDgxLjM4NzhDNDcuOTkz" \
|
|
34
|
+
"IDgxLjM4NzggNDUuNjcxMyA3OC41MDYzIDQ1LjY3MTMgNzQuNDU5OEM0NS42NzEzIDcyLjk4ODQgNDUuOTc2OCA3" \
|
|
35
|
+
"MS40NTU2IDQ2LjUyNjcgNjkuOTIyOVpNODMuNjc2MSA0Mi41Nzk0QzgzLjY3NjEgNDYuMTcwNCA4MS41NTc1IDQ3" \
|
|
36
|
+
"Ljk2NTggNzkuMTg3NSA0Ny45NjU4Qzc2Ljc4MTYgNDcuOTY1OCA3NC42OTg5IDQ2LjE3MDQgNzQuNjk4OSA0Mi41" \
|
|
37
|
+
"Nzk0Qzc0LjY5ODkgMzguOTg4NSA3Ni43ODE2IDM3LjE5MzEgNzkuMTg3NSAzNy4xOTMxQzgxLjU1NzUgMzcuMTkz" \
|
|
38
|
+
"MSA4My42NzYxIDM4Ljk4ODUgODMuNjc2MSA0Mi41Nzk0Wk03MC4yMTAzIDQyLjU3OTVDNzAuMjEwMyA0Ni4xNzA0" \
|
|
39
|
+
"IDY4LjA5MTYgNDcuOTY1OCA2NS43MjE2IDQ3Ljk2NThDNjMuMzE1NyA0Ny45NjU4IDYxLjIzMyA0Ni4xNzA0IDYx" \
|
|
40
|
+
"LjIzMyA0Mi41Nzk1QzYxLjIzMyAzOC45ODg1IDYzLjMxNTcgMzcuMTkzMSA2NS43MjE2IDM3LjE5MzFDNjguMDkx" \
|
|
41
|
+
"NiAzNy4xOTMxIDcwLjIxMDMgMzguOTg4NSA3MC4yMTAzIDQyLjU3OTVaIiBmaWxsPSIjRkZGREY4Ii8+Cjwvc3Zn" \
|
|
42
|
+
"Pg==",
|
|
43
|
+
String
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
sig { override.returns(String) }
|
|
47
|
+
def name = "Phantom"
|
|
48
|
+
|
|
49
|
+
sig { override.returns(String) }
|
|
50
|
+
def url = "https://phantom.app"
|
|
51
|
+
|
|
52
|
+
sig { override.returns(String) }
|
|
53
|
+
def icon = PHANTOM_ICON
|
|
54
|
+
|
|
55
|
+
sig { override.returns(WalletReadyState) }
|
|
56
|
+
def ready_state = WalletReadyState::Unsupported
|
|
57
|
+
|
|
58
|
+
sig { override.returns(T.nilable(PublicKey)) }
|
|
59
|
+
def public_key = nil
|
|
60
|
+
|
|
61
|
+
sig { override.returns(T::Boolean) }
|
|
62
|
+
def connecting? = false
|
|
63
|
+
|
|
64
|
+
sig { override.returns(SupportedTransactionVersions) }
|
|
65
|
+
def supported_transaction_versions
|
|
66
|
+
Set[TransactionVersion::Legacy, TransactionVersion::Version0].freeze
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Server-side adapters do not initiate connections directly.
|
|
70
|
+
sig { override.void }
|
|
71
|
+
def connect
|
|
72
|
+
raise WalletNotReadyError,
|
|
73
|
+
"PhantomWalletAdapter is a server-side adapter; connection is initiated in the browser"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
sig { override.void }
|
|
77
|
+
def disconnect
|
|
78
|
+
# no-op on server side
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# send_transaction is inherited from BaseSignerWalletAdapter and will
|
|
82
|
+
# forward raw bytes to the RPC endpoint. Override here if you need
|
|
83
|
+
# custom logic (e.g. a different commitment level for Phantom).
|
|
84
|
+
sig do
|
|
85
|
+
override
|
|
86
|
+
.params(
|
|
87
|
+
transaction: Transaction,
|
|
88
|
+
rpc_url: String,
|
|
89
|
+
options: SendTransactionOptions
|
|
90
|
+
)
|
|
91
|
+
.returns(String)
|
|
92
|
+
end
|
|
93
|
+
def send_transaction(transaction, rpc_url, options = SendTransactionOptions.new)
|
|
94
|
+
raise WalletNotConnectedError,
|
|
95
|
+
"send_transaction must be called with a pre-signed transaction on the server side"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Server-side adapters cannot sign – signing is done in the browser.
|
|
99
|
+
sig { override.params(transaction: Transaction).returns(Transaction) }
|
|
100
|
+
def sign_transaction(transaction)
|
|
101
|
+
raise WalletNotConnectedError,
|
|
102
|
+
"sign_transaction must be performed client-side by the Phantom extension"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
sig { override.params(transactions: T::Array[Transaction]).returns(T::Array[Transaction]) }
|
|
106
|
+
def sign_all_transactions(transactions)
|
|
107
|
+
raise WalletNotConnectedError,
|
|
108
|
+
"sign_all_transactions must be performed client-side by the Phantom extension"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
sig { override.params(message: String).returns(String) }
|
|
112
|
+
def sign_message(message)
|
|
113
|
+
raise WalletNotConnectedError,
|
|
114
|
+
"sign_message must be performed client-side by the Phantom extension"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
module SolanaWalletAdapter
|
|
5
|
+
module Wallets
|
|
6
|
+
class SolflareWalletAdapter < BaseMessageSignerWalletAdapter
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
SOLFLARE_ICON = T.let(
|
|
10
|
+
"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIGlkPSJTIiB4bWxucz0i" \
|
|
11
|
+
"aHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MCA1MCI+PGRlZnM+PHN0eWxlPi5jbHMt" \
|
|
12
|
+
"MXtmaWxsOiMwMjA1MGE7c3Ryb2tlOiNmZmVmNDY7c3Ryb2tlLW1pdGVybGltaXQ6MTA7c3Ryb2tlLXdpZHRoOi41" \
|
|
13
|
+
"cHg7fS5jbHMtMntmaWxsOiNmZmVmNDY7fTwvc3R5bGU+PC9kZWZzPjxyZWN0IGNsYXNzPSJjbHMtMiIgeD0iMCIg" \
|
|
14
|
+
"d2lkdGg9IjUwIiBoZWlnaHQ9IjUwIiByeD0iMTIiIHJ5PSIxMiIvPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTI0" \
|
|
15
|
+
"LjIzLDI2LjQybDIuNDYtMi4zOCw0LjU5LDEuNWMzLjAxLDEsNC41MSwyLjg0LDQuNTEsNS40MywwLDEuOTYtLjc1" \
|
|
16
|
+
"LDMuMjYtMi4yNSw0LjkzbC0uNDYuNS4xNy0xLjE3Yy42Ny00LjI2LS41OC02LjA5LTQuNzItNy40M2wtNC4zLTEu" \
|
|
17
|
+
"MzhoMFpNMTguMDUsMTEuODVsMTIuNTIsNC4xNy0yLjcxLDIuNTktNi41MS0yLjE3Yy0yLjI1LS43NS0zLjAxLTEu" \
|
|
18
|
+
"OTYtMy4zLTQuNTF2LS4wOGgwWk0xNy4zLDMzLjA2bDIuODQtMi43MSw1LjM0LDEuNzVjMi44LjkyLDMuNzYsMi4x" \
|
|
19
|
+
"MywzLjQ2LDUuMThsLTExLjY1LTQuMjJoMFpNMTMuNzEsMjAuOTVjMC0uNzkuNDItMS41NCwxLjEzLTIuMTcuNzUs" \
|
|
20
|
+
"MS4wOSwyLjA1LDIuMDUsNC4wOSwyLjcxbDQuNDIsMS40Ni0yLjQ2LDIuMzgtNC4zNC0xLjQyYy0yLS42Ny0yLjg0" \
|
|
21
|
+
"LTEuNjctMi44NC0yLjk2TTI2LjgyLDQyLjg3YzkuMTgtNi4wOSwxNC4xMS0xMC4yMywxNC4xMS0xNS4zMiwwLTMu" \
|
|
22
|
+
"MzgtMi01LjI2LTYuNDMtNi43MmwtMy4zNC0xLjEzLDkuMTQtOC43Ny0xLjg0LTEuOTYtMi43MSwyLjM4LTEyLjgx" \
|
|
23
|
+
"LTQuMjJjLTMuOTcsMS4yOS04Ljk3LDUuMDktOC45Nyw4Ljg5LDAsLjQyLjA0LjgzLjE3LDEuMjktMy4zLDEuODgt" \
|
|
24
|
+
"NC42MywzLjYzLTQuNjMsNS44LDAsMi4wNSwxLjA5LDQuMDksNC41NSw1LjIybDIuNzUuOTItOS41Miw5LjE0LDEu" \
|
|
25
|
+
"ODQsMS45NiwyLjk2LTIuNzEsMTQuNzMsNS4yMmgwWiIvPjwvc3ZnPg==",
|
|
26
|
+
String
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
sig { override.returns(String) }
|
|
30
|
+
def name = "Solflare"
|
|
31
|
+
|
|
32
|
+
sig { override.returns(String) }
|
|
33
|
+
def url = "https://solflare.com"
|
|
34
|
+
|
|
35
|
+
sig { override.returns(String) }
|
|
36
|
+
def icon = SOLFLARE_ICON
|
|
37
|
+
|
|
38
|
+
sig { override.returns(WalletReadyState) }
|
|
39
|
+
def ready_state = WalletReadyState::Unsupported
|
|
40
|
+
|
|
41
|
+
sig { override.returns(T.nilable(PublicKey)) }
|
|
42
|
+
def public_key = nil
|
|
43
|
+
|
|
44
|
+
sig { override.returns(T::Boolean) }
|
|
45
|
+
def connecting? = false
|
|
46
|
+
|
|
47
|
+
sig { override.returns(SupportedTransactionVersions) }
|
|
48
|
+
def supported_transaction_versions
|
|
49
|
+
Set[TransactionVersion::Legacy, TransactionVersion::Version0].freeze
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
sig { override.void }
|
|
53
|
+
def connect
|
|
54
|
+
raise WalletNotReadyError, "#{name} connection is initiated in the browser"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
sig { override.void }
|
|
58
|
+
def disconnect; end
|
|
59
|
+
|
|
60
|
+
sig do
|
|
61
|
+
override
|
|
62
|
+
.params(transaction: Transaction, rpc_url: String, options: SendTransactionOptions)
|
|
63
|
+
.returns(String)
|
|
64
|
+
end
|
|
65
|
+
def send_transaction(transaction, rpc_url, options = SendTransactionOptions.new)
|
|
66
|
+
raise WalletNotConnectedError, "send_transaction must use a pre-signed transaction server-side"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
sig { override.params(transaction: Transaction).returns(Transaction) }
|
|
70
|
+
def sign_transaction(transaction)
|
|
71
|
+
raise WalletNotConnectedError, "sign_transaction must be performed client-side"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
sig { override.params(transactions: T::Array[Transaction]).returns(T::Array[Transaction]) }
|
|
75
|
+
def sign_all_transactions(transactions)
|
|
76
|
+
raise WalletNotConnectedError, "sign_all_transactions must be performed client-side"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
sig { override.params(message: String).returns(String) }
|
|
80
|
+
def sign_message(message)
|
|
81
|
+
raise WalletNotConnectedError, "sign_message must be performed client-side"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|