diff --git a/.env.example b/.env.example index ff8490f..2307286 100644 --- a/.env.example +++ b/.env.example @@ -24,3 +24,7 @@ DEMO_AAVE_POOL= CHAIN=robinhood # robinhood (chain 46630) | arbitrumSepolia GUARDIAN_IMPL= # required: GuardianModule impl on the target chain (printed by DeployGuardian) RULES_ENGINE= # RulesEngineV1 (printed by DeployRules); also set VITE_RULES_ENGINE in site/.env +VAULT_FACTORY= # SafeVaultFactory (printed by DeployFactory); also set VITE_VAULT_FACTORY in site/.env + +# ── Gasless onboarding relayer (site/api/onboard.ts; set on the host, e.g. Vercel) ── +RELAYER_PRIVATE_KEY= # a funded testnet key that sponsors the delegate+configure tx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 350cb94..f8546b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,4 +59,5 @@ jobs: cache-dependency-path: site/pnpm-lock.yaml - run: pnpm install --frozen-lockfile - run: pnpm build + - run: pnpm typecheck:api - run: pnpm lint diff --git a/README.md b/README.md index d56d85a..20c8a77 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![Live demo](https://img.shields.io/badge/live-coincoin--five.vercel.app-000000?style=flat-square)](https://coincoin-five.vercel.app/) [![Deployed](https://img.shields.io/badge/deployed-Robinhood%20Chain%20testnet%20(46630)-7af7c0?style=flat-square)](#deployed-addresses) [![Solidity](https://img.shields.io/badge/Solidity-0.8.24-363636?style=flat-square&logo=solidity)](contracts/) -[![Tests](https://img.shields.io/badge/tests-90%20passing-27C93F?style=flat-square)](#testing) +[![CI](https://img.shields.io/github/actions/workflow/status/gamween/coincoin/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/gamween/coincoin/actions/workflows/ci.yml) [![License](https://img.shields.io/badge/License-MIT-F5D90A?style=flat-square)](LICENSE) @@ -49,7 +49,7 @@ coincoin closes the gap by enforcing at the account level via **EIP-7702** (a pr Four parts. The product is the watcher daemon and the contracts; there is no required UI. -1. **Delegate (once).** An EOA signs an EIP-7702 authorization pointing at `GuardianModule`, and in the same transaction calls `configure()` to set its policy: a **frozen** safe vault (settable once — a leaked key cannot repoint it) and an authorized **keeper** set. Policies can also be installed from an **EIP-712 signature** a relayer submits (`configureWithSig`), for gasless onboarding. +1. **Delegate (once).** An EOA signs an EIP-7702 authorization pointing at `GuardianModule`, and in the same transaction calls `configure()` to set its policy: a **frozen** safe vault (settable once — a leaked key cannot repoint it) and an authorized **keeper** set. Policies can also be installed from an **EIP-712 signature** a relayer submits (`configureWithSig`) — this powers the **one-click, gasless onboarding** in the `/app` dashboard (the wallet just signs; a relayer pays). 2. **Watch.** A TypeScript/viem daemon (`ChainThreatSource`) polls `Drained` logs of the monitored protocols directly from chain — no third-party feed, no websocket dependency. It catches up to head in bounded `getLogs` windows. 3. **React.** On a verified threat, the **keeper** — which is cryptographically bounded to two actions and nothing else — calls `exitAaveV3()` to unwind deposited positions back into the account, then `evacuateERC20()` to sweep every token to the safe vault. 4. **Firewall (proactive).** Calls routed through `execute()` are scored by a stateless `RulesEngineV1`; an unlimited `approve` / `increaseAllowance` / EIP-2612 `permit` / blanket `setApprovalForAll` to an untrusted spender reverts at the account level before it lands. @@ -93,13 +93,14 @@ Deployed and exercised on **Robinhood Chain testnet (chain 46630)** unless noted |---|---| | ✅ | EIP-7702 delegation + self-config · ERC-20 sweep · approval revocation | | ✅ | Frozen vault · signed multi-keeper policy (EIP-712 `configureWithSig`) | +| ✅ | **Gasless in-browser onboarding** — the wallet signs (EIP-712 policy + EIP-7702 authorization); a relayer (`site/api`) deploys the user's deterministic `SafeVaultFactory` vault and sponsors the delegate+configure tx (zero gas for the user) | | ✅ | Local firewall (`RulesEngineV1`) — unlimited-approval / `permit` / `setApprovalForAll` rules | | ✅ | Real on-chain detection → rescue loop, run end-to-end (funds at rest **and** a deposited DeFi position rescued in one keeper-driven sequence) | | ✅ | DeFi-exit engine (`exitAaveV3`) — built and verified against the **live Aave V3 Pool** via an Arbitrum One fork test | | 🛣️ | **Native Aave V3 integration** — built and ready; goes live the day Aave V3 is deployed on Robinhood Chain. Waiting on availability, not on code. | | 🛣️ | **GMX V2 position exit** — same pattern, once GMX is available on the target chain | | 🛣️ | Broader firewall coverage — Permit2, multicall, direct-transfer heuristics | -| 🛣️ | Policy asset/protocol scoping · Stylus rules engine · in-browser 7702 onboarding · security audit | +| 🛣️ | Policy asset/protocol scoping · Stylus rules engine · security audit | > [!NOTE] > Aave V3 and GMX V2 are not deployed on Robinhood Chain testnet, so the live end-to-end run exercises the exit engine against an Aave-V3-shaped lending pool. The exit code itself is verified against the real Aave V3 Pool in the fork test (`AaveRealFork.t.sol`) — it ships against the real protocol the moment one is available on the chain coincoin runs on. @@ -117,11 +118,11 @@ Deployed and exercised on **Robinhood Chain testnet (chain 46630)** unless noted ``` coincoin/ -├── contracts/ Foundry — GuardianModule (EIP-7702), RulesEngineV1, SafeVault, mocks, deploy scripts +├── contracts/ Foundry — GuardianModule (EIP-7702), RulesEngineV1, SafeVault(+Factory), mocks, scripts ├── watcher/ TypeScript/viem detection → rescue daemon (onboard · watch · exploit · revoke) -├── site/ Vite/React/Tailwind landing + /app dashboard +├── site/ Vite/React/Tailwind landing + /app dashboard + api/ (gasless onboarding relayer) ├── deployments/ On-chain address records (robinhood-testnet.json, arbitrum-sepolia.json) -├── docs/ Brand kit, design specs, submission notes +├── docs/readme/ README images ├── video/ Remotion pitch + demo videos (code → MP4) ├── .env.example Config template (RPC, disposable keys, demo addresses) └── LICENSE MIT @@ -160,9 +161,10 @@ Robinhood Chain testnet (chain `46630`) — [explorer](https://explorer.testnet. | Contract | Address | |---|---| -| `GuardianModule` (EIP-7702 guardian) | [`0xd0d301Aeaa7AA5Ced16C927030f131c9Cb083b77`](https://explorer.testnet.chain.robinhood.com/address/0xd0d301Aeaa7AA5Ced16C927030f131c9Cb083b77) | +| `GuardianModule` (EIP-7702 guardian) | [`0x9953BB30cFef2ac842C74417eA6DC661b492E8dA`](https://explorer.testnet.chain.robinhood.com/address/0x9953BB30cFef2ac842C74417eA6DC661b492E8dA) | | `RulesEngineV1` (firewall) | [`0xc20A9d7D38B07a9C74A1fD87A2e25CA1973Cbc52`](https://explorer.testnet.chain.robinhood.com/address/0xc20A9d7D38B07a9C74A1fD87A2e25CA1973Cbc52) | -| `SafeVault` (demo) | [`0x49be3DC48fC0540346A064fCC6Fc94FBaE62f479`](https://explorer.testnet.chain.robinhood.com/address/0x49be3DC48fC0540346A064fCC6Fc94FBaE62f479) | +| `SafeVaultFactory` (deterministic per-user vault) | [`0x1ef2B2539fa842A9c7e4EA07790aA6dBc47ec4A5`](https://explorer.testnet.chain.robinhood.com/address/0x1ef2B2539fa842A9c7e4EA07790aA6dBc47ec4A5) | +| `SafeVault` (demo) | [`0x530921CFFCeCc01B3Ad20E48A8c1707d27204b91`](https://explorer.testnet.chain.robinhood.com/address/0x530921CFFCeCc01B3Ad20E48A8c1707d27204b91) | The `GuardianModule` was also initially deployed and **Arbiscan-verified** on Arbitrum Sepolia at [`0x6671…200F`](https://sepolia.arbiscan.io/address/0x6671b4B73b79c284A710B00ef777d8E65f55200F). @@ -187,15 +189,15 @@ Non-custodial by construction, but **experimental and unaudited** — a research ## Testing -90 tests, written test-first. +95 tests, written test-first. ```bash -cd contracts && forge test # 64 unit/integration tests (+1 fork test, gated on ARBITRUM_ONE_RPC) +cd contracts && forge test # 69 unit/integration tests (+1 fork test, gated on ARBITRUM_ONE_RPC) ARBITRUM_ONE_RPC= forge test # includes AaveRealFork.t.sol against the live Aave V3 Pool cd ../watcher && pnpm test # 25 watcher tests (vitest) ``` -`AaveRealFork.t.sol` exits a real position against the **live Aave V3 Pool on Arbitrum One** (forked) — the same code path the guardian runs. The watcher suite covers the alert schema, exposure registry, keeper client, and the end-to-end orchestrator. +`AaveRealFork.t.sol` exits a real position against the **live Aave V3 Pool on Arbitrum One** (forked) — the same code path the guardian runs. The watcher suite covers the alert schema, exposure registry, keeper client, and the end-to-end orchestrator. The gasless onboarding path is proven end-to-end on testnet by `pnpm onboard:gasless` (a fresh, unfunded EOA signs; a relayer sponsors the delegate+configure tx). ## Buildathon @@ -206,5 +208,5 @@ Built for **[Arbitrum Open House London](https://arbitrum-london.hackquest.io/)* [MIT](LICENSE) © 2026 coincoin
-Built for ETHGlobal New York 2026 · Hedera — Tokenization track +Built for Arbitrum Open House London 2026 · Robinhood Chain track
diff --git a/contracts/script/DeployFactory.s.sol b/contracts/script/DeployFactory.s.sol new file mode 100644 index 0000000..6bc5bd7 --- /dev/null +++ b/contracts/script/DeployFactory.s.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Script, console2} from "forge-std/Script.sol"; +import {SafeVaultFactory} from "../src/SafeVaultFactory.sol"; + +/// @notice Deploys the SafeVaultFactory (deterministic per-owner SafeVault deployer). +/// Set its address as `VAULT_FACTORY` (.env) and `VITE_VAULT_FACTORY` (site) +/// to enable the gasless in-browser onboarding flow. +contract DeployFactory is Script { + function run() external returns (SafeVaultFactory factory) { + uint256 pk = vm.envUint("DEPLOYER_PRIVATE_KEY"); + vm.startBroadcast(pk); + factory = new SafeVaultFactory(); + vm.stopBroadcast(); + console2.log("SafeVaultFactory deployed at:", address(factory)); + } +} diff --git a/contracts/src/GuardianModule.sol b/contracts/src/GuardianModule.sol index 397d0ce..18d6ba8 100644 --- a/contracts/src/GuardianModule.sol +++ b/contracts/src/GuardianModule.sol @@ -89,7 +89,7 @@ contract GuardianModule { if (block.timestamp > deadline) revert Expired(); if (nonce != policyNonce) revert BadNonce(); bytes32 structHash = keccak256( - abi.encode(POLICY_TYPEHASH, p.safeVault, keccak256(abi.encodePacked(p.keepers)), nonce, deadline) + abi.encode(POLICY_TYPEHASH, p.safeVault, _hashKeepers(p.keepers), nonce, deadline) ); bytes32 digest = keccak256(abi.encodePacked("\x19\x01", _domainSeparator(), structHash)); if (ECDSA.recover(digest, sig) != address(this)) revert BadSignature(); @@ -99,6 +99,17 @@ contract GuardianModule { _applyPolicy(p.safeVault, p.keepers); } + /// @dev EIP-712 hash of the `address[] keepers` field: keccak256 of each address encoded as a + /// 32-byte word, concatenated — the standard array encoding, so a wallet's `signTypedData` + /// (e.g. in-browser gasless onboarding) recovers correctly. + function _hashKeepers(address[] calldata keepers_) internal pure returns (bytes32) { + bytes32[] memory words = new bytes32[](keepers_.length); + for (uint256 i; i < keepers_.length; ++i) { + words[i] = bytes32(uint256(uint160(keepers_[i]))); + } + return keccak256(abi.encodePacked(words)); + } + /// @notice The full authorized keeper set. function keepers() external view returns (address[] memory) { return _keeperList; diff --git a/contracts/src/SafeVaultFactory.sol b/contracts/src/SafeVaultFactory.sol new file mode 100644 index 0000000..8b2aff3 --- /dev/null +++ b/contracts/src/SafeVaultFactory.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; +import {SafeVault} from "./SafeVault.sol"; + +/// @title SafeVaultFactory +/// @notice Deploys one SafeVault per owner at a deterministic CREATE2 address. The +/// address is therefore known (counterfactual) BEFORE deployment, which is +/// what the gasless onboarding flow needs: the user signs an EIP-712 policy +/// against `vaultOf(user)`, then a relayer deploys the vault and configures +/// the guardian in one sponsored transaction. +contract SafeVaultFactory { + event VaultDeployed(address indexed owner, address vault); + + /// @dev One vault per owner → the salt is the owner address. + function _salt(address owner) internal pure returns (bytes32) { + return bytes32(uint256(uint160(owner))); + } + + /// @dev Init code = SafeVault creation bytecode + abi-encoded constructor arg (owner). + function _initCode(address owner) internal pure returns (bytes memory) { + return abi.encodePacked(type(SafeVault).creationCode, abi.encode(owner)); + } + + /// @notice The deterministic SafeVault address for `owner`, deployed or not. + function vaultOf(address owner) public view returns (address) { + return Create2.computeAddress(_salt(owner), keccak256(_initCode(owner))); + } + + /// @notice Whether `owner`'s SafeVault has already been deployed. + function isDeployed(address owner) external view returns (bool) { + return vaultOf(owner).code.length != 0; + } + + /// @notice Deploy `owner`'s SafeVault if it doesn't exist yet; returns its address. + /// Idempotent: a second call just returns the existing vault. + function deploy(address owner) external returns (address vault) { + vault = vaultOf(owner); + if (vault.code.length == 0) { + address deployed = Create2.deploy(0, _salt(owner), _initCode(owner)); + require(deployed == vault, "factory: address mismatch"); + emit VaultDeployed(owner, vault); + } + } +} diff --git a/contracts/test/PreAuthRegistry.t.sol b/contracts/test/PreAuthRegistry.t.sol index 022aeb9..bbfe34e 100644 --- a/contracts/test/PreAuthRegistry.t.sol +++ b/contracts/test/PreAuthRegistry.t.sol @@ -139,7 +139,10 @@ contract PreAuthSigTest is Test { bytes32 domainSep = keccak256( abi.encode(DOMAIN_TYPEHASH, keccak256(bytes("coincoin GuardianModule")), keccak256(bytes("1")), block.chainid, user) ); - bytes32 structHash = keccak256(abi.encode(POLICY_TYPEHASH, v, keccak256(abi.encodePacked(ks)), nonce, deadline)); + bytes32[] memory words = new bytes32[](ks.length); + for (uint256 i; i < ks.length; ++i) words[i] = bytes32(uint256(uint160(ks[i]))); + bytes32 keepersHash = keccak256(abi.encodePacked(words)); // standard EIP-712 address[] hash + bytes32 structHash = keccak256(abi.encode(POLICY_TYPEHASH, v, keepersHash, nonce, deadline)); return keccak256(abi.encodePacked("\x19\x01", domainSep, structHash)); } diff --git a/contracts/test/SafeVaultFactory.t.sol b/contracts/test/SafeVaultFactory.t.sol new file mode 100644 index 0000000..d5f2e29 --- /dev/null +++ b/contracts/test/SafeVaultFactory.t.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Test} from "forge-std/Test.sol"; +import {SafeVaultFactory} from "../src/SafeVaultFactory.sol"; +import {SafeVault} from "../src/SafeVault.sol"; + +contract SafeVaultFactoryTest is Test { + SafeVaultFactory factory; + address alice = address(0xA11CE); + address bob = address(0xB0B); + + function setUp() public { + factory = new SafeVaultFactory(); + } + + function test_VaultOfMatchesDeployedAddress() public { + address predicted = factory.vaultOf(alice); + assertEq(predicted.code.length, 0, "should not exist yet"); + address deployed = factory.deploy(alice); + assertEq(deployed, predicted, "deployed address must equal vaultOf"); + assertGt(deployed.code.length, 0, "vault should have code"); + } + + function test_DeployedVaultIsOwnedByOwner() public { + address vault = factory.deploy(alice); + assertEq(SafeVault(payable(vault)).owner(), alice, "vault owner must be the user"); + } + + function test_DeployIsIdempotent() public { + address first = factory.deploy(alice); + address second = factory.deploy(alice); // must not revert + assertEq(first, second, "second deploy returns the same vault"); + } + + function test_IsDeployedReflectsState() public { + assertFalse(factory.isDeployed(alice)); + factory.deploy(alice); + assertTrue(factory.isDeployed(alice)); + } + + function test_DistinctOwnersGetDistinctVaults() public { + assertTrue(factory.vaultOf(alice) != factory.vaultOf(bob), "vaults must differ per owner"); + } +} diff --git a/deployments/robinhood-testnet.json b/deployments/robinhood-testnet.json index 3e8fa90..5cd8f10 100644 --- a/deployments/robinhood-testnet.json +++ b/deployments/robinhood-testnet.json @@ -3,19 +3,21 @@ "network": "robinhood-chain-testnet", "rpc": "https://rpc.testnet.chain.robinhood.com/rpc", "contracts": { - "GuardianModule": "0xd0d301Aeaa7AA5Ced16C927030f131c9Cb083b77", - "RulesEngineV1": "0xc20A9d7D38B07a9C74A1fD87A2e25CA1973Cbc52" + "GuardianModule": "0x9953BB30cFef2ac842C74417eA6DC661b492E8dA", + "RulesEngineV1": "0xc20A9d7D38B07a9C74A1fD87A2e25CA1973Cbc52", + "SafeVaultFactory": "0x1ef2B2539fa842A9c7e4EA07790aA6dBc47ec4A5" }, "demo": { "_comment": "Live end-to-end detection->evacuation + DeFi-exit scenario (pnpm onboard/watch/exploit). MockAavePool holds a deposited position credited to the victim (exitAaveV3 target).", - "SafeVault": "0x49be3DC48fC0540346A064fCC6Fc94FBaE62f479", - "MockERC20": "0xC32C2eB815F1413ee2c7A68d2EFf3760d828841E", - "MockVulnerableProtocol": "0x6e8086CF791754b93bAf04F039b9f72e2bCF80Db", - "MockAavePool": "0xaf57676673B71CED42767841F1317A20484052BE", - "victim": "0xfa142e801447fc359E54e8E797357aA7cBf23368" + "SafeVault": "0x530921CFFCeCc01B3Ad20E48A8c1707d27204b91", + "MockERC20": "0x31052145BBFB8aA9B4e3713a2fD34e57b3A942f3", + "MockVulnerableProtocol": "0x43FfEEA2eAea3e2FB504cFF746b77171f86f6Ec0", + "MockAavePool": "0x7E8c3a15E45418c4B187505Bb21FBB05015aca93", + "victim": "0x46d167056f22BFB13bc2F940ae42b29c514280Cc" }, - "deployedAt": "2026-06-14", + "deployedAt": "2026-06-16", "deployer": "0xdae8992a9b5Fe850bE63781d1c2e65a3e496F728", - "explorer": "https://explorer.testnet.chain.robinhood.com/address/0xd0d301Aeaa7AA5Ced16C927030f131c9Cb083b77", - "note": "Robinhood Chain testnet (Arbitrum Orbit). This GuardianModule is the CURRENT build: EIP-7702 delegation + self-config, ERC-20 sweep, approval revocation, on-chain detection->evacuation, frozen vault, signed multi-keeper policy (configureWithSig), exitAaveV3 (DeFi exit), and the local firewall (execute + RulesEngineV1). The DeFi exit is also fork-verified against real Aave V3 on Arbitrum One (Aave isn't deployed on this chain; MockAavePool stands in here). Supersedes the earlier build 0x6671b4B73b79c284A710B00ef777d8E65f55200F (delegation + sweep + revoke only)." + "keeper": "0x627872F35b724222413e7421C9e40A26B2762B9e", + "explorer": "https://explorer.testnet.chain.robinhood.com/address/0x9953BB30cFef2ac842C74417eA6DC661b492E8dA", + "note": "Robinhood Chain testnet (Arbitrum Orbit). Current GuardianModule build: EIP-7702 delegation + self-config, ERC-20 sweep, approval revocation, on-chain detection->evacuation, frozen vault, signed multi-keeper policy (configureWithSig, EIP-712 standard encoding so a browser wallet's signTypedData verifies), exitAaveV3 (DeFi exit), and the local firewall (execute + RulesEngineV1). SafeVaultFactory deploys a deterministic per-user SafeVault for gasless in-browser onboarding (relayer-sponsored). Supersedes 0xd0d301Aeaa7AA5Ced16C927030f131c9Cb083b77 (pre-EIP-712-fix)." } diff --git a/site/.env.example b/site/.env.example index ae83752..5c9c6a5 100644 --- a/site/.env.example +++ b/site/.env.example @@ -3,3 +3,7 @@ # RulesEngineV1 address on Robinhood Chain — enables the dashboard's firewall controls. # Deploy it with contracts/script/DeployRules.s.sol; empty = the Enable button stays off. VITE_RULES_ENGINE= + +# SafeVaultFactory address (gasless onboarding). Optional — falls back to the deployed constant +# in src/app/contracts.ts. Deploy with contracts/script/DeployFactory.s.sol. +VITE_VAULT_FACTORY= diff --git a/site/api/onboard.ts b/site/api/onboard.ts new file mode 100644 index 0000000..dd11cc0 --- /dev/null +++ b/site/api/onboard.ts @@ -0,0 +1,116 @@ +import type { VercelRequest, VercelResponse } from "@vercel/node"; +import { + createPublicClient, + createWalletClient, + http, + encodeFunctionData, + parseAbi, + isAddress, + defineChain, + type SignedAuthorization, +} from "viem"; +import { verifyAuthorization } from "viem/utils"; +import { privateKeyToAccount } from "viem/accounts"; + +/// Gasless onboarding relayer. The browser sends the user's two off-chain signatures (an EIP-7702 +/// authorization delegating to the GuardianModule + an EIP-712 Policy for `configureWithSig`). The +/// relayer validates them against the canonical policy, deploys the user's deterministic SafeVault if +/// needed, then submits ONE sponsored type-4 transaction that delegates AND configures. The user +/// pays no gas. The relayer can only ever apply the canonical policy (official keeper + the user's +/// own factory vault) — it cannot delegate an account to arbitrary code or redirect funds. + +const CHAIN_ID = 46630; +const GUARDIAN = (process.env.GUARDIAN_IMPL ?? "0x9953BB30cFef2ac842C74417eA6DC661b492E8dA").toLowerCase(); +const FACTORY = (process.env.VAULT_FACTORY ?? "0x1ef2B2539fa842A9c7e4EA07790aA6dBc47ec4A5") as `0x${string}`; +const KEEPER = (process.env.KEEPER_ADDRESS ?? "0x627872F35b724222413e7421C9e40A26B2762B9e").toLowerCase(); +const RPC = process.env.ROBINHOOD_TESTNET_RPC ?? "https://rpc.testnet.chain.robinhood.com/rpc"; +const ORBIT_GAS = 5_000_000n; + +const robinhood = defineChain({ + id: CHAIN_ID, + name: "Robinhood Chain Testnet", + nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, + rpcUrls: { default: { http: [RPC] } }, +}); + +const FACTORY_ABI = parseAbi([ + "function vaultOf(address owner) view returns (address)", + "function deploy(address owner) returns (address)", + "function isDeployed(address owner) view returns (bool)", +]); +const GUARDIAN_ABI = parseAbi([ + "function configureWithSig((address safeVault, address[] keepers) p, uint256 nonce, uint256 deadline, bytes sig)", +]); + +type Body = { + owner: `0x${string}`; + authorization: SignedAuthorization; + policy: { safeVault: `0x${string}`; keepers: `0x${string}`[] }; + nonce: string | number; + deadline: string | number; + sig: `0x${string}`; +}; + +export default async function handler(req: VercelRequest, res: VercelResponse) { + if (req.method !== "POST") return res.status(405).json({ error: "POST only" }); + const relayerKey = process.env.RELAYER_PRIVATE_KEY as `0x${string}` | undefined; + if (!relayerKey) return res.status(500).json({ error: "relayer not configured" }); + + let body: Body; + try { + body = typeof req.body === "string" ? JSON.parse(req.body) : (req.body as Body); + } catch { + return res.status(400).json({ error: "invalid JSON" }); + } + const { owner, authorization, policy, nonce, deadline, sig } = body ?? ({} as Body); + + // ── validate ── + if (!owner || !isAddress(owner)) return res.status(400).json({ error: "bad owner" }); + if (!policy || !isAddress(policy.safeVault) || !Array.isArray(policy.keepers)) return res.status(400).json({ error: "bad policy" }); + if (Number(deadline) <= Math.floor(Date.now() / 1000)) return res.status(400).json({ error: "deadline passed" }); + if (policy.keepers.length !== 1 || policy.keepers[0].toLowerCase() !== KEEPER) { + return res.status(400).json({ error: "policy must use the canonical keeper" }); + } + if (!authorization || (authorization.address ?? "").toLowerCase() !== GUARDIAN) { + return res.status(400).json({ error: "authorization must target the GuardianModule" }); + } + if (authorization.chainId !== 0 && authorization.chainId !== CHAIN_ID) { + return res.status(400).json({ error: "authorization chainId mismatch" }); + } + + const pub = createPublicClient({ chain: robinhood, transport: http(RPC) }); + + // the policy must point at the user's OWN deterministic factory vault — nothing else + const expectedVault = (await pub.readContract({ address: FACTORY, abi: FACTORY_ABI, functionName: "vaultOf", args: [owner] })) as `0x${string}`; + if (policy.safeVault.toLowerCase() !== expectedVault.toLowerCase()) { + return res.status(400).json({ error: "safeVault is not the caller's factory vault" }); + } + // the authorization must have been signed by the owner + let validSigner: boolean; + try { + validSigner = await verifyAuthorization({ address: owner, authorization }); + } catch { + validSigner = false; + } + if (!validSigner) return res.status(400).json({ error: "authorization not signed by owner" }); + + // ── relay (pay gas) ── + const relayer = privateKeyToAccount(relayerKey); + const wallet = createWalletClient({ account: relayer, chain: robinhood, transport: http(RPC) }); + try { + const deployed = (await pub.readContract({ address: FACTORY, abi: FACTORY_ABI, functionName: "isDeployed", args: [owner] })) as boolean; + if (!deployed) { + const dtx = await wallet.writeContract({ address: FACTORY, abi: FACTORY_ABI, functionName: "deploy", args: [owner], gas: ORBIT_GAS }); + await pub.waitForTransactionReceipt({ hash: dtx }); + } + const data = encodeFunctionData({ + abi: GUARDIAN_ABI, + functionName: "configureWithSig", + args: [{ safeVault: policy.safeVault, keepers: policy.keepers }, BigInt(nonce), BigInt(deadline), sig], + }); + const txHash = await wallet.sendTransaction({ to: owner, data, authorizationList: [authorization], gas: ORBIT_GAS }); + return res.status(200).json({ ok: true, txHash, vault: expectedVault }); + } catch (e) { + return res.status(500).json({ error: "relay failed", detail: (e as Error).message }); + } +} diff --git a/site/package.json b/site/package.json index a804d72..cc7e799 100644 --- a/site/package.json +++ b/site/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", + "typecheck:api": "tsc -p tsconfig.api.json", "lint": "eslint .", "preview": "vite preview" }, @@ -23,6 +24,7 @@ "@types/node": "^24.12.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@vercel/node": "^5.8.17", "@vitejs/plugin-react": "^6.0.1", "autoprefixer": "^10.5.0", "eslint": "^10.3.0", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 370063e..089fa62 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -42,9 +42,12 @@ importers: '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.17) + '@vercel/node': + specifier: ^5.8.17 + version: 5.8.17 '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.2(vite@8.0.16(@types/node@24.13.2)(jiti@1.21.7)) + version: 6.0.2(vite@8.0.16(@types/node@24.13.2)(esbuild@0.27.0)(jiti@1.21.7)(tsx@4.21.0)) autoprefixer: specifier: ^10.5.0 version: 10.5.0(postcss@8.5.15) @@ -65,7 +68,7 @@ importers: version: 8.5.15 tailwindcss: specifier: ^3.4.19 - version: 3.4.19 + version: 3.4.19(tsx@4.21.0) typescript: specifier: ~6.0.2 version: 6.0.3 @@ -74,7 +77,7 @@ importers: version: 8.61.0(eslint@10.5.0(jiti@1.21.7))(typescript@6.0.3) vite: specifier: ^8.0.12 - version: 8.0.16(@types/node@24.13.2)(jiti@1.21.7) + version: 8.0.16(@types/node@24.13.2)(esbuild@0.27.0)(jiti@1.21.7)(tsx@4.21.0) packages: @@ -152,6 +155,29 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} + '@bytecodealliance/preview2-shim@0.17.6': + resolution: {integrity: sha512-n3cM88gTen5980UOBAD6xDcNNL3ocTK8keab21bpx1ONdA+ARj7uD1qoFxOWCyKlkpSi195FH+GeAut7Oc6zZw==} + + '@edge-runtime/format@2.2.1': + resolution: {integrity: sha512-JQTRVuiusQLNNLe2W9tnzBlV/GvSVcozLl4XZHk5swnRZ/v6jp8TqR8P7sqmJsQqblDZ3EztcWmLDbhRje/+8g==} + engines: {node: '>=16'} + + '@edge-runtime/node-utils@2.3.0': + resolution: {integrity: sha512-uUtx8BFoO1hNxtHjp3eqVPC/mWImGb2exOfGjMLUoipuWgjej+f4o/VP4bUI8U40gu7Teogd5VTeZUkGvJSPOQ==} + engines: {node: '>=16'} + + '@edge-runtime/ponyfill@2.4.2': + resolution: {integrity: sha512-oN17GjFr69chu6sDLvXxdhg0Qe8EZviGSuqzR9qOiKh4MhFYGdBBcqRNzdmYeAdeRzOW2mM9yil4RftUQ7sUOA==} + engines: {node: '>=16'} + + '@edge-runtime/primitives@4.1.0': + resolution: {integrity: sha512-Vw0lbJ2lvRUqc7/soqygUX216Xb8T3WBZ987oywz6aJqRxcwSVWwr9e+Nqo2m9bxobA9mdbWNNoRY6S9eko1EQ==} + engines: {node: '>=16'} + + '@edge-runtime/vm@3.2.0': + resolution: {integrity: sha512-0dEVyRLM/lG4gp1R/Ik5bfPl/1wX00xFwd5KcNH602tzBa09oF7pbTKETEhR1GjZ75K6OJnYFu8II2dyMhONMw==} + engines: {node: '>=16'} + '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -161,6 +187,162 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@esbuild/aix-ppc64@0.27.0': + resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.0': + resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.0': + resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.0': + resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.0': + resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.0': + resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.0': + resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.0': + resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.0': + resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.0': + resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.0': + resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.0': + resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.0': + resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.0': + resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.0': + resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.0': + resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.0': + resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.0': + resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.0': + resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.0': + resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.0': + resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.0': + resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.0': + resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.0': + resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.0': + resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.0': + resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -200,6 +382,10 @@ packages: resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@fastify/busboy@2.1.1': + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -220,6 +406,18 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.1': + resolution: {integrity: sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==} + engines: {node: 20 || >=22} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -236,6 +434,11 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@mapbox/node-pre-gyp@2.0.3': + resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==} + engines: {node: '>=18'} + hasBin: true + '@napi-rs/wasm-runtime@1.1.5': resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==} peerDependencies: @@ -269,6 +472,10 @@ packages: '@oxc-project/types@0.133.0': resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + '@renovatebot/pep440@4.2.1': + resolution: {integrity: sha512-2FK1hF93Fuf1laSdfiEmJvSJPVIDHEUTz68D3Fi9s0IZrrpaEcj6pTFBTbYvsgC5du4ogrtf5re7yMMvrKNgkw==} + engines: {node: ^20.9.0 || ^22.11.0 || ^24, pnpm: ^10.0.0} + '@rolldown/binding-android-arm64@1.0.3': resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -367,6 +574,15 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rollup/pluginutils@5.4.0': + resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@scure/base@1.2.6': resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} @@ -384,6 +600,9 @@ packages: peerDependencies: react: ^18 || ^19 + '@ts-morph/common@0.11.1': + resolution: {integrity: sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g==} + '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} @@ -396,6 +615,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node@20.11.0': + resolution: {integrity: sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==} + '@types/node@24.13.2': resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==} @@ -466,6 +688,26 @@ packages: resolution: {integrity: sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vercel/build-utils@13.30.0': + resolution: {integrity: sha512-fLIa8cpELsSoWbcxshaqegwlTCfKnbgsDmA6uIb+oA797xvSmYkPu5a781aRYCRdghgyOZF7NCSVgtZjHjunnQ==} + + '@vercel/error-utils@2.2.0': + resolution: {integrity: sha512-WFWiRxfPzoYWYifaj4thSKvAaZZwUOqD4k5GINRIgZgCiS2E3iAJbWbIsIZmkQdTecWFHcWGA6q48CjisgpOBA==} + + '@vercel/nft@1.10.0': + resolution: {integrity: sha512-iLOW4fcsgkipfOh2Bw3wB38YDfxTlxr7+j4uFeui2OswkNT28jIitS/aMce7tS0mef1YPQ8zLIDYr3a0aahNrA==} + engines: {node: '>=20'} + hasBin: true + + '@vercel/node@5.8.17': + resolution: {integrity: sha512-n2DVzblqS43LTs4BV1iLfx8tjjrTegcSsyDgRzn70h6tQePYWjl+h0bU3X1stpITZtaYJSZYwVevuX1BJNZHHg==} + + '@vercel/python-analysis@0.11.1': + resolution: {integrity: sha512-EPPLuXJQhIDUx08H9nG76AR2HSgBquwe3OAX5s2w20M923iaWeGGVkhX/4yZ89CJfXEZgE1Aj/mX7lVHOVIcYA==} + + '@vercel/static-config@3.4.0': + resolution: {integrity: sha512-wCq90CMUB//ggnFh77NQO1xaLFsS4LigQIqKrH6ohnr9Br/KI1FhlErx62WfCOuueWaW+LVsbLOqNXIUjK8t6A==} + '@vitejs/plugin-react@6.0.2': resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -528,6 +770,10 @@ packages: typescript: optional: true + abbrev@3.0.1: + resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} + engines: {node: ^18.17.0 || >=20.5.0} + abitype@1.2.3: resolution: {integrity: sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==} peerDependencies: @@ -539,6 +785,11 @@ packages: zod: optional: true + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -549,9 +800,16 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + ajv@8.6.3: + resolution: {integrity: sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -562,6 +820,20 @@ packages: arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + async-listen@3.0.0: + resolution: {integrity: sha512-V+SsTpDqkrWTimiotsyl33ePSjA5/KrithwupuvJ6ztsqPvGv6ge4OredFhPffVXiLN/QUWvE0XcqJaYgt6fOg==} + engines: {node: '>= 14'} + + async-listen@3.0.1: + resolution: {integrity: sha512-cWMaNwUJnf37C/S5TfCkk/15MwbPRwVYALA2jtjkbHjCmAPiDXyNJy2q3p1KAZzDLHAWyarUWSujUoHR4pEgrA==} + engines: {node: '>= 14'} + + async-sema@3.1.1: + resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} + autoprefixer@10.5.0: resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} engines: {node: ^10 || ^12 || >=14} @@ -569,6 +841,9 @@ packages: peerDependencies: postcss: ^8.1.0 + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -582,6 +857,12 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + brace-expansion@1.1.15: + resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} + brace-expansion@5.0.6: resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} @@ -606,10 +887,31 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + cjs-module-lexer@1.2.3: + resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} + + code-block-writer@10.1.1: + resolution: {integrity: sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw==} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + convert-hrtime@3.0.0: + resolution: {integrity: sha512-7V+KqSvMiHp8yWDuwfww06XleMWVVB9b9tURBx+G7UTADuo5hYPuowKloz4OzOqbPezxgo+fdQ1522WzPG4OeA==} + engines: {node: '>=8'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -647,6 +949,11 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + edge-runtime@2.5.9: + resolution: {integrity: sha512-pk+k0oK0PVXdlT4oRp4lwh+unuKB7Ng4iZ2HB+EZ7QCEQizX360Rp/F4aRpgpRgdP2ufB35N+1KppHmYjqIGSg==} + engines: {node: '>=16'} + hasBin: true + electron-to-chromium@1.5.372: resolution: {integrity: sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==} @@ -654,6 +961,17 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@1.4.1: + resolution: {integrity: sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==} + + es-module-lexer@1.5.0: + resolution: {integrity: sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==} + + esbuild@0.27.0: + resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -711,10 +1029,17 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} @@ -747,6 +1072,9 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -779,6 +1107,10 @@ packages: react-dom: optional: true + fs-extra@11.1.1: + resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} + engines: {node: '>=14.14'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -791,6 +1123,9 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -799,10 +1134,17 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + globals@17.6.0: resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} engines: {node: '>=18'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + gsap@3.15.0: resolution: {integrity: sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==} @@ -816,6 +1158,10 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -863,6 +1209,10 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -871,9 +1221,15 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-schema-to-ts@1.6.4: + resolution: {integrity: sha512-pR4yQ9DHz6itqswtHCm26mw45FSNfQ9rEQjosaZErhn5J3J2sIViQiz8rDaezjKAhFGpmsoczYVBgGHzFw/stA==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -882,6 +1238,9 @@ packages: engines: {node: '>=6'} hasBin: true + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -974,6 +1333,10 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lru-cache@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -985,10 +1348,33 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + minimatch@10.1.1: + resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} + engines: {node: 20 || >=22} + minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + mipd@0.0.7: resolution: {integrity: sha512-aAPZPNDQ3uMTdKbuO2YmAw2TxLHO0moa4YKAyETM/DTj5FloZo+a+8tU+iv4GmW+sOxKLSRwcSFuczk+Cpt6fg==} peerDependencies: @@ -997,12 +1383,21 @@ packages: typescript: optional: true + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + motion-dom@12.40.0: resolution: {integrity: sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg==} motion-utils@12.39.0: resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==} + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1017,10 +1412,28 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + node-fetch@2.6.9: + resolution: {integrity: sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + node-releases@2.0.47: resolution: {integrity: sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==} engines: {node: '>=18'} + nopt@8.1.0: + resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -1053,6 +1466,13 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + parse-ms@2.1.0: + resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==} + engines: {node: '>=6'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1064,6 +1484,19 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + path-to-regexp@6.1.0: + resolution: {integrity: sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1134,6 +1567,10 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + pretty-ms@7.0.1: + resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} + engines: {node: '>=10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1157,6 +1594,17 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.12: resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} engines: {node: '>= 0.4'} @@ -1194,6 +1642,14 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + signal-exit@4.0.2: + resolution: {integrity: sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==} + engines: {node: '>=14'} + + smol-toml@1.5.2: + resolution: {integrity: sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ==} + engines: {node: '>= 18'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1212,6 +1668,10 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + tar@7.5.16: + resolution: {integrity: sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==} + engines: {node: '>=18'} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -1219,6 +1679,10 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + time-span@4.0.0: + resolution: {integrity: sha512-MyqZCTGLDZ77u4k+jqg4UlrzPTPZ49NDlaekU6uuFaJLzPIN1woaRXCbGeqOfxwc3Y37ZROGAJ614Rdv7Olt+g==} + engines: {node: '>=10'} + tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} @@ -1227,6 +1691,9 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -1236,9 +1703,20 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-morph@12.0.0: + resolution: {integrity: sha512-VHC8XgU2fFW7yO1f/b3mxKDje1vmyzFXHWzOYmKEkCEwcLjDtbdLgBQviqj4ZwP4MJkQtRo6Ha2I29lq/B+VxA==} + + ts-toolbelt@6.15.5: + resolution: {integrity: sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -1250,14 +1728,30 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} hasBin: true + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici@5.28.4: + resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} + engines: {node: '>=14.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -1337,6 +1831,12 @@ packages: typescript: optional: true + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1361,6 +1861,10 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -1371,6 +1875,9 @@ packages: peerDependencies: zod: ^3.25.0 || ^4.0.0 + zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} @@ -1498,6 +2005,20 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 + '@bytecodealliance/preview2-shim@0.17.6': {} + + '@edge-runtime/format@2.2.1': {} + + '@edge-runtime/node-utils@2.3.0': {} + + '@edge-runtime/ponyfill@2.4.2': {} + + '@edge-runtime/primitives@4.1.0': {} + + '@edge-runtime/vm@3.2.0': + dependencies: + '@edge-runtime/primitives': 4.1.0 + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -1514,6 +2035,84 @@ snapshots: tslib: 2.8.1 optional: true + '@esbuild/aix-ppc64@0.27.0': + optional: true + + '@esbuild/android-arm64@0.27.0': + optional: true + + '@esbuild/android-arm@0.27.0': + optional: true + + '@esbuild/android-x64@0.27.0': + optional: true + + '@esbuild/darwin-arm64@0.27.0': + optional: true + + '@esbuild/darwin-x64@0.27.0': + optional: true + + '@esbuild/freebsd-arm64@0.27.0': + optional: true + + '@esbuild/freebsd-x64@0.27.0': + optional: true + + '@esbuild/linux-arm64@0.27.0': + optional: true + + '@esbuild/linux-arm@0.27.0': + optional: true + + '@esbuild/linux-ia32@0.27.0': + optional: true + + '@esbuild/linux-loong64@0.27.0': + optional: true + + '@esbuild/linux-mips64el@0.27.0': + optional: true + + '@esbuild/linux-ppc64@0.27.0': + optional: true + + '@esbuild/linux-riscv64@0.27.0': + optional: true + + '@esbuild/linux-s390x@0.27.0': + optional: true + + '@esbuild/linux-x64@0.27.0': + optional: true + + '@esbuild/netbsd-arm64@0.27.0': + optional: true + + '@esbuild/netbsd-x64@0.27.0': + optional: true + + '@esbuild/openbsd-arm64@0.27.0': + optional: true + + '@esbuild/openbsd-x64@0.27.0': + optional: true + + '@esbuild/openharmony-arm64@0.27.0': + optional: true + + '@esbuild/sunos-x64@0.27.0': + optional: true + + '@esbuild/win32-arm64@0.27.0': + optional: true + + '@esbuild/win32-ia32@0.27.0': + optional: true + + '@esbuild/win32-x64@0.27.0': + optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@10.5.0(jiti@1.21.7))': dependencies: eslint: 10.5.0(jiti@1.21.7) @@ -1548,6 +2147,8 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 + '@fastify/busboy@2.1.1': {} + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -1564,6 +2165,16 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.1': + dependencies: + '@isaacs/balanced-match': 4.0.1 + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.3 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1583,6 +2194,19 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@mapbox/node-pre-gyp@2.0.3': + dependencies: + consola: 3.4.2 + detect-libc: 2.1.2 + https-proxy-agent: 7.0.6 + node-fetch: 2.6.9 + nopt: 8.1.0 + semver: 7.8.4 + tar: 7.5.16 + transitivePeerDependencies: + - encoding + - supports-color + '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 @@ -1612,6 +2236,8 @@ snapshots: '@oxc-project/types@0.133.0': {} + '@renovatebot/pep440@4.2.1': {} + '@rolldown/binding-android-arm64@1.0.3': optional: true @@ -1663,6 +2289,12 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@rollup/pluginutils@5.4.0': + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.4 + '@scure/base@1.2.6': {} '@scure/bip32@1.7.0': @@ -1683,6 +2315,13 @@ snapshots: '@tanstack/query-core': 5.101.0 react: 19.2.7 + '@ts-morph/common@0.11.1': + dependencies: + fast-glob: 3.3.3 + minimatch: 3.1.5 + mkdirp: 1.0.4 + path-browserify: 1.0.1 + '@tybys/wasm-util@0.10.2': dependencies: tslib: 2.8.1 @@ -1694,6 +2333,10 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/node@20.11.0': + dependencies: + undici-types: 5.26.5 + '@types/node@24.13.2': dependencies: undici-types: 7.18.2 @@ -1797,10 +2440,82 @@ snapshots: '@typescript-eslint/types': 8.61.0 eslint-visitor-keys: 5.0.1 - '@vitejs/plugin-react@6.0.2(vite@8.0.16(@types/node@24.13.2)(jiti@1.21.7))': + '@vercel/build-utils@13.30.0': + dependencies: + '@vercel/python-analysis': 0.11.1 + cjs-module-lexer: 1.2.3 + es-module-lexer: 1.5.0 + + '@vercel/error-utils@2.2.0': {} + + '@vercel/nft@1.10.0': + dependencies: + '@mapbox/node-pre-gyp': 2.0.3 + '@rollup/pluginutils': 5.4.0 + acorn: 8.17.0 + acorn-import-attributes: 1.9.5(acorn@8.17.0) + async-sema: 3.1.1 + bindings: 1.5.0 + estree-walker: 2.0.2 + glob: 13.0.6 + graceful-fs: 4.2.11 + node-gyp-build: 4.8.4 + picomatch: 4.0.4 + resolve-from: 5.0.0 + transitivePeerDependencies: + - encoding + - rollup + - supports-color + + '@vercel/node@5.8.17': + dependencies: + '@edge-runtime/node-utils': 2.3.0 + '@edge-runtime/primitives': 4.1.0 + '@edge-runtime/vm': 3.2.0 + '@types/node': 20.11.0 + '@vercel/build-utils': 13.30.0 + '@vercel/error-utils': 2.2.0 + '@vercel/nft': 1.10.0 + '@vercel/static-config': 3.4.0 + async-listen: 3.0.0 + cjs-module-lexer: 1.2.3 + edge-runtime: 2.5.9 + es-module-lexer: 1.4.1 + esbuild: 0.27.0 + etag: 1.8.1 + mime-types: 2.1.35 + node-fetch: 2.6.9 + path-to-regexp: 6.1.0 + path-to-regexp-updated: path-to-regexp@6.3.0 + ts-morph: 12.0.0 + tsx: 4.21.0 + typescript: 5.9.3 + undici: 5.28.4 + transitivePeerDependencies: + - encoding + - rollup + - supports-color + + '@vercel/python-analysis@0.11.1': + dependencies: + '@bytecodealliance/preview2-shim': 0.17.6 + '@renovatebot/pep440': 4.2.1 + fs-extra: 11.1.1 + js-yaml: 4.1.1 + minimatch: 10.1.1 + smol-toml: 1.5.2 + zod: 3.22.4 + + '@vercel/static-config@3.4.0': + dependencies: + ajv: 8.6.3 + json-schema-to-ts: 1.6.4 + ts-morph: 12.0.0 + + '@vitejs/plugin-react@6.0.2(vite@8.0.16(@types/node@24.13.2)(esbuild@0.27.0)(jiti@1.21.7)(tsx@4.21.0))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.0.16(@types/node@24.13.2)(jiti@1.21.7) + vite: 8.0.16(@types/node@24.13.2)(esbuild@0.27.0)(jiti@1.21.7)(tsx@4.21.0) '@wagmi/connectors@8.0.15(@wagmi/core@3.5.0(@tanstack/query-core@5.101.0)(@types/react@19.2.17)(react@19.2.7)(typescript@6.0.3)(use-sync-external-store@1.4.0(react@19.2.7))(viem@2.52.2(typescript@6.0.3)(zod@4.4.3)))(typescript@6.0.3)(viem@2.52.2(typescript@6.0.3)(zod@4.4.3))': dependencies: @@ -1824,17 +2539,25 @@ snapshots: - react - use-sync-external-store + abbrev@3.0.1: {} + abitype@1.2.3(typescript@6.0.3)(zod@4.4.3): optionalDependencies: typescript: 6.0.3 zod: 4.4.3 + acorn-import-attributes@1.9.5(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + acorn-jsx@5.3.2(acorn@8.17.0): dependencies: acorn: 8.17.0 acorn@8.17.0: {} + agent-base@7.1.4: {} + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 @@ -1842,6 +2565,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.6.3: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + any-promise@1.3.0: {} anymatch@3.1.3: @@ -1851,6 +2581,14 @@ snapshots: arg@5.0.2: {} + argparse@2.0.1: {} + + async-listen@3.0.0: {} + + async-listen@3.0.1: {} + + async-sema@3.1.1: {} + autoprefixer@10.5.0(postcss@8.5.15): dependencies: browserslist: 4.28.2 @@ -1860,12 +2598,23 @@ snapshots: postcss: 8.5.15 postcss-value-parser: 4.2.0 + balanced-match@1.0.2: {} + balanced-match@4.0.4: {} baseline-browser-mapping@2.10.37: {} binary-extensions@2.3.0: {} + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + brace-expansion@1.1.15: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 @@ -1898,8 +2647,20 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chownr@3.0.0: {} + + cjs-module-lexer@1.2.3: {} + + code-block-writer@10.1.1: {} + commander@4.1.1: {} + concat-map@0.0.1: {} + + consola@3.4.2: {} + + convert-hrtime@3.0.0: {} + convert-source-map@2.0.0: {} cross-spawn@7.0.6: @@ -1924,10 +2685,55 @@ snapshots: dlv@1.1.3: {} + edge-runtime@2.5.9: + dependencies: + '@edge-runtime/format': 2.2.1 + '@edge-runtime/ponyfill': 2.4.2 + '@edge-runtime/vm': 3.2.0 + async-listen: 3.0.1 + mri: 1.2.0 + picocolors: 1.0.0 + pretty-ms: 7.0.1 + signal-exit: 4.0.2 + time-span: 4.0.0 + electron-to-chromium@1.5.372: {} es-errors@1.3.0: {} + es-module-lexer@1.4.1: {} + + es-module-lexer@1.5.0: {} + + esbuild@0.27.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.0 + '@esbuild/android-arm': 0.27.0 + '@esbuild/android-arm64': 0.27.0 + '@esbuild/android-x64': 0.27.0 + '@esbuild/darwin-arm64': 0.27.0 + '@esbuild/darwin-x64': 0.27.0 + '@esbuild/freebsd-arm64': 0.27.0 + '@esbuild/freebsd-x64': 0.27.0 + '@esbuild/linux-arm': 0.27.0 + '@esbuild/linux-arm64': 0.27.0 + '@esbuild/linux-ia32': 0.27.0 + '@esbuild/linux-loong64': 0.27.0 + '@esbuild/linux-mips64el': 0.27.0 + '@esbuild/linux-ppc64': 0.27.0 + '@esbuild/linux-riscv64': 0.27.0 + '@esbuild/linux-s390x': 0.27.0 + '@esbuild/linux-x64': 0.27.0 + '@esbuild/netbsd-arm64': 0.27.0 + '@esbuild/netbsd-x64': 0.27.0 + '@esbuild/openbsd-arm64': 0.27.0 + '@esbuild/openbsd-x64': 0.27.0 + '@esbuild/openharmony-arm64': 0.27.0 + '@esbuild/sunos-x64': 0.27.0 + '@esbuild/win32-arm64': 0.27.0 + '@esbuild/win32-ia32': 0.27.0 + '@esbuild/win32-x64': 0.27.0 + escalade@3.2.0: {} escape-string-regexp@4.0.0: {} @@ -2011,8 +2817,12 @@ snapshots: estraverse@5.3.0: {} + estree-walker@2.0.2: {} + esutils@2.0.3: {} + etag@1.8.1: {} + eventemitter3@5.0.1: {} fast-deep-equal@3.1.3: {} @@ -2041,6 +2851,8 @@ snapshots: dependencies: flat-cache: 4.0.1 + file-uri-to-path@1.0.0: {} + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -2068,6 +2880,12 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) + fs-extra@11.1.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + fsevents@2.3.3: optional: true @@ -2075,6 +2893,10 @@ snapshots: gensync@1.0.0-beta.2: {} + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -2083,8 +2905,16 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + globals@17.6.0: {} + graceful-fs@4.2.11: {} + gsap@3.15.0: {} hasown@2.0.4: @@ -2097,6 +2927,13 @@ snapshots: dependencies: hermes-estree: 0.25.1 + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + ignore@5.3.2: {} ignore@7.0.5: {} @@ -2129,16 +2966,33 @@ snapshots: js-tokens@4.0.0: {} + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + jsesc@3.1.0: {} json-buffer@3.0.1: {} + json-schema-to-ts@1.6.4: + dependencies: + '@types/json-schema': 7.0.15 + ts-toolbelt: 6.15.5 + json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@2.2.3: {} + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -2205,6 +3059,8 @@ snapshots: dependencies: p-locate: 5.0.0 + lru-cache@11.5.1: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -2216,20 +3072,44 @@ snapshots: braces: 3.0.3 picomatch: 2.3.2 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + minimatch@10.1.1: + dependencies: + '@isaacs/brace-expansion': 5.0.1 + minimatch@10.2.5: dependencies: brace-expansion: 5.0.6 + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.15 + + minipass@7.1.3: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.3 + mipd@0.0.7(typescript@6.0.3): optionalDependencies: typescript: 6.0.3 + mkdirp@1.0.4: {} + motion-dom@12.40.0: dependencies: motion-utils: 12.39.0 motion-utils@12.39.0: {} + mri@1.2.0: {} + ms@2.1.3: {} mz@2.7.0: @@ -2242,8 +3122,18 @@ snapshots: natural-compare@1.4.0: {} + node-fetch@2.6.9: + dependencies: + whatwg-url: 5.0.0 + + node-gyp-build@4.8.4: {} + node-releases@2.0.47: {} + nopt@8.1.0: + dependencies: + abbrev: 3.0.1 + normalize-path@3.0.0: {} object-assign@4.1.1: {} @@ -2282,12 +3172,27 @@ snapshots: dependencies: p-limit: 3.1.0 + parse-ms@2.1.0: {} + + path-browserify@1.0.1: {} + path-exists@4.0.0: {} path-key@3.1.1: {} path-parse@1.0.7: {} + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.1 + minipass: 7.1.3 + + path-to-regexp@6.1.0: {} + + path-to-regexp@6.3.0: {} + + picocolors@1.0.0: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -2310,12 +3215,13 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.5.15 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.15): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.15)(tsx@4.21.0): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 1.21.7 postcss: 8.5.15 + tsx: 4.21.0 postcss-nested@6.2.0(postcss@8.5.15): dependencies: @@ -2337,6 +3243,10 @@ snapshots: prelude-ls@1.2.1: {} + pretty-ms@7.0.1: + dependencies: + parse-ms: 2.1.0 + punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -2356,6 +3266,12 @@ snapshots: dependencies: picomatch: 2.3.2 + require-from-string@2.0.2: {} + + resolve-from@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + resolve@1.22.12: dependencies: es-errors: 1.3.0 @@ -2402,6 +3318,10 @@ snapshots: shebang-regex@3.0.0: {} + signal-exit@4.0.2: {} + + smol-toml@1.5.2: {} + source-map-js@1.2.1: {} sucrase@3.35.1: @@ -2416,7 +3336,7 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - tailwindcss@3.4.19: + tailwindcss@3.4.19(tsx@4.21.0): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -2435,7 +3355,7 @@ snapshots: postcss: 8.5.15 postcss-import: 15.1.0(postcss@8.5.15) postcss-js: 4.1.0(postcss@8.5.15) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.15) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.15)(tsx@4.21.0) postcss-nested: 6.2.0(postcss@8.5.15) postcss-selector-parser: 6.1.4 resolve: 1.22.12 @@ -2444,6 +3364,14 @@ snapshots: - tsx - yaml + tar@7.5.16: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -2452,6 +3380,10 @@ snapshots: dependencies: any-promise: 1.3.0 + time-span@4.0.0: + dependencies: + convert-hrtime: 3.0.0 + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -2461,14 +3393,30 @@ snapshots: dependencies: is-number: 7.0.0 + tr46@0.0.3: {} + ts-api-utils@2.5.0(typescript@6.0.3): dependencies: typescript: 6.0.3 ts-interface-checker@0.1.13: {} + ts-morph@12.0.0: + dependencies: + '@ts-morph/common': 0.11.1 + code-block-writer: 10.1.1 + + ts-toolbelt@6.15.5: {} + tslib@2.8.1: {} + tsx@4.21.0: + dependencies: + esbuild: 0.27.0 + get-tsconfig: 4.14.0 + optionalDependencies: + fsevents: 2.3.3 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -2484,10 +3432,20 @@ snapshots: transitivePeerDependencies: - supports-color + typescript@5.9.3: {} + typescript@6.0.3: {} + undici-types@5.26.5: {} + undici-types@7.18.2: {} + undici@5.28.4: + dependencies: + '@fastify/busboy': 2.1.1 + + universalify@2.0.1: {} + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: browserslist: 4.28.2 @@ -2521,7 +3479,7 @@ snapshots: - utf-8-validate - zod - vite@8.0.16(@types/node@24.13.2)(jiti@1.21.7): + vite@8.0.16(@types/node@24.13.2)(esbuild@0.27.0)(jiti@1.21.7)(tsx@4.21.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -2530,8 +3488,10 @@ snapshots: tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.13.2 + esbuild: 0.27.0 fsevents: 2.3.3 jiti: 1.21.7 + tsx: 4.21.0 wagmi@3.6.16(@tanstack/query-core@5.101.0)(@tanstack/react-query@5.101.0(react@19.2.7))(@types/react@19.2.17)(react@19.2.7)(typescript@6.0.3)(viem@2.52.2(typescript@6.0.3)(zod@4.4.3)): dependencies: @@ -2556,6 +3516,13 @@ snapshots: - immer - porto + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -2566,12 +3533,16 @@ snapshots: yallist@3.1.1: {} + yallist@5.0.0: {} + yocto-queue@0.1.0: {} zod-validation-error@4.0.2(zod@4.4.3): dependencies: zod: 4.4.3 + zod@3.22.4: {} + zod@4.4.3: {} zustand@5.0.0(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.4.0(react@19.2.7)): diff --git a/site/src/app/contracts.ts b/site/src/app/contracts.ts index 20837dc..aa03491 100644 --- a/site/src/app/contracts.ts +++ b/site/src/app/contracts.ts @@ -7,14 +7,29 @@ export const EXPLORER = robinhood.blockExplorers.default.url; /// reads the guardian view functions at the connected address — not at the impl address. export const ADDR = { /// GuardianModule implementation (the 7702 delegate target). - guardianImpl: "0xd0d301Aeaa7AA5Ced16C927030f131c9Cb083b77", + guardianImpl: "0x9953BB30cFef2ac842C74417eA6DC661b492E8dA", /// Demo dUSD token used in the end-to-end scenario. - token: "0xC32C2eB815F1413ee2c7A68d2EFf3760d828841E", + token: "0x31052145BBFB8aA9B4e3713a2fD34e57b3A942f3", /// RulesEngineV1 — deploy on Robinhood (script/DeployRules.s.sol) and set its address as /// VITE_RULES_ENGINE to enable the firewall controls. Empty = the Enable button stays off. rulesEngine: ((import.meta.env.VITE_RULES_ENGINE as string | undefined) ?? "") as `0x${string}` | "", + /// SafeVaultFactory — deterministic per-user SafeVault, for gasless in-browser onboarding. + factory: ((import.meta.env.VITE_VAULT_FACTORY as string | undefined) ?? + "0x1ef2B2539fa842A9c7e4EA07790aA6dBc47ec4A5") as `0x${string}`, + /// Canonical coincoin keeper the gasless onboarding policy authorizes. + keeper: "0x627872F35b724222413e7421C9e40A26B2762B9e" as `0x${string}`, } as const; +export const factoryAbi = [ + { + type: "function", + name: "vaultOf", + stateMutability: "view", + inputs: [{ name: "owner", type: "address" }], + outputs: [{ type: "address" }], + }, +] as const; + export const guardianAbi = [ { type: "function", name: "configured", stateMutability: "view", inputs: [], outputs: [{ type: "bool" }] }, { type: "function", name: "safeVault", stateMutability: "view", inputs: [], outputs: [{ type: "address" }] }, diff --git a/site/src/app/useGaslessOnboard.ts b/site/src/app/useGaslessOnboard.ts new file mode 100644 index 0000000..28ea118 --- /dev/null +++ b/site/src/app/useGaslessOnboard.ts @@ -0,0 +1,98 @@ +import { useCallback, useState } from "react"; +import { useConnectorClient, usePublicClient } from "wagmi"; +import { readContract, signTypedData, signAuthorization, waitForTransactionReceipt, getTransactionCount } from "viem/actions"; +import { ADDR, factoryAbi } from "./contracts"; +import { robinhood } from "./chain"; + +export type OnboardStatus = "idle" | "signing" | "relaying" | "done" | "error"; + +/// Gasless in-browser onboarding: the wallet signs (off-chain, no gas) an EIP-712 Policy and an +/// EIP-7702 authorization; the relayer (/api/onboard) deploys the user's deterministic SafeVault and +/// submits the sponsored delegate+configure transaction. Falls back to the CLI if the wallet can't +/// sign an EIP-7702 authorization yet. +export function useGaslessOnboard(onDone?: () => void) { + const { data: walletClient } = useConnectorClient(); + const publicClient = usePublicClient(); + const [status, setStatus] = useState("idle"); + const [error, setError] = useState(null); + + const onboard = useCallback(async () => { + setError(null); + if (!walletClient?.account || !publicClient) { + setError("Connect a wallet on Robinhood Chain first."); + setStatus("error"); + return; + } + const owner = walletClient.account.address; + try { + setStatus("signing"); + const vault = await readContract(publicClient, { + address: ADDR.factory, + abi: factoryAbi, + functionName: "vaultOf", + args: [owner], + }); + const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600); + + // 1) EIP-712 Policy (standard signTypedData) + const sig = await signTypedData(walletClient, { + account: walletClient.account, + domain: { name: "coincoin GuardianModule", version: "1", chainId: robinhood.id, verifyingContract: owner }, + types: { + Policy: [ + { name: "safeVault", type: "address" }, + { name: "keepers", type: "address[]" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + ], + }, + primaryType: "Policy", + message: { safeVault: vault, keepers: [ADDR.keeper], nonce: 0n, deadline }, + }); + + // 2) EIP-7702 authorization (wallet-support dependent → CLI fallback) + let auth; + try { + // EIP-7702 authorization. nonce = the account's CURRENT tx nonce (relayer-sponsored, so no + // +1). Fetched explicitly at `latest` for determinism — a wrong nonce is silently skipped + // (the type-4 tx still succeeds but the delegation never applies), so this must be exact. + const txNonce = await getTransactionCount(publicClient, { address: owner, blockTag: "latest" }); + auth = await signAuthorization(walletClient, { + account: walletClient.account, + contractAddress: ADDR.guardianImpl, + chainId: robinhood.id, + nonce: txNonce, + }); + } catch { + setError("This wallet can't sign an EIP-7702 authorization yet. Use the CLI: `pnpm onboard`."); + setStatus("error"); + return; + } + + // 3) relayer submits (pays gas) + setStatus("relaying"); + const res = await fetch("/api/onboard", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + owner, + authorization: { address: auth.address, chainId: auth.chainId, nonce: auth.nonce, r: auth.r, s: auth.s, yParity: auth.yParity }, + policy: { safeVault: vault, keepers: [ADDR.keeper] }, + nonce: "0", + deadline: deadline.toString(), + sig, + }), + }); + const json = (await res.json()) as { ok?: boolean; txHash?: `0x${string}`; error?: string }; + if (!res.ok || !json.txHash) throw new Error(json.error ?? "relayer error"); + await waitForTransactionReceipt(publicClient, { hash: json.txHash }); + setStatus("done"); + onDone?.(); + } catch (e) { + setError((e as Error).message ?? "onboarding failed"); + setStatus("error"); + } + }, [walletClient, publicClient, onDone]); + + return { onboard, status, error }; +} diff --git a/site/src/data.ts b/site/src/data.ts index f69a33b..a17be2c 100644 --- a/site/src/data.ts +++ b/site/src/data.ts @@ -11,8 +11,8 @@ export const LICENSE = "MIT"; // On-chain proof: the GuardianModule deployed on Robinhood Chain Testnet (chain 46630), // on the chain's own block explorer. export const DEPLOY_PROOF = { - address: "0xd0d301Aeaa7AA5Ced16C927030f131c9Cb083b77", - explorer: "https://explorer.testnet.chain.robinhood.com/address/0xd0d301Aeaa7AA5Ced16C927030f131c9Cb083b77", + address: "0x9953BB30cFef2ac842C74417eA6DC661b492E8dA", + explorer: "https://explorer.testnet.chain.robinhood.com/address/0x9953BB30cFef2ac842C74417eA6DC661b492E8dA", }; // Contacts (footer + contact page). @@ -121,9 +121,9 @@ export const GUARDIAN_TERMINAL: ProcTerminal = { { text: "→ scanning on-chain threat logs in bounded windows (≤10 blocks, RPC cap)…", tone: "muted" }, { text: "→ caught up to head · idle, polling every few seconds…", tone: "muted" }, { text: "→ threat detected on-chain → CRITICAL drain alert decoded", tone: "warn" }, - { text: "[coincoin] 🦆 COIN COIN ! threat detected → evacuating 0xfa14…3368", tone: "danger" }, + { text: "[coincoin] 🦆 COIN COIN ! threat detected → evacuating 0x46d1…80Cc", tone: "danger" }, { text: "→ keeper signs evacuateERC20([dUSD]) → your EOA (EIP-7702 guardian context)", tone: "normal" }, - { text: "[coincoin] ✅ evacuated to your SafeVault 0x49be…f479 — confirmed on-chain", tone: "success" }, + { text: "[coincoin] ✅ evacuated to your SafeVault 0x5309…4b91 — confirmed on-chain", tone: "success" }, { text: "→ funds safe in your own vault · daemon still watching. (Ctrl-C to stop)", tone: "success" }, ], }; diff --git a/site/src/pages/Dashboard.tsx b/site/src/pages/Dashboard.tsx index 6f681fa..7f71d0e 100644 --- a/site/src/pages/Dashboard.tsx +++ b/site/src/pages/Dashboard.tsx @@ -14,6 +14,7 @@ import { robinhood } from "../app/chain"; import { ADDR, EXPLORER, erc20Abi, guardianAbi, safeVaultAbi } from "../app/contracts"; import { fmtUnits, short } from "../app/format"; import { Pill, PageHeader } from "../components/ui"; +import { useGaslessOnboard } from "../app/useGaslessOnboard"; const REFRESH = { refetchInterval: 8000 } as const; @@ -78,6 +79,7 @@ export function Dashboard() { // ── Writes ── const { writeContractAsync } = useWriteContract(); + const { onboard, status: onbStatus, error: onbError } = useGaslessOnboard(() => state.refetch()); const [busy, setBusy] = useState(null); const [tx, setTx] = useState(null); const [err, setErr] = useState(null); @@ -187,10 +189,37 @@ export function Dashboard() { {!configured && ( -

