solana-ruby-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/core_extensions/tapioca/name_patch.rb +32 -0
- data/lib/core_extensions/tapioca/required_ancestors.rb +13 -0
- data/lib/generators/solana/ruby/kit/install/install_generator.rb +17 -0
- data/lib/generators/solana/ruby/kit/install/templates/solana_ruby_kit.rb.tt +8 -0
- data/lib/solana/ruby/kit/accounts/account.rb +47 -0
- data/lib/solana/ruby/kit/accounts/maybe_account.rb +86 -0
- data/lib/solana/ruby/kit/accounts.rb +6 -0
- data/lib/solana/ruby/kit/addresses/address.rb +133 -0
- data/lib/solana/ruby/kit/addresses/curve.rb +112 -0
- data/lib/solana/ruby/kit/addresses/program_derived_address.rb +155 -0
- data/lib/solana/ruby/kit/addresses/public_key.rb +39 -0
- data/lib/solana/ruby/kit/addresses.rb +11 -0
- data/lib/solana/ruby/kit/codecs/bytes.rb +58 -0
- data/lib/solana/ruby/kit/codecs/codec.rb +135 -0
- data/lib/solana/ruby/kit/codecs/data_structures.rb +177 -0
- data/lib/solana/ruby/kit/codecs/decoder.rb +43 -0
- data/lib/solana/ruby/kit/codecs/encoder.rb +52 -0
- data/lib/solana/ruby/kit/codecs/numbers.rb +217 -0
- data/lib/solana/ruby/kit/codecs/strings.rb +116 -0
- data/lib/solana/ruby/kit/codecs.rb +25 -0
- data/lib/solana/ruby/kit/configuration.rb +48 -0
- data/lib/solana/ruby/kit/encoding/base58.rb +62 -0
- data/lib/solana/ruby/kit/errors.rb +226 -0
- data/lib/solana/ruby/kit/fast_stable_stringify.rb +62 -0
- data/lib/solana/ruby/kit/functional.rb +29 -0
- data/lib/solana/ruby/kit/instruction_plans/plans.rb +27 -0
- data/lib/solana/ruby/kit/instruction_plans.rb +47 -0
- data/lib/solana/ruby/kit/instructions/accounts.rb +80 -0
- data/lib/solana/ruby/kit/instructions/instruction.rb +71 -0
- data/lib/solana/ruby/kit/instructions/roles.rb +84 -0
- data/lib/solana/ruby/kit/instructions.rb +7 -0
- data/lib/solana/ruby/kit/keys/key_pair.rb +84 -0
- data/lib/solana/ruby/kit/keys/private_key.rb +39 -0
- data/lib/solana/ruby/kit/keys/public_key.rb +31 -0
- data/lib/solana/ruby/kit/keys/signatures.rb +171 -0
- data/lib/solana/ruby/kit/keys.rb +11 -0
- data/lib/solana/ruby/kit/offchain_messages/codec.rb +107 -0
- data/lib/solana/ruby/kit/offchain_messages/message.rb +22 -0
- data/lib/solana/ruby/kit/offchain_messages.rb +16 -0
- data/lib/solana/ruby/kit/options/option.rb +132 -0
- data/lib/solana/ruby/kit/options.rb +5 -0
- data/lib/solana/ruby/kit/plugin_core.rb +58 -0
- data/lib/solana/ruby/kit/programs.rb +42 -0
- data/lib/solana/ruby/kit/promises.rb +85 -0
- data/lib/solana/ruby/kit/railtie.rb +18 -0
- data/lib/solana/ruby/kit/rpc/api/get_account_info.rb +76 -0
- data/lib/solana/ruby/kit/rpc/api/get_balance.rb +41 -0
- data/lib/solana/ruby/kit/rpc/api/get_block_height.rb +29 -0
- data/lib/solana/ruby/kit/rpc/api/get_epoch_info.rb +47 -0
- data/lib/solana/ruby/kit/rpc/api/get_latest_blockhash.rb +52 -0
- data/lib/solana/ruby/kit/rpc/api/get_minimum_balance_for_rent_exemption.rb +29 -0
- data/lib/solana/ruby/kit/rpc/api/get_multiple_accounts.rb +56 -0
- data/lib/solana/ruby/kit/rpc/api/get_program_accounts.rb +60 -0
- data/lib/solana/ruby/kit/rpc/api/get_signature_statuses.rb +56 -0
- data/lib/solana/ruby/kit/rpc/api/get_slot.rb +30 -0
- data/lib/solana/ruby/kit/rpc/api/get_token_account_balance.rb +38 -0
- data/lib/solana/ruby/kit/rpc/api/get_token_accounts_by_owner.rb +48 -0
- data/lib/solana/ruby/kit/rpc/api/get_transaction.rb +36 -0
- data/lib/solana/ruby/kit/rpc/api/get_vote_accounts.rb +62 -0
- data/lib/solana/ruby/kit/rpc/api/is_blockhash_valid.rb +41 -0
- data/lib/solana/ruby/kit/rpc/api/request_airdrop.rb +35 -0
- data/lib/solana/ruby/kit/rpc/api/send_transaction.rb +61 -0
- data/lib/solana/ruby/kit/rpc/api/simulate_transaction.rb +47 -0
- data/lib/solana/ruby/kit/rpc/client.rb +83 -0
- data/lib/solana/ruby/kit/rpc/transport.rb +137 -0
- data/lib/solana/ruby/kit/rpc.rb +13 -0
- data/lib/solana/ruby/kit/rpc_parsed_types/address_lookup_table.rb +33 -0
- data/lib/solana/ruby/kit/rpc_parsed_types/nonce_account.rb +33 -0
- data/lib/solana/ruby/kit/rpc_parsed_types/stake_account.rb +51 -0
- data/lib/solana/ruby/kit/rpc_parsed_types/token_account.rb +52 -0
- data/lib/solana/ruby/kit/rpc_parsed_types/vote_account.rb +38 -0
- data/lib/solana/ruby/kit/rpc_parsed_types.rb +16 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/api/account_notifications.rb +29 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/api/logs_notifications.rb +28 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/api/program_notifications.rb +30 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/api/root_notifications.rb +19 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/api/signature_notifications.rb +28 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/api/slot_notifications.rb +19 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/autopinger.rb +42 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/client.rb +80 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/subscription.rb +58 -0
- data/lib/solana/ruby/kit/rpc_subscriptions/transport.rb +163 -0
- data/lib/solana/ruby/kit/rpc_subscriptions.rb +12 -0
- data/lib/solana/ruby/kit/rpc_types/account_info.rb +53 -0
- data/lib/solana/ruby/kit/rpc_types/cluster_url.rb +56 -0
- data/lib/solana/ruby/kit/rpc_types/commitment.rb +52 -0
- data/lib/solana/ruby/kit/rpc_types/lamports.rb +43 -0
- data/lib/solana/ruby/kit/rpc_types.rb +8 -0
- data/lib/solana/ruby/kit/signers/keypair_signer.rb +126 -0
- data/lib/solana/ruby/kit/signers.rb +5 -0
- data/lib/solana/ruby/kit/subscribable/async_iterable.rb +80 -0
- data/lib/solana/ruby/kit/subscribable/data_publisher.rb +90 -0
- data/lib/solana/ruby/kit/subscribable.rb +13 -0
- data/lib/solana/ruby/kit/sysvars/addresses.rb +19 -0
- data/lib/solana/ruby/kit/sysvars/clock.rb +37 -0
- data/lib/solana/ruby/kit/sysvars/epoch_schedule.rb +34 -0
- data/lib/solana/ruby/kit/sysvars/last_restart_slot.rb +22 -0
- data/lib/solana/ruby/kit/sysvars/rent.rb +29 -0
- data/lib/solana/ruby/kit/sysvars.rb +33 -0
- data/lib/solana/ruby/kit/transaction_confirmation.rb +159 -0
- data/lib/solana/ruby/kit/transaction_messages/transaction_message.rb +168 -0
- data/lib/solana/ruby/kit/transaction_messages.rb +5 -0
- data/lib/solana/ruby/kit/transactions/transaction.rb +135 -0
- data/lib/solana/ruby/kit/transactions.rb +5 -0
- data/lib/solana/ruby/kit/version.rb +10 -0
- data/lib/solana/ruby/kit.rb +100 -0
- data/solana-ruby-kit.gemspec +29 -0
- metadata +311 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 328621bd69f0601b600df302ea5169001983cf56fa44cf23225e3f2515d5d34a
|
|
4
|
+
data.tar.gz: 5e2bc165e53ff6818b5650f5d8551e6ec3331f65df3e656d0bb7ecd588c16fb3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7664963dae246c46ef5d09c7f1d3d05b5c67cecc38dbd961955da8e86cce9a4d0aeff55fcc47425b35826dfe552bb032715f5f178dea8e8c716828013ebb151d
|
|
7
|
+
data.tar.gz: a5ee1ed23cca39029dc7a5e84f6319f06ef3319aaeb62488b12b702c6f7c391b8256ed7c706c140e4242285042a4d5b8c0ce7eb0fa484368a182556aa7ac0b84
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module T
|
|
5
|
+
module Types
|
|
6
|
+
class Simple
|
|
7
|
+
module NamePatch
|
|
8
|
+
NAME_METHOD = T.let(::Module.instance_method(:name), UnboundMethod)
|
|
9
|
+
|
|
10
|
+
def name
|
|
11
|
+
# Sorbet memoizes this method into the `@name` instance variable but
|
|
12
|
+
# doing so means that types get memoized before this patch is applied
|
|
13
|
+
qualified_name_of(@raw_type)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def qualified_name_of(constant)
|
|
17
|
+
name = NAME_METHOD.bind_call(constant)
|
|
18
|
+
name = nil if name&.start_with?("#<")
|
|
19
|
+
return if name.nil?
|
|
20
|
+
|
|
21
|
+
if name.start_with?("::")
|
|
22
|
+
name
|
|
23
|
+
else
|
|
24
|
+
"::#{name}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
prepend NamePatch
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module T
|
|
2
|
+
module Helpers
|
|
3
|
+
prepend(::Module.new do
|
|
4
|
+
def requires_ancestor(&block)
|
|
5
|
+
# We can't directly call the block since the ancestor might not be loaded yet.
|
|
6
|
+
# We save the block in the map and will resolve it later.
|
|
7
|
+
Tapioca::Runtime::Trackers::RequiredAncestor.register(self, block)
|
|
8
|
+
|
|
9
|
+
super
|
|
10
|
+
end
|
|
11
|
+
end)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# typed: ignore
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'rails/generators'
|
|
5
|
+
|
|
6
|
+
module Solana::Ruby::Kit
|
|
7
|
+
module Generators
|
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
|
9
|
+
source_root File.expand_path('templates', __dir__)
|
|
10
|
+
desc 'Creates a SolanaRubyKit initializer in config/initializers'
|
|
11
|
+
|
|
12
|
+
def copy_initializer
|
|
13
|
+
template 'solana_ruby_kit.rb.tt', 'config/initializers/solana_ruby_kit.rb'
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../addresses/address'
|
|
5
|
+
require_relative '../errors'
|
|
6
|
+
|
|
7
|
+
module Solana::Ruby::Kit
|
|
8
|
+
module Accounts
|
|
9
|
+
# The number of bytes required to store the on-chain account header
|
|
10
|
+
# (lamports, owner, executable flag, rent epoch, and padding).
|
|
11
|
+
# Mirrors TypeScript's `BASE_ACCOUNT_SIZE = 128`.
|
|
12
|
+
BASE_ACCOUNT_SIZE = T.let(128, Integer)
|
|
13
|
+
|
|
14
|
+
# All on-chain attributes shared by every Solana account.
|
|
15
|
+
# Mirrors TypeScript's `BaseAccount`.
|
|
16
|
+
class BaseAccount < T::Struct
|
|
17
|
+
# Whether the account holds a program (executable code).
|
|
18
|
+
const :executable, T::Boolean
|
|
19
|
+
# Balance of the account in lamports (1 SOL = 1_000_000_000 lamports).
|
|
20
|
+
# TypeScript uses `bigint`; Ruby uses arbitrary-precision Integer.
|
|
21
|
+
const :lamports, Integer
|
|
22
|
+
# Address of the program that owns this account.
|
|
23
|
+
const :program_address, Addresses::Address
|
|
24
|
+
# Allocated storage in bytes.
|
|
25
|
+
const :space, Integer
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# A fully-populated Solana account including its address and data.
|
|
29
|
+
# Mirrors TypeScript's `Account<TData, TAddress>`.
|
|
30
|
+
#
|
|
31
|
+
# `data` is either:
|
|
32
|
+
# - a binary String (when the account is encoded / raw bytes), or
|
|
33
|
+
# - any Ruby object (when the account has been decoded by a codec).
|
|
34
|
+
class Account < T::Struct
|
|
35
|
+
const :address, Addresses::Address
|
|
36
|
+
const :data, T.untyped # String (binary) or decoded struct
|
|
37
|
+
const :executable, T::Boolean
|
|
38
|
+
const :lamports, Integer
|
|
39
|
+
const :program_address, Addresses::Address
|
|
40
|
+
const :space, Integer
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Convenience alias for an account whose data is still raw bytes.
|
|
44
|
+
# Mirrors TypeScript's `EncodedAccount<TAddress>`.
|
|
45
|
+
EncodedAccount = T.type_alias { Account }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../addresses/address'
|
|
5
|
+
require_relative '../errors'
|
|
6
|
+
require_relative 'account'
|
|
7
|
+
|
|
8
|
+
module Solana::Ruby::Kit
|
|
9
|
+
module Accounts
|
|
10
|
+
extend T::Sig
|
|
11
|
+
# Represents an account that may or may not exist on-chain.
|
|
12
|
+
# Mirrors TypeScript's discriminated union:
|
|
13
|
+
# MaybeAccount<TData, TAddress> = Account<TData, TAddress> & { exists: true }
|
|
14
|
+
# | { address: Address<TAddress>; exists: false }
|
|
15
|
+
#
|
|
16
|
+
# In Ruby we use a single class with an `exists` flag rather than a union
|
|
17
|
+
# type, since Sorbet cannot narrow struct unions at runtime as easily.
|
|
18
|
+
class MaybeAccount < T::Struct
|
|
19
|
+
# true → the account exists; all Account fields are populated.
|
|
20
|
+
# false → the account does not exist; only `address` is set.
|
|
21
|
+
const :exists, T::Boolean
|
|
22
|
+
const :address, Addresses::Address
|
|
23
|
+
# The following fields are only valid when exists == true.
|
|
24
|
+
const :data, T.untyped # nil when exists == false
|
|
25
|
+
const :executable, T.nilable(T::Boolean)
|
|
26
|
+
const :lamports, T.nilable(Integer)
|
|
27
|
+
const :program_address, T.nilable(Addresses::Address)
|
|
28
|
+
const :space, T.nilable(Integer)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
# Builds a MaybeAccount representing a found account.
|
|
34
|
+
sig { params(account: Account).returns(MaybeAccount) }
|
|
35
|
+
def existing_account(account)
|
|
36
|
+
MaybeAccount.new(
|
|
37
|
+
exists: true,
|
|
38
|
+
address: account.address,
|
|
39
|
+
data: account.data,
|
|
40
|
+
executable: account.executable,
|
|
41
|
+
lamports: account.lamports,
|
|
42
|
+
program_address: account.program_address,
|
|
43
|
+
space: account.space
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Builds a MaybeAccount representing a missing account.
|
|
48
|
+
sig { params(address: Addresses::Address).returns(MaybeAccount) }
|
|
49
|
+
def missing_account(address)
|
|
50
|
+
MaybeAccount.new(
|
|
51
|
+
exists: false,
|
|
52
|
+
address: address,
|
|
53
|
+
data: nil,
|
|
54
|
+
executable: nil,
|
|
55
|
+
lamports: nil,
|
|
56
|
+
program_address: nil,
|
|
57
|
+
space: nil
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Raises SolanaError if the account does not exist.
|
|
62
|
+
# Mirrors `assertAccountExists()`.
|
|
63
|
+
sig { params(maybe_account: MaybeAccount).void }
|
|
64
|
+
def assert_account_exists!(maybe_account)
|
|
65
|
+
return if maybe_account.exists
|
|
66
|
+
|
|
67
|
+
Kernel.raise SolanaError.new(
|
|
68
|
+
:SOLANA_ERROR__ACCOUNTS__ACCOUNT_NOT_FOUND,
|
|
69
|
+
address: maybe_account.address.value
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Raises SolanaError listing all missing addresses if any account is absent.
|
|
74
|
+
# Mirrors `assertAccountsExist()`.
|
|
75
|
+
sig { params(maybe_accounts: T::Array[MaybeAccount]).void }
|
|
76
|
+
def assert_accounts_exist!(maybe_accounts)
|
|
77
|
+
missing = maybe_accounts.reject(&:exists).map { |a| a.address.value }
|
|
78
|
+
return if missing.empty?
|
|
79
|
+
|
|
80
|
+
Kernel.raise SolanaError.new(
|
|
81
|
+
:SOLANA_ERROR__ACCOUNTS__ONE_OR_MORE_ACCOUNTS_NOT_FOUND,
|
|
82
|
+
addresses: missing
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../errors'
|
|
5
|
+
require_relative '../encoding/base58'
|
|
6
|
+
|
|
7
|
+
module Solana::Ruby::Kit
|
|
8
|
+
module Addresses
|
|
9
|
+
extend T::Sig
|
|
10
|
+
# A validated, base58-encoded Solana address (32 bytes on-wire).
|
|
11
|
+
# Mirrors the TypeScript branded type:
|
|
12
|
+
# type Address<TAddress extends string = string> = Brand<EncodedString<TAddress, 'base58'>, 'Address'>
|
|
13
|
+
#
|
|
14
|
+
# Because Sorbet does not support nominal/branded string types, Address is a
|
|
15
|
+
# lightweight value object whose string representation is always a validated
|
|
16
|
+
# base58 string.
|
|
17
|
+
class Address
|
|
18
|
+
extend T::Sig
|
|
19
|
+
include Comparable
|
|
20
|
+
|
|
21
|
+
sig { returns(String) }
|
|
22
|
+
attr_reader :value
|
|
23
|
+
|
|
24
|
+
sig { params(value: String).void }
|
|
25
|
+
def initialize(value)
|
|
26
|
+
@value = T.let(value, String)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
sig { returns(String) }
|
|
30
|
+
def to_s = @value
|
|
31
|
+
|
|
32
|
+
sig { returns(String) }
|
|
33
|
+
def inspect = "#<#{self.class} \"#{@value}\">"
|
|
34
|
+
|
|
35
|
+
sig { params(other: T.untyped).returns(T.nilable(Integer)) }
|
|
36
|
+
def <=>(other)
|
|
37
|
+
return nil unless other.is_a?(Address)
|
|
38
|
+
|
|
39
|
+
@value <=> other.value
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
sig { params(other: T.untyped).returns(T::Boolean) }
|
|
43
|
+
def ==(other)
|
|
44
|
+
!!(other.is_a?(Address) && @value == other.value)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
sig { returns(Integer) }
|
|
48
|
+
def hash = @value.hash
|
|
49
|
+
|
|
50
|
+
alias eql? ==
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Constants
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
# Expected byte length of a Solana address.
|
|
58
|
+
ADDRESS_BYTE_LENGTH = T.let(32, Integer)
|
|
59
|
+
|
|
60
|
+
# Minimum / maximum character lengths for a base58-encoded 32-byte address.
|
|
61
|
+
ADDRESS_MIN_STR_LEN = T.let(32, Integer)
|
|
62
|
+
ADDRESS_MAX_STR_LEN = T.let(44, Integer)
|
|
63
|
+
|
|
64
|
+
module_function
|
|
65
|
+
|
|
66
|
+
# Encodes raw bytes (binary String, length == 32) to a base58 address string.
|
|
67
|
+
# Mirrors `getAddressEncoder()` in TypeScript.
|
|
68
|
+
sig { params(bytes: String).returns(String) }
|
|
69
|
+
def encode_address(bytes)
|
|
70
|
+
Kernel.raise SolanaError.new(
|
|
71
|
+
SolanaError::ADDRESSES__INVALID_BYTE_LENGTH_FOR_ADDRESS,
|
|
72
|
+
byte_length: bytes.bytesize
|
|
73
|
+
) unless bytes.bytesize == ADDRESS_BYTE_LENGTH
|
|
74
|
+
|
|
75
|
+
Encoding::Base58.encode(bytes)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Decodes a base58 address string to a 32-byte binary String.
|
|
79
|
+
# Mirrors `getAddressDecoder()` in TypeScript.
|
|
80
|
+
sig { params(addr: Address).returns(String) }
|
|
81
|
+
def decode_address(addr)
|
|
82
|
+
bytes = Encoding::Base58.decode(addr.value)
|
|
83
|
+
Kernel.raise SolanaError.new(
|
|
84
|
+
SolanaError::ADDRESSES__INVALID_BYTE_LENGTH_FOR_ADDRESS,
|
|
85
|
+
byte_length: bytes.bytesize
|
|
86
|
+
) unless bytes.bytesize == ADDRESS_BYTE_LENGTH
|
|
87
|
+
|
|
88
|
+
bytes
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Returns true if the string is a syntactically valid Solana address.
|
|
92
|
+
# Mirrors `isAddress()` in TypeScript.
|
|
93
|
+
sig { params(putative: String).returns(T::Boolean) }
|
|
94
|
+
def address?(putative)
|
|
95
|
+
return false unless putative.length.between?(ADDRESS_MIN_STR_LEN, ADDRESS_MAX_STR_LEN)
|
|
96
|
+
return false unless putative.chars.all? { |c| Encoding::Base58::ALPHABET.include?(c) }
|
|
97
|
+
|
|
98
|
+
bytes = Encoding::Base58.decode(putative)
|
|
99
|
+
bytes.bytesize == ADDRESS_BYTE_LENGTH
|
|
100
|
+
rescue ArgumentError
|
|
101
|
+
false
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Raises SolanaError if the string is not a valid Solana address.
|
|
105
|
+
# Mirrors `assertIsAddress()` in TypeScript.
|
|
106
|
+
sig { params(putative: String).void }
|
|
107
|
+
def assert_address!(putative)
|
|
108
|
+
unless putative.length.between?(ADDRESS_MIN_STR_LEN, ADDRESS_MAX_STR_LEN)
|
|
109
|
+
Kernel.raise SolanaError.new(
|
|
110
|
+
SolanaError::ADDRESSES__STRING_LENGTH_OUT_OF_RANGE,
|
|
111
|
+
actual_length: putative.length
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
Kernel.raise SolanaError.new(SolanaError::ADDRESSES__INVALID_BASE58_ENCODED_ADDRESS) unless address?(putative)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Validates and wraps a string in an Address value object.
|
|
119
|
+
# Mirrors `address()` in TypeScript.
|
|
120
|
+
sig { params(putative: String).returns(Address) }
|
|
121
|
+
def address(putative)
|
|
122
|
+
assert_address!(putative)
|
|
123
|
+
Address.new(putative)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Returns a Proc that compares two Address values lexicographically,
|
|
127
|
+
# matching the semantics of `getAddressComparator()` in TypeScript.
|
|
128
|
+
sig { returns(T.proc.params(arg0: T.untyped, arg1: T.untyped).returns(T.untyped)) }
|
|
129
|
+
def address_comparator
|
|
130
|
+
->(a, b) { a.value <=> b.value }
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative 'address'
|
|
5
|
+
require_relative '../errors'
|
|
6
|
+
|
|
7
|
+
module Solana::Ruby::Kit
|
|
8
|
+
module Addresses
|
|
9
|
+
extend T::Sig
|
|
10
|
+
# OffCurveAddress marks an Address whose 32-byte representation does NOT lie
|
|
11
|
+
# on the Ed25519 curve. PDAs are always off-curve by definition.
|
|
12
|
+
#
|
|
13
|
+
# Mirrors TypeScript:
|
|
14
|
+
# type OffCurveAddress = Brand<Address, AffinePoint>
|
|
15
|
+
class OffCurveAddress < Address; end
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Ed25519 curve parameters (RFC 8032, Section 5.1)
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
# Field prime p = 2^255 − 19
|
|
22
|
+
CURVE_P = T.let(T.unsafe(2**255 - 19), Integer)
|
|
23
|
+
|
|
24
|
+
# Curve constant d = −121665/121666 mod p (computed to avoid oversized literals)
|
|
25
|
+
CURVE_D = T.let((-121665 * 121666.pow(CURVE_P - 2, CURVE_P)) % CURVE_P, Integer)
|
|
26
|
+
|
|
27
|
+
# sqrt(−1) mod p = 2^((p−1)/4) mod p
|
|
28
|
+
CURVE_SQRT_M1 = T.let(2.pow((CURVE_P - 1) / 4, CURVE_P), Integer)
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# Returns true if the 32-byte binary string represents a point on the
|
|
33
|
+
# Ed25519 twisted-Edwards curve.
|
|
34
|
+
#
|
|
35
|
+
# Algorithm follows RFC 8032, Section 5.1.3 (point decompression):
|
|
36
|
+
# 1. Extract y coordinate and the sign bit of x.
|
|
37
|
+
# 2. Compute x² = (y²−1) / (d·y²+1) mod p.
|
|
38
|
+
# 3. Recover x using the curve's square-root formula.
|
|
39
|
+
# 4. If no valid x exists the bytes are off-curve → return false.
|
|
40
|
+
sig { params(bytes: String).returns(T::Boolean) }
|
|
41
|
+
def on_ed25519_curve?(bytes)
|
|
42
|
+
return false unless bytes.bytesize == 32
|
|
43
|
+
|
|
44
|
+
p = CURVE_P
|
|
45
|
+
d = CURVE_D
|
|
46
|
+
|
|
47
|
+
y_arr = bytes.bytes.dup
|
|
48
|
+
x_sign = (y_arr[31] >> 7) & 1
|
|
49
|
+
y_arr[31] &= 0x7f
|
|
50
|
+
|
|
51
|
+
# Little-endian byte array → big integer
|
|
52
|
+
y = y_arr.each_with_index.sum { |b, i| b << (8 * i) }
|
|
53
|
+
return false if y >= p
|
|
54
|
+
|
|
55
|
+
y2 = y.pow(2, p)
|
|
56
|
+
u = (y2 - 1) % p # numerator: y² − 1
|
|
57
|
+
v = (d * y2 + 1) % p # denominator: d·y² + 1
|
|
58
|
+
|
|
59
|
+
# RFC 8032 square-root formula:
|
|
60
|
+
# x = (u·v³) · (u·v⁷)^((p−5)/8) mod p
|
|
61
|
+
v3 = v.pow(3, p)
|
|
62
|
+
v7 = v.pow(7, p)
|
|
63
|
+
exp = (p - 5) / 8
|
|
64
|
+
x = u * v3 % p * (u * v7 % p).pow(exp, p) % p
|
|
65
|
+
vx2 = v * x.pow(2, p) % p
|
|
66
|
+
|
|
67
|
+
if vx2 == u % p
|
|
68
|
+
# Valid root found; adjust sign.
|
|
69
|
+
x = (p - x) % p if (x & 1) != x_sign
|
|
70
|
+
return true
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if vx2 == (p - u) % p
|
|
74
|
+
# x must be multiplied by sqrt(−1).
|
|
75
|
+
x = x * CURVE_SQRT_M1 % p
|
|
76
|
+
x = (p - x) % p if (x & 1) != x_sign
|
|
77
|
+
return true
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
false
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Returns true if the address bytes are NOT on the Ed25519 curve.
|
|
84
|
+
sig { params(bytes: String).returns(T::Boolean) }
|
|
85
|
+
def off_curve_bytes?(bytes)
|
|
86
|
+
!on_ed25519_curve?(bytes)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Type guard — returns true if the given Address is off-curve.
|
|
90
|
+
# Mirrors `isOffCurveAddress()` in TypeScript.
|
|
91
|
+
sig { params(addr: Address).returns(T::Boolean) }
|
|
92
|
+
def off_curve_address?(addr)
|
|
93
|
+
bytes = decode_address(addr)
|
|
94
|
+
off_curve_bytes?(bytes)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Raises SolanaError if the address is on the Ed25519 curve.
|
|
98
|
+
# Mirrors `assertIsOffCurveAddress()` in TypeScript.
|
|
99
|
+
sig { params(addr: Address).void }
|
|
100
|
+
def assert_off_curve_address!(addr)
|
|
101
|
+
Kernel.raise SolanaError.new(SolanaError::ADDRESSES__SEEDS_POINT_ON_CURVE) if on_ed25519_curve?(decode_address(addr))
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Validates and narrows an Address to OffCurveAddress.
|
|
105
|
+
# Mirrors `offCurveAddress()` in TypeScript.
|
|
106
|
+
sig { params(addr: Address).returns(OffCurveAddress) }
|
|
107
|
+
def off_curve_address(addr)
|
|
108
|
+
assert_off_curve_address!(addr)
|
|
109
|
+
OffCurveAddress.new(addr.value)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'digest'
|
|
5
|
+
require_relative 'address'
|
|
6
|
+
require_relative 'curve'
|
|
7
|
+
require_relative '../errors'
|
|
8
|
+
|
|
9
|
+
module Solana::Ruby::Kit
|
|
10
|
+
module Addresses
|
|
11
|
+
extend T::Sig
|
|
12
|
+
# The integer bump seed used when deriving a PDA. Must be in [0, 255].
|
|
13
|
+
# Mirrors TypeScript: `type ProgramDerivedAddressBump = Brand<number, 'ProgramDerivedAddressBump'>`
|
|
14
|
+
ProgramDerivedAddressBump = T.type_alias { Integer }
|
|
15
|
+
|
|
16
|
+
# A PDA is an (address, bump_seed) pair.
|
|
17
|
+
# Mirrors TypeScript: `type ProgramDerivedAddress = [Address, ProgramDerivedAddressBump]`
|
|
18
|
+
class ProgramDerivedAddress < T::Struct
|
|
19
|
+
const :address, Address
|
|
20
|
+
const :bump, Integer # 0–255
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Accepted seed types mirror TypeScript's `Seeds` union:
|
|
24
|
+
# type Seed = ReadonlyUint8Array | string
|
|
25
|
+
# In Ruby, a seed is either a binary String or an Integer Array.
|
|
26
|
+
Seed = T.type_alias { T.any(String, T::Array[Integer]) }
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Constants
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
# Maximum byte length of a single seed.
|
|
33
|
+
MAX_SEED_LENGTH = T.let(32, Integer)
|
|
34
|
+
|
|
35
|
+
# Maximum number of seeds per PDA derivation.
|
|
36
|
+
MAX_SEEDS = T.let(16, Integer)
|
|
37
|
+
|
|
38
|
+
# Marker bytes appended during hashing: UTF-8 "ProgramDerivedAddress".
|
|
39
|
+
PDA_MARKER_BYTES = T.let('ProgramDerivedAddress'.b, String)
|
|
40
|
+
|
|
41
|
+
module_function
|
|
42
|
+
|
|
43
|
+
# Returns true if the value is a well-formed ProgramDerivedAddress.
|
|
44
|
+
# Mirrors `isProgramDerivedAddress()` in TypeScript.
|
|
45
|
+
sig { params(value: T.untyped).returns(T::Boolean) }
|
|
46
|
+
def program_derived_address?(value)
|
|
47
|
+
return false unless value.is_a?(ProgramDerivedAddress)
|
|
48
|
+
return false unless address?(value.address.value)
|
|
49
|
+
|
|
50
|
+
bump = value.bump
|
|
51
|
+
bump.between?(0, 255)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Raises SolanaError if the value is not a valid ProgramDerivedAddress.
|
|
55
|
+
# Mirrors `assertIsProgramDerivedAddress()` in TypeScript.
|
|
56
|
+
sig { params(value: T.untyped).void }
|
|
57
|
+
def assert_program_derived_address!(value)
|
|
58
|
+
Kernel.raise SolanaError.new(SolanaError::ADDRESSES__INVALID_SEEDS_POINT_ON_CURVE) unless value.is_a?(ProgramDerivedAddress)
|
|
59
|
+
Kernel.raise SolanaError.new(SolanaError::ADDRESSES__PDA_BUMP_SEED_OUT_OF_RANGE) unless value.bump.between?(0, 255)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Derives the Program Derived Address for a given program and up to 16 seeds.
|
|
63
|
+
#
|
|
64
|
+
# Searches from bump seed 255 down to 0, returning the first address whose
|
|
65
|
+
# 32-byte SHA-256 hash does NOT lie on the Ed25519 curve.
|
|
66
|
+
#
|
|
67
|
+
# Mirrors `getProgramDerivedAddress()` in TypeScript.
|
|
68
|
+
sig do
|
|
69
|
+
params(
|
|
70
|
+
program_address: Address,
|
|
71
|
+
seeds: T::Array[Seed]
|
|
72
|
+
).returns(ProgramDerivedAddress)
|
|
73
|
+
end
|
|
74
|
+
def get_program_derived_address(program_address:, seeds:)
|
|
75
|
+
if seeds.length > MAX_SEEDS
|
|
76
|
+
Kernel.raise SolanaError.new(SolanaError::ADDRESSES__TOO_MANY_SEEDS)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
seeds.each do |seed|
|
|
80
|
+
seed_bytes = seed_to_bytes(seed)
|
|
81
|
+
if seed_bytes.bytesize > MAX_SEED_LENGTH
|
|
82
|
+
Kernel.raise SolanaError.new(
|
|
83
|
+
SolanaError::ADDRESSES__MAX_SEED_LENGTH_EXCEEDED,
|
|
84
|
+
actual_length: seed_bytes.bytesize
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
program_bytes = decode_address(program_address)
|
|
90
|
+
|
|
91
|
+
255.downto(0) do |bump|
|
|
92
|
+
seed_bytes_list = seeds.map { |s| seed_to_bytes(s) }
|
|
93
|
+
bump_bytes = [bump].pack('C').b
|
|
94
|
+
|
|
95
|
+
# hash_input = seed1 || seed2 || ... || bump || program_address || "ProgramDerivedAddress"
|
|
96
|
+
hash_input = (seed_bytes_list + [bump_bytes, program_bytes, PDA_MARKER_BYTES]).join
|
|
97
|
+
|
|
98
|
+
candidate_bytes = Digest::SHA256.digest(hash_input)
|
|
99
|
+
|
|
100
|
+
next if on_ed25519_curve?(candidate_bytes)
|
|
101
|
+
|
|
102
|
+
candidate_address = Address.new(encode_address(candidate_bytes))
|
|
103
|
+
return ProgramDerivedAddress.new(address: candidate_address, bump: bump)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
Kernel.raise SolanaError.new(SolanaError::ADDRESSES__FAILED_TO_FIND_VIABLE_PDA_BUMP_SEED)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Derives an address from a base address, program address, and an arbitrary seed.
|
|
110
|
+
# Uses SHA-256 of (base_address || seed || program_address).
|
|
111
|
+
#
|
|
112
|
+
# Mirrors `createAddressWithSeed()` in TypeScript.
|
|
113
|
+
sig do
|
|
114
|
+
params(
|
|
115
|
+
base_address: Address,
|
|
116
|
+
program_address: Address,
|
|
117
|
+
seed: String
|
|
118
|
+
).returns(Address)
|
|
119
|
+
end
|
|
120
|
+
def create_address_with_seed(base_address:, program_address:, seed:)
|
|
121
|
+
seed_bytes = seed.b
|
|
122
|
+
|
|
123
|
+
if seed_bytes.bytesize > MAX_SEED_LENGTH
|
|
124
|
+
Kernel.raise SolanaError.new(
|
|
125
|
+
SolanaError::ADDRESSES__MAX_SEED_LENGTH_EXCEEDED,
|
|
126
|
+
actual_length: seed_bytes.bytesize
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
base_bytes = decode_address(base_address)
|
|
131
|
+
program_bytes = decode_address(program_address)
|
|
132
|
+
|
|
133
|
+
# hash_input = base_address || seed || program_address
|
|
134
|
+
hash_input = base_bytes + seed_bytes + program_bytes
|
|
135
|
+
result_bytes = Digest::SHA256.digest(hash_input)
|
|
136
|
+
|
|
137
|
+
Address.new(encode_address(result_bytes))
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
# Private helpers
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
# Converts a Seed (binary String or Integer Array) to a binary String.
|
|
145
|
+
sig { params(seed: Seed).returns(String) }
|
|
146
|
+
def seed_to_bytes(seed)
|
|
147
|
+
case seed
|
|
148
|
+
when String then seed.b
|
|
149
|
+
when Array then seed.pack('C*').b
|
|
150
|
+
else T.absurd(seed)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
private_class_method :seed_to_bytes
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'rbnacl'
|
|
5
|
+
require_relative 'address'
|
|
6
|
+
require_relative '../errors'
|
|
7
|
+
|
|
8
|
+
module Solana::Ruby::Kit
|
|
9
|
+
module Addresses
|
|
10
|
+
extend T::Sig
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# Given an RbNaCl::VerifyKey (Ed25519 public key), returns its Solana Address.
|
|
14
|
+
#
|
|
15
|
+
# Mirrors TypeScript's `getAddressFromPublicKey(publicKey: CryptoKey)`.
|
|
16
|
+
# In TypeScript this is async because it uses Web Crypto API's `exportKey`.
|
|
17
|
+
# In Ruby, RbNaCl exposes the raw bytes directly, so no async overhead is needed.
|
|
18
|
+
sig { params(verify_key: T.untyped).returns(Address) }
|
|
19
|
+
def get_address_from_public_key(verify_key)
|
|
20
|
+
unless verify_key.is_a?(RbNaCl::VerifyKey)
|
|
21
|
+
Kernel.raise SolanaError.new(SolanaError::ADDRESSES__INVALID_ED25519_PUBLIC_KEY)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Address.new(encode_address(verify_key.to_bytes))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Given a Solana Address, returns the corresponding RbNaCl::VerifyKey.
|
|
28
|
+
# Raises SolanaError if the address bytes are not a valid Ed25519 public key.
|
|
29
|
+
#
|
|
30
|
+
# Mirrors TypeScript's `getPublicKeyFromAddress(address: Address)`.
|
|
31
|
+
sig { params(addr: Address).returns(T.untyped) }
|
|
32
|
+
def get_public_key_from_address(addr)
|
|
33
|
+
bytes = decode_address(addr)
|
|
34
|
+
RbNaCl::VerifyKey.new(bytes)
|
|
35
|
+
rescue RangeError, ScriptError => e
|
|
36
|
+
Kernel.raise SolanaError.new(SolanaError::ADDRESSES__INVALID_ED25519_PUBLIC_KEY)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Utilities for generating and validating Solana account addresses.
|
|
5
|
+
# Mirrors the TypeScript package @solana/addresses.
|
|
6
|
+
#
|
|
7
|
+
# Can be used standalone or as part of Solana::Ruby::Kit.
|
|
8
|
+
require_relative 'addresses/address'
|
|
9
|
+
require_relative 'addresses/curve'
|
|
10
|
+
require_relative 'addresses/program_derived_address'
|
|
11
|
+
require_relative 'addresses/public_key'
|