NUT-30: Payment Method: Onchain¶
optional
depends on: NUT-04 NUT-05 NUT-20
This document describes minting and melting ecash with the onchain payment method, which uses Bitcoin onchain payments. It is an extension of NUT-04 and NUT-05 which cover the protocol steps of minting and melting ecash shared by any supported payment method.
Mint Quote¶
For the onchain method, the wallet includes the following specific PostMintQuoteOnchainRequest data:
{
"unit": <str_enum[UNIT]>,
"pubkey": <str>
}
Note: A NUT-20
pubkeyis required in this NUT and the mint MUST NOT issue a mint quote if one is not included.
The mint responds with a PostMintQuoteOnchainResponse:
{
"quote": <str>,
"request": <str>,
"unit": <str_enum[UNIT]>,
"expiry": <int|null>,
"pubkey": <str>,
"amount_paid": <int>,
"amount_issued": <int>
}
Where:
quoteis the quote IDrequestis the Bitcoin address to send funds toexpiryis the Unix timestamp until which the mint quote is validpubkeyis the public key from the requestamount_paidis the total confirmed amount paid to the request in UTXOs that are eligible for mintingamount_issuedis the amount of ecash that has been issued for the given mint quote
If expiry is not null, the wallet SHOULD NOT send payments to the request after expiry. Mints MUST keep monitoring transactions they detected before expiry until the transaction reaches the required number of confirmations or is evicted or replaced. Payments first detected by the mint after expiry MUST NOT increase amount_paid.
Example¶
Request with curl:
curl -X POST http://localhost:3338/v1/mint/quote/onchain -d \
'{"unit": "sat", "pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac"}' \
-H "Content-Type: application/json"
Response:
{
"quote": "DSGLX9kevM...",
"request": "bc1q...",
"unit": "sat",
"expiry": 1701704757,
"pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
"amount_paid": 0,
"amount_issued": 0
}
Check quote state:
curl -X GET http://localhost:3338/v1/mint/quote/onchain/DSGLX9kevM...
Minting Tokens¶
The quote state will only update to show amount_paid once a Bitcoin transaction has reached the minimum number of confirmations specified in the mint's settings.
If the onchain mint method has a min_amount setting, each UTXO paid to the quote address is evaluated independently against min_amount. UTXOs with an amount less than min_amount MUST NOT increase amount_paid and MUST NOT count towards the mintable balance for the quote. Multiple UTXOs below min_amount MUST NOT be aggregated to reach min_amount.
For the onchain method, the wallet includes the following specific PostMintOnchainRequest data:
{
"quote": <str>,
"outputs": <Array[BlindedMessage]>,
"signature": <str>
}
Since onchain mint quotes require a pubkey, the wallet MUST include a NUT-20 signature in the mint request.
Minting tokens:
curl -X POST https://mint.host:3338/v1/mint/onchain -H "Content-Type: application/json" -d \
'{
"quote": "DSGLX9kevM...",
"outputs": [
{
"amount": 8,
"id": "009a1f293253e41e",
"B_": "035015e6d7ade60ba8426cefaf1832bbd27257636e44a76b922d78e79b47cb689d"
},
{
"amount": 2,
"id": "009a1f293253e41e",
"B_": "0288d7649652d0a83fc9c966c969fb217f15904431e61a44b14999fabc1b5d9ac6"
}
],
"signature": "f2a1..."
}'
Response:
{
"signatures": [
{
"id": "009a1f293253e41e",
"amount": 2,
"C_": "0224f1c4c564230ad3d96c5033efdc425582397a5a7691d600202732edc6d4b1ec"
},
{
"id": "009a1f293253e41e",
"amount": 8,
"C_": "0277d1de806ed177007e5b94a8139343b6382e472c752a74e99949d511f7194f6c"
}
]
}
Multiple Deposits¶
Onchain addresses can receive multiple payments, allowing the wallet to mint multiple times for one quote. The wallet can call the check onchain endpoint, where the mint will return the PostMintQuoteOnchainResponse including amount_paid and amount_issued. The difference between these values represents how much the wallet can mint by calling the mint endpoint. Wallets MAY mint any amount up to this available difference; in particular, they can mint less than the amount mintable. Mints MUST accept mint requests whose total output amount is less than or equal to (amount_paid - amount_issued).
Only eligible UTXOs increase amount_paid. If a quote receives both eligible and ineligible UTXOs, only the eligible UTXOs count towards amount_paid and mintable balance. UTXOs that do not increase amount_paid are not recoverable through the mint quote protocol.
Mint Settings¶
A confirmations option SHOULD be set to indicate the minimum depth in the blockchain for a transaction to be considered confirmed.
For the onchain mint method, min_amount indicates both the minimum mint operation amount and the minimum amount of an individual UTXO that the mint will credit to amount_paid. Wallets SHOULD NOT send onchain payments below min_amount to a quote address.
Example MintMethodSetting¶
{
"method": "onchain",
"unit": <str>,
"min_amount": <int|null>,
"max_amount": <int|null>,
"options": {
"confirmations": <int>
}
}
Melt Quote¶
For the onchain method, the wallet includes the following specific PostMeltQuoteOnchainRequest data:
{
"request": <str>,
"unit": <str_enum[UNIT]>,
"amount": <int>
}
Where:
requestis the Bitcoin address that should receive the onchain paymentunitis the unit the wallet would like to pay withamountis the amount to send in the specified unit
Unlike other melt methods, a single onchain melt quote can contain multiple fee options for the same payment. This allows the wallet to choose between different fee and confirmation estimates while preserving a single quote ID.
The mint responds with a PostMeltQuoteOnchainResponse:
{
"quote": <str>,
"amount": <int>,
"unit": <str_enum[UNIT]>,
"state": <str_enum[STATE]>,
"expiry": <int>,
"request": <str>,
"fee_options": [
{
"fee_index": <int>,
"fee_reserve": <int>,
"estimated_blocks": <int>
}
],
"selected_fee_index": <int|null>,
"outpoint": <str|null>
}
Each item in fee_options represents one available fee reserve and confirmation estimate for the same payment. The wallet selects one of these options when executing the melt quote by including the option's fee_index value in the melt request. The mint MUST return at least one fee_options item. The returned fee_options are fixed for the lifetime of the quote.
For each fee option with fee_index, fee_reserve is the maximum onchain transaction fee the mint may charge for that option, and estimated_blocks is the estimated number of blocks until confirmation. selected_fee_index is null before the quote is executed and is set by the mint to the selected fee option once the wallet executes the quote. The mint expects the wallet to include Proofs of at least total_amount = amount + selected_fee_reserve + input_fee where selected_fee_reserve is the fee_reserve from the selected fee_index item and input_fee is calculated from the keyset's input_fee_ppk as described in NUT-02. If the mint does not claim the full selected_fee_reserve as the actual fee, the mint returns the unclaimed amount as change to the wallet as described in NUT-08.
state is an enum string field with possible values "UNPAID", "PENDING", "PAID":
"UNPAID"means that the transaction has not been broadcast yet."PENDING"means that the transaction is being processed by the mint but has not reached the required number of confirmations."PAID"means that the transaction has been mined and confirmed.
outpoint is the transaction ID and output index of the payment in the format txid:vout, present once the transaction has been broadcast.
Melting Tokens¶
Onchain melt requests are always asynchronous. The mint MUST return a "PENDING" state after validating the melt request and then broadcast the Bitcoin transaction in the background. The wallet MUST monitor the quote state through the check quote endpoint until the transaction reaches the required number of confirmations.
For the onchain method, the wallet includes the following specific PostMeltOnchainRequest data. The wallet can include an optional outputs field in the melt request to receive change for overpaid onchain fees (see NUT-08):
{
"quote": <str>,
"fee_index": <int>,
"inputs": <Array[Proof]>,
"outputs": <Array[BlindedMessage]> // Optional
}
Where fee_index is the selected fee of the quote's fee_options. The mint MUST reject a melt request with a fee_index that was not returned in the quote. Once selected_fee_index is set, the mint MUST NOT execute the quote again with a different fee_index value.
If the outputs field is included and the mint does not claim the full selected_fee_reserve as the actual fee, the mint will respond with a change field containing blind signatures for the unclaimed amount (see NUT-08). The change field is omitted if there are no blind signatures to return. Note that because onchain transactions may be batched or have unpredictable costs, the mint is entitled to claim the full selected_fee_reserve as the actual fee.
{
"quote": <str>,
"amount": <int>,
"unit": <str_enum[UNIT]>,
"state": <str_enum[STATE]>,
"expiry": <int>,
"request": <str>,
"fee_options": [
{
"fee_index": <int>,
"fee_reserve": <int>,
"estimated_blocks": <int>
}
],
"selected_fee_index": <int>,
"outpoint": <str|null>,
"change": <Array[BlindSignature]> // Optional; present if outputs were included and there's change
}
Example¶
Melt quote request:
curl -X POST https://mint.host:3338/v1/melt/quote/onchain -d \
'{"request": "bc1q...", "unit": "sat", "amount": 100000}'
Melt quote response:
{
"quote": "TRmjduhIsPxd...",
"amount": 100000,
"unit": "sat",
"state": "UNPAID",
"expiry": 1701704757,
"request": "bc1q...",
"fee_options": [
{
"fee_reserve": 5000,
"estimated_blocks": 1
},
{
"fee_reserve": 2000,
"estimated_blocks": 6
},
{
"fee_reserve": 800,
"estimated_blocks": 144
}
],
"selected_fee_index": null,
"outpoint": null
}
Check quote state:
curl -X GET http://localhost:3338/v1/melt/quote/onchain/TRmjduhIsPxd...
Melt request:
curl -X POST https://mint.host:3338/v1/melt/onchain -d \
'{
"quote": "TRmjduhIsPxd...",
"fee_index": 1,
"inputs": [...],
"outputs": [
{
"amount": 1,
"id": "009a1f293253e41e",
"B_": "03327fc4fa333909b70f08759e217ce5c94e6bf1fc2382562f3c560c5580fa69f4"
}
]
}'
Pending melt response:
{
"quote": "TRmjduhIsPxd...",
"amount": 100000,
"unit": "sat",
"state": "PENDING",
"expiry": 1701704757,
"request": "bc1q...",
"fee_options": [
{
"fee_index": 0,
"fee_reserve": 5000,
"estimated_blocks": 1
},
{
"fee_index": 1,
"fee_reserve": 2000,
"estimated_blocks": 6
},
{
"fee_index": 2,
"fee_reserve": 800,
"estimated_blocks": 144
}
],
"selected_fee_index": 1,
"outpoint": null
}
The wallet selects one of the returned fee_options by including that option's fee_index value in the melt request. Once the quote is executed, quote state checks return the same response shape with selected_fee_index set to the selected value and outpoint set once the transaction has been broadcast.
Paid quote state response:
{
"quote": "TRmjduhIsPxd...",
"amount": 100000,
"unit": "sat",
"state": "PAID",
"expiry": 1701704757,
"request": "bc1q...",
"fee_options": [
{
"fee_index": 0,
"fee_reserve": 5000,
"estimated_blocks": 1
},
{
"fee_index": 1,
"fee_reserve": 2000,
"estimated_blocks": 6
},
{
"fee_index": 2,
"fee_reserve": 800,
"estimated_blocks": 144
}
],
"selected_fee_index": 1,
"outpoint": "4d5e6f...:0",
"change": [
{
"id": "009a1f293253e41e",
"amount": 1000,
"C_": "03c668f551855ddc792e22ea61d32ddfa6a45b1eb659ce66e915bf5127a8657be0"
}
]
}
Example MeltMethodSetting¶
{
"method": "onchain",
"unit": <str>,
"min_amount": <int|null>,
"max_amount": <int|null>
}