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]>,
"method": "onchain",
"expiry": <int|null>,
"pubkey": <str>,
"amount_paid": <int>,
"amount_issued": <int>,
"updated_at": <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 quoteupdated_atis defined in NUT-04
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": "019e6d5a-2347-7000-8850-39c85ed1b5d3",
"request": "bc1q...",
"unit": "sat",
"method": "onchain",
"expiry": 1701704757,
"pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
"amount_paid": 0,
"amount_issued": 0,
"updated_at": 1701704657
}
Check mint quote:
curl -X GET http://localhost:3338/v1/mint/quote/onchain/019e6d5a-2347-7000-8850-39c85ed1b5d3
Minting Tokens
The quote accounting 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": "019e6d5a-2347-7000-8850-39c85ed1b5d3",
"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]>,
"method": "onchain",
"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]>,
"method": "onchain",
"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": "019e6d5a-2347-7000-83ad-72a9a83f3105",
"amount": 100000,
"unit": "sat",
"method": "onchain",
"state": "UNPAID",
"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": null,
"outpoint": null
}
Check quote state:
curl -X GET http://localhost:3338/v1/melt/quote/onchain/019e6d5a-2347-7000-83ad-72a9a83f3105
Melt request:
curl -X POST https://mint.host:3338/v1/melt/onchain -d \
'{
"quote": "019e6d5a-2347-7000-83ad-72a9a83f3105",
"fee_index": 1,
"inputs": [...],
"outputs": [
{
"amount": 1,
"id": "009a1f293253e41e",
"B_": "03327fc4fa333909b70f08759e217ce5c94e6bf1fc2382562f3c560c5580fa69f4"
}
]
}'
Pending melt response:
{
"quote": "019e6d5a-2347-7000-83ad-72a9a83f3105",
"amount": 100000,
"unit": "sat",
"method": "onchain",
"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": "019e6d5a-2347-7000-83ad-72a9a83f3105",
"amount": 100000,
"unit": "sat",
"method": "onchain",
"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>
}