Skip to main content

palinurus_core/
base58.rs

1//! Base58 `Pubkey` newtype — the typed 32-byte Solana address used across the crate.
2//!
3//! A thin wrapper over `bs58` so the rest of `palinurus-core` and the Palinurus
4//! plugins work with typed addresses (`Pubkey`) rather than raw `[u8; 32]` arrays.
5//! No new cryptography: base58 is Bitcoin-alphabet encoding, exactly as Solana
6//! uses it for `Pubkey::to_string` / `Pubkey::from_str`.
7//!
8//! Constants:
9//! - [`Pubkey::SYSTEM`] — the System program (`111…111`).
10//! - [`Pubkey::MEMO`]   — the SPL Memo program v3 (`MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr`).
11//! - [`Pubkey::SAS`]    — the Solana Attestation Service (`22zoJMtdu4tQc2PzL74ZUT7FrwgB1Udec8DdW4yw4BdG`).
12
13use std::fmt;
14use std::str::FromStr;
15
16use borsh::{BorshDeserialize, BorshSerialize};
17
18/// A Solana public key / program id / PDA — 32 bytes, base58-encoded on the wire.
19#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, BorshSerialize, BorshDeserialize)]
20pub struct Pubkey([u8; 32]);
21
22impl fmt::Display for Base58Error {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        match self {
25            Self::Empty => f.write_str("empty base58 string"),
26            Self::InvalidChar => f.write_str("invalid base58 character"),
27            Self::InvalidLength => f.write_str("base58 decoded to a length other than 32 bytes"),
28        }
29    }
30}
31
32impl std::error::Error for Base58Error {}
33
34/// Errors from base58 decoding a string into a [`Pubkey`].
35#[derive(Debug, PartialEq, Eq)]
36pub enum Base58Error {
37    /// The input string was empty.
38    Empty,
39    /// The input contained a character outside the Bitcoin base58 alphabet
40    /// (excludes `0`, `O`, `I`, `l`).
41    InvalidChar,
42    /// The input decoded to a byte stream that is not exactly 32 bytes long.
43    InvalidLength,
44}
45
46impl Pubkey {
47    /// Construct from raw 32 bytes (e.g. a PDA hash output or an account owner).
48    pub const fn from_bytes(b: [u8; 32]) -> Self {
49        Self(b)
50    }
51
52    /// The raw 32 bytes.
53    pub const fn to_bytes(&self) -> [u8; 32] {
54        self.0
55    }
56
57    /// A reference to the inner 32 bytes.
58    pub fn as_bytes(&self) -> &[u8; 32] {
59        &self.0
60    }
61
62    /// The System program (`11111111111111111111111111111111`).
63    pub const SYSTEM: Self = Self([0x00; 32]);
64
65    /// The SPL Memo program v3 (`MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr`).
66    /// Bytes captured from `@solana/spl-memo` `MEMO_PROGRAM_ID` (2026-07-19).
67    pub const MEMO: Self = Self([
68        0x05, 0x4a, 0x53, 0x5a, 0x99, 0x29, 0x21, 0x06, 0x4d, 0x24, 0xe8, 0x71,
69        0x60, 0xda, 0x38, 0x7c, 0x7c, 0x35, 0xb5, 0xdd, 0xbc, 0x92, 0xbb, 0x81,
70        0xe4, 0x1f, 0xa8, 0x40, 0x41, 0x05, 0x44, 0x8d,
71    ]);
72
73    /// The Solana Attestation Service program
74    /// (`22zoJMtdu4tQc2PzL74ZUT7FrwgB1Udec8DdW4yw4BdG`).
75    /// Bytes captured from `@solana/web3.js` (2026-07-19).
76    pub const SAS: Self = Self([
77        0x0f, 0x5e, 0x9e, 0xd5, 0x37, 0x1e, 0x2c, 0x70, 0x89, 0x8c, 0xa9, 0xfd,
78        0x0e, 0x77, 0xc0, 0x06, 0x5c, 0xab, 0x5d, 0xa0, 0x2e, 0x56, 0x67, 0x8b,
79        0x27, 0x13, 0x38, 0x2a, 0xf3, 0x74, 0x59, 0xb7,
80    ]);
81}
82
83impl fmt::Display for Pubkey {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        // bs58 encode with the default Bitcoin alphabet (Solana's).
86        f.write_str(&bs58::encode(self.0).into_string())
87    }
88}
89
90impl fmt::Debug for Pubkey {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        // Debug as the base58 string (readable) tagged `Pubkey(<b58>)`.
93        f.debug_tuple("Pubkey").field(&bs58::encode(self.0).into_string()).finish()
94    }
95}
96
97impl FromStr for Pubkey {
98    type Err = Base58Error;
99    fn from_str(s: &str) -> Result<Self, Self::Err> {
100        if s.is_empty() {
101            return Err(Base58Error::Empty);
102        }
103        // bs58 only fails to decode on invalid characters (length is unconstrained
104        // — it decodes whatever bytes result). The 32-byte length check follows.
105        let bytes = bs58::decode(s).into_vec().map_err(|_| Base58Error::InvalidChar)?;
106        if bytes.len() != 32 {
107            return Err(Base58Error::InvalidLength);
108        }
109        let mut arr = [0u8; 32];
110        arr.copy_from_slice(&bytes);
111        Ok(Self(arr))
112    }
113}
114
115impl From<[u8; 32]> for Pubkey {
116    fn from(b: [u8; 32]) -> Self {
117        Self(b)
118    }
119}
120
121impl From<Pubkey> for [u8; 32] {
122    fn from(pk: Pubkey) -> Self {
123        pk.0
124    }
125}
126
127impl AsRef<[u8; 32]> for Pubkey {
128    fn as_ref(&self) -> &[u8; 32] {
129        &self.0
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn system_constant_is_all_zero() {
139        assert_eq!(Pubkey::SYSTEM.to_bytes(), [0x00; 32]);
140        assert_eq!(Pubkey::SYSTEM.to_string(), "11111111111111111111111111111111");
141    }
142}