diff --git a/src/legacy/legacy-cli.ts b/src/legacy/legacy-cli.ts index 80f9994..c4354e7 100644 --- a/src/legacy/legacy-cli.ts +++ b/src/legacy/legacy-cli.ts @@ -1511,15 +1511,19 @@ const COMMAND_HELP: Record = { }, 'invoice-pay': { usage: 'invoice-pay [--amount ] [--target-index ]', - description: 'Pay an invoice. By default pays the remaining amount for the first target. For multi-target invoices, specify --target-index.', + description: 'Pay an invoice. By default pays the remaining amount for the first target. Use --amount for partial or over-payment; the coin is implicit in the invoice.', flags: [ - { flag: '--amount ', description: 'Amount to pay in smallest units (positive integer). Defaults to remaining amount.' }, + { flag: '--amount ', description: 'Amount to pay in HUMAN units of the invoice\'s coin (e.g. "50" = 50 UCT, "0.5" = 0.5 UCT). Defaults to remaining amount needed to cover the target.' }, { flag: '--target-index ', description: 'Target index for multi-target invoices (0-based)', default: '0' }, ], examples: [ - 'npm run cli -- invoice-pay a1b2c3d4', - 'npm run cli -- invoice-pay a1b2c3d4 --amount 500000', - 'npm run cli -- invoice-pay a1b2c3d4 --target-index 1 --amount 250000', + 'sphere invoice pay a1b2c3d4', + 'sphere invoice pay a1b2c3d4 --amount 0.5', + 'sphere invoice pay a1b2c3d4 --target-index 1 --amount 25', + ], + notes: [ + '--amount is in human units (whole coins, fractional allowed) — matches `payments send` and `invoice create --asset`.', + 'NFT targets reject --amount (use full-pay form without --amount).', ], }, 'invoice-return': { @@ -4793,11 +4797,39 @@ async function main(): Promise { const payParamsMut: Record = { targetIndex }; if (amountIdx2 !== -1 && args[amountIdx2 + 1]) { - const rawAmount = args[amountIdx2 + 1]; - if (!/^[1-9][0-9]*$/.test(rawAmount!)) { - failWithHelp('invoice-pay', `invalid amount "${rawAmount}" — must be a positive integer in smallest units (no decimals, no leading zeros)`); + const rawAmount = args[amountIdx2 + 1]!; + // Issue #36 — canonical UX: `--amount ` is in HUMAN units + // (e.g. "50" = 50 whole UCT, "0.5" = 0.5 UCT), matching + // `payments send 50 UCT` and `invoice create --asset 50 UCT`. + // The coin is implicit in the invoice's target/asset entry. + // Convert to smallest-unit integer here before handing off; the + // SDK's PayInvoiceParams.amount is a raw atom-count string. + const invoiceRef = sphere.accounting!.getInvoice(invoiceId); + if (!invoiceRef) { + failWithHelp('invoice-pay', `invoice ${invoiceId} not found in memory`); + } + const target = invoiceRef.terms.targets[targetIndex]; + if (!target) { + failWithHelp('invoice-pay', `--target-index ${targetIndex} out of range (invoice has ${invoiceRef.terms.targets.length} target(s))`); + } + // assetIndex is currently CLI-fixed to 0 (see "Out of scope" in #36). + const asset = target.assets[0]; + if (!asset || !asset.coin) { + failWithHelp('invoice-pay', '--amount only applies to coin-asset targets, not NFT'); + } + const [coinSymbol] = asset.coin; + const { decimals } = resolveCoin(coinSymbol); + let smallest: bigint; + try { + smallest = toSmallestUnit(rawAmount, decimals); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + failWithHelp('invoice-pay', `invalid amount "${rawAmount}" — ${msg}`); + } + if (smallest <= 0n) { + failWithHelp('invoice-pay', `invalid amount "${rawAmount}" — must be positive`); } - payParamsMut['amount'] = rawAmount; + payParamsMut['amount'] = smallest.toString(); } const payParams = payParamsMut as unknown as PayInvoiceParams;