Spark
Web3Wallet exposes Bitcoin Layer 2 functionality via the Spark network. It supports Bitcoin transfers, Spark tokens, and message signing while following the official Spark Wallet API specification.
Spark runs on Bitcoin Layer 2 for fast, low‑cost transactions while inheriting Bitcoin security. It supports REGTEST (development) and MAINNET.
This implementation follows the Spark Wallet API and uses the Sparkscan API for enriched features (e.g., balances, history).
Integration Options
Spark requires this configuration:
# .env — add to your existing UTXOS credentials
NEXT_PUBLIC_UTXOS_PROJECT_ID=your_project_id
NEXT_PUBLIC_NETWORK_ID=0 # 0 = REGTEST, 1 = MAINNET
# Server-side only (for proxying Sparkscan)
SPARKSCAN_API_KEY=your_sparkscan_api_key # get from https://sparkscan.ioHello world
import { EnableWeb3WalletOptions, Web3Wallet } from "@meshsdk/web3-sdk";
// Configure UTXOS wallet options (client-side)
const options: EnableWeb3WalletOptions = {
networkId: parseInt(process.env.NEXT_PUBLIC_NETWORK_ID) || 0, // 0: REGTEST, 1: MAINNET
projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
// Use a server-side proxy to keep SPARKSCAN_API_KEY secret
baseUrl: "/api/sparkscan",
};
// Enable the unified wallet
const wallet = await Web3Wallet.enable(options);
// Access Spark wallet through unified interface
const sparkWallet = wallet.spark;1. Install Dependencies
npm install @meshsdk/web3-sdk2. Initialize The Unified Wallet
import { EnableWeb3WalletOptions, Web3Wallet } from "@meshsdk/web3-sdk";
async function initializeWallet() {
try {
const options: EnableWeb3WalletOptions = {
networkId: parseInt(process.env.NEXT_PUBLIC_NETWORK_ID) || 0, // 0: REGTEST, 1: MAINNET
projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
baseUrl: "/api/sparkscan", // Use a proxy to protect SPARKSCAN_API_KEY
};
const wallet = await Web3Wallet.enable(options);
return wallet;
} catch (error) {
console.error("Failed to initialize wallet:", error);
throw error;
}
}For baseUrl, use a server-side proxy to keep SPARKSCAN_API_KEY secure. See Set Up Sparkscan API Proxy.
3. Verify Spark Connection
const wallet = await initializeWallet();
const sparkWallet = wallet.spark;
// Get Spark address
const sparkAddress = await sparkWallet.getSparkAddress();
console.log("Spark Address:", sparkAddress);
// Get wallet balance
const balance = await sparkWallet.getBalance();
console.log("Bitcoin Balance:", balance.balance.toString(), "sats");Core Wallet Operations
Wallet Information
// Get Spark address (Layer 2 address)
const sparkAddress = await sparkWallet.getSparkAddress();
console.log("Spark Address:", sparkAddress);
// Get identity public key
const identityKey = await sparkWallet.getIdentityPublicKey();
console.log("Identity Public Key:", identityKey);
// Get Bitcoin deposit address (for L1 → L2 deposits)
const depositAddress = await sparkWallet.getStaticDepositAddress();
console.log("Bitcoin Deposit Address:", depositAddress);
// Get complete wallet info in one call
const walletInfo = await sparkWallet.getWalletInfo();
console.log("Wallet Info:", walletInfo);Balance Management
// Get wallet balance
const balanceData = await sparkWallet.getBalance();
// Bitcoin balance in satoshis
const bitcoinBalance = balanceData.balance;
console.log(`Bitcoin Balance: ${bitcoinBalance.toString()} sats`);
// Token balances (Map<tokenIdentifier, balanceData>)
balanceData.tokenBalances.forEach((tokenData, tokenId) => {
console.log(`Token: ${tokenId}`);
console.log(` Balance: ${tokenData.balance}`);
console.log(` Identifier: ${tokenData.bech32mTokenIdentifier}`);
});
// Get enhanced balance with metadata
const enhancedBalance = await sparkWallet.getBalanceWithMetadata();
enhancedBalance.tokenBalances.forEach(token => {
if (token.metadata) {
console.log(`Token: ${token.metadata.name} (${token.metadata.ticker})`);
console.log(` Balance: ${token.balance}`);
console.log(` Decimals: ${token.metadata.decimals}`);
}
});Bitcoin Transfers
// Transfer Bitcoin on Spark Layer 2
try {
const transferResult = await sparkWallet.transfer({
receiverSparkAddress: "sprt1...", // Replace with actual Spark address
amountSats: 100000, // 0.001 BTC in satoshis
});
// Returns complete WalletTransfer object
console.log("Transfer ID:", transferResult.id);
console.log("Status:", transferResult.status);
console.log("Total Value:", transferResult.totalValue);
console.log("Direction:", transferResult.transferDirection);
} catch (error) {
console.error("Transfer failed:", error);
}Token Transfers
// Transfer custom tokens on Spark
try {
const tokenTxId = await sparkWallet.transferTokens({
tokenIdentifier: "btkn1...", // Bech32m token identifier
tokenAmount: 1000n, // Amount as bigint
receiverSparkAddress: "sprt1...",
selectedOutputs: undefined, // Optional: specific UTXOs to use
});
console.log("Token transfer successful:", tokenTxId);
} catch (error) {
console.error("Token transfer failed:", error);
}Message Signing
// Sign message with wallet's identity key
const message = new TextEncoder().encode("Authentication message");
const signature = await sparkWallet.signMessageWithIdentityKey(
message,
false // compact format (optional)
);
console.log("Identity signature:", signature);
// Standardized signing for wallet bridge compatibility
const hexSignature = await sparkWallet.signData("Hello Spark");
console.log("Hex signature:", hexSignature);Advanced Operations
Transaction History
// Get transaction history with filtering
const history = await sparkWallet.getTransfers(
50, // limit
0, // offset
null, // asset filter (null = all assets)
"2024-01-01", // from timestamp
null, // to timestamp (null = now)
"created_at", // sort field
"desc" // order
);
// Process transactions
history.transactions.forEach(tx => {
console.log(`Transaction: ${tx.id}`);
console.log(` Type: ${tx.type}`);
console.log(` Amount: ${tx.amountSats} sats`);
console.log(` Status: ${tx.status}`);
});
// Pagination info
const { totalItems, limit, offset } = history.meta;
console.log(`Showing ${offset + 1}-${Math.min(offset + limit, totalItems)} of ${totalItems}`);Bitcoin Deposit Claims
// Check Bitcoin addresses for claimable deposits
const bitcoinAddresses = ["bc1q...", "3..."];
const latestTxids = await sparkWallet.getLatestTxid(bitcoinAddresses);
// Process each address
for (const [address, txid] of Object.entries(latestTxids)) {
if (txid) {
console.log(`Found transaction ${txid} for ${address}`);
try {
const claimResult = await sparkWallet.claimStaticDeposit(txid);
if (claimResult) {
console.log(`Claimed deposit: ${claimResult}`);
}
} catch (error) {
console.log(`No claimable deposit in ${txid}`);
}
}
}Complete Examples
Example 1: Bitcoin L1 to Spark L2 Deposit Flow
import { Web3Wallet } from "@meshsdk/web3-sdk";
async function bitcoinToSparkDeposit() {
try {
// Initialize unified wallet
const wallet = await Web3Wallet.enable({
networkId: 0, // REGTEST for development
projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
baseUrl: "/api/sparkscan",
});
const sparkWallet = wallet.spark;
// Step 1: Get Bitcoin deposit address
const depositAddress = await sparkWallet.getStaticDepositAddress();
console.log("Send Bitcoin to this address:", depositAddress);
// Step 2: Monitor for deposits
const latestTxids = await sparkWallet.getLatestTxid([depositAddress]);
// Step 3: Claim deposit when Bitcoin transaction confirms
for (const [address, txid] of Object.entries(latestTxids)) {
if (txid) {
console.log(`Found deposit transaction: ${txid}`);
const claimTxId = await sparkWallet.claimStaticDeposit(txid);
if (claimTxId) {
console.log(`Deposit claimed: ${claimTxId}`);
// Check new balance
const balance = await sparkWallet.getBalance();
console.log(`New balance: ${balance.balance} sats`);
}
}
}
} catch (error) {
console.error("Deposit flow failed:", error);
}
}Error Handling Best Practices
import { ApiError } from "@meshsdk/web3-sdk";
async function handleSparkOperation() {
try {
const result = await sparkWallet.transfer({
receiverSparkAddress: "sprt1...",
amountSats: 75000,
});
return result;
} catch (error) {
if (error instanceof ApiError) {
// Handle specific API error codes
switch (error.code) {
case 1:
throw new Error("Wallet not properly configured");
case 3:
throw new Error("User cancelled the operation");
case 4:
throw new Error("Transfer failed - check balance and address");
default:
throw new Error(`Wallet error: ${error.info}`);
}
}
throw error;
}
}Security Considerations
- Keep
SPARKSCAN_API_KEYserver-side: route requests through a proxy - Validate Spark addresses before sending:
- MAINNET:
spark1...(new) orsprt1...(legacy) - REGTEST:
sparkrt1...(new) orsprt1...(legacy)
- MAINNET:
- Use REGTEST for development and fully test before MAINNET
- Add robust error handling for production
Set Up Sparkscan API Proxy
Use a minimal App Router endpoint to keep your API key server-side:
// app/api/sparkscan/[...slug]/route.ts (Next.js App Router)
export default async function handler(req, res) {
try {
const { slug } = req.query;
const endpoint = slug.join('/');
const apiKey = process.env.SPARKSCAN_API_KEY;
if (!apiKey) {
return res.status(500).json({
error: "Missing Sparkscan API key",
message: "Please add SPARKSCAN_API_KEY to your .env.local file"
});
}
// Forward request to Sparkscan
const baseURL = "https://api.sparkscan.io/v1";
const url = `${baseURL}/${endpoint}`;
const response = await fetch(url, {
method: req.method,
headers: {
'Authorization': `Bearer ${apiKey}`,
'Accept': 'application/json',
...(req.method !== 'GET' && { 'Content-Type': 'application/json' })
},
body: req.method !== 'GET' ? JSON.stringify(req.body) : undefined,
});
const data = await response.json();
return res.status(response.status).json(data);
} catch (error) {
console.error("Sparkscan API route error:", error);
return res.status(500).json({ error: error.message });
}
}Then use the proxy in your wallet configuration:
// Use proxy instead of direct API key
const sparkWallet = await Web3SparkWallet.enable({
network: "REGTEST",
projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
appUrl: "https://your-app.com",
baseUrl: "/api/sparkscan", // Uses secure proxy
});
// Or with unified wallet
const wallet = await Web3Wallet.enable({
networkId: 0, // REGTEST (use 1 for MAINNET)
projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
baseUrl: "/api/sparkscan", // Uses secure proxy
});With the proxy in place, you can safely use Spark functionality without exposing SPARKSCAN_API_KEY to the client.