sgraph/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(clippy::all)]
3#![allow(clippy::result_large_err)]
4
5use anchor_lang::{prelude::*, solana_program::keccak, AnchorDeserialize, AnchorSerialize, Id};
6use spl_account_compression::{cpi as spl_ac_cpi, program::SplAccountCompression, Node, Noop};
7
8pub use spl_account_compression;
9
10#[cfg(not(feature = "no-entrypoint"))]
11use solana_security_txt::security_txt;
12
13pub const CONTROLLER_SEED: &[u8] = b"controller";
14
15declare_id!("graph8zS8zjLVJHdiSvP7S9PP7hNJpnHdbnJLR81FMg");
16
17#[program]
18pub mod graph {
19    use super::*;
20
21    pub fn initialize_tree(ctx: Context<InitializeTree>) -> Result<()> {
22        const MAX_DEPTH: u32 = 30; // 1 billion possible entries
23        const MAX_BUFFER_SIZE: u32 = 2048; // tbd
24
25        let accounts = spl_ac_cpi::accounts::Initialize {
26            merkle_tree: ctx.accounts.tree.to_account_info(),
27            authority: ctx.accounts.tree_controller.to_account_info(),
28            noop: ctx.accounts.noop_program.to_account_info(),
29        };
30
31        let signer_seeds: &[&[&[u8]]] = &[&[
32            CONTROLLER_SEED,
33            &[*ctx.bumps.get("tree_controller").unwrap()],
34        ]];
35
36        let cpi_ctx = CpiContext::new_with_signer(
37            ctx.accounts.ac_program.to_account_info(),
38            accounts,
39            signer_seeds,
40        );
41
42        spl_ac_cpi::init_empty_merkle_tree(cpi_ctx, MAX_DEPTH, MAX_BUFFER_SIZE)?;
43
44        ctx.accounts.tree_controller.set_inner(Controller {
45            authority: ctx.accounts.authority.key(),
46            tree: ctx.accounts.tree.key(),
47        });
48
49        Ok(())
50    }
51
52    pub fn initialize_provider(
53        ctx: Context<InitializeProvider>,
54        args: InitializeProviderParams,
55    ) -> Result<()> {
56        let provider = crate::Provider {
57            authority: args.authority,
58            name: args.name,
59            website: args.website,
60            relations_count: 0,
61        };
62        ctx.accounts.provider.set_inner(provider);
63        spl_account_compression::program::SplAccountCompression::id();
64        Ok(())
65    }
66
67    pub fn add_relation(ctx: Context<AddRelation>, args: AddRelationParams) -> Result<()> {
68        let clock = Clock::get()?;
69
70        let leaf = RelationLeaf {
71            version: LeafType::RelationV1,
72            relation: Relation {
73                from: args.from,
74                to: args.to,
75                provider: ctx.accounts.provider.key(),
76                connected_at: clock.unix_timestamp,
77                disconnected_at: None,
78                extra: args.extra,
79            },
80        };
81
82        let node = leaf.to_node();
83
84        let accounts = spl_ac_cpi::accounts::Modify {
85            merkle_tree: ctx.accounts.tree.to_account_info(),
86            authority: ctx.accounts.tree_controller.to_account_info(),
87            noop: ctx.accounts.noop_program.to_account_info(),
88        };
89
90        let bump = *ctx.bumps.get("tree_controller").unwrap();
91        let signer_seeds: &[&[&[u8]]] = &[&[CONTROLLER_SEED, &[bump]]];
92
93        let cpi_ctx = CpiContext::new(ctx.accounts.ac_program.to_account_info(), accounts)
94            .with_signer(signer_seeds);
95
96        spl_ac_cpi::append(cpi_ctx, node)?;
97
98        ctx.accounts.provider.relations_count = ctx
99            .accounts
100            .provider
101            .relations_count
102            .checked_add(1)
103            .ok_or_else(|| error!(GraphError::Overflow))?;
104
105        Ok(())
106    }
107
108    // todo edit extra
109
110    // todo close relation
111
112    // todo change provider info/authority
113}
114
115#[derive(AnchorSerialize, AnchorDeserialize)]
116pub struct InitializeProviderParams {
117    // authority that must sign transactions to create relations
118    pub authority: Pubkey,
119    // name of the provider
120    pub name: String,
121    // url to docs of the provider
122    pub website: String,
123}
124
125#[derive(Accounts)]
126#[instruction(args: InitializeProviderParams)]
127pub struct InitializeProvider<'info> {
128    #[account(init, payer = payer, space = Provider::calculate_space(args))]
129    pub provider: Account<'info, Provider>,
130    #[account(mut)]
131    pub payer: Signer<'info>,
132    pub system_program: Program<'info, System>,
133}
134
135#[derive(Accounts)]
136pub struct InitializeTree<'info> {
137    /// CHECK: first call to initialize is permissionless
138    #[account(mut)]
139    pub tree: AccountInfo<'info>,
140
141    #[account(
142        init,
143        space = 8 + 32 + 32,
144        payer = payer,
145        seeds = [CONTROLLER_SEED],
146        bump,
147    )]
148    pub tree_controller: Account<'info, Controller>,
149
150    pub authority: Signer<'info>,
151
152    #[account(mut)]
153    pub payer: Signer<'info>,
154
155    pub ac_program: Program<'info, SplAccountCompression>,
156    pub noop_program: Program<'info, Noop>,
157    pub system_program: Program<'info, System>,
158}
159
160#[derive(Accounts)]
161#[instruction(args: AddRelationParams)]
162pub struct AddRelation<'info> {
163    #[account(mut, has_one = authority)]
164    pub provider: Account<'info, Provider>,
165    pub authority: Signer<'info>,
166
167    /// CHECK: key is checked
168    #[account(mut)]
169    pub tree: AccountInfo<'info>,
170
171    #[account(
172        seeds = [CONTROLLER_SEED],
173        bump,
174        has_one = tree,
175    )]
176    pub tree_controller: Account<'info, Controller>,
177
178    #[account(mut)]
179    pub payer: Signer<'info>,
180
181    pub ac_program: Program<'info, SplAccountCompression>,
182    pub noop_program: Program<'info, Noop>,
183}
184
185#[derive(AnchorSerialize, AnchorDeserialize)]
186pub struct AddRelationParams {
187    pub from: Pubkey,
188    pub to: Pubkey,
189    pub extra: Vec<u8>,
190}
191
192#[derive(Debug, PartialEq)]
193#[account]
194pub struct Provider {
195    pub authority: Pubkey,
196    pub relations_count: u64,
197    pub name: String,
198    pub website: String,
199}
200
201impl Provider {
202    fn calculate_space(args: InitializeProviderParams) -> usize {
203        8 + 32 + 8 + 4 + args.name.len() + 4 + args.website.len() + 100
204    }
205}
206
207#[account]
208pub struct Relation {
209    pub from: Pubkey,
210    pub to: Pubkey,
211    pub provider: Pubkey,
212    pub connected_at: i64,
213    pub disconnected_at: Option<i64>,
214    pub extra: Vec<u8>,
215}
216
217pub enum LeafType {
218    Unknown = 0,
219    RelationV1 = 1,
220}
221
222pub struct RelationLeaf {
223    pub version: LeafType,
224    pub relation: Relation,
225}
226
227impl RelationLeaf {
228    fn to_node(&self) -> Node {
229        keccak::hashv(&[
230            1u8.to_le_bytes().as_ref(),
231            self.relation.from.as_ref(),
232            self.relation.to.as_ref(),
233            self.relation.provider.as_ref(),
234            self.relation.connected_at.to_le_bytes().as_ref(),
235            self.relation
236                .disconnected_at
237                .unwrap_or(0)
238                .to_le_bytes()
239                .as_ref(),
240            self.relation.extra.as_ref(),
241        ])
242        .to_bytes()
243    }
244}
245
246#[account]
247pub struct Controller {
248    pub authority: Pubkey,
249    pub tree: Pubkey,
250}
251
252#[error_code]
253pub enum GraphError {
254    #[msg("Closing relations twice is unsupported for now")]
255    RelationAlreadyClosed,
256    #[msg("Overflow occured")]
257    Overflow,
258}
259
260#[cfg(not(feature = "no-entrypoint"))]
261security_txt! {
262    name: "sgraph core contract",
263    project_url: "https://sgraph.io",
264    contacts: "email:security@sgraph.io",
265    policy: "Please report (suspected) security vulnerabilities to email above.
266You will receive a response from us within 48 hours.",
267    source_code: "https://github.com/sgraph-protocol/sgraph",
268    source_revision: env!("GIT_HASH"),
269    acknowledgements: "Everyone in the Solana community"
270}