Module: Solana::Ruby::Kit::Addresses

Extended by:
T::Sig
Defined in:
lib/solana/ruby/kit/addresses/curve.rb,
lib/solana/ruby/kit/addresses/address.rb,
lib/solana/ruby/kit/addresses/public_key.rb,
lib/solana/ruby/kit/addresses/program_derived_address.rb

Defined Under Namespace

Classes: Address, OffCurveAddress, ProgramDerivedAddress

Constant Summary collapse

CURVE_P =

Field prime p = 2^255 − 19

T.let(T.unsafe(2**255 - 19), Integer)
CURVE_D =

Curve constant d = −121665/121666 mod p (computed to avoid oversized literals)

T.let((-121665 * 121666.pow(CURVE_P - 2, CURVE_P)) % CURVE_P, Integer)
CURVE_SQRT_M1 =

sqrt(−1) mod p = 2^((p−1)/4) mod p

T.let(2.pow((CURVE_P - 1) / 4, CURVE_P), Integer)
ADDRESS_BYTE_LENGTH =

Expected byte length of a Solana address.

T.let(32, Integer)
ADDRESS_MIN_STR_LEN =

Minimum / maximum character lengths for a base58-encoded 32-byte address.

T.let(32, Integer)
ADDRESS_MAX_STR_LEN =
T.let(44, Integer)
ProgramDerivedAddressBump =

The integer bump seed used when deriving a PDA. Must be in [0, 255]. Mirrors TypeScript: ‘type ProgramDerivedAddressBump = Brand<number, ’ProgramDerivedAddressBump’>‘

T.type_alias { Integer }
Seed =

Accepted seed types mirror TypeScript’s Seeds union:

type Seed = ReadonlyUint8Array | string

In Ruby, a seed is either a binary String or an Integer Array.

T.type_alias { T.any(String, T::Array[Integer]) }
MAX_SEED_LENGTH =

Maximum byte length of a single seed.

T.let(32, Integer)
MAX_SEEDS =

Maximum number of seeds per PDA derivation.

T.let(16, Integer)
PDA_MARKER_BYTES =

Marker bytes appended during hashing: UTF-8 “ProgramDerivedAddress”.

T.let('ProgramDerivedAddress'.b, String)

Class Method Summary collapse

Class Method Details

.address(putative) ⇒ Object



121
122
123
124
# File 'lib/solana/ruby/kit/addresses/address.rb', line 121

def address(putative)
  assert_address!(putative)
  Address.new(putative)
end

.address?(putative) ⇒ Boolean

Returns:

  • (Boolean)


94
95
96
97
98
99
100
101
102
# File 'lib/solana/ruby/kit/addresses/address.rb', line 94

def address?(putative)
  return false unless putative.length.between?(ADDRESS_MIN_STR_LEN, ADDRESS_MAX_STR_LEN)
  return false unless putative.chars.all? { |c| Encoding::Base58::ALPHABET.include?(c) }

  bytes = Encoding::Base58.decode(putative)
  bytes.bytesize == ADDRESS_BYTE_LENGTH
rescue ArgumentError
  false
end

.address_comparatorObject



129
130
131
# File 'lib/solana/ruby/kit/addresses/address.rb', line 129

def address_comparator
  ->(a, b) { a.value <=> b.value }
end

.assert_address!(putative) ⇒ Object



107
108
109
110
111
112
113
114
115
116
# File 'lib/solana/ruby/kit/addresses/address.rb', line 107

def assert_address!(putative)
  unless putative.length.between?(ADDRESS_MIN_STR_LEN, ADDRESS_MAX_STR_LEN)
    Kernel.raise SolanaError.new(
      SolanaError::ADDRESSES__STRING_LENGTH_OUT_OF_RANGE,
      actual_length: putative.length
    )
  end

  Kernel.raise SolanaError.new(SolanaError::ADDRESSES__INVALID_BASE58_ENCODED_ADDRESS) unless address?(putative)
end

.assert_off_curve_address!(addr) ⇒ Object



