Transaction proposal format (protobuf)
The transaction proposal is a protobuf 3 payload (package ledger_swap).
This page describes that payload; the message used depends on the flow TYPE:
| Flow | TYPE | Protobuf message |
|---|---|---|
| SWAP | SWAP_LEGACY/SWAP_NEW | NewTransactionResponse |
| SELL | SELL_LEGACY/SELL_NEW | NewSellResponse |
| FUND | FUND_LEGACY/FUND_NEW | NewFundResponse |
The canonical definition lives in
src/proto/protocol.proto,
and the per-field maximum sizes in
src/proto/protocol.options.
The .proto snippets below are included directly from that source file, so they always reflect the
current schema.
Which fields must be set
Protobuf 3 marks every field as optional on the wire, but the application enforces its own requirements: it consumes some fields for the signature, the address checks and the on-screen review, and rejects the flow if a required field is missing or inconsistent. The Requirement column of each table below states what the application actually expects. The values are:
- Required — the field must be set; omitting it makes the flow fail (rejected APDU, failed address/nonce check, or a wrong signature that the partner will not accept).
- Optional — safely omitted; used only for the on-screen review or passed through as a memo.
- Conditional — required only in specific cases; the row explains when.
SWAP: NewTransactionResponse
message NewTransactionResponse {
string payin_address = 1;
string payin_extra_id = 2;
bytes payin_extra_data = 13;
string refund_address = 3;
string refund_extra_id = 4;
string payout_address = 5;
string payout_extra_id = 6;
string currency_from = 7;
string currency_to = 8;
bytes amount_to_provider = 9;
bytes amount_to_wallet = 10;
string device_transaction_id = 11;
bytes device_transaction_id_ng = 12;
}
| Field | # | Type | Max size | Requirement | Description |
|---|---|---|---|---|---|
payin_address |
1 | string | 151 | Required | Address the FROM funds are sent to (the partner's deposit address). Signed by the FROM app. |
payin_extra_id |
2 | string | 20 | Conditional | Memo/tag/destination-tag for the payin address. Set at most one of payin_extra_id / payin_extra_data (see Extra id vs extra data). |
payin_extra_data |
13 | bytes | 37 | Conditional | Templated extra data blob, alternative to payin_extra_id (see Extra id vs extra data). |
refund_address |
3 | string | 151 | Required | Address the FROM funds are refunded to if the swap fails. Checked to belong to the device (mismatch is fatal). |
refund_extra_id |
4 | string | 20 | Optional | Memo/tag for the refund address, passed to the FROM app's address check. |
payout_address |
5 | string | 151 | Required | Address the TO funds are credited to. Checked to belong to the device (mismatch triggers the cross-seed warning). |
payout_extra_id |
6 | string | 20 | Optional | Memo/tag for the payout address, passed to the TO app's address check. |
currency_from |
7 | string | 10 | Required | Ticker of the FROM currency; must match the FROM coin configuration (see Currencies). |
currency_to |
8 | string | 10 | Required | Ticker of the TO currency; must match the TO coin configuration (see Currencies). |
amount_to_provider |
9 | bytes | 16 | Required | Amount of FROM sent to payin_address; signed by the FROM app (see Amounts). |
amount_to_wallet |
10 | bytes | 16 | Required | Amount of TO credited to payout_address; shown on screen (see Amounts). |
device_transaction_id |
11 | string | 11 | Conditional | Nonce, required for SWAP_LEGACY only: the 10-char string from START_NEW_TRANSACTION. |
device_transaction_id_ng |
12 | bytes | 32 | Conditional | Nonce, required for SWAP_NEW only: the 32-byte value from START_NEW_TRANSACTION. |
See Device transaction id for which nonce field to set per flow.
SELL: NewSellResponse
message NewSellResponse {
string trader_email = 1; // traderEmail
string in_currency = 2; // inCurrency
bytes in_amount = 3; // inAmount
string in_address = 4; // account
string in_extra_id = 8; // memo
string out_currency = 5; // outCurrency
UDecimal out_amount = 6; // outAmount
bytes device_transaction_id = 7; // nonce
}
Legacy (SELL_LEGACY) and new (SELL_NEW) flows use the exact same message and field requirements.
| Field | # | Type | Max size | Requirement | Description |
|---|---|---|---|---|---|
trader_email |
1 | string | 50 | Optional | Email of the trader (partner field traderEmail); shown on screen only. |
in_currency |
2 | string | 10 | Required | Ticker of the FROM currency; must match the coin configuration (partner field inCurrency). |
in_amount |
3 | bytes | 16 | Required | Amount of FROM sold; signed by the FROM app (partner field inAmount, see Amounts). |
in_address |
4 | string | 151 | Required | Address the FROM funds are sent to; signed by the FROM app (partner field account). |
in_extra_id |
8 | string | 20 | Optional | Memo/tag for in_address, passed to the FROM app (partner field memo). |
out_currency |
5 | string | 10 | Required | FIAT currency received (partner field outCurrency); shown on screen. |
out_amount |
6 | UDecimal |
— | Required | FIAT amount received (partner field outAmount); shown on screen (see UDecimal). |
device_transaction_id |
7 | bytes | 32 | Required | 32-byte nonce returned by START_NEW_TRANSACTION, checked by the application. |
FUND: NewFundResponse
message NewFundResponse {
string user_id = 1; // user ID
string account_name = 2; // funded account name ex: Card 1234
string in_currency = 3; // inCurrency
bytes in_amount = 4; // inAmount
string in_address = 5; // account
string in_extra_id = 7; // memo
bytes device_transaction_id = 6; // nonce
}
Legacy (FUND_LEGACY) and new (FUND_NEW) flows use the exact same message and field requirements.
| Field | # | Type | Max size | Requirement | Description |
|---|---|---|---|---|---|
user_id |
1 | string | 50 | Optional | Identifier of the funding user; shown on screen only. |
account_name |
2 | string | 50 | Optional | Human-readable name of the funded account (e.g. Card 1234); shown on screen only. |
in_currency |
3 | string | 10 | Required | Ticker of the FROM currency; must match the coin configuration (partner field inCurrency). |
in_amount |
4 | bytes | 16 | Required | Amount of FROM funded; signed by the FROM app (partner field inAmount, see Amounts). |
in_address |
5 | string | 151 | Required | Address the FROM funds are sent to; signed by the FROM app (partner field account). |
in_extra_id |
7 | string | 20 | Optional | Memo/tag for in_address, passed to the FROM app (partner field memo). |
device_transaction_id |
6 | bytes | 32 | Required | 32-byte nonce returned by START_NEW_TRANSACTION, checked by the application. |
Encoding notes
Field sizes
The Max size column of each table reports the value declared in
protocol.options,
which is the buffer size the application allocates for the field:
- For string fields the buffer includes the terminating null byte, so the maximum usable length
is one character less than the listed size (e.g.
payin_addressaccepts up to 150 characters,currency_fromup to 9, the SWAP_LEGACYdevice_transaction_idup to 10). - For bytes fields the listed size is the maximum number of bytes that can be sent.
Amounts
All amount fields carried as bytes (amount_to_provider, amount_to_wallet, in_amount) are
big-endian unsigned integers expressed in the currency's smallest unit (e.g. wei, satoshi), with
a maximum of 16 bytes. The application trims leading zero bytes on reception, so 0x00 0x2A and
0x2A are equivalent.
UDecimal
The SELL out_amount uses the UDecimal helper message, which represents a decimal value as:
The value is coefficient * 10^(-exponent), where coefficient is a big-endian unsigned integer
(max 16 bytes) and exponent a uint32. For example a coefficient of 12345 with exponent 2
represents 123.45.
Device transaction id
The nonce returned by START_NEW_TRANSACTION must be echoed back in the proposal, and the
application rejects the flow (WRONG_TRANSACTION_ID) if it does not match. Which field carries it,
and its form, depends on the flow — this is the only field-level difference between a legacy flow and
its NG counterpart:
| Flow | Field | Type | Value |
|---|---|---|---|
| SWAP_LEGACY | device_transaction_id |
string | 10-char string |
| SWAP_NEW | device_transaction_id_ng |
bytes | 32-byte value |
| SELL_LEGACY / SELL_NEW | device_transaction_id |
bytes | 32-byte value |
| FUND_LEGACY / FUND_NEW | device_transaction_id |
bytes | 32-byte value |
Extra id vs extra data
For SWAP flows, payin_extra_id and payin_extra_data are mutually exclusive — providing both
is rejected (WRONG_EXTRA_ID_OR_EXTRA_DATA). Setting neither is valid and means the payin address
needs no extra id.
payin_extra_idis a plain string memo/tag/destination-tag (e.g. XRP tag, Cosmos memo).-
payin_extra_datais a templated binary blob whose first byte selects the template:Header byte Template Total size 0x00NATIVE 1 byte 0x01EVM_CALLDATA 33 bytes (header + 32-byte hash) 0x02OP_RETURN 33 bytes (header + 32-byte hash) 0x03SOL_TEMPLATE 37 bytes (header + 32-byte hash + 4-byte template id) The application enforces the size matching the header byte. A lone
0x00byte (NATIVE) is equivalent to sending no extra data.payin_extra_datais not supported on Nano S.
Currencies
The crypto ticker fields — currency_from and currency_to (SWAP), and in_currency (SELL and
FUND) — are upper-cased by the application and mapped to the canonical Ledger currency name before
use. The SELL out_currency (a FIAT ticker) is displayed as received, without normalization.