# getAccountInfo
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/er/getAccountInfo
pages/ephemeral-rollups-ers/api-reference/er/openapi/openapi-getAccountInfo.json POST /
Get AccountInfo for a single account.
# getBlockhashForAccounts
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/er/getBlockhashForAccounts
pages/ephemeral-rollups-ers/api-reference/er/openapi/openapi-getBlockhashForAccounts.json POST /
Get blockhash for multiple account addresses.
# getDelegationStatus
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/er/getDelegationStatus
pages/ephemeral-rollups-ers/api-reference/er/openapi/openapi-getDelegationStatus.json POST /
Get delegation status for a single account from Magic Router.
# getIdentity
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/er/getIdentity
pages/ephemeral-rollups-ers/api-reference/er/openapi/openapi-getIdentity.json POST /
Get the identity information of the current ER Validator
# getRoutes
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/er/getRoutes
pages/ephemeral-rollups-ers/api-reference/er/openapi/openapi-getRoutes.json POST /
Get available ephemeral rollup nodes from the Magic Router
# getSignatureStatuses
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/er/getSignatureStatuses
pages/ephemeral-rollups-ers/api-reference/er/openapi/openapi-getSignatureStatuses.json POST /
Returns the confirmation status for one or more signatures
# Introduction
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/er/introduction
MagicBlock Router API documentation
## Overview
The MagicBlock Router API is a JSON-RPC API that implements almost all standard Solana RPC methods and adds router-specific methods. It simplifies multi-node setups by handling node selection and routing behind a single endpoint.
It can be used to retrieve delegation status for accounts and identify the closest node for request execution. For example, `getBlockhashForAccounts` returns a blockhash from the appropriate node based on the provided accounts and their delegation status.
**Mainnet URL:** `https://router.magicblock.app`
**Devnet URL:** `https://devnet-router.magicblock.app`
**API Version:** 2.0
## Main Operations
### Network Information
* [**Get Routes**](/pages/ephemeral-rollups-ers/api-reference/er/getRoutes) - Query available routing information
* [**Get Identity**](/pages/ephemeral-rollups-ers/api-reference/er/getIdentity) - Retrieve identity information
### Account Operations
* [**Get Account Info**](/pages/ephemeral-rollups-ers/api-reference/er/getAccountInfo) - Fetch account information
* [**Get Blockhash For Accounts**](/pages/ephemeral-rollups-ers/api-reference/er/getBlockhashForAccounts) - Get blockhash for specific accounts
### Status & Delegation
* [**Get Signature Statuses**](/pages/ephemeral-rollups-ers/api-reference/er/getSignatureStatuses) - Check transaction signature statuses
* [**Get Delegation Status**](/pages/ephemeral-rollups-ers/api-reference/er/getDelegationStatus) - Query delegation information
## Key Features
* **Solana RPC Coverage** - Implements almost all standard Solana RPC methods through the router
* **Router-Specific Methods** - Adds methods such as `getBlockhashForAccounts` and `getDelegationStatus`
* **Simplified Multi-Node Setup** - Use one router endpoint instead of manually managing multiple node endpoints
* **Fast Route Query** - Efficiently determine routing paths for transactions
* **Account State** - Access current account information and balances
* **Transaction Tracking** - Monitor transaction status and confirmation
* **Delegation Queries** - Check delegation relationships through the router
## Use Cases
* Retrieve account balances and state
* Check transaction confirmation status
* Query route information for transaction routing
* Verify delegation configurations
* Avoid manual multi-node endpoint management
# getAccountInfo
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getAccountInfo
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getAccountInfo.json POST /
Returns all information associated with an account of provided Pubkey.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"method": "getAccountInfo",
"jsonrpc": "2.0",
"params": [
"5RgeA5P8bRaynJovch3zQURfJxXL3QK2JYg1YamSvyLb",
{
"encoding": "base64"
}
],
"id": 0
}'
```
# getBalance
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getBalance
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getBalance.json POST /
Solana JSON-RPC method getBalance.
# getBlock
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getBlock
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getBlock.json POST /
Returns identity and transaction information about a confirmed block in the ledger.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getBlock",
"params": [
378967388,
{
"commitment": "finalized",
"encoding": "json",
"transactionDetails": "full",
"maxSupportedTransactionVersion": 0,
"rewards": false
}
]
}'
```
# getBlockCommitment
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getBlockCommitment
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getBlockCommitment.json POST /
Returns commitment for particular block.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getBlockCommitment",
"params": [
5
]
}'
```
# getBlockHeight
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getBlockHeight
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getBlockHeight.json POST /
Returns the current block height of the node.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getBlockHeight",
"params": [
{
"commitment": "finalized"
}
]
}'
```
# getBlockTime
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getBlockTime
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getBlockTime.json POST /
Returns the estimated production time of a block.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getBlockTime",
"params": [
5
]
}'
```
# getBlocks
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getBlocks
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getBlocks.json POST /
Returns a list of confirmed blocks between two slots.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getBlocks",
"params": [
5,
10,
{
"commitment": "finalized"
}
]
}'
```
# getBlocksWithLimit
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getBlocksWithLimit
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getBlocksWithLimit.json POST /
Returns a list of confirmed blocks starting at the given slot for a given limit.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getBlocksWithLimit",
"params": [
5,
3
]
}'
```
# getFirstAvailableBlock
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getFirstAvailableBlock
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getFirstAvailableBlock.json POST /
Solana JSON-RPC method getFirstAvailableBlock.
# getGenesisHash
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getGenesisHash
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getGenesisHash.json POST /
Solana JSON-RPC method getGenesisHash.
# getHealth
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getHealth
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getHealth.json POST /
Solana JSON-RPC method getHealth.
# getIdentity
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getIdentity
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getIdentity.json POST /
Solana JSON-RPC method getIdentity.
# getLargestAccounts
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getLargestAccounts
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getLargestAccounts.json POST /
Returns the 20 largest accounts, by lamport balance.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getLargestAccounts",
"params": [
{
"commitment": "finalized"
}
]
}'
```
# getLatestBlockhash
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getLatestBlockhash
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getLatestBlockhash.json POST /
Returns the latest blockhash.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getLatestBlockhash",
"params": [
{
"commitment": "processed"
}
]
}'
```
# getLeaderSchedule
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getLeaderSchedule
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getLeaderSchedule.json POST /
Returns the leader schedule for an epoch.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getLeaderSchedule",
"params": [
null,
{
"commitment": "processed",
"identity": "dv2eQHeP4RFrJZ6UeiZWoc3XTtmtZCUKxxCApCDcRNV"
}
]
}'
```
# getMinimumBalanceForRentExemption
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getMinimumBalanceForRentExemption
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getMinimumBalanceForRentExemption.json POST /
Returns minimum balance required to make account rent exempt.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getMinimumBalanceForRentExemption",
"params": [
50,
{
"commitment": "processed"
}
]
}'
```
# getMultipleAccounts
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getMultipleAccounts
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getMultipleAccounts.json POST /
Returns the account information for a list of Pubkeys.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getMultipleAccounts",
"params": [
[
"vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg",
"4fYNw3dojWmQ4dXtSGE9epjRGy9pFSx62YypT7avPYvA"
],
{
"encoding": "base58",
"commitment": "finalized"
}
]
}'
```
# getProgramAccounts
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getProgramAccounts
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getProgramAccounts.json POST /
Returns all accounts owned by the provided program Pubkey.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getProgramAccounts",
"params": [
"4Nd1mBQtrMJVYVfKf2PJy9NZUZdTAsp7D4xWLs4gDB4T",
{
"commitment": "finalized",
"filters": [
{
"dataSize": 17
},
{
"memcmp": {
"offset": 4,
"bytes": "3Mc6vR"
}
}
]
}
]
}'
```
# getRecentPerformanceSamples
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getRecentPerformanceSamples
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getRecentPerformanceSamples.json POST /
Returns a list of recent performance samples, in reverse slot order.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getRecentPerformanceSamples",
"params": [
2
]
}'
```
# getSignatureStatuses
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getSignatureStatuses
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getSignatureStatuses.json POST /
Returns the statuses of a list of signatures.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getSignatureStatuses",
"params": [
[
"4cdd1oX7cfVALfr26tP52BZ6cSzrgnNGtYD7BFhm6FFeZV5sPTnRvg6NRn8yC6DbEikXcrNChBM5vVJnTgKhGhVu"
],
{
"searchTransactionHistory": true
}
]
}'
```
# getSignaturesForAddress
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getSignaturesForAddress
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getSignaturesForAddress.json POST /
Returns signatures for confirmed transactions that include the given address.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getSignaturesForAddress",
"params": [
"Vote111111111111111111111111111111111111111",
{
"commitment": "finalized",
"limit": 1
}
]
}'
```
# getSlot
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getSlot
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getSlot.json POST /
Returns the slot that has reached the given or default commitment level.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getSlot",
"params": [
{
"commitment": "finalized"
}
]
}'
```
# getTokenAccountBalance
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getTokenAccountBalance
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getTokenAccountBalance.json POST /
Returns the token balance of an SPL Token account.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getTokenAccountBalance",
"params": [
"7fUAJdStEuGbc3sM84cKRL6yYaaSstyLSU4ve5oovLS7",
{
"commitment": "finalized"
}
]
}'
```
# getTokenAccountsByDelegate
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getTokenAccountsByDelegate
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getTokenAccountsByDelegate.json POST /
Returns all SPL Token accounts by approved Delegate.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getTokenAccountsByDelegate",
"params": [
"4Nd1mBQtrMJVYVfKf2PJy9NZUZdTAsp7D4xWLs4gDB4T",
{
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
},
{
"commitment": "finalized",
"encoding": "jsonParsed"
}
]
}'
```
# getTokenAccountsByOwner
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getTokenAccountsByOwner
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getTokenAccountsByOwner.json POST /
Returns all SPL Token accounts by token owner.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getTokenAccountsByOwner",
"params": [
"A1TMhSGzQxMr1TboBKtgixKz1sS6REASMxPo1qsyTSJd",
{
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
},
{
"commitment": "finalized",
"encoding": "jsonParsed"
}
]
}'
```
# getTokenLargestAccounts
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getTokenLargestAccounts
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getTokenLargestAccounts.json POST /
Returns the 20 largest accounts of a particular SPL Token type.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getTokenLargestAccounts",
"params": [
"3wyAj7Rt1TWVPZVteFJPLa26JmLvdb1CAKEFZm3NY75E",
{
"commitment": "finalized"
}
]
}'
```
# getTokenSupply
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getTokenSupply
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getTokenSupply.json POST /
Returns the total supply of an SPL Token type.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getTokenSupply",
"params": [
"3wyAj7Rt1TWVPZVteFJPLa26JmLvdb1CAKEFZm3NY75E",
{
"commitment": "finalized"
}
]
}'
```
# getTransaction
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getTransaction
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getTransaction.json POST /
Returns transaction details for a confirmed transaction.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getTransaction",
"params": [
"4ReKprwf3WdLHRrzp4ctPWNBsQDPL3VZz3zMmoZfcGJMJCHh5Vq937mPdyxhCbw54wNnA6hZ7KfNpQdpt13yY7A9",
{
"commitment": "confirmed",
"maxSupportedTransactionVersion": 0,
"encoding": "json"
}
]
}'
```
# getTransactionCount
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getTransactionCount
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getTransactionCount.json POST /
Returns the current transaction count from the ledger.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "getTransactionCount",
"params": [
{
"commitment": "finalized"
}
]
}'
```
# getVersion
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/getVersion
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-getVersion.json POST /
Solana JSON-RPC method getVersion.
# RPC API
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/introduction
Solana JSON-RPC HTTP methods on MagicBlock devnet RPC endpoints.
This section documents the Solana JSON-RPC HTTP method catalog on MagicBlock endpoints:
**Devnet:**
* `https://devnet-as.magicblock.app/`
* `https://devnet-us.magicblock.app/`
* `https://devnet-eu.magicblock.app/`
* `https://devnet-tee.magicblock.app/`
**Mainnet:**
* `https://as.magicblock.app/`
* `https://us.magicblock.app/`
* `https://eu.magicblock.app/`
* `https://mainnet-tee.magicblock.app/`
The method catalog is aligned with Solana RPC HTTP methods:
* [Solana RPC HTTP Methods](https://solana.com/docs/rpc/http)
TEE RPC endpoints may require an authentication token for certain methods. See
[Authorization
guide](/pages/private-ephemeral-rollups-pers/how-to-guide/quickstart#4-authorize)
for details.
# isBlockhashValid
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/isBlockhashValid
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-isBlockhashValid.json POST /
Returns whether a blockhash is still valid or not.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "isBlockhashValid",
"params": [
"J7rBdM6AecPDEZp8aPq5iPSNKVkU5Q76F3oAV4eW5wsW",
{
"commitment": "processed"
}
]
}'
```
# sendTransaction
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/sendTransaction
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-sendTransaction.json POST /
Submits a signed transaction to the cluster for processing.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "sendTransaction",
"params": [
"4hXTCkRzt9WyecNzV1XPgCDfGAZzQKNxLXgynz5QDuWWPSAZBZSHptvWRL3BjCvzUXRdKvHL2b7yGrRQcWyaqsaBCncVG7BFggS8w9snUts67BSh3EqKpXLUm5UMHfD7ZBe9GhARjbNQMLJ1QD3Spr6oMTBU6EhdB4RD8CP2xUxr2u3d6fos36PD98XS6oX8TQjLpsMwncs5DAMiD4nNnR8NBfyghGCWvCVifVwvA8B8TJxE1aiyiv2L429BCWfyzAme5sZW8rDb14NeCQHhZbtNqfXhcp2tAnaAT"
]
}'
```
# simulateTransaction
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/api-reference/rpc/simulateTransaction
pages/ephemeral-rollups-ers/api-reference/rpc/openapi/openapi-rpc-simulateTransaction.json POST /
Simulate sending a transaction.
```bash cURL theme={null}
curl --request POST \
--url https://devnet-as.magicblock.app/ \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "simulateTransaction",
"params": [
"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEEjNmKiZGiOtSZ+g0//wH5kEQo3+UzictY+KlLV8hjXcs44M/Xnr+1SlZsqS6cFMQc46yj9PIsxqkycxJmXT+veJjIvefX4nhY9rY+B5qreeqTHu4mG6Xtxr5udn4MN8PnBt324e51j94YQl285GzN2rYa/E2DuQ0n/r35KNihi/zamQ6EeyeeVDvPVgUO2W3Lgt9hT+CfyqHvIa11egFPCgEDAwIBAAkDZAAAAAAAAAA=",
{
"commitment": "confirmed",
"encoding": "base64",
"replaceRecentBlockhash": true
}
]
}'
```
# Anchor Example
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/how-to-guide/anchor
Learn how to write a simple Anchor program that increments a counter on Solana
This guide will walk you through the process of writing a simple Anchor program that increments a counter. You'll learn how to deploy this program on Solana and interact with it using a React client.
## Software Packages
This program is developed and tested with the following software packages. Other versions may also be compatible.
| Software | Version | Installation Guide |
| ---------- | ------- | --------------------------------------------------------------- |
| **Solana** | 3.1.9 | [Install Solana](https://docs.anza.xyz/cli/install) |
| **Rust** | 1.89.0 | [Install Rust](https://www.rust-lang.org/tools/install) |
| **Anchor** | 1.0.2 | [Install Anchor](https://www.anchor-lang.com/docs/installation) |
## Quick Access to Source Code
If you prefer to dive straight into the code:
## Writing the Anchor Program
Let's break down the key components of our counter program:
## Core Functionality
The program implements two main instructions:
1. `initialize`: Sets the counter to 0
2. `increment`: Increments the counter by 1
Here's the core structure of our program:
```rust theme={null}
#[ephemeral]
#[program]
pub mod public_counter {
use super::*;
/// Initialize the counter.
pub fn initialize(ctx: Context) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count = 0;
Ok(())
}
/// Increment the counter.
pub fn increment(ctx: Context) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count += 1;
Ok(())
}
// ... Additional instructions will be added here
}
```
Nothing special here, just a simple Anchor program that increments a counter. The only difference is that we're adding the `delegate` macro to inject some useful logic to interact with the delegation program.
### Delegating the Counter PDA
In order to delegate the counter PDA, and make it writable in an Ephemeral Rollup session, we need to add an instruction which
internally calls the `delegate_account` function. `delegate_account` will CPI to the delegation program, which upon validation will gain ownership of the account.
After this step, an ephemeral validator can start processing transactions on the counter PDA and propose state diff trough the delegation program.
Inspect transactions details on Solana Explorer
```rust theme={null}
/// Add delegate function to the context
#[delegate]
#[derive(Accounts)]
pub struct DelegateInput<'info> {
pub payer: Signer<'info>,
/// CHECK The pda to delegate
#[account(mut, del)]
pub pda: AccountInfo<'info>,
}
```
```rust theme={null}
/// Delegate the account to the delegation program
/// Set specific validator based on ER, see https://docs.magicblock.gg/pages/ephemeral-rollups-ers/how-to-guide/local-development
pub fn delegate(ctx: Context) -> Result<()> {
ctx.accounts.delegate_pda(
&ctx.accounts.payer,
&[COUNTER_SEED],
DelegateConfig {
// Optionally set a specific validator from the first remaining account
validator: ctx.remaining_accounts.first().map(|acc| acc.key()),
..Default::default()
},
)?;
Ok(())
}
```
### Committing while the PDA is delegated
The ephemeral runtime allows committing the state of the PDA while it is delegated. This is done by building a `MagicIntentBundleBuilder` with the `commit` intent and invoking it.
Inspect transaction details on Solana Explorer
Inspect transaction details on Solana Explorer
```rust theme={null}
use ephemeral_rollups_sdk::ephem::MagicIntentBundleBuilder;
/// Increment the counter and manually commit the account in the Ephemeral Rollup session.
pub fn increment_and_commit(ctx: Context) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count += 1;
// Serialize the Anchor account before the CPI sees it
counter.exit(&crate::ID)?;
MagicIntentBundleBuilder::new(
ctx.accounts.payer.to_account_info(),
ctx.accounts.magic_context.to_account_info(),
ctx.accounts.magic_program.to_account_info(),
)
.commit(&[ctx.accounts.counter.to_account_info()])
.build_and_invoke()?;
Ok(())
}
```
### Undelegating the PDA
Undelegating the PDA is done by building a `MagicIntentBundleBuilder` with the `commit_and_undelegate` intent.
This commits the latest state and returns ownership of the PDA to the owner program.
Inspect transaction details on Solana Explorer
Inspect transaction details on Solana Explorer
```rust theme={null}
use ephemeral_rollups_sdk::ephem::MagicIntentBundleBuilder;
/// Undelegate the account from the delegation program
pub fn undelegate(ctx: Context) -> Result<()> {
MagicIntentBundleBuilder::new(
ctx.accounts.payer.to_account_info(),
ctx.accounts.magic_context.to_account_info(),
ctx.accounts.magic_program.to_account_info(),
)
.commit_and_undelegate(&[ctx.accounts.counter.to_account_info()])
.build_and_invoke()?;
Ok(())
}
```
## Connecting the React Client
The React client is a simple interface that allows you to interact with the Anchor program.
It uses the Anchor bindings to interact with the program and the MagicBlock SDK to interact with the Ephemeral Rollup session. Source lives alongside the program at [`anchor-counter/app`](https://github.com/magicblock-labs/magicblock-engine-examples/tree/main/anchor-counter/app).
Iframes only work with some wallets (e.g. Backpack). Alternatively, try the
deployed demo here: [https://counter-example.magicblock.app/](https://counter-example.magicblock.app/)
### Ephemeral Endpoint Configuration
To interact with the Ephemeral Rollup session, you need to configure the appropriate endpoint:
* For devnet, use the following ephemeral endpoint:
[https://devnet.magicblock.app](https://devnet.magicblock.app)
* For mainnet, please reach out to the MagicBlock team to receive the appropriate endpoint.
* For localhost, download, install, and run the ephemeral validator locally with the appropriate environment variables.
Make sure to update your client configuration to use the correct endpoint based on your development or production environment.
These public RPC endpoints are currently free and supported for development:
Magic Router Devnet: [https://devnet-router.magicblock.app](https://devnet-router.magicblock.app)
Solana Devnet: [https://api.devnet.solana.com](https://api.devnet.solana.com)
ER Devnet: [https://devnet.magicblock.app](https://devnet.magicblock.app)
TEE Devnet: [https://devnet-tee.magicblock.app/](https://devnet-tee.magicblock.app/)
Find out more details
here
.
# Local Development
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/how-to-guide/local-development
Run and test your Native Rust or Anchor programs with a fully local Ephemeral Rollup stack, Surfpool, or a local VRF oracle.
***
### Quick Access
Explore program and test scripts for both Anchor and Native Rust:
Anchor Implementation
Native Rust Implementation
***
## Local Setup Options
You can run Ephemeral Rollups locally in three ways:
* A fully local stack with `mb-test-validator` as the base layer and a local `ephemeral-validator`.
* A local Surfpool instance as the base layer alternative, while still running the rollup locally.
* A local `ephemeral-validator` connected directly to a public base layer such as Devnet.
Use the fully local path when you want everything on your machine. Use Surfpool when you want to keep the Surfpool workflow while testing against a local Ephemeral Rollup. Use the Devnet option when you want a local rollup process without running a local Solana validator.
### Important: upgrade your program with the correct validator identity
When using a local ER validator, connect it to the base layer where the accounts are delegated. If you delegate your PDA to a specific ER validator identity, update the delegation config in your program so commits and undelegations can complete correctly on the base layer.
These public validators are supported for development. Make sure to add the
specific ER validator in your delegation instruction:
**Mainnet**
-
Asia (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Devnet**
-
Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Localnet**
-
Local ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
```bash theme={null}
npm install -g @magicblock-labs/ephemeral-validator@latest
```
`mb-test-validator` starts a local Solana validator you can use as the base layer for a fully local setup.
```bash theme={null}
mb-test-validator --reset
```
This setup uses `http://localhost:8899` for RPC and `ws://localhost:8900` for WebSocket connections.
```bash theme={null}
cargo build-sbf
solana config set --url localhost
solana program deploy YOUR_PROGRAM_PATH
```
```bash theme={null}
anchor build && anchor deploy \
--provider.cluster localnet
```
Connect the local Ephemeral Rollup to the local Solana validator:
```bash theme={null}
ephemeral-validator --remotes "http://localhost:8899" --remotes "ws://localhost:8900" -l "7799" --lifecycle ephemeral
```
The local rollup is exposed on `http://localhost:7799` for RPC and `ws://localhost:7800` for WebSocket connections.
```bash theme={null}
EPHEMERAL_PROVIDER_ENDPOINT=http://localhost:7799 \
EPHEMERAL_WS_ENDPOINT=ws://localhost:7800 \
PROVIDER_ENDPOINT=http://localhost:8899 \
WS_ENDPOINT=ws://localhost:8900 \
yarn test
```
```bash theme={null}
EPHEMERAL_PROVIDER_ENDPOINT="http://localhost:7799" \
EPHEMERAL_WS_ENDPOINT="ws://localhost:7800" \
anchor test \
--provider.cluster localnet \
--skip-local-validator \
--skip-build \
--skip-deploy
```
```bash theme={null}
curl -sL https://run.surfpool.run/ | bash
```
```bash theme={null}
npm install -g @magicblock-labs/ephemeral-validator@latest
```
This example keeps Surfpool local while using Solana Devnet as the upstream base layer:
```bash theme={null}
surfpool start --rpc-url https://api.devnet.solana.com
```
Surfpool exposes the local RPC and WebSocket endpoints that the ephemeral validator connects to.
Point the validator to Surfpool's local RPC and WebSocket endpoints:
```bash theme={null}
ephemeral-validator --remotes "http://localhost:8899" --remotes "ws://localhost:8900" -l "7799" --lifecycle ephemeral
```
```bash theme={null}
solana transfer 0 -u "http://localhost:7799"
```
The transaction should appear in the ER TUI, where you can inspect it and open it in the explorer.
This flow is based on [Running Ephemeral Rollups locally with Surfpool](https://x.com/PiccoGabriele/status/2030045550230524212).
Upgrade your program with MagicBlock delegation and deploy it to Devnet:
```bash theme={null}
cargo build-sbf
solana config set --url devnet
solana program deploy YOUR_PROGRAM_PATH
```
```bash theme={null}
anchor build && anchor deploy \
--provider.cluster devnet
```
```bash theme={null}
npm install -g @magicblock-labs/ephemeral-validator@latest
```
```bash theme={null}
RUST_LOG=info ephemeral-validator \
--lifecycle ephemeral \
--remote-url "https://rpc.magicblock.app/devnet" \
--rpc-port 7799
```
```bash theme={null}
EPHEMERAL_PROVIDER_ENDPOINT=http://localhost:7799 \
EPHEMERAL_WS_ENDPOINT=ws://localhost:7800 \
yarn test
```
```bash theme={null}
EPHEMERAL_PROVIDER_ENDPOINT="http://localhost:7799" \
EPHEMERAL_WS_ENDPOINT="ws://localhost:7800" \
anchor test \
--provider.cluster devnet \
--skip-local-validator \
--skip-build \
--skip-deploy
```
## Run the VRF Oracle Locally
If you also need to test VRF end to end, run a local `vrf-oracle` against a local test queue.
```bash theme={null}
npm install -g @magicblock-labs/ephemeral-validator@latest
```
```bash theme={null}
mb-test-validator --reset
```
```bash theme={null}
ephemeral-validator --remote-url "http://localhost:8899" --rpc-port 7799 --lifecycle ephemeral
```
This oracle adds requests to the local test queue:
```bash theme={null}
VRF_ORACLE_SKIP_PREFLIGHT="true" RPC_URL="http://localhost:8899" WEBSOCKET_URL="ws://localhost:8999" RUST_LOG=info vrf-oracle
```
If your local validator exposes a different WebSocket port, update `WEBSOCKET_URL` accordingly.
***
# Quickstart
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/how-to-guide/quickstart
Any Solana program can be upgraded with Ephemeral Rollups by adding delegation capabilities.
***
**Building with an AI coding agent?** Install the MagicBlock Dev Skill to give your agent MagicBlock-specific patterns — delegation flows, Magic Actions, cranks, VRF, and more.
Quick install for Claude Code:
```bash theme={null}
npx add-skill https://github.com/magicblock-labs/magicblock-dev-skill
```
Using Cursor, Codex, Windsurf, Cline, or another agent? See the [AI Dev Skill](/pages/overview/additional-information/ai-dev-skill) page for all install targets.
### Quick Access
Check out basic counter example:
Anchor Implementation
React Implementation
***
***
## Step-By-Step Guide
Build your program and upgrade it with delegation hooks with MagicBlock's Delegation Program `DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh`:
Write your program}>
Write your Solana program as you normally would.
Add delegation and undelegation hooks in your program
}
>
Add CPI hooks to delegate, commit and undelegate state accounts through
Ephemeral Rollup sessions.
These public validators are supported for development. Make sure to add the
specific ER validator in your delegation instruction:
**Mainnet**
-
Asia (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Devnet**
-
Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Localnet**
-
Local ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
Deploy your program on Solana.}>
Deploy your program directly on Solana using Anchor or Solana CLI.
Ready to execute transactions for delegation and real-time speed
}
>
Send transactions without modifications on-chain and off-chain that also
comply with the SVM RPC specification.
***
## Counter Example
The following software packages may be required, other versions may also be compatible:
| Software | Version | Installation Guide |
| ---------- | ------- | --------------------------------------------------------------- |
| **Solana** | 3.1.9 | [Install Solana](https://docs.anza.xyz/cli/install) |
| **Rust** | 1.89.0 | [Install Rust](https://www.rust-lang.org/tools/install) |
| **Anchor** | 1.0.2 | [Install Anchor](https://www.anchor-lang.com/docs/installation) |
| **Node** | 24.10.0 | [Install Node](https://nodejs.org/en/download/current) |
### Code Snippets
The program implements two main instructions:
1. `initialize`: Sets the counter to 0
2. `increment`: Increments the counter by 1
The program implements specific instructions for delegating and undelegating the counter:
1. `Delegate`: Delegates counter from Base Layer to ER (called on Base Layer)
2. `CommitAndUndelegate`: Schedules sync of counter from ER to Base Layer, and undelegates counter on ER (called on ER)
3. `Commit`: Schedules sync of counter from ER to Base Layer (called on ER)
4. `Undelegate`:
* Schedules sync and undelegation of counter (called on ER)
* Undelegation triggered through callback instruction injected through #\[ephemeral] (called on Base Layer through validator CPI)
The undelegation callback discriminator `[196, 28, 41, 206, 48, 37, 51, 167]`
and its instruction processor must be specified in your program. This
instruction triggered by Delegation Program reverts account ownership on the
Base Layer after calling undelegation on ER.
With [`[#ephemeral]`](/pages/ephemeral-rollups-ers/how-to-guide/quickstart#1-write-program) Anchor macro from MagicBlock's Ephemeral Rollup SDK, the undelegation callback discriminator and processor are injected into your program.
```rust theme={null}
#[ephemeral]
#[program]
pub mod public_counter {
use super::*;
/// Initialize the counter.
pub fn initialize(ctx: Context) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count = 0;
Ok(())
}
/// Increment the counter.
pub fn increment(ctx: Context) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count += 1;
Ok(())
}
/// Delegate the account to the delegation program
/// Set specific validator based on ER, see https://docs.magicblock.gg/pages/get-started/how-integrate-your-program/local-setup
pub fn delegate(ctx: Context) -> Result<()> {
// ...
}
/// Manually commit the counter state in the Ephemeral Rollup session.
pub fn commit(ctx: Context) -> Result<()> {
// ...
}
/// Increment the counter and commit in the same instruction.
pub fn increment_and_commit(ctx: Context) -> Result<()> {
// ...
}
/// Undelegate the account from the delegation program.
pub fn undelegate(ctx: Context) -> Result<()> {
// ...
}
}
pub const COUNTER_SEED: &[u8] = b"counter";
/// Context for initializing counter
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init_if_needed, payer = user, space = 8 + 8, seeds = [COUNTER_SEED], bump)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
/// Context for incrementing counter
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(mut, seeds = [COUNTER_SEED], bump)]
pub counter: Account<'info, Counter>,
}
/// Counter struct
#[account]
pub struct Counter {
pub count: u64,
}
/// Other context for delegation
```
Nothing special here, just a simple Anchor program that increments a counter. The only difference is that we're adding the `ephemeral` macro for undelegation and `delegate` macro to inject some useful logic to interact with the delegation program.
[⬆️ Back to Top](#code-snippets)
These public validators are supported for development. Make sure to add the
specific ER validator in your delegation instruction:
**Mainnet**
-
Asia (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Devnet**
-
Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Localnet**
-
Local ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
1. Add `ephemeral-rollups-sdk` with Anchor features to your program
```bash theme={null}
cargo add ephemeral-rollups-sdk --features anchor
```
Import `delegate`, `commit`, `ephemeral`, `DelegateConfig`, and `MagicIntentBundleBuilder` (which replaces the deprecated `commit_accounts` and `commit_and_undelegate_accounts` helpers):
```rust theme={null}
use ephemeral_rollups_sdk::anchor::{
commit,
delegate,
ephemeral
};
use ephemeral_rollups_sdk::cpi::DelegateConfig;
use ephemeral_rollups_sdk::ephem::MagicIntentBundleBuilder;
```
2. Add `delegate` macro and instruction, `ephemeral` macro, and `undelegate` instruction to your program. Specify your preferred delegation config such as auto commits and specific ER validator:
```rust theme={null}
/// Add delegate function to the context
#[delegate]
#[derive(Accounts)]
pub struct DelegateInput<'info> {
pub payer: Signer<'info>,
/// CHECK: The pda to delegate
#[account(mut, del)]
pub pda: AccountInfo<'info>,
}
```
```rust theme={null}
/// Delegate the account to the delegation program
/// Set specific validator based on ER, see https://docs.magicblock.gg/pages/get-started/how-integrate-your-program/local-setup
pub fn delegate(ctx: Context) -> Result<()> {
ctx.accounts.delegate_pda(
&ctx.accounts.payer,
&[COUNTER_SEED],
DelegateConfig {
// Optionally set a specific validator from the first remaining account
validator: ctx.remaining_accounts.first().map(|acc| acc.key()),
..Default::default()
},
)?;
Ok(())
}
```
```rust theme={null}
use ephemeral_rollups_sdk::ephem::MagicIntentBundleBuilder;
/// Manually commit the counter state in the Ephemeral Rollup session.
pub fn commit(ctx: Context) -> Result<()> {
MagicIntentBundleBuilder::new(
ctx.accounts.payer.to_account_info(),
ctx.accounts.magic_context.to_account_info(),
ctx.accounts.magic_program.to_account_info(),
)
.commit(&[ctx.accounts.counter.to_account_info()])
.build_and_invoke()?;
Ok(())
}
/// Increment the counter and commit the new state in the same instruction.
pub fn increment_and_commit(ctx: Context) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count += 1;
// Serialize the Anchor account before the CPI sees it
counter.exit(&crate::ID)?;
MagicIntentBundleBuilder::new(
ctx.accounts.payer.to_account_info(),
ctx.accounts.magic_context.to_account_info(),
ctx.accounts.magic_program.to_account_info(),
)
.commit(&[ctx.accounts.counter.to_account_info()])
.build_and_invoke()?;
Ok(())
}
```
```rust theme={null}
use ephemeral_rollups_sdk::ephem::MagicIntentBundleBuilder;
/// Undelegate the account from the delegation program.
/// Commits the latest state and returns ownership of the PDA back to the owner program.
pub fn undelegate(ctx: Context) -> Result<()> {
MagicIntentBundleBuilder::new(
ctx.accounts.payer.to_account_info(),
ctx.accounts.magic_context.to_account_info(),
ctx.accounts.magic_program.to_account_info(),
)
.commit_and_undelegate(&[ctx.accounts.counter.to_account_info()])
.build_and_invoke()?;
Ok(())
}
```
> `Delegation` is the process of transferring ownership of one or more of your program's `PDAs` to the delegation program. Ephemeral Validators will then be able to use the `PDAs` to perform transactions in the SVM runtime.
> `Commit` is the process of updating the state of the `PDAs` from ER to the base layer. After the finalization process, the `PDAs` remain locked on base layer.
> `Undelegation` is the process of transferring ownership of the `PDAs` back to your program. On undelegation, the state is committed and it trigger the finalization process. Once state it validated, the `PDAs` are unlocked and can be used as normal on base layer.
[⬆️ Back to Top](#code-snippets)
Now you’re program is upgraded and ready! Build and deploy to the desired
cluster:
```bash theme={null}
anchor build && anchor deploy
```
[⬆️ Back to Top](#code-snippets)
Ready to execute transactions for delegation and real-time speed.
These public validators are supported for development. Make sure to add the
specific ER validator in your delegation instruction:
**Mainnet**
-
Asia (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Devnet**
-
Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Localnet**
-
Local ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
```bash theme={null}
anchor test --skip-build --skip-deploy --skip-local-validator
```
Run the following test:
```typescript theme={null}
const COUNTER_SEED = "counter";
// Set Anchor providers
const provider = new anchor.AnchorProvider(
new anchor.web3.Connection(
process.env.PROVIDER_ENDPOINT || "https://api.devnet.solana.com",
{
wsEndpoint: process.env.PROVIDER_WS_ENDPOINT || undefined,
commitment: "confirmed",
},
),
anchor.Wallet.local(),
);
anchor.setProvider(provider);
const providerEphemeralRollup = new anchor.AnchorProvider(
new anchor.web3.Connection(
process.env.EPHEMERAL_PROVIDER_ENDPOINT ||
"https://devnet-as.magicblock.app/",
{
wsEndpoint:
process.env.EPHEMERAL_WS_ENDPOINT || "wss://devnet-as.magicblock.app/",
commitment: "confirmed",
},
),
anchor.Wallet.local(),
);
// Set program and PDA
const program = anchor.workspace.PublicCounter as Program;
const [counterPDA] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from(COUNTER_SEED)],
program.programId,
);
// Initialize counter on base layer
let initTx = await program.methods
.initialize()
.accounts({
user: provider.wallet.publicKey,
})
.transaction();
const initTxHash = await provider.sendAndConfirm(initTx, [
provider.wallet.payer,
]);
// Increment counter on base layer
let incBaseTx = await program.methods
.increment()
.accounts({
counter: counterPDA,
})
.transaction();
const incBaseTxHash = await provider.sendAndConfirm(incBaseTx, [
provider.wallet.payer,
]);
// Delegate counter to ER
// Pin a specific validator by passing it in remaining_accounts
const ER_VALIDATOR = new anchor.web3.PublicKey(
"MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57", // Asia ER validator
);
let delTx = await program.methods
.delegate()
.accounts({
payer: provider.wallet.publicKey,
pda: counterPDA,
})
.remainingAccounts([
{ pubkey: ER_VALIDATOR, isSigner: false, isWritable: false },
])
.transaction();
const delTxHash = await provider.sendAndConfirm(delTx, [
provider.wallet.payer,
]);
// Increment counter in real time on ER
let incErTx = await program.methods
.increment()
.accounts({
counter: counterPDA,
})
.transaction();
incErTx.feePayer = providerEphemeralRollup.wallet.publicKey;
incErTx.recentBlockhash = (
await providerEphemeralRollup.connection.getLatestBlockhash()
).blockhash;
incErTx = await providerEphemeralRollup.wallet.signTransaction(incErTx);
const incErTxHash = await providerEphemeralRollup.sendAndConfirm(incErTx);
// Commit and undelegate counter from ER back to base layer
let undelTx = await program.methods
.undelegate()
.accounts({
payer: providerEphemeralRollup.wallet.publicKey,
})
.transaction();
undelTx.feePayer = providerEphemeralRollup.wallet.publicKey;
undelTx.recentBlockhash = (
await providerEphemeralRollup.connection.getLatestBlockhash()
).blockhash;
undelTx = await providerEphemeralRollup.wallet.signTransaction(undelTx);
const undelTxHash = await providerEphemeralRollup.sendAndConfirm(undelTx);
```
To make it easier to integrate via the frontend, we created the [Magic Router](/pages/ephemeral-rollups-ers/introduction/magic-router). You send transactions directly to the magic router, and we can determine for you whether it should be routed to the [Ephemeral Rollup](/pages/ephemeral-rollups-ers/introduction/ephemeral-rollup) or base layer.
These public RPC endpoints are currently free and supported for development:
Magic Router Devnet: [https://devnet-router.magicblock.app](https://devnet-router.magicblock.app)
Solana Devnet: [https://api.devnet.solana.com](https://api.devnet.solana.com)
ER Devnet: [https://devnet.magicblock.app](https://devnet.magicblock.app)
TEE Devnet: [https://devnet-tee.magicblock.app/](https://devnet-tee.magicblock.app/)
Find out more details
here
.
[⬆️ Back to Top](#code-snippets)
***
### Advanced Code Snippets
When resizing a delegated PDA:
* PDA must have enough lamports to remain rent-exempt for the new account size.
* If additional lamports are needed, the **payer account must be delegated** to provide the difference.
* PDA must be owned by the program, and the transaction must include any signer(s) required for transferring lamports.
* Use `system_instruction::allocate`
```rust theme={null}
#[account]
pub struct Counter {
pub count: u64,
pub extra_data: Vec,
}
#[derive(Accounts)]
pub struct ResizeCounter<'info> {
#[account(mut)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}
// Resize the counter (e.g., to store more extra_data)
pub fn resize_counter(ctx: Context, new_size: usize) -> Result<()> {
let account_to_resize = &mut ctx.accounts.counter.to_account_info();
let payer = &mut ctx.accounts.payer.to_account_info();
// Calculate rent-exemption for the new size
let rent = Rent::get()?;
let min_balance = rent.minimum_balance(new_size);
// Top up lamports if needed
let current_lamports = **account_to_resize.lamports.borrow();
if current_lamports < min_balance {
let to_transfer = min_balance - current_lamports;
**payer.try_borrow_mut_lamports()? -= to_transfer;
**account_to_resize.try_borrow_mut_lamports()? += to_transfer;
}
// Resize account
account_to_resize.resize(new_size)?;
Ok(())
}
```
[⬆️ Back to Top](#advanced-code-snippets)
Initialize connection with Magic Router before you send transactions dynamically.
These public RPC endpoints are currently free and supported for development:
Magic Router Devnet: [https://devnet-router.magicblock.app](https://devnet-router.magicblock.app)
Choose your preferred SDK to initialize, send and confirm transactions:
* `ephemeral-rollups-kit` for `@solana/kit`
* `ephemeral-rollups-sdk` for `@solana/web.js`
```typescript Kit theme={null}
import { Connection } from "@magicblock-labs/ephemeral-rollups-kit";
// Initialize connection
const connection = await Connection.create(
"https://devnet-router.magicblock.app",
"wss://devnet-router.magicblock.app"
);
// ... create transaction
// Send and confirm transaction
const txHash = await connection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
import { sendAndConfirmTransaction } from "@solana/web3.js";
import { ConnectionMagicRouter } from "@magicblock-labs/ephemeral-rollups-sdk";
// Initialize connection
const connection = new ConnectionMagicRouter(
"https://devnet-router.magicblock.app/",
{ wsEndpoint: "wss://devnet-router.magicblock.app/" }
);
// ... create transaction
// Send and confirm transaction
const txHash = await sendAndConfirmTransaction(connection, tx, [payer], {
skipPreflight: true,
commitment: "confirmed",
});
```
[Learn more about Magic Router](/pages/ephemeral-rollups-ers/introduction/magic-router)
[⬆️ Back to Top](#advanced-code-snippets)
### Quick Access
Explore reference implementation on GitHub
Attach one or more instructions that run automatically on the Solana base layer immediately after an Ephemeral Rollup
(ER) commit.
[Learn more about Magic Action](/pages/ephemeral-rollups-ers/magic-actions/overview)
### 1) Create action instruction
The instruction `update_leaderboard` runs on the base layer immediately after the commit lands. The `#[action]` attribute on its accounts context marks it as callable from a post-commit action.
```rust theme={null}
// program instruction
pub fn update_leaderboard(ctx: Context) -> Result<()> {
let leaderboard = &mut ctx.accounts.leaderboard;
let counter_info = &mut ctx.accounts.counter.to_account_info();
let mut data: &[u8] = &counter_info.try_borrow_data()?;
let counter = Counter::try_deserialize(&mut data)?;
if counter.count > leaderboard.high_score {
leaderboard.high_score = counter.count;
}
msg!(
"Leaderboard updated! High score: {}",
leaderboard.high_score
);
Ok(())
}
// instruction context
#[action]
#[derive(Accounts)]
pub struct UpdateLeaderboard<'info> {
#[account(mut, seeds = [LEADERBOARD_SEED], bump)]
pub leaderboard: Account<'info, Leaderboard>,
/// CHECK: PDA owner depends on: 1) Delegated: Delegation Program; 2) Undelegated: Your program ID
pub counter: UncheckedAccount<'info>,
}
```
### 2) Build the commit instruction with the action
The commit instruction `commit_and_update_leaderboard` runs on the ER. It uses `MagicIntentBundleBuilder` to schedule both the commit and the post-commit action onto `magic_context` — both are applied together when the ER transaction is sealed back to the base layer.
```rust theme={null}
// commit action instruction on ER
pub fn commit_and_update_leaderboard(ctx: Context) -> Result<()> {
// Build the post-commit action that updates the leaderboard on base layer
let instruction_data =
anchor_lang::InstructionData::data(&crate::instruction::UpdateLeaderboard {});
let action_args = ActionArgs::new(instruction_data);
let action_accounts = vec![
ShortAccountMeta {
pubkey: ctx.accounts.leaderboard.key(),
is_writable: true,
},
ShortAccountMeta {
pubkey: ctx.accounts.counter.key(),
is_writable: false,
},
];
let action = CallHandler {
destination_program: crate::ID,
accounts: action_accounts,
args: action_args,
// Signer that pays transaction fees for the action from its escrow PDA
escrow_authority: ctx.accounts.payer.to_account_info(),
compute_units: 200_000,
};
// Schedule commit + post-commit action on magic_context
MagicIntentBundleBuilder::new(
ctx.accounts.payer.to_account_info(),
ctx.accounts.magic_context.to_account_info(),
ctx.accounts.magic_program.to_account_info(),
)
.commit(&[ctx.accounts.counter.to_account_info()])
.add_post_commit_actions([action])
.build_and_invoke()?;
Ok(())
}
// commit action context on ER
#[commit]
#[derive(Accounts)]
pub struct CommitAndUpdateLeaderboard<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(mut, seeds = [COUNTER_SEED], bump)]
pub counter: Account<'info, Counter>,
/// CHECK: Leaderboard PDA - not mut here, writable set in handler
#[account(seeds = [LEADERBOARD_SEED], bump)]
pub leaderboard: UncheckedAccount<'info>,
/// CHECK: Your program ID
pub program_id: AccountInfo<'info>,
}
```
### Execute multiple actions
You can commit multiple accounts and chain several actions in one call. Actions execute sequentially in the order they're passed to `add_post_commit_actions`.
```rust theme={null}
// Chain several actions — they execute sequentially on base layer after the commit lands.
MagicIntentBundleBuilder::new(
ctx.accounts.payer.to_account_info(),
ctx.accounts.magic_context.to_account_info(),
ctx.accounts.magic_program.to_account_info(),
)
.commit(&[
ctx.accounts.counter.to_account_info(),
// ... additional committed accounts
])
.add_post_commit_actions([action_1, action_2, action_3])
.build_and_invoke()?;
```
### Undelegate with actions
Actions can also be chained onto an undelegation — the counter commits, undelegates, and the actions run, all atomically in one ER transaction.
```rust theme={null}
// Commit, undelegate, AND execute actions — all atomically on base layer after the ER transaction seals.
MagicIntentBundleBuilder::new(
ctx.accounts.payer.to_account_info(),
ctx.accounts.magic_context.to_account_info(),
ctx.accounts.magic_program.to_account_info(),
)
.commit_and_undelegate(&[ctx.accounts.counter.to_account_info()])
.add_post_commit_actions([action])
.build_and_invoke()?;
```
[⬆️ Back to Top](#advanced-code-snippets)
Top up a delegated account's lamports on the ER side. The transaction is submitted on the **base layer** and uses the Ephemeral SPL Token program to shuttle lamports to the destination's delegated balance via a single-use lamports PDA.
Common use case: keeping a delegated fee payer funded so it can keep paying its own commits past the default 10-commit sponsorship cap.
Notes:
* Generate a fresh 32-byte salt per top-up via `crypto.getRandomValues` — re-using a salt collides with an existing PDA.
* Submit to the base-layer RPC, not the ER.
* The destination must already be delegated.
```typescript theme={null}
import {
Connection,
Keypair,
PublicKey,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
lamportsDelegatedTransferIx,
deriveLamportsPda,
} from "@magicblock-labs/ephemeral-rollups-sdk";
/**
* Top up a delegated account with lamports.
*
* The transaction is submitted on the BASE LAYER. The Ephemeral SPL Token
* program creates a single-use lamports PDA, funds it from the payer, and
* delegates it so the ER credits the destination's delegated balance.
*/
async function topUpDelegatedAccount(
connection: Connection, // base-layer connection
payer: Keypair,
destination: PublicKey, // delegated account to top up
amountLamports: bigint,
) {
// Generate a fresh 32-byte salt per top-up.
// Re-using a salt collides with an existing lamports PDA and the call fails.
const salt = crypto.getRandomValues(new Uint8Array(32));
const [lamportsPda] = deriveLamportsPda(payer.publicKey, destination, salt);
const ix = await lamportsDelegatedTransferIx(
payer.publicKey,
destination,
amountLamports,
salt,
);
const tx = new Transaction().add(ix);
tx.feePayer = payer.publicKey;
// CRITICAL: send to the base-layer RPC, not the ER.
const sig = await sendAndConfirmTransaction(connection, tx, [payer], {
commitment: "confirmed",
skipPreflight: true,
});
return { sig, lamportsPda };
}
```
[⬆️ Back to Top](#advanced-code-snippets)
### Quick Access
On-Curve Delegation
Required signers for delegating an on-curve account:
1. On-curve account to be delegated
2. Fee payer
Required instructions for delegating on-curve accounts:
1. Assign System Account to Delegation Program
2. Delegate to Delegation Program
```typescript Kit theme={null}
// Create assign instruction
// The on-curve account must sign this instruction to change its owner
const accountSigner = await cryptoKeyPairToTransactionSigner(userKeypair);
const delegationProgramAddress = address(DELEGATION_PROGRAM_ID.toString());
const assignInstruction = getAssignInstruction({
account: accountSigner,
programAddress: delegationProgramAddress,
});
// Create delegate instruction
const delegateInstruction = await createDelegateInstruction({
payer: feePayerAddress,
delegatedAccount: userAddress,
ownerProgram: ownerProgramAddress,
validator: validatorAddress,
});
// Prepare transaction
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayer(feePayerAddress, tx),
(tx) =>
appendTransactionMessageInstructions(
[assignInstruction, delegateInstruction],
tx
)
);
// Send and confirm transaction (fee payer need to sign, on-curve account cannot be signer since delegated)
const txHash = await connection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair, feePayerKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
// Create assign instruction
const assignInstruction = SystemProgram.assign({
accountPubkey: userPubkey,
programId: DELEGATION_PROGRAM_ID,
});
// Create delegate instruction
const delegateInstruction = createDelegateInstruction({
payer: feePayerKeypair.publicKey,
delegatedAccount: userPubkey,
ownerProgram: ownerProgram,
validator: validator,
});
// Create and send transaction (fee payer need to sign, on-curve account cannot be signer since delegated)
const tx = new Transaction().add(assignInstruction, delegateInstruction);
tx.feePayer = feePayerKeypair.publicKey;
const txSignature = await sendAndConfirmTransaction(
connectionBaseLayer,
tx,
[userKeypair, feePayerKeypair],
{
skipPreflight: true,
}
);
```
Direct commit and undelegate through Magic Program only.
```typescript Kit theme={null}
// Create commit and undelegate instruction
const commitAndUndelegateInstruction = createCommitAndUndelegateInstruction(
userAddress,
[userAddress]
);
// Prepare transaction
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayer(feePayerAddress, tx),
(tx) =>
appendTransactionMessageInstructions([commitAndUndelegateInstruction], tx)
);
// Send and confirm transaction on ephemeral connection
const txHash = await ephemeralConnection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair, feePayerKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
// Create commit and undelegate instruction
const commitAndUndelegateInstruction = createCommitAndUndelegateInstruction(
userPubkey,
[userPubkey]
);
// Send and confirm transaction on ephemeral connection
const tx = new Transaction().add(commitAndUndelegateInstruction);
tx.feePayer = feePayerKeypair.publicKey;
const txSignature = await sendAndConfirmTransaction(
ephemeralConnection,
tx,
[userKeypair, feePayerKeypair],
{
skipPreflight: true,
}
);
```
[⬆️ Back to Top](#advanced-code-snippets)
### Quick Access
Explore reference implementation on GitHub
Attach instructions to a delegation that the ER validator runs automatically
inside the rollup, right after the account is delegated — no extra transaction:
* Build the action(s) as standard `Instruction`s and convert them to the
compact payload with `.cleartext()` (public) — encrypted actions are built
off-chain by a client holding the validator key.
* The base-layer delegation program stores the payload in the delegation
record; the ER validator executes it once the account lands in the rollup.
* CPI with `delegate_account_with_actions` instead of the plain `delegate_pda`
helper; the `#[delegate]` macro still provides the buffer/record/metadata
accounts.
```rust theme={null}
/// Reuse the same accounts context as a normal delegation
#[delegate]
#[derive(Accounts)]
pub struct DelegateInput<'info> {
pub payer: Signer<'info>,
/// CHECK: The pda to delegate
#[account(mut, del)]
pub pda: AccountInfo<'info>,
}
```
```rust theme={null}
use anchor_lang::solana_program::instruction::{AccountMeta, Instruction};
use anchor_lang::InstructionData;
use ephemeral_rollups_sdk::cpi::{
delegate_account_with_actions, DelegateAccounts, DelegateConfig,
};
use ephemeral_rollups_sdk::dlp_api::compact::ClearText;
/// Delegate the account AND attach a post-delegation action. The action is stored
/// in the delegation record on the base layer and executed automatically by the ER
/// validator inside the rollup, right after the account is delegated — no extra
/// transaction. Here the action is a self-CPI back into `increment`.
pub fn delegate_with_actions(ctx: Context) -> Result<()> {
let counter_key = ctx.accounts.pda.key();
// The instruction the ER validator runs post-delegation, inside the rollup.
let increment_action = Instruction {
program_id: crate::ID,
accounts: vec![AccountMeta::new(counter_key, false)],
data: crate::instruction::Increment {}.data(),
};
// Convert to the compact, cleartext post-delegation actions payload.
// (Use `cleartext` for public actions; encrypted actions are built off-chain
// by a client that holds the validator key.)
let actions = vec![increment_action].cleartext();
let payer = ctx.accounts.payer.to_account_info();
let pda = ctx.accounts.pda.to_account_info();
let delegate_accounts = DelegateAccounts {
payer: &payer,
pda: &pda,
owner_program: &ctx.accounts.owner_program,
buffer: &ctx.accounts.buffer_pda,
delegation_record: &ctx.accounts.delegation_record_pda,
delegation_metadata: &ctx.accounts.delegation_metadata_pda,
delegation_program: &ctx.accounts.delegation_program,
system_program: &ctx.accounts.system_program,
};
delegate_account_with_actions(
delegate_accounts,
&[COUNTER_SEED],
DelegateConfig {
// Optionally set a specific validator from the first remaining account
validator: ctx.remaining_accounts.first().map(|acc| acc.key()),
..Default::default()
},
actions,
// No extra signers are required by the increment action.
&[],
)?;
Ok(())
}
```
[⬆️ Back to Top](#advanced-code-snippets)
***
### Quick Access
Learn more about private ER, Rust Native implementation, and local development:
Quickstart
Quickstart
Local Development
***
## Solana Explorer
Get insights about your transactions and accounts on Solana:
Official Solana Explorer
Explore Solana Blockchain
## Solana RPC Providers
Send transactions and requests through existing RPC providers:
Free Public Nodes
Free Shared Nodes
Dedicated High-Performance Nodes
## Solana Validator Dashboard
Find real-time updates on Solana's validator infrastructure:
Get Validator Insights
Discover Validator Metrics
## Server Status
Subscribe to Solana's and MagicBlock's server status:
Subscribe to Solana Server Updates
Subscribe to MagicBlock Server Status
***
## MagicBlock Products
Execute real-time, zero-fee transactions securely on Solana.
Protect sensitive data with compliance — built on top of Ephemeral Rollups.
Add private onchain transfers to your app in seconds — compliant by default.
Add provably fair onchain randomness to games, raffles, and real-time apps.
Access low-latency onchain price feeds for trading and DeFi.
***
# Solana Program
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/how-to-guide/rust-program
Learn how to write a simple Rust program that delegates and increments a counter on Solana
***
### Quick Access
Check out basic counter example in other implementations:
Native Rust Implementation
Pinocchio Implementation
Local Development
***
***
## Step-By-Step Guide
Build your program and upgrade it with delegation hooks with MagicBlock’s Delegation Program `DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh`:
Write program and add delegation instructions
}
>
Write your Solana program as you normally would.
Delegate PDA on Base Layer}>
Add CPI hooks to delegate, commit and undelegate state accounts through
Ephemeral Rollup sessions.
These public validators are supported for development. Make sure to add the
specific ER validator in your delegation instruction:
**Mainnet**
-
Asia (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Devnet**
-
Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Localnet**
-
Local ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
Commit PDA on ER}>
Deploy your program directly on Solana using Solana CLI.
Undelegate PDA on ER}>
Send transactions without modifications on-chain and off-chain that also
comply with the SVM RPC specification.
***
## Counter Example
The following software packages may be required, other versions may also be compatible:
| Software | Version | Installation Guide |
| ---------- | ------- | ------------------------------------------------------- |
| **Solana** | 3.1.9 | [Install Solana](https://docs.anza.xyz/cli/install) |
| **Rust** | 1.89.0 | [Install Rust](https://www.rust-lang.org/tools/install) |
| **Node** | 24.10.0 | [Install Node](https://nodejs.org/en/download/current) |
### Code Snippets
The program implements two main instructions:
1. `InitializeCounter`: Initialize and sets the counter to 0 (called on Base Layer)
2. `IncreaseCounter`: Increments the initialized counter by X amount (called on Base Layer or ER)
The program implements specific instructions for delegating and undelegating the counter:
1. `Delegate`: Delegates counter from Base Layer to ER (called on Base Layer)
2. `CommitAndUndelegate`: Schedules sync of counter from ER to Base Layer, and undelegates counter on ER (called on ER)
3. `Commit`: Schedules sync of counter from ER to Base Layer (called on ER)
4. `Undelegate`: Undelegates counter on the Base Layer (called on Base Layer through validator CPI)
5. `IncrementAndCommit`: Increments and commits in a single ER transaction (called on ER)
6. `IncrementAndUndelegate`: Increments, commits, and undelegates in a single ER transaction (called on ER)
The undelegation callback discriminator `[196, 28, 41, 206, 48, 37, 51, 167]`
and its instruction processor must be specified in your program. This
instruction triggered by Delegation Program reverts account ownership on the
Base Layer after calling undelegation on ER.
With [`[#ephemeral]`](/pages/ephemeral-rollups-ers/how-to-guide/quickstart#1-write-program) Anchor macro from MagicBlock's Ephemeral Rollup SDK, the undelegation callback discriminator and processor are injected into your program.
Here's the core structure of our program:
```rust theme={null}
use borsh::BorshDeserialize;
use solana_program::program_error::ProgramError;
pub enum ProgramInstruction {
InitializeCounter,
IncreaseCounter { increase_by: u64 },
Delegate,
CommitAndUndelegate,
Commit,
Undelegate { pda_seeds: Vec> },
IncrementAndCommit { increase_by: u64 },
IncrementAndUndelegate { increase_by: u64 },
}
#[derive(BorshDeserialize)]
struct IncreaseCounterPayload {
increase_by: u64,
}
impl ProgramInstruction {
pub fn unpack(input: &[u8]) -> Result {
// Ensure the input has at least 8 bytes for the variant
if input.len() < 8 {
return Err(ProgramError::InvalidInstructionData);
}
// Extract the first 8 bytes as variant
let (ix_discriminator, rest) = input.split_at(8);
// Match instruction discriminator with process and deserialize payload
Ok(match ix_discriminator {
[0, 0, 0, 0, 0, 0, 0, 0] => Self::InitializeCounter,
[1, 0, 0, 0, 0, 0, 0, 0] => {
let payload = IncreaseCounterPayload::try_from_slice(rest)?;
Self::IncreaseCounter {
increase_by: payload.increase_by,
}
}
[2, 0, 0, 0, 0, 0, 0, 0] => Self::Delegate,
[3, 0, 0, 0, 0, 0, 0, 0] => Self::CommitAndUndelegate,
[4, 0, 0, 0, 0, 0, 0, 0] => Self::Commit,
[5, 0, 0, 0, 0, 0, 0, 0] => {
let payload = IncreaseCounterPayload::try_from_slice(rest)?;
Self::IncrementAndCommit {
increase_by: payload.increase_by,
}
}
[6, 0, 0, 0, 0, 0, 0, 0] => {
let payload = IncreaseCounterPayload::try_from_slice(rest)?;
Self::IncrementAndUndelegate {
increase_by: payload.increase_by,
}
}
[196, 28, 41, 206, 48, 37, 51, 167] => {
let pda_seeds: Vec> = Vec::>::try_from_slice(rest)?;
Self::Undelegate { pda_seeds }
}
_ => return Err(ProgramError::InvalidInstructionData),
})
}
}
```
Your "Undelegate" instruction must have the exact discriminator. It is never
called by you, instead the validator on the Base Layer will callback with a
CPI into your program after undelegating your account on ER.
[⬆️ Back to Top](#code-snippets)
### Delegating the Counter PDA
In order to delegate the counter PDA, and make it writable in an Ephemeral Rollup session, we need to add an instruction which
internally calls the `delegate_account` function. `delegate_account` will CPI to the delegation program, which upon validation will gain ownership of the account.
After this step, an ephemeral validator can start processing transactions on the counter PDA and propose state diff trough the delegation program.
Inspect transactions details on Solana Explorer
```rust theme={null}
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
msg,
pubkey::Pubkey,
};
use ephemeral_rollups_sdk::cpi::{delegate_account, DelegateAccounts, DelegateConfig};
// For Base Layer only
// Set specific validator based on ER, see https://docs.magicblock.gg/pages/get-started/how-integrate-your-program/local-setup
pub fn process_delegate(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
// Get accounts
let account_info_iter = &mut accounts.iter();
let initializer = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;
let pda_to_delegate = next_account_info(account_info_iter)?;
let owner_program = next_account_info(account_info_iter)?;
let delegation_buffer = next_account_info(account_info_iter)?;
let delegation_record = next_account_info(account_info_iter)?;
let delegation_metadata = next_account_info(account_info_iter)?;
let delegation_program = next_account_info(account_info_iter)?;
let validator_account = account_info_iter.next();
// Optional: client-provided validator or default validator
let validator_pubkey: Option = validator_account.map(|acc_info| acc_info.key.clone());
// Prepare counter pda seeds
let seed_1 = b"counter";
let seed_2 = initializer.key.as_ref();
let pda_seeds: &[&[u8]] = &[seed_1, seed_2];
let delegate_accounts = DelegateAccounts {
payer: initializer,
pda: pda_to_delegate,
owner_program,
buffer: delegation_buffer,
delegation_record,
delegation_metadata,
delegation_program,
system_program,
};
let delegate_config = DelegateConfig {
validator: validator_pubkey, // Set delegating ER validator
..Default::default()
};
delegate_account(delegate_accounts, pda_seeds, delegate_config)?;
Ok(())
}
```
[⬆️ Back to Top](#code-snippets)
### Committing while the PDA is delegated
The ephemeral runtime allows committing the state of the PDA while it is delegated. This is done by building a `MagicIntentBundleBuilder` with the `commit` intent.
Inspect transaction details on Solana Explorer
Inspect transaction details on Solana Explorer
```rust theme={null}
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
};
use ephemeral_rollups_sdk::ephem::{FoldableIntentBuilder, MagicIntentBundleBuilder};
// For ER only
pub fn process_commit(
_program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
// Get accounts
let account_info_iter = &mut accounts.iter();
let initializer = next_account_info(account_info_iter)?;
let counter_account = next_account_info(account_info_iter)?;
let magic_program = next_account_info(account_info_iter)?;
let magic_context = next_account_info(account_info_iter)?;
// Signer should be the same as the initializer
if !initializer.is_signer {
msg!("Initializer {} should be the signer", initializer.key);
return Err(ProgramError::MissingRequiredSignature);
}
MagicIntentBundleBuilder::new(
initializer.clone(),
magic_context.clone(),
magic_program.clone(),
)
.commit(&[counter_account.clone()])
.build_and_invoke()?;
Ok(())
}
```
[⬆️ Back to Top](#code-snippets)
### Undelegating the PDA
Undelegating the PDA is done by building a `MagicIntentBundleBuilder` with the `commit_and_undelegate` intent as part of some instruction.
This commits the latest state and returns ownership of the PDA to the owner program. After undelegating and finalizing the state, the validator will create a callback CPI into `undelegate` on the base layer.
The undelegation callback discriminator `[196, 28, 41, 206, 48, 37, 51, 167]`
and its instruction processor must be specified in your program. This
instruction triggered by Delegation Program reverts account ownership on the
Base Layer after calling undelegation on ER.
With [`[#ephemeral]`](/pages/ephemeral-rollups-ers/how-to-guide/quickstart#1-write-program) Anchor macro from MagicBlock's Ephemeral Rollup SDK, the undelegation callback discriminator and processor are injected into your program.
Inspect transaction details on Solana Explorer
Inspect transaction details on Solana Explorer
```rust theme={null}
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
};
use ephemeral_rollups_sdk::cpi::undelegate_account;
use ephemeral_rollups_sdk::ephem::{FoldableIntentBuilder, MagicIntentBundleBuilder};
// For ER only
pub fn process_commit_and_undelegate(
_program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
// Get accounts
let account_info_iter = &mut accounts.iter();
let initializer = next_account_info(account_info_iter)?;
let counter_account = next_account_info(account_info_iter)?;
let magic_program = next_account_info(account_info_iter)?;
let magic_context = next_account_info(account_info_iter)?;
// Signer should be the same as the initializer
if !initializer.is_signer {
msg!("Initializer {} should be the signer", initializer.key);
return Err(ProgramError::MissingRequiredSignature);
}
// Commit and undelegate counter_account on ER
MagicIntentBundleBuilder::new(
initializer.clone(),
magic_context.clone(),
magic_program.clone(),
)
.commit_and_undelegate(&[counter_account.clone()])
.build_and_invoke()?;
Ok(())
}
// For Base Layer CPI callback
pub fn process_undelegate(
program_id: &Pubkey,
accounts: &[AccountInfo],
pda_seeds: Vec>,
) -> ProgramResult {
// Get accounts
let account_info_iter = &mut accounts.iter();
let delegated_pda = next_account_info(account_info_iter)?;
let delegation_buffer = next_account_info(account_info_iter)?;
let initializer = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;
// CPI on Solana
undelegate_account(
delegated_pda,
program_id,
delegation_buffer,
initializer,
system_program,
pda_seeds,
)?;
Ok(())
}
```
[⬆️ Back to Top](#code-snippets)
***
### Advanced Code Snippets
When resizing a delegated PDA:
* PDA must have enough lamports to remain rent-exempt for the new account size.
* If additional lamports are needed, the **payer account must be delegated** to provide the difference.
* PDA must be owned by the program, and the transaction must include any signer(s) required for transferring lamports.
* Use `system_instruction::allocate`
```rust theme={null}
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct Counter {
pub count: u64,
}
// Resize counter account
pub fn resize_counter_account(
counter_acc: &AccountInfo,
payer: &AccountInfo,
program_id: &Pubkey,
new_size: usize,
bump: u8,
) -> ProgramResult {
let rent = Rent::get()?;
let lamports_required = rent.minimum_balance(new_size);
let current_lamports = counter_acc.lamports();
if lamports_required > current_lamports {
let lamports_to_add = lamports_required - current_lamports;
invoke_signed(
&system_instruction::transfer(
&payer.key,
&counter_acc.key,
lamports_to_add,
),
&[payer.clone(), counter_acc.clone()],
&[&[COUNTER_SEED, &[bump]]],
)?;
}
// Allocate new size
invoke_signed(
&system_instruction::allocate(&counter_acc.key, new_size as u64),
&[counter_acc.clone()],
&[&[COUNTER_SEED, &[bump]]],
)?;
// Assign back to program
invoke_signed(
&system_instruction::assign(&counter_acc.key, program_id),
&[counter_acc.clone()],
&[&[COUNTER_SEED, &[bump]]],
)?;
msg!("Counter account resized to {} bytes", new_size);
Ok(())
}
```
[⬆️ Back to Top](#advanced-code-snippets)
Initialize connection with Magic Router before you send transactions dynamically.
These public RPC endpoints are currently free and supported for development:
Magic Router Devnet: [https://devnet-router.magicblock.app](https://devnet-router.magicblock.app)
Choose your preferred SDK to initialize, send and confirm transactions:
* `ephemeral-rollups-kit` for `@solana/kit`
* `ephemeral-rollups-sdk` for `@solana/web.js`
```typescript Kit theme={null}
import { Connection } from "@magicblock-labs/ephemeral-rollups-kit";
// Initialize connection
const connection = await Connection.create(
"https://devnet-router.magicblock.app",
"wss://devnet-router.magicblock.app"
);
// ... create transaction
// Send and confirm transaction
const txHash = await connection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
import { sendAndConfirmTransaction } from "@solana/web3.js";
import { ConnectionMagicRouter } from "@magicblock-labs/ephemeral-rollups-sdk";
// Initialize connection
const connection = new ConnectionMagicRouter(
"https://devnet-router.magicblock.app/",
{ wsEndpoint: "wss://devnet-router.magicblock.app/" }
);
// ... create transaction
// Send and confirm transaction
const txHash = await sendAndConfirmTransaction(connection, tx, [payer], {
skipPreflight: true,
commitment: "confirmed",
});
```
[Learn more about Magic Router](/pages/ephemeral-rollups-ers/introduction/magic-router)
[⬆️ Back to Top](#advanced-code-snippets)
Attach one or more instructions that run automatically on the Solana base
layer immediately after an ER commit, scheduled with `MagicIntentBundleBuilder`.
[Learn more about Magic Action](/pages/ephemeral-rollups-ers/magic-actions/overview)
```rust theme={null}
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
pubkey::Pubkey,
};
use ephemeral_rollups_sdk::ephem::{CallHandler, MagicIntentBundleBuilder};
use ephemeral_rollups_sdk::{ActionArgs, ShortAccountMeta};
// Runs on the ER. Commits the counter to the base layer and attaches a
// post-commit action (`update_leaderboard`) that the base layer executes
// automatically once the commit lands — both applied in one ER transaction.
pub fn process_commit_with_action(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let payer = next_account_info(account_info_iter)?;
let counter = next_account_info(account_info_iter)?;
let leaderboard = next_account_info(account_info_iter)?;
let magic_context = next_account_info(account_info_iter)?;
let magic_program = next_account_info(account_info_iter)?;
// The instruction that runs on the base layer after the commit.
// `args` is your program's own instruction payload (e.g. update_leaderboard).
let action = CallHandler {
destination_program: *program_id,
accounts: vec![
ShortAccountMeta { pubkey: *leaderboard.key, is_writable: true },
ShortAccountMeta { pubkey: *counter.key, is_writable: false },
],
args: ActionArgs::new(update_leaderboard_instruction_data()),
// Signer that pays the action's fees from its escrow PDA.
escrow_authority: payer.clone(),
compute_units: 200_000,
};
// Schedule the commit + post-commit action on magic_context.
MagicIntentBundleBuilder::new(
payer.clone(),
magic_context.clone(),
magic_program.clone(),
)
.commit(&[counter.clone()])
.add_post_commit_actions([action])
.build_and_invoke()?;
Ok(())
}
```
[⬆️ Back to Top](#advanced-code-snippets)
Top up a delegated account's lamports on the ER side. The transaction is submitted on the **base layer** and uses the Ephemeral SPL Token program to shuttle lamports to the destination's delegated balance via a single-use lamports PDA.
Common use case: keeping a delegated fee payer funded so it can keep paying its own commits past the default 10-commit sponsorship cap.
Notes:
* Generate a fresh 32-byte salt per top-up via `crypto.getRandomValues` — re-using a salt collides with an existing PDA.
* Submit to the base-layer RPC, not the ER.
* The destination must already be delegated.
```typescript theme={null}
import {
Connection,
Keypair,
PublicKey,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
lamportsDelegatedTransferIx,
deriveLamportsPda,
} from "@magicblock-labs/ephemeral-rollups-sdk";
/**
* Top up a delegated account with lamports.
*
* The transaction is submitted on the BASE LAYER. The Ephemeral SPL Token
* program creates a single-use lamports PDA, funds it from the payer, and
* delegates it so the ER credits the destination's delegated balance.
*/
async function topUpDelegatedAccount(
connection: Connection, // base-layer connection
payer: Keypair,
destination: PublicKey, // delegated account to top up
amountLamports: bigint,
) {
// Generate a fresh 32-byte salt per top-up.
// Re-using a salt collides with an existing lamports PDA and the call fails.
const salt = crypto.getRandomValues(new Uint8Array(32));
const [lamportsPda] = deriveLamportsPda(payer.publicKey, destination, salt);
const ix = await lamportsDelegatedTransferIx(
payer.publicKey,
destination,
amountLamports,
salt,
);
const tx = new Transaction().add(ix);
tx.feePayer = payer.publicKey;
// CRITICAL: send to the base-layer RPC, not the ER.
const sig = await sendAndConfirmTransaction(connection, tx, [payer], {
commitment: "confirmed",
skipPreflight: true,
});
return { sig, lamportsPda };
}
```
[⬆️ Back to Top](#advanced-code-snippets)
### Quick Access
On-Curve Delegation
Required signers for delegating an on-curve account:
1. On-curve account to be delegated
2. Fee payer
Required instructions for delegating on-curve accounts:
1. Assign System Account to Delegation Program
2. Delegate to Delegation Program
```typescript Kit theme={null}
// Create assign instruction
// The on-curve account must sign this instruction to change its owner
const accountSigner = await cryptoKeyPairToTransactionSigner(userKeypair);
const delegationProgramAddress = address(DELEGATION_PROGRAM_ID.toString());
const assignInstruction = getAssignInstruction({
account: accountSigner,
programAddress: delegationProgramAddress,
});
// Create delegate instruction
const delegateInstruction = await createDelegateInstruction({
payer: feePayerAddress,
delegatedAccount: userAddress,
ownerProgram: ownerProgramAddress,
validator: validatorAddress,
});
// Prepare transaction
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayer(feePayerAddress, tx),
(tx) =>
appendTransactionMessageInstructions(
[assignInstruction, delegateInstruction],
tx
)
);
// Send and confirm transaction (fee payer need to sign, on-curve account cannot be signer since delegated)
const txHash = await connection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair, feePayerKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
// Create assign instruction
const assignInstruction = SystemProgram.assign({
accountPubkey: userPubkey,
programId: DELEGATION_PROGRAM_ID,
});
// Create delegate instruction
const delegateInstruction = createDelegateInstruction({
payer: feePayerKeypair.publicKey,
delegatedAccount: userPubkey,
ownerProgram: ownerProgram,
validator: validator,
});
// Create and send transaction (fee payer need to sign, on-curve account cannot be signer since delegated)
const tx = new Transaction().add(assignInstruction, delegateInstruction);
tx.feePayer = feePayerKeypair.publicKey;
const txSignature = await sendAndConfirmTransaction(
connectionBaseLayer,
tx,
[userKeypair, feePayerKeypair],
{
skipPreflight: true,
}
);
```
Direct commit and undelegate through Magic Program only.
```typescript Kit theme={null}
// Create commit and undelegate instruction
const commitAndUndelegateInstruction = createCommitAndUndelegateInstruction(
userAddress,
[userAddress]
);
// Prepare transaction
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayer(feePayerAddress, tx),
(tx) =>
appendTransactionMessageInstructions([commitAndUndelegateInstruction], tx)
);
// Send and confirm transaction on ephemeral connection
const txHash = await ephemeralConnection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair, feePayerKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
// Create commit and undelegate instruction
const commitAndUndelegateInstruction = createCommitAndUndelegateInstruction(
userPubkey,
[userPubkey]
);
// Send and confirm transaction on ephemeral connection
const tx = new Transaction().add(commitAndUndelegateInstruction);
tx.feePayer = feePayerKeypair.publicKey;
const txSignature = await sendAndConfirmTransaction(
ephemeralConnection,
tx,
[userKeypair, feePayerKeypair],
{
skipPreflight: true,
}
);
```
[⬆️ Back to Top](#advanced-code-snippets)
### Quick Access
Explore reference implementation on GitHub
Attach instructions to a delegation that the ER validator runs automatically
inside the rollup, right after the account is delegated — no extra transaction:
* Build the action(s) as standard `Instruction`s and convert them to the
compact payload with `.cleartext()` (public) — encrypted actions are built
off-chain by a client holding the validator key.
* The base-layer delegation program stores the payload in the delegation
record; the ER validator executes it once the account lands in the rollup.
* CPI with `delegate_account_with_actions` (same `DelegateAccounts` /
`DelegateConfig` as a normal delegation, plus the actions payload).
```rust theme={null}
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
};
use ephemeral_rollups_sdk::cpi::{
delegate_account_with_actions, DelegateAccounts, DelegateConfig,
};
use ephemeral_rollups_sdk::dlp_api::compact::ClearText;
// Delegate a PDA AND attach a post-delegation action. The action is stored in the
// delegation record on the base layer and executed automatically by the ER
// validator inside the rollup, right after the account is delegated — no extra
// transaction. Here the action is a self-CPI back into the program's increment.
pub fn process_delegate_with_actions(
_program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let initializer = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;
let pda_to_delegate = next_account_info(account_info_iter)?;
let owner_program = next_account_info(account_info_iter)?;
let delegation_buffer = next_account_info(account_info_iter)?;
let delegation_record = next_account_info(account_info_iter)?;
let delegation_metadata = next_account_info(account_info_iter)?;
let delegation_program = next_account_info(account_info_iter)?;
let validator_account = account_info_iter.next();
let pda_seeds: &[&[u8]] = &[b"counter", initializer.key.as_ref()];
// The instruction the ER validator runs post-delegation, inside the rollup.
// `data` is your program's own instruction payload (e.g. the increment ix).
let increment_action = Instruction {
program_id: *owner_program.key,
accounts: vec![AccountMeta::new(*pda_to_delegate.key, false)],
data: increment_instruction_data(),
};
// Convert to the compact, cleartext post-delegation actions payload.
// (Use `cleartext` for public actions; encrypted actions are built off-chain
// by a client that holds the validator key.)
let actions = vec![increment_action].cleartext();
let delegate_accounts = DelegateAccounts {
payer: initializer,
pda: pda_to_delegate,
owner_program,
buffer: delegation_buffer,
delegation_record,
delegation_metadata,
delegation_program,
system_program,
};
let delegate_config = DelegateConfig {
validator: validator_account.map(|acc| *acc.key),
..Default::default()
};
delegate_account_with_actions(
delegate_accounts,
pda_seeds,
delegate_config,
actions,
// No extra signers are required by the increment action.
&[],
)?;
Ok(())
}
```
[⬆️ Back to Top](#advanced-code-snippets)
***
## Solana Explorer
Get insights about your transactions and accounts on Solana:
Official Solana Explorer
Explore Solana Blockchain
## Solana RPC Providers
Send transactions and requests through existing RPC providers:
Free Public Nodes
Free Shared Nodes
Dedicated High-Performance Nodes
## Solana Validator Dashboard
Find real-time updates on Solana's validator infrastructure:
Get Validator Insights
Discover Validator Metrics
## Server Status
Subscribe to Solana's and MagicBlock's server status:
Subscribe to Solana Server Updates
Subscribe to MagicBlock Server Status
***
# Test Your Program
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/how-to-guide/rust-tests
Learn how to test a simple Rust program
***
### Quick Access
If you prefer to dive straight into the code:
Tests for Rust Counter
Tests for Pinocchio Counter
Local Development
***
## Step-By-Step Guide
Build valid transactions that calls your program instructions for delegation and undelegation.
The complete test for this project can be found in the [Typescript Test Script](https://github.com/magicblock-labs/magicblock-engine-examples/tree/main/counter/native-rust/tests).
Import SDK and initialize Connection}>
Set up connection and accounts
Delegate PDA on Base Layer}>
Test CPI hook to delegate state account on Base Layer
These public validators are supported for development. Make sure to add the
specific ER validator in your delegation instruction:
**Mainnet**
-
Asia (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Devnet**
-
Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Localnet**
-
Local ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
Commit PDA on ER}>
Test CPI hook to commit state account on ER
Undelegate PDA on ER}>
Test CPI hook to undelegate state account on ER
***
## Counter Example
The following software packages may be required, other versions may also be compatible:
| Software | Version | Installation Guide |
| ---------- | ------- | ------------------------------------------------------- |
| **Solana** | 3.1.9 | [Install Solana](https://docs.anza.xyz/cli/install) |
| **Rust** | 1.89.0 | [Install Rust](https://www.rust-lang.org/tools/install) |
| **Node** | 24.10.0 | [Install Node](https://nodejs.org/en/download/current) |
### Code Snippets
### Import SDK and create Connection
Import relevant libraries `@magicblock-labs/ephemeral-rollups-sdk` or `@magicblock-labs/ephemeral-rollups-kit`. Initialize connection before testing and sending transactions.
```bash kit theme={null}
yarn add @magicblock-labs/ephemeral-rollups-kit@latest
```
```bash web3.js theme={null}
yarn add @magicblock-labs/ephemeral-rollups-sdk@latest
```
These public RPC endpoints are currently free and supported for development:
Magic Router Devnet: [https://devnet-router.magicblock.app](https://devnet-router.magicblock.app)
Solana Devnet: [https://api.devnet.solana.com](https://api.devnet.solana.com)
ER Devnet: [https://devnet.magicblock.app](https://devnet.magicblock.app)
TEE Devnet: [https://devnet-tee.magicblock.app/](https://devnet-tee.magicblock.app/)
Find out more details
here
.
```typescript Kit theme={null}
import {
Instruction,
getAddressEncoder,
getProgramDerivedAddress,
AccountRole,
createKeyPairFromBytes,
getAddressFromPublicKey,
address,
createTransactionMessage,
appendTransactionMessageInstructions,
pipe,
setTransactionMessageFeePayer,
} from "@solana/kit";
import {
Connection,
DELEGATION_PROGRAM_ID,
delegationRecordPdaFromDelegatedAccount,
delegationMetadataPdaFromDelegatedAccount,
delegateBufferPdaFromDelegatedAccountAndOwnerProgram,
MAGIC_CONTEXT_ID,
MAGIC_PROGRAM_ID,
} from "@magicblock-labs/ephemeral-rollups-kit";
// Set up a base and ephemeral connection (alternatively use router, see Magic Router)
const connection = await Connection.create(
process.env.PROVIDER_ENDPOINT || "https://api.devnet.solana.com",
process.env.WS_ENDPOINT || "wss://api.devnet.solana.com"
);
const ephemeralConnection = await Connection.create(
process.env.EPHEMERAL_PROVIDER_ENDPOINT || "https://devnet-as.magicblock.app",
process.env.EPHEMERAL_WS_ENDPOINT || "wss://devnet-as.magicblock.app"
);
// Prepare user
const userKeypair = await initializeSolSignerKeypair();
const userPubkey = await getAddressFromPublicKey(userKeypair.publicKey);
// Get PDA
const addressEncoder = getAddressEncoder();
const [counterPda, bump] = await getProgramDerivedAddress({
programAddress: PROGRAM_ID,
seeds: [Buffer.from("counter"), addressEncoder.encode(userPubkey)],
});
```
```typescript Web3.js theme={null}
import {
Keypair,
PublicKey,
SystemProgram,
Transaction,
TransactionInstruction,
Connection,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
DELEGATION_PROGRAM_ID,
delegationRecordPdaFromDelegatedAccount,
delegationMetadataPdaFromDelegatedAccount,
delegateBufferPdaFromDelegatedAccountAndOwnerProgram,
MAGIC_CONTEXT_ID,
MAGIC_PROGRAM_ID,
GetCommitmentSignature,
} from "@magicblock-labs/ephemeral-rollups-sdk";
// Set up a base and ephemeral connection (alternatively use router, see Magic Router)
const connectionBaseLayer = new Connection(
process.env.PROVIDER_ENDPOINT || "https://api.devnet.solana.com",
{ wsEndpoint: process.env.WS_ENDPOINT || "wss://api.devnet.solana.com" }
);
const connectionEphemeralRollup = new Connection(
process.env.EPHEMERAL_PROVIDER_ENDPOINT ||
"https://devnet-as.magicblock.app/",
{
wsEndpoint:
process.env.EPHEMERAL_WS_ENDPOINT || "wss://devnet-as.magicblock.app/",
}
);
// Create user keypair and airdrop SOL if needed
const userKeypair = initializeSolSignerKeypair();
// Get pda
let [counterPda, bump] = PublicKey.findProgramAddressSync(
[Buffer.from("counter"), userKeypair.publicKey.toBuffer()],
PROGRAM_ID
);
```
[⬆️ Back to Top](#code-snippets)
### Test `delegation` transaction
Create a instruction with the right order and attributes of accounts, and the instruction discriminator for `delegation` of your program. Send the transaction with the instruction to Base Layer (Solana) network.
Inspect transactions details on Solana Explorer
```typescript Kit theme={null}
// "Delegate" transaction
// Add local validator identity to the remaining accounts if running on localnet
const remainingAccounts =
connection.clusterUrlHttp.includes("localhost") ||
connection.clusterUrlHttp.includes("127.0.0.1")
? [
{
address: address("mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev"),
role: AccountRole.READONLY,
},
]
: [];
const accounts = [
{ address: userPubkey, role: AccountRole.WRITABLE_SIGNER },
{ address: SYSTEM_PROGRAM_ADDRESS, role: AccountRole.READONLY },
{ address: counterPda, role: AccountRole.WRITABLE },
{ address: PROGRAM_ID, role: AccountRole.READONLY },
{
address: await delegateBufferPdaFromDelegatedAccountAndOwnerProgram(
counterPda,
PROGRAM_ID
),
role: AccountRole.WRITABLE,
},
{
address: await delegationRecordPdaFromDelegatedAccount(counterPda),
role: AccountRole.WRITABLE,
},
{
address: await delegationMetadataPdaFromDelegatedAccount(counterPda),
role: AccountRole.WRITABLE,
},
{ address: DELEGATION_PROGRAM_ID, role: AccountRole.READONLY },
...remainingAccounts,
];
const serializedInstructionData = Buffer.from(
CounterInstruction.Delegate,
"hex"
);
const delegateIx: Instruction = {
accounts,
programAddress: PROGRAM_ID,
data: serializedInstructionData,
};
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayer(userPubkey, tx),
(tx) => appendTransactionMessageInstructions([delegateIx], tx)
);
// Send and confirm transaction on base layer
const txHash = await connection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
// "Delegate" transaction
// Add local validator identity to the remaining accounts if running on localnet
const remainingAccounts =
connectionEphemeralRollup.rpcEndpoint.includes("localhost") ||
connectionEphemeralRollup.rpcEndpoint.includes("127.0.0.1")
? [
{
pubkey: new PublicKey("mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev"),
isSigner: false,
isWritable: false,
},
]
: [];
const tx = new web3.Transaction();
const keys = [
// Initializer
{
pubkey: userKeypair.publicKey,
isSigner: true,
isWritable: true,
},
// System Program
{
pubkey: web3.SystemProgram.programId,
isSigner: false,
isWritable: false,
},
// Counter Account
{
pubkey: counterPda,
isSigner: false,
isWritable: true,
},
// Owner Program
{
pubkey: PROGRAM_ID,
isSigner: false,
isWritable: false,
},
// Delegation Buffer
{
pubkey: getDelegationBufferPda(counterPda, PROGRAM_ID),
isSigner: false,
isWritable: true,
},
// Delegation Record
{
pubkey: getDelegationRecordPda(counterPda),
isSigner: false,
isWritable: true,
},
// Delegation Metadata
{
pubkey: getDelegationMetadataPda(counterPda),
isSigner: false,
isWritable: true,
},
// Delegation Program
{
pubkey: DELEGATION_PROGRAM_ID,
isSigner: false,
isWritable: false,
},
// ER Validator
...remainingAccounts,
];
const serializedInstructionData = Buffer.from(
CounterInstruction.Delegate,
"hex"
);
const delegateIx = new web3.TransactionInstruction({
keys: keys,
programId: PROGRAM_ID,
data: serializedInstructionData,
});
tx.add(delegateIx);
// Send and confirm transaction to Base Layer
const txHash = await sendAndConfirmTransaction(
connectionBaseLayer,
tx,
[userKeypair],
{
skipPreflight: true,
commitment: "confirmed",
}
);
```
[⬆️ Back to Top](#code-snippets)
### Test `commit` transaction
Create a instruction with the right order and attributes of accounts, and the instruction discriminator for `commit` of your program. Send the transaction with the instruction to ER network.
Inspect transaction details on Solana Explorer
Inspect transaction details on Solana Explorer
```typescript Kit theme={null}
// "Commit" transaction
const accounts = [
{ address: userPubkey, role: AccountRole.WRITABLE_SIGNER },
{ address: counterPda, role: AccountRole.WRITABLE },
{ address: address(MAGIC_PROGRAM_ID.toString()), role: AccountRole.READONLY },
{ address: address(MAGIC_CONTEXT_ID.toString()), role: AccountRole.WRITABLE },
];
const serializedInstructionData = Buffer.from(CounterInstruction.Commit, "hex");
const commitIx: Instruction = {
accounts,
programAddress: PROGRAM_ID,
data: serializedInstructionData,
};
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayer(userPubkey, tx),
(tx) => appendTransactionMessageInstructions([commitIx], tx)
);
// Send and confirm transaction on ER
const txHash = await ephemeralConnection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
// "Commit" transaction
const tx = new web3.Transaction();
const keys = [
// Initializer
{
pubkey: userKeypair.publicKey,
isSigner: true,
isWritable: true,
},
// Counter Account
{
pubkey: counterPda,
isSigner: false,
isWritable: true,
},
// Magic Program
{
pubkey: MAGIC_PROGRAM_ID,
isSigner: false,
isWritable: false,
},
// Magic Context
{
pubkey: MAGIC_CONTEXT_ID,
isSigner: false,
isWritable: true,
},
];
const serializedInstructionData = Buffer.from(CounterInstruction.Commit, "hex");
const commitIx = new web3.TransactionInstruction({
keys: keys,
programId: PROGRAM_ID,
data: serializedInstructionData,
});
tx.add(commitIx);
// Send and confirm transaction to ER
const txHash = await sendAndConfirmTransaction(
connectionEphemeralRollup,
tx,
[userKeypair],
{
skipPreflight: true,
commitment: "confirmed",
}
);
```
[⬆️ Back to Top](#code-snippets)
### Test `undelegation` transaction
Create a instruction with the right order and attributes of accounts, and the instruction discriminator for `undelegation` of your program. Send the transaction with the instruction to ER network.
Inspect transaction details on Solana Explorer
Inspect transaction details on Solana Explorer
```typescript Kit theme={null}
// "Undelegate" transaction
const accounts = [
{ address: userPubkey, role: AccountRole.WRITABLE_SIGNER },
{ address: counterPda, role: AccountRole.WRITABLE },
{ address: address(MAGIC_PROGRAM_ID.toString()), role: AccountRole.READONLY },
{ address: address(MAGIC_CONTEXT_ID.toString()), role: AccountRole.WRITABLE },
];
const serializedInstructionData = Buffer.from(
CounterInstruction.CommitAndUndelegate,
"hex"
);
const undelegateIx: Instruction = {
accounts,
programAddress: PROGRAM_ID,
data: serializedInstructionData,
};
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayer(userPubkey, tx),
(tx) => appendTransactionMessageInstructions([undelegateIx], tx)
);
// Send and confirm transaction on ER
const txHash = await ephemeralConnection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
// "Undelegate" transaction
const tx = new web3.Transaction();
const keys = [
// Initializer
{
pubkey: userKeypair.publicKey,
isSigner: true,
isWritable: true,
},
// Counter Account
{
pubkey: counterPda,
isSigner: false,
isWritable: true,
},
// Magic Program
{
pubkey: MAGIC_PROGRAM_ID,
isSigner: false,
isWritable: false,
},
// Magic Context
{
pubkey: MAGIC_CONTEXT_ID,
isSigner: false,
isWritable: true,
},
];
const serializedInstructionData = Buffer.from(
CounterInstruction.CommitAndUndelegate,
"hex"
);
const undelegateIx = new web3.TransactionInstruction({
keys: keys,
programId: PROGRAM_ID,
data: serializedInstructionData,
});
tx.add(undelegateIx);
// Send and confirm transaction to ER. Afterwards CPI callback will be triggered to "Undelegate" instruction of your program on the Base Layer.
const txHash = await sendAndConfirmTransaction(
connectionEphemeralRollup,
tx,
[userKeypair],
{
skipPreflight: true,
commitment: "confirmed",
}
);
```
[⬆️ Back to Top](#code-snippets)
***
## Solana Explorer
Get insights about your transactions and accounts on Solana:
Official Solana Explorer
Explore Solana Blockchain
## Solana RPC Providers
Send transactions and requests through existing RPC providers:
Free Public Nodes
Free Shared Nodes
Dedicated High-Performance Nodes
## Solana Validator Dashboard
Find real-time updates on Solana's validator infrastructure:
Get Validator Insights
Discover Validator Metrics
## Server Status
Subscribe to Solana's and MagicBlock's server status:
Subscribe to Solana Server Updates
Subscribe to MagicBlock Server Status
***
# Ephemeral Accounts
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/introduction/ephemeral-accounts
Create, resize, and close accounts that live entirely on the Ephemeral Rollup.
Ephemeral accounts are accounts that exist only within the Ephemeral Rollup. A **sponsor** account (which is delegated to the ER) pays rent on behalf of ephemeral accounts at **32 lamports/byte** — 109x cheaper than Solana's base rent.
**Key properties:**
* Born, live, and die entirely on the ER
* Owned by the calling program (inferred from CPI context)
* Funded by a sponsor account's lamports
* Can be created, resized, and closed
***
## The `#[ephemeral_accounts]` Macro
This proc-macro attribute goes on an Anchor `Accounts` struct. It recognizes two custom markers inside `#[account(...)]`:
| Marker | Purpose |
| --------- | ------------------------------------------------------- |
| `sponsor` | Marks the account that pays rent for ephemeral accounts |
| `eph` | Marks an account as ephemeral (ER-only) |
### Validation Rules
* At least one `sponsor` is required if any `eph` fields exist
* Only **one** `sponsor` is allowed per struct
* `eph` cannot be combined with `init` or `init_if_needed` (use the generated methods instead)
* If the sponsor is a PDA (not a `Signer`), it must have `seeds` for PDA signing
### Generated Methods
For a field named `conversation`, the macro generates:
| Method | Signature | Description |
| --------------------------------------- | ----------------------------------- | --------------------------------------- |
| `create_ephemeral_conversation` | `(data_len: u32) -> Result<()>` | Creates the ephemeral account |
| `init_if_needed_ephemeral_conversation` | `(data_len: u32) -> Result<()>` | Creates only if `data_len == 0` |
| `resize_ephemeral_conversation` | `(new_data_len: u32) -> Result<()>` | Grows or shrinks the account |
| `close_ephemeral_conversation` | `() -> Result<()>` | Closes account, refunds rent to sponsor |
***
## Signing Requirements
* **Sponsor**: Must be a signer for all operations (create, resize, close)
* **Ephemeral**: Must be a signer **only on create** (prevents pubkey squatting). Not required for resize or close.
* For PDA accounts, the macro auto-derives signer seeds via `find_program_address`
***
## Rent Model
```rust theme={null}
pub const EPHEMERAL_RENT_PER_BYTE: u64 = 32;
const ACCOUNT_OVERHEAD: u32 = 60;
// rent = (data_len + 60) * 32
pub const fn rent(data_len: u32) -> u64 {
(data_len as u64 + ACCOUNT_OVERHEAD as u64) * EPHEMERAL_RENT_PER_BYTE
}
```
* **Growing**: sponsor pays additional rent to vault
* **Shrinking**: vault refunds excess rent to sponsor
* **Close**: all rent refunded from vault to sponsor
***
## Create an Ephemeral Account
```rust theme={null}
use ephemeral_rollups_sdk::anchor::ephemeral_accounts;
pub fn create_conversation(ctx: Context) -> Result<()> {
ctx.accounts
.create_ephemeral_conversation((8 + Conversation::space_for_message_count(0)) as u32)?;
let conversation = Conversation {
handle_owner: ctx.accounts.profile_owner.handle.clone(),
handle_other: ctx.accounts.profile_other.handle.clone(),
bump: ctx.bumps.conversation,
messages: Vec::new(),
};
let mut data = ctx.accounts.conversation.try_borrow_mut_data()?;
conversation.try_serialize(&mut &mut data[..])?;
Ok(())
}
#[ephemeral_accounts]
#[derive(Accounts)]
pub struct CreateConversation<'info> {
#[account(mut)]
pub authority: Signer<'info>,
#[account(
mut,
sponsor,
seeds = [b"profile", profile_owner.handle.as_bytes()],
bump = profile_owner.bump,
has_one = authority
)]
pub profile_owner: Account<'info, Profile>,
#[account(
seeds = [b"profile", profile_other.handle.as_bytes()],
bump = profile_other.bump,
)]
pub profile_other: Account<'info, Profile>,
/// CHECK: Ephemeral conversation PDA sponsored by the profile.
#[account(
mut,
eph,
seeds = [b"conversation", profile_owner.handle.as_bytes(), profile_other.handle.as_bytes()],
bump
)]
pub conversation: AccountInfo<'info>,
// vault and magic_program are auto-injected by the macro
}
```
After calling `create_ephemeral_*`, you must manually serialize your data struct into the raw account data. The macro allocates space but does not initialize the data.
***
## Resize an Ephemeral Account
```rust theme={null}
pub fn extend_conversation(
ctx: Context,
additional_messages: u32,
) -> Result<()> {
let current_capacity =
Conversation::message_capacity(ctx.accounts.conversation.to_account_info().data_len());
let new_capacity = current_capacity + additional_messages as usize;
ctx.accounts.resize_ephemeral_conversation(
(8 + Conversation::space_for_message_count(new_capacity)) as u32,
)?;
Ok(())
}
#[ephemeral_accounts]
#[derive(Accounts)]
pub struct ExtendConversation<'info> {
#[account(mut)]
pub authority: Signer<'info>,
#[account(mut, sponsor, seeds = [...], bump = ..., has_one = authority)]
pub profile_sender: Account<'info, Profile>,
#[account(seeds = [...], bump = ...)]
pub profile_other: Account<'info, Profile>,
/// CHECK: Ephemeral conversation PDA
#[account(mut, eph, seeds = [...], bump)]
pub conversation: AccountInfo<'info>,
}
```
***
## Close an Ephemeral Account
```rust theme={null}
pub fn close_conversation(ctx: Context) -> Result<()> {
let profile = &mut ctx.accounts.profile_owner;
profile.active_conversation_count = profile
.active_conversation_count
.checked_sub(1)
.ok_or(ChatError::ConversationCountUnderflow)?;
ctx.accounts.close_ephemeral_conversation()?;
Ok(())
}
#[ephemeral_accounts]
#[derive(Accounts)]
pub struct CloseConversation<'info> {
#[account(mut)]
pub authority: Signer<'info>,
#[account(mut, sponsor, seeds = [...], bump = ..., has_one = authority)]
pub profile_owner: Account<'info, Profile>,
#[account(seeds = [...], bump = ...)]
pub profile_other: Account<'info, Profile>,
/// CHECK: Ephemeral conversation PDA
#[account(mut, eph, seeds = [...], bump)]
pub conversation: AccountInfo<'info>,
}
```
***
## Using a Wallet as Sponsor
A `Signer` can be used directly as the sponsor instead of a PDA:
```rust theme={null}
#[ephemeral_accounts]
#[derive(Accounts)]
pub struct CreateGame<'info> {
#[account(mut, sponsor)]
pub payer: Signer<'info>,
/// CHECK: Ephemeral PDA
#[account(mut, eph, seeds = [b"game", payer.key().as_ref()], bump)]
pub game_state: AccountInfo<'info>,
}
```
***
## TypeScript Client Usage
All ephemeral account operations are sent to the **ER connection**, not the base layer:
```typescript theme={null}
// Create
await erProgram.methods
.createConversation()
.accounts({
authority: userA.publicKey,
profileOwner: profileAPda,
profileOther: profileBPda,
conversation: conversationPda,
systemProgram: SystemProgram.programId,
})
.rpc();
// Resize
await erProgram.methods
.extendConversation(5)
.accounts({
authority: userA.publicKey,
profileSender: profileAPda,
profileOther: profileBPda,
})
.rpc();
// Close
await erProgram.methods
.closeConversation()
.accounts({
authority: userA.publicKey,
profileOwner: profileAPda,
profileOther: profileBPda,
conversation: conversationPda,
})
.rpc();
```
***
## Common Gotchas
`eph` fields must use `AccountInfo<'info>`, not `Account<'info, T>`. The account doesn't exist yet at validation time, so Anchor cannot deserialize it.
After calling `create_ephemeral_*`, you must serialize your data struct into the raw account data yourself. The macro allocates space but does not write any data.
The macro enforces this at compile time. Use the generated `create_ephemeral_*` method instead of Anchor's `init` constraint.
The sponsor account needs lamports on the ER to pay ephemeral rent. It must be delegated before creating ephemeral accounts.
Transfer extra SOL to the sponsor account before delegating it, so it has enough lamports to fund ephemeral accounts on the ER.
You don't need to declare them in your struct, but they appear in the IDL and must be passed from the client. Anchor resolves them automatically if named correctly.
***
## Learn More
Full example program on GitHub
How delegation and state synchronization work
Build your first program with Ephemeral Rollups
# Delegation, Commitment & Undelegation
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/introduction/ephemeral-rollup
Execute Transactions, Synchronize States, all in real-time.
[Magicblock's Ephemeral Rollup](/pages/overview/additional-information/whitepaper) **leverages the Solana Virtual Machine (SVM)’s account-based structure and parallel execution** to optimize state management. By structuring the state into **clusters**, users can **lock one or multiple accounts** and temporarily shift state execution to a **dedicated auxiliary layer**— "Ephemeral Rollup (ER)". A dynamic fraud-proof mechanism enables fast state finalization through a decentralized Security Committee, see [whitepaper](/public/Ephemeral_Rollups_Fraud_Proof.pdf).
***
## Account Lifecycle for executing transactions in real-time with ER:
State accounts must be delegated to an specific ER validator first by
changing the account owner to the [Delegation
Program](https://github.com/magicblock-labs/delegation-program)
`DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh` and specifying parameters
like ER validator, account lifetime and synchronization frequency.
These public validators are supported for development. Make sure to add the
specific ER validator in your delegation instruction:
**Mainnet**
-
Asia (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Devnet**
-
Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Localnet**
-
Local ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
Delegated state accounts are updated in real-time with transactions on the
ER directly or via [Magic
Router](/pages/ephemeral-rollups-ers/introduction/magic-router). The
**initial transaction on ER clones the delegated account** from base layer
to the ephemeral rollup.
The operator commits the ephemeral state to the base layer **periodically or
on-demand**, including new state and relevant pointers. The account state is
finalized using a fraud-proof mechanism as detailed in the paper.
Delegated account states can continuously be updated in real-time on the ER
directly or via [Magic
Router](/pages/ephemeral-rollups-ers/introduction/magic-router).
Delegated account states are committed through ER validator to the base
layer and the account owner are reversed from the [Delegation
Program](https://github.com/magicblock-labs/delegation-program)
`DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh` to the original owner.
# FAQ
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/introduction/faq
Execute Transactions, Synchronize States, all in real-time.
**No.** Delegated account must exist on Solana beforehand. Delegated accounts are cloned after requesting the account on ER or submitting a transaction with the delegated account.
**No.** Program accounts are never delegated. Only state accounts can be delegated, while **program accounts are cloned** when a transaction is submitted on ER and subscribed for updates thereafter.
**Yes.** Delegated accounts can benefit from all Solana programs and accounts. Every account on Solana is readable on ER, while only delegated account can be changed on ER within an atomic transaction.
**No.** As of 2025-11-20, Solana uses a **400 ms slot time**, while MagicBlock’s Execution Runtime (ER) operates with a **50 ms slot time**. Developers may be tempted to rely on slot time as a fixed-duration measure; however, **slot times are not guaranteed and may change over time**. Code or logic that depends on a specific slot duration should therefore be written with caution.
# Magic Router
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/introduction/magic-router
Learn how Magicblock's Magic Router accelerates transactions by routing them to the right execution environment.
***
## Dynamic Transaction Routing
**Magicblock's Magic Router** is an dynamic transaction routing engine that accelerates transactions by intelligently deciding where they should be executed — either on **Ephemeral Rollups** or **Solana** — based on transaction metadata.
This eliminates the need for manual routing logic from the developer, providing significant benefits in transaction speed and development experience.
* ✅ **Simple Integration with Single Endpoint**: Just connect to a single RPC endpoint.
* ✅ **Seamless Wallet Experience**: Connect, sign, and submit — no need to know what’s happening behind the scenes.
* ✅ **Faster Confirmations**: Magic Router routes to the fastest available endpoint.
***
## Quick Access
Integrate with an Anchor program
Integrate with a Native Rust program
Experiment with Magic Router API
***
## Code Snippets
Initialize connection with Magic Router before you send transactions dynamically.
These public RPC endpoints are currently free and supported for development:
Magic Router Devnet: [https://devnet-router.magicblock.app](https://devnet-router.magicblock.app)
Choose your preferred SDK to initialize, send and confirm transactions:
* `ephemeral-rollups-kit` for `@solana/kit`
* `ephemeral-rollups-sdk` for `@solana/web.js`
```typescript Kit theme={null}
import { Connection } from "@magicblock-labs/ephemeral-rollups-kit";
// Initialize connection
const connection = await Connection.create(
"https://devnet-router.magicblock.app",
"wss://devnet-router.magicblock.app"
);
// ... create transaction
// Send and confirm transaction
const txHash = await connection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
import { sendAndConfirmTransaction } from "@solana/web3.js";
import { ConnectionMagicRouter } from "@magicblock-labs/ephemeral-rollups-sdk";
// Initialize connection
const connection = new ConnectionMagicRouter(
"https://devnet-router.magicblock.app/",
{ wsEndpoint: "wss://devnet-router.magicblock.app/" }
);
// ... create transaction
// Send and confirm transaction
const txHash = await sendAndConfirmTransaction(connection, tx, [payer], {
skipPreflight: true,
commitment: "confirmed",
});
```
The Magic Router analyzes each transaction’s metadata (e.g. writable accounts, owner, and signer) and automatically routes it to the closest dedicated endpoint:
1. **Client - Transaction Submission** The dApp or user sends a transaction to the
Magic Router RPC endpoint.
2. **RPC - Metadata Inspection**
The Magic Router inspects the transaction metadata and checks the owner of writable accounts.
3. **Validator - Smart Routing and Execution**
Based on the metadata, the router determines whether to send it to:
* **Ephemeral Rollup** for fast, low-latency, zero-cost execution
* **Solana** for persistent, high-throughput execution
***
# Ephemeral Rollup
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/introduction/why
The high-performance engine for real-time applications on Solana
[MagicBlock' Ephemeral
Rollup](/pages/ephemeral-rollups-ers/introduction/ephemeral-rollup) is an
extension of the Solana network designed for high-performance decentralized
applications. It enhances Solana’s capabilities while preserving its
composability and integrity.
***
## Why Ephemeral Rollups?
While blockchain technology is revolutionizing decentralized applications, it still faces fundamental challenges in:
* **Latency** – Blockchain transaction speeds are too slow for real-time applications.
* **Cost** – Even "low-fee" blockchains can become expensive at scale.
* **Scalability** – Current architectures struggle to handle high-throughput applications.
* **Privacy** – Blockchains are by default public, meaning all data onchain can be read.
## **Benefits of MagicBlock's Ephemeral Rollup**
The ephemeral rollup functions as a **specialized SVM runtime** and can be **customized** to include:
* ✅ **Gasless Transactions** – Zero fees enabling scalability and mass adoption.
* ✅ **Faster Block Times** – Sub 10ms latency for seamless UX.
* ✅ **High-precision Scheduling** – Built-in automation to execute transactions.
* ✅ **Program and States Synchronization** – No fragmentation, composable and upgradable.
* ✅ **Horizontal Scaling** – Multiple rollups on-demand for millions of transactions.
* ✅ **Familiar Tooling** – Reusability of existing and familiar programming language, libraries and testing tools.
Learn how Ephemeral Rollups works
Learn how Magic Router works
Try out with Rust, Anchor, and Typescript
Build private, verifiable applications with TEEs
# Client Implementation
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/magic-actions/client
Client-side guide to building with Magic Actions
***
### Quick Access
Explore reference implementation on GitHub
***
### Setup Router Connection
Use Magic Router to route and send transactions to ER and base layer.
```typescript Kit theme={null}
import { Connection } from "@magicblock-labs/ephemeral-rollups-kit";
// Initialize connection
const connection = await Connection.create(
"https://devnet-router.magicblock.app",
"wss://devnet-router.magicblock.app"
);
// ... create transaction
// Send and confirm transaction
const txHash = await connection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
import { sendAndConfirmTransaction } from "@solana/web3.js";
import { ConnectionMagicRouter } from "@magicblock-labs/ephemeral-rollups-sdk";
// Initialize connection
const connection = new ConnectionMagicRouter(
"https://devnet-router.magicblock.app/",
{ wsEndpoint: "wss://devnet-router.magicblock.app/" }
);
// ... create transaction
// Send and confirm transaction
const txHash = await sendAndConfirmTransaction(connection, tx, [payer], {
skipPreflight: true,
commitment: "confirmed",
});
```
### Transaction Flow
1. Delegate counter to ER
```ts theme={null}
const delegateTx = await program.methods
.delegate()
.accounts({
payer: anchor.Wallet.local().publicKey,
pda: pda,
})
.transaction();
```
2. Increment counter on ER in real-time
```ts theme={null}
const incrementTx = await program.methods
.increment()
.accounts({
counter: pda,
})
.transaction();
```
3. Commit with Magic Action
```ts theme={null}
const commitTx = await program.methods
.commitAndUpdateLeaderboard()
.accounts({ payer: wallet.publicKey /* your accounts */ })
.transaction();
```
### Examples
End-to-end walkthrough on ER usage
Router overview and flow
# Program Implementation
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/magic-actions/implementation
Program-side guide to building with Magic Actions
***
### Quick Access
Explore reference implementation on GitHub
***
### 1) Create action instruction
The instruction `update_leaderboard` runs on the base layer immediately after the commit lands. The `#[action]` attribute on its accounts context marks it as callable from a post-commit action.
```rust theme={null}
// program instruction
pub fn update_leaderboard(ctx: Context) -> Result<()> {
let leaderboard = &mut ctx.accounts.leaderboard;
let counter_info = &mut ctx.accounts.counter.to_account_info();
let mut data: &[u8] = &counter_info.try_borrow_data()?;
let counter = Counter::try_deserialize(&mut data)?;
if counter.count > leaderboard.high_score {
leaderboard.high_score = counter.count;
}
msg!(
"Leaderboard updated! High score: {}",
leaderboard.high_score
);
Ok(())
}
// instruction context
#[action]
#[derive(Accounts)]
pub struct UpdateLeaderboard<'info> {
#[account(mut, seeds = [LEADERBOARD_SEED], bump)]
pub leaderboard: Account<'info, Leaderboard>,
/// CHECK: PDA owner depends on: 1) Delegated: Delegation Program; 2) Undelegated: Your program ID
pub counter: UncheckedAccount<'info>,
}
```
### 2) Build the commit instruction with the action
The commit instruction `commit_and_update_leaderboard` runs on the ER. It uses `MagicIntentBundleBuilder` to schedule both the commit and the post-commit action onto `magic_context` — both are applied together when the ER transaction is sealed back to the base layer.
```rust theme={null}
// commit action instruction on ER
pub fn commit_and_update_leaderboard(ctx: Context) -> Result<()> {
// Build the post-commit action that updates the leaderboard on base layer
let instruction_data =
anchor_lang::InstructionData::data(&crate::instruction::UpdateLeaderboard {});
let action_args = ActionArgs::new(instruction_data);
let action_accounts = vec![
ShortAccountMeta {
pubkey: ctx.accounts.leaderboard.key(),
is_writable: true,
},
ShortAccountMeta {
pubkey: ctx.accounts.counter.key(),
is_writable: false,
},
];
let action = CallHandler {
destination_program: crate::ID,
accounts: action_accounts,
args: action_args,
// Signer that pays transaction fees for the action from its escrow PDA
escrow_authority: ctx.accounts.payer.to_account_info(),
compute_units: 200_000,
};
// Schedule commit + post-commit action on magic_context
MagicIntentBundleBuilder::new(
ctx.accounts.payer.to_account_info(),
ctx.accounts.magic_context.to_account_info(),
ctx.accounts.magic_program.to_account_info(),
)
.commit(&[ctx.accounts.counter.to_account_info()])
.add_post_commit_actions([action])
.build_and_invoke()?;
Ok(())
}
// commit action context on ER
#[commit]
#[derive(Accounts)]
pub struct CommitAndUpdateLeaderboard<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(mut, seeds = [COUNTER_SEED], bump)]
pub counter: Account<'info, Counter>,
/// CHECK: Leaderboard PDA - not mut here, writable set in handler
#[account(seeds = [LEADERBOARD_SEED], bump)]
pub leaderboard: UncheckedAccount<'info>,
/// CHECK: Your program ID
pub program_id: AccountInfo<'info>,
}
```
### Execute multiple actions
You can commit multiple accounts and chain several actions in one call. Actions execute sequentially in the order they're passed to `add_post_commit_actions`.
```rust theme={null}
// Chain several actions — they execute sequentially on base layer after the commit lands.
MagicIntentBundleBuilder::new(
ctx.accounts.payer.to_account_info(),
ctx.accounts.magic_context.to_account_info(),
ctx.accounts.magic_program.to_account_info(),
)
.commit(&[
ctx.accounts.counter.to_account_info(),
// ... additional committed accounts
])
.add_post_commit_actions([action_1, action_2, action_3])
.build_and_invoke()?;
```
### Undelegate with actions
Actions can also be chained onto an undelegation — the counter commits, undelegates, and the actions run, all atomically in one ER transaction.
```rust theme={null}
// Commit, undelegate, AND execute actions — all atomically on base layer after the ER transaction seals.
MagicIntentBundleBuilder::new(
ctx.accounts.payer.to_account_info(),
ctx.accounts.magic_context.to_account_info(),
ctx.accounts.magic_program.to_account_info(),
)
.commit_and_undelegate(&[ctx.accounts.counter.to_account_info()])
.add_post_commit_actions([action])
.build_and_invoke()?;
```
***
# Introduction
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/magic-actions/overview
Automatically execute base-layer actions while Delegated
***
### Quick Access
Explore reference implementation on GitHub
***
### What are Magic Actions?
Magic Actions let you attach one or more call instructions that run automatically on the Solana base layer immediately after an Ephemeral Rollup
(ER) commit. Use committed state as inputs to orchestrate multi-step workflows in
a single transaction.
* **State sync**: propagate ER changes to base-layer accounts
* **Cross-program composition**: invoke other programs after commit
* **Automated workflows**: chain complex operations from one commit
* **Event-driven actions**: branch logic based on committed state
### How it works
1. Delegate accounts to an Ephemeral Rollup
2. Execute low-latency transactions on Ephemeral Rollup
3. Commit to base layer with attached instruction actions while staying delegated
4. Handlers execute automatically using the freshly committed state
***
### See also
Magic Action work along with Ephemeral Rollups.
Delegation, Commitment & Undelegation
# Troubleshooting
Source: https://docs.magicblock.gg/pages/ephemeral-rollups-ers/magic-actions/troubleshooting
Diagnose common issues with Magic Actions
### Handler not executing
* Verify `instruction discriminator`
* Allocate sufficient `compute_units`
### Deserialization errors
* Use `UncheckedAccount` for committed accounts in action context
* Manually deserialize via `try_deserialize` on borrowed data
* Check account discriminator matches expected type
### Transaction failures
* Ensure all action accounts are listed in `ShortAccountMeta`
* Match `is_writable` flags to actual usage
* Increase compute budget for all actions in the commit
### Limitations & considerations
* Handlers execute on base layer and consume base-layer fees
* Standard Solana limits apply (compute, account locks)
* Atomicity: any action failure reverts the commit
* First two action accounts are injected (`escrow`, `escrow_auth`)
### Helpful links
Delegation, Commitment & Undelegation
Router overview and flow
Explore reference implementation on GitHub
Chat with the team and community
# AI Dev Skill
Source: https://docs.magicblock.gg/pages/overview/additional-information/ai-dev-skill
MagicBlock Ephemeral Rollups development skill for AI coding agents, with MagicBlock-specific patterns for delegation, Magic Actions, cranks, VRF, lamports top-up, commit sponsorship, private payments with swaps, and dual-connection architecture.
View the repository, installation steps, and source files for the skill.
## Quick install
```bash theme={null}
npx add-skill https://github.com/magicblock-labs/magicblock-dev-skill
```
## What It Is
The MagicBlock Dev Skill is an AI development skill that packages MagicBlock-specific patterns into a reusable workflow that activates when you ask for MagicBlock or Ephemeral Rollups help. Instead of re-explaining the same integration details in every prompt, the skill gives your coding agent structured guidance for building on MagicBlock.
It is designed for teams working on:
* MagicBlock Ephemeral Rollups integration
* Delegating and undelegating Solana accounts
* Dual-connection Solana + MagicBlock architectures
* High-performance, low-latency transaction flows
* Cranks for recurring automated transactions
* VRF for provable randomness
* Magic Actions — base-layer instructions chained atomically to an ER commit
* Topping up a delegated account's lamports via `lamportsDelegatedTransferIx`
* Commit sponsorship and lifting the default 10-commit cap with `magic_fee_vault`
* Private payments — deposits, transfers, withdrawals, and swaps via the Payments API, including the challenge/login bearer-token flow for private reads
* Gaming and real-time app development on Solana
* Anchor and TypeScript-based integrations
## Installation
### Quick install
```bash theme={null}
npx add-skill https://github.com/magicblock-labs/magicblock-dev-skill
```
### Manual install
```bash theme={null}
git clone https://github.com/magicblock-labs/magicblock-dev-skill
cd magicblock-dev-skill
./install.sh
```
By default, `./install.sh` installs the skill to both personal skill directories:
* `~/.claude/skills/magicblock`
* `${CODEX_HOME:-~/.codex}/skills/magicblock`
### Targeting specific agents
Global / per-user targets:
```bash theme={null}
./install.sh --claude
./install.sh --codex
```
Project-scoped targets (always install into the current directory):
```bash theme={null}
./install.sh --cursor # .cursor/rules/magicblock.mdc
./install.sh --windsurf # .windsurf/rules/magicblock.md
./install.sh --cline # .clinerules/magicblock.md
./install.sh --continue # .continue/rules/magicblock.md
./install.sh --agents-md # ./AGENTS.md
```
Combined:
```bash theme={null}
./install.sh --all # everything for the current project
./install.sh --project # Claude + Codex into .claude/.codex inside the project
./install.sh --path /custom/path/magicblock
```
The single-file targets for Cursor, Windsurf, Cline, Continue, and `AGENTS.md` are generated from `dist/` artifacts. `install.sh` runs `./build.sh` automatically if `dist/` is missing.
### Building dist artifacts manually
```bash theme={null}
./build.sh
```
Produces:
* `dist/AGENTS.md` - full flattened skill (`SKILL.md` plus all references)
* `dist/system-prompt.md` - trimmed `SKILL.md` plus reference URLs for chat-only platforms
* `dist/magicblock.cursor.mdc` - Cursor-formatted rule with `.mdc` frontmatter
* `dist/magicblock.zip` - zipped `skill/` folder for Claude.ai upload
## Usage
The skill activates automatically when you ask about MagicBlock or Ephemeral Rollups.
* In Claude Code, you can also invoke it directly with `/magicblock`.
* In Codex, mention it explicitly by name, for example: `use the magicblock skill`.
* In Cursor / Windsurf / Cline / Continue, the rule's description triggers contextually when you mention MagicBlock topics.
* For chat-only platforms, load `dist/system-prompt.md` once as the system prompt, custom instructions, or project context.
Examples:
```text theme={null}
Add delegation hooks to my player account
Change my roll_dice function to use VRF
Set up a crank that updates game state every 100ms
Add a Magic Action that updates my onchain leaderboard after every commit
Top up my delegated fee payer with lamports
Build a private USDC transfer flow using the Payments API
Help me integrate MagicBlock into my Anchor program
```
## What the Skill Adds
The skill goes beyond a simple prompt template. Its main entrypoint and supporting references guide the agent toward MagicBlock-specific implementation details such as:
* When to use the base layer connection vs. the ephemeral rollup connection
* How to structure delegation, commit, and undelegation flows correctly with `MagicIntentBundleBuilder` (SDK 0.11+)
* Common Anchor patterns for `#[ephemeral]`, `#[delegate]`, and `#[commit]`
* Magic Actions: scheduling base-layer instructions inside an ER transaction via `MagicIntentBundleBuilder.add_post_commit_actions(...)` so they execute atomically once the commit is sealed back
* Topping up delegated accounts with `lamportsDelegatedTransferIx` (single-use lamports PDA, submitted on base layer, credited on the ER)
* Lifting the default 10-commit sponsorship cap by attaching a `magic_fee_vault` PDA and delegated fee payer to the intent bundle
* Private Ephemeral Rollups (PER) patterns — delegating the permission account alongside the permissioned account so member updates execute on the ER
* Private Payments API workflows including the challenge → login → bearer-token flow for reading private balances and the public/private swap modes
* VRF and crank setup for real-time apps and games
* Environment variables, versions, and dependencies for MagicBlock development
For the full skill, source files, and installation instructions, visit the [MagicBlock Dev Skill repository](https://github.com/magicblock-labs/magicblock-dev-skill).
# Pricing
Source: https://docs.magicblock.gg/pages/overview/additional-information/pricing
MagicBlock’s pricing is modeled after decentralized cloud infrastructure. The goal is to keep computing costs **predictable for developers**, while still providing **flexibility for enterprises** that need priority access or dedicated resources.
***
## Product Pricing
**Public nodes** make it simple to start building on MagicBlock with free transactions and zero friction. Fees are only charged when you close a session or commit data back to Solana.
| Fee type |
Amount (SOL) |
Description |
| Base fee |
0 |
Per transaction (Tx) |
| Session fee |
0.0003 |
Per ER session (at undelegation) |
| Commit fee |
0.0001 |
Per commit to Solana |
**Dedicated nodes** are ideal for enterprises and high-scale teams. They provide maximum reliability, predictable costs, and MEV protection with your own dedicated infrastructure.
[Learn more about ER →](/pages/ephemeral-rollups-ers/how-to-guide/quickstart)
### ER Cost Simulator: 30-Days
Private Ephemeral Rollup supports custom private computation defined by your smart contract.
Custom PER logic uses standard ER pricing. [See Ephemeral Rollup pricing →](#ephemeral-rollup)
The VRF service provides **provably fair randomness on-chain**. Fees cover proof generation + posting on-chain.
> ⚠️ Note: Costs do not include the transaction to request randomness. On ER transactions are free, on Solana transactions may vary based on your priority fees.
| VRF type |
Amount (SOL) |
Description |
| ER (\<50 ms) |
Free |
Per randomness request |
| Solana (\<500 ms) |
0.0008 |
Per randomness request |
| Solana (1/2 seconds) |
0.0005 |
Per randomness request |
### VRF Cost Simulator: 30-Days
[Learn more about Solana VRF →](/pages/verifiable-randomness-functions-vrfs/introduction/solana-vrf)
Private Payment API make it simple to send private stablecoin transfers on Solana Mainnet and/or on an a Private ER.
| Payment type |
Fixed fee (SOL) |
Volume fee |
| Solana Mainnet |
0.002 |
0.1% |
Explore the Private Payments API endpoints
***
## Customer Support
For support to **run your own nodes**, reach out to:
📧 [development@magicblock.xyz](mailto:development@magicblock.xyz)
Execute real-time, zero-fee transactions securely on Solana.
Protect sensitive data with compliance — built on top of Ephemeral Rollups.
Add private onchain transfers to your app in seconds — compliant by default.
Add provably fair onchain randomness to games, raffles, and real-time apps.
Access low-latency onchain price feeds for trading and DeFi.
# Request For Products
Source: https://docs.magicblock.gg/pages/overview/additional-information/request-for-products
Go beyond tutorials by working on real-world problems and community challenges. The Request for Products (RFPs) showcase open ideas and challenges waiting for contributors like you.
Build on community ideas and solve meaningful challenges.
# Security & Audits
Source: https://docs.magicblock.gg/pages/overview/additional-information/security-and-audits
At MagicBlock, security is a top priority. All our core programs have undergone professional audits to ensure robustness and safety for our users.
***
## [Delegation Program](/pages/ephemeral-rollups-ers/introduction/why)
* **GitHub:** [https://github.com/magicblock-labs/delegation-program](https://github.com/magicblock-labs/delegation-program)
* **Program Id:** `DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh`
* **Audit Firm:** Halborn
* **Audit Report:** [View audit report](https://github.com/magicblock-labs/delegation-program/tree/429f86fd56f5e8956cf132da0063b971346f6c67/security_audits)
* **Details:** Comprehensive security review of the delegation program, covering account delegation, transaction safety, and program logic.
***
## [Solana VRF Program](/pages/verifiable-randomness-functions-vrfs/introduction/solana-vrf)
* **GitHub:** [https://github.com/magicblock-labs/solana-vrf](https://github.com/magicblock-labs/solana-vrf)
* **Program Id:** `Vrf1RNUjXmQGjmQrQLvJHs9SNkvDJEsRVFPkfSQUwGz`
* **Audit Firm:** Zenith
* **Audit Report:** [View audit report](https://github.com/magicblock-labs/solana-vrf/blob/main/security_audits/2025-08-06%20VRF%20Program%20Audit%20Report%20by%20Zenith.pdf)
* **Details:** Detailed audit of the Verifiable Randomness Function (VRF) program, ensuring secure randomness generation and safe integration with your program.
***
## [Permission Program](/pages/private-ephemeral-rollups-pers/introduction/onchain-privacy)
* **GitHub:** Currently Private
* **Program Id:** `BTWAqWNBmF2TboMh3fxMJfgR16xGHYD7Kgr2dPwbRPBi`
* **Audit:** TBC
***
> These audits reflect our commitment to building secure, reliable programs for the MagicBlock ecosystem.
# Systems Status
Source: https://docs.magicblock.gg/pages/overview/additional-information/system-status
# Whitepaper
Source: https://docs.magicblock.gg/pages/overview/additional-information/whitepaper
Ephemeral Rollups Are All You Need (https://arxiv.org/abs/2311.02650)
**Authors:** Gabriele Picco, Andrea Fortugno
We propose a framework that leverages the **Solana Virtual Machine (SVM)** to scale fully on-chain applications without state fragmentation or compromised trust assumptions.
To enhance scalability and resource optimization, we introduce the concept of **Ephemeral Rollups (ERs)**:
* Dedicated runtimes customizable for higher operational speed.
* Configurable ticking mechanisms.
* Provable sessions and gasless transactions.
* Achieves scalability without compromising composability.
***
> This whitepaper outlines the foundations of MagicBlock’s architecture and the innovative solutions we propose for fully on-chain ecosystems.
# Products
Source: https://docs.magicblock.gg/pages/overview/products
MagicBlock is your backend for the new financial internet. Upgrade your Solana program and build composable applications with real-time UX, on-chain privacy, and verifiable randomness.
Execute real-time, zero-fee transactions securely on Solana.
Protect sensitive data with compliance — built on top of Ephemeral Rollups.
Add private onchain transfers to your app in seconds — compliant by default.
Add provably fair onchain randomness to games, raffles, and real-time apps.
Access low-latency onchain price feeds for trading and DeFi.
# Balance
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/api-reference/per/balance
pages/private-ephemeral-rollups-pers/api-reference/per/openapi/balance.openapi.json GET /v1/spl/balance
Reads the owner's associated token account on the base RPC.
# Challenge
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/api-reference/per/challenge
pages/private-ephemeral-rollups-pers/api-reference/per/openapi/challenge.openapi.json GET /v1/spl/challenge
Returns a challenge string that the wallet must sign. The signature is then submitted to `/v1/spl/login` in exchange for a bearer token used to read private data.
# Deposit SPL Tokens
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/api-reference/per/deposit
pages/private-ephemeral-rollups-pers/api-reference/per/openapi/deposit.openapi.json POST /v1/spl/deposit
Wraps the SDK `delegateSpl(...)` flow. The API generates `shuttleId` server-side and pins `escrowIndex` to `0`.
# Health
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/api-reference/per/health
pages/private-ephemeral-rollups-pers/api-reference/per/openapi/health.openapi.json GET /health
# Initialize Mint
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/api-reference/per/initialize-mint
pages/private-ephemeral-rollups-pers/api-reference/per/openapi/initialize-mint.openapi.json POST /v1/spl/initialize-mint
# Introduction
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/api-reference/per/introduction
Private Payments API documentation
Reference program for private SPL token flows
Explore the example private payments application and API flow
## Overview
The Private Payments API builds unsigned SPL token transactions for deposits, transfers, withdrawals, swaps, and mint initialization across Solana and MagicBlock ephemeral rollups. It also exposes balance queries, mint-initialization status, and a wallet challenge/login flow that issues bearer tokens for reading private data. The canonical public reference is available at [payments.magicblock.app/reference](https://payments.magicblock.app/reference).
### Meta
* [**Health**](/pages/private-ephemeral-rollups-pers/api-reference/per/health) - Check API health and availability
### Auth
* [**Challenge**](/pages/private-ephemeral-rollups-pers/api-reference/per/challenge) - Generate a challenge string for the wallet to sign
* [**Login**](/pages/private-ephemeral-rollups-pers/api-reference/per/login) - Exchange a signed challenge for a bearer token
### SPL
* [**Deposit SPL Tokens**](/pages/private-ephemeral-rollups-pers/api-reference/per/deposit) - Build an unsigned deposit transaction from Solana into an ephemeral rollup
* [**Transfer SPL Tokens**](/pages/private-ephemeral-rollups-pers/api-reference/per/transfer) - Build an unsigned public or private SPL transfer
* [**Withdraw SPL Tokens**](/pages/private-ephemeral-rollups-pers/api-reference/per/withdraw) - Build an unsigned withdrawal transaction back to Solana
* [**Initialize Mint**](/pages/private-ephemeral-rollups-pers/api-reference/per/initialize-mint) - Build an unsigned transaction that initializes a validator-scoped transfer queue for a mint
* [**Balance**](/pages/private-ephemeral-rollups-pers/api-reference/per/balance) - Get the base-chain SPL token balance for an address
* [**Private Balance**](/pages/private-ephemeral-rollups-pers/api-reference/per/private-balance) - Get the ephemeral-rollup SPL token balance for an address (auth required)
* [**Is Mint Initialized**](/pages/private-ephemeral-rollups-pers/api-reference/per/is-mint-initialized) - Check whether a mint has a validator-scoped transfer queue on the ephemeral RPC
### Swap
* [**Swap Quote**](/pages/private-ephemeral-rollups-pers/api-reference/per/quote) - Get a swap quote between two SPL mints
* [**Swap**](/pages/private-ephemeral-rollups-pers/api-reference/per/swap) - Build an unsigned swap transaction (public pass-through or private with scheduled transfer)
### MCP
* [**MCP**](/pages/private-ephemeral-rollups-pers/api-reference/per/mcp) - Access the stateless Streamable HTTP MCP endpoint
```
┌────────────────────────────────────────────┐
│ 1. Deposit │
├────────────────────────────────────────────┤
│ • Build an unsigned deposit transaction │
│ • Solana base balance → ephemeral rollup │
└────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────┐
│ 2. Transfer / Swap │
├────────────────────────────────────────────┤
│ • Build SPL transfer or swap │
│ • base/ephemeral → base/ephemeral │
│ • public or private (delayed + split) │
└────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────┐
│ 3. Withdraw │
├────────────────────────────────────────────┤
│ • Build an unsigned withdrawal │
│ • ephemeral rollup → Solana base balance │
└────────────────────────────────────────────┘
```
## Auth Flow
Endpoints that read private data inside the Private Ephemeral Rollup require a bearer token:
1. `GET /v1/spl/challenge?pubkey=` returns a `challenge` string
2. The wallet signs the challenge
3. `POST /v1/spl/login` with `{ pubkey, challenge, signature }` returns a `token`
4. Pass `Authorization: Bearer ` on `/v1/spl/private-balance` (required) and on `/v1/spl/transfer` requests that need to connect to the Private Ephemeral Rollup (optional)
## Response Format
Successful transaction-building endpoints return an unsigned transaction payload:
```json theme={null}
{
"kind": "deposit",
"version": "legacy",
"transactionBase64": "base64-encoded-transaction",
"sendTo": "base",
"recentBlockhash": "blockhash",
"lastValidBlockHeight": 284512337,
"instructionCount": 3,
"requiredSigners": ["3rXKwQ1kpjBd5tdcco32qsvqUh1BnZjcYnS5kYrP7AYE"]
}
```
The expected client flow is:
1. Call the API
2. Decode `transactionBase64`
3. Optionally adjust the transaction if the client needs to
4. Sign with the required wallet(s)
5. Send to the RPC indicated by `sendTo` (`"base"` or `"ephemeral"`)
# Is Mint Initialized
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/api-reference/per/is-mint-initialized
pages/private-ephemeral-rollups-pers/api-reference/per/openapi/is-mint-initialized.openapi.json GET /v1/spl/is-mint-initialized
# Login
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/api-reference/per/login
pages/private-ephemeral-rollups-pers/api-reference/per/openapi/login.openapi.json POST /v1/spl/login
Verifies the wallet's signature over the challenge issued by `/v1/spl/challenge` and returns an authentication token. Pass the token as `Authorization: Bearer ` on `/v1/spl/private-balance` and on `/v1/spl/transfer` requests that need to connect to the Private Ephemeral Rollup.
# MCP
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/api-reference/per/mcp
pages/private-ephemeral-rollups-pers/api-reference/per/openapi/mcp.openapi.json POST /mcp
# Private Balance
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/api-reference/per/private-balance
pages/private-ephemeral-rollups-pers/api-reference/per/openapi/private-balance.openapi.json GET /v1/spl/private-balance
Reads the owner's associated token account on the ephemeral RPC. Requires an `Authorization: Bearer ` header obtained from `/v1/spl/login`.
# Swap Quote
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/api-reference/per/quote
pages/private-ephemeral-rollups-pers/api-reference/per/openapi/quote.openapi.json GET /v1/swap/quote
Returns a swap quote between two SPL mints. The quote response can be passed as-is into `POST /v1/swap/swap` to build the swap transaction.
# Swap
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/api-reference/per/swap
pages/private-ephemeral-rollups-pers/api-reference/per/openapi/swap.openapi.json POST /v1/swap/swap
Build an unsigned swap transaction from a quote.
**Visibility modes:**
- **`visibility: "public"`** (default) — pure pass-through to the Jupiter/Metis upstream. The returned transaction is whatever the upstream produces.
- **`visibility: "private"`** — the server forces Jupiter's output into a program-owned stash ATA (deterministically derived from `(userPublicKey, quoteResponse.outputMint)`), prepends an idempotent ATA-create, and appends a `schedule_private_transfer` instruction that registers a one-shot Hydra crank. When the crank fires, it self-CPIs into the on-chain private-transfer flow to deliver the swapped tokens to `destination` with the requested delay/split policy. The returned transaction is a v0 `VersionedTransaction` that is still unsigned — the client signs and submits.
When `visibility = "private"`, the fields `destination`, `minDelayMs`, `maxDelayMs`, and `split` are **required**. `clientRefId` and `validator` are optional. Explicitly setting `destinationTokenAccount` to anything other than the server-derived stash ATA returns `400`.
# Transfer SPL Tokens
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/api-reference/per/transfer
pages/private-ephemeral-rollups-pers/api-reference/per/openapi/transfer.openapi.json POST /v1/spl/transfer
Transfer SPL tokens publicly or privately through an ephemeral rollup. Accepts an optional `Authorization: Bearer ` header obtained from the `/v1/spl/login` flow when the request needs to read or write data inside the Private Ephemeral Rollup.
# Withdraw SPL Tokens
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/api-reference/per/withdraw
pages/private-ephemeral-rollups-pers/api-reference/per/openapi/withdraw.openapi.json POST /v1/spl/withdraw
Wraps the SDK `withdrawSpl(...)` flow. The API generates `shuttleId` server-side.
# Access Control
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/how-to-guide/access-control
Learn how to manage fine-grained access control and member permissions in Private Ephemeral Rollups.
***
On-chain Permission Management (Coming soon)
SDK for Private Ephemeral Rollups
***
## Overview
Private Ephemeral Rollups are [Ephemeral Rollups](/pages/ephemeral-rollups-ers/introduction/why) that enable fine-grained permission over permissioned accounts in a [Trusted Execution Environment](/pages/private-ephemeral-rollups-pers/introduction/onchain-privacy) with [compliance](/pages/private-ephemeral-rollups-pers/introduction/compliance-framework) at its heart. Each permission account maintains a list of members with specific flags that determine what actions they can perform.
### Key Concepts
* **Permission Account**: A PDA that stores access control rules for a specific account
* **Members**: Addresses granted specific permissions via flags
* **Flags**: Bitmasks that define what a member can do (authority, view logs, view balances, etc.)
* **Public Permissions**: When members are set to `None`, the permissioned account becomes temporarily visible
***
## Member Flags
Member flags define fine-grained permissions for each member. Flags can be combined using bitwise OR to grant multiple permissions.
**Flag Descriptions:**
* **AUTHORITY**: Allows a member to update and delegate permission settings, add/remove other members, and update member flags.
* **TX\_LOGS**: Allows a member to view transaction execution logs.
* **TX\_BALANCES**: Allows a member to view account balance changes.
* **TX\_MESSAGE**: Allows a member to view transaction message data.
* **ACCOUNT\_SIGNATURES**: Allows a member to view account signatures
```rust theme={null}
use ephemeral_rollups_sdk::access_control::structs::{
Member,
AUTHORITY_FLAG,
TX_LOGS_FLAG,
TX_BALANCES_FLAG,
TX_MESSAGE_FLAG,
ACCOUNT_SIGNATURES_FLAG,
};
// Set flags by combining them with bitwise OR
let flags = AUTHORITY_FLAG | TX_LOGS_FLAG;
// Create a member with combined flags
let mut member = Member {
flags,
pubkey: user_pubkey,
};
// Check if member has a specific flag using bitwise AND
let is_authority = (member.flags & AUTHORITY_FLAG) != 0;
let can_see_logs = (member.flags & TX_LOGS_FLAG) != 0;
// Use helper methods to set/remove flags
member.set_flags(TX_BALANCES_FLAG); // Add a flag
member.remove_flags(TX_LOGS_FLAG); // Remove a flag
```
```rust theme={null}
use ephemeral_rollups_pinocchio::types::{Member, MemberFlags};
use pinocchio::Address;
// Create and set flags using individual methods
let mut flags = MemberFlags::new();
flags.set(MemberFlags::AUTHORITY);
flags.set(MemberFlags::TX_LOGS);
flags.set(MemberFlags::TX_BALANCES);
// Create a member with flags
let member = Member {
flags,
pubkey: user_address,
};
// Remove a flag
flags.remove(MemberFlags::TX_LOGS);
// Create flags from individual boolean values
let flags = MemberFlags::from_acl_flags(
true, // authority
true, // tx_logs
false, // tx_balances
true, // tx_message
false, // account_signatures
);
// Convert flags to byte value
let flag_byte = flags.to_acl_flag_byte();
// Create flags from byte value
let flags = MemberFlags::from_acl_flag_byte(flag_byte);
```
```typescript theme={null}
import { PublicKey } from "@solana/web3.js";
import {
AUTHORITY_FLAG,
TX_LOGS_FLAG,
TX_BALANCES_FLAG,
TX_MESSAGE_FLAG,
ACCOUNT_SIGNATURES_FLAG,
type Member,
} from "@magicblock-labs/ephemeral-rollups-sdk";
// Set flags by combining them with bitwise OR
const flags = AUTHORITY_FLAG | TX_LOGS_FLAG;
// Create a member with combined flags
const member: Member = {
flags,
pubkey: new PublicKey(userAddress),
};
// Check if a flag is present using bitwise AND
const isAuthority = (member.flags & AUTHORITY_FLAG) !== 0;
const canSeeLogs = (member.flags & TX_LOGS_FLAG) !== 0;
const canSeeBalances = (member.flags & TX_BALANCES_FLAG) !== 0;
// Add a flag to existing flags
const updatedFlags = member.flags | TX_BALANCES_FLAG;
// Remove a flag from existing flags
const removedFlags = member.flags & ~TX_LOGS_FLAG;
```
```typescript theme={null}
import {
AUTHORITY_FLAG,
TX_LOGS_FLAG,
TX_BALANCES_FLAG,
TX_MESSAGE_FLAG,
ACCOUNT_SIGNATURES_FLAG,
isAuthority,
canSeeTxLogs,
canSeeTxBalances,
canSeeTxMessages,
canSeeAccountSignatures,
type Member,
} from "@magicblock-labs/ephemeral-rollups-sdk";
// Set flags by combining them with bitwise OR
const flags = AUTHORITY_FLAG | TX_LOGS_FLAG | TX_BALANCES_FLAG;
// Create a member with combined flags
const member: Member = {
flags,
pubkey: userAddress,
};
// Use helper functions to check specific permissions
const canModifyPermission = isAuthority(member, userAddress);
const canViewLogs = canSeeTxLogs(member, userAddress);
const canViewBalances = canSeeTxBalances(member, userAddress);
const canViewMessages = canSeeTxMessages(member, userAddress);
const canViewSignatures = canSeeAccountSignatures(member, userAddress);
// Add a flag to existing member
const updatedFlags = member.flags | TX_MESSAGE_FLAG;
// Remove a flag from existing member
const removedFlags = member.flags & ~TX_LOGS_FLAG;
```
***
## Ephemeral Permission
`EphemeralPermission` accounts live entirely on the Ephemeral Rollup and are
paid for by the delegated PDA — no base-layer permission account to create,
delegate, or commit-and-undelegate. Three CPI ops cover the full lifecycle:
**Create**, **Update**, **Close** — all PDA-signed by the data account on
the ER, via MagicBlock's Permission Program `ACLseoPoyC3cBqoUtkbjZ4aDrkurZW86v19pXz2XQnp1`.
**Prerequisite — delegate the data PDA.** Only the data account is delegated to
the TEE validator (on the base layer, via MagicBlock's Delegation Program
`DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh`). Once delegated, the PDA signs
all three permission ops on the ER using its program seeds and pays the
ephemeral permission rent — so it must be pre-funded at `initialize` time. See
[Quickstart](/pages/private-ephemeral-rollups-pers/how-to-guide/quickstart#2-delegate-and-create-permission)
for the end-to-end flow.
These public validators are supported for development. Make sure to add the
specific ER validator in your delegation instruction:
**Mainnet**
-
Asia (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Devnet**
-
Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Localnet**
-
Local ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
Create Ephemeral Permission}>
Initialize a new `EphemeralPermission` account on the ER with initial
members and the privacy flag. Idempotent — skip if it already exists.
Update Ephemeral Permission}>
Toggle the privacy flag, or add / remove / re-flag members. Updates take
effect immediately on the ER.
Close Ephemeral Permission}>
Close the `EphemeralPermission` account on the ER and refund the rent to
the data PDA when the permission is no longer needed.
***
## Ephemeral Permission Operations
Initialize a new `EphemeralPermission` account on the ER via
`CreateEphemeralPermissionCpi`. Payer = the delegated data PDA, which
signs with its program seeds and covers the rent from the lamports
pre-funded at `initialize` time.
```rust theme={null}
use ephemeral_rollups_sdk::access_control::{
instructions::CreateEphemeralPermissionCpi,
structs::{EphemeralMembersArgs, Member},
};
// Counter PDA pays for its own permission rent (it carries lamports onto the ER
// after delegation and signs as PDA via seeds).
let signers = [
COUNTER_SEED,
ctx.accounts.counter.authority.as_ref(),
&[ctx.bumps.counter],
];
CreateEphemeralPermissionCpi {
payer: ctx.accounts.counter.to_account_info(), // pays ephemeral rent
permissioned_account: ctx.accounts.counter.to_account_info(), // what the permission gates
permission: ctx.accounts.permission.to_account_info(),
vault: ctx.accounts.ephemeral_vault.to_account_info(),
magic_program: ctx.accounts.magic_program.to_account_info(),
permission_program: ctx.accounts.permission_program.to_account_info(),
args: EphemeralMembersArgs {
is_private: false, // start public — flip via UpdateEphemeralPermission
members: vec![],
},
}
.invoke_signed(&[&signers])?;
```
```rust theme={null}
use ephemeral_rollups_sdk::access_control::{
instructions::CreateEphemeralPermissionCpi,
structs::{EphemeralMembersArgs, Member},
};
// `permissioned_account` (here a counter PDA) signs as PDA via seeds; pass the
// same seeds you used for `find_program_address` to derive it.
let seeds: &[&[u8]] = &[
COUNTER_SEED,
permissioned_account_authority.as_ref(),
&[bump],
];
CreateEphemeralPermissionCpi {
payer: &counter_account_info, // pays ephemeral rent
permissioned_account: &counter_account_info, // what the permission gates
permission: &permission_account_info,
vault: &ephemeral_vault_account_info,
magic_program: &magic_program_account_info,
permission_program: &permission_program_account_info,
args: EphemeralMembersArgs {
is_private: false, // start public — flip via UpdateEphemeralPermission
members: vec![],
},
}
.invoke_signed(&[seeds])?;
```
```rust theme={null}
use ephemeral_rollups_pinocchio::acl::{
CreateEphemeralPermission, EphemeralMembersArgs, Member,
};
use pinocchio::cpi::{Seed, Signer};
// Buffer size: discriminator (8) + EphemeralMembersArgs body.
// 64 bytes covers up to 1 member with slack for future Update calls.
const PERMISSION_CPI_BUF: usize = 64;
// PDA-signed CPI — the counter PDA pays rent and authorizes the permission.
let bump_seed = [bump];
let seeds_array: [Seed; 3] = [
Seed::from(b"counter"),
Seed::from(authority.address().as_ref()),
Seed::from(&bump_seed),
];
let signer = Signer::from(&seeds_array);
let members: [Member; 0] = []; // start public; toggle via Update
CreateEphemeralPermission {
payer: counter_account,
permissioned_account: counter_account,
permission,
vault,
magic_program,
permission_program,
args: EphemeralMembersArgs {
is_private: false,
members: &members,
},
}
.invoke_signed::(&[signer])?;
```
```typescript theme={null}
import {
MAGIC_PROGRAM_ID,
PERMISSION_PROGRAM_ID,
EPHEMERAL_VAULT_ID,
} from "@magicblock-labs/ephemeral-rollups-sdk";
import { pipe, createTransactionMessage, appendTransactionMessageInstructions } from "@solana/kit";
// EphemeralPermissions are created on the ER by the delegated PDA (via the
// user-program's wrapper instruction). Submit to the ER connection, not base.
const initIx = await counterProgram.methods
.initPermission()
.accountsPartial({
authority: tempKeypair.address,
counter: counterPda,
permission: permissionPda,
permissionProgram: PERMISSION_PROGRAM_ID,
ephemeralVault: EPHEMERAL_VAULT_ID,
magicProgram: MAGIC_PROGRAM_ID,
})
.instruction();
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => appendTransactionMessageInstructions([initIx], tx),
);
const sig = await ephemeralConnection.sendAndConfirmTransaction(
transactionMessage,
[tempKeypair],
{ commitment: "confirmed" },
);
console.log("init_permission tx:", sig);
```
```typescript theme={null}
import {
MAGIC_PROGRAM_ID,
PERMISSION_PROGRAM_ID,
EPHEMERAL_VAULT_ID,
} from "@magicblock-labs/ephemeral-rollups-sdk";
import { Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
// EphemeralPermissions are created on the ER by the delegated PDA (via the
// user-program's wrapper instruction). Submit to the ER connection, not base.
const initIx = await counterProgram.methods
.initPermission()
.accountsPartial({
authority: tempKeypair.publicKey,
counter: counterPda,
permission: permissionPda,
permissionProgram: PERMISSION_PROGRAM_ID,
ephemeralVault: EPHEMERAL_VAULT_ID,
magicProgram: MAGIC_PROGRAM_ID,
})
.instruction();
const tx = new Transaction().add(initIx);
const sig = await sendAndConfirmTransaction(ephemeralConnection, tx, [tempKeypair]);
console.log("init_permission tx:", sig);
```
**Use Cases:**
* Bootstrap access control for a newly delegated PDA on the ER
* Start public (`is_private: false`, empty members) and tighten later via Update
[⬆️ Back to Top](#ephemeral-permission)
Flip the privacy flag and rewrite the member list via
`UpdateEphemeralPermissionCpi`. Rebuild the full member list every call
(including the authority) so the data PDA can never lock itself out.
```rust theme={null}
use ephemeral_rollups_sdk::access_control::{
instructions::UpdateEphemeralPermissionCpi,
structs::{
EphemeralMembersArgs, Member,
TX_LOGS_FLAG, TX_MESSAGE_FLAG, TX_BALANCES_FLAG,
},
};
let signers = [
COUNTER_SEED,
ctx.accounts.counter.authority.as_ref(),
&[ctx.bumps.counter],
];
// When private, only listed members can read ER state via the TEE.
// Empty member list + is_private=false = fully public.
let members = if is_private {
vec![Member {
flags: TX_LOGS_FLAG | TX_MESSAGE_FLAG | TX_BALANCES_FLAG,
pubkey: ctx.accounts.counter.authority,
}]
} else {
vec![]
};
UpdateEphemeralPermissionCpi {
payer: ctx.accounts.counter.to_account_info(),
permissioned_account: ctx.accounts.counter.to_account_info(),
permission: ctx.accounts.permission.to_account_info(),
vault: ctx.accounts.ephemeral_vault.to_account_info(),
magic_program: ctx.accounts.magic_program.to_account_info(),
permission_program: ctx.accounts.permission_program.to_account_info(),
authority: ctx.accounts.counter.to_account_info(),
authority_is_signer: false, // PDA signs via the seeds above
args: EphemeralMembersArgs { is_private, members },
}
.invoke_signed(&[&signers])?;
```
```rust theme={null}
use ephemeral_rollups_sdk::access_control::{
instructions::UpdateEphemeralPermissionCpi,
structs::{
EphemeralMembersArgs, Member,
TX_LOGS_FLAG, TX_MESSAGE_FLAG, TX_BALANCES_FLAG,
},
};
let seeds: &[&[u8]] = &[
COUNTER_SEED,
permissioned_account_authority.as_ref(),
&[bump],
];
// When private, only listed members can read ER state via the TEE.
let members = if is_private {
vec![Member {
flags: TX_LOGS_FLAG | TX_MESSAGE_FLAG | TX_BALANCES_FLAG,
pubkey: permissioned_account_authority,
}]
} else {
vec![]
};
UpdateEphemeralPermissionCpi {
payer: &counter_account_info,
permissioned_account: &counter_account_info,
permission: &permission_account_info,
vault: &ephemeral_vault_account_info,
magic_program: &magic_program_account_info,
permission_program: &permission_program_account_info,
authority: &counter_account_info,
authority_is_signer: false, // PDA signs via the seeds above
args: EphemeralMembersArgs { is_private, members },
}
.invoke_signed(&[seeds])?;
```
```rust theme={null}
use ephemeral_rollups_pinocchio::acl::{
EphemeralMembersArgs, Member, MemberFlags, UpdateEphemeralPermission,
};
use pinocchio::cpi::{Seed, Signer};
const PERMISSION_CPI_BUF: usize = 64;
let bump_seed = [bump];
let seeds_array: [Seed; 3] = [
Seed::from(b"counter"),
Seed::from(authority.address().as_ref()),
Seed::from(&bump_seed),
];
let signer = Signer::from(&seeds_array);
// Read the on-chain Counter to grab `authority` — the sole "private" member.
let counter_authority = {
let data = counter_account.try_borrow()?;
Counter::load(&data)?.authority
};
let single_member = [Member {
flags: MemberFlags::from_acl_flag_byte(
MemberFlags::TX_LOGS | MemberFlags::TX_MESSAGE | MemberFlags::TX_BALANCES,
),
pubkey: counter_authority,
}];
let members: &[Member] = if is_private { &single_member } else { &[] };
UpdateEphemeralPermission {
payer: counter_account,
permissioned_account: counter_account,
permission,
vault,
magic_program,
permission_program,
authority: counter_account,
authority_is_signer: false, // PDA signs via the seeds above
args: EphemeralMembersArgs { is_private, members },
}
.invoke_signed::(&[signer])?;
```
```typescript theme={null}
import {
MAGIC_PROGRAM_ID,
PERMISSION_PROGRAM_ID,
EPHEMERAL_VAULT_ID,
} from "@magicblock-labs/ephemeral-rollups-sdk";
import { pipe, createTransactionMessage, appendTransactionMessageInstructions } from "@solana/kit";
// Toggle the `is_private` flag. Idempotent — the program rebuilds the member
// list every call so the authority never locks itself out.
const updateIx = await counterProgram.methods
.setPrivacy(isPrivate)
.accountsPartial({
authority: tempKeypair.address,
counter: counterPda,
permission: permissionPda,
permissionProgram: PERMISSION_PROGRAM_ID,
ephemeralVault: EPHEMERAL_VAULT_ID,
magicProgram: MAGIC_PROGRAM_ID,
})
.instruction();
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => appendTransactionMessageInstructions([updateIx], tx),
);
const sig = await ephemeralConnection.sendAndConfirmTransaction(
transactionMessage,
[tempKeypair],
{ commitment: "confirmed" },
);
console.log("set_privacy tx:", sig);
```
```typescript theme={null}
import {
MAGIC_PROGRAM_ID,
PERMISSION_PROGRAM_ID,
EPHEMERAL_VAULT_ID,
} from "@magicblock-labs/ephemeral-rollups-sdk";
import { Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
// Toggle the `is_private` flag. Idempotent — the program rebuilds the member
// list every call so the authority never locks itself out.
const updateIx = await counterProgram.methods
.setPrivacy(isPrivate)
.accountsPartial({
authority: tempKeypair.publicKey,
counter: counterPda,
permission: permissionPda,
permissionProgram: PERMISSION_PROGRAM_ID,
ephemeralVault: EPHEMERAL_VAULT_ID,
magicProgram: MAGIC_PROGRAM_ID,
})
.instruction();
const tx = new Transaction().add(updateIx);
const sig = await sendAndConfirmTransaction(ephemeralConnection, tx, [tempKeypair]);
console.log("set_privacy tx:", sig);
```
**Use Cases:**
* Toggle `is_private` on demand (e.g. private play, public reveal)
* Add new viewers with `TX_LOGS | TX_MESSAGE | TX_BALANCES` flags
* Revoke a member by omitting them from the next call's member list
[⬆️ Back to Top](#ephemeral-permission)
Close the `EphemeralPermission` account on the ER via
`CloseEphemeralPermissionCpi`. Rent is refunded to the data PDA (the
original payer). Optional — only call when the permission is no longer
needed.
```rust theme={null}
use ephemeral_rollups_sdk::access_control::instructions::CloseEphemeralPermissionCpi;
let signers = [
COUNTER_SEED,
ctx.accounts.counter.authority.as_ref(),
&[ctx.bumps.counter],
];
// Refunds the permission's rent to `payer` (the counter PDA).
CloseEphemeralPermissionCpi {
payer: ctx.accounts.counter.to_account_info(),
permissioned_account: ctx.accounts.counter.to_account_info(),
permission: ctx.accounts.permission.to_account_info(),
vault: ctx.accounts.ephemeral_vault.to_account_info(),
magic_program: ctx.accounts.magic_program.to_account_info(),
permission_program: ctx.accounts.permission_program.to_account_info(),
authority: ctx.accounts.counter.to_account_info(),
authority_is_signer: false,
}
.invoke_signed(&[&signers])?;
```
```rust theme={null}
use ephemeral_rollups_sdk::access_control::instructions::CloseEphemeralPermissionCpi;
let seeds: &[&[u8]] = &[
COUNTER_SEED,
permissioned_account_authority.as_ref(),
&[bump],
];
// Refunds the permission's rent to `payer` (the counter PDA).
CloseEphemeralPermissionCpi {
payer: &counter_account_info,
permissioned_account: &counter_account_info,
permission: &permission_account_info,
vault: &ephemeral_vault_account_info,
magic_program: &magic_program_account_info,
permission_program: &permission_program_account_info,
authority: &counter_account_info,
authority_is_signer: false,
}
.invoke_signed(&[seeds])?;
```
```rust theme={null}
use ephemeral_rollups_pinocchio::acl::CloseEphemeralPermission;
use pinocchio::cpi::{Seed, Signer};
let bump_seed = [bump];
let seeds_array: [Seed; 3] = [
Seed::from(b"counter"),
Seed::from(authority.address().as_ref()),
Seed::from(&bump_seed),
];
let signer = Signer::from(&seeds_array);
// Refunds the permission's rent to `payer` (the counter PDA).
CloseEphemeralPermission {
payer: counter_account,
permissioned_account: counter_account,
permission,
vault,
magic_program,
permission_program,
authority: counter_account,
authority_is_signer: false,
}
.invoke_signed(&[signer])?;
```
```typescript theme={null}
import {
MAGIC_PROGRAM_ID,
PERMISSION_PROGRAM_ID,
EPHEMERAL_VAULT_ID,
} from "@magicblock-labs/ephemeral-rollups-sdk";
import { pipe, createTransactionMessage, appendTransactionMessageInstructions } from "@solana/kit";
// Refunds the permission's rent to the counter PDA.
const closeIx = await counterProgram.methods
.closePermission()
.accountsPartial({
authority: tempKeypair.address,
counter: counterPda,
permission: permissionPda,
permissionProgram: PERMISSION_PROGRAM_ID,
ephemeralVault: EPHEMERAL_VAULT_ID,
magicProgram: MAGIC_PROGRAM_ID,
})
.instruction();
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => appendTransactionMessageInstructions([closeIx], tx),
);
const sig = await ephemeralConnection.sendAndConfirmTransaction(
transactionMessage,
[tempKeypair],
{ commitment: "confirmed" },
);
console.log("close_permission tx:", sig);
```
```typescript theme={null}
import {
MAGIC_PROGRAM_ID,
PERMISSION_PROGRAM_ID,
EPHEMERAL_VAULT_ID,
} from "@magicblock-labs/ephemeral-rollups-sdk";
import { Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
// Refunds the permission's rent to the counter PDA.
const closeIx = await counterProgram.methods
.closePermission()
.accountsPartial({
authority: tempKeypair.publicKey,
counter: counterPda,
permission: permissionPda,
permissionProgram: PERMISSION_PROGRAM_ID,
ephemeralVault: EPHEMERAL_VAULT_ID,
magicProgram: MAGIC_PROGRAM_ID,
})
.instruction();
const tx = new Transaction().add(closeIx);
const sig = await sendAndConfirmTransaction(ephemeralConnection, tx, [tempKeypair]);
console.log("close_permission tx:", sig);
```
**Use Cases:**
* Reclaim ephemeral rent before undelegating the data PDA
* Tear down access control once private execution is finished
[⬆️ Back to Top](#ephemeral-permission)
Reference implementations:
[`private-counter/anchor`](https://github.com/magicblock-labs/magicblock-engine-examples/tree/main/private-counter/anchor)
(Anchor)
and
[`private-counter/pinocchio`](https://github.com/magicblock-labs/magicblock-engine-examples/tree/main/private-counter/pinocchio).
***
## Best Practices
1. **Authority Management**: Always assign AUTHORITY\_FLAG to at least one trusted member
2. **Least Privilege**: Grant only necessary flags to each member
3. **Real-time Updates**: Permissions can be updated in real-time on Private Ephemeral Rollup without undelegating, allowing dynamic access control adjustments
4. **Cleanup**: Undelegate and close unused permission accounts to free SOL
***
## Security Considerations
* **Signer Validation**: Only members with AUTHORITY\_FLAG or program with permissioned account can authorize changes
* **Public Accounts**: Setting members to `None` makes the account publicly visible
* **Default Authority**: By default, the owner of the permissioned account is added as permission authority to members of permission account.
* **Empty Member List**: If members field is set to empty list, the permissioned account is fully restricted and private. Only the owner of permissioned account can modify the permission.
* **Access Auditing**: Use member flags to audit and control access
***
Fine-grained Access Control
Privacy Mechanisms and Concepts
Authorization Framework
Compliance Standards and Guidelines
# Local Development
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/how-to-guide/local-development
Run and test your Private Ephemeral Rollup programs locally, with the Query Filtering Service emulating the TEE privacy layer — no hardware attestation required.
***
### Quick Access
Explore private program and test scripts for Anchor, Native Rust, and Pinocchio:
Anchor implementation with permissions.
Pinocchio implementation with magic permission accounts.
***
## Why a Local PER Setup Is Different
In production a **Private Ephemeral Rollup** runs the Ephemeral Rollup inside a
**Trusted Execution Environment (TEE)** on Intel TDX. Clients don't reach the ER
directly — they connect through a **token-gated TEE endpoint** that enforces
privacy and compliance (IP geofencing, OFAC screening, and per-account
[permission filtering](/pages/private-ephemeral-rollups-pers/how-to-guide/access-control))
at ingress, before any transaction is accepted or executed.
You can't run real TDX hardware locally. Instead, the TEE ingress is emulated by
a dedicated process — the **Query Filtering Service (QFS)** — which replicates
the same token-auth and permission-filtering logic **without** hardware
attestation, and fronts an ordinary local ephemeral validator.
That is the only structural difference from a plain
[Ephemeral Rollup local setup](/pages/ephemeral-rollups-ers/how-to-guide/local-development):
**you run the QFS in front of the ER and point your client at the QFS instead of
the ER directly.**
## Topology
Four processes, each fronting the next:
```mermaid theme={null}
graph LR
User((Client))
QFS["Query Filtering Service
:6699 / :6700"]
ER["Ephemeral Rollup
:7799 / :7800"]
SN["Base Solana
:8899 / :8900"]
User <--> QFS
QFS <--> ER
ER <--> SN
```
| Process | RPC / WS | Role |
| ------------------------- | ----------- | --------------------------------------------------------------------------------------------------------- |
| `mb-test-validator` | 8899 / 8900 | Base layer. Wraps `solana-test-validator` and pre-clones the MagicBlock delegation + permission programs. |
| `ephemeral-validator` | 7799 / 7800 | The Ephemeral Rollup itself. |
| `query-filtering-service` | 6699 / 6700 | **PER-specific.** Emulates the TEE ingress — token auth + permission filtering. |
| `vrf-oracle` (optional) | — | Only needed if the program under test uses VRF. |
Point your client at the **QFS (6699)** to test privacy features. Point it at
the **ER (7799)** directly when you don't need the privacy layer.
## Quickstart with mb-stack
`mb-stack` is a single CLI that wraps `mb-test-validator`, `ephemeral-validator`,
and `query-filtering-service`, and handles the startup ordering between them for
you, in a single command. It ships in the same package as `ephemeral-validator`.
```bash theme={null}
npm install -g @magicblock-labs/ephemeral-validator@latest
```
```bash theme={null}
mb-stack --reset
```
This starts `mb-test-validator` (8899 / 8900), `ephemeral-validator`
(7799 / 7800), and `query-filtering-service` (6699 / 6700) together, on the
same default ports used throughout this guide.
Once the stack is up, deploy against the base layer as usual:
```bash theme={null}
anchor build && anchor deploy --provider.cluster localnet
```
```bash theme={null}
cargo build-sbf
solana config set --url localhost
solana program deploy YOUR_PROGRAM_PATH
```
## Per-Service Setup
`mb-stack`, `ephemeral-validator`, `query-filtering-service`, and `mb-test-validator` ship together:
```bash theme={null}
npm install -g @magicblock-labs/ephemeral-validator@latest
```
```bash theme={null}
mb-test-validator --reset
```
```bash theme={null}
anchor build && anchor deploy --provider.cluster localnet
```
```bash theme={null}
cargo build-sbf
solana config set --url localhost
solana program deploy YOUR_PROGRAM_PATH
```
Alternatively, inject the program when the base validator boots so it is
on-chain at its declared address from slot 0:
```bash theme={null}
mb-test-validator --reset \
--upgradeable-program
```
```bash theme={null}
ephemeral-validator \
--lifecycle ephemeral \
--remotes http://127.0.0.1:8899 \
--remotes ws://127.0.0.1:8900 \
--listen 127.0.0.1:7799 \
--reset
```
This is the process that makes it a **private** rollup. It points upstream at
the ER and exposes the client-facing endpoint:
```bash theme={null}
RUST_LOG=info query-filtering-service \
--listen-addr 127.0.0.1:6699 \
--listen-addr-ws 127.0.0.1:6700 \
--ephemeral-url http://127.0.0.1:7799 \
--ephemeral-url-ws ws://127.0.0.1:7800 \
--token-expiry-days 180 \
--add-cors-headers
```
`--add-cors-headers` is required if you test from a browser app. Wait for
port `6699` to accept a connection before firing any request.
If your program requests randomness, run one oracle against the base layer and
one against the ER:
```bash theme={null}
# Base-layer VRF requests
VRF_ORACLE_SKIP_PREFLIGHT=true RPC_URL=http://localhost:8899 \
WEBSOCKET_URL=ws://localhost:8900 RUST_LOG=info vrf-oracle &
# ER VRF requests
VRF_ORACLE_SKIP_PREFLIGHT=true RPC_URL=http://localhost:7799 \
WEBSOCKET_URL=ws://localhost:7800 RUST_LOG=info vrf-oracle &
```
## Connect Your Client Through the QFS
Your client treats the **QFS endpoint as the TEE endpoint**: point
`TEE_PROVIDER_ENDPOINT` at `http://localhost:6699` and reuse the same
[authorization flow](/pages/private-ephemeral-rollups-pers/how-to-guide/quickstart#4-authorize)
from the quickstart — fetch a token by signing a challenge, then open the
connection with `?token=...` attached. When a PDA is set to private via its
`EphemeralPermission`, the QFS blocks any wallet not in the member list — exactly
as the TEE would on devnet.
```ts theme={null}
import { getAuthToken } from "@magicblock-labs/ephemeral-rollups-sdk";
import nacl from "tweetnacl";
// Locally, TEE_PROVIDER_ENDPOINT = http://localhost:6699 (the QFS).
const teeUrl =
process.env.TEE_PROVIDER_ENDPOINT || "https://devnet-tee.magicblock.app";
const teeWsUrl =
process.env.TEE_WS_ENDPOINT || "wss://devnet-tee.magicblock.app";
const token = await getAuthToken(
teeUrl,
payer.publicKey,
(message: Uint8Array) =>
Promise.resolve(nacl.sign.detached(message, payer.secretKey)),
);
const erProvider = new anchor.AnchorProvider(
new anchor.web3.Connection(`${teeUrl}?token=${token.token}`, {
wsEndpoint: `${teeWsUrl}?token=${token.token}`,
commitment: "confirmed",
}),
anchor.Wallet.local(),
);
```
`verifyTeeRpcIntegrity` checks a real TDX attestation and is meant for the
devnet/mainnet TEE endpoints. The local QFS has no hardware attestation, so
skip that check when running fully local.
## Delegate to the Local Validator Identity
When delegating your PDA in a local test, delegate to the **localnet ER
identity** — not a devnet/mainnet one — so commits and undelegations settle
correctly:
```
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
```
These public validators are supported for development. Make sure to add the
specific ER validator in your delegation instruction:
**Mainnet**
-
Asia (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Devnet**
-
Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Localnet**
-
Local ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
## Endpoint Environment Variables
Centralize the endpoints so tests hit the local cluster instead of silently
falling back to devnet. The PER-relevant variables:
```bash theme={null}
export PROVIDER_ENDPOINT=http://localhost:8899 # base layer
export WS_ENDPOINT=ws://localhost:8900
export EPHEMERAL_PROVIDER_ENDPOINT=http://localhost:7799 # ER direct
export EPHEMERAL_WS_ENDPOINT=ws://localhost:7800
export QFS_ENDPOINT=http://localhost:6699 # QFS
export QFS_WS_ENDPOINT=ws://localhost:6700
export TEE_PROVIDER_ENDPOINT=$QFS_ENDPOINT # private tests read this
export TEE_WS_ENDPOINT=$QFS_WS_ENDPOINT
export VALIDATOR=mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
```
***
# Quickstart
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/how-to-guide/quickstart
Enable privacy in any Solana program state account through MagicBlock's ER and Trusted Execution Environment on Intel TDX.
***
**Building with an AI coding agent?** Install the MagicBlock Dev Skill to give your agent MagicBlock-specific patterns — delegation flows, Magic Actions, cranks, VRF, and more.
Quick install for Claude Code:
```bash theme={null}
npx add-skill https://github.com/magicblock-labs/magicblock-dev-skill
```
Using Cursor, Codex, Windsurf, Cline, or another agent? See the [AI Dev Skill](/pages/overview/additional-information/ai-dev-skill) page for all install targets.
### Quick Access
Check out example:
Private Counter Anchor Implementation
Try the Private Counter
MagicBlock's Private Ephemeral Rollup enforces compliance based on node-level
IP geofencing, OFAC-sanction list and restricted jurisdictions at ingress,
before any transaction is accepted or executed. [Find out
more](/pages/private-ephemeral-rollups-pers/introduction/compliance-framework)
***
## Step-By-Step Guide
Build your program, delegate state to the TEE validator, and create an `EphemeralPermission` account directly on the ER via MagicBlock's Permission Program `ACLseoPoyC3cBqoUtkbjZ4aDrkurZW86v19pXz2XQnp1` and Delegation Program `DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh`:
Write your program}>
Write your Solana program as you normally.
Delegate and create permission
}
>
`delegate` delegates the counter to the TEE validator on the base layer.
`init_permission` then runs on the ER — the delegated PDA signs as PDA and
pays its own ephemeral permission rent (pre-funded at `initialize` time).
`set_privacy` flips the public/private flag on demand. No base-layer
permission account to create, delegate, or commit-and-undelegate. [See
access control details](/pages/private-ephemeral-rollups-pers/how-to-guide/access-control).
These public validators are supported for development. Make sure to add the
specific ER validator in your delegation instruction:
**Mainnet**
-
Asia (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Devnet**
-
Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Localnet**
-
Local ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
Deploy your program on Solana}>
Deploy your Solana program using Anchor CLI.
Implement authorization in your client}>
Sign user message to retrieve authorization token from TEE endpoint.
Execute transactions and test privacy}>
Request for authorization token and send confidential transactions.
***
## Private Counter Example
The following software packages may be required, other versions may also be compatible:
| Software | Version | Installation Guide |
| ---------- | ------- | --------------------------------------------------------------- |
| **Solana** | 3.1.9 | [Install Solana](https://docs.anza.xyz/cli/install) |
| **Rust** | 1.89.0 | [Install Rust](https://www.rust-lang.org/tools/install) |
| **Anchor** | 1.0.2 | [Install Anchor](https://www.anchor-lang.com/docs/installation) |
| **Node** | 24.10.0 | [Install Node](https://nodejs.org/en/download/current) |
The EphemeralPermission flow shown below requires `ephemeral-rollups-sdk`
v0.14+ (introduces `CreateEphemeralPermissionCpi` /
`UpdateEphemeralPermissionCpi` / `CloseEphemeralPermissionCpi`). For older
SDK and Anchor versions, see
[legacy examples](https://github.com/magicblock-labs/magicblock-engine-examples/tree/main/00-LEGACY_EXAMPLES).
### Code Snippets
A simple counter program with `initialize` and `increment` instructions, identical in shape to the public counter — privacy is added in the next steps:
```rust theme={null}
#[ephemeral]
#[program]
pub mod private_counter {
use super::*;
/// Initialize the counter.
pub fn initialize(ctx: Context) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count = 0;
Ok(())
}
/// Increment the counter.
pub fn increment(ctx: Context) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count += 1;
Ok(())
}
/// ... Other instructions for delegation, permission, and privacy
}
pub const COUNTER_SEED: &[u8] = b"counter";
/// Context for initializing counter
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init_if_needed, payer = user, space = 8 + 8, seeds = [COUNTER_SEED], bump)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
/// Context for incrementing counter
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(mut, seeds = [COUNTER_SEED], bump)]
pub counter: Account<'info, Counter>,
}
/// Counter struct
#[account]
pub struct Counter {
pub count: u64,
}
/// Other context and accounts for delegation and privacy ...
```
[⬆️ Back to Top](#code-snippets)
The full privacy lifecycle is split across two layers: the **base layer** delegates the counter to a TEE validator; the **ER** then creates / updates / closes its own `EphemeralPermission` account, signed by the delegated PDA itself.
* `initialize` pre-funds the counter PDA with rent for the ephemeral permission, so step 3+ never need a separate lamports-top-up.
* `delegate` delegates only the counter to the TEE validator.
* `init_permission` runs on the ER — the delegated PDA signs a [`CreateEphemeralPermissionCpi`](https://github.com/magicblock-labs/ephemeral-rollups-sdk) using its program seeds and pays the rent. Idempotent.
* `set_privacy(is_private)` toggles privacy on the ER via [`UpdateEphemeralPermissionCpi`](https://github.com/magicblock-labs/ephemeral-rollups-sdk). When private, only the counter's `authority` is in the member list with `TX_LOGS_FLAG | TX_MESSAGE_FLAG | TX_BALANCES_FLAG` — every other wallet is blocked at the TEE ingress.
* `close_permission` refunds the rent back to the PDA when the permission is no longer needed (optional).
* `undelegate` commits and undelegates the counter via `MagicIntentBundleBuilder`.
See [access control](/pages/private-ephemeral-rollups-pers/how-to-guide/access-control) for the full lifecycle and the per-language snippet variants.
These public validators are supported for development. Make sure to add the
specific ER validator in your delegation instruction:
**Mainnet**
-
Asia (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Devnet**
-
Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Localnet**
-
Local ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
```rust theme={null}
use anchor_lang::system_program::{transfer, Transfer};
use ephemeral_rollups_sdk::{
access_control::{
instructions::{
CloseEphemeralPermissionCpi, CreateEphemeralPermissionCpi,
UpdateEphemeralPermissionCpi,
},
structs::{
EphemeralMembersArgs, EphemeralPermission, Member,
TX_BALANCES_FLAG, TX_LOGS_FLAG, TX_MESSAGE_FLAG,
},
},
anchor::{commit, delegate, ephemeral},
cpi::DelegateConfig,
ephem::MagicIntentBundleBuilder,
};
#[ephemeral] // Adds undelegation instruction for the ER validator
#[program]
pub mod private_counter {
use super::*;
/// Initialize on the base layer. Pre-funds the counter PDA with enough
/// lamports to cover the ephemeral permission rent that will be paid on
/// the ER (rent = ~32 lamports/byte × (size + 60); use
/// `EphemeralPermission::size_of(N)` for the exact byte count).
pub fn initialize(ctx: Context) -> Result<()> {
transfer(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
Transfer {
from: ctx.accounts.authority.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
},
),
ephemeral_rollups_sdk::ephemeral_accounts::rent(
EphemeralPermission::size_of(1) as u32,
),
)?;
let counter = &mut ctx.accounts.counter;
counter.count = 0;
counter.authority = ctx.accounts.authority.key();
Ok(())
}
/// Delegate the counter to the (TEE) ER. No permission CPI here —
/// the EphemeralPermission is created directly on the ER via
/// `init_permission` (next instruction).
pub fn delegate(ctx: Context) -> Result<()> {
if ctx.accounts.counter.owner != &ephemeral_rollups_sdk::id() {
let validator = ctx.accounts.validator.as_ref();
ctx.accounts.delegate_counter(
&ctx.accounts.authority,
&[COUNTER_SEED, ctx.accounts.authority.key().as_ref()],
DelegateConfig {
validator: validator.map(|v| v.key()),
..Default::default()
},
)?;
}
Ok(())
}
/// Create the ephemeral permission directly on the ER. Payer = the
/// counter PDA (delegated), which carries its base-layer lamports onto
/// the ER and signs via its program seeds. Idempotent: skip if the
/// permission account already exists. Starts public; flip with
/// `set_privacy`.
pub fn init_permission(ctx: Context) -> Result<()> {
if ctx.accounts.permission.lamports() > 0 {
return Ok(());
}
let signers = [
COUNTER_SEED,
ctx.accounts.counter.authority.as_ref(),
&[ctx.bumps.counter],
];
CreateEphemeralPermissionCpi {
payer: ctx.accounts.counter.to_account_info(),
permissioned_account: ctx.accounts.counter.to_account_info(),
permission: ctx.accounts.permission.to_account_info(),
vault: ctx.accounts.ephemeral_vault.to_account_info(),
magic_program: ctx.accounts.magic_program.to_account_info(),
permission_program: ctx.accounts.permission_program.to_account_info(),
args: EphemeralMembersArgs {
is_private: false,
members: vec![],
},
}
.invoke_signed(&[&signers])?;
Ok(())
}
/// Toggle the privacy flag on the ER. When private, only the counter's
/// `authority` is allowed to read state via the TEE (logs, messages,
/// balances). The authority is the only member; the member list is
/// rebuilt every call so the authority can never lock itself out.
pub fn set_privacy(ctx: Context, is_private: bool) -> Result<()> {
let signers = [
COUNTER_SEED,
ctx.accounts.counter.authority.as_ref(),
&[ctx.bumps.counter],
];
let members = if is_private {
vec![Member {
flags: TX_LOGS_FLAG | TX_MESSAGE_FLAG | TX_BALANCES_FLAG,
pubkey: ctx.accounts.counter.authority,
}]
} else {
vec![]
};
UpdateEphemeralPermissionCpi {
payer: ctx.accounts.counter.to_account_info(),
permissioned_account: ctx.accounts.counter.to_account_info(),
permission: ctx.accounts.permission.to_account_info(),
vault: ctx.accounts.ephemeral_vault.to_account_info(),
magic_program: ctx.accounts.magic_program.to_account_info(),
permission_program: ctx.accounts.permission_program.to_account_info(),
authority: ctx.accounts.counter.to_account_info(),
authority_is_signer: false, // PDA signs via the seeds above
args: EphemeralMembersArgs { is_private, members },
}
.invoke_signed(&[&signers])?;
Ok(())
}
/// Close the ephemeral permission account on the ER, refunding rent to
/// the counter PDA (the payer that originally deposited it).
pub fn close_permission(ctx: Context) -> Result<()> {
let signers = [
COUNTER_SEED,
ctx.accounts.counter.authority.as_ref(),
&[ctx.bumps.counter],
];
CloseEphemeralPermissionCpi {
payer: ctx.accounts.counter.to_account_info(),
permissioned_account: ctx.accounts.counter.to_account_info(),
permission: ctx.accounts.permission.to_account_info(),
vault: ctx.accounts.ephemeral_vault.to_account_info(),
magic_program: ctx.accounts.magic_program.to_account_info(),
permission_program: ctx.accounts.permission_program.to_account_info(),
authority: ctx.accounts.counter.to_account_info(),
authority_is_signer: false,
}
.invoke_signed(&[&signers])?;
Ok(())
}
/// Commit + undelegate the counter when private execution is done. No
/// separate permission undelegation step — ephemeral permissions are
/// confined to the ER and were already cleaned up via `close_permission`
/// (if called).
pub fn undelegate(ctx: Context) -> Result<()> {
MagicIntentBundleBuilder::new(
ctx.accounts.payer.to_account_info(),
ctx.accounts.magic_context.to_account_info(),
ctx.accounts.magic_program.to_account_info(),
)
.commit_and_undelegate(&[ctx.accounts.counter.to_account_info()])
.build_and_invoke()?;
Ok(())
}
}
```
[⬆️ Back to Top](#code-snippets)
Now you’re program is upgraded and ready! Build and deploy to the desired cluster:
```bash theme={null}
anchor build && anchor deploy
```
[⬆️ Back to Top](#code-snippets)
Set up interaction with ER RPC in TEE:
1. Verify integrity of TEE RPC via `https://pccs.phala.network/tdx/certification/v4`
2. Request an authorization token for user to interact with TEE endpoint
```typescript Web3.js theme={null}
import {
verifyTeeRpcIntegrity,
getAuthToken,
} from "@magicblock-labs/ephemeral-rollups-sdk";
// Verify the integrity of the TEE RPC
const isVerified = await verifyTeeRpcIntegrity(EPHEMERAL_RPC_URL);
// Get an auth token before making requests to the TEE
const token = await getAuthToken(
EPHEMERAL_RPC_URL,
wallet.publicKey,
(message: Uint8Array) =>
Promise.resolve(nacl.sign.detached(message, wallet.secretKey)),
);
```
[⬆️ Back to Top](#code-snippets)
Test your program with the Private Ephemeral Rollup connection:
`https://devnet-tee.magicblock.app?token=${token}`
These public validators are supported for development. Make sure to add the
specific ER validator in your delegation instruction:
**Mainnet**
-
Asia (as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (mainnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Devnet**
-
Asia (devnet-as.magicblock.app):
MAS1Dt9qreoRMQ14YQuhg8UTZMMzDdKhmkZMECCzk57
-
EU (devnet-eu.magicblock.app):
MEUGGrYPxKk17hCr7wpT6s8dtNokZj5U2L57vjYMS8e
-
US (devnet-us.magicblock.app):
MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd
-
TEE (devnet-tee.magicblock.app):
MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo
**Localnet**
-
Local ER (localhost:7799):
mAGicPQYBMvcYveUZA5F5UNNwyHvfYh5xkLS2Fr1mev
### Quick Access
Check out example:
Private Counter Anchor Implementation
Try the Private Counter
[⬆️ Back to Top](#code-snippets)
***
### Advanced Code Snippets
These ER building blocks work the same way inside a Private Ephemeral Rollup.
When resizing a delegated PDA:
* PDA must have enough lamports to remain rent-exempt for the new account size.
* If additional lamports are needed, the **payer account must be delegated** to provide the difference.
* PDA must be owned by the program, and the transaction must include any signer(s) required for transferring lamports.
* Use `system_instruction::allocate`
```rust theme={null}
#[account]
pub struct Counter {
pub count: u64,
pub extra_data: Vec,
}
#[derive(Accounts)]
pub struct ResizeCounter<'info> {
#[account(mut)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}
// Resize the counter (e.g., to store more extra_data)
pub fn resize_counter(ctx: Context, new_size: usize) -> Result<()> {
let account_to_resize = &mut ctx.accounts.counter.to_account_info();
let payer = &mut ctx.accounts.payer.to_account_info();
// Calculate rent-exemption for the new size
let rent = Rent::get()?;
let min_balance = rent.minimum_balance(new_size);
// Top up lamports if needed
let current_lamports = **account_to_resize.lamports.borrow();
if current_lamports < min_balance {
let to_transfer = min_balance - current_lamports;
**payer.try_borrow_mut_lamports()? -= to_transfer;
**account_to_resize.try_borrow_mut_lamports()? += to_transfer;
}
// Resize account
account_to_resize.resize(new_size)?;
Ok(())
}
```
[⬆️ Back to Top](#advanced-code-snippets)
Initialize connection with Magic Router before you send transactions dynamically.
These public RPC endpoints are currently free and supported for development:
Magic Router Devnet: [https://devnet-router.magicblock.app](https://devnet-router.magicblock.app)
Choose your preferred SDK to initialize, send and confirm transactions:
* `ephemeral-rollups-kit` for `@solana/kit`
* `ephemeral-rollups-sdk` for `@solana/web.js`
```typescript Kit theme={null}
import { Connection } from "@magicblock-labs/ephemeral-rollups-kit";
// Initialize connection
const connection = await Connection.create(
"https://devnet-router.magicblock.app",
"wss://devnet-router.magicblock.app"
);
// ... create transaction
// Send and confirm transaction
const txHash = await connection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
import { sendAndConfirmTransaction } from "@solana/web3.js";
import { ConnectionMagicRouter } from "@magicblock-labs/ephemeral-rollups-sdk";
// Initialize connection
const connection = new ConnectionMagicRouter(
"https://devnet-router.magicblock.app/",
{ wsEndpoint: "wss://devnet-router.magicblock.app/" }
);
// ... create transaction
// Send and confirm transaction
const txHash = await sendAndConfirmTransaction(connection, tx, [payer], {
skipPreflight: true,
commitment: "confirmed",
});
```
[Learn more about Magic Router](/pages/ephemeral-rollups-ers/introduction/magic-router)
[⬆️ Back to Top](#advanced-code-snippets)
### Quick Access
Explore reference implementation on GitHub
Attach one or more instructions that run automatically on the Solana base layer immediately after an Ephemeral Rollup
(ER) commit.
[Learn more about Magic Action](/pages/ephemeral-rollups-ers/magic-actions/overview)
### 1) Create action instruction
The instruction `update_leaderboard` runs on the base layer immediately after the commit lands. The `#[action]` attribute on its accounts context marks it as callable from a post-commit action.
```rust theme={null}
// program instruction
pub fn update_leaderboard(ctx: Context) -> Result<()> {
let leaderboard = &mut ctx.accounts.leaderboard;
let counter_info = &mut ctx.accounts.counter.to_account_info();
let mut data: &[u8] = &counter_info.try_borrow_data()?;
let counter = Counter::try_deserialize(&mut data)?;
if counter.count > leaderboard.high_score {
leaderboard.high_score = counter.count;
}
msg!(
"Leaderboard updated! High score: {}",
leaderboard.high_score
);
Ok(())
}
// instruction context
#[action]
#[derive(Accounts)]
pub struct UpdateLeaderboard<'info> {
#[account(mut, seeds = [LEADERBOARD_SEED], bump)]
pub leaderboard: Account<'info, Leaderboard>,
/// CHECK: PDA owner depends on: 1) Delegated: Delegation Program; 2) Undelegated: Your program ID
pub counter: UncheckedAccount<'info>,
}
```
### 2) Build the commit instruction with the action
The commit instruction `commit_and_update_leaderboard` runs on the ER. It uses `MagicIntentBundleBuilder` to schedule both the commit and the post-commit action onto `magic_context` — both are applied together when the ER transaction is sealed back to the base layer.
```rust theme={null}
// commit action instruction on ER
pub fn commit_and_update_leaderboard(ctx: Context) -> Result<()> {
// Build the post-commit action that updates the leaderboard on base layer
let instruction_data =
anchor_lang::InstructionData::data(&crate::instruction::UpdateLeaderboard {});
let action_args = ActionArgs::new(instruction_data);
let action_accounts = vec![
ShortAccountMeta {
pubkey: ctx.accounts.leaderboard.key(),
is_writable: true,
},
ShortAccountMeta {
pubkey: ctx.accounts.counter.key(),
is_writable: false,
},
];
let action = CallHandler {
destination_program: crate::ID,
accounts: action_accounts,
args: action_args,
// Signer that pays transaction fees for the action from its escrow PDA
escrow_authority: ctx.accounts.payer.to_account_info(),
compute_units: 200_000,
};
// Schedule commit + post-commit action on magic_context
MagicIntentBundleBuilder::new(
ctx.accounts.payer.to_account_info(),
ctx.accounts.magic_context.to_account_info(),
ctx.accounts.magic_program.to_account_info(),
)
.commit(&[ctx.accounts.counter.to_account_info()])
.add_post_commit_actions([action])
.build_and_invoke()?;
Ok(())
}
// commit action context on ER
#[commit]
#[derive(Accounts)]
pub struct CommitAndUpdateLeaderboard<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(mut, seeds = [COUNTER_SEED], bump)]
pub counter: Account<'info, Counter>,
/// CHECK: Leaderboard PDA - not mut here, writable set in handler
#[account(seeds = [LEADERBOARD_SEED], bump)]
pub leaderboard: UncheckedAccount<'info>,
/// CHECK: Your program ID
pub program_id: AccountInfo<'info>,
}
```
### Execute multiple actions
You can commit multiple accounts and chain several actions in one call. Actions execute sequentially in the order they're passed to `add_post_commit_actions`.
```rust theme={null}
// Chain several actions — they execute sequentially on base layer after the commit lands.
MagicIntentBundleBuilder::new(
ctx.accounts.payer.to_account_info(),
ctx.accounts.magic_context.to_account_info(),
ctx.accounts.magic_program.to_account_info(),
)
.commit(&[
ctx.accounts.counter.to_account_info(),
// ... additional committed accounts
])
.add_post_commit_actions([action_1, action_2, action_3])
.build_and_invoke()?;
```
### Undelegate with actions
Actions can also be chained onto an undelegation — the counter commits, undelegates, and the actions run, all atomically in one ER transaction.
```rust theme={null}
// Commit, undelegate, AND execute actions — all atomically on base layer after the ER transaction seals.
MagicIntentBundleBuilder::new(
ctx.accounts.payer.to_account_info(),
ctx.accounts.magic_context.to_account_info(),
ctx.accounts.magic_program.to_account_info(),
)
.commit_and_undelegate(&[ctx.accounts.counter.to_account_info()])
.add_post_commit_actions([action])
.build_and_invoke()?;
```
[⬆️ Back to Top](#advanced-code-snippets)
Top up a delegated account's lamports on the ER side. The transaction is submitted on the **base layer** and uses the Ephemeral SPL Token program to shuttle lamports to the destination's delegated balance via a single-use lamports PDA.
Common use case: keeping a delegated fee payer funded so it can keep paying its own commits past the default 10-commit sponsorship cap.
Notes:
* Generate a fresh 32-byte salt per top-up via `crypto.getRandomValues` — re-using a salt collides with an existing PDA.
* Submit to the base-layer RPC, not the ER.
* The destination must already be delegated.
```typescript theme={null}
import {
Connection,
Keypair,
PublicKey,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
lamportsDelegatedTransferIx,
deriveLamportsPda,
} from "@magicblock-labs/ephemeral-rollups-sdk";
/**
* Top up a delegated account with lamports.
*
* The transaction is submitted on the BASE LAYER. The Ephemeral SPL Token
* program creates a single-use lamports PDA, funds it from the payer, and
* delegates it so the ER credits the destination's delegated balance.
*/
async function topUpDelegatedAccount(
connection: Connection, // base-layer connection
payer: Keypair,
destination: PublicKey, // delegated account to top up
amountLamports: bigint,
) {
// Generate a fresh 32-byte salt per top-up.
// Re-using a salt collides with an existing lamports PDA and the call fails.
const salt = crypto.getRandomValues(new Uint8Array(32));
const [lamportsPda] = deriveLamportsPda(payer.publicKey, destination, salt);
const ix = await lamportsDelegatedTransferIx(
payer.publicKey,
destination,
amountLamports,
salt,
);
const tx = new Transaction().add(ix);
tx.feePayer = payer.publicKey;
// CRITICAL: send to the base-layer RPC, not the ER.
const sig = await sendAndConfirmTransaction(connection, tx, [payer], {
commitment: "confirmed",
skipPreflight: true,
});
return { sig, lamportsPda };
}
```
[⬆️ Back to Top](#advanced-code-snippets)
### Quick Access
On-Curve Delegation
Required signers for delegating an on-curve account:
1. On-curve account to be delegated
2. Fee payer
Required instructions for delegating on-curve accounts:
1. Assign System Account to Delegation Program
2. Delegate to Delegation Program
```typescript Kit theme={null}
// Create assign instruction
// The on-curve account must sign this instruction to change its owner
const accountSigner = await cryptoKeyPairToTransactionSigner(userKeypair);
const delegationProgramAddress = address(DELEGATION_PROGRAM_ID.toString());
const assignInstruction = getAssignInstruction({
account: accountSigner,
programAddress: delegationProgramAddress,
});
// Create delegate instruction
const delegateInstruction = await createDelegateInstruction({
payer: feePayerAddress,
delegatedAccount: userAddress,
ownerProgram: ownerProgramAddress,
validator: validatorAddress,
});
// Prepare transaction
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayer(feePayerAddress, tx),
(tx) =>
appendTransactionMessageInstructions(
[assignInstruction, delegateInstruction],
tx
)
);
// Send and confirm transaction (fee payer need to sign, on-curve account cannot be signer since delegated)
const txHash = await connection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair, feePayerKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
// Create assign instruction
const assignInstruction = SystemProgram.assign({
accountPubkey: userPubkey,
programId: DELEGATION_PROGRAM_ID,
});
// Create delegate instruction
const delegateInstruction = createDelegateInstruction({
payer: feePayerKeypair.publicKey,
delegatedAccount: userPubkey,
ownerProgram: ownerProgram,
validator: validator,
});
// Create and send transaction (fee payer need to sign, on-curve account cannot be signer since delegated)
const tx = new Transaction().add(assignInstruction, delegateInstruction);
tx.feePayer = feePayerKeypair.publicKey;
const txSignature = await sendAndConfirmTransaction(
connectionBaseLayer,
tx,
[userKeypair, feePayerKeypair],
{
skipPreflight: true,
}
);
```
Direct commit and undelegate through Magic Program only.
```typescript Kit theme={null}
// Create commit and undelegate instruction
const commitAndUndelegateInstruction = createCommitAndUndelegateInstruction(
userAddress,
[userAddress]
);
// Prepare transaction
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayer(feePayerAddress, tx),
(tx) =>
appendTransactionMessageInstructions([commitAndUndelegateInstruction], tx)
);
// Send and confirm transaction on ephemeral connection
const txHash = await ephemeralConnection.sendAndConfirmTransaction(
transactionMessage,
[userKeypair, feePayerKeypair],
{ commitment: "confirmed", skipPreflight: true }
);
```
```typescript Web3.js theme={null}
// Create commit and undelegate instruction
const commitAndUndelegateInstruction = createCommitAndUndelegateInstruction(
userPubkey,
[userPubkey]
);
// Send and confirm transaction on ephemeral connection
const tx = new Transaction().add(commitAndUndelegateInstruction);
tx.feePayer = feePayerKeypair.publicKey;
const txSignature = await sendAndConfirmTransaction(
ephemeralConnection,
tx,
[userKeypair, feePayerKeypair],
{
skipPreflight: true,
}
);
```
[⬆️ Back to Top](#advanced-code-snippets)
### Quick Access
Explore reference implementation on GitHub
Attach instructions to a delegation that the ER validator runs automatically
inside the rollup, right after the account is delegated — no extra transaction:
* Build the action(s) as standard `Instruction`s and convert them to the
compact payload with `.cleartext()` (public) — encrypted actions are built
off-chain by a client holding the validator key.
* The base-layer delegation program stores the payload in the delegation
record; the ER validator executes it once the account lands in the rollup.
* CPI with `delegate_account_with_actions` instead of the plain `delegate_pda`
helper; the `#[delegate]` macro still provides the buffer/record/metadata
accounts.
```rust theme={null}
/// Reuse the same accounts context as a normal delegation
#[delegate]
#[derive(Accounts)]
pub struct DelegateInput<'info> {
pub payer: Signer<'info>,
/// CHECK: The pda to delegate
#[account(mut, del)]
pub pda: AccountInfo<'info>,
}
```
```rust theme={null}
use anchor_lang::solana_program::instruction::{AccountMeta, Instruction};
use anchor_lang::InstructionData;
use ephemeral_rollups_sdk::cpi::{
delegate_account_with_actions, DelegateAccounts, DelegateConfig,
};
use ephemeral_rollups_sdk::dlp_api::compact::ClearText;
/// Delegate the account AND attach a post-delegation action. The action is stored
/// in the delegation record on the base layer and executed automatically by the ER
/// validator inside the rollup, right after the account is delegated — no extra
/// transaction. Here the action is a self-CPI back into `increment`.
pub fn delegate_with_actions(ctx: Context) -> Result<()> {
let counter_key = ctx.accounts.pda.key();
// The instruction the ER validator runs post-delegation, inside the rollup.
let increment_action = Instruction {
program_id: crate::ID,
accounts: vec![AccountMeta::new(counter_key, false)],
data: crate::instruction::Increment {}.data(),
};
// Convert to the compact, cleartext post-delegation actions payload.
// (Use `cleartext` for public actions; encrypted actions are built off-chain
// by a client that holds the validator key.)
let actions = vec![increment_action].cleartext();
let payer = ctx.accounts.payer.to_account_info();
let pda = ctx.accounts.pda.to_account_info();
let delegate_accounts = DelegateAccounts {
payer: &payer,
pda: &pda,
owner_program: &ctx.accounts.owner_program,
buffer: &ctx.accounts.buffer_pda,
delegation_record: &ctx.accounts.delegation_record_pda,
delegation_metadata: &ctx.accounts.delegation_metadata_pda,
delegation_program: &ctx.accounts.delegation_program,
system_program: &ctx.accounts.system_program,
};
delegate_account_with_actions(
delegate_accounts,
&[COUNTER_SEED],
DelegateConfig {
// Optionally set a specific validator from the first remaining account
validator: ctx.remaining_accounts.first().map(|acc| acc.key()),
..Default::default()
},
actions,
// No extra signers are required by the increment action.
&[],
)?;
Ok(())
}
```
[⬆️ Back to Top](#advanced-code-snippets)
***
Fine-grained Access Control
Privacy Mechanisms and Concepts
Authorization Framework
Compliance Standards and Guidelines
***
## Solana Explorer
Get insights about your transactions and accounts on Solana:
Official Solana Explorer
Explore Solana Blockchain
## Solana RPC Providers
Send transactions and requests through existing RPC providers:
Free Public Nodes
Free Shared Nodes
Dedicated High-Performance Nodes
## Solana Validator Dashboard
Find real-time updates on Solana's validator infrastructure:
Get Validator Insights
Discover Validator Metrics
## Server Status
Subscribe to Solana's and MagicBlock's server status:
Subscribe to Solana Server Updates
Subscribe to MagicBlock Server Status
***
## MagicBlock Products
Execute real-time, zero-fee transactions securely on Solana.
Protect sensitive data with compliance — built on top of Ephemeral Rollups.
Add private onchain transfers to your app in seconds — compliant by default.
Add provably fair onchain randomness to games, raffles, and real-time apps.
Access low-latency onchain price feeds for trading and DeFi.
***
# Authorization
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/introduction/authorization
Customize authorized access through onchain restrictions on account level for user groups.
***
### Authorization Model
Private Ephemeral Rollups use a Permission Program to manage fine-grained privacy controls for accounts and account groups. This runs on Solana L1 and can be updated on the fly.
* **Permission Groups**: Define groups with arbitrary membership and IDs via CPI. A group aggregates users and the accounts governed by its permissions.
* **Permissions**: Add permissions to groups. Today a permission implies read access for the delegated account; read/write splits may be added in the future.
* **Access**: Client access to permissioned ER state requires authenticating ownership of a specified public key. Successful authentication yields a token used to query the ER.
Private Ephemeral Rollup (devnet) endpoint:
`https://devnet-tee.magicblock.app?token= {authToken}`. Replace `{authToken}` with your authorization token obtained
from the TEE RPC to send requests.
This abstraction into groups lets you modify the permissions for many users/accounts atomically in a single transaction.
***
Fine-grained Access Control
Privacy Mechanisms and Concepts
Authorization Framework
Compliance Standards and Guidelines
# Compliance Framework
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/introduction/compliance-framework
MagicBlock enables confidential execution without compromising performance, compliance, or control.
### Overview
MagicBlock Private Ephemeral Rollups (PERs) enable confidential execution while enforcing account-level state access and regulatory-compliant controls. We believe in a form of privacy that is respectful of users' rights and lawful. Private ERs are not open anonymity rails, but rather private environments with enforced boundaries that can be customized for specific use cases.
### Built for Institutional Requirements: Performance, Compliance, and Control
Businesses and institutions do not choose between performance, compliance, and control; they require all three. Systems that sacrifice performance for compliance are unusable at scale, while systems that optimize solely for speed or privacy without enforceable controls are incompatible with institutional mandates or the day-to-day operations of lawful businesses.
MagicBlock's Trusted Execution Environment (TEE) architecture is explicitly designed to satisfy these requirements simultaneously.
* **Performance**: Private ERs deliver low-latency, high-throughput execution suitable for real-time applications, market-sensitive workflows, and on-chain systems that cannot tolerate delayed or probabilistic settlement.
* **Compliance**: Jurisdictional enforcement, real-time AML and sanctions screening, and clear, upfront licensing ensure private execution operates within clearly defined legal and regulatory boundaries.
* **Control**: Access to private execution is conditional, configurable, and enforceable at the on-chain program level. Institutions retain control over who can connect and under what constraints assets may enter or exit.
This approach allows institutions to unlock the benefits of confidential execution while preserving the guarantees they require to operate responsibly at scale.
### Compliance Safeguards
* **Jurisdiction & Network Access Controls**\
Private ER access is enforced at the infrastructure layer through node-level IP geofencing. Connections originating from OFAC-sanctioned or otherwise restricted jurisdictions are blocked at ingress, before any transaction is accepted or executed. These controls ensure jurisdictional restrictions are enforced deterministically and upstream of execution.
* **Real-Time AML & Sanctions Screening**\
All relevant interaction points with Private ERs are subject to continuous, real-time AML and sanctions screening via Range. This includes sanctions list verification, exposure and counterparty risk assessment, and behavioral risk signals. Transactions that fail screening are rejected or halted prior to execution or settlement, preventing tainted flows from entering or exiting the private environment.
* **EULA & Licensed Deployments**\
Private ER instances are operated under explicit licensing and policy constraints defined by MagicBlock Labs and, where applicable, its partners. Different licenses can be applied to different instances to ensure the open-source software we provide explicitly forbids illicit use cases or misuse of the technology for unwarranted transactions.
MagicBlock Private ERs deliver confidential execution within clearly enforced legal and regulatory boundaries.
***
Fine-grained Access Control
Privacy Mechanisms and Concepts
Authorization Framework
Compliance Standards and Guidelines
# Onchain Privacy
Source: https://docs.magicblock.gg/pages/private-ephemeral-rollups-pers/introduction/onchain-privacy
The high-performance engine for real-time applications that require privacy and compliance on Solana
***
### MagicBlock Private Ephemeral Rollup (PER)
MagicBlock is bringing high-performance, general-purpose Trusted Execution Environments (TEEs) to Solana. We call this the Private Ephemeral Rollup (PER).
By using the security guarantees of Intel Trust Domain Extension (TDX) architecture combined with MagicBlock’s Ephemeral Rollup (ER) technology, we’re making it possible to run sensitive logic inside of a hardware-secured environment, while inheriting the composability and speed of Solana.
For the first time, builders can design applications that are simultaneously:
* **Confidential**: state is protected from all unauthorized parties
* **Scalable**: running inside an ER that can execute blocks at high throughput and low latency
* **Composable**: still able to interoperate with other Solana programs
* **Compliant**: easy-to-enforce compliance thanks to a fine-grained access control layer
This enables use cases like confidential transfers, sealed-bid auctions, and secure identity flows on Solana.
### Trusted Execution Environments on Intel TDX
A trusted execution environment acts as a vault inside a CPU. Generally, when you run a program (e.g., a validator), the operating system can see and influence everything: the code, the state, and the memory. A TEE creates a hardware-secured space that prevents interference, even by the machine it’s running on.
We use a TEE to protect the state of an Ephemeral Rollup. Normally, when you execute a smart contract onchain, every step of the process is visible: the program being called, the accounts, and the resulting state changes. Using the TEE, programs can selectively shield ER state.
* Every account is public by default, like in Solana
* Programs can explicitly define access rules for their accounts
This means transfers, program calls, and interactions can avoid being broadcast publicly when desired.
### Why TEE
There are multiple privacy-preserving approaches used in blockchains. Each has different tradeoffs.
| Solution | What is it? | Pros | Cons |
| ------------------------------ | --------------------------------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
| Trusted Execution Environments | Hardware-secured execution within a CPU | Near-native performance, run normal code | Trust assumption in vendor hardware |
| Fully Homomorphic Encryption | Compute directly on encrypted data | Data never decrypted | Extremely slow, specialized tooling, difficult key management, bugs and vulnerabilities |
| Zero Knowledge Proofs | Prove something without revealing inputs | Efficient verification; strong for identity/compliance/correctness | Proving is heavy, bugs and vulnerabilities |
| Multi-party Computation | Split a secret across multiple parties to compute jointly | Strong cryptographic guarantees | Coordination overhead, high latency, specialized tooling, bugs and vulnerabilities |
* **ZK**: efficient verification, not optimized for general low-latency computation
* **MPC**: shared trust, higher latency and coordination
* **FHE**: powerful in theory, not ready for general usage today
* **TEE**: practical confidentiality with real-time performance and familiar developer UX on Solana
### Application Unlocks
* **Confidential Transfers**: move assets privately without exposing balances or counterparties
* **Sealed-Bid Auctions**: keep bids hidden until settlement for fair price discovery
* **Private Games**: support games where revealing state undermines gameplay
* **Enterprise and Compliance Flows**: run sensitive operations with blockchain guarantees while keeping data private
* **Identity-Based Access**: verify group membership without revealing unnecessary account history
Private Ephemeral Rollup (devnet) endpoint:
`https://devnet-tee.magicblock.app?token= {authToken}`. Replace `{authToken}` with your authorization token obtained
from the TEE RPC to send requests.
***
Fine-grained Access Control
Privacy Mechanisms and Concepts
Authorization Framework
Compliance Standards and Guidelines
# Counter
Source: https://docs.magicblock.gg/pages/templates/counter
Explore the source code and implementation
***
## Overview
Track and update counters on-chain with low latency using Ephemeral Rollups. This simple example demonstrates the core concepts of delegating accounts to ephemeral rollups and achieving high-frequency state updates.
***
## Related Products
Learn how Ephemeral Rollups work
Get started with Ephemeral Rollups
Build with Anchor framework
Understand transaction routing
# Gachapon
Source: https://docs.magicblock.gg/pages/templates/gachapon
Explore the source code and implementation
***
## Overview
Mint Metaplex NFTs through a gachapon-style machine powered by MagicBlock VRF. This template requests verifiable randomness to select the prize outcome, then mints the matching NFT so each pull is unpredictable, auditable, and suitable for games or collectible drops.
***
## Related Products
Learn why verifiable randomness matters
Get started with verifiable randomness
Learn VRF implementation best practices
Explore Metaplex NFT standards and tooling
# Onchain Dice
Source: https://docs.magicblock.gg/pages/templates/onchain-dice
Explore the source code and implementation
***
## Overview
Roll dice with provable on-chain randomness using VRFs. This demo shows how to implement fair and verifiable dice rolling mechanics.
***
## Related Products
Learn why verifiable randomness matters
Get started with verifiable randomness
Understand VRF security guarantees
Common questions about VRFs
# Private Payments
Source: https://docs.magicblock.gg/pages/templates/private-payments
Try the live application
Explore the source code and implementation
Fastest and simplest way to build private payments
Learn more about building private payments
***
## Overview
Send and receive payments privately on-chain using Private Ephemeral Rollups. This demo showcases how to implement confidential transactions while maintaining the security and composability of blockchain technology.
***
## Related Products
Learn how Private ERs enable confidential on-chain computations
Understand authorization mechanisms in Private ERs
Get started with building private applications
Compliance Standards and Guidelines
# Random Character Generator
Source: https://docs.magicblock.gg/pages/templates/random-character-generator
Try the live application
Explore the source code and implementation
Learn more about verifiable randomness
***
## Overview
Generate unique characters using verifiable randomness on-chain. This demo showcases how to leverage VRFs (Verifiable Random Functions) to create provably fair and unpredictable character attributes for gaming and NFT applications.
***
## Related Products
Learn why verifiable randomness matters
Get started with verifiable randomness
Learn VRF implementation best practices
Understand how VRF works under the hood
# Real-Time Price Feed
Source: https://docs.magicblock.gg/pages/templates/real-time-price-feed
Try the live application
Explore the source code and implementation
Learn more about real-time price feeds
***
## Overview
Access live market prices via Pyth oracles on Ephemeral Rollups. This template demonstrates how to integrate real-time price data into your on-chain applications with minimal latency.
***
## Related Products
Learn how Ephemeral Rollups enable real-time operations
Understand how to integrate price oracles
Learn how to implement oracle-based applications
Get started with Ephemeral Rollups
# Rock Paper Scissors
Source: https://docs.magicblock.gg/pages/templates/rock-paper-scissors
Try the live application
Explore the source code and implementation
***
## Overview
Play confidential Rock Paper Scissors using Private Ephemeral Rollups. Each move lives in a private TEE account that neither the opponent nor the RPC can read — the winner is revealed automatically the instant the last choice lands, and either player can start a rematch that reuses the same accounts with no new rent.
The example includes a web app with a solo mode against a robot and a two-player mode where a friend joins via link or QR code.
***
## Related Products
Learn how Private ERs enable confidential on-chain computations
Understand authorization mechanisms in Private ERs
Get started with building private applications
Set fine-grained permissions on ephemeral accounts
# Introduction
Source: https://docs.magicblock.gg/pages/tools/introduction
Utilize existing SDKs and Frameworks to accelerate your development process
* [**Wallets & Onramp**](/pages/tools/wallets-and-onramp/overview) – Integrate with existing **wallet and onramp** providers
* [**Session Keys**](/pages/tools/session-keys/introduction) – Implement **tiered access management** for Solana programs
* [**Oracles**](/pages/tools/oracle/introduction) – Access real-time price data streams for DeFi applications.
* [**Cranks**](/pages/tools/crank/introduction) – Schedule **automated, time-based execution** of on-chain instructions.
* [**Solana Unity SDK**](/pages/tools/solana-unity-sdk/overview) – Integrate Solana into **Unity-based games** effortlessly.
* [**SOAR**](/pages/tools/open-source-programs/SOAR) – Enable **on-chain achievements, leaderboards, and rewards**.
Onboard users with wallets frictionlessly
Integrate with Session Keys
Pyth Lazer on MagicBlock Ephemeral Rollups
Schedule automated, time-based execution of on-chain instructions
Create games with the Solana Unity SDK
Create on-chain achievement, leaderboard and rewards with SOAR
# How do Session Keys work?
Source: https://docs.magicblock.gg/pages/tools/session-keys/how-do-session-keys-work
What are Session Keys?
Session Keys are meant to be used as secondary signers in your program, especially for frequent interactions like liking a post or moving a piece in a game of chess where constant popups can get in the way of smooth user experience. **They are not burner wallets**.
Session Keys work in tandem with our on chain program to validate the token and it's scope.
## **Session Keys have two components**
1. An **Ephemeral Keypair**, intended to be used as a **secondary signer** in the target program.
2. A **Session Token**, a PDA containing information about **expiry and scope** of the keypair.
## **How do they work?**
* Ephemeral Keys are stored on the client side, to invoke transactions.
* The transactions invoked by these ephemeral keys are validated in the target program for their validity, expiry and scope.
* Every transaction needs to present both the ephemeral signer and the session token
* This is the general idea behind *account abstraction*, where instead of just an externally owned key there is also smart contract that enhances security.
# Introduction
Source: https://docs.magicblock.gg/pages/tools/session-keys/introduction
What are Session Keys?
Session Keys are ephemeral keys with fine-grained instruction scoping for tiered access in your Solana Programs.
Session keys are a giant leap for improving UX for users as they take away the need for repeated wallet popups while a user performs actions in-game like purchases or on-chain interactions. The expiry and access are stored at the contract level, which reduces the exposure of session keys to potential security vulnerabilities, though it does not eliminate risks from browser compromise, malicious extensions, or application bugs. The Session Keys parameters can be duration, the maximum amount of tokens spent, amount of transactions or any other function specific to your use case.
You can also provide a layered security model which allows tiered access to a session key making sure a user’s assets are always secure and can’t be accessed by the session keys.
This type of layered security is a standard model in web2 applications and provides a stronger defence against attacks and helps ensure the security of your users' asset. This is now possible in web3 with the use of Session Keys at the contract level.
#### Example use cases for Session Keys
* An uninterrupted gaming experience for an on-chain game.
* A seamless experience for in-app NFTs purchase
* A layered security model for your game/dApp
The original Session key program was developed by *[Gum](https://gum.fun/)*.
The newly deployed program is 'KeyspM2ssCJbqUhQ4k7sveSiY4WjnYsrXkC8oDbwde5'
# Overview
Source: https://docs.magicblock.gg/pages/tools/wallets-and-onramp/overview
Accelerate user onboarding with existing wallet and onramp solutions
***
## Quick Access
Find a wallet that works for you
Move funds at the speed of light in 100+ countries
Solana's Official Developer Resource
***
## Wallet Comparison
When interacting with Solana, your wallet choice affects user **security**, **usability**, and **onboarding experience**. Here’s a breakdown of different wallet types:
| **Wallet Type** | **Description** | **Examples** | **Recommended** |
| -------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| **Hot Wallet** | Internet-connected, fast, and convenient for frequent use. | [Phantom](https://phantom.app), [Backpack](https://backpack.app), [Solflare](https://solflare.com) | Everyday users, DeFi, NFTs, dApps |
| **Cold Wallet (Hardware)** | Offline storage with top-tier security. Private keys never touch the internet. | [Ledger](https://www.ledger.com), [Trezor](https://trezor.io/) | Long-term holders, large portfolio protection |
| **Custodial Wallet** | Keys managed by an exchange or third party for simplicity and recovery support. | [Coinbase Exchange](https://www.coinbase.com), [Binance](https://www.binance.com) | Beginners, users who prefer convenience over full control |
| **Smart Contract Wallet** | On-chain programmable wallets with features like multi-sig and recovery. | [Squads](https://squads.so), | Teams, DAOs, advanced DeFi setups |
| **Embedded Wallet** | Abstracts keys for smooth Web2-like onboarding (via email, social, etc.). | [Privy](https://www.privy.io), [Web3Auth](https://web3auth.io) | Apps that onboard non-crypto users |
***
## Privy
You can easily empower non-crypto users with social login and onramp through Privy's embedded wallet.
Web3 onboarding for non-crypto users
***
# Best Practices
Source: https://docs.magicblock.gg/pages/verifiable-randomness-functions-vrfs/how-to-guide/best-practices
Guidelines for integrating verifiable randomness
Use these tips to get the most reliable results when requesting randomness.
Ensure your rollup delegates to a trusted oracle queue with the `DelegateOracleQueue` instruction so that requests are fulfilled promptly.
### Seed Selection
* Combine user input with server timestamps or game state to create unpredictable seeds.
* Avoid letting players specify the entire `caller_seed` to prevent grinding.
### Callback Validation
* Always check the `vrf_program_identity` to confirm the callback is from MagicBlock's official signer.
* Reject callbacks that do not match the original request parameters.
### State Management
* Store any pending request identifiers so that retries or unexpected callbacks can be handled safely.
* Track the oracle queue your requests were delegated to so results can be matched back correctly.
* Keep random outputs ephemeral—consume them immediately within your [rollup logic](/pages/get-started/introduction/ephemeral-rollup) and avoid reusing them.
Following these guidelines reduces the risk of manipulation and ensures consistent randomness across sessions.
# Quickstart
Source: https://docs.magicblock.gg/pages/verifiable-randomness-functions-vrfs/how-to-guide/quickstart
Learn how to request and consume Solana VRF randomness onchain using the MagicBlock VRF SDK.
***
**Building with an AI coding agent?** Install the MagicBlock Dev Skill to give your agent MagicBlock-specific patterns — delegation flows, Magic Actions, cranks, VRF, and more.
Quick install for Claude Code:
```bash theme={null}
npx add-skill https://github.com/magicblock-labs/magicblock-dev-skill
```
Using Cursor, Codex, Windsurf, Cline, or another agent? See the [AI Dev Skill](/pages/overview/additional-information/ai-dev-skill) page for all install targets.
### Quick Access
Check out basic VRF examples:
Repo for roll dice example
Roll a dice onchain
Roll a dice within 100 ms onchain
***
Need the product overview first? Start with the Solana VRF landing page, then follow this quickstart.
## Step-By-Step Guide
Any Solana program can request and consume verifiable randomness onchain within seconds using the MagicBlock VRF SDK. By the end of this guide, you'll have a working example that rolls a dice using verifiable randomness.
Write your program}>
Write your Solana program as you normally.
Add request and consume randomness instructions.
}
>
Add CPI hooks that request and consume randomness via callback from a
verified oracle.
Deploy your program on Solana}>
Deploy your Solana program using Anchor CLI.
Execute transactions for onchain randomness.}>
Send transactions to generate and consume randomness onchain.
***
## Roll Dice Example
The following software packages may be required, other versions may also be compatible:
| Software | Version | Installation Guide |
| ---------- | ------- | --------------------------------------------------------------- |
| **Solana** | 3.1.9 | [Install Solana](https://docs.anza.xyz/cli/install) |
| **Rust** | 1.89.0 | [Install Rust](https://www.rust-lang.org/tools/install) |
| **Anchor** | 1.0.2 | [Install Anchor](https://www.anchor-lang.com/docs/installation) |
| **Node** | 24.10.0 | [Install Node](https://nodejs.org/en/download/current) |
### Code Snippets
A simple roll dice program where player initialize state account to store, request and consume randomness:
```rust theme={null}
pub const PLAYER: &[u8] = b"playerd";
#[program]
pub mod random_dice {
use super::*;
pub fn initialize(ctx: Context) -> Result<()> {
msg!(
"Initializing player account: {:?}",
ctx.accounts.player.key()
);
Ok(())
}
// ... Additional instructions will be added here
}
/// Context for initializing player
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(init_if_needed, payer = payer, space = 8 + 1, seeds = [PLAYER, payer.key().to_bytes().as_slice()], bump)]
pub player: Account<'info, Player>,
pub system_program: Program<'info, System>,
}
/// Player struct
#[account]
pub struct Player {
pub last_result: u8,
}
```
[⬆️ Back to Top](#code-snippets)
1. Add `ephemeral_vrf_sdk` with Anchor features to your program
```bash theme={null}
cargo add ephemeral_vrf_sdk --features anchor
```
Import `vrf` , `create_request_randomness_ix`, `RequestRandomnessParams`, and `SerializableAccountMeta`:
```rust theme={null}
use ephemeral_vrf_sdk::anchor::vrf;
use ephemeral_vrf_sdk::instructions::{create_request_randomness_ix, RequestRandomnessParams};
use ephemeral_vrf_sdk::types::SerializableAccountMeta;
```
2. Add instructions `roll_dice` to request randomness and `callback_roll_dice` to consume randomness, along with its context:
```rust theme={null}
#[program]
pub mod random_dice {
use super::*;
// ... `initialize` instruction
// Request Randomness
pub fn roll_dice(ctx: Context, client_seed: u8) -> Result<()> {
msg!("Requesting randomness...");
let ix = create_request_randomness_ix(RequestRandomnessParams {
payer: ctx.accounts.payer.key(),
oracle_queue: ctx.accounts.oracle_queue.key(),
callback_program_id: ID,
callback_discriminator: instruction::CallbackRollDice::DISCRIMINATOR.to_vec(),
caller_seed: [client_seed; 32],
// Specify any account that is required by the callback
accounts_metas: Some(vec![SerializableAccountMeta {
pubkey: ctx.accounts.player.key(),
is_signer: false,
is_writable: true,
}]),
..Default::default()
});
ctx.accounts
.invoke_signed_vrf(&ctx.accounts.payer.to_account_info(), &ix)?;
Ok(())
}
// Consume Randomness
pub fn callback_roll_dice(
ctx: Context,
randomness: [u8; 32],
) -> Result<()> {
let rnd_u8 = ephemeral_vrf_sdk::rnd::random_u8_with_range(&randomness, 1, 6);
msg!("Consuming random number: {:?}", rnd_u8);
let player = &mut ctx.accounts.player;
player.last_result = rnd_u8; // Update the player's last result
Ok(())
}
}
#[vrf]
#[derive(Accounts)]
pub struct DoRollDiceCtx<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(seeds = [PLAYER, payer.key().to_bytes().as_slice()], bump)]
pub player: Account<'info, Player>,
/// CHECK: The oracle queue
#[account(mut, address = ephemeral_vrf_sdk::consts::DEFAULT_QUEUE)]
pub oracle_queue: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct CallbackRollDiceCtx<'info> {
/// This check ensure that the vrf_program_identity (which is a PDA) is a singer
/// enforcing the callback is executed by the VRF program trough CPI
#[account(address = ephemeral_vrf_sdk::consts::VRF_PROGRAM_IDENTITY)]
pub vrf_program_identity: Signer<'info>,
#[account(mut)]
pub player: Account<'info, Player>,
}
// ... Other context and account struct.
```
**VRF SDK constants** (`ephemeral_vrf_sdk::consts`) — reference these instead of hardcoding addresses, both in your program and in client/test code:
| Constant | Purpose | Address |
| ------------------------------ | --------------------------------- | ---------------------------------------------- |
| `VRF_PROGRAM_ID` | VRF program | `Vrf1RNUjXmQGjmQrQLvJHs9SNkvDJEsRVFPkfSQUwGz` |
| `VRF_PROGRAM_IDENTITY` | Callback signer PDA | `9irBy75QS2BN81FUgXuHcjqceJJRuc9oDkAe8TKVvvAw` |
| `DEFAULT_QUEUE` | Base-layer queue (mainnet/devnet) | `Cuj97ggrhhidhbu39TijNVqE74xvKJ69gDervRUXAxGh` |
| `DEFAULT_EPHEMERAL_QUEUE` | ER queue (mainnet/devnet) | `5hBR571xnXppuCPveTrctfTU7tJLSN94nq7kv7FRK5Tc` |
| `DEFAULT_TEST_QUEUE` | Base-layer queue (localnet) | `GKE6d7iv8kCBrsxr78W3xVdjGLLLJnxsGiuzrsZCGEvb` |
| `DEFAULT_EPHEMERAL_TEST_QUEUE` | ER queue (localnet) | `Sc9MJUngNbQXSXGP3F67KvKwVnhaYn6kcioxXNVowYT` |
Pass the queue that matches where your transaction runs as `oracle_queue`. Mainnet and Devnet share the same queue addresses; localnet uses the test queues.
> `Request Randomness` is the process of generating a random `hashId` with the relevant callback instruction for the verified oracles to be triggered.
> `Consume Randomness` is the process of using the verifiable randomness by your program which is provided and triggered through verified oracle.
[⬆️ Back to Top](#code-snippets)
Now you’re program is upgraded and ready! Build and deploy to the desired
cluster:
```bash theme={null}
anchor build && anchor deploy
```
[⬆️ Back to Top](#code-snippets)
Ready to execute transactions for onchain randomness!
```bash theme={null}
anchor test --skip-build --skip-deploy --skip-local-validator
```
**VRF SDK constants** (`ephemeral_vrf_sdk::consts`) — reference these instead of hardcoding addresses, in both your program and client/test code:
| Constant | Purpose | Address |
| ------------------------------ | --------------------------------- | ---------------------------------------------- |
| `VRF_PROGRAM_ID` | VRF program | `Vrf1RNUjXmQGjmQrQLvJHs9SNkvDJEsRVFPkfSQUwGz` |
| `VRF_PROGRAM_IDENTITY` | Callback signer PDA | `9irBy75QS2BN81FUgXuHcjqceJJRuc9oDkAe8TKVvvAw` |
| `DEFAULT_QUEUE` | Base-layer queue (mainnet/devnet) | `Cuj97ggrhhidhbu39TijNVqE74xvKJ69gDervRUXAxGh` |
| `DEFAULT_EPHEMERAL_QUEUE` | ER queue (mainnet/devnet) | `5hBR571xnXppuCPveTrctfTU7tJLSN94nq7kv7FRK5Tc` |
| `DEFAULT_TEST_QUEUE` | Base-layer queue (localnet) | `GKE6d7iv8kCBrsxr78W3xVdjGLLLJnxsGiuzrsZCGEvb` |
| `DEFAULT_EPHEMERAL_TEST_QUEUE` | ER queue (localnet) | `Sc9MJUngNbQXSXGP3F67KvKwVnhaYn6kcioxXNVowYT` |
Pass the queue that matches where your transaction runs as `oracle_queue`. Mainnet and Devnet share the same queue addresses; localnet uses the test queues.
Run the following test:
```typescript theme={null}
import * as anchor from "@coral-xyz/anchor";
import { Program, web3 } from "@coral-xyz/anchor";
import { RandomDice } from "../target/types/random_dice";
describe("roll-dice", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.RandomDice as Program;
it("Initialized player!", async () => {
const tx = await program.methods.initialize().rpc();
console.log("Your transaction signature", tx);
});
it("Do Roll Dice!", async () => {
const tx = await program.methods.rollDice(0).rpc();
console.log("Your transaction signature", tx);
const playerPk = web3.PublicKey.findProgramAddressSync(
[Buffer.from("playerd"), anchor.getProvider().publicKey.toBytes()],
program.programId
)[0];
let player = await program.account.player.fetch(playerPk, "processed");
await new Promise((resolve) => setTimeout(resolve, 3000));
console.log("Player PDA: ", playerPk.toBase58());
console.log("player: ", player);
});
});
```
[⬆️ Back to Top](#code-snippets)
***
Want to run VRF end to end on your machine? Use the Local Validator Setup guide for the fully local stack, the Surfpool alternative, and the local vrf-oracle flow.
***
## Solana Explorer
Get insights about your transactions and accounts on Solana:
Official Solana Explorer
Explore Solana Blockchain
## Solana RPC Providers
Send transactions and requests through existing RPC providers:
Free Public Nodes
Free Shared Nodes
Dedicated High-Performance Nodes
## Solana Validator Dashboard
Find real-time updates on Solana's validator infrastructure:
Get Validator Insights
Discover Validator Metrics
## Server Status Subscriptions
Subscribe to Solana's and MagicBlock's server status:
Subscribe to Solana Server Updates
Subscribe to MagicBlock Server Status
***
## MagicBlock Products
Execute real-time, zero-fee transactions securely on Solana.
Protect sensitive data with compliance — built on top of Ephemeral Rollups.
Add private onchain transfers to your app in seconds — compliant by default.
Add provably fair onchain randomness to games, raffles, and real-time apps.
Access low-latency onchain price feeds for trading and DeFi.
***
# FAQ
Source: https://docs.magicblock.gg/pages/verifiable-randomness-functions-vrfs/introduction/faq
Common questions about Solana VRF
Solana VRF covers the most common questions about verifiable randomness before you ship on-chain apps.
### Why not use block hashes for randomness?
Block hashes can be predicted or manipulated by miners and validators, making them unsuitable for fair gameplay. MagicBlock's VRF proves the randomness was generated independently of block production.
### How fast is the randomness callback?
The VRF operates within the [ephemeral rollup](/pages/ephemeral-rollups-ers/introduction/ephemeral-rollup) execution window, so results are typically available within a single transaction round, without external polling.
### Can I audit the randomness proofs?
Yes. Each request includes a proof that anyone can verify on-chain using the same cryptography as the VRF signer. This transparency ensures players can confirm fairness.
### Who runs the oracles?
MagicBlock Solana VRF uses a permissioned set of oracles managed through on-chain queues. You can delegate your rollup to a specific queue so trusted operators fulfill your requests.
# Pricing
Source: https://docs.magicblock.gg/pages/verifiable-randomness-functions-vrfs/introduction/pricing
Pricing for provably fair randomness requests with MagicBlock Solana VRF.
MagicBlock Solana VRF pricing is charged per fulfilled randomness request. Fees cover proof generation and posting the verified result on-chain.
| VRF type |
Amount (SOL) |
Description |
| ER (\<50 ms) |
Free |
Per randomness request |
| Solana (\<500 ms) |
0.0008 |
Per randomness request |
| Solana (1/2 seconds) |
0.0005 |
Per randomness request |
## VRF Cost Simulator: 30 Days
Build a Solana VRF request and callback flow with the MagicBlock VRF SDK.
Return to the Solana VRF overview.
# Security
Source: https://docs.magicblock.gg/pages/verifiable-randomness-functions-vrfs/introduction/security
How Solana VRF randomness proofs are verified
Solana VRF proofs are cryptographically bound to the input `caller_seed` and to MagicBlock's VRF signer identity. Your callback enforces this with:
```rust theme={null}
#[account(address = ephemeral_vrf_sdk::consts::VRF_PROGRAM_IDENTITY)]
pub vrf_program_identity: Signer<'info>,
```
Only the official MagicBlock oracle can trigger the callback, preventing spoofed or manipulated results. Invalid proofs automatically fail, and other programs cannot front-run the request.
MagicBlock Solana VRF checks for conditions like `InvalidProof` and `Unauthorized` so incorrect signatures or unauthorized callers are rejected before your game logic runs. The VRF program has a published audit, so treat that report as the source of truth before going live.
Because everything executes inside the same deterministic [ephemeral rollup](/pages/get-started/introduction/ephemeral-rollup) that runs your game logic, the random value cannot be reused or delayed.
For actionable integration guidance — seed selection, callback validation, and state management — see [Best Practices](/pages/verifiable-randomness-functions-vrfs/how-to-guide/best-practices).
Read the audit report and security notes.
Return to the Solana VRF overview.
# Overview
Source: https://docs.magicblock.gg/pages/verifiable-randomness-functions-vrfs/introduction/solana-vrf
Use MagicBlock Solana VRF to request provably fair randomness
MagicBlock's Solana VRF gives Solana programs a verifiable randomness primitive for gameplay, lotteries, matchmaking, and other real-time flows that need fair outcomes without trusting blockhashes or app servers.
Solana VRF is available on both Solana mainnet and MagicBlock Ephemeral Rollups.
Build a Solana VRF request and callback flow with the MagicBlock VRF SDK.
See how requests, proofs, oracle queues, and callbacks work.
Review per-request pricing and estimate monthly VRF costs.
Read the audit report and security notes.
Inspect the program, SDK, proof implementation, and examples on GitHub.
## What is Solana VRF?
Solana VRF is a verifiable random function implementation for Solana programs. Your program requests randomness, an oracle computes a random value with a cryptographic proof, and the MagicBlock VRF program verifies that proof before calling back into your program.
That proof makes the result auditable. Users and programs can verify that a random outcome was produced from the committed request instead of being chosen after the fact by a validator, server, or game operator.
## Why use MagicBlock Solana VRF?
Traditional randomness sources fall short on-chain: users cannot verify how off-chain numbers were generated, validators or operators can influence blockhash-based outcomes, and off-chain delivery does not always align with on-chain execution timing. Solana VRF addresses this:
* **Built for Solana programs**: request randomness through the `ephemeral_vrf_sdk` and consume the result in your own callback instruction.
* **Designed for real-time apps**: MagicBlock's ephemeral rollup execution model keeps randomness delivery low-latency for games and interactive flows.
* **Verifiable by design**: proofs are validated on-chain before your callback logic runs.
* **Open source and audited**: the VRF program is public, with audit coverage linked from the security docs.
## How Solana VRF works
1. Your program submits a randomness request with a `caller_seed`, callback discriminator, and callback accounts.
2. The request is added to an oracle queue for fulfillment.
3. A verified oracle computes the random value and proof.
4. The MagicBlock VRF program verifies the proof.
5. Your callback receives the random bytes and converts them into game or app logic, such as dice rolls, loot drops, or raffle winners.
## Common use cases
* Loot drops and gacha
* Matchmaking and shuffling
* Raffles and giveaways
* Randomized resource generation
* Random character attributes or NFT traits
* Any Solana app that needs auditable fairness
## Start with the right guide
Use the [Solana VRF quickstart](/pages/verifiable-randomness-functions-vrfs/how-to-guide/quickstart) when you are ready to implement request and callback instructions. Use the [security guide](/pages/verifiable-randomness-functions-vrfs/introduction/security) to review callback validation, signer checks, and audit links before going live.
# Technical Details
Source: https://docs.magicblock.gg/pages/verifiable-randomness-functions-vrfs/introduction/technical-details
How Solana VRF integrates with MagicBlock
Random numbers are generated via a VRF built on Curve25519's Ristretto group and proven using a Schnorr-like signature as described in [RFC 9381](https://datatracker.ietf.org/doc/html/rfc9381). The proof and output are returned to the rollup with a signed callback from the MagicBlock VRF signer PDA. Your program verifies the caller and then uses the randomness in gameplay logic.
Helper utilities like `random_u32`, `random_u8_with_range`, and `random_bool` make it simple to convert the `[u8; 32]` output into usable values. Because the request and consume steps occur inside the ephemeral execution window, users get real-time results with verifiable fairness and without relying on external servers.
## Flow
The flow starts with a “Request for randomness”.
1. Your program will CPI into the MagicBlock VRF program and append a request to the queue.
2. Once your randomness request is in the queue, an oracle will release the request and perform the randomness computation.
3. Upon completion, it returns the result and proof to the MagicBlock VRF program. After verifying the proof, the VRF program will callback into your program into a predefined function that will “consume” the randomness.
MagicBlock's VRF Program is open-source and audited. See the Solana VRF overview for the product summary.
## Oracle queues
Every randomness request names an **oracle queue** account (the `oracle_queue` field of `RequestRandomnessParams`). Like every Solana account, the queue lives on Solana — but a **delegated** queue is directly writable only from inside an ephemeral rollup, while a **non-delegated** queue is directly writable on the base layer. Request randomness from the queue that matches where your transaction runs — the base-layer queue from Solana, or the delegated queue from inside the ephemeral rollup. Reference the SDK constants from `ephemeral_vrf_sdk::consts` instead of hardcoding addresses wherever possible.
| Network | Base-layer queue | Delegated queue (ephemeral rollup) |
| -------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------- |
| Mainnet | `DEFAULT_QUEUE`
`Cuj97ggrhhidhbu39TijNVqE74xvKJ69gDervRUXAxGh` | `DEFAULT_EPHEMERAL_QUEUE`
`5hBR571xnXppuCPveTrctfTU7tJLSN94nq7kv7FRK5Tc` |
| Devnet | `DEFAULT_QUEUE`
`Cuj97ggrhhidhbu39TijNVqE74xvKJ69gDervRUXAxGh` | `DEFAULT_EPHEMERAL_QUEUE`
`5hBR571xnXppuCPveTrctfTU7tJLSN94nq7kv7FRK5Tc` |
| Localnet | `DEFAULT_TEST_QUEUE`
`GKE6d7iv8kCBrsxr78W3xVdjGLLLJnxsGiuzrsZCGEvb` | `DEFAULT_EPHEMERAL_TEST_QUEUE`
`Sc9MJUngNbQXSXGP3F67KvKwVnhaYn6kcioxXNVowYT` |
Mainnet and Devnet share the same default queue addresses — only the cluster you connect to differs. Localnet uses dedicated **test queues** that the local validator clones from Devnet; the `DEFAULT_TEST_QUEUE` / `DEFAULT_EPHEMERAL_TEST_QUEUE` constants ship with the VRF SDK.
Read the full audit report
Learn how to add randomness capabilities
# Implementation
Source: https://docs.magicblock.gg/pages/tools/crank/implementation
Learn how to implement cranks in your Solana program using MagicBlock Ephemeral Rollups
## Overview
This guide demonstrates how to implement cranks using **MagicBlock's Ephemeral Rollups (ER)** with the Anchor framework. The implementation follows this flow:
1. Initialize counter on Solana base layer
2. Delegate counter account to Ephemeral Rollup for faster execution
3. Schedule a crank task that automatically increments the counter
4. Execute the crank automatically at specified intervals
5. Undelegate the account back to Solana base layer when done
## Execution Flow
```
1. User calls initialize() on Solana base layer
└─> Creates Counter PDA with count = 0
2. User calls delegate() on Solana base layer
└─> Moves Counter account to Ephemeral Rollup
3. User calls schedule_increment() on Ephemeral Rollup
└─> CPI to MagicBlock program
└─> Schedules task with:
- task_id: 1
- interval: 100ms
- iterations: 3
- instruction: increment()
4. MagicBlock automatically executes increment() 3 times:
└─> Execution 1: count = 1 (at T+0ms)
└─> Execution 2: count = 2 (at T+100ms)
└─> Execution 3: count = 3 (at T+200ms)
5. User calls undelegate() on Ephemeral Rollup
└─> Commits changes and moves Counter back to Solana base layer
```
## Core Components
### Scheduled Function
Let's take for example that you wanted to schedule the following simple increment instruction for a counter.
```rust theme={null}
pub fn increment(ctx: Context) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count += 1;
if counter.count > 1000 {
counter.count = 0;
}
msg!("PDA {} count: {}", counter.key(), counter.count);
Ok(())
}
```
### Schedule Increment Function
The core crank scheduling logic:
```rust theme={null}
pub fn schedule_increment(ctx: Context, args: ScheduleIncrementArgs) -> Result<()> {
let increment_ix = Instruction {
program_id: crate::ID,
accounts: vec![AccountMeta::new(ctx.accounts.counter.key(), false)],
// Defining the instruction to call.
data: anchor_lang::InstructionData::data(&crate::instruction::Increment {}),
};
let ix_data = bincode::serialize(&MagicBlockInstruction::ScheduleTask(
ScheduleTaskArgs {
task_id: args.task_id,
execution_interval_millis: args.execution_interval_millis,
iterations: args.iterations,
instructions: vec![increment_ix],
},
))
.map_err(|err| {
msg!("ERROR: failed to serialize args {:?}", err);
ProgramError::InvalidArgument
})?;
let schedule_ix = Instruction::new_with_bytes(
MAGIC_PROGRAM_ID,
&ix_data,
vec![
AccountMeta::new(ctx.accounts.payer.key(), true),
AccountMeta::new(ctx.accounts.counter.key(), false),
],
);
invoke_signed(
&schedule_ix,
&[
ctx.accounts.payer.to_account_info(),
ctx.accounts.counter.to_account_info(),
],
&[],
)?;
Ok(())
}
```
**Key Points:**
* Creates an instruction to increment the counter
* Serializes it into a `ScheduleTask` instruction for MagicBlock
* Uses CPI (Cross-Program Invocation) to call the MagicBlock program
* The MagicBlock program handles the scheduling and execution
### Schedule Arguments
```rust theme={null}
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct ScheduleIncrementArgs {
pub task_id: u64, // Unique identifier for the task
pub execution_interval_millis: u64, // Time between executions in milliseconds
pub iterations: u64, // Number of times to execute
}
```
### ScheduleIncrement Context
```rust theme={null}
#[derive(Accounts)]
pub struct ScheduleIncrement<'info> {
/// CHECK: used for CPI
#[account()]
pub magic_program: AccountInfo<'info>,
#[account(mut)]
pub payer: Signer<'info>,
/// CHECK: Passed to CPI - using AccountInfo to avoid Anchor re-serializing stale data after CPI
#[account(mut, seeds = [COUNTER_SEED], bump)]
pub counter: AccountInfo<'info>,
/// CHECK: used for CPI
pub program: AccountInfo<'info>,
}
```
**Important**: Uses `AccountInfo` instead of `Account` to avoid Anchor re-serializing stale data after CPI calls.
# Introduction
Source: https://docs.magicblock.gg/pages/tools/crank/introduction
Automated, time-based execution of on-chain instructions
## What Are Cranks?
Cranks (scheduled tasks) enable **automated, time-based execution** of on-chain instructions without requiring manual user intervention. In traditional blockchain systems, every action requires a user to sign and submit a transaction, which limits automation capabilities.
With MagicBlock's Ephemeral Rollups, you can schedule tasks that execute automatically at predetermined intervals, bringing powerful automation capabilities to your Solana programs.
## Key Benefits
* **Automated Execution**: Programs can execute instructions at predetermined intervals without user interaction
* **Cost Efficiency**: Reduces the need for off-chain cron jobs or monitoring services
* **Decentralization**: Execution happens on-chain, maintaining trustless guarantees
* **Reliability**: Scheduled tasks execute reliably within the blockchain's consensus mechanism
## Use Cases
* **Periodic State Updates**: Update prices, clear expired data, or refresh game state
* **Automated Workflows**: Recurring payments, vesting schedules, or subscription renewals
* **Game Mechanics**: Periodic rewards, time-based events, or automated game progression
* **DeFi Operations**: Rebalancing portfolios, liquidation checks, or yield compounding
Learn how to implement cranks in your program
Check out our GitHub repository
## How It Works
Cranks leverage MagicBlock's Ephemeral Rollups to provide scheduled execution:
1. **Initialize** your program state on Solana base layer
2. **Delegate** accounts to Ephemeral Rollup for faster execution
3. **Schedule** a crank task that automatically executes instructions
4. **Execute** automatically at specified intervals
5. **Undelegate** accounts back to Solana base layer when done
The scheduling happens through Cross-Program Invocation (CPI) to MagicBlock's scheduling program, which handles the actual execution timing and reliability.
Cranks execute within the Ephemeral Rollup's consensus mechanism, ensuring reliable and trustless execution. For detailed implementation steps, see the Implementation guide.
# SOAR
Source: https://docs.magicblock.gg/pages/tools/open-source-programs/SOAR
Solana On-Chain Achievement & Ranking
SOAR is a program that provides a seamless solution for managing leaderboards, achievements, players' profiles and automatic rewards distribution on the Solana blockchain. Currently supporting invocation from a TypeScript client, the integration in Solana.Unity-SDK will be coming soon.
## Contents
* [SOAR SDK](#soar-sdk)
* [Getting started](#quick-start)
* [Classes](#classes)
* [SoarProgram](#soarprogram)
* [GameClient](#gameclient)
* [InstructionBuilder](#instructionbuilder)
* [Tests](#tests)
## Quick start
### Create a new game
```typescript theme={null}
import { SoarProgram, GameType, Genre } from "@magicblock-labs/soar-sdk";
// Create a Soar client using the '@solana/web3.js' active Connection and a defaultPayer
const client = SoarProgram.getFromConnection(connection, defaultPayer);
let game = Keypair.generate();
let title = "Game1";
let description = "Description";
let genre = Genre.Action;
let gameType = GameType.Web;
let nftMeta = Keypair.generate().publicKey;
let _auths = auths.map((keypair) => keypair.publicKey);
// Retrieve the bundled transaction.
let { newGame, transaction } = await client.initializeNewGame(
game.publicKey,
title,
description,
genre,
gameType,
nftMeta,
_auths
);
// Send and confirm the transaction with the game keypair as signer.
await web3.sendAndConfirmTransaction(connection, transaction);
```
### Create a leaderboard
```typescript theme={null}
const transactionIx = await client.addNewGameLeaderBoard(
newGame,
authWallet.publicKey,
"my leaderboard", // description
leaderboardNft, // nft associated with the leaderboard
100,
true // isAscending
);
await web3.sendAndConfirmTransaction(connection, transactionIx.transaction, [
authWallet,
]);
```
### Submit a score
```typescript theme={null}
const score = 10;
const playerAddress = new web3.PublicKey("..."); // The player publicKey
const authWallet = web3.Keypair.fromSecretKey(bs58.decode("")); // AUTH_WALLET_PRIVATE_KEY
const leaderboardPda = new web3.PublicKey(""); // LEADERBOARD_PDA
const transactionIx = await client.submitScoreToLeaderBoard(
playerAddress,
authWallet.publicKey,
leaderboardPda,
new BN(score)
);
await web3.sendAndConfirmTransaction(connection, transactionIx.transaction, [
authWallet,
]);
```
## Classes
### SoarProgram
The `SoarProgram` class gives client access to every instruction in the on-chain SOAR program.
It also gives utility functions for deriving PDAs:
```typescript theme={null}
const user = Keypair.generate().publicKey;
const playerAddress = client.utils.derivePlayerAddress(user)[0];
```
fetching an account:
```typescript theme={null}
const account = await client.fetchLeaderBoardAccount(address);
```
and fetching multiple accounts:
```typescript theme={null}
const accounts = await client.fetchAllLeaderboardAccounts([]);
```
### GameClient
The `GameClient` provides a more specific set of functions tailored to a single Game account.
```typescript theme={null}
import { GameClient } from "@magicblock-labs/soar-sdk";
```
Get an instance representing an existing on-chain Game account:
```typescript theme={null}
const soar = SoarProgram.getFromConnection(connection, defaultPayer);
const gameClient = new GameClient(soar, address);
```
Register a new game:
```typescript theme={null}
const soar = SoarProgram.getFromConnection(connection, defaultPayer);
const game = new GameClient.register(soar, ...);
```
```typescript theme={null}
// Create a new leaderboard:
await game.addLeaderboard(....);
// Access the game's state.
await game.init();
// Refresh the game's state.
await game.refresh();
// Get the most recently-created achievement for a game
const achievement = game.recentAchievementAddress();
```
## InstructionBuilder
```typescript theme={null}
import { InstructionBuilder } from "@magicblock-labs/soar-sdk";
```
The InstructionBuilder provides a set of methods for conveniently bundling transactions.
```typescript theme={null}
const transaction = await this.builder
.andInitializePlayer({ username, nftMeta }, user)
.andRegisterPlayerEntry(/*...*/)
.andSubmitScoreToLeaderboard(/*...*/)
.and(/*some other transaction*/)
.then((builder) => builder.build());
```
# Implementation
Source: https://docs.magicblock.gg/pages/tools/oracle/implementation
Derive the Pyth Lazer price feed account and decode its bytes
## Deriving the Price Feed Account
We derive the PDA of the price account using the following seeds:
* **"price feed"** (buffer)
* **"pyth-lazer"** (buffer)
* **feedID** (buffer) — the asset you want. Find the supported feeds [here](https://github.com/magicblock-labs/real-time-pricing-oracle/blob/main/pyth_lazer_list.json).
* **price program id** — `PriCems5tHihc6UDXDjzjeawomAwBduWMGAi8ZUjppd`
```ts theme={null}
// seeds: ["price_feed", "pyth-lazer", feed_id_as_string]
function deriveFeedAddress (feedId: string) {
const [addr] = web3.PublicKey.findProgramAddressSync(
[Buffer.from('price_feed'), Buffer.from('pyth-lazer'), Buffer.from(feedId)],
PROGRAM ID
);
return addr
}
```
## Parsing the Account
The on‑chain account stores a header and fields for price data. We read raw bytes and decode.
* **Price offset**: 73 bytes from start
* **Type**: signed 64‑bit integer (`i64`)
* **Apply exponent**: scale the raw price using the account's exponent
```ts theme={null}
const addr = deriveFeedAddress(feed.id);
const PRICE_OFFSET = 73;
const dv = new DataView(ai.data.buffer, ai.data.byteOffset, ai.data.byteLength);
const raw = dv.getBigUint64(PRICE_OFFSET, true);
const price = Number(raw) * Math.pow(10, feed.exponent);
```
Real‑time price stream
Learn how to access our oracles onchain
Check out our Github Repo
# Introduction
Source: https://docs.magicblock.gg/pages/tools/oracle/introduction
Real-time onchain data with MagicBlock Ephemeral Rollups
## Oracles on MagicBlock
MagicBlock provides low‑latency, high‑throughput oracle data by ingesting Pyth Lazer feeds and updating Ephemeral Rollup accounts at 50–200 ms intervals (asset‑dependent).
Real‑time price stream
Learn how to access our oracles onchain
Check out our Github Repo
This Oracle example uses Pyth Lazer, but we can create Oracles for any arbitrary data source.
## What Are Onchain Oracles?
Onchain oracles deliver verifiable off‑chain data that programs can trust. Use cases range from asset prices to event outcomes.
On Solana, oracles typically keep accounts updated on‑chain. Programs read these accounts directly—no external API calls at execution time. We use Pyth, a widely adopted cross‑chain oracle network.
## Why Oracles Matter
* **Finance**: liquidations, funding, TWAPs — inaccurate quotes cause loss and risk
* **Games**: settle sports results; sync in‑game state with real‑world events
* **Composability**: reliable, on‑chain data enables secure program composition
Accuracy and latency directly impact correctness, safety, and UX.
## Oracles on MagicBlock
MagicBlock follows the standard oracle pattern—writing data into composable on‑chain accounts—while updating at 50–200 ms (asset‑dependent) versus \~400 ms on Solana slots. This latency profile is well‑suited for liquidations, copy‑trading, and other time‑sensitive flows.
### The two pieces of an oracle
* **Data Source**: The upstream truth. We can ingest arbitrary on/off‑chain feeds to surface assets Pyth doesn’t cover (e.g., new PumpFun or Raydium R‑tokens) into Ephemeral Rollups.
* **Chain Pusher**: Processes the source feed and writes updates on‑chain. MagicBlock’s chain pusher will be open‑sourced.
### Flow
1. Receive Pyth Lazer updates at fixed intervals (50 ms or 200 ms by asset).
2. Push updates to predefined on‑chain accounts.
3. Programs read the relevant account directly.
These public RPC endpoints are currently free and supported for development:
Magic Router Devnet: [https://devnet-router.magicblock.app](https://devnet-router.magicblock.app)
Solana Devnet: [https://api.devnet.solana.com](https://api.devnet.solana.com)
ER Devnet: [https://devnet.magicblock.app](https://devnet.magicblock.app)
TEE Devnet: [https://devnet-tee.magicblock.app/](https://devnet-tee.magicblock.app/)
Find out more details
here
.
This page is an overview. For byte‑level details and code examples, see the Implementation.
Code snippets intentionally use placeholders to keep the focus on the flow.
# Installation
Source: https://docs.magicblock.gg/pages/tools/session-keys/installation
Step-by-step guide on setting up and integrating the session wallet management system into your dApp
Session Keys are a part of the Gum React-SDK, so you need to install the package using either yarn or npm:
```bash Yarn theme={null}
yarn add @magicblock-labs/gum-react-sdk
```
```bash Npm theme={null}
npm i @magicblock-labs/gum-react-sdk
```
With the package installed, you can now begin integrating the session wallet management system into your dApp for enhanced security and user experience.
# Integrating Sessions in your Program
Source: https://docs.magicblock.gg/pages/tools/session-keys/integrating-sessions-in-your-program
Integrate and manage sessions in your Solana Programs
This guide demonstrates how to integrate Session Keys into your Solana Anchor programs, using a simple counter program as an example.
## Installation
First, add the session-keys crate to your Cargo.toml:
```toml theme={null}
[dependencies]
session-keys = { version = "1.0.0", features = ["no-entrypoint"] }
```
Or use the cargo command:
```bash theme={null}
cargo add session-keys --features no-entrypoint
```
## Usage
1. Importing session-keys:
```rust theme={null}
use session_keys::{SessionError, SessionToken, session_auth_or, Session};
```
This line imports the necessary components from the session-keys crate.
2. Deriving the Session trait:
```rust theme={null}
#[derive(Accounts, Session)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [ COUNTER_SEED, counter.authority.key().as_ref() ],
bump
)]
pub counter: Account<'info, Counter>,
...
}
```
The `Session` trait is derived on the `Increment` struct, enabling session functionality.
3. Defining the session token account:
```rust theme={null}
#[session(
signer = signer,
authority = counter.authority.key()
)]
pub session_token: Option>,
```
This defines an optional `SessionToken` account, specifying the signer and authority for the session.
* session\_token.authority: account which created the session token
* counter.authority.key(): account which created the counter
The authority condition checks if the session token is created by the same user as the counter.
4. Using the `session_auth_or` macro:
```rust theme={null}
#[session_auth_or(
ctx.accounts.counter.authority.key() == ctx.accounts.signer.key(),
SessionError::InvalidToken
)]
pub fn increment(ctx: Context) -> Result<()> {
...
}
```
This macro is applied to the `increment` function.
It checks for a valid session token, or if not present, verifies that the signer is the counter's authority.
## Full Example
Here's a complete example of a counter program using session keys.
Each user gets his own counter account, so we can show how authentication is done with session keys.
```rust theme={null}
use anchor_lang::prelude::*;
use session_keys::{SessionError, SessionToken, session_auth_or, Session};
declare_id!("...");
const COUNTER_SEED: &[u8] = b"counter";
#[program]
pub mod counter_session {
use super::*;
pub fn initialize(ctx: Context) -> Result<()> {
let counter: &mut Counter = &mut ctx.accounts.counter;
counter.count = 0;
counter.authority = *ctx.accounts.owner.key;
Ok(())
}
#[session_auth_or(
ctx.accounts.counter.authority.key() == ctx.accounts.signer.key(),
SessionError::InvalidToken
)]
pub fn increment(ctx: Context) -> Result<()> {
let counter: &mut Counter = &mut ctx.accounts.counter;
counter.count += 1;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = owner,
space = Counter::INIT_SPACE + 8 ,
seeds = [ COUNTER_SEED, owner.key().as_ref() ], bump
)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub owner: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts, Session)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [ COUNTER_SEED, counter.authority.key().as_ref() ],
bump
)]
pub counter: Account<'info, Counter>,
#[session(
signer = signer,
authority = counter.authority.key()
)]
pub session_token: Option>,
#[account(mut)]
pub signer: Signer<'info>,
}
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub authority: Pubkey,
pub count: u64,
}
```
## Tests
Here's an example of how to test the counter program with session keys:
```typescript theme={null}
import * as anchor from "@project-serum/anchor";
import { Program } from "@project-serum/anchor";
import { CounterSession } from "../target/types/counter_session";
import { createSessionToken } from "@session-keys/anchor";
import { expect } from "chai";
describe("counter_session", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.CounterSession as Program;
let counterPDA: anchor.web3.PublicKey;
let sessionToken: anchor.web3.PublicKey;
it("Initializes the counter", async () => {
const [pda] = await anchor.web3.PublicKey.findProgramAddress(
[Buffer.from("counter"), provider.wallet.publicKey.toBuffer()],
program.programId
);
counterPDA = pda;
await program.methods
.initialize()
.accounts({
counter: counterPDA,
owner: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
const counterAccount = await program.account.counter.fetch(counterPDA);
expect(counterAccount.count).to.equal(0);
expect(counterAccount.authority.toString()).to.equal(provider.wallet.publicKey.toString());
});
it("Increments the counter without session", async () => {
await program.methods
.increment()
.accounts({
counter: counterPDA,
signer: provider.wallet.publicKey,
})
.rpc();
const counterAccount = await program.account.counter.fetch(counterPDA);
expect(counterAccount.count).to.equal(1);
});
it("Increments the counter with session token", async () => {
await program.methods
.increment()
.accounts({
counter: counterPDA,
sessionToken: sessionToken,
signer: provider.wallet.publicKey,
})
.rpc();
const counterAccount = await program.account.counter.fetch(counterPDA);
expect(counterAccount.count).to.equal(2);
});
it("fails to increment with wrong session token owner", async () => {
const user = anchor.web3.Keypair.generate();
await topUp(user);
let counterPDA = await createCounterPDA(user.publicKey);
await createCounter(user);
const secondUser = anchor.web3.Keypair.generate();
await topUp(secondUser);
const { sessionSigner, sessionToken } = await createSessionSigner(secondUser);
try {
await increment_with_session(counterPDA, sessionSigner, sessionToken);
assert(false, "Expected to fail");
} catch (err) {}
const counterData = await program.account.counter.fetch(counterPDA);
assert(counterData.count.eq(new anchor.BN(0)));
});
});
```
This test suite demonstrates initializing the counter, incrementing it without a session, creating a session token, and then incrementing with the session token.
The last test is important, as it ensures that only the owner of the counter can increment it.
## Testing locally
To test it locally with `solana-test-validator`, you need to start it with the session keys program and account.
1. Make sure your Solana CLI points to DEVNET:
```
solana config set --url https://api.devnet.solana.com
```
2. Dump Session Keys program to local file:
```
solana program dump KeyspM2ssCJbqUhQ4k7sveSiY4WjnYsrXkC8oDbwde5 ./session-keys.so
```
3. Start solana-test-validator with session keys program and account:
> `-r` - reset the ledger to genesis
> `-ud` - URL for Solana's JSON RPC or moniker (-ud = DEVNET)
> `--clone` - Copy an account from the cluster
> `--bpf-program` - add a SBF program to the genesis configuration
```
solana-test-validator -ud --clone KeyspM2ssCJbqUhQ4k7sveSiY4WjnYsrXkC8oDbwde5 -r --bpf-program KeyspM2ssCJbqUhQ4k7sveSiY4WjnYsrXkC8oDbwde5 ./session-keys.so
```
# Security
Source: https://docs.magicblock.gg/pages/tools/session-keys/security
Key management and security model
## **Key Management**
Key Management is an extremely important aspect of the security. One thing to note here however is that how securely one can manage the keys is platform dependent, for example a mobile app with access to local keystore/keychain is a lot more secure compared to a web browser.\
\
Our current client side key management is only on the web. Given the constraints, we do take adequate security measures on the browser.
\
The ephemeral keypair is encrypted and safely stored in the user's browser using IndexedDB, an in-browser database. When a user initiates an action, such as signing and sending a transaction, the session token signs the transaction using the temporary key pair. The smart contract can then validate the transaction, confirming that the user's wallet authorized the session token.\\
1. Generate a random keypair using `web3.Keypair.generate()`
2. Generate a random encryption key
3. Using the encryption key encrypt the generated keypair.
4. Store the encrypted key in IndexedDB
## Security Model
1\. The session keys are like your JWT Token adapted to web3
2\. These keys have an expiry and a scope
3\. Once the key has expired they can't be reused in the target program. A new session token is to be generated.
4\. They are also designed to be revokable, so that in the worst case if something wrong happens the attack surface is limited to the ephemeral keypair and the assets contained in them. i.e **0.01 SOL**
### Note on IndexedDB
Web Browser is an extremely adversarial environment, no amount of security is enough there, right from cookies to session to extension's sandbox.
Attackers could always inject arbitrary code via XSS or malicious extension. This is why users are discouraged from storing serious funds in a browser wallet, they are only for day to day expenses and it is really important to establish the distinction that **Session Keys are not burner wallets.**
However, majority of web3 today is on web browsers and that's how users primarily interact with other dApps. Given the constraints of today, we have to **design around them** and **harden** them via other means.
1. Drastically reduce the scope of what's possible with an ephemeral signer, they are highly context and use case specific.
2. This is similar to the approach to JWT in a typical client server architecture in web2.
3. For example the session or JWT tokens on Facebook, twitter or even banking website could be vulnerable to the same issue. The way they address is this by **limiting the scope** of what **you can do with a token**, have **intelligent systems** in place to **revoke** them and further introduce 2FA for suspicious activity.
4. We follow a similar model to limit the scope, we are also working on adding intelligent revocation systems which can revoke a compromised token as soon as we witness a malicious transaction like out of scope usage etc. **The absolute worst case scenario in terms of loss of funds is the 0.01 SOL topped up to pay for the gas fee.**
5. Also\*\*, developers can work around this today by pairing it with a gasless relay like octane and setting `topUp to false`\*\*. However, we don't have a seamless way to do it directly from our SDK yet, although it is on our roadmap.
# Session Provider & Context
Source: https://docs.magicblock.gg/pages/tools/session-keys/session-provider-and-context
Understand the usage of the SessionWalletProvider and the context it provides, enabling easy access to the session wallet functionalities across your application components?
`SessionWalletProvider` is a higher-order component that wraps around your app components to provide the `sessionWallet` context throughout the application.
Here's an example of how to use the `SessionWalletProvider`:
1. Create a new file named `components/SessionProvider.tsx`
```typescript theme={null}
// components/SessionProvider.tsx
// The SessionProvider component initializes the SessionKeyManager and provides it to its children via context.
// Wrap any component that needs access to the SessionKeyManager with this provider.
import {
SessionWalletProvider,
useSessionKeyManager,
} from "@magicblock-labs/gum-react-sdk";
import {
AnchorWallet,
useAnchorWallet,
useConnection,
} from "@solana/wallet-adapter-react";
interface SessionProviderProps {
children: React.ReactNode;
}
const SessionProvider: React.FC = ({ children }) => {
const { connection } = useConnection();
const anchorWallet = useAnchorWallet() as AnchorWallet;
const cluster = "devnet"; // or "mainnet-beta", "testnet", "localnet"
//here the useSessionKeyManager takes in 3 properties
const sessionWallet = useSessionKeyManager(anchorWallet, connection, cluster);
return (
{children}
);
};
export default SessionProvider;
```
2. In your `_app.tsx` file, wrap the SessionProvider around the entire app to ensure it's accessible within every component:
```typescript theme={null}
// pages/_app.tsx
import SessionProvider from "@/components/SessionProvider";
import { WalletContextProvider } from "@/contexts/WalletContextProvider";
import "@/styles/globals.css";
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
import {
PhantomWalletAdapter,
SolflareWalletAdapter,
} from "@solana/wallet-adapter-wallets";
import { clusterApiUrl } from "@solana/web3.js";
import type { AppProps } from "next/app";
import React, { useMemo } from "react";
import dotenv from "dotenv";
// Use require instead of import since order matters
require("@solana/wallet-adapter-react-ui/styles.css");
dotenv.config();
export default function App({ Component, pageProps }: AppProps) {
const network = WalletAdapterNetwork.Devnet;
const endpoint =
process.env.NEXT_PUBLIC_SOLANA_ENDPOINT || clusterApiUrl(network);
const wallets = useMemo(
() => [
new PhantomWalletAdapter({ network }),
new SolflareWalletAdapter({ network }),
],
[network]
);
return (
);
}
```
***Note: Ensure that all your Solana wallet adapter contexts are the parent of the SessionProvider.***
3. With the SessionWalletProvider set up, you can now use the `useSessionWallet` hook in your components:
### Using `useSessionWallet` in components
`useSessionWallet` is a custom hook that provides access to the session wallet context value. Use this hook in any component wrapped by the SessionWalletProvider.
To properly understand what the `useSessionWallet` hook provides, we need to examine the `SessionWalletInterface`. This is what `useSessionKeyManager` returns, providing the methods needed to utilize session keys as well as transaction signing and sending capabilities.
```typescript theme={null}
interface SessionWalletInterface {
publicKey: PublicKey | null; // Public key associated with the session wallet
ownerPublicKey: PublicKey | null; // The Publickey of the session token authority(The creator of the session token)
isLoading: boolean; // Indicates whether the session wallet is loading
error: string | null; // An error message, if any
sessionToken: string | null; // Session token for the current session
signTransaction:
| ((
transaction: T,
connection?: Connection,
sendOptions?: SendTransactionOptions
) => Promise)
| undefined; // Sign a single transaction
signAllTransactions:
| ((
transactions: T[],
connection?: Connection,
sendOptions?: SendTransactionOptions
) => Promise)
| undefined; // Sign multiple transactions
signMessage: ((message: Uint8Array) => Promise) | undefined; // Sign a message
sendTransaction:
| ((
transaction: T,
connection?: Connection,
options?: SendTransactionOptions
) => Promise)
| undefined; // Send a signed transaction
signAndSendTransaction:
| ((
transactions: T | T[],
connection?: Connection,
options?: SendTransactionOptions
) => Promise)
| undefined; // Sign and send transactions
createSession: (
targetProgram: PublicKey, // Target Solana program
topUpLamports?: number, // Top up session wallet with lamports or not
validUntil?: number, // Duration of session token before expiration
sessionCreatedCallback?: (sessionInfo: {
sessionToken: string;
publicKey: string;
}) => void
) => Promise; // Create a new session
revokeSession: () => Promise; // Revoke the current session
getSessionToken: () => Promise; // Retrieve the current session token
}
```
This will help us understand how to use the sessionWallet in our code for creating session tokens as well as revoking them when no longer needed.
```typescript theme={null}
import { useSessionWallet } from "@magicblock-labs/gum-react-sdk";
function YourComponent() {
const sessionWallet = useSessionWallet();
//create session token
const session = await sessionWallet.createSession(
// pass needed params here
);
//Revoke Session Wallet
const result = await sessionWallet.revokeSession();
//Access the session signer Publickey
sessionWallet.publicKey,
//To access the session token
sessionWallet.sessionToken
return (
// Your component JSX
);
}
```
# Usage Example
Source: https://docs.magicblock.gg/pages/tools/session-keys/usage-examples
Learn how to interact with your dApp using the useSessionKeyManager and SessionWalletProvider through various practical examples
Now that you have set up the hooks and provider, let's look at some examples of using the provided methods.
### Creating a Session
To create a session, call the `createSession` method from the `sessionWallet`. This method accepts three parameters:
1. `targetProgramPublicKey`: A `PublicKey` instance representing the target program you want to interact with.
2. `topUp`: A boolean value, set to `true` if you want to top up a session keypair with `0.01 SOL` initially and \`false\` if you don't want to top up the session keypair.
3. `expiryInMinutes`: An optional parameter, representing the session's expiry time in minutes. The default value is 60 minutes.
```typescript theme={null}
const handleCreateSession = async () => {
const targetProgramPublicKey = new PublicKey(
"your_target_program_public_key"
);
const topUp = true;
const expiryInMinutes = 60;
const session = await sessionWallet.createSession(
targetProgramPublicKey,
topUp,
expiryInMinutes
);
// you can also specify the amount you want to topUp the session wallet.
//This will top up the amount specified, and when revoked, send back to the authority
const session = await sessionWallet.createSession(
targetProgramPublicKey,
topUp ? 10000000 : 0, // 0.01 SOL
expiryInMinutes
);
if (session) {
console.log("Session created:", session);
} else {
console.error("Failed to create session");
}
};
```
By calling `createSession`, a new ephemeral keypair is generated and stored on the client-side. The session token is then created and stored alongside the keypair. This enables the user to securely sign transactions using the generated keypair without revealing their actual wallet's private key.
### Signing and Sending a Transaction
To sign and send a transaction, use the `signAndSendTransaction` method. This method first signs the transaction using the ephemeral key pair created during the session. Then, it sends the signed transaction to the Solana network.
```typescript theme={null}
const handleSendTransaction = async () => {
const transaction = new Transaction();
// Add instructions to the transaction
const txids = await sessionWallet.signAndSendTransaction(transaction);
if (txids && txids.length > 0) {
console.log("Transaction sent:", txids);
} else {
console.error("Failed to send transaction");
}
};
```
The `signAndSendTransaction` method provides an extra layer of security by ensuring that the actual wallet's private key is not exposed. The ephemeral key pair stored on the client-side is used to sign the transaction, thus keeping the user's main wallet secure.
### Revoking a Session
To revoke a session, call the `revokeSession` method from the `sessionWallet`. This method performs three actions:
1. It removes the ephemeral key pair and the session token from the client-side storage.
2. It revokes the session from the contract.
3. Returns the lamports to the authority and closes the session token pda
```typescript theme={null}
const handleRevokeSession = async () => {
await sessionWallet.revokeSession();
console.log("Session revoked");
};
```
Revoking a session ensures that the ephemeral key pair is no longer valid and usable.
These examples should help you get started with implementing session management and wallet functionality in your app.
***Please refer to the "Create Post" section in the*** [***Example App***](https://github.com/gumhq/gum-example-app) ***to see how the Session Token implementation is done.***
# Use session key manager
Source: https://docs.magicblock.gg/pages/tools/session-keys/use-sessionkey-manager
A custom React hook that manages the creation and revocation of session tokens, and provides essential methods for signing and sending transactions in a secure and user-friendly way
`useSessionKeyManager` is a custom hook that takes an `AnchorWallet`, `Connection`, and `Cluster` as arguments and returns a `SessionWalletInterface`. This hook manages the session keys, tokens, and provides methods for signing and sending transactions.
The `SessionWalletInterface` consists of the following properties and methods:
```typescript theme={null}
interface SessionWalletInterface {
publicKey: PublicKey | null; // Public key associated with the session wallet
ownerPublicKey: PublicKey | null; // The Publickey of the session token authority(The creator of the session token)
isLoading: boolean; // Indicates whether the session wallet is loading
error: string | null; // An error message, if any
sessionToken: string | null; // Session token for the current session
signTransaction:
| ((
transaction: T,
connection?: Connection,
sendOptions?: SendTransactionOptions
) => Promise)
| undefined; // Sign a single transaction
signAllTransactions:
| ((
transactions: T[],
connection?: Connection,
sendOptions?: SendTransactionOptions
) => Promise)
| undefined; // Sign multiple transactions
signMessage: ((message: Uint8Array) => Promise) | undefined; // Sign a message
sendTransaction:
| ((
transaction: T,
connection?: Connection,
options?: SendTransactionOptions
) => Promise)
| undefined; // Send a signed transaction
signAndSendTransaction:
| ((
transactions: T | T[],
connection?: Connection,
options?: SendTransactionOptions
) => Promise)
| undefined; // Sign and send transactions
createSession: (
targetProgram: PublicKey, // Target Solana program
topUpLamports?: number, // Top up session wallet with lamports or not
validUntil?: number, // Duration of session token before expiration
sessionCreatedCallback?: (sessionInfo: {
sessionToken: string;
publicKey: string;
}) => void
) => Promise; // Create a new session
revokeSession: () => Promise; // Revoke the current session
getSessionToken: () => Promise; // Retrieve the current session token
}
```
Here's an example of how to use the `useSessionKeyManager`:
```typescript theme={null}
import { useAnchorWallet, useConnection } from '@solana/wallet-adapter-react';
import { useSessionKeyManager } from '@gumhq/react-sdk';
function YourComponent() {
const wallet = useAnchorWallet();
const connection = useConnection();
const cluster = "devnet"; // or "mainnet-beta", "testnet", "localnet"
const sessionWallet = useSessionKeyManager(wallet, connection, cluster);
// Access session wallet properties and methods here
// Example: sessionWallet.publicKey
// Example: sessionWallet.createSession
return (
// Your component JSX
);
}
```
To use the Session Key Manager across multiple components, you can set up the Provider Component and Context in your application in the following section.
# Contribution Guide
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/contribution-guide
Thank you for your interest in contributing to the Solana.Unity-SDK! This is an open-source community project and all contributions are welcomed, no matter how big or small. Contributions might include (but is not limited to) filing issues, adding documentation, fixing bugs, creating examples, and/or implementing features.
## Finding issues to work on
If you're looking to get started, check out the [open issues](https://github.com/magicblock-labs/Solana.Unity-SDK/issues). For simple documentation changes or typos, feel free to just open a pull request.
If you're considering larger changes or self motivated features, please file an issue and engage with the maintainers in [Telegram](https://t.me/+78KHQkUsy0ViMzQ6).
## Choosing an issue
If you'd like to contribute, please claim an issue by commenting, forking, and opening a pull request, even if empty. This allows the maintainers to track who is working on what issue as to not overlap work.
## Issue Guidelines
Please follow these guidelines:
Before coding:
* choose a branch name that describes the issue you're working on
While coding:
* Submit a draft PR asap
* Only change code directly relevant to your PR. Sometimes you might find some code that could really need some refactoring. However, if it's not relevant to your PR, do not touch it. File an issue instead. This allows the reviewer to focus on a single problem at a time.
# Add Signature
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/core-concepts/add-signature
Learn about Signatures on the official Solana [documentation](https://docs.solana.com/developing/programming-model/accounts#signers)
```csharp theme={null}
public class AddSignatureExample : IExample
{
private static readonly IRpcClient rpcClient = ClientFactory.GetClient(Cluster.TestNet);
private const string MnemonicWords =
"route clerk disease box emerge airport loud waste attitude film army tray " +
"forward deal onion eight catalog surface unit card window walnut wealth medal";
public void Run()
{
Wallet.Wallet wallet = new Wallet.Wallet(MnemonicWords);
Account fromAccount = wallet.GetAccount(10);
Account toAccount = wallet.GetAccount(8);
RequestResult> blockHash = rpcClient.GetRecentBlockHash();
Console.WriteLine($"BlockHash >> {blockHash.Result.Value.Blockhash}");
TransactionBuilder txBuilder = new TransactionBuilder()
.SetRecentBlockHash(blockHash.Result.Value.Blockhash)
.SetFeePayer(fromAccount)
.AddInstruction(SystemProgram.Transfer(fromAccount.PublicKey, toAccount.PublicKey, 10000000))
.AddInstruction(MemoProgram.NewMemo(fromAccount.PublicKey, "Hello from Sol.Net :)"));
byte[] msgBytes = txBuilder.CompileMessage();
byte[] signature = fromAccount.Sign(msgBytes);
byte[] tx = txBuilder.AddSignature(signature)
.Serialize();
Console.WriteLine($"Tx base64: {Convert.ToBase64String(tx)}");
RequestResult> txSim = rpcClient.SimulateTransaction(tx);
string logs = Examples.PrettyPrintTransactionSimulationLogs(txSim.Result.Value.Logs);
Console.WriteLine($"Transaction Simulation:\n\tError: {txSim.Result.Value.Error}\n\tLogs: \n" + logs);
RequestResult firstSig = rpcClient.SendTransaction(tx);
Console.WriteLine($"First Tx Signature: {firstSig.Result}");
}
}
```
# Associated Token Account
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/core-concepts/associated-token-account
Associated Token Account Program defines the convention and provides the mechanism for mapping the user's wallet address to the associated token accounts they hold.
For a comprehensive overview of Associated Token Account you can rely on the official Solana [documentation](https://spl.solana.com/associated-token-account)
```csharp theme={null}
using Solana.Unity.Programs;
using Solana.Unity.Rpc;
using Solana.Unity.Rpc.Builders;
using Solana.Unity.Rpc.Core.Http;
using Solana.Unity.Rpc.Messages;
using Solana.Unity.Rpc.Models;
using Solana.Unity.Rpc.Types;
using Solana.Unity.Wallet;
using System;
using System.Collections.Generic;
using System.Threading;
namespace Solana.Unity.Examples
{
public class AssociatedTokenAccountsExample : IExample
{
private static readonly IRpcClient RpcClient = ClientFactory.GetClient(Cluster.TestNet);
private const string MnemonicWords =
"route clerk disease box emerge airport loud waste attitude film army tray " +
"forward deal onion eight catalog surface unit card window walnut wealth medal";
public void Run()
{
Wallet.Wallet wallet = new Wallet.Wallet(MnemonicWords);
/*
* The following region creates and initializes a mint account, it also creates a token account
* that is initialized with the same mint account and then mints tokens to this newly created token account.
*/
#region Create and Initialize a token Mint Account
RequestResult> blockHash = RpcClient.GetRecentBlockHash();
ulong minBalanceForExemptionAcc =
RpcClient.GetMinimumBalanceForRentExemption(TokenProgram.TokenAccountDataSize).Result;
ulong minBalanceForExemptionMint =
RpcClient.GetMinimumBalanceForRentExemption(TokenProgram.MintAccountDataSize).Result;
Console.WriteLine($"MinBalanceForRentExemption Account >> {minBalanceForExemptionAcc}");
Console.WriteLine($"MinBalanceForRentExemption Mint Account >> {minBalanceForExemptionMint}");
Account ownerAccount = wallet.GetAccount(10);
Account mintAccount = wallet.GetAccount(1004);
Account initialAccount = wallet.GetAccount(1104);
Console.WriteLine($"OwnerAccount: {ownerAccount}");
Console.WriteLine($"MintAccount: {mintAccount}");
Console.WriteLine($"InitialAccount: {initialAccount}");
byte[] createAndInitializeMintToTx = new TransactionBuilder().
SetRecentBlockHash(blockHash.Result.Value.Blockhash).
SetFeePayer(ownerAccount).
AddInstruction(SystemProgram.CreateAccount(
ownerAccount,
mintAccount,
minBalanceForExemptionMint,
TokenProgram.MintAccountDataSize,
TokenProgram.ProgramIdKey)).
AddInstruction(TokenProgram.InitializeMint(
mintAccount.PublicKey,
2,
ownerAccount.PublicKey,
ownerAccount.PublicKey)).
AddInstruction(SystemProgram.CreateAccount(
ownerAccount,
initialAccount,
minBalanceForExemptionAcc,
TokenProgram.TokenAccountDataSize,
TokenProgram.ProgramIdKey)).
AddInstruction(TokenProgram.InitializeAccount(
initialAccount.PublicKey,
mintAccount.PublicKey,
ownerAccount.PublicKey)).
AddInstruction(TokenProgram.MintTo(
mintAccount.PublicKey,
initialAccount.PublicKey,
1_000_000,
ownerAccount)).
AddInstruction(MemoProgram.NewMemo(initialAccount, "Hello from Sol.Net")).
Build(new List { ownerAccount, mintAccount, initialAccount });
string createAndInitializeMintToTxSignature = Examples.SubmitTxSendAndLog(createAndInitializeMintToTx);
Examples.PollConfirmedTx(createAndInitializeMintToTxSignature);
#endregion
/*
* The following region creates an associated token account (ATA) for a random account and a certain token mint
* (in this case it's the previously created token mintAccount) and transfers tokens from the previously created
* token account to the newly created ATA.
*/
#region Create Associated Token Account
// this public key is from a random account created via www.sollet.io
// to test this locally I recommend creating a wallet on sollet and deriving this
PublicKey associatedTokenAccountOwner = new PublicKey("65EoWs57dkMEWbK4TJkPDM76rnbumq7r3fiZJnxggj2G");
PublicKey associatedTokenAccount =
AssociatedTokenAccountProgram.DeriveAssociatedTokenAccount(associatedTokenAccountOwner, mintAccount);
Console.WriteLine($"AssociatedTokenAccountOwner: {associatedTokenAccountOwner}");
Console.WriteLine($"AssociatedTokenAccount: {associatedTokenAccount}");
byte[] createAssociatedTokenAccountTx = new TransactionBuilder().
SetRecentBlockHash(blockHash.Result.Value.Blockhash).
SetFeePayer(ownerAccount).
AddInstruction(AssociatedTokenAccountProgram.CreateAssociatedTokenAccount(
ownerAccount,
associatedTokenAccountOwner,
mintAccount)).
AddInstruction(TokenProgram.Transfer(
initialAccount,
associatedTokenAccount,
25000,
ownerAccount)).// the ownerAccount was set as the mint authority
AddInstruction(MemoProgram.NewMemo(ownerAccount, "Hello from Sol.Net")).
Build(new List { ownerAccount });
string createAssociatedTokenAccountTxSignature = Examples.SubmitTxSendAndLog(createAssociatedTokenAccountTx);
Examples.PollConfirmedTx(createAssociatedTokenAccountTxSignature);
#endregion
}
}
}
```
# Staking
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/core-concepts/staking
SOL token holders can earn rewards and help secure the network by staking tokens to one or more validators. Rewards for staked tokens are based on the current inflation rate, total number of SOL staked on the network, and an individual validator’s uptime and commission (fee).
You can learn about Staking on the official Solana [documentation](https://spl.solana.com/stake-pool/overview#staking)
```csharp theme={null}
public class CreateAccountAndInitializeStakeExample : IExample
{
private static readonly IRpcClient rpcClient = ClientFactory.GetClient(Cluster.TestNet);
private const string MnemonicWords =
"clerk shoe noise umbrella apple gold alien swap desert rubber truck okay twenty fiscal near talent drastic present leg put balcony leader access glimpse";
public void Run()
{
var wallet = new Wallet.Wallet(new Mnemonic(MnemonicWords));
rpcClient.RequestAirdrop(wallet.Account.PublicKey, 100_000_000);
RequestResult> blockHash = rpcClient.GetRecentBlockHash();
ulong minbalanceforexception = rpcClient.GetMinimumBalanceForRentExemption(StakeProgram.StakeAccountDataSize).Result;
Account fromAccount = wallet.Account;
Account stakeAccount = wallet.GetAccount(22);
Authorized authorized = new Authorized()
{
Staker = fromAccount,
Withdrawer = fromAccount
};
Lockup lockup = new Lockup()
{
Custodian = fromAccount.PublicKey,
Epoch = 0,
UnixTimestamp = 0
};
Console.WriteLine($"BlockHash >> {blockHash.Result.Value.Blockhash}");
byte[] tx = new TransactionBuilder()
.SetRecentBlockHash(blockHash.Result.Value.Blockhash)
.SetFeePayer(fromAccount)
.AddInstruction(SystemProgram.CreateAccount(
fromAccount.PublicKey,
stakeAccount,
minbalanceforexception + 42,
StakeProgram.StakeAccountDataSize,
StakeProgram.ProgramIdKey))
.AddInstruction(StakeProgram.Initialize(
stakeAccount.PublicKey,
authorized,
lockup))
.Build(new List { fromAccount, stakeAccount });
Console.WriteLine($"Tx base64: {Convert.ToBase64String(tx)}");
RequestResult> txSim = rpcClient.SimulateTransaction(tx);
string logs = Examples.PrettyPrintTransactionSimulationLogs(txSim.Result.Value.Logs);
Console.WriteLine($"Transaction Simulation:\n\tError: {txSim.Result.Value.Error}\n\tLogs: \n" + logs);
RequestResult firstSig = rpcClient.SendTransaction(tx);
Console.WriteLine($"First Tx Result: {firstSig.Result}");
}
}
```
# Transaction Builder
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/core-concepts/transaction-builder
Learn about Transactions on the official Anchor [documentation](https://www.anchor-lang.com/docs/intro-to-solana)
```csharp theme={null}
using Solana.Unity.Programs;
using Solana.Unity.Programs.Models;
using Solana.Unity.Rpc;
using Solana.Unity.Rpc.Builders;
using Solana.Unity.Rpc.Core.Http;
using Solana.Unity.Rpc.Messages;
using Solana.Unity.Rpc.Models;
using Solana.Unity.Wallet;
using System;
using System.Collections.Generic;
namespace Solana.Unity.Examples
{
public class TransactionBuilderExample : IExample
{
private static readonly IRpcClient rpcClient = ClientFactory.GetClient(Cluster.TestNet);
private const string MnemonicWords =
"route clerk disease box emerge airport loud waste attitude film army tray " +
"forward deal onion eight catalog surface unit card window walnut wealth medal";
public void Run()
{
Wallet.Wallet wallet = new Wallet.Wallet(MnemonicWords);
Account fromAccount = wallet.GetAccount(10);
Account toAccount = wallet.GetAccount(8);
RequestResult> blockHash = rpcClient.GetRecentBlockHash();
Console.WriteLine($"BlockHash >> {blockHash.Result.Value.Blockhash}");
byte[] tx = new TransactionBuilder()
.SetRecentBlockHash(blockHash.Result.Value.Blockhash)
.SetFeePayer(fromAccount)
.AddInstruction(SystemProgram.Transfer(fromAccount.PublicKey, toAccount.PublicKey, 10000000))
.AddInstruction(MemoProgram.NewMemo(fromAccount.PublicKey, "Hello from Sol.Net :)"))
.Build(fromAccount);
Console.WriteLine($"Tx base64: {Convert.ToBase64String(tx)}");
RequestResult> txSim = rpcClient.SimulateTransaction(tx);
string logs = Examples.PrettyPrintTransactionSimulationLogs(txSim.Result.Value.Logs);
Console.WriteLine($"Transaction Simulation:\n\tError: {txSim.Result.Value.Error}\n\tLogs: \n" + logs);
RequestResult firstSig = rpcClient.SendTransaction(tx);
Console.WriteLine($"First Tx Signature: {firstSig.Result}");
}
}
}
```
# Transfer Token
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/core-concepts/transfer-token
The Token program defines a common implementation for Fungible and Non Fungible tokens.
You can learn about Token Program on the official Solana [documentation](https://spl.solana.com/token)
Balances can be transferred between Accounts using the Transfer instruction. The owner of the source Account must be present as a signer in the Transfer instruction when the source and destination accounts are different.
```csharp theme={null}
public class TransferTokenExample : IExample
{
private static readonly IRpcClient rpcClient = ClientFactory.GetClient(Cluster.TestNet);
private const string MnemonicWords =
"route clerk disease box emerge airport loud waste attitude film army tray " +
"forward deal onion eight catalog surface unit card window walnut wealth medal";
public void Run()
{
Wallet.Wallet wallet = new Wallet.Wallet(MnemonicWords);
RequestResult> blockHash = rpcClient.GetRecentBlockHash();
ulong minBalanceForExemptionAcc = rpcClient.GetMinimumBalanceForRentExemption(TokenProgram.TokenAccountDataSize).Result;
Console.WriteLine($"MinBalanceForRentExemption Account >> {minBalanceForExemptionAcc}");
Account mintAccount = wallet.GetAccount(31);
Console.WriteLine($"MintAccount: {mintAccount}");
Account ownerAccount = wallet.GetAccount(10);
Console.WriteLine($"OwnerAccount: {ownerAccount}");
Account initialAccount = wallet.GetAccount(32);
Console.WriteLine($"InitialAccount: {initialAccount}");
Account newAccount = wallet.GetAccount(33);
Console.WriteLine($"NewAccount: {newAccount}");
byte[] tx = new TransactionBuilder().SetRecentBlockHash(blockHash.Result.Value.Blockhash)
.SetFeePayer(ownerAccount)
.AddInstruction(SystemProgram.CreateAccount(
ownerAccount.PublicKey,
newAccount.PublicKey,
minBalanceForExemptionAcc,
TokenProgram.TokenAccountDataSize,
TokenProgram.ProgramIdKey))
.AddInstruction(TokenProgram.InitializeAccount(
newAccount.PublicKey,
mintAccount.PublicKey,
ownerAccount.PublicKey))
.AddInstruction(TokenProgram.Transfer(
initialAccount.PublicKey,
newAccount.PublicKey,
25000,
ownerAccount))
.AddInstruction(MemoProgram.NewMemo(initialAccount, "Hello from Sol.Net"))
.Build(new List { ownerAccount, newAccount, initialAccount });
Console.WriteLine($"Tx: {Convert.ToBase64String(tx)}");
RequestResult> txSim = rpcClient.SimulateTransaction(tx);
string logs = Examples.PrettyPrintTransactionSimulationLogs(txSim.Result.Value.Logs);
Console.WriteLine($"Transaction Simulation:\n\tError: {txSim.Result.Value.Error}\n\tLogs: \n" + logs);
RequestResult txReq = rpcClient.SendTransaction(tx);
Console.WriteLine($"Tx Signature: {txReq.Result}");
}
}
```
# Configurations
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/getting-started/configuration
Learn how to configure your preferred wallet
Learn how to configure your preferred wallet
The SDK supports a variety of wallets, including
| Wallet | Support | Type |
| ------------------------ | ------- | -------- |
| In-game (new or restore) | ✅ | In-app |
| In-game (Web3auth) | ✅ | In-app |
| Wallet Adapter | ✅ | External |
| Mobile Wallet Adapter | ✅ | External |
| WalletConnect | ✅ | External |
| Seed Vault | 🏗 | In-app |
## Interface
`IWalletBase` defines the common [interface](https://github.com/garbles-labs/Solana.Unity-SDK/blob/main/Runtime/codebase/IWalletBase.cs)
The WalletBase abstract class implements `IWalletBase` interface and provides convenient methods shared by all wallet adapters.
A few examples are:
* Connection to Mainnet/Devnet/Testnet or custom RPC
* Login/logout
* Account creation
* Get balance
* Get token accounts
* Sign/partially sign transactions
* Send transactions
## Login example
You can attach the [Web3.cs](https://github.com/magicblock-labs/Solana.Unity-SDK/blob/main/Runtime/codebase/Web3.cs) script
(../Runtime/Codebase/Web3.cs) to any game object on the scene, then call Web3.Instance.LoginWalletAdapter();
The complete list of methods is available
[here](https://github.com/garbles-labs/Solana.Unity-SDK/blob/main/Runtime/codebase/WalletBase.cs)
## Wallet Adapter
To configure a wallet following the [Wallet Adapter](https://solana-mobile.github.io/mobile-wallet-adapter/spec/spec.html) standard use the [SolanaWalletAdapterWebGL](https://github.com/magicblock-labs/Solana.Unity-SDK/blob/main/Runtime/codebase/SolanaWalletAdapterWebGL.cs) wallet implementation.
```csharp theme={null}
WalletBase wallet = new SolanaWalletAdapterWebGL(walletAdapterOptions, RpcCluster.DevNet, ...);
```
## WalletConnect
[WalletConnect](https://walletconnect.network/) enables your game to connect with a wide range of Solana wallets — including Binance Wallet, Jupiter Mobile, Trust, OKX Wallet, and many more — through a single integration.
On mobile, users can connect any supported wallet installed on their device. On desktop, they pair with their wallet by scanning a QR code. See the [full list of supported Solana wallets](https://walletguide.walletconnect.network/?chains=solana%3A5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp).
The integration is powered by [Reown AppKit for Unity](https://docs.reown.com/appkit/unity/core/usage#appkit-with-solana-unity-sdk), which provides an adapter package that plugs directly into the Solana Unity SDK. Once set up, all signing and session management go through AppKit while you continue using the same `Web3` APIs from `Solana.Unity.SDK`.
For installation and setup instructions, see the [Reown AppKit documentation](https://docs.reown.com/appkit/unity/core/usage#appkit-with-solana-unity-sdk).
## SMS
Solana Mobile Stack is a set of libraries for wallets and apps, allowing developers to create rich mobile experiences for the Solana network.
For more information about SMS check out the official [documentation](https://solanamobile.com/developers).
## Mobile Wallet Adapter
To configure a wallet following the Mobile Wallet Adapter standard use the [SolanaMobileWalletAdapter](https://github.com/magicblock-labs/Solana.Unity-SDK/blob/main/Runtime/codebase/SolanaMobileWalletAdapter.cs) wallet implementation.
```csharp theme={null}
WalletBase wallet = new SolanaMobileWalletAdapter(solanaMobileWalletOptions, RpcCluster.DevNet, ...);
```
## Configuring Deeplinks
Some of the wallet, e.g. Phantom, are currently implemented using DeepLinks. Deep links are URLs that link to a specific piece of content or functionality within an app, in the context of Solana transactions, deep links can be used to sign a transaction by allowing users to approve a transaction using their Solana wallet.
### Enabling deep linking for Android applications
To enable deep linking for Android applications, use an [intent filter](https://developer.android.com/guide/components/intents-filters). An intent filter overrides the standard Android App [Manifest](https://docs.unity3d.com/Manual/android-manifest.html) to include a specific intent filter section for [Activity](https://developer.android.com/reference/android/app/Activity).
To set up the wallet intent filter:
1. In the Project window, go to Assets > Plugins > Android.
2. Create a new file and call it AndroidManifest.xml. Unity automatically processes this file when you build your application.
3. Copy the [code sample](https://github.com/magicblock-labs/Solana.Unity-SDK/blob/main/Samples~/Solana%20Wallet/Plugins/Android/AndroidManifest.xml) into the new file and save it.
*android:scheme="unitydl" should match the value defined in the wallet configuration*
See the detailed explanation on the Unity [documentation page](https://docs.unity3d.com/Manual/deep-linking-android.html).
### Enabling deep linking for IOS applications
See the detailed explanation on the Unity [documentation page](https://docs.unity3d.com/Manual/deep-linking-android.html) .
*the defined schema should match the value defined in the wallet configuration*
# Installation
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/getting-started/installation
## Unity
Go [here](https://unity.com/download) to install Unity.
## Import the SDK package
* Open [Unity Package Manager](https://docs.unity3d.com/Manual/upm-ui.html) window.
* Click the add + button in the status bar.
* The options for adding packages appear.
* Select Add package from git URL from the add menu. A text box and an Add button appear.
*
* Enter the `https://github.com/magicblock-labs/Solana.Unity-SDK.git` Git URL in the text box and click Add.
* Once the package is installed, in the Package Manager inspector you will have Samples. Click on Import
* You may also install a specific package version by using the URL with the specified version.
* `https://github.com/magicblock-labs/Solana.Unity-SDK.git#X.Y.X`
* Please note that the version X.Y.Z stated here is to be replaced with the version you would like to get.
* You can find all the available releases [here](https://github.com/magicblock-labs/Solana.Unity-SDK/releases).
* The latest available release version is- [](https://github.com/magicblock-labs/Solana.Unity-SDK/releases/latest)
* Import the Sample Scene
*
* You will find a sample scene with a configured wallet in `Samples/Solana SDK/0.0.2/Simple Wallet/Solana Wallet/scenes/wallet_scene.unity`
This tutorial is made in Unity 2021.3.5f1
# Sample scene
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/getting-started/sample-scene
Play around with the sample scene
## Features
* Prefabs are templates that store Objects configuration. They can be used to create instances of the same assets and are easily reusable
* Scenes folder contains the full demo deployed here.
* Scrips folder contains different scripts examples (I.e. Assets/Samples/Solana SDK/0.1.1/Sample Wallet/Solana Wallet/Scripts/example/screens/SwapScreen.cs for Orca Swaps)
* Animations, Materials, Plugins and Textures are self-explanatory
Play around with the demo scene to familiarize yourself with the basic functionality of Wallets, NFT loading, transfers, swaps etc.
# Additional Examples
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/guides/additional-examples
More examples
[here](https://github.com/garbles-labs/Solana.Unity-Core/tree/master/src/Solana.Unity.Examples)
# DEX integration with Orca
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/guides/dex-integration
Orca is natively supported in the SDK. Utility methods are provided to easily build the transactions needed to make swaps, open positions, manage cash and interact with Whirlpools.
## Orca
Orca is the easiest place to trade cryptocurrency on the Solana blockchain. For a detailed description refer to the official [Orca documentation](https://docs.orca.so/orca-for-traders/master) and [Orca Developer Portal](https://orca-so.gitbook.io/orca-developer-portal/orca/welcome).
## Perform a Swap
* Create an IDex instance, providing a default account and the RPC client instance:
```csharp theme={null}
IDex dex = new OrcaDex(
WalletH.Account,
WalletH.Rpc
)
```
* Fetch token data:
```csharp theme={null}
TokenData tokenA = await dex.GetTokenBySymbol("USDC");
TokenData tokenB = await dex.GetTokenBySymbol("ORCA");
```
* Find the whirlpool:
```csharp theme={null}
Pool whirlpool = await dex.FindWhirlpoolAddress(tokenA.MintAddress, tokenB.MintAddress)
```
* Get a swap quote for 1 USDC:
```csharp theme={null}
SwapQuote swapQuote = await dex.GetSwapQuoteFromWhirlpool(
whirlpool.Address,
DecimalUtil.ToUlong(1, tokenA.Decimals),
tokenA.MintAddress,
slippageTolerance: 0.1,
);
```
```csharp theme={null}
var quote = DecimalUtil.FromBigInteger(swapQuote.EstimatedAmountOut, tokenB.Decimals);
Debug.Log(quote); // Amount of espected Orca token to receive
```
* Create the swap transaction:
```csharp theme={null}
Transaction tx = await dex.SwapWithQuote(
whirlpool,
swapQuote
);
```
* Sign and send the swap transaction:
```csharp theme={null}
await WalletH.Base.SignAndSendTransaction(tx);
```
## Open a position and increase the liquidity of the ORCA/USDC whirlpool
An example of adding 5 ORCA and 5 USDC to the liquidity of the pool, minting a metaplex NFT representing the position
```csharp theme={null}
OrcaDex dex = new OrcaDex(
WalletH.Account,
WalletH.Rpc
);
var orcaToken = await dex.GetTokenBySymbol("ORCA");
var usdcToken = await dex.GetTokenBySymbol("USDC");
var whirlpool = await dex.FindWhirlpoolAddress(
usdcToken.MintAddress,
orcaToken.MintAddress
);
Account mint = new Account();
Transaction tx = await dex.OpenPositionWithLiquidity(
whirlpool,
mint,
-1792,
1152,
DecimalUtil.ToUlong(5, tokenA.Decimals),
DecimalUtil.ToUlong(5, tokenB.Decimals),
commitment: Commitment.Confirmed
);
var txSer = tx.Build(new List() {
WalletH.Account,
mint
});
await WalletH.Base.SignAndSendTransaction(tx);
```
# Host Your Game on Github Pages
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/guides/host-your-game
Host your game for free using Github pages
Solana.Unity SDK is fully compatible with WebGL. In this tutorial you will compile the Solana.Unity-SDK [demo scene](https://garbles-labs.github.io/Solana.Unity-SDK/) and publish it using [Github pages](https://pages.github.com/).
With GitHub pages, GitHub allows you to host a webpage from your repository.
## Compile the game to WebGL
1. Download and Install [Unity](https://unity3d.com/get-unity/download)
2. Install the Solana.Unity-SDK following the [instructions](https://github.com/garbles-labs/Solana.Unity-SDK#installation) and import the example
3. Compile the scene to WebGL (be sure to [disable compression](https://www.youtube.com/watch?v=2jjESP58jsA), as GH pages does not support serving compressed files)
If you want to skip the compilation step, you can fork the SDK repository,
which contained a pre-compiled WebGL build in the
[gp-pages](https://github.com/garbles-labs/Solana.Unity-SDK/tree/gh-pages)
branch
## Host the demo on Github pages
* Create a new repository
* Navigate to the build folder containing the index.html
```shell theme={null}
git init
git add .
git commit -m "WebGL game"
git remote add origin
git push origin
```
* You repository should now looks similar to the SDK [gp-pages](https://github.com/garbles-labs/Solana.Unity-SDK/tree/gh-pages) branch.
* Enable gh-pages deployment from the repository settings
Github will provide a url for the live deployment: [garbles-labs.github.io/Solana.Unity-SDK](https://garbles-labs.github.io/Solana.Unity-SDK/)
Learn how to setup a [custom
domain](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site)
on Github pages
Follow [this guide](/pages/tools/solana-unity-sdk/guides/publishing-a-game) to publish your game as an xNFT
in less than 2 minutes.
Happy game development 🎈 and don't forget to ⭐ the repo
# Jupiter V6 integration
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/guides/jupiter
Integrate Jupiter v6 API in your game
## Jupiter
Jupiter V6 is natively supported in the SDK. Utility methods are provided to easily get swap quotes, build and send the transactions needed to perform swaps.
Jupiter is the key liquidity aggregator for Solana, offering the widest range of tokens and best route discovery between any token pair. For a detailed description refer to the official [Jupiter documentation](https://station.jup.ag/).
***
## Perform a Swap
Create an IDex instance, providing a default account:
```csharp theme={null}
IDexAggregator dex = new JupiterDexAg(Web3.Account);
```
Fetch token data:
```csharp theme={null}
TokenData tokenA = await dex.GetTokenBySymbol("SOL");
TokenData tokenB = await dex.GetTokenBySymbol("USDC");
```
Get a swap quote for 1 SOL:
```csharp theme={null}
SwapQuoteAg swapQuote = await dex.GetSwapQuote(
tokenA.MintAddress,
tokenB.MintAddress,
DecimalUtil.ToUlong(1, tokenA.Decimals)
);
```
```csharp theme={null}
var quote = DecimalUtil.FromBigInteger(swapQuote.OutputAmount, tokenB.Decimals);
Debug.Log(quote); // Amount of espected USDC token to receive
```
Display the route path:
```csharp theme={null}
Debug.Log(string.Join(" -> ", swapQuote.RoutePlan.Select(p => p.SwapInfo.Label)));
// Lifinity V2 -> Whirlpool
```
Create the swap transaction:
```csharp theme={null}
Transaction tx = await dex.Swap(swapQuote);
```
Sign and send the swap transaction:
```csharp theme={null}
await Web3.Wallet.SignAndSendTransaction(tx);
```
## Use the Jupiter Payments API
The Jupiter Payments API is also available, enabling you to utilize Jupiter + SolanaPay for facilitating user payments with any SPL token, allowing pricing in USDC or other tokens.
Create an IDex instance, providing a default account:
```csharp theme={null}
IDexAggregator dex = new JupiterDexAg(Web3.Account);
```
Fetch token data:
```csharp theme={null}
TokenData tokenA = await dex.GetTokenBySymbol("SOL");
TokenData tokenB = await dex.GetTokenBySymbol("USDC");
```
Get a swap quote for the amount of SOL needed for obtaining 5 UDSC:
```csharp theme={null}
SwapQuoteAg swapQuote = await dex.GetSwapQuote(
tokenA.MintAddress,
tokenB.MintAddress,
DecimalUtil.ToUlong(5, tokenB.Decimals),
SwapMode.ExactOut
);
```
```csharp theme={null}
var quote = DecimalUtil.FromBigInteger(swapQuote.InputAmount, tokenA.Decimals);
Debug.Log(quote); // Amount of espected SOL token to pay
```
Create the swap transaction:
```csharp theme={null}
Transaction tx = await dex.Swap(swapQuote);
```
Sign and send the swap transaction:
```csharp theme={null}
await Web3.Wallet.SignAndSendTransaction(tx);
```
# Minting an NFT
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/guides/mint-an-nft
For minting an NFT we will interact with the [Token Metadata](https://docs.metaplex.com/programs/token-metadata/) program, see the metaplex [documentation](https://docs.metaplex.com/) for a comprehensive overview.
Firstly, we need to create a new mint account for the NFT we want to mint and an associated token account for owning it.
```csharp theme={null}
var mint = new Account();
var associatedTokenAccount = AssociatedTokenAccountProgram
.DeriveAssociatedTokenAccount(Web3.Account, mint.PublicKey);
```
Secondly, let's define the metadata of the NFT.
```csharp theme={null}
var metadata = new Metadata()
{
name = "Test",
symbol = "TST",
uri = "https://y5fi7acw5f5r4gu6ixcsnxs6bhceujz4ijihcebjly3zv3lcoqkq.arweave.net/x0qPgFbpex4ankXFJt5eCcRKJzxCUHEQKV43mu1idBU",
sellerFeeBasisPoints = 0,
creators = new List { new(Web3.Account.PublicKey, 100, true)}
};
```
We can now construct the transaction, which consists of 5 instructions:
* Creating the Mint Account
* Initializing the Mint Account
* Creating the AssociatedTokenAccount
* Minting the NFT
* Creating the Metadata Account
* Creating the Master Edition
```csharp theme={null}
var transaction = new TransactionBuilder()
.SetRecentBlockHash(blockHash)
.SetFeePayer(Web3.Account)
.AddInstruction(
SystemProgram.CreateAccount(
Web3.Account,
mint.PublicKey,
minimumRent.Result,
TokenProgram.MintAccountDataSize,
TokenProgram.ProgramIdKey))
.AddInstruction(
TokenProgram.InitializeMint(
mint.PublicKey,
0,
Web3.Account,
Web3.Account))
.AddInstruction(
AssociatedTokenAccountProgram.CreateAssociatedTokenAccount(
Web3.Account,
Web3.Account,
mint.PublicKey))
.AddInstruction(
TokenProgram.MintTo(
mint.PublicKey,
associatedTokenAccount,
1,
Web3.Account))
.AddInstruction(MetadataProgram.CreateMetadataAccount(
PDALookup.FindMetadataPDA(mint),
mint.PublicKey,
Web3.Account,
Web3.Account,
Web3.Account.PublicKey,
metadata,
TokenStandard.NonFungible,
true,
true,
null,
metadataVersion: MetadataVersion.V3))
.AddInstruction(MetadataProgram.CreateMasterEdition(
maxSupply: null,
masterEditionKey: PDALookup.FindMasterEditionPDA(mint),
mintKey: mint,
updateAuthorityKey: Web3.Account,
mintAuthority: Web3.Account,
payer: Web3.Account,
metadataKey: PDALookup.FindMetadataPDA(mint),
version: CreateMasterEditionVersion.V3
)
);
```
Finally, let's sign and send the transaction:
```csharp theme={null}
var tx = Transaction.Deserialize(transaction.Build(new List {Web3.Account, mint}));
var res = await Web3.Wallet.SignAndSendTransaction(tx);
Debug.Log(res.Result);
```
The console will print the transaction signature, which you can investigate in the inspector and should looks similar to this [transaction](https://explorer.solana.com/tx/TPSviDzpzTFEyfJkYwmQzqaPJTTsGMZTuPuG9q1LiKrhZnwg5WWHH7ARR8eYAdoB8rt8qcjKwqbcZj43b84Ls5C?cluster=devnet),
You can lookup the mint address in the explorer, which should be similar to this [NFT](https://explorer.solana.com/address/4X199VtLKVJUeLMXzwXzSsFgapVQcrYx9vnqxNDkH2Xa?cluster=devnet)
# Publish Your Game as an Xnft
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/guides/publishing-a-game
## Compile the game for xNFT
You can now just compile your game to WebGL without any extra steps. Just make sure you have the latest version of the SDK and you're good to go.
Deploy your game as a normal WebGL game as you will need the url for publishing the xNFT in next step.
Your WebGL game will work both in the browser and inside Backpack, no need to
build and host a separate version for each platform.
Follow [this guide](/pages/tools/solana-unity-sdk/guides/host-your-game) to compile your game to WebGL and
host the build on Github pages
### (Optional) Use the xNFT WebGL Template
A customized WebGL Template is also provided for easier deployment of your game as an xNFT. It's not required, but it's recommended as it makes the game fully responsive to the screen size, and it will just look good when running in both the browser and as a xNFT app.
To use the xNFT WebGL Template, follow these steps:
* Open [Build Settings](https://docs.unity3d.com/Manual/BuildSettings.html) window and change to WebGL platform if you haven't already:
* Select WebGL as the platform
* Click the **Switch Platform** button to apply changes.
When your Unity project is set to WebGL Platform, the SDK automagically imports a new WebGL Template into the /Assets/WebGLTemplate folder, named xNFT:
* Open **Player Settings** window from the Build Settings, and select the xNFT template.
Now you can build your game and host it as a normal WebGL game.
## Publish your game as an xNFT
If you haven't already, go get your Backpack user. You can download Backpack [here](https://www.backpack.app/downloads).
After you have your game hosted in a webserver and you have Backpack account, you can just go to [https://www.xnft.gg/publish](https://www.xnft.gg/publish) to deploy your xnft in mainnet-beta or [https://devnet.xnft.gg/publish](https://devnet.xnft.gg/publish) to deploy the xnft in devnet.
Connect your Backpack wallet and then follow the instructions:
* copy this basic [xnft.json](https://github.com/coral-xyz/xnft-quickstart/blob/master/xnft.json) configuration file.
* edit the "entrypoints" "default" "web" to point to your game's url
* edit xnft.json (add a "tag": "game" line) and add your app's icon and screenshots in an Assets folder.
* Zip togehter the xnft.json file and the Assets folder.
* Go to [xnft.gg/publish](https://www.xnft.gg/publish)
* Drop the zipped Manifest and mint.
Congrats! You're done publishing your game as an xNFT!
## Video Demo
Here's a quick demo on how you can build your game and test it in the browser and as an xNFT app inside Backpack.
# Introduction
Source: https://docs.magicblock.gg/pages/tools/solana-unity-sdk/overview
Open-Source Unity-Solana SDK with NFT support & Full RPC coverage.
Step-by-step guides to setting up your system and installing the
Solana.Unity-SDK.
Learn how to set up your game wallets.
Solana.Unity-SDK core concepts.
Guides to help you get started.
## Solana.Unity-SDK
Solana.Unity SDK is comprehensive set of open-source tools to easily access Solana in your Unity-based games. You can install the SDK with the Unity Package Manager or on the Unity Asset Store. and set up your preferred wallet among the available options. Solana.Unity-SDK uses [Solana.Unity-Core](https://github.com/garbles-labs/Solana.Unity-Core) implementation, native .NET Standard 2.0 (Unity compatible) with full RPC API coverage, MPL, native DEXes operations and more.
## Features
* Full JSON RPC API coverage
* Wallet and accounts: Set up of a non-custodial Solana wallet in Unity (sollet and solana-keygen compatible)
* Phantom and Web3auth support (non-custodial signup/login through social accounts)
* Transaction decoding from base64 and wire format and encoding back into wire format
* Message decoding from base64 and wire format and encoding back into wire format
* Instruction decompilation
* TokenWallet object to send and receive SPL tokens and JIT provisioning of Associated Token Accounts
* Basic UI examples
* NFTs
* Compile games to xNFTs (Backpack)
* Native DEX operations (Orca, Jupiter coming soon...)
* Websockets to register/trigger custom events (account change, signature status, programs, ...)
* Solana Mobile Stack support
* Solana Wallet Adapter