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; const MAX_BUFFER_SIZE: u32 = 2048; 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 }
114
115#[derive(AnchorSerialize, AnchorDeserialize)]
116pub struct InitializeProviderParams {
117 pub authority: Pubkey,
119 pub name: String,
121 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 #[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 #[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}