diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1cbf69..18ca400 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,22 +41,30 @@ jobs: # integrity — a branch pointer can be force-pushed or rebased, # silently changing the code CI builds against. # - # This SHA is the tip of sphere-sdk `main` ("merge: PR #395 #394 - # automated CID delivery re-enabled + 512 KiB inline cap + demo - # playbook"). That commit exports the symbols `src/shared/sphere- - # providers.ts` consumes from `@unicitylabs/sphere-sdk/impl/nodejs`: - # `createUxfCarPublisher`, `DEFAULT_IPFS_GATEWAYS`, - # `PublishToIpfsCallback`. It also exposes `AccountingModule. - # deliverInvoice`, which `invoice-deliver` (PR #18 / issue #226) - # calls. Pinning to `main` (rather than the previous integration- - # branch tip 02cb4550) avoids the "unable to read tree" failure - # when an integration tip is rebased away. + # This SHA is the tip of sphere-sdk `main` (PR #402 / #401 — invoice + # OUTBOX verifier wiring on top of PR #400 / #397's route-invoice- + # delivery-through-the-token-pipeline architectural fix). + # + # Why this matters for sphere-cli: #397 replaces the bespoke + # `invoice_delivery:` NIP-17 DM with the standard TOKEN_TRANSFER + # pipeline (Nostr kind 31113). The `manual-test-accounting- + # roundtrip.sh` soak (and any cross-device invoice consumer) only + # works against this new pipeline; the prior pin (3f3dadf, the + # CID-delivery merge BEFORE #397) leaves the receiver decoder + # unable to ingest invoice DMs. Local repro: alice's `invoice + # list` returns "No invoices found" forever even though bob's + # `invoice deliver` reports `sent: 1, failed: 0`. + # + # This SHA also keeps the exports `src/shared/sphere-providers.ts` + # consumes (`createUxfCarPublisher`, `DEFAULT_IPFS_GATEWAYS`, + # `PublishToIpfsCallback`) and `AccountingModule.deliverInvoice` + # that `invoice-deliver` (PR #18 / issue #226) calls. # # Bump this SHA when a new sphere-sdk commit is required; remove # this whole workaround once sphere-sdk publishes v0.7.1+ to npm # with the post-extraction exports. env: - SPHERE_SDK_SHA: 3f3dadf9d03eb29db87f062921751f24bfefdec8 + SPHERE_SDK_SHA: d9498445e96be63f6a474f2607372eb4c16247d2 run: | git clone https://github.com/unicity-sphere/sphere-sdk.git ../../sphere-sdk # The pinned SHA may not be on a branch tip after future merges; diff --git a/src/legacy/legacy-cli.ts b/src/legacy/legacy-cli.ts index b76dbc0..80f9994 100644 --- a/src/legacy/legacy-cli.ts +++ b/src/legacy/legacy-cli.ts @@ -4499,22 +4499,33 @@ async function main(): Promise { const assets: InvoiceRequestedAsset[] = []; // Collect ALL `--asset ` occurrences (multi-asset). + // Canonical UX (#32): the amount is in HUMAN-READABLE units (e.g. + // "7 UCT" = 7 whole UCT, "0.5 BTC" = 0.5 BTC), matching what + // `payments send` accepts. Convert to smallest-unit integers here + // before handing off to the SDK; AccountingModule.createInvoice + // itself treats the amount as a raw atom count (decimals: 0). for (let i = 0; i < args.length; i++) { if (args[i] !== '--asset') continue; const pair = consumeAssetPair(args, i + 1); if (!pair) { failWithHelp('invoice-create', '--asset expects two positional tokens: --asset '); } - if (!/^[1-9][0-9]*$/.test(pair.amount)) { - failWithHelp('invoice-create', `invalid amount "${pair.amount}" — must be a positive integer in smallest units (no decimals, no leading zeros)`); + // resolveCoin fails fast on unknown symbols. Use it for both the + // canonical symbol (handed to the SDK) and the decimals (used + // for the human → smallest unit conversion). + const { symbol: resolvedSymbol, decimals } = resolveCoin(pair.coin); + let smallest: bigint; + try { + smallest = toSmallestUnit(pair.amount, decimals); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + failWithHelp('invoice-create', `invalid amount "${pair.amount}" — ${msg}`); + } + if (smallest <= 0n) { + failWithHelp('invoice-create', `invalid amount "${pair.amount}" — must be positive`); } - // AccountingModule.createInvoice validates coinId as - // /^[A-Za-z0-9]+$/ with length ≤20 — i.e. it expects the - // human-readable symbol (UCT, USDU, ...), NOT the 64-char - // hex token-type id that `payments.send` uses. resolveCoin - // fails fast on unknown symbols; we hand the SYMBOL through. - const { symbol: resolvedSymbol } = resolveCoin(pair.coin); - assets.push({ coin: [resolvedSymbol, pair.amount] }); + // SDK convention: coin: [symbol, smallest-unit-as-decimal-string]. + assets.push({ coin: [resolvedSymbol, smallest.toString()] }); } // NFT input — collect when no --asset was provided. if (assets.length === 0 && nftId) { @@ -4823,19 +4834,27 @@ async function main(): Promise { if (!pair) { failWithHelp('invoice-return', '--asset expects two positional tokens: --asset '); } - if (!/^[1-9][0-9]*$/.test(pair.amount)) { - failWithHelp('invoice-return', `invalid amount "${pair.amount}" — must be a positive integer string (smallest unit, no leading zeros, e.g. 1000000)`); + // Canonical UX (#32): amount is in HUMAN units (e.g. "100 UCT" = 100 + // whole UCT). Convert to smallest-unit integer before handing off to + // the SDK. Same SDK convention as invoice-create — the + // AccountingModule's ReturnPaymentParams.coinId is the symbol + // (UCT, USDU, ...), not the 64-char hex. + const { symbol: returnSymbol, decimals: returnDecimals } = resolveCoin(pair.coin); + let returnSmallest: bigint; + try { + returnSmallest = toSmallestUnit(pair.amount, returnDecimals); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + failWithHelp('invoice-return', `invalid amount "${pair.amount}" — ${msg}`); + } + if (returnSmallest <= 0n) { + failWithHelp('invoice-return', `invalid amount "${pair.amount}" — must be positive`); } - // Same SDK convention as invoice-create — the AccountingModule's - // ReturnPaymentParams.coinId is the symbol (UCT, USDU, ...), not - // the 64-char hex. resolveCoin still validates that the symbol - // is known to the TokenRegistry. - const returnCoinId = resolveCoin(pair.coin).symbol; const returnParams: ReturnPaymentParams = { recipient: args[recipientIdx + 1], - amount: pair.amount, - coinId: returnCoinId, + amount: returnSmallest.toString(), + coinId: returnSymbol, }; const result = await sphere.accounting!.returnInvoicePayment(invoiceId, returnParams);