Skip to content

Plugin reference

This page gathers every plugin’s actions in one place. Params are a Map<String, dynamic> in the PluginOutputSpec, with 'action' naming the action. Key hashes, txids (in display order), Attestations and raw Transactions are hex strings, while heights, prices and amounts are ints. “Coins” is how many wallet coins the action takes. Where that is 0, send amount: 0 (see How much to pay).

The examples call each plugin with a stand-in wallet (test/support/standin_wallet.dart). In an app, the same params go in a payment.

Id and script type pushdrop. We build the spec with PushDropPlugin.outputSpec(outputs:, spends:, label:) rather than by hand. Its action is create when it makes tokens, and spend when it only spends them. It takes one or more coins for the fee, by the spec’s amount. See PushDrop.

Metadata: ownerAddress (the locking key’s Address), lockingPublicKey, fields (hex).

Id tsl1_lt_nft, script type lt_nft. The token is Output 1.

Action Params Coins Result
issue issuer (the Rabin key hash); optional payload 2 The issuance, owned by the first coin’s key. With payload, the primary transaction is the payload transaction and the paired one the issuance.
transfer token, to; attestation when token is the issuance 1 A transfer
end token, irreversible: true 1 The token spent on the cheap path; everything to its owner
reissue token, issuer (must be the token’s issuer) 2 The end (primary) and the successor issued on its output 0 (paired)
swapBid, swapAccept, swapAsk, swapTake see the swap actions 0

Metadata: ownerAddress, ownerPKH, issuer, tokenId (display order; null on an issuance, whose txid is the id).

final plugin = LtNftPlugin(network: NetworkType.TEST);
// issue takes two funding coins, and the token is owned by the first coin's key
final issued = await plugin.buildTransaction(issuerWallet.request({'action': 'issue', 'issuer': issuerHex()}, coins: 2));
final iss = issued.primaryTx;
issuerWallet.record(iss);
expect(plugin.extractMetadata(iss.outputs[1].script)!['ownerAddress'], addressOf(issuerKey));
// The first transfer carries the Issuer's Attestation (hex), which
// the plugin checks before it builds anything.
final t1 = (await plugin.buildTransaction(issuerWallet.request({
'action': 'transfer',
'token': iss.id, // display-order txid
'to': pkhHex(alice), // key hash, hex
'attestation': attestationFor(issuer.lt, iss),
})))
.primaryTx;

end and reissue stop a token for good. The irreversible: true and issuer params are your app’s statement of intent, so give them only after a person or a policy has approved the operation.

// end destroys the token, so the plugin refuses it without the statement.
expect(() => plugin.buildTransaction(aliceWallet.request({'action': 'end', 'token': t1.id})), throwsArgumentError);
final end = (await plugin.buildTransaction(aliceWallet.request({'action': 'end', 'token': t1.id, 'irreversible': true}))).primaryTx;
expect(plugin.validateTransactionStructure(end, 'end'), isTrue);

Receive check: LtNftReceive.check(plugin:, beef:, ownerAddress:, anchorTx:, links:, payloadTx:), which returns an LtHolding. Helpers: plugin.anchorTxid(transferRaw), plugin.link(endRaw, redeemedRaw), plugin.builder.

Id tsl1_lt_lnft, script type lt_lnft. It has every LtNftPlugin action, and in addition:

Action Who Params Coins Result
list the owner token, price (at least 1) 1 A transfer to the owner’s own key, listed at price
withdraw the owner token 1 A transfer to the owner’s own key, unlisted
buy anyone listingTx (raw), coin (txid:vout, worth exactly price plus fee), maxPrice, anchorTx (raw); optional links, newOwner 0 The purchase

Metadata adds price (0 means not listed). buy runs the payee check on the listing first. It refuses a token that is not listed, is listed above maxPrice, or is held by this wallet, as well as a coin that leaves no fee. For received tokens, use LtNftReceive.check with this plugin. See Listed NFT.

Id tsl1_lt_fnft, script type lt_fnft. The token is Output 0.

Action Params Coins Result
issue issuer 1 or 2, by the amount The issuance; the coins less the fee are the fuel
transfer token, to; attestation for an issuance 0 A transfer paid from the fuel
topup token, to; attestation for an issuance 1 A transfer that sweeps the coin into the fuel. To refuel without moving it, give the owner’s own key hash as to.
end token, irreversible: true 0 The burn: the fuel less the fee to the owner
reissue token, issuer 0 The end and the successor, fuelled by the ended token

Metadata adds fuel. Receive check: LtFnftReceive.check, which returns an FnftHolding (value is the fuel). The errors you are most likely to meet are the token would hold <n> sats: the fuel does not cover the fee (top up, reissue or burn it), topup needs 1 funding coin(s), got 0 and an issuance takes one or two coins of fuel. See Fuelled NFT.

Id tsl1_lt_vnft, script type lt_vnft. No end.

Action Who Params Coins Result
issue the issuer issuer, maxHops, expiry, feeCap; optional redeemTo (default the first coin’s key) 1 or 2 The issuance
transfer the owner token, to; attestation for an issuance 0 A transfer; refused when no hops are left
topup the owner as transfer 1 A transfer that adds the coin to the fuel; it uses a hop
redeem the owner token 0 One output to redeemTo, fee from the fuel up to feeCap
redeemFunded the owner token 1 redeem with the fee paid by the coin
forceRedeem the issuer token; tokenRaw when the wallet does not have it; optional lockHeight (default expiry) 0 The forced redemption, time-locked
reissue the issuer ended or endedRaw (a redemption or forced redemption), issuer, the new terms 0 The successor on the ended transaction’s output 0

