Sending and receiving tokens
Spark supports tokens using the BTKN protocol. The Breez SDK enables you to send and receive these tokens using the standard payments API.
Fetching token balances API docs
Token balances for all tokens currently held in the wallet can be retrieved along with general wallet information. Each token balance includes both the balance amount and the token metadata (identifier, name, ticker, issuer public key, etc.).
let info = sdk.get_info(GetInfoRequest {
// ensure_synced: true will ensure the SDK is synced with the Spark network
// before returning the balance
ensure_synced: Some(false),
}).await?;
// Token balances are a map of token identifier to balance
let token_balances = info.token_balances;
for (token_id, token_balance) in token_balances {
info!("Token ID: {}", token_id);
info!("Balance: {}", token_balance.balance);
info!("Name: {}", token_balance.token_metadata.name);
info!("Ticker: {}", token_balance.token_metadata.ticker);
info!("Decimals: {}", token_balance.token_metadata.decimals);
}
// ensureSynced: true will ensure the SDK is synced with the Spark network
// before returning the balance
let info = try await sdk.getInfo(
request: GetInfoRequest(
ensureSynced: false
))
// Token balances are a map of token identifier to balance
let tokenBalances = info.tokenBalances
for (tokenId, tokenBalance) in tokenBalances {
print("Token ID: \(tokenId)")
print("Balance: \(tokenBalance.balance)")
print("Name: \(tokenBalance.tokenMetadata.name)")
print("Ticker: \(tokenBalance.tokenMetadata.ticker)")
print("Decimals: \(tokenBalance.tokenMetadata.decimals)")
}
try {
// ensureSynced: true will ensure the SDK is synced with the Spark network
// before returning the balance
val info = sdk.getInfo(GetInfoRequest(false))
// Token balances are a map of token identifier to balance
val tokenBalances = info.tokenBalances
for ((tokenId, tokenBalance) in tokenBalances) {
println("Token ID: $tokenId")
println("Balance: ${tokenBalance.balance}")
println("Name: ${tokenBalance.tokenMetadata.name}")
println("Ticker: ${tokenBalance.tokenMetadata.ticker}")
println("Decimals: ${tokenBalance.tokenMetadata.decimals}")
}
} catch (e: Exception) {
// handle error
}
const info = await sdk.getInfo({
// ensureSynced: true will ensure the SDK is synced with the Spark network
// before returning the balance
ensureSynced: false
})
// Token balances are a map of token identifier to balance
const tokenBalances = info.tokenBalances
for (const [tokenId, tokenBalance] of Object.entries(tokenBalances)) {
console.log(`Token ID: ${tokenId}`)
console.log(`Balance: ${tokenBalance.balance}`)
console.log(`Name: ${tokenBalance.tokenMetadata.name}`)
console.log(`Ticker: ${tokenBalance.tokenMetadata.ticker}`)
console.log(`Decimals: ${tokenBalance.tokenMetadata.decimals}`)
}
const info = await sdk.getInfo({
// ensureSynced: true will ensure the SDK is synced with the Spark network
// before returning the balance
ensureSynced: false
})
// Token balances are a map of token identifier to balance
const tokenBalances = info.tokenBalances
for (const [tokenId, tokenBalance] of Object.entries(tokenBalances)) {
console.log(`Token ID: ${tokenId}`)
console.log(`Balance: ${tokenBalance.balance}`)
console.log(`Name: ${tokenBalance.tokenMetadata.name}`)
console.log(`Ticker: ${tokenBalance.tokenMetadata.ticker}`)
console.log(`Decimals: ${tokenBalance.tokenMetadata.decimals}`)
}
// ensureSynced: true will ensure the SDK is synced with the Spark network
// before returning the balance
final info = await sdk.getInfo(request: GetInfoRequest(ensureSynced: false));
// Token balances are a map of token identifier to balance
final tokenBalances = info.tokenBalances;
tokenBalances.forEach((tokenId, tokenBalance) {
print('Token ID: $tokenId');
print('Balance: ${tokenBalance.balance}');
print('Name: ${tokenBalance.tokenMetadata.name}');
print('Ticker: ${tokenBalance.tokenMetadata.ticker}');
print('Decimals: ${tokenBalance.tokenMetadata.decimals}');
});
try:
# ensure_synced: True will ensure the SDK is synced with the Spark network
# before returning the balance
info = await sdk.get_info(request=GetInfoRequest(ensure_synced=False))
# Token balances are a map of token identifier to balance
token_balances = info.token_balances
for token_id, token_balance in token_balances.items():
print(f"Token ID: {token_id}")
print(f"Balance: {token_balance.balance}")
print(f"Name: {token_balance.token_metadata.name}")
print(f"Ticker: {token_balance.token_metadata.ticker}")
print(f"Decimals: {token_balance.token_metadata.decimals}")
except Exception as error:
logging.error(error)
raise
ensureSynced := false
info, err := sdk.GetInfo(breez_sdk_spark.GetInfoRequest{
// EnsureSynced: true will ensure the SDK is synced with the Spark network
// before returning the balance
EnsureSynced: &ensureSynced,
})
if sdkErr := err.(*breez_sdk_spark.SdkError); sdkErr != nil {
return err
}
// Token balances are a map of token identifier to balance
tokenBalances := info.TokenBalances
for tokenId, tokenBalance := range tokenBalances {
log.Printf("Token ID: %v", tokenId)
log.Printf("Balance: %v", tokenBalance.Balance)
log.Printf("Name: %v", tokenBalance.TokenMetadata.Name)
log.Printf("Ticker: %v", tokenBalance.TokenMetadata.Ticker)
log.Printf("Decimals: %v", tokenBalance.TokenMetadata.Decimals)
}
Developer note
Token balances are cached for fast responses. For details on ensuring up-to-date balances, see the Fetching the balance section.Fetching token metadata API docs
Token metadata can be fetched for specific tokens by providing their identifiers. This is especially useful for retrieving metadata for tokens that are not currently held in the wallet. The metadata is cached locally after the first fetch for faster subsequent lookups.
let response = sdk.get_tokens_metadata(GetTokensMetadataRequest {
token_identifiers: vec![String::from("<token identifier 1>"), String::from("<token identifier 2>")],
}).await?;
let tokens_metadata = response.tokens_metadata;
for token_metadata in tokens_metadata {
info!("Token ID: {}", token_metadata.identifier);
info!("Name: {}", token_metadata.name);
info!("Ticker: {}", token_metadata.ticker);
info!("Decimals: {}", token_metadata.decimals);
info!("Max Supply: {}", token_metadata.max_supply);
info!("Is Freezable: {}", token_metadata.is_freezable);
}
let response = try await sdk.getTokensMetadata(
request: GetTokensMetadataRequest(tokenIdentifiers: [
"<token identifier 1>", "<token identifier 2>",
]))
let tokensMetadata = response.tokensMetadata
for tokenMetadata in tokensMetadata {
print("Token ID: \(tokenMetadata.identifier)")
print("Name: \(tokenMetadata.name)")
print("Ticker: \(tokenMetadata.ticker)")
print("Decimals: \(tokenMetadata.decimals)")
print("Max Supply: \(tokenMetadata.maxSupply)")
print("Is Freezable: \(tokenMetadata.isFreezable)")
}
try {
val response =
sdk.getTokensMetadata(
GetTokensMetadataRequest(
tokenIdentifiers = listOf("<token identifier 1>", "<token identifier 2>")
)
)
val tokensMetadata = response.tokensMetadata
for (tokenMetadata in tokensMetadata) {
println("Token ID: ${tokenMetadata.identifier}")
println("Name: ${tokenMetadata.name}")
println("Ticker: ${tokenMetadata.ticker}")
println("Decimals: ${tokenMetadata.decimals}")
println("Max Supply: ${tokenMetadata.maxSupply}")
println("Is Freezable: ${tokenMetadata.isFreezable}")
}
} catch (e: Exception) {
// handle error
}
const response = await sdk.getTokensMetadata({
tokenIdentifiers: ['<token identifier 1>', '<token identifier 2>']
})
const tokensMetadata = response.tokensMetadata
for (const tokenMetadata of tokensMetadata) {
console.log(`Token ID: ${tokenMetadata.identifier}`)
console.log(`Name: ${tokenMetadata.name}`)
console.log(`Ticker: ${tokenMetadata.ticker}`)
console.log(`Decimals: ${tokenMetadata.decimals}`)
console.log(`Max Supply: ${tokenMetadata.maxSupply}`)
console.log(`Is Freezable: ${tokenMetadata.isFreezable}`)
}
const response = await sdk.getTokensMetadata({
tokenIdentifiers: ['<token identifier 1>', '<token identifier 2>']
})
const tokensMetadata = response.tokensMetadata
for (const tokenMetadata of tokensMetadata) {
console.log(`Token ID: ${tokenMetadata.identifier}`)
console.log(`Name: ${tokenMetadata.name}`)
console.log(`Ticker: ${tokenMetadata.ticker}`)
console.log(`Decimals: ${tokenMetadata.decimals}`)
console.log(`Max Supply: ${tokenMetadata.maxSupply}`)
console.log(`Is Freezable: ${tokenMetadata.isFreezable}`)
}
final response = await sdk.getTokensMetadata(
request: GetTokensMetadataRequest(
tokenIdentifiers: ['<token identifier 1>', '<token identifier 2>']
)
);
final tokensMetadata = response.tokensMetadata;
for (final tokenMetadata in tokensMetadata) {
print('Token ID: $tokenMetadata.identifier');
print('Name: ${tokenMetadata.name}');
print('Ticker: ${tokenMetadata.ticker}');
print('Decimals: ${tokenMetadata.decimals}');
print('Max Supply: ${tokenMetadata.maxSupply}');
print('Is Freezable: ${tokenMetadata.isFreezable}');
}
try:
response = await sdk.get_tokens_metadata(
request=GetTokensMetadataRequest(
token_identifiers=["<token identifier 1>", "<token identifier 2>"]
)
)
tokens_metadata = response.tokens_metadata
for token_metadata in tokens_metadata:
print(f"Token ID: {token_metadata.identifier}")
print(f"Name: {token_metadata.name}")
print(f"Ticker: {token_metadata.ticker}")
print(f"Decimals: {token_metadata.decimals}")
print(f"Max Supply: {token_metadata.max_supply}")
print(f"Is Freezable: {token_metadata.is_freezable}")
except Exception as error:
logging.error(error)
raise
tokenIdentifiers := []string{"<token identifier 1>", "<token identifier 2>"}
response, err := sdk.GetTokensMetadata(breez_sdk_spark.GetTokensMetadataRequest{
TokenIdentifiers: tokenIdentifiers,
})
if sdkErr := err.(*breez_sdk_spark.SdkError); sdkErr != nil {
return err
}
tokensMetadata := response.TokensMetadata
for _, tokenMetadata := range tokensMetadata {
log.Printf("Token ID: %v", tokenMetadata.Identifier)
log.Printf("Name: %v", tokenMetadata.Name)
log.Printf("Ticker: %v", tokenMetadata.Ticker)
log.Printf("Decimals: %v", tokenMetadata.Decimals)
log.Printf("Max Supply: %v", tokenMetadata.MaxSupply)
log.Printf("Is Freezable: %v", tokenMetadata.IsFreezable)
}
Receiving a token payment API docs
Token payments use the same Spark address as Bitcoin payments - no separate address is required. Your application can retrieve the Spark address as described in the Receiving a payment guide. The payer will use this address to send tokens to the wallet.
Sending a token payment API docs
To send tokens, provide a Spark address as the payment request. The token identifier must be specified in one of two ways:
- Using a Spark invoice: If the payee provides a Spark address with an embedded token identifier and amount (a Spark invoice), the SDK automatically extracts and uses those values.
- Manual specification: For a plain Spark address without embedded payment details, your application must provide both the token identifier and amount parameters when preparing the payment.
Your application can use the parse functionality to determine if a Spark address contains embedded token payment details before preparing the payment.
The code example below demonstrates manual specification. Follow the standard prepare/send payment flow as described in the Sending a payment guide.
let payment_request = "<spark address>".to_string();
let token_identifier = Some("<token identifier>".to_string());
// Set the amount of tokens you wish to send
let amount = Some(1_000);
let prepare_response = sdk
.prepare_send_payment(PrepareSendPaymentRequest {
payment_request,
amount,
token_identifier,
})
.await?;
// If the fees are acceptable, continue to send the token payment
if let SendPaymentMethod::SparkAddress {
fee,
token_identifier: token_id,
..
} = &prepare_response.payment_method {
info!("Token ID: {:?}", token_id);
info!("Fees: {} token units", fee);
}
// Send the token payment
let send_response = sdk
.send_payment(SendPaymentRequest {
prepare_response,
options: None,
})
.await?;
let payment = send_response.payment;
info!("Payment: {payment:?}");
let paymentRequest = "<spark address>"
// The token identifier (e.g., asset ID or token contract)
let tokenIdentifier = "<token identifier>"
// Set the amount of tokens you wish to send (requires 'import BigNumber')
let amount = BInt(1_000)
let prepareResponse = try await sdk.prepareSendPayment(
request: PrepareSendPaymentRequest(
paymentRequest: paymentRequest,
amount: amount,
tokenIdentifier: tokenIdentifier
))
// If the fees are acceptable, continue to send the token payment
if case let .sparkAddress(address, fee, tokenId) = prepareResponse.paymentMethod {
print("Token ID: \(String(describing: tokenId))")
print("Fees: \(fee) sats")
}
// Send the token payment
let sendResponse = try await sdk.sendPayment(
request: SendPaymentRequest(
prepareResponse: prepareResponse,
options: nil
))
let payment = sendResponse.payment
print("Payment: \(payment)")
try {
val paymentRequest = "<spark address>"
// The token identifier (e.g., asset ID or token contract)
val tokenIdentifier = "<token identifier>"
// Set the amount of tokens you wish to send
// Kotlin MPP (BigInteger from com.ionspin.kotlin.bignum.integer, which is included in package)
val amount = BigInteger.fromLong(1_000L)
// Android (BigInteger from java.math)
// val amount = BigInteger.valueOf(1_000L) // Android (BigInteger from java.math)
val prepareResponse =
sdk.prepareSendPayment(
PrepareSendPaymentRequest(
paymentRequest = paymentRequest,
amount = amount,
tokenIdentifier = tokenIdentifier
)
)
// If the fees are acceptable, continue to send the token payment
when (val method = prepareResponse.paymentMethod) {
is SendPaymentMethod.SparkAddress -> {
println("Token ID: ${method.tokenIdentifier}")
println("Fees: ${method.fee} sats")
}
else -> {}
}
// Send the token payment
val sendResponse =
sdk.sendPayment(
SendPaymentRequest(prepareResponse = prepareResponse, options = null)
)
val payment = sendResponse.payment
println("Payment: $payment")
} catch (e: Exception) {
// handle error
}
const paymentRequest = '<spark address>'
// The token identifier (e.g., asset ID or token contract)
const tokenIdentifier = '<token identifier>'
// Set the amount of tokens you wish to send
const amount = BigInt(1_000)
const prepareResponse = await sdk.prepareSendPayment({
paymentRequest,
amount,
tokenIdentifier
})
// If the fees are acceptable, continue to send the token payment
if (prepareResponse.paymentMethod.type === 'sparkAddress') {
console.log(`Token ID: ${prepareResponse.paymentMethod.tokenIdentifier}`)
console.log(`Fees: ${prepareResponse.paymentMethod.fee} sats`)
}
// Send the token payment
const sendResponse = await sdk.sendPayment({
prepareResponse,
options: undefined
})
const payment = sendResponse.payment
console.log(`Payment: ${JSON.stringify(payment)}`)
const paymentRequest = '<spark address>'
// The token identifier (e.g., asset ID or token contract)
const tokenIdentifier = '<token identifier>'
// Set the amount of tokens you wish to send
const amount = BigInt(1_000)
const prepareResponse = await sdk.prepareSendPayment({
paymentRequest,
amount,
tokenIdentifier
})
// If the fees are acceptable, continue to send the token payment
if (prepareResponse.paymentMethod instanceof SendPaymentMethod.SparkAddress) {
console.log(`Token ID: ${prepareResponse.paymentMethod.inner.tokenIdentifier}`)
console.log(`Fees: ${prepareResponse.paymentMethod.inner.fee} sats`)
}
// Send the token payment
const sendResponse = await sdk.sendPayment({
prepareResponse,
options: undefined
})
const payment = sendResponse.payment
console.log(`Payment: ${JSON.stringify(payment)}`)
final paymentRequest = '<spark address>';
final tokenIdentifier = '<token identifier>';
// Set the amount of tokens you wish to send
final amount = BigInt.from(1000);
final prepareResponse = await sdk.prepareSendPayment(
request: PrepareSendPaymentRequest(
paymentRequest: paymentRequest,
amount: amount,
tokenIdentifier: tokenIdentifier,
),
);
// If the fees are acceptable, continue to send the token payment
if (prepareResponse.paymentMethod is SendPaymentMethod_SparkAddress) {
final method = prepareResponse.paymentMethod as SendPaymentMethod_SparkAddress;
print('Token ID: ${method.tokenIdentifier}');
print('Fees: ${method.fee} sats');
}
// Send the token payment
final sendResponse = await sdk.sendPayment(
request: SendPaymentRequest(
prepareResponse: prepareResponse,
options: null,
),
);
final payment = sendResponse.payment;
print('Payment: $payment');
try:
payment_request = "<spark address>"
# The token identifier (e.g., asset ID or token contract)
token_identifier = "<token identifier>"
# Set the amount of tokens you wish to send
amount = 1_000
prepare_response = await sdk.prepare_send_payment(
request=PrepareSendPaymentRequest(
payment_request=payment_request,
amount=amount,
token_identifier=token_identifier,
)
)
# If the fees are acceptable, continue to send the token payment
if isinstance(prepare_response.payment_method, SendPaymentMethod.SPARK_ADDRESS):
print(f"Token ID: {prepare_response.payment_method.token_identifier}")
print(f"Fees: {prepare_response.payment_method.fee} sats")
# Send the token payment
send_response = await sdk.send_payment(
request=SendPaymentRequest(
prepare_response=prepare_response,
options=None,
)
)
payment = send_response.payment
print(f"Payment: {payment}")
except Exception as error:
logging.error(error)
raise
paymentRequest := "<spark address>"
tokenIdentifier := "<token identifier>"
// Set the amount of tokens you wish to send
amount := new(big.Int).SetInt64(1_000)
prepareResponse, err := sdk.PrepareSendPayment(breez_sdk_spark.PrepareSendPaymentRequest{
PaymentRequest: paymentRequest,
Amount: &amount,
TokenIdentifier: &tokenIdentifier,
})
if sdkErr := err.(*breez_sdk_spark.SdkError); sdkErr != nil {
return err
}
// If the fees are acceptable, continue to send the token payment
switch method := prepareResponse.PaymentMethod.(type) {
case breez_sdk_spark.SendPaymentMethodSparkAddress:
log.Printf("Token ID: %v", method.TokenIdentifier)
log.Printf("Fees: %v sats", method.Fee)
}
// Send the token payment
sendResponse, err := sdk.SendPayment(breez_sdk_spark.SendPaymentRequest{
PrepareResponse: prepareResponse,
Options: nil,
})
if sdkErr := err.(*breez_sdk_spark.SdkError); sdkErr != nil {
return err
}
payment := sendResponse.Payment
log.Printf("Payment: %#v", payment)
Listing token payments API docs
Token payments are included in the regular payment history alongside Bitcoin payments. Your application can retrieve and distinguish token payments from other payment types using the standard payment listing functionality. See the Listing payments guide for more details.