Vexidus SDK & Wallet Developer Guide
Build wallets, dApps, and tools that interact with the Vexidus blockchain.
Table of Contents
- JavaScript SDK (Quick Start)
- Vexidus Address System
- Wallet Compatibility
- MetaMask Integration
- Quick Start
- Transaction Signing Flow
- Address Format Conversion
- Security
- Troubleshooting
JavaScript SDK
The @vexidus/sdk is a pure TypeScript SDK for building on Vexidus. Zero native dependencies — works in Node.js 18+, Bun, Deno, and browsers.
Install
npm install @vexidus/sdk
Quick Start
import { WalletKeypair, BundleBuilder, RpcClient, ONE_VXS } from '@vexidus/sdk';
// Connect to testnet
const rpc = new RpcClient('https://testnet.vexidus.io');
// Generate a wallet
const wallet = WalletKeypair.generate();
console.log('Address:', wallet.vx0Address()); // Vx0abc...
console.log('EVM:', wallet.evmAddress()); // 0xabc...
// Send 10 VXS
const nonce = await rpc.getNonce(wallet.vx0Address());
const txHash = await rpc.submitBundleHex(
new BundleBuilder(wallet.vx0Address())
.transfer('Vx0recipient...', 'VXS', 10n * ONE_VXS)
.nonce(nonce)
.signAndSerialize(wallet)
);
console.log('TX:', txHash);
What's Included
| Module | Purpose |
|---|---|
WalletKeypair | Ed25519 keypair generation, Vx0/0x address derivation, bundle signing |
BundleBuilder | Fluent API for Transfer, Swap, Stake, DEX, Governance, VNS, Token creation |
RpcClient | Typed JSON-RPC wrappers (submitBundle, getBalance, getNonce, quoteSwap, etc.) |
| Address utilities | Parse/validate/convert all Vx and 0x address formats |
Swap Tokens
new BundleBuilder(myAddress)
.swap('VXS', 'Vx1tokenMint...', 1n * ONE_VXS, 0n)
.nonce(nonce)
.signAndSerialize(wallet);
Stake VXS
new BundleBuilder(myAddress)
.delegate('0xvalidator...', 100n * ONE_VXS)
.nonce(nonce)
.signAndSerialize(wallet);
Multi-Operation Bundles
Combine multiple operations in a single atomic transaction:
new BundleBuilder(myAddress)
.transfer(recipient1, 'VXS', 5n * ONE_VXS)
.transfer(recipient2, 'VXS', 3n * ONE_VXS)
.claimRewards()
.nonce(nonce)
.signAndSerialize(wallet);
Key Details
- VXS has 9 decimals:
1 VXS = 1_000_000_000nraw units (useONE_VXSconstant) - Bundle = atomic multi-op: Unlike Ethereum (1 tx = 1 action), Vexidus bundles multiple operations in a single signature
- Ed25519 signatures: Not secp256k1. Keys are 32-byte secret + 32-byte public
- Borsh serialization: Bundles are Borsh-encoded (not RLP/ABI) for wire format
For complete API reference, see the SDK README on GitHub.
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.