Skip to content

Note NFT

The Note NFT (TSL1_LT_NOTE) is a fuelled token whose terms say, for each way it can end, how much of its satoshis the holder gets. The Issuer (payTo) gets the rest. It turns out that one Script covers a whole family of instruments: a voucher, a time-locked claim, a callable claim, a deposit, an insured voucher.

A note ends in one of three ways:

Ending Who sends it When Holder’s amount
exit the holder any time exit
mature exit the holder from block maturity mature
call the issuer (the payTo key) from block callAt call

An amount of 0 gives the holder nothing on that path, and NoteTerms.everything gives the holder everything. A height of NoteTerms.never is never reached. Neither side can take the other’s share, because the Script fixes the Outputs: the holder’s at least its amount, and the Issuer’s at least the rest less feeCap. There is no burn.

The examples on this page follow a deposit. The holder can leave at any time with 30,000 satoshis, or with everything from block 900,000.

// A deposit: the holder may leave at any time with 30,000 sats, or with
// everything from block 900,000. The Issuer keeps the difference on an
// early exit and can never call it.
final terms = NoteTerms.deposit(payTo: pkhOf(issuerKey), feeCap: 500, at: 900000, early: 30000);
final iss = b.issue(first: coinOf(issuerKey, 50300), issuerPkh: issuer.lt.pkh, owner: pkhOf(issuerKey), terms: terms, fee: 300);
iss.x = issuer.lt.attest(iss.txid);
final note = b.transfer(from: iss, ownerKey: issuerKey.lt, to: pkhOf(alice), fee: 100);
expect(note.value, 49900);

The fuel is not checked against the amounts, and each transfer pays its fee from it. So before a person accepts or pays for a note, we show them what they get now, at maturity, and if called, at the note’s current value. NoteMath computes the builder’s own numbers:

// Before Alice accepts the note, we show her what each ending pays at the
// current height and value. NoteMath gives the builder's own numbers.
final now = terms.endingsAt(height: 899000, value: note.value, fee: 300);
expect([for (final e in now) (e.name, e.available, e.holder, e.issuer)], [
('exit', true, 30000, 19600),
('mature', false, 49600, 0),
('call', false, 0, 49600),
]);
expect(terms.blocksToMaturity(899000), 1000);
expect(terms.blocksToCall(899000), isNull); // never callable

Instrument math lists all of NoteMath.

The holder ends the note before maturity. With an amount somewhere between nothing and everything, the ending has two Outputs: Output 0 to the Issuer, and Output 1 to the holder.

final exit = b.exit(from: note, ownerKey: alice.lt, fee: 300);
spends(exit, 0, note.lock(b.code), note.value);
expect(paid(exit), [('issuer', 19600), ('holder', 30000)]);

From maturity onwards, the holder’s ending pays the mature amount. The Transaction carries the maturity height as its nLockTime, with a non-final sequence, so it is only mined from that height:

// From maturity onwards. nLockTime is the maturity height, so it is mined
// only from then.
final mature = b.matureExit(from: note, ownerKey: alice.lt, fee: 300);
spends(mature, 0, note.lock(b.code), note.value);
expect(paid(mature), [('holder', 49600)]);
expect(Transaction.fromHex(hex.encode(mature)).nLockTime, 900000);

A callable note lets the Issuer end it from callAt, paying the holder the call amount. callCost is what the holder loses if the note is called instead of being left to mature:

final terms = NoteTerms.callable(payTo: pkhOf(issuerKey), feeCap: 500, at: 900000, callAt: 880000, floor: 20000);
final note = heldByAlice(terms);
expect(terms.callableBeforeMaturity, isTrue);
expect(terms.callCost(value: note.value, fee: 300), note.value - 300 - 20000); // what a call costs Alice
final call = b.call(from: note, issuerKey: issuerKey.lt, fee: 300); // signed by the payTo key
spends(call, 0, note.lock(b.code), note.value);
expect(paid(call), [('issuer', note.value - 300 - 20000), ('holder', 20000)]);
expect(Transaction.fromHex(hex.encode(call)).nLockTime, 880000);

NoteTerms has a constructor per instrument:

Preset exit mature (at) call (callAt) The holder is exposed to
voucher 0 never 0 from expiry the issuer’s word for everything
zeroCoupon 0 everything from at never time only: it is fully collateralised
callable 0 everything from at floor from callAt being ended early at the floor
deposit early everything from at never its own impatience
insured 0 never floor from callAt being taken back, never for less than the floor
final presets = {
'voucher': NoteTerms.voucher(payTo: payTo, feeCap: 500, expiry: 900000),
'zeroCoupon': NoteTerms.zeroCoupon(payTo: payTo, feeCap: 500, at: 900000),
'callable': NoteTerms.callable(payTo: payTo, feeCap: 500, at: 900000, callAt: 880000, floor: 20000),
'deposit': NoteTerms.deposit(payTo: payTo, feeCap: 500, at: 900000, early: 30000),
'insured': NoteTerms.insured(payTo: payTo, feeCap: 500, callAt: 880000, floor: 20000),
};
// What the holder gets at maturity, from a note of 50,000 sats:
expect({for (final e in presets.entries) e.key: e.value.holderAtMaturity(value: 50000, fee: 300)}, {
'voucher': 0,
'zeroCoupon': 49700,
'callable': 49700,
'deposit': 49700,
'insured': 0,
});

A zero-coupon note is a time-locked claim, priced at a discount for time. Nobody owes anything. For anything else, NoteTerms(...) takes the eight fields directly, and maxHops (default 0, i.e. no limit) caps the transfers.

A reissue from an ending that paid payTo works as it does for the voucher, and its link carries the predecessor’s 61 bytes of terms. An ending that paid only the holder leaves nothing for a successor. The sale version is FnftBuilder.saleableNote(). A buyer gets the same terms, so price a note at what its amounts are worth at their heights.

LtNotePlugin takes the terms as params, and its metadata gives holderOnExit, holderAtMaturity, holderIfCalled and callableBeforeMaturity at the current fuel. See Plugin reference.

The code is 3,479 bytes, and the header 215. A two-Output exit is about 3,890 bytes.