Skip to content

Listed NFT

The Listed NFT (TSL1_LT_LNFT) is the covenant NFT with a price in its header. When the price is not zero, anyone may buy the token by paying the owner at least that price, without the owner signing or even being online. The buyer’s Output is unlisted.

We use it for a shop or a marketplace where items sit at a posted price. For a negotiated sale of an ordinary NFT, use the swap instead.

A covenant NFT cannot become a Listed NFT. If you want one, issue it as one.

final b = LtBuilder(LtLayout.lnft, LtScriptGen.lnft.code().buffer);

A listing is a transfer to yourself, with a price. Changing the price is another such transfer, and each one costs a hop and a funding coin.

// Listing is a transfer to yourself, with a price.
final listing = b.transfer(
from: t1, k: 1, ownerKey: seller.lt, fund: coinOf(seller), outs: [LtOut(pkhOf(seller))], price: 25000);
spends(listing.raw, 0, t1.lock(1, b.code), 1);
expect(listing.price, 25000);

A purchase spends the listed Output with no Signature on Input 0. The buyer’s coin at Input 1 pays the price to the owner at Output 0, and the token goes to the buyer at Output 1. The fee is whatever the coin holds over and above the price, and there is no change. So the buyer first makes a coin of exactly the price plus the fee.

// The buyer first checks the listing as a payee would, for the
// seller's key, and reads the price from it.
await LtReceive.check(b: b, raw: listing.raw, ownerPkh: pkhOf(seller), anchorTx: issuer.anchor);
expect(listing.price, lessThanOrEqualTo(30000)); // the most this buyer pays
// The buyer's coin is the price plus the fee. There is no change.
final purchase = b.buy(from: listing, fund: coinOf(buyer, 25000 + 400), newOwner: pkhOf(buyer));
spends(purchase.raw, 0, listing.lock(1, b.code), 1); // nobody signs Input 0
spends(purchase.raw, 1, p2pkhOf(buyer), 25400);
expect(purchase.bought, isTrue);
final payment = Transaction.fromHex(hex.encode(purchase.raw)).outputs[0];
expect(payment.satoshis.toInt(), 25000);
expect(hex.encode(payment.script.buffer), hex.encode(p2pkhOf(seller)));
final mine = await LtReceive.check(b: b, raw: purchase.raw, ownerPkh: pkhOf(buyer), anchorTx: issuer.anchor);
expect(mine.tx.price, 0); // the buyer's Output is not listed

Before buying, also check with your chain service that the listed Output is still unspent. A purchase of a spent Output is refused by the network, and the buyer’s coin stays unspent. The buyer’s Signature on Input 1 covers every Output, so nobody can redirect a purchase once it has been sent.

The Script refuses a payment below the price, a payment to anyone but the owner, and a buyer’s Output that is left listed. Here we are one satoshi short:

final cheap = b.buy(from: listing, fund: coinOf(buyer, 25400), newOwner: pkhOf(buyer), pay: 24999);
expect(() => spends(cheap.raw, 0, listing.lock(1, b.code), 1), refusedByScript);
// Withdrawing is another transfer to yourself, at price 0.
final withdrawn = b.transfer(from: listing, k: 1, ownerKey: seller.lt, fund: coinOf(seller), outs: [LtOut(pkhOf(seller))]);
spends(withdrawn.raw, 0, listing.lock(1, b.code), 1);
expect(withdrawn.price, 0);

The seller’s wallet does not sign the purchase, so it does not know about it. We watch the listed outpoint with libspiffy’s CheckForeignSpendsCommand (see libspiffy.dev). It reports a spend by a Transaction that the wallet did not make, and returns that Transaction. The payment is Output 0 of it.

An issuance cannot be listed for purchase. The Issuer distributes it first.

LtLnftPlugin has the covenant NFT’s actions, as well as list, withdraw and buy (which takes a maxPrice). See Plugin reference.

The code is 2,417 bytes, the header 180, and the growth 394 bytes per hop. A purchase at hop 3 is about 7,100 bytes.