Keyboard shortcuts

Press โ† or โ†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SDK Documentation Spark
Request API Key

Testing and development

There are three networks to test an integration against:

Regtest Network

For most testing and development, we recommend using the Regtest Network - a deployed test network maintained by Lightspark that is free to use and carries no real-world value.

What you can test on Regtest

  • Spark Payments: Bitcoin and token payments using the Spark protocol
  • Deposits: Receiving test Bitcoin from the Lightspark Regtest Faucet
  • Withdrawals: Sending funds back to on-chain addresses
  • Token Issuance: Creating and testing tokens using the SDK's issuing functionality

Getting started

  1. Initialize the SDK using the default regtest config (no API key required)
  2. Generate a Bitcoin receiving address
  3. Request funds from the faucet to your generated address
  4. Test all Spark-related functionality in a controlled development environment

Local environment

The local environment is a complete Spark network on your own machine: a Bitcoin Core node in regtest mode, three Spark operators, a Spark service provider (SSP) with its Lightning node, a second Lightning node the SSP's holds a channel with, an LNURL server, a data-sync service, and a mempool block explorer with its API. It shares nothing with any other network, so the chain moves only when the environment mines, funds come from the environment's own Bitcoin node, and a reset returns everything to an empty chain.

What you can test locally

  • Spark payments: Bitcoin and token payments between wallets on the environment
  • Deposits: On-chain deposits, funded from the environment's Bitcoin node
  • Withdrawals: Sending funds back to on-chain addresses
  • Lightning payments, between wallets on the environment and with Alice, the second Lightning node
  • Lightning addresses: registered with, and served by, the environment's own LNURL server
  • Multi-device sync: a wallet's data kept in step across its instances by the environment's data-sync service
  • Token issuance, using the SDK's issuing functionality
  • Unilateral exits, which need the chain mined past a timelock

Alice stands in for the Lightning network outside the environment: she and the SSP's node share a 50 BTC channel, funded on both sides, so a wallet can pay her and be paid by her. A payment to any other node fails, since the environment is not connected to one.

Starting the environment

The environment runs on macOS and Linux, either in Docker or natively with Nix. The first start builds the Spark operator, the SSP and the Lightning node from source, which takes a while. A new environment then needs a few more minutes before it can serve wallets: the operators generate the signing keys the SSP needs to build its pool of leaves.

Docker: from a clone of the spark-sdk repository, run:

docker compose -f regtest/local/docker-compose.yml up

It builds what it needs and starts every service in order, reporting what it waits on as it goes: the operators generating their signing keys, then the SSP stocking its leaf pool. It ends by printing the addresses below. make local-env-up does the same in the background, make local-env-down stops it and keeps its state, and make local-env-reset deletes it.

Nix: with flakes enabled, run:

nix run github:breez/spark-sdk#local-env

The environment runs in the foreground until you quit it, and keeps its state in ./.spark-local, or in SPARK_LOCAL_DIR when set. Deleting that directory resets it.

Docker writes the environment's Spark configuration to regtest/local/data/spark-config.json, and Nix to spark-config.json in its state directory. Both print where it is, along with these endpoints:

EndpointAddressPort setting
Chain API (mempool.space)http://127.0.0.1:8090/apiMEMPOOL_PORT
Mempool explorerhttp://127.0.0.1:8090MEMPOOL_PORT
Spark service provider (SSP)http://127.0.0.1:59049SSP_PORT
LNURL serverhttp://127.0.0.1:8080LNURL_PORT
Data-sync servicehttp://127.0.0.1:8081DATA_SYNC_PORT
Data-sync service, for JavaScripthttp://127.0.0.1:8082DATA_SYNC_WEB_PORT
Bitcoin Core RPC (rpcuser / rpcpassword)http://127.0.0.1:18443BITCOIND_RPC_PORT

Every port is an environment variable, so an address already in use can be moved: BITCOIND_RPC_PORT=18500 docker compose -f regtest/local/docker-compose.yml up. The ports, what the environment mines and what the SSP keeps are listed in the environment's README.

Connecting the SDK

