From 2d9e7a43043adeefdbe545bc98e9d08d634eedfb Mon Sep 17 00:00:00 2001 From: bonomat Date: Tue, 14 Apr 2026 14:22:24 +1000 Subject: [PATCH 01/11] =?UTF-8?q?feat(transfer):=20add=20Satora=20provider?= =?UTF-8?q?=20-=20BTC=20Lightning=20=E2=86=92=20USDT0=20on=20Rootstock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds LendaSwap Swaps as the 6th transfer provider. Single pair in v1: `native:lightning → token:rootstock:usdt0`. The user enters a BTC amount, picks which internal LN wallet (Liquid / Spark / Ark) pays the BOLT11, and receives USDT0 on their existing Rootstock wallet. Why Rootstock via USDT0: Satora already supports Rootstock via LayerZero/USDT0 and Rootstock is a first-class wallet network. How it works ------------ 1. User enters BTC amount → `SatoraTransferService.getQuote` calls the Satora SDK's `client.getQuote({sourceChain:'Lightning', targetChain:'30', targetToken: USDT0_ROOTSTOCK_ADDR, sourceAmount: sats})`. 2. Confirm screen (`mobile/app/transfer/confirm.tsx`) discovers LN-capable wallets for the current account and renders a "Pay from" picker. 3. On tap: - Fetch the user's Rootstock address via `BackgroundExecutor.getAddress(NETWORK_ROOTSTOCK, accountNumber)` - `createSwap({source:Lightning, target:USDT0@Rootstock, gasless:true})` returns a BOLT11 invoice + swap id - `lnWallet.payLightningInvoice(bolt11)` on the picked wallet - `commitTransfer` persists + navigate to `/TransferDetails` 4. Background polling (`getOngoingTransfers`) tracks status and fires `client.claim(swapId)` exactly once on `serverfunded` (idempotent via `claimCalled` flag, retries on failure). The SDK reads the stored swap from our `SatoraSwapStorageAdapter`, extracts `target_evm_address`, and POSTs to `/swap/{id}/claim-gasless` which finalizes the swap. USDT0 is delivered to the user's Rootstock address. Storage ------- The SDK manages its own seed (separate from the wallet master mnemonic). Persisted via two new `IStorage` keys: - STORAGE_KEY_SATORA_WALLET — SDK mnemonic + key index - STORAGE_KEY_SATORA_SWAPS — `StoredSwap[]` (required for claim signing) This is a TODO: we should wire in user's seed so that a user recovering the wallet will find all past swaps again Environment ----------- Set `EXPO_PUBLIC_SATORA_API_KEY` to your Satora API key. What's working (verified) ------------------------- - Quote fetching in the send direction (type BTC, see USDT0) - Create + auto-pay via the LN wallet picker (Liquid/Spark/Ark) - Auto-claim on `serverfunded` via `client.claim(swapId)` - SDK's bridge-only-chain remap for Rootstock target (auto) Manually verified end-to-end on Android emulator: BTC from internal LN wallet → USDT0 lands on Rootstock. Known gaps / Open TODOs ----------------------- - **Refund UI not implemented.** The SDK exposes `refundSwap` but we don't call it anywhere. If a swap expires / fails / the server gets stuck, we expect the wallet to auto-refund the lightning payment. - **Reverse quoting is broken for Satora.** Typing an amount in the USDT0 receive field does nothing. - **Pair min/max gating not enforced.** `getPairInfo` is not implemented, so the transfer index screen can't warn the user about below-min / above-max amounts before they tap Continue. The swap will fail serverside with a reasonable error message. - **No tracking URL.** Satora docs don't expose a public order-status page; `getTrackingUrl()` returns undefined. `TransferDetails` hides the "View Online" button accordingly. - **No Maestro E2E flow** for Satora. The existing `swap.yml` covers the Fake provider only. - **SDK has own seed**. We should derive a child seed from the master seed for Satora Other notes ----------- - Base URL is `https://api.lendaswap.com` (Satora's production endpoint); hardcoded in the service for now. --- .agents/swap.md | 16 + ext/package-lock.json | 424 ++++++++++++++++- ext/package.json | 1 + mobile/app/transfer/confirm.tsx | 159 ++++++- mobile/app/transfer/index.tsx | 4 + mobile/package-lock.json | 447 ++++++++++++++++++ mobile/package.json | 1 + mobile/utils/networkAssets.ts | 2 + shared/hooks/useTransferService.ts | 2 + shared/models/asset-info.ts | 5 +- shared/services/satora-storage-adapter.ts | 127 +++++ shared/services/transfer-service-satora.ts | 414 ++++++++++++++++ .../unit-vi/transfer-service-satora.test.ts | 368 ++++++++++++++ shared/types/IStorage.ts | 2 + shared/types/asset.ts | 1 + 15 files changed, 1959 insertions(+), 14 deletions(-) create mode 100644 shared/services/satora-storage-adapter.ts create mode 100644 shared/services/transfer-service-satora.ts create mode 100644 shared/tests/unit-vi/transfer-service-satora.test.ts diff --git a/.agents/swap.md b/.agents/swap.md index 99c650c83..a93ad146a 100644 --- a/.agents/swap.md +++ b/.agents/swap.md @@ -85,6 +85,21 @@ getTrackingUrl?(execution): string | undefined - **Manual claim**: `SwapXArkClaim` screen with serialized CommonSwap (when `autoClaim=false` on Spark) - No tracking URL +### Satora (`shared/services/transfer-service-satora.ts`) +- **Pairs**: `native:lightning → token:rootstock:usdt0` (one-way, LN-only in v1). Arkade-as-source deferred. +- **Model**: SDK returns a BOLT11; the wallet's internal LN wallet (user-picked) pays it; Satora server handles DEX swap + LayerZero USDT0 OFT bridge end-to-end. **No client-side claim/relay action**. +- **SDK**: `@lendasat/lendaswap-sdk-pure` (npm, `preview` tag). Initialized lazily via `Client.builder().withSignerStorage().withSwapStorage().withBaseUrl('https://api.lendaswap.com').withApiKey(...)`. The SDK auto-generates a Satora-only mnemonic on first build (separate from the wallet's master seed) and persists it via the storage adapter. +- **Storage adapters**: `shared/services/satora-storage-adapter.ts` — `SatoraWalletStorageAdapter` (mnemonic + key index) and `SatoraSwapStorageAdapter` (`StoredSwap[]`) backed by `IStorage`. Storage keys: `STORAGE_KEY_SATORA_WALLET`, `STORAGE_KEY_SATORA_SWAPS`. +- **Asset**: `token:rootstock:usdt0` — registered in `shared/types/asset.ts`, resolved to USDT0 contract `0x779dED0C9e1022225F8e0630b35A9B54Be713736` (6 decimals) via `shared/models/asset-info.ts:resolveTokenId`. A `manuallyDefinedTokens` entry in `shared/models/token-list.ts` overrides the bundled evm tokenlist's `"$"` symbol with a clean `USDT0` — the manually-defined list is spread before the bundled lists so `getTokenInfo` returns it first. +- **Bridge remap**: handled entirely by the SDK at runtime. When you pass `targetChain: '30'` + `targetToken: USDT0_ROOTSTOCK_ADDR` to `createSwap`, the SDK detects `isBridgeOnlyChain('30')` (`client.ts:3132-3149`), remaps the DEX swap to Arbitrum (`42161`), and populates `bridgeParams.targetChain='Rootstock'` + `bridgeParams.targetTokenAddress=USDT0_ROOTSTOCK_ADDR`. The SDK's declared `Chain` type omits '30' so we cast `ROOTSTOCK_CHAIN_ID as unknown as 'Lightning'` in the call site — a narrow escape hatch, documented inline. +- **Status flow**: `pending → clientfundingseen → clientfunded → serverfunded → clientredeeming → clientredeemed → serverredeemed`. `clientredeemed` is the terminal success (USDT0 delivered to the user's Rootstock address). +- **Auto-claim**: `getOngoingTransfers()` polls `getSwap()`. When the status reaches `serverfunded` it fires `client.claim(swapId)` exactly once (idempotency via per-transfer `claimCalled` flag). The SDK reads the stored swap from `SatoraSwapStorageAdapter`, extracts the target Rootstock address from the stored response, and internally delegates to `claimViaGasless` which POSTs to `/swap/{id}/claim-gasless`. The server then runs `coordinator.redeemAndExecute` which performs the DEX swap on Arbitrum and bridges USDT0 to Rootstock via LayerZero OFT. Status advances `serverfunded → clientredeeming → clientredeemed` (terminal). Failures (thrown network error or `ClaimResult.success === false`) retry on the next poll tick. +- **Rootstock destination**: captured at create time via `BackgroundExecutor.getAddress(NETWORK_ROOTSTOCK, accountNumber)` (returns the user's EVM wallet address for Rootstock) and persisted on the `PersistedTransfer` record. +- **Confirm-screen flow** (`mobile/app/transfer/confirm.tsx`): branches on `quote.serviceName === 'Satora'`. On mount, discovers LN-capable wallets via `BackgroundExecutor.lazyInitWallet` on `[NETWORK_LIQUID, NETWORK_SPARK, NETWORK_ARK]` + `walletSupportsLightning` type guard, default-selects the first. Renders a "Pay from" picker chip row. On confirm tap: `getAddress(NETWORK_ROOTSTOCK, accountNumber)` → `transferService.executeTransfer(quote, accountNumber, rootstockAddr)` → `lnWallet.payLightningInvoice(execution.depositAddress)` → `commitTransfer` → `router.replace('/TransferDetails')`. +- **Quote**: 60-second TTL (informational; the actual swap is created at confirm time). BTC 8 decimals on the source side, USDT0 6 decimals on the target side. +- Conditional on `EXPO_PUBLIC_SATORA_API_KEY` env var (threaded into `Client.builder().withApiKey(...)` — attached at the SDK client level so it flows into every outbound call, rather than being passed per-request). +- No tracking URL exposed by Satora docs at time of writing. + ### Fake (`shared/services/transfer-service-fake.ts`) - **Pairs**: Liquid Testnet BTC <-> Botanix Testnet BTC - **Model**: Dev/test stub. Instant completion. Throws error when amount=1. @@ -128,6 +143,7 @@ getTrackingUrl?(execution): string | undefined - `shared/tests/unit-vi/transfer-service-garden.test.ts` - `shared/tests/unit-vi/transfer-service-symbiosis.test.ts` - `shared/tests/unit-vi/transfer-service-flashnet.test.ts` +- `shared/tests/unit-vi/transfer-service-satora.test.ts` - `shared/tests/unit-vi/transfer-service-manager.test.ts` - `shared/tests/unit-vi/transfer-service-native-deposit.test.ts` - `shared/tests/unit-vi/sideshift-mappings.test.ts` diff --git a/ext/package-lock.json b/ext/package-lock.json index cc1d1226e..f4ce3d7cd 100644 --- a/ext/package-lock.json +++ b/ext/package-lock.json @@ -15,6 +15,7 @@ "@breeztech/breez-sdk-liquid": "0.12.2", "@buildonspark/spark-sdk": "0.7.1", "@flashnet/sdk": "0.5.7", + "@lendasat/lendaswap-sdk-pure": "0.2.21-1", "@metamask/eth-sig-util": "8.2.0", "@noble/hashes": "1.7.1", "@noble/secp256k1": "1.6.3", @@ -3627,6 +3628,133 @@ "dev": true, "license": "MIT" }, + "node_modules/@lendasat/lendaswap-sdk-pure": { + "version": "0.2.21-1", + "resolved": "https://registry.npmjs.org/@lendasat/lendaswap-sdk-pure/-/lendaswap-sdk-pure-0.2.21-1.tgz", + "integrity": "sha512-B+usTm49BVc1GHhNqSPM2q1GId80rcmiW2Ck6+q49lje4CKc2TVnhiujHdONSk1JKIcG/QpR7pK4G2/ZbMA3sA==", + "license": "MIT", + "dependencies": { + "@arkade-os/sdk": "^0.4.6", + "@noble/curves": "^2.2.0", + "@noble/hashes": "^2.2.0", + "@scure/bip32": "^2.0.1", + "@scure/bip39": "^2.0.1", + "@scure/btc-signer": "^2.0.1", + "dexie": "^4.4.2", + "openapi-fetch": "^0.17.0" + }, + "optionalDependencies": { + "better-sqlite3": "^12.6.2" + } + }, + "node_modules/@lendasat/lendaswap-sdk-pure/node_modules/@noble/curves": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.2.0.tgz", + "integrity": "sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.2.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@lendasat/lendaswap-sdk-pure/node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@lendasat/lendaswap-sdk-pure/node_modules/@scure/bip32": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-2.0.1.tgz", + "integrity": "sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==", + "license": "MIT", + "dependencies": { + "@noble/curves": "2.0.1", + "@noble/hashes": "2.0.1", + "@scure/base": "2.0.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@lendasat/lendaswap-sdk-pure/node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@lendasat/lendaswap-sdk-pure/node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@lendasat/lendaswap-sdk-pure/node_modules/@scure/bip39": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.0.1.tgz", + "integrity": "sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1", + "@scure/base": "2.0.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@lendasat/lendaswap-sdk-pure/node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@lendasat/lendaswap-sdk-pure/node_modules/openapi-fetch": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.17.0.tgz", + "integrity": "sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig==", + "license": "MIT", + "dependencies": { + "openapi-typescript-helpers": "^0.1.0" + } + }, + "node_modules/@lendasat/lendaswap-sdk-pure/node_modules/openapi-typescript-helpers": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.1.0.tgz", + "integrity": "sha512-OKTGPthhivLw/fHz6c3OPtg72vi86qaMlqbJuVJ23qOvQ+53uw1n7HdmkJFibloF7QEjDrDkzJiOJuockM/ljw==", + "license": "MIT" + }, "node_modules/@lightsparkdev/core": { "version": "1.4.9", "resolved": "https://registry.npmjs.org/@lightsparkdev/core/-/core-1.4.9.tgz", @@ -7860,6 +7988,21 @@ "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", "license": "MIT" }, + "node_modules/better-sqlite3": { + "version": "12.9.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.9.0.tgz", + "integrity": "sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, "node_modules/bignumber.js": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", @@ -8090,7 +8233,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -8102,7 +8245,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -8127,7 +8270,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -8774,6 +8917,13 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC", + "optional": true + }, "node_modules/chrome-trace-event": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", @@ -9560,6 +9710,22 @@ "node": ">=14.16" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -9574,7 +9740,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=4.0.0" @@ -9789,7 +9955,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", "optional": true, "engines": { @@ -10056,6 +10221,12 @@ "typescript": "^5.4.4" } }, + "node_modules/dexie": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.4.2.tgz", + "integrity": "sha512-zMtV8q79EFE5U8FKZvt0Y/77PCU/Hr/RDxv1EDeo228L+m/HTbeN2AjoQm674rhQCX8n3ljK87lajt7UQuZfvw==", + "license": "Apache-2.0" + }, "node_modules/diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", @@ -10361,6 +10532,16 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/engine.io-client": { "version": "6.6.4", "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", @@ -11639,6 +11820,16 @@ "safe-buffer": "^5.1.1" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -12107,6 +12298,13 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT", + "optional": true + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -12304,6 +12502,13 @@ "node": ">=16" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT", + "optional": true + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -14585,6 +14790,19 @@ "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -14614,12 +14832,19 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT", + "optional": true + }, "node_modules/module-definition": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/module-definition/-/module-definition-6.0.1.tgz", @@ -14733,6 +14958,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT", + "optional": true + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -14830,6 +15062,32 @@ "tslib": "^2.0.3" } }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -15104,7 +15362,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -15940,6 +16198,34 @@ "postcss": "^8.2.9" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/precinct": { "version": "12.2.0", "resolved": "https://registry.npmjs.org/precinct/-/precinct-12.2.0.tgz", @@ -16142,6 +16428,17 @@ "safe-buffer": "^5.1.2" } }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -16294,7 +16591,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, + "devOptional": true, "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", @@ -16310,14 +16607,14 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -17652,6 +17949,53 @@ "bs58check": "^4.0.0" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -18274,6 +18618,51 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/terser": { "version": "5.46.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", @@ -18895,6 +19284,19 @@ "dev": true, "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/tweetnacl": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", @@ -20172,7 +20574,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/ws": { diff --git a/ext/package.json b/ext/package.json index 580aba0a5..fab82dd80 100755 --- a/ext/package.json +++ b/ext/package.json @@ -27,6 +27,7 @@ "@breeztech/breez-sdk-liquid": "0.12.2", "@buildonspark/spark-sdk": "0.7.1", "@flashnet/sdk": "0.5.7", + "@lendasat/lendaswap-sdk-pure": "0.2.21-1", "@metamask/eth-sig-util": "8.2.0", "@noble/hashes": "1.7.1", "@noble/secp256k1": "1.6.3", diff --git a/mobile/app/transfer/confirm.tsx b/mobile/app/transfer/confirm.tsx index 0c01ab33f..117803d3f 100644 --- a/mobile/app/transfer/confirm.tsx +++ b/mobile/app/transfer/confirm.tsx @@ -13,6 +13,7 @@ import TransferAssetIcon from '@/components/transfer/TransferAssetIcon'; import { BackgroundExecutor, getOnchainDepositAddress } from '@/src/modules/background-executor'; import { EvmWallet } from '@shared/class/evm-wallet'; import { InterfaceSendQuotable, walletCanSendQuote } from '@shared/class/wallets/interface-send-quotable'; +import { walletSupportsLightning } from '@shared/class/wallets/interface-lightning-wallet'; import { AccountNumberContext } from '@shared/hooks/AccountNumberContext'; import { useAssetExchangeRate } from '@shared/hooks/useAssetExchangeRate'; import { AllNetworkInfos } from '@shared/models/all-network-infos'; @@ -22,9 +23,12 @@ import { TSupportedLazyInitWalletNetworks } from '@shared/modules/wallet-utils'; import type { AssetId } from '@shared/types/asset'; import type { SendQuote } from '@shared/types/send-quote'; import { EXECUTION_CLAIM, EXECUTION_INSTANT, type TransferExecution } from '@shared/types/transfer'; -import { NETWORK_SPARK } from '@shared/types/networks'; +import { NETWORK_ARK, NETWORK_LIQUID, NETWORK_ROOTSTOCK, NETWORK_SPARK, Networks } from '@shared/types/networks'; import { useTransferFlow } from '@/src/transfer/TransferFlowContext'; +const SATORA_PROVIDER = 'Satora'; +const LN_CAPABLE_NETWORKS: TSupportedLazyInitWalletNetworks[] = [NETWORK_LIQUID, NETWORK_SPARK, NETWORK_ARK]; + const DISMISS_THRESHOLD = 150; const CLAIM_OPTIONS_HEIGHT = 40 * 2; // 2 option rows @@ -51,7 +55,11 @@ export default function TransferConfirm() { const isConfirmingRef = useRef(false); const isFakeProvider = quote?.serviceName === 'Fake'; const isNativeDeposit = quote?.serviceName === 'Native'; + const isSatora = quote?.serviceName === SATORA_PROVIDER; const isSparkDeposit = isNativeDeposit && receiveAsset ? getAssetInfo(receiveAsset).network === NETWORK_SPARK : false; + const [lnPayNetworks, setLnPayNetworks] = useState([]); + const [selectedLnPayNetwork, setSelectedLnPayNetwork] = useState(undefined); + const [discoveringLnPay, setDiscoveringLnPay] = useState(false); useEffect(() => { if (!quote) return; @@ -92,8 +100,39 @@ export default function TransferConfirm() { return walletCanSendQuote(wallet) ? wallet : undefined; }; + // Satora flow: discover LN-capable wallets up front so the "Pay from" picker has its options. + // We skip the standard auto-prepare entirely — the source asset is `native:lightning` and the actual + // swap + invoice payment happens in handleSatoraConfirm atomically. + useEffect(() => { + if (!isSatora) return; + let cancelled = false; + setDiscoveringLnPay(true); + (async () => { + const found: Networks[] = []; + for (const network of LN_CAPABLE_NETWORKS) { + try { + const w = await BackgroundExecutor.lazyInitWallet(network, accountNumber); + if (walletSupportsLightning(w)) { + found.push(network); + } + } catch { + // wallet not initialized for this account — skip + } + } + if (cancelled) return; + setLnPayNetworks(found); + setSelectedLnPayNetwork(found[0]); + setDiscoveringLnPay(false); + setIsPreparing(false); + })(); + return () => { + cancelled = true; + }; + }, [isSatora, accountNumber]); + // On mount: create shift + get send quote so everything is ready for one-tap confirm useEffect(() => { + if (isSatora) return; // Satora flow handled by the dedicated effect above let cancelled = false; const prepare = async () => { @@ -159,8 +198,51 @@ export default function TransferConfirm() { }; }, []); // eslint-disable-line react-hooks/exhaustive-deps + // Satora flow confirm: fetch Rootstock address → executeTransfer (gets BOLT11) → pay invoice + // from the user-picked LN wallet → commit → navigate to details. + const handleSatoraConfirm = async () => { + if (isConfirmingRef.current) return; + if (!quote || !sendAsset || !receiveAsset) return; + if (!selectedLnPayNetwork) { + setError('Pick a Lightning wallet to pay the invoice'); + return; + } + isConfirmingRef.current = true; + setIsConfirming(true); + setError(''); + try { + const rootstockAddress = await BackgroundExecutor.getAddress(NETWORK_ROOTSTOCK, accountNumber); + if (!rootstockAddress) { + throw new Error('No Rootstock address available for this account'); + } + const execution = await transferService.executeTransfer(quote, accountNumber, rootstockAddress); + executionRef.current = execution; + + // Pay the BOLT11 from the picked LN wallet. + const lnWallet = await BackgroundExecutor.lazyInitWallet(selectedLnPayNetwork as TSupportedLazyInitWalletNetworks, accountNumber); + if (!walletSupportsLightning(lnWallet)) { + throw new Error(`${selectedLnPayNetwork} wallet does not support Lightning`); + } + if (!execution.depositAddress) { + throw new Error('Satora did not return a BOLT11 invoice'); + } + await lnWallet.payLightningInvoice(execution.depositAddress); + + await transferService.commitTransfer(execution); + setPreparedExecution(undefined); + setCommitted(true); + router.replace({ pathname: '/TransferDetails', params: { execution: JSON.stringify(execution) } }); + } catch (e: any) { + setError(e.message || 'Failed to start Satora swap'); + } finally { + isConfirmingRef.current = false; + setIsConfirming(false); + } + }; + // Single confirm: commit + broadcast const handleConfirm = async () => { + if (isSatora) return handleSatoraConfirm(); if (isConfirmingRef.current) return; isConfirmingRef.current = true; setIsConfirming(true); @@ -295,7 +377,7 @@ export default function TransferConfirm() { const receiveFiat = receiveRate && receiveAmount ? `$${new BigNumber(receiveAmount).multipliedBy(receiveRate).toFixed(2)}` : ''; const isExpired = !isNativeDeposit && expirySeconds <= 0; - const isReady = !isPreparing && !error && !isExpired; + const isReady = !isPreparing && !error && !isExpired && (!isSatora || !!selectedLnPayNetwork); if (!sendAsset || !receiveAsset || !quote) { router.back(); @@ -396,6 +478,38 @@ export default function TransferConfirm() { )} + {isSatora && ( + + + Pay from + + {discoveringLnPay ? 'Discovering...' : selectedLnPayNetwork ? AllNetworkInfos[selectedLnPayNetwork].displayName : 'No LN wallet'} + + + {!discoveringLnPay && lnPayNetworks.length > 0 && ( + + {lnPayNetworks.map((network) => { + const isSelected = network === selectedLnPayNetwork; + return ( + setSelectedLnPayNetwork(network)} + testID={`SatoraLnPayOption-${network}`} + > + {AllNetworkInfos[network].displayName} + {isSelected && } + + ); + })} + + )} + {!discoveringLnPay && lnPayNetworks.length === 0 && ( + No Lightning-capable wallet found for this account. Activate Liquid, Spark, or Ark first. + )} + + )} + {isSparkDeposit && ( @@ -625,4 +739,45 @@ const styles = StyleSheet.create({ alignItems: 'center', padding: 20, }, + lnPayPickerContainer: { + marginTop: 4, + }, + lnPayPickerOptions: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 8, + paddingHorizontal: 16, + paddingTop: 8, + paddingBottom: 12, + }, + lnPayPickerOption: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + paddingHorizontal: 12, + paddingVertical: 8, + borderRadius: 12, + backgroundColor: 'rgba(255, 255, 255, 0.06)', + borderWidth: 1, + borderColor: 'rgba(255, 255, 255, 0.08)', + }, + lnPayPickerOptionSelected: { + backgroundColor: 'rgba(255, 255, 255, 0.15)', + borderColor: 'rgba(255, 255, 255, 0.35)', + }, + lnPayPickerOptionText: { + fontSize: 13, + color: 'rgba(255, 255, 255, 0.6)', + }, + lnPayPickerOptionTextSelected: { + color: '#FFFFFF', + fontWeight: '600', + }, + lnPayPickerEmpty: { + fontSize: 12, + color: 'rgba(255, 165, 0, 0.9)', + paddingHorizontal: 16, + paddingTop: 8, + paddingBottom: 12, + }, }); diff --git a/mobile/app/transfer/index.tsx b/mobile/app/transfer/index.tsx index 2b28acd2c..9b260ea89 100644 --- a/mobile/app/transfer/index.tsx +++ b/mobile/app/transfer/index.tsx @@ -230,6 +230,10 @@ export default function TransferInput() { if (!sendAsset || !sendAmount || !sendBalance) return ''; const amount = parseFloat(sendAmount); if (isNaN(amount) || amount <= 0) return ''; + // `native:lightning` is a meta source asset — the actual funds live across + // individual LN-capable wallets (Breez/Spark/Ark) and the confirm screen picks + // which one pays. There is no unified balance to check here. + if (sendAsset === 'native:lightning') return ''; const info = getAssetInfo(sendAsset); if (AllNetworkInfos[info.network]?.isTestnet) return ''; const amountSmallest = new BigNumber(sendAmount).times(new BigNumber(10).pow(info.decimals)); diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 8952c2a07..165391ffb 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -20,6 +20,7 @@ "@expo/vector-icons": "15.0.3", "@flashnet/sdk": "0.5.7", "@gorhom/bottom-sheet": "5.2.8", + "@lendasat/lendaswap-sdk-pure": "0.2.21-1", "@metamask/eth-sig-util": "8.2.0", "@noble/curves": "2.0.1", "@noble/hashes": "1.7.1", @@ -5630,6 +5631,133 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@lendasat/lendaswap-sdk-pure": { + "version": "0.2.21-1", + "resolved": "https://registry.npmjs.org/@lendasat/lendaswap-sdk-pure/-/lendaswap-sdk-pure-0.2.21-1.tgz", + "integrity": "sha512-B+usTm49BVc1GHhNqSPM2q1GId80rcmiW2Ck6+q49lje4CKc2TVnhiujHdONSk1JKIcG/QpR7pK4G2/ZbMA3sA==", + "license": "MIT", + "dependencies": { + "@arkade-os/sdk": "^0.4.6", + "@noble/curves": "^2.2.0", + "@noble/hashes": "^2.2.0", + "@scure/bip32": "^2.0.1", + "@scure/bip39": "^2.0.1", + "@scure/btc-signer": "^2.0.1", + "dexie": "^4.4.2", + "openapi-fetch": "^0.17.0" + }, + "optionalDependencies": { + "better-sqlite3": "^12.6.2" + } + }, + "node_modules/@lendasat/lendaswap-sdk-pure/node_modules/@noble/curves": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.2.0.tgz", + "integrity": "sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.2.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@lendasat/lendaswap-sdk-pure/node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@lendasat/lendaswap-sdk-pure/node_modules/@scure/bip32": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-2.0.1.tgz", + "integrity": "sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==", + "license": "MIT", + "dependencies": { + "@noble/curves": "2.0.1", + "@noble/hashes": "2.0.1", + "@scure/base": "2.0.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@lendasat/lendaswap-sdk-pure/node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@lendasat/lendaswap-sdk-pure/node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@lendasat/lendaswap-sdk-pure/node_modules/@scure/bip39": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.0.1.tgz", + "integrity": "sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1", + "@scure/base": "2.0.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@lendasat/lendaswap-sdk-pure/node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@lendasat/lendaswap-sdk-pure/node_modules/openapi-fetch": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.17.0.tgz", + "integrity": "sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig==", + "license": "MIT", + "dependencies": { + "openapi-typescript-helpers": "^0.1.0" + } + }, + "node_modules/@lendasat/lendaswap-sdk-pure/node_modules/openapi-typescript-helpers": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.1.0.tgz", + "integrity": "sha512-OKTGPthhivLw/fHz6c3OPtg72vi86qaMlqbJuVJ23qOvQ+53uw1n7HdmkJFibloF7QEjDrDkzJiOJuockM/ljw==", + "license": "MIT" + }, "node_modules/@lightsparkdev/core": { "version": "1.4.9", "resolved": "https://registry.npmjs.org/@lightsparkdev/core/-/core-1.4.9.tgz", @@ -10703,6 +10831,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/better-sqlite3": { + "version": "12.9.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.9.0.tgz", + "integrity": "sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, "node_modules/big-integer": { "version": "1.6.52", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", @@ -10975,6 +11118,43 @@ "node": ">=4.0.0" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/bn.js": { "version": "4.12.2", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", @@ -11656,6 +11836,13 @@ "node": ">= 16" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC", + "optional": true + }, "node_modules/chrome-launcher": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", @@ -12456,6 +12643,22 @@ "node": ">=0.10" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dedent": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", @@ -12481,6 +12684,16 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -12645,6 +12858,12 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/dexie": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.4.2.tgz", + "integrity": "sha512-zMtV8q79EFE5U8FKZvt0Y/77PCU/Hr/RDxv1EDeo228L+m/HTbeN2AjoQm674rhQCX8n3ljK87lajt7UQuZfvw==", + "license": "Apache-2.0" + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -14112,6 +14331,16 @@ "node": ">= 0.8.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/expect": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", @@ -15682,6 +15911,13 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT", + "optional": true + }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -15891,6 +16127,13 @@ "node": ">=6" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT", + "optional": true + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -16439,6 +16682,13 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC", + "optional": true + }, "node_modules/inline-style-prefixer": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz", @@ -20585,6 +20835,19 @@ "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -20671,6 +20934,13 @@ "node": ">=10" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT", + "optional": true + }, "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", @@ -20713,6 +20983,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT", + "optional": true + }, "node_modules/napi-postinstall": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", @@ -20807,6 +21084,32 @@ "nice-grpc-common": "^2.0.2" } }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-addon-api": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", @@ -21834,6 +22137,34 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -22344,6 +22675,32 @@ "node": ">= 0.6" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", @@ -24205,6 +24562,53 @@ "bs58check": "^4.0.0" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-plist": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz", @@ -24877,6 +25281,36 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", @@ -25502,6 +25936,19 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/tweetnacl": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", diff --git a/mobile/package.json b/mobile/package.json index 39e639d6f..b5e3f7ee5 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -52,6 +52,7 @@ "@buildonspark/spark-sdk": "0.7.1", "@expo/vector-icons": "15.0.3", "@flashnet/sdk": "0.5.7", + "@lendasat/lendaswap-sdk-pure": "0.2.21-1", "@gorhom/bottom-sheet": "5.2.8", "@metamask/eth-sig-util": "8.2.0", "@noble/curves": "2.0.1", diff --git a/mobile/utils/networkAssets.ts b/mobile/utils/networkAssets.ts index 4cce2e3ad..cc9a5cf15 100644 --- a/mobile/utils/networkAssets.ts +++ b/mobile/utils/networkAssets.ts @@ -29,6 +29,7 @@ const TRANSFER_ASSET_COLORS: Partial> = { 'native:spark': '#7297A6', 'token:liquid:usdt': '#26A17B', 'token:stacks:stx': '#5546FF', + 'token:rootstock:usdt0': '#26A17B', }; /** @@ -46,6 +47,7 @@ export const getTransferAssetColor = (assetId: AssetId): string | undefined => { export const getTransferAssetIcon = (assetId: AssetId, network: string): string | null => { switch (assetId) { case 'token:liquid:usdt': + case 'token:rootstock:usdt0': return require('../assets/images/ui/network/tether.png'); case 'token:stacks:stx': return require('../assets/images/ui/network/stacks.png'); diff --git a/shared/hooks/useTransferService.ts b/shared/hooks/useTransferService.ts index b12381c9b..719f03c3b 100644 --- a/shared/hooks/useTransferService.ts +++ b/shared/hooks/useTransferService.ts @@ -4,6 +4,7 @@ import { FlashnetTransferService } from '../services/transfer-service-flashnet'; import { GardenTransferService } from '../services/transfer-service-garden'; import { TransferServiceManager } from '../services/transfer-service-manager'; import { NativeDepositClaimExecutor, NativeDepositSwapsFetcher, NativeDepositTransferService } from '../services/transfer-service-native-deposit'; +import { SatoraTransferService } from '../services/transfer-service-satora'; import { SideshiftTransferService } from '../services/transfer-service-sideshift'; import { SymbiosisTransferService } from '../services/transfer-service-symbiosis'; import { IStorage } from '../types/IStorage'; @@ -48,6 +49,7 @@ export function useTransferService(storage: IStorage): TransferServiceManager { console.warn('EXPO_PUBLIC_GARDEN_APP_ID not set — Garden Finance disabled'); } services.push(new SymbiosisTransferService(storage)); + services.push(new SatoraTransferService(storage, process.env.EXPO_PUBLIC_SATORA_API_KEY)); _flashnetService = new FlashnetTransferService(storage, (accountNumber) => SparkWallet.getSDKWalletForAccount(accountNumber)); services.push(_flashnetService); _nativeDepositService = new NativeDepositTransferService(storage); diff --git a/shared/models/asset-info.ts b/shared/models/asset-info.ts index cfd246851..4833ba36a 100644 --- a/shared/models/asset-info.ts +++ b/shared/models/asset-info.ts @@ -1,6 +1,6 @@ import { AllNetworkInfos } from './all-network-infos'; import { getTokenInfo, USDT_TOKENS } from './token-list'; -import { NETWORK_BITCOIN, NETWORK_BOTANIX_TESTNET, NETWORK_LIQUID, NETWORK_LIQUID_TESTNET, NETWORK_SPARK, Networks } from '../types/networks'; +import { NETWORK_BITCOIN, NETWORK_BOTANIX_TESTNET, NETWORK_LIQUID, NETWORK_LIQUID_TESTNET, NETWORK_ROOTSTOCK, NETWORK_SPARK, Networks } from '../types/networks'; import { ASSET_IDS, AssetId, AssetInfo } from '../types/asset'; const ASSET_ID_SET = new Set(ASSET_IDS); @@ -34,6 +34,9 @@ function resolveTokenId(network: Networks, tokenRef: string): string { if (tokenRef === 'usdb' && network === NETWORK_SPARK) { return USDT_TOKENS[NETWORK_SPARK][0]; } + if (tokenRef === 'usdt0' && network === NETWORK_ROOTSTOCK) { + return '0x779dED0C9e1022225F8e0630b35A9B54Be713736'; + } return tokenRef; } diff --git a/shared/services/satora-storage-adapter.ts b/shared/services/satora-storage-adapter.ts new file mode 100644 index 000000000..e41d4532f --- /dev/null +++ b/shared/services/satora-storage-adapter.ts @@ -0,0 +1,127 @@ +import type { GetSwapResponse } from '@lendasat/lendaswap-sdk-pure'; +import type { StoredSwap, SwapStorage, WalletStorage } from '@lendasat/lendaswap-sdk-pure'; + +import { IStorage, STORAGE_KEY_SATORA_SWAPS, STORAGE_KEY_SATORA_WALLET } from '../types/IStorage'; + +interface PersistedWallet { + mnemonic: string | null; + keyIndex: number; +} + +const EMPTY_WALLET: PersistedWallet = { mnemonic: null, keyIndex: 0 }; + +/** + * Backs the Satora SDK's WalletStorage with our IStorage. The mnemonic stored here + * is a Satora-only seed generated by the SDK on first build — it is independent + * from the user's main wallet seed. Only used to derive ephemeral EVM keys for + * gasless Permit2 signing of Satora swaps. + */ +export class SatoraWalletStorageAdapter implements WalletStorage { + constructor(private readonly storage: IStorage) {} + + private async load(): Promise { + const raw = await this.storage.getItem(STORAGE_KEY_SATORA_WALLET); + if (!raw) return { ...EMPTY_WALLET }; + try { + const parsed = JSON.parse(raw) as Partial; + return { + mnemonic: typeof parsed.mnemonic === 'string' ? parsed.mnemonic : null, + keyIndex: typeof parsed.keyIndex === 'number' ? parsed.keyIndex : 0, + }; + } catch { + return { ...EMPTY_WALLET }; + } + } + + private async save(value: PersistedWallet): Promise { + await this.storage.setItem(STORAGE_KEY_SATORA_WALLET, JSON.stringify(value)); + } + + async getMnemonic(): Promise { + return (await this.load()).mnemonic; + } + + async setMnemonic(mnemonic: string): Promise { + const current = await this.load(); + await this.save({ ...current, mnemonic }); + } + + async getKeyIndex(): Promise { + return (await this.load()).keyIndex; + } + + async setKeyIndex(index: number): Promise { + const current = await this.load(); + await this.save({ ...current, keyIndex: index }); + } + + async incrementKeyIndex(): Promise { + const current = await this.load(); + const used = current.keyIndex; + await this.save({ ...current, keyIndex: used + 1 }); + return used; + } + + async clear(): Promise { + await this.save({ ...EMPTY_WALLET }); + } +} + +/** Backs the Satora SDK's SwapStorage with a JSON blob in IStorage. */ +export class SatoraSwapStorageAdapter implements SwapStorage { + constructor(private readonly storage: IStorage) {} + + private async loadAll(): Promise> { + const raw = await this.storage.getItem(STORAGE_KEY_SATORA_SWAPS); + if (!raw) return {}; + try { + const parsed = JSON.parse(raw); + return parsed && typeof parsed === 'object' ? (parsed as Record) : {}; + } catch { + return {}; + } + } + + private async saveAll(map: Record): Promise { + await this.storage.setItem(STORAGE_KEY_SATORA_SWAPS, JSON.stringify(map)); + } + + async get(swapId: string): Promise { + const all = await this.loadAll(); + return all[swapId] ?? null; + } + + async store(swap: StoredSwap): Promise { + const all = await this.loadAll(); + all[swap.swapId] = swap; + await this.saveAll(all); + } + + async update(swapId: string, response: GetSwapResponse): Promise { + const all = await this.loadAll(); + const existing = all[swapId]; + if (!existing) { + throw new Error(`Satora swap not found: ${swapId}`); + } + all[swapId] = { ...existing, response, updatedAt: Date.now() }; + await this.saveAll(all); + } + + async delete(swapId: string): Promise { + const all = await this.loadAll(); + delete all[swapId]; + await this.saveAll(all); + } + + async list(): Promise { + return Object.keys(await this.loadAll()); + } + + async getAll(): Promise { + return Object.values(await this.loadAll()); + } + + async clear(): Promise { + await this.saveAll({}); + } +} diff --git a/shared/services/transfer-service-satora.ts b/shared/services/transfer-service-satora.ts new file mode 100644 index 000000000..d01ecb5a0 --- /dev/null +++ b/shared/services/transfer-service-satora.ts @@ -0,0 +1,414 @@ +import { Client, type GetSwapResponse, type LightningToEvmSwapResponse, type SwapStatus } from '@lendasat/lendaswap-sdk-pure'; +import BigNumber from 'bignumber.js'; + +import { IStorage, STORAGE_KEY_SATORA_SWAPS } from '../types/IStorage'; +import type { AssetId } from '../types/asset'; +import { EXECUTION_DEPOSIT, ITransferService, isTerminalStatus, TimelineStep, TransferExecution, TransferPair, TransferQuote, TransferStatus } from '../types/transfer'; +import { SatoraSwapStorageAdapter, SatoraWalletStorageAdapter } from './satora-storage-adapter'; + +const PRUNE_AGE_SECONDS = 7 * 24 * 60 * 60; // 7 days +const QUOTE_TTL_SECONDS = 60; // 1 minute +const SATORA_BASE_URL = 'https://api.lendaswap.com'; +const SDK_GETSWAP_TIMEOUT_MS = 10_000; + +const USDT0_ROOTSTOCK_ADDR = '0x779dED0C9e1022225F8e0630b35A9B54Be713736'; +const ROOTSTOCK_CHAIN_ID = '30'; +const USDT0_DECIMALS = 6; +const BTC_DECIMALS = 8; + +const SEND_ASSET: AssetId = 'native:lightning'; +const RECEIVE_ASSET: AssetId = 'token:rootstock:usdt0'; + +interface PersistedTransfer { + execution: TransferExecution; + satoraSwapId: string; + /** Rootstock address captured at create time — where USDT0 lands. Informational: `client.claim` + * reads the destination from the SDK's own swap storage, so we don't re-pass it. */ + rootstockTargetAddress: string; + /** Whether we've already fired `client.claim(id)` for this swap. Idempotency guard against + * multiple pollers double-claiming. */ + claimCalled: boolean; +} + +/** + * Satora Swaps provider. + * + * v1 supports a single direction: BTC on Lightning → USDT0 on Rootstock. + * + * Flow: + * 1. `createSwap({source: Lightning, target: USDT0@Rootstock, gasless: true})` returns a BOLT11 invoice. + * The Satora SDK detects that Rootstock is a bridge-only chain (LayerZero USDT0 OFT) and + * transparently remaps the DEX swap to Arbitrum + sets `bridgeParams` so the server bridges + * the output to Rootstock. + * 2. The wallet's confirm screen pays the BOLT11 from a user-picked internal LN wallet + * (BreezWallet / SparkWallet / ArkWallet — all implement `InterfaceLightningWallet`). + * 3. Satora server detects the payment, executes the DEX swap on Arbitrum, bridges USDT0 + * via LayerZero OFT to Rootstock, and delivers to the user's Rootstock address. + * 4. We just poll `getSwap(id)` until terminal — no client-side claim/relay action required. + * + * Docs: https://docs.satora.io/ + * SDK: https://www.npmjs.com/package/@lendasat/lendaswap-sdk-pure + */ +export class SatoraTransferService implements ITransferService { + readonly name = 'Satora'; + + private clientPromise?: Promise; + private readonly storage: IStorage; + private readonly apiKey: string | undefined; + private readonly walletStorage: SatoraWalletStorageAdapter; + private readonly swapStorage: SatoraSwapStorageAdapter; + private readonly uncommitted = new Map(); + + constructor(storage: IStorage, apiKey?: string) { + this.storage = storage; + this.apiKey = apiKey && apiKey.length > 0 ? apiKey : undefined; + this.walletStorage = new SatoraWalletStorageAdapter(storage); + this.swapStorage = new SatoraSwapStorageAdapter(storage); + } + + private getClient(): Promise { + if (!this.clientPromise) { + let builder = Client.builder().withSignerStorage(this.walletStorage).withSwapStorage(this.swapStorage).withBaseUrl(SATORA_BASE_URL); + if (this.apiKey) { + builder = builder.withApiKey(this.apiKey); + } + this.clientPromise = builder.build().catch((e) => { + this.clientPromise = undefined; + throw e; + }); + } + return this.clientPromise; + } + + getSupportedPairs(): TransferPair[] { + return [{ sendAssetId: SEND_ASSET, receiveAssetId: RECEIVE_ASSET }]; + } + + async getQuote(sendAsset: AssetId, receiveAsset: AssetId, sendAmount: string): Promise { + if (sendAsset !== SEND_ASSET) { + throw new Error(`Satora only supports ${SEND_ASSET} as send asset (got ${sendAsset})`); + } + if (receiveAsset !== RECEIVE_ASSET) { + throw new Error(`Satora only supports ${RECEIVE_ASSET} as receive asset (got ${receiveAsset})`); + } + + const sats = new BigNumber(sendAmount).multipliedBy(new BigNumber(10).pow(BTC_DECIMALS)).integerValue(BigNumber.ROUND_DOWN); + if (!sats.isFinite() || sats.lte(0)) { + throw new Error('Invalid send amount'); + } + if (!sats.isLessThan(Number.MAX_SAFE_INTEGER)) { + throw new Error('Amount too large for Satora quote'); + } + + const client = await this.getClient(); + const quote = await client.getQuote({ + sourceChain: 'Lightning', + sourceToken: 'btc', + // SDK declares Chain as a narrow union that omits Rootstock's '30', + // but the runtime dispatcher (`client.ts` → `isBridgeOnlyChain`) accepts it + // and transparently bridges via LayerZero USDT0 after DEX-swapping on Arbitrum. + targetChain: ROOTSTOCK_CHAIN_ID as unknown as 'Lightning', + targetToken: USDT0_ROOTSTOCK_ADDR, + sourceAmount: sats.toNumber(), + }); + + // target_amount is in USDT0 smallest units (6 decimals). + const receiveAmount = new BigNumber(quote.target_amount).dividedBy(new BigNumber(10).pow(USDT0_DECIMALS)).toFixed(USDT0_DECIMALS); + + // Fee is denominated in sats (BTC side). Sum protocol/network/gasless fees plus optional bridge_fee. + // Note: bridge_fee is reported in USDC smallest units (6 decimals) per the SDK OpenAPI spec — + // we surface only the sat-denominated fees here for display simplicity. + const totalFeeSats = new BigNumber(quote.protocol_fee).plus(quote.network_fee).plus(quote.gasless_network_fee); + const feeBtc = totalFeeSats.dividedBy(new BigNumber(10).pow(BTC_DECIMALS)).toFixed(BTC_DECIMALS); + + // Display rate as "1 BTC = X USDT0" for clarity. + const btcAmount = sats.dividedBy(new BigNumber(10).pow(BTC_DECIMALS)); + const usdt0Amount = new BigNumber(receiveAmount); + const usdt0PerBtc = btcAmount.gt(0) ? usdt0Amount.dividedBy(btcAmount).toFixed(2) : '0'; + + const now = Math.floor(Date.now() / 1000); + return { + id: `satora-quote-${now}-${Math.random().toString(36).slice(2, 8)}`, + sendAsset, + receiveAsset, + sendAmount, + receiveAmount, + rate: `1 BTC = ${usdt0PerBtc} USDT0`, + fee: feeBtc, + feeTicker: 'BTC', + estimatedTime: 300, + expiresAt: now + QUOTE_TTL_SECONDS, + serviceName: this.name, + }; + } + + async executeTransfer(quote: TransferQuote, accountNumber: number, settleAddress: string): Promise { + if (Date.now() / 1000 > quote.expiresAt) { + throw new Error('Quote has expired. Please get a new quote.'); + } + if (quote.sendAsset !== SEND_ASSET || quote.receiveAsset !== RECEIVE_ASSET) { + throw new Error(`Satora does not support ${quote.sendAsset} → ${quote.receiveAsset}`); + } + if (!settleAddress || !settleAddress.toLowerCase().startsWith('0x')) { + throw new Error('Satora requires a Rootstock EVM address as the settle address'); + } + + const sats = new BigNumber(quote.sendAmount).multipliedBy(new BigNumber(10).pow(BTC_DECIMALS)).integerValue(BigNumber.ROUND_DOWN); + + const client = await this.getClient(); + const result = await client.createSwap({ + source: { chain: 'Lightning', tokenId: 'btc' }, + target: { chain: ROOTSTOCK_CHAIN_ID, tokenId: USDT0_ROOTSTOCK_ADDR }, + targetAddress: settleAddress, + sourceAmount: sats.toNumber(), + gasless: true, + }); + + const response = result.response as LightningToEvmSwapResponse; + if (!('bolt11_invoice' in response) || !response.bolt11_invoice) { + throw new Error('Satora response missing BOLT11 invoice'); + } + + const now = Math.floor(Date.now() / 1000); + const execution: TransferExecution = { + type: EXECUTION_DEPOSIT, + id: response.id, + providerId: response.id, + status: mapSatoraStatus(response.status), + sendAmount: quote.sendAmount, + receiveAmount: quote.receiveAmount, + sendAsset: quote.sendAsset, + receiveAsset: quote.receiveAsset, + depositAddress: response.bolt11_invoice, // What the user's LN wallet pays + settleAddress, // Rootstock address (where USDT0 lands) + createdAt: now, + updatedAt: now, + accountNumber, + serviceName: this.name, + }; + + this.uncommitted.set(execution.id, execution); + return execution; + } + + async commitTransfer(execution: TransferExecution): Promise { + const transfers = await this.loadTransfers(); + const existingIdx = transfers.findIndex((t) => t.execution.id === execution.id); + if (existingIdx >= 0) { + transfers[existingIdx].execution = { ...transfers[existingIdx].execution, ...execution }; + await this.saveTransfers(transfers); + return; + } + + const uncommitted = this.uncommitted.get(execution.id) ?? execution; + if (!execution.settleAddress) { + throw new Error('Cannot commit Satora transfer without a Rootstock destination address'); + } + transfers.push({ + execution: { ...uncommitted, ...execution }, + satoraSwapId: execution.providerId ?? execution.id, + rootstockTargetAddress: execution.settleAddress, + claimCalled: false, + }); + await this.saveTransfers(transfers); + this.uncommitted.delete(execution.id); + } + + async getOngoingTransfers(accountNumber: number): Promise { + const transfers = await this.loadTransfers(); + const now = Math.floor(Date.now() / 1000); + const active: PersistedTransfer[] = []; + let client: Client | undefined; + + for (const t of transfers) { + if (isTerminalStatus(t.execution.status) && now - t.execution.createdAt > PRUNE_AGE_SECONDS) { + continue; + } + + if (!isTerminalStatus(t.execution.status)) { + try { + if (!client) client = await this.getClient(); + const swap = await withTimeout(client.getSwap(t.satoraSwapId), SDK_GETSWAP_TIMEOUT_MS); + t.execution.status = mapSatoraStatus(swap.status); + t.execution.updatedAt = now; + + // Once the Satora server has funded the EVM HTLC (serverfunded), the client must + // call `client.claim(swapId)` to trigger `coordinator.redeemAndExecute` — which + // runs the DEX swap on Arbitrum and bridges USDT0 to Rootstock via LayerZero OFT. + // The SDK reads the stored swap from SatoraSwapStorageAdapter, extracts the + // target Rootstock address, and internally calls `claimViaGasless`. + // Idempotency via `claimCalled`; retry on thrown errors or `success: false`. + if (!t.claimCalled && shouldTriggerClaim(swap.status)) { + try { + const result = await client.claim(t.satoraSwapId); + if (result.success) { + t.claimCalled = true; + } else { + console.warn(`Satora claim reported failure for ${t.satoraSwapId}: ${result.message}`); + // Leave flag false → retry next poll. + } + } catch (e) { + console.warn(`Satora claim threw for ${t.satoraSwapId}: ${(e as Error).message}`); + // Leave flag false → retry next poll. + } + } + } catch (e) { + console.warn(`Failed to poll Satora swap ${t.satoraSwapId}: ${(e as Error).message}`); + } + } + + active.push(t); + } + + await this.saveTransfers(active); + return active.filter((t) => t.execution.accountNumber === accountNumber).map((t) => t.execution); + } + + async refreshTransferStatus(executionId: string, _accountNumber: number): Promise { + const transfers = await this.loadTransfers(); + const transfer = transfers.find((t) => t.execution.id === executionId); + if (!transfer) { + throw new Error(`Satora transfer ${executionId} not found`); + } + const client = await this.getClient(); + const swap = await client.getSwap(transfer.satoraSwapId); + transfer.execution.status = mapSatoraStatus(swap.status); + transfer.execution.updatedAt = Math.floor(Date.now() / 1000); + await this.saveTransfers(transfers); + return transfer.execution; + } + + getTimelineSteps(execution: TransferExecution): TimelineStep[] { + return getSatoraTimelineSteps(execution); + } + + getTrackingUrl(_execution: TransferExecution): string | undefined { + return undefined; + } + + private async loadTransfers(): Promise { + const raw = await this.storage.getItem(STORAGE_KEY_SATORA_SWAPS + '_TRANSFERS'); + if (!raw) return []; + try { + const parsed = JSON.parse(raw) as PersistedTransfer[]; + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } + } + + private async saveTransfers(transfers: PersistedTransfer[]): Promise { + await this.storage.setItem(STORAGE_KEY_SATORA_SWAPS + '_TRANSFERS', JSON.stringify(transfers)); + } +} + +export function mapSatoraStatus(status: SwapStatus): TransferStatus { + switch (status) { + case 'pending': + return 'waiting'; + case 'clientfundingseen': + return 'confirming'; + case 'clientfunded': + // LN paid, server dispatching DEX + LayerZero bridge to Rootstock. + return 'pending'; + case 'serverfunded': + // Bridge in-flight. + return 'pending'; + case 'clientredeeming': + return 'pending'; + case 'clientredeemed': + case 'serverredeemed': + // USDT0 delivered to the user's Rootstock address. + return 'completed'; + case 'clientrefunded': + case 'clientfundedserverrefunded': + case 'clientrefundedserverfunded': + case 'clientrefundedserverrefunded': + case 'clientredeemedandclientrefunded': + return 'refunded'; + case 'expired': + case 'clientfundedtoolate': + return 'expired'; + case 'clientinvalidfunded': + return 'failed'; + default: + return 'failed'; + } +} + +/** + * Whether Satora's status indicates the server has funded the EVM HTLC and we should + * call `client.claim(swapId)` to trigger the server-side redeem + DEX + bridge. + * Only fires on `serverfunded`; after the SDK's claim call the status advances + * `serverfunded → clientredeeming → clientredeemed`. + */ +export function shouldTriggerClaim(status: SwapStatus): boolean { + return status === 'serverfunded'; +} + +/** 4-step timeline for Satora Lightning → USDT0 on Rootstock. */ +export function getSatoraTimelineSteps(execution: TransferExecution): TimelineStep[] { + const { status, createdAt, updatedAt } = execution; + const now = Math.floor(Date.now() / 1000); + + if (status === 'expired') { + return [ + { title: 'Swap Created', description: 'Waiting for Lightning payment', status: 'completed', timestamp: createdAt }, + { title: 'Expired', description: 'Invoice was not paid in time', status: 'error', timestamp: updatedAt }, + ]; + } + + const step1: TimelineStep = { + title: 'Pay Invoice', + description: 'Lightning invoice issued', + status: status === 'waiting' ? 'active' : 'completed', + timestamp: createdAt, + }; + + const step2Active = status === 'confirming'; + const step2: TimelineStep = { + title: 'Payment Detected', + description: 'Lightning payment received', + status: status === 'waiting' ? 'upcoming' : step2Active ? 'active' : 'completed', + timestamp: step2Active ? now : undefined, + }; + + const step3Active = status === 'pending'; + const isTerminalNonExpired = status === 'completed' || status === 'refunded' || status === 'failed'; + const step3: TimelineStep = { + title: 'Bridge to Rootstock', + description: 'DEX swap + LayerZero USDT0 bridge', + status: status === 'waiting' || status === 'confirming' ? 'upcoming' : isTerminalNonExpired ? 'completed' : step3Active ? 'active' : 'upcoming', + timestamp: step3Active ? now : undefined, + }; + + const isTerminal = isTerminalStatus(status); + const finalTitle = status === 'failed' ? 'Failed' : status === 'refunded' ? 'Refunded' : 'USDT0 Received'; + const finalDesc = status === 'failed' ? 'The swap could not be completed' : status === 'refunded' ? 'Funds have been returned' : 'USDT0 delivered to your Rootstock wallet'; + const step4: TimelineStep = { + title: finalTitle, + description: finalDesc, + status: isTerminal ? (status === 'failed' ? 'error' : 'completed') : 'upcoming', + timestamp: isTerminal ? updatedAt : undefined, + }; + + return [step1, step2, step3, step4]; +} + +function withTimeout(p: Promise, ms: number): Promise { + return new Promise((resolve, reject) => { + const t = setTimeout(() => reject(new Error(`Satora request timed out after ${ms}ms`)), ms); + p.then( + (v) => { + clearTimeout(t); + resolve(v); + }, + (e) => { + clearTimeout(t); + reject(e); + } + ); + }); +} + +export type { GetSwapResponse }; diff --git a/shared/tests/unit-vi/transfer-service-satora.test.ts b/shared/tests/unit-vi/transfer-service-satora.test.ts new file mode 100644 index 000000000..0113d12e8 --- /dev/null +++ b/shared/tests/unit-vi/transfer-service-satora.test.ts @@ -0,0 +1,368 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { mapSatoraStatus, SatoraTransferService, shouldTriggerClaim } from '../../services/transfer-service-satora'; +import type { TransferQuote } from '../../types/transfer'; + +const mockGetQuote = vi.fn(); +const mockCreateSwap = vi.fn(); +const mockGetSwap = vi.fn(); +const mockClaim = vi.fn(); +const mockFundSwapGasless = vi.fn(); +const mockClaimArkade = vi.fn(); + +vi.mock('@lendasat/lendaswap-sdk-pure', () => { + class FakeClientBuilder { + withSignerStorage() { + return this; + } + withSwapStorage() { + return this; + } + withBaseUrl() { + return this; + } + withApiKey() { + return this; + } + async build() { + return { + getQuote: mockGetQuote, + createSwap: mockCreateSwap, + getSwap: mockGetSwap, + claim: mockClaim, + fundSwapGasless: mockFundSwapGasless, + claimArkade: mockClaimArkade, + }; + } + } + return { + Client: { builder: () => new FakeClientBuilder() }, + }; +}); + +function makeStorage() { + const store: Record = {}; + return { + _store: store, + setItem: vi.fn(async (k: string, v: string) => { + store[k] = v; + }), + getItem: vi.fn(async (k: string) => store[k] || ''), + }; +} + +const ROOTSTOCK_ADDRESS = '0x1234567890abcdefABCDEF1234567890abcdefAB'; +const USDT0_ROOTSTOCK_ADDR = '0x779dED0C9e1022225F8e0630b35A9B54Be713736'; + +// 0.0001 BTC → 10_000 sats. SDK quote returns target_amount in USDT0 smallest units +// (6 decimals). 1 BTC ≈ 100k USDT0 → 10k sats → ~10 USDT0 = 10_000_000 smallest units. +const QUOTE_RESPONSE = { + exchange_rate: '100000', + network_fee: 100, + gasless_network_fee: 50, + protocol_fee: 25, + protocol_fee_rate: 0.0025, + min_amount: 1000, + max_amount: 100_000_000, + source_amount: '10000', + target_amount: '10000000', + // bridge_fee intentionally omitted — should be handled as undefined +}; + +const CREATE_SWAP_RESPONSE = { + id: 'swap-abc', + status: 'pending' as const, + bolt11_invoice: 'lnbc100u1pxxxxxxexample', + boltz_invoice: 'lnbc100u1pxxxxxxexample', // deprecated alias + boltz_swap_id: 'boltz-123', + bridge_target_chain: 'Rootstock', + bridge_target_token_address: USDT0_ROOTSTOCK_ADDR, + client_evm_address: '0xCAFE0000000000000000000000000000DEADBEEF', + evm_coordinator_address: '0xCOORD', + evm_chain_id: 42161, // Arbitrum internally (SDK remaps Rootstock to Arb + bridge) + evm_htlc_address: '0xHTLC', + evm_expected_sats: '10000', + evm_refund_locktime: 0, + fee_sats: 175, + hash_lock: '0x', + network: 'arbitrum', + receiver_pk: '', + sender_pk: '', + server_evm_address: '0xSRV', + arkade_server_pk: '', + chain: 'Arbitrum', + created_at: '2026-04-14T00:00:00Z', + source_amount: '10000', + target_amount: '10000000', + source_token: { symbol: 'BTC', decimals: 8, name: 'Bitcoin', token_id: 'btc', chain: 'Lightning' }, + target_token: { symbol: 'USDT0', decimals: 6, name: 'USDT0', token_id: USDT0_ROOTSTOCK_ADDR, chain: 'Arbitrum' }, + unilateral_claim_delay: 0, + unilateral_refund_delay: 0, + unilateral_refund_without_receiver_delay: 0, + vhtlc_refund_locktime: 0, +}; + +describe('SatoraTransferService', () => { + let service: SatoraTransferService; + let storage: ReturnType; + + beforeEach(() => { + mockGetQuote.mockReset(); + mockCreateSwap.mockReset(); + mockGetSwap.mockReset(); + mockClaim.mockReset(); + mockFundSwapGasless.mockReset(); + mockClaimArkade.mockReset(); + storage = makeStorage(); + service = new SatoraTransferService(storage); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('getSupportedPairs', () => { + it('returns exactly one pair: native:lightning → token:rootstock:usdt0', () => { + const pairs = service.getSupportedPairs(); + expect(pairs).toEqual([{ sendAssetId: 'native:lightning', receiveAssetId: 'token:rootstock:usdt0' }]); + }); + }); + + describe('getQuote', () => { + it('calls the SDK with sourceChain=Lightning, targetChain=30, and USDT0 address', async () => { + mockGetQuote.mockResolvedValue(QUOTE_RESPONSE); + + const q = await service.getQuote('native:lightning', 'token:rootstock:usdt0', '0.0001'); + + expect(mockGetQuote).toHaveBeenCalledTimes(1); + const args = mockGetQuote.mock.calls[0][0]; + expect(args.sourceChain).toBe('Lightning'); + expect(args.sourceToken).toBe('btc'); + expect(args.targetChain).toBe('30'); + expect(args.targetToken).toBe(USDT0_ROOTSTOCK_ADDR); + expect(args.sourceAmount).toBe(10_000); // 0.0001 BTC * 1e8 + + expect(q.serviceName).toBe('Satora'); + expect(q.sendAsset).toBe('native:lightning'); + expect(q.receiveAsset).toBe('token:rootstock:usdt0'); + expect(q.sendAmount).toBe('0.0001'); + expect(q.receiveAmount).toBe('10.000000'); // 10_000_000 / 1e6 + expect(q.feeTicker).toBe('BTC'); + // Total fee = protocol + network + gasless = 175 sats → 0.00000175 BTC + expect(q.fee).toBe('0.00000175'); + expect(q.expiresAt).toBeGreaterThan(Math.floor(Date.now() / 1000)); + expect(q.rate).toContain('USDT0'); + }); + + it('handles missing bridge_fee gracefully', async () => { + mockGetQuote.mockResolvedValue(QUOTE_RESPONSE); // no bridge_fee field + + const q = await service.getQuote('native:lightning', 'token:rootstock:usdt0', '0.0001'); + expect(q.fee).toBe('0.00000175'); // unchanged — bridge_fee handled as undefined + }); + + it('rejects unsupported send assets', async () => { + await expect(service.getQuote('native:bitcoin' as any, 'token:rootstock:usdt0', '0.0001')).rejects.toThrow(); + }); + + it('rejects unsupported receive assets', async () => { + await expect(service.getQuote('native:lightning', 'native:bitcoin' as any, '0.0001')).rejects.toThrow(); + }); + + it('rejects non-positive amounts', async () => { + await expect(service.getQuote('native:lightning', 'token:rootstock:usdt0', '0')).rejects.toThrow(); + }); + }); + + describe('executeTransfer', () => { + it('creates a gasless swap and returns a deposit-address execution with the BOLT11 invoice', async () => { + mockGetQuote.mockResolvedValue(QUOTE_RESPONSE); + mockCreateSwap.mockResolvedValue({ response: CREATE_SWAP_RESPONSE }); + + const quote = await service.getQuote('native:lightning', 'token:rootstock:usdt0', '0.0001'); + const exec = await service.executeTransfer(quote, 0, ROOTSTOCK_ADDRESS); + + expect(mockCreateSwap).toHaveBeenCalledTimes(1); + const createArgs = mockCreateSwap.mock.calls[0][0]; + expect(createArgs.gasless).toBe(true); + expect(createArgs.source).toEqual({ chain: 'Lightning', tokenId: 'btc' }); + expect(createArgs.target).toEqual({ chain: '30', tokenId: USDT0_ROOTSTOCK_ADDR }); + expect(createArgs.targetAddress).toBe(ROOTSTOCK_ADDRESS); + expect(createArgs.sourceAmount).toBe(10_000); + + expect(exec.id).toBe('swap-abc'); + expect(exec.providerId).toBe('swap-abc'); + expect(exec.depositAddress).toBe('lnbc100u1pxxxxxxexample'); // the BOLT11 to pay + expect(exec.settleAddress).toBe(ROOTSTOCK_ADDRESS); + expect(exec.status).toBe('waiting'); + expect(exec.serviceName).toBe('Satora'); + expect(exec.accountNumber).toBe(0); + }); + + it('rejects a non-0x settleAddress', async () => { + mockGetQuote.mockResolvedValue(QUOTE_RESPONSE); + mockCreateSwap.mockResolvedValue({ response: CREATE_SWAP_RESPONSE }); + const quote = await service.getQuote('native:lightning', 'token:rootstock:usdt0', '0.0001'); + await expect(service.executeTransfer(quote, 0, 'not-a-rootstock-address')).rejects.toThrow(/Rootstock/i); + }); + + it('rejects expired quotes', async () => { + const expired: TransferQuote = { + id: 'q1', + serviceName: 'Satora', + sendAsset: 'native:lightning', + receiveAsset: 'token:rootstock:usdt0', + sendAmount: '0.0001', + receiveAmount: '10', + rate: '1 BTC = 100000 USDT0', + fee: '0', + feeTicker: 'BTC', + estimatedTime: 300, + expiresAt: Math.floor(Date.now() / 1000) - 10, + }; + await expect(service.executeTransfer(expired, 0, ROOTSTOCK_ADDRESS)).rejects.toThrow(/expired/i); + }); + }); + + describe('mapSatoraStatus', () => { + it('maps every documented status to the expected TransferStatus', () => { + expect(mapSatoraStatus('pending')).toBe('waiting'); + expect(mapSatoraStatus('clientfundingseen')).toBe('confirming'); + expect(mapSatoraStatus('clientfunded')).toBe('pending'); + expect(mapSatoraStatus('serverfunded')).toBe('pending'); + expect(mapSatoraStatus('clientredeeming')).toBe('pending'); + expect(mapSatoraStatus('clientredeemed')).toBe('completed'); + expect(mapSatoraStatus('serverredeemed')).toBe('completed'); + expect(mapSatoraStatus('clientrefunded')).toBe('refunded'); + expect(mapSatoraStatus('clientfundedserverrefunded')).toBe('refunded'); + expect(mapSatoraStatus('expired')).toBe('expired'); + expect(mapSatoraStatus('clientfundedtoolate')).toBe('expired'); + expect(mapSatoraStatus('clientinvalidfunded')).toBe('failed'); + }); + }); + + describe('shouldTriggerClaim', () => { + it('only fires on serverfunded', () => { + expect(shouldTriggerClaim('serverfunded')).toBe(true); + expect(shouldTriggerClaim('pending')).toBe(false); + expect(shouldTriggerClaim('clientfundingseen')).toBe(false); + expect(shouldTriggerClaim('clientfunded')).toBe(false); + expect(shouldTriggerClaim('clientredeeming')).toBe(false); + expect(shouldTriggerClaim('clientredeemed')).toBe(false); + }); + }); + + describe('getOngoingTransfers + auto-claim', () => { + async function setupCommittedSwap() { + mockGetQuote.mockResolvedValue(QUOTE_RESPONSE); + mockCreateSwap.mockResolvedValue({ response: CREATE_SWAP_RESPONSE }); + const quote = await service.getQuote('native:lightning', 'token:rootstock:usdt0', '0.0001'); + const exec = await service.executeTransfer(quote, 0, ROOTSTOCK_ADDRESS); + await service.commitTransfer(exec); + } + + it('drives the full lifecycle and fires client.claim exactly once on serverfunded', async () => { + await setupCommittedSwap(); + + // Poll 1: pending → no claim. + mockGetSwap.mockResolvedValueOnce({ ...CREATE_SWAP_RESPONSE, status: 'pending' }); + let active = await service.getOngoingTransfers(0); + expect(mockClaim).not.toHaveBeenCalled(); + expect(active[0].status).toBe('waiting'); + + // Poll 2: clientfundingseen → still no claim. + mockGetSwap.mockResolvedValueOnce({ ...CREATE_SWAP_RESPONSE, status: 'clientfundingseen' }); + active = await service.getOngoingTransfers(0); + expect(mockClaim).not.toHaveBeenCalled(); + expect(active[0].status).toBe('confirming'); + + // Poll 3: clientfunded → still no claim. + mockGetSwap.mockResolvedValueOnce({ ...CREATE_SWAP_RESPONSE, status: 'clientfunded' }); + active = await service.getOngoingTransfers(0); + expect(mockClaim).not.toHaveBeenCalled(); + expect(active[0].status).toBe('pending'); + + // Poll 4: serverfunded → claim fires. + mockGetSwap.mockResolvedValueOnce({ ...CREATE_SWAP_RESPONSE, status: 'serverfunded' }); + mockClaim.mockResolvedValueOnce({ success: true, message: 'ok', txHash: '0xredeem' }); + active = await service.getOngoingTransfers(0); + expect(mockClaim).toHaveBeenCalledTimes(1); + expect(mockClaim).toHaveBeenCalledWith('swap-abc'); + expect(active[0].status).toBe('pending'); + + // Poll 5: clientredeeming → claim must NOT fire again (flag flipped true). + mockGetSwap.mockResolvedValueOnce({ ...CREATE_SWAP_RESPONSE, status: 'clientredeeming' }); + active = await service.getOngoingTransfers(0); + expect(mockClaim).toHaveBeenCalledTimes(1); + expect(active[0].status).toBe('pending'); + + // Poll 6: clientredeemed → terminal completed, claim never fired again. + mockGetSwap.mockResolvedValueOnce({ ...CREATE_SWAP_RESPONSE, status: 'clientredeemed' }); + active = await service.getOngoingTransfers(0); + expect(mockClaim).toHaveBeenCalledTimes(1); + expect(active[0].status).toBe('completed'); + + // Neither fundSwapGasless nor claimArkade should have been touched. + expect(mockFundSwapGasless).not.toHaveBeenCalled(); + expect(mockClaimArkade).not.toHaveBeenCalled(); + }); + + it('retries client.claim on the next poll when it reports success: false', async () => { + await setupCommittedSwap(); + + // First serverfunded poll: claim returns failure. + mockGetSwap.mockResolvedValueOnce({ ...CREATE_SWAP_RESPONSE, status: 'serverfunded' }); + mockClaim.mockResolvedValueOnce({ success: false, message: 'temporary glitch' }); + await service.getOngoingTransfers(0); + expect(mockClaim).toHaveBeenCalledTimes(1); + + // Second serverfunded poll: retry succeeds. + mockGetSwap.mockResolvedValueOnce({ ...CREATE_SWAP_RESPONSE, status: 'serverfunded' }); + mockClaim.mockResolvedValueOnce({ success: true, message: 'ok', txHash: '0xredeem' }); + await service.getOngoingTransfers(0); + expect(mockClaim).toHaveBeenCalledTimes(2); + }); + + it('retries client.claim on the next poll when it throws', async () => { + await setupCommittedSwap(); + + mockGetSwap.mockResolvedValueOnce({ ...CREATE_SWAP_RESPONSE, status: 'serverfunded' }); + mockClaim.mockRejectedValueOnce(new Error('network down')); + await service.getOngoingTransfers(0); + expect(mockClaim).toHaveBeenCalledTimes(1); + + mockGetSwap.mockResolvedValueOnce({ ...CREATE_SWAP_RESPONSE, status: 'serverfunded' }); + mockClaim.mockResolvedValueOnce({ success: true, message: 'ok', txHash: '0xredeem' }); + await service.getOngoingTransfers(0); + expect(mockClaim).toHaveBeenCalledTimes(2); + }); + + it('filters by accountNumber', async () => { + await setupCommittedSwap(); + mockGetSwap.mockResolvedValue({ ...CREATE_SWAP_RESPONSE, status: 'pending' }); + const acct0 = await service.getOngoingTransfers(0); + const acct1 = await service.getOngoingTransfers(1); + expect(acct0).toHaveLength(1); + expect(acct1).toHaveLength(0); + }); + }); + + describe('storage round-trip', () => { + it('preserves rootstockTargetAddress across service instances', async () => { + mockGetQuote.mockResolvedValue(QUOTE_RESPONSE); + mockCreateSwap.mockResolvedValue({ response: CREATE_SWAP_RESPONSE }); + const quote = await service.getQuote('native:lightning', 'token:rootstock:usdt0', '0.0001'); + const exec = await service.executeTransfer(quote, 0, ROOTSTOCK_ADDRESS); + await service.commitTransfer(exec); + + // New instance, same storage — the persisted transfer should be readable. + const service2 = new SatoraTransferService(storage); + mockGetSwap.mockResolvedValueOnce({ ...CREATE_SWAP_RESPONSE, status: 'pending' }); + const transfers = await service2.getOngoingTransfers(0); + expect(transfers).toHaveLength(1); + expect(transfers[0].id).toBe('swap-abc'); + expect(transfers[0].settleAddress).toBe(ROOTSTOCK_ADDRESS); + expect(transfers[0].depositAddress).toBe('lnbc100u1pxxxxxxexample'); + }); + }); +}); diff --git a/shared/types/IStorage.ts b/shared/types/IStorage.ts index 47a19391c..0ec368111 100644 --- a/shared/types/IStorage.ts +++ b/shared/types/IStorage.ts @@ -13,6 +13,8 @@ export const STORAGE_KEY_GARDEN_TRANSFERS = 'STORAGE_KEY_GARDEN_TRANSFERS'; export const STORAGE_KEY_SYMBIOSIS_TRANSFERS = 'STORAGE_KEY_SYMBIOSIS_TRANSFERS'; export const STORAGE_KEY_FLASHNET_TRANSFERS = 'STORAGE_KEY_FLASHNET_TRANSFERS'; export const STORAGE_KEY_SPARK_REFUNDED_DEPOSITS = 'STORAGE_KEY_SPARK_REFUNDED_DEPOSITS'; +export const STORAGE_KEY_SATORA_WALLET = 'STORAGE_KEY_SATORA_WALLET'; +export const STORAGE_KEY_SATORA_SWAPS = 'STORAGE_KEY_SATORA_SWAPS'; export interface IStorage { setItem(key: string, value: string): Promise; diff --git a/shared/types/asset.ts b/shared/types/asset.ts index e627301ee..678cb73e1 100644 --- a/shared/types/asset.ts +++ b/shared/types/asset.ts @@ -42,6 +42,7 @@ export const ASSET_IDS = [ `token:${NETWORK_LIQUID}:usdt`, `token:${NETWORK_SPARK}:usdb`, `token:${NETWORK_STACKS}:stx`, + `token:${NETWORK_ROOTSTOCK}:usdt0`, ] as const; /** Strict asset identity — used as a universal identifier across the app */ From f5a218f2ceffacf2a86156afb9b451b32a44c11a Mon Sep 17 00:00:00 2001 From: bonomat Date: Mon, 20 Apr 2026 11:14:57 +1000 Subject: [PATCH 02/11] fix(Satora): commit swap before LN payment and check payment result MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move commitTransfer() before payLightningInvoice() so the swap is persisted even if the app is killed mid-payment. Also check the boolean return value of payLightningInvoice() — previously a resolved false (e.g. insufficient balance or fee rejection) was silently ignored, letting the flow navigate to success with an unfunded swap --- mobile/app/transfer/confirm.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/mobile/app/transfer/confirm.tsx b/mobile/app/transfer/confirm.tsx index 117803d3f..0cbdcfced 100644 --- a/mobile/app/transfer/confirm.tsx +++ b/mobile/app/transfer/confirm.tsx @@ -198,8 +198,8 @@ export default function TransferConfirm() { }; }, []); // eslint-disable-line react-hooks/exhaustive-deps - // Satora flow confirm: fetch Rootstock address → executeTransfer (gets BOLT11) → pay invoice - // from the user-picked LN wallet → commit → navigate to details. + // Satora flow confirm: fetch Rootstock address → executeTransfer (gets BOLT11) → + // commit immediately (so the swap is tracked even if the app dies) → pay invoice. const handleSatoraConfirm = async () => { if (isConfirmingRef.current) return; if (!quote || !sendAsset || !receiveAsset) return; @@ -218,6 +218,10 @@ export default function TransferConfirm() { const execution = await transferService.executeTransfer(quote, accountNumber, rootstockAddress); executionRef.current = execution; + // Persist BEFORE paying so the swap is tracked even if the app is killed mid-payment. + // The transfer starts in 'waiting' status; background polling will pick it up. + await transferService.commitTransfer(execution); + // Pay the BOLT11 from the picked LN wallet. const lnWallet = await BackgroundExecutor.lazyInitWallet(selectedLnPayNetwork as TSupportedLazyInitWalletNetworks, accountNumber); if (!walletSupportsLightning(lnWallet)) { @@ -226,9 +230,11 @@ export default function TransferConfirm() { if (!execution.depositAddress) { throw new Error('Satora did not return a BOLT11 invoice'); } - await lnWallet.payLightningInvoice(execution.depositAddress); + const paid = await lnWallet.payLightningInvoice(execution.depositAddress); + if (!paid) { + throw new Error('Lightning payment failed — the invoice was not paid'); + } - await transferService.commitTransfer(execution); setPreparedExecution(undefined); setCommitted(true); router.replace({ pathname: '/TransferDetails', params: { execution: JSON.stringify(execution) } }); From 22a38a562089fb45f24f9a30dafe9a638d11d29c Mon Sep 17 00:00:00 2001 From: bonomat Date: Mon, 20 Apr 2026 11:19:25 +1000 Subject: [PATCH 03/11] fix(transfer): add bounds check on sats in Satora executeTransfer getQuote() validates isFinite/gt(0)/MAX_SAFE_INTEGER but executeTransfer() did not. Large amounts would silently round via toNumber() and request the wrong swap size --- shared/services/transfer-service-satora.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/shared/services/transfer-service-satora.ts b/shared/services/transfer-service-satora.ts index d01ecb5a0..3b80b5a12 100644 --- a/shared/services/transfer-service-satora.ts +++ b/shared/services/transfer-service-satora.ts @@ -154,6 +154,12 @@ export class SatoraTransferService implements ITransferService { } const sats = new BigNumber(quote.sendAmount).multipliedBy(new BigNumber(10).pow(BTC_DECIMALS)).integerValue(BigNumber.ROUND_DOWN); + if (!sats.isFinite() || sats.lte(0)) { + throw new Error('Invalid send amount'); + } + if (!sats.isLessThan(Number.MAX_SAFE_INTEGER)) { + throw new Error('Amount too large for Satora swap'); + } const client = await this.getClient(); const result = await client.createSwap({ From add81012bfc73f4ca0583b1f29b7b8a332d57b58 Mon Sep 17 00:00:00 2001 From: bonomat Date: Mon, 20 Apr 2026 11:23:47 +1000 Subject: [PATCH 04/11] chore: bump version --- ext/package-lock.json | 8 ++++---- ext/package.json | 2 +- mobile/package-lock.json | 8 ++++---- mobile/package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ext/package-lock.json b/ext/package-lock.json index f4ce3d7cd..0e24354c9 100644 --- a/ext/package-lock.json +++ b/ext/package-lock.json @@ -15,7 +15,7 @@ "@breeztech/breez-sdk-liquid": "0.12.2", "@buildonspark/spark-sdk": "0.7.1", "@flashnet/sdk": "0.5.7", - "@lendasat/lendaswap-sdk-pure": "0.2.21-1", + "@lendasat/lendaswap-sdk-pure": "0.2.23", "@metamask/eth-sig-util": "8.2.0", "@noble/hashes": "1.7.1", "@noble/secp256k1": "1.6.3", @@ -3629,9 +3629,9 @@ "license": "MIT" }, "node_modules/@lendasat/lendaswap-sdk-pure": { - "version": "0.2.21-1", - "resolved": "https://registry.npmjs.org/@lendasat/lendaswap-sdk-pure/-/lendaswap-sdk-pure-0.2.21-1.tgz", - "integrity": "sha512-B+usTm49BVc1GHhNqSPM2q1GId80rcmiW2Ck6+q49lje4CKc2TVnhiujHdONSk1JKIcG/QpR7pK4G2/ZbMA3sA==", + "version": "0.2.23", + "resolved": "https://registry.npmjs.org/@lendasat/lendaswap-sdk-pure/-/lendaswap-sdk-pure-0.2.23.tgz", + "integrity": "sha512-fuUF5NfsTtjF9fWxWjJgPyLBB/YHWFTmS2zkMbXzkt1QbZB9bRHYPxhzkDrC+/o7SD90cdTiaVxvo273WtGjow==", "license": "MIT", "dependencies": { "@arkade-os/sdk": "^0.4.6", diff --git a/ext/package.json b/ext/package.json index fab82dd80..485cfd891 100755 --- a/ext/package.json +++ b/ext/package.json @@ -27,7 +27,7 @@ "@breeztech/breez-sdk-liquid": "0.12.2", "@buildonspark/spark-sdk": "0.7.1", "@flashnet/sdk": "0.5.7", - "@lendasat/lendaswap-sdk-pure": "0.2.21-1", + "@lendasat/lendaswap-sdk-pure": "0.2.23", "@metamask/eth-sig-util": "8.2.0", "@noble/hashes": "1.7.1", "@noble/secp256k1": "1.6.3", diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 165391ffb..bcb5bfddd 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -20,7 +20,7 @@ "@expo/vector-icons": "15.0.3", "@flashnet/sdk": "0.5.7", "@gorhom/bottom-sheet": "5.2.8", - "@lendasat/lendaswap-sdk-pure": "0.2.21-1", + "@lendasat/lendaswap-sdk-pure": "0.2.23", "@metamask/eth-sig-util": "8.2.0", "@noble/curves": "2.0.1", "@noble/hashes": "1.7.1", @@ -5632,9 +5632,9 @@ } }, "node_modules/@lendasat/lendaswap-sdk-pure": { - "version": "0.2.21-1", - "resolved": "https://registry.npmjs.org/@lendasat/lendaswap-sdk-pure/-/lendaswap-sdk-pure-0.2.21-1.tgz", - "integrity": "sha512-B+usTm49BVc1GHhNqSPM2q1GId80rcmiW2Ck6+q49lje4CKc2TVnhiujHdONSk1JKIcG/QpR7pK4G2/ZbMA3sA==", + "version": "0.2.23", + "resolved": "https://registry.npmjs.org/@lendasat/lendaswap-sdk-pure/-/lendaswap-sdk-pure-0.2.23.tgz", + "integrity": "sha512-fuUF5NfsTtjF9fWxWjJgPyLBB/YHWFTmS2zkMbXzkt1QbZB9bRHYPxhzkDrC+/o7SD90cdTiaVxvo273WtGjow==", "license": "MIT", "dependencies": { "@arkade-os/sdk": "^0.4.6", diff --git a/mobile/package.json b/mobile/package.json index b5e3f7ee5..673c2c11e 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -52,7 +52,7 @@ "@buildonspark/spark-sdk": "0.7.1", "@expo/vector-icons": "15.0.3", "@flashnet/sdk": "0.5.7", - "@lendasat/lendaswap-sdk-pure": "0.2.21-1", + "@lendasat/lendaswap-sdk-pure": "0.2.23", "@gorhom/bottom-sheet": "5.2.8", "@metamask/eth-sig-util": "8.2.0", "@noble/curves": "2.0.1", From f71d16aabc5cf543feef59926af5663e88d27ec2 Mon Sep 17 00:00:00 2001 From: bonomat Date: Mon, 20 Apr 2026 11:36:23 +1000 Subject: [PATCH 05/11] fix(transfer): route LN wallet discovery errors to globalThis.handleError --- mobile/app/transfer/confirm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mobile/app/transfer/confirm.tsx b/mobile/app/transfer/confirm.tsx index 0cbdcfced..77350a805 100644 --- a/mobile/app/transfer/confirm.tsx +++ b/mobile/app/transfer/confirm.tsx @@ -115,8 +115,8 @@ export default function TransferConfirm() { if (walletSupportsLightning(w)) { found.push(network); } - } catch { - // wallet not initialized for this account — skip + } catch (e) { + globalThis.handleError?.(e, 'transfer-confirm-ln-discovery'); } } if (cancelled) return; From de40c8c1da4dcb2fceed16d1852362f4102eac22 Mon Sep 17 00:00:00 2001 From: bonomat Date: Mon, 20 Apr 2026 11:59:15 +1000 Subject: [PATCH 06/11] fix(transfer): trigger Satora auto-claim from refreshTransferStatus The TransferDetails screen polls via refreshTransferStatus(), but client.claim() was only called from getOngoingTransfers(). A swap reaching serverfunded while the user was on the details screen would stall indefinitely. Now both code paths trigger the claim. --- shared/services/transfer-service-satora.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/shared/services/transfer-service-satora.ts b/shared/services/transfer-service-satora.ts index 3b80b5a12..0bd962ab5 100644 --- a/shared/services/transfer-service-satora.ts +++ b/shared/services/transfer-service-satora.ts @@ -280,6 +280,23 @@ export class SatoraTransferService implements ITransferService { const swap = await client.getSwap(transfer.satoraSwapId); transfer.execution.status = mapSatoraStatus(swap.status); transfer.execution.updatedAt = Math.floor(Date.now() / 1000); + + // Same auto-claim logic as getOngoingTransfers — if the user is watching + // the details screen when the swap hits serverfunded, trigger the claim + // here too so it doesn't stall. + if (!transfer.claimCalled && shouldTriggerClaim(swap.status)) { + try { + const result = await client.claim(transfer.satoraSwapId); + if (result.success) { + transfer.claimCalled = true; + } else { + console.warn(`Satora claim reported failure for ${transfer.satoraSwapId}: ${result.message}`); + } + } catch (e) { + console.warn(`Satora claim threw for ${transfer.satoraSwapId}: ${(e as Error).message}`); + } + } + await this.saveTransfers(transfers); return transfer.execution; } From 5b72f260449dcd38a35019e06c207a2331507138 Mon Sep 17 00:00:00 2001 From: bonomat Date: Mon, 20 Apr 2026 12:09:03 +1000 Subject: [PATCH 07/11] refactor(transfer): move Satora create+commit+pay flow into the service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce executeAndPay() on SatoraTransferService that encapsulates executeTransfer → commitTransfer → payInvoice(bolt11) with proper ordering and result checking. The confirm screen passes the LN wallet's payLightningInvoice as a callback, keeping the UI thin and the service testable independently. --- mobile/app/transfer/confirm.tsx | 20 +++----------- shared/services/transfer-service-manager.ts | 8 ++++++ shared/services/transfer-service-satora.ts | 30 +++++++++++++++++++++ 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/mobile/app/transfer/confirm.tsx b/mobile/app/transfer/confirm.tsx index 77350a805..2ca116fd4 100644 --- a/mobile/app/transfer/confirm.tsx +++ b/mobile/app/transfer/confirm.tsx @@ -198,8 +198,7 @@ export default function TransferConfirm() { }; }, []); // eslint-disable-line react-hooks/exhaustive-deps - // Satora flow confirm: fetch Rootstock address → executeTransfer (gets BOLT11) → - // commit immediately (so the swap is tracked even if the app dies) → pay invoice. + // Satora flow confirm: the service handles create → commit → pay via callback. const handleSatoraConfirm = async () => { if (isConfirmingRef.current) return; if (!quote || !sendAsset || !receiveAsset) return; @@ -215,25 +214,14 @@ export default function TransferConfirm() { if (!rootstockAddress) { throw new Error('No Rootstock address available for this account'); } - const execution = await transferService.executeTransfer(quote, accountNumber, rootstockAddress); - executionRef.current = execution; - - // Persist BEFORE paying so the swap is tracked even if the app is killed mid-payment. - // The transfer starts in 'waiting' status; background polling will pick it up. - await transferService.commitTransfer(execution); - // Pay the BOLT11 from the picked LN wallet. const lnWallet = await BackgroundExecutor.lazyInitWallet(selectedLnPayNetwork as TSupportedLazyInitWalletNetworks, accountNumber); if (!walletSupportsLightning(lnWallet)) { throw new Error(`${selectedLnPayNetwork} wallet does not support Lightning`); } - if (!execution.depositAddress) { - throw new Error('Satora did not return a BOLT11 invoice'); - } - const paid = await lnWallet.payLightningInvoice(execution.depositAddress); - if (!paid) { - throw new Error('Lightning payment failed — the invoice was not paid'); - } + + const execution = await transferService.executeAndPay(quote, accountNumber, rootstockAddress, (bolt11: string) => lnWallet.payLightningInvoice(bolt11)); + executionRef.current = execution; setPreparedExecution(undefined); setCommitted(true); diff --git a/shared/services/transfer-service-manager.ts b/shared/services/transfer-service-manager.ts index dc1645012..49063250c 100644 --- a/shared/services/transfer-service-manager.ts +++ b/shared/services/transfer-service-manager.ts @@ -115,6 +115,14 @@ export class TransferServiceManager { return (service as any).executeInstantSwap(executionId); } + async executeAndPay(quote: TransferQuote, accountNumber: number, settleAddress: string, payInvoice: (bolt11: string) => Promise): Promise { + const service = this.resolveServiceByName(quote.serviceName); + if (!service || typeof (service as any).executeAndPay !== 'function') { + throw new Error(`Service "${quote.serviceName}" does not support executeAndPay`); + } + return (service as any).executeAndPay(quote, accountNumber, settleAddress, payInvoice); + } + async commitTransfer(execution: TransferExecution): Promise { const service = this.resolveServiceByName(execution.serviceName); if (service) { diff --git a/shared/services/transfer-service-satora.ts b/shared/services/transfer-service-satora.ts index 0bd962ab5..b48ddb63b 100644 --- a/shared/services/transfer-service-satora.ts +++ b/shared/services/transfer-service-satora.ts @@ -197,6 +197,36 @@ export class SatoraTransferService implements ITransferService { return execution; } + /** + * Full Satora swap flow: create swap → commit (persist) → pay the BOLT11 via callback. + * + * Commit happens BEFORE payment so the swap is tracked even if the app dies mid-payment. + * The callback receives the BOLT11 invoice string and must return true on success. + * + * @param quote - The TransferQuote from getQuote() + * @param accountNumber - Wallet account number + * @param settleAddress - User's Rootstock EVM address (where USDT0 lands) + * @param payInvoice - Callback that pays the BOLT11 (e.g. lnWallet.payLightningInvoice) + * @returns The committed TransferExecution + */ + async executeAndPay(quote: TransferQuote, accountNumber: number, settleAddress: string, payInvoice: (bolt11: string) => Promise): Promise { + const execution = await this.executeTransfer(quote, accountNumber, settleAddress); + + // Persist BEFORE paying so the swap is tracked even if the app is killed mid-payment. + await this.commitTransfer(execution); + + if (!execution.depositAddress) { + throw new Error('Satora did not return a BOLT11 invoice'); + } + + const paid = await payInvoice(execution.depositAddress); + if (!paid) { + throw new Error('Lightning payment failed — the invoice was not paid'); + } + + return execution; + } + async commitTransfer(execution: TransferExecution): Promise { const transfers = await this.loadTransfers(); const existingIdx = transfers.findIndex((t) => t.execution.id === execution.id); From cbe21273d3070f92d1e1babc7aafb7c00018d5c8 Mon Sep 17 00:00:00 2001 From: bonomat Date: Mon, 20 Apr 2026 12:23:58 +1000 Subject: [PATCH 08/11] chore(satora): add todo to derive satora key from wallet master seed --- shared/services/satora-storage-adapter.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/shared/services/satora-storage-adapter.ts b/shared/services/satora-storage-adapter.ts index e41d4532f..e3297bb7f 100644 --- a/shared/services/satora-storage-adapter.ts +++ b/shared/services/satora-storage-adapter.ts @@ -15,6 +15,10 @@ const EMPTY_WALLET: PersistedWallet = { mnemonic: null, keyIndex: 0 }; * is a Satora-only seed generated by the SDK on first build — it is independent * from the user's main wallet seed. Only used to derive ephemeral EVM keys for * gasless Permit2 signing of Satora swaps. + * + * TODO: derive the Satora signer from the wallet master seed instead. Once that's + * done, getMnemonic() returns a derived value and there's nothing to persist — + * SecureStorage becomes unnecessary. */ export class SatoraWalletStorageAdapter implements WalletStorage { constructor(private readonly storage: IStorage) {} From 6d5dfa5a9985021c99918ba08e1a83d084f4ca48 Mon Sep 17 00:00:00 2001 From: bonomat Date: Tue, 28 Apr 2026 10:46:11 +1000 Subject: [PATCH 09/11] chore(satora): bump sdk --- ext/package-lock.json | 249 ++++++++- ext/package.json | 2 +- mobile/package-lock.json | 1138 ++++++++++---------------------------- mobile/package.json | 2 +- 4 files changed, 552 insertions(+), 839 deletions(-) diff --git a/ext/package-lock.json b/ext/package-lock.json index 0e24354c9..40d6d03ee 100644 --- a/ext/package-lock.json +++ b/ext/package-lock.json @@ -15,7 +15,7 @@ "@breeztech/breez-sdk-liquid": "0.12.2", "@buildonspark/spark-sdk": "0.7.1", "@flashnet/sdk": "0.5.7", - "@lendasat/lendaswap-sdk-pure": "0.2.23", + "@lendasat/lendaswap-sdk-pure": "0.2.25", "@metamask/eth-sig-util": "8.2.0", "@noble/hashes": "1.7.1", "@noble/secp256k1": "1.6.3", @@ -3629,22 +3629,38 @@ "license": "MIT" }, "node_modules/@lendasat/lendaswap-sdk-pure": { - "version": "0.2.23", - "resolved": "https://registry.npmjs.org/@lendasat/lendaswap-sdk-pure/-/lendaswap-sdk-pure-0.2.23.tgz", - "integrity": "sha512-fuUF5NfsTtjF9fWxWjJgPyLBB/YHWFTmS2zkMbXzkt1QbZB9bRHYPxhzkDrC+/o7SD90cdTiaVxvo273WtGjow==", + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@lendasat/lendaswap-sdk-pure/-/lendaswap-sdk-pure-0.2.25.tgz", + "integrity": "sha512-8JFtAOEd+NakflFump8fPLABMNdWd72BLo+lWmoZi9dmSv6s/XyS24Fvopl8nsgNmFBunaUMgX9nPdbBRVSs4w==", "license": "MIT", "dependencies": { "@arkade-os/sdk": "^0.4.6", "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", + "@scure/base": "^2.0.0", "@scure/bip32": "^2.0.1", "@scure/bip39": "^2.0.1", "@scure/btc-signer": "^2.0.1", + "@zerodev/ecdsa-validator": "^5.4.0", + "@zerodev/sdk": "^5.5.0", "dexie": "^4.4.2", - "openapi-fetch": "^0.17.0" + "openapi-fetch": "^0.17.0", + "viem": "2.46.3" }, "optionalDependencies": { "better-sqlite3": "^12.6.2" + }, + "peerDependencies": { + "@circle-fin/adapter-viem-v2": "^1.8.0", + "@circle-fin/bridge-kit": "^1.8.0" + }, + "peerDependenciesMeta": { + "@circle-fin/adapter-viem-v2": { + "optional": true + }, + "@circle-fin/bridge-kit": { + "optional": true + } } }, "node_modules/@lendasat/lendaswap-sdk-pure/node_modules/@noble/curves": { @@ -3937,6 +3953,18 @@ "node": ">=4.0" } }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/curves": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", @@ -6994,6 +7022,61 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/@zerodev/ecdsa-validator": { + "version": "5.4.9", + "resolved": "https://registry.npmjs.org/@zerodev/ecdsa-validator/-/ecdsa-validator-5.4.9.tgz", + "integrity": "sha512-9NVE8/sQIKRo42UOoYKkNdmmHJY8VlT4t+2MHD2ipLg21cpbY9fS17TGZh61+Bl3qlqc8pP23I6f89z9im7kuA==", + "license": "MIT", + "peerDependencies": { + "@zerodev/sdk": "^5.4.13", + "viem": "^2.28.0" + } + }, + "node_modules/@zerodev/sdk": { + "version": "5.5.10", + "resolved": "https://registry.npmjs.org/@zerodev/sdk/-/sdk-5.5.10.tgz", + "integrity": "sha512-WVyj2XR9F6zK2GdXrvappx7yo6zoJ46cWe42dOIArp3xDjFBninjA4O1O94MwohB0G+yFqtXNsEU+WxdE67SgQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.6.0" + }, + "peerDependencies": { + "viem": "^2.28.0" + } + }, + "node_modules/@zerodev/sdk/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/abitype": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", + "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/abort-controller-x": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/abort-controller-x/-/abort-controller-x-0.4.3.tgz", @@ -14041,6 +14124,21 @@ "ws": "*" } }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -15511,6 +15609,69 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ox": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.12.4.tgz", + "integrity": "sha512-+P+C7QzuwPV8lu79dOwjBKfB2CbnbEXe/hfyyrff1drrO1nOOj3Hc87svHfcW1yneRr3WXaKr6nz11nq+/DF9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.2.3", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ox/node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/ox/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ox/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -19763,6 +19924,84 @@ "node": ">= 0.8" } }, + "node_modules/viem": { + "version": "2.46.3", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.46.3.tgz", + "integrity": "sha512-2LJS+Hyh2sYjHXQtzfv1kU9pZx9dxFzvoU/ZKIcn0FNtOU0HQuIICuYdWtUDFHaGXbAdVo8J1eCvmjkL9JVGwg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.2.3", + "isows": "1.0.7", + "ox": "0.12.4", + "ws": "8.18.3" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", diff --git a/ext/package.json b/ext/package.json index 485cfd891..42a7dd77a 100755 --- a/ext/package.json +++ b/ext/package.json @@ -27,7 +27,7 @@ "@breeztech/breez-sdk-liquid": "0.12.2", "@buildonspark/spark-sdk": "0.7.1", "@flashnet/sdk": "0.5.7", - "@lendasat/lendaswap-sdk-pure": "0.2.23", + "@lendasat/lendaswap-sdk-pure": "0.2.25", "@metamask/eth-sig-util": "8.2.0", "@noble/hashes": "1.7.1", "@noble/secp256k1": "1.6.3", diff --git a/mobile/package-lock.json b/mobile/package-lock.json index bcb5bfddd..56980fd4f 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -20,7 +20,7 @@ "@expo/vector-icons": "15.0.3", "@flashnet/sdk": "0.5.7", "@gorhom/bottom-sheet": "5.2.8", - "@lendasat/lendaswap-sdk-pure": "0.2.23", + "@lendasat/lendaswap-sdk-pure": "0.2.25", "@metamask/eth-sig-util": "8.2.0", "@noble/curves": "2.0.1", "@noble/hashes": "1.7.1", @@ -5632,22 +5632,38 @@ } }, "node_modules/@lendasat/lendaswap-sdk-pure": { - "version": "0.2.23", - "resolved": "https://registry.npmjs.org/@lendasat/lendaswap-sdk-pure/-/lendaswap-sdk-pure-0.2.23.tgz", - "integrity": "sha512-fuUF5NfsTtjF9fWxWjJgPyLBB/YHWFTmS2zkMbXzkt1QbZB9bRHYPxhzkDrC+/o7SD90cdTiaVxvo273WtGjow==", + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@lendasat/lendaswap-sdk-pure/-/lendaswap-sdk-pure-0.2.25.tgz", + "integrity": "sha512-8JFtAOEd+NakflFump8fPLABMNdWd72BLo+lWmoZi9dmSv6s/XyS24Fvopl8nsgNmFBunaUMgX9nPdbBRVSs4w==", "license": "MIT", "dependencies": { "@arkade-os/sdk": "^0.4.6", "@noble/curves": "^2.2.0", "@noble/hashes": "^2.2.0", + "@scure/base": "^2.0.0", "@scure/bip32": "^2.0.1", "@scure/bip39": "^2.0.1", "@scure/btc-signer": "^2.0.1", + "@zerodev/ecdsa-validator": "^5.4.0", + "@zerodev/sdk": "^5.5.0", "dexie": "^4.4.2", - "openapi-fetch": "^0.17.0" + "openapi-fetch": "^0.17.0", + "viem": "2.46.3" }, "optionalDependencies": { "better-sqlite3": "^12.6.2" + }, + "peerDependencies": { + "@circle-fin/adapter-viem-v2": "^1.8.0", + "@circle-fin/bridge-kit": "^1.8.0" + }, + "peerDependenciesMeta": { + "@circle-fin/adapter-viem-v2": { + "optional": true + }, + "@circle-fin/bridge-kit": { + "optional": true + } } }, "node_modules/@lendasat/lendaswap-sdk-pure/node_modules/@noble/curves": { @@ -5919,6 +5935,18 @@ "@tybys/wasm-util": "^0.10.0" } }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/curves": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", @@ -6438,142 +6466,6 @@ "node": ">= 20.19.4" } }, - "node_modules/@react-native/babel-plugin-codegen": { - "version": "0.85.1", - "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.85.1.tgz", - "integrity": "sha512-Klex4kTsRxoswZmo7EBXobvpg+HO6h7xeGo87CLXSKPq3qHlJ8ilpgtmzYCTK+Qr/0Mk3cz2zv3bA9VTXR+NDA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/traverse": "^7.29.0", - "@react-native/codegen": "0.85.1" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/babel-plugin-codegen/node_modules/@react-native/codegen": { - "version": "0.85.1", - "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.85.1.tgz", - "integrity": "sha512-Ge8F5VejnI7ng/NGObqBBovuLbItvmmZDFQ1Qwt/nBhHtk7l2tOffNMVNTta9Jt8TW0oXxVj6FG3hr6nx03JrQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/core": "^7.25.2", - "@babel/parser": "^7.29.0", - "hermes-parser": "0.33.3", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "tinyglobby": "^0.2.15", - "yargs": "^17.6.2" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - }, - "peerDependencies": { - "@babel/core": "*" - } - }, - "node_modules/@react-native/babel-plugin-codegen/node_modules/hermes-estree": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.33.3.tgz", - "integrity": "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/@react-native/babel-plugin-codegen/node_modules/hermes-parser": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.33.3.tgz", - "integrity": "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "hermes-estree": "0.33.3" - } - }, - "node_modules/@react-native/babel-preset": { - "version": "0.85.1", - "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.85.1.tgz", - "integrity": "sha512-Mplsn13fCxQElOfWg6wIuXJP+tyO980etTQ1gQFTt5Zstj3rs33GzTLMNlo6EnT8PQghO3GxIrg/2im5GwodnA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/core": "^7.25.2", - "@babel/plugin-proposal-export-default-from": "^7.24.7", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-default-from": "^7.24.7", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-transform-async-generator-functions": "^7.25.4", - "@babel/plugin-transform-async-to-generator": "^7.24.7", - "@babel/plugin-transform-block-scoping": "^7.25.0", - "@babel/plugin-transform-class-properties": "^7.25.4", - "@babel/plugin-transform-classes": "^7.25.4", - "@babel/plugin-transform-destructuring": "^7.24.8", - "@babel/plugin-transform-flow-strip-types": "^7.25.2", - "@babel/plugin-transform-for-of": "^7.24.7", - "@babel/plugin-transform-modules-commonjs": "^7.24.8", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", - "@babel/plugin-transform-optional-catch-binding": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.8", - "@babel/plugin-transform-private-methods": "^7.24.7", - "@babel/plugin-transform-private-property-in-object": "^7.24.7", - "@babel/plugin-transform-react-display-name": "^7.24.7", - "@babel/plugin-transform-react-jsx": "^7.25.2", - "@babel/plugin-transform-react-jsx-self": "^7.24.7", - "@babel/plugin-transform-react-jsx-source": "^7.24.7", - "@babel/plugin-transform-regenerator": "^7.24.7", - "@babel/plugin-transform-runtime": "^7.24.7", - "@babel/plugin-transform-typescript": "^7.25.2", - "@babel/plugin-transform-unicode-regex": "^7.24.7", - "@react-native/babel-plugin-codegen": "0.85.1", - "babel-plugin-syntax-hermes-parser": "0.33.3", - "babel-plugin-transform-flow-enums": "^0.0.2", - "react-refresh": "^0.14.0" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - }, - "peerDependencies": { - "@babel/core": "*" - } - }, - "node_modules/@react-native/babel-preset/node_modules/babel-plugin-syntax-hermes-parser": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.33.3.tgz", - "integrity": "sha512-/Z9xYdaJ1lC0pT9do6TqCqhOSLfZ5Ot8D5za1p+feEfWYupCOfGbhhEXN9r2ZgJtDNUNRw/Z+T2CvAGKBqtqWA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "hermes-parser": "0.33.3" - } - }, - "node_modules/@react-native/babel-preset/node_modules/hermes-estree": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.33.3.tgz", - "integrity": "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/@react-native/babel-preset/node_modules/hermes-parser": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.33.3.tgz", - "integrity": "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "hermes-estree": "0.33.3" - } - }, "node_modules/@react-native/codegen": { "version": "0.83.4", "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.83.4.tgz", @@ -6620,693 +6512,73 @@ "@react-native-community/cli": { "optional": true }, - "@react-native/metro-config": { - "optional": true - } - } - }, - "node_modules/@react-native/community-cli-plugin/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@react-native/debugger-frontend": { - "version": "0.83.4", - "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.83.4.tgz", - "integrity": "sha512-mCE2s/S7SEjax3gZb6LFAraAI3x13gRVWJWqT0HIm71e4ITObENNTDuMw4mvZ/wr4Gz2wv4FcBH5/Nla9LXOcg==", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 20.19.4" - } - }, - "node_modules/@react-native/debugger-shell": { - "version": "0.83.4", - "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.83.4.tgz", - "integrity": "sha512-FtAnrvXqy1xeZ+onwilvxEeeBsvBlhtfrHVIC2R/BOJAK9TbKEtFfjio0wsn3DQIm+UZq48DSa+p9jJZ2aJUww==", - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.6", - "fb-dotslash": "0.5.8" - }, - "engines": { - "node": ">= 20.19.4" - } - }, - "node_modules/@react-native/dev-middleware": { - "version": "0.83.4", - "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.83.4.tgz", - "integrity": "sha512-3s9nXZc/kj986nI2RPqxiIJeTS3o7pvZDxbHu7GE9WVIGX9YucA1l/tEiXd7BAm3TBFOfefDOT08xD46wH+R3Q==", - "license": "MIT", - "dependencies": { - "@isaacs/ttlcache": "^1.4.1", - "@react-native/debugger-frontend": "0.83.4", - "@react-native/debugger-shell": "0.83.4", - "chrome-launcher": "^0.15.2", - "chromium-edge-launcher": "^0.2.0", - "connect": "^3.6.5", - "debug": "^4.4.0", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "open": "^7.0.3", - "serve-static": "^1.16.2", - "ws": "^7.5.10" - }, - "engines": { - "node": ">= 20.19.4" - } - }, - "node_modules/@react-native/dev-middleware/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@react-native/gradle-plugin": { - "version": "0.83.4", - "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.83.4.tgz", - "integrity": "sha512-AhaSWw2k3eMKqZ21IUdM7rpyTYOpAfsBbIIiom1QQii3QccX0uW2AWTcRhfuWRxqr2faGFaOBYedWl2fzp5hgw==", - "license": "MIT", - "engines": { - "node": ">= 20.19.4" - } - }, - "node_modules/@react-native/js-polyfills": { - "version": "0.83.4", - "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.83.4.tgz", - "integrity": "sha512-wYUdv0rt4MjhKhQloO1AnGDXhZQOFZHDxm86dEtEA0WcsCdVrFdRULFM+rKUC/QQtJW2rS6WBqtBusgtrsDADg==", - "license": "MIT", - "engines": { - "node": ">= 20.19.4" - } - }, - "node_modules/@react-native/metro-babel-transformer": { - "version": "0.85.1", - "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.85.1.tgz", - "integrity": "sha512-oXAVv9GfGYxkqdf20o+gbJSw4yqaUZr7AZMZ4bJG8Nom/T9GmLu/Pd2kJo5U6NQYIndgfgU73pzRgL8H7YCIWw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/core": "^7.25.2", - "@react-native/babel-preset": "0.85.1", - "hermes-parser": "0.33.3", - "nullthrows": "^1.1.1" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - }, - "peerDependencies": { - "@babel/core": "*" - } - }, - "node_modules/@react-native/metro-babel-transformer/node_modules/hermes-estree": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.33.3.tgz", - "integrity": "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/@react-native/metro-babel-transformer/node_modules/hermes-parser": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.33.3.tgz", - "integrity": "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "hermes-estree": "0.33.3" - } - }, - "node_modules/@react-native/metro-config": { - "version": "0.85.1", - "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.85.1.tgz", - "integrity": "sha512-Na0OD2YFM7rESHJ3ETuYHnXNc5TJU/fpwlLmN2/uDTM9ZDb6EaEfFKZaXGbUm2lBYyeo/FG3Ur4glu8jLWMNgQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@react-native/js-polyfills": "0.85.1", - "@react-native/metro-babel-transformer": "0.85.1", - "metro-config": "^0.84.0", - "metro-runtime": "^0.84.0" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/@react-native/js-polyfills": { - "version": "0.85.1", - "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.85.1.tgz", - "integrity": "sha512-VseQZAKnDbmpZThLWviDIJ0NmuSiwiHA6vc2HNJTTVqTy2mQR0+858y9kDdDBQPYe0HH8+W1mYui2i4eUWGh4g==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@react-native/metro-config/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@react-native/metro-config/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@react-native/metro-config/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@react-native/metro-config/node_modules/ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/@react-native/metro-config/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/@react-native/metro-config/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@react-native/metro-config/node_modules/hermes-estree": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.35.0.tgz", - "integrity": "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/@react-native/metro-config/node_modules/hermes-parser": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.35.0.tgz", - "integrity": "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "hermes-estree": "0.35.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro/-/metro-0.84.3.tgz", - "integrity": "sha512-1h3lbVrE6hGf1e/764HfhPGg/bGrWMJDDh7G2rc4gFYZboVuI40BlG/y+UhtbhQDNlO/csMvrcnK0YrTlHUVew==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/core": "^7.25.2", - "@babel/generator": "^7.29.1", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "accepts": "^2.0.0", - "chalk": "^4.0.0", - "ci-info": "^2.0.0", - "connect": "^3.6.5", - "debug": "^4.4.0", - "error-stack-parser": "^2.0.6", - "flow-enums-runtime": "^0.0.6", - "graceful-fs": "^4.2.4", - "hermes-parser": "0.35.0", - "image-size": "^1.0.2", - "invariant": "^2.2.4", - "jest-worker": "^29.7.0", - "jsc-safe-url": "^0.2.2", - "lodash.throttle": "^4.1.1", - "metro-babel-transformer": "0.84.3", - "metro-cache": "0.84.3", - "metro-cache-key": "0.84.3", - "metro-config": "0.84.3", - "metro-core": "0.84.3", - "metro-file-map": "0.84.3", - "metro-resolver": "0.84.3", - "metro-runtime": "0.84.3", - "metro-source-map": "0.84.3", - "metro-symbolicate": "0.84.3", - "metro-transform-plugins": "0.84.3", - "metro-transform-worker": "0.84.3", - "mime-types": "^3.0.1", - "nullthrows": "^1.1.1", - "serialize-error": "^2.1.0", - "source-map": "^0.5.6", - "throat": "^5.0.0", - "ws": "^7.5.10", - "yargs": "^17.6.2" - }, - "bin": { - "metro": "src/cli.js" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro-babel-transformer": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.84.3.tgz", - "integrity": "sha512-svAA+yMLpeMiGcz/jKJs4oHpIGEx4nBqNEJ5AGj4CYIg1efvK+A0TjR6tgIuc6tKO5e8JmN/1lglpN2+f3/z/w==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/core": "^7.25.2", - "flow-enums-runtime": "^0.0.6", - "hermes-parser": "0.35.0", - "metro-cache-key": "0.84.3", - "nullthrows": "^1.1.1" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro-cache": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.84.3.tgz", - "integrity": "sha512-0QElxwLaHqLZf+Xqio8QrjVbuXP/8sJfQBGSPiITlKDVXrVLefuzYVSH9Sj+QL6lrPj2gYZd/iwQh1yZuVKnLA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "exponential-backoff": "^3.1.1", - "flow-enums-runtime": "^0.0.6", - "https-proxy-agent": "^7.0.5", - "metro-core": "0.84.3" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro-cache-key": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.84.3.tgz", - "integrity": "sha512-TnSL1Fdvrw+2glTdBSRmA5TL8l/i16ECjsrUdf3E5HncA+sNx8KcwDG8r+3ct1UhfYcusJypzZqTN55FZZcwGg==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "flow-enums-runtime": "^0.0.6" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro-config": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.84.3.tgz", - "integrity": "sha512-JmCzZWOETR+O22q8oPBWyQppx3roU9EbkbGzD8Gf1jukQ4b5T1fTzqqHruu6K4sTiNq5zVQySmKF6bp4kVARew==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "connect": "^3.6.5", - "flow-enums-runtime": "^0.0.6", - "jest-validate": "^29.7.0", - "metro": "0.84.3", - "metro-cache": "0.84.3", - "metro-core": "0.84.3", - "metro-runtime": "0.84.3", - "yaml": "^2.6.1" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro-core": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.84.3.tgz", - "integrity": "sha512-cc0pvAa80ai1nDmqqz0P59a+0ZqCZ/YHU/3jEekZL6spFnYDfX8iDLdn9FR6kX+67rmzKxHNrbrSRFLX2AYocw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "flow-enums-runtime": "^0.0.6", - "lodash.throttle": "^4.1.1", - "metro-resolver": "0.84.3" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro-file-map": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.84.3.tgz", - "integrity": "sha512-1cL4m4Jv1yRUt9RJExZQLfccscdlMNOcRG6LHLtmJhf3BG9j3MujPVc7CIpKYdFl+KUl+sdjge6oO3+meKCHQA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "debug": "^4.4.0", - "fb-watchman": "^2.0.0", - "flow-enums-runtime": "^0.0.6", - "graceful-fs": "^4.2.4", - "invariant": "^2.2.4", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "nullthrows": "^1.1.1", - "walker": "^1.0.7" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro-minify-terser": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.84.3.tgz", - "integrity": "sha512-3ofrG2OQyJbO9RNhCfOcl8QU7EE2WrSsnN5dFkuZaJO5+4Imujr9bUXmspeNlXRsOVk0F/rVRbEFH98lFSCkBQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "flow-enums-runtime": "^0.0.6", - "terser": "^5.15.0" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro-resolver": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.84.3.tgz", - "integrity": "sha512-pjEzGDtoM8DTHAIPK/9u9ZxszEiuRohYUVImWvgbnB91V4gqYJpQcoEYUugf2NIm1lrX5HNu0OvNqWmPBnGYjA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "flow-enums-runtime": "^0.0.6" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro-runtime": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.84.3.tgz", - "integrity": "sha512-o7HLRfMyVk9N2dUZ9VjQfB6xxUItL9Pi9WcqxURE7MEKOH6wbGt9/E92YdYLluTOtkzYAEVfdC6h6lcxqA+hMQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/runtime": "^7.25.0", - "flow-enums-runtime": "^0.0.6" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro-source-map": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.84.3.tgz", - "integrity": "sha512-jS48CeSzw78M8y6VE0f9uy3lVmfbOS677j2VCxnlmlYmnahcXuC6IhoN9K6LynNvos9517yUadcfgioju38xYQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "flow-enums-runtime": "^0.0.6", - "invariant": "^2.2.4", - "metro-symbolicate": "0.84.3", - "nullthrows": "^1.1.1", - "ob1": "0.84.3", - "source-map": "^0.5.6", - "vlq": "^1.0.0" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro-symbolicate": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.84.3.tgz", - "integrity": "sha512-J9Tpo8NCycYrozRvBIUyOwGAu4xkawOsAppmTscFiaegK0WvuDGwIM53GbzVSnytCHjVAF0io5GQxpkrKTuc7g==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "flow-enums-runtime": "^0.0.6", - "invariant": "^2.2.4", - "metro-source-map": "0.84.3", - "nullthrows": "^1.1.1", - "source-map": "^0.5.6", - "vlq": "^1.0.0" - }, - "bin": { - "metro-symbolicate": "src/index.js" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro-transform-plugins": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.84.3.tgz", - "integrity": "sha512-8S3baq2XhBaafHEH5Q8sJW6tmzsEJk80qKc3RU/nZV1MsnYq94RdjTUR6AyKjQd6Rfsk1BtBxhtiNnk7mgslCg==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/core": "^7.25.2", - "@babel/generator": "^7.29.1", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "flow-enums-runtime": "^0.0.6", - "nullthrows": "^1.1.1" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/metro-transform-worker": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.84.3.tgz", - "integrity": "sha512-Wjba7PyYktNRsHbPmkx2J2UX32rAzcDXjCu49zPHeF/viJlYJhwRaNePQcHaCRqQ+kmgQT4ThprsnJfDj71ZMA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/core": "^7.25.2", - "@babel/generator": "^7.29.1", - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "flow-enums-runtime": "^0.0.6", - "metro": "0.84.3", - "metro-babel-transformer": "0.84.3", - "metro-cache": "0.84.3", - "metro-cache-key": "0.84.3", - "metro-minify-terser": "0.84.3", - "metro-source-map": "0.84.3", - "metro-transform-plugins": "0.84.3", - "nullthrows": "^1.1.1" - }, - "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@react-native/metro-config/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "mime-db": "^1.54.0" + "@react-native/metro-config": { + "optional": true + } + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">=10" } }, - "node_modules/@react-native/metro-config/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "optional": true, - "peer": true, + "node_modules/@react-native/debugger-frontend": { + "version": "0.83.4", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.83.4.tgz", + "integrity": "sha512-mCE2s/S7SEjax3gZb6LFAraAI3x13gRVWJWqT0HIm71e4ITObENNTDuMw4mvZ/wr4Gz2wv4FcBH5/Nla9LXOcg==", + "license": "BSD-3-Clause", "engines": { - "node": ">= 0.6" + "node": ">= 20.19.4" } }, - "node_modules/@react-native/metro-config/node_modules/ob1": { - "version": "0.84.3", - "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.84.3.tgz", - "integrity": "sha512-J7554Ef8bzmKaDY365Afq6PF+qtdnY/d5PKUQFrsKlZHV/N3OGZewVrvDrQDyX5V5NJjTpcAKtlrFZcDr+HvpQ==", + "node_modules/@react-native/debugger-shell": { + "version": "0.83.4", + "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.83.4.tgz", + "integrity": "sha512-FtAnrvXqy1xeZ+onwilvxEeeBsvBlhtfrHVIC2R/BOJAK9TbKEtFfjio0wsn3DQIm+UZq48DSa+p9jJZ2aJUww==", "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "flow-enums-runtime": "^0.0.6" + "cross-spawn": "^7.0.6", + "fb-dotslash": "0.5.8" }, "engines": { - "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" - } - }, - "node_modules/@react-native/metro-config/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "license": "BSD-3-Clause", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" + "node": ">= 20.19.4" } }, - "node_modules/@react-native/metro-config/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/@react-native/dev-middleware": { + "version": "0.83.4", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.83.4.tgz", + "integrity": "sha512-3s9nXZc/kj986nI2RPqxiIJeTS3o7pvZDxbHu7GE9WVIGX9YucA1l/tEiXd7BAm3TBFOfefDOT08xD46wH+R3Q==", "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "has-flag": "^4.0.0" + "@isaacs/ttlcache": "^1.4.1", + "@react-native/debugger-frontend": "0.83.4", + "@react-native/debugger-shell": "0.83.4", + "chrome-launcher": "^0.15.2", + "chromium-edge-launcher": "^0.2.0", + "connect": "^3.6.5", + "debug": "^4.4.0", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "open": "^7.0.3", + "serve-static": "^1.16.2", + "ws": "^7.5.10" }, "engines": { - "node": ">=8" + "node": ">= 20.19.4" } }, - "node_modules/@react-native/metro-config/node_modules/ws": { + "node_modules/@react-native/dev-middleware/node_modules/ws": { "version": "7.5.10", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "license": "MIT", - "optional": true, - "peer": true, "engines": { "node": ">=8.3.0" }, @@ -7323,6 +6595,24 @@ } } }, + "node_modules/@react-native/gradle-plugin": { + "version": "0.83.4", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.83.4.tgz", + "integrity": "sha512-AhaSWw2k3eMKqZ21IUdM7rpyTYOpAfsBbIIiom1QQii3QccX0uW2AWTcRhfuWRxqr2faGFaOBYedWl2fzp5hgw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/js-polyfills": { + "version": "0.83.4", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.83.4.tgz", + "integrity": "sha512-wYUdv0rt4MjhKhQloO1AnGDXhZQOFZHDxm86dEtEA0WcsCdVrFdRULFM+rKUC/QQtJW2rS6WBqtBusgtrsDADg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.4" + } + }, "node_modules/@react-native/normalize-colors": { "version": "0.83.4", "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.83.4.tgz", @@ -9621,6 +8911,40 @@ "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", "license": "BSD-2-Clause" }, + "node_modules/@zerodev/ecdsa-validator": { + "version": "5.4.9", + "resolved": "https://registry.npmjs.org/@zerodev/ecdsa-validator/-/ecdsa-validator-5.4.9.tgz", + "integrity": "sha512-9NVE8/sQIKRo42UOoYKkNdmmHJY8VlT4t+2MHD2ipLg21cpbY9fS17TGZh61+Bl3qlqc8pP23I6f89z9im7kuA==", + "license": "MIT", + "peerDependencies": { + "@zerodev/sdk": "^5.4.13", + "viem": "^2.28.0" + } + }, + "node_modules/@zerodev/sdk": { + "version": "5.5.10", + "resolved": "https://registry.npmjs.org/@zerodev/sdk/-/sdk-5.5.10.tgz", + "integrity": "sha512-WVyj2XR9F6zK2GdXrvappx7yo6zoJ46cWe42dOIArp3xDjFBninjA4O1O94MwohB0G+yFqtXNsEU+WxdE67SgQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.6.0" + }, + "peerDependencies": { + "viem": "^2.28.0" + } + }, + "node_modules/@zerodev/sdk/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -9629,6 +8953,27 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/abitype": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", + "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -15482,33 +14827,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/expo/node_modules/react-native-worklets": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.8.1.tgz", - "integrity": "sha512-oWP/lStsAHU6oYCaWDXrda/wOHVdhusQJz1e6x9gPnXdFf4ndNDAOtWCmk2zGrAnlapfyA3rM6PCQq94mPg9cw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-class-properties": "^7.27.1", - "@babel/plugin-transform-classes": "^7.28.4", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1", - "@babel/plugin-transform-shorthand-properties": "^7.27.1", - "@babel/plugin-transform-template-literals": "^7.27.1", - "@babel/plugin-transform-unicode-regex": "^7.27.1", - "@babel/preset-typescript": "^7.27.1", - "convert-source-map": "^2.0.0", - "semver": "^7.7.3" - }, - "peerDependencies": { - "@babel/core": "*", - "@react-native/metro-config": "*", - "react": "*", - "react-native": "0.81 - 0.85" - } - }, "node_modules/expo/node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -17333,6 +16651,21 @@ "ws": "*" } }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -21601,6 +20934,69 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ox": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.12.4.tgz", + "integrity": "sha512-+P+C7QzuwPV8lu79dOwjBKfB2CbnbEXe/hfyyrff1drrO1nOOj3Hc87svHfcW1yneRr3WXaKr6nz11nq+/DF9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.2.3", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ox/node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/ox/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ox/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -26752,6 +26148,84 @@ } } }, + "node_modules/viem": { + "version": "2.46.3", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.46.3.tgz", + "integrity": "sha512-2LJS+Hyh2sYjHXQtzfv1kU9pZx9dxFzvoU/ZKIcn0FNtOU0HQuIICuYdWtUDFHaGXbAdVo8J1eCvmjkL9JVGwg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.2.3", + "isows": "1.0.7", + "ox": "0.12.4", + "ws": "8.18.3" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/vite": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", diff --git a/mobile/package.json b/mobile/package.json index 673c2c11e..227324fce 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -52,7 +52,7 @@ "@buildonspark/spark-sdk": "0.7.1", "@expo/vector-icons": "15.0.3", "@flashnet/sdk": "0.5.7", - "@lendasat/lendaswap-sdk-pure": "0.2.23", + "@lendasat/lendaswap-sdk-pure": "0.2.25", "@gorhom/bottom-sheet": "5.2.8", "@metamask/eth-sig-util": "8.2.0", "@noble/curves": "2.0.1", From a5376222af1ec6aee3631b6f382acf642206effd Mon Sep 17 00:00:00 2001 From: bonomat Date: Thu, 7 May 2026 15:17:07 +1000 Subject: [PATCH 10/11] feat(satora): derive signer xprv from wallet master seed The Satora SDK previously generated and persisted its own mnemonic, which meant a user restoring the wallet from their master seed would lose access to past Satora swaps (the signing identity was orphaned). Derive the signer xprv at m/44'/60'/100' from the master mnemonic and pass it via Client.builder().withXprv(). The path is a hardened sibling of the user's main EVM tree (m/44'/60'/0'/0/{n}) so the Satora signing identity shares no ancestor xprv with regular wallet keys but is fully recoverable from the single master backup. STORAGE_KEY_SATORA_WALLET now persists only the per-swap key index counter; the mnemonic methods on the WalletStorage adapter throw to make a regression in SDK behavior loud. --- .agents/swap.md | 4 +- shared/hooks/useTransferService.ts | 3 +- shared/services/satora-storage-adapter.ts | 34 ++++++--------- shared/services/transfer-service-satora.ts | 43 ++++++++++++++++--- .../unit-vi/transfer-service-satora.test.ts | 8 +++- 5 files changed, 60 insertions(+), 32 deletions(-) diff --git a/.agents/swap.md b/.agents/swap.md index a93ad146a..36b6f05de 100644 --- a/.agents/swap.md +++ b/.agents/swap.md @@ -88,8 +88,8 @@ getTrackingUrl?(execution): string | undefined ### Satora (`shared/services/transfer-service-satora.ts`) - **Pairs**: `native:lightning → token:rootstock:usdt0` (one-way, LN-only in v1). Arkade-as-source deferred. - **Model**: SDK returns a BOLT11; the wallet's internal LN wallet (user-picked) pays it; Satora server handles DEX swap + LayerZero USDT0 OFT bridge end-to-end. **No client-side claim/relay action**. -- **SDK**: `@lendasat/lendaswap-sdk-pure` (npm, `preview` tag). Initialized lazily via `Client.builder().withSignerStorage().withSwapStorage().withBaseUrl('https://api.lendaswap.com').withApiKey(...)`. The SDK auto-generates a Satora-only mnemonic on first build (separate from the wallet's master seed) and persists it via the storage adapter. -- **Storage adapters**: `shared/services/satora-storage-adapter.ts` — `SatoraWalletStorageAdapter` (mnemonic + key index) and `SatoraSwapStorageAdapter` (`StoredSwap[]`) backed by `IStorage`. Storage keys: `STORAGE_KEY_SATORA_WALLET`, `STORAGE_KEY_SATORA_SWAPS`. +- **SDK**: `@lendasat/lendaswap-sdk-pure` (npm, `preview` tag). Initialized lazily via `Client.builder().withXprv(...).withSignerStorage().withSwapStorage().withBaseUrl('https://api.lendaswap.com').withApiKey(...)`. The signing xprv is derived from the wallet's master mnemonic at `m/44'/60'/100'` — a hardened BIP44 account sibling of the user's main EVM tree (`m/44'/60'/0'/0/{n}`), so the Satora signing identity shares no ancestor xprv with regular wallet keys but is fully recoverable from the master seed. +- **Storage adapters**: `shared/services/satora-storage-adapter.ts` — `SatoraWalletStorageAdapter` (per-swap key index counter only — the SDK's xprv path never reads/writes a mnemonic) and `SatoraSwapStorageAdapter` (`StoredSwap[]`) backed by `IStorage`. Storage keys: `STORAGE_KEY_SATORA_WALLET`, `STORAGE_KEY_SATORA_SWAPS`. - **Asset**: `token:rootstock:usdt0` — registered in `shared/types/asset.ts`, resolved to USDT0 contract `0x779dED0C9e1022225F8e0630b35A9B54Be713736` (6 decimals) via `shared/models/asset-info.ts:resolveTokenId`. A `manuallyDefinedTokens` entry in `shared/models/token-list.ts` overrides the bundled evm tokenlist's `"$"` symbol with a clean `USDT0` — the manually-defined list is spread before the bundled lists so `getTokenInfo` returns it first. - **Bridge remap**: handled entirely by the SDK at runtime. When you pass `targetChain: '30'` + `targetToken: USDT0_ROOTSTOCK_ADDR` to `createSwap`, the SDK detects `isBridgeOnlyChain('30')` (`client.ts:3132-3149`), remaps the DEX swap to Arbitrum (`42161`), and populates `bridgeParams.targetChain='Rootstock'` + `bridgeParams.targetTokenAddress=USDT0_ROOTSTOCK_ADDR`. The SDK's declared `Chain` type omits '30' so we cast `ROOTSTOCK_CHAIN_ID as unknown as 'Lightning'` in the call site — a narrow escape hatch, documented inline. - **Status flow**: `pending → clientfundingseen → clientfunded → serverfunded → clientredeeming → clientredeemed → serverredeemed`. `clientredeemed` is the terminal success (USDT0 delivered to the user's Rootstock address). diff --git a/shared/hooks/useTransferService.ts b/shared/hooks/useTransferService.ts index 719f03c3b..5fb06d5c6 100644 --- a/shared/hooks/useTransferService.ts +++ b/shared/hooks/useTransferService.ts @@ -1,4 +1,5 @@ import { SparkWallet } from '../class/wallets/spark-wallet'; +import { getMasterSeed } from '../modules/wallet-utils'; import { FakeTransferService } from '../services/transfer-service-fake'; import { FlashnetTransferService } from '../services/transfer-service-flashnet'; import { GardenTransferService } from '../services/transfer-service-garden'; @@ -49,7 +50,7 @@ export function useTransferService(storage: IStorage): TransferServiceManager { console.warn('EXPO_PUBLIC_GARDEN_APP_ID not set — Garden Finance disabled'); } services.push(new SymbiosisTransferService(storage)); - services.push(new SatoraTransferService(storage, process.env.EXPO_PUBLIC_SATORA_API_KEY)); + services.push(new SatoraTransferService(storage, () => getMasterSeed(), process.env.EXPO_PUBLIC_SATORA_API_KEY)); _flashnetService = new FlashnetTransferService(storage, (accountNumber) => SparkWallet.getSDKWalletForAccount(accountNumber)); services.push(_flashnetService); _nativeDepositService = new NativeDepositTransferService(storage); diff --git a/shared/services/satora-storage-adapter.ts b/shared/services/satora-storage-adapter.ts index e3297bb7f..10294aed1 100644 --- a/shared/services/satora-storage-adapter.ts +++ b/shared/services/satora-storage-adapter.ts @@ -4,21 +4,17 @@ import type { StoredSwap, SwapStorage, WalletStorage } from '@lendasat/lendaswap import { IStorage, STORAGE_KEY_SATORA_SWAPS, STORAGE_KEY_SATORA_WALLET } from '../types/IStorage'; interface PersistedWallet { - mnemonic: string | null; keyIndex: number; } -const EMPTY_WALLET: PersistedWallet = { mnemonic: null, keyIndex: 0 }; +const EMPTY_WALLET: PersistedWallet = { keyIndex: 0 }; /** - * Backs the Satora SDK's WalletStorage with our IStorage. The mnemonic stored here - * is a Satora-only seed generated by the SDK on first build — it is independent - * from the user's main wallet seed. Only used to derive ephemeral EVM keys for - * gasless Permit2 signing of Satora swaps. + * Backs the Satora SDK's WalletStorage with our IStorage. * - * TODO: derive the Satora signer from the wallet master seed instead. Once that's - * done, getMnemonic() returns a derived value and there's nothing to persist — - * SecureStorage becomes unnecessary. + * We hand the SDK an xprv derived from the wallet's master seed (see + * `transfer-service-satora.ts`), so the mnemonic methods on this interface + * are never called by the SDK. Only the per-swap key index counter is persisted. */ export class SatoraWalletStorageAdapter implements WalletStorage { constructor(private readonly storage: IStorage) {} @@ -28,10 +24,7 @@ export class SatoraWalletStorageAdapter implements WalletStorage { if (!raw) return { ...EMPTY_WALLET }; try { const parsed = JSON.parse(raw) as Partial; - return { - mnemonic: typeof parsed.mnemonic === 'string' ? parsed.mnemonic : null, - keyIndex: typeof parsed.keyIndex === 'number' ? parsed.keyIndex : 0, - }; + return { keyIndex: typeof parsed.keyIndex === 'number' ? parsed.keyIndex : 0 }; } catch { return { ...EMPTY_WALLET }; } @@ -41,13 +34,13 @@ export class SatoraWalletStorageAdapter implements WalletStorage { await this.storage.setItem(STORAGE_KEY_SATORA_WALLET, JSON.stringify(value)); } + // The SDK's xprv path never reads/writes a mnemonic — see Client.builder().build(). + // These remain only to satisfy the WalletStorage interface; async getMnemonic(): Promise { - return (await this.load()).mnemonic; + throw new Error('SatoraWalletStorageAdapter: mnemonic methods not used in xprv mode'); } - - async setMnemonic(mnemonic: string): Promise { - const current = await this.load(); - await this.save({ ...current, mnemonic }); + async setMnemonic(_mnemonic: string): Promise { + throw new Error('SatoraWalletStorageAdapter: mnemonic methods not used in xprv mode'); } async getKeyIndex(): Promise { @@ -55,14 +48,13 @@ export class SatoraWalletStorageAdapter implements WalletStorage { } async setKeyIndex(index: number): Promise { - const current = await this.load(); - await this.save({ ...current, keyIndex: index }); + await this.save({ keyIndex: index }); } async incrementKeyIndex(): Promise { const current = await this.load(); const used = current.keyIndex; - await this.save({ ...current, keyIndex: used + 1 }); + await this.save({ keyIndex: used + 1 }); return used; } diff --git a/shared/services/transfer-service-satora.ts b/shared/services/transfer-service-satora.ts index b48ddb63b..208171779 100644 --- a/shared/services/transfer-service-satora.ts +++ b/shared/services/transfer-service-satora.ts @@ -1,4 +1,7 @@ +import ecc from '@bitcoinerlab/secp256k1'; import { Client, type GetSwapResponse, type LightningToEvmSwapResponse, type SwapStatus } from '@lendasat/lendaswap-sdk-pure'; +import BIP32Factory from 'bip32'; +import * as bip39 from 'bip39'; import BigNumber from 'bignumber.js'; import { IStorage, STORAGE_KEY_SATORA_SWAPS } from '../types/IStorage'; @@ -6,6 +9,22 @@ import type { AssetId } from '../types/asset'; import { EXECUTION_DEPOSIT, ITransferService, isTerminalStatus, TimelineStep, TransferExecution, TransferPair, TransferQuote, TransferStatus } from '../types/transfer'; import { SatoraSwapStorageAdapter, SatoraWalletStorageAdapter } from './satora-storage-adapter'; +const bip32 = BIP32Factory(ecc); + +/** + * Path used to derive the Satora signing xprv from the wallet's master seed. + * + * Hardened BIP44 account 100' under coin_type=60' — a sibling of the user's + * main EVM account tree (m/44'/60'/0'/0/{n}), so the Satora signing identity + * shares no common ancestor xprv with regular wallet keys. + * + * The SDK treats this xprv as a fresh master and derives swap-signing children + * underneath it (see Signer.deriveSwapParams). + */ +const SATORA_XPRV_PATH = "m/44'/60'/100'"; + +export type GetMasterMnemonic = () => string | Promise; + const PRUNE_AGE_SECONDS = 7 * 24 * 60 * 60; // 7 days const QUOTE_TTL_SECONDS = 60; // 1 minute const SATORA_BASE_URL = 'https://api.lendaswap.com'; @@ -55,24 +74,36 @@ export class SatoraTransferService implements ITransferService { private clientPromise?: Promise; private readonly storage: IStorage; private readonly apiKey: string | undefined; + private readonly getMasterMnemonic: GetMasterMnemonic; private readonly walletStorage: SatoraWalletStorageAdapter; private readonly swapStorage: SatoraSwapStorageAdapter; private readonly uncommitted = new Map(); - constructor(storage: IStorage, apiKey?: string) { + constructor(storage: IStorage, getMasterMnemonic: GetMasterMnemonic, apiKey?: string) { this.storage = storage; this.apiKey = apiKey && apiKey.length > 0 ? apiKey : undefined; + this.getMasterMnemonic = getMasterMnemonic; this.walletStorage = new SatoraWalletStorageAdapter(storage); this.swapStorage = new SatoraSwapStorageAdapter(storage); } + private async deriveSatoraXprv(): Promise { + const master = await this.getMasterMnemonic(); + if (!master) throw new Error('Satora signer requires the wallet to be unlocked'); + const seed = bip39.mnemonicToSeedSync(master); + return bip32.fromSeed(seed).derivePath(SATORA_XPRV_PATH).toBase58(); + } + private getClient(): Promise { if (!this.clientPromise) { - let builder = Client.builder().withSignerStorage(this.walletStorage).withSwapStorage(this.swapStorage).withBaseUrl(SATORA_BASE_URL); - if (this.apiKey) { - builder = builder.withApiKey(this.apiKey); - } - this.clientPromise = builder.build().catch((e) => { + this.clientPromise = (async () => { + const xprv = await this.deriveSatoraXprv(); + let builder = Client.builder().withXprv(xprv).withSignerStorage(this.walletStorage).withSwapStorage(this.swapStorage).withBaseUrl(SATORA_BASE_URL); + if (this.apiKey) { + builder = builder.withApiKey(this.apiKey); + } + return builder.build(); + })().catch((e) => { this.clientPromise = undefined; throw e; }); diff --git a/shared/tests/unit-vi/transfer-service-satora.test.ts b/shared/tests/unit-vi/transfer-service-satora.test.ts index 0113d12e8..2206165af 100644 --- a/shared/tests/unit-vi/transfer-service-satora.test.ts +++ b/shared/tests/unit-vi/transfer-service-satora.test.ts @@ -24,6 +24,9 @@ vi.mock('@lendasat/lendaswap-sdk-pure', () => { withApiKey() { return this; } + withXprv() { + return this; + } async build() { return { getQuote: mockGetQuote, @@ -53,6 +56,7 @@ function makeStorage() { const ROOTSTOCK_ADDRESS = '0x1234567890abcdefABCDEF1234567890abcdefAB'; const USDT0_ROOTSTOCK_ADDR = '0x779dED0C9e1022225F8e0630b35A9B54Be713736'; +const TEST_MASTER_MNEMONIC = 'install scatter logic circle pencil average fall shoe quantum disease suspect usage'; // 0.0001 BTC → 10_000 sats. SDK quote returns target_amount in USDT0 smallest units // (6 decimals). 1 BTC ≈ 100k USDT0 → 10k sats → ~10 USDT0 = 10_000_000 smallest units. @@ -114,7 +118,7 @@ describe('SatoraTransferService', () => { mockFundSwapGasless.mockReset(); mockClaimArkade.mockReset(); storage = makeStorage(); - service = new SatoraTransferService(storage); + service = new SatoraTransferService(storage, () => TEST_MASTER_MNEMONIC); }); afterEach(() => { @@ -356,7 +360,7 @@ describe('SatoraTransferService', () => { await service.commitTransfer(exec); // New instance, same storage — the persisted transfer should be readable. - const service2 = new SatoraTransferService(storage); + const service2 = new SatoraTransferService(storage, () => TEST_MASTER_MNEMONIC); mockGetSwap.mockResolvedValueOnce({ ...CREATE_SWAP_RESPONSE, status: 'pending' }); const transfers = await service2.getOngoingTransfers(0); expect(transfers).toHaveLength(1); From f66c3faef76437746bc151ffa1fbd93bfc37881b Mon Sep 17 00:00:00 2001 From: bonomat Date: Thu, 7 May 2026 15:42:15 +1000 Subject: [PATCH 11/11] chore(satora): bump sdk to 0.2.26 --- ext/package-lock.json | 8 ++++---- ext/package.json | 2 +- mobile/package-lock.json | 8 ++++---- mobile/package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ext/package-lock.json b/ext/package-lock.json index 40d6d03ee..9a1307ef4 100644 --- a/ext/package-lock.json +++ b/ext/package-lock.json @@ -15,7 +15,7 @@ "@breeztech/breez-sdk-liquid": "0.12.2", "@buildonspark/spark-sdk": "0.7.1", "@flashnet/sdk": "0.5.7", - "@lendasat/lendaswap-sdk-pure": "0.2.25", + "@lendasat/lendaswap-sdk-pure": "0.2.26", "@metamask/eth-sig-util": "8.2.0", "@noble/hashes": "1.7.1", "@noble/secp256k1": "1.6.3", @@ -3629,9 +3629,9 @@ "license": "MIT" }, "node_modules/@lendasat/lendaswap-sdk-pure": { - "version": "0.2.25", - "resolved": "https://registry.npmjs.org/@lendasat/lendaswap-sdk-pure/-/lendaswap-sdk-pure-0.2.25.tgz", - "integrity": "sha512-8JFtAOEd+NakflFump8fPLABMNdWd72BLo+lWmoZi9dmSv6s/XyS24Fvopl8nsgNmFBunaUMgX9nPdbBRVSs4w==", + "version": "0.2.26", + "resolved": "https://registry.npmjs.org/@lendasat/lendaswap-sdk-pure/-/lendaswap-sdk-pure-0.2.26.tgz", + "integrity": "sha512-DHMND4yXB/RH2T4RDKfM4Bey+N0FXeqKoACHbX1mvhEdEGZB2zSWuwIc38jY7MoXk6x6IG0IZKkTPyPGfsSs+w==", "license": "MIT", "dependencies": { "@arkade-os/sdk": "^0.4.6", diff --git a/ext/package.json b/ext/package.json index 42a7dd77a..f5c20c6ea 100755 --- a/ext/package.json +++ b/ext/package.json @@ -27,7 +27,7 @@ "@breeztech/breez-sdk-liquid": "0.12.2", "@buildonspark/spark-sdk": "0.7.1", "@flashnet/sdk": "0.5.7", - "@lendasat/lendaswap-sdk-pure": "0.2.25", + "@lendasat/lendaswap-sdk-pure": "0.2.26", "@metamask/eth-sig-util": "8.2.0", "@noble/hashes": "1.7.1", "@noble/secp256k1": "1.6.3", diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 56980fd4f..0a14a8704 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -20,7 +20,7 @@ "@expo/vector-icons": "15.0.3", "@flashnet/sdk": "0.5.7", "@gorhom/bottom-sheet": "5.2.8", - "@lendasat/lendaswap-sdk-pure": "0.2.25", + "@lendasat/lendaswap-sdk-pure": "0.2.26", "@metamask/eth-sig-util": "8.2.0", "@noble/curves": "2.0.1", "@noble/hashes": "1.7.1", @@ -5632,9 +5632,9 @@ } }, "node_modules/@lendasat/lendaswap-sdk-pure": { - "version": "0.2.25", - "resolved": "https://registry.npmjs.org/@lendasat/lendaswap-sdk-pure/-/lendaswap-sdk-pure-0.2.25.tgz", - "integrity": "sha512-8JFtAOEd+NakflFump8fPLABMNdWd72BLo+lWmoZi9dmSv6s/XyS24Fvopl8nsgNmFBunaUMgX9nPdbBRVSs4w==", + "version": "0.2.26", + "resolved": "https://registry.npmjs.org/@lendasat/lendaswap-sdk-pure/-/lendaswap-sdk-pure-0.2.26.tgz", + "integrity": "sha512-DHMND4yXB/RH2T4RDKfM4Bey+N0FXeqKoACHbX1mvhEdEGZB2zSWuwIc38jY7MoXk6x6IG0IZKkTPyPGfsSs+w==", "license": "MIT", "dependencies": { "@arkade-os/sdk": "^0.4.6", diff --git a/mobile/package.json b/mobile/package.json index 227324fce..ad9f8f0f2 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -52,7 +52,7 @@ "@buildonspark/spark-sdk": "0.7.1", "@expo/vector-icons": "15.0.3", "@flashnet/sdk": "0.5.7", - "@lendasat/lendaswap-sdk-pure": "0.2.25", + "@lendasat/lendaswap-sdk-pure": "0.2.26", "@gorhom/bottom-sheet": "5.2.8", "@metamask/eth-sig-util": "8.2.0", "@noble/curves": "2.0.1",