diff --git a/cli/package.json b/cli/package.json index da18bd3..0ab7e5c 100644 --- a/cli/package.json +++ b/cli/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "@arkade-os/boltz-swap": "^0.2.18", - "@arkade-os/sdk": "^0.3.12", + "@arkade-os/sdk": "^0.4.10", "@lendasat/lendaswap-sdk-pure": "^0.0.2", "@noble/hashes": "^2.0.1", "@scure/base": "^2.0.0", diff --git a/cli/src/config.ts b/cli/src/config.ts index cc285da..0c19978 100644 --- a/cli/src/config.ts +++ b/cli/src/config.ts @@ -9,6 +9,7 @@ export interface CashConfig { publicKey: string; arkServerUrl: string; network: string; + delegatorUrl?: string; } const CONFIG_DIR = join(homedir(), ".clw-cash"); @@ -55,6 +56,10 @@ export function loadConfig(overrides?: Partial): CashConfig { process.env.CLW_NETWORK ?? fileConfig.network ?? "bitcoin", + delegatorUrl: + overrides?.delegatorUrl ?? + process.env.CLW_DELEGATOR_URL ?? + fileConfig.delegatorUrl, }; return config; @@ -69,22 +74,24 @@ export interface ConfigEntry { export type CashConfigWithSources = Record; -const ENV_KEYS: Record = { +const ENV_KEYS: Record, string> = { apiBaseUrl: "CLW_API_URL", sessionToken: "CLW_SESSION_TOKEN", identityId: "CLW_IDENTITY_ID", publicKey: "CLW_PUBLIC_KEY", arkServerUrl: "CLW_ARK_SERVER_URL", network: "CLW_NETWORK", + delegatorUrl: "CLW_DELEGATOR_URL", }; -const DEFAULTS: Record = { +const DEFAULTS: Record, string> = { apiBaseUrl: "https://api.clw.cash", sessionToken: "", identityId: "", publicKey: "", arkServerUrl: "https://arkade.computer", network: "bitcoin", + delegatorUrl: "", }; export function loadConfigWithSources(): CashConfigWithSources { diff --git a/cli/src/context.ts b/cli/src/context.ts index c1f53dd..743968f 100644 --- a/cli/src/context.ts +++ b/cli/src/context.ts @@ -2,7 +2,7 @@ import { join } from "node:path"; import { homedir } from "node:os"; import { mkdirSync } from "node:fs"; import { RemoteSignerIdentity } from "@clw-cash/sdk"; -import { Wallet } from "@arkade-os/sdk"; +import { Wallet, RestDelegatorProvider } from "@arkade-os/sdk"; import { FileSystemStorageAdapter } from "@arkade-os/sdk/adapters/fileSystem"; import { SqliteWalletStorage, @@ -46,6 +46,9 @@ export async function createContext(config: CashConfig, opts?: CreateContextOpts identity, arkServerUrl: config.arkServerUrl, storage: walletStorage, + ...(config.delegatorUrl + ? { delegatorProvider: new RestDelegatorProvider(config.delegatorUrl) } + : {}), }); const bitcoin = new ArkadeBitcoinSkill(wallet); diff --git a/cli/src/index.ts b/cli/src/index.ts index 4bc4ef8..a6ad85f 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -1,4 +1,6 @@ import minimist from "minimist"; +import type { ExtendedVirtualCoin, IncomingFunds } from "@arkade-os/sdk"; +import { VtxoManager } from "@arkade-os/sdk"; import { loadConfig, validateConfig, getSessionStatus, saveConfig } from "./config.js"; import { createContext } from "./context.js"; import { outputError } from "./output.js"; @@ -74,6 +76,7 @@ Environment: CLW_ARK_SERVER_URL Arkade server URL CLW_NETWORK Network (bitcoin|testnet) CLW_DAEMON_PORT Daemon port (default: 3457) + CLW_DELEGATOR_URL Delegator service URL (e.g. https://delegate.arkade.money) `; const argv = minimist(process.argv.slice(2), { @@ -309,6 +312,57 @@ async function runDaemon() { monitor.start(); console.error("[daemon] lendaswap monitor started"); + // Recover swept VTXOs on startup + const wallet = ctx.bitcoin.getWallet(); + const vtxoManager = new VtxoManager(wallet); + void (async () => { + try { + const recoverable = await vtxoManager.getRecoverableBalance(); + if (recoverable.recoverable > 0n) { + console.error(`[daemon] recovering ${recoverable.recoverable} sats in swept vtxo(s)...`); + await vtxoManager.recoverVtxos(); + console.error("[daemon] vtxo recovery complete"); + } + } catch (err) { + console.error(`[daemon] vtxo recovery error: ${err instanceof Error ? err.message : err}`); + } + })(); + + // Auto-delegate VTXOs when delegator is configured + let stopVtxoDelegate: (() => void) | undefined; + const delegatorManager = await wallet.getDelegatorManager(); + if (delegatorManager) { + const delegateSettled = async () => { + try { + const vtxos = (await wallet.getVtxos({ withRecoverable: false })).filter( + (v: ExtendedVirtualCoin) => v.virtualStatus.state === "settled" + ); + if (vtxos.length === 0) return; + const address = await wallet.getAddress(); + const result = await delegatorManager.delegate(vtxos, address); + if (result.delegated.length > 0) { + console.error(`[daemon] delegated ${result.delegated.length} vtxo(s) for auto-renewal`); + } + } catch (err) { + console.error(`[daemon] vtxo delegation error: ${err instanceof Error ? err.message : err}`); + } + }; + + // Delegate any existing settled VTXOs on startup + void delegateSettled(); + + // Subscribe and delegate on every incoming VTXO + wallet.notifyIncomingFunds((funds: IncomingFunds) => { + if (funds.type === "vtxo" && funds.newVtxos.length > 0) { + void delegateSettled(); + } + }).then((stop: () => void) => { stopVtxoDelegate = stop; }).catch((err: unknown) => { + console.error(`[daemon] vtxo delegate subscription error: ${err instanceof Error ? err.message : err}`); + }); + + console.error("[daemon] vtxo auto-delegation started"); + } + // Start HTTP server server.listen(port, "127.0.0.1", () => { saveDaemonPid(process.pid, port); @@ -318,6 +372,7 @@ async function runDaemon() { // Graceful shutdown const shutdown = async () => { console.error("[daemon] shutting down..."); + stopVtxoDelegate?.(); monitor.stop(); authMonitor.stop(); await ctx.lightning.stopSwapManager(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 71e062b..83af3bf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -55,8 +55,8 @@ importers: specifier: ^0.2.18 version: 0.2.20 '@arkade-os/sdk': - specifier: ^0.3.12 - version: 0.3.13 + specifier: ^0.4.10 + version: 0.4.10 '@lendasat/lendaswap-sdk-pure': specifier: ^0.0.2 version: 0.0.2 @@ -148,8 +148,8 @@ importers: specifier: ^0.2.18 version: 0.2.20 '@arkade-os/sdk': - specifier: ^0.3.12 - version: 0.3.13 + specifier: ^0.4.10 + version: 0.4.10 '@clw-cash/sdk': specifier: workspace:* version: link:../remote-signer-identity @@ -193,6 +193,30 @@ packages: resolution: {integrity: sha512-eC6XGifqVf2Xu+sf/3T2duUgLiXlgh/IQXHD+v09wsu8ik/CZedH4gvQpetTjRMKti3DdY/QVLC1OQTKKWgvqQ==} engines: {node: '>=20.0.0'} + '@arkade-os/sdk@0.4.10': + resolution: {integrity: sha512-7Ga7ywIDGiSfeLGyrvnJAPicOg7srcaYEdwlQOeDIWM+cmzDwKThj2KTJ2ZL50YAksoFMradAPAD+UAJs/Dwag==} + engines: {node: '>=22.12.0 <23', pnpm: '>=10.25.0 <11'} + peerDependencies: + '@react-native-async-storage/async-storage': '>=1.0.0' + expo: '>=54.0.0' + expo-background-task: ~1.0.10 + expo-sqlite: ~16.0.10 + expo-task-manager: ~14.0.9 + peerDependenciesMeta: + '@react-native-async-storage/async-storage': + optional: true + expo: + optional: true + expo-background-task: + optional: true + expo-sqlite: + optional: true + expo-task-manager: + optional: true + + '@bitcoinerlab/miniscript@1.4.3': + resolution: {integrity: sha512-gf7WK4dKJJJl+IgLGmkTOxXQLiCje9c9y4wTLC+cyt0tBDTiSXgsG0X8FFaXf4d+34b8B5p/EJGvRd7mEYB6mQ==} + '@cloudflare/kv-asset-handler@0.4.2': resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} engines: {node: '>=18.0.0'} @@ -711,6 +735,9 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@kukks/bitcoin-descriptors@3.1.0': + resolution: {integrity: sha512-7HOCZjufw3DIIDebx7/ZOeol6khkWoccM6vzee6t5bXOJoj/6ISdssEDhjxDgy88in/I39pKz+kzFukxK5eqVw==} + '@lendasat/lendaswap-sdk-pure@0.0.2': resolution: {integrity: sha512-4N9ugC/5ja7p5U0pRfRhopxu7Nld5u94117kklucmjDd8zdLd4BUdIVXObLhwgze+/WvAaU6jCp/S3Maks54Lw==} @@ -735,6 +762,10 @@ packages: resolution: {integrity: sha512-RiwZZeJnsTnhT+/gg2KvITJZhK5oagQrpZo+yQyd3mv3D5NAG2qEeEHpw7IkXRlpkoD45wl2o4ydHAvY9wyEfw==} engines: {node: '>= 20.19.0'} + '@noble/curves@2.0.1': + resolution: {integrity: sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==} + engines: {node: '>= 20.19.0'} + '@noble/hashes@1.8.0': resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} @@ -896,9 +927,18 @@ packages: '@scure/bip32@1.7.0': resolution: {integrity: sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==} + '@scure/bip32@2.0.0': + resolution: {integrity: sha512-V24pyBWzKQqhB0CrV3Xz7JZr/cC2Nhp9LuIr9gOHkAKxSGwYqTIs8u0AkZgKA7HFqRiyoNg/PymiXOExm+3KlQ==} + + '@scure/bip32@2.0.1': + resolution: {integrity: sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==} + '@scure/bip39@1.6.0': resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} + '@scure/bip39@2.0.1': + resolution: {integrity: sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==} + '@scure/btc-signer@2.0.1': resolution: {integrity: sha512-vk5a/16BbSFZkhh1JIJ0+4H9nceZVo5WzKvJGGWiPp3sQOExeW+L53z3dI6u0adTPoE8ZbL+XEb6hEGzVZSvvQ==} @@ -1918,6 +1958,22 @@ snapshots: '@scure/btc-signer': 2.0.1 bip68: 1.0.4 + '@arkade-os/sdk@0.4.10': + dependencies: + '@kukks/bitcoin-descriptors': 3.1.0 + '@marcbachmann/cel-js': 7.3.1 + '@noble/curves': 2.0.0 + '@noble/secp256k1': 3.0.0 + '@scure/base': 2.0.0 + '@scure/bip32': 2.0.0 + '@scure/bip39': 2.0.1 + '@scure/btc-signer': 2.0.1 + bip68: 1.0.4 + + '@bitcoinerlab/miniscript@1.4.3': + dependencies: + bip68: 1.0.4 + '@cloudflare/kv-asset-handler@0.4.2': {} '@cloudflare/unenv-preset@2.12.1(unenv@2.0.0-rc.24)(workerd@1.20260212.0)': @@ -2223,6 +2279,15 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@kukks/bitcoin-descriptors@3.1.0': + dependencies: + '@bitcoinerlab/miniscript': 1.4.3 + '@noble/curves': 2.0.1 + '@noble/hashes': 2.0.1 + '@scure/base': 2.0.0 + '@scure/bip32': 2.0.1 + '@scure/btc-signer': 2.0.1 + '@lendasat/lendaswap-sdk-pure@0.0.2': dependencies: '@arkade-os/sdk': 0.3.13 @@ -2251,6 +2316,10 @@ snapshots: dependencies: '@noble/hashes': 2.0.0 + '@noble/curves@2.0.1': + dependencies: + '@noble/hashes': 2.0.1 + '@noble/hashes@1.8.0': {} '@noble/hashes@2.0.0': {} @@ -2358,11 +2427,28 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 + '@scure/bip32@2.0.0': + dependencies: + '@noble/curves': 2.0.0 + '@noble/hashes': 2.0.0 + '@scure/base': 2.0.0 + + '@scure/bip32@2.0.1': + dependencies: + '@noble/curves': 2.0.1 + '@noble/hashes': 2.0.1 + '@scure/base': 2.0.0 + '@scure/bip39@1.6.0': dependencies: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 + '@scure/bip39@2.0.1': + dependencies: + '@noble/hashes': 2.0.1 + '@scure/base': 2.0.0 + '@scure/btc-signer@2.0.1': dependencies: '@noble/curves': 2.0.0 diff --git a/skills/package.json b/skills/package.json index 22b237c..ca881aa 100644 --- a/skills/package.json +++ b/skills/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@clw-cash/sdk": "workspace:*", - "@arkade-os/sdk": "^0.3.12", + "@arkade-os/sdk": "^0.4.10", "@arkade-os/boltz-swap": "^0.2.18", "@lendasat/lendaswap-sdk-pure": "^0.0.2" },