diff --git a/Cargo.lock b/Cargo.lock index 657fa0005..39052d001 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1137,6 +1137,13 @@ dependencies = [ "once_cell", ] +[[package]] +name = "clockwork-gasless-program" +version = "2.0.17" +dependencies = [ + "anchor-lang", +] + [[package]] name = "clockwork-network-program" version = "2.0.17" diff --git a/programs/gasless/Cargo.toml b/programs/gasless/Cargo.toml new file mode 100644 index 000000000..2d5bb6ad4 --- /dev/null +++ b/programs/gasless/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "clockwork-gasless-program" +version = "2.0.17" +description = "Clockwork gasless program" +edition = "2021" +license = "AGPL-3.0-or-later" +homepage = "https://clockwork.xyz" +repository = "https://github.com/clockwork-xyz/clockwork" +documentation = "https://docs.clockwork.xyz" +readme = "./README.md" +keywords = ["solana"] + +[lib] +crate-type = ["cdylib", "lib"] +name = "clockwork_gasless_program" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +anchor-lang = { features = ["init-if-needed"], version = "0.27.0" } +clockwork-network-program = { path = "../network", features = ["cpi"], version = "=2.0.17" } diff --git a/programs/gasless/README.md b/programs/gasless/README.md new file mode 100644 index 000000000..1300bc25a --- /dev/null +++ b/programs/gasless/README.md @@ -0,0 +1 @@ +# Clockwork Http diff --git a/programs/gasless/src/lib.rs b/programs/gasless/src/lib.rs new file mode 100644 index 000000000..907bc62ca --- /dev/null +++ b/programs/gasless/src/lib.rs @@ -0,0 +1,161 @@ +use std::mem::size_of; + +use anchor_lang::{prelude::*, solana_program::system_program}; +use clockwork_network_program::state::{Fee, Pool, Worker, WorkerAccount}; + +declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); + +/// The ID of the pool workers must be a member of to collect fees. +const POOL_ID: u64 = 1; + +#[program] +pub mod gasless { + pub use super::*; + + pub fn payer_create<'info>(ctx: Context, amount: u64) -> Result<()> { + let authority = &ctx.accounts.authority; + let payer = &mut ctx.accounts.payer; + let spender = &ctx.accounts.spender; + let system_program = &ctx.accounts.system_program; + + // Initialize payer account. + payer.bump = *ctx.bumps.get("payer").unwrap(); + payer.authority = authority.key(); + payer.spenders = vec![spender.key()]; + + // Fund the payer account. + anchor_lang::system_program::transfer( + CpiContext::new( + system_program.to_account_info(), + anchor_lang::system_program::Transfer { + from: authority.to_account_info(), + to: payer.to_account_info(), + }, + ), + amount, + )?; + + Ok(()) + } + + pub fn add_spender<'info>(ctx: Context) -> Result<()> { + let authority = &ctx.accounts.authority; + let payer = &mut ctx.accounts.payer; + let spender = &ctx.accounts.spender; + let system_program = &ctx.accounts.system_program; + + // Add authorized spender. + payer.spenders.push(spender.key()); + + // Realloc account size. + let new_data_len = payer.to_account_info().data_len().checked_add(32).unwrap(); + payer.to_account_info().realloc(new_data_len, false)?; + + // Pay min rent requirement. + let rent_requirement = Rent::get().unwrap().minimum_balance(new_data_len); + if rent_requirement.gt(&payer.to_account_info().lamports()) { + anchor_lang::system_program::transfer( + CpiContext::new( + system_program.to_account_info(), + anchor_lang::system_program::Transfer { + from: authority.to_account_info(), + to: payer.to_account_info(), + }, + ), + rent_requirement.saturating_sub(payer.to_account_info().lamports()), + )?; + } + + Ok(()) + } + + pub fn spend<'info>(_ctx: Context) -> Result<()> { + Ok(()) + } +} + +#[derive(Accounts)] +#[instruction(amount: u64)] +pub struct PayerCreate<'info> { + #[account(mut)] + pub authority: Signer<'info>, + + #[account( + init, + seeds = [ + SEED_PAYER, + authority.key().as_ref(), + ], + bump, + space = 8 + size_of::() + 32, + payer = authority + )] + pub payer: Account<'info, Payer>, + + #[account()] + pub spender: SystemAccount<'info>, + + #[account(address = system_program::ID)] + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct AddSpender<'info> { + #[account(mut)] + pub authority: Signer<'info>, + + #[account()] + pub spender: SystemAccount<'info>, + + #[account( + mut, + seeds = [ + SEED_PAYER, + authority.key().as_ref(), + ], + bump = payer.bump, + )] + pub payer: Account<'info, Payer>, + + #[account(address = system_program::ID)] + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct Spend<'info> { + #[account(mut)] + pub fee: Account<'info, Fee>, + + #[account()] + pub spender: Signer<'info>, + + #[account(mut)] + pub signatory: Signer<'info>, + + #[account( + mut, + seeds = [ + SEED_PAYER, + payer.authority.key().as_ref(), + ], + bump = payer.bump, + constraint = payer.spenders.contains(&spender.key()) @ GaslessError::UnauthorizedSpender, + )] + pub payer: Account<'info, Payer>, +} + +pub const SEED_PAYER: &[u8] = b"payer"; + +#[account] +#[derive(Debug)] +pub struct Payer { + pub authority: Pubkey, + pub spenders: Vec, + pub bump: u8, +} + +#[error_code] +pub enum GaslessError { + #[msg("This signer is not authorized to spend from this PDA")] + UnauthorizedSpender, +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index f54e3b8be..184939d3f 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.60.0" +channel = "1.65.0" components = [ "rustfmt", "rust-analyzer" ] profile = "minimal" targets = [ "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "aarch64-apple-darwin"]