diff --git a/.agents/swap.md b/.agents/swap.md index 99c650c8..b18c2d6d 100644 --- a/.agents/swap.md +++ b/.agents/swap.md @@ -68,8 +68,10 @@ getTrackingUrl?(execution): string | undefined ### Flashnet AMM (`shared/services/transfer-service-flashnet.ts`) - **Pairs**: BTC <-> USDB on Spark (both directions) -- **Model**: Instant atomic swap via `@flashnet/sdk`. No deposit address. Executes atomically in `executeTransfer()`. +- **Model**: Two-phase instant swap. `executeTransfer()` stages params in `pendingSwaps` and returns `status: 'pending'` *without* moving funds. `executeInstantSwap(executionId)` then runs `FlashnetClient.executeSwap()` and returns `status: 'completed'`. This split lets the UI / MCP show fee + impact before commit. - **API**: `FlashnetClient.simulateSwap()` for quotes, `executeSwap()` for execution +- **Fees**: Derived from the pool's configured `lpFeeBps + hostFeeBps` (read from the cached `AmmPool` after `listPools`), as `amountIn × totalFeeBps / 10000`. `TransferQuote.feeBaseUnits` is then in the input asset's smallest units. **We deliberately do NOT use `SimulateSwapResponse.feePaidAssetIn`** — despite the name suggesting input-asset units, empirically the field is denominated in the OUTPUT asset's smallest units, which on a real BTC→USDB swap caused us to report a ~38% fee on a pool actually configured for 5 bps. The pool-bps approach is unit-unambiguous and direction-symmetric. **Price impact is NOT a fee** — exposed separately on `TransferQuote.priceImpactPct`. +- **Slippage**: `maxSlippageBps: 300` + hard `minAmountOut = receiveAmount * 0.97`. - **SparkWallet access**: `SparkWallet.getSDKWalletForAccount(accountNumber)` static getter - No tracking URL (instant) @@ -112,7 +114,7 @@ getTrackingUrl?(execution): string | undefined - `mobile/app/TransferDetails.tsx` — Timeline from `getTimelineSteps()`. Detail rows: provider, status, transfer ID, addresses, deposit/claim txids. Claim button for NativeDeposit (disabled during auto-claim). "View Online" button when tracking URL available. ## Shared Hooks -- `useTransferService(storage)` — singleton TransferServiceManager (`shared/hooks/useTransferService.ts`). Also exports: `setNativeDepositSwapsFetcher`, `setNativeDepositClaimExecutor`, `startAutoClaimMonitor`, `stopAutoClaimMonitor`, `processAutoClaimsNow` +- `useTransferService(storage)` — singleton TransferServiceManager (`shared/hooks/useTransferService.ts`). Also exports: `setNativeDepositSwapsFetcher`, `setNativeDepositClaimExecutor`, `startAutoClaimMonitor`, `stopAutoClaimMonitor`, `processAutoClaimsNow`, `setFlashnetAccountNumber`, `getTransferServiceManager` (non-hook singleton accessor — used by MCP). - `useTransactionHistory(network, account)` — merges transfers into tx list, deduplicates (`shared/hooks/useTransactionHistory.ts`) - `useAssetExchangeRate(assetId)` — fiat rate for transfer assets (`shared/hooks/useAssetExchangeRate.ts`) - `useAssetBalance(assetId, account, bg)` — unified native/token balance (`shared/hooks/useAssetBalance.ts`) @@ -123,6 +125,15 @@ getTrackingUrl?(execution): string | undefined - **Interface**: `InterfaceSendQuotable` (`shared/class/wallets/interface-send-quotable.ts`) - **Implementations**: `EvmWallet`, `BreezWallet` +## MCP swap surface (`mobile/src/features/mcp/modules/mcp-calls.ts`) + +Two tools expose Flashnet to remote AI agents. They run on `MCP_BALANCE_ACCOUNT_NUMBER` (= 4) so they don't touch the user's primary account. + +- **`get_swap_quote(send_asset, receive_asset, send_amount_base_units)`** — `send_asset` / `receive_asset` are strict `AssetId` strings (currently `native:spark` / `token:spark:usdb`). Internally: `lazyInitWallet(NETWORK_SPARK, 4)` → `setFlashnetAccountNumber(4)` → `manager.getQuote()` → `manager.executeTransfer()` (Flashnet: stages params, no funds movement — in-memory only, NOT persisted). Returns `{ quote_id, send_amount_base_units, receive_amount_base_units, fee_base_units, fee_asset, fee_ticker, price_impact_pct, rate, estimated_time_seconds, expires_at_unix, service }`. +- **`execute_swap(quote_id)`** — `manager.executeInstantSwap(quote_id)` → `commitTransfer()` (persists completed row). The manager looks up the owning service from `executionOwners` (populated in `executeTransfer`), so the agent only needs `quote_id`. Idempotency: the manager pops the owner entry on execute and the owning service pops the quote from its pending map; replay fails with *"No pending swap found"*. Quote expiry is enforced by Flashnet's internal `PENDING_SWAP_TTL` (5 min) on top of `TransferQuote.expiresAt` (60 s). + +Adding more pairs is purely additive: extend `MCP_SWAP_ASSET_IDS` and ensure the relevant provider quotes the pair and implements `executeInstantSwap`. The manager routes by `executionOwners` so any such provider works without touching the MCP layer. + ## Tests - `shared/tests/unit-vi/transfer-service-sideshift.test.ts` - `shared/tests/unit-vi/transfer-service-garden.test.ts` @@ -135,6 +146,7 @@ getTrackingUrl?(execution): string | undefined - `shared/tests/unit-vi/use-asset-balance.test.ts` - `shared/tests/integration-vi/sideshift-transfer.test.ts` - `shared/tests/integration-vi/garden-transfer.test.ts` +- `mobile/src/tests/unit-vi/mcp-calls-swap.test.ts` — MCP `get_swap_quote` / `execute_swap` handlers - `mobile/.maestro/swap.yml` — e2e flow with Fake service ## Adding a New Transfer Service diff --git a/ext/package-lock.json b/ext/package-lock.json index cc1d1226..55f98ec6 100644 --- a/ext/package-lock.json +++ b/ext/package-lock.json @@ -13,8 +13,8 @@ "@arkade-os/sdk": "0.4.10", "@bitcoinerlab/secp256k1": "1.2.0", "@breeztech/breez-sdk-liquid": "0.12.2", - "@buildonspark/spark-sdk": "0.7.1", - "@flashnet/sdk": "0.5.7", + "@buildonspark/spark-sdk": "0.8.0", + "@flashnet/sdk": "0.5.9", "@metamask/eth-sig-util": "8.2.0", "@noble/hashes": "1.7.1", "@noble/secp256k1": "1.6.3", @@ -287,6 +287,7 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -877,6 +878,7 @@ "integrity": "sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, @@ -1721,6 +1723,7 @@ "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-module-imports": "^7.28.6", @@ -2263,23 +2266,15 @@ "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@buildonspark/spark-sdk": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@buildonspark/spark-sdk/-/spark-sdk-0.7.1.tgz", - "integrity": "sha512-4EkIlkXpCfojUVYwuHFKj4y8hTUm3/1T4Gf0ruz7Q5DlKkmjtr5KY2oNxvebfjMrctVj5ZnMQZXZl6pSLLMngg==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@buildonspark/spark-sdk/-/spark-sdk-0.8.0.tgz", + "integrity": "sha512-Ks/KtXht/3S6G4kIPQ59SRAAqG7Mlgw80GfNTnLTzePKW+tJse7prnJje27Dethz3XcT7Kzob58Rpaed+V1grA==", "license": "Apache-2.0", "dependencies": { "@bufbuild/protobuf": "^2.2.5", - "@lightsparkdev/core": "^1.4.9", + "@lightsparkdev/core": "^1.5.0", "@noble/curves": "^1.9.7", "@noble/hashes": "^1.7.0", - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^2.0.0", - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.203.0", - "@opentelemetry/instrumentation-undici": "^0.14.0", - "@opentelemetry/sdk-trace-base": "^2.0.0", - "@opentelemetry/sdk-trace-node": "^2.0.1", - "@opentelemetry/sdk-trace-web": "^2.0.1", "@scure/base": "^1.2.4", "@scure/bip32": "^1.6.2", "@scure/bip39": "^1.5.4", @@ -2288,7 +2283,7 @@ "abortcontroller-polyfill": "^1.7.8", "async-mutex": "^0.5.0", "bare-crypto": "^1.9.2", - "bare-fetch": "^2.4.1", + "bare-fetch": "^3.0.0", "buffer": "^6.0.3", "eventemitter3": "^5.0.1", "js-base64": "^3.7.7", @@ -2296,7 +2291,6 @@ "nice-grpc": "^2.1.10", "nice-grpc-client-middleware-retry": "^3.1.10", "nice-grpc-common": "^2.0.2", - "nice-grpc-opentelemetry": "^0.1.18", "nice-grpc-web": "^3.3.7", "ts-proto": "2.8.3", "ua-parser-js": "^2.0.6", @@ -3293,9 +3287,9 @@ } }, "node_modules/@flashnet/sdk": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@flashnet/sdk/-/sdk-0.5.7.tgz", - "integrity": "sha512-hm//AKWeYOT9eYoDG80fkr5EDsY1fo12T9/rxBooXLRJwWubmF+XzSQ530ca6QKEw3I0USlPm9Gfihk0MaUX1g==", + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@flashnet/sdk/-/sdk-0.5.9.tgz", + "integrity": "sha512-+1FQ/l+4pcq7lSgcBXzZagUG66cCniEhVnWVLqLD0ZHZMuhcgViSYjMUwh8/6pCKDeOqY1wfEQ3Ngrsnuup7+w==", "license": "MIT", "dependencies": { "bech32": "^2.0.0", @@ -3628,9 +3622,9 @@ "license": "MIT" }, "node_modules/@lightsparkdev/core": { - "version": "1.4.9", - "resolved": "https://registry.npmjs.org/@lightsparkdev/core/-/core-1.4.9.tgz", - "integrity": "sha512-nAtAq+oEITHF9C3o410Ll8RpAwsIaWElBXJBCYMDKK3JeHBMccKg7/1TkEOgD/YPQ8fXOFv0nMjQnvWCzExwGA==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lightsparkdev/core/-/core-1.5.2.tgz", + "integrity": "sha512-7EiV/Ld+IqAQJYvSLN2gS6E/UrcCCQ/H4voUz+nAPUDSk8U1P06afTKALh1FeizAXIzqxt1jFQWmQlXPSXmxIA==", "license": "Apache-2.0", "dependencies": { "@noble/curves": "^1.9.7", @@ -3898,162 +3892,6 @@ "node": ">= 8" } }, - "node_modules/@opentelemetry/api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", - "license": "Apache-2.0", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/api-logs": { - "version": "0.203.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", - "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/context-async-hooks": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.4.0.tgz", - "integrity": "sha512-jn0phJ+hU7ZuvaoZE/8/Euw3gvHJrn2yi+kXrymwObEPVPjtwCmkvXDRQCWli+fCTTF/aSOtXaLr7CLIvv3LQg==", - "license": "Apache-2.0", - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/core": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz", - "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/instrumentation": { - "version": "0.203.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", - "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.203.0", - "import-in-the-middle": "^1.8.1", - "require-in-the-middle": "^7.1.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-undici": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.14.0.tgz", - "integrity": "sha512-2HN+7ztxAReXuxzrtA3WboAKlfP5OsPA57KQn2AdYZbJ3zeRPcLXyW4uO/jpLE6PLm0QRtmeGCmfYpqRlwgSwg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.203.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.7.0" - } - }, - "node_modules/@opentelemetry/resources": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.4.0.tgz", - "integrity": "sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.4.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.4.0.tgz", - "integrity": "sha512-WH0xXkz/OHORDLKqaxcUZS0X+t1s7gGlumr2ebiEgNZQl2b0upK2cdoD0tatf7l8iP74woGJ/Kmxe82jdvcWRw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.4.0", - "@opentelemetry/resources": "2.4.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-node": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.4.0.tgz", - "integrity": "sha512-MBc2l04hZPYygnWPT38UiOPy9ueutPqmJ47z0m9IKuoVQh3MblmbSgwspjhdHagZLfSfmlzhWR1xtbgVNmjX2A==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/context-async-hooks": "2.4.0", - "@opentelemetry/core": "2.4.0", - "@opentelemetry/sdk-trace-base": "2.4.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-web": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-web/-/sdk-trace-web-2.4.0.tgz", - "integrity": "sha512-1FYg7qnrgTugPev51SehxCp0v9J4P97MJn2MaXQ8QK//psfyLDorKAAC3LmSIhq7XaC726WSZ/Wm69r8NdjIsA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.4.0", - "@opentelemetry/sdk-trace-base": "2.4.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.39.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.39.0.tgz", - "integrity": "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, "node_modules/@parcel/watcher": { "version": "2.5.4", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.4.tgz", @@ -5771,6 +5609,7 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -5924,6 +5763,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.9.tgz", "integrity": "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -5955,6 +5795,7 @@ "integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -6136,6 +5977,7 @@ "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -6906,7 +6748,9 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6914,15 +6758,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", - "license": "MIT", - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/acorn-import-phases": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", @@ -6958,6 +6793,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -7614,6 +7450,16 @@ "dev": true, "license": "MIT" }, + "node_modules/bare-buffer": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/bare-buffer/-/bare-buffer-3.6.0.tgz", + "integrity": "sha512-/maRWEQ2eBkVNMbNFVsq1pHXJYVj4Y3AixwruB24eKZDs5Gtu0fixzvjYmBIuTsBMtVH5Yb27pQO9BhFa+IlIQ==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "bare": ">=1.20.0" + } + }, "node_modules/bare-crypto": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.13.0.tgz", @@ -7655,54 +7501,57 @@ } }, "node_modules/bare-fetch": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/bare-fetch/-/bare-fetch-2.5.1.tgz", - "integrity": "sha512-BdJie1S9y3TW0pzF6Q/dP95QDjlUPXexiJWSnKFIM/OHID6ITJk2XEQQ25rsGqwLqxQ4felfGkj13mC/ao27mg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bare-fetch/-/bare-fetch-3.0.1.tgz", + "integrity": "sha512-OWC8Z62E8JmomltTkXt9cCPMPj2DNi2vp66FOj3BkglNKNshZuk8n98Ba3afUxrrM4kv9/eMzh9+U9dXZSQyOg==", "license": "Apache-2.0", "dependencies": { - "bare-form-data": "^1.1.3", - "bare-http1": "^4.0.2", - "bare-https": "^2.0.0", - "bare-stream": "^2.7.0", + "bare-form-data": "^1.2.0", + "bare-http1": "^4.5.2", + "bare-https": "^3.0.0", + "bare-mime": "^1.0.0", + "bare-stream": "^2.9.1", + "bare-url": "^2.4.0", "bare-zlib": "^1.3.0" }, "peerDependencies": { - "bare-buffer": "*", - "bare-url": "*" + "bare-abort-controller": "*", + "bare-buffer": "*" }, "peerDependenciesMeta": { - "bare-buffer": { + "bare-abort-controller": { "optional": true }, - "bare-url": { + "bare-buffer": { "optional": true } } }, "node_modules/bare-form-data": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/bare-form-data/-/bare-form-data-1.1.6.tgz", - "integrity": "sha512-q1IN7dVo/lEhTlVkVQdULZvoBx6eTI94co0NtO7/A3JLFL/aZGA1wAHgcNEPrlkqTK9jTEdtzQXSoqGzlVjzgg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/bare-form-data/-/bare-form-data-1.2.2.tgz", + "integrity": "sha512-DQyAkCf5mgKT07orewuvaJfoalw7RBSHia4wgkrG7+seI6aHLB+r6gMRdCGrlO+BmCqMwgTeHAHxDU2NrOjQnQ==", "license": "Apache-2.0", "dependencies": { + "bare-buffer": "^3.6.0", "bare-stream": "^2.6.5" } }, "node_modules/bare-http-parser": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.0.1.tgz", - "integrity": "sha512-A3LTDTcELcmNJ3g5liIaS038v/BQxOhA9cjhBESn7eoV7QCuMoIRBKLDadDe08flxyLbxI2f+1l2MZ/5+HnKPA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.1.4.tgz", + "integrity": "sha512-DL+7fTEUWzAEj/Baw9e/BwNAidARbxuUf5bonQ/Wt3VPUdJNyf562ydaono9ZkQBAUw0NydzYEI97rSs/93ruA==", "license": "Apache-2.0" }, "node_modules/bare-http1": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.2.2.tgz", - "integrity": "sha512-XL1aeSSjKNIIjyo5czdWZb7C1fVWiL7Y0CPLLgKy6fWMOXZksLY84QjRmvKTAfRN2beNQuIexccCWknI8sStNg==", + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.5.6.tgz", + "integrity": "sha512-31OAwMkSU+z1VuUOCk65hx3aWQgzCfH/zQ6LGxbJtmiy2Czsw0+uvOBM9YkqaL6zUSTSYG2pLbL0v/TjME3Buw==", "license": "Apache-2.0", "dependencies": { "bare-events": "^2.6.0", - "bare-http-parser": "^1.0.0", - "bare-stream": "^2.3.0", + "bare-http-parser": "^1.1.1", + "bare-stream": "^2.10.0", "bare-tcp": "^2.2.0" }, "peerDependencies": { @@ -7719,20 +7568,26 @@ } }, "node_modules/bare-https": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-2.1.2.tgz", - "integrity": "sha512-Q+TTydUDsuKQJvh8dX2dvOXCR9fM3xR5TBmKaFrs5p7Lj7XbKX7v4vIUJ36H0SXg2xCOQxXKIbjwrLg5tfJNYg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-3.0.0.tgz", + "integrity": "sha512-W1GRSCzn+xXKf5bMcPs/hg6Ga1bxPqb7owGfS+tvlBQfPe5Q2STcanRuKZrgU60v5uKrhXH5cgWwM+DLqvXZgQ==", "license": "Apache-2.0", "dependencies": { - "bare-http1": "^4.0.0", + "bare-http1": "^4.4.0", "bare-tcp": "^2.2.0", - "bare-tls": "^2.0.0" + "bare-tls": "^3.0.0" } }, + "node_modules/bare-mime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/bare-mime/-/bare-mime-1.0.0.tgz", + "integrity": "sha512-lUOswzBkfqham4zjLDueKOd4Qj3gS56BiZ3q2f0g0adoFhF+HFNupvTUfZBWoicl7fWJ7Hp2RUZjmkY47dxxOQ==", + "license": "Apache-2.0" + }, "node_modules/bare-net": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.2.0.tgz", - "integrity": "sha512-UF7cAbHsGE+H6uEqWF5IULBow1x58chZz4g3ALgHtv7wZsFcCbRDt0JKWEumf5Oma3QWS1Q6aLi0Rpll8RElMg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.3.1.tgz", + "integrity": "sha512-MypSqDKpDU2Xt7FIfazn5yGvRnV09gFcIPHGWstW0gxuzA4tucTcwJSZeos97C4F89vtU5oGwXDN/HrGN6Y4Jw==", "license": "Apache-2.0", "dependencies": { "bare-events": "^2.2.2", @@ -7741,10 +7596,28 @@ "bare-tcp": "^2.0.0" } }, + "node_modules/bare-os": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz", + "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, "node_modules/bare-pipe": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.2.tgz", - "integrity": "sha512-btXtZLlABEDRp50cfLj9iweISqAJSNMCjeq5v0v9tBY2a7zSSqmfa2ZoE1ki2qxAvubagLUqw6VDifpsuI/qmg==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.5.tgz", + "integrity": "sha512-6OfxaG8JSkRh3Gc4hzHRsxNt+yu2PpN7lrv1V+T78GdknWQkVGwiEvu4m+1nbfk8cMVQ0TGxRvQ90XA4rhnTuw==", "license": "Apache-2.0", "dependencies": { "bare-events": "^2.0.0", @@ -7755,18 +7628,23 @@ } }, "node_modules/bare-stream": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", - "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz", + "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==", "license": "Apache-2.0", "dependencies": { - "streamx": "^2.21.0" + "streamx": "^2.25.0", + "teex": "^1.0.1" }, "peerDependencies": { + "bare-abort-controller": "*", "bare-buffer": "*", "bare-events": "*" }, "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, "bare-buffer": { "optional": true }, @@ -7776,9 +7654,9 @@ } }, "node_modules/bare-tcp": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.2.tgz", - "integrity": "sha512-bYnw1AhzGlfLOD4nTceUXkhhgznZKvDuwjX1Au0VWaVitwqG40oaTvvhEQVCcK3FEwjRTiukUzHnAFsYXUI+3Q==", + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.13.tgz", + "integrity": "sha512-4KQPgqYugvK6QxcSnVGbl87XslBebxmXlv7Glf4M9iwwoSCDKtYmC1t6zsMctTNhzKXbWCId7mB4R9qLWj3JMw==", "license": "Apache-2.0", "dependencies": { "bare-dns": "^2.0.4", @@ -7790,9 +7668,9 @@ } }, "node_modules/bare-tls": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-2.1.7.tgz", - "integrity": "sha512-h6wcNXQdBeTX7fed9tjPp0/9cA/QfcBTv3ItgjnbUk4rWAU8bEFalZCZnUDdCK/t9zrNfJ+yvcPx4D/1Y6biyA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-3.1.4.tgz", + "integrity": "sha512-0zmlDYkHjsU3h/I3Z69QZetBZibMUlcLI+OtHhQHeso/73si7/wN58EslxmG3SRx/b5Vx2kzqexlEBMDRvFveg==", "license": "Apache-2.0", "dependencies": { "bare-net": "^2.0.1", @@ -7802,10 +7680,20 @@ "bare": ">=1.7.0" } }, + "node_modules/bare-url": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.3.tgz", + "integrity": "sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/bare-zlib": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/bare-zlib/-/bare-zlib-1.3.1.tgz", - "integrity": "sha512-VP93GFzhrTdWh9mXNocn7XsP/nF5JQluiiSsbTvsQ4yIYlhEHRMF9lQmZZDXwzK9PNYaVGUV1bdQuqp0Mj7MHw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/bare-zlib/-/bare-zlib-1.3.3.tgz", + "integrity": "sha512-rXNczo+SQg6cn20olmh/mUiGeJK9maipFH/zI/QwYgwhEmOns1R7fl1GV5apNO+aAp4x2d4uUa7HLhO4mhOnBQ==", "license": "Apache-2.0", "dependencies": { "bare-stream": "^2.0.0" @@ -8444,6 +8332,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -8798,12 +8687,6 @@ "node": ">= 0.10" } }, - "node_modules/cjs-module-lexer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", - "license": "MIT" - }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", @@ -9529,9 +9412,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.19", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", - "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", "license": "MIT" }, "node_modules/debug": { @@ -10766,6 +10649,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -10822,6 +10706,7 @@ "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -12482,10 +12367,11 @@ "license": "MIT" }, "node_modules/graphql": { - "version": "16.12.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", - "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.0.tgz", + "integrity": "sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==", "license": "MIT", + "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -13009,18 +12895,6 @@ "node": ">=4" } }, - "node_modules/import-in-the-middle": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.15.0.tgz", - "integrity": "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==", - "license": "Apache-2.0", - "dependencies": { - "acorn": "^8.14.0", - "acorn-import-attributes": "^1.9.5", - "cjs-module-lexer": "^1.2.2", - "module-details-from-path": "^1.0.3" - } - }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -13119,6 +12993,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 10" @@ -13247,6 +13122,7 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -14637,12 +14513,6 @@ "node": ">=18" } }, - "node_modules/module-details-from-path": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", - "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", - "license": "MIT" - }, "node_modules/module-lookup-amd": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/module-lookup-amd/-/module-lookup-amd-9.0.5.tgz", @@ -14794,19 +14664,6 @@ "ts-error": "^1.0.6" } }, - "node_modules/nice-grpc-opentelemetry": { - "version": "0.1.20", - "resolved": "https://registry.npmjs.org/nice-grpc-opentelemetry/-/nice-grpc-opentelemetry-0.1.20.tgz", - "integrity": "sha512-dRH6lmm8OgqY21WRo9BP6cHHqIhbG5UT/INFne0qIDSlSseYc6s1+qNTE3Up0z/4zY50V8tVTOH30yyhkwNXTw==", - "license": "MIT", - "dependencies": { - "@opentelemetry/api": "^1.8.0", - "@opentelemetry/semantic-conventions": "^1.22.0", - "abort-controller-x": "^0.4.0", - "ipaddr.js": "^2.0.1", - "nice-grpc-common": "^2.0.2" - } - }, "node_modules/nice-grpc-web": { "version": "3.3.9", "resolved": "https://registry.npmjs.org/nice-grpc-web/-/nice-grpc-web-3.3.9.tgz", @@ -15521,6 +15378,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, "license": "MIT" }, "node_modules/path-to-regexp": { @@ -15829,6 +15687,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -15996,6 +15855,7 @@ "integrity": "sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -16328,6 +16188,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -16337,6 +16198,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -16350,6 +16212,7 @@ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -16587,20 +16450,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-in-the-middle": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz", - "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "module-details-from-path": "^1.0.3", - "resolve": "^1.22.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, "node_modules/requirejs": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.8.tgz", @@ -16640,6 +16489,7 @@ "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.1", @@ -16967,6 +16817,7 @@ "integrity": "sha512-y5LWb0IlbO4e97Zr7c3mlpabcbBtS+ieiZ9iwDooShpFKWXf62zz5pEPdwrLYm+Bxn1fnbwFGzHuCLSA9tBmrw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -17928,9 +17779,9 @@ } }, "node_modules/streamx": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", - "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", "license": "MIT", "dependencies": { "events-universal": "^1.0.0", @@ -18223,6 +18074,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -18274,6 +18126,15 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, "node_modules/terser": { "version": "5.46.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", @@ -18388,18 +18249,18 @@ } }, "node_modules/text-decoder": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", - "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", "license": "Apache-2.0", "dependencies": { "b4a": "^1.6.4" } }, "node_modules/text-decoder/node_modules/b4a": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", - "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", "license": "Apache-2.0", "peerDependencies": { "react-native-b4a": "*" @@ -18548,6 +18409,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -18830,7 +18692,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsutils": { "version": "3.21.0", @@ -18861,6 +18724,7 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -18920,6 +18784,7 @@ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, "license": "(MIT OR CC0-1.0)", + "peer": true, "engines": { "node": ">=10" }, @@ -19029,6 +18894,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -19367,6 +19233,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -19483,6 +19350,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -19639,6 +19507,7 @@ "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -19688,6 +19557,7 @@ "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.6.1", "@webpack-cli/configtest": "^3.0.1", @@ -19798,6 +19668,7 @@ "integrity": "sha512-9Gyu2F7+bg4Vv+pjbovuYDhHX+mqdqITykfzdM9UyKqKHlsE5aAjRhR+oOEfXW5vBeu8tarzlJFIZva4ZjAdrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", @@ -19922,6 +19793,7 @@ "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.13.0" } @@ -20180,6 +20052,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, diff --git a/ext/package.json b/ext/package.json index 580aba0a..58658b2a 100755 --- a/ext/package.json +++ b/ext/package.json @@ -25,8 +25,8 @@ "@arkade-os/sdk": "0.4.10", "@bitcoinerlab/secp256k1": "1.2.0", "@breeztech/breez-sdk-liquid": "0.12.2", - "@buildonspark/spark-sdk": "0.7.1", - "@flashnet/sdk": "0.5.7", + "@buildonspark/spark-sdk": "0.8.0", + "@flashnet/sdk": "0.5.9", "@metamask/eth-sig-util": "8.2.0", "@noble/hashes": "1.7.1", "@noble/secp256k1": "1.6.3", diff --git a/mobile/.eas/workflows/submit-android.yml b/mobile/.eas/workflows/submit-android.yml index f63c63c3..547cfccd 100644 --- a/mobile/.eas/workflows/submit-android.yml +++ b/mobile/.eas/workflows/submit-android.yml @@ -2,6 +2,16 @@ on: push: branches: ['master'] +concurrency: + cancel_in_progress: true + group: ${{ workflow.filename }}-${{ github.ref }} + +# Submit jobs still run `npm ci` on the worker; pin Node to match `eas.json` +# `build.base.node` so install matches production builds (see eas-cli#3589). +defaults: + tools: + node: '22.14.0' + jobs: build_android: name: Build Android app diff --git a/mobile/.eas/workflows/submit-ios.yml b/mobile/.eas/workflows/submit-ios.yml index d92fda0e..c64be969 100644 --- a/mobile/.eas/workflows/submit-ios.yml +++ b/mobile/.eas/workflows/submit-ios.yml @@ -2,6 +2,16 @@ on: push: branches: ['master'] +concurrency: + cancel_in_progress: true + group: ${{ workflow.filename }}-${{ github.ref }} + +# TestFlight jobs still run `npm ci` on the worker; pin Node to match `eas.json` +# `build.base.node` so install matches production builds (see eas-cli#3589). +defaults: + tools: + node: '22.14.0' + jobs: build_ios: name: Build iOS app diff --git a/mobile/app/transfer/confirm.tsx b/mobile/app/transfer/confirm.tsx index 0c01ab33..cc77d35c 100644 --- a/mobile/app/transfer/confirm.tsx +++ b/mobile/app/transfer/confirm.tsx @@ -187,7 +187,7 @@ export default function TransferConfirm() { // Instant swap (e.g. Flashnet): execute the actual swap now, then commit if (execution.type === EXECUTION_INSTANT) { - const completed = await transferService.executeInstantSwap(execution.id, execution.serviceName); + const completed = await transferService.executeInstantSwap(execution.id); executionRef.current = completed; await transferService.commitTransfer(completed); setPreparedExecution(undefined); diff --git a/mobile/package-lock.json b/mobile/package-lock.json index f50d58b6..734d33c6 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -16,9 +16,9 @@ "@breeztech/breez-sdk-liquid": "0.12.2", "@breeztech/breez-sdk-liquid-react-native": "0.12.2", "@bugsnag/expo": "55.0.0", - "@buildonspark/spark-sdk": "0.7.1", + "@buildonspark/spark-sdk": "0.8.0", "@expo/vector-icons": "15.0.3", - "@flashnet/sdk": "0.5.7", + "@flashnet/sdk": "0.5.9", "@gorhom/bottom-sheet": "5.2.8", "@metamask/eth-sig-util": "8.2.0", "@modelcontextprotocol/sdk": "^1.29.0", @@ -1999,23 +1999,15 @@ "license": "MIT" }, "node_modules/@buildonspark/spark-sdk": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@buildonspark/spark-sdk/-/spark-sdk-0.7.1.tgz", - "integrity": "sha512-4EkIlkXpCfojUVYwuHFKj4y8hTUm3/1T4Gf0ruz7Q5DlKkmjtr5KY2oNxvebfjMrctVj5ZnMQZXZl6pSLLMngg==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@buildonspark/spark-sdk/-/spark-sdk-0.8.0.tgz", + "integrity": "sha512-Ks/KtXht/3S6G4kIPQ59SRAAqG7Mlgw80GfNTnLTzePKW+tJse7prnJje27Dethz3XcT7Kzob58Rpaed+V1grA==", "license": "Apache-2.0", "dependencies": { "@bufbuild/protobuf": "^2.2.5", - "@lightsparkdev/core": "^1.4.9", + "@lightsparkdev/core": "^1.5.0", "@noble/curves": "^1.9.7", "@noble/hashes": "^1.7.0", - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^2.0.0", - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.203.0", - "@opentelemetry/instrumentation-undici": "^0.14.0", - "@opentelemetry/sdk-trace-base": "^2.0.0", - "@opentelemetry/sdk-trace-node": "^2.0.1", - "@opentelemetry/sdk-trace-web": "^2.0.1", "@scure/base": "^1.2.4", "@scure/bip32": "^1.6.2", "@scure/bip39": "^1.5.4", @@ -2024,7 +2016,7 @@ "abortcontroller-polyfill": "^1.7.8", "async-mutex": "^0.5.0", "bare-crypto": "^1.9.2", - "bare-fetch": "^2.4.1", + "bare-fetch": "^3.0.0", "buffer": "^6.0.3", "eventemitter3": "^5.0.1", "js-base64": "^3.7.7", @@ -2032,7 +2024,6 @@ "nice-grpc": "^2.1.10", "nice-grpc-client-middleware-retry": "^3.1.10", "nice-grpc-common": "^2.0.2", - "nice-grpc-opentelemetry": "^0.1.18", "nice-grpc-web": "^3.3.7", "ts-proto": "2.8.3", "ua-parser-js": "^2.0.6", @@ -4616,9 +4607,9 @@ } }, "node_modules/@flashnet/sdk": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@flashnet/sdk/-/sdk-0.5.7.tgz", - "integrity": "sha512-hm//AKWeYOT9eYoDG80fkr5EDsY1fo12T9/rxBooXLRJwWubmF+XzSQ530ca6QKEw3I0USlPm9Gfihk0MaUX1g==", + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@flashnet/sdk/-/sdk-0.5.9.tgz", + "integrity": "sha512-+1FQ/l+4pcq7lSgcBXzZagUG66cCniEhVnWVLqLD0ZHZMuhcgViSYjMUwh8/6pCKDeOqY1wfEQ3Ngrsnuup7+w==", "license": "MIT", "dependencies": { "bech32": "^2.0.0", @@ -5648,9 +5639,9 @@ } }, "node_modules/@lightsparkdev/core": { - "version": "1.4.9", - "resolved": "https://registry.npmjs.org/@lightsparkdev/core/-/core-1.4.9.tgz", - "integrity": "sha512-nAtAq+oEITHF9C3o410Ll8RpAwsIaWElBXJBCYMDKK3JeHBMccKg7/1TkEOgD/YPQ8fXOFv0nMjQnvWCzExwGA==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lightsparkdev/core/-/core-1.5.2.tgz", + "integrity": "sha512-7EiV/Ld+IqAQJYvSLN2gS6E/UrcCCQ/H4voUz+nAPUDSk8U1P06afTKALh1FeizAXIzqxt1jFQWmQlXPSXmxIA==", "license": "Apache-2.0", "dependencies": { "@noble/curves": "^1.9.7", @@ -5948,163 +5939,6 @@ "node": ">=12.4.0" } }, - "node_modules/@opentelemetry/api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/api-logs": { - "version": "0.203.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.203.0.tgz", - "integrity": "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/context-async-hooks": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.4.0.tgz", - "integrity": "sha512-jn0phJ+hU7ZuvaoZE/8/Euw3gvHJrn2yi+kXrymwObEPVPjtwCmkvXDRQCWli+fCTTF/aSOtXaLr7CLIvv3LQg==", - "license": "Apache-2.0", - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/core": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz", - "integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/instrumentation": { - "version": "0.203.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", - "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.203.0", - "import-in-the-middle": "^1.8.1", - "require-in-the-middle": "^7.1.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-undici": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.14.0.tgz", - "integrity": "sha512-2HN+7ztxAReXuxzrtA3WboAKlfP5OsPA57KQn2AdYZbJ3zeRPcLXyW4uO/jpLE6PLm0QRtmeGCmfYpqRlwgSwg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.203.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.7.0" - } - }, - "node_modules/@opentelemetry/resources": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.4.0.tgz", - "integrity": "sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.4.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.4.0.tgz", - "integrity": "sha512-WH0xXkz/OHORDLKqaxcUZS0X+t1s7gGlumr2ebiEgNZQl2b0upK2cdoD0tatf7l8iP74woGJ/Kmxe82jdvcWRw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.4.0", - "@opentelemetry/resources": "2.4.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-node": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.4.0.tgz", - "integrity": "sha512-MBc2l04hZPYygnWPT38UiOPy9ueutPqmJ47z0m9IKuoVQh3MblmbSgwspjhdHagZLfSfmlzhWR1xtbgVNmjX2A==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/context-async-hooks": "2.4.0", - "@opentelemetry/core": "2.4.0", - "@opentelemetry/sdk-trace-base": "2.4.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-web": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-web/-/sdk-trace-web-2.4.0.tgz", - "integrity": "sha512-1FYg7qnrgTugPev51SehxCp0v9J4P97MJn2MaXQ8QK//psfyLDorKAAC3LmSIhq7XaC726WSZ/Wm69r8NdjIsA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.4.0", - "@opentelemetry/sdk-trace-base": "2.4.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz", - "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, "node_modules/@pkgr/core": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", @@ -9090,15 +8924,6 @@ "acorn-walk": "^8.0.2" } }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", - "license": "MIT", - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -9551,9 +9376,9 @@ } }, "node_modules/b4a": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", - "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", "license": "Apache-2.0", "peerDependencies": { "react-native-b4a": "*" @@ -9951,6 +9776,16 @@ "zxing-wasm": "3.0.1" } }, + "node_modules/bare-buffer": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/bare-buffer/-/bare-buffer-3.6.0.tgz", + "integrity": "sha512-/maRWEQ2eBkVNMbNFVsq1pHXJYVj4Y3AixwruB24eKZDs5Gtu0fixzvjYmBIuTsBMtVH5Yb27pQO9BhFa+IlIQ==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "bare": ">=1.20.0" + } + }, "node_modules/bare-crypto": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.13.0.tgz", @@ -9992,54 +9827,57 @@ } }, "node_modules/bare-fetch": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/bare-fetch/-/bare-fetch-2.5.1.tgz", - "integrity": "sha512-BdJie1S9y3TW0pzF6Q/dP95QDjlUPXexiJWSnKFIM/OHID6ITJk2XEQQ25rsGqwLqxQ4felfGkj13mC/ao27mg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bare-fetch/-/bare-fetch-3.0.1.tgz", + "integrity": "sha512-OWC8Z62E8JmomltTkXt9cCPMPj2DNi2vp66FOj3BkglNKNshZuk8n98Ba3afUxrrM4kv9/eMzh9+U9dXZSQyOg==", "license": "Apache-2.0", "dependencies": { - "bare-form-data": "^1.1.3", - "bare-http1": "^4.0.2", - "bare-https": "^2.0.0", - "bare-stream": "^2.7.0", + "bare-form-data": "^1.2.0", + "bare-http1": "^4.5.2", + "bare-https": "^3.0.0", + "bare-mime": "^1.0.0", + "bare-stream": "^2.9.1", + "bare-url": "^2.4.0", "bare-zlib": "^1.3.0" }, "peerDependencies": { - "bare-buffer": "*", - "bare-url": "*" + "bare-abort-controller": "*", + "bare-buffer": "*" }, "peerDependenciesMeta": { - "bare-buffer": { + "bare-abort-controller": { "optional": true }, - "bare-url": { + "bare-buffer": { "optional": true } } }, "node_modules/bare-form-data": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/bare-form-data/-/bare-form-data-1.1.6.tgz", - "integrity": "sha512-q1IN7dVo/lEhTlVkVQdULZvoBx6eTI94co0NtO7/A3JLFL/aZGA1wAHgcNEPrlkqTK9jTEdtzQXSoqGzlVjzgg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/bare-form-data/-/bare-form-data-1.2.2.tgz", + "integrity": "sha512-DQyAkCf5mgKT07orewuvaJfoalw7RBSHia4wgkrG7+seI6aHLB+r6gMRdCGrlO+BmCqMwgTeHAHxDU2NrOjQnQ==", "license": "Apache-2.0", "dependencies": { + "bare-buffer": "^3.6.0", "bare-stream": "^2.6.5" } }, "node_modules/bare-http-parser": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.0.1.tgz", - "integrity": "sha512-A3LTDTcELcmNJ3g5liIaS038v/BQxOhA9cjhBESn7eoV7QCuMoIRBKLDadDe08flxyLbxI2f+1l2MZ/5+HnKPA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.1.4.tgz", + "integrity": "sha512-DL+7fTEUWzAEj/Baw9e/BwNAidARbxuUf5bonQ/Wt3VPUdJNyf562ydaono9ZkQBAUw0NydzYEI97rSs/93ruA==", "license": "Apache-2.0" }, "node_modules/bare-http1": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.2.2.tgz", - "integrity": "sha512-XL1aeSSjKNIIjyo5czdWZb7C1fVWiL7Y0CPLLgKy6fWMOXZksLY84QjRmvKTAfRN2beNQuIexccCWknI8sStNg==", + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.5.6.tgz", + "integrity": "sha512-31OAwMkSU+z1VuUOCk65hx3aWQgzCfH/zQ6LGxbJtmiy2Czsw0+uvOBM9YkqaL6zUSTSYG2pLbL0v/TjME3Buw==", "license": "Apache-2.0", "dependencies": { "bare-events": "^2.6.0", - "bare-http-parser": "^1.0.0", - "bare-stream": "^2.3.0", + "bare-http-parser": "^1.1.1", + "bare-stream": "^2.10.0", "bare-tcp": "^2.2.0" }, "peerDependencies": { @@ -10056,20 +9894,26 @@ } }, "node_modules/bare-https": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-2.1.2.tgz", - "integrity": "sha512-Q+TTydUDsuKQJvh8dX2dvOXCR9fM3xR5TBmKaFrs5p7Lj7XbKX7v4vIUJ36H0SXg2xCOQxXKIbjwrLg5tfJNYg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-3.0.0.tgz", + "integrity": "sha512-W1GRSCzn+xXKf5bMcPs/hg6Ga1bxPqb7owGfS+tvlBQfPe5Q2STcanRuKZrgU60v5uKrhXH5cgWwM+DLqvXZgQ==", "license": "Apache-2.0", "dependencies": { - "bare-http1": "^4.0.0", + "bare-http1": "^4.4.0", "bare-tcp": "^2.2.0", - "bare-tls": "^2.0.0" + "bare-tls": "^3.0.0" } }, + "node_modules/bare-mime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/bare-mime/-/bare-mime-1.0.0.tgz", + "integrity": "sha512-lUOswzBkfqham4zjLDueKOd4Qj3gS56BiZ3q2f0g0adoFhF+HFNupvTUfZBWoicl7fWJ7Hp2RUZjmkY47dxxOQ==", + "license": "Apache-2.0" + }, "node_modules/bare-net": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.2.0.tgz", - "integrity": "sha512-UF7cAbHsGE+H6uEqWF5IULBow1x58chZz4g3ALgHtv7wZsFcCbRDt0JKWEumf5Oma3QWS1Q6aLi0Rpll8RElMg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.3.1.tgz", + "integrity": "sha512-MypSqDKpDU2Xt7FIfazn5yGvRnV09gFcIPHGWstW0gxuzA4tucTcwJSZeos97C4F89vtU5oGwXDN/HrGN6Y4Jw==", "license": "Apache-2.0", "dependencies": { "bare-events": "^2.2.2", @@ -10078,10 +9922,28 @@ "bare-tcp": "^2.0.0" } }, + "node_modules/bare-os": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz", + "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, "node_modules/bare-pipe": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.2.tgz", - "integrity": "sha512-btXtZLlABEDRp50cfLj9iweISqAJSNMCjeq5v0v9tBY2a7zSSqmfa2ZoE1ki2qxAvubagLUqw6VDifpsuI/qmg==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.5.tgz", + "integrity": "sha512-6OfxaG8JSkRh3Gc4hzHRsxNt+yu2PpN7lrv1V+T78GdknWQkVGwiEvu4m+1nbfk8cMVQ0TGxRvQ90XA4rhnTuw==", "license": "Apache-2.0", "dependencies": { "bare-events": "^2.0.0", @@ -10092,18 +9954,23 @@ } }, "node_modules/bare-stream": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", - "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz", + "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==", "license": "Apache-2.0", "dependencies": { - "streamx": "^2.21.0" + "streamx": "^2.25.0", + "teex": "^1.0.1" }, "peerDependencies": { + "bare-abort-controller": "*", "bare-buffer": "*", "bare-events": "*" }, "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, "bare-buffer": { "optional": true }, @@ -10113,9 +9980,9 @@ } }, "node_modules/bare-tcp": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.2.tgz", - "integrity": "sha512-bYnw1AhzGlfLOD4nTceUXkhhgznZKvDuwjX1Au0VWaVitwqG40oaTvvhEQVCcK3FEwjRTiukUzHnAFsYXUI+3Q==", + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.13.tgz", + "integrity": "sha512-4KQPgqYugvK6QxcSnVGbl87XslBebxmXlv7Glf4M9iwwoSCDKtYmC1t6zsMctTNhzKXbWCId7mB4R9qLWj3JMw==", "license": "Apache-2.0", "dependencies": { "bare-dns": "^2.0.4", @@ -10127,9 +9994,9 @@ } }, "node_modules/bare-tls": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-2.1.7.tgz", - "integrity": "sha512-h6wcNXQdBeTX7fed9tjPp0/9cA/QfcBTv3ItgjnbUk4rWAU8bEFalZCZnUDdCK/t9zrNfJ+yvcPx4D/1Y6biyA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-3.1.4.tgz", + "integrity": "sha512-0zmlDYkHjsU3h/I3Z69QZetBZibMUlcLI+OtHhQHeso/73si7/wN58EslxmG3SRx/b5Vx2kzqexlEBMDRvFveg==", "license": "Apache-2.0", "dependencies": { "bare-net": "^2.0.1", @@ -10139,10 +10006,20 @@ "bare": ">=1.7.0" } }, + "node_modules/bare-url": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.3.tgz", + "integrity": "sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/bare-zlib": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/bare-zlib/-/bare-zlib-1.3.1.tgz", - "integrity": "sha512-VP93GFzhrTdWh9mXNocn7XsP/nF5JQluiiSsbTvsQ4yIYlhEHRMF9lQmZZDXwzK9PNYaVGUV1bdQuqp0Mj7MHw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/bare-zlib/-/bare-zlib-1.3.3.tgz", + "integrity": "sha512-rXNczo+SQg6cn20olmh/mUiGeJK9maipFH/zI/QwYgwhEmOns1R7fl1GV5apNO+aAp4x2d4uUa7HLhO4mhOnBQ==", "license": "Apache-2.0", "dependencies": { "bare-stream": "^2.0.0" @@ -11313,6 +11190,7 @@ "version": "1.4.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, "license": "MIT" }, "node_modules/cli-cursor": { @@ -12023,9 +11901,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.19", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", - "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", "license": "MIT" }, "node_modules/debug": { @@ -15848,9 +15726,9 @@ "license": "ISC" }, "node_modules/graphql": { - "version": "16.12.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", - "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.0.tgz", + "integrity": "sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==", "license": "MIT", "peer": true, "engines": { @@ -16250,18 +16128,6 @@ "node": ">=4" } }, - "node_modules/import-in-the-middle": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.15.0.tgz", - "integrity": "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==", - "license": "Apache-2.0", - "dependencies": { - "acorn": "^8.14.0", - "acorn-import-attributes": "^1.9.5", - "cjs-module-lexer": "^1.2.2", - "module-details-from-path": "^1.0.3" - } - }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -16369,15 +16235,6 @@ "node": ">= 12" } }, - "node_modules/ipaddr.js": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", - "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, "node_modules/is-arguments": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", @@ -20601,12 +20458,6 @@ "node": ">=10" } }, - "node_modules/module-details-from-path": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", - "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", - "license": "MIT" - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -20712,19 +20563,6 @@ "ts-error": "^1.0.6" } }, - "node_modules/nice-grpc-opentelemetry": { - "version": "0.1.20", - "resolved": "https://registry.npmjs.org/nice-grpc-opentelemetry/-/nice-grpc-opentelemetry-0.1.20.tgz", - "integrity": "sha512-dRH6lmm8OgqY21WRo9BP6cHHqIhbG5UT/INFne0qIDSlSseYc6s1+qNTE3Up0z/4zY50V8tVTOH30yyhkwNXTw==", - "license": "MIT", - "dependencies": { - "@opentelemetry/api": "^1.8.0", - "@opentelemetry/semantic-conventions": "^1.22.0", - "abort-controller-x": "^0.4.0", - "ipaddr.js": "^2.0.1", - "nice-grpc-common": "^2.0.2" - } - }, "node_modules/nice-grpc-web": { "version": "3.3.9", "resolved": "https://registry.npmjs.org/nice-grpc-web/-/nice-grpc-web-3.3.9.tgz", @@ -23245,20 +23083,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-in-the-middle": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz", - "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "module-details-from-path": "^1.0.3", - "resolve": "^1.22.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -24576,9 +24400,9 @@ } }, "node_modules/streamx": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", - "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", "license": "MIT", "dependencies": { "events-universal": "^1.0.0", @@ -24919,6 +24743,15 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, "node_modules/terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", @@ -25060,9 +24893,9 @@ } }, "node_modules/text-decoder": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", - "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", "license": "Apache-2.0", "dependencies": { "b4a": "^1.6.4" diff --git a/mobile/package.json b/mobile/package.json index d2914d87..55f65796 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -51,9 +51,9 @@ "@breeztech/breez-sdk-liquid": "0.12.2", "@breeztech/breez-sdk-liquid-react-native": "0.12.2", "@bugsnag/expo": "55.0.0", - "@buildonspark/spark-sdk": "0.7.1", + "@buildonspark/spark-sdk": "0.8.0", "@expo/vector-icons": "15.0.3", - "@flashnet/sdk": "0.5.7", + "@flashnet/sdk": "0.5.9", "@gorhom/bottom-sheet": "5.2.8", "@metamask/eth-sig-util": "8.2.0", "@modelcontextprotocol/sdk": "^1.29.0", diff --git a/mobile/src/features/mcp/components/McpAgentDashboard.tsx b/mobile/src/features/mcp/components/McpAgentDashboard.tsx index 78213a71..44353540 100644 --- a/mobile/src/features/mcp/components/McpAgentDashboard.tsx +++ b/mobile/src/features/mcp/components/McpAgentDashboard.tsx @@ -179,7 +179,7 @@ export function McpAgentDashboard() { Permissions - 5 out of 5 + 6 out of 6 diff --git a/mobile/src/features/mcp/components/McpPermissionsModal.tsx b/mobile/src/features/mcp/components/McpPermissionsModal.tsx index 79e13c1c..22e19405 100644 --- a/mobile/src/features/mcp/components/McpPermissionsModal.tsx +++ b/mobile/src/features/mcp/components/McpPermissionsModal.tsx @@ -26,6 +26,7 @@ const PERMISSION_ROWS: McpPermissionRow[] = [ { key: 'send_tokens', label: 'Send tokens' }, { key: 'send_nfts', label: 'Send NFTs' }, { key: 'pay_invoices', label: 'Pay Lightning invoices' }, + { key: 'execute_swaps', label: 'Execute swaps' }, ]; export default function McpPermissionsModal() { diff --git a/mobile/src/features/mcp/modules/mcp-calls.ts b/mobile/src/features/mcp/modules/mcp-calls.ts index 97ecbc35..eb15d818 100644 --- a/mobile/src/features/mcp/modules/mcp-calls.ts +++ b/mobile/src/features/mcp/modules/mcp-calls.ts @@ -3,6 +3,7 @@ * No HTTP, tunnel, or session lifecycle (that stays in `mcp.ts`). */ +import BigNumber from 'bignumber.js'; import * as bolt11 from 'bolt11'; import { isValidSparkAddress } from '@buildonspark/spark-sdk'; import * as z from 'zod'; @@ -14,8 +15,11 @@ import { walletCanHaveTokens } from '@shared/class/wallets/interface-can-have-to import { walletSupportsLightning } from '@shared/class/wallets/interface-lightning-wallet'; import { exchangeRateFetcher } from '@shared/hooks/useExchangeRate'; import { balanceFetcher } from '@shared/hooks/useBalance'; +import { getTransferServiceManager, setFlashnetAccountNumber, useTransferService } from '@shared/hooks/useTransferService'; +import { getAssetInfo } from '@shared/models/asset-info'; import { getDecimalsByNetwork, getIsTestnet, getTickerByNetwork } from '@shared/models/network-getters'; import { validateAddress } from '@shared/modules/wallet-utils'; +import { AssetId } from '@shared/types/asset'; import { getAvailableNetworks, NETWORK_ARK, @@ -28,7 +32,9 @@ import { NETWORK_USDT, type Networks, } from '@shared/types/networks'; +import { EXECUTION_INSTANT } from '@shared/types/transfer'; +import { LayerzStorage } from '@/src/class/layerz-storage'; import { BackgroundExecutor } from '@/src/modules/background-executor'; import { AnalyticsEvents, trackAnalyticsEvent } from '@/src/modules/analytics'; @@ -73,6 +79,14 @@ const mcpNftNetworkSchema = z.enum(MCP_NFT_NETWORKS); const MCP_RECEIVE_ADDRESS_NETWORKS = [NETWORK_SPARK, NETWORK_STACKS, NETWORK_ARK] as const; const mcpReceiveAddressNetworkSchema = z.enum(MCP_RECEIVE_ADDRESS_NETWORKS); +/** + * AssetIds the MCP swap tools accept. Today: only BTC↔USDB on Spark (Flashnet AMM). + * Adding more pairs is purely additive: extend this list and the routing falls through + * to whatever provider TransferServiceManager picks. + */ +const MCP_SWAP_ASSET_IDS = ['native:spark', 'token:spark:usdb'] as const satisfies readonly AssetId[]; +const mcpSwapAssetSchema = z.enum(MCP_SWAP_ASSET_IDS); + function walletHasOffchainReceiveAddress(w: unknown): w is { getOffchainReceiveAddress(): Promise } { return typeof w === 'object' && w !== null && typeof (w as { getOffchainReceiveAddress?: unknown }).getOffchainReceiveAddress === 'function'; } @@ -884,4 +898,197 @@ export function registerWalletMcpCalls(mcp: McpServer): void { } } ); + + mcp.registerTool( + 'get_swap_quote', + { + title: 'Quote an in-wallet swap (no funds move)', + description: + 'Returns a quote for swapping `send_amount_base_units` of `send_asset` into `receive_asset`. Currently only BTC↔USDB on Spark. The response includes a `quote_id` you must pass verbatim to `execute_swap` to actually trade. Quotes expire at `expires_at_unix` (typically 60s, TELL USER HOW MUCH TIME IS LEFT); call this tool again after expiry. **No funds move on this call** — it only stages the swap so the user/agent can review fees before committing.\n\n' + + '**Present the EXACT outcome to the user with zero mental math.** `receive_amount_base_units`, `effective_exchange_rate`, and `rate` are all already net of the AMM fee — quote them verbatim, do **NOT** subtract anything on top.\n\n' + + '- `effective_exchange_rate`: precomputed BTC price in USDB the user is actually paying, factoring in fees (e.g. "99500.00"). Always normalized to USDB-per-BTC regardless of swap direction, so the user can compare it directly to a market BTC price. Prefer this over `rate` when presenting — `rate` reads poorly in the USDB→BTC direction ("1 USDB = 0.00001 BTC").\n' + + '- `effective_fee_rate`: precomputed `fee_base_units / send_amount_base_units × 100` as a percent string. Always surface it for transparency about what the AMM is keeping — but show it as transparency, **not** as a further deduction on top of the rate/amounts.\n\n' + + 'Good: "You\'ll send 0.001 BTC and receive 99.5 USDB (effective price: 99,500 USDB per BTC, includes a 0.4% AMM fee)."\n' + + 'Bad: "You\'ll send 0.001 BTC at 99,500 USDB per BTC, with a 0.4% fee on top." (the fee is **not** on top — it\'s already baked into `effective_exchange_rate` and `receive_amount_base_units`.)', + inputSchema: { + send_asset: mcpSwapAssetSchema.describe(`Asset to sell. One of: ${MCP_SWAP_ASSET_IDS.join(', ')}.`), + receive_asset: mcpSwapAssetSchema.describe(`Asset to buy. Must differ from \`send_asset\`. One of: ${MCP_SWAP_ASSET_IDS.join(', ')}.`), + send_amount_base_units: mcpPositiveBaseUnitsString.describe("Amount to sell, in the send asset's smallest units (sats for BTC, 6-decimal base units for USDB)."), + }, + }, + async ({ send_asset, receive_asset, send_amount_base_units }) => { + mcpCallLog(`get_swap_quote: start - ${send_asset} -> ${receive_asset}, amount ${send_amount_base_units}`); + trackMcpCall('get_swap_quote'); + + if (send_asset === receive_asset) { + mcpCallLog('get_swap_quote: error - send_asset and receive_asset are the same'); + return { + isError: true, + content: [{ type: 'text', text: JSON.stringify({ error: '`send_asset` and `receive_asset` must differ.' }, null, 2) }], + }; + } + + try { + useTransferService(LayerzStorage); // ensure the singleton + Flashnet service are constructed + await BackgroundExecutor.lazyInitWallet(NETWORK_SPARK, MCP_BALANCE_ACCOUNT_NUMBER); + setFlashnetAccountNumber(MCP_BALANCE_ACCOUNT_NUMBER); + + const manager = getTransferServiceManager(); + if (!manager) { + mcpCallLog('get_swap_quote: error - transfer service manager not initialized'); + return { + isError: true, + content: [{ type: 'text', text: JSON.stringify({ error: 'Transfer service is not initialized yet. Open the wallet UI once, then retry.' }, null, 2) }], + }; + } + + const sendInfo = getAssetInfo(send_asset); + const receiveInfo = getAssetInfo(receive_asset); + + const sendAmountHuman = new BigNumber(send_amount_base_units).div(new BigNumber(10).pow(sendInfo.decimals)).toFixed(); + + const quote = await manager.getQuote(send_asset, receive_asset, sendAmountHuman); + // Stage in-memory only (5min TTL); execute_swap persists the completed row. + const execution = await manager.executeTransfer(quote, MCP_BALANCE_ACCOUNT_NUMBER, ''); + + const receiveAmountBaseUnits = new BigNumber(quote.receiveAmount).times(new BigNumber(10).pow(receiveInfo.decimals)).integerValue(BigNumber.ROUND_FLOOR).toFixed(0); + + // Trading fee as a percentage of the user's input (e.g. "0.4000" for 0.4%). + // Kept as smallest-unit math (fee_base_units / send_amount_base_units) so it stays exact + // regardless of asset decimals. `quote.rate` is already the post-fee effective rate, + // since the AMM's amountOut is net of fees — surfacing this percentage lets the agent + // explain the cost of the trade alongside that rate. + const feeBaseUnitsStr = quote.feeBaseUnits ?? '0'; + const effectiveFeeRate = new BigNumber(feeBaseUnitsStr).div(new BigNumber(send_amount_base_units)).times(100).toFixed(4); + + // The actual BTC-priced-in-USDB rate the user is paying, factoring in fees. + // `quote.rate` is direction-specific ("1 USDB = 0.00001 BTC" reads poorly), so we + // always normalize to USDB-per-BTC for the BTC↔USDB pair. Uses human-unit amounts + // — both sides are decimal-corrected, so the ratio is exact regardless of decimals. + // If new swap pairs are added beyond MCP_SWAP_ASSET_IDS, revisit this normalization. + const sendIsBtc = send_asset === 'native:spark'; + const usdbHuman = sendIsBtc ? quote.receiveAmount : sendAmountHuman; + const btcHuman = sendIsBtc ? sendAmountHuman : quote.receiveAmount; + const effectiveExchangeRate = new BigNumber(usdbHuman).div(new BigNumber(btcHuman)).toFixed(2); + + const summary = `${sendAmountHuman} ${sendInfo.ticker} \u2192 ${quote.receiveAmount} ${receiveInfo.ticker}`; + mcpCallLog( + `get_swap_quote: ok - ${summary}, price ${effectiveExchangeRate} USDB/BTC, fee ${feeBaseUnitsStr} ${quote.feeTicker} base units (${effectiveFeeRate}%), impact ${quote.priceImpactPct ?? '?'}%, quote_id ${execution.id}` + ); + showMcpSuccessToast('Quoted swap', summary); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + quote_id: execution.id, + send_asset, + receive_asset, + send_amount_base_units, + receive_amount_base_units: receiveAmountBaseUnits, + fee_base_units: feeBaseUnitsStr, + fee_asset: send_asset, + fee_ticker: quote.feeTicker, + effective_fee_rate: effectiveFeeRate, + effective_exchange_rate: effectiveExchangeRate, + price_impact_pct: quote.priceImpactPct ?? '0', + rate: quote.rate, + estimated_time_seconds: quote.estimatedTime, + expires_at_unix: quote.expiresAt, + service: quote.serviceName, + }, + null, + 2 + ), + }, + ], + }; + } catch (e) { + const message = e instanceof Error ? e.message : String(e); + mcpCallLog(`get_swap_quote: error - ${message}`); + return { + isError: true, + content: [{ type: 'text', text: JSON.stringify({ error: message }, null, 2) }], + }; + } + } + ); + + mcp.registerTool( + 'execute_swap', + { + title: 'Execute a previously quoted swap', + description: + 'Executes the swap staged by an earlier `get_swap_quote` call. Pass `quote_id` exactly as returned. The trade is atomic (a few seconds, no on-chain confirmations on Spark) and **irreversible** once it returns success. Each `quote_id` can only be executed **once**; expired or already-executed quotes return an error and you must re-quote. Slippage is capped at 3% (300 bps); execution fails rather than filling beyond that.', + inputSchema: { + quote_id: z.string().min(1).describe('Exact `quote_id` from `get_swap_quote` — copy verbatim. Leading/trailing whitespace is trimmed.'), + }, + }, + async ({ quote_id }) => { + const qid = quote_id.trim(); + mcpCallLog(`execute_swap: start - quote_id ${qid}`); + trackMcpCall('execute_swap'); + + try { + const manager = getTransferServiceManager(); + if (!manager) { + mcpCallLog('execute_swap: error - transfer service not initialized'); + return { + isError: true, + content: [{ type: 'text', text: JSON.stringify({ error: 'Transfer service is not initialized yet. Call `get_swap_quote` first from a warm wallet.' }, null, 2) }], + }; + } + + // Re-pin Flashnet at the MCP account in case other code mutated it between quote and execute. + // No-op for non-Flashnet quotes (setFlashnetAccountNumber only touches the Flashnet singleton). + setFlashnetAccountNumber(MCP_BALANCE_ACCOUNT_NUMBER); + + const completed = await manager.executeInstantSwap(qid); + await manager.commitTransfer(completed); + + if (completed.type !== EXECUTION_INSTANT) { + throw new Error(`Unexpected execution type for swap: ${completed.type}`); + } + + const sendInfo = getAssetInfo(completed.sendAsset); + const receiveInfo = getAssetInfo(completed.receiveAsset); + const receiveBaseUnits = new BigNumber(completed.receiveAmount).times(new BigNumber(10).pow(receiveInfo.decimals)).integerValue(BigNumber.ROUND_FLOOR).toFixed(0); + const sendBaseUnits = new BigNumber(completed.sendAmount).times(new BigNumber(10).pow(sendInfo.decimals)).integerValue(BigNumber.ROUND_FLOOR).toFixed(0); + const summary = `${completed.sendAmount} ${sendInfo.ticker} \u2192 ${completed.receiveAmount} ${receiveInfo.ticker}`; + + mcpCallLog(`execute_swap: ok - ${summary}`); + showMcpSuccessToast('Swapped', summary); + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + success: true, + quote_id: qid, + send_asset: completed.sendAsset, + receive_asset: completed.receiveAsset, + send_amount_base_units: sendBaseUnits, + receive_amount_base_units: receiveBaseUnits, + service: completed.serviceName, + }, + null, + 2 + ), + }, + ], + }; + } catch (e) { + const message = e instanceof Error ? e.message : String(e); + mcpCallLog(`execute_swap: error - ${message}`); + return { + isError: true, + content: [{ type: 'text', text: JSON.stringify({ error: message, quote_id: qid }, null, 2) }], + }; + } + } + ); } diff --git a/mobile/src/tests/unit-vi/mcp-calls-swap.test.ts b/mobile/src/tests/unit-vi/mcp-calls-swap.test.ts new file mode 100644 index 00000000..f90ad025 --- /dev/null +++ b/mobile/src/tests/unit-vi/mcp-calls-swap.test.ts @@ -0,0 +1,489 @@ +/** + * Tests the MCP `get_swap_quote` / `execute_swap` tools against the **real** + * `FlashnetTransferService` + `TransferServiceManager`, with only the + * `@flashnet/sdk` network boundary mocked. This avoids the trap where the + * test just re-asserts whatever string was stuffed into a mock — assertions + * here exercise actual BigNumber conversions, direction resolution, the + * slippage floor, and the `pendingSwaps` replay map. + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import { registerWalletMcpCalls } from '../../features/mcp/modules/mcp-calls'; +import { FlashnetTransferService } from '@shared/services/transfer-service-flashnet'; +import { TransferServiceManager } from '@shared/services/transfer-service-manager'; + +// Constants must match the production code (`transfer-service-flashnet.ts`). +const BTC_PUBKEY = '020202020202020202020202020202020202020202020202020202020202020202'; +const USDB_PUBKEY = '3206c93b24a4d18ea19d0a9a213204af2c7e74a6d16c7535cc5d33eca4ad1eca'; +const POOL_ID = 'pool-btc-usdb'; +const MCP_ACCOUNT = 4; + +// vi.hoisted — vi.mock factories are lifted above imports, so their closures need +// stable references that exist at hoist time. SDK call spies live here so we can +// assert on them from inside tests after vi.clearAllMocks(). +const { lazyInitWallet, getSparkWallet, sdkSimulateSwap, sdkExecuteSwap, sdkListPools, sdkInitialize, getTransferServiceManager, setFlashnetAccountNumber, useTransferService } = vi.hoisted(() => ({ + lazyInitWallet: vi.fn().mockResolvedValue(undefined), + getSparkWallet: vi.fn(), + sdkSimulateSwap: vi.fn(), + sdkExecuteSwap: vi.fn(), + sdkListPools: vi.fn(), + sdkInitialize: vi.fn().mockResolvedValue(undefined), + getTransferServiceManager: vi.fn(), + setFlashnetAccountNumber: vi.fn(), + useTransferService: vi.fn(), +})); + +vi.mock('@flashnet/sdk', () => ({ + FlashnetClient: vi.fn().mockImplementation(() => ({ + initialize: sdkInitialize, + simulateSwap: sdkSimulateSwap, + executeSwap: sdkExecuteSwap, + listPools: sdkListPools, + })), + isFlashnetError: vi.fn().mockReturnValue(false), +})); + +vi.mock('react-native-toast-message', () => ({ default: { show: vi.fn() } })); +vi.mock('@/src/class/layerz-storage', () => ({ LayerzStorage: { getItem: vi.fn().mockResolvedValue(''), setItem: vi.fn().mockResolvedValue(undefined) } })); +vi.mock('@/src/modules/background-executor', () => ({ BackgroundExecutor: { lazyInitWallet } })); +vi.mock('@/src/modules/analytics', () => ({ AnalyticsEvents: { McpCall: 'mcp_call' }, trackAnalyticsEvent: vi.fn() })); +vi.mock('@shared/hooks/useTransferService', () => ({ + useTransferService, + getTransferServiceManager, + setFlashnetAccountNumber, +})); +vi.mock('@shared/hooks/useExchangeRate', () => ({ exchangeRateFetcher: vi.fn() })); +vi.mock('@shared/hooks/useBalance', () => ({ balanceFetcher: vi.fn() })); +vi.mock('../../features/mcp/modules/mcp-activity-log', () => ({ pushMcpActivityLog: vi.fn() })); + +type ToolHandler = (input: any) => Promise<{ content: { text: string }[]; isError?: boolean }>; + +function parseToolJson(result: { content: { text: string }[] }): any { + return JSON.parse(result.content[0].text); +} + +/** + * Real Flashnet service backed by a real in-memory storage Map plus the mocked SDK. + * Each test re-creates them so `pendingSwaps` and persisted transfers are isolated. + */ +function makeRealStack() { + const storageMap = new Map(); + const realStorage = { + getItem: async (k: string) => storageMap.get(k) ?? '', + setItem: async (k: string, v: string) => { + storageMap.set(k, v); + }, + }; + const flashnet = new FlashnetTransferService(realStorage as any, getSparkWallet); + const manager = new TransferServiceManager([flashnet]); + return { flashnet, manager, storageMap }; +} + +function buildHandlers(): Map { + const handlers = new Map(); + const fakeServer = { + registerTool: vi.fn((name: string, _config: unknown, handler: ToolHandler) => { + handlers.set(name, handler); + }), + }; + registerWalletMcpCalls(fakeServer as any); + return handlers; +} + +describe('MCP swap tools', () => { + let handlers: Map; + let flashnet: FlashnetTransferService; + let manager: TransferServiceManager; + + beforeEach(() => { + vi.clearAllMocks(); + sdkInitialize.mockResolvedValue(undefined); + + // Same wallet stub for every account number — FlashnetClient is mocked anyway, + // it never inspects the wallet beyond identity equality (for client reuse). + const sparkWalletStub = { pubkey: 'spark-stub' }; + getSparkWallet.mockReturnValue(sparkWalletStub); + + // Default SDK responses: BTC -> USDB pool present, simulation gives a healthy quote. + // Pool is configured at 30 + 10 = 40 bps (0.40%). Fee is computed deterministically as + // amountIn × totalFeeBps / 10000 — `feePaidAssetIn` on the simulation is not consumed + // (its real units don't match the SDK's name; see transfer-service-flashnet.ts comment). + sdkListPools.mockResolvedValue({ + pools: [{ id: 'p', lpPublicKey: POOL_ID, assetAAddress: BTC_PUBKEY, assetBAddress: USDB_PUBKEY, lpFeeBps: 30, hostFeeBps: 10 }], + }); + sdkSimulateSwap.mockResolvedValue({ + amountOut: '99500000', // 99.5 USDB (6 decimals) + executionPrice: '99500', + feePaidAssetIn: '49750', // ignored — left as a realistic value to detect accidental reuse + priceImpactPct: '0.5', + }); + sdkExecuteSwap.mockResolvedValue({ + amountOut: '99400000', // 99.4 USDB realized after slippage + }); + + const stack = makeRealStack(); + flashnet = stack.flashnet; + manager = stack.manager; + + // Wire the production singleton accessors to point at the real services. + setFlashnetAccountNumber.mockImplementation((n: number) => flashnet.setCurrentAccountNumber(n)); + getTransferServiceManager.mockReturnValue(manager); + useTransferService.mockReturnValue(manager); + + handlers = buildHandlers(); + }); + + describe('get_swap_quote — happy path', () => { + it('BTC -> USDB: passes correct amount to AMM, returns realized base-unit output', async () => { + const result = await handlers.get('get_swap_quote')!({ + send_asset: 'native:spark', + receive_asset: 'token:spark:usdb', + send_amount_base_units: '100000', + }); + + expect(result.isError).toBeUndefined(); + + // SDK invariants — verify the wrapper drove the real Flashnet service correctly. + // (1) Direction: BTC = assetIn, USDB = assetOut. + // (2) AMM is queried in smallest units, not human units — so amountIn must stay as '100000'. + expect(sdkSimulateSwap).toHaveBeenCalledWith({ + poolId: POOL_ID, + assetInAddress: BTC_PUBKEY, + assetOutAddress: USDB_PUBKEY, + amountIn: '100000', + }); + + const body = parseToolJson(result); + // Real conversion: 99.500000 (human) × 10^6 = 99,500,000 base units USDB. + expect(body.receive_amount_base_units).toBe('99500000'); + // Fee derived from pool bps: 100,000 sats × 40 bps / 10,000 = 400 sats. + // Crucially, this is computed from `pool.lpFeeBps + hostFeeBps`, NOT from + // `simulation.feePaidAssetIn` (49750 in the mock) — if a regression went back to + // using the simulation field, the assertion would land at 49750 and fail loudly. + expect(body.fee_base_units).toBe('400'); + expect(body.fee_ticker).toBe('BTC'); + expect(body.fee_asset).toBe('native:spark'); + // 400 / 100000 × 100 = 0.4000% — matches pool's configured 40 bps. Sanity check that + // the percentage matches the pool config exactly when fee is derived from bps. + expect(body.effective_fee_rate).toBe('0.4000'); + // 99.5 USDB / 0.001 BTC = 99500.00 USDB per BTC. Always normalized to USDB/BTC, + // even though we're swapping BTC -> USDB, so the user gets a market-comparable price. + // A direction-swap bug would land at 0.00001 here. + expect(body.effective_exchange_rate).toBe('99500.00'); + // priceImpactPct is plumbed straight through (it's slippage info, not a fee). + expect(body.price_impact_pct).toBe('0.5'); + // quote_id is generated by Flashnet's executeTransfer using `flashnet-${unix}-${rand}`. + // We don't pin the exact value (it's clock-dependent), only the prefix the AI relies on for execute. + expect(body.quote_id).toMatch(/^flashnet-\d+-[a-z0-9]+$/); + expect(body.service).toBe('Flashnet'); + }); + + it('USDB -> BTC: reverses direction at the SDK boundary and scales for 6-vs-8 decimals', async () => { + sdkSimulateSwap.mockResolvedValueOnce({ amountOut: '50000', feePaidAssetIn: '300000', priceImpactPct: '0.1' }); + + const result = await handlers.get('get_swap_quote')!({ + send_asset: 'token:spark:usdb', + receive_asset: 'native:spark', + send_amount_base_units: '50000000', + }); + + expect(result.isError).toBeUndefined(); + // Direction inverts: USDB is now `assetIn`, BTC is `assetOut`. + expect(sdkSimulateSwap).toHaveBeenCalledWith({ + poolId: POOL_ID, + assetInAddress: USDB_PUBKEY, + assetOutAddress: BTC_PUBKEY, + amountIn: '50000000', + }); + + const body = parseToolJson(result); + // 50000 sats from the AMM converts back to '50000' sats base units (no rounding). + expect(body.receive_amount_base_units).toBe('50000'); + // Fee derived from pool bps: 50,000,000 USDB units × 40 bps / 10,000 = 200,000 USDB units. + // This is in the INPUT asset's smallest units (USDB) — direction-asymmetric vs the BTC→USDB + // case which yielded 400 sats. A regression that hardcoded sats or forgot to use the input + // asset's decimals would fail at one direction or the other. + expect(body.fee_base_units).toBe('200000'); + expect(body.fee_ticker).toBe('USDB'); + expect(body.fee_asset).toBe('token:spark:usdb'); + // Pool bps are the same in both directions (40 bps), so the percentage is symmetric. + expect(body.effective_fee_rate).toBe('0.4000'); + // 50 USDB / 0.0005 BTC = 100000.00 USDB per BTC. Different number than the BTC->USDB + // direction (99500.00), so a bug that always picks `sendAmountHuman / receiveAmount` + // (i.e. forgets to normalize) would land at 100000 in one direction and 0.00001 in the other. + expect(body.effective_exchange_rate).toBe('100000.00'); + }); + }); + + describe('get_swap_quote — wiring guarantees', () => { + it('pins all swap activity to MCP_BALANCE_ACCOUNT_NUMBER (4), even if a UI flow set a different one', async () => { + // Pretend the UI was on account 7 right before the agent came in. + flashnet.setCurrentAccountNumber(7); + + await handlers.get('get_swap_quote')!({ + send_asset: 'native:spark', + receive_asset: 'token:spark:usdb', + send_amount_base_units: '100000', + }); + + // Spark wallet for the MCP account must have been initialized. + expect(lazyInitWallet).toHaveBeenCalledWith('spark', MCP_ACCOUNT); + + // FlashnetTransferService.ensureClient resolves its wallet via getSparkWallet(currentAccountNumber). + // If the wrapper had failed to re-point the service at MCP_ACCOUNT, this would be called with 7. + expect(getSparkWallet).toHaveBeenCalledWith(MCP_ACCOUNT); + expect(getSparkWallet).not.toHaveBeenCalledWith(7); + }); + + it('preserves sub-percent precision in effective_fee_rate (1 bps pool renders as "0.0100")', async () => { + // Mirrors the real Flashnet BTC/USDB pool which is configured at 5 bps; using 1 bps here + // proves that .toFixed(4) carries enough precision for any realistic AMM tier. If someone + // regresses to .toFixed(2) this would render "0.01" (3 chars) instead of "0.0100" (6 chars) + // and fail. + sdkListPools.mockResolvedValueOnce({ + pools: [{ id: 'p', lpPublicKey: POOL_ID, assetAAddress: BTC_PUBKEY, assetBAddress: USDB_PUBKEY, lpFeeBps: 1, hostFeeBps: 0 }], + }); + sdkSimulateSwap.mockResolvedValueOnce({ amountOut: '99500000', priceImpactPct: '0' }); + + const result = await handlers.get('get_swap_quote')!({ + send_asset: 'native:spark', + receive_asset: 'token:spark:usdb', + send_amount_base_units: '100000', + }); + + const body = parseToolJson(result); + // 100,000 sats × 1 bps / 10,000 = 10 sats. 10 / 100,000 × 100 = 0.0100%. + expect(body.fee_base_units).toBe('10'); + expect(body.effective_fee_rate).toBe('0.0100'); + }); + + it('treats a pool with missing fee bps as zero-fee (no NaN, no crash)', async () => { + // Defensive: if Flashnet ever changes the AmmPool shape or returns a pool without + // lpFeeBps/hostFeeBps, we must degrade gracefully — never produce Infinity, NaN, or throw. + sdkListPools.mockResolvedValueOnce({ + pools: [{ id: 'p', lpPublicKey: POOL_ID, assetAAddress: BTC_PUBKEY, assetBAddress: USDB_PUBKEY }], + }); + sdkSimulateSwap.mockResolvedValueOnce({ amountOut: '99500000' }); + + const result = await handlers.get('get_swap_quote')!({ + send_asset: 'native:spark', + receive_asset: 'token:spark:usdb', + send_amount_base_units: '100000', + }); + + expect(result.isError).toBeUndefined(); + const body = parseToolJson(result); + expect(body.fee_base_units).toBe('0'); + expect(body.price_impact_pct).toBe('0'); + expect(body.effective_fee_rate).toBe('0.0000'); + expect(body.receive_amount_base_units).toBe('99500000'); + }); + + it('preserves precision through the base-unit ⇄ human round-trip for 1-sat inputs', async () => { + // 1 sat is the boundary case: '1' → BigNumber.div(10^8) → '1e-8' → BigNumber.times(10^8).floor → '1'. + // Regresses if anyone replaces BigNumber with Number (which would round '1e-8' weirdly). + await handlers.get('get_swap_quote')!({ + send_asset: 'native:spark', + receive_asset: 'token:spark:usdb', + send_amount_base_units: '1', + }); + expect(sdkSimulateSwap).toHaveBeenCalledWith(expect.objectContaining({ amountIn: '1' })); + }); + + it('preserves precision for amounts beyond Number.MAX_SAFE_INTEGER', async () => { + // 10^18 sats > Number.MAX_SAFE_INTEGER (~9 × 10^15). Any Number-based math would lose digits here. + const huge = '1000000000000000001'; // 19 digits + await handlers.get('get_swap_quote')!({ + send_asset: 'native:spark', + receive_asset: 'token:spark:usdb', + send_amount_base_units: huge, + }); + expect(sdkSimulateSwap).toHaveBeenCalledWith(expect.objectContaining({ amountIn: huge })); + }); + }); + + describe('get_swap_quote — rejections', () => { + it('rejects identical send/receive assets without touching the wallet or AMM', async () => { + const result = await handlers.get('get_swap_quote')!({ + send_asset: 'native:spark', + receive_asset: 'native:spark', + send_amount_base_units: '100000', + }); + + expect(result.isError).toBe(true); + expect(parseToolJson(result).error).toMatch(/must differ/i); + expect(lazyInitWallet).not.toHaveBeenCalled(); + expect(sdkSimulateSwap).not.toHaveBeenCalled(); + expect(sdkListPools).not.toHaveBeenCalled(); + }); + + it('reports a friendly error when the singleton has not been constructed yet', async () => { + getTransferServiceManager.mockReturnValueOnce(undefined); + + const result = await handlers.get('get_swap_quote')!({ + send_asset: 'native:spark', + receive_asset: 'token:spark:usdb', + send_amount_base_units: '100000', + }); + + expect(result.isError).toBe(true); + expect(parseToolJson(result).error).toMatch(/not initialized/i); + // lazyInitWallet runs before the manager check, but the AMM must never be reached. + expect(sdkSimulateSwap).not.toHaveBeenCalled(); + }); + + it('surfaces AMM simulation errors verbatim', async () => { + sdkSimulateSwap.mockRejectedValueOnce(new Error('Pool unavailable')); + + const result = await handlers.get('get_swap_quote')!({ + send_asset: 'native:spark', + receive_asset: 'token:spark:usdb', + send_amount_base_units: '100000', + }); + + expect(result.isError).toBe(true); + expect(parseToolJson(result).error).toBe('Pool unavailable'); + }); + + it('fails the quote if no BTC/USDB pool is published by Flashnet', async () => { + sdkListPools.mockResolvedValueOnce({ pools: [] }); + + const result = await handlers.get('get_swap_quote')!({ + send_asset: 'native:spark', + receive_asset: 'token:spark:usdb', + send_amount_base_units: '100000', + }); + + expect(result.isError).toBe(true); + expect(parseToolJson(result).error).toMatch(/pool not found/i); + expect(sdkSimulateSwap).not.toHaveBeenCalled(); + }); + }); + + describe('execute_swap — happy path', () => { + it('forwards the staged quote_id to the AMM with the 3% slippage floor and returns realized output', async () => { + const quoted = await handlers.get('get_swap_quote')!({ + send_asset: 'native:spark', + receive_asset: 'token:spark:usdb', + send_amount_base_units: '100000', + }); + const quoteId = parseToolJson(quoted).quote_id; + + const result = await handlers.get('execute_swap')!({ quote_id: quoteId }); + + expect(result.isError).toBeUndefined(); + // Slippage discipline: minAmountOut = floor(99500000 * 0.97) = 96515000. + // maxSlippageBps must stay at 300 (3%) — change this and the test should fail loudly. + expect(sdkExecuteSwap).toHaveBeenCalledWith({ + poolId: POOL_ID, + assetInAddress: BTC_PUBKEY, + assetOutAddress: USDB_PUBKEY, + amountIn: '100000', + minAmountOut: '96515000', + maxSlippageBps: 300, + }); + + const body = parseToolJson(result); + // Realized output came from the executeSwap mock (99400000), confirms the + // wrapper reports realized (not quoted) amounts to the agent. + expect(body.receive_amount_base_units).toBe('99400000'); + expect(body.send_amount_base_units).toBe('100000'); + expect(body.service).toBe('Flashnet'); + }); + + it('trims surrounding whitespace before looking the quote up', async () => { + const quoted = await handlers.get('get_swap_quote')!({ + send_asset: 'native:spark', + receive_asset: 'token:spark:usdb', + send_amount_base_units: '100000', + }); + const quoteId = parseToolJson(quoted).quote_id; + + const result = await handlers.get('execute_swap')!({ quote_id: ` ${quoteId} ` }); + + expect(result.isError).toBeUndefined(); + expect(sdkExecuteSwap).toHaveBeenCalledTimes(1); + }); + }); + + describe('execute_swap — invariants', () => { + it('re-pins the Flashnet service to MCP_BALANCE_ACCOUNT_NUMBER even if it drifted between quote and execute', async () => { + const quoted = await handlers.get('get_swap_quote')!({ + send_asset: 'native:spark', + receive_asset: 'token:spark:usdb', + send_amount_base_units: '100000', + }); + const quoteId = parseToolJson(quoted).quote_id; + + // UI flow sneaks in between quote and execute. + flashnet.setCurrentAccountNumber(9); + getSparkWallet.mockClear(); + + await handlers.get('execute_swap')!({ quote_id: quoteId }); + + // Execute must have asked for account 4's wallet, not 9. + expect(getSparkWallet).toHaveBeenCalledWith(MCP_ACCOUNT); + expect(getSparkWallet).not.toHaveBeenCalledWith(9); + }); + + it('replay of the same quote_id is rejected by the real pendingSwaps map', async () => { + const quoted = await handlers.get('get_swap_quote')!({ + send_asset: 'native:spark', + receive_asset: 'token:spark:usdb', + send_amount_base_units: '100000', + }); + const quoteId = parseToolJson(quoted).quote_id; + + const first = await handlers.get('execute_swap')!({ quote_id: quoteId }); + expect(first.isError).toBeUndefined(); + // Real provider only ran the AMM once — the second call must short-circuit before the SDK. + expect(sdkExecuteSwap).toHaveBeenCalledTimes(1); + + const second = await handlers.get('execute_swap')!({ quote_id: quoteId }); + expect(second.isError).toBe(true); + expect(parseToolJson(second).error).toMatch(/no pending swap/i); + expect(sdkExecuteSwap).toHaveBeenCalledTimes(1); + }); + }); + + describe('execute_swap — error mapping', () => { + it('errors out cleanly when no quote has been staged with that id', async () => { + const result = await handlers.get('execute_swap')!({ quote_id: 'flashnet-never-existed' }); + + expect(result.isError).toBe(true); + const body = parseToolJson(result); + expect(body.error).toMatch(/no pending swap/i); + expect(body.quote_id).toBe('flashnet-never-existed'); + expect(sdkExecuteSwap).not.toHaveBeenCalled(); + }); + + it('surfaces mid-execute AMM errors (e.g. slippage exceeded) and echoes the quote_id', async () => { + const quoted = await handlers.get('get_swap_quote')!({ + send_asset: 'native:spark', + receive_asset: 'token:spark:usdb', + send_amount_base_units: '100000', + }); + const quoteId = parseToolJson(quoted).quote_id; + + sdkExecuteSwap.mockRejectedValueOnce(new Error('Slippage exceeded')); + + const result = await handlers.get('execute_swap')!({ quote_id: quoteId }); + + expect(result.isError).toBe(true); + const body = parseToolJson(result); + expect(body.error).toBe('Slippage exceeded'); + expect(body.quote_id).toBe(quoteId); + }); + + it('reports the friendly error when the transfer service manager is not wired', async () => { + getTransferServiceManager.mockReturnValueOnce(undefined); + + const result = await handlers.get('execute_swap')!({ quote_id: 'flashnet-anything' }); + + expect(result.isError).toBe(true); + expect(parseToolJson(result).error).toMatch(/not initialized/i); + expect(sdkExecuteSwap).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/shared/hooks/useTransferService.ts b/shared/hooks/useTransferService.ts index b12381c9..33f413fc 100644 --- a/shared/hooks/useTransferService.ts +++ b/shared/hooks/useTransferService.ts @@ -37,6 +37,11 @@ export function setFlashnetAccountNumber(accountNumber: number): void { _flashnetService?.setCurrentAccountNumber(accountNumber); } +/** Returns the singleton TransferServiceManager if it's been constructed yet. Module-level singleton; MCP and other non-hook callers should use this after the app boot has run `useTransferService`. */ +export function getTransferServiceManager(): TransferServiceManager | undefined { + return _instance; +} + export function useTransferService(storage: IStorage): TransferServiceManager { if (!_instance) { const services: ITransferService[] = []; diff --git a/shared/services/transfer-service-flashnet.ts b/shared/services/transfer-service-flashnet.ts index ccd5e1f8..ea3e97a3 100644 --- a/shared/services/transfer-service-flashnet.ts +++ b/shared/services/transfer-service-flashnet.ts @@ -44,6 +44,12 @@ export class FlashnetTransferService implements ITransferService { private client: any | undefined; private clientWallet: SparkSDKWallet | undefined; private poolId: string | undefined; + /** + * Cached full pool object (from `listPools`). Needed for fee computation because the + * authoritative fee config lives on the pool (`lpFeeBps + hostFeeBps`), not on the swap + * simulation response. + */ + private poolDetails: any | undefined; private currentAccountNumber: number = 0; private pendingSwaps: Map = new Map(); @@ -79,11 +85,22 @@ export class FlashnetTransferService implements ITransferService { const receiveAmount = new BigNumber(simulation.amountOut).div(new BigNumber(10).pow(receiveInfo.decimals)).toFixed(receiveInfo.decimals); const rateValue = new BigNumber(receiveAmount).div(sendAmount).toFixed(8); - const priceImpact = parseFloat(simulation.priceImpactPct || '0'); - const feeEstimate = new BigNumber(sendAmount) - .times(priceImpact / 100) - .abs() - .toFixed(sendInfo.decimals); + + // Fee is derived from the pool's configured basis points (lpFeeBps + hostFeeBps), + // NOT from simulation.feePaidAssetIn. Why: the SDK type names `feePaidAssetIn` but + // empirically the field is denominated in the OUTPUT asset's smallest units (verified + // against `pool.lpFeeBps+hostFeeBps` in [FLASHNET-FEE-DIAG] log analysis — interpreting + // it as input units gives a nonsense ~38% fee on a pool configured for 5 bps). + // + // Using pool bps is also unit-unambiguous, direction-symmetric, and slightly more accurate + // than the realized-fee field for pricing intent (e.g. 0.05% nominal vs 0.048% realized + // due to V3 tick rounding — the diff is invisible to users). + const lpFeeBps = this.poolDetails?.lpFeeBps ?? 0; + const hostFeeBps = this.poolDetails?.hostFeeBps ?? 0; + const totalFeeBps = lpFeeBps + hostFeeBps; + const feeBaseUnits = new BigNumber(amountInSmallest).times(totalFeeBps).div(10000).integerValue(BigNumber.ROUND_CEIL).toFixed(0); + const feeHuman = new BigNumber(feeBaseUnits).div(new BigNumber(10).pow(sendInfo.decimals)).toFixed(sendInfo.decimals); + const priceImpactPct = simulation.priceImpactPct ?? '0'; return { id: `flashnet-${Date.now()}`, @@ -92,8 +109,10 @@ export class FlashnetTransferService implements ITransferService { sendAmount, receiveAmount, rate: `1 ${sendInfo.ticker} = ${rateValue} ${receiveInfo.ticker}`, - fee: feeEstimate, + fee: feeHuman, feeTicker: sendInfo.ticker, + feeBaseUnits, + priceImpactPct, estimatedTime: 5, expiresAt: Math.floor(Date.now() / 1000) + 60, serviceName: this.name, @@ -254,6 +273,7 @@ export class FlashnetTransferService implements ITransferService { this.client = new FlashnetClient(wallet); this.clientWallet = wallet; this.poolId = undefined; // pool discovery may differ per wallet context + this.poolDetails = undefined; await this.client.initialize(); return this.client; } @@ -275,6 +295,7 @@ export class FlashnetTransferService implements ITransferService { } this.poolId = pool.lpPublicKey || pool.publicKey || pool.id; + this.poolDetails = pool; return this.poolId!; } diff --git a/shared/services/transfer-service-manager.ts b/shared/services/transfer-service-manager.ts index dc164501..3678e93b 100644 --- a/shared/services/transfer-service-manager.ts +++ b/shared/services/transfer-service-manager.ts @@ -21,6 +21,12 @@ export class TransferServiceManager { private services: ITransferService[]; onTransferCompleted?: (execution: TransferExecution) => void; private lastSeenStatuses = new Map(); + // Tracks which service staged each instant-swap execution so executeInstantSwap can + // route by id alone. Populated in executeTransfer ONLY for services that implement + // executeInstantSwap (gating by capability avoids permanent orphans from non-instant + // providers, whose executions never call back into executeInstantSwap). Popped in + // executeInstantSwap. + private executionOwners = new Map(); constructor(services: ITransferService[]) { this.services = services; @@ -104,15 +110,25 @@ export class TransferServiceManager { const service = this.resolveServiceForQuote(quote); const execution = await service.executeTransfer(quote, accountNumber, settleAddress, fromAddress); execution.serviceName = service.name; + if (typeof service.executeInstantSwap === 'function') { + this.executionOwners.set(execution.id, service.name); + } return execution; } - async executeInstantSwap(executionId: string, serviceName: string): Promise { + async executeInstantSwap(executionId: string): Promise { + const serviceName = this.executionOwners.get(executionId); + // Pop before invoking so a retry yields the service's own "No pending swap" error + // rather than a stale routing hit. + this.executionOwners.delete(executionId); + if (!serviceName) { + throw new Error(`No pending swap found for execution ${executionId}. It may have expired or already been executed.`); + } const service = this.resolveServiceByName(serviceName); - if (!service || typeof (service as any).executeInstantSwap !== 'function') { + if (!service || typeof service.executeInstantSwap !== 'function') { throw new Error(`Service "${serviceName}" does not support instant swap execution`); } - return (service as any).executeInstantSwap(executionId); + return service.executeInstantSwap(executionId); } async commitTransfer(execution: TransferExecution): Promise { diff --git a/shared/tests/unit-vi/transfer-service-flashnet.test.ts b/shared/tests/unit-vi/transfer-service-flashnet.test.ts index 31d96538..ceb6b0b0 100644 --- a/shared/tests/unit-vi/transfer-service-flashnet.test.ts +++ b/shared/tests/unit-vi/transfer-service-flashnet.test.ts @@ -35,6 +35,10 @@ function makeMockClient() { simulateSwap: vi.fn().mockResolvedValue({ amountOut: '99500000', // 99.5 USDB (6 decimals) executionPrice: '99500', + // Note: we no longer trust `feePaidAssetIn` (its real units don't match the name). + // Fee is derived from pool.lpFeeBps + pool.hostFeeBps instead; this field is left for + // realism but is no longer consumed. + feePaidAssetIn: '49750', // ignored priceImpactPct: '0.5', }), executeSwap: vi.fn().mockResolvedValue({ @@ -48,6 +52,10 @@ function makeMockClient() { lpPublicKey: MOCK_POOL_ID, assetAAddress: '020202020202020202020202020202020202020202020202020202020202020202', assetBAddress: '3206c93b24a4d18ea19d0a9a213204af2c7e74a6d16c7535cc5d33eca4ad1eca', + // 30 + 10 = 40 bps = 0.40%. Chosen so 0.001 BTC × 40 bps / 10000 = 400 sats fee, + // matching the value the previous test fixture used for `feePaidAssetIn`. + lpFeeBps: 30, + hostFeeBps: 10, }, ], }), @@ -94,6 +102,44 @@ describe('FlashnetTransferService', () => { expect(quote.estimatedTime).toBe(5); }); + it('derives fee from pool.lpFeeBps + pool.hostFeeBps (not from simulation.feePaidAssetIn)', async () => { + const quote = await service.getQuote(BTC_SPARK, USDB, '0.001'); + + // 0.001 BTC = 100,000 sats. Pool is configured at 30 + 10 = 40 bps (0.40%). + // Expected fee = 100,000 × 40 / 10,000 = 400 sats. Note: `feePaidAssetIn: '49750'` in the + // mock — if we were still using that (in output units), we'd get a wildly wrong answer. + expect(quote.feeBaseUnits).toBe('400'); + expect(quote.fee).toBe('0.00000400'); + expect(quote.feeTicker).toBe('BTC'); + // priceImpactPct is plumbed through unchanged (it's slippage, not a fee). + expect(quote.priceImpactPct).toBe('0.5'); + }); + + it('falls back to zero fee when the pool object omits fee bps fields', async () => { + const { FlashnetClient } = await import('@flashnet/sdk'); + (FlashnetClient as any).mockImplementationOnce(() => ({ + initialize: vi.fn().mockResolvedValue(undefined), + simulateSwap: vi.fn().mockResolvedValue({ amountOut: '99500000', executionPrice: '99500' }), + executeSwap: vi.fn(), + listPools: vi.fn().mockResolvedValue({ + pools: [ + { + id: 'p', + lpPublicKey: MOCK_POOL_ID, + assetAAddress: '020202020202020202020202020202020202020202020202020202020202020202', + assetBAddress: '3206c93b24a4d18ea19d0a9a213204af2c7e74a6d16c7535cc5d33eca4ad1eca', + // No lpFeeBps, no hostFeeBps — exercise the `?? 0` defensive fallbacks. + }, + ], + }), + })); + const noFeeService = new FlashnetTransferService(mockStorage, () => mockWallet); + const quote = await noFeeService.getQuote(BTC_SPARK, USDB, '0.001'); + + expect(quote.feeBaseUnits).toBe('0'); + expect(quote.priceImpactPct).toBe('0'); + }); + it('handles USDB→BTC direction', async () => { const quote = await service.getQuote(USDB, BTC_SPARK, '100'); diff --git a/shared/types/transfer.ts b/shared/types/transfer.ts index ea45e780..cd52fb2b 100644 --- a/shared/types/transfer.ts +++ b/shared/types/transfer.ts @@ -23,10 +23,14 @@ export interface TransferQuote { receiveAmount: string; /** Human-readable rate, e.g. "1 BTC = 120,000 USDT" */ rate: string; - /** Fee amount as string */ + /** Fee amount as a human-readable string (e.g. "0.0003") in the send asset */ fee: string; /** Ticker for the fee denomination */ feeTicker: string; + /** Optional: fee amount in the send asset's smallest units. Set by providers that expose precise fee info (e.g. Flashnet). */ + feeBaseUnits?: string; + /** Optional: AMM price-impact percentage (slippage from curve, not a fee), e.g. "0.50". Set by AMM-backed providers. */ + priceImpactPct?: string; /** Estimated completion time in seconds */ estimatedTime: number; /** Unix timestamp when this quote expires */ @@ -214,4 +218,12 @@ export interface ITransferService { /** Return a URL where the user can track this transfer online */ getTrackingUrl?(execution: TransferExecution): string | undefined; + + /** + * Execute a previously staged instant-swap quote (e.g. AMM swap). + * Only providers that use the stage-then-execute pattern implement this. + * `executeTransfer` stages parameters in memory; `executeInstantSwap` commits the trade. + * Must throw if the executionId is unknown, expired, or already executed. + */ + executeInstantSwap?(executionId: string): Promise; }