Every service a wallet uses is served over plain HTTP and answers cross-origin requests, so a wallet connects the same way in a browser. Start from the default config for Network::RegtestNetwork.REGTESTNetwork.regtestNetwork.RegtestNetwork.RegtestNetwork.RegtestNetwork.RegtestNetworkRegtestNetwork.Regtest and change five things:

  • Spark environment: parse_spark_configparse_spark_configparseSparkConfigparseSparkConfigparseSparkConfigparseSparkConfigparseSparkConfigParseSparkConfigParseSparkConfig reads the configuration file the environment writes, which carries its operators and its SSP. Set the result on spark_configspark_configsparkConfigsparkConfigsparkConfigsparkConfigsparkConfigSparkConfigSparkConfig. The fields are those of the Spark environment configuration.
  • Chain service: use a REST chain service at http://127.0.0.1:8090/api, of type ChainApiType::MempoolSpaceChainApiType.MEMPOOL_SPACEChainApiType.mempoolSpaceChainApiType.MempoolSpaceChainApiType.MempoolSpaceChainApiType.MempoolSpaceChainApiType.MempoolSpaceChainApiTypeMempoolSpaceChainApiType.MempoolSpace.
  • Deposit claim fee: the environment's SSP can quote more to claim a deposit than the default max_deposit_claim_feemax_deposit_claim_feemaxDepositClaimFeemaxDepositClaimFeemaxDepositClaimFeemaxDepositClaimFeemaxDepositClaimFeeMaxDepositClaimFeeMaxDepositClaimFee of 1 sat/vbyte allows. Deposits it quotes above the ceiling wait to be claimed manually, unless the ceiling is raised.
  • Lightning address domain: set lnurl_domainlnurl_domainlnurlDomainlnurlDomainlnurlDomainlnurlDomainlnurlDomainLnurlDomainLnurlDomain to the environment's LNURL server, http://127.0.0.1:8080, which registering an address then goes to.
  • Sync server: set real_time_sync_server_urlreal_time_sync_server_urlrealTimeSyncServerUrlrealTimeSyncServerUrlrealTimeSyncServerUrlrealTimeSyncServerUrlrealTimeSyncServerUrlRealTimeSyncServerUrlRealTimeSyncServerUrl to the environment's data-sync service, http://127.0.0.1:8081, which keeps a wallet's instances in step. The JavaScript SDK reaches it over gRPC-Web, at http://127.0.0.1:8082.
Rust
let mut config = default_config(Network::Regtest);

// The local environment writes this file when it starts
let spark_config = std::fs::read_to_string("regtest/local/data/spark-config.json")?;
config.spark_config = Some(parse_spark_config(spark_config)?);

// Its SSP charges more than the default ceiling to claim a deposit
config.max_deposit_claim_fee = Some(MaxFee::Rate { sat_per_vbyte: 5 });

// Its LNURL server serves the lightning addresses wallets register
config.lnurl_domain = Some("http://127.0.0.1:8080".to_string());

// Its data-sync service keeps this wallet's instances in step
config.real_time_sync_server_url = Some("http://127.0.0.1:8081".to_string());
Swift
var config = defaultConfig(network: Network.regtest)

// The local environment writes this file when it starts
let sparkConfig = try String(
    contentsOfFile: "regtest/local/data/spark-config.json", encoding: .utf8)
config.sparkConfig = try parseSparkConfig(json: sparkConfig)

// Its SSP charges more than the default ceiling to claim a deposit
config.maxDepositClaimFee = MaxFee.rate(satPerVbyte: 5)

// Its LNURL server serves the lightning addresses wallets register
config.lnurlDomain = "http://127.0.0.1:8080"

// Its data-sync service keeps this wallet's instances in step
config.realTimeSyncServerUrl = "http://127.0.0.1:8081"
Kotlin
val config = defaultConfig(Network.REGTEST)

// The local environment writes regtest/local/data/spark-config.json when it
// starts; read that file and pass its contents here
config.sparkConfig = parseSparkConfig(sparkConfigJson)

// Its SSP charges more than the default ceiling to claim a deposit
config.maxDepositClaimFee = MaxFee.Rate(5u)

// Its LNURL server serves the lightning addresses wallets register
config.lnurlDomain = "http://127.0.0.1:8080"

// Its data-sync service keeps this wallet's instances in step
config.realTimeSyncServerUrl = "http://127.0.0.1:8081"
C#
// The local environment writes this file when it starts
var sparkConfigJson = File.ReadAllText("regtest/local/data/spark-config.json");

var config = BreezSdkSparkMethods.DefaultConfig(Network.Regtest) with
{
    sparkConfig = BreezSdkSparkMethods.ParseSparkConfig(sparkConfigJson),

    // Its SSP charges more than the default ceiling to claim a deposit
    maxDepositClaimFee = new MaxFee.Rate(satPerVbyte: 5),

    // Its LNURL server serves the lightning addresses wallets register
    lnurlDomain = "http://127.0.0.1:8080",

    // Its data-sync service keeps this wallet's instances in step
    realTimeSyncServerUrl = "http://127.0.0.1:8081"
};
Javascript
const config = defaultConfig('regtest')

// The local environment writes this file when it starts
config.sparkConfig = parseSparkConfig(
  readFileSync('regtest/local/data/spark-config.json', 'utf8')
)

// Its SSP charges more than the default ceiling to claim a deposit
config.maxDepositClaimFee = { type: 'rate', satPerVbyte: 5 }

// Its LNURL server serves the lightning addresses wallets register
config.lnurlDomain = 'http://127.0.0.1:8080'

// Its data-sync service keeps this wallet's instances in step, over gRPC-Web
config.realTimeSyncServerUrl = 'http://127.0.0.1:8082'
React Native
const config = defaultConfig(Network.Regtest)

// The local environment writes this file when it starts
config.sparkConfig = parseSparkConfig(
  await RNFS.readFile(`${RNFS.DocumentDirectoryPath}/spark-config.json`, 'utf8')
)

