Testing and development
There are three networks to test an integration against:
- The Regtest Network maintained by Lightspark, for most testing and development
- A local environment: a Spark regtest network of your own, running on your machine
- Mainnet with small amounts, for features that depend on live networks
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
- Initialize the SDK using the default regtest config (no API key required)
- Generate a Bitcoin receiving address
- Request funds from the faucet to your generated address
- 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:
| Endpoint | Address | Port setting |
|---|---|---|
| Chain API (mempool.space) | http://127.0.0.1:8090/api | MEMPOOL_PORT |
| Mempool explorer | http://127.0.0.1:8090 | MEMPOOL_PORT |
| Spark service provider (SSP) | http://127.0.0.1:59049 | SSP_PORT |
| LNURL server | http://127.0.0.1:8080 | LNURL_PORT |
| Data-sync service | http://127.0.0.1:8081 | DATA_SYNC_PORT |
| Data-sync service, for JavaScript | http://127.0.0.1:8082 | DATA_SYNC_WEB_PORT |
Bitcoin Core RPC (rpcuser / rpcpassword) | http://127.0.0.1:18443 | BITCOIND_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_configparseSparkConfigparseSparkConfigparseSparkConfigparseSparkConfigparseSparkConfigParseSparkConfigParseSparkConfigreads the configuration file the environment writes, which carries its operators and its SSP. Set the result onspark_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 typeChainApiType::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_feemaxDepositClaimFeemaxDepositClaimFeemaxDepositClaimFeemaxDepositClaimFeemaxDepositClaimFeeMaxDepositClaimFeeMaxDepositClaimFeeof 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_domainlnurlDomainlnurlDomainlnurlDomainlnurlDomainlnurlDomainLnurlDomainLnurlDomainto 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_urlrealTimeSyncServerUrlrealTimeSyncServerUrlrealTimeSyncServerUrlrealTimeSyncServerUrlrealTimeSyncServerUrlRealTimeSyncServerUrlRealTimeSyncServerUrlto 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, athttp://127.0.0.1:8082.
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());
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"
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"
// 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"
};
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'
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'
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');
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"
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:
| Docker | Nix | |
|---|---|---|
| Fund an address | make local-env-fund ADDRESS=<address> AMOUNT_SATS=<sats> | nix run github:breez/spark-sdk#local-env -- fund <address> <sats> |
| Mine blocks | make 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_HOSTto your machine's address on that network, andBIND_ADDRESS=0.0.0.0so the services accept connections from other machines, for exampleBIND_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