100
101
102
# File 'lib/solana/ruby/kit/addresses/curve.rb', line 100

def assert_off_curve_address!(addr)
  Kernel.raise SolanaError.new(SolanaError::ADDRESSES__SEEDS_POINT_ON_CURVE) if on_ed25519_curve?(decode_address(addr))
end

.assert_program_derived_address!(value) ⇒ Object



57
58
59
60
# File 'lib/solana/ruby/kit/addresses/program_derived_address.rb', line 57

def assert_program_derived_address!(value)
  Kernel.raise SolanaError.new(SolanaError::ADDRESSES__INVALID_SEEDS_POINT_ON_CURVE) unless value.is_a?(ProgramDerivedAddress)
  Kernel.raise SolanaError.new(SolanaError::ADDRESSES__PDA_BUMP_SEED_OUT_OF_RANGE) unless value.bump.between?(0, 255)
end

.create_address_with_seed(base_address:, program_address:, seed:) ⇒ Object



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/solana/ruby/kit/addresses/program_derived_address.rb', line 120

def create_address_with_seed(base_address:, program_address:, seed:)
  seed_bytes = seed.b

  if seed_bytes.bytesize > MAX_SEED_LENGTH
    Kernel.raise SolanaError.new(
      SolanaError::ADDRESSES__MAX_SEED_LENGTH_EXCEEDED,
      actual_length: seed_bytes.bytesize
    )
  end

  base_bytes    = decode_address(base_address)
  program_bytes = decode_address(program_address)

  # hash_input = base_address || seed || program_address
  hash_input   = base_bytes + seed_bytes + program_bytes
  result_bytes = Digest::SHA256.digest(hash_input)

  Address.new(encode_address(result_bytes))
end

.decode_address(addr) ⇒ Object



81
82
83
84
85
86
87
88
89
# File 'lib/solana/ruby/kit/addresses/address.rb', line 81

def decode_address(addr)
  bytes = Encoding::Base58.decode(addr.value)
  Kernel.raise SolanaError.new(
    SolanaError::ADDRESSES__INVALID_BYTE_LENGTH_FOR_ADDRESS,
    byte_length: bytes.bytesize
  ) unless bytes.bytesize == ADDRESS_BYTE_LENGTH

  bytes
end

.encode_address(bytes) ⇒ Object



69
70
71
72
73
74
75
76
# File 'lib/solana/ruby/kit/addresses/address.rb', line 69

def encode_address(bytes)
  Kernel.raise SolanaError.new(
    SolanaError::ADDRESSES__INVALID_BYTE_LENGTH_FOR_ADDRESS,
    byte_length: bytes.bytesize
  ) unless bytes.bytesize == ADDRESS_BYTE_LENGTH

  Encoding::Base58.encode(bytes)
end

.get_address_from_public_key(verify_key) ⇒ Object



19
20
21
22
23
24
25
# File 'lib/solana/ruby/kit/addresses/public_key.rb', line 19

def get_address_from_public_key(verify_key)
  unless verify_key.is_a?(RbNaCl::VerifyKey)
    Kernel.raise SolanaError.new(SolanaError::ADDRESSES__INVALID_ED25519_PUBLIC_KEY)
  end

  Address.new(encode_address(verify_key.to_bytes))
end

.get_program_derived_address(program_address:, seeds:) ⇒ Object



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/solana/ruby/kit/addresses/program_derived_address.rb', line 74

