278 lines
8.7 KiB
TypeScript
278 lines
8.7 KiB
TypeScript
import type { XOTemplate } from '@xo-cash/types';
|
||
|
||
/**
|
||
* Vending machine payment template.
|
||
*
|
||
* Merchant creates a purchaseItems invitation with receipt variables;
|
||
* customer funds and signs the composable transaction.
|
||
*/
|
||
export const vendingMachineTemplate: XOTemplate = {
|
||
$schema: 'https://libauth.org/schemas/wallet-template-v0.schema.json',
|
||
name: 'Vending Machine',
|
||
description: 'Purchase items from a vending machine with an itemized receipt.',
|
||
icon: 'wallet',
|
||
version: '1',
|
||
supported: ['BCH_2023_05', 'BCH_2024_05', 'BCH_2025_05', 'BCH_2026_05'],
|
||
|
||
defaults: {
|
||
change: {
|
||
output: 'changeOutput',
|
||
role: 'merchant',
|
||
generate: ['merchantKey'],
|
||
},
|
||
},
|
||
|
||
roles: {
|
||
merchant: {
|
||
name: 'Merchant',
|
||
description: 'The vending machine operator receiving payment.',
|
||
icon: 'owner',
|
||
},
|
||
customer: {
|
||
name: 'Customer',
|
||
description: 'The customer paying for items.',
|
||
icon: 'sender',
|
||
},
|
||
},
|
||
|
||
start: [
|
||
{
|
||
action: 'purchaseItems',
|
||
role: 'merchant',
|
||
generate: ['merchantKey'],
|
||
},
|
||
],
|
||
|
||
actions: {
|
||
purchaseItems: {
|
||
name: 'Purchase Items',
|
||
description: 'Purchase: $(<receiptSummary>) for $(<totalSatoshis>) sats',
|
||
icon: 'request',
|
||
|
||
roles: {
|
||
merchant: {
|
||
name: 'Sell Items',
|
||
description: 'Receive payment for $(<receiptSummary>)',
|
||
icon: 'request',
|
||
requirements: {
|
||
secrets: ['merchantKey'],
|
||
variables: [
|
||
'totalSatoshis',
|
||
'orderId',
|
||
'merchantName',
|
||
'receiptSummary',
|
||
'lineItemsJson',
|
||
],
|
||
},
|
||
},
|
||
customer: {
|
||
name: 'Pay',
|
||
description: 'Pay $(<totalSatoshis>) sats for $(<receiptSummary>)',
|
||
icon: 'send',
|
||
requirements: {},
|
||
},
|
||
},
|
||
|
||
requirements: {
|
||
participants: [
|
||
{ role: 'merchant', slots: { min: 1, max: 1 } },
|
||
{ role: 'customer', slots: { min: 1 } },
|
||
],
|
||
},
|
||
|
||
transaction: 'purchaseItemsTransaction',
|
||
},
|
||
},
|
||
|
||
transactions: {
|
||
purchaseItemsTransaction: {
|
||
name: 'Vending Purchase',
|
||
description: 'Order $(<orderId>): $(<receiptSummary>)',
|
||
icon: 'request',
|
||
|
||
roles: {
|
||
merchant: {
|
||
name: 'Received Payment',
|
||
description: 'Received $(<totalSatoshis>) sats from $(<merchantName>) sale',
|
||
icon: 'receive',
|
||
},
|
||
customer: {
|
||
name: 'Sent Payment',
|
||
description: 'Paid $(<totalSatoshis>) sats for $(<receiptSummary>)',
|
||
icon: 'send',
|
||
},
|
||
},
|
||
|
||
inputs: [],
|
||
outputs: [{ output: 'purchaseOutput' }],
|
||
version: 2,
|
||
locktime: 0,
|
||
composable: true,
|
||
},
|
||
},
|
||
|
||
/** No custom input templates — customer UTXOs are selected at funding time. */
|
||
inputs: {},
|
||
|
||
outputs: {
|
||
changeOutput: {
|
||
name: 'Change',
|
||
description: 'Funds returned as change.',
|
||
icon: 'receive',
|
||
lockingScript: 'merchantReceivingLockingScript',
|
||
},
|
||
purchaseOutput: {
|
||
name: 'Purchase Payment',
|
||
description: '$(<totalSatoshis>) sats to $(<merchantName>)',
|
||
icon: 'request',
|
||
|
||
roles: {
|
||
merchant: {
|
||
name: 'Payment Received',
|
||
description: 'Received $(<totalSatoshis>) sats for $(<receiptSummary>)',
|
||
},
|
||
customer: {
|
||
name: 'Payment Sent',
|
||
description: 'Sent $(<totalSatoshis>) sats for $(<receiptSummary>)',
|
||
},
|
||
},
|
||
|
||
lockingScript: 'merchantReceivingLockingScript',
|
||
valueSatoshis: '$(<totalSatoshis>)',
|
||
token: null,
|
||
},
|
||
},
|
||
|
||
lockingScripts: {
|
||
merchantReceivingLockingScript: {
|
||
name: 'Merchant Receive',
|
||
description: 'Funds received by the vending machine merchant.',
|
||
icon: 'address',
|
||
lockingType: 'p2pkh',
|
||
lockingBytecode: 'lockMerchantP2PKH',
|
||
unlockingBytecode: 'unlockMerchantP2PKH',
|
||
actions: [],
|
||
state: { variables: [], secrets: [] },
|
||
balance: {},
|
||
roles: {
|
||
merchant: {
|
||
state: {
|
||
variables: [],
|
||
secrets: ['merchantKey'],
|
||
},
|
||
actions: [],
|
||
balance: {
|
||
satoshis: true,
|
||
fungibleTokens: true,
|
||
nonfungibleTokens: true,
|
||
},
|
||
selectable: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
|
||
scripts: {
|
||
lockMerchantP2PKH:
|
||
'OP_DUP OP_HASH160 <$(<merchantKey.public_key> OP_HASH160)> OP_EQUALVERIFY OP_CHECKSIG',
|
||
unlockMerchantP2PKH:
|
||
'<merchantKey.schnorr_signature.all_outputs> <merchantKey.public_key>',
|
||
},
|
||
|
||
constants: {
|
||
dustLimit: {
|
||
name: 'Dust Limit',
|
||
description: 'Minimum satoshis for P2PKH outputs.',
|
||
type: 'integer',
|
||
value: 546,
|
||
},
|
||
},
|
||
|
||
variables: {
|
||
merchantKey: {
|
||
name: 'Merchant Private Key',
|
||
description: 'Private key for the vending machine merchant wallet.',
|
||
type: 'bytes',
|
||
hint: 'private_key',
|
||
},
|
||
totalSatoshis: {
|
||
name: 'Total Price',
|
||
description: 'Total purchase price in satoshis',
|
||
type: 'integer',
|
||
hint: 'satoshis',
|
||
},
|
||
orderId: {
|
||
name: 'Order ID',
|
||
description: 'Unique order identifier',
|
||
type: 'string',
|
||
},
|
||
merchantName: {
|
||
name: 'Merchant Name',
|
||
description: 'Display name of the vending machine',
|
||
type: 'string',
|
||
},
|
||
receiptSummary: {
|
||
name: 'Receipt Summary',
|
||
description: 'Human-readable list of purchased items',
|
||
type: 'string',
|
||
},
|
||
lineItemsJson: {
|
||
name: 'Line Items',
|
||
description: 'JSON-encoded line items for the purchase',
|
||
type: 'string',
|
||
},
|
||
},
|
||
|
||
icons: [
|
||
{ name: 'wallet', hash: '0000000000000000000000' },
|
||
{ name: 'owner', hash: '0000000000000000000000' },
|
||
{ name: 'sender', hash: '0000000000000000000000' },
|
||
{ name: 'request', hash: '0000000000000000000000' },
|
||
{ name: 'receive', hash: '0000000000000000000000' },
|
||
{ name: 'send', hash: '0000000000000000000000' },
|
||
],
|
||
|
||
scenarios: [
|
||
{
|
||
name: 'purchase items happy path',
|
||
description: 'Merchant requests payment for vending machine items.',
|
||
action: 'purchaseItems',
|
||
roles: [
|
||
{
|
||
role: 'merchant',
|
||
values: {
|
||
generated: {
|
||
merchantKey: 'KyRQa5pEXuzVcDwnXRLpYAascjchQW5DoxVRMbj4DTxS83573mz8',
|
||
},
|
||
variables: {
|
||
totalSatoshis: 3500,
|
||
orderId: 'order-demo-1',
|
||
merchantName: 'XO Snack Machine',
|
||
receiptSummary: '2× Cola, 1× Chips',
|
||
lineItemsJson: '[{"name":"Cola","qty":2},{"name":"Chips","qty":1}]',
|
||
},
|
||
secrets: {},
|
||
inputs: [],
|
||
outputs: [
|
||
{
|
||
lockingBytecode: '76a91475c715ecb74178fe87933e57e947e5e92d904b8188ac',
|
||
valueSatoshis: 3500,
|
||
},
|
||
],
|
||
},
|
||
},
|
||
{
|
||
role: 'customer',
|
||
values: {
|
||
generated: {},
|
||
variables: {},
|
||
secrets: {},
|
||
inputs: [],
|
||
outputs: [],
|
||
},
|
||
},
|
||
],
|
||
},
|
||
],
|
||
};
|