Vexidus Wallet SDK Developer Guide
Build wallets, dApps, and tools that interact with the Vexidus blockchain.
Table of Contents
- Vexidus Address System
- Wallet Compatibility
- MetaMask Integration
- Quick Start
- Transaction Signing Flow
- Address Format Conversion
- Security
- Troubleshooting
Vexidus Address System
Vexidus is not Ethereum. It is its own L1 blockchain with Ethereum compatibility. This has direct implications for wallet developers.
The Dual-Format Model
| Format | Size | Encoding | Used By | Example |
|---|---|---|---|---|
| Vx0 | 20-byte payload | Base58 + SHA256 checksum | Native wallets, vex_* RPC | Vx0Hk8pQwB5Z... |
| Vx1 | 20-byte payload | Base58 + SHA256 checksum | Token/contract mints | Vx1RjT4nQ9... |
| 0x (32-byte) | 32 bytes | Hex | Internal state, system addresses | 0x0000...0001 |
| 0x (20-byte) | 20 bytes | Hex | MetaMask, eth_* RPC | 0x71C7...976F |
Vx0 is the canonical user-facing format. The 0x format exists only for EVM tooling compatibility.
How Addresses Are Derived
Ed25519 Public Key (32 bytes)
|
v
SHA256 hash
|
v
First 20 bytes (payload)
|
v
[0x01] + [20 bytes] + [SHA256 checksum, 4 bytes]
|
v
Base58 encode
|
v
Prefix "Vx0"
|
v
"Vx0Hk8pQwB5Z..." (native address)
For internal state storage, the 20-byte payload is right-aligned in a 32-byte array:
[0x00, 0x00, ..., 0x00, payload_byte_1, ..., payload_byte_20]
|--- 12 zero bytes ---|------------ 20 bytes --------------|
For EVM wallets (MetaMask), only the last 20 bytes of the 32-byte array are shown:
32-byte internal: 0x000000000000000000000000abcdef...
EVM display: 0xabcdef... (last 20 bytes)
Key Differences from Ethereum
| Ethereum | Vexidus | |
|---|---|---|
| Address format | 0x (20 bytes) | Vx0 (native) or 0x (EVM compat) |
| Signature scheme | secp256k1 (ECDSA) | Ed25519 |
| Native decimals | 18 (ETH) | 9 (VXS) |
| Transaction model | Single operation | Bundle (atomic multi-op) |
| Key derivation | Keccak256(pubkey) -> 20 bytes | SHA256(pubkey) -> 20 bytes -> Vx0 |
Wallet Compatibility
| Wallet | Works? | Method | Notes |
|---|---|---|---|
| Native Vexidus wallet | Full support | vex_submitBundle | Vx0 addresses, Ed25519, 9 decimals |
| MetaMask | Via EVM adapter | eth_sendRawTransaction | 20-byte 0x, ECDSA, 18->9 decimal conversion |
| Trust Wallet | Via EVM adapter | Same as MetaMask | Add as custom network |
| Phantom | Planned (post-beta) | SVM adapter (svm_impl.rs) | Ed25519 compatible -- same curve as Vexidus, needs RPC translation layer |
| Ledger / Trezor | Planned | SDK bridge | Ed25519 app required |
What This Means for Developers
- Building a native wallet? Use the Vexidus Wallet SDK directly -- full access to all features (Vx0 addresses, VSA v2 key management, bundle signing).
- Supporting MetaMask users? Point them to the EVM adapter -- they will get basic send/receive, but addresses show as
0xand some features (VSA v2, multi-op bundles) are not available. - Both? Your backend uses the native SDK, your frontend detects MetaMask and falls back to
eth_*methods.
MetaMask Integration
MetaMask works with Vexidus via the built-in EVM compatibility adapter. Users add Vexidus as a custom network.
Adding Vexidus to MetaMask (JavaScript)
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [{
chainId: '0x18b070', // 1618032 (testnet)
chainName: 'Vexidus Testnet',
nativeCurrency: {
name: 'VXS',
symbol: 'VXS',
decimals: 18, // MetaMask expects 18; adapter scales internally
},
rpcUrls: ['https://vexidus.io/rpc'],
blockExplorerUrls: ['https://vexscan.io']
}]
});
Chain IDs
| Network | Decimal | Hex |
|---|---|---|
| Testnet | 1618032 | 0x18b070 |
| Mainnet | 1618033 | 0x18b071 |
The Decimal Issue
VXS natively has 9 decimals, but MetaMask's UI works best with 18. The EVM adapter handles this:
eth_getBalancereturns VXS balance scaled to 18 decimals (balance * 10^9)eth_sendRawTransactionconverts value from 18 decimals back to 9 (value / 10^9)- This means MetaMask displays correct human-readable amounts
If you are building a native wallet, always use 9 decimals. The 18-decimal scaling only applies to MetaMask's EVM adapter.
1.0 VXS = 1,000,000,000 base units (9 decimals, native)
1.0 VXS = 1,000,000,000,000,000,000 wei (18 decimals, MetaMask display)
Address Mapping
When a MetaMask user connects:
- MetaMask provides a 20-byte
0xaddress (derived from secp256k1, NOT Ed25519) - The EVM adapter right-aligns this to 32 bytes for internal use
- This 32-byte address is the user's Vexidus account for
eth_*operations
Important: A MetaMask 0x address and a native Vx0 address for the same person are different accounts (different key derivation). To use the same funds in both, a transfer between them is needed.
Quick Start
Installation
Add to your Cargo.toml:
[dependencies]
vexidus-sdk = { git = "https://github.com/vexidus/vexidus-blockchain", branch = "main" }
tokio = { version = "1", features = ["full"] }
Generate a Wallet
use vexidus_sdk::WalletKeypair;
fn main() {
let wallet = WalletKeypair::generate();
wallet.save("my_wallet.key").unwrap();
println!("Native address: {}", wallet.vx0_address()); // Vx0...
println!("Hex address: {}", wallet.hex_address()); // 0x... (32-byte)
println!("EVM address: {}", wallet.evm_address()); // 0x... (20-byte)
}
Load an Existing Wallet
// From file
let wallet = WalletKeypair::load("my_wallet.key")?;
// From hex string
let wallet = WalletKeypair::from_secret_hex("abcdef1234...")?;
Check Balance
use vexidus_sdk::WalletClient;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = WalletClient::new("https://testnet.vexidus.io");
let wallet = WalletKeypair::load("my_wallet.key")?;
let balance = client.get_balance(&wallet.vx0_address(), "VXS").await?;
println!("Balance: {} VXS", balance);
Ok(())
}
Send a Transfer
use vexidus_sdk::{WalletKeypair, WalletClient};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = WalletClient::new("https://testnet.vexidus.io");
let wallet = WalletKeypair::load("my_wallet.key")?;
// Convenience method: auto-fetches nonce, signs, submits
let tx_hash = client.transfer(
&wallet,
"Vx0RecipientAddress...",
"VXS",
5_000_000_000, // 5.0 VXS (9 decimals)
).await?;
println!("Transaction: {}", tx_hash);
Ok(())
}
Build a Custom Bundle
For more control, use the BundleBuilder:
use vexidus_sdk::{BundleBuilder, WalletKeypair, WalletClient};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = WalletClient::new("https://testnet.vexidus.io");
let wallet = WalletKeypair::load("my_wallet.key")?;
let nonce = client.get_nonce(&wallet.hex_address()).await?;
let bundle = BundleBuilder::new(&wallet.vx0_address())?
.transfer("Vx0Recipient...", "VXS", 5_000_000_000)?
.nonce(nonce)
.max_gas(100_000)
.valid_for(3600) // Valid for 1 hour
.sign(&wallet); // Ed25519 signature over bundle hash
let tx_hash = client.submit_bundle(&bundle).await?;
println!("Transaction: {}", tx_hash);
Ok(())
}
Transaction Signing Flow
Vexidus uses a bundle model -- all operations in a bundle are executed atomically.
Bundle Structure
TransactionBundle {
user_account: Address, // 32-byte sender
operations: Vec<Operation>, // One or more atomic operations
max_gas: u64, // Gas limit
max_priority_fee: u64, // Priority fee
valid_until: Timestamp, // Expiry (Unix seconds)
nonce: Nonce, // Replay protection
signature: Signature, // 64-byte Ed25519 signature
}
Available Operations
| Operation | Description |
|---|---|
Transfer | Send tokens (VXS or VSC-7) to an address |
Stake | Register as a validator with VXS |
Unstake | Begin 21-day unbonding |
ClaimUnstake | Claim matured unstake |
Delegate | Delegate VXS to a validator |
Undelegate | Undelegate from a validator |
ClaimRewards | Claim validator rewards |
AddKey | VSA v2: Add a signing key to account |
RemoveKey | VSA v2: Remove a key by hash |
RotateKey | VSA v2: Atomic key swap |
SetRecovery | VSA v2: Configure guardian recovery |
Signing Process
1. Build TransactionBundle (empty signature)
|
v
2. Compute hash: Blake3(user_account || operations || gas || fee || valid_until || nonce)
|
v
3. Sign 32-byte hash with Ed25519 -> 64-byte signature
|
v
4. Set bundle.signature = Signature(sig_bytes)
|
v
5. Borsh-serialize entire bundle -> bytes
|
v
6. Hex-encode -> "0x..."
|
v
7. Submit via vex_submitBundle("0x...")
The SDK handles steps 2-7 automatically when you call .sign(&wallet) and client.submit_bundle().
Pubkey Revelation (First-Time Sends)
Accounts created by receiving a transfer have a dummy public key (address bytes, not a real Ed25519 key). Ed25519 cannot recover the public key from a signature alone, so the first signed send from such an account must include the sender's public key for verification.
How it works:
- Wallet includes
sender_pubkey: Some(ed25519_pubkey_bytes)on the TransactionBundle - Node verifies
SHA256(pubkey)[0..20]matches the sender address[12..32] - Node verifies the Ed25519 signature against the provided pubkey
- On success, the pubkey is registered in
active_keys-- future sends do not need revelation
SDK usage:
// First send from a receive-only account
let bundle = BundleBuilder::new(&sender_hex)?
.transfer(to, "VXS", amount)?
.sender_pubkey(my_ed25519_pubkey.to_vec()) // Include pubkey for revelation
.sign(&wallet);
The native wallet VexSpark (https://wallet.vexspark.com) handles pubkey revelation automatically -- users do not need to think about it.
Testnet Note
On testnet, bundles with empty signatures are accepted with a warning log. This allows unsigned convenience methods (vex_transfer, vex_stake) to work during development. Mainnet will reject unsigned bundles.
Address Format Conversion
Using the SDK
use vexidus_sdk::address_utils;
// Derive Vx0 from public key
let vx0 = address_utils::vx0_from_pubkey(&pubkey_bytes);
// Vx0 -> 0x hex (32-byte internal format)
let hex_addr = address_utils::vx0_to_hex("Vx0abc...")?;
// Vx0 -> 0x EVM (20-byte, what MetaMask shows)
let evm_addr = address_utils::vx0_to_evm("Vx0abc...")?;
// Validate
assert!(address_utils::is_valid_vx0("Vx0abc..."));
assert!(address_utils::is_valid_hex_address("0x71C7..."));
// Parse any format -> 32-byte Address
let addr = address_utils::parse_address("Vx0abc...")?; // Works
let addr = address_utils::parse_address("0x71C7...")?; // Also works
Conversion Matrix
| From | To | Method | Lossless? |
|---|---|---|---|
| Public key -> Vx0 | vx0_from_pubkey() | Yes | |
| Vx0 -> 0x (32-byte) | vx0_to_hex() | Yes | |
| Vx0 -> 0x (20-byte EVM) | vx0_to_evm() | Yes | |
| 0x (20-byte) -> 32-byte | parse_address() | Yes (right-aligned) | |
| 0x (32-byte) -> Vx0 | Not available offline | No -- requires pubkey |
The conversion from 0x back to Vx0 is not possible offline because the Vx0 encoding includes a SHA256 checksum computed from the original pubkey hash, not from the padded 32-byte form. You would need the original public key or a node-side lookup.
Security
Key Storage
- Save wallet keys with
chmod 600(the SDK does this automatically onsave()) - Never log or print secret keys in production
- Store backups encrypted on offline media
- For web wallets, use the browser's
SubtleCryptoAPI to encrypt keys at rest
Testnet vs Mainnet
| Behavior | Testnet | Mainnet |
|---|---|---|
| Chain ID | 1618032 (0x18b070) | 1618033 (0x18b071) |
| Unsigned bundles | Accepted (with warning) | Rejected |
vex_generateKeypair | Available | Disabled |
Never reuse testnet keys on mainnet. Generate fresh keypairs for production.
Nonce Management
- Nonces are sequential: transaction with nonce=5 only executes after nonce=4
- Always fetch the current nonce immediately before signing
- For sequential transactions, increment locally:
nonce,nonce+1,nonce+2 - If a transaction fails, the nonce is not consumed -- retry with the same nonce
Replay Protection
Each bundle includes:
- nonce: Prevents duplicate execution
- valid_until: Time-bounds the transaction (stale bundles are rejected)
- chain_id: Prevents cross-chain replay (mainnet only)
Troubleshooting
"Invalid signature"
- Vexidus uses Ed25519, not secp256k1 (Ethereum). Make sure you are signing with the correct algorithm.
- The signature must be over the Blake3 bundle hash (32 bytes), not the raw bundle bytes.
- On testnet, empty signatures are accepted. On mainnet, they will be rejected.
"Invalid nonce"
- The nonce must exactly match the account's current nonce (not current+1).
- Query with
client.get_nonce()immediately before signing. - If you see
expected N, got M: a previous transaction may be pending in the mempool.
"Address must start with Vx0, Vx1, or 0x"
- Check that you are passing the address in the right format for the RPC method.
vex_*methods accept both Vx0 and 0x.eth_*methods expect 0x only.
"Vexidus address error: Invalid checksum"
- The Vx0 address has been corrupted (truncated, modified, or copy-paste error).
- Regenerate from the public key using
address_utils::vx0_from_pubkey().
Balance shows 0 but I funded the account
- Check you are querying the right address format. A Vx0 address and an 0x address for the same person may map to different internal accounts if derived from different keys.
- Use
vex_getBalancewith the exact address you funded.
MetaMask shows wrong balance
- VXS has 9 decimals natively. The EVM adapter scales to 18 for MetaMask display.
- If your custom RPC does not scale, MetaMask will show 10^9x too small.
Borsh serialization errors
- Ensure your SDK version matches the node version. The
TransactionBundleBorsh layout must be identical. - Update with
git pull && cargo build --release.
API Reference
WalletKeypair
| Method | Returns | Description |
|---|---|---|
generate() | WalletKeypair | New random Ed25519 keypair |
from_secret_hex(hex) | Result<WalletKeypair> | Load from hex string |
from_secret_bytes(bytes) | WalletKeypair | Load from 32-byte array |
load(path) | Result<WalletKeypair> | Load from file |
save(path) | Result<()> | Save to file (chmod 600) |
vx0_address() | String | Native Vx0 address |
hex_address() | String | 0x hex (32-byte) |
evm_address() | String | 0x EVM (20-byte) |
public_key_bytes() | [u8; 32] | Raw public key |
sign(message) | Vec<u8> | 64-byte Ed25519 signature |
sign_bundle(bundle) | Signature | Sign a TransactionBundle |
BundleBuilder
| Method | Returns | Description |
|---|---|---|
new(sender) | Result<BundleBuilder> | Create builder (accepts Vx0 or 0x) |
.transfer(to, token, amount) | Result<Self> | Add transfer operation |
.stake(amount, pubkey) | Self | Add stake operation |
.unstake(amount) | Self | Add unstake operation |
.delegate(validator, amount) | Result<Self> | Add delegate operation |
.claim_rewards() | Self | Add claim rewards operation |
.add_key(pubkey, type, role) | Self | VSA v2: Add key |
.nonce(n) | Self | Set nonce |
.max_gas(g) | Self | Set gas limit |
.valid_for(seconds) | Self | Set validity window |
.build() | TransactionBundle | Build unsigned bundle |
.sign(wallet) | TransactionBundle | Build and sign |
WalletClient
| Method | Returns | Description |
|---|---|---|
new(rpc_url) | WalletClient | Create client |
get_balance(addr, token) | Result<String> | Query token balance |
get_nonce(addr) | Result<u64> | Query account nonce |
submit_bundle(bundle) | Result<String> | Submit signed bundle |
transfer(wallet, to, token, amt) | Result<String> | Convenience: sign + submit |
get_token_info(symbol) | Result<Value> | Token metadata |
list_tokens(limit) | Result<Value> | List registered tokens |
chain_id() | Result<String> | Chain ID (hex) |
block_number() | Result<u64> | Current block height |
is_healthy() | bool | Node reachability check |
address_utils
| Function | Returns | Description |
|---|---|---|
vx0_from_pubkey(bytes) | String | Derive Vx0 from public key |
vx0_to_bytes(vx0) | Result<[u8; 32]> | Decode to 32-byte internal |
vx0_to_hex(vx0) | Result<String> | Convert to 0x hex (32-byte) |
vx0_to_evm(vx0) | Result<String> | Convert to 0x EVM (20-byte) |
is_valid_vx0(addr) | bool | Validate Vx0 checksum |
is_valid_hex_address(addr) | bool | Validate 0x format |
parse_address(input) | Result<Address> | Parse any format to 32-byte |
Vexidus is its own L1 blockchain. Native addresses use the Vx prefix. The 0x format exists only for EVM compatibility.