def get_program_derived_address(program_address:, seeds:)
  if seeds.length > MAX_SEEDS
    Kernel.raise SolanaError.new(SolanaError::ADDRESSES__TOO_MANY_SEEDS)
  end

  seeds.each do |seed|
    seed_bytes = seed_to_bytes(seed)
    if seed_bytes.bytesize > MAX_SEED_LENGTH
      Kernel.raise SolanaError.new(
        SolanaError::ADDRESSES__MAX_SEED_LENGTH_EXCEEDED,
        actual_length: seed_bytes.bytesize
      )
    end
  end

  program_bytes = decode_address(program_address)

  255.downto(0) do |bump|
    seed_bytes_list = seeds.map { |s| seed_to_bytes(s) }
    bump_bytes       = [bump].pack('C').b

    # hash_input = seed1 || seed2 || ... || bump || program_address || "ProgramDerivedAddress"
    hash_input = (seed_bytes_list + [bump_bytes, program_bytes, PDA_MARKER_BYTES]).join

    candidate_bytes = Digest::SHA256.digest(hash_input)

    next if on_ed25519_curve?(candidate_bytes)

    candidate_address = Address.new(encode_address(candidate_bytes))
    return ProgramDerivedAddress.new(address: candidate_address, bump: bump)
  end

  Kernel.raise SolanaError.new(SolanaError::ADDRESSES__FAILED_TO_FIND_VIABLE_PDA_BUMP_SEED)
end

.get_public_key_from_address(addr) ⇒ Object



32
33
34
35
36
37
# File 'lib/solana/ruby/kit/addresses/public_key.rb', line 32

def get_public_key_from_address(addr)
  bytes = decode_address(addr)
  RbNaCl::VerifyKey.new(bytes)
rescue RangeError, ScriptError => e
  Kernel.raise SolanaError.new(SolanaError::ADDRESSES__INVALID_ED25519_PUBLIC_KEY)
end

.off_curve_address(addr) ⇒ Object



107
108
109
110
# File 'lib/solana/ruby/kit/addresses/curve.rb', line 107

def off_curve_address(addr)
  assert_off_curve_address!(addr)
  OffCurveAddress.new(addr.value)
end

.off_curve_address?(addr) ⇒ Boolean

Returns:

  • (Boolean)


92
93
94
95
# File 'lib/solana/ruby/kit/addresses/curve.rb', line 92

def off_curve_address?(addr)
  bytes = decode_address(addr)
  off_curve_bytes?(bytes)
end

.off_curve_bytes?(bytes) ⇒ Boolean

Returns:

  • (Boolean)


85
86
87
# File 'lib/solana/ruby/kit/addresses/curve.rb', line 85

def off_curve_bytes?(bytes)
  !on_ed25519_curve?(bytes)
end

.on_ed25519_curve?(bytes) ⇒ Boolean

Returns:

  • (Boolean)


41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/solana/ruby/kit/addresses/curve.rb', line 41

def on_ed25519_curve?(bytes)
  return false unless bytes.bytesize == 32

  p = CURVE_P
  d = CURVE_D

  y_arr = bytes.bytes.dup
  x_sign = (y_arr[31] >> 7) & 1
  y_arr[31] &= 0x7f

  # Little-endian byte array → big integer
  y = y_arr.each_with_index.sum { |b, i| b << (8 * i) }
  return false if y >= p

  y2 = y.pow(2, p)
  u  = (y2 - 1) % p       # numerator:   y² − 1
  v  = (d * y2 + 1) % p   # denominator: d·y² + 1

  # RFC 8032 square-root formula:
  #   x = (u·v³) · (u·v⁷)^((p−5)/8)  mod p
  v3   = v.pow(3, p)
  v7   = v.pow(7, p)
  exp  = (p - 5) / 8
  x    = u * v3 % p * (u * v7 % p).pow(exp, p) % p
  vx2  = v * x.pow(2, p) % p

  if vx2 == u % p
    # Valid root found; adjust sign.
    x = (p - x) % p if (x & 1) != x_sign
    return true
  end

  if vx2 == (p - u) % p
    # x must be multiplied by sqrt(−1).
    x = x * CURVE_SQRT_M1 % p
    x = (p - x) % p if (x & 1) != x_sign
    return true
  end

  false
end

.program_derived_address?(value) ⇒ Boolean

Returns:

  • (Boolean)


46
47
48
49
50
51
52
# File 'lib/solana/ruby/kit/addresses/program_derived_address.rb', line 46

def program_derived_address?(value)
  return false unless value.is_a?(ProgramDerivedAddress)
  return false unless address?(value.address.value)

  bump = value.bump
  bump.between?(0, 255)
end