diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ab02e4..fea3692 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 0.4.0 + +- Add staging-only UPI platform discovery, quotes, maker registration and + deposit/intent previews. Production and preproduction continue rejecting UPI. +- Adopt Peer Cash 0.5.3 and SDK 0.14.1 with stable contracts 0.4.1, + including the current Cash App readiness risk window. +- Pin the packaged MCP launcher to the matching CLI release. +- Update the MCP transport’s Hono and qs dependencies to resolve current + production dependency audit findings. + ## 0.3.1 - Made free-form MCP inputs emit explicit recursive JSON schemas so strict MCP diff --git a/README.md b/README.md index eea1881..d776047 100644 --- a/README.md +++ b/README.md @@ -23,14 +23,14 @@ The package is `peer-protocol-cli`; do not install the unrelated unscoped `peer-cli` package. ```bash -npm install --global peer-protocol-cli@0.3.1 +npm install --global peer-protocol-cli@0.4.0 peer --help ``` Run without a global install: ```bash -npx -y peer-protocol-cli@0.3.1 quote --from USD --amount 100 --platform wise +npx -y peer-protocol-cli@0.4.0 quote --from USD --amount 100 --platform wise ``` Successful output is JSON by default. Errors always use the canonical JSON @@ -73,7 +73,7 @@ The current tool counts are generated in `agents/runtime-manifest.json`. "command": "npx", "args": [ "-y", - "peer-protocol-cli@0.3.1", + "peer-protocol-cli@0.4.0", "mcp", "--profile", "read-only" @@ -196,3 +196,12 @@ The documentation application for `agents.peer.xyz` is owned by `zkp2p/zkp2p-clients` at `clients/agents`; this repository owns the executable runtime, package manifests, portable skill, and generated contracts it documents. + +### Staging UPI + +UPI/INR is available only with `--env staging`. Use `peer config platforms +--env staging` to inspect that environment’s catalog and `peer quote --env +staging --from INR --amount 100 --platform upi` to request a quote. Deposit +creation, payee registration and intent previews accept the same staging +platform. Peerlytics analytics and Peer Cash keep their own supported rails; +this does not add UPI to those services. diff --git a/agents/tool-catalog.json b/agents/tool-catalog.json index d5810f3..d08eff1 100644 --- a/agents/tool-catalog.json +++ b/agents/tool-catalog.json @@ -3671,7 +3671,7 @@ "config", "platforms" ], - "description": "List supported payment platforms.", + "description": "List payment platforms supported in the selected environment.", "parameter_schema": { "type": "object", "properties": { diff --git a/llms.txt b/llms.txt index badd2c5..0ef5b95 100644 --- a/llms.txt +++ b/llms.txt @@ -24,7 +24,7 @@ catalogs own enumerations; prose must not duplicate a command inventory. ## MCP -Run `npx -y https://github.com/zkp2p/peer-cli/releases/download/v0.3.1/peer-protocol-cli-0.3.1.tgz mcp --profile `. +Run `npx -y peer-protocol-cli@0.4.0 mcp --profile `. - read-only: generic and Peer Cash reads; no signing or broadcasting - cash: complete peer_cash_* surface; prepares unsigned plans and never accepts diff --git a/mcp.json b/mcp.json index 4dd1bca..5bb2e79 100644 --- a/mcp.json +++ b/mcp.json @@ -6,7 +6,7 @@ "command": "npx", "args": [ "-y", - "peer-protocol-cli", + "peer-protocol-cli@0.4.0", "mcp", "--profile", "cash" diff --git a/package-lock.json b/package-lock.json index 98f2a7c..48935a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "peer-protocol-cli", - "version": "0.3.1", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "peer-protocol-cli", - "version": "0.3.1", + "version": "0.4.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.30.0", - "@zkp2p/cash": "^0.5.1", - "@zkp2p/sdk": "^0.14.0", + "@zkp2p/cash": "^0.5.3", + "@zkp2p/sdk": "^0.14.1", "commander": "^14.0.3", "viem": "^2.55.19", "zod": "^4.4.3" @@ -1741,7 +1741,6 @@ "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", @@ -2129,13 +2128,13 @@ } }, "node_modules/@zkp2p/cash": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@zkp2p/cash/-/cash-0.5.1.tgz", - "integrity": "sha512-ccbbKgh9s9SwdpsoP2vFGBpXguNE/evNdkP1WJt672excsWlfrY2MrrEPpvpnA5QaVCiWqLcVWo+lY9G/JyjpA==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@zkp2p/cash/-/cash-0.5.3.tgz", + "integrity": "sha512-EdHfd4DNiC2r+M6Kzl3sFnLQntbaJkXkvKnoZ6wq/aaYePpkXKnyOPm5EqdnGv7GNTG0jYtLPHduKVDsJKonDw==", "license": "MIT", "dependencies": { "@relayprotocol/relay-sdk": "^7.0.1", - "@zkp2p/sdk": "0.13.0", + "@zkp2p/sdk": "0.14.1", "zod": "^4.4.3" }, "engines": { @@ -2151,35 +2150,10 @@ } } }, - "node_modules/@zkp2p/cash/node_modules/@zkp2p/sdk": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@zkp2p/sdk/-/sdk-0.13.0.tgz", - "integrity": "sha512-i1MR+rzo+xmnO7I6tvbpPLkMChGdaBgqfGPQkDQh4bGL8sRWJ8RoBTFDbgpcjqKMJlDkCPzFObBaQJDuuSoMTA==", - "license": "MIT", - "dependencies": { - "@zkp2p/contracts-v2": "0.4.1-rc.9", - "@zkp2p/indexer-schema": "0.22.0", - "@zkp2p/zkp2p-attestation": "3.1.0", - "ethers": "^6.0.0", - "ox": "^0.11.1" - }, - "engines": { - "node": ">=22" - }, - "peerDependencies": { - "react": ">=16.8.0", - "viem": "^2.37.3" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - } - } - }, "node_modules/@zkp2p/contracts-v2": { - "version": "0.4.1-rc.9", - "resolved": "https://registry.npmjs.org/@zkp2p/contracts-v2/-/contracts-v2-0.4.1-rc.9.tgz", - "integrity": "sha512-ZT5L0TnXh7sNvYZTg5TlLBLJ24WiM6ENal1Uc9SucOFd7AIh+4/GetWM77Jy7TWTlHR1wBwjAixYe9GTfPaYdg==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@zkp2p/contracts-v2/-/contracts-v2-0.4.1.tgz", + "integrity": "sha512-1p6GFXIMf7pPZJN19tizelibxkvBD3J9ySuB87gRweUyw760Dx+B9YN0b5/9gwksHXzdQ1+ySWGE7McrdQPsBQ==", "license": "MIT", "dependencies": { "abitype": "^1.0.0" @@ -2198,12 +2172,12 @@ } }, "node_modules/@zkp2p/sdk": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@zkp2p/sdk/-/sdk-0.14.0.tgz", - "integrity": "sha512-BhGYH6P+VLb/9oVULTij2M8YK6e0haYolAAZjCUl098mC9tyosZRTuhKuTl6z/zrcG+4lB0XiamR2sP9i3gg+g==", + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@zkp2p/sdk/-/sdk-0.14.1.tgz", + "integrity": "sha512-TicILJ9eShOuZu2Wa27nHNvlqcTHQ4N/Ga3k2fsVGW2jQVn8wVrzlobPBt/nq93Z4Y89B8AERvHTlAvXEqegKw==", "license": "MIT", "dependencies": { - "@zkp2p/contracts-v2": "0.4.1-rc.9", + "@zkp2p/contracts-v2": "0.4.1", "@zkp2p/indexer-schema": "0.22.0", "@zkp2p/zkp2p-attestation": "3.1.0", "ethers": "^6.0.0", @@ -2277,7 +2251,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2922,7 +2895,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -2983,7 +2955,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3190,7 +3161,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@adraffy/ens-normalize": "1.11.1", "@noble/curves": "1.2.0", @@ -3267,7 +3237,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -3765,11 +3734,10 @@ } }, "node_modules/hono": { - "version": "4.13.4", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.4.tgz", - "integrity": "sha512-AGEwKIyRMHRv1t8Wjwa3LHxQ61X5CqrdFT+4BRNTpqS5aJNnpl5WLjADb7vFlJzI/8uK7T5QLVApCMQKNa3LgQ==", + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.7.tgz", + "integrity": "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -4633,7 +4601,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4692,7 +4659,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", @@ -4806,9 +4772,9 @@ } }, "node_modules/qs": { - "version": "6.15.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", - "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", "license": "BSD-3-Clause", "dependencies": { "es-define-property": "^1.0.1", @@ -5606,7 +5572,6 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -5689,7 +5654,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5751,7 +5715,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", @@ -5834,7 +5797,6 @@ "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", @@ -5933,7 +5895,6 @@ "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.7", @@ -6149,7 +6110,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -6184,7 +6144,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 44d8775..88db3d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "peer-protocol-cli", - "version": "0.3.1", + "version": "0.4.0", "mcpName": "io.github.zkp2p/peer", "description": "Agent-first CLI and MCP server for the Peer (ZKP2P) protocol and Peer Cash", "bin": { @@ -77,8 +77,8 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.30.0", - "@zkp2p/cash": "^0.5.1", - "@zkp2p/sdk": "^0.14.0", + "@zkp2p/cash": "^0.5.3", + "@zkp2p/sdk": "^0.14.1", "commander": "^14.0.3", "viem": "^2.55.19", "zod": "^4.4.3" diff --git a/plugin.json b/plugin.json index d799188..7df1a29 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "peer", - "version": "0.3.1", + "version": "0.4.0", "description": "Operate the Peer protocol and prepare custody-separated Peer Cash transactions through one CLI and MCP server.", "author": { "name": "Peer", diff --git a/server.json b/server.json index a881c59..f841a66 100644 --- a/server.json +++ b/server.json @@ -3,7 +3,7 @@ "name": "io.github.zkp2p/peer", "title": "Peer", "description": "Read Peer protocol, market, checkout, and Peer Cash data through one MCP server.", - "version": "0.3.1", + "version": "0.4.0", "websiteUrl": "https://agents.peer.xyz", "repository": { "url": "https://github.com/zkp2p/peer-cli", diff --git a/skills/peer-cash/SKILL.md b/skills/peer-cash/SKILL.md index 345345a..83c7656 100644 --- a/skills/peer-cash/SKILL.md +++ b/skills/peer-cash/SKILL.md @@ -14,7 +14,7 @@ broadcasts. Configure the host to run: ```bash -npx -y https://github.com/zkp2p/peer-cli/releases/download/v0.3.1/peer-protocol-cli-0.3.1.tgz mcp --profile cash +npx -y peer-protocol-cli@0.4.0 mcp --profile cash ``` Keep optional `PEER_CASH_*` values in the host's secret or MCP configuration, diff --git a/src/commands/config.ts b/src/commands/config.ts index 5b29906..0ee7f5e 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -1,7 +1,7 @@ import { privateKeyToAccount } from 'viem/accounts'; import type { CommandDefinition } from './framework.js'; import { readStoredConfig, replaceStoredConfig, writeStoredConfig } from '../sdk/config.js'; -import { SUPPORTED_CURRENCIES, SUPPORTED_ENVS, SUPPORTED_PLATFORMS } from '../utils/constants.js'; +import { SUPPORTED_CURRENCIES, SUPPORTED_ENVS, supportedPlatforms } from '../utils/constants.js'; import { createError } from '../output/errors.js'; import { ensureHexPrivateKey, ensureOneOf, ensureString } from '../utils/validation.js'; @@ -137,9 +137,9 @@ export const configDefinitions: CommandDefinition[] = [ }, { path: ['config', 'platforms'], - description: 'List supported payment platforms.', + description: 'List payment platforms supported in the selected environment.', readOnly: true, - handler: async () => [...SUPPORTED_PLATFORMS], + handler: async (_input, context) => [...supportedPlatforms(context.config.env)], }, { path: ['config', 'currencies'], diff --git a/src/commands/deposit.ts b/src/commands/deposit.ts index 858e32c..f7afdb4 100644 --- a/src/commands/deposit.ts +++ b/src/commands/deposit.ts @@ -109,7 +109,7 @@ function parseSupportedCurrencies(value: unknown, fieldName: string): string[] { return ensureSupportedCurrencyList(parseCsv(value as string | undefined), fieldName) ?? []; } -function parseConversionRates(input: Record): { currency: string; conversionRate: string }[][] { +function parseConversionRates(input: Record, processors: string[]): { currency: string; conversionRate: string }[][] { if (input.conversionRates) { const conversionRates = parseJsonArray(input.conversionRates, 'conversionRates') as { currency?: unknown; conversionRate: string }[][]; return conversionRates.map((entries, rowIndex) => entries.map((entry, entryIndex) => ({ @@ -118,7 +118,6 @@ function parseConversionRates(input: Record): { currency: strin }))); } - const processors = parseSupportedPlatforms(input.platforms, 'platforms'); const currencies = parseSupportedCurrencies(input.currencies, 'currencies'); if (processors.length === 0 || currencies.length === 0 || input.rate === undefined) { throw createError('VALIDATION_ERROR', 'Provide --conversion-rates JSON or --platforms, --currencies, and --rate.'); @@ -278,7 +277,7 @@ export const depositDefinitions: CommandDefinition[] = [ ['prepareCreateDeposit'], ['createDeposit'], async (input, context) => withUsdcAddress(input, context, async (token) => { - const processorNames = parseSupportedPlatforms(input.platforms, 'platforms'); + const processorNames = parseSupportedPlatforms(input.platforms, 'platforms', context.config.env); return { token: ensureAddress(token, 'token'), amount: parseUnits(ensurePositiveNumber(input.amount, 'amount').toString(), 6), @@ -288,7 +287,7 @@ export const depositDefinitions: CommandDefinition[] = [ }, processorNames, depositData: parseDepositDataEntries(input, processorNames), - conversionRates: parseConversionRates(input), + conversionRates: parseConversionRates(input, processorNames), delegate: input.delegate ? ensureAddress(input.delegate, 'delegate') : undefined, intentGuardian: input.intentGuardian ? ensureAddress(input.intentGuardian, 'intentGuardian') : undefined, retainOnEmpty: Boolean(input.retainOnEmpty), diff --git a/src/commands/intent.ts b/src/commands/intent.ts index 89daea6..55bc6d2 100644 --- a/src/commands/intent.ts +++ b/src/commands/intent.ts @@ -41,11 +41,11 @@ export const intentDefinitions: CommandDefinition[] = [ { name: 'payeeDetails', flags: '--payee-details ', description: 'Payee details string or serialized data.', schema: { type: 'string', description: 'Payee details.' } }, { name: 'processorIntentData', flags: '--processor-intent-data ', description: 'JSON processor intent data.', schema: { type: 'object', description: 'Processor intent data.' } }, ], - handler: sdkWriteHandler(['signalIntent'], async (input) => ({ + handler: sdkWriteHandler(['signalIntent'], async (input, context) => ({ depositId: asBigInt(input.deposit, 'deposit'), amount: parseUnits(ensurePositiveNumber(input.amount, 'amount').toString(), 6), toAddress: ensureAddress(input.to, 'to'), - processorName: ensureSupportedPlatform(input.platform, 'platform'), + processorName: ensureSupportedPlatform(input.platform, 'platform', context.config.env), payeeDetails: ensureString(input.payeeDetails, 'payeeDetails'), fiatCurrencyCode: ensureSupportedCurrency(input.currency, 'currency'), conversionRate: parseConversionRate(input.rate), diff --git a/src/commands/market.ts b/src/commands/market.ts index ea216c8..87e71b0 100644 --- a/src/commands/market.ts +++ b/src/commands/market.ts @@ -1,7 +1,7 @@ import type { CommandDefinition } from './framework.js'; import { sdkReadHandler } from './helpers.js'; import { createError } from '../output/errors.js'; -import { DEFAULT_CHAIN_ID, SUPPORTED_MARKET_PERIODS, SUPPORTED_PLATFORMS } from '../utils/constants.js'; +import { DEFAULT_CHAIN_ID, SUPPORTED_MARKET_PERIODS, supportedPlatforms } from '../utils/constants.js'; import { amountToUnits, ensureAddress, @@ -88,7 +88,7 @@ export const marketDefinitions: CommandDefinition[] = [ const recipient = input.recipient ? ensureAddress(input.recipient, 'recipient') : caller!; return [{ - paymentPlatforms: ensureSupportedPlatformList(parseCsv(input.platform as string | undefined) ?? [...SUPPORTED_PLATFORMS], 'platform'), + paymentPlatforms: ensureSupportedPlatformList(parseCsv(input.platform as string | undefined) ?? [...supportedPlatforms(context.config.env)], 'platform', context.config.env), fiatCurrency: ensureSupportedCurrency(input.from, 'from'), amount: amountToUnits(input.amount, 'amount', FIAT_AMOUNT_DECIMALS).toString(), isExactFiat: true, diff --git a/src/commands/payee-data.ts b/src/commands/payee-data.ts index e032b03..5bcad93 100644 --- a/src/commands/payee-data.ts +++ b/src/commands/payee-data.ts @@ -1,3 +1,4 @@ +import type { PeerEnv } from '../sdk/config.js'; import { createError } from '../output/errors.js'; import { parseJsonArray } from '../utils/parsing.js'; import { ensureSupportedPlatformList, parseCsv } from '../utils/validation.js'; @@ -29,8 +30,8 @@ function normalizePayeeDetailEntry(entry: Record, processorName return normalized; } -export function parseProcessorNames(value: unknown, fieldName: string): string[] { - return ensureSupportedPlatformList(parseCsv(value as string | undefined), fieldName) ?? []; +export function parseProcessorNames(value: unknown, fieldName: string, env: PeerEnv = 'production'): string[] { + return ensureSupportedPlatformList(parseCsv(value as string | undefined), fieldName, env) ?? []; } export function parsePayeeDepositData( diff --git a/src/commands/quote.ts b/src/commands/quote.ts index c4b0d74..7d89463 100644 --- a/src/commands/quote.ts +++ b/src/commands/quote.ts @@ -12,7 +12,7 @@ import { createError } from '../output/errors.js'; import type { CommandDefinition } from './framework.js'; import { sdkDirectWriteHandler, sdkReadHandler } from './helpers.js'; import { parsePayeeDepositData, parseProcessorNames } from './payee-data.js'; -import { DEFAULT_CHAIN_ID, SUPPORTED_PLATFORMS } from '../utils/constants.js'; +import { DEFAULT_CHAIN_ID, supportedPlatforms } from '../utils/constants.js'; const FIAT_AMOUNT_DECIMALS = 6; @@ -96,7 +96,7 @@ export const quoteDefinitions: CommandDefinition[] = [ const destinationToken = resolveDestinationToken(input.to, client.getUsdcAddress()); return [ { - paymentPlatforms: ensureSupportedPlatformList(parseCsv(input.platform as string | undefined) ?? [...SUPPORTED_PLATFORMS], 'platform'), + paymentPlatforms: ensureSupportedPlatformList(parseCsv(input.platform as string | undefined) ?? [...supportedPlatforms(context.config.env)], 'platform', context.config.env), fiatCurrency: ensureSupportedCurrency(input.from, 'from'), user: input.user ? ensureAddress(input.user, 'user') : walletClient.account?.address ?? zeroAddress, recipient: input.recipient ? ensureAddress(input.recipient, 'recipient') : walletClient.account?.address ?? zeroAddress, @@ -118,8 +118,8 @@ export const quoteDefinitions: CommandDefinition[] = [ { name: 'processors', flags: '--processors ', description: 'Comma-separated processor names.', schema: { type: 'string', description: 'Processor names.' } }, { name: 'depositData', flags: '--deposit-data ', description: 'JSON array of deposit detail objects.', schema: { type: 'array', description: 'Deposit details array.' } }, ], - handler: sdkDirectWriteHandler(['registerPayeeDetails'], async (input) => { - const processorNames = parseProcessorNames(input.processors, 'processors'); + handler: sdkDirectWriteHandler(['registerPayeeDetails'], async (input, context) => { + const processorNames = parseProcessorNames(input.processors, 'processors', context.config.env); return [{ processorNames, depositData: parsePayeeDepositData(input.depositData, processorNames), diff --git a/src/utils/constants.ts b/src/utils/constants.ts index eac6749..b89d993 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -23,6 +23,11 @@ export const SUPPORTED_PLATFORMS = [ 'wise', 'venmo', 'revolut', 'cashapp', 'mercadopago', 'zelle', 'paypal', 'monzo', 'alipay', 'chime', ] as const; export const LEGACY_PLATFORMS = ['luxon', 'n26'] as const; -export const KNOWN_PLATFORMS = [...SUPPORTED_PLATFORMS, ...LEGACY_PLATFORMS] as const; +export const STAGING_PLATFORMS = [...SUPPORTED_PLATFORMS, 'upi'] as const; +export const KNOWN_PLATFORMS = [...STAGING_PLATFORMS, ...LEGACY_PLATFORMS] as const; + +export function supportedPlatforms(env: (typeof SUPPORTED_ENVS)[number]) { + return env === 'staging' ? STAGING_PLATFORMS : SUPPORTED_PLATFORMS; +} export const DUMMY_PRIVATE_KEY = '0x59c6995e998f97a5a0044966f0945383f0d7d1f5eb53d3d16c23f0a3077ec12e' as const; diff --git a/src/utils/validation.ts b/src/utils/validation.ts index 16a312b..772af12 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -1,6 +1,7 @@ import { isAddress, parseUnits } from 'viem'; import { createError } from '../output/errors.js'; -import { SUPPORTED_CURRENCIES, SUPPORTED_PLATFORMS } from './constants.js'; +import { SUPPORTED_CURRENCIES, supportedPlatforms } from './constants.js'; +import type { PeerEnv } from '../sdk/config.js'; export function parseCsv(value: string | undefined): string[] | undefined { if (!value) return undefined; @@ -107,13 +108,13 @@ export function ensureSupportedCurrencyList(values: string[] | undefined, fieldN return values?.map((value, index) => ensureSupportedCurrency(value, `${fieldName}[${index}]`)); } -export function ensureSupportedPlatform(value: unknown, fieldName: string): (typeof SUPPORTED_PLATFORMS)[number] { +export function ensureSupportedPlatform(value: unknown, fieldName: string, env: PeerEnv = 'production') { const parsed = ensureString(value, fieldName).toLowerCase(); - return ensureSupportedValue(value, parsed, SUPPORTED_PLATFORMS, 'platform'); + return ensureSupportedValue(value, parsed, supportedPlatforms(env), 'platform'); } -export function ensureSupportedPlatformList(values: string[] | undefined, fieldName: string): string[] | undefined { - return values?.map((value, index) => ensureSupportedPlatform(value, `${fieldName}[${index}]`)); +export function ensureSupportedPlatformList(values: string[] | undefined, fieldName: string, env: PeerEnv = 'production'): string[] | undefined { + return values?.map((value, index) => ensureSupportedPlatform(value, `${fieldName}[${index}]`, env)); } export function amountToUnits(value: unknown, fieldName: string, decimals = 6): bigint { diff --git a/tests/commands.integration.test.ts b/tests/commands.integration.test.ts index 59c8057..743b6ff 100644 --- a/tests/commands.integration.test.ts +++ b/tests/commands.integration.test.ts @@ -192,6 +192,56 @@ describe('registry-backed command handlers', () => { }); }); + it.each(['production', 'preproduction', 'staging'] as const)( + 'scopes UPI quote discovery to staging in %s', + async (env) => { + const runtime = createMockRuntime({ config: { env } }); + const platforms = await call(['config', 'platforms'], {}, runtime); + expect(platforms).toEqual(env === 'staging' + ? expect.arrayContaining(['upi', 'cashapp']) + : expect.not.arrayContaining(['upi'])); + const result = await run(['quote'], { from: 'INR', amount: 100, platform: 'UPI' }, runtime); + if (env === 'staging') { + expect(result.ok).toBe(true); + expect(runtime.calls.find((entry) => entry.path === 'getQuote')?.args[0]).toMatchObject({ + paymentPlatforms: ['upi'], fiatCurrency: 'INR', amount: '100000000', + }); + } else { + expect(result).toMatchObject({ ok: false, error: { code: 'VALIDATION_ERROR' } }); + expect(runtime.calls.some((entry) => entry.path === 'getQuote')).toBe(false); + } + }, + ); + + it('passes staging UPI through quote comparison and payee registration', async () => { + const runtime = createMockRuntime({ config: { env: 'staging' } }); + await call(['market', 'compare'], { from: 'INR', amount: 100, platform: 'upi' }, runtime); + expect(runtime.calls.find((entry) => entry.path === 'getQuote')?.args[0]).toMatchObject({ + paymentPlatforms: ['upi'], fiatCurrency: 'INR', + }); + await call(['payee', 'register'], { processors: 'upi', depositData: [{ vpa: 'maker@bank' }] }, runtime); + expect(runtime.calls.find((entry) => entry.path === 'registerPayeeDetails')?.args[0]).toMatchObject({ + processorNames: ['upi'], depositData: [{ vpa: 'maker@bank' }], + }); + }); + + it('prepares staging UPI deposits and intents without broadcasting', async () => { + const runtime = createMockRuntime({ config: { env: 'staging' } }); + await call(['deposit', 'create'], { + amount: 10, min: 1, max: 10, platforms: 'upi', currencies: 'INR', rate: 90, + depositData: [{ vpa: 'maker@bank' }], + }, runtime); + expect(runtime.calls.find((entry) => entry.path === 'prepareCreateDeposit')?.args[0]).toMatchObject({ + processorNames: ['upi'], conversionRates: [[{ currency: 'INR', conversionRate: '90000000000000000000' }]], + }); + await call(['intent', 'create'], { + deposit: '1', amount: 1, platform: 'upi', currency: 'INR', rate: 90, + to: '0x1111111111111111111111111111111111111111', payeeDetails: 'maker@bank', + }, runtime); + expect(runtime.calls.some((entry) => entry.path === 'signalIntent')).toBe(false); + expect(runtime.preparedResults).toHaveLength(2); + }); + it('validates supported currencies and platforms locally before outbound calls', async () => { const quoteRuntime = createMockRuntime(); const invalidQuote = await run(['quote'], {