Skip to content

Commit

Permalink
Merge pull request #70 from kevinheavey/fix-account-serde
Browse files Browse the repository at this point in the history
Fix account serde
  • Loading branch information
kevinheavey authored Dec 20, 2023
2 parents e48ee73 + b1c601a commit d3e248d
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 51 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ jobs:
options: -v ${{ github.workspace }}:/io -w /io
run: |
apk add py3-pip
pip3 install -U pip pytest pytest-asyncio based58 pybip39 typing-extensions jsonalias
pip3 install ${{ env.name }} --find-links /io/dist/ --force-reinstall --no-index --no-dependencies
pip3 install -U pip pytest pytest-asyncio based58 pybip39 typing-extensions jsonalias --break-system-packages
pip3 install ${{ env.name }} --find-links /io/dist/ --force-reinstall --no-index --no-dependencies --break-system-packages
python3 -m pytest
- name: Upload wheels
uses: actions/upload-artifact@v2
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Changed

- Fix (de)serialization of Account `owner` field [(#70)](https://github.com/kevinheavey/solders/pull/70)
- Use PyO3 v0.19.2 [(#64)](https://github.com/kevinheavey/solders/pull/64)
- Upgrade to Solana 1.17.6 [(#65)](https://github.com/kevinheavey/solders/pull/65)

Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ solana-program = "1.17.6"
bincode = "1.3.3"
base64 = "0.13.0"
serde = "^1.0.188"
serde_bytes = "0.11.12"
serde_json = "^1.0.106"
serde_cbor = "^0.11.2"
derive_more = "0.99.17"
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ lint:
cargo clippy --no-default-features && cargo clippy && ruff python tests && mypy .

fmt:
cargo fmt && ruff format python tests.
cargo fmt && ruff format python tests

serve:
python -m http.server -d docs/_build/html
Expand Down
2 changes: 2 additions & 0 deletions crates/account/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ description = "Solders Account crate."
[dependencies]
pyo3 = { workspace = true, features = ["macros"] }
serde = { workspace = true }
serde_bytes.workspace = true
serde_with.workspace = true
derive_more = { workspace = true }
solana-sdk = { workspace = true }
solders-macros = { workspace = true }
Expand Down
113 changes: 76 additions & 37 deletions crates/account/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
#![allow(clippy::redundant_closure)]
use std::str::FromStr;

use derive_more::{From, Into};
use pyo3::{prelude::*, types::PyBytes};
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr};
use solana_sdk::{account::Account as AccountOriginal, clock::Epoch};
use solders_macros::{common_methods, richcmp_eq_only};
use solders_pubkey::Pubkey;
use solders_traits_core::{
impl_display, py_from_bytes_general_via_bincode, pybytes_general_via_bincode,
RichcmpEqualityOnly,
py_from_bytes_general_via_bincode, pybytes_general_via_bincode, RichcmpEqualityOnly,
};

use solana_account_decoder::{UiAccount, UiAccountData, UiAccountEncoding};
use solders_account_decoder::ParsedAccount;

// The Account from solana_sdk doesn't serialize the owner pubkey as base58,
// so we copy it and change that.
/// An Account with data that is stored on chain.
///
/// Args:
Expand All @@ -24,9 +25,66 @@ use solders_account_decoder::ParsedAccount;
/// executable (bool): Whether this account's data contains a loaded program (and is now read-only). Defaults to False.
/// epoch_info (int): The epoch at which this account will next owe rent. Defaults to 0.
///
#[serde_as]
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Default, Debug)]
#[serde(rename_all = "camelCase")]
#[pyclass(module = "solders.account", subclass)]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Default, From, Into)]
pub struct Account(pub AccountOriginal);
pub struct Account {
/// lamports in the account
#[pyo3(get)]
pub lamports: u64,
/// data held in this account
#[serde(with = "serde_bytes")]
pub data: Vec<u8>,
/// the program that owns this account. If executable, the program that loads this account.
#[serde_as(as = "DisplayFromStr")]
#[pyo3(get)]
pub owner: Pubkey,
/// this account's data contains a loaded program (and is now read-only)
#[pyo3(get)]
pub executable: bool,
/// the epoch at which this account will next owe rent
#[pyo3(get)]
pub rent_epoch: Epoch,
}

impl From<AccountOriginal> for Account {
fn from(value: AccountOriginal) -> Self {
let AccountOriginal {
lamports,
data,
owner,
executable,
rent_epoch,
} = value;
Self {
lamports,
data,
owner: owner.into(),
executable,
rent_epoch,
}
}
}

impl From<Account> for AccountOriginal {
fn from(value: Account) -> Self {
let Account {
lamports,
data,
owner,
executable,
rent_epoch,
} = value;
Self {
lamports,
data,
owner: owner.into(),
executable,
rent_epoch,
}
}
}

#[richcmp_eq_only]
#[common_methods]
Expand All @@ -51,34 +109,10 @@ impl Account {
.into()
}

/// int: Lamports in the account.
#[getter]
pub fn lamports(&self) -> u64 {
self.0.lamports
}

/// bytes: Data held in this account.
#[getter]
pub fn data<'a>(&self, py: Python<'a>) -> &'a PyBytes {
PyBytes::new(py, &self.0.data)
}

/// Pubkey: The program that owns this account. If executable, the program that loads this account.
#[getter]
pub fn owner(&self) -> Pubkey {
self.0.owner.into()
}

/// Whether this account's data contains a loaded program (and is now read-only).
#[getter]
pub fn executable(&self) -> bool {
self.0.executable
}

/// int: The epoch at which this account will next owe rent.
#[getter]
pub fn rent_epoch(&self) -> Epoch {
self.0.rent_epoch
PyBytes::new(py, &self.data)
}

#[staticmethod]
Expand All @@ -93,7 +127,11 @@ impl Account {
}
}

