-
Notifications
You must be signed in to change notification settings - Fork 71
feat: Add NUT-XX Payment Request Bech32m Encoding specification #294
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+590
−0
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
cfd9cd3
feat: add .gitignore
thesimplekid e729963
feat: Add NUT-26 Payment Request Bech32m Encoding specification
thesimplekid 6f25bfb
Update 26.md
thesimplekid 467baf8
Update 26.md
thesimplekid a402806
Update 26.md
thesimplekid 90d83cc
fix: tests u16 -> u8
thesimplekid 312f9e1
fix: remove sats default
thesimplekid 26f003d
fix: remove sub-feilds for tags
thesimplekid 5f0b45e
fix: use post not http_post
thesimplekid 9fd7c76
chore: prettier
thesimplekid b77f830
fix: test vectors post code review
thesimplekid 87a894e
feat: add more tests
thesimplekid 5232ce4
feat: use nprofile
thesimplekid 887f985
feat: add more tests with nprofile not npub
thesimplekid 6651459
chore: prettier
thesimplekid 0242c24
chore: remove redundant section from test vectors
thesimplekid fa3657e
fix: use valid npubs
thesimplekid 5d34d6d
fix: remove inband
thesimplekid 10409b0
fix: btc unit not gold
thesimplekid 1702dc5
fix: htlc test
thesimplekid 4622bec
fix: htlc test
thesimplekid 1c91d89
chore: prettier
thesimplekid a96323a
feat: remove duplicate sections
thesimplekid File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| .pre-commit-config.yaml |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.