@exodus/safeguard-solana
v1.2.1
Published
Solana smart contract clients for Exodus Safeguard
Downloads
3,134
Maintainers
Readme
Safeguard Delegation Program
A Solana smart contract enabling multi-agent delegation for SPL tokens. Users authorize multiple AI agent wallets with independent spending limits while maintaining a single SPL token delegation.
Table of Contents
- Problem
- Solution: Proxy Delegate Pattern
- Program IDs
- TypeScript Client
- Account Structures
- Data Model
- Instructions
- Sequence Diagrams
- Security & Trust Model
- Integration with Safeguard
- Development
- Reference
Problem
Solana's SPL Token program only allows one delegate per token account. Safeguard needs multiple agent connections per external wallet, each with independent spending limits.
Solution: Proxy Delegate Pattern
The program acts as an intermediary between the user's token account and multiple agent wallets.
┌──────────────────┐ SPL Approve ┌─────────────────────┐
│ User's Token │─────────────────────>│ Safeguard Program │
│ Account (ATA) │ (one-time setup) │ Authority PDA │
└──────────────────┘ └─────────────────────┘
│
│ Internal routing
▼
┌────────────────────┬────────────────────┬
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Agent Connection │ │ Agent Connection │ │ Agent Connection │
│ Wallet #1 │ │ Wallet #2 │ │ Wallet #3 │
│ (max spend: 100) │ │ (max spend: 500) │ │(max spend: 1000) │
└──────────────────┘ └──────────────────┘ └──────────────────┘Program IDs
| Network | Program ID |
| ------- | ---------------------------------------------- |
| Devnet | 2NFrZTi8A51yYe5GMXQap2up1HLsGuB95oAaNexw2sHM |
| Mainnet | 2NFrZTi8A51yYe5GMXQap2up1HLsGuB95oAaNexw2sHM |
TypeScript Packages
This repository ships two packages:
| Package | Description |
| ------------------------------------ | -------------------------------------------------------- |
| @exodus/safeguard-solana | TypeScript client for building and querying transactions |
| @exodus/safeguard-solana-interface | IDL and TypeScript types for the on-chain program |
Installation
pnpm add @exodus/safeguard-solana
# Optional: only needed if you need the IDL or raw types directly
pnpm add @exodus/safeguard-solana-interfaceUsage
import { SafeguardDelegationClient } from "@exodus/safeguard-solana";
import { safeguardDelegation } from "@exodus/safeguard-solana-interface";
import type { SafeguardDelegation } from "@exodus/safeguard-solana-interface";
const client = new SafeguardDelegationClient("https://api.devnet.solana.com");
// Derive PDA addresses
const vaultPda = client.deriveVaultPda({
owner: ownerPublicKey,
userTokenAccount,
});
const delegationPda = client.deriveDelegationPda({
vault: vaultPda,
agentWallet: agentWalletPublicKey,
});
// Build a transaction (returns base64-encoded serialized tx for the user to sign)
// payer can differ from owner to sponsor transaction fees on the owner's behalf
// mint is required so the client can create the owner's ATA if it doesn't exist yet
const tx = await client.buildInitializeVaultTx({
owner: ownerPublicKey,
payer: payerPublicKey, // covers rent and tx fee; pass ownerPublicKey if owner pays
mint: usdcMintPublicKey,
userTokenAccount,
agent: {
agentWallet: agentWalletPublicKey,
limits: { label: "My AI Agent", maxSpendAmount },
},
});API Reference
| Method | Returns | Description |
| -------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------- |
| deriveVaultPda(params) | PublicKey | Derive vault PDA address |
| deriveDelegationPda(params) | PublicKey | Derive delegation PDA address |
| buildInitializeVaultTx(params) | Promise<string> | Serialized tx: create owner ATA (if needed) + vault; optionally bundles first agent if agent provided |
| buildAddAgentTx(params) | Promise<string> | Serialized tx: add agent to existing vault |
| buildRevokeAgentTx(params) | Promise<string> | Serialized tx: deactivate agent (account kept) |
| buildRestoreAgentTx(params) | Promise<string> | Serialized tx: reactivate a revoked agent |
| buildRemoveAgentTx(params) | Promise<string> | Serialized tx: permanently close agent account; automatically closes vault if it's the last active agent |
| buildUpdateLimitsTx(params) | Promise<string> | Serialized tx: modify agent spending limits |
| buildEmergencyRevokeTx(params) | Promise<string> | Serialized tx: pause vault + zero out SPL delegation (delegate field preserved, amount set to 0) |
| buildUnpauseVaultTx(params) | Promise<string> | Serialized tx: unpause vault + SPL approve for newDelegatedAmount (required param) |
| buildExecuteTransferTx(params) | Promise<string> | Serialized tx: agent executes a token transfer |
| buildCloseVaultTx(params) | Promise<string> | Serialized tx: remove all agent accounts, SPL revoke, then close vault (funder reclaims rent) |
| getAgentBalance(params) | Promise<AgentBalance> | Query current spend capacity for an agent |
| getVaults(owner) | Promise<VaultInfo[]> | Fetch all vaults owned by a wallet |
| getTokenVault(params) | Promise<VaultInfo\|null> | Fetch vault for a specific token mint |
All methods (except getVaults) accept a named param object. See the exported Build*Params, Derive*Params, and Get*Params interfaces in @exodus/safeguard-solana for the exact shape of each.
AgentBalance
getAgentBalance returns the current spending state for an agent:
interface AgentBalance {
maxSpendAmount: NumberUnit; // Configured lifetime cap
totalSpent: NumberUnit; // Amount used so far
tokenAccountBalance: NumberUnit; // Actual balance in token account
expendableBalance: NumberUnit; // min(remaining allowance, token balance)
}expendableBalance is the amount the agent can actually spend right now — capped by both the remaining delegation allowance and the real token account balance.
AgentLimits
Used by buildAddAgentTx to configure an agent's spending parameters:
interface AgentLimits {
label: string; // Human-readable name (max 64 chars)
maxSpendAmount: NumberUnit; // Lifetime spending cap
expiresAt?: number; // Optional Solana slot number for expiry
}VaultInfo
Returned by getVaults and getTokenVault:
interface VaultInfo {
vaultPda: PublicKey; // On-chain vault PDA address
userTokenAccount: PublicKey; // Token account delegated to this vault
mint: PublicKey; // SPL token mint address
isPaused: boolean; // Whether the vault is emergency-paused
activeDelegationsCount: number; // Number of active agents
}Account Structures
DelegationVault
Created per user token account. Tracks total delegation and manages agent connections.
| Field | Type | Description |
| -------------------------- | ------ | -------------------------------------------------------- |
| owner | Pubkey | User's wallet address |
| funder | Pubkey | Account that paid vault rent (receives it back on close) |
| user_token_account | Pubkey | Token account that delegated to this vault |
| mint | Pubkey | SPL token mint address |
| total_delegated_amount | u64 | Amount approved via SPL Token |
| active_delegations_count | u32 | Number of currently active agents |
| total_delegations_count | u32 | Total agents ever added (including revoked/removed) |
| is_paused | bool | Emergency freeze flag |
| bump | u8 | PDA bump seed |
| version | u8 | Account schema version |
AgentDelegation
Created per agent per vault. Tracks lifetime spending limit and usage.
| Field | Type | Description |
| ------------------ | ----------- | ----------------------------------------------------- |
| vault | Pubkey | Parent vault reference |
| funder | Pubkey | Account that paid delegation rent (reclaims on close) |
| agent_wallet | Pubkey | Authorized signer (server wallet) |
| label | String | Human-readable name (max 64 chars) |
| max_spend_amount | u64 | Lifetime spending cap |
| total_spent | u64 | Cumulative amount spent |
| is_active | bool | Whether delegation is active |
| expires_at | Option | Optional expiry slot number |
| bump | u8 | PDA bump seed |
| version | u8 | Account schema version |
PDA Derivation
Vault PDA = hash("vault", owner, userTokenAccount)
Delegation PDA = hash("delegation", vault, agentWallet)Each delegation is cryptographically bound to a specific vault — an agent authorized for Vault A cannot spend from Vault B.
Data Model
On-Chain Entities (Solana - Trustless)
erDiagram
Mint ||--o{ TokenAccount : "mints"
TokenAccount ||--o| DelegationVault : "delegates to"
DelegationVault ||--o{ AgentDelegation : "authorizes"
AgentDelegation }o--|| ServerWallet : "signed by"
Mint {
pubkey address PK
u8 decimals
string symbol
}
TokenAccount {
pubkey address PK
pubkey owner
pubkey mint FK
u64 amount
pubkey delegate
u64 delegated_amount
}
DelegationVault {
pubkey owner PK
pubkey funder
pubkey user_token_account FK
pubkey mint FK
u64 total_delegated_amount
u32 active_delegations_count
u32 total_delegations_count
bool is_paused
u8 bump
u8 version
}
AgentDelegation {
pubkey vault FK
pubkey funder
pubkey agent_wallet FK
string label
u64 max_spend_amount
u64 total_spent
bool is_active
u64 expires_at
u8 bump
u8 version
}
ServerWallet {
pubkey address PK
}Instructions
| Instruction | Signer | Description |
| ------------------ | ------ | ----------------------------------------------------------------------------------------- |
| initialize_vault | Owner | Create vault PDA; the TypeScript client optionally bundles add_agent in the same tx |
| add_agent | Owner | Authorize an additional agent with a spending limit; re-approves SPL delegation |
| revoke_agent | Owner | Deactivate agent (account kept, can be restored); re-approves SPL with reduced total |
| restore_agent | Owner | Reactivate a previously revoked agent; re-approves SPL with restored total |
| remove_agent | Owner | Permanently close agent account, return rent; re-approves SPL with remaining amount |
| execute_transfer | Agent | Transfer within lifetime spend limit |
| update_limits | Owner | Modify agent max_spend_amount and/or expiry; re-approves SPL delegation when amount grows |
| emergency_revoke | Owner | Pause vault + zero out SPL delegation amount (delegate field preserved; amount set to 0) |
| unpause_vault | Owner | Unpause vault and re-approve SPL delegation |
revoke_agent vs remove_agent
Both deactivate an agent, but differ in reversibility and account lifecycle:
| Aspect | revoke_agent | remove_agent |
| --------------- | --------------------------------- | ------------------------------ |
| Agent status | Deactivated (is_active = false) | Deleted |
| Account | Kept on-chain | Closed, rent returned to owner |
| Reversible? | Yes — via restore_agent | No |
| SPL re-approval | Yes — reduces total allocated | Yes — reduces total allocated |
| Use case | Temporary suspension | Permanent removal |
restore_agent will fail if the vault is paused, the delegation has expired, or the vault has reached the maximum agent count.
Sequence Diagrams
1. Vault Initialization
initialize_vault atomically creates the vault PDA, the first agent delegation, and calls the SPL Token approve CPI — all in a single transaction. The user signs only once.
sequenceDiagram
participant User as User Wallet
participant Frontend as Safeguard Frontend
participant Backend as Auth Server
participant Solana as Solana Network
participant Program as Safeguard Program
User->>Frontend: Connect wallet
Frontend->>User: Request wallet signature
User->>Frontend: Sign authentication
Frontend->>Backend: Create vault + first agent request
Backend->>Backend: Generate vault PDA + delegation PDA
Backend->>Frontend: Return initialize_vault transaction
Frontend->>User: Request user signature (initialize_vault tx)
User->>Solana: Sign & send initialize_vault tx
Program->>Program: Create DelegationVault account
Program->>Program: Create AgentDelegation account
Program->>Solana: CPI: SPL Token Approve (vault PDA as delegate)
Program->>Solana: Emit VaultInitialized event
Solana-->>Backend: Transaction confirmed
Backend->>Frontend: Vault + agent created successfully
Frontend->>User: Display vault status2. MCP Tool Execution (Transfer)
sequenceDiagram
participant Agent as AI Agent (ChatGPT/etc)
participant MCP as MCP Server
participant Backend as Auth Server
participant Policy as Policy Evaluator
participant Solana as Solana Network
participant Program as Safeguard Program
Agent->>MCP: tools/call transaction_send
MCP->>Backend: POST /api/mcp (with API key)
Backend->>Backend: Validate API key
Backend->>Backend: Load agent connection
Backend->>Policy: Evaluate transfer request
Policy->>Policy: Check off-chain rules
Policy-->>Backend: Allowed (or denied)
alt Policy Denied
Backend-->>MCP: Error: Policy violation
MCP-->>Agent: Transfer denied by policy
end
Backend->>Solana: Call execute_transfer
Program->>Program: Check vault not paused
Program->>Program: Check delegation active
Program->>Program: Check not expired
Program->>Program: Check cumulative spend <= max_spend_amount
alt Limit Exceeded
Program-->>Backend: Error: SpendLimitExceeded
Backend-->>MCP: Error: On-chain limit exceeded
MCP-->>Agent: Transfer denied by limits
end
Program->>Solana: CPI: SPL Token Transfer
Program->>Program: Update total_spent
Program->>Solana: Emit TransferExecuted event
Solana-->>Backend: Transaction confirmed
Backend-->>MCP: Success response
MCP-->>Agent: Transfer completed3. Agent Revoke and Restore
revoke_agent deactivates an agent without closing its account — useful for temporary suspensions. restore_agent reactivates it.
sequenceDiagram
participant User as User Wallet
participant Frontend as Safeguard Frontend
participant Backend as Auth Server
participant Solana as Solana Network
participant Program as Safeguard Program
User->>Frontend: Pause agent connection
Frontend->>Backend: POST /agent-connections/:id/revoke
Backend->>Solana: Call revoke_agent
Program->>Program: Set delegation.is_active = false
Program->>Program: Decrement vault.active_delegations_count
Program->>Program: Reduce vault.total_allocated by max_spend_amount
Program->>Solana: CPI: SPL Token Approve (updated total)
Program->>Solana: Emit AgentRevoked event
Program->>Solana: Emit VaultStateChanged event
Solana-->>Backend: Transaction confirmed
Backend->>Backend: Update agent status in DB
Backend-->>Frontend: Agent paused
Frontend->>User: Display paused status
Note over User,Program: Agent can no longer execute transfers
User->>Frontend: Restore agent connection
Frontend->>Backend: POST /agent-connections/:id/restore
Backend->>Solana: Call restore_agent
Program->>Program: Check vault not paused
Program->>Program: Check max agents not reached
Program->>Program: Check delegation not expired
Program->>Program: Set delegation.is_active = true
Program->>Program: Increment vault.active_delegations_count
Program->>Program: Restore vault.total_allocated by max_spend_amount
Program->>Solana: CPI: SPL Token Approve (updated total)
Program->>Solana: Emit AgentRestored event
Program->>Solana: Emit VaultStateChanged event
Solana-->>Backend: Transaction confirmed
Backend->>Backend: Update agent status in DB
Backend-->>Frontend: Agent restored
Frontend->>User: Display active status4. Emergency Revoke
sequenceDiagram
participant User as User Wallet
participant Frontend as Safeguard Frontend
participant Backend as Auth Server
participant Solana as Solana Network
participant Program as Safeguard Program
User->>Frontend: Click Emergency Revoke
Frontend->>User: Confirm action
User->>Frontend: Confirm
Frontend->>Backend: POST /vaults/:id/emergency-revoke
Backend->>Solana: Call emergency_revoke
Program->>Program: Set vault.is_paused = true
Program->>Program: Set total_delegated_amount = 0
Program->>Solana: CPI: SPL Token Approve(0)
Program->>Solana: Emit VaultPaused event
Program->>Solana: Emit EmergencyRevokeTriggered event
Solana-->>Backend: Transaction confirmed
Backend->>Backend: Mark all agents as revoked
Backend-->>Frontend: Revoke successful
Frontend->>User: Display revoked status
Note over User,Program: All agent transfers now blocked5. Agent Removal
When removing an agent the TypeScript client checks active_delegations_count on-chain. If the agent being removed is the last active one, the client automatically batches remove_agent + SPL revoke + close_vault into a single transaction, closing all remaining delegation PDAs (including any revoked ones) and the vault itself. The vault funder (not the owner) signs the close_vault instruction and reclaims the vault rent.
sequenceDiagram
participant User as User Wallet
participant Frontend as Safeguard Frontend
participant Backend as Auth Server
participant Client as TS Client
participant Solana as Solana Network
participant Program as Safeguard Program
User->>Frontend: Remove agent connection
Frontend->>User: Confirm removal
User->>Frontend: Confirm
Frontend->>Backend: DELETE /agent-connections/:id
Backend->>Client: buildRemoveAgentTx(params)
Client->>Solana: Fetch vault (active_delegations_count)
alt Last active agent (count == 1)
Client->>Client: Build close_vault tx (removes all PDAs + SPL revoke + vault)
Backend->>Solana: Submit close_vault tx (signed by funder)
Program->>Program: Close all delegation accounts
Client->>Solana: SPL Token Revoke (clears delegate on token account)
Program->>Program: Close vault account (checks delegate cleared)
Program->>Program: Return vault rent to funder
Program->>Solana: Emit AgentRemoved + VaultClosed events
Solana-->>Backend: Transaction confirmed
Backend->>Backend: Delete vault + delegation records from DB
else More agents remain
Client->>Client: Build remove_agent tx
Backend->>Solana: Submit remove_agent tx (signed by user)
Program->>Program: Decrement vault.active_delegations_count
Program->>Program: Reduce vault.total_allocated
Program->>Program: Close delegation account
Program->>Program: Return rent to owner
Program->>Solana: CPI: SPL Token Approve (updated total)
Program->>Solana: Emit AgentRemoved event
Solana-->>Backend: Transaction confirmed
Backend->>Backend: Remove agent delegation record from DB
end
Backend-->>Frontend: Agent removed
Frontend->>User: Display updated agent listSecurity & Trust Model
User Controls
- Create vault with first agent (single transaction)
- Add/remove/revoke/restore additional agents
- Update spending limits
- Emergency freeze all
- Revoke SPL delegation directly (bypass program)
Agent Controls (within limits)
- Execute transfers up to
max_spend_amountlifetime cap - Cannot exceed the lifetime spend limit
- Cannot transfer after expiry
- Cannot reactivate revoked delegation
Authorization Requirements
Before any agent can spend from a user's wallet, a user-signed transaction is required:
| initialize_vault (first agent) | add_agent (additional) |
| ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| User signs one transaction that atomically creates the vault, first AgentDelegation, and approves SPL token delegation | User (vault owner) signs to authorize each new agent_wallet pubkey with its spending limit |
Without a user signature, nothing can be created on-chain.
The Safeguard backend cannot unilaterally add itself as an agent — only the vault owner's signature can authorize an agent.
PDA Isolation (Cross-Vault Protection)
Each agent delegation is cryptographically bound to a specific vault:
Delegation PDA = hash("delegation", vault_pda, agent_wallet)
─────────
│
└── Agent authorized for Vault A
CANNOT spend from Vault BWhat is Trustless (On-Chain Guarantees)
These guarantees are enforced by the Solana program and cannot be bypassed by anyone, including Safeguard:
| Guarantee | Enforcement |
| ------------------------------------ | ----------------------------------------------- |
| Lifetime spend limit cannot exceed | On-chain cumulative check before every transfer |
| Only authorized agents can transfer | PDA derivation + signature verification |
| Users can always emergency revoke | Owner signature required, always succeeds |
| Agents cannot reactivate themselves | Only owner can call add_agent/restore_agent |
| Limits cannot be increased by agents | Only owner can call update_limits |
| Expired delegations cannot transfer | On-chain timestamp check |
What Requires Trusting Safeguard (Off-Chain)
| Aspect | Trust Requirement | | -------------------------- | --------------------------------------- | | Server wallet private keys | Safeguard stores these securely | | API key → wallet mapping | Database integrity | | Policy enforcement | Off-chain rules applied before on-chain | | Transfer initiation | Only on legitimate AI agent requests |
Worst Case: Backend Compromise
If an attacker fully compromises the Safeguard backend:
| Attacker CAN | Attacker CANNOT | | --------------------------------------------------- | -------------------------------------------- | | Access server wallet keys | Exceed on-chain limits | | Initiate transfers for any agent they have keys for | Spend from vaults that never added the agent | | Bypass off-chain policy rules | Prevent user emergency revoke | | | Modify on-chain limits | | | Reactivate revoked agents |
Maximum damage = sum of
(max_spend_amount - total_spent)across all active agents.
The on-chain limits act as a "blast radius cap" — even total backend compromise cannot exceed the spending limits users configured.
User Self-Protection
| Action | Effect | | ----------------------- | ----------------------------- | | Set conservative limits | Reduces maximum possible loss | | Set expiry dates | Automatic agent deactivation | | Monitor on-chain events | Detect unauthorized transfers | | Emergency revoke | Instantly stops all agents | | Direct SPL revoke | Bypass Safeguard entirely |
Trust Model Summary
| Layer | Trust Model | Failure Impact | | ------------------ | --------------- | -------------------------- | | On-chain program | Trustless | Cannot fail (code is law) | | On-chain limits | Trustless | Cannot be exceeded | | User authorization | Trustless | Cannot be bypassed | | Backend security | Trust Safeguard | Limited by on-chain limits | | Policy enforcement | Trust Safeguard | Limited by on-chain limits |
Design Philosophy: The on-chain program assumes the backend might be compromised and enforces hard limits regardless. Users should set max_spend_amount to the maximum they're comfortable losing in a worst-case scenario.
Integration with Safeguard
Architecture Overview
┌─────────────────────────────────────────────────────────────────────────────────┐
│ SAFEGUARD SYSTEM │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Frontend │───>│ Auth Server │───>│ Policy │───>│ On-Chain │ │
│ │ (Next.js) │ │ (Fastify) │ │ Evaluator │ │ (Solana) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ User │ │ Agent │ │ Wallet │ │ Safeguard │ │
│ │ Wallet │ │ Connection │ │ Policy │ │ Delegation │ │
│ │ (Browser) │ │ (DB) │ │ Rules │ │ Program │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘Limit Enforcement Model
The system uses a complementary limit model:
- Off-chain (Policy Evaluator): Nuanced rules (time-of-day, recipient whitelist, AI-based risk)
- On-chain (Solana Program): Hard safety net (lifetime spend cap via
max_spend_amount)
This provides defense-in-depth: even if the off-chain system is compromised, the on-chain limit prevents catastrophic losses.
API Key vs On-Chain Relationship
┌─────────────────────────────────────────────────────────────────────────┐
│ │
│ OFF-CHAIN (Backend) │
│ ┌─────────────┐ ┌─────────────────────┐ ┌────────────────┐ │
│ │ API Key │─────>│ Agent Connection │─────>│ Server Wallet │ │
│ │ (secret) │ │ (database) │ │ (keypair) │ │
│ └─────────────┘ └─────────────────────┘ └────────────────┘ │
│ │ │
│ Used for MCP Stores mapping │ │
│ authentication and metadata │ │
│ │ │
└────────────────────────────────────────────────────────────┼───────────┘
│
Signs transactions │
with private key │
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ │
│ ON-CHAIN (Solana) │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ DelegationVault │ │ AgentDelegation │ │
│ │ (user's vault) │◄─────────►│ agent_wallet = │ │
│ │ │ │ server wallet │ │
│ └─────────────────────┘ │ PUBKEY only │ │
│ └─────────────────────┘ │
│ │
│ The API key NEVER touches the blockchain. │
│ Only the server wallet's PUBLIC KEY is stored on-chain. │
│ │
└─────────────────────────────────────────────────────────────────────────┘Development
Prerequisites
- Rust 1.89+ (for edition 2024 support)
- Solana CLI 3.0+
- Anchor CLI 0.32+
- Node.js 18+
Build
From the repo root:
pnpm contracts:solana:buildOr from the package directory:
# Build TypeScript client + copy IDL to dist/
pnpm build
# Rebuild IDL only (after Rust program changes)
pnpm idl:buildTest
From the repo root:
pnpm contracts:solana:testOr from the package directory:
pnpm testanchor test handles starting the local validator, deploying the program, and running the full test suite automatically.
Deploy
1. Set up your deploy wallet
Copy the env example and set the absolute path to your deploy keypair:
cp .env.example .envEdit .env:
DEPLOY_WALLET_PATH=/absolute/path/to/deploy-wallet.jsonDEPLOY_WALLET_PATH— the upgrade authority wallet used to sign and fund the upgrade transaction. This wallet must be the current upgrade authority for program2NFrZTi8A51yYe5GMXQap2up1HLsGuB95oAaNexw2sHM.
Note: use an absolute path —
~is not expanded in.envfiles.
2. Fund the deploy wallet
The deploy wallet needs enough SOL to cover the program data account rent (~2.4 SOL for the current binary size). Use the Solana faucet UI to airdrop devnet SOL:
Paste the deploy wallet address and request SOL. You can check the balance with:
solana balance <DEPLOY_WALLET_ADDRESS> --url devnet3. Transfer upgrade authority (first time only)
If upgrading an existing program, transfer the upgrade authority to your deploy wallet:
solana program set-upgrade-authority <PROGRAM_ID> \
--new-upgrade-authority <DEPLOY_WALLET_ADDRESS> \
--skip-new-upgrade-authority-signer-check \
--url devnet4. Deploy
From the repo root:
# Devnet
pnpm contracts:solana:deploy:devnet
# Mainnet
pnpm contracts:solana:deploy:mainnetReference
Error Codes
| Code | Name | Description | | ---- | ----------------------------- | -------------------------------------------------------------- | | 6000 | VaultPaused | Vault is paused - no operations allowed | | 6001 | DelegationInactive | Delegation is not active | | 6002 | DelegationExpired | Delegation has expired | | 6003 | SpendLimitExceeded | Transfer exceeds lifetime spend limit | | 6004 | InvalidTokenAccountOwner | Token account not owned by signer | | 6005 | MintMismatch | Token mint doesn't match vault | | 6006 | InsufficientDelegation | SPL delegation insufficient | | 6007 | ArithmeticOverflow | Math overflow | | 6008 | LabelTooLong | Agent label exceeds 64 chars | | 6009 | Unauthorized | Only owner can perform action | | 6010 | InvalidExpiry | Expiry must be in the future | | 6011 | MaxAgentsReached | Vault has reached the maximum agent count | | 6012 | DelegationAlreadyActive | Delegation is already active | | 6013 | InvalidAgentWallet | Agent wallet must differ from vault owner | | 6014 | InvalidAmount | Amount must be greater than zero | | 6015 | AgentVaultMismatch | Agent account does not belong to this vault | | 6016 | VaultStillDelegated | Token account still delegating to vault; revoke before closing | | 6017 | VaultNotPaused | Vault is not paused | | 6018 | InvalidDelegationAccountCount | Remaining accounts count must match total delegation count |