impl_display!(Account);
impl std::fmt::Display for Account {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
pybytes_general_via_bincode!(Account);
py_from_bytes_general_via_bincode!(Account);

Expand All @@ -112,13 +150,12 @@ impl TryFrom<UiAccount> for Account {

impl From<Account> for UiAccount {
fn from(acc: Account) -> Self {
let underlying = acc.0;
Self {
lamports: underlying.lamports,
data: UiAccountData::Binary(base64::encode(underlying.data), UiAccountEncoding::Base64),
owner: underlying.owner.to_string(),
executable: underlying.executable,
rent_epoch: underlying.rent_epoch,
lamports: acc.lamports,
data: UiAccountData::Binary(base64::encode(acc.data), UiAccountEncoding::Base64),
owner: acc.owner.to_string(),
executable: acc.executable,
rent_epoch: acc.rent_epoch,
space: None,
}
}
Expand All @@ -133,6 +170,7 @@ impl From<Account> for UiAccount {
/// executable (bool): Whether this account's data contains a loaded program (and is now read-only). Defaults to False.
/// epoch_info (int): The epoch at which this account will next owe rent. Defaults to 0.
///
#[serde_as]
#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug)]
#[serde(rename_all = "camelCase")]
#[pyclass(module = "solders.account", subclass)]
Expand All @@ -145,6 +183,7 @@ pub struct AccountJSON {
pub data: ParsedAccount,
/// Pubkey: The program that owns this account. If executable, the program that loads this account.
#[pyo3(get)]
#[serde_as(as = "DisplayFromStr")]
pub owner: Pubkey,
/// bool: Whether this account's data contains a loaded program (and is now read-only).
#[pyo3(get)]
Expand Down
12 changes: 8 additions & 4 deletions crates/bankrun/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ use {
ProgramTestContext as ProgramTestContextOriginal,
},
solana_sdk::{
account::AccountSharedData, clock::Clock as ClockOriginal,
commitment_config::CommitmentLevel as CommitmentLevelOriginal, slot_history::Slot,
account::{Account as AccountOriginal, AccountSharedData},
clock::Clock as ClockOriginal,
commitment_config::CommitmentLevel as CommitmentLevelOriginal,
slot_history::Slot,
},
};

Expand Down Expand Up @@ -590,8 +592,10 @@ impl ProgramTestContext {
/// account (Account): The account object to write.
///
pub fn set_account(&mut self, address: &Pubkey, account: Account) {
self.0
.set_account(address.as_ref(), &AccountSharedData::from(account.0));
self.0.set_account(
address.as_ref(),
&AccountSharedData::from(AccountOriginal::from(account)),
);
}

/// Overwrite the clock sysvar.
Expand Down
27 changes: 20 additions & 7 deletions tests/test_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,23 @@ def test_bytes(account: Account) -> None:


def test_pickle(account: Account) -> None:
obj = Account.default()
assert pickle.loads(pickle.dumps(obj)) == obj


def test_json() -> None:
obj = Account.default()
assert Account.from_json(obj.to_json()) == obj
assert pickle.loads(pickle.dumps(account)) == account


def test_json(account: Account) -> None:
assert Account.from_json(account.to_json()) == account

def test_account_from_json() -> None:
# https://github.com/kevinheavey/solders/issues/69
raw = """{
"lamports": 16258560,
"data": "error: data too large for bs58 encoding",
"owner": "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8",
"executable": false,
"rentEpoch": 0,
"space": 2208
}
"""
parsed = Account.from_json(raw)
assert parsed.rent_epoch == 0
assert parsed.data == b"error: data too large for bs58 encoding"
16 changes: 16 additions & 0 deletions tests/test_rpc_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -1191,6 +1191,22 @@ def test_get_program_accounts_without_context() -> None:
pubkey=Pubkey.from_string("CxELquR1gPP8wHe33gZ4QxqGB3sZ9RSwsJ2KshVewkFY"),
)

def test_keyed_account_from_json() -> None:
raw = """{
"pubkey": "7wZpAKYM1uygtosoF42V4a5tVLsrzpSN6Uedaxc6vGrQ",
"account": {
"lamports": 16258560,
"data": "error: data too large for bs58 encoding",
"owner": "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8",
"executable": false,
"rentEpoch": 0,
"space": 2208
}
}
"""
with raises(ValueError, match="Cannot decode JsonParsed here"):
RpcKeyedAccount.from_json(raw)


def test_get_program_accounts_without_context_json_parsed() -> None:
raw = """{
Expand Down

0 comments on commit d3e248d

Please sign in to comment.