// Its SSP charges more than the default ceiling to claim a deposit
config.maxDepositClaimFee = new MaxFee.Rate({ satPerVbyte: BigInt(5) })

// Its LNURL server serves the lightning addresses wallets register
config.lnurlDomain = 'http://127.0.0.1:8080'

// Its data-sync service keeps this wallet's instances in step
config.realTimeSyncServerUrl = 'http://127.0.0.1:8081'
Flutter
var config = defaultConfig(network: Network.regtest);

// The local environment writes this file when it starts
final sparkConfig =
    await File('regtest/local/data/spark-config.json').readAsString();
config = config.copyWith(sparkConfig: parseSparkConfig(json: sparkConfig));

// Its SSP charges more than the default ceiling to claim a deposit
config = config.copyWith(
    maxDepositClaimFee: MaxFee.rate(satPerVbyte: BigInt.from(5)));

// Its LNURL server serves the lightning addresses wallets register
config = config.copyWith(lnurlDomain: 'http://127.0.0.1:8080');

// Its data-sync service keeps this wallet's instances in step
config = config.copyWith(realTimeSyncServerUrl: 'http://127.0.0.1:8081');
Python
config = default_config(network=Network.REGTEST)

# The local environment writes this file when it starts
with open("regtest/local/data/spark-config.json", encoding="utf-8") as spark_config_file:
    spark_config = spark_config_file.read()
config.spark_config = parse_spark_config(json=spark_config)

# Its SSP charges more than the default ceiling to claim a deposit
config.max_deposit_claim_fee = MaxFee.RATE(sat_per_vbyte=5)

# Its LNURL server serves the lightning addresses wallets register
config.lnurl_domain = "http://127.0.0.1:8080"

# Its data-sync service keeps this wallet's instances in step
config.real_time_sync_server_url = "http://127.0.0.1:8081"
Go
config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkRegtest)

// The local environment writes this file when it starts
sparkConfigJson, err := os.ReadFile("regtest/local/data/spark-config.json")
if err != nil {
	return err
}
sparkConfig, err := breez_sdk_spark.ParseSparkConfig(string(sparkConfigJson))
if err != nil {
	return err
}
config.SparkConfig = &sparkConfig

// Its SSP charges more than the default ceiling to claim a deposit
feeRateInterface := breez_sdk_spark.MaxFee(breez_sdk_spark.MaxFeeRate{SatPerVbyte: 5})
config.MaxDepositClaimFee = &feeRateInterface

// Its LNURL server serves the lightning addresses wallets register
lnurlDomain := "http://127.0.0.1:8080"
config.LnurlDomain = &lnurlDomain

// Its data-sync service keeps this wallet's instances in step
syncServerUrl := "http://127.0.0.1:8081"
config.RealTimeSyncServerUrl = &syncServerUrl

Both setups print a command for each service, including ldk-server-cli calls that invoice from Alice and pay a wallet's invoice with her.

Funding wallets and mining blocks

The environment mines a block every 5 seconds. To send a wallet funds from the environment's Bitcoin node, or to mine blocks at once, for example to pass a timelock:

DockerNix
Fund an addressmake local-env-fund ADDRESS=<address> AMOUNT_SATS=<sats>nix run github:breez/spark-sdk#local-env -- fund <address> <sats>
Mine blocksmake local-env-mine BLOCKS=<blocks>nix run github:breez/spark-sdk#local-env -- mine <blocks>

Testing from another device

By default every service accepts connections only from the machine the environment runs on, and the Spark configuration file points wallets at 127.0.0.1. PUBLIC_HOST points them at another address, and takes effect when a stopped environment starts. The configuration file and the endpoints the environment prints then use that address.

  • Android emulator: set PUBLIC_HOST=10.0.2.2, the address under which the emulator reaches its host.
  • A phone on your network: set PUBLIC_HOST to your machine's address on that network, and BIND_ADDRESS=0.0.0.0 so the services accept connections from other machines, for example BIND_ADDRESS=0.0.0.0 PUBLIC_HOST=192.168.1.10 make local-env-up.

Mainnet testing

Some features rely on live networks that Regtest doesn't reproduce. Test these on Mainnet with small amounts: use real satoshis, but keep transaction values very low while verifying the flows work correctly.

Lightning payments

The Regtest Network doesn't have a developed Lightning Network, so test Lightning send and receive flows on Mainnet.

Stable balance and USDC/USDT

The stablecoin assets are only available on Mainnet:

  • USDB is the Spark-native stablecoin behind Stable Balance.
  • USDC and USDT are cross-chain assets. Use USDC/USDT to pay recipients on their native chains or receive from them. The cross-chain providers operate against live external networks and have no testnet equivalent.

Test these integrations on Mainnet with small amounts.

Development best practices

  • Start with Regtest for most development and testing
  • Use a local environment to control the chain, or to test without depending on a shared network
  • Use Mainnet for Lightning, stable balance, and USDC/USDT testing
  • Test all payment types you plan to support in your application