Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.pre-commit-config.yaml
172 changes: 172 additions & 0 deletions 26.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# NUT-26: Payment Request Bech32m Encoding

`optional` `depends on: NUT-18`

---

This specification defines an alternative encoding format for Payment Requests using Bech32m encoding with TLV (Tag-Length-Value) serialization. This format provides better QR code compatibility and typically 30-60% size reduction compared to the CBOR+base64 encoding defined in NUT-18.

## Encoded Request Format

Payment requests are serialized using TLV encoding, then encoded with Bech32m:

`"creqb" + "1" + bech32m(TLV(PaymentRequest))`

The human-readable part (HRP) is `"creqb"` and the version separator is `"1"`. The data payload is TLV-encoded as described below, then encoded with Bech32m (not standard Bech32).

> [!NOTE]
> Implementations SHOULD output uppercase Bech32m strings for optimal QR code compatibility. Uppercase alphanumeric characters use QR "alphanumeric mode" which is more space-efficient than "byte mode" required for mixed-case. Decoders MUST accept both uppercase and lowercase input.

When parsing a `creq` parameter, implementations SHOULD support both formats:

1. If the parameter starts with `creqA` (case-insensitive), parse as NUT-18 CBOR+base64 format
2. If the parameter is valid Bech32m with HRP `creqb`, parse as NUT-26 format
3. Otherwise, return an error

## TLV Structure

The payment request is encoded as a sequence of TLV fields. Each TLV entry consists of:

- **Type** (1 byte): Field identifier
- **Length** (2 bytes, big-endian): Length of value in bytes
- **Value** (variable): Field data

### Top-Level TLV Tags

| Tag | Field | Type | Description |
| ---- | ----------- | --------- | -------------------------------------------------------------------- |
| 0x01 | id | string | Payment identifier (corresponds to `i` in JSON) |
| 0x02 | amount | u64 | Amount in base units (corresponds to `a` in JSON) |
| 0x03 | unit | u8/string | Currency unit (corresponds to `u` in JSON) |
| 0x04 | single_use | u8 | Single-use flag: 0=false, 1=true (corresponds to `s` in JSON) |
| 0x05 | mint | string | Mint URL (repeatable for multiple mints, corresponds to `m` in JSON) |
| 0x06 | description | string | Human-readable description (corresponds to `d` in JSON) |
| 0x07 | transport | sub-TLV | Transport configuration (repeatable, corresponds to `t` in JSON) |
| 0x08 | nut10 | sub-TLV | NUT-10 spending conditions (corresponds to `nut10` in JSON) |

All fields are optional. Unknown tags MUST be ignored to maintain forward compatibility.

### Unit Encoding (Tag 0x03)

The unit field uses a compact encoding:

- **Value 0x00**: Represents `sat` (Bitcoin satoshis)
- **String value**: Any other unit is encoded as a UTF-8 string (e.g., `"msat"`, `"usd"`, `"eur"`)

### Transport Sub-TLV (Tag 0x07)

Transport configurations are encoded as nested TLV structures. Each transport has the following sub-tags:

| Sub-Tag | Field | Type | Description |
| ------- | --------- | ----------- | ------------------------------------------------- |
| 0x01 | kind | u8 | Transport type: 0=nostr, 1=http_post |
| 0x02 | target | bytes | Transport target (interpretation depends on kind) |
| 0x03 | tag_tuple | sub-sub-TLV | Generic tag tuple (repeatable) |

#### Transport Type Mapping

The kind field (sub-tag 0x01) identifies the transport method. The following transport types are defined:

| Kind Value | Transport Type | Description | Target Format |
| ---------- | -------------- | -------------------------------------- | ------------------------------------- |
| 0x00 | nostr | Nostr-based transport using NIP-04 DMs | 32-byte X-only public key (raw bytes) |
| 0x01 | http_post | HTTP POST to specified URL | UTF-8 encoded URL string |

> [!NOTE]
> If no transport is specified (tag 0x07 is absent), the payment is assumed to be in-band, consistent with NUT-18 semantics.

**JSON Representation:**

In the NUT-18 JSON format, transports are represented with a `type` field:

