Skip to content

Commit 07b4075

Browse files
authored
XLS-35d: URITokens — Lightweight first-class NFTs (#110)
1 parent 7ce783a commit 07b4075

File tree

1 file changed

+288
-0
lines changed

1 file changed

+288
-0
lines changed

XLS-0035-uritoken/README.md

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
<pre>
2+
xls: 35
3+
title: URITokens
4+
description: Lightweight first-class NFTs for XRPL Protocol Chains
5+
author: Richard Holland (@RichardAH), Wietse Wind (@WietseWind)
6+
discussion-from: https://github.com/XRPLF/XRPL-Standards/discussions/89
7+
status: Draft
8+
category: Amendment
9+
created: 2023-02-09
10+
</pre>
11+
12+
# XLS-35d URITokens — Lightweight first-class NFTs for XRPL Protocol Chains
13+
14+
# Problem Statement
15+
16+
XLS-20 is a Non-Fungible Token standard that is currently active and in-use on the XRP Ledger main-net. Despite this, many developers and users of the XRPL remain unsatisfied by its complexity, unusual edge-cases, lack of first-class object NFTs, and general difficulty to understand and write integrations for. We therefore propose a light-weight alternative: _URIToken_.
17+
18+
# Amendment
19+
20+
The URIToken Amendment provides a lightweight alternative to XLS20 suitable for both main-net and side-chains.
21+
22+
The amendment adds:
23+
24+
A new type of ledger object: `ltURI_TOKEN`
25+
A new serialized field: `URITokenID`
26+
Five new transaction types:
27+
28+
- `URITokenMint`
29+
- `URITokenBurn`
30+
- `URITokenBuy`
31+
- `URITokenCreateSellOffer`
32+
- `URITokenCancelSellOffer`
33+
34+
## New Ledger Object Type: `URIToken`
35+
36+
The `ltURI_TOKEN` object is an owned first-class on-ledger object which lives in its owner's directory. It is uniquely identified by the combined hash of its `Issuer` (minter) and the `URI`. Therefore an issuer can only issue one `URIToken` per URI. Upon creation (minting) the Issuer is the object's first Owner. You cannot mint on behalf of a third party. As with other first class objects, each URIToken locks up an owner reserve on the account that currently owns it. Disposing of a URIToken frees up these reserved funds.
37+
38+
The object has the following fields:
39+
40+
| Field | Type | Required | Description |
41+
| ------------- | --------- | -------- | -------------------------------------------------------------------------------------------------------- |
42+
| sfIssuer | AccountID | ✔️ | The minter who issued the token. |
43+
| sfOwner | AccountID | ✔️ | The current owner of the token. |
44+
| sfURI | VL blob | ✔️ | The URI the token points to. |
45+
| sfFlags | UInt32 | ✔️ | A flag indicating whether or not the URIToken is burnable, and whether or not it is for sale. |
46+
| sfDigest | Hash256 || An sha512half integrity digest of the contents pointed to by the URI |
47+
| sfAmount | Amount || If the URIToken is for sale, then this is the amount the seller is asking for. |
48+
| sfDestination | AccountID || If the URIToken is for sale and this field has been set then only this AccountID may purchase the token. |
49+
50+
Example URIToken object:
51+
52+
```json
53+
{
54+
"Flags": 0,
55+
"Issuer": "rN38hTretqygfgcvADnJwZzHu5rawAvmkX",
56+
"LedgerEntryType": "URIToken",
57+
"Owner": "rN38hTretqygfgcvADnJwZzHu5rawAvmkX",
58+
"URI": "68747470733A2F2F6D656469612E74656E6F722E636F6D2F666752755A7A662D374B5541414141642F6465616C2D776974682D69742D73756E676C61737365732E676966"
59+
}
60+
```
61+
62+
## New Transaction Type: `URITokenMint`
63+
64+
| Field | Type | Required | Description |
65+
| -------- | ------- | -------- | --------------------------------------------------------------------- |
66+
| sfURI | VL blob | ✔️ | The URI the token points to. |
67+
| sfDigest | Hash256 || An SHA512-Half integrity digest of the contents pointed to by the URI |
68+
| sfFlags | UInt32 || tfBurnable (0x00000001) or 0 or absent |
69+
70+
If `sfDigest` is specified then the minted token will contain the hash specified by this field. For the end user this means they can verify the content served at the URI against this immutable hash, to ensure, for example that the properties of the NFT are not maliciously altered by changing the content at the URI. It may also be desirable to have a dynamic NFT where the content is intended to be altered, in which case simply omit `sfDigest` during minting, and the resulting URIToken will not contain this field.
71+
72+
‼️ If `sfFlags` is present and set to tfBurnable then the URIToken may be later burned by the Issuer. If the Hooks amendment is active on the chain this flag also indicates that the Issuer is a _strong transactional stakeholder_. In this event the Issuer's hooks will be executed whenever an attempt to buy or sell this URIToken occurs, and those hooks may reject the transaction and prevent it from happening if their own internal logic is not satisfied. It is therefore highly advisable to check whether or not a URIToken has `tfBurnable` set before purchasing or accepting it in trade.
73+
74+
Example Mint:
75+
76+
```json
77+
{
78+
"Account": "raKG2uCwu71ohFGo1BJr7xqeGfWfYWZeh3",
79+
"Digest": "894E3B7ECDC9F6D00EE1D892F86E9BF0098F86BBD6CBB94D6ABFD78030EB5B9B",
80+
"TransactionType": "URITokenMint",
81+
"URI": "68747470733A2F2F6D656469612E74656E6F722E636F6D2F666752755A7A662D374B5541414141642F6465616C2D776974682D69742D73756E676C61737365732E6A736F6E"
82+
}
83+
```
84+
85+
## New Transaction Type: `URITokenBurn`
86+
87+
| Field | Type | Required | Description |
88+
| ------------ | ------- | -------- | -------------------------------------------------- |
89+
| sfURITokenID | Hash256 | ✔️ | The Keylet for the URIToken object being destroyed |
90+
91+
The current owner of the URIToken can burn it at any time.
92+
93+
The Issuer of the token may also burn it at any time but only if `tfBurnable` was set during minting.
94+
95+
Burning a URIToken removes the specified `ltURI_TOKEN` object from the ledger and from the owner’s directory.
96+
97+
Example Burn:
98+
99+
```json
100+
{
101+
"Account": "r9XAC6zP5Db4qZBgRbweKUPtroxYnydTEQ",
102+
"TransactionType": "URITokenBurn",
103+
"URITokenID": "0FAC3CD45FCB800BB9CCCF907775E7D4FB167847D8999FF05CE7456D6C3A70FA"
104+
}
105+
```
106+
107+
## New Transaction Type: `URITokenCreateSellOffer`
108+
109+
A user may offer to sell their URIToken for a preset amount. A given URIToken may have at most one current sell offer. There are no buy offers. If a user executes a URITokenBuy then it must immediately cross an existing sell offer.
110+
111+
To offer the URIToken for sale: specify its `URITokenID`, an `Amount` to sell for, and optionally a `Destination`. If Destination is set then only the specified account may purchase the URIToken. If the Amount is 0 then a Destination must be set. (This prevents an accidental "transfer to anyone" scenario.)
112+
113+
If a previous sell offer was present on the URIToken then it is simply replaced with the new offer.
114+
115+
| Field | Type | Required | Description |
116+
| ------------- | --------- | -------- | ------------------------------------------------------------------------------------ |
117+
| sfURITokenID | Hash256 | ✔️ | The Keylet for the URIToken object being offered for sale |
118+
| sfAmount | Amount | ✔️ | The minimum amount a buyer must pay to purchase this URIToken. May be an IOU or XRP. |
119+
| sfDestination | AccountID || If provided then only this account may purchase the URIToken. |
120+
121+
Example Sell:
122+
123+
```json
124+
{
125+
"Account": "r9XAC6zP5Db4qZBgRbweKUPtroxYnydTEQ",
126+
"Amount": "100000",
127+
"Flags": 524288,
128+
"TransactionType": "URITokenCreateSellOffer",
129+
"URITokenID": "0FAC3CD45FCB800BB9CCCF907775E7D4FB167847D8999FF05CE7456D6C3A70FA"
130+
}
131+
```
132+
133+
## New Transaction Type: `URITokenBuy`
134+
135+
A user may purchase a URIToken from another user if that URIToken has an active sell offer on it.
136+
137+
Whether a URIToken is for sale is indicated by the presence of the `Amount` field in the `lt_URI_TOKEN` object. If a `Destination` is also present in the object then only that AccountID may perform the purchase.
138+
139+
To purchase the URIToken, a user specifies its `URITokenID` and a purchase `Amount`. The purchase amount must be at least the amount specified in the sell offer (but may also exceed if the user wishes to tip the seller.) The purchase amount must be the same currency as the amount in the sell offer. No pathing is allowed in this transaction. The user must have sufficient currency available to cover the purchase.
140+
141+
| Field | Type | Required | Description |
142+
| ------------ | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- |
143+
| sfURITokenID | Hash256 | ✔️ | The Keylet for the URIToken object being purchased. |
144+
| sfAmount | Amount | ✔️ | The purchase price the buyer is willing to send. Must be the same currency as the sell offer. May not be less than the sale amount. |
145+
146+
Example Buy:
147+
148+
```json
149+
{
150+
"Account": "rpiLN1C94hGKGpLUbhsadVHzdSXtB2Ldra",
151+
"Amount": "100001",
152+
"TransactionType": "URITokenBuy",
153+
"URITokenID": "0FAC3CD45FCB800BB9CCCF907775E7D4FB167847D8999FF05CE7456D6C3A70FA"
154+
}
155+
```
156+
157+
## New Transaction Type: `URITokenCancelSellOffer`
158+
159+
When a user has offered their URIToken for sale and later changes their mind, they may perform a clear operation. A clear operation simply clears the current sell offer from the URIToken.
160+
161+
| Field | Type | Required | Description |
162+
| ------------ | ------- | -------- | -------------------------------------------------------------------------- |
163+
| sfURITokenID | Hash256 | ✔️ | The Keylet for the URIToken object being cleared of any active sell offer. |
164+
165+
Example Clear
166+
167+
```json
168+
{
169+
"Account": "rpiLN1C94hGKGpLUbhsadVHzdSXtB2Ldra",
170+
"TransactionType": "URITokenCancelSellOffer",
171+
"URITokenID": "0FAC3CD45FCB800BB9CCCF907775E7D4FB167847D8999FF05CE7456D6C3A70FA"
172+
}
173+
```
174+
175+
# Schema / Metadata Content
176+
177+
The URI pointed to by a URIToken should resolve to a JSON document that follows the below schema.
178+
179+
This schema may be extended over time as additional categories and use-cases present themselves.
180+
181+
‼️ Note that the `Digest`, if provided during Minting, is the hash of this JSON document **not** the content pointed to by the JSON document. The `Digest` is calculated by taking the SHA-512 Half of the stringified, whitespace trimmed content JSON.
182+
183+
- Schema gist: https://gist.github.com/WietseWind/83cd89906ed79fb510ec1eae3fc70bb6
184+
- Sample digest generator gist: https://gist.github.com/WietseWind/d5072777814b6f239c3baba5cbe29e39
185+
186+
## JSON Schema
187+
188+
```js
189+
export interface xls35category {
190+
code: string
191+
description: string
192+
}
193+
194+
// EXAMPLES (!)
195+
export const xls35categories: xls35category[] = [
196+
{ code: '0000', description: 'Testing-purpose token' },
197+
198+
{ code: '0001', description: 'Art' },
199+
{ code: '0001.0001', description: 'Physical art' },
200+
{ code: '0001.0002', description: 'Digital art' },
201+
202+
{ code: '0002', description: 'Licenses' },
203+
{ code: '0002.0001', description: 'Software licenses' },
204+
205+
{ code: '0003', description: 'Admission tickets' },
206+
207+
{ code: '0004', description: 'Ownership (physical)' },
208+
{ code: '0004.0001', description: 'Land (plots)' },
209+
{ code: '0004.0002', description: 'Time shares' },
210+
]
211+
212+
export interface xls35attachment {
213+
description?: string
214+
filename: string
215+
url: string
216+
}
217+
218+
export interface xls35schema {
219+
// Custom schema for additional information
220+
schema?: {
221+
url: string
222+
digest?: string
223+
}
224+
225+
// Custom external information, to match your own specified schema (^^)
226+
content?: {
227+
url: string
228+
digest?: string
229+
}
230+
231+
// Basic information: to allow instant rendering, ...
232+
details: {
233+
title: string
234+
categories?: xls35category[]
235+
publisher?: {
236+
name: string
237+
url?: string
238+
email?: string
239+
}
240+
previewUrl?: {
241+
thumbnail: string
242+
regular?: string
243+
highres?: string
244+
}
245+
group?: {
246+
code?: string
247+
title: string
248+
}
249+
attachments?: xls35attachment[]
250+
}
251+
}
252+
```
253+
254+
## Example JSON document
255+
256+
```json
257+
{
258+
"content": {
259+
"url": "https://someuri"
260+
},
261+
"details": {
262+
"title": "Some URIToken",
263+
"categories": ["0000"],
264+
"publisher": {
265+
"name": "XRPL-Labs"
266+
}
267+
}
268+
}
269+
```
270+
271+
## Computing the `Digest` over your JSON document
272+
273+
Pseudo-code:
274+
275+
```js
276+
metadata = {
277+
details: {
278+
title: "Some Title",
279+
},
280+
};
281+
282+
jsonstring = json_encode(metadata);
283+
whitespaceremoved = trim(jsonstring);
284+
hash = sha512(whitespaceremoved);
285+
sha512half = slice(hash, 0, 64);
286+
287+
digest = sha512half;
288+
```

0 commit comments

Comments
 (0)