Metadata adds fuel, maxHops, hops, hopsLeft, expiry, forcedRedemptionHeight, redeemTo, feeCap.

final plugin = LtVnftPlugin(network: NetworkType.TEST);
final iss = (await plugin.buildTransaction(issuerWallet.request(
{'action': 'issue', 'issuer': issuerHex(), 'maxHops': 2, 'expiry': 900000, 'feeCap': 500})))
.primaryTx;
issuerWallet.record(iss);
// A transfer takes no wallet coin. libspiffy is told so, and selects none.
expect(plugin.requiredFundingUtxoCount('transfer'), 0);
final t1 = (await plugin.buildTransaction(issuerWallet.request(
{'action': 'transfer', 'token': iss.id, 'to': pkhHex(alice), 'attestation': attestationFor(issuer.lt, iss)},
coins: 0)))
.primaryTx;
// What the wallet records about the Output, to show before acceptance.
final m = plugin.extractMetadata(t1.outputs[0].script)!;
expect([m['hops'], m['hopsLeft'], m['expiry'], m['redeemTo']], [1, 1, 900000, pkhHex(issuerKey)]);
expect(m['fuel'], t1.outputs[0].satoshis.toInt());
// The Issuer's wallet does not follow the voucher, so it is given the
// holder's Transaction as tokenRaw.
final forced = (await plugin.buildTransaction(issuerWallet.request(
{'action': 'forceRedeem', 'token': t1.id, 'tokenRaw': t1.serialize()},
coins: 0)))
.primaryTx;
expect(forced.nLockTime, 900000);
final r = (await plugin.buildTransaction(aliceWallet.request({'action': 'redeem', 'token': t1.id}, coins: 0))).primaryTx;
expect(r.outputs.single.script.toHex(), hex.encode(p2pkhOf(issuerKey)));

Receive check: LtVnftReceive.check. Show holding.value, holding.tx.hopsLeft and holding.tx.terms.expiry before acceptance. The errors you are most likely to meet are the voucher has made its <n> transfers: it can only be redeemed, the fee <n> takes more than the redemption fee cap <c> from the token: add a funding coin (use redeemFunded), the wallet does not hold transaction <txid> (give tokenRaw) and maxHops is 1 to 255. See Voucher NFT.

Id tsl1_lt_note, script type lt_note. No end.

Action Who Params Coins
issue the issuer issuer, feeCap, exit, maturity, mature, callAt, call; optional maxHops (default 0, no limit), payTo (default the first coin’s key) 1 or 2
transfer, topup the owner as for the Fuelled NFT 0, 1
exit the owner token 0
matureExit the owner token 0
call the issuer token; tokenRaw when the wallet does not have it; optional lockHeight 0
reissue the issuer ended or endedRaw, issuer, the new terms 0

Metadata adds what the holder gets at the current fuel: holderOnExit, holderAtMaturity, holderIfCalled, and callableBeforeMaturity.

final plugin = LtNotePlugin(network: NetworkType.TEST);
// A deposit: 30,000 sats on an early exit, everything from block 900,000.
final iss = (await plugin.buildTransaction(issuerWallet.request({
'action': 'issue',
'issuer': issuerHex(),
'feeCap': 500,
'exit': 30000,
'maturity': 900000,
'mature': NoteTerms.everything,
'callAt': NoteTerms.never,
'call': 0,
})))
.primaryTx;
issuerWallet.record(iss);
final t1 = (await plugin.buildTransaction(issuerWallet.request(
{'action': 'transfer', 'token': iss.id, 'to': pkhHex(alice), 'attestation': attestationFor(issuer.lt, iss)},
coins: 0)))
.primaryTx;
final m = plugin.extractMetadata(t1.outputs[0].script)!;
expect(m['holderOnExit'], 30000);
expect(m['holderIfCalled'], 0);
expect(m['callableBeforeMaturity'], isFalse);

Receive check: LtNoteReceive.check. holding.tx.noteTerms and holding.value tell you what each ending pays. The errors you are most likely to meet are the note has made its <n> transfers: it can only be ended, the fee <n> is over the note's fee cap <c> and output 0 of the ended transaction is not paid to <key> (reissue only from an ending that paid payTo). See Note NFT.

LtFnftSalePlugin (tsl1_lt_fnft_s), LtVnftSalePlugin (tsl1_lt_vnft_s) and LtNoteSalePlugin (tsl1_lt_note_s) have every action of their base plugin, as well as the four swap actions. They only move tokens that were issued as sale versions.

All four take no coin. Do not settle a half. It is a deferred payment, which holds your Input until the other side completes it.

Action Who Params Result
swapBid the buyer, first tokenTx (raw), payTo, price, coin (txid:vout), anchorTx (raw); optional links, newOwner A half with the buyer’s signature
swapAccept the seller, second half (raw), fundTx (the coin’s transaction, raw), price The complete swap
swapAsk the seller, first token, price, fundOut (txid:vout), fundSats, fundPub, newOwner; optional payTo A half with the seller’s signature
swapTake the buyer, second half (raw), tokenTx (raw), price, anchorTx (raw); optional links The complete swap

swapBid runs the payee check on the token before signing, and refuses a token that this wallet holds. swapAccept refuses a half that pays a different price from the one you pass, pays a key this wallet does not hold, is for a token this wallet does not hold, or whose buyer Signature or coin does not verify. After the second side settles the swap, the first side hands it to libspiffy with CompleteDeferredPaymentCommand(walletId, txid: halfTxid, rawHex: swapRaw). See Sales and swaps.