Multi-output invoices
A multi-output invoice asks the payer for one transaction with several outputs: split payments, a multisig
escrow, an OP_RETURN data record, or an output whose script a plugin builds. You describe each output with an
InvoiceOutputSpec and pass the list as outputs to CreateInvoiceCommand and PayInvoiceCommand.
The basic invoice flow is on Invoices & SPV. This page covers what changes when an
invoice has outputs.
Output kinds
Section titled “Output kinds”InvoiceOutputSpec is a sealed class with four subclasses (see lib/src/models/invoice_output_spec.dart).
Every spec has an amount in satoshis and an optional label.
The specs are exported from package:libspiffy/libspiffy.dart, not from package:libspiffy/coordinator.dart.
Some names in the two libraries collide, so import the specs by name next to the coordinator API:
import 'package:libspiffy/coordinator.dart';import 'package:libspiffy/libspiffy.dart' show InvoiceOutputSpec, P2PKHOutputSpec, P2MSOutputSpec, OPReturnOutputSpec, PluginOutputSpec;| Spec | Fields | Builds | Amount |
|---|---|---|---|
P2PKHOutputSpec |
address |
A payment to an address | Must be positive |
P2MSOutputSpec |
publicKeys (hex), threshold |
A bare m-of-n multisig output | Must be positive |
OPReturnOutputSpec |
dataChunks, separateOutputs |
An unspendable OP_FALSE OP_RETURN data output |
Always zero |
PluginOutputSpec |
pluginId, pluginScriptType, params |
An output whose locking script a registered plugin builds | Must be positive |
When an invoice has outputs, they take precedence over amount and numberOfAddresses, and the invoice’s
amount is the sum of the outputs.
Give an address to pay a fixed address, such as a platform’s fee address. Give an empty address
('') and the payee’s wallet generates a fresh receive address for that output when it creates the invoice.
P2PKHOutputSpec(address: '', amount: BigInt.from(80000), label: 'Merchant');P2PKHOutputSpec(address: platformFeeAddress, amount: BigInt.from(5000), label: 'Platform fee');InvoiceCreatedEvent.outputs holds the specs with the generated addresses filled in, and
InvoiceCreatedEvent.issuedAddresses lists only the addresses the wallet generated, not the ones you
supplied.
P2MS (multisig)
Section titled “P2MS (multisig)”// 2-of-3 escrow: buyer, seller, arbitrator.P2MSOutputSpec( publicKeys: [buyerKeyHex, sellerKeyHex, arbitratorKeyHex], threshold: 2, amount: BigInt.from(15000), label: 'Escrow',);The invoice is refused (ask throws CoordinatorFailure) unless isValid holds: the threshold is at least 1 and at most the number of keys,
there are at most 16 keys, and each key is a valid secp256k1 public key in hex (66 characters compressed or
130 uncompressed). totalKeys is the n of m-of-n.
The payer builds the script with the keys sorted (BIP67), and the payee matches it by comparing the key set and threshold, so the order you list the keys in does not matter.
OP_RETURN
Section titled “OP_RETURN”import 'dart:convert';
OPReturnOutputSpec(dataChunks: [utf8.encode('order-12345')]);Each chunk is a separate data push. By default all chunks go in one output; with separateOutputs: true
each chunk gets its own output. The data must be non-empty and at most OPReturnOutputSpec.maxTotalDataSize
(99,000) bytes in total. The amount is always zero.
Plugin-delegated
Section titled “Plugin-delegated”PluginOutputSpec hands the locking script to a registered script plugin: the
payer’s wallet calls the plugin’s createLockBuilder(spec). If the plugin is a TransactionBuilderPlugin
and params holds an action it supports, the plugin builds the whole transaction instead.
PluginOutputSpec( pluginId: 'mytoken', pluginScriptType: 'token_v1', params: {'tokenId': tokenId, 'ownerPKH': ownerPkhHex}, amount: BigInt.one,);The payment fails when no plugin with that pluginId is registered on the payer’s side.
Create the invoice
Section titled “Create the invoice”final invoice = await bob.coordinator.ask(CreateInvoiceCommand( walletId: 'bob-wallet', outputs: [ P2PKHOutputSpec(address: '', amount: BigInt.from(80000), label: 'Merchant'), P2PKHOutputSpec(address: platformFeeAddress, amount: BigInt.from(5000), label: 'Platform fee'), P2MSOutputSpec( publicKeys: [buyerKeyHex, sellerKeyHex, arbitratorKeyHex], threshold: 2, amount: BigInt.from(15000), label: 'Escrow', ), ], description: 'Order #12345', expiresIn: const Duration(hours: 24),));// invoice.amount is 100000; invoice.addresses holds the P2PKH addresses only.To send the specs to the payer, serialize each with toMap() and rebuild it with
InvoiceOutputSpec.fromMap(map). The map’s type is p2pkh, p2ms, op_return or plugin; amounts are
decimal strings and OP_RETURN chunks are hex.
Pay it
Section titled “Pay it”The payer passes the specs back as outputs:
final payment = await alice.coordinator.ask(PayInvoiceCommand( walletId: 'alice-wallet', invoiceId: invoiceId, addresses: addresses, // the invoice's P2PKH addresses amount: amount, // the invoice total outputs: outputs, // rebuilt with InvoiceOutputSpec.fromMap));The payment coordinator creates one transaction output per spec (or one per chunk for an OPReturnOutputSpec
with separateOutputs), adds change, and answers with PaymentReadyEvent as for any payment. A payment it cannot build throws
CoordinatorFailure; see Waiting for an answer. A P2PKH
address of the other network (a testnet address in a mainnet wallet, or the reverse) is refused.
How the payee checks it
Section titled “How the payee checks it”When the payee runs ValidateBEEFCommand with the invoice id, the SPV actor counts an output towards the
invoice when:
- P2PKH: it pays one of the invoice’s addresses;
- P2MS: its threshold and key set equal those of one of the invoice’s multisig outputs;
- plugin: a registered plugin recognises the script and the
ownerAddressit reports is one of the invoice’s addresses.
The outputs that pay the invoice must add up to at least the invoice amount. OP_RETURN outputs pay nothing
and are not counted.
A multisig output pays the invoice even when the payee’s wallet cannot spend it alone. It becomes a wallet UTXO only when the wallet holds at least m of its keys; otherwise the transaction is recorded but the output is not in the balance.