- This account isn't delegated yet. Onboard once from the CLI:{" "} - pnpm onboard — then it shows up here. -

+
+ {isConnected && onRobinhood && isSelf ? ( + <> + +

+ Two wallet signatures, zero gas — a relayer sponsors the delegation. Or onboard from the CLI:{" "} + pnpm onboard. +

+ {onbError &&

{onbError}

} + + ) : ( +

+ {isConnected + ? onRobinhood + ? "Connect the account you want to protect to onboard it gasless, " + : "Switch to Robinhood Chain to onboard, " + : "Connect your wallet to onboard gasless, "} + or use the CLI: pnpm onboard. +

+ )} +
)} diff --git a/site/src/pages/Docs.tsx b/site/src/pages/Docs.tsx index 366a398..909a9ee 100644 --- a/site/src/pages/Docs.tsx +++ b/site/src/pages/Docs.tsx @@ -138,6 +138,9 @@ forge script script/Deploy.s.sol:DeployGuardian \\ # local-firewall scorer (RulesEngineV1) forge script script/DeployRules.s.sol:DeployRules \\ --rpc-url "$ROBINHOOD_TESTNET_RPC" --broadcast --skip-simulation +# SafeVaultFactory (deterministic per-user vault — gasless onboarding) +forge script script/DeployFactory.s.sol:DeployFactory \\ + --rpc-url "$ROBINHOOD_TESTNET_RPC" --broadcast --skip-simulation # demo set: token + SafeVault + MockAavePool (+ throwaway protocol for local testing) forge script script/SetupDemo.s.sol:SetupDemo \\ --rpc-url "$ROBINHOOD_TESTNET_RPC" --broadcast --skip-simulation`} @@ -145,11 +148,13 @@ forge script script/SetupDemo.s.sol:SetupDemo \\ Copy the printed addresses into .env: GUARDIAN_IMPL, RULES_ENGINE, + VAULT_FACTORY, DEMO_TOKEN, DEMO_PROTO, DEMO_VAULT, DEMO_AAVE_POOL (and - VITE_RULES_ENGINE in site/.env). + VITE_RULES_ENGINE / + VITE_VAULT_FACTORY in site/.env).

