Fungible token
The Fungible Token (TSL1_LT_FT) carries an amount in its header: points, credits, shares, a stable unit. A transfer can keep the amount whole, split it into two Outputs, or merge two Outputs of the same token into one. Whichever it does, the Script checks that no transfer creates amount.
Instead of redeem-and-reissue, a Fungible Token’s record can be reset in place. Three attesters are named at issuance, and any two of them can sign for an Output so that its record restarts there.
The builder and the attesters
Section titled “The builder and the attesters”The attesters are Rabin Keys, like the Issuer’s. Choose them as you would choose who may issue, since two of them together can mint (see below).
final b = LtBuilder(LtLayout.ft, LtScriptGen.ft.code().buffer);// Three attesters are fixed at issuance. Any two of them can reset the record.final attesters = [for (final k in attesterRabins) LtAttester(k)];Issuing and distributing
Section titled “Issuing and distributing”The issuance holds the whole supply, in the token’s smallest unit. The header commits to the attesters by the hash of their three keys:
final issuance = b.issue( first: coinOf(issuerKey), fund: coinOf(issuerKey), issuerPkh: issuer.lt.pkh, attesters: attestersOf(attesters), // HASH160 of the three keys owner: pkhOf(issuerKey), amount: 1000000, // the whole supply, in the token's smallest unit);issuance.x = issuer.lt.attest(issuance.txid);
final t1 = b.transfer( from: issuance, k: 1, ownerKey: issuerKey.lt, fund: coinOf(issuerKey), outs: [LtOut(pkhOf(alice), 1000000)], // the amount must be kept whole);spends(t1.raw, 0, issuance.lock(1, b.code), 1);expect(t1.amount(1), 1000000);Splitting
Section titled “Splitting”If we give a transfer two Outputs, their amounts must add up to the Input’s. The token Outputs are 1 and 2:
// Alice pays Bob 300 and keeps 700, giving two token Outputs, 1 and 2.final split = b.transfer( from: t1, k: 1, ownerKey: alice.lt, fund: coinOf(alice), outs: [LtOut(pkhOf(bob), 300), LtOut(pkhOf(alice), 700)],);spends(split.raw, 0, t1.lock(1, b.code), 1);expect([split.amount(1), split.amount(2)], [300, 700]);Merging
Section titled “Merging”A merge spends two token Outputs. The driver sits at Input 0 (on the normal path), and the passenger at Input 1, signed by its owner. The driver’s Script also rebuilds the passenger’s Transaction and its parent, while the passenger’s own Script only checks its owner’s Signature. The Output holds the sum:
// Later Bob is paid Alice's 700 too, at Output 2 of the split, and// merges the two into one Output of 1000. The driver (Input 0) is Bob's// 300, and the passenger (Input 1) is the 700, signed by its owner.final toBob = b.transfer(from: split, k: 2, ownerKey: alice.lt, fund: coinOf(alice), outs: [LtOut(pkhOf(bob), 700)]);final merged = b.transfer( from: split, k: 1, ownerKey: bob.lt, passenger: LtPassenger(toBob, 1, bob.lt), fund: coinOf(bob), outs: [LtOut(pkhOf(carol), 1000)],);spends(merged.raw, 0, split.lock(1, b.code), 1); // the driverspends(merged.raw, 1, toBob.lock(1, b.code), 1); // the passengerexpect(merged.amount(1), 1000);A merge’s record holds both parents’ records, so merging tokens with long histories makes for a large Transaction. Reset them first.
Amounts are checked by the Script
Section titled “Amounts are checked by the Script”It should be noted that the builder does not check that amounts add up. It writes whatever we give it. It is the Script that refuses a transfer whose Outputs hold more than its Input:
// The builder writes whatever amounts it is given. It is the Script that// refuses Outputs that add up to more than the Input.final inflated = b.transfer(from: iss, k: 1, ownerKey: issuerKey.lt, fund: coinOf(issuerKey), outs: [LtOut(pkhOf(alice), 1001)]);expect(() => spends(inflated.raw, 0, iss.lock(1, b.code), 1), refusedByScript);So run a transfer you built through the interpreter before you broadcast it.
Resetting the record
Section titled “Resetting the record”Each hop adds 455 bytes of record. A reset replaces the parent’s record with evidence: the issuance’s record (which names the attesters), and two attesters’ Rabin Signatures over the Token ID and the outpoint being spent. The record then starts over from there:
// Two of the three attesters sign for the outpoint being spent.final at = outpoint(t.txid, 1);final evidence = resetEvidence( issuance, // the issuance names the attesters attesters, attesters[0].attest(t.tokenId, at), attesters[2].attest(t.tokenId, at),);// The reset hop carries that evidence in place of its parent's record.final r = b.transfer( from: t, k: 1, ownerKey: holder.lt, fund: coinOf(holder), outs: [LtOut(pkhOf(alice), 1000)], reset: evidence,);spends(r.raw, 0, t.lock(1, b.code), 1);// The next hop's record starts over from the reset.final next = b.transfer(from: r, k: 1, ownerKey: alice.lt, fund: coinOf(alice), outs: [LtOut(pkhOf(bob), 1000)]);spends(next.raw, 0, r.lock(1, b.code), 1);expect(next.rec.length, lessThan(grown));| bytes | |
|---|---|
| code after the separator | 5,343 |
| locking script (header 241) | 5,587 |
| issuance | 5,938 |
| hop 1 / hop 10 / hop 20 | 12,470 / 16,564 / 21,114 |
| growth per ancestor | 455 |
| a reset hop (a normal hop at the same depth: 17,019) | 12,728 |

