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;
}