Error responses for applications started by Exchange in SWAP context
This specification applies to the error responses returned by the Coin applications when started by Exchange for the final payment transaction of a SWAP.
Replying valuable data when a final payment transaction is refused eases a lot the analysis, especially if the issue happens in production context and/or is hard to reproduce.
RAPDU status word
Each application must define a unique status word for every Exchange-related error.
RAPDU data
The first 2 bytes of the RAPDU data represent the error code. Format is 16 bits integer in big endian.
The upper byte is common between all applications. It must be one of the following value:
| Name | Value | Description |
|---|---|---|
| ERROR_INTERNAL | 0x00 | Internal application error, forward to the Firmware team for analysis. |
| ERROR_WRONG_AMOUNT | 0x01 | The amount does not match the one validated in Exchange. |
| ERROR_WRONG_DESTINATION | 0x02 | The destination address does not match the one validated in Exchange. |
| ERROR_WRONG_FEES | 0x03 | The fees are different from what was validated in Exchange. |
| ERROR_WRONG_METHOD | 0x04 | The method used is invalid in Exchange context. |
| ERROR_CROSSCHAIN_WRONG_MODE | 0x05 | The mode used for the cross-chain hash validation is not supported. |
| ERROR_CROSSCHAIN_WRONG_METHOD | 0x06 | The method used is invalid in cross-chain Exchange context. |
| ERROR_CROSSCHAIN_WRONG_HASH | 0x07 | The hash for the cross-chain transaction does not match the validated value. |
| ERROR_GENERIC | 0xFF | A generic or unspecified error not covered by the specific error codes above. Refer to the remaining bytes for further details on the error. |
The lower byte can be set by the application to refine the error code returned.
So the error code for ERROR_WRONG_METHOD would be 0x04XX with XX being application specific
(can be 00 if there is nothing to refine).
The remaining bytes of the data are application-specific and can include, but are not limited to:
- Debugging information (e.g., error logs or internal state).
- Field values (e.g., expected vs actual amounts, destination, fees).
- More specific error codes tailored to the application's context.
C Helpers API
The standard application library define several helper function to return error codes from the Coin application.
ledger-secure-sdk/lib_standard_app/swap_error_code_helpers.h
/**
* Sends a basic swap error with no extra data.
*
* @param status_word RAPDU status word.
* @param common_error_code Common error code defined in swap_error_common_code_t.
* @param application_specific_error_code Application-specific error code.
*/
__attribute__((noreturn)) void send_swap_error_simple(uint16_t status_word,
uint8_t common_error_code,
uint8_t application_specific_error_code);
/**
* Sends a swap error with one additional buffer data.
*
* @param status_word RAPDU status word.
* @param common_error_code Common error code.
* @param application_specific_error_code Application-specific error code.
* @param buffer_data Additional application-specific error details.
*/
__attribute__((noreturn)) void send_swap_error_with_buffer(uint16_t status_word,
uint8_t common_error_code,
uint8_t application_specific_error_code,
const buffer_t buffer_data);
/**
* Sends a swap error with multiple buffers containing error details as data.
*
* @param status_word RAPDU status word.
* @param common_error_code Common error code.
* @param application_specific_error_code Application-specific error code.
* @param buffer_data Array of buffers with error details. SWAP_ERROR_HELPER_MAX_BUFFER_COUNT
* @param count Number of buffers provided.
*/
#define SWAP_ERROR_HELPER_MAX_BUFFER_COUNT 8
__attribute__((noreturn)) void send_swap_error_with_buffers(uint16_t status_word,
uint8_t common_error_code,
uint8_t application_specific_error_code,
const buffer_t *buffer_data,
size_t count);
/**
* Macro to send a swap error with a formatted string as data.
*
* Constructs a buffer from a formatted string and passes it to send_swap_error_with_buffers.
* @param status_word RAPDU status word.
* @param common_error_code Common error code.
* @param application_specific_error_code Application-specific error code.
* @param format printf-style format string.
* @param ... Additional arguments for formatting.
*/
// Immediately call snprintf here (no function wrapping it cleanly in a .c file).
// This is because we don't have a vsnprintf implementation which would be needed if
// we were to pass the va_args to an intermediate function.
// See https://stackoverflow.com/a/150578
#define send_swap_error_with_string( \
status_word, common_error_code, application_specific_error_code, format, ...) \
do { \
/* Up to a full data apdu minus the status word and the swap error code */ \
char format_buffer[sizeof(G_io_apdu_buffer) - sizeof(status_word) - 2] = {0}; \
/* snprintf always returns 0 on our platform, don't check the return value */ \
/* See https://github.com/LedgerHQ/ledger-secure-sdk/issues/236 */ \
snprintf(format_buffer, sizeof(format_buffer), format, ##__VA_ARGS__); \
PRINTF("send_swap_error_with_string %s\n", format_buffer); \
buffer_t string_buffer; \
string_buffer.ptr = (uint8_t *) &format_buffer; \
string_buffer.size = strnlen(format_buffer, sizeof(format_buffer)); \
string_buffer.offset = 0; \
send_swap_error_with_buffers( \
status_word, common_error_code, application_specific_error_code, &string_buffer, 1); \
} while (0)
Example of API usage in C Boilerplate
app-boilerplate/src/swap/handle_swap_sign_transaction.c
/* Check if the Tx to sign have the same parameters as the ones previously validated */
bool swap_check_validity(uint64_t amount,
uint64_t fee,
const uint8_t* destination,
const token_info_t* token_info) {
PRINTF("Inside swap_check_validity\n");
if (!G_swap_validated.initialized) {
PRINTF("Swap structure is not initialized\n");
send_swap_error_simple(SW_SWAP_FAIL, SWAP_EC_ERROR_GENERIC, SWAP_ERROR_CODE);
// unreachable
os_sched_exit(0);
}
// Reject token transactions in swap context
if (G_context.tx_info.is_token_tx) {
if (is_token_swap()) {
// Check that the token is the expected one
if (strcmp(G_swap_validated.ticker, token_info->ticker) != 0 ||
G_swap_validated.decimals != token_info->decimals) {
PRINTF("Token info does not match\n");
PRINTF("Validated: %s (decimals: %d)\n",
G_swap_validated.ticker,
G_swap_validated.decimals);
PRINTF("Received: %s (decimals: %d)\n", token_info->ticker, token_info->decimals);
send_swap_error_simple(SW_SWAP_FAIL,
SWAP_EC_ERROR_WRONG_AMOUNT,
SWAP_ERROR_WRONG_TOKEN_INFO);
// unreachable
os_sched_exit(0);
} else {
PRINTF("Token info match\n");
}
} else {
PRINTF("Unexpected token transaction from swap context\n");
send_swap_error_simple(SW_SWAP_FAIL, SWAP_EC_ERROR_WRONG_METHOD, SWAP_ERROR_CODE);
// unreachable
os_sched_exit(0);
}
} else if (is_token_swap()) {
PRINTF("Token transactions expected from swap context\n");
send_swap_error_simple(SW_SWAP_FAIL, SWAP_EC_ERROR_WRONG_METHOD, SWAP_ERROR_CODE);
// unreachable
os_sched_exit(0);
}
if (G_swap_validated.amount != amount) {
PRINTF("Amount does not match, promised %lld, received %lld\n",
G_swap_validated.amount,
amount);
send_swap_error_simple(SW_SWAP_FAIL, SWAP_EC_ERROR_WRONG_AMOUNT, SWAP_ERROR_CODE);
// unreachable
os_sched_exit(0);
} else {
PRINTF("Amounts match \n");
}
if (G_swap_validated.fee != fee) {
PRINTF("Fee does not match, promised %lld, received %lld\n", G_swap_validated.fee, fee);
send_swap_error_simple(SW_SWAP_FAIL, SWAP_EC_ERROR_WRONG_FEES, SWAP_ERROR_CODE);
// unreachable
os_sched_exit(0);
} else {
PRINTF("Fees match \n");
}
char to[ADDRESS_LEN * 2 + 1] = {0};
format_hex(destination, ADDRESS_LEN, to, sizeof(to));
if (strcmp(G_swap_validated.recipient, to) != 0) {
PRINTF("Destination does not match\n");
PRINTF("Validated: %s\n", G_swap_validated.recipient);
PRINTF("Received: %s \n", to);
send_swap_error_simple(SW_SWAP_FAIL, SWAP_EC_ERROR_WRONG_DESTINATION, SWAP_ERROR_CODE);
// unreachable
os_sched_exit(0);
} else {
PRINTF("Destination is valid\n");
}
return true;
}
Rust Helpers API
ledger-device-rust-sdk/ledger_device_sdk/src/libcall/swap.rs
/// Common swap error codes for Exchange integration.
///
/// These error codes are standardized across all Ledger applications to ensure
/// consistent error reporting when called by the Exchange app during swap transactions.
///
/// The upper byte of the 2-byte error code must be one of these values. The lower byte
/// can be set by the application to provide additional refinement.
///
/// This enum matches the C SDK definition in `swap_error_code_helpers.h`.
///
/// # Error Response Format
///
/// When returning an error in swap context, the RAPDU data should begin with:
/// - **Byte 0**: One of these common error codes (upper byte)
/// - **Byte 1**: Application-specific error code (lower byte, can be 0x00)
/// - **Remaining bytes**: Optional error details (messages, field values, etc.)
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SwapErrorCommonCode {
/// Internal application error.
///
/// Forward to the Firmware team for analysis.
ErrorInternal = 0x00,
/// The amount does not match the one validated in Exchange.
///
/// Use when the transaction amount differs from what the user approved in Exchange.
ErrorWrongAmount = 0x01,
/// The destination address does not match the one validated in Exchange.
///
/// Use when the transaction destination differs from the Exchange-validated address.
ErrorWrongDestination = 0x02,
/// The fees are different from what was validated in Exchange.
///
/// Use when transaction fees don't match Exchange expectations.
ErrorWrongFees = 0x03,
/// The method used is invalid in Exchange context.
///
/// Use when an unsupported transaction method/type is encountered.
ErrorWrongMethod = 0x04,
/// The mode used for the cross-chain hash validation is not supported.
///
/// Only relevant for applications that handle cross chain swap, not all applications.
ErrorCrosschainWrongMode = 0x05,
/// The method used is invalid in cross-chain Exchange context.
///
/// Only relevant for applications that handle cross chain swap, not all applications.
ErrorCrosschainWrongMethod = 0x06,
/// The hash for the cross-chain transaction does not match the validated value.
///
/// Only relevant for applications that handle cross chain swap, not all applications.
ErrorCrosschainWrongHash = 0x07,
/// A generic or unspecified error not covered by specific error codes.
///
/// Refer to the remaining bytes of the RAPDU data for further details.
/// Use this when the error doesn't fit into any of the above categories.
ErrorGeneric = 0xFF,
}
/// Trait for application-specific swap error codes.
///
/// This trait must be implemented by application-defined error code enums
/// to allow them to be used with [`SwapError`]. The trait ensures the error
/// code can be converted to a u8 for the APDU response.
///
/// # Example
///
/// ```rust,ignore
/// #[repr(u8)]
/// #[derive(Clone, Copy)]
/// pub enum MyAppErrorCode {
/// Default = 0x00,
/// SpecialCase = 0x01,
/// }
///
/// impl SwapAppErrorCodeTrait for MyAppErrorCode {
/// fn as_u8(self) -> u8 {
/// self as u8
/// }
/// }
/// ```
pub trait SwapAppErrorCodeTrait: Copy {
/// Convert the error code to a u8 byte value.
fn as_u8(self) -> u8;
}
/// Swap error containing the 2-byte error code and optional descriptive message.
///
/// This structure encapsulates the complete error information for swap failures:
/// - Upper byte: Common error code from [`SwapErrorCommonCode`]
/// - Lower byte: Application-specific error code (must implement [`SwapAppErrorCodeTrait`])
/// - Message: Optional human-readable error description with actual values
///
/// # Usage
///
/// The error bytes should be prepended to the APDU response before the optional message:
/// ```rust,ignore
/// comm.append(&[error.common_code as u8, error.app_code.as_u8()]);
/// if let Some(ref msg) = error.message {
/// comm.append(msg.as_bytes());
/// }
/// comm.reply(sw);
/// ```
///
/// # Generic Parameter
///
/// * `T` - Application-specific error code type implementing [`SwapAppErrorCodeTrait`]
pub struct SwapError<T: SwapAppErrorCodeTrait> {
/// Common error code (upper byte) from SDK
pub common_code: SwapErrorCommonCode,
/// Application-specific error code (lower byte)
pub app_code: T,
/// Optional descriptive error message with actual values for debugging
pub message: Option<alloc::string::String>,
}
impl<T: SwapAppErrorCodeTrait> SwapError<T> {
/// Create a new SwapError with a formatted message.
pub fn with_message(
common_code: SwapErrorCommonCode,
app_code: T,
message: alloc::string::String,
) -> Self {
Self {
common_code,
app_code,
message: Some(message),
}
}
/// Create a new SwapError without a message.
pub fn without_message(common_code: SwapErrorCommonCode, app_code: T) -> Self {
Self {
common_code,
app_code,
message: None,
}
}
/// Append this swap error to the communication buffer in the standard format.
///
/// Appends the 2-byte error code followed by the optional message string.
/// This ensures all applications format swap errors consistently.
///
/// # Format
///
/// The data appended to the communication buffer:
/// - Byte 0: Common error code from [`SwapErrorCommonCode`]
/// - Byte 1: Application-specific error code
/// - Bytes 2+: Optional UTF-8 encoded error message (if present)
///
/// # Arguments
///
/// * `comm` - Mutable reference to the communication buffer
///
/// # Returns
///
/// The 2-byte error code as `[u8; 2]` for logging/debugging purposes.
///
/// # Example
///
/// ```rust,ignore
/// if let Err(error) = check_swap_params(params, &tx) {
/// error.append_to_comm(comm);
/// return Err(AppSW::SwapFail);
/// }
/// ```
pub fn append_to_comm(&self, comm: &mut crate::io::Comm) -> [u8; 2] {
let error_bytes = [self.common_code as u8, self.app_code.as_u8()];
comm.append(&error_bytes);
if let Some(ref msg) = self.message {
comm.append(msg.as_bytes());
}
error_bytes
}
}
Example of API usage in Rust Boilerplate
app-boilerplate-rust/src/swap.rs
if tx.value != swap_amount {
debug_print("Swap amount mismatch\n");
debug_u64("Tx: ", tx.value);
debug_u64("Swap: ", swap_amount);
// Error detected, we return the error with detailed message in common SDK defined format
return Err(SwapError::with_message(
SwapErrorCommonCode::ErrorWrongAmount,
SwapAppErrorCode::Default,
format!("Amount tx {} != swap {}", tx.value, swap_amount),
));
}
app-boilerplate-rust/src/handlers/sign_tx.rs
if let Err(error) = crate::swap::check_swap_params(params, &tx) {
// The swap validation failed and returned us the common format error defined by the SDK
// Use SDK method to append error code and message in standard format
error.append_to_comm(comm);
Err(AppSW::SwapFail)
} else {
debug_print("Swap validation success, bypassing UI\n");
compute_signature_and_append(comm, ctx)
}