```json
{
"t": [
{ "type": "nostr", "target": "npub1...", "tags": [["n", "17"]] },
{ "type": "post", "target": "https://callback.example.com/pay" }
]
}
```

When encoding to TLV, the `type` string is converted to the corresponding numeric kind value.

#### Transport Target Encoding (Sub-Tag 0x02)

The target field is interpreted based on the transport kind:

- **kind=0 (nostr)**: 32-byte X-only public key (raw bytes, not bech32-encoded)
- **kind=1 (http_post)**: UTF-8 encoded URL string

#### Nostr Transport Details

For Nostr transports (`kind=0`), the target field contains the raw 32-byte X-only public key (not bech32-encoded). NIPs and relay URLs are encoded using generic tag tuples (sub-tag 0x03), consistent with NUT-18's tags array.

**Encoding (JSON to TLV):**

1. Parse the `nprofile` or `npub` from the JSON target field using NIP-19
2. Store the raw 32-byte X-only public key in target (sub-tag 0x02)
3. Store any relay URLs from the nprofile as tag tuples with key `"r"`
4. Store NIPs from the tags array as tag tuples with key `"n"`

**Decoding (TLV to JSON):**

- If no `"r"` tag tuples are present: encode public key as `npub`
- If `"r"` tag tuples are present: encode as `nprofile` using NIP-19 format

#### Tag Tuple Encoding (Sub-Tag 0x03)

Generic tag tuples are encoded as:

1. Key length (1 byte)
2. Key string (UTF-8)
3. For each value:
- Value length (1 byte)
- Value string (UTF-8)

This allows encoding arbitrary key-value pairs for extensibility.

### NUT-10 Sub-TLV (Tag 0x08)

NUT-10 spending conditions are encoded as nested TLV structures:

| Sub-Tag | Field | Type | Description |
| ------- | --------- | ----------- | ------------------------------------------------------------ |
| 0x01 | kind | u8 | Secret kind (0=P2PK, 1=HTLC, etc.) |
| 0x02 | data | bytes | Kind-specific data (UTF-8 encoded) |
| 0x03 | tag_tuple | sub-sub-TLV | Tag tuple (repeatable, uses same encoding as transport tags) |

#### NUT-10 Kind Enumeration

The following kind values are defined for NUT-10 spending conditions:

| Kind Value | Name | Description |
| ---------- | ---- | ---------------------------------------------------------------- |
| 0x00 | P2PK | Pay to Public Key - requires signature from specified public key |
| 0x01 | HTLC | Hash Time Locked Contract - requires preimage of hash |

Additional kind values may be defined in future NUT specifications. Unknown kind values SHOULD be preserved when re-encoding but MAY be ignored during validation.

## Example

This is an example payment request expressed as JSON:

```json
{
"i": "demo123",
"a": 1000,
"u": "sat",
"s": true,
"m": ["https://mint.example.com"],
"d": "Coffee payment"
}
```

This payment request encodes to the NUT-26 format as:

```
CREQB1QYQQWER9D4HNZV3NQGQQSQQQQQQQQQQRAQPSQQGQQSQQZQG9QQVXSAR5WPEN5TE0D45KUAPWV4UXZMTSD3JJUCM0D5RQQRJRDANXVET9YPCXZ7TDV4H8GXHR3TQ
```

[00]: 00.md
[10]: 10.md
[18]: 18.md
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Wallets and mints `MUST` implement all mandatory specs and `CAN` implement optio
| [23][23] | Payment Method: BOLT11 | [Nutshell][py], [cdk-cli] | [Nutshell][py], [cdk-mintd], [nutmix] |
| [24][24] | HTTP 402 Payment Required | - | - |
| [25][25] | Payment Method: BOLT12 | [cdk-cli], [cashu-ts][ts] | [cdk-mintd] |
| [26][26] | Payment Request Bech32m Encoding | - | - |

#### Wallets:

Expand Down Expand Up @@ -99,3 +100,4 @@ Wallets and mints `MUST` implement all mandatory specs and `CAN` implement optio
[23]: 23.md
[24]: 24.md
[25]: 25.md
[26]: 26.md
Loading