solana-studio 0.4.5 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c2ffb18603f4ee37c57d42d010090922d657450172cf5d6623e9d637f1650eb5
4
- data.tar.gz: 1e50dff5950f8dd69dae20590c899930f0e6c4342173cbac93d36b8828554307
3
+ metadata.gz: 3fca2972e96c5cc005be5505673727eb85a24549f7632f0b1d3d9809ec27411d
4
+ data.tar.gz: 6dc069c098e0b6e664ed5fededf8c15848b73a39d845199357c009acf785298e
5
5
  SHA512:
6
- metadata.gz: 41cba2106cced3ae803cb268ed5a5b94d46052ba20045ec50004b40e6f4745443515a015244417d0ab65558d79ff274ceffcaa1a43398d6c11bcdfc7256a3eb6
7
- data.tar.gz: 444c36f828dcc16865f48cd749b5490cf541298f470b8c8f1628875f3d05df33f23e4c1295170772e6f9adb92d9aad3064bd31104b2e5b1c065b06cad4cdf8b0
6
+ metadata.gz: 8186c3bf21e01bda36d230e1c43a4fb775a9e98812cdd87fa3d7c2513c44ac12178f981b083b3566f8e26eb3062daaaba2427ec6f32310ccfa8b3108ab8ab6ec
7
+ data.tar.gz: 67274a43c5f27c1e9765b0ab3ffbd0f871acb22484506bdfdfe74785c02ae4598e702244cf23a90f9ad0e86c08a4bada67ba0e1bbac2d3fbf3cf89907652f376
data/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
4
 
5
+ ## v0.4.6 (2026-06-02)
6
+
7
+ ### Added
8
+ - **`Solana::SystemProgram`** — System-Program instruction encoders for durable nonce support: `create_account` (ix 0), `advance_nonce_account` (4), `withdraw_nonce_account` (5), `initialize_nonce_account` (6), `authorize_nonce_account` (7). Plus constants `RECENT_BLOCKHASHES_SYSVAR`, `RENT_SYSVAR`, `NONCE_ACCOUNT_LENGTH` (80). A durable nonce lets a tx stay valid indefinitely (until consumed) instead of expiring with a ~90s recent blockhash — the canonical pattern for long / async / multi-party signing.
9
+ - **`Solana::NonceAccount.parse(bytes)`** — parses an 80-byte nonce account (version, state, authority, stored nonce, lamports_per_signature) with `initialized?` + `authority?(expected)` guards.
10
+
11
+ ### Tests
12
+ - `test/system_program_test.rb` (8 tests): **byte-match** each encoder against the exact `@solana/web3.js` layout (u32 LE index + fields, account metas + signer flags), nonce-account parse round-trip (init + uninit), and an advance-instruction-into-partial-tx composition check.
13
+
14
+ ## v0.4.5 (2026-06-02)
15
+
16
+ ### Fixed
17
+ - **Fully keyless `serialize_partial`** — a build with zero local `@signers` (every required signature supplied externally) now works: the empty-signers guard fires only when neither a local nor an additional signer is present, the fee payer falls back to the first additional signer, and `@signers.drop(1)` is nil-safe. Enables the no-server-key multi-party signing console. (v0.4.4 began this; v0.4.5 completed the fee-payer/signers fallback.)
18
+
5
19
  ## v0.4.3 (2026-05-27)
6
20
 
7
21
  ### Fixed
@@ -0,0 +1,64 @@
1
+ module Solana
2
+ # Parser for a System-Program durable NONCE account's on-chain data (80 bytes):
3
+ #
4
+ # version u32 (offset 0)
5
+ # state u32 (offset 4) 0 = Uninitialized, 1 = Initialized
6
+ # authority [32] (offset 8)
7
+ # stored_nonce/blockhash [32] (offset 40) ← anchor a tx's recentBlockhash on this
8
+ # fee_calculator u64 (offset 72) lamports_per_signature
9
+ #
10
+ # Used to read the value a durable-nonce-anchored tx must use, and to verify the
11
+ # account is initialized + owned by the expected authority before trusting it.
12
+ class NonceAccount
13
+ UNINITIALIZED = 0
14
+ INITIALIZED = 1
15
+
16
+ attr_reader :version, :state, :authority, :nonce, :lamports_per_signature
17
+
18
+ def initialize(version:, state:, authority:, nonce:, lamports_per_signature:)
19
+ @version = version
20
+ @state = state
21
+ @authority = authority # base58
22
+ @nonce = nonce # base58 — use as the tx recentBlockhash
23
+ @lamports_per_signature = lamports_per_signature
24
+ end
25
+
26
+ # `data` is the raw account bytes (binary). Accepts a base64 string too.
27
+ def self.parse(data)
28
+ bytes = data
29
+ if bytes.is_a?(String) && bytes.encoding != Encoding::ASCII_8BIT
30
+ bytes = bytes.b
31
+ end
32
+ # Tolerate a base64-encoded blob (what getAccountInfo returns in data[0]).
33
+ if bytes.bytesize != NonceLength && looks_base64?(bytes)
34
+ require "base64"
35
+ bytes = Base64.decode64(bytes).b
36
+ end
37
+ raise ArgumentError, "nonce account too small (#{bytes.bytesize} bytes, need >= 80)" if bytes.bytesize < 80
38
+
39
+ new(
40
+ version: bytes[0, 4].unpack1("V"),
41
+ state: bytes[4, 4].unpack1("V"),
42
+ authority: Keypair.encode_base58(bytes.byteslice(8, 32)),
43
+ nonce: Keypair.encode_base58(bytes.byteslice(40, 32)),
44
+ lamports_per_signature: bytes[72, 8].unpack1("Q<")
45
+ )
46
+ end
47
+
48
+ NonceLength = 80
49
+
50
+ def self.looks_base64?(str)
51
+ str.bytesize > 80 && str.match?(%r{\A[A-Za-z0-9+/=\r\n]+\z})
52
+ end
53
+
54
+ def initialized?
55
+ state == INITIALIZED
56
+ end
57
+
58
+ # True when the account is initialized AND its authority matches `expected`
59
+ # (base58). The guard before anchoring any tx on this nonce.
60
+ def authority?(expected)
61
+ initialized? && authority == expected.to_s
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,112 @@
1
+ module Solana
2
+ # System Program instruction encoders — the subset needed for DURABLE NONCE
3
+ # accounts (plus CreateAccount, which nonce creation needs). Mirrors the
4
+ # SplToken encoder pattern: each method returns a { program_id, accounts, data }
5
+ # hash for Transaction#add_instruction.
6
+ #
7
+ # A durable nonce lets a transaction stay valid INDEFINITELY (until consumed)
8
+ # instead of expiring with a recent blockhash (~90s) — the canonical pattern
9
+ # for long / async / multi-party signing. A nonce-anchored tx sets
10
+ # recentBlockhash = the account's stored nonce and MUST carry advance_nonce_account
11
+ # as its FIRST instruction, signed by the nonce authority.
12
+ #
13
+ # Instruction data is `u32 LE index` + fields (the System Program's bincode
14
+ # layout). Indices: CreateAccount 0, AdvanceNonceAccount 4, WithdrawNonceAccount 5,
15
+ # InitializeNonceAccount 6, AuthorizeNonceAccount 7. Each encoder is byte-match
16
+ # tested against a known-good @solana/web3.js reference before being trusted.
17
+ module SystemProgram
18
+ module_function
19
+
20
+ PROGRAM_ID = Transaction::SYSTEM_PROGRAM_ID # 32 zero bytes
21
+ RECENT_BLOCKHASHES_SYSVAR = Keypair.decode_base58("SysvarRecentB1ockHashes11111111111111111111")
22
+ RENT_SYSVAR = Transaction::SYSVAR_RENT_PUBKEY
23
+ NONCE_ACCOUNT_LENGTH = 80
24
+
25
+ # CreateAccount (ix 0): fund + allocate `space` bytes owned by `owner`.
26
+ # Both `from` (payer) and `new_account` must sign.
27
+ def create_account(from:, new_account:, lamports:, space:, owner:)
28
+ data = u32(0) + u64(lamports) + u64(space) + normalize(owner)
29
+ {
30
+ program_id: PROGRAM_ID,
31
+ accounts: [
32
+ { pubkey: normalize(from), is_signer: true, is_writable: true },
33
+ { pubkey: normalize(new_account), is_signer: true, is_writable: true }
34
+ ],
35
+ data: data
36
+ }
37
+ end
38
+
39
+ # AdvanceNonceAccount (ix 4): MUST be the first instruction of any tx anchored
40
+ # on this nonce. The `authority` signs it.
41
+ def advance_nonce_account(nonce:, authority:)
42
+ {
43
+ program_id: PROGRAM_ID,
44
+ accounts: [
45
+ { pubkey: normalize(nonce), is_signer: false, is_writable: true },
46
+ { pubkey: RECENT_BLOCKHASHES_SYSVAR, is_signer: false, is_writable: false },
47
+ { pubkey: normalize(authority), is_signer: true, is_writable: false }
48
+ ],
49
+ data: u32(4)
50
+ }
51
+ end
52
+
53
+ # WithdrawNonceAccount (ix 5): reclaim lamports from the nonce account.
54
+ def withdraw_nonce_account(nonce:, to:, authority:, lamports:)
55
+ {
56
+ program_id: PROGRAM_ID,
57
+ accounts: [
58
+ { pubkey: normalize(nonce), is_signer: false, is_writable: true },
59
+ { pubkey: normalize(to), is_signer: false, is_writable: true },
60
+ { pubkey: RECENT_BLOCKHASHES_SYSVAR, is_signer: false, is_writable: false },
61
+ { pubkey: RENT_SYSVAR, is_signer: false, is_writable: false },
62
+ { pubkey: normalize(authority), is_signer: true, is_writable: false }
63
+ ],
64
+ data: u32(5) + u64(lamports)
65
+ }
66
+ end
67
+
68
+ # InitializeNonceAccount (ix 6): turn a freshly-created account into a nonce
69
+ # account owned by `authority`. Paired with create_account in one tx.
70
+ def initialize_nonce_account(nonce:, authority:)
71
+ {
72
+ program_id: PROGRAM_ID,
73
+ accounts: [
74
+ { pubkey: normalize(nonce), is_signer: false, is_writable: true },
75
+ { pubkey: RECENT_BLOCKHASHES_SYSVAR, is_signer: false, is_writable: false },
76
+ { pubkey: RENT_SYSVAR, is_signer: false, is_writable: false }
77
+ ],
78
+ data: u32(6) + normalize(authority)
79
+ }
80
+ end
81
+
82
+ # AuthorizeNonceAccount (ix 7): rotate the nonce authority. Current authority signs.
83
+ def authorize_nonce_account(nonce:, authority:, new_authority:)
84
+ {
85
+ program_id: PROGRAM_ID,
86
+ accounts: [
87
+ { pubkey: normalize(nonce), is_signer: false, is_writable: true },
88
+ { pubkey: normalize(authority), is_signer: true, is_writable: false }
89
+ ],
90
+ data: u32(7) + normalize(new_authority)
91
+ }
92
+ end
93
+
94
+ # --- helpers --------------------------------------------------------------
95
+
96
+ def u32(n) = [n].pack("V")
97
+ def u64(n) = [n].pack("Q<")
98
+
99
+ # base58 string / Keypair / 32-byte binary → 32-byte binary.
100
+ def normalize(value)
101
+ if value.is_a?(Keypair)
102
+ value.public_key_bytes
103
+ elsif value.is_a?(String) && value.bytesize == 32
104
+ value.b
105
+ elsif value.is_a?(String)
106
+ Keypair.decode_base58(value)
107
+ else
108
+ value
109
+ end
110
+ end
111
+ end
112
+ end
data/lib/solana_studio.rb CHANGED
@@ -3,8 +3,10 @@ require_relative "solana/borsh"
3
3
  require_relative "solana/client"
4
4
  require_relative "solana/transaction"
5
5
  require_relative "solana/spl_token"
6
+ require_relative "solana/system_program"
7
+ require_relative "solana/nonce_account"
6
8
  require_relative "solana/auth_verifier"
7
9
 
8
10
  module SolanaStudio
9
- VERSION = "0.4.5"
11
+ VERSION = "0.4.6"
10
12
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solana-studio
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.5
4
+ version: 0.4.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
@@ -42,7 +42,9 @@ files:
42
42
  - lib/solana/borsh.rb
43
43
  - lib/solana/client.rb
44
44
  - lib/solana/keypair.rb
45
+ - lib/solana/nonce_account.rb
45
46
  - lib/solana/spl_token.rb
47
+ - lib/solana/system_program.rb
46
48
  - lib/solana/transaction.rb
47
49
  - lib/solana_studio.rb
48
50
  homepage: https://github.com/amcritchie/solana-studio