From 09ff7cc3d4c57a0d7d7c9304b971eca761c79c67 Mon Sep 17 00:00:00 2001 From: Jon C Date: Fri, 1 Aug 2025 18:17:20 +0200 Subject: [PATCH 1/5] interface: Introduce lightweight crate for instruction + id #### Problem In order to publish the v3 SDK crates and have them usable in Agave, we also need to have SPL crates using the v3 SDK crates. However, we have a circular dependency between Agave and SPL which currently makes this impossible. The overall plan is to have Agave only use "interface" crates from SPL, which have no dependencies on Agave crates. You can see more info about the project at https://github.com/orgs/anza-xyz/projects/27 Memo is very simple, since it just exposes an instruction creator and id to Agave. #### Summary of changes Create the new interface crate with the instruction and id. The Rust scripts needed to be adapted to having more than one crate, so I copied all the scripts from token-2022 and updated the commands in package.json accordingly. NOTE: There is a difference in the `build_memo` instruction creator. In the interface crate, it accepts any program id, whereas the program crate hardcodes the v3 id. Since we're planning on deploying p-memo soon under a new program id, this new function will be easier to use. --- .github/workflows/main.yml | 38 ++++++++++++++++++++ Cargo.lock | 9 +++++ Cargo.toml | 2 +- package.json | 13 ++++--- program/Cargo.toml | 1 + program/src/lib.rs | 40 ++++------------------ scripts/program/build.mjs | 13 ------- scripts/program/format.mjs | 29 ---------------- scripts/program/test.mjs | 20 ----------- scripts/{audit-rust.mjs => rust/audit.mjs} | 0 scripts/rust/build-sbf.mjs | 10 ++++++ scripts/rust/format.mjs | 23 +++++++++++++ scripts/{program => rust}/lint.mjs | 18 +++++----- scripts/rust/test-sbf.mjs | 11 ++++++ scripts/rust/test.mjs | 12 +++++++ 15 files changed, 127 insertions(+), 112 deletions(-) delete mode 100644 scripts/program/build.mjs delete mode 100644 scripts/program/format.mjs delete mode 100644 scripts/program/test.mjs rename scripts/{audit-rust.mjs => rust/audit.mjs} (100%) create mode 100644 scripts/rust/build-sbf.mjs create mode 100644 scripts/rust/format.mjs rename scripts/{program => rust}/lint.mjs (52%) create mode 100644 scripts/rust/test-sbf.mjs create mode 100644 scripts/rust/test.mjs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1067f0e6..9667c85e 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 @@ -151,6 +172,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 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/package.json b/package.json index c7bbfc19..80c1510d 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,13 @@ { "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", "solana:check": "zx ./scripts/check-solana-version.mjs", "solana:link": "zx ./scripts/link-solana-version.mjs", "generate": "pnpm generate:clients", @@ -22,7 +25,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}`; From eaf48169a15c6393354e5bc342749032a1b27e32 Mon Sep 17 00:00:00 2001 From: Jon C Date: Fri, 1 Aug 2025 18:23:26 +0200 Subject: [PATCH 2/5] Actually add the interface crate files --- interface/Cargo.toml | 18 ++++++++++++++++++ interface/src/instruction.rs | 37 ++++++++++++++++++++++++++++++++++++ interface/src/lib.rs | 17 +++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 interface/Cargo.toml create mode 100644 interface/src/instruction.rs create mode 100644 interface/src/lib.rs 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"); +} From e43b299a5e1a4de170eb27912e61cdbbc0b1bf81 Mon Sep 17 00:00:00 2001 From: Jon C Date: Fri, 1 Aug 2025 18:29:41 +0200 Subject: [PATCH 3/5] Remove semver check for now --- .github/workflows/main.yml | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9667c85e..ee4b491f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -102,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 From 2b8d01502df8233114f57726175cc4cb82238ab2 Mon Sep 17 00:00:00 2001 From: Jon C Date: Fri, 1 Aug 2025 18:43:40 +0200 Subject: [PATCH 4/5] Wait a bit longer for the program to be deployed and ready --- scripts/start-validator.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/start-validator.mjs b/scripts/start-validator.mjs index a5ebb388..bfc2a319 100644 --- a/scripts/start-validator.mjs +++ b/scripts/start-validator.mjs @@ -78,7 +78,7 @@ const waitForValidator = spinner( const logs = fs.readFileSync(cliLogs, 'utf8'); if (validator.exitCode !== null) { reject(logs); - } else if (logs.includes('Confirmed Slot: 16')) { + } else if (logs.includes('Confirmed Slot: 32')) { resolve(); } }, 1000); From 14b24a81512e06998d710535dcd18323cfe3211c Mon Sep 17 00:00:00 2001 From: Jon C Date: Fri, 1 Aug 2025 19:36:12 +0200 Subject: [PATCH 5/5] Explicitly build and test p-memo --- .github/workflows/main.yml | 6 ++++++ package.json | 2 ++ scripts/start-validator.mjs | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ee4b491f..2f3468a3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -139,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: @@ -187,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/package.json b/package.json index 80c1510d..d2967eb6 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "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", diff --git a/scripts/start-validator.mjs b/scripts/start-validator.mjs index bfc2a319..a5ebb388 100644 --- a/scripts/start-validator.mjs +++ b/scripts/start-validator.mjs @@ -78,7 +78,7 @@ const waitForValidator = spinner( const logs = fs.readFileSync(cliLogs, 'utf8'); if (validator.exitCode !== null) { reject(logs); - } else if (logs.includes('Confirmed Slot: 32')) { + } else if (logs.includes('Confirmed Slot: 16')) { resolve(); } }, 1000);