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 +4 -4
- data/CHANGELOG.md +14 -0
- data/lib/solana/nonce_account.rb +64 -0
- data/lib/solana/system_program.rb +112 -0
- data/lib/solana_studio.rb +3 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3fca2972e96c5cc005be5505673727eb85a24549f7632f0b1d3d9809ec27411d
|
|
4
|
+
data.tar.gz: 6dc069c098e0b6e664ed5fededf8c15848b73a39d845199357c009acf785298e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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
|