3 · Connect and run

@@ -159,13 +164,18 @@ forge script script/SetupDemo.s.sol:SetupDemo \\ ))}

- Run pnpm onboard once to connect, then + Prefer no CLI? The /app dashboard onboards your + connected wallet gasless in one click: you sign an EIP-712 policy and an EIP-7702 + authorization, and a relayer sponsors the delegate+configure transaction. +

+

+ Or run pnpm onboard once to connect, then leave pnpm watch running. The guardian catches a real on-chain Drained log itself and evacuates to your vault — no human in the loop:

- {`🦆 COIN COIN ! threat detected → evacuating 0xfa14…3368 -→ evacuated to your SafeVault 0x49be…f479 (EIP-7702 guardian context) + {`🦆 COIN COIN ! threat detected → evacuating 0x46d1…80Cc +→ evacuated to your SafeVault 0x5309…4b91 (EIP-7702 guardian context) ✓ funds safe in your own vault. Daemon still watching.`}

Want to see it react on your own machine? pnpm exploit{" "} diff --git a/site/tsconfig.api.json b/site/tsconfig.api.json new file mode 100644 index 0000000..a9842a1 --- /dev/null +++ b/site/tsconfig.api.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022"], + "types": ["node"], + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["api"] +} diff --git a/video/src/content.ts b/video/src/content.ts index 770c3a0..1688b6d 100644 --- a/video/src/content.ts +++ b/video/src/content.ts @@ -3,9 +3,10 @@ export const REPO_URL = "github.com/gamween/coincoin"; export const LIVE_URL = "coincoin-five.vercel.app"; -// Real on-chain values from the 2026-06-14 redeploy (used by the demo). -export const VICTIM_SHORT = "0xfa14…3368"; +// Real on-chain values (used by the demo). Shorts reflect the 2026-06-16 redeploy; +// TX_EXIT/TX_EVAC are from a representative end-to-end run. +export const VICTIM_SHORT = "0x46d1…80Cc"; export const TX_EXIT = "0x4fcec0…cfb0"; export const TX_EVAC = "0xd7c0c5…79df"; -export const GUARDIAN_SHORT = "0xd0d3…3b77"; +export const GUARDIAN_SHORT = "0x9953…E8dA"; export const RULES_SHORT = "0xc20A…bc52"; diff --git a/watcher/package.json b/watcher/package.json index 5c190e6..8e29316 100644 --- a/watcher/package.json +++ b/watcher/package.json @@ -6,6 +6,7 @@ "scripts": { "test": "vitest run", "onboard": "tsx scripts/onboard.ts", + "onboard:gasless": "tsx scripts/onboard-gasless.ts", "watch": "tsx scripts/watch.ts", "exploit": "tsx scripts/exploit.ts", "revoke": "tsx scripts/revoke.ts" diff --git a/watcher/scripts/onboard-gasless.ts b/watcher/scripts/onboard-gasless.ts new file mode 100644 index 0000000..33c8e3d --- /dev/null +++ b/watcher/scripts/onboard-gasless.ts @@ -0,0 +1,107 @@ +import { config } from "dotenv"; +config({ path: "../.env" }); +import { createWalletClient, createPublicClient, http, encodeFunctionData, parseAbi } from "viem"; +import { privateKeyToAccount, generatePrivateKey } from "viem/accounts"; +import { resolveChainConfig, ORBIT_TX_GAS } from "../src/config"; + +/// End-to-end proof of the GASLESS onboarding path — the exact flow the browser does, but with a +/// local key standing in for the wallet's two signatures: +/// 1) a FRESH EOA (zero balance — it never sends a tx) signs the EIP-712 Policy via the STANDARD +/// viem signTypedData (identical to a wallet's signTypedData), and signs the 7702 authorization; +/// 2) the RELAYER pays gas: deploys the user's SafeVault via the factory, then submits ONE type-4 +/// tx (to: user, authorizationList: [auth], data: configureWithSig(...)) — delegating AND +/// configuring in one sponsored transaction. +/// If this succeeds, the contract's EIP-712 encoding is wallet-compatible and the relayer flow works. +const FACTORY_ABI = parseAbi([ + "function vaultOf(address owner) view returns (address)", + "function deploy(address owner) returns (address)", + "function isDeployed(address owner) view returns (bool)", +]); +const GUARDIAN_ABI = parseAbi([ + "function configureWithSig((address safeVault, address[] keepers) p, uint256 nonce, uint256 deadline, bytes sig)", + "function configured() view returns (bool)", + "function safeVault() view returns (address)", + "function isKeeper(address) view returns (bool)", +]); + +async function main() { + const cfg = resolveChainConfig(); + const factory = process.env.VAULT_FACTORY as `0x${string}` | undefined; + const relayerKey = process.env.RELAYER_PRIVATE_KEY as `0x${string}` | undefined; + const keeperKey = process.env.KEEPER_PRIVATE_KEY as `0x${string}` | undefined; + if (!factory) throw new Error("gasless: VAULT_FACTORY missing"); + if (!relayerKey) throw new Error("gasless: RELAYER_PRIVATE_KEY missing"); + if (!keeperKey) throw new Error("gasless: KEEPER_PRIVATE_KEY missing"); + const keeper = privateKeyToAccount(keeperKey).address; + const relayer = privateKeyToAccount(relayerKey); + + const transport = http(cfg.rpcUrl); + const pub = createPublicClient({ chain: cfg.chain, transport }); + + // ── 1) the user (a fresh, unfunded EOA — only signs) ── + const user = privateKeyToAccount(generatePrivateKey()); + const userWallet = createWalletClient({ account: user, chain: cfg.chain, transport }); + const vault = await pub.readContract({ address: factory, abi: FACTORY_ABI, functionName: "vaultOf", args: [user.address] }); + const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600); + console.log(`[gasless] user ${user.address} (0 gas) · vault ${vault} · keeper ${keeper}`); + + // EIP-712 Policy signature (standard signTypedData — what the wallet will do in-browser) + const sig = await userWallet.signTypedData({ + account: user, + domain: { name: "coincoin GuardianModule", version: "1", chainId: cfg.chain.id, verifyingContract: user.address }, + types: { + Policy: [ + { name: "safeVault", type: "address" }, + { name: "keepers", type: "address[]" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + ], + }, + primaryType: "Policy", + message: { safeVault: vault, keepers: [keeper], nonce: 0n, deadline }, + }); + + // EIP-7702 authorization (relayer-sponsored → no +1). Explicit current nonce (latest) for + // determinism — a wrong nonce is silently skipped, so it must be exact. + const authNonce = await pub.getTransactionCount({ address: user.address, blockTag: "latest" }); + const authorization = await userWallet.signAuthorization({ + account: user, + contractAddress: cfg.guardianImpl, + chainId: cfg.chain.id, + nonce: authNonce, + }); + + // ── 2) the relayer pays gas ── + const relayerWallet = createWalletClient({ account: relayer, chain: cfg.chain, transport }); + if (!(await pub.readContract({ address: factory, abi: FACTORY_ABI, functionName: "isDeployed", args: [user.address] }))) { + const dtx = await relayerWallet.writeContract({ address: factory, abi: FACTORY_ABI, functionName: "deploy", args: [user.address], gas: ORBIT_TX_GAS }); + await pub.waitForTransactionReceipt({ hash: dtx }); + console.log(`[gasless] relayer deployed vault (tx ${dtx})`); + } + const data = encodeFunctionData({ + abi: GUARDIAN_ABI, + functionName: "configureWithSig", + args: [{ safeVault: vault, keepers: [keeper] }, 0n, deadline, sig], + }); + const tx = await relayerWallet.sendTransaction({ to: user.address, data, authorizationList: [authorization], gas: ORBIT_TX_GAS }); + const rcpt = await pub.waitForTransactionReceipt({ hash: tx }); + if (rcpt.status !== "success") throw new Error(`[gasless] configure tx reverted: ${tx}`); + console.log(`[gasless] relayer submitted delegate+configure (tx ${tx})`); + + // ── verify ── + const g = { address: user.address, abi: GUARDIAN_ABI } as const; + const [configured, sv, isK] = await Promise.all([ + pub.readContract({ ...g, functionName: "configured" }), + pub.readContract({ ...g, functionName: "safeVault" }), + pub.readContract({ ...g, functionName: "isKeeper", args: [keeper] }), + ]); + const ok = configured === true && sv.toLowerCase() === vault.toLowerCase() && isK === true; + console.log(`[gasless] verify → configured=${configured} safeVault=${sv} isKeeper=${isK}`); + if (!ok) throw new Error("[gasless] verification failed"); + console.log("[gasless] ✅ gasless onboarding works end-to-end (user paid 0 gas)"); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +});