> ## Documentation Index
> Fetch the complete documentation index at: https://build.onswig.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Authorization Locks

> Hold wallet funds for a delegated role, then spend or release them

This recipe gives a card-processor role the ability to place holds on a wallet's
funds, the way a card network authorizes a payment before settling it. Read
[Authorization Locks](https://build.onswig.com/protocol/authorization-locks.md) for the full rules.

The flow:

1. The owner adds a delegated role with `ManageAuthorizationLocks` and a
   spending limit.
2. The delegate locks part of the wallet's balance. Every other role, the
   owner included, can now spend only the rest.
3. The delegate captures the hold by spending through `SignV2`, which lowers
   the lock by the amount spent. It can release whatever is left. Otherwise the
   hold expires.
4. Once the delegate's holds are inactive, the owner can remove its role, which
   also deletes its lock records.

## Rust Protocol SDK

These examples target the auth-lock draft of
[`swig-protocol-sdk/solana/rust/sdk`](https://github.com/anagrambuild/swig-protocol-sdk/tree/codex/protocol-rust-auth-locks/solana/rust/sdk).
Use that branch through Git or a local checkout while reviewing it. The published
`swig-sdk`, `swig-interface`, and `swig-state` 3.0.0 crates do not yet include
these lock APIs. The direct `swig_interface` examples also require wallet
revision `4628591963a1f6d9196bae67b25fba6c4a9fb0ab`.

The [hosted Developer SDK](https://build.onswig.com/developer-sdk/authorization-locks.md) has separate
TypeScript, Python, and Rust APIs for preparing backend-built transactions.

### Grant the delegated role

The delegate can lock an asset up to its remaining spending allowance for that
mint, minus its existing active holds. The new hold must also fit the asset
account's unreserved, lockable balance; native SOL excludes the rent-exempt
minimum. Each hold lasts at most `max_duration_slots`.

A role with `All` or `AllButManageAuthority` also needs
`ManageAuthorizationLocks` to lock. Its cap is the asset account's unreserved,
lockable balance, still within `max_duration_slots`. A non-root role cannot set
or change its own duration bound; the root role can. To let the root role lock,
have it grant itself the permission:

```rust theme={null}
use swig_sdk::{types::UpdateAuthorityData, Permission};

wallet.update_authority(
    0,
    UpdateAuthorityData::AddActions(vec![Permission::ManageAuthorizationLocks {
        max_duration_slots: 216_000,
    }]),
)?;
```

```rust theme={null}
use swig_sdk::{authority::AuthorityType, Permission};

wallet.add_authority(
    AuthorityType::Ed25519,
    card_authority.pubkey().as_ref(),
    vec![
        // Holds may last at most ~1 day at 400ms slots (less as slot time drops).
        Permission::ManageAuthorizationLocks {
            max_duration_slots: 216_000,
        },
        // Caps the role's SOL holds; charged when the role spends.
        Permission::Sol {
            amount: 2_000_000_000,
            recurring: None,
        },
    ],
)?;
```

### Place a hold

With the wallet switched to the delegate's role:

```rust theme={null}
let slot = wallet.get_current_slot()?;

// Hold 1.5 SOL for the next 400 slots.
wallet.add_sol_authorization_lock(1_500_000_000, slot + 400)?;
```

To hold tokens instead, first grant the delegate a matching token spending
permission with capacity for at least 600 base units. The SOL-only grant above
cannot authorize this hold. SPL Token and Token-2022 mints are supported:

```rust theme={null}
wallet.add_token_authorization_lock(&mint, 600, slot + 400)?;
```

For a token account that is not the wallet's ATA, call
`add_authorization_lock(asset_account, mint.to_bytes(), amount, expiry_slot)`
directly. For native SOL, pass the wallet address and an all-zero mint.

### Read holds

```rust theme={null}
// Every stored lock, including expired and fully captured (amount 0) locks
// that have not been pruned. `amount` is what is still reserved.
let all = wallet.get_authorization_locks()?;

// Only locks that still bind: unexpired, with a non-zero amount.
let active = wallet.get_active_authorization_locks()?;
for lock in active {
    println!(
        "lock {} reserves {} until slot {} (role {})",
        lock.lock_id, lock.amount, lock.expiry_slot, lock.role_id
    );
}
```

`display_swig()` also prints an **AUTHORIZATION LOCKS** section and shows each
role's `ManageAuthorizationLocks` bound.

### Capture a hold

The holder captures by spending. With the wallet switched to the delegate's
role, spend from the locked account through `SignV2`. No other instruction is
needed:

```rust theme={null}
use solana_system_interface::instruction::transfer;

let wallet_address = wallet.get_swig_wallet_address()?;

// Spend 1 SOL of the 1.5 SOL hold. The lock drops to 0.5 SOL.
wallet.sign_v2(vec![transfer(&wallet_address, &merchant, 1_000_000_000)], None)?;
```

After each `SignV2` the delegate signs, the net amount that left each locked
account is taken off its locks on that account, oldest first. Spending more
than the hold takes the rest from free balance, within the role's limits. The
role's spending limits are charged here, not when it locked.

A lock captured to 0 stays stored with `amount == 0`. It no longer binds and
`get_active_authorization_locks` skips it.

Other roles, the owner's `All` role included, can only spend the balance minus
the delegate's remaining hold. If they try to spend into it, the transaction
fails with `AuthorizationLockViolated`.

### Release or prune a hold

Set `lock_id` to the existing hold you selected from `get_authorization_locks()`
or the active-hold output above. Creation returns a transaction signature, not
the lock ID.

```rust theme={null}
wallet.remove_authorization_lock(lock_id)?;
```

The delegate can release its lock at any time, which voids whatever is still
reserved. After a lock expires or is fully captured, any role with `All`,
`AllButManageAuthority`, `ManageAuthority`, or `ManageAuthorizationLocks` can
prune it to reclaim its rent.

### Remove the delegate

Once all of the delegate's holds are inactive (expired or fully captured), the
owner can remove its role. The removal also deletes the delegate's lock records
and refunds their rent with the role's:

```rust theme={null}
// As a role with All or ManageAuthority:
wallet.remove_authority(card_authority.pubkey().as_ref())?;
```

While any of its holds is still active, removal fails with
`AuthorizationLockActive`, even if its other holds have expired. Release the
remaining holds as the delegate, or wait for them to expire.

### Lock from a program (`ProgramExec`)

A `ProgramExec` role can hold `ManageAuthorizationLocks` too, so an on-chain
program can place and release holds. The program spends a hold through
`SignV2` as usual. The SDK lock helpers work with a
`ProgramExecClientRole`. To build the instructions directly, use the
`swig_interface` builders. The program's instruction must pass the Swig config
and the wallet address as its first two accounts.

The authorizing program must also validate the exact Swig lock instruction and
its account metas before approving it: add/remove operation, acting role,
asset/mint, amount and expiry for add, or lock ID and rent recipient for remove.
Bind those values to the caller's authorization, for example by inspecting the
following instruction through the instructions sysvar. A matching prefix and
wallet pair alone do not authorize a particular hold; a generic approval could
otherwise be reused with a changed amount, expiry, or lock ID.

The builder example assumes `authorize_hold_data` and the authorizing program
implement that binding:

```rust theme={null}
use swig_interface::{AddAuthorizationLockV1Instruction, RemoveAuthorizationLockV1Instruction};

// Your program's instruction: accounts 0 and 1 must be the swig config and
// the swig wallet address, and its data must start with the role's prefix.
let proof = Instruction {
    program_id: card_program_id,
    accounts: vec![
        AccountMeta::new_readonly(swig_account, false),
        AccountMeta::new_readonly(wallet_address, false),
        // ...the program's own accounts
    ],
    data: authorize_hold_data,
};

// Returns [proof, add_lock]. Use new_with_program_exec_ix_index to point at
// a proof instruction at a specific transaction index instead.
let ixs = AddAuthorizationLockV1Instruction::new_with_program_exec(
    swig_account,
    fee_payer.pubkey(),
    proof.clone(),
    wallet_address,
    wallet_address,  // asset account: the wallet address for SOL
    card_role_id,
    [0u8; 32],       // all-zero mint for SOL
    1_500_000_000,
    slot + 400,
)?;

// Returns [proof, remove_lock].
let ixs = RemoveAuthorizationLockV1Instruction::new_with_program_exec(
    swig_account,
    fee_payer.pubkey(),
    proof,
    wallet_address,
    card_role_id,
    lock_id,
    None, // the wallet's rent claimer, if one is configured
)?;
```

The Ed25519, Secp256k1, and Secp256r1 remove builders also take the wallet
address, right after the authority arguments:

```rust theme={null}
let ix = RemoveAuthorizationLockV1Instruction::new_with_ed25519_authority(
    swig_account,
    fee_payer.pubkey(),
    card_authority.pubkey(),
    wallet_address,
    card_role_id,
    lock_id,
    None,
)?;
```

## CLI

These commands require a CLI build containing
[wallet PR #240](https://github.com/anagrambuild/swig-wallet/pull/240), which has
merged. Build that revision or a later one until a CLI release includes it.
The CLI remains in `swig-wallet`; the Rust SDK lives in the Protocol SDK repository.

The `swig` CLI has the same operations. Each command also takes
`--authority-type`, `--authority`, and `--authority-kp`, or falls back to the
saved default authority.

Grant the delegated role. Repeat `--permissions` once per permission:

```bash theme={null}
swig add-authority --swig-id <ID> \
    --new-authority <CARD_PUBKEY> --new-authority-type Ed25519 \
    --permissions '{"type":"manageAuthorizationLocks","maxDurationSlots":216000}' \
    --permissions '{"type":"sol","amount":2000000000}' \
    --permissions '{"type":"tokenDestination","mint":"<MINT>","destination":"<MERCHANT_TOKEN_ACCOUNT>","amount":600}'
```

The token permission covers the mint used below and limits its spending to the
merchant's token account. Replace both placeholders with that mint and destination.

Place, list, and release holds as the delegate:

```bash theme={null}
# Hold 1.5 SOL for 400 slots (or use --expiry-slot <SLOT>)
swig add-authorization-lock --swig-id <ID> --amount 1500000000 --duration-slots 400

# Hold tokens in the wallet's ATA for a mint
swig add-authorization-lock --swig-id <ID> --mint <MINT> --amount 600 --duration-slots 400

# List active holds; add --all to include expired and fully captured holds
swig list-authorization-locks --swig-id <ID>

# Release a hold, or prune an expired or fully captured one
swig remove-authorization-lock --swig-id <ID> --lock-id <LOCK_ID>
```

The CLI's command mode has no `SignV2` spend command, so capture holds by
spending through the SDK as shown above. `list-authorization-locks --all` shows
fully captured holds as `fully captured`.

Revoke the delegate's ability to place new holds. This is allowed even while it
holds active locks:

```bash theme={null}
swig update-authority --swig-id <ID> --authority-to-update-id <ROLE_ID> \
    --operation RemoveActionsByType --action-types ManageAuthorizationLocks
```

In interactive mode (`swig -i`), use the **Authorization Locks** menu.

## Common errors

| Error | Cause |
| - | - |
| `PermissionDeniedMissingPermission` | the role lacks `ManageAuthorizationLocks`, even if it has `All` |
| `InvalidAuthorizationLockExpiry` | expiry is not in the future, or exceeds the role's `max_duration_slots` |
| `AuthorizationLockDurationSelfUpdate` | a non-root role tried to set or change its own `max_duration_slots` |
| `AuthorizationLockExceedsSpendLimit` | the role's holds on this asset would exceed what its limits still allow, either when locking or after a spend |
| `AuthorizationLockExceedsBalance` | active holds on the account would exceed its balance |
| `AuthorizationLockNotOwner` | another role tried to release an active hold |
| `AuthorizationLockActive` | the operation (remove role, close wallet, ...) is blocked by an active hold. Removing a role waits until all of its holds are inactive |
| `AuthorizationLockViolated` | a transaction would spend into a hold that another role holds, or change a locked token account's authority, delegate, or state |
| `InvalidSeedSwigAccount` | account 1 is not the wallet's canonical wallet address |
| `PermissionDeniedProgramExecInvalidConfigAccount` / `...InvalidWalletAccount` | the `ProgramExec` proof instruction's first two accounts are not the Swig config and wallet address |

See [Error Types](https://build.onswig.com/reference/error-types.md) for codes.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.