Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 69 additions & 37 deletions docs/lez/extensions/admin-authority.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

:::warning
This page is an early draft and may be incomplete or incorrect. Expect changes, missing prerequisites, and commands that might not work in your setup. We are actively working to complete and verify this content.

This page tracks unreleased code. The dependency snippets pin a personal fork of the framework and pre-release library tags. The pins move to logos-co sources once the extension mechanism lands upstream ([logos-co/spel#257](https://github.com/logos-co/spel/pull/257)).
:::

`admin-authority` is a SPEL extension that adds a single transferable admin role to your LEZ program. The admin is the only account allowed to call admin-gated instructions. The role can be transferred to another signer or PDA, or renounced permanently. This page walks through using `admin-authority` from an app developer's perspective. If you are building a different extension, see [Build a SPEL extension library](build-a-spel-extension-library.md) instead.
Expand All @@ -29,23 +31,45 @@

If your program needs multi-party approval rather than single-admin gating, `admin-authority` is the wrong primitive, wait for `multisig-authority` (RFP-TBD) or compose admin-authority with a multisig PDA as the admin.

## Prerequisites

You need a stable Rust toolchain, git, and the native build tools the dependency tree leans on. The `spel` CLI additionally needs `unzip` (a build script unpacks a prebuilt rapidsnark archive) and the Python development library (the CLI links against libpython). The verification step at the end uses `jq`. On a fresh Ubuntu 24.04 this covers everything:

Check failure on line 36 in docs/lez/extensions/admin-authority.md

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 'libpython' is not in British English. Raw Output: {"message":"'libpython' is not in British English.","location":{"path":"docs/lez/extensions/admin-authority.md","range":{"start":{"line":36,"column":252},"end":{"line":36,"column":261}}},"severity":"ERROR","code":{"value":"Logos.SpellingGB"}}

Check warning on line 36 in docs/lez/extensions/admin-authority.md

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 Use 'command-line tool' instead of 'CLI'. Raw Output: {"message":"Use 'command-line tool' instead of 'CLI'.","location":{"path":"docs/lez/extensions/admin-authority.md","range":{"start":{"line":36,"column":234},"end":{"line":36,"column":237}}},"severity":"WARNING","code":{"value":"Google.WordList","url":"https://developers.google.com/style/word-list"}}

Check warning on line 36 in docs/lez/extensions/admin-authority.md

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 Use parentheses judiciously. Raw Output: {"message":"Use parentheses judiciously.","location":{"path":"docs/lez/extensions/admin-authority.md","range":{"start":{"line":36,"column":229},"end":{"line":36,"column":262}}},"severity":"INFO","code":{"value":"Google.Parens","url":"https://developers.google.com/style/parentheses"}}

Check failure on line 36 in docs/lez/extensions/admin-authority.md

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 'rapidsnark' is not in British English. Raw Output: {"message":"'rapidsnark' is not in British English.","location":{"path":"docs/lez/extensions/admin-authority.md","range":{"start":{"line":36,"column":174},"end":{"line":36,"column":184}}},"severity":"ERROR","code":{"value":"Logos.SpellingGB"}}

Check warning on line 36 in docs/lez/extensions/admin-authority.md

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 Use 'command-line tool' instead of 'CLI'. Raw Output: {"message":"Use 'command-line tool' instead of 'CLI'.","location":{"path":"docs/lez/extensions/admin-authority.md","range":{"start":{"line":36,"column":108},"end":{"line":36,"column":111}}},"severity":"WARNING","code":{"value":"Google.WordList","url":"https://developers.google.com/style/word-list"}}

Check failure on line 36 in docs/lez/extensions/admin-authority.md

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 'toolchain' is not in British English. Raw Output: {"message":"'toolchain' is not in British English.","location":{"path":"docs/lez/extensions/admin-authority.md","range":{"start":{"line":36,"column":24},"end":{"line":36,"column":33}}},"severity":"ERROR","code":{"value":"Logos.SpellingGB"}}

```bash
sudo apt-get install curl git build-essential pkg-config libssl-dev ca-certificates unzip python3 python3-dev cmake jq
```

The build and IDL verification steps on this page were verified on a clean Ubuntu 24.04 with rustc 1.98. The lifecycle commands have not been run against a live node yet.

Check warning on line 42 in docs/lez/extensions/admin-authority.md

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 In general, use active voice instead of passive voice ('been run'). Raw Output: {"message":"In general, use active voice instead of passive voice ('been run').","location":{"path":"docs/lez/extensions/admin-authority.md","range":{"start":{"line":42,"column":138},"end":{"line":42,"column":146}}},"severity":"INFO","code":{"value":"Google.Passive","url":"https://developers.google.com/style/voice"}}

Check warning on line 42 in docs/lez/extensions/admin-authority.md

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 Use 'haven't' instead of 'have not'. Raw Output: {"message":"Use 'haven't' instead of 'have not'.","location":{"path":"docs/lez/extensions/admin-authority.md","range":{"start":{"line":42,"column":129},"end":{"line":42,"column":137}}},"severity":"INFO","code":{"value":"Google.Contractions","url":"https://developers.google.com/style/contractions"}}

Check failure on line 42 in docs/lez/extensions/admin-authority.md

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 'lifecycle' is not in British English. Raw Output: {"message":"'lifecycle' is not in British English.","location":{"path":"docs/lez/extensions/admin-authority.md","range":{"start":{"line":42,"column":110},"end":{"line":42,"column":119}}},"severity":"ERROR","code":{"value":"Logos.SpellingGB"}}

Check failure on line 42 in docs/lez/extensions/admin-authority.md

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 'rustc' is not in British English. Raw Output: {"message":"'rustc' is not in British English.","location":{"path":"docs/lez/extensions/admin-authority.md","range":{"start":{"line":42,"column":94},"end":{"line":42,"column":99}}},"severity":"ERROR","code":{"value":"Logos.SpellingGB"}}

Check warning on line 42 in docs/lez/extensions/admin-authority.md

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 In general, use active voice instead of passive voice ('were verified'). Raw Output: {"message":"In general, use active voice instead of passive voice ('were verified').","location":{"path":"docs/lez/extensions/admin-authority.md","range":{"start":{"line":42,"column":51},"end":{"line":42,"column":64}}},"severity":"INFO","code":{"value":"Google.Passive","url":"https://developers.google.com/style/voice"}}

## Add the dependency

In your program's `Cargo.toml`:

```toml
[dependencies]
admin-authority = { git = "https://github.com/mmlado/spel-admin-authority" }
spel-framework = { git = "https://github.com/logos-co/spel" }
admin-authority = { git = "https://github.com/mmlado/spel-admin-authority", tag = "v0.1.0" }
spel-framework = { git = "https://github.com/mmlado/spel", rev = "f7aa464b2c6c72ef513a25ede16584bca85b722f" }
nssa_core = { git = "https://github.com/logos-blockchain/logos-execution-zone.git", tag = "v0.2.0", package = "lee_core" }
borsh = { version = "1", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
```

All five are needed: the reference samples use exactly this set. `nssa_core` carries the on-chain account types, `borsh` encodes your state, and `serde` is required by the instruction plumbing even when your own types never touch it. The `admin-authority-macros` sub-crate is pulled in transitively. You do not need to declare it directly. The library README documents the framework revision each release is verified against.
All five are needed: the reference samples use exactly this set. `nssa_core` carries the on-chain account types, `borsh` encodes your state, and `serde` is required by the instruction plumbing even when your own types never touch it. The `admin-authority-macros` sub-crate is pulled in transitively. You do not need to declare it directly.

Check warning on line 57 in docs/lez/extensions/admin-authority.md

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 Use 'don't' instead of 'do not'. Raw Output: {"message":"Use 'don't' instead of 'do not'.","location":{"path":"docs/lez/extensions/admin-authority.md","range":{"start":{"line":57,"column":305},"end":{"line":57,"column":311}}},"severity":"INFO","code":{"value":"Google.Contractions","url":"https://developers.google.com/style/contractions"}}

Check warning on line 57 in docs/lez/extensions/admin-authority.md

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 In general, use active voice instead of passive voice ('is pulled'). Raw Output: {"message":"In general, use active voice instead of passive voice ('is pulled').","location":{"path":"docs/lez/extensions/admin-authority.md","range":{"start":{"line":57,"column":274},"end":{"line":57,"column":283}}},"severity":"INFO","code":{"value":"Google.Passive","url":"https://developers.google.com/style/voice"}}

Check warning on line 57 in docs/lez/extensions/admin-authority.md

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 In general, use active voice instead of passive voice ('is required'). Raw Output: {"message":"In general, use active voice instead of passive voice ('is required').","location":{"path":"docs/lez/extensions/admin-authority.md","range":{"start":{"line":57,"column":154},"end":{"line":57,"column":165}}},"severity":"INFO","code":{"value":"Google.Passive","url":"https://developers.google.com/style/voice"}}

Check warning on line 57 in docs/lez/extensions/admin-authority.md

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 In general, use active voice instead of passive voice ('are needed'). Raw Output: {"message":"In general, use active voice instead of passive voice ('are needed').","location":{"path":"docs/lez/extensions/admin-authority.md","range":{"start":{"line":57,"column":10},"end":{"line":57,"column":20}}},"severity":"INFO","code":{"value":"Google.Passive","url":"https://developers.google.com/style/voice"}}

The `spel-framework` entry points at a fork on purpose. It must be the exact revision `admin-authority` itself pins, and the library README documents that revision for each release. Pointing at `logos-co/spel` instead puts two copies of the framework into your dependency graph, and the build fails with a `From<AdminError>` trait error plus name resolution errors inside the `require_admin` expansion. The dependency moves to `logos-co/spel` once the extension mechanism lands upstream ([logos-co/spel#257](https://github.com/logos-co/spel/pull/257)).

After adding the dependencies, run `cargo fetch` once. The framework's extension scanner resolves your dependency graph with an offline metadata call, which fails deterministically for a fresh consumer whose git dependencies were never fetched.

## Install the spel CLI

Check failure on line 63 in docs/lez/extensions/admin-authority.md

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 'spel' is not in British English. Raw Output: {"message":"'spel' is not in British English.","location":{"path":"docs/lez/extensions/admin-authority.md","range":{"start":{"line":63,"column":16},"end":{"line":63,"column":20}}},"severity":"ERROR","code":{"value":"Logos.SpellingGB"}}

The lifecycle commands below and the IDL check at the end use the `spel` CLI. Install it from the same fork revision the framework dependency pins:

Check failure on line 65 in docs/lez/extensions/admin-authority.md

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 'lifecycle' is not in British English. Raw Output: {"message":"'lifecycle' is not in British English.","location":{"path":"docs/lez/extensions/admin-authority.md","range":{"start":{"line":65,"column":5},"end":{"line":65,"column":14}}},"severity":"ERROR","code":{"value":"Logos.SpellingGB"}}

```bash
cargo install --git https://github.com/mmlado/spel --rev f7aa464b2c6c72ef513a25ede16584bca85b722f spel
```

The package name is `spel`, not `spel-cli` as the repository directory suggests, asking cargo for `spel-cli` fails with "could not find `spel-cli`".

Check failure on line 71 in docs/lez/extensions/admin-authority.md

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 Commas and periods go inside quotation marks. Raw Output: {"message":"Commas and periods go inside quotation marks.","location":{"path":"docs/lez/extensions/admin-authority.md","range":{"start":{"line":71,"column":1},"end":{"line":71,"column":149}}},"severity":"ERROR","code":{"value":"Google.Quotes","url":"https://developers.google.com/style/quotation-marks"}}

## Annotate the module

If you started from `cargo new`, delete the default `fn main` first. The `#[lez_program]` macro generates the program's entry point, and the leftover stub collides with it as a duplicate `main`.
Expand All @@ -54,21 +78,19 @@

```rust
use spel_framework::prelude::*;
use admin_authority::{admin_authority, require_admin};

#[lez_program]
#[admin_authority]
mod my_program {
use super::*;

#[instruction]
pub fn create_pool(
#[account(init, pda = literal("pool"))] pool: AccountWithMetadata,
#[account(signer)] caller: AccountWithMetadata,
) -> SpelResult { /* ... */ }
}
```

Nothing is imported from the library at this point. The `#[admin_authority]` marker is consumed by the framework's scanner during expansion, not resolved as an import, so importing the name only earns an unused import warning. The gate attribute gets imported when the first instruction uses it, next section. The module body does not need `use super::*;` either, the macro resolves paths to items declared outside the module on its own.

Check failure on line 92 in docs/lez/extensions/admin-authority.md

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 'framework's' is not in British English. Raw Output: {"message":"'framework's' is not in British English.","location":{"path":"docs/lez/extensions/admin-authority.md","range":{"start":{"line":92,"column":104},"end":{"line":92,"column":115}}},"severity":"ERROR","code":{"value":"Logos.SpellingGB"}}

That single annotation exposes three new instructions in your program's IDL:

| Instruction | Purpose |
Expand All @@ -86,32 +108,45 @@
Add `#[require_admin]` to any instruction that should only succeed when the caller is the current admin:

```rust
#[account_type]
#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)]
pub struct PoolConfig {
pub fee_bps: u16,
}

// ... inside the #[lez_program] module:
use admin_authority::require_admin;

#[instruction]
#[require_admin]
pub fn set_fee_bps(
#[account(mut, pda = literal("pool_config"))] mut config: AccountWithMetadata,
new_fee_bps: u16,
) -> SpelResult {
// Admin check has already run. Just mutate.
todo!()
// The admin check has already run.
PoolConfig { fee_bps: new_fee_bps }.write_to(&mut config)?;
Ok(SpelOutput::execute(vec![config], vec![]))
}
```

The gate needs two accounts, the `admin_config` PDA holding the current admin state and a signing `caller`. You do not have to write them: the framework injects both from metadata the library declares, and they appear in the IDL like declared parameters. Declaring them explicitly produces the same program:
The `write_to` helper is yours to write, the library does not provide it. The reference sample uses this one:

```rust
#[instruction]
#[require_admin]
pub fn set_fee_bps(
#[account(pda = literal("admin_config"))] admin_config: AccountWithMetadata,
#[account(signer)] caller: AccountWithMetadata,
#[account(mut, pda = literal("pool_config"))] mut config: AccountWithMetadata,
new_fee_bps: u16,
) -> SpelResult {
todo!()
impl PoolConfig {
fn write_to(&self, account: &mut AccountWithMetadata) -> Result<(), SpelError> {
account.account.data = borsh::to_vec(self)
.map_err(|_| SpelError::SerializationError { message: "encoding failed".into() })?
.try_into()
.map_err(|_| SpelError::SerializationError { message: "data too large".into() })?;
Ok(())
}
}
```

The `#[account_type]` struct sits outside the `#[lez_program]` module, the instruction inside it. The handler returns `Ok(SpelOutput::execute(post_states, messages))`, where `post_states` lists your declared accounts in declaration order. The injected `admin_config` and `caller` are appended to the post-states automatically, you only handle the parameters you wrote.

The gate needs two accounts, the `admin_config` PDA holding the current admin state and a signing `caller`. You do not have to write them: the framework injects both from metadata the library declares, and they appear in the IDL like declared parameters. Declaring them explicitly produces the same program, and then they are your parameters, appearing in your post-states list like any other account.

If your instruction already has parameters by different names, point the gate at them with the inject-account names as keys: `#[require_admin(admin_config = my_cfg, caller = owner)]`. The framework also recognises declared parameters by role, a `#[account(signer)]` parameter or a PDA parameter with the matching seed is reused under its declared name instead of being injected twice.

## Become the first admin
Expand Down Expand Up @@ -173,6 +208,8 @@
Instead of a dedicated Config PDA, the admin slot can live inside one of your program's own accounts at a byte offset. Declared once, program wide, on the marker:

```rust
use admin_authority::AdminConfig;

#[account_type]
#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)]
pub struct ProgramConfig {
Expand All @@ -191,28 +228,21 @@
#[instruction]
pub fn initialize(
#[account(init, pda = literal("program_config"))] mut config: AccountWithMetadata,
#[account(signer)] signer: AccountWithMetadata,
) -> SpelResult {
ProgramConfig { value: 0, padding: [0; 24], admin: AdminConfig::default() }
.write_to(&mut config)?;
// ...
ProgramConfig {
value: 0,
padding: [0; 24],
admin: AdminConfig::default(),
}
.write_to(&mut config)?;
// the signing caller is injected, and the injected bootstrap
// installs it as admin in this same transaction
Ok(SpelOutput::execute(vec![config], vec![]))
}
}
```

The `write_to` helper is yours to write, the library does not provide it. The reference sample uses this one:

```rust
impl ProgramConfig {
fn write_to(&self, account: &mut AccountWithMetadata) -> Result<(), SpelError> {
account.account.data = borsh::to_vec(self)
.map_err(|_| SpelError::SerializationError { message: "encoding failed".into() })?
.try_into()
.map_err(|_| SpelError::SerializationError { message: "data too large".into() })?;
Ok(())
}
}
```
`write_to` is the same helper pattern from the gating section, implemented on `ProgramConfig`.

What changes:

Expand Down Expand Up @@ -241,6 +271,8 @@
spel generate-idl path/to/your/program/src/main.rs | jq '.instructions[].name'
```

The `spel` binary must be built from the same framework revision your `Cargo.toml` pins. A CLI built without the extension scanner omits the admin instructions from this output without reporting an error, so the check appears to pass while the surface is missing. The install command in [Install the spel CLI](#install-the-spel-cli) pins the right revision.

Expected output includes:

```
Expand All @@ -249,7 +281,7 @@
"admin_renounce"
```

Plus your own instructions. A marker that matches no discoverable extension is a hard compile error naming the marker, so a broken setup refuses loudly rather than building without the trio. When you hit that error, the most common causes are:
Plus your own instructions. On a framework build that carries the extension scanner, a marker that matches no discoverable extension is a hard compile error naming the marker, so a broken setup refuses loudly rather than building without the trio. That safety net is a property of the pinned framework revision: on a framework without the scanner, upstream `logos-co/spel` main today, the marker is ignored and the program builds cleanly without the trio. When you hit the hard error, the most common causes are:

- `admin-authority` not declared as a direct path or git dependency in your `Cargo.toml`. Transitive dependencies are never discovered.
- `#[admin_authority]` placed outside `#[lez_program]` rather than inside.
Expand Down
Loading
Loading