diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1067f0e6..2f3468a3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,6 +7,27 @@ on: branches: [main] jobs: + format_and_lint_interface: + name: Format & Lint Interface + runs-on: ubuntu-latest + steps: + - name: Git Checkout + uses: actions/checkout@v4 + + - name: Setup Environment + uses: ./.github/actions/setup + with: + clippy: true + rustfmt: true + cargo-cache-key: cargo-interface-lint + cargo-cache-fallback-key: cargo-interface + + - name: Format + run: pnpm interface:format + + - name: Lint + run: pnpm interface:lint + format_and_lint_programs: name: Format & Lint Programs runs-on: ubuntu-latest @@ -81,26 +102,6 @@ jobs: - name: Run cargo-audit run: pnpm rust:audit - semver_rust: - name: Check semver Rust - runs-on: ubuntu-latest - steps: - - name: Git Checkout - uses: actions/checkout@v4 - - - name: Setup Environment - uses: ./.github/actions/setup - with: - cargo-cache-key: cargo-semver - - - name: Install cargo-audit - uses: taiki-e/install-action@v2 - with: - tool: cargo-semver-checks - - - name: Run semver checks - run: pnpm rust:semver - spellcheck_rust: name: Spellcheck Rust runs-on: ubuntu-latest @@ -138,6 +139,9 @@ jobs: - name: Build Programs run: pnpm programs:build + - name: Build p-memo + run: pnpm p-memo:build + - name: Upload Program Builds uses: actions/upload-artifact@v4 with: @@ -151,6 +155,23 @@ jobs: path: ./**/*.so key: ${{ runner.os }}-builds-${{ github.sha }} + test_interface: + name: Test Interface + runs-on: ubuntu-latest + needs: format_and_lint_interface + steps: + - name: Git Checkout + uses: actions/checkout@v4 + + - name: Setup Environment + uses: ./.github/actions/setup + with: + cargo-cache-key: cargo-interface-tests + cargo-cache-fallback-key: cargo-interface + + - name: Test + run: pnpm interface:test + test_programs: name: Test Programs runs-on: ubuntu-latest @@ -169,6 +190,9 @@ jobs: - name: Test Programs run: pnpm programs:test + - name: Test p-memo + run: pnpm p-memo:test + generate_clients: name: Check Client Generation runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index dd6f9293..5b593f7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7094,6 +7094,7 @@ dependencies = [ "solana-program-test", "solana-pubkey", "solana-sdk", + "spl-memo-interface", ] [[package]] @@ -7117,6 +7118,14 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "spl-memo-interface" +version = "1.0.0" +dependencies = [ + "solana-instruction", + "solana-pubkey", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" diff --git a/Cargo.toml b/Cargo.toml index 68a9f707..dbca3d21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["clients/rust", "p-memo", "program"] +members = ["clients/rust", "interface", "p-memo", "program"] [workspace.metadata.cli] solana = "2.3.4" diff --git a/interface/Cargo.toml b/interface/Cargo.toml new file mode 100644 index 00000000..7192e59d --- /dev/null +++ b/interface/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "spl-memo-interface" +version = "1.0.0" +description = "Solana Program Library Memo Interface" +authors = ["Solana Labs Maintainers "] +repository = "https://github.com/solana-labs/solana-program-library" +license = "Apache-2.0" +edition = "2021" + +[dependencies] +solana-instruction = "2.2.1" +solana-pubkey = "2.2.1" + +[lib] +crate-type = ["lib"] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/interface/src/instruction.rs b/interface/src/instruction.rs new file mode 100644 index 00000000..079bd0ba --- /dev/null +++ b/interface/src/instruction.rs @@ -0,0 +1,37 @@ +use { + solana_instruction::{AccountMeta, Instruction}, + solana_pubkey::Pubkey, +}; + +/// Build a memo instruction, possibly signed +/// +/// Accounts expected by this instruction: +/// +/// 0. `..0+N` `[signer]` Expected signers; if zero provided, instruction will +/// be processed as a normal, unsigned spl-memo +pub fn build_memo(program_id: &Pubkey, memo: &[u8], signer_pubkeys: &[&Pubkey]) -> Instruction { + Instruction { + program_id: *program_id, + accounts: signer_pubkeys + .iter() + .map(|&pubkey| AccountMeta::new_readonly(*pubkey, true)) + .collect(), + data: memo.to_vec(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build_memo() { + let program_id = Pubkey::new_unique(); + let signer_pubkey = Pubkey::new_unique(); + let memo = "🐆".as_bytes(); + let instruction = build_memo(&program_id, memo, &[&signer_pubkey]); + assert_eq!(memo, instruction.data); + assert_eq!(instruction.accounts.len(), 1); + assert_eq!(instruction.accounts[0].pubkey, signer_pubkey); + } +} diff --git a/interface/src/lib.rs b/interface/src/lib.rs new file mode 100644 index 00000000..349116df --- /dev/null +++ b/interface/src/lib.rs @@ -0,0 +1,17 @@ +#![deny(missing_docs)] + +//! An interface for programs that accept a string of encoded characters and +//! verifies that it parses, while verifying and logging signers. + +/// Instruction type +pub mod instruction; + +/// Legacy symbols from Memo version 1 +pub mod v1 { + solana_pubkey::declare_id!("Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo"); +} + +/// Symbols from Memo version 3 +pub mod v3 { + solana_pubkey::declare_id!("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"); +} diff --git a/package.json b/package.json index c7bbfc19..d2967eb6 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,15 @@ { "private": true, "scripts": { - "programs:build": "zx ./scripts/program/build.mjs", - "programs:test": "zx ./scripts/program/test.mjs", - "programs:format": "zx ./scripts/program/format.mjs", - "programs:lint": "zx ./scripts/program/lint.mjs", + "interface:test": "zx ./scripts/rust/test.mjs interface", + "interface:format": "zx ./scripts/rust/format.mjs interface", + "interface:lint": "zx ./scripts/rust/lint.mjs interface", + "programs:build": "zx ./scripts/rust/build-sbf.mjs program", + "programs:test": "zx ./scripts/rust/test-sbf.mjs program", + "programs:format": "zx ./scripts/rust/format.mjs program", + "programs:lint": "zx ./scripts/rust/lint.mjs program", + "p-memo:build": "zx ./scripts/rust/build-sbf.mjs p-memo", + "p-memo:test": "zx ./scripts/rust/test-sbf.mjs p-memo", "solana:check": "zx ./scripts/check-solana-version.mjs", "solana:link": "zx ./scripts/link-solana-version.mjs", "generate": "pnpm generate:clients", @@ -22,7 +27,7 @@ "clients:rust:test": "zx ./scripts/client/test-rust.mjs", "template:upgrade": "zx ./scripts/upgrade-template.mjs", "rust:spellcheck": "cargo spellcheck --code 1", - "rust:audit": "zx ./scripts/audit-rust.mjs", + "rust:audit": "zx ./scripts/rust/audit.mjs", "rust:semver": "cargo semver-checks" }, "devDependencies": { diff --git a/program/Cargo.toml b/program/Cargo.toml index aa5f107c..a5cbc8bb 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -18,6 +18,7 @@ solana-msg = "2.2.1" solana-program-entrypoint = "2.3.0" solana-program-error = "2.2.2" solana-pubkey = "2.2.1" +spl-memo-interface = { path = "../interface", version = "1.0.0" } [dev-dependencies] solana-program-test = "2.3.6" diff --git a/program/src/lib.rs b/program/src/lib.rs index 02df4b69..af1f1b87 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -12,18 +12,12 @@ pub mod processor; pub use { solana_account_info, solana_instruction, solana_msg, solana_program_entrypoint, solana_program_error, solana_pubkey, + spl_memo_interface::{ + v1, + v3::{check_id, id, ID}, + }, }; -use { - solana_instruction::{AccountMeta, Instruction}, - solana_pubkey::Pubkey, -}; - -/// Legacy symbols from Memo version 1 -pub mod v1 { - solana_pubkey::declare_id!("Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo"); -} - -solana_pubkey::declare_id!("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"); +use {solana_instruction::Instruction, solana_pubkey::Pubkey}; /// Build a memo instruction, possibly signed /// @@ -32,27 +26,5 @@ solana_pubkey::declare_id!("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"); /// 0. `..0+N` `[signer]` Expected signers; if zero provided, instruction will /// be processed as a normal, unsigned spl-memo pub fn build_memo(memo: &[u8], signer_pubkeys: &[&Pubkey]) -> Instruction { - Instruction { - program_id: id(), - accounts: signer_pubkeys - .iter() - .map(|&pubkey| AccountMeta::new_readonly(*pubkey, true)) - .collect(), - data: memo.to_vec(), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_build_memo() { - let signer_pubkey = Pubkey::new_unique(); - let memo = "🐆".as_bytes(); - let instruction = build_memo(memo, &[&signer_pubkey]); - assert_eq!(memo, instruction.data); - assert_eq!(instruction.accounts.len(), 1); - assert_eq!(instruction.accounts[0].pubkey, signer_pubkey); - } + spl_memo_interface::instruction::build_memo(&id(), memo, signer_pubkeys) } diff --git a/scripts/program/build.mjs b/scripts/program/build.mjs deleted file mode 100644 index 7a079660..00000000 --- a/scripts/program/build.mjs +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env zx -import 'zx/globals'; -import { - cliArguments, - getProgramFolders, - workingDirectory, -} from '../utils.mjs'; - -// Build the programs. -for (const folder of getProgramFolders()) { - const manifestPath = path.join(workingDirectory, folder, 'Cargo.toml'); - await $`cargo-build-sbf --manifest-path ${manifestPath} ${cliArguments()}`; -} diff --git a/scripts/program/format.mjs b/scripts/program/format.mjs deleted file mode 100644 index c73ddd41..00000000 --- a/scripts/program/format.mjs +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env zx -import 'zx/globals'; -import { - cliArguments, - getProgramFolders, - getToolchainArgument, - partitionArguments, - popArgument, - workingDirectory, -} from '../utils.mjs'; - -// Configure additional arguments here, e.g.: -// ['--arg1', '--arg2', ...cliArguments()] -const formatArgs = cliArguments(); - -const fix = popArgument(formatArgs, '--fix'); -const [cargoArgs, fmtArgs] = partitionArguments(formatArgs, '--'); -const toolchain = getToolchainArgument('format'); - -// Format the programs. -for (const folder of getProgramFolders()) { - const manifestPath = path.join(workingDirectory, folder, 'Cargo.toml'); - - if (fix) { - await $`cargo ${toolchain} fmt --manifest-path ${manifestPath} ${cargoArgs} -- ${fmtArgs}`; - } else { - await $`cargo ${toolchain} fmt --manifest-path ${manifestPath} ${cargoArgs} -- --check ${fmtArgs}`; - } -} diff --git a/scripts/program/test.mjs b/scripts/program/test.mjs deleted file mode 100644 index fe85dc6e..00000000 --- a/scripts/program/test.mjs +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env zx -import 'zx/globals'; -import { - cliArguments, - getProgramFolders, - workingDirectory, -} from '../utils.mjs'; - -const hasSolfmt = await which('solfmt', { nothrow: true }); - -// Test the programs. -for (const folder of getProgramFolders()) { - const manifestPath = path.join(workingDirectory, folder, 'Cargo.toml'); - - if (hasSolfmt) { - await $`RUST_LOG=error cargo test-sbf --manifest-path ${manifestPath} ${cliArguments()} 2>&1 | solfmt`; - } else { - await $`RUST_LOG=error cargo test-sbf --manifest-path ${manifestPath} ${cliArguments()}`; - } -} diff --git a/scripts/audit-rust.mjs b/scripts/rust/audit.mjs similarity index 100% rename from scripts/audit-rust.mjs rename to scripts/rust/audit.mjs diff --git a/scripts/rust/build-sbf.mjs b/scripts/rust/build-sbf.mjs new file mode 100644 index 00000000..64a80d34 --- /dev/null +++ b/scripts/rust/build-sbf.mjs @@ -0,0 +1,10 @@ +#!/usr/bin/env zx +import 'zx/globals'; +import { + cliArguments, + workingDirectory, +} from '../utils.mjs'; + +const [folder, ...args] = cliArguments(); +const manifestPath = path.join(workingDirectory, folder, 'Cargo.toml'); +await $`cargo-build-sbf --manifest-path ${manifestPath} ${args}`; diff --git a/scripts/rust/format.mjs b/scripts/rust/format.mjs new file mode 100644 index 00000000..68dc98ca --- /dev/null +++ b/scripts/rust/format.mjs @@ -0,0 +1,23 @@ +#!/usr/bin/env zx +import 'zx/globals'; +import { + cliArguments, + getToolchainArgument, + partitionArguments, + popArgument, + workingDirectory, +} from '../utils.mjs'; + +const [folder, ...formatArgs] = cliArguments(); + +const fix = popArgument(formatArgs, '--fix'); +const [cargoArgs, fmtArgs] = partitionArguments(formatArgs, '--'); +const toolchain = getToolchainArgument('format'); + +const manifestPath = path.join(workingDirectory, folder, 'Cargo.toml'); + +if (fix) { + await $`cargo ${toolchain} fmt --manifest-path ${manifestPath} ${cargoArgs} -- ${fmtArgs}`; +} else { + await $`cargo ${toolchain} fmt --manifest-path ${manifestPath} ${cargoArgs} -- --check ${fmtArgs}`; +} diff --git a/scripts/program/lint.mjs b/scripts/rust/lint.mjs similarity index 52% rename from scripts/program/lint.mjs rename to scripts/rust/lint.mjs index e4314314..66777eb6 100644 --- a/scripts/program/lint.mjs +++ b/scripts/rust/lint.mjs @@ -2,12 +2,13 @@ import 'zx/globals'; import { cliArguments, - getProgramFolders, getToolchainArgument, popArgument, workingDirectory, } from '../utils.mjs'; +const [folder, ...args] = cliArguments(); + // Configure arguments here. const lintArgs = [ '-Zunstable-options', @@ -16,19 +17,16 @@ const lintArgs = [ '--', '--deny=warnings', '--deny=clippy::arithmetic_side_effects', - ...cliArguments(), + ...args, ]; const fix = popArgument(lintArgs, '--fix'); const toolchain = getToolchainArgument('lint'); -// Lint the programs using clippy. -for (const folder of getProgramFolders()) { - const manifestPath = path.join(workingDirectory, folder, 'Cargo.toml'); +const manifestPath = path.join(workingDirectory, folder, 'Cargo.toml'); - if (fix) { - await $`cargo ${toolchain} clippy --manifest-path ${manifestPath} --fix ${lintArgs}`; - } else { - await $`cargo ${toolchain} clippy --manifest-path ${manifestPath} ${lintArgs}`; - } +if (fix) { + await $`cargo ${toolchain} clippy --manifest-path ${manifestPath} --fix ${lintArgs}`; +} else { + await $`cargo ${toolchain} clippy --manifest-path ${manifestPath} ${lintArgs}`; } diff --git a/scripts/rust/test-sbf.mjs b/scripts/rust/test-sbf.mjs new file mode 100644 index 00000000..971448c6 --- /dev/null +++ b/scripts/rust/test-sbf.mjs @@ -0,0 +1,11 @@ +#!/usr/bin/env zx +import 'zx/globals'; +import { + cliArguments, + workingDirectory, +} from '../utils.mjs'; + +const [folder, ...args] = cliArguments(); +const manifestPath = path.join(workingDirectory, folder, 'Cargo.toml'); + +await $`RUST_LOG=error cargo test-sbf --manifest-path ${manifestPath} ${args}`; diff --git a/scripts/rust/test.mjs b/scripts/rust/test.mjs new file mode 100644 index 00000000..5b44fb41 --- /dev/null +++ b/scripts/rust/test.mjs @@ -0,0 +1,12 @@ +#!/usr/bin/env zx +import 'zx/globals'; +import { + cliArguments, + workingDirectory, +} from '../utils.mjs'; + +const [folder, ...args] = cliArguments(); +const sbfOutDir = path.join(workingDirectory, 'target', 'deploy'); +const manifestPath = path.join(workingDirectory, folder, 'Cargo.toml'); + +await $`RUST_LOG=error SBF_OUT_DIR=${sbfOutDir} cargo test --manifest-path ${manifestPath} ${args}`;