Browse Source

Add fetch function helpers to accounts page (#299)

* Add fetch function helpers to accounts page

* Move fetch types to shared page

* Spelling fix

* Add changeset and fix an issue with sharedPage not being rendered

* Final tweaks

* Run tests

* Only render shared page when needed
Will 10 months ago
parent
commit
459492ca47

+ 5 - 0
.changeset/violet-worms-move.md

@@ -0,0 +1,5 @@
+---
+'@codama/renderers-rust': patch
+---
+
+Add account fetching helper functions to rust client

+ 63 - 0
packages/renderers-rust/e2e/system/src/generated/accounts/nonce.rs

@@ -50,6 +50,69 @@ impl<'a> TryFrom<&solana_program::account_info::AccountInfo<'a>> for Nonce {
     }
     }
 }
 }
 
 
+#[cfg(feature = "fetch")]
+pub fn fetch_nonce(
+    rpc: &solana_client::rpc_client::RpcClient,
+    address: &Pubkey,
+) -> Result<super::DecodedAccount<Nonce>, Error> {
+    let accounts = fetch_all_nonce(rpc, vec![address])?;
+    Ok(accounts[0].clone())
+}
+
+#[cfg(feature = "fetch")]
+pub fn fetch_all_nonce(
+    rpc: &solana_client::rpc_client::RpcClient,
+    addresses: Vec<Pubkey>,
+) -> Result<Vec<super::DecodedAccount<Nonce>>, Error> {
+    let accounts = rpc.get_multiple_accounts(&addresses)?;
+    let mut decoded_accounts: Vec<super::DecodedAccount<Nonce>> = Vec::new();
+    for i in 0..addresses.len() {
+        let address = addresses[i];
+        let account = accounts[i]
+            .as_ref()
+            .ok_or(format!("Account not found: {}", address))?;
+        let data = Nonce::from_bytes(&account.data)?;
+        decoded_accounts.push(super::DecodedAccount {
+            address,
+            account: account.clone(),
+            data,
+        });
+    }
+    Ok(decoded_accounts)
+}
+
+#[cfg(feature = "fetch")]
+pub fn fetch_maybe_nonce(
+    rpc: &solana_client::rpc_client::RpcClient,
+    address: &Pubkey,
+) -> Result<super::MaybeAccount<Nonce>, Error> {
+    let accounts = fetch_all_maybe_nonce(rpc, vec![address])?;
+    Ok(accounts[0].clone())
+}
+
+#[cfg(feature = "fetch")]
+pub fn fetch_all_maybe_nonce(
+    rpc: &solana_client::rpc_client::RpcClient,
+    addresses: Vec<Pubkey>,
+) -> Result<Vec<super::MaybeAccount<Nonce>>, Error> {
+    let accounts = rpc.get_multiple_accounts(&addresses)?;
+    let mut decoded_accounts: Vec<super::MaybeAccount<Nonce>> = Vec::new();
+    for i in 0..addresses.len() {
+        let address = addresses[i];
+        if let Some(account) = accounts[i].as_ref() {
+            let data = Nonce::from_bytes(&account.data)?;
+            decoded_accounts.push(super::MaybeAccount::Exists(super::DecodedAccount {
+                address,
+                account: account.clone(),
+                data,
+            }));
+        } else {
+            decoded_accounts.push(super::MaybeAccount::NotFound(address));
+        }
+    }
+    Ok(decoded_accounts)
+}
+
 #[cfg(feature = "anchor")]
 #[cfg(feature = "anchor")]
 impl anchor_lang::AccountDeserialize for Nonce {
 impl anchor_lang::AccountDeserialize for Nonce {
     fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
     fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result<Self> {

+ 1 - 0
packages/renderers-rust/e2e/system/src/generated/mod.rs

@@ -9,6 +9,7 @@ pub mod accounts;
 pub mod errors;
 pub mod errors;
 pub mod instructions;
 pub mod instructions;
 pub mod programs;
 pub mod programs;
+pub mod shared;
 pub mod types;
 pub mod types;
 
 
 pub(crate) use programs::*;
 pub(crate) use programs::*;

+ 21 - 0
packages/renderers-rust/e2e/system/src/generated/shared.rs

@@ -0,0 +1,21 @@
+//! This code was AUTOGENERATED using the codama library.
+//! Please DO NOT EDIT THIS FILE, instead use visitors
+//! to add features, then rerun codama to update it.
+//!
+//! <https://github.com/codama-idl/codama>
+//!
+
+#[cfg(feature = "fetch")]
+#[derive(Debug, Clone)]
+pub struct DecodedAccount<T> {
+    pub address: solana_program::pubkey::Pubkey,
+    pub account: solana_sdk::account::Account,
+    pub data: T,
+}
+
+#[cfg(feature = "fetch")]
+#[derive(Debug, Clone)]
+pub enum MaybeAccount<T> {
+    Exists(DecodedAccount<T>),
+    NotFound(solana_program::pubkey::Pubkey),
+}

+ 53 - 0
packages/renderers-rust/public/templates/accountsPage.njk

@@ -128,6 +128,59 @@ impl<'a> TryFrom<&solana_program::account_info::AccountInfo<'a>> for {{ account.
   }
   }
 }
 }
 
 
+#[cfg(feature = "fetch")]
+pub fn fetch_{{ account.name | snakeCase }}(
+  rpc: &solana_client::rpc_client::RpcClient,
+  address: &Pubkey,
+) -> Result<super::DecodedAccount<{{ account.name | pascalCase }}>, Error> {
+  let accounts = fetch_all_{{ account.name | snakeCase }}(rpc, vec![address])?;
+  Ok(accounts[0].clone())
+}
+
+#[cfg(feature = "fetch")]
+pub fn fetch_all_{{ account.name | snakeCase }}(
+  rpc: &solana_client::rpc_client::RpcClient,
+  addresses: Vec<Pubkey>,
+) -> Result<Vec<super::DecodedAccount<{{ account.name | pascalCase }}>>, Error> {
+    let accounts = rpc.get_multiple_accounts(&addresses)?;
+    let mut decoded_accounts: Vec<super::DecodedAccount<{{ account.name | pascalCase }}>> = Vec::new();
+    for i in 0..addresses.len() {
+      let address = addresses[i];
+      let account = accounts[i].as_ref().ok_or(format!("Account not found: {}", address))?;
+      let data = {{ account.name | pascalCase }}::from_bytes(&account.data)?;
+      decoded_accounts.push(super::DecodedAccount { address, account: account.clone(), data });
+    }
+    Ok(decoded_accounts)
+}
+
+#[cfg(feature = "fetch")]
+pub fn fetch_maybe_{{ account.name | snakeCase }}(
+  rpc: &solana_client::rpc_client::RpcClient,
+  address: &Pubkey,
+) -> Result<super::MaybeAccount<{{ account.name | pascalCase }}>, Error> {
+    let accounts = fetch_all_maybe_{{ account.name | snakeCase }}(rpc, vec![address])?;
+    Ok(accounts[0].clone())
+}
+
+#[cfg(feature = "fetch")]
+pub fn fetch_all_maybe_{{ account.name | snakeCase }}(
+  rpc: &solana_client::rpc_client::RpcClient,
+  addresses: Vec<Pubkey>,
+) -> Result<Vec<super::MaybeAccount<{{ account.name | pascalCase }}>>, Error> {
+    let accounts = rpc.get_multiple_accounts(&addresses)?;
+    let mut decoded_accounts: Vec<super::MaybeAccount<{{ account.name | pascalCase }}>> = Vec::new();
+    for i in 0..addresses.len() {
+      let address = addresses[i];
+      if let Some(account) = accounts[i].as_ref() {
+        let data = {{ account.name | pascalCase }}::from_bytes(&account.data)?;
+        decoded_accounts.push(super::MaybeAccount::Exists(super::DecodedAccount { address, account: account.clone(), data }));
+      } else {
+        decoded_accounts.push(super::MaybeAccount::NotFound(address));
+      }
+    }
+  Ok(decoded_accounts)
+}
+
 {% if anchorTraits %}
 {% if anchorTraits %}
   #[cfg(feature = "anchor")]
   #[cfg(feature = "anchor")]
   impl anchor_lang::AccountDeserialize for {{ account.name | pascalCase }} {
   impl anchor_lang::AccountDeserialize for {{ account.name | pascalCase }} {

+ 3 - 0
packages/renderers-rust/public/templates/rootMod.njk

@@ -15,6 +15,9 @@
   {% if programsToExport.length > 0 %}
   {% if programsToExport.length > 0 %}
     pub mod programs;
     pub mod programs;
   {% endif %}
   {% endif %}
+  {% if accountsToExport.length > 0 %}
+    pub mod shared;
+  {% endif %}
   {% if definedTypesToExport.length > 0 %}
   {% if definedTypesToExport.length > 0 %}
     pub mod types;
     pub mod types;
   {% endif %}
   {% endif %}

+ 25 - 85
packages/renderers-rust/public/templates/sharedPage.njk

@@ -1,93 +1,33 @@
 {% extends "layout.njk" %}
 {% extends "layout.njk" %}
 {% block main %}
 {% block main %}
 
 
-use std::fmt::Debug;
-use std::io::Write;
-use std::ops::{Deref, DerefMut};
-
-use borsh::maybestd::io::Read;
-use borsh::{BorshDeserialize, BorshSerialize};
-
-/// A vector that deserializes from a stream of bytes.
-///
-/// This is useful for deserializing a vector that does not have
-/// a length prefix. In order to determine how many elements to deserialize,
-/// the type of the elements must implement the trait `Sized`.
-pub struct RemainderVec<T: BorshSerialize + BorshDeserialize>(Vec<T>);
-
-/// Deferences the inner `Vec` type.
-impl<T> Deref for RemainderVec<T>
-where
-    T: BorshSerialize + BorshDeserialize,
-{
-    type Target = Vec<T>;
-
-    fn deref(&self) -> &Self::Target {
-        &self.0
-    }
-}
-
-/// Deferences the inner `Vec` type as mutable.
-impl<T> DerefMut for RemainderVec<T>
-where
-    T: BorshSerialize + BorshDeserialize,
-{
-    fn deref_mut(&mut self) -> &mut Self::Target {
-        &mut self.0
+{#
+The following types are used as responses for function that fetch accounts.
+Ideally, these types live in a shared crate that can be used by the client but
+at time of writing, there are some unresolved questions about how to do this.
+
+For now, we just define them here. This the following caveat:
+- These types are not compatible between different clients since the type
+  exists in each client individually.
+#}
+
+{% if accountsToExport.length > 0 %}
+
+    #[cfg(feature = "fetch")]
+    #[derive(Debug, Clone)]
+    pub struct DecodedAccount<T> {
+        pub address: solana_program::pubkey::Pubkey,
+        pub account: solana_sdk::account::Account,
+        pub data: T,
     }
     }
-}
 
 
-/// `Debug` implementation for `RemainderVec`.
-///
-/// This implementation simply forwards to the inner `Vec` type.
-impl<T> Debug for RemainderVec<T>
-where
-    T: BorshSerialize + BorshDeserialize + Debug,
-{
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.write_fmt(format_args!("{:?}", self.0))
+    #[cfg(feature = "fetch")]
+    #[derive(Debug, Clone)]
+    pub enum MaybeAccount<T> {
+        Exists(DecodedAccount<T>),
+        NotFound(solana_program::pubkey::Pubkey),
     }
     }
-}
 
 
-impl<T> BorshDeserialize for RemainderVec<T>
-where
-    T: BorshSerialize + BorshDeserialize,
-{
-    fn deserialize_reader<R: Read>(reader: &mut R) -> borsh::maybestd::io::Result<Self> {
-        let length = std::mem::size_of::<T>();
-        // buffer to read the data
-        let mut buffer = vec![0u8; length];
-        // vec to store the items
-        let mut items: Vec<T> = Vec::new();
+{% endif %}
 
 
-        loop {
-            match reader.read(&mut buffer)? {
-                0 => break,
-                n if n == length => items.push(T::deserialize(&mut buffer.as_slice())?),
-                e => {
-                    return Err(borsh::maybestd::io::Error::new(
-                        borsh::maybestd::io::ErrorKind::InvalidData,
-                        format!("unexpected number of bytes (read {e}, expected {length})"),
-                    ))
-                }
-            }
-        }
-
-        Ok(Self(items))
-    }
-}
-
-impl<T> BorshSerialize for RemainderVec<T>
-where
-    T: BorshSerialize + BorshDeserialize,
-{
-    fn serialize<W: Write>(&self, writer: &mut W) -> borsh::maybestd::io::Result<()> {
-        // serialize each item without adding a prefix for the length
-        for item in self.0.iter() {
-            item.serialize(writer)?;
-        }
-
-        Ok(())
-    }
-}
-{% endblock %}
+{% endblock %}

+ 3 - 0
packages/renderers-rust/src/getRenderMapVisitor.ts

@@ -270,6 +270,9 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
                     };
                     };
 
 
                     const map = new RenderMap();
                     const map = new RenderMap();
+                    if (accountsToExport.length > 0) {
+                        map.add('shared.rs', render('sharedPage.njk', ctx));
+                    }
                     if (programsToExport.length > 0) {
                     if (programsToExport.length > 0) {
                         map.add('programs.rs', render('programsMod.njk', ctx)).add(
                         map.add('programs.rs', render('programsMod.njk', ctx)).add(
                             'errors/mod.rs',
                             'errors/mod.rs',

+ 32 - 0
packages/renderers-rust/test/accountsPage.test.ts

@@ -135,6 +135,38 @@ test('it renders anchor traits impl', () => {
     ]);
     ]);
 });
 });
 
 
+test('it renders fetch functions', () => {
+    // Given the following account.
+    const node = programNode({
+        accounts: [
+            accountNode({
+                discriminators: [
+                    {
+                        kind: 'fieldDiscriminatorNode',
+                        name: camelCase('discriminator'),
+                        offset: 0,
+                    },
+                ],
+                name: 'testAccount',
+                pda: pdaLinkNode('testPda'),
+            }),
+        ],
+        name: 'myProgram',
+        publicKey: '1111',
+    });
+
+    // When we render it.
+    const renderMap = visit(node, getRenderMapVisitor());
+
+    // Then we expect the following fetch functions to be rendered.
+    codeContains(renderMap.get('accounts/test_account.rs'), [
+        'pub fn fetch_test_account',
+        'pub fn fetch_maybe_test_account',
+        'pub fn fetch_all_test_account',
+        'pub fn fetch_all_maybe_test_account',
+    ]);
+});
+
 test('it renders account without anchor traits', () => {
 test('it renders account without anchor traits', () => {
     // Given the following account.
     // Given the following account.
     const node = programNode({
     const node = programNode({