Skip to main content

Vexidus Wallet SDK Developer Guide

Build wallets, dApps, and tools that interact with the Vexidus blockchain.


Table of Contents

  1. Vexidus Address System
  2. Wallet Compatibility
  3. MetaMask Integration
  4. Quick Start
  5. Transaction Signing Flow
  6. Address Format Conversion
  7. Security
  8. 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

FormatSizeEncodingUsed ByExample
Vx020-byte payloadBase58 + SHA256 checksumNative wallets, vex_* RPCVx0Hk8pQwB5Z...
Vx120-byte payloadBase58 + SHA256 checksumToken/contract mintsVx1RjT4nQ9...
0x (32-byte)32 bytesHexInternal state, system addresses0x0000...0001
0x (20-byte)20 bytesHexMetaMask, eth_* RPC0x71C7...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

EthereumVexidus
Address format0x (20 bytes)Vx0 (native) or 0x (EVM compat)
Signature schemesecp256k1 (ECDSA)Ed25519
Native decimals18 (ETH)9 (VXS)
Transaction modelSingle operationBundle (atomic multi-op)
Key derivationKeccak256(pubkey) -> 20 bytesSHA256(pubkey) -> 20 bytes -> Vx0

Wallet Compatibility

WalletWorks?MethodNotes
Native Vexidus walletFull supportvex_submitBundleVx0 addresses, Ed25519, 9 decimals
MetaMaskVia EVM adaptereth_sendRawTransaction20-byte 0x, ECDSA, 18->9 decimal conversion
Trust WalletVia EVM adapterSame as MetaMaskAdd as custom network
PhantomPlanned (post-beta)SVM adapter (svm_impl.rs)Ed25519 compatible -- same curve as Vexidus, needs RPC translation layer
Ledger / TrezorPlannedSDK bridgeEd25519 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 0x and 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

NetworkDecimalHex
Testnet16180320x18b070
Mainnet16180330x18b071

The Decimal Issue

VXS natively has 9 decimals, but MetaMask's UI works best with 18. The EVM adapter handles this:

  • eth_getBalance returns VXS balance scaled to 18 decimals (balance * 10^9)
  • eth_sendRawTransaction converts 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:

  1. MetaMask provides a 20-byte 0x address (derived from secp256k1, NOT Ed25519)
  2. The EVM adapter right-aligns this to 32 bytes for internal use
  3. 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

OperationDescription
TransferSend tokens (VXS or VSC-7) to an address
StakeRegister as a validator with VXS
UnstakeBegin 21-day unbonding
ClaimUnstakeClaim matured unstake
DelegateDelegate VXS to a validator
UndelegateUndelegate from a validator
ClaimRewardsClaim validator rewards
AddKeyVSA v2: Add a signing key to account
RemoveKeyVSA v2: Remove a key by hash
RotateKeyVSA v2: Atomic key swap
SetRecoveryVSA 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:

  1. Wallet includes sender_pubkey: Some(ed25519_pubkey_bytes) on the TransactionBundle
  2. Node verifies SHA256(pubkey)[0..20] matches the sender address [12..32]
  3. Node verifies the Ed25519 signature against the provided pubkey
  4. 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

FromToMethodLossless?
Public key -> Vx0vx0_from_pubkey()Yes
Vx0 -> 0x (32-byte)vx0_to_hex()Yes
Vx0 -> 0x (20-byte EVM)vx0_to_evm()Yes
0x (20-byte) -> 32-byteparse_address()Yes (right-aligned)
0x (32-byte) -> Vx0Not available offlineNo -- 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 on save())
  • Never log or print secret keys in production
  • Store backups encrypted on offline media
  • For web wallets, use the browser's SubtleCrypto API to encrypt keys at rest

Testnet vs Mainnet

BehaviorTestnetMainnet
Chain ID1618032 (0x18b070)1618033 (0x18b071)
Unsigned bundlesAccepted (with warning)Rejected
vex_generateKeypairAvailableDisabled

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_getBalance with 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 TransactionBundle Borsh layout must be identical.
  • Update with git pull && cargo build --release.

API Reference

WalletKeypair

MethodReturnsDescription
generate()WalletKeypairNew random Ed25519 keypair
from_secret_hex(hex)Result<WalletKeypair>Load from hex string
from_secret_bytes(bytes)WalletKeypairLoad from 32-byte array
load(path)Result<WalletKeypair>Load from file
save(path)Result<()>Save to file (chmod 600)
vx0_address()StringNative Vx0 address
hex_address()String0x hex (32-byte)
evm_address()String0x EVM (20-byte)
public_key_bytes()[u8; 32]Raw public key
sign(message)Vec<u8>64-byte Ed25519 signature
sign_bundle(bundle)SignatureSign a TransactionBundle

BundleBuilder

MethodReturnsDescription
new(sender)Result<BundleBuilder>Create builder (accepts Vx0 or 0x)
.transfer(to, token, amount)Result<Self>Add transfer operation
.stake(amount, pubkey)SelfAdd stake operation
.unstake(amount)SelfAdd unstake operation
.delegate(validator, amount)Result<Self>Add delegate operation
.claim_rewards()SelfAdd claim rewards operation
.add_key(pubkey, type, role)SelfVSA v2: Add key
.nonce(n)SelfSet nonce
.max_gas(g)SelfSet gas limit
.valid_for(seconds)SelfSet validity window
.build()TransactionBundleBuild unsigned bundle
.sign(wallet)TransactionBundleBuild and sign

WalletClient

MethodReturnsDescription
new(rpc_url)WalletClientCreate 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()boolNode reachability check

address_utils

FunctionReturnsDescription
vx0_from_pubkey(bytes)StringDerive 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)boolValidate Vx0 checksum
is_valid_hex_address(addr)boolValidate 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.