diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index b1e0b4d..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,73 +0,0 @@ -name: CI - -permissions: {} - -on: - push: - branches: [main] - pull_request: - workflow_dispatch: - -jobs: - contracts: - name: Contracts - runs-on: ubuntu-latest - permissions: - contents: read - defaults: - run: - working-directory: contracts - steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - submodules: recursive - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - - - name: Check formatting - run: forge fmt --check - - - name: Build - run: forge build --sizes - - - name: Test - run: forge test -vv - env: - FOUNDRY_PROFILE: ci - - - name: Slither - uses: crytic/slither-action@v0.4.0 - continue-on-error: true - with: - target: contracts/ - slither-args: --filter-paths "lib/|test/|script/" --exclude-informational --exclude-optimization - - web: - name: Interface - runs-on: ubuntu-latest - permissions: - contents: read - defaults: - run: - working-directory: web - steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: npm - cache-dependency-path: web/package-lock.json - - - name: Install - run: npm ci - - - name: Type-check and build - run: npm run build - - - name: Test - run: npm test diff --git a/.github/workflows/sui.yml b/.github/workflows/sui.yml new file mode 100644 index 0000000..800ecfd --- /dev/null +++ b/.github/workflows/sui.yml @@ -0,0 +1,125 @@ +name: Sui + +permissions: {} + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +env: + # Pin the toolchain: the Move compiler and the bundled framework move together, and a job that + # silently upgrades either is a job that cannot reproduce a failure. + SUI_VERSION: mainnet-v1.80.1 + +jobs: + move: + name: Move + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Cache the Sui CLI + id: cache-sui + uses: actions/cache@v4 + with: + path: ~/.local/bin/sui + key: sui-${{ env.SUI_VERSION }}-ubuntu + + - name: Install the Sui CLI + if: steps.cache-sui.outputs.cache-hit != 'true' + run: | + mkdir -p ~/.local/bin + curl -fsSL --retry 5 \ + "https://github.com/MystenLabs/sui/releases/download/${SUI_VERSION}/sui-${SUI_VERSION}-ubuntu-x86_64.tgz" \ + -o /tmp/sui.tgz + tar -xzf /tmp/sui.tgz -C /tmp + find /tmp -maxdepth 3 -type f -name sui -perm -u+x -exec cp {} ~/.local/bin/sui \; + chmod +x ~/.local/bin/sui + + - name: Build + run: ~/.local/bin/sui move build --path sui + + - name: Test + run: ~/.local/bin/sui move test --path sui + + typescript: + name: TypeScript + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - uses: actions/setup-node@v4 + with: + node-version: 24 + + - name: Build and test the SDK + working-directory: sdk + run: | + npm install --no-audit --no-fund + npm run typecheck + npm run build + npm test + + - name: Test the backend + working-directory: server + env: + # The rule tests are pure; nothing here touches a database. + DATABASE_URL: postgres://localhost:5432/unused + SKUR_PACKAGE_ID: '0x0' + run: | + npm install --no-audit --no-fund + npm run typecheck + npm test + + - name: Typecheck the interface + working-directory: app + run: | + npm install --no-audit --no-fund + npm run typecheck + + migrations: + name: Migrations + runs-on: ubuntu-latest + permissions: + contents: read + services: + postgres: + image: postgres:17 + env: + # No password, rather than a hardcoded one. This container exists for the length of one + # job, is reachable only from it, and is thrown away afterwards — so a credential here + # protects nothing and is one more string to leak. `trust` removes it entirely. + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_DB: skur + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - uses: actions/setup-node@v4 + with: + node-version: 24 + + # A migration that does not apply to an empty database is a migration nobody can deploy. + - name: Apply the migrations + working-directory: server + env: + DATABASE_URL: postgres://postgres@localhost:5432/skur + SKUR_PACKAGE_ID: '0x0' + run: | + npm install --no-audit --no-fund + npm run migrate diff --git a/.gitignore b/.gitignore index 1d86deb..99e3487 100644 --- a/.gitignore +++ b/.gitignore @@ -12,8 +12,23 @@ contracts/broadcast/**/dry-run/ node_modules/ web/dist/ web/coverage/ +sdk/dist/ +app/.next/ +app/out/ +app/next-env.d.ts +*.tsbuildinfo + +# move +sui/build/ +sui/Move.lock # tooling .DS_Store *.log crytic-export/ + +# The EVM build was removed. These directories may still exist locally, holding build output, +# node_modules and a .env — their own .gitignore files went with them, so without this a +# `git add -A` happily commits an Expo prebuild. Never track these paths again. +/contracts/ +/web/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 5c7d5d6..0000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "contracts/lib/forge-std"] - path = contracts/lib/forge-std - url = https://github.com/foundry-rs/forge-std -[submodule "contracts/lib/openzeppelin-contracts"] - path = contracts/lib/openzeppelin-contracts - url = https://github.com/OpenZeppelin/openzeppelin-contracts diff --git a/README.md b/README.md index ae5ca85..c1d8276 100644 --- a/README.md +++ b/README.md @@ -1,80 +1,165 @@ # Skur -Programmable treasury security for onchain businesses. Protect funds with adaptive policies, loss limits, guardians, and risk-aware approvals. +**A programmable authorization and treasury security layer for Sui organizations.** -> A valid signature proves authorization. It does not prove that a transaction is safe. +> A valid signature proves authorization. It does not prove that a payment is safe. -Skur is a self-custodial vault whose security requirements change with transaction risk. Valid approvals are necessary but never sufficient: amount tiers, treasury exposure, recipient trust, cumulative outflow, guardian veto and security modes are all enforced by the vault contract itself. No Skur server sits in the execution path. +Every large onchain treasury loss of the last few years happened with valid signatures. The keys +were real, the threshold was met, the contract did what it was told. Nothing between the signature +and the money asked the second question: + +> Given the amount, the destination, what has already left today and what this organization knows +> right now, *should* this payment settle? + +Skur answers that question in Move, in a shared object anyone can read. It is not a wallet, and it +is not a nicer multisig — Sui has signature aggregation in the protocol already. It is the layer a +treasury sits behind. + +## What the vault enforces + +| Control | What it does | +|---|---| +| **Risk-tiered authorization** | Five signals — amount against the asset's limits, share of holdings, recipient standing, the day's outflow, the vault's mode — each able to raise the tier and none able to lower it. The tier sets the approvals, the guardian sign-off and the wait. | +| **Recipient trust** | An address the vault has never paid is escalated every time. A newly registered one serves an activation delay before it can be paid at all. | +| **Velocity limits** | Per-transaction and 24-hour caps, counted cumulatively. Crossing half the day's allowance escalates everything after it, so splitting a drain meets a higher bar partway through rather than never. | +| **Loss-envelope circuit breaker** | A share of the balance that may leave per window, measured against the balance when the window *opened*. The payment that would cross it is not made; the vault latches into Lockdown instead. | +| **Guardians** | A role the contract refuses to combine with any treasury role. It can freeze, veto, block a recipient and start a recovery. It can never propose, approve or execute a payment. | +| **Policy firewall** | Any change that gives up a control — including the ones that look like tightening, such as zeroing a threshold to disable it — waits out a delay, can be vetoed by any guardian, and cannot be proposed in Lockdown. | +| **An agent role** | `PROPOSER` can open a payment and nothing else. A compromised automation key produces a queue nobody approved, not a withdrawal. | + +Details, with the test that proves each invariant, in +[docs/SUI_SECURITY_MODEL.md](docs/SUI_SECURITY_MODEL.md). ## Repository layout | Path | What it is | |---|---| -| [contracts/](contracts/) | Foundry project: `SkurVault`, `SkurFactory`, `SkurPolicyLib`, `SkurRisk`, the devnet stablecoin, tests, invariants and deploy script | -| [web/](web/) | Client-side interface (Vite, React, wagmi/viem). Reads contract state and events directly; submits user-signed transactions | -| [docs/DECISIONS.md](docs/DECISIONS.md) | Every open question from the blueprint, resolved | -| [docs/SECURITY_MODEL.md](docs/SECURITY_MODEL.md) | Roles, lifecycle, the twelve invariants and the tests that prove each | -| [docs/scriipture-capability-report.md](docs/scriipture-capability-report.md) | Why the contracts are plain Solidity instead of Scriipture TypeScript | -| [scripts/export-abi.py](scripts/export-abi.py) | Copies ABIs from the Foundry build into the web app | +| [sui/](sui/) | The Move package: `types`, `policy`, `risk`, `vault`, and the test suites named after the attacks they defend against | +| [sdk/](sdk/) | TypeScript SDK on `@mysten/sui` v2 — reads, PTB builders, event decoding, and the chain-agnostic engines (risk, policy rules, templates, maximum loss, posture, attack simulator) | +| [app/](app/) | The web interface: Next.js, React, `@mysten/dapp-kit-react` | +| [mobile/](mobile/) | iOS and Android: Expo, React Native, with the signing key in the device's secure element | +| [server/](server/) | Node and PostgreSQL: the event indexer, the notification service and the advisory API | +| [docs/SUI_SECURITY_MODEL.md](docs/SUI_SECURITY_MODEL.md) | Roles, lifecycle, the sixteen invariants, the threat map and the known limitations | +| [docs/SUI_LANDSCAPE.md](docs/SUI_LANDSCAPE.md) | What exists on Sui today and what it does not do | +| [docs/SUI_POSITIONING.md](docs/SUI_POSITIONING.md) | The positioning and funding narrative | +| [docs/MIGRATION_EVM_TO_SUI.md](docs/MIGRATION_EVM_TO_SUI.md) | What moved from the previous EVM build, what changed shape, and why | -## Ark Constellation devnet deployment +## Transport: gRPC and GraphQL, never JSON-RPC -Chain id 9000, native token KASH. Sources are verified on the Blockscout explorer. +JSON-RPC was switched off on Sui Foundation mainnet full nodes in the week of 27 July 2026, with +full decommissioning — code removal included — scheduled for mid-October 2026. -| Contract | Address | -|---|---| -| SkurFactory | [`0x018c12BA8b085da81E434b9e4001e07CF48764E9`](https://explorer.34.60.137.196.sslip.io/address/0x018c12BA8b085da81E434b9e4001e07CF48764E9) | -| SkurVault implementation | [`0xE21C7ad3a56D187848F9915bb43f88820278DeA3`](https://explorer.34.60.137.196.sslip.io/address/0xE21C7ad3a56D187848F9915bb43f88820278DeA3) | -| SkurPolicyLib | [`0x6bAA1A24B55796d8346748EE3565e40d844bA576`](https://explorer.34.60.137.196.sslip.io/address/0x6bAA1A24B55796d8346748EE3565e40d844bA576) | -| SkurTestUSD (sUSD, 6 decimals, mintable) | [`0x3e41424ADF6Bb69B6271ef9B0541ACD59A0d454a`](https://explorer.34.60.137.196.sslip.io/address/0x3e41424ADF6Bb69B6271ef9B0541ACD59A0d454a) | -| Demo vault (Startup template) | [`0xCC31c7474267ca5600c2D530ebD7657A09c042Dc`](https://explorer.34.60.137.196.sslip.io/address/0xCC31c7474267ca5600c2D530ebD7657A09c042Dc) | +Nothing here depends on it, directly or transitively. That is why the interface uses +`@mysten/dapp-kit-react` 2.x and not `@mysten/dapp-kit` 1.x, which is deprecated precisely because +it is JSON-RPC only. The SDK's read layer targets the transport-agnostic Core API, so the same code +runs over gRPC today and over anything implementing that contract later. -The full record, including the deployer address, is in [contracts/deployments/9000.json](contracts/deployments/9000.json). Endpoints live only in `contracts/foundry.toml` and `web/src/config/chain.ts`. +## The short commands -## Quick start +```bash +npm run ios # the phone, on a simulator +npm run android +npm run dev # the web interface on :3000 +npm run indexer # the backend and its indexer +npm test # the Move suite and the TypeScript suites +npm run verify # everything CI runs +``` -Dependencies are git submodules, so clone with `--recurse-submodules` (or run -`git submodule update --init --recursive` in an existing clone). +## Verifying everything at once ```bash -# contracts -cd contracts -forge build -forge test # unit, fuzz and invariant suites -slither . --filter-paths "lib/|test/|script/" - -# interface -cd ../web -npm install -npm run dev # http://localhost:5173, connect MetaMask on chain 9000 -npm test # risk-engine mirror vectors +npm run verify ``` -Get devnet KASH from the faucet at `https://faucet.34.60.137.196.sslip.io/` and mint sUSD by calling `mint(address,uint256)` on the test token. +Runs what CI runs, job for job: the Move build (which must be warning-free) and its 75 tests, the +SDK's typecheck, build and tests, the backend's typecheck, tests and migrations against a real +Postgres, and the interface's typecheck and build. It skips rather than fails the steps whose +tooling is absent — a missing Postgres is not a broken migration — and exits non-zero if anything +genuinely breaks. -## Deploying your own instance +## Quick start ```bash -cd contracts -cp .env.example .env # set PRIVATE_KEY (devnet only) and DEMO_VAULT -forge script script/Deploy.s.sol --rpc-url ark_devnet --broadcast --private-key $PRIVATE_KEY -forge verify-contract
src/SkurVault.sol:SkurVault --verifier blockscout \ - --verifier-url https://explorer-api.34.60.137.196.sslip.io/api --chain 9000 \ - --libraries src/SkurPolicyLib.sol:SkurPolicyLib: -python3 ../scripts/export-abi.py && cp deployments/9000.json ../web/src/config/deployments.json +# the Move core +cd sui +sui move build +sui move test # 75 tests: the engine vectors and the end-to-end suite +scripts/verify-local.sh # publish to a throwaway local network and drive it + +# the SDK +cd ../sdk +npm install && npm run build +npm test # the same vectors as the Move suite + +# the backend +cd ../server +cp .env.example .env # set DATABASE_URL and SKUR_PACKAGE_ID +createdb skur && npm install && npm run migrate +npm run dev # the indexer and the read API together + +# the web interface +cd ../app +npm install +npm run dev # http://localhost:3000 + +# the phone +cd ../mobile +npm install +npm run ios # or: npm run android, npm start ``` -For a keyless deploy, `npx scriipture deploy` can broadcast the compiled artifact through a browser wallet. +The Sui CLI is the one prerequisite that is not an npm install: get it from +[the releases page](https://github.com/MystenLabs/sui/releases) or with +`cargo install --locked --git https://github.com/MystenLabs/sui.git sui`. -## How the vault decides +## Deploying -1. A proposal is created. The vault classifies it LOW, HIGH or CRITICAL from amount, share of holdings, recipient trust, today's outflow and the security mode, and pins the required approvals, guardian confirmations and delay. -2. Approvers approve. Guardians confirm when the tier demands it. A guardian can veto any critical, probationary or security-reducing proposal at any time. -3. An executor executes. The vault re-checks every limit at that moment: recipient not blocked and past activation, per-transaction cap, hard exposure block, daily bucket, and the loss envelope. Exceeding the envelope trips the vault into Lockdown instead of paying. -4. Governance changes follow the same path with owner approvals. Anything that loosens security waits for the policy-change delay, is vetoable, and is refused while in Lockdown. +```bash +cd sui +scripts/publish.sh testnet # builds, tests, publishes, writes deployments/testnet.json + +cd ../sdk +SUI_PRIVATE_KEY=suiprivkey1... SKUR_NETWORK=testnet SKUR_PACKAGE_ID=0x... \ + node --experimental-strip-types scripts/seed.ts +``` -Guardians can freeze, veto, confirm and recover. They hold no treasury role and can never move funds. +Then verify the whole stack against the live chain: + +```bash +cd sdk +SUI_PRIVATE_KEY=suiprivkey1... SKUR_NETWORK=testnet SKUR_PACKAGE_ID=0x... npm run e2e +``` + +`npm run e2e` is the test the unit suites cannot be: it creates a vault, funds it, previews a +payment, opens it, approves it, proves the activation delay holds, executes it, and checks that +every event decodes back into the shape the indexer projects. One wrong BCS field offset fails it +loudly where a unit test would pass. + +`sui/deployments/.json` is the only place a package address is configured. Nothing in the +app, the SDK or the backend hardcodes one. + +## How a payment goes out + +1. **Opened.** The vault classifies it routine, high risk or critical from the amount, the share of + holdings, the recipient's standing, the day's outflow and the security mode, and pins the + approvals, the guardian confirmations and the delay. +2. **Approved, or turned down.** Approvers approve; guardians confirm when the tier demands it. + Rejecting takes as many signers as approving, so a bad proposal can be cleared rather than left + to expire — and one signer cannot block the queue. A guardian can veto any critical payment, any + weakening of the vault, any recovery and any relaxation. +3. **Executed.** The vault re-checks everything against live state: the approval set is recounted + against the current roster, the payment is reclassified and the *stricter* of the pinned and + live requirements applies, the recipient's standing and activation are re-read, and the caps and + the hard block are re-applied. The loss envelope is checked last; a payment that would cross it + is refused and the vault latches into Lockdown. +4. **Governance** follows the same path with owner approvals. Anything that gives up a control waits + out the policy-change delay, is vetoable, and is refused in Lockdown. ## Status -V1 as scoped in the blueprint: vault creation, protected membership, four roles, native and ERC-20 deposits and transfer proposals, amount tiers, recipient trust with activation delay, per-transaction and 24-hour caps, circuit breaker, critical timelock and veto, three security modes, policy-change timelock, recovery, human-readable review, policy templates, policy simulator, maximum-loss view and posture indicator. Not yet done: independent audit and pilot. +Built and under test: the Move core with all seven proposal kinds, the SDK over gRPC and GraphQL, +the indexer and notification service on PostgreSQL, the interface, and seven starting policies — +startup, operating team, protocol/DAO, fund, payments company, nonprofit, family office. + +Not done, and not claimed: **no independent audit**, no mainnet deployment, no pilot. Do not put +real value behind Skur until an audit is complete. diff --git a/app/.env.example b/app/.env.example new file mode 100644 index 0000000..603ea61 --- /dev/null +++ b/app/.env.example @@ -0,0 +1,9 @@ +# Which network, and the package published on it. Both override sui/deployments/.json. +NEXT_PUBLIC_SKUR_NETWORK=testnet +NEXT_PUBLIC_SKUR_PACKAGE_ID=0x0 + +# A gRPC endpoint other than the public full node. +# NEXT_PUBLIC_SKUR_GRPC_URL=https://fullnode.testnet.sui.io:443 + +# The indexer. Optional: every screen works without it, with fewer round trips saved. +# NEXT_PUBLIC_SKUR_API=http://localhost:8787 diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..d78052c --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +public/skur-bytecode.json diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..03a741c --- /dev/null +++ b/app/README.md @@ -0,0 +1,65 @@ +# The Skur interface + +Next.js, React and `@mysten/dapp-kit-react` over gRPC. + +## Two route groups, two design languages + +| Group | Routes | Rendering | +|---|---|---| +| `(marketing)` | `/`, `/product`, `/solutions`, `/security`, `/docs`, `/docs/[slug]` | Static. No wallet, no chain reads at build time | +| `(dapp)` | `/app`, `/vault/[id]` | Client-only (`ssr: false`), because wallet detection needs a browser | + +Each group imports its own stylesheet from its layout, so the marketing design and the vault +interface never have to fight each other's base rules. The root layout carries no CSS. + +## Why dapp-kit 2.x + +`@mysten/dapp-kit` 1.x is deprecated because it speaks only JSON-RPC, which Sui switched off on +mainnet full nodes in July 2026 and is removing entirely in mid-October 2026. This app uses +`@mysten/dapp-kit-react` 2.x with a `SuiGrpcClient`. Nothing here depends on JSON-RPC, directly or +transitively. + +## Configuration + +```bash +cp .env.example .env +``` + +| Variable | What it does | +|---|---| +| `NEXT_PUBLIC_SKUR_NETWORK` | Which Sui network. Defaults to `testnet` | +| `NEXT_PUBLIC_SKUR_PACKAGE_ID` | The published package. Overrides the committed deployment | +| `NEXT_PUBLIC_SKUR_GRPC_URL` | A full node other than the public one | +| `NEXT_PUBLIC_SKUR_API` | The indexer. Optional — see below | + +Published addresses live in `sui/deployments/.json`. `npm run sync-deployments` copies them +into `src/config/deployments.json` so the app does not import across the workspace boundary. + +## The indexer is optional, on purpose + +Every screen works without `NEXT_PUBLIC_SKUR_API`. The vault, its policy, its roster, its +recipients and its proposals are all read from the chain through the SDK. The indexer saves round +trips and powers two conveniences — finding your vaults by address, and the recipient advisory — and +switching it off costs you those, not the ability to read the vault or move money. + +That is the test every endpoint had to pass: if this service disappears, what breaks must be speed, +not control. + +## The preview is the vault's + +The confirmation screen does not compute a tier. It simulates the vault's own `preview_transfer`, +which is the same Move function `execute_transfer` runs. An interface that computed its own answer +could promise something the vault would refuse, and on a confirmation screen that is the one thing +that must never happen. + +The TypeScript risk engine in the SDK is used only where there is no vault to ask: the policy +editor and the simulator, which run against a policy that is not deployed yet. + +## Running it + +```bash +npm install +npm run sync-deployments # after publishing the Move package +npm run dev # http://localhost:3000 +npm run typecheck +``` diff --git a/app/next.config.ts b/app/next.config.ts new file mode 100644 index 0000000..a9496a6 --- /dev/null +++ b/app/next.config.ts @@ -0,0 +1,21 @@ +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import type { NextConfig } from 'next'; + +const here = dirname(fileURLToPath(import.meta.url)); + +const config: NextConfig = { + reactStrictMode: true, + typedRoutes: true, + // Next writes AGENTS.md and CLAUDE.md into the project on startup. This repository does not + // carry those files. + agentRules: false, + turbopack: { + // `@skur/sdk` is a `file:` dependency, so it resolves through a symlink to a sibling + // directory. Turbopack will not follow a link out of the project unless it knows the + // workspace root is one level up. + root: resolve(here, '..'), + }, +}; + +export default config; diff --git a/app/package-lock.json b/app/package-lock.json new file mode 100644 index 0000000..6f2b31f --- /dev/null +++ b/app/package-lock.json @@ -0,0 +1,2052 @@ +{ + "name": "@skur/app", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@skur/app", + "version": "0.1.0", + "dependencies": { + "@mysten/dapp-kit-core": "^1.6.36", + "@mysten/dapp-kit-react": "^2.1.38", + "@mysten/sui": "^2.34.0", + "@skur/sdk": "file:../sdk", + "@tanstack/react-query": "^5.102.8", + "next": "^16.3.8", + "react": "^19.2.8", + "react-dom": "^19.2.8" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.7", + "typescript": "~6.0.2" + } + }, + "../sdk": { + "name": "@skur/sdk", + "version": "0.1.0", + "dependencies": { + "@mysten/sui": "^2.34.0" + }, + "devDependencies": { + "@types/node": "^24.19.1", + "typescript": "~6.0.2", + "vitest": "^3.2.7" + } + }, + "../sdk/node_modules/@0no-co/graphql.web": { + "version": "1.3.4", + "license": "MIT", + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "graphql": { + "optional": true + } + } + }, + "../sdk/node_modules/@0no-co/graphqlsp": { + "version": "1.17.5", + "license": "MIT", + "dependencies": { + "@gql.tada/internal": "^1.2.1", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "typescript": "^5.0.0 || ^6.0.0" + } + }, + "../sdk/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "../sdk/node_modules/@gql.tada/cli-utils": { + "version": "1.9.4", + "license": "MIT", + "dependencies": { + "@0no-co/graphqlsp": "^1.17.3", + "@gql.tada/internal": "1.2.2", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" + }, + "peerDependencies": { + "@0no-co/graphqlsp": "^1.16.0", + "@gql.tada/svelte-support": "1.0.3", + "@gql.tada/vue-support": "1.0.3", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@gql.tada/svelte-support": { + "optional": true + }, + "@gql.tada/vue-support": { + "optional": true + } + } + }, + "../sdk/node_modules/@gql.tada/internal": { + "version": "1.2.2", + "license": "MIT", + "dependencies": { + "@0no-co/graphql.web": "^1.3.1" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "../sdk/node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "license": "MIT", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "../sdk/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "dev": true, + "license": "MIT" + }, + "../sdk/node_modules/@mysten/bcs": { + "version": "2.1.2", + "license": "Apache-2.0", + "dependencies": { + "@mysten/utils": "^0.4.1", + "@scure/base": "^2.3.0" + } + }, + "../sdk/node_modules/@mysten/sui": { + "version": "2.34.0", + "license": "Apache-2.0", + "dependencies": { + "@graphql-typed-document-node/core": "^3.2.0", + "@mysten/bcs": "^2.1.2", + "@mysten/utils": "^0.4.1", + "@noble/curves": "^2.3.0", + "@noble/hashes": "^2.3.0", + "@protobuf-ts/grpcweb-transport": "^2.11.1", + "@protobuf-ts/runtime": "^2.11.1", + "@protobuf-ts/runtime-rpc": "^2.11.1", + "@scure/base": "^2.3.0", + "@scure/bip32": "^2.3.0", + "@scure/bip39": "^2.3.0", + "gql.tada": "^1.11.3", + "graphql": "^17.0.2", + "poseidon-lite": "0.3.0", + "valibot": "^1.4.2" + }, + "engines": { + "node": ">=22" + } + }, + "../sdk/node_modules/@mysten/utils": { + "version": "0.4.1", + "license": "Apache-2.0", + "dependencies": { + "@scure/base": "^2.3.0" + } + }, + "../sdk/node_modules/@noble/curves": { + "version": "2.4.0", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.4.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "../sdk/node_modules/@noble/hashes": { + "version": "2.4.0", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "../sdk/node_modules/@protobuf-ts/grpcweb-transport": { + "version": "2.11.1", + "license": "Apache-2.0", + "dependencies": { + "@protobuf-ts/runtime": "^2.11.1", + "@protobuf-ts/runtime-rpc": "^2.11.1" + } + }, + "../sdk/node_modules/@protobuf-ts/runtime": { + "version": "2.11.1", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "../sdk/node_modules/@protobuf-ts/runtime-rpc": { + "version": "2.11.1", + "license": "Apache-2.0", + "dependencies": { + "@protobuf-ts/runtime": "^2.11.1" + } + }, + "../sdk/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.64.0", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "../sdk/node_modules/@scure/base": { + "version": "2.4.0", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "../sdk/node_modules/@scure/bip32": { + "version": "2.4.0", + "license": "MIT", + "dependencies": { + "@noble/curves": "2.4.0", + "@noble/hashes": "2.4.0", + "@scure/base": "2.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "../sdk/node_modules/@scure/bip39": { + "version": "2.4.0", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "../sdk/node_modules/@types/chai": { + "version": "5.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "../sdk/node_modules/@types/deep-eql": { + "version": "4.0.2", + "dev": true, + "license": "MIT" + }, + "../sdk/node_modules/@types/estree": { + "version": "1.0.9", + "dev": true, + "license": "MIT" + }, + "../sdk/node_modules/@types/node": { + "version": "24.19.1", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "../sdk/node_modules/@vitest/expect": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../sdk/node_modules/@vitest/mocker": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "../sdk/node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../sdk/node_modules/@vitest/runner": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../sdk/node_modules/@vitest/snapshot": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../sdk/node_modules/@vitest/spy": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../sdk/node_modules/@vitest/utils": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../sdk/node_modules/assertion-error": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "../sdk/node_modules/cac": { + "version": "6.7.14", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../sdk/node_modules/chai": { + "version": "5.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "../sdk/node_modules/check-error": { + "version": "2.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "../sdk/node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "../sdk/node_modules/deep-eql": { + "version": "5.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "../sdk/node_modules/es-module-lexer": { + "version": "1.7.0", + "dev": true, + "license": "MIT" + }, + "../sdk/node_modules/esbuild": { + "version": "0.28.2", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "../sdk/node_modules/estree-walker": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "../sdk/node_modules/expect-type": { + "version": "1.4.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "../sdk/node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "../sdk/node_modules/fsevents": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "../sdk/node_modules/gql.tada": { + "version": "1.11.4", + "license": "MIT", + "dependencies": { + "@0no-co/graphql.web": "^1.3.2", + "@0no-co/graphqlsp": "^1.17.3", + "@gql.tada/cli-utils": "1.9.4", + "@gql.tada/internal": "1.2.2" + }, + "bin": { + "gql-tada": "bin/cli.js", + "gql.tada": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "../sdk/node_modules/graphql": { + "version": "17.0.2", + "license": "MIT", + "engines": { + "node": "^22.0.0 || ^24.0.0 || ^25.0.0 || >=26.0.0" + } + }, + "../sdk/node_modules/js-tokens": { + "version": "9.0.1", + "dev": true, + "license": "MIT" + }, + "../sdk/node_modules/loupe": { + "version": "3.2.1", + "dev": true, + "license": "MIT" + }, + "../sdk/node_modules/magic-string": { + "version": "0.30.21", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "../sdk/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "../sdk/node_modules/nanoid": { + "version": "3.3.19", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "../sdk/node_modules/pathe": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "../sdk/node_modules/pathval": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "../sdk/node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "../sdk/node_modules/picomatch": { + "version": "4.0.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "../sdk/node_modules/poseidon-lite": { + "version": "0.3.0", + "license": "MIT" + }, + "../sdk/node_modules/postcss": { + "version": "8.5.28", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "../sdk/node_modules/rollup": { + "version": "4.64.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.64.0", + "@rollup/rollup-android-arm64": "4.64.0", + "@rollup/rollup-darwin-arm64": "4.64.0", + "@rollup/rollup-darwin-x64": "4.64.0", + "@rollup/rollup-freebsd-arm64": "4.64.0", + "@rollup/rollup-freebsd-x64": "4.64.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.64.0", + "@rollup/rollup-linux-arm-musleabihf": "4.64.0", + "@rollup/rollup-linux-arm64-gnu": "4.64.0", + "@rollup/rollup-linux-arm64-musl": "4.64.0", + "@rollup/rollup-linux-loong64-gnu": "4.64.0", + "@rollup/rollup-linux-loong64-musl": "4.64.0", + "@rollup/rollup-linux-ppc64-gnu": "4.64.0", + "@rollup/rollup-linux-ppc64-musl": "4.64.0", + "@rollup/rollup-linux-riscv64-gnu": "4.64.0", + "@rollup/rollup-linux-riscv64-musl": "4.64.0", + "@rollup/rollup-linux-s390x-gnu": "4.64.0", + "@rollup/rollup-linux-x64-gnu": "4.64.0", + "@rollup/rollup-linux-x64-musl": "4.64.0", + "@rollup/rollup-openbsd-x64": "4.64.0", + "@rollup/rollup-openharmony-arm64": "4.64.0", + "@rollup/rollup-win32-arm64-msvc": "4.64.0", + "@rollup/rollup-win32-ia32-msvc": "4.64.0", + "@rollup/rollup-win32-x64-gnu": "4.64.0", + "@rollup/rollup-win32-x64-msvc": "4.64.0", + "fsevents": "~2.3.2" + } + }, + "../sdk/node_modules/siginfo": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "../sdk/node_modules/source-map-js": { + "version": "1.2.2", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "../sdk/node_modules/stackback": { + "version": "0.0.2", + "dev": true, + "license": "MIT" + }, + "../sdk/node_modules/std-env": { + "version": "3.10.0", + "dev": true, + "license": "MIT" + }, + "../sdk/node_modules/strip-literal": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "../sdk/node_modules/tinybench": { + "version": "2.9.0", + "dev": true, + "license": "MIT" + }, + "../sdk/node_modules/tinyexec": { + "version": "0.3.2", + "dev": true, + "license": "MIT" + }, + "../sdk/node_modules/tinyglobby": { + "version": "0.2.17", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "../sdk/node_modules/tinypool": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "../sdk/node_modules/tinyrainbow": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "../sdk/node_modules/tinyspy": { + "version": "4.0.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "../sdk/node_modules/typescript": { + "version": "6.0.3", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "../sdk/node_modules/undici-types": { + "version": "7.24.6", + "dev": true, + "license": "MIT" + }, + "../sdk/node_modules/valibot": { + "version": "1.5.0", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "../sdk/node_modules/vite": { + "version": "7.3.6", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "../sdk/node_modules/vite-node": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../sdk/node_modules/vitest": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "../sdk/node_modules/why-is-node-running": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@0no-co/graphql.web": { + "version": "1.3.4", + "license": "MIT", + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "graphql": { + "optional": true + } + } + }, + "node_modules/@0no-co/graphqlsp": { + "version": "1.17.5", + "license": "MIT", + "dependencies": { + "@gql.tada/internal": "^1.2.1", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "typescript": "^5.0.0 || ^6.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "license": "MIT" + }, + "node_modules/@gql.tada/cli-utils": { + "version": "1.9.4", + "license": "MIT", + "dependencies": { + "@0no-co/graphqlsp": "^1.17.3", + "@gql.tada/internal": "1.2.2", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" + }, + "peerDependencies": { + "@0no-co/graphqlsp": "^1.16.0", + "@gql.tada/svelte-support": "1.0.3", + "@gql.tada/vue-support": "1.0.3", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@gql.tada/svelte-support": { + "optional": true + }, + "@gql.tada/vue-support": { + "optional": true + } + } + }, + "node_modules/@gql.tada/internal": { + "version": "1.2.2", + "license": "MIT", + "dependencies": { + "@0no-co/graphql.web": "^1.3.1" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "license": "MIT", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@lit-labs/scoped-registry-mixin": { + "version": "1.0.4", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^1.0.0 || ^2.0.0", + "lit": "^2.0.0 || ^3.0.0" + } + }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.6.0", + "license": "BSD-3-Clause" + }, + "node_modules/@lit/react": { + "version": "1.0.8", + "license": "BSD-3-Clause", + "peerDependencies": { + "@types/react": "17 || 18 || 19" + } + }, + "node_modules/@lit/reactive-element": { + "version": "2.1.2", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0" + } + }, + "node_modules/@lit/task": { + "version": "1.0.3", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^1.0.0 || ^2.0.0" + } + }, + "node_modules/@mysten/bcs": { + "version": "2.1.2", + "license": "Apache-2.0", + "dependencies": { + "@mysten/utils": "^0.4.1", + "@scure/base": "^2.3.0" + } + }, + "node_modules/@mysten/dapp-kit-core": { + "version": "1.6.36", + "license": "Apache-2.0", + "dependencies": { + "@floating-ui/dom": "^1.8.0", + "@lit-labs/scoped-registry-mixin": "^1.0.4", + "@lit/task": "^1.0.3", + "@mysten/slush-wallet": "^1.3.2", + "@mysten/utils": "^0.4.1", + "@mysten/wallet-standard": "^0.21.32", + "@nanostores/lit": "^0.2.3", + "@wallet-standard/ui": "^1.0.3", + "@wallet-standard/ui-registry": "^1.1.1", + "@webcomponents/scoped-custom-element-registry": "^0.0.10", + "lit": "^3.3.3", + "nanostores": "^1.4.2" + }, + "peerDependencies": { + "@mysten/sui": "^2.34.0" + } + }, + "node_modules/@mysten/dapp-kit-react": { + "version": "2.1.38", + "license": "Apache-2.0", + "dependencies": { + "@lit/react": "^1.0.8", + "@mysten/dapp-kit-core": "^1.6.36", + "@nanostores/react": "^1.0.0", + "nanostores": "^1.4.2" + }, + "peerDependencies": { + "@types/react": ">=17.0.0", + "react": ">=17.0.0" + } + }, + "node_modules/@mysten/slush-wallet": { + "version": "1.3.2", + "license": "Apache-2.0", + "dependencies": { + "@mysten/utils": "^0.4.1", + "@mysten/wallet-standard": "^0.21.32", + "@mysten/window-wallet-core": "^0.4.0", + "valibot": "^1.4.2" + }, + "peerDependencies": { + "@mysten/sui": "^2.34.0" + } + }, + "node_modules/@mysten/sui": { + "version": "2.34.0", + "license": "Apache-2.0", + "dependencies": { + "@graphql-typed-document-node/core": "^3.2.0", + "@mysten/bcs": "^2.1.2", + "@mysten/utils": "^0.4.1", + "@noble/curves": "^2.3.0", + "@noble/hashes": "^2.3.0", + "@protobuf-ts/grpcweb-transport": "^2.11.1", + "@protobuf-ts/runtime": "^2.11.1", + "@protobuf-ts/runtime-rpc": "^2.11.1", + "@scure/base": "^2.3.0", + "@scure/bip32": "^2.3.0", + "@scure/bip39": "^2.3.0", + "gql.tada": "^1.11.3", + "graphql": "^17.0.2", + "poseidon-lite": "0.3.0", + "valibot": "^1.4.2" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@mysten/utils": { + "version": "0.4.1", + "license": "Apache-2.0", + "dependencies": { + "@scure/base": "^2.3.0" + } + }, + "node_modules/@mysten/wallet-standard": { + "version": "0.21.32", + "license": "Apache-2.0", + "dependencies": { + "@wallet-standard/core": "1.1.2" + }, + "peerDependencies": { + "@mysten/sui": "^2.34.0" + } + }, + "node_modules/@mysten/window-wallet-core": { + "version": "0.4.0", + "license": "Apache-2.0", + "dependencies": { + "@mysten/utils": "^0.4.1", + "jose": "^6.2.9", + "valibot": "^1.4.2" + } + }, + "node_modules/@nanostores/lit": { + "version": "0.2.3", + "license": "MIT", + "peerDependencies": { + "lit": "^2.6.0 || ^3.0.0", + "nanostores": ">=0.7 < 1.0.0 || ^1.0.0" + } + }, + "node_modules/@nanostores/react": { + "version": "1.1.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": "^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "nanostores": "^1.2.0", + "react": ">=18.0.0" + } + }, + "node_modules/@next/env": { + "version": "16.3.8", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.3.8", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.8.tgz", + "integrity": "sha512-GZtCCOBKJ4leVIT/Th0llWKhD1ca92lzbQiS5R5ON9QkoiFnilFsebDae1JU2a3HWoKMEmEZWGs1AGLavVM72Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.8.tgz", + "integrity": "sha512-O659ygeQYqneJ1fBKMpFxIFqYkYswu8IAS1OCKK/4f3ZgJJm1dRz4fVJZRi/kLLWjnBKnebOePA4WNv+sV1pVA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.8.tgz", + "integrity": "sha512-dSjKSyWpzxoO1d3DIZZcP4XJcNaKeLmxQMFOiYl5vuBRMmweIqnAhty8tAmRsvTss779cK1FtYnDMj40e4TQlg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.8.tgz", + "integrity": "sha512-lbqOuz3RPRcv+o9msNsJw5x4+Y1ZwPTs6vmL6DCf7i0fZfvng/F59wyeDwqHIvV0mK//RBy/jJkZ+nCKsSMXjQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.8.tgz", + "integrity": "sha512-+316WswI8ScVgZeUd+1KGaXkHhaYQzCjvH/05TZSpJ8zBizb1a4G7DtO7F12jcBIqMOtsz9ji1t48fmKtzqsGA==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.8.tgz", + "integrity": "sha512-ji0gd4kMYUxO+1fJBIbiBVRCjzG/lloiyCccnlebvb1ZJ5qXCPZqYg4Jl1DrrixnWNMKylzgpmMWx0yNDYXlzw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.8.tgz", + "integrity": "sha512-WcTlaKt/TWkh5kUjdJcUmB1XgZ+1c6fz4Y9fDHL73YNSdGaUWjceeWrrlwF0nv19iABYWC4iAq1oX1w4Bn0vfg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@noble/curves": { + "version": "2.4.0", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.4.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.4.0", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@protobuf-ts/grpcweb-transport": { + "version": "2.11.1", + "license": "Apache-2.0", + "dependencies": { + "@protobuf-ts/runtime": "^2.11.1", + "@protobuf-ts/runtime-rpc": "^2.11.1" + } + }, + "node_modules/@protobuf-ts/runtime": { + "version": "2.11.1", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@protobuf-ts/runtime-rpc": { + "version": "2.11.1", + "license": "Apache-2.0", + "dependencies": { + "@protobuf-ts/runtime": "^2.11.1" + } + }, + "node_modules/@scure/base": { + "version": "2.4.0", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "2.4.0", + "license": "MIT", + "dependencies": { + "@noble/curves": "2.4.0", + "@noble/hashes": "2.4.0", + "@scure/base": "2.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "2.4.0", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@skur/sdk": { + "resolved": "../sdk", + "link": true + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.104.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.104.1", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.104.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/node": { + "version": "24.19.1", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "license": "MIT" + }, + "node_modules/@wallet-standard/app": { + "version": "1.1.1", + "license": "Apache-2.0", + "dependencies": { + "@wallet-standard/base": "^1.1.1" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@wallet-standard/base": { + "version": "1.1.1", + "license": "Apache-2.0", + "engines": { + "node": ">=22" + } + }, + "node_modules/@wallet-standard/core": { + "version": "1.1.2", + "license": "Apache-2.0", + "dependencies": { + "@wallet-standard/app": "^1.1.1", + "@wallet-standard/base": "^1.1.1", + "@wallet-standard/errors": "^0.1.2", + "@wallet-standard/features": "^1.1.1", + "@wallet-standard/wallet": "^1.1.1" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@wallet-standard/errors": { + "version": "0.1.2", + "license": "Apache-2.0", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^13.1.0" + }, + "bin": { + "errors": "bin/cli.mjs" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@wallet-standard/features": { + "version": "1.1.1", + "license": "Apache-2.0", + "dependencies": { + "@wallet-standard/base": "^1.1.1" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@wallet-standard/ui": { + "version": "1.0.3", + "license": "Apache-2.0", + "dependencies": { + "@wallet-standard/ui-compare": "^1.0.3", + "@wallet-standard/ui-core": "^1.0.1", + "@wallet-standard/ui-features": "^1.0.3" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@wallet-standard/ui-compare": { + "version": "1.0.3", + "license": "Apache-2.0", + "dependencies": { + "@wallet-standard/base": "1.1.1", + "@wallet-standard/ui-core": "1.0.1", + "@wallet-standard/ui-registry": "1.1.1" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@wallet-standard/ui-core": { + "version": "1.0.1", + "license": "Apache-2.0", + "dependencies": { + "@wallet-standard/base": "1.1.1" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@wallet-standard/ui-features": { + "version": "1.0.3", + "license": "Apache-2.0", + "dependencies": { + "@wallet-standard/base": "1.1.1", + "@wallet-standard/errors": "0.1.2", + "@wallet-standard/ui-core": "1.0.1", + "@wallet-standard/ui-registry": "1.1.1" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@wallet-standard/ui-registry": { + "version": "1.1.1", + "license": "Apache-2.0", + "dependencies": { + "@wallet-standard/base": "1.1.1", + "@wallet-standard/errors": "0.1.2", + "@wallet-standard/ui-core": "1.0.1" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@wallet-standard/wallet": { + "version": "1.1.1", + "license": "Apache-2.0", + "dependencies": { + "@wallet-standard/base": "^1.1.1" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@webcomponents/scoped-custom-element-registry": { + "version": "0.0.10", + "license": "BSD-3-Clause" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.27", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001814", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "5.6.2", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "license": "MIT" + }, + "node_modules/commander": { + "version": "13.1.0", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/gql.tada": { + "version": "1.11.4", + "license": "MIT", + "dependencies": { + "@0no-co/graphql.web": "^1.3.2", + "@0no-co/graphqlsp": "^1.17.3", + "@gql.tada/cli-utils": "1.9.4", + "@gql.tada/internal": "1.2.2" + }, + "bin": { + "gql-tada": "bin/cli.js", + "gql.tada": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/graphql": { + "version": "17.0.2", + "license": "MIT", + "engines": { + "node": "^22.0.0 || ^24.0.0 || ^25.0.0 || >=26.0.0" + } + }, + "node_modules/jose": { + "version": "6.2.12", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/lit": { + "version": "3.3.3", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-element": { + "version": "4.2.2", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0", + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-html": { + "version": "3.3.3", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nanostores": { + "version": "1.5.4", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": "^20.0.0 || >=22.0.0" + } + }, + "node_modules/next": { + "version": "16.3.8", + "resolved": "https://registry.npmjs.org/next/-/next-16.3.8.tgz", + "integrity": "sha512-U7QEZaTini6wKrb8A8hqLLqYQyCetegKjCpJOyxk642vWoMoU1x5PyZCJFvgYgiptA8xc5j/9xYlZFO7w9Sjmw==", + "license": "MIT", + "dependencies": { + "@next/env": "16.3.8", + "@swc/helpers": "0.5.23", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.5.23", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.3.8", + "@next/swc-darwin-x64": "16.3.8", + "@next/swc-linux-arm64-gnu": "16.3.8", + "@next/swc-linux-arm64-musl": "16.3.8", + "@next/swc-linux-x64-gnu": "16.3.8", + "@next/swc-linux-x64-musl": "16.3.8", + "@next/swc-win32-arm64-msvc": "16.3.8", + "@next/swc-win32-x64-msvc": "16.3.8", + "sharp": "^0.35.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "license": "ISC" + }, + "node_modules/poseidon-lite": { + "version": "0.3.0", + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.23", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.3.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.5", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "6.0.3", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "dev": true, + "license": "MIT" + }, + "node_modules/valibot": { + "version": "1.5.0", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + } + } +} diff --git a/app/package.json b/app/package.json new file mode 100644 index 0000000..666ee79 --- /dev/null +++ b/app/package.json @@ -0,0 +1,29 @@ +{ + "name": "@skur/app", + "private": true, + "version": "0.1.0", + "scripts": { + "dev": "npm run sdk && next dev", + "build": "npm run sdk && next build", + "start": "next start", + "typecheck": "npm run sdk && tsc --noEmit", + "sync-deployments": "node scripts-sync-deployments.mjs", + "sdk": "npm --prefix ../sdk run --silent build" + }, + "dependencies": { + "@mysten/dapp-kit-core": "^1.6.36", + "@mysten/dapp-kit-react": "^2.1.38", + "@mysten/sui": "^2.34.0", + "@skur/sdk": "file:../sdk", + "@tanstack/react-query": "^5.102.8", + "next": "^16.3.8", + "react": "^19.2.8", + "react-dom": "^19.2.8" + }, + "devDependencies": { + "@types/node": "^24.13.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.7", + "typescript": "~6.0.2" + } +} \ No newline at end of file diff --git a/app/public/mark.png b/app/public/mark.png new file mode 100644 index 0000000..3bc9302 Binary files /dev/null and b/app/public/mark.png differ diff --git a/app/scripts-sync-deployments.mjs b/app/scripts-sync-deployments.mjs new file mode 100644 index 0000000..ce90725 --- /dev/null +++ b/app/scripts-sync-deployments.mjs @@ -0,0 +1,22 @@ +// Copy sui/deployments/*.json into the app, so the published addresses are configured in exactly +// one place and the app does not import across the workspace boundary. +import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises'; + +const from = new URL('../sui/deployments/', import.meta.url); +const to = new URL('./src/config/deployments.json', import.meta.url); + +const out = {}; +try { + for (const file of await readdir(from)) { + if (!file.endsWith('.json')) continue; + const body = JSON.parse(await readFile(new URL(file, from), 'utf8')); + out[body.network ?? file.replace('.json', '')] = body; + } +} catch { + console.log('no deployments yet; leaving the committed defaults in place'); + process.exit(0); +} + +await mkdir(new URL('./src/config/', import.meta.url), { recursive: true }); +await writeFile(to, `${JSON.stringify(out, null, 2)}\n`); +console.log(`wrote ${Object.keys(out).join(', ') || 'nothing'}`); diff --git a/app/src/app/(dapp)/app/page.tsx b/app/src/app/(dapp)/app/page.tsx new file mode 100644 index 0000000..ef1b690 --- /dev/null +++ b/app/src/app/(dapp)/app/page.tsx @@ -0,0 +1,21 @@ +'use client'; + +import dynamic from 'next/dynamic'; +import { Providers } from '@/app/providers'; +import { Boundary } from '@/components/Boundary'; + +// Wallet detection needs a browser, so nothing in the app tree is server-rendered. +const HomeApp = dynamic(() => import('@/app/HomeApp'), { + ssr: false, + loading: () =>
Loading…
, +}); + +export default function Page() { + return ( + + + + + + ); +} diff --git a/app/src/app/(dapp)/layout.tsx b/app/src/app/(dapp)/layout.tsx new file mode 100644 index 0000000..003aa54 --- /dev/null +++ b/app/src/app/(dapp)/layout.tsx @@ -0,0 +1,5 @@ +import '../globals.css'; + +export default function DappLayout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/app/src/app/(dapp)/vault/[id]/page.tsx b/app/src/app/(dapp)/vault/[id]/page.tsx new file mode 100644 index 0000000..b9106b4 --- /dev/null +++ b/app/src/app/(dapp)/vault/[id]/page.tsx @@ -0,0 +1,23 @@ +'use client'; + +import dynamic from 'next/dynamic'; +import { useParams } from 'next/navigation'; +import { Providers } from '@/app/providers'; +import { Boundary } from '@/components/Boundary'; + +const VaultApp = dynamic(() => import('@/app/VaultApp'), { + ssr: false, + loading: () =>
Loading…
, +}); + +export default function Page() { + const params = useParams<{ id: string }>(); + const id = Array.isArray(params.id) ? params.id[0] : params.id; + return ( + + + + + + ); +} diff --git a/app/src/app/(marketing)/docs/[slug]/page.tsx b/app/src/app/(marketing)/docs/[slug]/page.tsx new file mode 100644 index 0000000..46fcd03 --- /dev/null +++ b/app/src/app/(marketing)/docs/[slug]/page.tsx @@ -0,0 +1,14 @@ +import { notFound } from 'next/navigation'; +import { DocsView } from '@/marketing/docs'; +import { DOC_SLUGS, type DocSlug } from '@/marketing/docSlugs'; + +/** Every page is known ahead of time, so the documentation is static. */ +export function generateStaticParams() { + return DOC_SLUGS.filter((slug) => slug !== 'introduction').map((slug) => ({ slug })); +} + +export default async function Page({ params }: { params: Promise<{ slug: string }> }) { + const { slug } = await params; + if (!(DOC_SLUGS as readonly string[]).includes(slug)) notFound(); + return ; +} diff --git a/app/src/app/(marketing)/docs/page.tsx b/app/src/app/(marketing)/docs/page.tsx new file mode 100644 index 0000000..49da38b --- /dev/null +++ b/app/src/app/(marketing)/docs/page.tsx @@ -0,0 +1,7 @@ +import { DocsView } from '@/marketing/docs'; + +export const metadata = { title: 'Documentation' }; + +export default function Page() { + return ; +} diff --git a/app/src/app/(marketing)/layout.tsx b/app/src/app/(marketing)/layout.tsx new file mode 100644 index 0000000..5001861 --- /dev/null +++ b/app/src/app/(marketing)/layout.tsx @@ -0,0 +1,5 @@ +import '../../marketing/styles.css'; + +export default function MarketingLayout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/app/src/app/(marketing)/page.tsx b/app/src/app/(marketing)/page.tsx new file mode 100644 index 0000000..2bfe5fa --- /dev/null +++ b/app/src/app/(marketing)/page.tsx @@ -0,0 +1,568 @@ +'use client'; + +import Link from 'next/link'; +import { MarketingShell } from '@/marketing/Shell'; +import { AppMockup, PhoneMockup } from '@/marketing/Mockup'; +import { ScoreTicker } from '@/marketing/Ticker'; +import { Reveal } from '@/marketing/Reveal'; +import { + IconArrowRight, + IconCheck, + IconChevronRight, +} from '@/marketing/icons'; +import { useCountUp } from '@/marketing/motion'; +import { NETWORK } from '@/config'; + +function Stat({ n, label }: { n: number; label: string }) { + const { ref, value } = useCountUp(n, 1400); + return ( +
+ + {value} + +
{label}
+
+ ); +} + +const FAQ: [string, string][] = [ + [ + 'What is Skur?', + 'A self-custodial treasury vault on Sui whose authorization requirements rise and fall with the risk of each payment. It keeps the k-of-n approval you already trust and adds onchain rules about how much can move, where it can go, how fast, and how long the risky ones wait.', + ], + [ + 'How is this different from a multisig?', + 'A multisig asks one question: did enough keys sign? Skur asks a second one: should this payment settle right now? The amount, its share of holdings, the recipient’s standing, what already left today and the vault’s security posture all sit between a valid approval and the money. Sui has signature aggregation in the protocol already — that is not what is missing.', + ], + [ + 'Does Skur ever hold funds or keys?', + 'No. The vault is a shared object that holds the assets; your wallets hold the keys. No Skur service is in the execution path. The indexer makes a dashboard faster and can be switched off without affecting your ability to move money, read the policy or approve a payment.', + ], + [ + 'What happens if one of our signers is compromised?', + 'Alone, a stolen key cannot reach any tier’s threshold unless you set that tier to one — and even then it is bounded by the routine maximum, the per-payment cap, the day’s remaining allowance and the loss envelope. If several keys are stolen, the larger payments still wait out a delay any guardian can use to veto, and the envelope freezes the vault on the first attempt that would cross it.', + ], + [ + 'What can a guardian do?', + 'Freeze the vault, veto a critical payment or any weakening of the policy, confirm the largest payments, block a recipient immediately, and start a recovery for a lost signer. A guardian holds no treasury role — the contract refuses to give it one — so a stolen guardian key cannot move a single coin.', + ], + [ + 'Can an automated agent use it?', + 'Yes, and this is the part a threshold cannot express. An agent holds a proposer role: it can open a payment and nothing else. It cannot approve, confirm or execute. A fully compromised agent key produces a queue of proposals that no human approved.', + ], + [ + 'Why Sui, and why Move?', + 'A vault is one shared object, so “what are the rules right now” is a single read. Programmable transaction blocks make creation atomic without a factory. sui::clock gives millisecond time in the transaction, so timelocks are durations rather than block arithmetic. And the treasury is keyed by coin type, so one vault holds any asset with its own limits and its own envelope — no token allowlist to maintain or get wrong.', + ], + [ + 'Has it been audited?', + 'No. The Move core is covered by two test suites, each test named after the attack it defends against, and the TypeScript engines are pinned against the same vectors. That is not an audit. Do not put real value behind Skur before one is complete.', + ], +]; + +const HERO_CELLS: [number, number][] = [ + [0, 130], + [130, 260], + [1180, 0], + [1310, 0], + [1310, 130], + [910, 390], + [1050, 260], + [520, 0], +]; + +export default function Landing() { + const words = 'Treasury security that'.split(' '); + + return ( + +
+ + Every control runs in Move, in a shared object you can read → + +
+ +
+
+ {HERO_CELLS.map(([x, y], i) => ( + + ))} +
+
+ 16 invariants enforced by the vault, not by a server +
+

+ {words.map((w, i) => ( + + + {w} + {' '} + + ))} + + {['assumes', 'compromise'].map((w, i) => ( + + + {w} + + {i === 0 ? ' ' : ''} + + ))} + +

+

+ An approval proves someone was authorized. Skur decides whether the payment is safe: + how much can move, where it can go, how fast, and how long the risky ones wait — even + when your signers are compromised. +

+
+ + Launch app + + + See how it works + +
+
+ Self-custodial + Sui · {NETWORK} + Enforced in Move +
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+ +
+
+ +

Made for organizations that hold real money onchain

+

+ Startups, protocols, funds, payment companies, nonprofits and family offices that need + more than a signature count between a stolen key and the treasury. +

+
+
+
+ {[...Array(2)].flatMap((_, k) => + [ + 'Startups', + 'Protocols & DAOs', + 'Venture funds', + 'Payment companies', + 'Nonprofits', + 'Family offices', + 'Finance teams', + 'Grant programmes', + ].map((t) => ( + + {t} + + )), + )} +
+
+
+
+ +
+
+ +
01 / Controls
+

Your treasury, under control

+

+ Every rule below is Move code in a shared object, not a setting on our server. If Skur + disappeared tomorrow, your vault and its policy would not. +

+
+ +
+

Risk-tiered authorization

+

+ The vault scores every payment from its amount, its share of holdings, who receives + it, what already left today and the current security mode. The score sets how many + approvals it needs, whether a guardian must sign, and how long it waits. +

+
+
+ {[ + '1,000 USDC to a known supplier', + '12,000 USDC to a new address', + '60,000 USDC, 24% of holdings', + ].map((t) => ( + + {t} + + ))} +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + +
+
+ + Routine · 1 approval · executes now + + + High risk · 2 approvals · waits 1 day + + + Critical · 2 + a guardian · 1 day · vetoable + +
+
+
+ +
+

Recipient trust

+

+ Recipients are security objects, not strings. An address the vault has never paid is + escalated every time. A newly registered one serves an activation delay before it + can be paid at all. +

+
+ {[ + ['New', 'activates in 23h', '#1b3a5c', '#cfe2ff', '0x7c4f…41e2'], + ['Verified', 'normal policy', '#1b4332', '#d1e7dd', '0x3a91…c0de'], + ['Restricted', 'always critical', '#4d3a05', '#fff3cd', '0xe2b7…9f10'], + ['Blocked', 'refused', '#4a1c22', '#f8d7da', '0x0d55…77aa'], + ].map(([s, d, bg, fg, addr]) => ( +
+ + {addr} + + + {s} · {d} + +
+ ))} +
+
+ +
+

Cumulative limits

+

+ Per-payment and 24-hour caps, counted against what has already left. Crossing half + the day’s allowance escalates everything after it, so splitting a drain into twenty + pieces meets a higher bar partway through instead of never. +

+
+ {[ + ['Payment 1–5', '49,500 USDC', 'Routine'], + ['Payment 6', 'half the day gone', 'Escalated'], + ['Payment 11', 'envelope crossed', 'Refused'], + ].map(([a, b, c]) => ( +
+ {a} + {b} + {c} +
+ ))} +
+
+ +
+

A circuit breaker that latches

+

+ A share of the balance that may leave per window, measured against the balance when + the window opened. The payment that would cross it is not made — the vault freezes + instead, and the freeze survives the attempt. +

+
+ +
+

An agent that cannot pay itself

+

+ Software that requests payments holds a proposer role: it can open one and nothing + else. A compromised automation key produces a queue nobody approved, not a + withdrawal. No threshold product can express this, because to a threshold every key + is the same kind of key. +

+ + How the agent role works + +
+
+
+
+ +
+
+ +
02 / The firewall
+

Weakening the vault is the attack

+

+ An attacker with enough keys does not withdraw first. They lower the thresholds, widen + the caps and shorten the delays, and then withdraw. So that is the path Skur guards + hardest. +

+
+ +
+

Tightening is instant. Loosening is not.

+

+ Every policy change is compared field by field against the live one — including the + cases that look like tightening and are not, such as setting a threshold to zero to + disable it, or extending how long a proposal stays alive. +

+
    +
  • + Loosening any control waits out the policy-change delay +
  • +
  • + Any guardian can veto it inside that window +
  • +
  • + Lockdown refuses to let it be proposed at all +
  • +
+
+
+ +
+
+
+
+ +
+
+ +
03 / Compared
+

What else a Sui treasury can use today

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ SkurNative multisigSmart-account frameworksOffchain policy custody
AuthorizationFollows the risk of the paymentFixed k-of-nFixed, per configConfigurable, offchain
Cumulative outflowCounted per asset, split-proofNot expressibleNot implementedProvider-dependent
Enough signers compromisedCaps, delays, veto and a breakerFunds goneFunds goneDepends on the provider
Policy tamperingDelayed, vetoable, refused in LockdownNo policy to tamper withImmediateProvider-controlled
Where the rules liveA shared object anyone can readIn the protocolOnchainSomebody’s backend
+
+
+
+ +
+ +
+ “Control not only who can move treasury funds, but how much can move, where it can + go, and what happens when risk changes.” +
+ The Skur thesis +
+ + + +
+
+
+ +
+
+ +
04 / Who it is for
+

Two kinds of power, held by different people

+

+ Finance teams move the money. Guardians hold the brake. The contract will not let one + address do both jobs. +

+
+ +
+

For finance teams

+

+ Tiered approvals, trusted recipients, daily caps, and a plain-language review — read + from the vault itself — before any wallet opens. +

+ + How teams use Skur + +
+ +
+
+
+

For guardians

+

+ Freeze the vault, veto the critical and the security-reducing, confirm the largest + payments, block a recipient on the spot, recover a lost signer. Never touch a coin. +

+ + What a guardian can do + +
+ +
+
+
+
+
+ +
+
+ +
05 / Lifecycle
+

How a payment moves

+

+ Requirements are pinned at step one. Everything is checked again at step four, and the + stricter of the two applies. +

+
+ +
+
1
+

Opened and scored

+

+ The vault reads the amount, its share of holdings, the recipient’s standing, today’s + outflow and the security mode, then pins the tier and its requirements to the + proposal. +

+
+
+
2
+

Reviewed in plain language

+

+ Approvers see what leaves, what remains, who receives it and every reason the tier + was assigned — read from the vault, not computed by the interface. +

+
+
+
3
+

Confirmed, or vetoed

+

+ Approvals are recounted against the live roster, so a removed signer’s approval stops + counting. Any guardian can veto a critical or security-reducing proposal until it + executes. +

+
+
+
4
+

Executed only if still safe

+

+ Caps, exposure, the recipient’s standing and the loss envelope are all re-checked. + Over the envelope, the vault freezes instead of paying. +

+
+
+
+
+ +
+
+ +
06 / Questions
+

Questions, answered plainly

+
+ +
+ {FAQ.map(([q, a]) => ( +
+ + {q} + + + +
{a}
+
+ ))} +
+
+
+
+ + ); +} diff --git a/app/src/app/(marketing)/product/page.tsx b/app/src/app/(marketing)/product/page.tsx new file mode 100644 index 0000000..09a155d --- /dev/null +++ b/app/src/app/(marketing)/product/page.tsx @@ -0,0 +1,196 @@ +'use client'; + +import Link from 'next/link'; +import { MarketingShell, SubHero } from '@/marketing/Shell'; +import { AppMockup } from '@/marketing/Mockup'; +import { Reveal } from '@/marketing/Reveal'; +import { + IconBank, + IconBook, + IconClock, + IconGuard, + IconLab, + IconLayers, + IconShield, + IconTrend, + IconTx, +} from '@/marketing/icons'; + +export default function Product() { + return ( + + + Multisig approvals, +
+ plus a firewall + + } + sub="Keep the k-of-n you already trust. Add the controls that decide whether a valid approval should move money right now." + > + + Read the security model + +
+ +
+
+ +

Seven controls between an approval and the money

+

+ Each one is Move code you can read on the explorer. The interface never shows a rule + the vault does not enforce. +

+
+ +
+
+

Risk-tiered approvals

+

+ Amount, share of holdings, recipient standing, today’s outflow and the security mode + set the tier. The tier sets the approvals, the guardian sign-off and the wait. +

+
+
+
+

Recipient trust

+

+ A never-paid address is escalated every time. A newly registered one waits out an + activation delay. Owners can verify, trust, restrict or block any recipient through + governance; a guardian can block one on the spot. +

+
+
+
+

Velocity limits

+

+ Per-payment and 24-hour caps, counted cumulatively per asset. Splitting a payment + into pieces changes nothing, and crossing half the day escalates the rest. +

+
+
+
+

Circuit breaker

+

+ A loss envelope per window, measured against the balance when the window opened. The + payment that would cross it is not made; the vault latches into Lockdown, and the + latch survives the attempt. +

+
+
+
+

Guardians

+

+ An independent role that can freeze, veto, confirm critical payments, block a + recipient and recover a lost signer — and that the contract refuses to give any + path to withdraw. +

+
+
+
+

Policy-change firewall

+

+ Loosening any control waits out a delay, can be vetoed by a guardian, and cannot + even be proposed while the vault is in Lockdown. +

+
+
+
+

An automation role

+

+ A proposer can open a payment and nothing else — not approve, not confirm, not + execute. It is how an agent asks the treasury to pay without ever being able to. +

+
+
+
+
+ +
+
+ +

Know exactly what you are signing

+

+ Before the wallet opens, the vault itself describes the payment: what leaves, what + remains, who receives it, why it was scored the way it was, and what it needs to + execute. +

+
+ +
+

A review written by the contract

+

+ The preview is the vault’s own classification, simulated live against the same + function execution runs. The interface cannot promise something the vault would + later refuse. +

+
    +
  • Amount, treasury impact and remaining balance
  • +
  • Every reason the tier was assigned, not just the loudest
  • +
  • Approvals, guardian sign-off and delay, exactly as pinned
  • +
+
+
+ +
+
+
+
+ +
+
+ +

Test the policy before it is live

+

+ The simulator runs the attacks that actually happen against your policy, using the + same deterministic rules the Move modules apply. +

+
+ +
+
+

Attack scenarios

+

+ One stolen key paying a new address. Three stolen keys taking 40%. A drain split into + twenty payments. A policy weakened before a withdrawal. A stolen guardian key. A + compromised agent. +

+
+
+
+

Maximum possible loss

+

+ One conservative number per asset: what could leave with no delay, what could leave + within a day, and how long the largest payments wait. Assumptions stated alongside, + never a guarantee. +

+
+
+
+

Posture report

+

+ Confirmed controls and obvious weaknesses, in plain words. It produces no score, + because a score would imply a precision nobody has. +

+
+
+ +
+ +
+
+
+
+
+ ); +} diff --git a/app/src/app/(marketing)/security/page.tsx b/app/src/app/(marketing)/security/page.tsx new file mode 100644 index 0000000..7156999 --- /dev/null +++ b/app/src/app/(marketing)/security/page.tsx @@ -0,0 +1,181 @@ +'use client'; + +import { MarketingShell, SubHero } from '@/marketing/Shell'; +import { Reveal } from '@/marketing/Reveal'; +import { IconCheck } from '@/marketing/icons'; +import { NETWORK, PACKAGE_ID, CONFIGURED } from '@/config'; + +/** The sixteen invariants, each with the control that enforces it. */ +const INVARIANTS: [string, string][] = [ + ['Nothing executes before every active condition is met', 'ordered assertions in execute_transfer'], + ['A guardian cannot move treasury assets through guardian authority', 'types::roles_valid · role gates'], + ['Lockdown stops outgoing execution and new payments', 'assert_not_lockdown'], + ['A security-reducing change never takes effect immediately', 'policy::reductions · delay · veto'], + ['Cumulative outflow cannot be bypassed by splitting', 'day bucket · loss envelope'], + ['A proposal cannot execute twice', 'status written before the coin moves'], + ['A removed signer’s approval stops counting', 'count_with_role against the live roster'], + ['An approval cannot be duplicated or replayed', 'one entry per address in a VecSet'], + ['A new recipient cannot skip its activation delay', 'executable_at = max(delay, activates_at)'], + ['Recovery cannot quietly weaken the vault', 'roles copied · policy untouched · Elevated after'], + ['Balance accounting reconciles', 'one Balance per type · split is the only exit'], + ['The roster always satisfies the policy', 'assert_counts_valid after every change'], + ['The breaker’s latch survives the attempt', 'execute_transfer returns without aborting'], + ['Leaving a raised posture is never unilateral', 'owners + guardians + delay'], + ['An automation key cannot authorize itself', 'PROPOSER satisfies only can_propose'], + ['Rejecting takes as many signers as approving', 'reject · counted against live roles'], +]; + +export default function SecurityPage() { + return ( + + + Built for the day +
+ a key is stolen + + } + sub="A stolen key, a compromised laptop or a rogue signer should be a bad day, not the end of the treasury. This page is what holds, and what has not been proven yet." + > + {CONFIGURED ? ( + + The published package + + ) : null} +
+ +
+
+ +

What we claim, and what we do not

+

+ Skur does not make signer compromise harmless. It adds independent, programmable + controls that limit or delay the damage when authorized credentials are misused. +

+
+ +
+
1
+

One signer compromised

+

+ Cannot reach any tier’s threshold alone unless you set that tier to one — and even + then it is bounded by the routine maximum, the per-payment cap, the day’s remaining + allowance and the loss envelope. +

+
+
+
2
+

Enough signers compromised

+

+ The high and critical delays are a guardian’s veto window, critical payments need a + guardian, and the envelope freezes the vault on the first attempt that would cross + it. +

+
+
+
3
+

An owner quorum weakens the policy

+

+ Loosening waits out the policy-change delay, can be vetoed by any guardian, and + cannot be proposed at all while the vault is in Lockdown. +

+
+
+
4
+

A guardian is compromised

+

+ There is no withdrawal path. A recovery is delayed and any owner can cancel it. A + guardian can only freeze, veto and block. +

+
+
+
+
+ +
+
+ +

Sixteen invariants, each with a test

+

+ Every control is enforced by the Move code and proven by a test named after the attack + it defends against. The mapping is public in the security model. +

+
+ + {INVARIANTS.map(([t, how], i) => ( +
+
+ {i + 1} +
+

{t}

+

{how}

+
+ ))} +
+
+
+ +
+
+ +

Verification, honestly stated

+

What has been done, and what has not.

+
+ +
+
+

Two Move test suites

+

+ One pins the risk classifier and the policy rules as vectors; one drives the vault + end to end, with a test per attack rather than per function. +

+
+
+
+

The engines agree with the chain

+

+ The TypeScript risk engine is pinned against the same vectors as the Move suite, so + a preview and an execution cannot disagree about a tier. +

+
+
+
+

Reproducible builds

+

+ The toolchain version is pinned in CI, because the Move compiler and the bundled + framework move together and a job that silently upgrades either cannot reproduce a + failure. +

+
+
+
–
+

Independent audit

+

Not yet. Do not put real value behind Skur until one has been completed.

+
+
+
–
+

Mainnet deployment

+

Not yet. Testnet exists for evaluation.

+
+
+
+

Known limitations, listed

+

+ Anchored windows, per-asset exposure with no oracle, a breaker that latches rather + than refunds, single-guardian vaults, and a throughput ceiling per shared object. + All of it is in the model. +

+
+
+
+
+
+ ); +} diff --git a/app/src/app/(marketing)/solutions/page.tsx b/app/src/app/(marketing)/solutions/page.tsx new file mode 100644 index 0000000..79f8e74 --- /dev/null +++ b/app/src/app/(marketing)/solutions/page.tsx @@ -0,0 +1,175 @@ +'use client'; + +import Link from 'next/link'; +import { MarketingShell, SubHero } from '@/marketing/Shell'; +import { AppMockup } from '@/marketing/Mockup'; +import { Reveal } from '@/marketing/Reveal'; +import { + IconArrowRight, + IconBank, + IconClock, + IconGuard, + IconLayers, + IconShield, + IconUsers, +} from '@/marketing/icons'; +import { TEMPLATES, fmtDuration } from '@skur/sdk'; +import { NETWORK } from '@/config'; + +export default function Solutions() { + return ( + + + Treasury control +
+ for onchain organizations + + } + sub="Approvals that scale with risk, recipients that earn trust, and guardians who can stop a bad day without ever being able to cause one." + /> + +
+
+ +

Three steps to a protected treasury

+

+ Nobody designs seventeen policy fields from first principles. Pick a template, name + the people, sign once. +

+
+ +
+
+

Name signers and guardians

+

+ Owners govern, approvers confirm payments, executors execute, proposers can only + ask. Guardians hold the brake and nothing else; the contract will not let them hold + both. +

+
+
+
+

Pick a policy template

+

+ Seven profiles, from Startup to Family office, set the tiers, delays, caps and loss + envelope. Every number can be tuned later through governance. +

+
+
+
+

Operate behind a firewall

+

+ Every payment is scored and reviewed in plain language. Every loosening of the + policy waits, and any guardian can stop it. +

+
+
+
+
+ +
+
+ +

One vault, seven starting points

+

+ Each template is a complete, valid policy. Start with the closest fit and tighten from + there. +

+
+ + {TEMPLATES.map((t) => ( +
+
+

{t.name}

+

{t.tagline}

+

{t.audience}

+

+ Critical payments wait {fmtDuration(t.policy.delayCritical)} · new recipients wait{' '} + {fmtDuration(t.policy.recipientActivationDelay)} · loss envelope{' '} + {t.policy.envelopeBps / 100}% per {fmtDuration(t.policy.envelopeWindow)} +

+
+ ))} +
+
+
+ +
+
+ +

For guardians

+

+ A cold key in a drawer, a board member, a security firm: anyone who should be able to + stop money but must never be able to move it. +

+
+ +
+

Hold the brake, not the wheel

+

+ The contract refuses to give a guardian any treasury role, and refuses to give a + treasury member the guardian role. That makes guardians the second control plane an + attacker has to breach, and the one they cannot profit from. +

+
    +
  • + Freeze the vault the moment something looks wrong +
  • +
  • + Veto critical payments and any weakening of the policy +
  • +
  • + Replace a lost signer after a delay owners can cancel +
  • +
+
+
+ +
+
+
+
+ +
+
+ +

No custody. No fees on assets. No lock-in.

+

+ Skur never holds a key and never takes a percentage. The vault is a shared object you + can reach with any compatible interface. +

+
+ +
+

Self-custodial by construction

+

+ The vault object holds the assets. Your wallets hold the keys. Nothing created the + vault that retains authority over it, because there is no factory — a vault is + configured and shared in one transaction. +

+ + Read the security model + +
+
+

Reachable without Skur

+

+ The package is published on Sui and every endpoint lives in one configuration file. + The indexer makes a dashboard faster; switching it off costs you speed, not control. +

+ + Open the explorer + +
+ +
+
+
+
+
+
+ ); +} diff --git a/app/src/app/HomeApp.tsx b/app/src/app/HomeApp.tsx new file mode 100644 index 0000000..80809c0 --- /dev/null +++ b/app/src/app/HomeApp.tsx @@ -0,0 +1,139 @@ +'use client'; + +import { useCurrentAccount } from '@mysten/dapp-kit-react'; +import Link from 'next/link'; +import { useState } from 'react'; +import { MODE_LABELS, describeRoles } from '@skur/sdk'; +import { API_URL, CONFIGURED, KNOWN_VAULTS, NETWORK } from '@/config'; +import { useMyVaults, useVaultTx } from '@/hooks/useVault'; +import { CreateVault } from '@/components/CreateVault'; +import { PublishPackage } from '@/components/PublishPackage'; +import { Shell } from '@/components/Shell'; +import { Addr, Notice } from '@/components/ui'; + +export default function HomeApp() { + const account = useCurrentAccount(); + const [mode, setMode] = useState<'open' | 'create'>('open'); + const [manual, setManual] = useState(''); + const tx = useVaultTx(undefined); + const mine = useMyVaults(account?.address, API_URL); + + return ( + +
+

A valid signature is not a safe payment

+

+ Skur decides how much authorization a payment needs, and whether it may settle right now, + from the amount, the destination, the recent outflow and the vault's own security + posture. The rules are enforced in Move, in a shared object anyone can read. +

+
+ + + + {mode === 'create' ? ( + + ) : ( +
+
+

Your vaults

+ {!account ? ( +

+ Connect a wallet to see the vaults you hold a role in. +

+ ) : !API_URL ? ( +

+ Finding your vaults by address needs the indexer. Set{' '} + NEXT_PUBLIC_SKUR_API, or open a vault by its id. +

+ ) : mine.isLoading ? ( +
Looking…
+ ) : mine.error ? ( +

+ The indexer is not reachable. You can still open a vault by its id. +

+ ) : mine.data && mine.data.length > 0 ? ( +
+ {mine.data.map((v) => ( + +
+ {v.name} + + {MODE_LABELS[v.mode as 0 | 1 | 2]} +
+

+ {describeRoles(v.roles).join(' · ') || 'no role'} +

+ + ))} +
+ ) : ( +
No vaults found for this address.
+ )} +
+ +
+
+

Open by id

+

+ A vault is a shared object. Anyone can read one; acting on it needs the key that + holds the role. +

+
+ setManual(e.target.value.trim())} + spellCheck={false} + /> + + Open + +
+
+ + {KNOWN_VAULTS.length > 0 ? ( +
+

On {NETWORK}

+
+ {KNOWN_VAULTS.map((v) => ( +
+ + {v.name} + + + +
+ ))} +
+
+ ) : null} + + {!CONFIGURED ? : null} +
+
+ )} +
+ ); +} diff --git a/app/src/app/VaultApp.tsx b/app/src/app/VaultApp.tsx new file mode 100644 index 0000000..0104b23 --- /dev/null +++ b/app/src/app/VaultApp.tsx @@ -0,0 +1,91 @@ +'use client'; + +import { useCurrentAccount } from '@mysten/dapp-kit-react'; +import { useState } from 'react'; +import { Role, hasRole } from '@skur/sdk'; +import { CONFIGURED } from '@/config'; +import { useVaultTx, useVaultView } from '@/hooks/useVault'; +import { Overview } from '@/components/Overview'; +import { People } from '@/components/People'; +import { PolicyPanel } from '@/components/PolicyPanel'; +import { PublishPackage } from '@/components/PublishPackage'; +import { Queue } from '@/components/Queue'; +import { Security } from '@/components/Security'; +import { Send } from '@/components/Send'; +import { Shell } from '@/components/Shell'; +import { Notice } from '@/components/ui'; + +type Tab = 'overview' | 'queue' | 'send' | 'policy' | 'people' | 'security'; + +export default function VaultApp({ vaultId }: { vaultId: string }) { + const [tab, setTab] = useState('overview'); + const account = useCurrentAccount(); + const query = useVaultView(vaultId); + const tx = useVaultTx(vaultId); + + const tabs: { id: Tab; label: string }[] = [ + { id: 'overview', label: 'Overview' }, + { id: 'queue', label: 'Queue' }, + { id: 'send', label: 'Pay' }, + { id: 'policy', label: 'Policy' }, + { id: 'people', label: 'People' }, + { id: 'security', label: 'Security' }, + ]; + + return ( + ({ + id: t.id, + label: t.label, + active: tab === t.id, + onClick: () => setTab(t.id), + }))} + > + {!CONFIGURED ? ( + + ) : query.isLoading ? ( +
Reading the vault…
+ ) : query.error ? ( + + Could not read that vault. Check the id and the network. +

+ A vault id names the shared vault object — it is not a wallet address, even though the + two look alike. If you pasted your account address, go back and open one of your + vaults instead, or create one. +

+

+ {query.error instanceof Error ? query.error.message : String(query.error)} +

+
+ ) : query.data ? ( + <> +
+

{query.data.vault.name}

+ {account ? ( + m.address === account.address)?.roles ?? 0} /> + ) : null} + + {tx.pending ? working… : null} +
+ {tab === 'overview' ? : null} + {tab === 'queue' ? : null} + {tab === 'send' ? : null} + {tab === 'policy' ? : null} + {tab === 'people' ? : null} + {tab === 'security' ? : null} + + ) : null} +
+ ); +} + +function RoleBadge({ roles }: { roles: number }) { + if (roles === 0) return read only; + const names: string[] = []; + if (hasRole(roles, Role.OWNER)) names.push('owner'); + if (hasRole(roles, Role.APPROVER)) names.push('approver'); + if (hasRole(roles, Role.EXECUTOR)) names.push('executor'); + if (hasRole(roles, Role.GUARDIAN)) names.push('guardian'); + if (hasRole(roles, Role.PROPOSER)) names.push('proposer'); + return {names.join(' · ')}; +} diff --git a/app/src/app/apple-icon.png b/app/src/app/apple-icon.png new file mode 100644 index 0000000..8d2cd06 Binary files /dev/null and b/app/src/app/apple-icon.png differ diff --git a/app/src/app/globals.css b/app/src/app/globals.css new file mode 100644 index 0000000..03d79e3 --- /dev/null +++ b/app/src/app/globals.css @@ -0,0 +1,200 @@ +/* Design tokens. One accent, a neutral ramp, and a semantic set for the three risk tiers and the + three security modes — the interface should never invent a colour for a state the vault has. */ +:root { + --accent: #ffd000; + --accent-ink: #1a1500; + --bg: #ffffff; + --bg-sunk: #f6f7f8; + --bg-raised: #ffffff; + --line: #e4e6e9; + --line-strong: #cfd3d8; + --ink: #15181c; + --ink-soft: #5b6470; + --ink-faint: #8b949e; + --ok: #107a4d; + --warn: #9a6400; + --bad: #b3261e; + --tier-low: #107a4d; + --tier-high: #9a6400; + --tier-critical: #b3261e; + --radius: 10px; + --radius-lg: 14px; + --mono: ui-monospace, "SF Mono", "DM Mono", Menlo, monospace; + --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, system-ui, sans-serif; +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme='light']) { + --bg: #0e1013; + --bg-sunk: #15181c; + --bg-raised: #191d22; + --line: #262b31; + --line-strong: #363d45; + --ink: #eef1f4; + --ink-soft: #a3adb8; + --ink-faint: #70797f; + --ok: #4ade80; + --warn: #fbbf24; + --bad: #f87171; + --tier-low: #4ade80; + --tier-high: #fbbf24; + --tier-critical: #f87171; + } +} + +:root[data-theme='dark'] { + --bg: #0e1013; + --bg-sunk: #15181c; + --bg-raised: #191d22; + --line: #262b31; + --line-strong: #363d45; + --ink: #eef1f4; + --ink-soft: #a3adb8; + --ink-faint: #70797f; + --ok: #4ade80; + --warn: #fbbf24; + --bad: #f87171; + --tier-low: #4ade80; + --tier-high: #fbbf24; + --tier-critical: #f87171; +} + +* { box-sizing: border-box; } + +html, body { padding: 0; margin: 0; } + +body { + background: var(--bg); + color: var(--ink); + font-family: var(--sans); + font-size: 15px; + line-height: 1.55; + -webkit-font-smoothing: antialiased; +} + +a { color: inherit; } + +h1, h2, h3, h4 { margin: 0; line-height: 1.2; letter-spacing: -0.02em; font-weight: 650; } +h1 { font-size: 30px; } +h2 { font-size: 21px; } +h3 { font-size: 17px; } +h4 { font-size: 15px; } +p { margin: 0; } + +.mono { font-family: var(--mono); font-size: 13px; } +.soft { color: var(--ink-soft); } +.faint { color: var(--ink-faint); } +.small { font-size: 13px; } + +/* layout */ +.shell { max-width: 1120px; margin: 0 auto; padding: 0 20px 80px; } +.topbar { + position: sticky; top: 0; z-index: 20; + display: flex; align-items: center; gap: 16px; + padding: 14px 20px; margin-bottom: 28px; + background: color-mix(in srgb, var(--bg) 88%, transparent); + backdrop-filter: blur(10px); + border-bottom: 1px solid var(--line); +} +.topbar .grow { flex: 1; } +.brand { display: flex; align-items: center; gap: 9px; font-weight: 700; letter-spacing: -0.03em; } +.brand .mark { width: 26px; height: 26px; display: block; } +.nav { display: flex; gap: 2px; flex-wrap: wrap; } +.nav a, .nav button { + padding: 7px 12px; border-radius: 8px; border: 0; background: transparent; + color: var(--ink-soft); font: inherit; font-size: 14px; cursor: pointer; text-decoration: none; +} +.nav a:hover, .nav button:hover { background: var(--bg-sunk); color: var(--ink); } +.nav a[aria-current='page'], .nav button[data-active='true'] { background: var(--bg-sunk); color: var(--ink); font-weight: 600; } + +.grid { display: grid; gap: 16px; } +.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } +.cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } +@media (max-width: 860px) { + .cols-2, .cols-3, .cols-4 { grid-template-columns: minmax(0, 1fr); } + .shell { padding: 0 16px 60px; } + h1 { font-size: 25px; } +} + +.card { + background: var(--bg-raised); + border: 1px solid var(--line); + border-radius: var(--radius-lg); + padding: 18px; +} +.card.sunk { background: var(--bg-sunk); } +.card h3 { margin-bottom: 4px; } +.card + .card { margin-top: 16px; } + +.row { display: flex; align-items: center; gap: 10px; } +.row.wrap { flex-wrap: wrap; } +.row .grow { flex: 1; } +.stack { display: flex; flex-direction: column; gap: 12px; } +.stack.tight { gap: 6px; } + +.kv { display: flex; justify-content: space-between; gap: 16px; padding: 7px 0; border-bottom: 1px solid var(--line); font-size: 14px; } +.kv:last-child { border-bottom: 0; } +.kv dt { color: var(--ink-soft); } +.kv dd { margin: 0; font-weight: 550; text-align: right; } + +/* controls */ +.btn { + display: inline-flex; align-items: center; justify-content: center; gap: 7px; + padding: 9px 15px; border-radius: 9px; border: 1px solid var(--line-strong); + background: var(--bg-raised); color: var(--ink); + font: inherit; font-size: 14px; font-weight: 550; cursor: pointer; +} +.btn:hover:not(:disabled) { border-color: var(--ink-faint); } +.btn:disabled { opacity: 0.45; cursor: not-allowed; } +.btn.primary { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); font-weight: 650; } +.btn.danger { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 40%, var(--line)); } +.btn.ghost { border-color: transparent; background: transparent; } +.btn.sm { padding: 6px 11px; font-size: 13px; } + +input, select, textarea { + width: 100%; padding: 9px 11px; border-radius: 9px; + border: 1px solid var(--line-strong); background: var(--bg); + color: var(--ink); font: inherit; font-size: 14px; +} +input:focus, select:focus, textarea:focus { outline: 2px solid var(--accent); outline-offset: -1px; } +label { display: block; font-size: 13px; color: var(--ink-soft); margin-bottom: 5px; } +.field + .field { margin-top: 12px; } + +/* state */ +.pill { + display: inline-flex; align-items: center; gap: 5px; + padding: 3px 9px; border-radius: 999px; font-size: 12px; font-weight: 600; + border: 1px solid var(--line-strong); color: var(--ink-soft); +} +.pill.low { color: var(--tier-low); border-color: color-mix(in srgb, var(--tier-low) 35%, var(--line)); } +.pill.high { color: var(--tier-high); border-color: color-mix(in srgb, var(--tier-high) 35%, var(--line)); } +.pill.critical { color: var(--tier-critical); border-color: color-mix(in srgb, var(--tier-critical) 35%, var(--line)); } +.pill.accent { color: var(--accent-ink); background: var(--accent); border-color: var(--accent); } + +.notice { + padding: 12px 14px; border-radius: var(--radius); + border: 1px solid var(--line-strong); background: var(--bg-sunk); font-size: 14px; +} +.notice.warn { border-color: color-mix(in srgb, var(--warn) 45%, var(--line)); } +.notice.bad { border-color: color-mix(in srgb, var(--bad) 45%, var(--line)); } +.notice.ok { border-color: color-mix(in srgb, var(--ok) 45%, var(--line)); } + +.dot { width: 8px; height: 8px; border-radius: 999px; flex: 0 0 auto; } +.dot.ok { background: var(--ok); } +.dot.warn { background: var(--warn); } +.dot.bad { background: var(--bad); } + +.bar { height: 6px; border-radius: 999px; background: var(--line); overflow: hidden; } +.bar > span { display: block; height: 100%; background: var(--accent); } +.bar.warn > span { background: var(--warn); } +.bar.bad > span { background: var(--bad); } + +.reasons { margin: 8px 0 0; padding-left: 18px; font-size: 13px; color: var(--ink-soft); } +.reasons li + li { margin-top: 3px; } + +table { width: 100%; border-collapse: collapse; font-size: 14px; } +th { text-align: left; font-weight: 600; color: var(--ink-soft); font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; padding: 0 0 8px; } +td { padding: 10px 0; border-top: 1px solid var(--line); vertical-align: top; } + +.empty { padding: 36px 20px; text-align: center; color: var(--ink-soft); } diff --git a/app/src/app/icon.png b/app/src/app/icon.png new file mode 100644 index 0000000..ce61958 Binary files /dev/null and b/app/src/app/icon.png differ diff --git a/app/src/app/layout.tsx b/app/src/app/layout.tsx new file mode 100644 index 0000000..ca8a6c4 --- /dev/null +++ b/app/src/app/layout.tsx @@ -0,0 +1,23 @@ +import type { Metadata } from 'next'; + +export const metadata: Metadata = { + title: { + default: 'Skur — a programmable authorization layer for Sui treasuries', + template: '%s · Skur', + }, + description: + 'Skur decides how much authorization a payment needs, and whether it may settle right now, from the amount, the destination, the recent outflow and the vault’s own security posture. Enforced in Move.', +}; + +/** + * The root layout carries no stylesheet. The marketing pages and the vault interface are two + * different design languages, each loaded only on its own routes, so neither has to fight the + * other's base rules. + */ +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/app/src/app/providers.tsx b/app/src/app/providers.tsx new file mode 100644 index 0000000..f1f484f --- /dev/null +++ b/app/src/app/providers.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { DAppKitProvider } from '@mysten/dapp-kit-react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { useState } from 'react'; +import { dAppKit } from '@/dapp-kit'; + +/** + * Client-only. Wallet detection needs a browser, so every page that renders this is loaded with + * `ssr: false`. + */ +export function Providers({ children }: { children: React.ReactNode }) { + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + // A vault is a shared object: a write by anyone changes it, so reads stay fresh for + // only a few seconds. `refetchOnWindowFocus` is off because it made the old build + // look like it was constantly reloading. + staleTime: 5_000, + refetchInterval: 15_000, + refetchOnWindowFocus: false, + retry: 1, + }, + }, + }), + ); + return ( + + {children} + + ); +} diff --git a/app/src/components/Boundary.tsx b/app/src/components/Boundary.tsx new file mode 100644 index 0000000..aac500c --- /dev/null +++ b/app/src/components/Boundary.tsx @@ -0,0 +1,48 @@ +'use client'; + +import React from 'react'; + +/** + * The last line against a white screen. Anything that throws during render inside the dapp tree + * lands here as a readable panel with the actual message — never a blank page. Nothing was + * signed: a render crash happens on this machine, after reads, before any wallet prompt. + */ +export class Boundary extends React.Component< + { children: React.ReactNode }, + { error: Error | null } +> { + state = { error: null as Error | null }; + + static getDerivedStateFromError(error: Error) { + return { error }; + } + + componentDidCatch(error: Error, info: React.ErrorInfo) { + console.error('dapp render crashed', error, info.componentStack); + } + + render() { + if (!this.state.error) return this.props.children; + return ( +
+
+

Something broke while drawing this page

+

+ Nothing was signed and nothing left any vault — this is a display error on your + machine. Reloading usually clears it. +

+

+ {this.state.error.message} +

+ +
+
+ ); + } +} diff --git a/app/src/components/CreateVault.tsx b/app/src/components/CreateVault.tsx new file mode 100644 index 0000000..5810f11 --- /dev/null +++ b/app/src/components/CreateVault.tsx @@ -0,0 +1,253 @@ +'use client'; + +import { useCurrentAccount } from '@mysten/dapp-kit-react'; +import { useMemo, useState } from 'react'; +import { SUI_TYPE_ARG } from '@mysten/sui/utils'; +import { + Role, + TEMPLATES, + fmtDuration, + rolesValid, + templateById, + tx as build, + validateCounts, + validatePolicy, + type TemplateId, +} from '@skur/sdk'; +import { CONFIGURED, PACKAGE_ID } from '@/config'; +import type { TxState } from '@/hooks/useVault'; +import { Notice } from './ui'; + +type Seat = { address: string; roles: number }; + +/** + * Creating a vault. + * + * One transaction: the whole configuration goes into a single programmable transaction block, so + * there is never a moment when a vault exists without its policy, its signers and its assets. If + * any part is wrong the whole thing fails and nothing is created. + * + * The thresholds are checked against the roster before the wallet opens, because a vault whose + * threshold nobody can reach is a frozen vault and the contract refuses to create one. + */ +export function CreateVault({ tx }: { tx: TxState }) { + const account = useCurrentAccount(); + const me = account?.address; + const [templateId, setTemplateId] = useState('startup'); + const [name, setName] = useState('Treasury'); + const [seats, setSeats] = useState([]); + + const template = templateById(templateId); + const roster: Seat[] = useMemo( + () => [ + ...(me ? [{ address: me, roles: Role.OWNER | Role.APPROVER | Role.EXECUTOR }] : []), + ...seats.filter((s) => /^0x[0-9a-fA-F]{1,64}$/.test(s.address)), + ], + [me, seats], + ); + + const counts = useMemo(() => { + const has = (r: number) => roster.filter((s) => (s.roles & r) !== 0).length; + return { + owners: has(Role.OWNER), + approvers: has(Role.APPROVER), + executors: has(Role.EXECUTOR), + guardians: has(Role.GUARDIAN), + }; + }, [roster]); + + const policyErrors = validatePolicy(template.policy); + const countErrors = validateCounts( + template.policy, + counts.owners, + counts.approvers, + counts.executors, + counts.guardians, + ); + const badRoles = roster.filter((s) => !rolesValid(s.roles)); + const duplicates = roster.length !== new Set(roster.map((s) => s.address)).size; + + const ready = + Boolean(me) && + CONFIGURED && + name.trim().length > 0 && + policyErrors.length === 0 && + countErrors.length === 0 && + badRoles.length === 0 && + !duplicates; + + const create = () => + tx.run( + build.createVault(PACKAGE_ID, { + name: name.trim(), + policy: template.policy, + members: roster, + assets: [{ coinType: SUI_TYPE_ARG, limits: template.native }], + recipients: [], + }), + 'create', + ); + + return ( +
+
+

Pick a starting policy

+

+ Each one is a complete, valid policy. Start with the closest fit and tighten it afterwards + through governance. +

+
+ {TEMPLATES.map((t) => ( + + ))} +
+
+ +
+
+

Name it

+
+ setName(e.target.value)} maxLength={60} /> +
+
+ +
+

Who holds what

+

+ You are the first owner, approver and executor. Add the rest — and note that the + contract will not let one address be both a guardian and anything else. +

+ + {me ? ( +
+
+ {me.slice(0, 10)}…{me.slice(-6)} + you + + Owner + Approver + Executor +
+
+ ) : null} + +
+ {seats.map((s, i) => ( +
+ + setSeats((v) => v.map((x, j) => (j === i ? { ...x, address: e.target.value.trim() } : x))) + } + style={{ flex: 1, minWidth: 220 }} + spellCheck={false} + /> + + +
+ ))} +
+ + + +
+ {counts.owners} owner{counts.owners === 1 ? '' : 's'} + {counts.approvers} approver{counts.approvers === 1 ? '' : 's'} + {counts.executors} executor{counts.executors === 1 ? '' : 's'} + {counts.guardians} guardian{counts.guardians === 1 ? '' : 's'} +
+
+ + {!CONFIGURED ? ( + + No package is configured for this network. Publish with{' '} + sui/scripts/publish.sh and set{' '} + NEXT_PUBLIC_SKUR_PACKAGE_ID. + + ) : null} + {!me ? Connect a wallet to create a vault. : null} + {duplicates ? An address appears twice. : null} + {badRoles.length > 0 ? ( + + A guardian cannot also hold a treasury role. The contract would refuse this roster. + + ) : null} + {countErrors.length > 0 ? ( + + This template needs a larger roster. +
    + {countErrors.map((e) => ( +
  • {e}
  • + ))} +
+
+ ) : null} + {tx.error ? {tx.error} : null} + + +

+ One transaction creates the vault, sets the policy, installs the signers and approves SUI. + If any part is invalid, nothing is created. +

+
+
+ ); +} diff --git a/app/src/components/Overview.tsx b/app/src/components/Overview.tsx new file mode 100644 index 0000000..6f64fec --- /dev/null +++ b/app/src/components/Overview.tsx @@ -0,0 +1,247 @@ +'use client'; + +import { + Mode, + Status, + describeReasons, + fmtAmount, + fmtDuration, + type VaultView, +} from '@skur/sdk'; +import { EXPLORER } from '@/config'; +import { Addr, ModePill, Notice, coinDecimals, coinSymbol } from './ui'; + +/** + * What the vault is right now: its posture, what it holds, how much of each window is used, and + * the policy in one glance. Everything here is read from the chain. + */ +export function Overview({ view }: { view: VaultView }) { + const { vault, assets, proposals, members } = view; + const pending = proposals.filter((p) => p.status === Status.PENDING); + const p = vault.policy; + + return ( +
+ {vault.mode === Mode.LOCKDOWN ? ( + + This vault is in Lockdown. Nothing outgoing executes and no payment can + be opened. Deposits still work. Leaving Lockdown needs{' '} + {p.governanceThreshold} owner approval{p.governanceThreshold === 1 ? '' : 's'},{' '} + {p.guardianThreshold} guardian confirmation{p.guardianThreshold === 1 ? '' : 's'} and{' '} + {fmtDuration(p.policyChangeDelay)}. + {vault.postureReasons ? ( +
    + {describeReasons(vault.postureReasons).map((r) => ( +
  • {r}
  • + ))} +
+ ) : null} +
+ ) : null} + {vault.mode === Mode.ELEVATED ? ( + + Elevated. Every payment is escalated one tier and the per-transaction and + daily caps are halved, until owners and a guardian agree to lower the posture. + {vault.postureReasons ? ( +
    + {describeReasons(vault.postureReasons).map((r) => ( +
  • {r}
  • + ))} +
+ ) : null} +
+ ) : null} + +
+
+

Posture

+
+ +
+
+
+

Awaiting action

+

{pending.length}

+
+
+

Signers

+

{members.length}

+

+ {vault.ownerCount} owner{vault.ownerCount === 1 ? '' : 's'} · {vault.guardianCount}{' '} + guardian{vault.guardianCount === 1 ? '' : 's'} +

+
+
+

Policy version

+

{vault.policyVersion.toString()}

+
+
+ +
+

Holdings

+

+ Each asset has its own limits, its own day and its own loss envelope. There is no + portfolio-wide number, because computing one honestly would need a price oracle. +

+ {assets.length === 0 ? ( +

+ No assets configured yet. +

+ ) : ( + + + + + + + + + + + + + {assets.map((a) => { + const d = coinDecimals(a.coinType); + const sym = coinSymbol(a.coinType); + const daily = a.limits.dailyMax; + const spent = a.velocity?.daySpent ?? 0n; + const dayPct = daily === 0n ? 0 : Number((spent * 100n) / daily); + const basis = a.velocity?.envelopeBasis ?? a.balance; + const envLimit = + p.envelopeBps === 0 ? 0n : (basis * BigInt(p.envelopeBps)) / 10_000n; + const envSpent = a.velocity?.envelopeSpent ?? 0n; + const envPct = envLimit === 0n ? 0 : Number((envSpent * 100n) / envLimit); + return ( + + + + + + + + + ); + })} + +
AssetBalanceRoutine up toPer paymentTodayEnvelope
+ {sym} + {!a.limits.approved ? ( + + not approved + + ) : null} + {fmtAmount(a.balance, d)}{fmtAmount(a.limits.lowMax, d)} + {a.limits.perTxMax === 0n ? 'no cap' : fmtAmount(a.limits.perTxMax, d)} + + {daily === 0n ? ( + no cap + ) : ( + <> +
80 ? 'bad' : dayPct > 50 ? 'warn' : ''}`}> + +
+ + {fmtAmount(spent, d)} of {fmtAmount(daily, d)} + + + )} +
+ {envLimit === 0n ? ( + off + ) : ( + <> +
80 ? 'bad' : envPct > 50 ? 'warn' : ''}`}> + +
+ + {fmtAmount(envSpent, d)} of {fmtAmount(envLimit, d)} + + + )} +
+ )} +
+ +
+
+

Approvals

+
+
+
Routine payment
+
+ {p.approvalsLow} approval{p.approvalsLow === 1 ? '' : 's'}, no wait +
+
+
+
High risk
+
+ {p.approvalsHigh} approvals, {fmtDuration(p.delayHigh)} +
+
+
+
Critical
+
+ {p.approvalsCritical} approvals + {p.guardianRequiredCritical ? ` + ${p.guardianThreshold} guardian` : ''},{' '} + {fmtDuration(p.delayCritical)} +
+
+
+
Governance
+
+ {p.governanceThreshold} owner{p.governanceThreshold === 1 ? '' : 's'} +
+
+
+
+
+

Delays and limits

+
+
+
New recipient waits
+
{fmtDuration(p.recipientActivationDelay)}
+
+
+
Weakening the policy waits
+
{fmtDuration(p.policyChangeDelay)}
+
+
+
Escalates above
+
+ {p.highExposureBps / 100}% held, critical at {p.criticalExposureBps / 100}% +
+
+
+
Refused above
+
+ {p.hardBlockExposureBps === 0 + ? 'no hard block' + : `${p.hardBlockExposureBps / 100}% of an asset`} +
+
+
+
Loss envelope
+
+ {p.envelopeBps === 0 + ? 'off' + : `${p.envelopeBps / 100}% per ${fmtDuration(p.envelopeWindow)}`} +
+
+
+
+
+ +
+
+
+

This vault

+ +
+ + Open on the explorer + +
+
+
+ ); +} diff --git a/app/src/components/People.tsx b/app/src/components/People.tsx new file mode 100644 index 0000000..ed2367c --- /dev/null +++ b/app/src/components/People.tsx @@ -0,0 +1,351 @@ +'use client'; + +import { useCurrentAccount } from '@mysten/dapp-kit-react'; +import { useState } from 'react'; +import { + ROLE_LABELS, + Role, + Trust, + describeRoles, + fmtTimestamp, + hasRole, + rolesValid, + tx as build, + type VaultView, +} from '@skur/sdk'; +import { PACKAGE_ID } from '@/config'; +import type { TxState } from '@/hooks/useVault'; +import { Addr, Countdown, Empty, Notice, Roles, TrustPill } from './ui'; + +/** Signers and recipients: who can act, and who can be paid. */ +export function People({ view, tx }: { view: VaultView; tx: TxState }) { + const account = useCurrentAccount(); + const me = account?.address; + const roles = me ? (view.members.find((m) => m.address === me)?.roles ?? 0) : 0; + const isOwner = hasRole(roles, Role.OWNER); + + const [newMember, setNewMember] = useState(''); + const [newRoles, setNewRoles] = useState(Role.APPROVER); + const [newRecipient, setNewRecipient] = useState(''); + const [label, setLabel] = useState(''); + const [lost, setLost] = useState(''); + const [replacement, setReplacement] = useState(''); + + const rolesOk = rolesValid(newRoles); + + return ( +
+ {tx.error ? {tx.error} : null} + +
+

Signers

+

+ A guardian holds no treasury role and no treasury member holds the guardian role. The + contract refuses any other arrangement. +

+ + + + + + + + + {view.members.map((m) => ( + + + + + + ))} + +
AddressRoles +
+ + {m.address === me ? you : null} + + + + +
+ +
+

Add a signer

+

+ Granting a role is treated as weakening the vault — a new key is a new way in — so it + waits out the policy-change delay and any guardian can veto it. +

+
+
+ + setNewMember(e.target.value.trim())} + spellCheck={false} + /> +
+
+ +
+ {[Role.OWNER, Role.APPROVER, Role.EXECUTOR, Role.GUARDIAN, Role.PROPOSER].map((r) => ( + + ))} +
+
+ +
+ {!rolesOk ? ( +

+ {newRoles === 0 + ? 'Pick at least one role.' + : 'A guardian cannot also hold a treasury role. The contract would refuse this.'} +

+ ) : ( +

+ {describeRoles(newRoles).join(', ') || '—'} +

+ )} +
+ +
+

Replace a lost key

+

+ A guardian opens this. It waits {Math.round(view.vault.policy.recoveryDelay / 3_600_000)}{' '} + hours, any owner can cancel it, the roles move across unchanged, and the vault comes out + of it in Elevated. +

+
+
+ + setLost(e.target.value.trim())} /> +
+
+ + setReplacement(e.target.value.trim())} + /> +
+ +
+
+
+ +
+

Recipients

+

+ An address the vault has never paid is escalated every time. Registering one starts it at + the restrictive default and makes it serve an activation delay. +

+ {view.recipients.length === 0 ? ( + No recipients registered. Payments can still be made; they are just escalated. + ) : ( + + + + + + + + + + + + {view.recipients.map((r) => ( + + + + + + + + + ))} + +
AddressLabelStandingPaidActivates +
+ + {r.label || '—'} + + {r.paidCount} + {r.activatesAt > Date.now() ? ( + + ) : ( + active + )} + + {r.trust !== Trust.BLOCKED ? ( + + ) : ( + + )} +
+ )} + +
+

Register a recipient

+
+
+ + setNewRecipient(e.target.value.trim())} + spellCheck={false} + /> +
+
+ + setLabel(e.target.value)} + maxLength={60} + /> +
+ +
+

+ Takes effect at once, because trust “new” with an activation delay is + stricter than unknown. Raising it further is a governance action with a timelock. +

+
+ +

+ Last registered {view.recipients.length > 0 ? fmtTimestamp(Math.max(...view.recipients.map((r) => r.registeredAt))) : '—'} +

+
+
+ ); +} diff --git a/app/src/components/PolicyPanel.tsx b/app/src/components/PolicyPanel.tsx new file mode 100644 index 0000000..898cf95 --- /dev/null +++ b/app/src/components/PolicyPanel.tsx @@ -0,0 +1,265 @@ +'use client'; + +import { useCurrentAccount } from '@mysten/dapp-kit-react'; +import { useMemo, useState } from 'react'; +import { + POLICY_GROUPS, + Role, + fmtDuration, + fromHours, + hasRole, + policyReductions, + runScenarios, + toHours, + tx as build, + validateCounts, + validatePolicy, + type Policy, + type VaultView, +} from '@skur/sdk'; +import { PACKAGE_ID } from '@/config'; +import type { TxState } from '@/hooks/useVault'; +import { Notice, coinDecimals, coinSymbol } from './ui'; + +/** + * The policy editor and the simulator. + * + * Editing is one screen with the consequences on it: what the change gives up, whether it is + * coherent at all, and what the attacks look like against the edited policy rather than the live + * one. Everything here runs the same algorithms as the Move modules, which is the only reason it + * is safe to show a number for a policy that is not deployed. + */ +export function PolicyPanel({ view, tx }: { view: VaultView; tx: TxState }) { + const account = useCurrentAccount(); + const roles = account ? (view.members.find((m) => m.address === account.address)?.roles ?? 0) : 0; + const [draft, setDraft] = useState(view.vault.policy); + + const errors = useMemo(() => validatePolicy(draft), [draft]); + const countErrors = useMemo( + () => + validateCounts( + draft, + view.vault.ownerCount, + view.vault.approverCount, + view.vault.executorCount, + view.vault.guardianCount, + ), + [draft, view.vault], + ); + const reductions = useMemo(() => policyReductions(view.vault.policy, draft), [view.vault.policy, draft]); + const changed = JSON.stringify(draft) !== JSON.stringify(view.vault.policy); + const valid = errors.length === 0 && countErrors.length === 0; + + const set = (key: keyof Policy, value: number | boolean) => + setDraft((d) => ({ ...d, [key]: value }) as Policy); + + const biggest = view.assets.reduce( + (best, a) => (a.balance > (best?.balance ?? -1n) ? a : best), + view.assets[0], + ); + + const scenarios = useMemo(() => { + if (!biggest) return []; + return runScenarios({ + policy: draft, + limits: biggest.limits, + balance: biggest.balance, + decimals: coinDecimals(biggest.coinType), + symbol: coinSymbol(biggest.coinType), + approverCount: view.vault.approverCount, + guardianCount: view.vault.guardianCount, + }); + }, [draft, biggest, view.vault]); + + return ( +
+ {tx.error ? {tx.error} : null} + +
+
+ {POLICY_GROUPS.map((group) => ( +
+

{group.title}

+

{group.sub}

+
+ {group.fields.map((f) => { + const raw = draft[f.key]; + if (f.unit === 'bool') { + return ( +
+ set(f.key, e.target.checked)} + /> + +
+ ); + } + const value = + f.unit === 'hours' + ? toHours(Number(raw)) + : f.unit === 'bps' + ? Number(raw) / 100 + : Number(raw); + return ( +
+ + { + const n = Number(e.target.value); + if (Number.isNaN(n) || n < 0) return; + set( + f.key, + f.unit === 'hours' + ? fromHours(n) + : f.unit === 'bps' + ? Math.round(n * 100) + : Math.round(n), + ); + }} + /> +
+ ); + })} +
+
+ ))} +
+ +
+
+

What this change does

+ {!changed ? ( +

+ Nothing yet. Edit a field to see what it gives up. +

+ ) : ( +
+ {errors.length > 0 || countErrors.length > 0 ? ( + + The vault would not accept this policy. +
    + {[...errors, ...countErrors].map((e) => ( +
  • {e}
  • + ))} +
+
+ ) : reductions.length === 0 ? ( + + This change only tightens the vault, so it takes effect as soon as{' '} + {view.vault.policy.governanceThreshold} owner + {view.vault.policy.governanceThreshold === 1 ? '' : 's'} approve it. + + ) : ( + + This gives up {reductions.length} control{reductions.length === 1 ? '' : 's'}. +
    + {reductions.map((r) => ( +
  • {r}
  • + ))} +
+

+ It will wait {fmtDuration(view.vault.policy.policyChangeDelay)} and any + guardian can veto it. It cannot be opened at all while the vault is in + Lockdown. +

+
+ )} +
+ )} + +
+ + +
+
+ +
+

The attacks, against this policy

+

+ Run with the edited policy and this vault's largest holding. The verdicts are the + vault's own rules, not a score. +

+ {scenarios.length === 0 ? ( +
Fund an asset to run the scenarios.
+ ) : ( +
+ {scenarios.map((s) => ( +
+
+ {s.title} + + +
+

+ {s.narrative} +

+
    + {s.detail.map((d) => ( +
  • {d}
  • + ))} +
+
+ ))} +
+ )} +
+
+
+
+ ); +} + +function Verdict({ verdict }: { verdict: string }) { + const cls = + verdict === 'blocked' || verdict === 'impossible' + ? 'low' + : verdict === 'delayed' || verdict === 'escalated' + ? 'high' + : 'critical'; + const label = + verdict === 'blocked' + ? 'stopped' + : verdict === 'impossible' + ? 'impossible' + : verdict === 'delayed' + ? 'delayed' + : verdict === 'escalated' + ? 'escalated' + : 'would succeed'; + return {label}; +} diff --git a/app/src/components/PublishPackage.tsx b/app/src/components/PublishPackage.tsx new file mode 100644 index 0000000..593564c --- /dev/null +++ b/app/src/components/PublishPackage.tsx @@ -0,0 +1,115 @@ +'use client'; + +import { useCurrentAccount } from '@mysten/dapp-kit-react'; +import { Transaction } from '@mysten/sui/transactions'; +import { useState } from 'react'; +import { dAppKit } from '@/dapp-kit'; +import { NETWORK } from '@/config'; +import { Notice } from '@/components/ui'; + +/** + * Publish the Move package from the browser, signed by the connected wallet. + * + * Publishing is an ordinary transaction: the compiled modules ride in the payload and the sender + * pays the storage. The bytecode is produced by `sui move build --dump-bytecode-as-base64` and + * served next to the app, so getting a network live needs nothing but a funded wallet — no CLI, + * no faucet gymnastics on the machine that runs the web app. The UpgradeCap lands in the + * publisher's wallet: whoever publishes holds the upgrade rights, explicitly. + */ +type Dump = { modules: string[]; dependencies: string[] }; +type State = + | { phase: 'idle' } + | { phase: 'busy' } + | { phase: 'done'; digest: string; packageId: string | null } + | { phase: 'error'; message: string }; + +export function PublishPackage() { + const account = useCurrentAccount(); + const [state, setState] = useState({ phase: 'idle' }); + + const publish = async () => { + if (!account) return; + setState({ phase: 'busy' }); + try { + const res = await fetch('/skur-bytecode.json'); + if (!res.ok) throw new Error('the compiled package is not being served next to this app'); + const dump = (await res.json()) as Dump; + + const tx = new Transaction(); + const cap = tx.publish({ modules: dump.modules, dependencies: dump.dependencies }); + tx.transferObjects([cap], account.address); + + const result = await dAppKit.signAndExecuteTransaction({ transaction: tx }); + if ('FailedTransaction' in result && result.FailedTransaction) { + throw new Error( + result.FailedTransaction.status?.error?.message ?? 'the network refused the publish', + ); + } + const executed = (result as { Transaction: { digest: string; effects?: unknown } }).Transaction; + + // The package id, when the effects carry it; the chain is authoritative either way. + let packageId: string | null = null; + const effects = executed.effects as + | { changedObjects?: { objectId?: string; id?: string; idOperation?: string; outputOwner?: unknown; objectType?: string }[] } + | undefined; + for (const c of effects?.changedObjects ?? []) { + if (c.idOperation === 'Created' && (c.objectType === 'package' || c.outputOwner === undefined)) { + packageId = c.objectId ?? c.id ?? null; + break; + } + } + setState({ phase: 'done', digest: executed.digest, packageId }); + } catch (e) { + const message = e instanceof Error ? e.message : String(e); + setState({ + phase: 'error', + message: /reject|denied|dismiss/i.test(message) ? 'The wallet declined it. Nothing was published.' : message, + }); + } + }; + + if (state.phase === 'done') { + return ( + + Published to {NETWORK}. Digest {state.digest} + {state.packageId ? ( + <> + {' '}· package {state.packageId} + + ) : null} +

+ Set NEXT_PUBLIC_SKUR_PACKAGE_ID to the package id and + restart the app. The UpgradeCap is in your wallet. +

+
+ ); + } + + return ( +
+

No package on {NETWORK} yet

+

+ The Skur Move package has not been published to this network. Your connected wallet can do + it right here — publishing is a transaction like any other, and costs a fraction of a SUI + in storage. The upgrade rights land in your wallet. +

+ {!account ? ( +

Connect a wallet first.

+ ) : ( + + )} + {state.phase === 'error' ? ( +
+ {state.message} +
+ ) : null} +
+ ); +} diff --git a/app/src/components/Queue.tsx b/app/src/components/Queue.tsx new file mode 100644 index 0000000..b13ff94 --- /dev/null +++ b/app/src/components/Queue.tsx @@ -0,0 +1,360 @@ +'use client'; + +import { useCurrentAccount } from '@mysten/dapp-kit-react'; +import { + KIND_LABELS, + Kind, + Role, + Status, + Tier, + describeReasons, + fmtDuration, + fmtTimestamp, + hasRole, + policyReductions, + tx as build, + type Proposal, + type VaultView, +} from '@skur/sdk'; +import { PACKAGE_ID } from '@/config'; +import type { TxState } from '@/hooks/useVault'; +import { Addr, Amount, Countdown, Empty, Notice, StatusPill, TierPill } from './ui'; + +/** + * The queue, and the actions on it. + * + * Every button is gated on the role the contract requires, so a signer is never offered an action + * the vault would refuse. The requirement shown is the one pinned on the proposal; at execution + * the vault re-derives it and applies whichever is stricter, which is why a payment can need more + * than this says but never less. + */ +export function Queue({ view, tx }: { view: VaultView; tx: TxState }) { + const account = useCurrentAccount(); + const me = account?.address; + const roles = me ? (view.members.find((m) => m.address === me)?.roles ?? 0) : 0; + const pending = view.proposals.filter((p) => p.status === Status.PENDING); + const settled = view.proposals.filter((p) => p.status !== Status.PENDING); + + return ( +
+ {tx.error ? {tx.error} : null} +
+

Awaiting action

+

+ {pending.length === 0 + ? 'Nothing is waiting.' + : `${pending.length} proposal${pending.length === 1 ? '' : 's'} open.`} +

+ {pending.length === 0 ? ( + The queue is empty. + ) : ( +
+ {pending.map((p) => ( + + ))} +
+ )} +
+ +
+

History

+ {settled.length === 0 ? ( + Nothing has settled yet. + ) : ( + + + + + + + + + + + + + {settled.map((p) => ( + + + + + + + + + ))} + +
#WhatAmountToOutcomeWhen
{p.id.toString()}{KIND_LABELS[p.kind]} + {p.kind === Kind.TRANSFER && p.asset ? ( + + ) : ( + — + )} + + {p.kind === Kind.TRANSFER ? : —} + + + {fmtTimestamp(p.createdAt)}
+ )} +
+
+ ); +} + +function ProposalCard({ + p, + view, + roles, + me, + tx, +}: { + p: Proposal; + view: VaultView; + roles: number; + me: string | undefined; + tx: TxState; +}) { + const id = p.id; + const vaultId = view.vault.id; + const now = Date.now(); + const liveApprovals = p.approvals.filter((a) => + hasRole(view.members.find((m) => m.address === a)?.roles ?? 0, Role.APPROVER), + ).length; + const liveConfirmations = p.confirmations.filter((a) => + hasRole(view.members.find((m) => m.address === a)?.roles ?? 0, Role.GUARDIAN), + ).length; + + const isTransfer = p.kind === Kind.TRANSFER; + const voteRole = isTransfer ? Role.APPROVER : Role.OWNER; + const alreadyApproved = me ? p.approvals.includes(me) : false; + const alreadyConfirmed = me ? p.confirmations.includes(me) : false; + const alreadyRejected = me ? p.rejections.includes(me) : false; + const liveRejections = p.rejections.filter((a) => + hasRole(view.members.find((m) => m.address === a)?.roles ?? 0, voteRole), + ).length; + + const approvalsMet = liveApprovals >= p.reqApprovals; + const guardiansMet = liveConfirmations >= p.reqGuardians; + const timeMet = now >= p.executableAt; + const canExecute = approvalsMet && guardiansMet && timeMet; + + // A guardian can veto exactly what the contract lets them veto, and nothing else. + const vetoable = + p.reductionMask !== 0 || + (isTransfer && p.tier === Tier.CRITICAL) || + p.kind === Kind.RECOVERY || + p.kind === Kind.MODE_RELAX; + + const executeTx = () => { + if (isTransfer && p.asset) + return build.executeTransfer(PACKAGE_ID, { vaultId, coinType: p.asset, proposalId: id }); + if (p.kind === Kind.POLICY_UPDATE) return build.executePolicy(PACKAGE_ID, vaultId, id); + if (p.kind === Kind.MEMBER_SET) return build.executeMember(PACKAGE_ID, vaultId, id); + if (p.kind === Kind.RECIPIENT_TRUST) + return build.executeRecipientTrust(PACKAGE_ID, vaultId, id); + if (p.kind === Kind.MODE_RELAX) return build.executeModeRelax(PACKAGE_ID, vaultId, id); + if (p.kind === Kind.RECOVERY) return build.executeRecovery(PACKAGE_ID, vaultId, id); + if (p.kind === Kind.ASSET_LIMITS && p.asset) + return build.executeAssetLimits(PACKAGE_ID, { vaultId, coinType: p.asset, proposalId: id }); + return null; + }; + + // Executing a payment takes the executor role; settling a governance change takes any member, + // because its authorization is the owner approvals already recorded on it. + const mayExecute = isTransfer ? hasRole(roles, Role.EXECUTOR) : roles !== 0; + const reasons = describeReasons(p.reasons); + const reductions = p.newPolicy ? policyReductions(view.vault.policy, p.newPolicy) : []; + + return ( +
+
+ #{id.toString()} + {KIND_LABELS[p.kind]} + {isTransfer ? : null} + {p.reductionMask !== 0 ? weakens the vault : null} + + expires {fmtTimestamp(p.expiresAt)} +
+ + {isTransfer && p.asset ? ( +
+

+ +

+ to + + {p.exposureBps > 0 ? ( + {(p.exposureBps / 100).toFixed(2)}% of holdings + ) : null} +
+ ) : null} + {p.memo ?

{p.memo}

: null} + + {p.kind === Kind.MEMBER_SET ? ( +

+ {p.memberRoles === 0 ? 'Remove ' : 'Set roles for '} + + {p.memberRoles !== 0 ? ` to ${p.memberRoles}` : ''} +

+ ) : null} + {p.kind === Kind.RECOVERY ? ( +

+ Replace with , same roles. Any + owner can cancel this before it executes. +

+ ) : null} + + {reasons.length > 0 ? ( + <> +

+ Scored this way because: +

+
    + {reasons.map((r) => ( +
  • {r}
  • + ))} +
+ + ) : null} + {reductions.length > 0 ? ( + <> +

+ This change gives up: +

+
    + {reductions.map((r) => ( +
  • {r}
  • + ))} +
+ + ) : null} + +
+ + + {liveApprovals}/{p.reqApprovals} + {' '} + approvals + {liveApprovals < p.approvals.length ? ( + ({p.approvals.length - liveApprovals} no longer count) + ) : null} + + {p.reqGuardians > 0 ? ( + + + {liveConfirmations}/{p.reqGuardians} + {' '} + guardian + + ) : null} + + executable + + +
+ +
+ + + + + {p.reqGuardians > 0 || vetoable ? ( + + ) : null} + + {vetoable ? ( + + ) : null} + + + + + + + {now >= p.expiresAt ? ( + + ) : null} +
+ + {p.reductionMask !== 0 ? ( +

+ A weakening waits {fmtDuration(view.vault.policy.policyChangeDelay)} and any guardian can + stop it until then. +

+ ) : null} +
+ ); +} diff --git a/app/src/components/Security.tsx b/app/src/components/Security.tsx new file mode 100644 index 0000000..7d7d71f --- /dev/null +++ b/app/src/components/Security.tsx @@ -0,0 +1,228 @@ +'use client'; + +import { useCurrentAccount } from '@mysten/dapp-kit-react'; +import { useMemo, useState } from 'react'; +import { + Mode, + Role, + Trust, + computeMaxLoss, + fmtAmount, + fmtDuration, + hasRole, + postureItems, + tx as build, + type VaultView, +} from '@skur/sdk'; +import { PACKAGE_ID } from '@/config'; +import type { TxState } from '@/hooks/useVault'; +import { Notice, coinDecimals, coinSymbol } from './ui'; + +/** + * The security screen: what holds, what the worst case looks like, and the two controls a human + * reaches for in an incident. + */ +export function Security({ view, tx }: { view: VaultView; tx: TxState }) { + const account = useCurrentAccount(); + const roles = account ? (view.members.find((m) => m.address === account.address)?.roles ?? 0) : 0; + const posture = useMemo( + () => + postureItems({ + policy: view.vault.policy, + guardians: view.vault.guardianCount, + assets: view.assets.map((a) => ({ symbol: coinSymbol(a.coinType), limits: a.limits })), + }), + [view], + ); + + const canFreeze = hasRole(roles, Role.GUARDIAN) || hasRole(roles, Role.OWNER); + const [blockAddr, setBlockAddr] = useState(''); + + return ( +
+ {tx.error ? {tx.error} : null} + +
+
+

What holds

+

+ Confirmed controls and obvious gaps. This is not a score and does not claim to measure + absolute security. +

+
+ {posture.map((item) => ( +
+ + {item.text} +
+ ))} +
+
+ +
+

In an incident

+

+ Raising the posture is immediate and unilateral. Lowering it never is. +

+
+ +

+ Escalates every payment one tier and halves the per-payment and daily caps. +

+ + +

+ Stops all outgoing execution at once. Deposits keep working. Leaving needs{' '} + {view.vault.policy.governanceThreshold} owner + {view.vault.policy.governanceThreshold === 1 ? '' : 's'},{' '} + {view.vault.policy.guardianThreshold} guardian + {view.vault.policy.guardianThreshold === 1 ? '' : 's'} and{' '} + {fmtDuration(view.vault.policy.policyChangeDelay)}. +

+ + {view.vault.mode !== Mode.NORMAL ? ( + + ) : null} + +
+ +
+ setBlockAddr(e.target.value.trim())} + spellCheck={false} + /> + +
+

+ The one unilateral power in the system, and it can only ever stop money. +

+
+
+
+
+ +
+

Maximum possible loss

+

+ Per asset, assuming every signer a tier needs is compromised and cooperating, and that no + guardian acts. It is an upper bound under those assumptions, never a guarantee. +

+ {view.assets.length === 0 ? ( +
No assets configured.
+ ) : ( + + + + + + + + + + + + + {view.assets.map((a) => { + const r = computeMaxLoss(view.vault.policy, a.limits, a.balance, view.vault.mode); + const d = coinDecimals(a.coinType); + return ( + + + + + + + + + ); + })} + +
AssetWith no delayBounded byWithin a dayBounded byLargest payments wait
+ {coinSymbol(a.coinType)} + {fmtAmount(r.immediate, d)}{r.immediateBinding}{fmtAmount(r.day, d)}{r.dayBinding}{fmtDuration(r.criticalDelay)}
+ )} +
+ + The assumptions behind these numbers + +
    + {computeMaxLoss( + view.vault.policy, + view.assets[0]?.limits ?? { approved: false, lowMax: 0n, highMax: 0n, perTxMax: 0n, dailyMax: 0n }, + view.assets[0]?.balance ?? 0n, + view.vault.mode, + ).assumptions.map((a) => ( +
  • {a}
  • + ))} +
  • + A 24-hour bucket anchors on its first payment, so up to two days' allowance can + leave across a boundary. The envelope and the per-payment cap bound that. +
  • +
+
+
+
+ ); +} diff --git a/app/src/components/Send.tsx b/app/src/components/Send.tsx new file mode 100644 index 0000000..c5f567e --- /dev/null +++ b/app/src/components/Send.tsx @@ -0,0 +1,340 @@ +'use client'; + +import { useCurrentAccount } from '@mysten/dapp-kit-react'; +import { useMemo, useState } from 'react'; +import { + Trust, + canPropose, + describeReasons, + fmtAmount, + fmtDuration, + parseAmount, + tx as build, + type VaultView, +} from '@skur/sdk'; +import { PACKAGE_ID } from '@/config'; +import { useTransferPreview, type TxState } from '@/hooks/useVault'; +import { Addr, Amount, Countdown, Notice, TierPill, TrustPill, coinDecimals, coinSymbol } from './ui'; + +/** + * Opening a payment. + * + * The review under the form is the vault's own classification, simulated live. The interface does + * not compute it, so it cannot promise something the vault would refuse — which is the whole point + * of `preview_transfer` being the same code `execute_transfer` runs. + */ +export function Send({ view, tx }: { view: VaultView; tx: TxState }) { + const account = useCurrentAccount(); + const me = account?.address; + const roles = me ? (view.members.find((m) => m.address === me)?.roles ?? 0) : 0; + const approved = view.assets.filter((a) => a.limits.approved); + const [coinType, setCoinType] = useState(approved[0]?.coinType ?? ''); + const [recipient, setRecipient] = useState(''); + const [amountText, setAmountText] = useState(''); + const [memo, setMemo] = useState(''); + + const asset = approved.find((a) => a.coinType === coinType) ?? approved[0]; + const decimals = coinType ? coinDecimals(coinType) : 9; + const amount = useMemo(() => parseAmount(amountText, decimals) ?? 0n, [amountText, decimals]); + const recipientValid = /^0x[0-9a-fA-F]{1,64}$/.test(recipient); + + const known = view.recipients.find((r) => r.address === recipient); + const trust = known?.trust ?? Trust.UNKNOWN; + const activatesAt = known?.activatesAt ?? 0; + + const preview = useTransferPreview({ + vaultId: view.vault.id, + coinType, + amount, + recipient, + sender: me, + enabled: Boolean(coinType) && recipientValid && amount > 0n, + }); + + // Local checks that mirror what `propose_transfer` would refuse, so the form says why before a + // wallet opens rather than after a transaction fails. + const problems: string[] = []; + if (asset && amount > 0n) { + if (trust === Trust.BLOCKED) problems.push('This recipient is blocked by the vault.'); + if (asset.limits.perTxMax !== 0n && amount > asset.limits.perTxMax) + problems.push( + `Above the per-payment cap of ${fmtAmount(asset.limits.perTxMax, decimals, coinSymbol(coinType))}.`, + ); + const spent = asset.velocity?.daySpent ?? 0n; + if (asset.limits.dailyMax !== 0n && spent + amount > asset.limits.dailyMax) + problems.push( + `Above what is left of today's allowance (${fmtAmount(asset.limits.dailyMax - spent, decimals, coinSymbol(coinType))}).`, + ); + const hb = view.vault.policy.hardBlockExposureBps; + if (hb !== 0 && asset.balance > 0n && (amount * 10_000n) / asset.balance >= BigInt(hb)) + problems.push(`At or above the hard block of ${hb / 100}% of this asset. No approval clears it.`); + if (amount > asset.balance) + problems.push(`The vault holds ${fmtAmount(asset.balance, decimals, coinSymbol(coinType))}.`); + } + + const mayPropose = canPropose(roles); + const ready = mayPropose && recipientValid && amount > 0n && problems.length === 0 && Boolean(coinType); + + const submit = () => { + tx.run( + build.proposeTransfer(PACKAGE_ID, { + vaultId: view.vault.id, + coinType, + amount, + recipient, + memo, + }), + 'propose', + ).then((digest) => { + if (digest) { + setAmountText(''); + setMemo(''); + } + }); + }; + + if (approved.length === 0) { + return ( + + No asset is approved for this vault yet. An owner has to approve one through governance + before anything can be paid. + + ); + } + + return ( +
+
+

Open a payment

+

+ Opening a payment is not approving it. The vault scores it first and then says what it + needs. +

+ +
+
+ + +
+ +
+ + setRecipient(e.target.value.trim())} + spellCheck={false} + /> + {recipient && !recipientValid ? ( +

+ That is not a Sui address. +

+ ) : null} + {recipientValid ? ( +
+ + {known?.label ? {known.label} : null} + {known && known.paidCount > 0 ? ( + + paid {known.paidCount} time{known.paidCount === 1 ? '' : 's'} before + + ) : ( + never paid by this vault + )} + {activatesAt > Date.now() ? ( + + activates + + ) : null} +
+ ) : null} +
+ +
+ + setAmountText(e.target.value)} + inputMode="decimal" + /> + {asset ? ( +

+ Routine up to {fmtAmount(asset.limits.lowMax, decimals)} · high risk up to{' '} + {fmtAmount(asset.limits.highMax, decimals)} · above that, critical +

+ ) : null} +
+ +
+ + setMemo(e.target.value)} + maxLength={120} + /> +
+
+ + {!mayPropose ? ( +
+ + The connected key cannot open payments in this vault. That takes an owner, approver, + executor or proposer role. + +
+ ) : null} + + {problems.length > 0 ? ( +
+ + The vault would refuse this. +
    + {problems.map((p) => ( +
  • {p}
  • + ))} +
+
+
+ ) : null} + {tx.error ? ( +
+ {tx.error} +
+ ) : null} + + +
+ +
+

What the vault will demand

+

+ Read from the vault itself, not computed here. If this says three approvals and a day, + that is what execution will require. +

+ + {!recipientValid || amount === 0n ? ( +
Fill in a recipient and an amount.
+ ) : preview.isLoading ? ( +
Asking the vault…
+ ) : preview.error ? ( +
+ + Could not reach the vault for a preview. The checks on the left still apply, and + execution is governed by the chain either way. + +
+ ) : preview.data ? ( + + ) : null} +
+
+ ); +} + +function PreviewBody({ + data, + amount, + coinType, + balance, + activatesAt, +}: { + data: { tier: number; reasons: number; exposureBps: number; reqApprovals: number; reqGuardians: number; delay: number }; + amount: bigint; + coinType: string; + balance: bigint; + activatesAt: number; +}) { + const decimals = coinDecimals(coinType); + const reasons = describeReasons(data.reasons); + const executableAt = Math.max(Date.now() + data.delay, activatesAt); + return ( +
+
+ + + {(data.exposureBps / 100).toFixed(2)}% of this asset + +
+ +
+
+
Leaves the vault
+
+ +
+
+
+
Remains after
+
{fmtAmount(balance > amount ? balance - amount : 0n, decimals, coinSymbol(coinType))}
+
+
+
Approvals needed
+
{data.reqApprovals}
+
+
+
Guardian confirmations
+
{data.reqGuardians === 0 ? 'none' : data.reqGuardians}
+
+
+
Waits
+
{data.delay === 0 ? 'no wait' : fmtDuration(data.delay)}
+
+
+
Earliest execution
+
+ +
+
+
+ + {reasons.length > 0 ? ( + <> +

+ Scored this way because: +

+
    + {reasons.map((r) => ( +
  • {r}
  • + ))} +
+ + ) : ( +

+ Nothing about this payment is unusual, so it stays at the routine tier. +

+ )} + + {activatesAt > Date.now() + data.delay ? ( +

+ The recipient's activation delay, not the tier, is what governs when this can + execute. +

+ ) : null} +
+ ); +} diff --git a/app/src/components/Shell.tsx b/app/src/components/Shell.tsx new file mode 100644 index 0000000..88eeeee --- /dev/null +++ b/app/src/components/Shell.tsx @@ -0,0 +1,53 @@ +'use client'; + +import { ConnectButton } from '@mysten/dapp-kit-react/ui'; +import { useCurrentAccount } from '@mysten/dapp-kit-react'; +import Link from 'next/link'; +import { NETWORK } from '@/config'; +import { ThemeToggle } from './ui'; + +export function Shell({ + children, + tabs, +}: { + children: React.ReactNode; + tabs?: { id: string; label: string; active: boolean; onClick: () => void }[]; +}) { + const account = useCurrentAccount(); + return ( + <> +
+ + {/* eslint-disable-next-line @next/next/no-img-element */} + + Skur + + {NETWORK} + {tabs ? ( + + ) : null} + + + +
+
+ {!account ? ( +
+

Connect a wallet to act

+

+ Reading a vault needs no wallet — the policy, the queue and the history are public. + Approving, executing or opening a payment needs the key that holds the role. +

+
+ ) : null} + {children} +
+ + ); +} diff --git a/app/src/components/ui.tsx b/app/src/components/ui.tsx new file mode 100644 index 0000000..6d1d814 --- /dev/null +++ b/app/src/components/ui.tsx @@ -0,0 +1,161 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { + MODE_LABELS, + Mode, + STATUS_LABELS, + Status, + TIER_LABELS, + Tier, + TRUST_LABELS, + Trust, + describeRoles, + fmtAmount, + short, +} from '@skur/sdk'; +import { EXPLORER } from '@/config'; + +export function TierPill({ tier }: { tier: Tier }) { + const cls = tier === Tier.CRITICAL ? 'critical' : tier === Tier.HIGH ? 'high' : 'low'; + return {TIER_LABELS[tier]}; +} + +export function ModePill({ mode }: { mode: Mode }) { + const cls = mode === Mode.LOCKDOWN ? 'critical' : mode === Mode.ELEVATED ? 'high' : 'low'; + return {MODE_LABELS[mode]}; +} + +export function StatusPill({ status }: { status: Status }) { + const cls = + status === Status.EXECUTED + ? 'low' + : status === Status.PENDING + ? '' + : status === Status.BLOCKED || status === Status.VETOED + ? 'critical' + : 'high'; + return {STATUS_LABELS[status]}; +} + +export function TrustPill({ trust }: { trust: Trust }) { + const cls = + trust === Trust.BLOCKED || trust === Trust.RESTRICTED + ? 'critical' + : trust === Trust.TRUSTED || trust === Trust.VERIFIED + ? 'low' + : 'high'; + return {TRUST_LABELS[trust]}; +} + +export function Roles({ bits }: { bits: number }) { + const names = describeRoles(bits); + return ( + + {names.length === 0 ? none : null} + {names.map((n) => ( + + {n} + + ))} + + ); +} + +export function Addr({ value, label }: { value: string; label?: string }) { + return ( + + {label ? `${label} · ` : ''} + {short(value, 6)} + + ); +} + +/** A coin type is long and mostly noise; the struct name is what a person recognises. */ +export function coinSymbol(coinType: string): string { + const name = coinType.split('::').pop() ?? coinType; + return name.toUpperCase(); +} + +/** Decimals are a property of the coin's metadata; these are the ones we can know without a read. */ +const KNOWN_DECIMALS: Record = { SUI: 9, USDC: 6, USDT: 6, USDSUI: 6 }; + +export function coinDecimals(coinType: string): number { + return KNOWN_DECIMALS[coinSymbol(coinType)] ?? 9; +} + +export function Amount({ value, coinType }: { value: bigint; coinType: string }) { + return {fmtAmount(value, coinDecimals(coinType), coinSymbol(coinType))}; +} + +export function Notice({ + kind = 'warn', + children, +}: { + kind?: 'warn' | 'bad' | 'ok' | 'plain'; + children: React.ReactNode; +}) { + return
{children}
; +} + +export function Empty({ children }: { children: React.ReactNode }) { + return
{children}
; +} + +/** Light, dark or whatever the system says, remembered per browser. */ +export function ThemeToggle() { + const [theme, setTheme] = useState<'system' | 'light' | 'dark'>('system'); + + useEffect(() => { + try { + const saved = localStorage.getItem('skur.theme'); + if (saved === 'light' || saved === 'dark' || saved === 'system') setTheme(saved); + } catch { + // Private windows and blocked site data throw here. The default is fine. + } + }, []); + + useEffect(() => { + const root = document.documentElement; + if (theme === 'system') root.removeAttribute('data-theme'); + else root.setAttribute('data-theme', theme); + try { + localStorage.setItem('skur.theme', theme); + } catch { + // Not being able to remember the choice is not a reason to fail to apply it. + } + }, [theme]); + + const next = theme === 'system' ? 'light' : theme === 'light' ? 'dark' : 'system'; + const glyph = theme === 'light' ? '☀' : theme === 'dark' ? '☾' : '◐'; + return ( + + ); +} + +/** A relative time that updates, so "executable in 4m" does not sit there being wrong. */ +export function Countdown({ to }: { to: number }) { + const [now, setNow] = useState(() => Date.now()); + useEffect(() => { + const id = setInterval(() => setNow(Date.now()), 1_000); + return () => clearInterval(id); + }, []); + const left = to - now; + if (left <= 0) return now; + const s = Math.floor(left / 1000); + const d = Math.floor(s / 86400); + const h = Math.floor((s % 86400) / 3600); + const m = Math.floor((s % 3600) / 60); + const sec = s % 60; + const text = d > 0 ? `${d}d ${h}h` : h > 0 ? `${h}h ${m}m` : `${m}m ${sec}s`; + return in {text}; +} diff --git a/app/src/config/deployments.json b/app/src/config/deployments.json new file mode 100644 index 0000000..d7d8dda --- /dev/null +++ b/app/src/config/deployments.json @@ -0,0 +1,17 @@ +{ + "testnet": { + "network": "testnet", + "packageId": "0x0", + "vaults": [] + }, + "devnet": { + "network": "devnet", + "packageId": "0x0", + "vaults": [] + }, + "localnet": { + "network": "localnet", + "packageId": "0x0", + "vaults": [] + } +} diff --git a/app/src/config/index.ts b/app/src/config/index.ts new file mode 100644 index 0000000..785875d --- /dev/null +++ b/app/src/config/index.ts @@ -0,0 +1,25 @@ +import type { Network } from '@skur/sdk'; +import deployments from './deployments.json'; + +/** + * Where the package lives. Published addresses come from `sui/deployments/.json`, copied + * in by `npm run sync-deployments`, and an environment variable overrides them so a developer can + * point at their own publish without editing a committed file. + */ +export const NETWORK = (process.env.NEXT_PUBLIC_SKUR_NETWORK ?? 'testnet') as Network; + +type Deployment = { network: string; packageId: string; vaults: { id: string; name: string }[] }; + +const committed = (deployments as Record)[NETWORK]; + +export const PACKAGE_ID = process.env.NEXT_PUBLIC_SKUR_PACKAGE_ID ?? committed?.packageId ?? '0x0'; + +export const KNOWN_VAULTS = committed?.vaults ?? []; + +/** The indexer. Optional: every screen works without it, just with fewer round trips saved. */ +export const API_URL = process.env.NEXT_PUBLIC_SKUR_API ?? ''; + +export const EXPLORER = (kind: 'object' | 'tx', id: string) => + `https://suiscan.xyz/${NETWORK}/${kind === 'object' ? 'object' : 'tx'}/${id}`; + +export const CONFIGURED = PACKAGE_ID !== '0x0'; diff --git a/app/src/dapp-kit.ts b/app/src/dapp-kit.ts new file mode 100644 index 0000000..b75d019 --- /dev/null +++ b/app/src/dapp-kit.ts @@ -0,0 +1,28 @@ +import { createDAppKit } from '@mysten/dapp-kit-react'; +import { SuiGrpcClient } from '@mysten/sui/grpc'; +import { GRPC_ENDPOINTS } from '@skur/sdk'; +import { NETWORK } from './config'; + +/** + * One dApp Kit instance for the app. + * + * The client is `SuiGrpcClient`. This is the reason the app is on `@mysten/dapp-kit-react` 2.x and + * not `@mysten/dapp-kit` 1.x: the older package only speaks JSON-RPC, which Sui is in the middle + * of decommissioning. + */ +export const dAppKit = createDAppKit({ + networks: ['mainnet', 'testnet', 'devnet', 'localnet'] as const, + defaultNetwork: NETWORK, + createClient: (network) => + new SuiGrpcClient({ + network, + baseUrl: process.env.NEXT_PUBLIC_SKUR_GRPC_URL ?? GRPC_ENDPOINTS[network], + }), + autoConnect: true, +}); + +declare module '@mysten/dapp-kit-react' { + interface Register { + dAppKit: typeof dAppKit; + } +} diff --git a/app/src/hooks/useVault.ts b/app/src/hooks/useVault.ts new file mode 100644 index 0000000..58c4f54 --- /dev/null +++ b/app/src/hooks/useVault.ts @@ -0,0 +1,138 @@ +'use client'; + +import { useCurrentClient, useDAppKit } from '@mysten/dapp-kit-react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useCallback, useState } from 'react'; +import type { Transaction } from '@mysten/sui/transactions'; +import { + describeFailure, + fetchVaultView, + previewTransfer, + type TransferPreview, + type VaultView, +} from '@skur/sdk'; +import { NETWORK, PACKAGE_ID } from '@/config'; + +/** Everything one vault screen needs, in as few round trips as the transport allows. */ +export function useVaultView(vaultId: string | undefined) { + const client = useCurrentClient(); + return useQuery({ + queryKey: ['vault', NETWORK, vaultId], + enabled: Boolean(vaultId), + queryFn: () => fetchVaultView(client, vaultId!), + }); +} + +/** + * What the vault itself says this payment would need. + * + * This simulates the vault's own `preview_transfer` rather than running the TypeScript engine, so + * the number on the confirmation screen is the number the chain will demand. The engine is used + * only for a policy that is not deployed yet. + */ +export function useTransferPreview(input: { + vaultId: string; + coinType: string; + amount: bigint; + recipient: string; + sender: string | undefined; + enabled: boolean; +}) { + const client = useCurrentClient(); + return useQuery({ + queryKey: [ + 'preview', + NETWORK, + input.vaultId, + input.coinType, + input.amount.toString(), + input.recipient, + ], + enabled: input.enabled && Boolean(input.sender) && input.amount > 0n, + staleTime: 10_000, + refetchInterval: false, + queryFn: () => + previewTransfer(client, { + packageId: PACKAGE_ID, + vaultId: input.vaultId, + coinType: input.coinType, + amount: input.amount, + recipient: input.recipient, + sender: input.sender!, + }), + }); +} + +export type TxState = { + run: (tx: Transaction, label: string) => Promise; + pending: string | null; + error: string | null; + lastDigest: string | null; + clear: () => void; +}; + +/** + * Sign, execute, wait, invalidate. + * + * A resolved promise means the network executed the transaction, which is not the same as the + * transaction having succeeded — a Move abort comes back as `FailedTransaction`. The abort code is + * translated into the sentence the contract means by it. + */ +export function useVaultTx(vaultId: string | undefined): TxState { + const dAppKit = useDAppKit(); + const client = useCurrentClient(); + const queryClient = useQueryClient(); + const [pending, setPending] = useState(null); + const [error, setError] = useState(null); + const [lastDigest, setLastDigest] = useState(null); + + const mutation = useMutation({ + mutationFn: async ({ tx }: { tx: Transaction; label: string }) => { + const result = await dAppKit.signAndExecuteTransaction({ transaction: tx }); + if (result.FailedTransaction) { + throw new Error( + describeFailure(result.FailedTransaction.status.error?.message ?? 'the vault refused it'), + ); + } + const digest = result.Transaction.digest; + await client.core.waitForTransaction({ digest }); + return digest; + }, + }); + + const run = useCallback( + async (tx: Transaction, label: string) => { + setError(null); + setPending(label); + try { + const digest = await mutation.mutateAsync({ tx, label }); + setLastDigest(digest); + await queryClient.invalidateQueries({ queryKey: ['vault', NETWORK, vaultId] }); + await queryClient.invalidateQueries({ queryKey: ['preview', NETWORK, vaultId] }); + return digest; + } catch (e) { + setError(describeFailure(e)); + return null; + } finally { + setPending(null); + } + }, + [mutation, queryClient, vaultId], + ); + + return { run, pending, error, lastDigest, clear: () => setError(null) }; +} + +/** Vaults the connected address is a member of, via the indexer when one is configured. */ +export function useMyVaults(address: string | undefined, apiUrl: string) { + return useQuery<{ vault_id: string; name: string; roles: number; mode: number }[]>({ + queryKey: ['my-vaults', NETWORK, address], + enabled: Boolean(address && apiUrl), + queryFn: async () => { + const res = await fetch(`${apiUrl}/members/${address}/vaults`); + if (!res.ok) throw new Error(`the indexer returned ${res.status}`); + const body = (await res.json()) as { vaults: { vault_id: string; name: string; roles: number; mode: number }[] }; + return body.vaults; + }, + }); +} diff --git a/app/src/marketing/Mockup.tsx b/app/src/marketing/Mockup.tsx new file mode 100644 index 0000000..4b0646e --- /dev/null +++ b/app/src/marketing/Mockup.tsx @@ -0,0 +1,122 @@ +'use client'; + +/** Static miniature of the app used as artwork on the landing page, the way safe.global shows its dashboard. */ +export function AppMockup({ variant = "overview" }: { variant?: "overview" | "queue" | "policy" }) { + return ( +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} +
Home
+
+ New transaction
+ {["Overview", "Queue", "Pay", "Policy", "People", "Security"].map((n, i) => ( +
{n}
+ ))} +
Security
+ {["Templates", "Simulator", "Max loss"].map((n) => ( +
{n}
+ ))} +
+
+
+ + Main treasury
0xa91c…42dc
+ 2/3 · 250,000 USDC · Normal +
+ {variant === "overview" && ( + <> +
+
Total balance
+
250,000.00 USDC
+ ↗ Send⌗ Receive +
Top assets
+
$ USDC250,000
+
S SUI1,240
+
+
+
Pending transactions
+
Pay 12,000 USDC1 out of 2
+
Pay 900 USDC✓ 1 out of 1
+
Update policy⏱ 1d timelock
+
+ + )} + {variant === "queue" && ( +
+
Queue
+
↗ Pay 12,000 USDC to 0x4F…a1c2High risk · 1 out of 2
+
↗ Pay 60,000 USDC to 0x9bd2…77e0Critical · guardian · 1d
+
⚙ Lower approvals to 1Weakens the vault · vetoable
+
🤖 Agent: pay 2,400 USDC (invoice 1182)Routine · needs 1 human
+
+ )} + {variant === "policy" && ( +
+
Policy simulator
+
One finance signer compromisedBlocked
+
Drain split into twenty transfersBlocked
+
Attacker weakens policy, then withdrawsDelayed 1d
+
Guardian key compromisedNot possible
+
+ )} +
+
+ ); +} + + +/** + * The phone, drawn in CSS: the redesigned mobile app as the landing page's second piece of + * artwork. "home" is the dashboard with the donut; "security" is the guardian's brake. + */ +export function PhoneMockup({ variant = "home" }: { variant?: "home" | "security" }) { + return ( +
+
+
9:41●●
+ {variant === "home" ? ( + <> +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + + Lenzypay Treasury +
+
+
TOTAL TREASURY VALUE Normal
+
800SUI total
+
+
+
0Pending approvals
+
3Active members
+
+
+
RECENT ACTIVITY
+
5 SUI · Contractor invoiceExecuted
+
Policy change1 of 2
+
+ + ) : ( + <> +
Security Center
+
+
🛡
+ Your assets are protected +
+
6 of 7 controls hold
+
+
⚠ Raise to Elevated
+
🔒 Freeze the vault
+
+
Veto a critical payment✓
+
Never touches a coin✓
+
+ + )} +
+ {["Home", "Treasury", "Proposals", "Team", "More"].map((t, i) => ( + {t} + ))} +
+
+
+ ); +} diff --git a/web/src/components/Reveal.tsx b/app/src/marketing/Reveal.tsx similarity index 93% rename from web/src/components/Reveal.tsx rename to app/src/marketing/Reveal.tsx index 6acdadc..8dddcca 100644 --- a/web/src/components/Reveal.tsx +++ b/app/src/marketing/Reveal.tsx @@ -1,5 +1,7 @@ +'use client'; + import type { ReactNode } from "react"; -import { useReveal } from "../lib/motion"; +import { useReveal } from "./motion"; /** Scroll-reveal wrapper. `stagger` animates each direct child in sequence. */ export function Reveal({ children, stagger = false, className = "", as: Tag = "div" }: { children: ReactNode; stagger?: boolean; className?: string; as?: "div" | "section" | "ul" }) { diff --git a/app/src/marketing/Shell.tsx b/app/src/marketing/Shell.tsx new file mode 100644 index 0000000..2a18830 --- /dev/null +++ b/app/src/marketing/Shell.tsx @@ -0,0 +1,187 @@ +'use client'; + +import Link from 'next/link'; +import type { ReactNode } from 'react'; +import { NETWORK } from '@/config'; +import { IconChevronRight } from './icons'; +import { useScrolled } from './motion'; + +export type MarketingPage = 'landing' | 'product' | 'solutions' | 'security' | 'docs'; + +const REPO = 'https://github.com/Emmyhack/skur'; + +/** Nav, closing call to action and footer, shared by every marketing page. */ +export function MarketingShell({ + page, + children, + hideCta = false, +}: { + page: MarketingPage; + children: ReactNode; + hideCta?: boolean; +}) { + const scrolled = useScrolled(); + return ( +
+
+
+ + {/* eslint-disable-next-line @next/next/no-img-element */} + + Skur + + +
+ Sui · {NETWORK} + + Launch app + +
+
+
+ {children} + {!hideCta && ( +
+
+
+ Start here +
+

+ Put an authorization layer +
+ in front of the treasury +

+

+ Self-custodial, enforced in Move, and readable by the people who sign. +

+
+ + Read the docs + + + Launch app + +
+
+
+ )} +
+
+
+
+
+ + Skur + +

+ A programmable authorization and treasury security layer for Sui organizations. +

+
+
+
Product
+
    +
  • Risk-tiered authorization
  • +
  • Guardians
  • +
  • Policy simulator
  • +
  • Agent proposers
  • +
  • Move package
  • +
+
+
+
Solutions
+
    +
  • Finance teams
  • +
  • Guardians
  • +
  • Protocols and funds
  • +
+
+
+
Project
+ +
+
+
Resources
+ +
+
+
+ Skur V1 · Sui {NETWORK} + + MIT licensed. Skur limits and delays damage; it is not a guarantee against loss, and + it has not been audited. + +
+
Skur
+
+
+
+ ); +} + +/** The hero used by the product, solutions and security pages. */ +export function SubHero({ + eyebrow, + title, + sub, + children, +}: { + eyebrow: string; + title: ReactNode; + sub: string; + children?: ReactNode; +}) { + return ( + <> +
+
+ {[ + [0, 130], + [1310, 0], + [1180, 260], + [130, 390], + [910, 390], + ].map(([x, y], i) => ( + + ))} +
+
{eyebrow}
+

{title}

+

{sub}

+
+ + Launch app + + {children} +
+
+
+
+ + ); +} diff --git a/web/src/components/Ticker.tsx b/app/src/marketing/Ticker.tsx similarity index 79% rename from web/src/components/Ticker.tsx rename to app/src/marketing/Ticker.tsx index b2ee227..5c40ff1 100644 --- a/web/src/components/Ticker.tsx +++ b/app/src/marketing/Ticker.tsx @@ -1,19 +1,30 @@ +'use client'; + import { useEffect, useState } from "react"; type Sample = { amt: string; to: string; tier: "low" | "high" | "critical" | "blocked"; label: string; why: string[] }; const SAMPLES: Sample[] = [ - { amt: "1,000 sUSD", to: "Design retainer · verified", tier: "low", label: "LOW · 1 approval · executes now", why: ["Below the routine threshold", "Recipient verified, paid 14 times before", "Uses 3% of today's cap"] }, - { amt: "12,000 sUSD", to: "0x7c4f…41e2 · never paid", tier: "high", label: "HIGH · 2 approvals · waits 24h", why: ["Above the routine threshold", "This vault has never paid the address", "Activation delay applies; any guardian can veto"] }, - { amt: "60,000 sUSD", to: "Exchange deposit · verified", tier: "critical", label: "CRITICAL · 2 + guardian · 24h", why: ["24% of holdings in one payment", "Above the high threshold", "Needs a guardian; any guardian can veto"] }, - { amt: "9,900 sUSD × 20", to: "Split across one day", tier: "blocked", label: "BLOCKED · circuit breaker", why: ["Outflow is counted cumulatively", "Loss envelope crossed on payment 11", "Vault enters Lockdown instead of paying"] }, + { amt: "1,000 USDC", to: "Design retainer · verified", tier: "low", label: "LOW · 1 approval · executes now", why: ["Below the routine threshold", "Recipient verified, paid 14 times before", "Uses 3% of today's cap"] }, + { amt: "12,000 USDC", to: "0x7c4f…41e2 · never paid", tier: "high", label: "HIGH · 2 approvals · waits 24h", why: ["Above the routine threshold", "This vault has never paid the address", "Activation delay applies; any guardian can veto"] }, + { amt: "60,000 USDC", to: "Exchange deposit · verified", tier: "critical", label: "CRITICAL · 2 + guardian · 24h", why: ["24% of holdings in one payment", "Above the high threshold", "Needs a guardian; any guardian can veto"] }, + { amt: "9,900 USDC × 20", to: "Split across one day", tier: "blocked", label: "BLOCKED · circuit breaker", why: ["Outflow is counted cumulatively", "Loss envelope crossed on payment 11", "Vault enters Lockdown instead of paying"] }, ]; /** Live scorer for the hero: cycles through sample payments and shows how the vault would tier each one. */ export function ScoreTicker() { - const reduced = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches; + const [reduced, setReduced] = useState(false); const [i, setI] = useState(0); - const [phase, setPhase] = useState<"scoring" | "done">(reduced ? "done" : "scoring"); + const [phase, setPhase] = useState<"scoring" | "done">("scoring"); + useEffect(() => { + // Read the preference after mount: reading it during render makes the server pass and the + // hydration disagree. + const m = window.matchMedia?.("(prefers-reduced-motion: reduce)"); + if (m?.matches) { + setReduced(true); + setPhase("done"); + } + }, []); useEffect(() => { if (reduced) return; const t = setTimeout(() => setPhase("done"), phase === "scoring" ? 1100 : 3400); diff --git a/app/src/marketing/docSlugs.ts b/app/src/marketing/docSlugs.ts new file mode 100644 index 0000000..90b3be0 --- /dev/null +++ b/app/src/marketing/docSlugs.ts @@ -0,0 +1,27 @@ +/** + * The documentation's table of contents, in a module with no `'use client'`. + * + * `generateStaticParams` runs on the server, and a server component importing from a client module + * gets a client reference proxy rather than the real value — so the list of slugs has to live + * somewhere both sides can read it. + */ +export const DOC_SLUGS = [ + 'introduction', + 'quick-start', + 'roles', + 'risk-tiers', + 'policy', + 'recipients', + 'limits', + 'modes', + 'changes', + 'recovery', + 'agents', + 'move-package', + 'invariants', + 'limitations', +] as const; + +export type DocSlug = (typeof DOC_SLUGS)[number]; + +export const DOC_GROUPS = ['Start', 'Model', 'Reference', 'Assurance'] as const; diff --git a/app/src/marketing/docs.tsx b/app/src/marketing/docs.tsx new file mode 100644 index 0000000..2978747 --- /dev/null +++ b/app/src/marketing/docs.tsx @@ -0,0 +1,768 @@ +'use client'; + +import Link from 'next/link'; +import type { ReactNode } from 'react'; +import { POLICY_GROUPS, TEMPLATES, fmtDuration } from '@skur/sdk'; +import { CONFIGURED, NETWORK, PACKAGE_ID } from '@/config'; +import { MarketingShell } from './Shell'; +import { DOC_GROUPS, type DocSlug } from './docSlugs'; + +type Doc = { slug: DocSlug; title: string; group: string; body: () => ReactNode }; + +const H = ({ children }: { children: ReactNode }) => ( +

{children}

+); +const Note = ({ children }: { children: ReactNode }) =>
{children}
; + +function Table({ head, rows }: { head: string[]; rows: ReactNode[][] }) { + return ( +
+ + + + {head.map((h) => ( + + ))} + + + + {rows.map((r, i) => ( + + {r.map((c, j) => ( + + ))} + + ))} + +
{h}
{c}
+
+ ); +} + +const REPO = 'https://github.com/Emmyhack/skur'; + +const DOCS: Doc[] = [ + { + slug: 'introduction', + title: 'Introduction', + group: 'Start', + body: () => ( + <> +

+ Skur is a programmable authorization and treasury security layer for Sui organizations: a + self-custodial vault whose requirements rise and fall with the risk of each payment. +

+

+ A multisig asks one question — did enough keys sign? Skur asks a second one: should this + payment settle right now? Between a valid approval and the money sit amount tiers, + recipient standing, velocity caps, a loss envelope, guardian veto and a firewall around + policy changes. Every one is enforced by the Move code in a shared object, not by a + server. +

+ Why this is not a multisig +

+ Sui has signature aggregation in the protocol: a multisig address can hold up to ten keys + with weights and mixed schemes, for free. That is not the part that is missing. What is + missing is anything that depends on what the transaction does, who receives + it, or what has already happened today — and that is the whole of Skur. +

+ What Skur does not claim +

+ Skur does not make signer compromise harmless. It limits and delays the damage when + authorized credentials are misused. A stolen key alone cannot reach a tier’s threshold + unless you set that tier to one, and even then it is bounded by the per-payment cap, the + day’s remaining allowance and the loss envelope. +

+ + Skur has not been audited and is not deployed to mainnet. Do not put real value behind it + until an independent audit is complete. + + + ), + }, + { + slug: 'quick-start', + title: 'Quick start', + group: 'Start', + body: () => ( + <> +

Reading a vault needs no wallet. Acting on one needs the key that holds the role.

+ Open a vault +

+ A vault is a shared object, so anyone can read it: the balances, the queue, the policy and + the security posture all come from the chain. Paste a vault id into the app, or connect a + wallet to see the vaults you hold a role in. +

+ Create your own +
    +
  1. Connect a wallet on Sui {NETWORK}.
  2. +
  3. + Pick the template closest to your organization. Every value can be changed later through + governance — tightening takes effect at once, loosening waits. +
  4. +
  5. + List the signers and their roles. Owners govern, approvers confirm payments, executors + execute, proposers can only ask, guardians hold the brake. +
  6. +
  7. + Sign once. One transaction creates the vault, sets the policy, installs the roster and + approves the first asset — and if any part is invalid, nothing is created. +
  8. +
+ Open a payment +

+ Enter the recipient, the asset and the amount. Before the wallet opens, the vault itself + classifies the payment: the tier, the approvals it needs, whether a guardian must sign, + how long it waits, and every reason. That preview is a simulation of the same function + execution runs, so it cannot promise something the vault would refuse. +

+ Run it yourself +

+ The Move package, the SDK, the indexer and this interface are all in{' '} + + the repository + + . sui move test runs the security suite; each test is named after the attack + it defends against. +

+ + ), + }, + { + slug: 'roles', + title: 'Roles', + group: 'Model', + body: () => ( + <> +

+ Five roles, held as a bitmask. A member can hold several treasury roles; a guardian can + hold none. +

+ + Why guardians are exclusive +

+ The contract refuses to give a guardian a treasury role, and refuses to give a treasury + member the guardian role. Every path that writes a role goes through the same check, so + there is no way to install an exception. That makes guardians a second control plane an + attacker has to breach separately — and one they cannot profit from, because there is no + withdrawal path from guardian authority. +

+ Approving, and rejecting +

+ A signer who thinks a proposal is wrong can reject it, and it takes as many rejections as + the proposal needed approvals. Rejecting is therefore exactly as hard as approving: one + signer cannot block the queue, and a bad proposal can be cleared the same day instead of + sitting there until it expires or an owner cancels it. +

+ Why approvals are recounted +

+ An approval is recorded against the address, not counted into a number. At execution the + set is recounted against the live roster, so a signer removed after approving stops + counting. A tally frozen at approval time would let a compromised key’s approval outlive + its removal. +

+ + ), + }, + { + slug: 'risk-tiers', + title: 'Risk tiers', + group: 'Model', + body: () => ( + <> +

+ Every payment is scored when it is opened. Five signals, each able to raise the tier and + none able to lower it. +

+
+ What a tier costs +
+ Pinned, then tighten-only +

+ The requirements are pinned to the proposal when it is opened. At execution the payment is + reclassified against live state and the stricter of the pinned and live + requirements applies. Tightening the policy affects payments already in flight; loosening + it cannot rescue one. +

+ + Exposure is computed per asset, as a share of that asset’s own balance. A vault holding + several coin types has several independent calculations and no portfolio-wide number — + producing one would need a price oracle, and a wrong portfolio number is worse than an + honest per-asset one. + + + ), + }, + { + slug: 'policy', + title: 'The policy', + group: 'Model', + body: () => ( + <> +

+ Seventeen fields. Durations are stored in milliseconds, because that is what{' '} + sui::clock reports. +

+ {POLICY_GROUPS.map((g) => ( +
+ {g.title} +

{g.sub}

+
[ + {f.key}, + f.unit === 'hours' ? 'hours' : f.unit === 'bps' ? 'percent' : f.unit === 'bool' ? 'on/off' : 'count', + `${f.label}${f.hint ? ` — ${f.hint}` : ''}`, + ])} + /> + + ))} + Coherence rules the vault enforces +
    +
  • Approvals must be monotonic: routine ≤ high risk ≤ critical.
  • +
  • The critical delay cannot be shorter than the high-risk delay.
  • +
  • The high exposure threshold cannot exceed the critical one, and neither can exceed 100%.
  • +
  • The hard block cannot sit below the critical exposure threshold.
  • +
  • The envelope window must be at least an hour when the envelope is on.
  • +
  • + The proposal lifetime must exceed the longest delay by at least an hour — otherwise a + payment could expire before its own timelock elapsed. +
  • +
  • + The roster must be able to satisfy the thresholds. A threshold nobody can reach is a + frozen vault, and the contract refuses to create or leave one. +
  • +
+ Starting points +
[ + {t.name}, + t.audience, + fmtDuration(t.policy.delayCritical), + fmtDuration(t.policy.recipientActivationDelay), + `${t.policy.envelopeBps / 100}% / ${fmtDuration(t.policy.envelopeWindow)}`, + ])} + /> + + ), + }, + { + slug: 'recipients', + title: 'Recipients', + group: 'Model', + body: () => ( + <> +

+ A recipient is a security object with a standing and a clock, not a string in a form. +

+
+ The activation delay +

+ Registering a recipient starts it at New with an activation delay. A payment to it + cannot execute before that moment — the tier’s delay and the activation delay run + concurrently and the later one governs. Re-checked at execution, so a payment approved + early still waits. +

+ Raising and lowering trust +

+ Registering is immediate, because New with a delay is stricter than never-paid. Raising + trust beyond that is a governance action that waits out the policy-change delay and can be + vetoed. Restricting or blocking is immediate on an owner threshold — and a guardian can + block a recipient alone, instantly. That is the one unilateral power in the system, and it + can only ever stop money. +

+ + Paying an address does not promote it. Only governance changes trust, so a payment to a + never-registered address is escalated every time, forever. This is intended, and + occasionally surprising. + + + ), + }, + { + slug: 'limits', + title: 'Limits and the breaker', + group: 'Reference', + body: () => ( + <> +

+ Per-asset caps bound a single payment. Cumulative accounting bounds a campaign of them. +

+
+ Why splitting does not work +

+ The day’s outflow is counted, not inferred. Crossing half the day’s allowance escalates + every further payment to at least high risk, so a drain split into twenty pieces meets a + higher bar partway through rather than at the end — and the daily cap refuses the piece + that would cross it. +

+ The envelope, and its basis +

+ The envelope is a share of the balance when the window opened, not of the live + balance. Measuring against the live balance would let a drain shrink its own denominator + and keep every later payment under the same percentage. The cost of this choice is that a + large deposit mid-window does not raise the window’s allowance until the window rolls. +

+ Why the breaker does not revert +

+ When a payment would cross the envelope, the vault marks the proposal refused, enters + Lockdown, emits an event — and the transaction succeeds. The coin is never moved. + Aborting would be the obvious thing and it would be wrong: the abort would roll back the + Lockdown along with everything else, leaving the vault open and the attacker free to try + again for the price of gas. +

+ + A 24-hour bucket anchors on its first charge rather than sliding, so up to two days’ + allowance can leave across a boundary. The envelope and the per-payment cap bound that, + and the maximum-loss view states it rather than assuming the best case. + + + ), + }, + { + slug: 'modes', + title: 'Security modes', + group: 'Reference', + body: () => ( + <> +

Three postures. Raising one is immediate. Lowering one never is.

+
+ Leaving a raised posture +

+ It takes owners, a guardian and time, in that order: an owner threshold of approvals, the + guardian threshold of confirmations, and the policy-change delay. Whoever raised the + posture cannot lower it alone, which is the point. +

+ What else raises it +
    +
  • A guardian veto moves a Normal vault to Elevated — reaching for the brake is itself a signal.
  • +
  • A completed recovery leaves the vault Elevated, because a recovery means something went wrong.
  • +
  • The circuit breaker latches straight to Lockdown.
  • +
+ + Deposits are never gated, in any mode, from any address. The controls exist to govern money + leaving; refusing money arriving would only strand it. + + + ), + }, + { + slug: 'changes', + title: 'Changing the policy', + group: 'Reference', + body: () => ( + <> +

+ An attacker with enough keys does not withdraw first. They weaken the policy, then + withdraw. So this is the path guarded hardest. +

+ What counts as weakening +

+ Every change is compared to the live policy field by field. The interesting cases are the + ones where a number going down is a loosening: +

+
    +
  • + Setting a threshold to zero disables it, which is the loosest possible move — so + moving to zero is a weakening and moving away from zero can only tighten. +
  • +
  • + A longer proposal lifetime keeps a stale approval set alive for longer, so it + weakens the vault even though the number went up. +
  • +
  • + Granting a role to an address that did not hold one is a new way in, so adding a + signer is treated as a weakening while removing one is not. +
  • +
  • + Lifting a restriction on a recipient weakens the vault; imposing one does not. +
  • +
+ What a weakening costs +
    +
  1. An owner threshold of approvals, as with any governance action.
  2. +
  3. The policy-change delay, which runs before it can take effect.
  4. +
  5. A veto window: any guardian can stop it outright until it executes.
  6. +
  7. And it cannot be opened at all while the vault is in Lockdown.
  8. +
+

+ A change that only tightens the vault skips all of that and takes effect as soon as owners + agree. The policy is also re-validated against the roster at execution, not only when + proposed, so a change that became incoherent in the meantime is refused. +

+ + ), + }, + { + slug: 'recovery', + title: 'Recovery', + group: 'Reference', + body: () => ( + <> +

A lost key should not cost the treasury, and a stolen guardian key should not gain it.

+ How it works +
    +
  1. A guardian opens a recovery naming the lost signer and the replacement.
  2. +
  3. The guardian threshold of confirmations is required.
  4. +
  5. The recovery delay runs. Any owner can cancel it during that time.
  6. +
  7. + On execution the roles move across unchanged — whatever the lost signer holds at that + moment, not what they held when it was proposed. +
  8. +
  9. The policy is untouched, and the vault comes out of it in Elevated.
  10. +
+ What it cannot do +
    +
  • It cannot change the policy, the limits or anyone else’s roles.
  • +
  • It cannot name an address that is already a member.
  • +
  • A guardian cannot nominate themselves as the replacement.
  • +
  • It cannot leave the roster unable to satisfy the policy.
  • +
+ + With a single guardian, one key both opens the recovery and confirms it. The owner cancel + is what bounds that, and the protocol, fund and larger templates ship with two guardians + for exactly this reason. + + + ), + }, + { + slug: 'agents', + title: 'Automated proposers', + group: 'Reference', + body: () => ( + <> +

+ An organization increasingly wants software to request payments. Nobody sensibly + wants software to authorize them. +

+

+ An invoice processor, a rebalancer, an agent reading a vendor inbox: each needs to put a + payment in front of people. Giving it a signing key in a multisig gives it a fraction of + the authority to move money, because to a threshold every key is the same kind of key. +

+ The proposer role +

+ A member holding only PROPOSER can open a proposal and do nothing else. It + cannot approve, cannot confirm, cannot execute, cannot govern, and cannot be a guardian. + Its approval would not count even if it submitted one — the vault checks the approver role + when counting, not merely membership. +

+

+ A fully compromised agent key therefore produces a queue of proposals that no human + approved. That is the difference between an automation incident and a loss. +

+ What an agent still faces +
    +
  • Everything it opens is scored like any other payment.
  • +
  • Paying a new address is escalated and waits out the activation delay.
  • +
  • Its payments count against the day’s allowance and the loss envelope like anyone’s.
  • +
  • An agent that starts behaving oddly trips the same breaker a human would.
  • +
+ + Give an automation its own key and its own role. Sharing a human’s key with a script is + the thing this role exists to make unnecessary. + + + ), + }, + { + slug: 'move-package', + title: 'The Move package', + group: 'Assurance', + body: () => ( + <> +

+ Four modules. The three pure ones hold the rules; the fourth holds the state. +

+
+ Reading order for a review +
    +
  1. + risk::classify — if this is wrong, every number the product shows is wrong. +
  2. +
  3. + policy::reductions — the firewall, and the cases where a smaller number is + a weakening. +
  4. +
  5. + vault::execute_transfer — the last gate, where everything is re-evaluated + against live state. +
  6. +
  7. + vault::begin and finish — creation as a hot potato, so a + half-configured vault is never reachable. +
  8. +
+ Conventions +
    +
  • Durations in milliseconds; amounts in the coin’s smallest unit.
  • +
  • Any intermediate that could overflow is computed in 128-bit space.
  • +
  • + Validation returns a code and a separate function aborts with it, so an interface can + dry-run a draft policy and show the precise reason. +
  • +
  • Zero means “no limit” for every cap and threshold. This is load-bearing.
  • +
+ {CONFIGURED ? ( + <> + This deployment +
+ {PACKAGE_ID} + , + ], + ]} + /> + + ) : ( + + No package is published for {NETWORK} yet. Publish with{' '} + sui/scripts/publish.sh; it builds, runs the tests, and refuses to publish a + package whose tests fail. + + )} + + ), + }, + { + slug: 'invariants', + title: 'Invariants', + group: 'Assurance', + body: () => ( + <> +

+ Sixteen properties, each enforced by named code and proven by a test named after the + attack it defends against. +

+
    +
  1. Nothing executes before every active condition is met.
  2. +
  3. A guardian cannot move treasury assets through guardian authority.
  4. +
  5. Lockdown stops outgoing execution and new payments.
  6. +
  7. A security-reducing change never takes effect immediately.
  8. +
  9. Cumulative outflow cannot be bypassed by splitting.
  10. +
  11. A proposal cannot execute twice.
  12. +
  13. A removed signer’s approval stops counting.
  14. +
  15. An approval cannot be duplicated or replayed.
  16. +
  17. A new recipient cannot skip its activation delay.
  18. +
  19. Recovery cannot quietly weaken the vault.
  20. +
  21. Balance accounting reconciles.
  22. +
  23. The roster always satisfies the policy.
  24. +
  25. The breaker’s latch survives the attempt.
  26. +
  27. Leaving a raised posture is never unilateral.
  28. +
  29. An automation key cannot authorize itself.
  30. +
  31. Rejecting a proposal takes as many signers as approving it.
  32. +
+

+ The full mapping — invariant, the code that enforces it, the test that proves it — is in{' '} + + the security model + + . +

+ What is absent by construction +

+ Reentrancy is not defended against; it cannot happen. Move has no dynamic dispatch into a + caller, and the only outgoing value movement is a coin split after the proposal’s status + has already been written. The ordering is kept anyway, because it costs nothing and + survives a refactor. +

+ + ), + }, + { + slug: 'limitations', + title: 'Limitations', + group: 'Assurance', + body: () => ( + <> +

+ Design positions, not oversights. State these to anyone evaluating the system. +

+
    +
  • + No independent audit. Do not put real value behind Skur until one is complete. +
  • +
  • + Anchored windows, not sliding ones. A day bucket anchors on its first charge, so + up to two days’ allowance can leave across a boundary. +
  • +
  • + Exposure is per asset. No portfolio-wide number without a price oracle, and a + wrong portfolio number would be worse than an honest per-asset one. +
  • +
  • + The breaker latches; it does not refund. Everything that executed before the trip + stays paid. What it buys is that the trip happens instead of the payment that would have + crossed the line. +
  • +
  • + The envelope basis is the balance when the window opened. A large deposit + mid-window does not raise that window’s allowance. +
  • +
  • + Single-guardian vaults. One key both freezes and, with owners, unfreezes. The + larger templates ship with two. +
  • +
  • + A shared object serializes. Every write to a vault goes through consensus on that + object, so one vault has a throughput ceiling. For a treasury doing thousands of + payments an hour the answer is several vaults, not a pretence otherwise. +
  • +
  • + No arbitrary contract calls in V1. Only coin transfers exist, so “unlimited + approval” and “malicious contract” are excluded by construction rather than analysed — + which is also why decoding arbitrary calls is on the roadmap rather than claimed. +
  • +
  • + Paying an address does not promote it. Only governance changes trust, so payments + to a never-registered address are escalated every time. +
  • +
+ + ), + }, +]; + +const GROUPS = DOC_GROUPS; + +export function DocsView({ slug }: { slug: DocSlug }) { + const doc = DOCS.find((d) => d.slug === slug) ?? DOCS[0]; + const i = DOCS.indexOf(doc); + const prev = i > 0 ? DOCS[i - 1] : null; + const next = i < DOCS.length - 1 ? DOCS[i + 1] : null; + return ( + +
+ +
+
Docs / {doc.group}
+

{doc.title}

+ {doc.body()} + +
+
+
+ ); +} diff --git a/web/src/components/icons.tsx b/app/src/marketing/icons.tsx similarity index 99% rename from web/src/components/icons.tsx rename to app/src/marketing/icons.tsx index 94cebba..76018f8 100644 --- a/web/src/components/icons.tsx +++ b/app/src/marketing/icons.tsx @@ -1,3 +1,5 @@ +'use client'; + /** Small inline icon set (24px grid, 1.75 stroke), matching the line-icon style of the Safe interface. */ import type { SVGProps } from "react"; diff --git a/web/src/lib/motion.ts b/app/src/marketing/motion.ts similarity index 99% rename from web/src/lib/motion.ts rename to app/src/marketing/motion.ts index f1138c7..5740bd7 100644 --- a/web/src/lib/motion.ts +++ b/app/src/marketing/motion.ts @@ -1,3 +1,5 @@ +'use client'; + import { useEffect, useRef, useState } from "react"; /** Adds the `in` class when the element scrolls into view, once. Pairs with .reveal / .reveal-stagger. */ diff --git a/app/src/marketing/styles.css b/app/src/marketing/styles.css new file mode 100644 index 0000000..3bc25a3 --- /dev/null +++ b/app/src/marketing/styles.css @@ -0,0 +1,884 @@ +/* Skur. Identity: charcoal #212529 with #FFD000 used the way hazard tape and a highlighter are used: sparingly, to mark what matters. + Space Grotesk for display, DM Sans for text, DM Mono for numbers and labels. Sharp 4-8px radii, hairlines, numbered sections. + Light: charcoal primary, yellow accent on white. Dark: #212529 canvas, #2b3035 panels, yellow primary with charcoal text. + Motion: entrance reveals, staggered lists, live scorer, tape that runs in Lockdown, reduced-motion aware. */ +@import url("https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700;800&family=DM+Mono:wght@400;500&family=Space+Grotesk:wght@500;600;700&display=swap"); + +:root { + --text: #212529; + --text-2: #6c757d; + --text-3: #adb5bd; + --canvas: #f1f3f5; + --panel: #ffffff; + --panel-2: #e9ecef; + --panel-3: #f8f9fa; + --border: #dee2e6; + --border-2: #adb5bd; + --brand: #212529; + --brand-hover: #343a40; + --brand-contrast: #ffffff; + --brand-bg: #e9ecef; + --brand-bg-2: #dee2e6; + --accent: #ffd000; + --accent-hover: #e6bb00; + --accent-contrast: #212529; + --accent-bg: #fff6c2; + --accent-bg-2: #ffec85; + --accent-soft: rgba(255, 208, 0, 0.35); + --success: #198754; + --success-bg: #d1e7dd; + --success-text: #0f5132; + --warning: #e5850b; + --warning-bg: #fff3cd; + --warning-text: #664d03; + --error: #dc3545; + --error-bg: #f8d7da; + --error-text: #842029; + --info: #0a58ca; + --info-bg: #cfe2ff; + --info-text: #052c65; + --review: #c2410c; + --review-bg: #ffe8d6; + --radius: 8px; + --radius-sm: 6px; + --sidebar: 236px; + --ease: cubic-bezier(0.22, 1, 0.36, 1); + --shadow-1: 0 1px 2px rgba(33, 37, 41, 0.06), 0 0 0 1px rgba(33, 37, 41, 0.04); + --shadow-2: 0 12px 32px rgba(33, 37, 41, 0.12); +} +[data-theme="dark"] .land { + --text: #f8f9fa; + --text-2: #adb5bd; + --text-3: #6c757d; + --canvas: #212529; + --panel: #2b3035; + --panel-2: #343a40; + --panel-3: #30353a; + --border: #343a40; + --border-2: #6c757d; + --brand: #ffd000; + --brand-hover: #ffdc33; + --brand-contrast: #212529; + --brand-bg: #3d3400; + --brand-bg-2: #4f4400; + --accent: #ffd000; + --accent-hover: #ffdc33; + --accent-contrast: #212529; + --accent-bg: #3d3400; + --accent-bg-2: #4f4400; + --accent-soft: rgba(255, 208, 0, 0.3); + --success: #75b798; + --success-bg: #1b4332; + --success-text: #d1e7dd; + --warning: #ffa94d; + --warning-bg: #4d2e05; + --warning-text: #fff3cd; + --error: #ea868f; + --error-bg: #4a1c22; + --error-text: #f8d7da; + --info: #6ea8fe; + --info-bg: #1b3a5c; + --info-text: #cfe2ff; + --review: #ffa94d; + --review-bg: #4a2a0b; + --shadow-1: 0 0 0 1px rgba(255, 255, 255, 0.04); + --shadow-2: 0 12px 32px rgba(0, 0, 0, 0.4); +} + +.land, .land * { box-sizing: border-box; } +.land, .land { margin: 0; } +.land { scroll-behavior: smooth; } +.land { font-family: "DM Sans", system-ui, sans-serif; font-size: 14px; line-height: 20px; color: var(--text); background: var(--canvas); -webkit-font-smoothing: antialiased; transition: background 0.25s var(--ease), color 0.25s var(--ease); } +.land h1, .land h2, .land h3, .land h4 { margin: 0; font-weight: 700; letter-spacing: -0.01em; } +.land h1 { font-size: 32px; line-height: 36px; } +.land h2 { font-size: 27px; line-height: 34px; } +.land h3 { font-size: 20px; line-height: 26px; } +.land h4 { font-size: 16px; line-height: 22px; } +.land p { margin: 0; } +.land a { color: inherit; text-decoration: none; } +.land code, .land .mono { font-family: "DM Mono", ui-monospace, Menlo, monospace; font-size: 12.5px; } +.land button, .land input, .land select, .land textarea { font-family: inherit; } +.land h1, .land h2, .land h3, .land .display, .land .balance-card .amount, .land .scorecard .v, .land .stats .v { font-family: "Space Grotesk", "DM Sans", system-ui, sans-serif; } +.land .muted { color: var(--text-2); } +.land .faint { color: var(--text-3); } +.land .small { font-size: 12px; line-height: 16px; } +.land .caption { font-size: 12px; line-height: 16px; color: var(--text-2); } +.land .overline { font-size: 11px; line-height: 14px; letter-spacing: 0.08em; text-transform: uppercase; font-weight: 700; color: var(--text-2); } +.land .strong { font-weight: 700; } +.land .num { font-variant-numeric: tabular-nums; } +.land .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.land .accent { color: var(--text); background: var(--accent); padding: 0 4px; } +[data-theme="dark"] .land .accent { color: var(--accent); background: none; padding: 0; } + +/* ================================================================== motion */ +@keyframes rise { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: none; } } +@keyframes fade { from { opacity: 0; } to { opacity: 1; } } +@keyframes pop { from { opacity: 0; transform: scale(0.96) translateY(8px); } to { opacity: 1; transform: none; } } +@keyframes shimmer { from { background-position: -600px 0; } to { background-position: 600px 0; } } +@keyframes cell { 0%, 100% { opacity: 0.15; } 50% { opacity: 0.6; } } +@keyframes marquee { from { transform: translateX(0); } to { transform: translateX(-50%); } } +@keyframes pulse-ring { 0% { box-shadow: 0 0 0 0 var(--accent-soft); } 70% { box-shadow: 0 0 0 14px transparent; } 100% { box-shadow: 0 0 0 0 transparent; } } +@keyframes spin { to { transform: rotate(360deg); } } +@keyframes float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } } +@keyframes grow { from { transform: scaleX(0); } to { transform: scaleX(1); } } + +.land .reveal { opacity: 0; transform: translateY(24px); transition: opacity 0.7s var(--ease), transform 0.7s var(--ease); } +.land .reveal.in { opacity: 1; transform: none; } +.land .reveal-stagger > * { opacity: 0; transform: translateY(16px); transition: opacity 0.6s var(--ease), transform 0.6s var(--ease); } +.land .reveal-stagger.in > * { opacity: 1; transform: none; } +.land .reveal-stagger.in > *:nth-child(1) { transition-delay: 0.05s; } +.land .reveal-stagger.in > *:nth-child(2) { transition-delay: 0.12s; } +.land .reveal-stagger.in > *:nth-child(3) { transition-delay: 0.19s; } +.land .reveal-stagger.in > *:nth-child(4) { transition-delay: 0.26s; } +.land .reveal-stagger.in > *:nth-child(5) { transition-delay: 0.33s; } +.land .reveal-stagger.in > *:nth-child(6) { transition-delay: 0.40s; } +.land .reveal-stagger.in > *:nth-child(n+7) { transition-delay: 0.47s; } +.land .enter { animation: rise 0.5s var(--ease) both; } +.land .enter-2 { animation: rise 0.5s var(--ease) 0.08s both; } +.land .enter-3 { animation: rise 0.5s var(--ease) 0.16s both; } +.land .enter-4 { animation: rise 0.5s var(--ease) 0.24s both; } +.land .page-enter { animation: fade 0.35s var(--ease) both; } +.land .page-enter > * { animation: rise 0.45s var(--ease) both; } +.land .page-enter > *:nth-child(2) { animation-delay: 0.06s; } +.land .page-enter > *:nth-child(3) { animation-delay: 0.12s; } +.land .page-enter > *:nth-child(4) { animation-delay: 0.18s; } +.land .lift { transition: transform 0.25s var(--ease), box-shadow 0.25s var(--ease), border-color 0.25s var(--ease); } +.land .lift:hover { transform: translateY(-3px); box-shadow: var(--shadow-2); } +.land .skeleton { position: relative; overflow: hidden; background: var(--panel-2); border-radius: 6px; color: transparent !important; } +.land .skeleton::after { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.35), transparent); background-size: 600px 100%; animation: shimmer 1.4s linear infinite; } +[data-theme="dark"] .land .skeleton::after { background: linear-gradient(90deg, transparent, rgba(255,255,255,0.08), transparent); background-size: 600px 100%; } +.land .spinner { width: 16px; height: 16px; border-radius: 50%; border: 2px solid var(--border-2); border-top-color: var(--brand); animation: spin 0.8s linear infinite; display: inline-block; vertical-align: middle; } +@media (prefers-reduced-motion: reduce) { + .land, .land *, .land *::before, .land *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; scroll-behavior: auto !important; } + .land .reveal, .land .reveal-stagger > * { opacity: 1; transform: none; } +} + +/* ================================================================== buttons */ +.land .btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 40px; padding: 0 20px; border-radius: var(--radius-sm); border: 1px solid transparent; font-size: 14px; font-weight: 700; cursor: pointer; white-space: nowrap; transition: background 0.15s var(--ease), border-color 0.15s var(--ease), transform 0.15s var(--ease), box-shadow 0.15s var(--ease); } +.land .btn:active:not(:disabled) { transform: scale(0.98); } +.land .btn:disabled { opacity: 0.4; cursor: not-allowed; } +.land .btn-primary { background: var(--brand); color: var(--brand-contrast); } +.land .btn-primary:hover:not(:disabled) { background: var(--brand-hover); box-shadow: var(--shadow-2); } +.land .btn-secondary { background: transparent; color: var(--text); border-color: var(--border-2); } +.land .btn-secondary:hover:not(:disabled) { background: var(--panel-2); } +.land .btn-dark { background: var(--panel-2); color: var(--text); } +.land .btn-dark:hover:not(:disabled) { background: var(--brand-bg-2); } +.land .btn-danger { background: transparent; color: var(--error); border-color: var(--error); } +.land .btn-danger:hover:not(:disabled) { background: var(--error-bg); } +.land .btn-ghost { background: transparent; color: var(--text); } +.land .btn-ghost:hover:not(:disabled) { background: var(--panel-2); } +.land .btn-accent { background: var(--accent); color: var(--accent-contrast); } +.land .btn-accent:hover:not(:disabled) { background: var(--accent-hover); box-shadow: 0 10px 28px rgba(255,208,0,0.35); } +.land .btn-white { background: #fff; color: #212529; border-color: #dee2e6; } +.land .btn-white:hover:not(:disabled) { background: #f1f3f5; } +.land .btn-black { background: #212529; color: #fff; } +.land .btn-black:hover:not(:disabled) { background: #343a40; } +.land .btn-sm { height: 32px; padding: 0 12px; font-size: 13px; } +.land .btn-lg { height: 52px; padding: 0 28px; font-size: 16px; } +.land .btn-block { width: 100%; } +.land .icon-btn { width: 36px; height: 36px; display: inline-grid; place-items: center; border-radius: 999px; border: 0; background: var(--panel-2); color: var(--text-2); cursor: pointer; transition: background 0.15s, color 0.15s, transform 0.15s; } +.land .icon-btn:hover { color: var(--text); background: var(--brand-bg-2); } +.land .icon-btn:active { transform: scale(0.94); } +.land .icon-btn.small { width: 28px; height: 28px; background: transparent; } +.land .icon-btn.small:hover { background: var(--panel-2); } + +/* ================================================================== app shell */ +.land .shell { display: flex; min-height: 100vh; } +.land .shell.collapsed { --sidebar: 68px; } +.land .shell.collapsed .sidebar { padding: 12px 8px 16px; } +.land .shell.collapsed .lbl, .land .shell.collapsed .nav-group, .land .shell.collapsed .promo, .land .shell.collapsed .nav .count, .land .shell.collapsed .tag-new { display: none; } +.land .shell.collapsed .home-pill { padding: 0 8px; } +.land .shell.collapsed .sidebar-top { flex-direction: column; gap: 6px; } +.land .shell.collapsed .nav button { justify-content: center; padding: 0; } +.land .shell.collapsed .nav button.active::before { left: -6px; } +.land .shell.collapsed .newtx .btn { font-size: 0; gap: 0; padding: 0; } +.land .shell.collapsed .newtx .btn svg { width: 16px; height: 16px; } +.land .sidebar { transition: width 0.25s var(--ease), padding 0.25s var(--ease); } +.land .sidebar { width: var(--sidebar); flex: none; background: var(--panel); border-right: 1px solid var(--border); padding: 12px 10px 16px; display: flex; flex-direction: column; gap: 4px; position: sticky; top: 0; height: 100vh; overflow-y: auto; } +.land .home-pill { display: inline-flex; align-items: center; gap: 10px; height: 40px; padding: 0 14px 0 8px; border-radius: 6px; font-family: "Space Grotesk", sans-serif; background: var(--accent-bg); color: var(--text); font-weight: 700; cursor: pointer; border: 0; align-self: flex-start; transition: background 0.15s; } +.land .home-pill:hover { background: var(--accent-bg-2); } +.land .home-pill .mark { width: 24px; height: 24px; border-radius: 4px; background: var(--accent); color: var(--accent-contrast); display: grid; place-items: center; font-weight: 800; font-size: 13px; } +.land .sidebar-top { display: flex; align-items: center; justify-content: space-between; padding: 4px 4px 8px; } +.land .newtx { margin: 4px 0 16px; } +.land .newtx .btn { width: 100%; justify-content: center; border-color: var(--border-2); background: transparent; color: var(--text-2); font-weight: 500; } +.land .newtx .btn:not(:disabled) { color: var(--text); border-color: var(--accent); } +.land .newtx .btn:hover:not(:disabled) { background: var(--accent); color: var(--accent-contrast); } +.land .nav { display: flex; flex-direction: column; gap: 2px; position: relative; } +.land .nav button { display: flex; align-items: center; gap: 12px; width: 100%; text-align: left; height: 38px; padding: 0 12px; border-radius: var(--radius-sm); border: 0; background: transparent; color: var(--text); font-size: 14px; font-weight: 500; cursor: pointer; position: relative; transition: background 0.15s, color 0.15s; } +.land .nav button:hover { background: var(--panel-2); } +.land .nav button.active { background: var(--accent-bg); font-weight: 700; } +.land .nav button.active::before { content: ""; position: absolute; left: -10px; top: 8px; bottom: 8px; width: 3px; border-radius: 2px; background: var(--accent); animation: grow 0.3s var(--ease); transform-origin: center; } +.land .nav button .ico { width: 18px; height: 18px; color: var(--text-2); display: inline-grid; place-items: center; transition: color 0.15s, transform 0.2s var(--ease); } +.land .nav button:hover .ico { transform: translateX(2px); } +.land .nav button.active .ico { color: var(--text); } +[data-theme="dark"] .land .nav button.active .ico { color: var(--accent); } +.land .nav .count { margin-left: auto; font-size: 11px; font-weight: 700; padding: 1px 7px; border-radius: 999px; background: var(--review-bg); color: var(--review); } +.land .nav-group { font-size: 12px; color: var(--text-2); padding: 14px 12px 6px; } +.land .sidebar-bottom { margin-top: auto; display: flex; flex-direction: column; gap: 2px; } +.land .promo { border-radius: 10px; padding: 16px; background: linear-gradient(180deg, var(--brand-bg) 0%, var(--panel) 100%); margin: 0 4px 12px; animation: pop 0.4s var(--ease); } +.land .promo .tag { display: inline-block; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 999px; background: var(--accent); color: var(--accent-contrast); margin-bottom: 8px; } +.land .promo h4 { font-size: 14px; margin-bottom: 4px; } +.land .promo p { font-size: 12px; color: var(--text-2); margin-bottom: 10px; } +.land .tag-new { display: inline-flex; align-items: center; height: 18px; padding: 0 7px; border-radius: 999px; background: var(--accent); color: var(--accent-contrast); font-size: 11px; font-weight: 700; } + +.land .main { flex: 1; min-width: 0; padding: 20px 24px 48px; } +.land .main-inner { max-width: 1160px; } +.land .header-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 24px; flex-wrap: wrap; animation: fade 0.4s var(--ease); } +.land .acct-pill { display: inline-flex; align-items: center; gap: 12px; height: 56px; padding: 0 16px 0 8px; border-radius: 10px; background: var(--panel); box-shadow: var(--shadow-1); cursor: pointer; border: 0; color: var(--text); text-align: left; max-width: 100%; transition: box-shadow 0.2s; } +.land .acct-pill:hover { box-shadow: var(--shadow-2); } +.land .acct-pill .name { font-size: 14px; font-weight: 700; } +.land .acct-pill .addr { font-family: "DM Mono", ui-monospace, monospace; font-size: 11.5px; color: var(--text-2); } +.land .acct-pill .addr b { color: var(--text); font-weight: 600; } +.land .acct-pill .meta { display: inline-flex; align-items: center; gap: 12px; margin-left: 8px; padding-left: 12px; border-left: 1px solid var(--border); font-size: 14px; } +.land .acct-pill .meta .chip-s { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 6px; background: var(--panel-2); font-size: 12px; color: var(--text-2); } +.land .tools-pill { display: inline-flex; align-items: center; gap: 6px; height: 56px; padding: 0 10px; border-radius: 10px; background: var(--panel); box-shadow: var(--shadow-1); } +.land .tools-pill .btn { height: 36px; border-radius: 6px; } +.land .tools-pill .btn-secondary { border-color: var(--border-2); color: var(--text-2); } +.land .tools-pill .btn-secondary.on { color: var(--text); } + +/* ================================================================== cards, lists, tables */ +.land .card { background: var(--panel); border-radius: 10px; padding: 24px; margin-bottom: 24px; box-shadow: var(--shadow-1); } +.land .card.flush { padding: 0; overflow: hidden; } +.land .card.flush .card-head { padding: 20px 24px 8px; } +.land .card-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; } +.land .card-head h4 { font-size: 16px; } +.land .card-head .sub { color: var(--text-2); font-size: 14px; margin-top: 4px; } +.land .card-actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; } +.land .link { color: var(--text); font-weight: 500; cursor: pointer; background: none; border: 0; padding: 0; font-size: 14px; display: inline-flex; align-items: center; gap: 4px; } +.land .link:hover { text-decoration: underline; } +.land .link.accent { color: var(--text); font-weight: 700; box-shadow: inset 0 -6px 0 var(--accent); } +[data-theme="dark"] .land .link.accent { color: var(--accent); box-shadow: none; } +.land .grid { display: grid; gap: 24px; } +.land .grid-2 { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); } +.land .grid-3 { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); } +.land .overview-grid { display: grid; grid-template-columns: minmax(0, 2fr) minmax(320px, 1fr); gap: 24px; align-items: start; } +@media (max-width: 1000px) { .land .overview-grid { grid-template-columns: 1fr; } } + +.land .balance-card .label { font-size: 15px; font-weight: 500; margin-bottom: 4px; color: var(--text-2); } +.land .balance-card .amount { font-size: 42px; line-height: 46px; font-weight: 800; letter-spacing: -0.03em; } +.land .balance-card .amount .dec { color: var(--text-3); font-weight: 600; } +.land .balance-card .actions { display: flex; gap: 8px; flex-wrap: wrap; } +.land .balance-card .actions .btn { background: var(--panel-2); color: var(--text); border: 0; height: 38px; font-weight: 600; } +.land .balance-card .actions .btn.hi { background: var(--brand); color: var(--brand-contrast); } +.land .balance-card .actions .btn:hover:not(:disabled) { background: var(--brand-bg-2); } +.land .balance-card .actions .btn.hi:hover:not(:disabled) { background: var(--brand-hover); } +.land .balance-row { display: flex; justify-content: space-between; align-items: flex-end; gap: 16px; flex-wrap: wrap; } + +.land .banner { border-radius: 10px; padding: 20px 24px; display: flex; gap: 20px; align-items: center; margin-bottom: 24px; background: linear-gradient(90deg, #dee2e6 0%, #e9ecef 60%, #f1f3f5 100%); color: #212529; position: relative; overflow: hidden; } +[data-theme="dark"] .land .banner { background: linear-gradient(90deg, #343a40 0%, #2b3035 70%); color: #f8f9fa; } +.land .banner .glyph { width: 52px; height: 52px; border-radius: 50%; background: var(--accent); color: var(--accent-contrast); display: grid; place-items: center; flex: none; animation: pulse-ring 2.4s ease-out infinite; } +.land .banner h4 { font-size: 18px; margin-bottom: 4px; } +.land .banner p { color: var(--text-2); margin-bottom: 8px; } +.land .banner .cta { font-weight: 700; color: var(--text); display: inline-flex; align-items: center; gap: 6px; cursor: pointer; background: none; border: 0; padding: 0; font-size: 14px; } +.land .banner .cta svg { transition: transform 0.2s var(--ease); } +.land .banner .cta:hover svg { transform: translateX(4px); } +.land .banner.warn { background: linear-gradient(90deg, #ffe5b4 0%, #fff3cd 70%); color: #212529; } +[data-theme="dark"] .land .banner.warn { background: linear-gradient(90deg, #4d3a05 0%, #2b3035 70%); color: #f8f9fa; } +.land .banner.warn .glyph { background: var(--warning); color: #212529; } +.land .banner .close { margin-left: auto; align-self: flex-start; background: none; border: 0; cursor: pointer; color: inherit; opacity: 0.6; } +.land .banner .close:hover { opacity: 1; } + +.land .list { list-style: none; margin: 0; padding: 0; } +.land .list > li { display: flex; align-items: center; gap: 14px; padding: 14px 24px; border-top: 1px solid var(--border); transition: background 0.15s; } +.land .list > li:hover { background: var(--panel-3); } +.land .list > li:first-child { border-top: 0; } +.land .list .grow { flex: 1; min-width: 0; } +.land .list .title { font-weight: 700; } +.land .list .sub { font-size: 12px; color: var(--text-2); } +.land .list .right { text-align: right; } +/* Colour and glyph come from lib/tokens.ts, so both interfaces draw an asset the same way. */ +.land .token-ico { width: 32px; height: 32px; border-radius: 50%; display: inline-grid; place-items: center; flex: none; font-family: "Space Grotesk", sans-serif; font-weight: 700; line-height: 1; } +.land table { width: 100%; border-collapse: collapse; } +.land th, .land td { text-align: left; padding: 14px 24px; border-top: 1px solid var(--border); vertical-align: middle; } +.land thead th { border-top: 0; color: var(--text-2); font-weight: 500; font-size: 13px; } +.land tbody tr { transition: background 0.15s; } +.land tbody tr:hover { background: var(--panel-3); } +.land td.num, .land th.num { text-align: right; font-variant-numeric: tabular-nums; } +.land .card:not(.flush) table th, .land .card:not(.flush) table td { padding-left: 0; padding-right: 0; } +.land .explore { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 12px; } +.land .explore button { border: 1px solid var(--border); border-radius: 10px; background: var(--panel-3); color: var(--text); padding: 20px 16px; text-align: left; font-weight: 700; font-size: 15px; cursor: pointer; display: flex; flex-direction: column; gap: 20px; transition: transform 0.25s var(--ease), border-color 0.25s, box-shadow 0.25s; } +.land .explore button:hover { border-color: var(--accent); transform: translateY(-3px); box-shadow: var(--shadow-2); } +.land .explore .ico { width: 56px; height: 56px; color: var(--text); transition: transform 0.3s var(--ease); } +[data-theme="dark"] .land .explore .ico { color: var(--accent); } +.land .explore button { position: relative; overflow: hidden; } +.land .explore button::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 4px; background: var(--accent); transform: scaleX(0); transform-origin: left; transition: transform 0.3s var(--ease); } +.land .explore button:hover::after { transform: scaleX(1); } +.land .explore button:hover .ico { transform: scale(1.08) rotate(-4deg); } + +/* ================================================================== pills */ +.land .pill { display: inline-flex; align-items: center; gap: 6px; height: 24px; padding: 0 10px; border-radius: 6px; font-size: 12px; font-weight: 700; white-space: nowrap; } +.land .pill-neutral { background: var(--panel-2); color: var(--text-2); } +.land .pill-ok { background: var(--success-bg); color: var(--success-text); } +.land .pill-warn { background: var(--warning-bg); color: var(--warning-text); } +.land .pill-bad { background: var(--error-bg); color: var(--error-text); } +.land .pill-info { background: var(--info-bg); color: var(--info-text); } +.land .pill-review { background: var(--review-bg); color: var(--review); } +[data-theme="light"] .land .pill-review, .land:not([data-theme="dark"]) .pill-review { color: #7a5c00; } +.land .pill-brand { background: var(--accent); color: var(--accent-contrast); } +.land .pill svg { width: 12px; height: 12px; } + +/* ================================================================== tabs */ +.land .tabs { display: flex; gap: 8px; border-bottom: 1px solid var(--border); margin-bottom: 20px; overflow-x: auto; } +.land .tabs button { background: none; border: 0; padding: 10px 16px; font-size: 14px; font-weight: 700; cursor: pointer; color: var(--text-2); border-bottom: 2px solid transparent; margin-bottom: -1px; display: inline-flex; gap: 8px; align-items: center; white-space: nowrap; transition: color 0.15s, border-color 0.15s; } +.land .tabs button:hover { color: var(--text); } +.land .tabs button.active { color: var(--text); border-bottom-color: var(--accent); } +.land .tabs-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; border-bottom: 1px solid var(--border); margin-bottom: 20px; } +.land .tabs-row .tabs { margin-bottom: 0; border-bottom: 0; } + +/* ================================================================== transactions */ +.land .date-label { font-size: 12px; font-weight: 500; color: var(--text-2); letter-spacing: 0.06em; text-transform: uppercase; margin: 20px 0 10px; } +.land .txcard { background: var(--panel); border-radius: 10px; margin-bottom: 8px; box-shadow: var(--shadow-1); animation: rise 0.4s var(--ease) both; } +.land .txcard .row { display: grid; grid-template-columns: 24px minmax(120px, 1fr) minmax(0, 1.4fr) 110px auto 24px; align-items: center; gap: 16px; padding: 18px 20px; cursor: pointer; border-radius: 10px; transition: background 0.15s; } +.land .txcard .row:hover { background: var(--panel-3); } +.land .txcard .kind { font-weight: 600; } +.land .txcard .amt { display: inline-flex; align-items: center; gap: 10px; font-weight: 700; } +.land .txcard .when { color: var(--text-2); font-size: 13px; } +.land .txcard .status { text-align: right; font-weight: 700; } +.land .status-success { color: var(--success); } +.land .status-cancel { color: var(--text-2); } +.land .status-veto { color: var(--error); } +.land .txcard .detail { padding: 8px 20px 20px 60px; border-top: 1px solid var(--border); display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(260px, 1fr); gap: 24px; animation: rise 0.3s var(--ease); } +@media (max-width: 900px) { .land .txcard .row { grid-template-columns: 24px 1fr auto 24px; } .land .txcard .hide-sm { display: none; } .land .txcard .detail { grid-template-columns: 1fr; padding-left: 20px; } } +.land .chev { transition: transform 0.2s var(--ease); color: var(--text-2); } +.land .chev.open { transform: rotate(180deg); } +.land .timeline { list-style: none; margin: 0; padding: 0; } +.land .timeline li { position: relative; padding: 0 0 16px 24px; } +.land .timeline li::before { content: ""; position: absolute; left: 4px; top: 5px; width: 10px; height: 10px; border-radius: 50%; background: var(--border-2); transition: background 0.3s; } +.land .timeline li.done::before { background: var(--accent); } +.land .timeline li.active::before { background: var(--accent); box-shadow: 0 0 0 4px var(--accent-bg); animation: pulse-ring 2s ease-out infinite; } +.land .timeline li:not(:last-child)::after { content: ""; position: absolute; left: 8px; top: 16px; bottom: 0; width: 2px; background: var(--border); } +.land .timeline .t-title { font-weight: 700; } +.land .timeline .t-sub { font-size: 12px; color: var(--text-2); } +.land .timeline .signers { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; font-size: 12px; } +.land .tx-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 16px; } +.land .empty-illus { padding: 64px 24px; text-align: center; color: var(--text-2); animation: fade 0.5s var(--ease); } +.land .empty-illus .glyph { width: 96px; height: 96px; border-radius: 50%; background: var(--panel-2); display: grid; place-items: center; margin: 0 auto 16px; color: var(--accent); animation: float 4s ease-in-out infinite; } + +/* ================================================================== sections (settings-style) */ +.land .section { background: var(--panel); border-radius: 10px; padding: 28px 32px; margin-bottom: 20px; display: grid; grid-template-columns: 240px minmax(0, 1fr); gap: 24px; box-shadow: var(--shadow-1); } +.land .section > h3 { font-size: 20px; } +.land .section > h3 small { display: block; font-size: 13px; font-weight: 400; color: var(--text-2); margin-top: 6px; line-height: 18px; } +.land .section .body { min-width: 0; } +.land .section .body h4 { margin-bottom: 8px; } +.land .section .body p.desc { color: var(--text-2); margin-bottom: 16px; font-size: 15px; } +@media (max-width: 860px) { .land .section { grid-template-columns: 1fr; } } +.land .member-row { display: flex; align-items: center; gap: 14px; padding: 12px 0; border-top: 1px solid var(--border); } +.land .member-row:first-of-type { border-top: 0; } +.land .member-row .addr { font-family: "DM Mono", ui-monospace, monospace; font-size: 13px; } +.land .member-row .addr b { font-family: "DM Sans", sans-serif; font-weight: 700; } +.land .member-row .roles { display: flex; gap: 6px; flex-wrap: wrap; } +.land .member-row .tools { margin-left: auto; display: flex; gap: 4px; } +.land .kv { display: grid; grid-template-columns: 1fr; margin: 0; } +.land .kv div { display: flex; justify-content: space-between; gap: 16px; padding: 10px 0; border-top: 1px solid var(--border); font-size: 14px; } +.land .kv div:first-child { border-top: 0; } +.land .kv dt { color: var(--text-2); } +.land .kv dd { margin: 0; text-align: right; font-weight: 500; } +.land .addr { display: inline-flex; align-items: center; gap: 6px; color: var(--text); } +.land .addr code { font-size: 12.5px; } +.land .addr:hover code { text-decoration: underline; } + +/* policy editor controls */ +.land .pfield { display: grid; grid-template-columns: minmax(0, 1fr) 160px; gap: 16px; align-items: center; padding: 14px 0; border-top: 1px solid var(--border); } +.land .pfield:first-child { border-top: 0; } +.land .pfield .lbl { font-weight: 600; } +.land .pfield .hint { font-size: 12px; color: var(--text-2); margin-top: 2px; } +.land .pfield input, .land .pfield select { height: 40px; } +.land .pfield .unit { display: flex; align-items: center; gap: 8px; } +.land .pfield .unit span { color: var(--text-2); font-size: 12px; white-space: nowrap; } +.land .pfield.changed .lbl::after { content: " •"; color: var(--warning); } +.land .stepper { display: inline-flex; align-items: center; border: 1px solid var(--border-2); border-radius: 8px; overflow: hidden; height: 40px; } +.land .stepper button { width: 36px; height: 100%; border: 0; background: var(--panel-2); color: var(--text); cursor: pointer; font-size: 16px; } +.land .stepper button:hover { background: var(--brand-bg-2); } +.land .stepper input { border: 0; width: 64px; text-align: center; height: 100%; border-radius: 0; } +.land .mode-hero { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; } +.land .mode-hero > div { border: 1px solid var(--border); border-radius: 10px; padding: 18px; position: relative; transition: border-color 0.25s, transform 0.25s var(--ease); } +.land .mode-hero > div.on { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); transform: translateY(-2px); } +.land .mode-hero .m { font-size: 18px; font-weight: 800; margin-bottom: 4px; } +.land .mode-hero .d { color: var(--text-2); font-size: 13px; } +.land .mode-hero .cur { position: absolute; top: 12px; right: 12px; } +@media (max-width: 860px) { .land .mode-hero { grid-template-columns: 1fr; } } +.land .gauge { display: flex; align-items: center; gap: 16px; } +.land .gauge .ring { width: 72px; height: 72px; border-radius: 50%; display: grid; place-items: center; font-weight: 800; font-size: 18px; background: conic-gradient(var(--accent) calc(var(--pct) * 1%), var(--panel-2) 0); position: relative; } +.land .gauge .ring::after { content: ""; position: absolute; inset: 8px; border-radius: 50%; background: var(--panel); } +.land .gauge .ring span { position: relative; z-index: 1; font-size: 15px; } +.land .scorecard { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 20px; } +.land .scorecard > div { background: var(--panel); border-radius: 10px; padding: 16px 18px; box-shadow: var(--shadow-1); } +.land .scorecard .v { font-size: 26px; font-weight: 800; letter-spacing: -0.02em; } +.land .scorecard .l { font-size: 12px; color: var(--text-2); } + +/* ================================================================== forms */ +.land .field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; } +.land .field-label { font-size: 14px; font-weight: 700; } +.land .field-hint { font-size: 12px; color: var(--text-2); } +.land input, .land select, .land textarea { height: 48px; padding: 0 14px; border: 1px solid var(--border-2); border-radius: var(--radius-sm); background: var(--panel); color: var(--text); width: 100%; font-size: 14px; transition: border-color 0.15s, box-shadow 0.15s; } +.land select option { background: var(--panel); color: var(--text); } +.land input[type="checkbox"] { height: auto; width: auto; accent-color: var(--accent); } +.land input[type="range"] { height: auto; padding: 0; border: 0; accent-color: var(--accent); } +.land input:focus, .land select:focus, .land textarea:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } +.land input:read-only { color: var(--text-2); } +.land .row { display: flex; gap: 16px; flex-wrap: wrap; align-items: flex-end; } +.land .row > .field { flex: 1; min-width: 180px; } +.land .inline { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } +.land .stack { display: flex; flex-direction: column; gap: 8px; } +.land .notice { padding: 14px 16px; border-radius: var(--radius-sm); margin: 12px 0; font-size: 14px; display: flex; gap: 10px; align-items: flex-start; animation: rise 0.3s var(--ease); } +.land .notice-ok { background: var(--success-bg); color: var(--success-text); } +.land .notice-bad { background: var(--error-bg); color: var(--error-text); } +.land .notice-warn { background: var(--warning-bg); color: var(--warning-text); } +.land .notice-info { background: var(--info-bg); color: var(--info-text); } +.land .empty { padding: 40px 24px; text-align: center; color: var(--text-2); } +.land .switch { display: inline-flex; align-items: center; gap: 10px; cursor: pointer; } +.land .switch .track { width: 40px; height: 22px; border-radius: 999px; background: var(--border-2); position: relative; transition: background 0.2s; } +.land .switch .track::after { content: ""; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: #fff; transition: transform 0.2s var(--ease); } +.land .switch.on .track { background: var(--accent); } +.land .switch.on .track::after { transform: translateX(18px); } +.land .segmented { display: inline-flex; padding: 4px; border-radius: 999px; background: var(--panel); gap: 4px; box-shadow: var(--shadow-1); } +.land .segmented button { border: 0; background: transparent; color: var(--text-2); font-weight: 700; padding: 10px 28px; border-radius: 999px; cursor: pointer; transition: background 0.2s, color 0.2s; } +.land .segmented button.active { background: var(--brand); color: var(--brand-contrast); } +.land .review { border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--panel-3); padding: 4px 16px; } +.land .review-line { display: flex; justify-content: space-between; gap: 16px; padding: 10px 0; border-top: 1px solid var(--border); } +.land .review-line:first-child { border-top: 0; } +.land .review-line > span:first-child { color: var(--text-2); flex: none; } +.land .review-line > *:last-child { text-align: right; } +.land .reasons { margin: 8px 0 0; padding-left: 18px; color: var(--text-2); } +.land .bar { height: 6px; background: var(--panel-2); border-radius: 3px; overflow: hidden; margin-top: 8px; } +.land .bar > i { display: block; height: 100%; background: var(--accent); transform-origin: left; animation: grow 0.8s var(--ease); } +.land .bar.warn > i { background: var(--warning); } +.land .bar.bad > i { background: var(--error); } +.land .templates { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; } +.land .template { border: 1px solid var(--border); border-radius: 10px; padding: 16px; cursor: pointer; background: var(--panel-3); text-align: left; color: var(--text); transition: border-color 0.2s, transform 0.2s var(--ease), box-shadow 0.2s; } +.land .template:hover { border-color: var(--border-2); transform: translateY(-2px); } +.land .template.active { border-color: var(--accent); background: var(--accent-bg); box-shadow: 0 0 0 3px var(--accent-soft); } +.land .template strong { display: block; margin-bottom: 4px; } +.land .scenario { border-radius: 10px; padding: 20px 24px; margin-bottom: 12px; background: var(--panel); border-left: 4px solid var(--border-2); box-shadow: var(--shadow-1); transition: transform 0.25s var(--ease), box-shadow 0.25s; } +.land .scenario:hover { transform: translateX(4px); box-shadow: var(--shadow-2); } +.land .scenario.blocked, .land .scenario.impossible { border-left-color: var(--success); } +.land .scenario.delayed { border-left-color: var(--info); } +.land .scenario.escalated { border-left-color: var(--warning); } +.land .scenario.allowed { border-left-color: var(--error); } +.land .scenario ul { margin: 8px 0 0; padding-left: 18px; color: var(--text-2); } +.land .posture { list-style: none; margin: 0; padding: 0; } +.land .posture li { display: flex; gap: 10px; align-items: baseline; padding: 6px 0; } +.land .posture .dot { width: 8px; height: 8px; border-radius: 50%; flex: none; background: var(--text-3); } +.land .posture .dot.ok { background: var(--success); } +.land .posture .dot.warn { background: var(--warning); } +.land .posture .dot.bad { background: var(--error); } +.land .assumptions { font-size: 12px; color: var(--text-2); margin: 12px 0 0; padding-left: 18px; } + +/* modal */ +.land .modal-backdrop { position: fixed; inset: 0; background: rgba(33, 37, 41, 0.55); backdrop-filter: blur(4px); z-index: 50; display: flex; align-items: flex-start; justify-content: center; padding: 48px 16px; overflow-y: auto; animation: fade 0.2s var(--ease); } +.land .modal { width: 100%; max-width: 640px; background: var(--panel); border-radius: 14px; box-shadow: 0 24px 64px rgba(0,0,0,0.35); animation: pop 0.3s var(--ease); } +.land .modal-head { display: flex; align-items: center; justify-content: space-between; padding: 20px 24px; border-bottom: 1px solid var(--border); } +.land .modal-body { padding: 24px; } +.land .modal-foot { display: flex; justify-content: space-between; gap: 8px; padding: 16px 24px; border-top: 1px solid var(--border); } +.land .steps { display: flex; gap: 8px; align-items: center; font-size: 12px; color: var(--text-2); } +.land .steps .step { display: inline-flex; align-items: center; gap: 6px; } +.land .steps .n { width: 20px; height: 20px; border-radius: 50%; display: inline-grid; place-items: center; background: var(--panel-2); font-weight: 700; font-size: 11px; transition: background 0.2s, color 0.2s; } +.land .steps .step.on .n { background: var(--accent); color: var(--accent-contrast); } +.land .steps .step.on { color: var(--text); font-weight: 700; } + +/* ================================================================== welcome / create */ +.land .entry { min-height: 100vh; background: radial-gradient(60% 40% at 50% 28%, var(--panel-2) 0%, var(--canvas) 60%); display: flex; flex-direction: column; } +.land .entry-top { display: flex; justify-content: space-between; align-items: center; padding: 16px 24px; animation: fade 0.4s var(--ease); } +.land .entry-top .logo { display: inline-flex; align-items: center; gap: 8px; font-weight: 800; font-size: 18px; } +.land .entry-top .logo .mark { width: 28px; height: 28px; display: block; } +.land .entry-center { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 40px 16px; gap: 16px; } +.land .entry-card { width: 100%; max-width: 440px; background: var(--panel); border-radius: 16px; padding: 32px; text-align: center; box-shadow: var(--shadow-2); animation: pop 0.45s var(--ease) both; } +.land .entry-card h2 { font-size: 24px; margin: 12px 0 8px; } +.land .entry-card p { color: var(--text-2); margin-bottom: 20px; } +.land .entry-card .or { display: flex; align-items: center; gap: 12px; color: var(--text-2); font-size: 12px; margin: 16px 0; } +.land .entry-card .or::before, .land .entry-card .or::after { content: ""; flex: 1; height: 1px; background: var(--border-2); } +.land .entry-note { max-width: 440px; width: 100%; background: var(--panel); border-radius: 10px; padding: 10px 16px; display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 14px; box-shadow: var(--shadow-1); animation: rise 0.4s var(--ease) 0.1s both; } +.land .entry-foot { display: flex; gap: 16px; justify-content: center; padding: 24px; color: var(--text-2); font-size: 13px; flex-wrap: wrap; } +.land .entry-foot a { color: var(--text); font-weight: 700; box-shadow: inset 0 -6px 0 var(--accent); } +[data-theme="dark"] .land .entry-foot a { color: var(--accent); box-shadow: none; } +.land .wordmark-lg { font-size: 28px; font-weight: 800; letter-spacing: -0.02em; display: inline-flex; align-items: center; gap: 10px; justify-content: center; } +.land .wordmark-lg .brace { color: var(--text-2); font-weight: 500; } +.land .create { max-width: 1160px; margin: 0 auto; padding: 24px; } +.land .create-grid { display: grid; grid-template-columns: minmax(0, 1fr) 360px; gap: 24px; align-items: start; } +@media (max-width: 960px) { .land .create-grid { grid-template-columns: 1fr; } } +.land .stepcard { background: var(--panel); border-radius: 10px; overflow: hidden; box-shadow: var(--shadow-1); animation: rise 0.45s var(--ease); } +.land .stepcard .progress { height: 4px; background: var(--border); } +.land .stepcard .progress i { display: block; height: 100%; background: var(--accent); transition: width 0.5s var(--ease); } +.land .stepcard .step-head { display: flex; gap: 16px; align-items: flex-start; padding: 24px 28px; border-bottom: 1px solid var(--border); } +.land .stepcard .step-head .n { width: 22px; height: 22px; border-radius: 50%; background: var(--accent); color: var(--accent-contrast); display: grid; place-items: center; font-weight: 800; font-size: 12px; flex: none; margin-top: 2px; } +.land .stepcard .step-head h3 { font-size: 20px; } +.land .stepcard .step-head p { color: var(--text-2); margin-top: 2px; } +.land .stepcard .step-body { padding: 28px; animation: fade 0.3s var(--ease); } +.land .stepcard .step-foot { display: flex; justify-content: space-between; padding: 20px 28px; border-top: 1px solid var(--border); } +.land .preview { background: var(--panel); border-radius: 10px; padding: 24px; text-align: center; box-shadow: var(--shadow-1); animation: rise 0.45s var(--ease) 0.1s both; position: sticky; top: 24px; } +.land .preview h4 { font-size: 18px; margin: 8px 0 16px; } +.land .preview .kv div { text-align: left; } + +/* ================================================================== marketing site (safe.global) */ +.land { --lt: #212529; --ls: #6c757d; font-size: 16px; line-height: 24px; color: #212529; background: #fff; overflow-x: hidden; } +.land .wrap { max-width: 1216px; margin: 0 auto; padding: 0 24px; } +.land .land-nav { position: sticky; top: 0; z-index: 30; background: rgba(255,255,255,0.85); backdrop-filter: blur(12px); border-bottom: 1px solid transparent; transition: border-color 0.2s, box-shadow 0.2s; } +.land .land-nav.scrolled { border-bottom-color: #e9ecef; box-shadow: 0 4px 24px rgba(33,37,41,0.06); } +.land .land-nav .nav-right { display: flex; align-items: center; gap: 14px; } +.land .netchip { display: inline-flex; align-items: center; gap: 8px; font-family: "DM Mono", monospace; font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: #6c757d; border: 1px solid #dee2e6; padding: 7px 10px; border-radius: 4px; } +.land .netchip::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: #ffd000; box-shadow: 0 0 0 3px rgba(255,208,0,0.25); } +.land .land-nav .wrap { height: 72px; display: flex; align-items: center; justify-content: space-between; gap: 24px; } +.land .land-nav .logo { display: inline-flex; align-items: center; gap: 10px; font-weight: 700; font-size: 22px; letter-spacing: -0.02em; font-family: "Space Grotesk", sans-serif; } +.land .land-nav .logo .mark { width: 28px; height: 28px; display: block; } +.land .land-nav .logo .brace { color: #6c757d; font-weight: 500; } +.land .land-nav nav { display: flex; gap: 32px; } +.land .land-nav nav a { color: #6c757d; font-weight: 500; position: relative; z-index: 0; transition: color 0.15s; } +.land .land-nav nav a::after { content: ""; position: absolute; left: -4px; right: -4px; bottom: 1px; height: 9px; z-index: -1; background: #ffd000; transform: scaleX(0); transform-origin: left; transition: transform 0.25s var(--ease); } +.land .land-nav nav a:hover, .land .land-nav nav a.on { color: #212529; } +.land .land-nav nav a:hover::after, .land .land-nav nav a.on::after { transform: scaleX(1); } +.land .announce { background: #212529; color: #fff; text-align: center; padding: 12px 24px; font-size: 14px; } +.land .announce a { font-weight: 500; } +.land .announce a span { color: #ffd000; } +.land .announce a span { display: inline-block; transition: transform 0.2s var(--ease); } +.land .announce a:hover span { transform: translateX(4px); } +.land .hero { position: relative; overflow: hidden; background: #f8f9fa; padding: 104px 0 0; text-align: center; } +.land .hero.sub-hero { padding: 88px 0 72px; } +.land .hero .grid-bg { position: absolute; inset: 0; background-image: linear-gradient(#e9ecef 1px, transparent 1px), linear-gradient(90deg, #e9ecef 1px, transparent 1px); background-size: 131px 131px; } +.land .hero .cell { position: absolute; width: 130px; height: 130px; background: linear-gradient(180deg, rgba(255,208,0,0) 0%, rgba(255,208,0,0.55) 100%); animation: cell 6s ease-in-out infinite; } +.land .hero .wrap { position: relative; } +.land .hero .eyebrow { font-family: "DM Mono", monospace; font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase; color: #6c757d; margin-bottom: 24px; animation: rise 0.6s var(--ease) both; display: flex; align-items: center; justify-content: center; gap: 12px; } +.land .hero .eyebrow b { background: #ffd000; color: #212529; padding: 3px 7px; font-weight: 500; } +.land .hero h1 { font-size: 84px; line-height: 84px; letter-spacing: -0.035em; font-weight: 700; max-width: 960px; margin: 0 auto 24px; animation: rise 0.7s var(--ease) 0.08s both; } +.land .hero h1 .w { display: inline-block; } +.land .hero .sub { font-size: 19px; line-height: 28px; color: #6c757d; max-width: 600px; margin: 0 auto 32px; animation: rise 0.7s var(--ease) 0.18s both; } +.land .hero .cta-row { animation: rise 0.7s var(--ease) 0.26s both; display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; } +.land .hero .mockwrap { position: relative; max-width: 960px; margin: 72px auto 0; text-align: left; animation: rise 0.9s var(--ease) 0.35s both; } +.land .hero .mock { border-radius: 8px 8px 0 0; box-shadow: 0 -8px 60px rgba(33,37,41,0.18); overflow: hidden; border: 1px solid #dee2e6; border-bottom: 0; } +.land .hero .float { position: absolute; right: -56px; bottom: 36px; width: 400px; animation: rise 0.9s var(--ease) 0.7s both; } +.land .hero .float .ticker { box-shadow: 0 30px 80px rgba(33,37,41,0.35), 0 0 0 1px rgba(255,255,255,0.06); } +@media (max-width: 1120px) { .land .hero .float { position: static; width: auto; margin: 24px 0 0; } .land .hero .mock { border-bottom: 1px solid #dee2e6; border-radius: 8px; } } +@media (max-width: 900px) { .land .hero h1 { font-size: 48px; line-height: 50px; } } +.land .light { background: #f8f9fa; padding: 96px 0; } +.land .white { background: #fff; padding: 96px 0; } +.land .dark { background: #212529; color: #fff; padding: 96px 0; } +.land h2 { font-size: 44px; line-height: 48px; letter-spacing: -0.03em; text-align: left; margin-bottom: 12px; font-weight: 700; max-width: 760px; } +.land .lead { text-align: left; color: #adb5bd; font-size: 17px; max-width: 640px; margin: 0 0 56px; } +.land .center h2, .land .center .lead { text-align: center; margin-left: auto; margin-right: auto; } +.land .light .lead, .land .white .lead { color: #6c757d; } +.land .marquee { overflow: hidden; mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent); } +.land .marquee .track { display: flex; gap: 12px; width: max-content; animation: marquee 28s linear infinite; } +.land .marquee .track:hover { animation-play-state: paused; } +.land .marquee .chip-l { padding: 12px 22px; border-radius: 6px; font-family: "Space Grotesk", sans-serif; border: 1px solid #dee2e6; background: #fff; font-weight: 600; white-space: nowrap; color: #212529; } +.land .bento { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } +.land .bento .big { grid-column: 1 / -1; } +.land .bcard { background: #2b3035; border: 1px solid #343a40; border-radius: 8px; padding: 32px; min-height: 340px; background-image: radial-gradient(60% 40% at 50% 0%, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0) 100%); transition: transform 0.35s var(--ease), border-color 0.35s, box-shadow 0.35s; } +.land .bcard:hover { transform: translateY(-4px); border-color: #ffd000; box-shadow: 0 24px 60px rgba(0,0,0,0.35); } +.land .bcard h3 { font-size: 24px; margin-bottom: 8px; } +.land .bcard p { color: #adb5bd; margin-bottom: 24px; } +.land .bcard.center { text-align: center; } +.land .bcard .art { margin-top: 8px; } +@media (max-width: 860px) { .land .bento { grid-template-columns: 1fr; } } +.land .dashed { display: grid; grid-template-columns: 1fr 1fr; max-width: 900px; margin: 0 auto; } +.land .dashed > div { padding: 40px 32px 40px 48px; border-left: 1px dashed #495057; border-top: 1px dashed #495057; } +.land .dashed > div:nth-child(-n+2) { border-top: 0; } +.land .dashed > div:nth-child(odd) { border-left: 0; } +.land .dashed h4 { font-size: 18px; margin-bottom: 8px; } +.land .dashed p { color: #adb5bd; } +.land .badges { display: flex; gap: 40px; justify-content: center; align-items: center; flex-wrap: wrap; padding: 40px 0; border-top: 1px solid #343a40; color: #adb5bd; font-weight: 700; font-size: 18px; } +.land .badges .lbl { font-weight: 400; font-size: 14px; } +/* A dark panel inside a light section: it must carry its own ink, or it inherits the page's near-black and the words vanish into the ground. */ +.land .shield { background: #2b3035; border: 1px solid #343a40; border-radius: 8px; padding: 48px; display: grid; grid-template-columns: 1fr 1.2fr; gap: 40px; align-items: center; overflow: hidden; color: #f8f9fa; } +.land .shield h2 { text-align: left; font-size: 40px; margin-bottom: 16px; color: #fff; } +.land .shield p { color: #adb5bd; margin-bottom: 32px; max-width: 320px; } +.land .shield ul { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 16px; font-size: 20px; font-weight: 700; } +.land .shield ul li { display: flex; gap: 12px; align-items: center; } +.land .shield ul li svg { color: #212529; background: #ffd000; border-radius: 4px; padding: 3px; width: 26px; height: 26px; } +.land .shield .tilt { transform: perspective(1200px) rotateY(-12deg) rotateX(4deg) rotate(6deg) scale(1.05); transform-origin: left center; border-radius: 10px; overflow: hidden; box-shadow: 0 30px 80px rgba(0,0,0,0.5); transition: transform 0.6s var(--ease); } +.land .shield:hover .tilt { transform: perspective(1200px) rotateY(-6deg) rotateX(2deg) rotate(3deg) scale(1.03); } +@media (max-width: 900px) { .land .shield { grid-template-columns: 1fr; } .land .shield .tilt { transform: none; } } +.land .compare { background: #2b3035; border: 1px solid #343a40; border-radius: 8px; padding: 12px; max-width: 1000px; margin: 0 auto; overflow-x: auto; } +.land .compare table { border-collapse: separate; border-spacing: 8px 0; } +.land .compare th, .land .compare td { border: 0; padding: 20px 20px; vertical-align: top; background: #343a40; border-radius: 0; font-size: 15px; color: #adb5bd; } +.land .compare th { font-weight: 500; font-size: 16px; } +.land .compare td:first-child { color: #fff; font-weight: 500; } +.land .compare .hl { background: #ffd000; color: #212529 !important; font-weight: 700; } +.land .compare th.hl { border-radius: 8px 8px 0 0; } +.land .compare tr:last-child td.hl { border-radius: 0 0 8px 8px; } +.land .quote { text-align: center; max-width: 960px; margin: 0 auto; } +.land .quote blockquote { font-size: 36px; line-height: 44px; font-style: italic; color: #dee2e6; margin: 0 0 32px; font-weight: 400; letter-spacing: -0.01em; } +.land .quote cite { font-style: normal; font-weight: 700; font-size: 20px; } +.land .stats { display: flex; gap: 96px; justify-content: center; flex-wrap: wrap; margin-top: 56px; } +.land .stats .v { font-size: 64px; line-height: 68px; color: #ffd000; font-weight: 700; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; } +.land .stats .l { color: #adb5bd; margin-top: 8px; font-size: 14px; } +.land .audiences { display: grid; grid-template-columns: 1fr 2fr; gap: 20px; } +.land .acard { background: #212529; color: #fff; border-radius: 8px; padding: 28px; min-height: 420px; transition: transform 0.35s var(--ease), box-shadow 0.35s; } +.land .acard:hover { transform: translateY(-4px); box-shadow: 0 24px 60px rgba(33,37,41,0.3); } +.land .acard h3 { font-size: 22px; margin-bottom: 8px; } +.land .acard p { color: #adb5bd; margin-bottom: 20px; max-width: 320px; } +.land .acard a, .land .acard .link { color: #ffd000; font-weight: 700; } +.land .acard .art { margin-top: 24px; } +@media (max-width: 860px) { .land .audiences { grid-template-columns: 1fr; } } +.land .steps4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; } +.land .steps4 > div { background: #fff; border: 1px solid #e9ecef; border-radius: 8px; padding: 28px; transition: transform 0.3s var(--ease), box-shadow 0.3s; } +.land .steps4 > div:hover { transform: translateY(-4px); box-shadow: 0 16px 40px rgba(33,37,41,0.1); } +.land .steps4 .n { width: 32px; height: 32px; border-radius: 4px; background: #ffd000; color: #212529; font-family: "DM Mono", monospace; display: grid; place-items: center; font-weight: 800; margin-bottom: 20px; } +.land .steps4 h4 { font-size: 18px; margin-bottom: 8px; } +.land .steps4 p { color: #6c757d; } +@media (max-width: 900px) { .land .steps4 { grid-template-columns: 1fr 1fr; } } +.land .feat6 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; } +.land .feat6 > div { background: #fff; border: 1px solid #e9ecef; border-radius: 8px; padding: 28px; transition: transform 0.3s var(--ease), box-shadow 0.3s; } +.land .feat6 > div:hover { transform: translateY(-4px); box-shadow: 0 16px 40px rgba(33,37,41,0.1); } +.land .feat6 .ico { width: 44px; height: 44px; border-radius: 10px; background: #212529; color: #fff; display: grid; place-items: center; margin-bottom: 20px; } +.land .feat6 h4 { font-size: 18px; margin-bottom: 8px; } +.land .feat6 p { color: #6c757d; } +.land .dark .feat6 > div { background: #2b3035; border-color: #343a40; } +.land .dark .feat6 .ico { background: #ffd000; color: #212529; } +.land .dark .feat6 p { color: #adb5bd; } +@media (max-width: 900px) { .land .feat6 { grid-template-columns: 1fr; } } +.land .faq { max-width: 770px; margin: 0 auto; } +.land .faq details { border-bottom: 1px solid #e9ecef; } +.land .faq summary { list-style: none; cursor: pointer; display: flex; justify-content: space-between; align-items: center; padding: 20px 0; font-weight: 700; font-size: 17px; } +.land .faq summary::-webkit-details-marker { display: none; } +.land .faq summary .plus { width: 24px; height: 24px; border-radius: 4px; border: 1.5px solid #6c757d; display: grid; place-items: center; color: #6c757d; font-weight: 400; transition: transform 0.3s var(--ease), background 0.2s, color 0.2s; } +.land .faq details[open] summary .plus { transform: rotate(45deg); background: #ffd000; color: #212529; border-color: #ffd000; } +.land .faq .ans { padding: 0 0 20px; color: #6c757d; max-width: 680px; animation: rise 0.3s var(--ease); } +.land .cta { position: relative; overflow: hidden; background: #fff; background-image: radial-gradient(#d9dde2 1.2px, transparent 1.2px); background-size: 26px 26px; padding: 120px 0; text-align: center; } +.land .cta h2 { font-size: 56px; line-height: 60px; text-align: center; margin: 0 auto 12px; } +.land .cta h2 span { color: #6c757d; } +.land .cta .sub { color: #6c757d; font-size: 18px; margin: 20px 0 32px; } +.land .land-footer { background: #212529; color: #fff; padding: 72px 0 0; overflow: hidden; } +.land .land-footer .cols { display: grid; grid-template-columns: 2fr repeat(5, 1fr); gap: 24px; padding-bottom: 56px; border-bottom: 1px solid #343a40; } +.land .land-footer h5 { margin: 0 0 16px; font-size: 14px; color: #adb5bd; font-weight: 700; } +.land .land-footer ul { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 12px; font-size: 15px; } +.land .land-footer ul a { transition: color 0.15s; } +.land .land-footer ul a:hover { color: #adb5bd; } +.land .land-footer .legal { display: flex; justify-content: space-between; padding: 32px 0; color: #6c757d; font-size: 14px; } +.land .land-footer .giant { font-size: 30vw; line-height: 0.8; font-weight: 800; letter-spacing: -0.05em; color: transparent; -webkit-text-stroke: 1.5px #ffd000; text-align: left; margin-top: 24px; user-select: none; } +@media (max-width: 900px) { .land .land-footer .cols { grid-template-columns: 1fr 1fr; } .land .land-nav nav { display: none; } } + +/* mini app mockup used on the marketing pages */ +.land .mock-app { background: #212529; color: #fff; font-size: 12px; display: grid; grid-template-columns: 180px 1fr; min-height: 420px; } +.land .mock-app .sb { background: #2b3035; padding: 12px; border-right: 1px solid #343a40; } +.land .mock-app .sb .pill { height: 26px; border-radius: 4px; background: #3d3400; padding: 0 10px; font-weight: 700; margin-bottom: 8px; } +.land .mock-app .sb .ntx { border: 1px solid #6c757d; border-radius: 6px; padding: 6px; color: #adb5bd; text-align: center; margin-bottom: 12px; } +.land .mock-app .sb .it { padding: 6px 8px; color: #fff; border-radius: 6px; } +.land .mock-app .sb .it.on { background: #3d3400; font-weight: 700; } +.land .mock-app .ct { padding: 14px; display: grid; gap: 12px; grid-template-columns: 1.7fr 1fr; align-items: start; } +.land .mock-app .acct { grid-column: 1 / -1; background: #2b3035; border-radius: 10px; padding: 8px 12px; display: flex; align-items: center; gap: 10px; } +.land .mock-app .pn { background: #2b3035; border-radius: 10px; padding: 14px; } +.land .mock-app .big { font-size: 26px; font-weight: 800; letter-spacing: -0.02em; } +.land .mock-app .big span { color: #6c757d; } +.land .mock-app .mb { display: inline-block; background: #343a40; border-radius: 6px; padding: 4px 10px; margin-right: 6px; margin-top: 8px; } +.land .mock-app .li { display: flex; justify-content: space-between; padding: 8px 0; border-top: 1px solid #343a40; } +.land .mock-app .ok { color: #75b798; font-weight: 700; } +.land .mock-app .rv { color: #ffcd39; font-weight: 700; } +.land .mock-app .ident { width: 24px; height: 24px; border-radius: 50%; background: conic-gradient(#ffd000, #6c757d, #adb5bd, #ffd000); } + +@media (max-width: 860px) { + .land .shell { flex-direction: column; } + .land .sidebar { width: auto; height: auto; position: static; flex-direction: row; flex-wrap: wrap; align-items: center; gap: 6px; padding: 10px 12px; } + .land .newtx { margin: 0; flex: 1 1 100%; } + .land .nav { flex-direction: row; flex-wrap: wrap; } + .land .nav button.active::before { display: none; } + .land .nav-group, .land .sidebar-bottom { display: none; } + .land .main { padding: 16px; } +} + +/* ================================================================== identity: tape, section index, marker, live scorer, mode strip */ +.land .tape { height: 10px; background: repeating-linear-gradient(-45deg, #ffd000 0 16px, #212529 16px 32px); background-size: 45.25px 100%; } +.land .tape.thin { height: 6px; } +.land .tape.run { animation: tape 0.9s linear infinite; } +@keyframes tape { to { background-position: 45.25px 0; } } +.land .secno { display: flex; align-items: center; gap: 12px; font-family: "DM Mono", monospace; font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase; color: #6c757d; margin-bottom: 18px; } +.land .secno::before { content: ""; width: 28px; height: 3px; background: #ffd000; flex: none; } +.land .dark .secno { color: #adb5bd; } +.land .mark-hl { background-image: linear-gradient(180deg, transparent 60%, #ffd000 60%, #ffd000 92%, transparent 92%); background-repeat: no-repeat; background-size: 0% 100%; -webkit-box-decoration-break: clone; box-decoration-break: clone; animation: hlin 0.8s var(--ease) 0.7s forwards; } +@keyframes hlin { to { background-size: 100% 100%; } } +.land .hero-meta { margin-top: 32px; display: flex; gap: 24px; flex-wrap: wrap; justify-content: center; font-family: "DM Mono", monospace; font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: #6c757d; animation: rise 0.7s var(--ease) 0.4s both; } +.land .hero-meta span { display: inline-flex; align-items: center; gap: 8px; } +.land .hero-meta span::before { content: ""; width: 8px; height: 3px; background: #ffd000; } + +.land .ticker { background: #212529; color: #fff; border-radius: 8px; box-shadow: 0 30px 80px rgba(33,37,41,0.28); overflow: hidden; font-size: 14px; } +.land .ticker .head { display: flex; justify-content: space-between; padding: 14px 18px; border-bottom: 1px solid #343a40; font-family: "DM Mono", monospace; font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: #adb5bd; } +.land .ticker .head b { color: #ffd000; font-weight: 500; } +.land .ticker .body { padding: 20px 18px 16px; display: grid; gap: 14px; } +.land .ticker .row { display: flex; justify-content: space-between; gap: 12px; align-items: baseline; } +.land .ticker .row > span:first-child { color: #adb5bd; font-size: 13px; } +.land .ticker .amt { font-family: "Space Grotesk", sans-serif; font-size: 30px; font-weight: 700; letter-spacing: -0.02em; } +.land .ticker .to { font-family: "DM Mono", monospace; font-size: 12.5px; color: #dee2e6; } +.land .ticker .prog { height: 6px; background: #343a40; border-radius: 3px; overflow: hidden; } +.land .ticker .prog i { display: block; height: 100%; background: #ffd000; width: 0; } +.land .ticker .prog.on i { animation: growx 1.1s var(--ease) both; } +.land .ticker .prog.done i { width: 100%; } +@keyframes growx { from { width: 0; } to { width: 100%; } } +.land .ticker .verdict { display: flex; align-items: center; gap: 10px; min-height: 34px; flex-wrap: wrap; } +.land .ticker .tier { font-family: "DM Mono", monospace; font-size: 11px; letter-spacing: 0.1em; padding: 7px 10px; border-radius: 4px; font-weight: 500; color: #212529; animation: pop 0.3s var(--ease) both; } +.land .ticker .tier.low { background: #ffd000; } +.land .ticker .tier.high { background: #ffa94d; } +.land .ticker .tier.critical { background: #ea868f; } +.land .ticker .tier.blocked { background: #fff; } +.land .ticker .scoring { color: #adb5bd; font-family: "DM Mono", monospace; font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; } +.land .ticker .why { list-style: none; padding: 0; margin: 0; display: grid; gap: 7px; color: #dee2e6; font-size: 13px; min-height: 66px; } +.land .ticker .why li { animation: rise 0.4s var(--ease) both; padding-left: 18px; position: relative; } +.land .ticker .why li::before { content: ""; position: absolute; left: 0; top: 8px; width: 9px; height: 3px; background: #ffd000; } +.land .ticker .foot { padding: 12px 18px; border-top: 1px solid #343a40; font-size: 12px; color: #adb5bd; display: flex; justify-content: space-between; gap: 12px; } +.land .ticker .dots { display: flex; gap: 6px; } +.land .ticker .dots i { width: 18px; height: 3px; background: #495057; } +.land .ticker .dots i.on { background: #ffd000; } +.land .mode-strip { height: 6px; margin: -20px -24px 16px; background: transparent; transition: background 0.3s; } +.land .mode-strip.elevated { background: #ffd000; } +.land .mode-strip.tape { background: repeating-linear-gradient(-45deg, #ffd000 0 16px, #212529 16px 32px); background-size: 45.25px 100%; } +@media (prefers-reduced-motion: reduce) { .land .tape.run, .land .mark-hl, .land .ticker .prog.on i, .land .ticker .why li, .land .ticker .tier { animation: none; } .land .mark-hl { background-size: 100% 100%; } .land .ticker .prog.on i { width: 100%; } } + +/* ================================================================== phone mockups (marketing) + Authored at true device scale (393 x 852 points) and scaled as a unit, so the app's own type sizes + and spacing survive. --pwn is unitless: calc() dividing two lengths yields a length, which scale() + would reject. */ +.land .phone { --pwn: 268; --scale: calc(var(--pwn) / 417); position: relative; width: calc(var(--pwn) * 1px); height: calc(876px * var(--scale)); flex: none; } +.land .phone-device { position: absolute; top: 0; left: 0; width: 417px; height: 876px; transform: scale(var(--scale)); transform-origin: top left; background: #0b0d10; border-radius: 58px; padding: 12px; box-shadow: 0 40px 90px rgba(16,20,24,0.3), 0 0 0 1px rgba(255,255,255,0.08) inset; } +.land .ph-island { position: absolute; top: 24px; left: 50%; transform: translateX(-50%); width: 122px; height: 35px; background: #0b0d10; border-radius: 20px; z-index: 3; } +.land .phone-screen { position: relative; width: 100%; height: 100%; border-radius: 46px; background: #fff; overflow: hidden; display: flex; flex-direction: column; color: #101418; font-size: 15px; } +.land .ph-home-ind { position: absolute; bottom: 9px; left: 50%; transform: translateX(-50%); width: 140px; height: 5px; border-radius: 3px; background: #101418; opacity: 0.85; } +.land .ph-status { height: 56px; padding: 16px 30px 0; display: flex; align-items: center; justify-content: space-between; font-size: 16px; } +.land .ph-status b { font-family: "DM Sans", sans-serif; font-weight: 700; letter-spacing: -0.2px; } +.land .ph-status-icons { display: flex; align-items: center; gap: 6px; fill: #101418; color: #101418; } +.land .ph-body { flex: 1; padding: 6px 16px 0; display: flex; flex-direction: column; gap: 10px; overflow: hidden; } + +.land .ph-top { display: flex; align-items: center; gap: 10px; height: 44px; } +.land .ph-top > b { font-size: 17px; font-weight: 700; } +.land .ph-ident { position: relative; width: 36px; height: 36px; border-radius: 50%; background: conic-gradient(#2ec27e, #d8b4fe, #7dd3fc, #2ec27e); flex: none; } +.land .ph-ident i { position: absolute; top: -6px; left: 16px; background: #ffd000; color: #101418; font-style: normal; font-size: 10px; font-weight: 800; border-radius: 8px; padding: 1px 5px; border: 2px solid #fff; } +.land .ph-caret { color: #98a2b3; font-size: 13px; } +.land .ph-caret.r { margin-left: auto; } +.land .ph-top-btns { margin-left: auto; display: flex; gap: 8px; } +.land .ph-top-btns i { width: 40px; height: 40px; border-radius: 50%; background: #f6f7f9; display: block; position: relative; } +.land .ph-top-btns i::before { content: ""; position: absolute; inset: 13px; border: 2px solid #101418; border-radius: 3px; -webkit-mask: linear-gradient(#000 0 0) top/100% 4px no-repeat, linear-gradient(#000 0 0) bottom/100% 4px no-repeat, linear-gradient(#000 0 0) left/4px 100% no-repeat, linear-gradient(#000 0 0) right/4px 100% no-repeat; } +.land .ph-top-btns i.bell::before { inset: 11px 13px 12px; border: 0; background: #101418; -webkit-mask: none; border-radius: 7px 7px 2px 2px; } +.land .ph-top-btns i.bell::after { content: ""; position: absolute; top: 8px; right: 8px; width: 8px; height: 8px; border-radius: 50%; background: #ffd000; } +.land .ph-pending { display: flex; align-items: center; gap: 10px; background: #fff6cc; color: #8a6d00; border-radius: 14px; padding: 0 14px; height: 48px; font-weight: 700; font-size: 15px; } +.land .ph-pending .n { width: 22px; height: 22px; border-radius: 50%; background: #ffd000; color: #101418; display: grid; place-items: center; font-size: 12px; } +.land .ph-chain { display: flex; align-items: center; gap: 8px; font-size: 14px; } +.land .ph-chain small { color: #98a2b3; font-family: "DM Mono", monospace; font-size: 13px; } +.land .ph-ark { width: 22px; height: 22px; border-radius: 50%; background: #ffd000; color: #101418; display: grid; place-items: center; font-size: 11px; font-weight: 800; } +.land .ph-balance { font-family: "Space Grotesk", sans-serif; font-size: 44px; font-weight: 700; letter-spacing: -0.04em; line-height: 1; } +.land .ph-balance small { font-size: 20px; color: #667085; margin-left: 8px; letter-spacing: -0.02em; } +.land .ph-actions { display: flex; gap: 10px; } +.land .ph-actions span { flex: 1; height: 48px; display: grid; place-items: center; border-radius: 14px; background: #f6f7f9; font-weight: 700; font-size: 15px; } +.land .ph-actions .go { background: #ffd000; } +.land .ph-label { font-family: "DM Mono", monospace; font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: #98a2b3; margin-top: 4px; } +.land .ph-row { display: flex; align-items: center; gap: 12px; padding: 11px 0; border-bottom: 1px solid #eceef2; } +.land .ph-row.last, .land .ph-card .ph-row:last-child { border-bottom: 0; } +.land .ph-card { background: #f6f7f9; border-radius: 16px; padding: 0 14px; } +.land .ph-card.pad { padding: 4px 14px; } +.land .ph-mark, .land .ph-circle { border-radius: 50%; display: grid; place-items: center; flex: none; font-family: "Space Grotesk", sans-serif; font-weight: 700; } +.land .ph-circle { width: 40px; height: 40px; background: #eceef2; font-size: 16px; } +.land .ph-circle.sm { width: 34px; height: 34px; font-size: 15px; } +.land .ph-name { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; } +.land .ph-name b { font-size: 15px; font-weight: 700; } +.land .ph-name small { font-size: 13px; color: #667085; } +.land .ph-amt { font-size: 15px; font-weight: 700; } +.land .ph-badge { background: #eceef2; color: #667085; border-radius: 99px; padding: 4px 9px; font-weight: 700; font-size: 12px; } +.land .ph-badge.warn { background: #fff3d6; color: #a86a00; } +.land .ph-title { font-family: "Space Grotesk", sans-serif; font-size: 22px; font-weight: 700; display: flex; align-items: center; gap: 10px; height: 44px; } +.land .ph-title.back { font-size: 18px; } +.land .ph-title .ph-plus { margin-left: auto; width: 40px; height: 40px; border-radius: 50%; background: #ffd000; display: grid; place-items: center; font-size: 20px; } +.land .ph-tabsline { display: flex; gap: 22px; border-bottom: 1px solid #eceef2; font-size: 15px; } +.land .ph-tabsline b { border-bottom: 2px solid #ffd000; padding-bottom: 9px; } +.land .ph-tabsline span { color: #98a2b3; padding-bottom: 9px; } +.land .ph-chips { display: flex; gap: 8px; } +.land .ph-chips b, .land .ph-chips span { border-radius: 99px; padding: 6px 12px; font-size: 12px; font-weight: 700; background: #f6f7f9; color: #667085; } +.land .ph-chips b { background: #ffd000; color: #101418; } +.land .ph-hero { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 8px 0 4px; } +.land .ph-hero-mark { position: relative; } +.land .ph-hero-mark i { position: absolute; right: -4px; top: -4px; width: 24px; height: 24px; border-radius: 50%; background: #b42318; color: #fff; font-style: normal; font-size: 12px; display: grid; place-items: center; border: 3px solid #fff; } +.land .ph-hero b { font-family: "Space Grotesk", sans-serif; font-size: 30px; letter-spacing: -0.03em; } +.land .ph-hero small { color: #667085; font-size: 14px; } +.land .ph-kv { display: flex; justify-content: space-between; align-items: center; padding: 12px 0; border-bottom: 1px solid #eceef2; font-size: 14px; } +.land .ph-kv.last { border-bottom: 0; } +.land .ph-kv span { color: #667085; } +.land .ph-sign { margin-top: auto; margin-bottom: 12px; background: #ffd000; border-radius: 16px; height: 56px; padding: 0 18px; display: flex; align-items: center; gap: 10px; font-size: 16px; } +.land .ph-sign b { font-weight: 700; } +.land .ph-sign .who { margin-left: auto; background: rgba(16,20,24,0.12); border-radius: 99px; padding: 5px 10px; font-family: "DM Mono", monospace; font-size: 12px; } +.land .ph-tabs { height: 84px; border-top: 1px solid #eceef2; display: flex; align-items: flex-start; padding-top: 14px; } +.land .ph-tabs span { flex: 1; text-align: center; font-size: 24px; color: #98a2b3; position: relative; } +.land .ph-tabs span.on { color: #ffd000; } +.land .ph-tab-badge { position: absolute; top: -4px; left: 55%; background: #ffd000; color: #101418; font-style: normal; font-size: 11px; font-weight: 800; border-radius: 99px; padding: 1px 6px; } +.land .phones { display: flex; gap: 30px; justify-content: center; align-items: flex-start; flex-wrap: wrap; } +.land .phones .phone:nth-child(2) { margin-top: -26px; } +@media (max-width: 900px) { .land .phones .phone:nth-child(2) { margin-top: 0; } .land .phone { --pwn: 240; } } + +/* The landing's mobile strip: copy on the left, two phones on the right, the card sized by both. */ +.land .mstrip { background: #fff; border: 1px solid #e9ecef; border-radius: 8px; padding: 48px; display: grid; grid-template-columns: 1fr auto; gap: 40px; align-items: center; } +.land .mstrip h2 { text-align: left; margin-bottom: 16px; } +.land .mstrip p { color: #6c757d; max-width: 380px; margin-bottom: 26px; } +.land .mstrip ul { list-style: none; padding: 0; margin: 0 0 26px; display: flex; flex-direction: column; gap: 14px; font-size: 17px; font-weight: 700; color: #212529; } +.land .mstrip ul li { display: flex; gap: 12px; align-items: center; } +.land .mstrip ul li svg { color: #212529; background: #ffd000; border-radius: 4px; padding: 3px; width: 24px; height: 24px; flex: none; } +.land .phones.two { display: flex; gap: 16px; align-items: flex-start; } +.land .phones.two .phone { --pwn: 196; } +.land .phones.two .phone:nth-child(2) { margin-top: 30px; } +@media (max-width: 980px) { .land .mstrip { grid-template-columns: 1fr; } .land .phones.two { justify-content: center; } .land .phones.two .phone:nth-child(2) { display: none; } } + +/* ================================================================== documentation */ +.land .docs { max-width: 1216px; margin: 0 auto; padding: 0 24px; display: grid; grid-template-columns: 236px minmax(0, 1fr); gap: 56px; align-items: start; } +.land .docs-nav { position: sticky; top: 96px; padding: 40px 0 60px; display: flex; flex-direction: column; gap: 22px; } +.land .docs-group { font-family: "DM Mono", monospace; font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; color: #98a2b3; margin-bottom: 8px; } +.land .docs-nav button { display: block; width: 100%; text-align: left; background: none; border: 0; padding: 7px 12px; margin-left: -12px; border-radius: 8px; font: inherit; font-size: 15px; color: #6c757d; cursor: pointer; } +.land .docs-nav button:hover { background: #f1f3f5; color: #212529; } +.land .docs-nav button.on { background: #fff6cc; color: #212529; font-weight: 700; } +.land .docs-body { padding: 40px 0 96px; min-width: 0; } +.land .docs-crumb { font-family: "DM Mono", monospace; font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; color: #98a2b3; margin-bottom: 12px; } +.land .docs-body h1 { font-family: "Space Grotesk", sans-serif; font-size: 44px; line-height: 48px; letter-spacing: -0.03em; margin-bottom: 20px; } +.land .docs-body h3 { font-family: "Space Grotesk", sans-serif; font-size: 22px; letter-spacing: -0.02em; margin: 40px 0 10px; scroll-margin-top: 96px; } +.land .docs-body p { font-size: 17px; line-height: 27px; color: #495057; margin-bottom: 16px; max-width: 70ch; } +.land .docs-body .doc-lead { font-size: 20px; line-height: 30px; color: #212529; margin-bottom: 24px; } +.land .docs-body ul, .land .docs-body ol { margin: 0 0 20px 22px; padding: 0; display: flex; flex-direction: column; gap: 10px; font-size: 17px; line-height: 26px; color: #495057; max-width: 70ch; } +.land .docs-body li b { color: #212529; } +.land .docs-body ol.doc-inv { counter-reset: inv; list-style: none; margin-left: 0; } +.land .docs-body ol.doc-inv li { counter-increment: inv; padding-left: 40px; position: relative; color: #212529; font-weight: 500; } +.land .docs-body ol.doc-inv li::before { content: counter(inv); position: absolute; left: 0; top: 1px; width: 26px; height: 26px; border-radius: 6px; background: #ffd000; color: #212529; display: grid; place-items: center; font-family: "DM Mono", monospace; font-size: 12px; font-weight: 500; } +.land .doc-note { border-left: 3px solid #ffd000; background: #fffdf2; padding: 14px 18px; border-radius: 0 8px 8px 0; font-size: 16px; line-height: 25px; color: #495057; margin: 22px 0; max-width: 70ch; } +.land .doc-table { overflow-x: auto; margin: 0 0 24px; } +.land .doc-table table { border-collapse: collapse; width: 100%; font-size: 15px; } +.land .doc-table th { text-align: left; font-family: "DM Mono", monospace; font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: #98a2b3; font-weight: 400; padding: 0 16px 10px 0; border-bottom: 1px solid #e9ecef; } +.land .doc-table td { padding: 12px 16px 12px 0; border-bottom: 1px solid #f1f3f5; color: #495057; vertical-align: top; line-height: 23px; } +.land .doc-table td:first-child { color: #212529; font-weight: 700; white-space: nowrap; } +.land .doc-table code { font-family: "DM Mono", monospace; font-size: 13px; background: #f1f3f5; padding: 2px 6px; border-radius: 4px; color: #212529; } +.land .docs-paging { display: flex; justify-content: space-between; gap: 16px; margin-top: 56px; padding-top: 24px; border-top: 1px solid #e9ecef; } +.land .docs-paging button { background: none; border: 0; font: inherit; font-weight: 700; color: #212529; cursor: pointer; padding: 8px 0; } +.land .docs-paging button:hover { color: #6c757d; } +@media (max-width: 900px) { + .land .docs { grid-template-columns: 1fr; gap: 0; } + .land .docs-nav { position: static; flex-direction: row; flex-wrap: wrap; gap: 6px; padding: 24px 0 0; } + .land .docs-nav > div { display: contents; } + .land .docs-group { display: none; } + .land .docs-nav button { width: auto; margin: 0; border: 1px solid #e9ecef; font-size: 14px; } + .land .docs-body h1 { font-size: 34px; line-height: 38px; } +} + +/* ================================================================== the phone: the mobile app as artwork */ +.land .mock-phone { width: 272px; border-radius: 42px; background: #0b0d0f; padding: 10px; border: 1px solid #343a40; box-shadow: 0 30px 60px rgba(0, 0, 0, 0.45), 0 4px 12px rgba(0, 0, 0, 0.3); } +.land .mock-phone .mp-screen { border-radius: 33px; overflow: hidden; background: #212529; color: #f8f9fa; font-size: 11px; padding: 10px 12px 0; display: flex; flex-direction: column; gap: 10px; min-height: 480px; } +.land .mp-status { display: flex; align-items: center; justify-content: space-between; color: #adb5bd; font-weight: 700; font-size: 10px; } +.land .mp-island { width: 64px; height: 16px; border-radius: 10px; background: #0b0d0f; } +.land .mp-head { display: flex; align-items: center; gap: 8px; font-size: 15px; letter-spacing: -0.01em; padding-top: 2px; } +.land .mp-card { background: #2b3035; border-radius: 14px; padding: 12px; display: flex; flex-direction: column; gap: 8px; } +.land .mp-label { color: #6c757d; font-size: 9px; letter-spacing: 1px; font-weight: 700; display: flex; justify-content: space-between; align-items: center; } +.land .mp-ok { color: #75b798; font-weight: 700; letter-spacing: 0; } +.land .mp-rv { color: #ffcd39; font-weight: 700; } +.land .mp-donut { width: 128px; height: 128px; border-radius: 50%; margin: 6px auto; background: conic-gradient(#ffd000 0 100%); display: grid; place-items: center; } +.land .mp-donut > div { width: 92px; height: 92px; border-radius: 50%; background: #2b3035; display: flex; flex-direction: column; align-items: center; justify-content: center; } +.land .mp-donut b { font-size: 22px; letter-spacing: -0.02em; } +.land .mp-donut span { color: #adb5bd; font-size: 10px; } +.land .mp-tiles { display: flex; gap: 8px; } +.land .mp-tile { flex: 1; background: #2b3035; border-radius: 12px; padding: 10px; display: flex; flex-direction: column; gap: 2px; } +.land .mp-tile b { font-size: 18px; } +.land .mp-tile span { color: #adb5bd; font-size: 10px; } +.land .mp-list .mp-row { display: flex; justify-content: space-between; gap: 8px; padding: 7px 0; border-top: 1px solid #343a40; } +.land .mp-list .mp-row:first-of-type { border-top: 0; } +.land .mp-shield { font-size: 30px; } +.land .mp-meter { height: 6px; border-radius: 3px; background: #343a40; overflow: hidden; margin-top: 4px; } +.land .mp-meter i { display: block; height: 6px; border-radius: 3px; background: #ffd000; } +.land .mp-btn { border-radius: 10px; padding: 10px; text-align: center; font-weight: 800; } +.land .mp-btn-soft { background: #343a40; color: #ffcd39; } +.land .mp-btn-danger { background: #4a1c22; color: #ea868f; } +.land .mp-tabs { margin-top: auto; display: flex; border-top: 1px solid #343a40; padding: 9px 0 12px; color: #6c757d; font-size: 8px; font-weight: 700; } .land .mp-tabs span { flex: 1; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.land .mp-tabs .on { color: #ffd000; } + +/* in the hero, the phone leans on the desktop mock */ +.land .mockwrap { position: relative; } +.land .float-phone { position: absolute; left: -40px; bottom: -40px; transform: rotate(-4deg); z-index: 3; filter: drop-shadow(0 24px 40px rgba(33, 37, 41, 0.35)); } +@media (max-width: 1100px) { .land .float-phone { display: none; } } +/* in the audience cards, it stands alone */ +.land .art-phone { display: flex; justify-content: center; padding: 18px 0 6px; } +.land .art-phone .mock-phone { transform: rotate(-2deg); } diff --git a/app/tsconfig.json b/app/tsconfig.json new file mode 100644 index 0000000..2f8fe46 --- /dev/null +++ b/app/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": [ + "dom", + "dom.iterable", + "ES2023" + ], + "allowJs": false, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./src/*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/contracts/.env.example b/contracts/.env.example deleted file mode 100644 index c338c6e..0000000 --- a/contracts/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -# Deployer key for `forge script script/Deploy.s.sol --rpc-url ark_devnet --broadcast`. -# Devnet only. For anything with real value, use a hardware wallet or `scriipture deploy --browser`. -PRIVATE_KEY=0x -# Set to true to also create a funded demo vault owned by the deployer. -DEMO_VAULT=false diff --git a/contracts/.gitignore b/contracts/.gitignore deleted file mode 100644 index c8e4586..0000000 --- a/contracts/.gitignore +++ /dev/null @@ -1,20 +0,0 @@ -# Compiler files -cache/ -out/ - -# Commonly ignored directories -.logs/ -.ignore/ - -# Ignores development broadcast logs -!/broadcast -/broadcast/*/31337/ -/broadcast/**/dry-run/ - -# Docs -docs/ - -# Dotenv file -.env -.env.* -!.env.example diff --git a/contracts/broadcast/Deploy.s.sol/9000/run-1789433571992.json b/contracts/broadcast/Deploy.s.sol/9000/run-1789433571992.json deleted file mode 100644 index a4a7f1e..0000000 --- a/contracts/broadcast/Deploy.s.sol/9000/run-1789433571992.json +++ /dev/null @@ -1,402 +0,0 @@ -{ - "transactions": [ - { - "hash": "0xf315d135cecff9193c2eaa5a13e02d83728d9b1bf1b531a8b158610241a74cee", - "transactionType": "CREATE", - "contractName": "SkurPolicyLib", - "contractAddress": "0xd587b84371f2f55ab35cfe2f64787d6829b85a2d", - "function": null, - "arguments": null, - "transaction": { - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "gas": "0xeb984", - "input": "0x60808060405234601957610c75908161001e823930815050f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c8063099fe59914610660578063194e5f5c1461018b5780631c71447d146100b957637309439114610045575f80fd5b6101403660031901126100b55761005b366109c0565b60a03660a31901126100b55761006f61082f565b60a4359081151582036100b5576020926100ab92825260c4358483015260e4356040830152610104356060830152610124356080830152610bf7565b6040519015158152f35b5f80fd5b60a03660031901126100b5576100ce366109c0565b60208101516040820151106101525760608101518015159182610144575b82610136575b50506100fa57005b6040516353e95a6760e01b81526020600482015260136024820152720e0cae4a8f09ac2f0407c40c8c2d2d8f29ac2f606b1b6044820152606490fd5b6080015110905081806100f2565b6080810151151592506100ec565b6040516353e95a6760e01b815260206004820152601060248201526f0d8deee9ac2f0407c40d0d2ced09ac2f60831b6044820152606490fd5b6102203660031901126100b5576101a13661084f565b60ff8151161561062b576020810160ff81511660ff835116116105f55760ff80604084015116915116116105bb5760ff6060820151161561057f5760a0810151151580610570575b6105365760e0810163ffffffff81511663ffffffff60c084015116116104f0576101a0820161ffff81511680151590816104dd575b506104a65761271061ffff825116118015610492575b61045d576101c083019061ffff825116801515918261044e575b50506104115761ffff612710915116116103db576101e0820161271061ffff825116116103a6575161ffff16151580610390575b6103595763ffffffff9051169063ffffffff61012082015116828111610351575b5063ffffffff61014082015116828111610349575b5063ffffffff6101008201511682811161033f575b5061016063ffffffff91015116610e10820180921161032b57106102ed57005b6040516353e95a6760e01b81526020600482015260156024820152741c1c9bdc1bdcd85b151d1b081d1bdbc81cda1bdc9d605a1b6044820152606490fd5b634e487b7160e01b5f52601160045260245ffd5b91506101606102cd565b9150826102b8565b9150826102a3565b6040516353e95a6760e01b815260206004820152600e60248201526d656e76656c6f706557696e646f7760901b6044820152606490fd5b50610e1063ffffffff6102008401511610610282565b6040516353e95a6760e01b815260206004820152600c60248201526b656e76656c6f70652062707360a01b6044820152606490fd5b6040516353e95a6760e01b815260206004820152600d60248201526c68617264426c6f636b2062707360981b6044820152606490fd5b6040516353e95a6760e01b81526020600482015260146024820152731a185c99109b1bd8dac80f0818dc9a5d1a58d85b60621b6044820152606490fd5b5161ffff16119050848061024e565b6040516353e95a6760e01b815260206004820152600c60248201526b6578706f737572652062707360a01b6044820152606490fd5b5061271061ffff6101808501511611610234565b6040516353e95a6760e01b815260206004820152600e60248201526d32bc3837b9bab9329037b93232b960911b6044820152606490fd5b905061ffff61018085015116118461021e565b6040516353e95a6760e01b815260206004820152601960248201527f64656c6179437269746963616c203c2064656c617948696768000000000000006044820152606490fd5b6040516353e95a6760e01b815260206004820152601160248201527019dd585c991a585b951a1c995cda1bdb19607a1b6044820152606490fd5b5060ff608082015116156101e9565b6040516353e95a6760e01b815260206004820152601360248201527219dbdd995c9b985b98d9551a1c995cda1bdb19606a1b6044820152606490fd5b6040516353e95a6760e01b8152602060048201526011602482015270185c1c1c9bdd985b1cd0dc9a5d1a58d85b607a1b6044820152606490fd5b6040516353e95a6760e01b815260206004820152600d60248201526c0c2e0e0e4deecc2d8e690d2ced609b1b6044820152606490fd5b6040516353e95a6760e01b815260206004820152600c60248201526b617070726f76616c734c6f7760a01b6044820152606490fd5b6104403660031901126100b5576106763661084f565b610220366102231901126100b55761068c6107fa565b6102243560ff811681036100b55781526102443560ff811681036100b55760208201526102643560ff811681036100b55760408201526102843560ff811681036100b55760608201526102a43560ff811681036100b55760808201526102c43580151581036100b55760a08201526102e43563ffffffff811681036100b55760c08201526103043563ffffffff811681036100b55760e08201526103243563ffffffff811681036100b5576101008201526103443563ffffffff811681036100b5576101208201526103643563ffffffff811681036100b5576101408201526103843563ffffffff811681036100b5576101608201526103a43561ffff811681036100b5576101808201526103c43561ffff811681036100b5576101a08201526103e43561ffff811681036100b5576101c08201526104043561ffff811681036100b5576101e0820152610424359063ffffffff821682036100b5576020926100ab92610200830152610a05565b60405190610220820182811067ffffffffffffffff82111761081b57604052565b634e487b7160e01b5f52604160045260245ffd5b6040519060a0820182811067ffffffffffffffff82111761081b57604052565b6102209060031901126100b5576108646107fa565b9060043560ff811681036100b557825260243560ff811681036100b557602083015260443560ff811681036100b557604083015260643560ff811681036100b557606083015260843560ff811681036100b557608083015260a43580151581036100b55760a083015260c43563ffffffff811681036100b55760c083015260e43563ffffffff811681036100b55760e08301526101043563ffffffff811681036100b5576101008301526101243563ffffffff811681036100b5576101208301526101443563ffffffff811681036100b5576101408301526101643563ffffffff811681036100b5576101608301526101843561ffff811681036100b5576101808301526101a43561ffff811681036100b5576101a08301526101c43561ffff811681036100b5576101c08301526101e43561ffff811681036100b5576101e08301526102043563ffffffff811681036100b557610200830152565b60a09060031901126100b5576109d461082f565b9060043580151581036100b55782526024356020830152604435604083015260643560608301526084356080830152565b60ff82511660ff82511611610be45760ff60208301511660ff60208301511611610be45760ff60408301511660ff60408301511611610be45760ff60608301511660ff60608301511611610be45760ff60808301511660ff60808301511611610be45760a0810151151580610beb575b610be45763ffffffff60c08301511663ffffffff60c08301511611610be45763ffffffff60e08301511663ffffffff60e08301511611610be45763ffffffff6101008301511663ffffffff6101008301511611610be45763ffffffff6101208301511663ffffffff6101208301511611610be45763ffffffff6101408301511663ffffffff6101408301511611610be45763ffffffff6101608301511663ffffffff6101608301511610610be457610b4061ffff6101808301511661ffff6101808501511690610c5e565b610be457610b6161ffff6101a08301511661ffff6101a08501511690610c5e565b610be457610b8261ffff6101c08301511661ffff6101c08501511690610c5e565b610be45761ffff6101e082015116610ba361ffff6101e08501511682610c5e565b610bdc5715159182610bbf575b5050610bba575f90565b600190565b63ffffffff80929350610200809201511692015116115f80610bb0565b505050600190565b5050600190565b5060a082015115610a75565b9081511580610c54575b610be4576020810151602083015110610be4576040810151604083015110610be457610c366060830151606083015190610c5e565b610be457608080610c4c93015191015190610c5e565b610bba575f90565b5080511515610c01565b908115610c6f578015610be4571190565b50505f9056", - "nonce": "0x0", - "chainId": "0x2328" - }, - "additionalContracts": [], - "isFixedGasLimit": false - }, - { - "hash": "0xbe1f6926a7daaf7d0d004917a5636eea4ec79feec92de7990ff1309bc6cac2f9", - "transactionType": "CREATE", - "contractName": "SkurTestUSD", - "contractAddress": "0x1aa8c753ea542d6ccebc451d0d9cf7e8f2789835", - "function": null, - "arguments": null, - "transaction": { - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "to": null, - "gas": "0x98df4", - "value": "0x0", - "input": "0x60806040523461031757604080519081016001600160401b0381118282101761022a576040908152600d82526c14dadd5c8815195cdd081554d1609a1b602083015280519081016001600160401b0381118282101761022a5760405260048152631cd554d160e21b602082015281516001600160401b03811161022a57600354600181811c9116801561030d575b602082101461020c57601f81116102aa575b50602092601f821160011461024957928192935f9261023e575b50508160011b915f199060031b1c1916176003555b80516001600160401b03811161022a57600454600181811c91168015610220575b602082101461020c57601f81116101a9575b50602091601f8211600114610149579181925f9261013e575b50508160011b915f199060031b1c1916176004555b6040516106b5908161031c8239f35b015190505f8061011a565b601f1982169260045f52805f20915f5b85811061019157508360019510610179575b505050811b0160045561012f565b01515f1960f88460031b161c191690555f808061016b565b91926020600181928685015181550194019201610159565b60045f527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b601f830160051c81019160208410610202575b601f0160051c01905b8181106101f75750610101565b5f81556001016101ea565b90915081906101e1565b634e487b7160e01b5f52602260045260245ffd5b90607f16906100ef565b634e487b7160e01b5f52604160045260245ffd5b015190505f806100b9565b601f1982169360035f52805f20915f5b868110610292575083600195961061027a575b505050811b016003556100ce565b01515f1960f88460031b161c191690555f808061026c565b91926020600181928685015181550194019201610259565b60035f527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b601f830160051c81019160208410610303575b601f0160051c01905b8181106102f8575061009f565b5f81556001016102eb565b90915081906102e2565b90607f169061008d565b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c90816306fdde03146104e657508063095ea7b31461046457806318160ddd1461044757806323b872dd14610367578063313ce5671461034c57806340c10f191461028c57806370a082311461025557806395d89b411461013a578063a9059cbb14610109578063dd62ed3e146100b95763f0292a0314610095575f80fd5b346100b5575f3660031901126100b55760206040516509184e72a0008152f35b5f80fd5b346100b55760403660031901126100b5576100d26105df565b6100da6105f5565b6001600160a01b039182165f908152600160209081526040808320949093168252928352819020549051908152f35b346100b55760403660031901126100b55761012f6101256105df565b602435903361060b565b602060405160018152f35b346100b5575f3660031901126100b5576040515f6004548060011c9060018116801561024b575b6020831081146102375782855290811561021b57506001146101c6575b50819003601f01601f191681019067ffffffffffffffff8211818310176101b2576101ae829182604052826105b5565b0390f35b634e487b7160e01b5f52604160045260245ffd5b905060045f527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b5f905b8282106102055750602091508201018261017e565b60018160209254838588010152019101906101f0565b90506020925060ff191682840152151560051b8201018261017e565b634e487b7160e01b5f52602260045260245ffd5b91607f1691610161565b346100b55760203660031901126100b5576001600160a01b036102766105df565b165f525f602052602060405f2054604051908152f35b346100b55760403660031901126100b5576102a56105df565b602435906509184e72a000821161033d576001600160a01b031690811561032a57600254908082018092116103165760207fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef915f9360025584845283825260408420818154019055604051908152a3005b634e487b7160e01b5f52601160045260245ffd5b63ec442f0560e01b5f525f60045260245ffd5b638b01e07b60e01b5f5260045ffd5b346100b5575f3660031901126100b557602060405160068152f35b346100b55760603660031901126100b5576103806105df565b6103886105f5565b6001600160a01b0382165f8181526001602081815260408084203385529091529091205491936044359392909181016103c7575b5061012f935061060b565b83811061042c5784156104195733156104065761012f945f52600160205260405f2060018060a01b0333165f526020528360405f2091039055846103bc565b634a1406b160e11b5f525f60045260245ffd5b63e602df0560e01b5f525f60045260245ffd5b8390637dc7a0d960e11b5f523360045260245260445260645ffd5b346100b5575f3660031901126100b5576020600254604051908152f35b346100b55760403660031901126100b55761047d6105df565b602435903315610419576001600160a01b031690811561040657335f52600160205260405f20825f526020528060405f20556040519081527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560203392a3602060405160018152f35b346100b5575f3660031901126100b5575f6003548060011c906001811680156105ab575b6020831081146102375782855290811561021b57506001146105565750819003601f01601f191681019067ffffffffffffffff8211818310176101b2576101ae829182604052826105b5565b905060035f527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b5f905b8282106105955750602091508201018261017e565b6001816020925483858801015201910190610580565b91607f169161050a565b602060409281835280519182918282860152018484015e5f828201840152601f01601f1916010190565b600435906001600160a01b03821682036100b557565b602435906001600160a01b03821682036100b557565b6001600160a01b03169081156106a2576001600160a01b031691821561032a57815f525f60205260405f205481811061068957817fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef92602092855f525f84520360405f2055845f525f825260405f20818154019055604051908152a3565b8263391434e360e21b5f5260045260245260445260645ffd5b634b637e8f60e11b5f525f60045260245ffd", - "nonce": "0x1", - "chainId": "0x2328" - }, - "additionalContracts": [], - "isFixedGasLimit": false - }, - { - "hash": "0x613a6936596ef96adaafa9754739306bc23d0999f3c2edf17f2c1f7b8a063e9a", - "transactionType": "CREATE", - "contractName": "SkurFactory", - "contractAddress": "0x7a9c532528482b93191a68c6311762936dab60d5", - "function": null, - "arguments": null, - "transaction": { - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "to": null, - "gas": "0x6c95b5", - "value": "0x0", - "input": "0x60a080604052346081576157738181016001600160401b03811183821017606d5782916109a8833903905ff080156062576001600160a01b0316608052604051610922908161008682396080518181816101fc0152818161033401526107e90152f35b6040513d5f823e3d90fd5b634e487b7160e01b5f52604160045260245ffd5b5f80fdfe6080806040526004361015610012575f80fd5b5f905f3560e01c908163089ec70e14610783575080635492a6ec1461022b5780635c60da1b146101e6578063652b9b41146101a75780636cc811f8146100e05780638c13a4531461008a5763a7c6a1001461006b575f80fd5b3461008757806003193601126100875760209054604051908152f35b80fd5b5034610087576020366003190112610087576004359080548210156100cc5780805260208082209092015460405160039290921b1c6001600160a01b03168152f35b634e487b7160e01b81526032600452602490fd5b5034610087576020366003190112610087576001600160a01b0361010261084c565b168152600260205260408120604051908160208254918281520190819285526020852090855b818110610188575050508261013e910383610893565b604051928392602084019060208552518091526040840192915b818110610166575050500390f35b82516001600160a01b0316845285945060209384019390920191600101610158565b82546001600160a01b0316845260209093019260019283019201610128565b50346100875760203660031901126100875760209060ff906040906001600160a01b036101d261084c565b168152600184522054166040519015158152f35b50346100875780600319360112610087576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b34610468576102c03660031901126104685760043560243567ffffffffffffffff811161046857610260903690600401610862565b919060443567ffffffffffffffff811161046857610282903690600401610862565b9390610220366063190112610468576102843567ffffffffffffffff8111610468576102b2903690600401610862565b6102a435929167ffffffffffffffff841161046857366023850112156104685783600401359267ffffffffffffffff84116104685736602460a0860287010111610468576040805133602082019081529181018a905261031f81606081015b03601f198101835282610893565b5190206e5af43d82803e903d91602b57fd5bf37f0000000000000000000000000000000000000000000000000000000000000000763d602d80600a3d3981f3363d3d373d3d3d363d7300000062ffffff8260881c16175f5260781b1760205260018060a01b0390603760095ff51698891561077457895f52600160205260405f20600160ff198254161790555f54600160401b811015610454578060016103c992015f555f6108b5565b81546001600160a01b0360039290921b91821b1916908c901b1790555f5b8781101561046c57600581901b8901356001600160a01b03811690819003610468575f52600260205260405f20908154600160401b81101561045457610435816001948f93860181556108b5565b819291549060031b91821b91858060a01b03901b1916179055016103e7565b634e487b7160e01b5f52604160045260245ffd5b5f80fd5b50899697895f54905f19820191821161076057604051908152897fc18dea5f6b30c3fd606e8e38832933e2fa57407dd77800aad0658505ee94df9860203393a4873b1561046857604051632d8a98d960e01b81526102a06004820152986020916104db916102a48c01916108de565b8981036003190160248b01528281520191905f905b80821061073b5750505060643560ff811680910361046857604488015260843560ff811680910361046857606488015260a43560ff811680910361046857608488015260c43560ff81168091036104685760a488015260e43560ff81168091036104685760c4880152610104358015158091036104685760e48801526101243563ffffffff8116809103610468576101048801526101443563ffffffff8116809103610468576101248801526101643563ffffffff8116809103610468576101448801526101843563ffffffff8116809103610468576101648801526101a43563ffffffff8116809103610468576101848801526101c43563ffffffff8116809103610468576101a48801526101e43561ffff8116809103610468576101c48801526102043561ffff8116809103610468576101e48801526102243561ffff8116809103610468576102048801526102443561ffff811680910361046857610224880152610264359063ffffffff82168092036104685761068d90889695946024946020946102448a0152600319898403016102648a01526108de565b85810360031901610284870152838152019201905f905b8082106106eb5750505090805f92038183855af19182156106e0576020926106d0575b50604051908152f35b5f6106da91610893565b5f6106c7565b6040513d5f823e3d90fd5b919350918335908115158092036104685760a081600193829352602087013560208201526040870135604082015260608701356060820152608087013560808201520194019201859392916106a4565b90919283359060ff8216809203610468576020816001938293520194019201906104f0565b634e487b7160e01b5f52601160045260245ffd5b63b06ebf3d60e01b5f5260045ffd5b346104685760403660031901126104685760209061079f61084c565b6001600160a01b03168282019081526024356020820152906107c48160408401610311565b519020604051903060388301526f5af43d82803e903d91602b57fd5bf3ff60248301527f00000000000000000000000000000000000000000000000000000000000000006014830152733d602d80600a3d3981f3363d3d373d3d3d363d73825260588201526037600c82012060788201526055604360018060a01b0392012016604051908152f35b600435906001600160a01b038216820361046857565b9181601f840112156104685782359167ffffffffffffffff8311610468576020808501948460051b01011161046857565b90601f8019910116810190811067ffffffffffffffff82111761045457604052565b80548210156108ca575f5260205f2001905f90565b634e487b7160e01b5f52603260045260245ffd5b916020908281520191905f905b8082106108f85750505090565b90919283359060018060a01b038216809203610468576020816001938293520194019201906108eb566080806040523460255760015f55600160ff1981541617600155615749908161002a8239f35b5f80fdfe6080806040526004361015610062575b50361561001a575f80fd5b5f8052601160205260405f20610031348254612c7c565b90555f6040513481527f5548c837ab068cf56a2c2479df0882a4922fd203edb7517321831d95078c5f6260203392a3005b5f905f3560e01c9081630db026221461278357508063128e9be6146127605780632014055d146126b55780632358d5a81461266c57806328dca5ba14612642578063295a5212146126165780632d8a98d9146121f55780632de94807146121ba5780633a5d0d6f1461210a57806340e58ee514612072578063460fc01914611ea05780635305548114611e6757806354387ad714611e4357806358355ead14611e1c57806362812a3914611d6257806367e4ac2c14611ce457806368d525c714611b7957806378f3c95a14611a5b5780638ec1f4ca14611a3757806397feb926146119745780639d8b9a49146118ec5780639eab5253146117bf578063a15a2f8b14611786578063a2380d351461175d578063a8badaa5146116a8578063b314c57014611580578063b52f393e14611555578063b759f95414611535578063bacd2e9714611517578063bf81bf431461142b578063c0555a0a1461139b578063c65df7941461135c578063c75e0df21461120c578063c7f758a814610f26578063ce1e462614610e85578063d0ee62c714610939578063da35c6641461091b578063db02466414610574578063ea70b4af14610556578063ee5248941461026f5763fe0d94c10361000f573461026c57602036600319011261026c57600281541461025d576002815561025660043561385a565b6001815580f35b633ee5aeb560e01b8152600490fd5b80fd5b503461026c5761022036600319011261026c5761028a61486f565b73d587b84371f2f55ab35cfe2f64787d6829b85a2d803b1561052a5760405163065397d760e21b815282906102c1600482016136b2565b818161022481865af4801561053657610541575b50506020610444916040519283809263099fe59960e01b825260025460ff8116600484015260ff8160081c16602484015260ff8160101c16604484015260ff8160181c16606484015260ff81871c16608484015260ff8160281c16151560a484015263ffffffff8160301c1660c484015263ffffffff8160501c1660e484015263ffffffff8160701c1661010484015263ffffffff8160901c1661012484015263ffffffff8160b01c1661014484015263ffffffff8160d01c1661016484015260f01c61018483015263ffffffff60035461ffff81166101a485015261ffff8160101c166101c485015261ffff81881c166101e485015260301c166102048301526103e361022483016136b2565b5af49081156105365782916104f8575b5060405190610404602083016136b2565b6102208252610415610240836129d7565b61041e8161495e565b9183600260ff60015460081c16610434816127cc565b1490816104f0575b816104e4575b506104d5579261045b6104cd9260209583806001614cd3565b8082526012855260408220600254815463ff00000090911664ffff0000001990911617602084901b64ff000000001617815590936104b7906104ae9063ffffffff905b166001600160401b034216612c2e565b6004830161368a565b604051916104c586846129d7565b825283614f18565b604051908152f35b6372b89e9760e11b8452600484fd5b9450600190505f610442565b83915061043c565b90506020813d60201161052e575b81610513602093836129d7565b8101031261052a5761052490612ba2565b5f6103f3565b5080fd5b3d9150610506565b6040513d84823e3d90fd5b8161054b916129d7565b61052a57815f6102d5565b503461026c578060031936011261026c576020600554604051908152f35b503461026c57604036600319011261026c5761058e6127a0565b906105976127b6565b61059f6148c5565b6001600160a01b031691821561090c576001600160a01b03811682526008602052604082205460ff16156108fd57828252600860205260ff6040832054166108ee5760ff60025460201c16156108b457604051926020840152602083526106076040846129d7565b61061260045461381e565b908160045561062260055461381e565b600590815582845260126020526040842080546bffffffffffffffffffff0000163360601b6001600160601b031916176101061781556002810180546001600160a01b0319166001600160a01b0390941693909317909255600382018490558451939082016001600160401b0385116108a05761069f81546135b4565b601f811161085b575b506020958690601f87116001146107f6579580916104cd969785926107eb575b50508160011b915f199060031b1c19161790555b60ff60281b19825416825560015463ffffffff60401b83549160301b169063ffffffff60401b191617825560046001600160401b03421692016001600160401b0383166001600160401b031982541617815561074363ffffffff60025460d01c1684612c2e565b815467ffffffffffffffff60801b191660809190911b67ffffffffffffffff60801b161790558381526012855260408120805463ff000000198116825560025464ffff0000001990911690871c60201b64ff0000000016178155916107c1906107b89063ffffffff60025460b01c1690612c2e565b6004840161368a565b50604051906107d16040836129d7565b60088252677265636f7665727960c01b8583015283614f18565b015190505f806106c8565b8284528184209190601f198816855b81811061084457509160019391896104cd999a941061082c575b505050811b0190556106dc565b01515f1960f88460031b161c191690555f808061081f565b92938a600181928786015181550195019301610805565b81835260208320601f870160051c81019160208810610896575b601f0160051c01905b81811061088b57506106a8565b83815560010161087e565b9091508190610875565b634e487b7160e01b82526041600452602482fd5b604051637417de8f60e01b815260206004820152601160248201527019dd585c991a585b951a1c995cda1bdb19607a1b6044820152606490fd5b630c8ce74760e11b8252600482fd5b63148fe22160e11b8252600482fd5b63d92e233d60e01b8252600482fd5b503461026c578060031936011261026c576020600454604051908152f35b503461026c57608036600319011261026c576109536127a0565b9061095c6127b6565b60443590606435906001600160401b038211610e815736602383011215610e81578160040135936001600160401b03851161026c57366024868501011161026c576109a561489a565b600260ff60015460081c166109b9816127cc565b14610e72576001600160a01b03821694851561090c578415610e63576109e0858489612d80565b9260018060a01b0388168352600d602052610a4560408420600460405191610a0783612970565b60ff81541615158352600181015460208401526002810154604084015260038101546060840152015460808201528761ffff60408801511691614c58565b868352600e602052604083209060ff8254166006811015610e4f5715610e40575b506001600160401b03421697610a8663ffffffff60a0870151168a612c2e565b915460ff81166006811015610e2c5760011480610e0e575b610dfa575b5060209860405198610ab58b8b6129d7565b858a52610ac360045461381e565b9889600455610ad360055461381e565b60059081558a885260128d526040882080546bffffffffffffffffffff0000163360601b6001600160601b031916176101001781556002810180546001600160a01b03191690941790935560038301919091558a519a908201906001600160401b038c11610de657908b8d979695949392610b4e83546135b4565b601f8111610d78575b50601f899211600114610d065792610bec928e6104cd9e9f600494610ccf9a9998978e92610cfb575b50508160011b915f199060031b1c19161790555b60ff60281b19815416815560015463ffffffff60401b82549160301b169063ffffffff60401b191617815501916001600160401b0381166001600160401b031984541617835563ffffffff60025460d01c1690612c2e565b8154906001600160401b0360801b9060801b16906001600160401b0360801b191617905589865260128552610ca560ff608060408920998051610c2e816127cc565b610c37816127cc565b8b5462ff000067ffff0000000000008c85015160301b169260101b169067ffff000000ff0000191617178b55610c88836060830151168c9063ff00000082549160181b169063ff0000001916179055565b0151895464ff000000001916911660201b64ff0000000016178855565b6001870180546001600160a01b0319166001600160a01b039092169190911790556004860161368a565b806024610cdb82612c61565b96610ce960405198896129d7565b82885201838701378401015283614f18565b015190505f80610b80565b828a52888a209190601f198f168b5b818110610d6157506104cd9e9f600494610ccf9a99989794610bec97948360019510610d49575b505050811b019055610b94565b01515f1960f88460031b161c191690555f8080610d3c565b92938b600181928786015181550195019301610d15565b838b9a929495969798999a52818b2091601f840160051c83019310610ddc575b918f99989796959493918f93601f0160051c01905b818110610dba5750610b57565b90925060019193949596979899508a815501918d918f99989796959493610dad565b9091508190610d98565b634e487b7160e01b88526041600452602488fd5b60481c6001600160401b031691505f610aa3565b506001600160401b0383166001600160401b038260481c1611610a9e565b634e487b7160e01b86526021600452602486fd5b610e4990614b3b565b5f610a66565b634e487b7160e01b85526021600452602485fd5b6365e52d5160e11b8252600482fd5b6372b89e9760e11b8152600490fd5b8380fd5b503461026c578060031936011261026c57610200604051610ea58161299f565b8281528260208201528260408201528260608201528260808201528260a08201528260c08201528260e08201528261010082015282610120820152826101408201528261016082015282610180820152826101a0820152826101c0820152826101e08201520152610220610f17612c89565b610f246040518092612887565bf35b503461026c57602036600319011261026c5760606101e0604051610f49816129bb565b83815283602082015283604082015283838201528360808201528360a08201528360c08201528360e08201528361010082015283610120820152836101408201528361016082015283610180820152836101a0820152836101c08201520152600435815260126020526040812060405190610fc3826129bb565b80549060ff82166007811015610e4f57835260ff8260081c166005811015610e4f576110b691600591602086015260ff8460101c16611001816127cc565b604086015260ff8460181c16606086015260ff8460201c16608086015260ff8460281c16151560a086015261ffff8460301c1660c086015263ffffffff8460401c1660e086015261010085019360601c845260018060a01b0360018201541661012086015260018060a01b0360028201541661014086015260038101546101608601526001600160401b036004820154818116610180880152818160401c166101a088015260801c166101c0860152016135ec565b6101e08301526040519260208452825160078110156111f857602085015260208301519060058210156111e4575083926111e0926101e09260408601526040820151611101816127cc565b606086015260ff606083015116608086015260ff60808301511660a086015260a0820151151560c086015261ffff60c08301511660e086015263ffffffff60e08301511661010086015260018060a01b0390511661012085015260018060a01b036101208201511661014085015260018060a01b03610140820151166101608501526101608101516101808501526001600160401b03610180820151166101a08501526001600160401b036101a0820151166101c08501526001600160401b036101c08201511682850152015161020080840152610220830190612863565b0390f35b634e487b7160e01b81526021600452602490fd5b634e487b7160e01b82526021600452602482fd5b503461026c57602036600319011261026c57600435600381101561052a5761123261486f565b61123b816127cc565b60ff60015460081c1661124d816127cc565b60ff8216908082101561134d57600290611266816127cc565b036113435761128063ffffffff60025460901c16926127cc565b60ff60025460201c1690604051906112996020836129d7565b84825284600260ff60015460081c166112b1816127cc565b14908161133a575b81611330575b5061132157916104b76104ae63ffffffff6112e5600189966104cd9860209b6005614cd3565b8086526012895260408620600254815463ff00000090911664ffff000000199091161760209690961b64ff00000000169590951785559661049e565b6372b89e9760e11b8552600485fd5b955050845f6112bf565b600191506112b9565b61128083926127cc565b63710a5e0f60e01b8452600484fd5b503461026c57602036600319011261026c5761138760406111e0926004358152601360205220612bbb565b604051918291602083526020830190612827565b503461026c57604036600319011261026c576004356113b86148c5565b6113c1816148dc565b6113ca82613276565b1561141c5761040061ff00198254161790556113e7600554612b51565b6005556040519060243582527fcf4be29f7d91e92478a19a3eab52f6161f4e4580fcd2f5f8715f191dc2647bbb60203393a380f35b63ef28671760e01b8352600483fd5b503461026c57602036600319011261026c5760043580825260126020526040822060ff815460081c166005811015611503576001036114f4576001600160401b03600482015460801c164211156114c05761030061ff0019825416179055611494600554612b51565b60055533907f74c34a008ce735d9fcf0bd03a9b238d212ad4c441c020661f4ffbb6442645b858380a380f35b60405163935f84bf60e01b815260206004820152600b60248201526a1b9bdd08195e1c1a5c995960aa1b6044820152606490fd5b633ee3282d60e11b8352600483fd5b634e487b7160e01b84526021600452602484fd5b503461026c578060031936011261026c576020600654604051908152f35b503461026c57602036600319011261026c57611552600435613396565b80f35b503461026c57602036600319011261026c5761138760406111e0926004358152601460205220612bbb565b503461026c57604036600319011261026c5761159a6127a0565b6024359060ff821691828103610e81576115b261486f565b6001600160a01b0382168015611699576115cb82614bfa565b8452600860205260ff60408520541690838214611667576115eb91614c26565b906115f58261495e565b92604051916116056020846129d7565b85835285600260ff60015460081c1661161d816127cc565b14908161165f575b81611653575b5061164457926020959261045b926104cd956003614cd3565b6372b89e9760e11b8652600486fd5b9650600190505f61162b565b859150611625565b60405163935f84bf60e01b81526020600482015260096024820152686e6f206368616e676560b81b6044820152606490fd5b63d92e233d60e01b8552600485fd5b503461026c57602036600319011261026c576116c26127a0565b6116ca61489a565b6001600160a01b038116801561174e578252600e60205260ff604083205416600681101561173a576116ff5761155290614b3b565b60405163935f84bf60e01b8152602060048201526012602482015271185b1c9958591e481c9959da5cdd195c995960721b6044820152606490fd5b634e487b7160e01b83526021600452602483fd5b63d92e233d60e01b8352600483fd5b503461026c57602036600319011261026c57602061177c600435613276565b6040519015158152f35b503461026c57602036600319011261026c576020906040906001600160a01b036117ae6127a0565b168152601083522054604051908152f35b503461026c578060031936011261026c57604051906007548083528260208101600784526020842092845b8181106118ca5750506117ff925003836129d7565b815161182361180d8261324b565b9161181b60405193846129d7565b80835261324b565b602082019290601f1901368437805b8451811015611872576001906001600160a01b036118508288613262565b51168352600860205260ff60408420541661186b8286613262565b5201611832565b5061188f9392916020604051958695604087526040870190612827565b918583038287015251918281520192915b8181106118ae575050500390f35b825160ff168452859450602093840193909201916001016118a0565b84546001600160a01b03168352600194850194879450602090930192016117ea565b503461026c57606036600319011261026c5760c061191c61190b6127a0565b6119136127b6565b60443591612d80565b63ffffffff60a0604051928051611932816127cc565b845261ffff602082015116602085015261ffff604082015116604085015260ff606082015116606085015260ff608082015116608085015201511660a0820152f35b503461026c57604036600319011261026c5761198e6127a0565b6001600160a01b0316602435811561174e578015611a28576119e06040516323b872dd60e01b6020820152336024820152306044820152826064820152606481526119da6084826129d7565b8361563e565b8183526011602052604083206119f7828254612c7c565b90556040519081527f5548c837ab068cf56a2c2479df0882a4922fd203edb7517321831d95078c5f6260203392a380f35b6365e52d5160e11b8352600483fd5b503461026c578060031936011261026c57602060ff600a5460101c16604051908152f35b503461026c57604036600319011261026c57611a756127a0565b6024356006811015611b7557611a8961486f565b6001600160a01b0382168015611b66578115611b28578352600e602052611ab78160ff604086205416614a45565b90611ac18261495e565b9260405191611ad16020846129d7565b85835285600260ff60015460081c16611ae9816127cc565b149081611b20575b81611b14575b5061164457926020959261045b9260ff6104cd9616906004614cd3565b9650600190505f611af7565b859150611af1565b60405163935f84bf60e01b8152602060048201526015602482015274757365204e455720746f2072652d70726f6261746560581b6044820152606490fd5b63d92e233d60e01b8452600484fd5b8280fd5b503461026c57602036600319011261026c5760a0906001600160401b03611b9e6127a0565b8283908492600180881b03811695868152600f60205260408120968152600d602052611c0960408220600460405191611bd683612970565b60ff8154161515835260018101546020840152600281015460408401526003810154606084015201546080820152614976565b929050875487611c1a818316612c0e565b164210611cd95782985b6003549061ffff8260201c169283611c5a575b505050505050604051958652602086015260408501526060840152166080820152f35b9598509396509194509260401c8716919060301c63ffffffff1687611c7f8285612c2e565b164210611cae57505050611c9f61271091611c9a85946149d3565b612c4e565b0491925b5f8080808080611c37565b9194509250612710611ccc611cd39360026003870154960154612c4e565b0493612c2e565b92611ca3565b600189015498611c24565b503461026c578060031936011261026c57604051600b8054808352908352909160208301917f0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01db9915b818110611d43576111e085611387818703826129d7565b82546001600160a01b0316845260209093019260019283019201611d2c565b503461026c57602036600319011261026c57604060a091611d816127a0565b611d896129f8565b50600180851b03168152600e6020522060405190611da682612970565b6001600160401b038154611dbd60ff821685612baf565b816020850193818360081c1685528160016040880192828660481c1684528260608a019660881c16865201549560808801968752611dff60405180995161281a565b511660208701525116604085015251166060830152516080820152f35b503461026c578060031936011261026c57602063ffffffff60015460101c16604051908152f35b503461026c578060031936011261026c57602060ff600a5460181c16604051908152f35b503461026c57602036600319011261026c576020906040906001600160a01b03611e8f6127a0565b168152601183522054604051908152f35b503461206e5760c036600319011261206e57611eba6127a0565b60a036602319011261206e57611ece61486f565b73d587b84371f2f55ab35cfe2f64787d6829b85a2d803b1561206e57604051631c71447d60e01b8152611f0360048201612b71565b5f8160a481855af480156120635761204e575b5060018060a01b0382168352600d60205260206004610144604086206040519485938492637309439160e01b845260ff81541615158285015260018101546024850152600281015460448501526003810154606485015201546084830152611f8060a48301612b71565b5af4908115612043578391612009575b50604051611fa060208201612b71565b60a08152611faf60c0826129d7565b611fb88261495e565b9284600260ff60015460081c16611fce816127cc565b149081612001575b81611ff5575b50611321576104cd92859260209661045b936002614cd3565b9550600190505f611fdc565b849150611fd6565b90506020813d60201161203b575b81612024602093836129d7565b81010312611b755761203590612ba2565b5f611f90565b3d9150612017565b6040513d85823e3d90fd5b61205b9193505f906129d7565b5f915f611f16565b6040513d5f823e3d90fd5b5f80fd5b3461206e57602036600319011261206e5760043561208f816148dc565b805460601c331415806120f3575b6120e45761030061ff00198254161790556120b9600554612b51565b60055533907f74c34a008ce735d9fcf0bd03a9b238d212ad4c441c020661f4ffbb6442645b855f80a3005b6367909b1560e01b5f5260045ffd5b50335f526008602052600160405f2054161561209d565b3461206e57604036600319011261206e57600435600381101561206e5761212f614844565b612138816127cc565b60ff60015460081c1661214a816127cc565b60ff821611156121ab576001549060ff8260081c1691612169826127cc565b61ff001916600882901b61ff001617600155612184826127cc565b61218d816127cc565b6040519160243583525f5160206157295f395f51905f5260203394a4005b63216a142160e11b5f5260045ffd5b3461206e57602036600319011261206e576001600160a01b036121db6127a0565b165f526008602052602060ff60405f205416604051908152f35b3461206e576102a036600319011261206e576004356001600160401b03811161206e576122269036906004016127ea565b906024356001600160401b03811161206e576122469036906004016127ea565b90919061022036604319011261206e57610264356001600160401b03811161206e576122769036906004016127ea565b909261028435926001600160401b03841161206e573660238501121561206e578360040135956001600160401b03871161206e5736602460a089028701011161206e5760015460ff81166126085760019060ff1916176001558188148015906125fe575b6125cf575f5b88811061253e57505050505f5b8181106124a457856040516123018161299f565b60443560ff8116810361206e57815260643560ff8116810361206e57602082015260843560ff8116810361206e57604082015260a43560ff8116810361206e57606082015260c43560ff8116810361206e57608082015260e435801515810361206e5760a08201526101043563ffffffff8116810361206e5760c08201526101243563ffffffff8116810361206e5760e08201526101443563ffffffff8116810361206e576101008201526101643563ffffffff8116810361206e576101208201526101843563ffffffff8116810361206e576101408201526101a43563ffffffff8116810361206e576101608201526101c43561ffff8116810361206e576101808201526101e43561ffff8116810361206e576101a08201526102043561ffff8116810361206e576101c08201526102243561ffff8116810361206e576101e0820152610244359163ffffffff8316830361206e5761248a827f4488062cfafbbb2c1d440f13796e120c4522761f74ecade440ee8f31f1bfbee09461020060409501526145c5565b63ffffffff60015460101c169082519182526020820152a1005b6124b76124b2828487612b1f565b612b2f565b908581101561252a5760a08102840160a0602319823603011261206e576040516124e081612970565b602482013593841515850361206e5760a4612524936001968452604481013560208501526064810135604085015260848101356060850152013560808301526143f9565b016122ed565b634e487b7160e01b5f52603260045260245ffd5b6001600160a01b036125546124b2838c88612b1f565b165f52600860205260ff60405f2054166125c05760ff61257d612578838686612b1f565b612b43565b16156125b157806125ab6125976124b26001948d89612b1f565b6125a5612578848888612b1f565b9061426b565b016122e0565b633a767e1d60e21b5f5260045ffd5b630c8ce74760e11b5f5260045ffd5b60405163935f84bf60e01b81526020600482015260066024820152650d8cadccee8d60d31b6044820152606490fd5b50868414156122da565b62dc149f60e41b5f5260045ffd5b3461206e575f36600319011261206e57602060ff60015460081c166040519061263e816127cc565b8152f35b3461206e57602036600319011261206e576040612660600435612a22565b82519182526020820152f35b3461206e57604036600319011261206e576126856127b6565b6004355f52601560205260405f209060018060a01b03165f52602052602060ff60405f2054166040519015158152f35b3461206e57602036600319011261206e576126ce6127a0565b6126d66129f8565b5060018060a01b03165f52600d60205260a060405f206004604051916126fb83612970565b60ff8154161515835260018101546020840152600281015460408401526003810154606084015201546080820152610f246040518092608080918051151584526020810151602085015260408101516040850152606081015160608501520151910152565b3461206e575f36600319011261206e57602060ff600a5460081c16604051908152f35b3461206e575f36600319011261206e5760209060ff600a54168152f35b600435906001600160a01b038216820361206e57565b602435906001600160a01b038216820361206e57565b600311156127d657565b634e487b7160e01b5f52602160045260245ffd5b9181601f8401121561206e578235916001600160401b03831161206e576020808501948460051b01011161206e57565b9060068210156127d65752565b90602080835192838152019201905f5b8181106128445750505090565b82516001600160a01b0316845260209384019390920191600101612837565b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b63ffffffff610200809260ff815116855260ff602082015116602086015260ff604082015116604086015260ff606082015116606086015260ff608082015116608086015260a0810151151560a08601528260c08201511660c08601528260e08201511660e0860152826101008201511661010086015282610120820151166101208601528261014082015116610140860152826101608201511661016086015261ffff6101808201511661018086015261ffff6101a0820151166101a086015261ffff6101c0820151166101c086015261ffff6101e0820151166101e0860152015116910152565b60a081019081106001600160401b0382111761298b57604052565b634e487b7160e01b5f52604160045260245ffd5b61022081019081106001600160401b0382111761298b57604052565b61020081019081106001600160401b0382111761298b57604052565b90601f801991011681019081106001600160401b0382111761298b57604052565b60405190612a0582612970565b5f6080838281528260208201528260408201528260608201520152565b90815f52601260205260405f20825f52601360205260ff60405f2091541660078110156127d657612b17576002905b5f918154915f5b838110612acc5750505050915f52601460205260405f205f908054905f5b828110612a835750505090565b612a8d8183613315565b60018060a01b0391549060031b1c165f526008602052600860405f205416612ab8575b600101612a76565b92612ac460019161381e565b939050612ab0565b612ad68183613315565b60018060a01b0391549060031b1c165f52600860205260ff8360405f20541616612b03575b600101612a58565b93612b0f60019161381e565b949050612afb565b600190612a51565b919081101561252a5760051b0190565b356001600160a01b038116810361206e5790565b3560ff8116810361206e5790565b8015612b5d575f190190565b634e487b7160e01b5f52601160045260245ffd5b60243580151580910361206e578152604435602082015260643560408201526084356060820152608060a435910152565b5190811515820361206e57565b60068210156127d65752565b90604051918281549182825260208201905f5260205f20925f5b818110612bec575050612bea925003836129d7565b565b84546001600160a01b0316835260019485019487945060209093019201612bd5565b6001600160401b0362015180911601906001600160401b038211612b5d57565b906001600160401b03809116911601906001600160401b038211612b5d57565b81810292918115918404141715612b5d57565b6001600160401b03811161298b57601f01601f191660200190565b91908201809211612b5d57565b60405190612c968261299f565b8160025460ff8116825260ff8160081c16602083015260ff8160101c16604083015260ff8160181c16606083015260ff8160201c16608083015260ff8160281c16151560a083015263ffffffff8160301c1660c083015263ffffffff8160501c1660e083015263ffffffff8160701c1661010083015263ffffffff8160901c1661012083015263ffffffff8160b01c1661014083015263ffffffff8160d01c1661016083015260f01c61018082015261020063ffffffff60035461ffff81166101a085015261ffff8160101c166101c085015261ffff8160201c166101e085015260301c16910152565b92916040519160c083018381106001600160401b0382111761298b576040525f835260208301925f845260408101915f835260608201945f865260808301935f85525f60a0850152839860018060a01b038116805f52600d60205260405f209460405195612ded87612970565b60ff81541615918215885260018201549260208901938452600460028401549360408b01948552600381015460608c0152015460808a01526132385760018060a01b031693845f52600e60205260405f209760405195612e4c87612970565b60018a549a612e5e60ff8d168a612baf565b6001600160401b038c60081c1660208a01526001600160401b0360408a019c8d828260481c16905260881c1660608a015201546080880152865160068110156127d6576005146132265750612eb290614976565b9490505f52600f602052612ec960405f20916149d3565b92519151906001600160401b03612ee281835416612c0e565b16421061321c57505f9a5b85519560068710156127d6575160068110156127d6576001149889613208575b5060ff60015460081c1660025460f01c9161ffff6003541697604051976101608901928984106001600160401b0385111761298b57612f75936040528952602089019788526040890196875260608901958652608089015260a088019e8f5260c08801612baf565b60e086019915158a52612f87816127cc565b6101008601526101208501526101408401948552835190515f9283918311156131e3575050612fc161ffff916002809417945b5190615696565b94511680151590816131d4575b501561317d5750600860029117955b60c083015160068110156127d65760040361311c575050602060029517975b6080820151151590816130d8575b509360ff9860a0989794613074948b989463ffffffff986130a1575b6101006001910151613037816127cc565b613040816127cc565b14613085575b61ffff90811690915216905261305b816127cc565b808352613067816127cc565b61306f612c89565b614ab3565b939093169190950152169052169052565b9061ffff60406130968295976156f5565b961793505090613046565b92946130ac816127cc565b6001818b16106130ca5761010060806001925b971794915050613026565b5060016101006080826130bf565b6130ef915097969397959194929551855190612c7c565b918260011b9280840460021490151715612b5d5760808501519697939691959094909290911060ff61300a565b60c083015160068110156127d657613143575061313b610110916156f5565b951797612ffc565b60c0839a92979a015160068110156127d6576001149081613172575b5015612ffc57979461313b6010916156f5565b90505115155f61315f565b909561ffff6101208401511680151590816131c5575b5015612fdd5795906131a4816127cc565b600160ff8216106131bb576004905b911795612fdd565b50600460016131b3565b905061ffff851610155f613193565b905061ffff851610155f612fce565b939092935182116131fc575b61ffff91612fc191612fba565b600193508392506131ef565b516001600160401b0316421098505f612f0d565b600101549a612eed565b63325fbd1560e11b5f5260045260245ffd5b8363a6e7da2360e01b5f5260045260245ffd5b6001600160401b03811161298b5760051b60200190565b805182101561252a5760209160051b010190565b5f52601260205260405f205460ff8160081c1660058110156127d6576001036132f85760ff8160281c166132f25760ff811660078110156127d6576132f857600260ff8260101c166132c7816127cc565b146132f25760301c6010166132ed5760ff60015460081c166132e8816127cc565b151590565b600190565b50600190565b505f90565b60075481101561252a5760075f5260205f2001905f90565b805482101561252a575f5260205f2001905f90565b60075490600160401b82101561298b5761334f826001612bea94016007556007613315565b81546001600160a01b0393841660039290921b91821b9390911b1916919091179055565b90815491600160401b83101561298b578261334f916001612bea95018155613315565b61339f816148dc565b335f52600860205260405f205490600882166134bb575460ff1660078110156127d657600681146134a75761349c5760ff600280925b16161561348657505f81815260156020908152604080832033845290915290205460ff1661347757805f52601560205260405f2060018060a01b0333165f5260205260405f20600160ff19825416179055805f52601360205261343b3360405f20613373565b805f52601360205260405f2054906040519182527f2f0f51d2f12c71357e60d575d47e93e081dbaf32072245ab1e01540f0aadbf1a60203393a3565b63080fc0bd60e11b5f5260045ffd5b60ff9063e84c979760e01b5f521660045260245ffd5b60ff600180926133d5565b63e84c979760e01b5f52600860045260245ffd5b60ff91505460201c1615613563575f81815260156020908152604080832033845290915290205460ff1661347757805f52601560205260405f2060018060a01b0333165f5260205260405f20600160ff19825416179055805f5260146020526135273360405f20613373565b805f52601460205260405f2054906040519182527fb8c7de51f1e8e2b6cec2574c8731fba6de6e3e3002b2e454ebccdf32f591ca8660203393a3565b60405163935f84bf60e01b815260206004820152602260248201527f677561726469616e20636f6e6669726d6174696f6e206e6f7420726571756972604482015261195960f21b6064820152608490fd5b90600182811c921680156135e2575b60208310146135ce57565b634e487b7160e01b5f52602260045260245ffd5b91607f16916135c3565b9060405191825f8254926135ff846135b4565b80845293600181169081156136685750600114613624575b50612bea925003836129d7565b90505f9291925260205f20905f915b81831061364c575050906020612bea928201015f613617565b6020919350806001915483858901015201910190918492613633565b905060209250612bea94915060ff191682840152151560051b8201015f613617565b9067ffffffffffffffff60401b82549160401b169067ffffffffffffffff60401b1916179055565b60043560ff811680910361206e57815260243560ff811680910361206e57602082015260443560ff811680910361206e57604082015260643560ff811680910361206e57606082015260843560ff811680910361206e57608082015260a43580151580910361206e5760a082015260c43563ffffffff811680910361206e5760c082015260e43563ffffffff811680910361206e5760e08201526101043563ffffffff811680910361206e576101008201526101243563ffffffff811680910361206e576101208201526101443563ffffffff811680910361206e576101408201526101643563ffffffff811680910361206e576101608201526101843561ffff811680910361206e576101808201526101a43561ffff811680910361206e576101a08201526101c43561ffff811680910361206e576101c08201526101e43561ffff811680910361206e576101e0820152610204359063ffffffff821680920361206e576102000152565b5f198114612b5d5760010190565b519060ff8216820361206e57565b519063ffffffff8216820361206e57565b519061ffff8216820361206e57565b613863816148dc565b805460ff8116906007821015806127d657821591821561422b57335f526008602052600460405f20541615614218575b6001600160401b03600486015460401c1680421061420657506001549082600260ff8460081c166138c3816127cc565b1490816141d6575b506141c75760ff8160181c169160ff8260201c16936127d6578490816141aa575b5061416f575b506138fc86612a22565b919081811061415a57505081811061414557505080614132575b61020061ff001984541617835561392e600554612b51565b60055561393c60065461381e565b60065533847f9c85b616f29fca57a17eafe71cf9ff82ffef41766e2cf01ea7f8f7878dd3ec245f80a315613a56575060018060a01b03600182015416906002810191600360018060a01b0384541692019382828654927feb669e0a01fff40cbe45de851f42334fe70673136abfd91399651b7ae199a07b6020604051868152a481613a1e575050905491545f92839250829182916001600160a01b03165af13d15613a19573d6139eb81612c61565b906139f960405192836129d7565b81525f60203d92013e5b15613a0a57565b633d2cec6f60e21b5f5260045ffd5b613a03565b90919250612bea93506040519263a9059cbb60e01b60208501526024840152604483015260448252613a516064836129d7565b61563e565b60018103613bc05750613a6c91506005016135ec565b6102208180518101031261206e57612bea90613bb561022060405192613a918461299f565b613a9d6020820161382c565b8452613aab6040820161382c565b6020850152613abc6060820161382c565b6040850152613acd6080820161382c565b6060850152613ade60a0820161382c565b6080850152613aef60c08201612ba2565b60a0850152613b0060e0820161383a565b60c0850152613b12610100820161383a565b60e0850152613b24610120820161383a565b610100850152613b37610140820161383a565b610120850152613b4a610160820161383a565b610140850152613b5d610180820161383a565b610160850152613b706101a0820161384b565b610180850152613b836101c0820161384b565b6101a0850152613b966101e0820161384b565b6101c0850152613ba9610200820161384b565b6101e08501520161383a565b6102008201526145c5565b60028103613c3d575060028101546001600160a01b03169150613be5906005016135ec565b60a08180518101031261206e57612bea9160a060405192613c0584612970565b613c1160208201612ba2565b8452604081015160208501526060810151604085015260808101516060850152015160808301526143f9565b60038103613c7057506002810154600390910154613c68925060ff16906001600160a01b031661426b565b612bea61550f565b60048103613d8a5750905060ff600360018060a01b0360028401541692015416600681109081156127d657825f52600e60205260405f209160ff8354169060068210156127d6578115613d65575b60018314613d17575b156127d657613d14827f155590dadece0bf62c36a0e5cabcce51e1280ff0e8f6cc53c14d8900e0f43daf9460409460ff19825416179055613d0a8451809461281a565b602083019061281a565ba2565b613d60613d3863ffffffff60025460701c166001600160401b034216612c2e565b855467ffffffffffffffff60481b191660489190911b67ffffffffffffffff60481b16178555565b613cc7565b835468ffffffffffffffff0019164260081b68ffffffffffffffff0016178455613cbe565b600503613e325760039150015460ff8116613da4816127cc565b613dad816127cc565b60ff60015460081c16613dbf816127cc565b811015613e235760015461ff008060ff8360081c1694613dde856127cc565b60081b16169061ff00191617600155613df6826127cc565b613dff816127cc565b60405191640e4cad8c2f60db1b83525f5160206157295f395f51905f5260203394a4565b63710a5e0f60e01b5f5260045ffd5b60028101546001600160a01b031690613e4d906005016135ec565b60208180518101031261206e57602001516001600160a01b038116929083900361206e57815f52600860205260ff60405f205416801561412357835f52600860205260ff60405f2054166125c057821561411457613eaa5f614bfa565b825f5260086020528260ff60405f205416801580809161410d575b15614007575060405f5160206157095f395f51905f5291613ee58461332a565b600754845f526009602052825f20555b613eff5f826153c3565b613f095f8261540d565b613f135f82615461565b613f1d5f826154b7565b835f526008602052815f2060ff19815416905581519081525f6020820152a2613f46818561426b565b613f4e61550f565b60ff60015460081c16613f60816127cc565b15613f93575b60207f607a6564adeb3eec35d536d94fa031ad71407e7510a784e53f7bf36d44f3bc4191604051908152a4565b600154600881901c60ff169061ff00191661010017600155613fb4816127cc565b7f607a6564adeb3eec35d536d94fa031ad71407e7510a784e53f7bf36d44f3bc4191602091600160405191677265636f7665727960c01b83525f5160206157295f395f51905f52853394a4915050613f66565b1580614105575b614029575b60405f5160206157095f395f51905f5291613ef5565b905f52600960205260405f20545f198101818111612b5d576007545f198101908111612b5d578082036140c7575b5050506007549081156140b35760405f5160206157095f395f51905f529186935f190161409b614086826132fd565b81549060018060a01b039060031b1b19169055565b600755835f5260096020525f82812055915050614013565b634e487b7160e01b5f52603160045260245ffd5b61334f916140d76140f1926132fd565b905460039190911b1c6001600160a01b03169283916132fd565b5f52600960205260405f20555f8080614057565b50600161400e565b505f613ec5565b63d92e233d60e01b5f5260045ffd5b63148fe22160e11b5f5260045ffd5b61413b8361500b565b6139165750505050565b63d495223560e01b5f5260045260245260445ffd5b635ab4617560e01b5f5260045260245260445ffd5b61418260ff809260101c1661306f612c89565b5091168281116141a1575b5060ff16828111156138f25791505f6138f2565b915060ff61418d565b63ffffffff915060101c1663ffffffff8260401c1614155f6138ec565b6372b89e9760e11b5f5260045ffd5b90506127d6578384156141ea575b836138cb565b5060ff8160281c1680156141e457505f925060058514156141e4565b6384280dad60e01b5f5260045260245ffd5b63e84c979760e01b5f526004805260245ffd5b5f9150600684036142435761423e614844565b613893565b335f526008602052600560405f2054166138935763e84c979760e01b5f52600560045260245ffd5b6001600160a01b0381169182156141145761428581614bfa565b825f52600860205260ff60405f2054169182158080916143ed575b1561431f57509160ff6040926142c35f5160206157095f395f51905f529561332a565b600754865f526009602052845f20555b6142dd81846153c3565b6142e7818461540d565b6142f18184615461565b6142fb81846154b7565b855f526008602052835f2082821683198254161790558351928352166020820152a2565b905015806143e2575b614346575b5f5160206157095f395f51905f529160ff6040926142d3565b825f52600960205260405f20545f198101818111612b5d576007545f198101908111612b5d578082036143be575b5050506007549182156140b35760ff6040925f5160206157095f395f51905f52945f19016143a4614086826132fd565b600755855f5260096020525f84812055925050915061432d565b61334f916140d76143ce926132fd565b5f52600960205260405f20555f8080614374565b5060ff811615614328565b5060ff831615156142a0565b9073d587b84371f2f55ab35cfe2f64787d6829b85a2d803b1561206e57604051631c71447d60e01b81525f8160a48183956144616004830189608080918051151584526020810151602085015260408101516040850152606081015160608501520151910152565b5af48015612063576145b2575b506001600160a01b038316808252600c602052604082205490939060ff1615614532575b507f9d182ff993316b1c662980e27e42d42fae0bd158f1bed04e9988ec637fea8da4916040828560a09452600d602052208151151560ff8019835416911617815560208201516001820155604082015160028201556060820151600382015560046080830151910155613d146040518092608080918051151584526020810151602085015260408101516040850152606081015160608501520151910152565b838252600c60205260408220805460ff19166001179055600b54600160401b81101561459e5760a0926145957f9d182ff993316b1c662980e27e42d42fae0bd158f1bed04e9988ec637fea8da4959361334f84600160409601600b55600b613315565b92505091614492565b634e487b7160e01b83526041600452602483fd5b6145be91505f906129d7565b5f5f61446e565b73d587b84371f2f55ab35cfe2f64787d6829b85a2d803b1561206e5760405163065397d760e21b81525f816102248183956146036004830189612887565b5af4801561206357614831575b50815160208084015160408501516060860151608087015160a088015160c089015160e08a01516101008b01516101208c01516101408d01516101608e01516101808f015160089b909b1b61ff001660ff9d909d169c909c176010998a1b62ff0000161760189890981b63ff0000001697909717958a1b64ff00000000169590951793151560281b60ff60281b1693909317603092831b69ffffffff0000000000009081169190911760509290921b63ffffffff60501b169190911760709390931b63ffffffff60701b169290921760909390931b63ffffffff60901b169290921760b09390931b63ffffffff60b01b169290921760d09690961b63ffffffff60d01b169590951760f09390931b6001600160f01b031916929092176002556101a0860151600380546101c08901516101e08a01516102008b015169ffffffffffffffffffff199390931661ffff959095169490941790851b63ffff000016179290951b65ffff0000000016919091179390941b9091169190911790915560018054929183901c63ffffffff9081169091019190821161481d575063ffffffff7f1a734774792c149a91deb03650b2a2bc51de7bbab7262857ace6fb1699b430419265ffffffff00006102409360101b169065ffffffff0000191617806001556147f861550f565b614815604051956001600160401b03421687526020870190612887565b60101c1692a2565b634e487b7160e01b81526011600452602490fd5b61483d91505f906129d7565b5f5f614610565b335f526008602052600960405f2054161561485b57565b63e84c979760e01b5f52600960045260245ffd5b335f526008602052600160405f2054161561488657565b63e84c979760e01b5f52600160045260245ffd5b335f526008602052600360405f205416156148b157565b63e84c979760e01b5f52600360045260245ffd5b335f526008602052600860405f205416156134a757565b80158015614953575b614944575f52601260205260405f209060ff825460081c1660058110156127d657600103614935576001600160401b03600483015460801c16421161492657565b6328a7237960e01b5f5260045ffd5b633ee3282d60e11b5f5260045ffd5b6302eae9e560e51b5f5260045ffd5b5060045481116148e5565b156149725763ffffffff60025460901c1690565b5f90565b906080606083015192015190600160ff815460081c16614995816127cc565b1461499c57565b826149bb575b816149a957565b9060011c806149b85750600190565b90565b9160011c806149ce575060015b916149a2565b6149c8565b6001600160a01b03168015614a40576020602491604051928380926370a0823160e01b82523060048301525afa908115612063575f91614a11575090565b90506020813d602011614a38575b81614a2c602093836129d7565b8101031261206e575190565b3d9150614a1f565b504790565b906006811015806127d657600282148015614aa7575b614a9f576127d6576001149081614a76575b506132ed575f90565b905060068110156127d65760048114908115614a94575b505f614a6d565b60059150145f614a8d565b505050600190565b50505f60038214614a5b565b9190614abe816127cc565b8015614b2e57600190614ad0816127cc565b14614b135760ff6040830151169060a083015115155f14614b055763ffffffff60e060ff608086015116945b01511691929190565b63ffffffff60e05f94614afc565b9063ffffffff60c060ff6020840151169201511690915f9190565b50905160ff16905f908190565b6001600160a01b03165f818152600e60209081526040909120805468ffffffffffffffffff191642600881901b68ffffffffffffffff00169190911760011782557fa834c3aabec8913fbfd0591ec9c2a7494c727a364cc1f231c948c0f9564b5d0692916001600160401b0391614bec90614bc490841663ffffffff60025460701c1690612c2e565b825467ffffffffffffffff60481b191660489190911b67ffffffffffffffff60481b16178255565b5460481c16604051908152a2565b600f60ff8216116125b1576008811615159081614c19575b506125b157565b600791501615155f614c12565b60ff8119831616614c515760081615159081614c4557506132ed575f90565b6008915016155f614a6d565b5050600190565b614c6190614976565b509081151580614cca575b614cb557505061ffff60035460101c169081151580614ca8575b614c8e575050565b61ffff90630a8a293d60e01b5f521660045260245260445ffd5b508161ffff821611614c86565b634fa5781f60e11b5f5260045260245260445ffd5b50818111614c6c565b9290949391614ce360045461381e565b958660045586614cf460055461381e565b6005555f52601260205260405f209460078110156127d65785546bffffffffffffffffffffffff61ffff1990911660ff929092169190911761010017163360601b6001600160601b0319161785556002850180546001600160a01b0319166001600160a01b039092169190911790556003840155805160058401916001600160401b03821161298b578190614d8984546135b4565b601f8111614ec8575b50602090601f8311600114614e65575f92614e5a575b50508160011b915f199060031b1c19161790555b60ff60281b825491151560281b169060ff60281b191617815560015463ffffffff60401b82549160301b169063ffffffff60401b1916178155614e3260046001600160401b0342169201916001600160401b0381166001600160401b031984541617835563ffffffff60025460d01c1690612c2e565b815467ffffffffffffffff60801b191660809190911b67ffffffffffffffff60801b16179055565b015190505f80614da8565b5f8581528281209350601f198516905b818110614eb05750908460019594939210614e98575b505050811b019055614dbc565b01515f1960f88460031b161c191690555f8080614e8b565b92936020600181928786015181550195019301614e75565b909150835f5260205f20601f840160051c81019160208510614f0e575b90601f859493920160051c01905b818110614f005750614d92565b5f8155849350600101614ef3565b9091508190614ee5565b815460ff81169260018060a01b0360018201541660018060a01b0360028301541691600381015493600460ff8260101c169201549460078810156127d6577fcf5d862ed732f5aec4f98da191cd4e962a57abdc7bab078c436b0517229d388f95615006936001600160401b0392604051978897885260208801526040870152614fa0816127cc565b606086015261ffff8360301c16608086015260ff8360181c1660a086015260ff8360201c1660c0860152818160401c1660e086015260801c1661010084015260ff8160281c16151561012084015261016061014084015260601c96610160830190612863565b0390a4565b60018060a01b0360018201541690600381015491805f52600d60205260405f20916040519261503984612970565b600460ff8254161591821586526001810154602087015260028101546040870152600381015460608701520154608085015261538c57600201546001600160a01b03165f818152600e60205260409020805490939160ff82169060068210156127d65760058214613226575060011480615377575b61535857506150bc826149d3565b90818511615341576150e1906150dc6150d58488615696565b8783614c58565b614976565b919050825f52600f60205260405f20916001600160401b0361510581855416612c0e565b1642101561531c575b80151580615306575b6152e3575060035461ffff8160201c166151b5575b505060010161513c848254612c7c565b90555f52601060205260405f20615154838254612c7c565b905580549060016001600160401b038360881c1601906001600160401b038211612b5d5767ffffffffffffffff60881b1990921660889190911b67ffffffffffffffff60881b16178155600101805490916151ae91612c7c565b9055600190565b6151d86001600160401b039163ffffffff83865460401c169160301c1690612c2e565b164210156152bf575b506127106151fd600283015461ffff60035460201c1690612c4e565b0460038201908154816152108883612c7c565b1161522f57505090816152268660019454612c7c565b9055905f61512c565b9091507f72f5745158d621f0076c165a1d23a925d0b2f8c5b5fb643933649247142d156d945060609395925060405192835260208301526040820152a2600154600881901c60ff169061ff0019166102001760015561528d816127cc565b6002604051916e31b4b931bab4ba16b13932b0b5b2b960891b83525f5160206157295f395f51905f5260203394a45f90565b6152d26001600160401b0342168361368a565b60028201555f60038201555f6151e1565b6152f1866001850154612c7c565b6398b5494560e01b5f5260045260245260445ffd5b5080615316876001860154612c7c565b11615117565b825467ffffffffffffffff1916426001600160401b03161783555f600184015561510e565b508363cf47918160e01b5f5260045260245260445ffd5b6001600160401b03906384280dad60e01b5f5260481c1660045260245ffd5b506001600160401b038160481c1642106150ae565b5063a6e7da2360e01b5f5260045260245ffd5b60ff5f199116019060ff8211612b5d57565b60ff60019116019060ff8211612b5d57565b6001918216151591161515811461540a57156153f65760ff6153e881600a54166153b1565b1660ff19600a541617600a55565b60ff61540581600a541661539f565b6153e8565b50565b6002918216151591161515811461540a571561544b5761543460ff600a5460081c166153b1565b61ff00600a549160081b169061ff00191617600a55565b61545c60ff600a5460081c1661539f565b615434565b6004918216151591161515811461540a57156154a15761548860ff600a5460101c166153b1565b62ff0000600a549160101b169062ff0000191617600a55565b6154b260ff600a5460101c1661539f565b615488565b6008918216151591161515811461540a57156154f9576154de60ff600a5460181c166153b1565b63ff000000600a549160181b169063ff000000191617600a55565b61550a60ff600a5460181c1661539f565b6154de565b600a5460ff81166001811061560d57600160ff8360101c16106155d9576002549060ff8260181c161161559d5760ff8160101c1660ff8360081c16106155635760ff809160201c169160181c16106108b457565b604051637417de8f60e01b8152602060048201526011602482015270185c1c1c9bdd985b1cd0dc9a5d1a58d85b607a1b6044820152606490fd5b604051637417de8f60e01b815260206004820152601360248201527219dbdd995c9b985b98d9551a1c995cda1bdb19606a1b6044820152606490fd5b604051637417de8f60e01b815260206004820152600b60248201526a37379032bc32b1baba37b960a91b6044820152606490fd5b604051637417de8f60e01b815260206004820152600860248201526737379037bbb732b960c11b6044820152606490fd5b905f602091828151910182855af115612063575f513d61568d57506001600160a01b0381163b155b61566d5750565b635274afe760e01b5f9081526001600160a01b0391909116600452602490fd5b60011415615666565b9081156156ef5780158080156156e5575b6156dc57612710830292830461271003612b5d576156c85761ffff91041690565b634e487b7160e01b5f52601260045260245ffd5b50505061271090565b50818310156156a7565b50505f90565b6156fe816127cc565b156132ed5760029056fe5d3de0fa15f594659e83f792fff19b5062cbc430ea0e9cf465377cdfaab8845467ac1adbe98dcfa9ea51aab9f87f1ae3ca912bf37e541afd86007c5bde3e5311", - "nonce": "0x2", - "chainId": "0x2328" - }, - "additionalContracts": [ - { - "transactionType": "CREATE", - "contractName": "SkurVault", - "address": "0xada220cacb3b0bff5944bbf35000d85df454806a", - "initCode": "0x6080806040523460255760015f55600160ff1981541617600155615749908161002a8239f35b5f80fdfe6080806040526004361015610062575b50361561001a575f80fd5b5f8052601160205260405f20610031348254612c7c565b90555f6040513481527f5548c837ab068cf56a2c2479df0882a4922fd203edb7517321831d95078c5f6260203392a3005b5f905f3560e01c9081630db026221461278357508063128e9be6146127605780632014055d146126b55780632358d5a81461266c57806328dca5ba14612642578063295a5212146126165780632d8a98d9146121f55780632de94807146121ba5780633a5d0d6f1461210a57806340e58ee514612072578063460fc01914611ea05780635305548114611e6757806354387ad714611e4357806358355ead14611e1c57806362812a3914611d6257806367e4ac2c14611ce457806368d525c714611b7957806378f3c95a14611a5b5780638ec1f4ca14611a3757806397feb926146119745780639d8b9a49146118ec5780639eab5253146117bf578063a15a2f8b14611786578063a2380d351461175d578063a8badaa5146116a8578063b314c57014611580578063b52f393e14611555578063b759f95414611535578063bacd2e9714611517578063bf81bf431461142b578063c0555a0a1461139b578063c65df7941461135c578063c75e0df21461120c578063c7f758a814610f26578063ce1e462614610e85578063d0ee62c714610939578063da35c6641461091b578063db02466414610574578063ea70b4af14610556578063ee5248941461026f5763fe0d94c10361000f573461026c57602036600319011261026c57600281541461025d576002815561025660043561385a565b6001815580f35b633ee5aeb560e01b8152600490fd5b80fd5b503461026c5761022036600319011261026c5761028a61486f565b73d587b84371f2f55ab35cfe2f64787d6829b85a2d803b1561052a5760405163065397d760e21b815282906102c1600482016136b2565b818161022481865af4801561053657610541575b50506020610444916040519283809263099fe59960e01b825260025460ff8116600484015260ff8160081c16602484015260ff8160101c16604484015260ff8160181c16606484015260ff81871c16608484015260ff8160281c16151560a484015263ffffffff8160301c1660c484015263ffffffff8160501c1660e484015263ffffffff8160701c1661010484015263ffffffff8160901c1661012484015263ffffffff8160b01c1661014484015263ffffffff8160d01c1661016484015260f01c61018483015263ffffffff60035461ffff81166101a485015261ffff8160101c166101c485015261ffff81881c166101e485015260301c166102048301526103e361022483016136b2565b5af49081156105365782916104f8575b5060405190610404602083016136b2565b6102208252610415610240836129d7565b61041e8161495e565b9183600260ff60015460081c16610434816127cc565b1490816104f0575b816104e4575b506104d5579261045b6104cd9260209583806001614cd3565b8082526012855260408220600254815463ff00000090911664ffff0000001990911617602084901b64ff000000001617815590936104b7906104ae9063ffffffff905b166001600160401b034216612c2e565b6004830161368a565b604051916104c586846129d7565b825283614f18565b604051908152f35b6372b89e9760e11b8452600484fd5b9450600190505f610442565b83915061043c565b90506020813d60201161052e575b81610513602093836129d7565b8101031261052a5761052490612ba2565b5f6103f3565b5080fd5b3d9150610506565b6040513d84823e3d90fd5b8161054b916129d7565b61052a57815f6102d5565b503461026c578060031936011261026c576020600554604051908152f35b503461026c57604036600319011261026c5761058e6127a0565b906105976127b6565b61059f6148c5565b6001600160a01b031691821561090c576001600160a01b03811682526008602052604082205460ff16156108fd57828252600860205260ff6040832054166108ee5760ff60025460201c16156108b457604051926020840152602083526106076040846129d7565b61061260045461381e565b908160045561062260055461381e565b600590815582845260126020526040842080546bffffffffffffffffffff0000163360601b6001600160601b031916176101061781556002810180546001600160a01b0319166001600160a01b0390941693909317909255600382018490558451939082016001600160401b0385116108a05761069f81546135b4565b601f811161085b575b506020958690601f87116001146107f6579580916104cd969785926107eb575b50508160011b915f199060031b1c19161790555b60ff60281b19825416825560015463ffffffff60401b83549160301b169063ffffffff60401b191617825560046001600160401b03421692016001600160401b0383166001600160401b031982541617815561074363ffffffff60025460d01c1684612c2e565b815467ffffffffffffffff60801b191660809190911b67ffffffffffffffff60801b161790558381526012855260408120805463ff000000198116825560025464ffff0000001990911690871c60201b64ff0000000016178155916107c1906107b89063ffffffff60025460b01c1690612c2e565b6004840161368a565b50604051906107d16040836129d7565b60088252677265636f7665727960c01b8583015283614f18565b015190505f806106c8565b8284528184209190601f198816855b81811061084457509160019391896104cd999a941061082c575b505050811b0190556106dc565b01515f1960f88460031b161c191690555f808061081f565b92938a600181928786015181550195019301610805565b81835260208320601f870160051c81019160208810610896575b601f0160051c01905b81811061088b57506106a8565b83815560010161087e565b9091508190610875565b634e487b7160e01b82526041600452602482fd5b604051637417de8f60e01b815260206004820152601160248201527019dd585c991a585b951a1c995cda1bdb19607a1b6044820152606490fd5b630c8ce74760e11b8252600482fd5b63148fe22160e11b8252600482fd5b63d92e233d60e01b8252600482fd5b503461026c578060031936011261026c576020600454604051908152f35b503461026c57608036600319011261026c576109536127a0565b9061095c6127b6565b60443590606435906001600160401b038211610e815736602383011215610e81578160040135936001600160401b03851161026c57366024868501011161026c576109a561489a565b600260ff60015460081c166109b9816127cc565b14610e72576001600160a01b03821694851561090c578415610e63576109e0858489612d80565b9260018060a01b0388168352600d602052610a4560408420600460405191610a0783612970565b60ff81541615158352600181015460208401526002810154604084015260038101546060840152015460808201528761ffff60408801511691614c58565b868352600e602052604083209060ff8254166006811015610e4f5715610e40575b506001600160401b03421697610a8663ffffffff60a0870151168a612c2e565b915460ff81166006811015610e2c5760011480610e0e575b610dfa575b5060209860405198610ab58b8b6129d7565b858a52610ac360045461381e565b9889600455610ad360055461381e565b60059081558a885260128d526040882080546bffffffffffffffffffff0000163360601b6001600160601b031916176101001781556002810180546001600160a01b03191690941790935560038301919091558a519a908201906001600160401b038c11610de657908b8d979695949392610b4e83546135b4565b601f8111610d78575b50601f899211600114610d065792610bec928e6104cd9e9f600494610ccf9a9998978e92610cfb575b50508160011b915f199060031b1c19161790555b60ff60281b19815416815560015463ffffffff60401b82549160301b169063ffffffff60401b191617815501916001600160401b0381166001600160401b031984541617835563ffffffff60025460d01c1690612c2e565b8154906001600160401b0360801b9060801b16906001600160401b0360801b191617905589865260128552610ca560ff608060408920998051610c2e816127cc565b610c37816127cc565b8b5462ff000067ffff0000000000008c85015160301b169260101b169067ffff000000ff0000191617178b55610c88836060830151168c9063ff00000082549160181b169063ff0000001916179055565b0151895464ff000000001916911660201b64ff0000000016178855565b6001870180546001600160a01b0319166001600160a01b039092169190911790556004860161368a565b806024610cdb82612c61565b96610ce960405198896129d7565b82885201838701378401015283614f18565b015190505f80610b80565b828a52888a209190601f198f168b5b818110610d6157506104cd9e9f600494610ccf9a99989794610bec97948360019510610d49575b505050811b019055610b94565b01515f1960f88460031b161c191690555f8080610d3c565b92938b600181928786015181550195019301610d15565b838b9a929495969798999a52818b2091601f840160051c83019310610ddc575b918f99989796959493918f93601f0160051c01905b818110610dba5750610b57565b90925060019193949596979899508a815501918d918f99989796959493610dad565b9091508190610d98565b634e487b7160e01b88526041600452602488fd5b60481c6001600160401b031691505f610aa3565b506001600160401b0383166001600160401b038260481c1611610a9e565b634e487b7160e01b86526021600452602486fd5b610e4990614b3b565b5f610a66565b634e487b7160e01b85526021600452602485fd5b6365e52d5160e11b8252600482fd5b6372b89e9760e11b8152600490fd5b8380fd5b503461026c578060031936011261026c57610200604051610ea58161299f565b8281528260208201528260408201528260608201528260808201528260a08201528260c08201528260e08201528261010082015282610120820152826101408201528261016082015282610180820152826101a0820152826101c0820152826101e08201520152610220610f17612c89565b610f246040518092612887565bf35b503461026c57602036600319011261026c5760606101e0604051610f49816129bb565b83815283602082015283604082015283838201528360808201528360a08201528360c08201528360e08201528361010082015283610120820152836101408201528361016082015283610180820152836101a0820152836101c08201520152600435815260126020526040812060405190610fc3826129bb565b80549060ff82166007811015610e4f57835260ff8260081c166005811015610e4f576110b691600591602086015260ff8460101c16611001816127cc565b604086015260ff8460181c16606086015260ff8460201c16608086015260ff8460281c16151560a086015261ffff8460301c1660c086015263ffffffff8460401c1660e086015261010085019360601c845260018060a01b0360018201541661012086015260018060a01b0360028201541661014086015260038101546101608601526001600160401b036004820154818116610180880152818160401c166101a088015260801c166101c0860152016135ec565b6101e08301526040519260208452825160078110156111f857602085015260208301519060058210156111e4575083926111e0926101e09260408601526040820151611101816127cc565b606086015260ff606083015116608086015260ff60808301511660a086015260a0820151151560c086015261ffff60c08301511660e086015263ffffffff60e08301511661010086015260018060a01b0390511661012085015260018060a01b036101208201511661014085015260018060a01b03610140820151166101608501526101608101516101808501526001600160401b03610180820151166101a08501526001600160401b036101a0820151166101c08501526001600160401b036101c08201511682850152015161020080840152610220830190612863565b0390f35b634e487b7160e01b81526021600452602490fd5b634e487b7160e01b82526021600452602482fd5b503461026c57602036600319011261026c57600435600381101561052a5761123261486f565b61123b816127cc565b60ff60015460081c1661124d816127cc565b60ff8216908082101561134d57600290611266816127cc565b036113435761128063ffffffff60025460901c16926127cc565b60ff60025460201c1690604051906112996020836129d7565b84825284600260ff60015460081c166112b1816127cc565b14908161133a575b81611330575b5061132157916104b76104ae63ffffffff6112e5600189966104cd9860209b6005614cd3565b8086526012895260408620600254815463ff00000090911664ffff000000199091161760209690961b64ff00000000169590951785559661049e565b6372b89e9760e11b8552600485fd5b955050845f6112bf565b600191506112b9565b61128083926127cc565b63710a5e0f60e01b8452600484fd5b503461026c57602036600319011261026c5761138760406111e0926004358152601360205220612bbb565b604051918291602083526020830190612827565b503461026c57604036600319011261026c576004356113b86148c5565b6113c1816148dc565b6113ca82613276565b1561141c5761040061ff00198254161790556113e7600554612b51565b6005556040519060243582527fcf4be29f7d91e92478a19a3eab52f6161f4e4580fcd2f5f8715f191dc2647bbb60203393a380f35b63ef28671760e01b8352600483fd5b503461026c57602036600319011261026c5760043580825260126020526040822060ff815460081c166005811015611503576001036114f4576001600160401b03600482015460801c164211156114c05761030061ff0019825416179055611494600554612b51565b60055533907f74c34a008ce735d9fcf0bd03a9b238d212ad4c441c020661f4ffbb6442645b858380a380f35b60405163935f84bf60e01b815260206004820152600b60248201526a1b9bdd08195e1c1a5c995960aa1b6044820152606490fd5b633ee3282d60e11b8352600483fd5b634e487b7160e01b84526021600452602484fd5b503461026c578060031936011261026c576020600654604051908152f35b503461026c57602036600319011261026c57611552600435613396565b80f35b503461026c57602036600319011261026c5761138760406111e0926004358152601460205220612bbb565b503461026c57604036600319011261026c5761159a6127a0565b6024359060ff821691828103610e81576115b261486f565b6001600160a01b0382168015611699576115cb82614bfa565b8452600860205260ff60408520541690838214611667576115eb91614c26565b906115f58261495e565b92604051916116056020846129d7565b85835285600260ff60015460081c1661161d816127cc565b14908161165f575b81611653575b5061164457926020959261045b926104cd956003614cd3565b6372b89e9760e11b8652600486fd5b9650600190505f61162b565b859150611625565b60405163935f84bf60e01b81526020600482015260096024820152686e6f206368616e676560b81b6044820152606490fd5b63d92e233d60e01b8552600485fd5b503461026c57602036600319011261026c576116c26127a0565b6116ca61489a565b6001600160a01b038116801561174e578252600e60205260ff604083205416600681101561173a576116ff5761155290614b3b565b60405163935f84bf60e01b8152602060048201526012602482015271185b1c9958591e481c9959da5cdd195c995960721b6044820152606490fd5b634e487b7160e01b83526021600452602483fd5b63d92e233d60e01b8352600483fd5b503461026c57602036600319011261026c57602061177c600435613276565b6040519015158152f35b503461026c57602036600319011261026c576020906040906001600160a01b036117ae6127a0565b168152601083522054604051908152f35b503461026c578060031936011261026c57604051906007548083528260208101600784526020842092845b8181106118ca5750506117ff925003836129d7565b815161182361180d8261324b565b9161181b60405193846129d7565b80835261324b565b602082019290601f1901368437805b8451811015611872576001906001600160a01b036118508288613262565b51168352600860205260ff60408420541661186b8286613262565b5201611832565b5061188f9392916020604051958695604087526040870190612827565b918583038287015251918281520192915b8181106118ae575050500390f35b825160ff168452859450602093840193909201916001016118a0565b84546001600160a01b03168352600194850194879450602090930192016117ea565b503461026c57606036600319011261026c5760c061191c61190b6127a0565b6119136127b6565b60443591612d80565b63ffffffff60a0604051928051611932816127cc565b845261ffff602082015116602085015261ffff604082015116604085015260ff606082015116606085015260ff608082015116608085015201511660a0820152f35b503461026c57604036600319011261026c5761198e6127a0565b6001600160a01b0316602435811561174e578015611a28576119e06040516323b872dd60e01b6020820152336024820152306044820152826064820152606481526119da6084826129d7565b8361563e565b8183526011602052604083206119f7828254612c7c565b90556040519081527f5548c837ab068cf56a2c2479df0882a4922fd203edb7517321831d95078c5f6260203392a380f35b6365e52d5160e11b8352600483fd5b503461026c578060031936011261026c57602060ff600a5460101c16604051908152f35b503461026c57604036600319011261026c57611a756127a0565b6024356006811015611b7557611a8961486f565b6001600160a01b0382168015611b66578115611b28578352600e602052611ab78160ff604086205416614a45565b90611ac18261495e565b9260405191611ad16020846129d7565b85835285600260ff60015460081c16611ae9816127cc565b149081611b20575b81611b14575b5061164457926020959261045b9260ff6104cd9616906004614cd3565b9650600190505f611af7565b859150611af1565b60405163935f84bf60e01b8152602060048201526015602482015274757365204e455720746f2072652d70726f6261746560581b6044820152606490fd5b63d92e233d60e01b8452600484fd5b8280fd5b503461026c57602036600319011261026c5760a0906001600160401b03611b9e6127a0565b8283908492600180881b03811695868152600f60205260408120968152600d602052611c0960408220600460405191611bd683612970565b60ff8154161515835260018101546020840152600281015460408401526003810154606084015201546080820152614976565b929050875487611c1a818316612c0e565b164210611cd95782985b6003549061ffff8260201c169283611c5a575b505050505050604051958652602086015260408501526060840152166080820152f35b9598509396509194509260401c8716919060301c63ffffffff1687611c7f8285612c2e565b164210611cae57505050611c9f61271091611c9a85946149d3565b612c4e565b0491925b5f8080808080611c37565b9194509250612710611ccc611cd39360026003870154960154612c4e565b0493612c2e565b92611ca3565b600189015498611c24565b503461026c578060031936011261026c57604051600b8054808352908352909160208301917f0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01db9915b818110611d43576111e085611387818703826129d7565b82546001600160a01b0316845260209093019260019283019201611d2c565b503461026c57602036600319011261026c57604060a091611d816127a0565b611d896129f8565b50600180851b03168152600e6020522060405190611da682612970565b6001600160401b038154611dbd60ff821685612baf565b816020850193818360081c1685528160016040880192828660481c1684528260608a019660881c16865201549560808801968752611dff60405180995161281a565b511660208701525116604085015251166060830152516080820152f35b503461026c578060031936011261026c57602063ffffffff60015460101c16604051908152f35b503461026c578060031936011261026c57602060ff600a5460181c16604051908152f35b503461026c57602036600319011261026c576020906040906001600160a01b03611e8f6127a0565b168152601183522054604051908152f35b503461206e5760c036600319011261206e57611eba6127a0565b60a036602319011261206e57611ece61486f565b73d587b84371f2f55ab35cfe2f64787d6829b85a2d803b1561206e57604051631c71447d60e01b8152611f0360048201612b71565b5f8160a481855af480156120635761204e575b5060018060a01b0382168352600d60205260206004610144604086206040519485938492637309439160e01b845260ff81541615158285015260018101546024850152600281015460448501526003810154606485015201546084830152611f8060a48301612b71565b5af4908115612043578391612009575b50604051611fa060208201612b71565b60a08152611faf60c0826129d7565b611fb88261495e565b9284600260ff60015460081c16611fce816127cc565b149081612001575b81611ff5575b50611321576104cd92859260209661045b936002614cd3565b9550600190505f611fdc565b849150611fd6565b90506020813d60201161203b575b81612024602093836129d7565b81010312611b755761203590612ba2565b5f611f90565b3d9150612017565b6040513d85823e3d90fd5b61205b9193505f906129d7565b5f915f611f16565b6040513d5f823e3d90fd5b5f80fd5b3461206e57602036600319011261206e5760043561208f816148dc565b805460601c331415806120f3575b6120e45761030061ff00198254161790556120b9600554612b51565b60055533907f74c34a008ce735d9fcf0bd03a9b238d212ad4c441c020661f4ffbb6442645b855f80a3005b6367909b1560e01b5f5260045ffd5b50335f526008602052600160405f2054161561209d565b3461206e57604036600319011261206e57600435600381101561206e5761212f614844565b612138816127cc565b60ff60015460081c1661214a816127cc565b60ff821611156121ab576001549060ff8260081c1691612169826127cc565b61ff001916600882901b61ff001617600155612184826127cc565b61218d816127cc565b6040519160243583525f5160206157295f395f51905f5260203394a4005b63216a142160e11b5f5260045ffd5b3461206e57602036600319011261206e576001600160a01b036121db6127a0565b165f526008602052602060ff60405f205416604051908152f35b3461206e576102a036600319011261206e576004356001600160401b03811161206e576122269036906004016127ea565b906024356001600160401b03811161206e576122469036906004016127ea565b90919061022036604319011261206e57610264356001600160401b03811161206e576122769036906004016127ea565b909261028435926001600160401b03841161206e573660238501121561206e578360040135956001600160401b03871161206e5736602460a089028701011161206e5760015460ff81166126085760019060ff1916176001558188148015906125fe575b6125cf575f5b88811061253e57505050505f5b8181106124a457856040516123018161299f565b60443560ff8116810361206e57815260643560ff8116810361206e57602082015260843560ff8116810361206e57604082015260a43560ff8116810361206e57606082015260c43560ff8116810361206e57608082015260e435801515810361206e5760a08201526101043563ffffffff8116810361206e5760c08201526101243563ffffffff8116810361206e5760e08201526101443563ffffffff8116810361206e576101008201526101643563ffffffff8116810361206e576101208201526101843563ffffffff8116810361206e576101408201526101a43563ffffffff8116810361206e576101608201526101c43561ffff8116810361206e576101808201526101e43561ffff8116810361206e576101a08201526102043561ffff8116810361206e576101c08201526102243561ffff8116810361206e576101e0820152610244359163ffffffff8316830361206e5761248a827f4488062cfafbbb2c1d440f13796e120c4522761f74ecade440ee8f31f1bfbee09461020060409501526145c5565b63ffffffff60015460101c169082519182526020820152a1005b6124b76124b2828487612b1f565b612b2f565b908581101561252a5760a08102840160a0602319823603011261206e576040516124e081612970565b602482013593841515850361206e5760a4612524936001968452604481013560208501526064810135604085015260848101356060850152013560808301526143f9565b016122ed565b634e487b7160e01b5f52603260045260245ffd5b6001600160a01b036125546124b2838c88612b1f565b165f52600860205260ff60405f2054166125c05760ff61257d612578838686612b1f565b612b43565b16156125b157806125ab6125976124b26001948d89612b1f565b6125a5612578848888612b1f565b9061426b565b016122e0565b633a767e1d60e21b5f5260045ffd5b630c8ce74760e11b5f5260045ffd5b60405163935f84bf60e01b81526020600482015260066024820152650d8cadccee8d60d31b6044820152606490fd5b50868414156122da565b62dc149f60e41b5f5260045ffd5b3461206e575f36600319011261206e57602060ff60015460081c166040519061263e816127cc565b8152f35b3461206e57602036600319011261206e576040612660600435612a22565b82519182526020820152f35b3461206e57604036600319011261206e576126856127b6565b6004355f52601560205260405f209060018060a01b03165f52602052602060ff60405f2054166040519015158152f35b3461206e57602036600319011261206e576126ce6127a0565b6126d66129f8565b5060018060a01b03165f52600d60205260a060405f206004604051916126fb83612970565b60ff8154161515835260018101546020840152600281015460408401526003810154606084015201546080820152610f246040518092608080918051151584526020810151602085015260408101516040850152606081015160608501520151910152565b3461206e575f36600319011261206e57602060ff600a5460081c16604051908152f35b3461206e575f36600319011261206e5760209060ff600a54168152f35b600435906001600160a01b038216820361206e57565b602435906001600160a01b038216820361206e57565b600311156127d657565b634e487b7160e01b5f52602160045260245ffd5b9181601f8401121561206e578235916001600160401b03831161206e576020808501948460051b01011161206e57565b9060068210156127d65752565b90602080835192838152019201905f5b8181106128445750505090565b82516001600160a01b0316845260209384019390920191600101612837565b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b63ffffffff610200809260ff815116855260ff602082015116602086015260ff604082015116604086015260ff606082015116606086015260ff608082015116608086015260a0810151151560a08601528260c08201511660c08601528260e08201511660e0860152826101008201511661010086015282610120820151166101208601528261014082015116610140860152826101608201511661016086015261ffff6101808201511661018086015261ffff6101a0820151166101a086015261ffff6101c0820151166101c086015261ffff6101e0820151166101e0860152015116910152565b60a081019081106001600160401b0382111761298b57604052565b634e487b7160e01b5f52604160045260245ffd5b61022081019081106001600160401b0382111761298b57604052565b61020081019081106001600160401b0382111761298b57604052565b90601f801991011681019081106001600160401b0382111761298b57604052565b60405190612a0582612970565b5f6080838281528260208201528260408201528260608201520152565b90815f52601260205260405f20825f52601360205260ff60405f2091541660078110156127d657612b17576002905b5f918154915f5b838110612acc5750505050915f52601460205260405f205f908054905f5b828110612a835750505090565b612a8d8183613315565b60018060a01b0391549060031b1c165f526008602052600860405f205416612ab8575b600101612a76565b92612ac460019161381e565b939050612ab0565b612ad68183613315565b60018060a01b0391549060031b1c165f52600860205260ff8360405f20541616612b03575b600101612a58565b93612b0f60019161381e565b949050612afb565b600190612a51565b919081101561252a5760051b0190565b356001600160a01b038116810361206e5790565b3560ff8116810361206e5790565b8015612b5d575f190190565b634e487b7160e01b5f52601160045260245ffd5b60243580151580910361206e578152604435602082015260643560408201526084356060820152608060a435910152565b5190811515820361206e57565b60068210156127d65752565b90604051918281549182825260208201905f5260205f20925f5b818110612bec575050612bea925003836129d7565b565b84546001600160a01b0316835260019485019487945060209093019201612bd5565b6001600160401b0362015180911601906001600160401b038211612b5d57565b906001600160401b03809116911601906001600160401b038211612b5d57565b81810292918115918404141715612b5d57565b6001600160401b03811161298b57601f01601f191660200190565b91908201809211612b5d57565b60405190612c968261299f565b8160025460ff8116825260ff8160081c16602083015260ff8160101c16604083015260ff8160181c16606083015260ff8160201c16608083015260ff8160281c16151560a083015263ffffffff8160301c1660c083015263ffffffff8160501c1660e083015263ffffffff8160701c1661010083015263ffffffff8160901c1661012083015263ffffffff8160b01c1661014083015263ffffffff8160d01c1661016083015260f01c61018082015261020063ffffffff60035461ffff81166101a085015261ffff8160101c166101c085015261ffff8160201c166101e085015260301c16910152565b92916040519160c083018381106001600160401b0382111761298b576040525f835260208301925f845260408101915f835260608201945f865260808301935f85525f60a0850152839860018060a01b038116805f52600d60205260405f209460405195612ded87612970565b60ff81541615918215885260018201549260208901938452600460028401549360408b01948552600381015460608c0152015460808a01526132385760018060a01b031693845f52600e60205260405f209760405195612e4c87612970565b60018a549a612e5e60ff8d168a612baf565b6001600160401b038c60081c1660208a01526001600160401b0360408a019c8d828260481c16905260881c1660608a015201546080880152865160068110156127d6576005146132265750612eb290614976565b9490505f52600f602052612ec960405f20916149d3565b92519151906001600160401b03612ee281835416612c0e565b16421061321c57505f9a5b85519560068710156127d6575160068110156127d6576001149889613208575b5060ff60015460081c1660025460f01c9161ffff6003541697604051976101608901928984106001600160401b0385111761298b57612f75936040528952602089019788526040890196875260608901958652608089015260a088019e8f5260c08801612baf565b60e086019915158a52612f87816127cc565b6101008601526101208501526101408401948552835190515f9283918311156131e3575050612fc161ffff916002809417945b5190615696565b94511680151590816131d4575b501561317d5750600860029117955b60c083015160068110156127d65760040361311c575050602060029517975b6080820151151590816130d8575b509360ff9860a0989794613074948b989463ffffffff986130a1575b6101006001910151613037816127cc565b613040816127cc565b14613085575b61ffff90811690915216905261305b816127cc565b808352613067816127cc565b61306f612c89565b614ab3565b939093169190950152169052169052565b9061ffff60406130968295976156f5565b961793505090613046565b92946130ac816127cc565b6001818b16106130ca5761010060806001925b971794915050613026565b5060016101006080826130bf565b6130ef915097969397959194929551855190612c7c565b918260011b9280840460021490151715612b5d5760808501519697939691959094909290911060ff61300a565b60c083015160068110156127d657613143575061313b610110916156f5565b951797612ffc565b60c0839a92979a015160068110156127d6576001149081613172575b5015612ffc57979461313b6010916156f5565b90505115155f61315f565b909561ffff6101208401511680151590816131c5575b5015612fdd5795906131a4816127cc565b600160ff8216106131bb576004905b911795612fdd565b50600460016131b3565b905061ffff851610155f613193565b905061ffff851610155f612fce565b939092935182116131fc575b61ffff91612fc191612fba565b600193508392506131ef565b516001600160401b0316421098505f612f0d565b600101549a612eed565b63325fbd1560e11b5f5260045260245ffd5b8363a6e7da2360e01b5f5260045260245ffd5b6001600160401b03811161298b5760051b60200190565b805182101561252a5760209160051b010190565b5f52601260205260405f205460ff8160081c1660058110156127d6576001036132f85760ff8160281c166132f25760ff811660078110156127d6576132f857600260ff8260101c166132c7816127cc565b146132f25760301c6010166132ed5760ff60015460081c166132e8816127cc565b151590565b600190565b50600190565b505f90565b60075481101561252a5760075f5260205f2001905f90565b805482101561252a575f5260205f2001905f90565b60075490600160401b82101561298b5761334f826001612bea94016007556007613315565b81546001600160a01b0393841660039290921b91821b9390911b1916919091179055565b90815491600160401b83101561298b578261334f916001612bea95018155613315565b61339f816148dc565b335f52600860205260405f205490600882166134bb575460ff1660078110156127d657600681146134a75761349c5760ff600280925b16161561348657505f81815260156020908152604080832033845290915290205460ff1661347757805f52601560205260405f2060018060a01b0333165f5260205260405f20600160ff19825416179055805f52601360205261343b3360405f20613373565b805f52601360205260405f2054906040519182527f2f0f51d2f12c71357e60d575d47e93e081dbaf32072245ab1e01540f0aadbf1a60203393a3565b63080fc0bd60e11b5f5260045ffd5b60ff9063e84c979760e01b5f521660045260245ffd5b60ff600180926133d5565b63e84c979760e01b5f52600860045260245ffd5b60ff91505460201c1615613563575f81815260156020908152604080832033845290915290205460ff1661347757805f52601560205260405f2060018060a01b0333165f5260205260405f20600160ff19825416179055805f5260146020526135273360405f20613373565b805f52601460205260405f2054906040519182527fb8c7de51f1e8e2b6cec2574c8731fba6de6e3e3002b2e454ebccdf32f591ca8660203393a3565b60405163935f84bf60e01b815260206004820152602260248201527f677561726469616e20636f6e6669726d6174696f6e206e6f7420726571756972604482015261195960f21b6064820152608490fd5b90600182811c921680156135e2575b60208310146135ce57565b634e487b7160e01b5f52602260045260245ffd5b91607f16916135c3565b9060405191825f8254926135ff846135b4565b80845293600181169081156136685750600114613624575b50612bea925003836129d7565b90505f9291925260205f20905f915b81831061364c575050906020612bea928201015f613617565b6020919350806001915483858901015201910190918492613633565b905060209250612bea94915060ff191682840152151560051b8201015f613617565b9067ffffffffffffffff60401b82549160401b169067ffffffffffffffff60401b1916179055565b60043560ff811680910361206e57815260243560ff811680910361206e57602082015260443560ff811680910361206e57604082015260643560ff811680910361206e57606082015260843560ff811680910361206e57608082015260a43580151580910361206e5760a082015260c43563ffffffff811680910361206e5760c082015260e43563ffffffff811680910361206e5760e08201526101043563ffffffff811680910361206e576101008201526101243563ffffffff811680910361206e576101208201526101443563ffffffff811680910361206e576101408201526101643563ffffffff811680910361206e576101608201526101843561ffff811680910361206e576101808201526101a43561ffff811680910361206e576101a08201526101c43561ffff811680910361206e576101c08201526101e43561ffff811680910361206e576101e0820152610204359063ffffffff821680920361206e576102000152565b5f198114612b5d5760010190565b519060ff8216820361206e57565b519063ffffffff8216820361206e57565b519061ffff8216820361206e57565b613863816148dc565b805460ff8116906007821015806127d657821591821561422b57335f526008602052600460405f20541615614218575b6001600160401b03600486015460401c1680421061420657506001549082600260ff8460081c166138c3816127cc565b1490816141d6575b506141c75760ff8160181c169160ff8260201c16936127d6578490816141aa575b5061416f575b506138fc86612a22565b919081811061415a57505081811061414557505080614132575b61020061ff001984541617835561392e600554612b51565b60055561393c60065461381e565b60065533847f9c85b616f29fca57a17eafe71cf9ff82ffef41766e2cf01ea7f8f7878dd3ec245f80a315613a56575060018060a01b03600182015416906002810191600360018060a01b0384541692019382828654927feb669e0a01fff40cbe45de851f42334fe70673136abfd91399651b7ae199a07b6020604051868152a481613a1e575050905491545f92839250829182916001600160a01b03165af13d15613a19573d6139eb81612c61565b906139f960405192836129d7565b81525f60203d92013e5b15613a0a57565b633d2cec6f60e21b5f5260045ffd5b613a03565b90919250612bea93506040519263a9059cbb60e01b60208501526024840152604483015260448252613a516064836129d7565b61563e565b60018103613bc05750613a6c91506005016135ec565b6102208180518101031261206e57612bea90613bb561022060405192613a918461299f565b613a9d6020820161382c565b8452613aab6040820161382c565b6020850152613abc6060820161382c565b6040850152613acd6080820161382c565b6060850152613ade60a0820161382c565b6080850152613aef60c08201612ba2565b60a0850152613b0060e0820161383a565b60c0850152613b12610100820161383a565b60e0850152613b24610120820161383a565b610100850152613b37610140820161383a565b610120850152613b4a610160820161383a565b610140850152613b5d610180820161383a565b610160850152613b706101a0820161384b565b610180850152613b836101c0820161384b565b6101a0850152613b966101e0820161384b565b6101c0850152613ba9610200820161384b565b6101e08501520161383a565b6102008201526145c5565b60028103613c3d575060028101546001600160a01b03169150613be5906005016135ec565b60a08180518101031261206e57612bea9160a060405192613c0584612970565b613c1160208201612ba2565b8452604081015160208501526060810151604085015260808101516060850152015160808301526143f9565b60038103613c7057506002810154600390910154613c68925060ff16906001600160a01b031661426b565b612bea61550f565b60048103613d8a5750905060ff600360018060a01b0360028401541692015416600681109081156127d657825f52600e60205260405f209160ff8354169060068210156127d6578115613d65575b60018314613d17575b156127d657613d14827f155590dadece0bf62c36a0e5cabcce51e1280ff0e8f6cc53c14d8900e0f43daf9460409460ff19825416179055613d0a8451809461281a565b602083019061281a565ba2565b613d60613d3863ffffffff60025460701c166001600160401b034216612c2e565b855467ffffffffffffffff60481b191660489190911b67ffffffffffffffff60481b16178555565b613cc7565b835468ffffffffffffffff0019164260081b68ffffffffffffffff0016178455613cbe565b600503613e325760039150015460ff8116613da4816127cc565b613dad816127cc565b60ff60015460081c16613dbf816127cc565b811015613e235760015461ff008060ff8360081c1694613dde856127cc565b60081b16169061ff00191617600155613df6826127cc565b613dff816127cc565b60405191640e4cad8c2f60db1b83525f5160206157295f395f51905f5260203394a4565b63710a5e0f60e01b5f5260045ffd5b60028101546001600160a01b031690613e4d906005016135ec565b60208180518101031261206e57602001516001600160a01b038116929083900361206e57815f52600860205260ff60405f205416801561412357835f52600860205260ff60405f2054166125c057821561411457613eaa5f614bfa565b825f5260086020528260ff60405f205416801580809161410d575b15614007575060405f5160206157095f395f51905f5291613ee58461332a565b600754845f526009602052825f20555b613eff5f826153c3565b613f095f8261540d565b613f135f82615461565b613f1d5f826154b7565b835f526008602052815f2060ff19815416905581519081525f6020820152a2613f46818561426b565b613f4e61550f565b60ff60015460081c16613f60816127cc565b15613f93575b60207f607a6564adeb3eec35d536d94fa031ad71407e7510a784e53f7bf36d44f3bc4191604051908152a4565b600154600881901c60ff169061ff00191661010017600155613fb4816127cc565b7f607a6564adeb3eec35d536d94fa031ad71407e7510a784e53f7bf36d44f3bc4191602091600160405191677265636f7665727960c01b83525f5160206157295f395f51905f52853394a4915050613f66565b1580614105575b614029575b60405f5160206157095f395f51905f5291613ef5565b905f52600960205260405f20545f198101818111612b5d576007545f198101908111612b5d578082036140c7575b5050506007549081156140b35760405f5160206157095f395f51905f529186935f190161409b614086826132fd565b81549060018060a01b039060031b1b19169055565b600755835f5260096020525f82812055915050614013565b634e487b7160e01b5f52603160045260245ffd5b61334f916140d76140f1926132fd565b905460039190911b1c6001600160a01b03169283916132fd565b5f52600960205260405f20555f8080614057565b50600161400e565b505f613ec5565b63d92e233d60e01b5f5260045ffd5b63148fe22160e11b5f5260045ffd5b61413b8361500b565b6139165750505050565b63d495223560e01b5f5260045260245260445ffd5b635ab4617560e01b5f5260045260245260445ffd5b61418260ff809260101c1661306f612c89565b5091168281116141a1575b5060ff16828111156138f25791505f6138f2565b915060ff61418d565b63ffffffff915060101c1663ffffffff8260401c1614155f6138ec565b6372b89e9760e11b5f5260045ffd5b90506127d6578384156141ea575b836138cb565b5060ff8160281c1680156141e457505f925060058514156141e4565b6384280dad60e01b5f5260045260245ffd5b63e84c979760e01b5f526004805260245ffd5b5f9150600684036142435761423e614844565b613893565b335f526008602052600560405f2054166138935763e84c979760e01b5f52600560045260245ffd5b6001600160a01b0381169182156141145761428581614bfa565b825f52600860205260ff60405f2054169182158080916143ed575b1561431f57509160ff6040926142c35f5160206157095f395f51905f529561332a565b600754865f526009602052845f20555b6142dd81846153c3565b6142e7818461540d565b6142f18184615461565b6142fb81846154b7565b855f526008602052835f2082821683198254161790558351928352166020820152a2565b905015806143e2575b614346575b5f5160206157095f395f51905f529160ff6040926142d3565b825f52600960205260405f20545f198101818111612b5d576007545f198101908111612b5d578082036143be575b5050506007549182156140b35760ff6040925f5160206157095f395f51905f52945f19016143a4614086826132fd565b600755855f5260096020525f84812055925050915061432d565b61334f916140d76143ce926132fd565b5f52600960205260405f20555f8080614374565b5060ff811615614328565b5060ff831615156142a0565b9073d587b84371f2f55ab35cfe2f64787d6829b85a2d803b1561206e57604051631c71447d60e01b81525f8160a48183956144616004830189608080918051151584526020810151602085015260408101516040850152606081015160608501520151910152565b5af48015612063576145b2575b506001600160a01b038316808252600c602052604082205490939060ff1615614532575b507f9d182ff993316b1c662980e27e42d42fae0bd158f1bed04e9988ec637fea8da4916040828560a09452600d602052208151151560ff8019835416911617815560208201516001820155604082015160028201556060820151600382015560046080830151910155613d146040518092608080918051151584526020810151602085015260408101516040850152606081015160608501520151910152565b838252600c60205260408220805460ff19166001179055600b54600160401b81101561459e5760a0926145957f9d182ff993316b1c662980e27e42d42fae0bd158f1bed04e9988ec637fea8da4959361334f84600160409601600b55600b613315565b92505091614492565b634e487b7160e01b83526041600452602483fd5b6145be91505f906129d7565b5f5f61446e565b73d587b84371f2f55ab35cfe2f64787d6829b85a2d803b1561206e5760405163065397d760e21b81525f816102248183956146036004830189612887565b5af4801561206357614831575b50815160208084015160408501516060860151608087015160a088015160c089015160e08a01516101008b01516101208c01516101408d01516101608e01516101808f015160089b909b1b61ff001660ff9d909d169c909c176010998a1b62ff0000161760189890981b63ff0000001697909717958a1b64ff00000000169590951793151560281b60ff60281b1693909317603092831b69ffffffff0000000000009081169190911760509290921b63ffffffff60501b169190911760709390931b63ffffffff60701b169290921760909390931b63ffffffff60901b169290921760b09390931b63ffffffff60b01b169290921760d09690961b63ffffffff60d01b169590951760f09390931b6001600160f01b031916929092176002556101a0860151600380546101c08901516101e08a01516102008b015169ffffffffffffffffffff199390931661ffff959095169490941790851b63ffff000016179290951b65ffff0000000016919091179390941b9091169190911790915560018054929183901c63ffffffff9081169091019190821161481d575063ffffffff7f1a734774792c149a91deb03650b2a2bc51de7bbab7262857ace6fb1699b430419265ffffffff00006102409360101b169065ffffffff0000191617806001556147f861550f565b614815604051956001600160401b03421687526020870190612887565b60101c1692a2565b634e487b7160e01b81526011600452602490fd5b61483d91505f906129d7565b5f5f614610565b335f526008602052600960405f2054161561485b57565b63e84c979760e01b5f52600960045260245ffd5b335f526008602052600160405f2054161561488657565b63e84c979760e01b5f52600160045260245ffd5b335f526008602052600360405f205416156148b157565b63e84c979760e01b5f52600360045260245ffd5b335f526008602052600860405f205416156134a757565b80158015614953575b614944575f52601260205260405f209060ff825460081c1660058110156127d657600103614935576001600160401b03600483015460801c16421161492657565b6328a7237960e01b5f5260045ffd5b633ee3282d60e11b5f5260045ffd5b6302eae9e560e51b5f5260045ffd5b5060045481116148e5565b156149725763ffffffff60025460901c1690565b5f90565b906080606083015192015190600160ff815460081c16614995816127cc565b1461499c57565b826149bb575b816149a957565b9060011c806149b85750600190565b90565b9160011c806149ce575060015b916149a2565b6149c8565b6001600160a01b03168015614a40576020602491604051928380926370a0823160e01b82523060048301525afa908115612063575f91614a11575090565b90506020813d602011614a38575b81614a2c602093836129d7565b8101031261206e575190565b3d9150614a1f565b504790565b906006811015806127d657600282148015614aa7575b614a9f576127d6576001149081614a76575b506132ed575f90565b905060068110156127d65760048114908115614a94575b505f614a6d565b60059150145f614a8d565b505050600190565b50505f60038214614a5b565b9190614abe816127cc565b8015614b2e57600190614ad0816127cc565b14614b135760ff6040830151169060a083015115155f14614b055763ffffffff60e060ff608086015116945b01511691929190565b63ffffffff60e05f94614afc565b9063ffffffff60c060ff6020840151169201511690915f9190565b50905160ff16905f908190565b6001600160a01b03165f818152600e60209081526040909120805468ffffffffffffffffff191642600881901b68ffffffffffffffff00169190911760011782557fa834c3aabec8913fbfd0591ec9c2a7494c727a364cc1f231c948c0f9564b5d0692916001600160401b0391614bec90614bc490841663ffffffff60025460701c1690612c2e565b825467ffffffffffffffff60481b191660489190911b67ffffffffffffffff60481b16178255565b5460481c16604051908152a2565b600f60ff8216116125b1576008811615159081614c19575b506125b157565b600791501615155f614c12565b60ff8119831616614c515760081615159081614c4557506132ed575f90565b6008915016155f614a6d565b5050600190565b614c6190614976565b509081151580614cca575b614cb557505061ffff60035460101c169081151580614ca8575b614c8e575050565b61ffff90630a8a293d60e01b5f521660045260245260445ffd5b508161ffff821611614c86565b634fa5781f60e11b5f5260045260245260445ffd5b50818111614c6c565b9290949391614ce360045461381e565b958660045586614cf460055461381e565b6005555f52601260205260405f209460078110156127d65785546bffffffffffffffffffffffff61ffff1990911660ff929092169190911761010017163360601b6001600160601b0319161785556002850180546001600160a01b0319166001600160a01b039092169190911790556003840155805160058401916001600160401b03821161298b578190614d8984546135b4565b601f8111614ec8575b50602090601f8311600114614e65575f92614e5a575b50508160011b915f199060031b1c19161790555b60ff60281b825491151560281b169060ff60281b191617815560015463ffffffff60401b82549160301b169063ffffffff60401b1916178155614e3260046001600160401b0342169201916001600160401b0381166001600160401b031984541617835563ffffffff60025460d01c1690612c2e565b815467ffffffffffffffff60801b191660809190911b67ffffffffffffffff60801b16179055565b015190505f80614da8565b5f8581528281209350601f198516905b818110614eb05750908460019594939210614e98575b505050811b019055614dbc565b01515f1960f88460031b161c191690555f8080614e8b565b92936020600181928786015181550195019301614e75565b909150835f5260205f20601f840160051c81019160208510614f0e575b90601f859493920160051c01905b818110614f005750614d92565b5f8155849350600101614ef3565b9091508190614ee5565b815460ff81169260018060a01b0360018201541660018060a01b0360028301541691600381015493600460ff8260101c169201549460078810156127d6577fcf5d862ed732f5aec4f98da191cd4e962a57abdc7bab078c436b0517229d388f95615006936001600160401b0392604051978897885260208801526040870152614fa0816127cc565b606086015261ffff8360301c16608086015260ff8360181c1660a086015260ff8360201c1660c0860152818160401c1660e086015260801c1661010084015260ff8160281c16151561012084015261016061014084015260601c96610160830190612863565b0390a4565b60018060a01b0360018201541690600381015491805f52600d60205260405f20916040519261503984612970565b600460ff8254161591821586526001810154602087015260028101546040870152600381015460608701520154608085015261538c57600201546001600160a01b03165f818152600e60205260409020805490939160ff82169060068210156127d65760058214613226575060011480615377575b61535857506150bc826149d3565b90818511615341576150e1906150dc6150d58488615696565b8783614c58565b614976565b919050825f52600f60205260405f20916001600160401b0361510581855416612c0e565b1642101561531c575b80151580615306575b6152e3575060035461ffff8160201c166151b5575b505060010161513c848254612c7c565b90555f52601060205260405f20615154838254612c7c565b905580549060016001600160401b038360881c1601906001600160401b038211612b5d5767ffffffffffffffff60881b1990921660889190911b67ffffffffffffffff60881b16178155600101805490916151ae91612c7c565b9055600190565b6151d86001600160401b039163ffffffff83865460401c169160301c1690612c2e565b164210156152bf575b506127106151fd600283015461ffff60035460201c1690612c4e565b0460038201908154816152108883612c7c565b1161522f57505090816152268660019454612c7c565b9055905f61512c565b9091507f72f5745158d621f0076c165a1d23a925d0b2f8c5b5fb643933649247142d156d945060609395925060405192835260208301526040820152a2600154600881901c60ff169061ff0019166102001760015561528d816127cc565b6002604051916e31b4b931bab4ba16b13932b0b5b2b960891b83525f5160206157295f395f51905f5260203394a45f90565b6152d26001600160401b0342168361368a565b60028201555f60038201555f6151e1565b6152f1866001850154612c7c565b6398b5494560e01b5f5260045260245260445ffd5b5080615316876001860154612c7c565b11615117565b825467ffffffffffffffff1916426001600160401b03161783555f600184015561510e565b508363cf47918160e01b5f5260045260245260445ffd5b6001600160401b03906384280dad60e01b5f5260481c1660045260245ffd5b506001600160401b038160481c1642106150ae565b5063a6e7da2360e01b5f5260045260245ffd5b60ff5f199116019060ff8211612b5d57565b60ff60019116019060ff8211612b5d57565b6001918216151591161515811461540a57156153f65760ff6153e881600a54166153b1565b1660ff19600a541617600a55565b60ff61540581600a541661539f565b6153e8565b50565b6002918216151591161515811461540a571561544b5761543460ff600a5460081c166153b1565b61ff00600a549160081b169061ff00191617600a55565b61545c60ff600a5460081c1661539f565b615434565b6004918216151591161515811461540a57156154a15761548860ff600a5460101c166153b1565b62ff0000600a549160101b169062ff0000191617600a55565b6154b260ff600a5460101c1661539f565b615488565b6008918216151591161515811461540a57156154f9576154de60ff600a5460181c166153b1565b63ff000000600a549160181b169063ff000000191617600a55565b61550a60ff600a5460181c1661539f565b6154de565b600a5460ff81166001811061560d57600160ff8360101c16106155d9576002549060ff8260181c161161559d5760ff8160101c1660ff8360081c16106155635760ff809160201c169160181c16106108b457565b604051637417de8f60e01b8152602060048201526011602482015270185c1c1c9bdd985b1cd0dc9a5d1a58d85b607a1b6044820152606490fd5b604051637417de8f60e01b815260206004820152601360248201527219dbdd995c9b985b98d9551a1c995cda1bdb19606a1b6044820152606490fd5b604051637417de8f60e01b815260206004820152600b60248201526a37379032bc32b1baba37b960a91b6044820152606490fd5b604051637417de8f60e01b815260206004820152600860248201526737379037bbb732b960c11b6044820152606490fd5b905f602091828151910182855af115612063575f513d61568d57506001600160a01b0381163b155b61566d5750565b635274afe760e01b5f9081526001600160a01b0391909116600452602490fd5b60011415615666565b9081156156ef5780158080156156e5575b6156dc57612710830292830461271003612b5d576156c85761ffff91041690565b634e487b7160e01b5f52601260045260245ffd5b50505061271090565b50818310156156a7565b50505f90565b6156fe816127cc565b156132ed5760029056fe5d3de0fa15f594659e83f792fff19b5062cbc430ea0e9cf465377cdfaab8845467ac1adbe98dcfa9ea51aab9f87f1ae3ca912bf37e541afd86007c5bde3e5311" - } - ], - "isFixedGasLimit": false - }, - { - "hash": "0x82e91e93384bbf8323e5419f1729c86595b72d92c3b6a9d9f4bb13ba97f522f5", - "transactionType": "CALL", - "contractName": "SkurFactory", - "contractAddress": "0x7a9c532528482b93191a68c6311762936dab60d5", - "function": "createVault(bytes32,address[],uint8[],(uint8,uint8,uint8,uint8,uint8,bool,uint32,uint32,uint32,uint32,uint32,uint32,uint16,uint16,uint16,uint16,uint32),address[],(bool,uint256,uint256,uint256,uint256)[])", - "arguments": [ - "0xfb983e9dbbaa3ef70fb8bb403a3f4b14671884646e3181a121177cf14b3664eb", - "[0x01e06CA25BF1e8711053D7Aa704A6B11eD7F4A22, 0x92e2349ADBDE041FB823552Ba99EbDba895a4A2d, 0xe90D2FE71BFeC93F730cF6b122675Da4437eC831]", - "[7, 2, 8]", - "(1, 2, 2, 1, 1, true, 21600, 86400, 86400, 86400, 172800, 604800, 500, 2000, 5000, 1000, 86400)", - "[0x1aA8c753EA542D6cCEbc451d0d9Cf7e8f2789835, 0x0000000000000000000000000000000000000000]", - "[(true, 5000000000, 25000000000, 100000000000, 100000000000), (true, 100000000000000000000, 1000000000000000000000, 0, 5000000000000000000000)]" - ], - "transaction": { - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "to": "0x7a9c532528482b93191a68c6311762936dab60d5", - "gas": "0x1376c0", - "value": "0x0", - "input": "0x5492a6ecfb983e9dbbaa3ef70fb8bb403a3f4b14671884646e3181a121177cf14b3664eb00000000000000000000000000000000000000000000000000000000000002c000000000000000000000000000000000000000000000000000000000000003400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000005460000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000000000000000015180000000000000000000000000000000000000000000000000000000000002a3000000000000000000000000000000000000000000000000000000000000093a8000000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000007d0000000000000000000000000000000000000000000000000000000000000138800000000000000000000000000000000000000000000000000000000000003e8000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000000000000003c00000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000000000000000000000000000000300000000000000000000000001e06ca25bf1e8711053d7aa704a6b11ed7f4a2200000000000000000000000092e2349adbde041fb823552ba99ebdba895a4a2d000000000000000000000000e90d2fe71bfec93f730cf6b122675da4437ec831000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000020000000000000000000000001aa8c753ea542d6ccebc451d0d9cf7e8f2789835000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000012a05f20000000000000000000000000000000000000000000000000000000005d21dba00000000000000000000000000000000000000000000000000000000174876e800000000000000000000000000000000000000000000000000000000174876e80000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000056bc75e2d6310000000000000000000000000000000000000000000000000003635c9adc5dea00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010f0cf064dd59200000", - "nonce": "0x3", - "chainId": "0x2328" - }, - "additionalContracts": [ - { - "transactionType": "CREATE2", - "contractName": null, - "address": "0xec07b7b9313d2e5bd881c2111054c24c0adcaf71", - "initCode": "0x3d602d80600a3d3981f3363d3d373d3d3d363d73ada220cacb3b0bff5944bbf35000d85df454806a5af43d82803e903d91602b57fd5bf3" - } - ], - "isFixedGasLimit": false - }, - { - "hash": "0xa062f737f24b4257cbafa20f44a7df8470c47e94a4f0a7211566aba87801136b", - "transactionType": "CALL", - "contractName": "SkurTestUSD", - "contractAddress": "0x1aa8c753ea542d6ccebc451d0d9cf7e8f2789835", - "function": "mint(address,uint256)", - "arguments": [ - "0xEC07B7B9313D2e5BD881c2111054C24c0adcAf71", - "250000000000" - ], - "transaction": { - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "to": "0x1aa8c753ea542d6ccebc451d0d9cf7e8f2789835", - "gas": "0x16f70", - "value": "0x0", - "input": "0x40c10f19000000000000000000000000ec07b7b9313d2e5bd881c2111054c24c0adcaf710000000000000000000000000000000000000000000000000000003a35294400", - "nonce": "0x4", - "chainId": "0x2328" - }, - "additionalContracts": [], - "isFixedGasLimit": false - }, - { - "hash": "0xcc23e3c8d6cca83d83770ec34b0d1bae0f69fe7298d114c813b2884268cc3558", - "transactionType": "CALL", - "contractName": null, - "contractAddress": "0xec07b7b9313d2e5bd881c2111054c24c0adcaf71", - "function": null, - "arguments": null, - "transaction": { - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "to": "0xec07b7b9313d2e5bd881c2111054c24c0adcaf71", - "gas": "0x10197", - "value": "0x1bc16d674ec80000", - "input": "0x", - "nonce": "0x5", - "chainId": "0x2328" - }, - "additionalContracts": [], - "isFixedGasLimit": false - } - ], - "receipts": [ - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0xb53a1", - "logs": [], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "transactionHash": "0xf315d135cecff9193c2eaa5a13e02d83728d9b1bf1b531a8b158610241a74cee", - "transactionIndex": "0x0", - "blockHash": "0xa062e658903eae464ab7aa330f9539abcab7df32894cf03b85b30b9fe81944ce", - "blockNumber": "0x739a1", - "gasUsed": "0xb53a1", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "to": null, - "contractAddress": "0xd587b84371f2f55ab35cfe2f64787d6829b85a2d" - }, - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0x75981", - "logs": [], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "transactionHash": "0xbe1f6926a7daaf7d0d004917a5636eea4ec79feec92de7990ff1309bc6cac2f9", - "transactionIndex": "0x0", - "blockHash": "0x999ff8d6ec6318fd77977adefe2f059920fc2f91dfc2d9a47fb3b90c7481fda0", - "blockNumber": "0x739a4", - "gasUsed": "0x75981", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "to": null, - "contractAddress": "0x1aa8c753ea542d6ccebc451d0d9cf7e8f2789835" - }, - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0x5386da", - "logs": [], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "transactionHash": "0x613a6936596ef96adaafa9754739306bc23d0999f3c2edf17f2c1f7b8a063e9a", - "transactionIndex": "0x0", - "blockHash": "0xb0934e52a05a8d8bf29b57550f942a5f72d47bc17a6f786567079e0bae52aa96", - "blockNumber": "0x739ab", - "gasUsed": "0x5386da", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "to": null, - "contractAddress": "0x7a9c532528482b93191a68c6311762936dab60d5" - }, - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0xe176d", - "logs": [ - { - "address": "0x7a9c532528482b93191a68c6311762936dab60d5", - "topics": [ - "0xc18dea5f6b30c3fd606e8e38832933e2fa57407dd77800aad0658505ee94df98", - "0x000000000000000000000000ec07b7b9313d2e5bd881c2111054c24c0adcaf71", - "0x00000000000000000000000001e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "0x0000000000000000000000000000000000000000000000000000000000000000" - ], - "data": "0xfb983e9dbbaa3ef70fb8bb403a3f4b14671884646e3181a121177cf14b3664eb", - "blockHash": "0x52f8f89537cab5c7cc212c1b72b7509eb129b4360469c20a03da5b118ca737f5", - "blockNumber": "0x739b0", - "blockTimestamp": "0x6aa8968f", - "transactionHash": "0x82e91e93384bbf8323e5419f1729c86595b72d92c3b6a9d9f4bb13ba97f522f5", - "transactionIndex": "0x0", - "logIndex": "0x0", - "removed": false - }, - { - "address": "0xec07b7b9313d2e5bd881c2111054c24c0adcaf71", - "topics": [ - "0x5d3de0fa15f594659e83f792fff19b5062cbc430ea0e9cf465377cdfaab88454", - "0x00000000000000000000000001e06ca25bf1e8711053d7aa704a6b11ed7f4a22" - ], - "data": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007", - "blockHash": "0x52f8f89537cab5c7cc212c1b72b7509eb129b4360469c20a03da5b118ca737f5", - "blockNumber": "0x739b0", - "blockTimestamp": "0x6aa8968f", - "transactionHash": "0x82e91e93384bbf8323e5419f1729c86595b72d92c3b6a9d9f4bb13ba97f522f5", - "transactionIndex": "0x0", - "logIndex": "0x1", - "removed": false - }, - { - "address": "0xec07b7b9313d2e5bd881c2111054c24c0adcaf71", - "topics": [ - "0x5d3de0fa15f594659e83f792fff19b5062cbc430ea0e9cf465377cdfaab88454", - "0x00000000000000000000000092e2349adbde041fb823552ba99ebdba895a4a2d" - ], - "data": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002", - "blockHash": "0x52f8f89537cab5c7cc212c1b72b7509eb129b4360469c20a03da5b118ca737f5", - "blockNumber": "0x739b0", - "blockTimestamp": "0x6aa8968f", - "transactionHash": "0x82e91e93384bbf8323e5419f1729c86595b72d92c3b6a9d9f4bb13ba97f522f5", - "transactionIndex": "0x0", - "logIndex": "0x2", - "removed": false - }, - { - "address": "0xec07b7b9313d2e5bd881c2111054c24c0adcaf71", - "topics": [ - "0x5d3de0fa15f594659e83f792fff19b5062cbc430ea0e9cf465377cdfaab88454", - "0x000000000000000000000000e90d2fe71bfec93f730cf6b122675da4437ec831" - ], - "data": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008", - "blockHash": "0x52f8f89537cab5c7cc212c1b72b7509eb129b4360469c20a03da5b118ca737f5", - "blockNumber": "0x739b0", - "blockTimestamp": "0x6aa8968f", - "transactionHash": "0x82e91e93384bbf8323e5419f1729c86595b72d92c3b6a9d9f4bb13ba97f522f5", - "transactionIndex": "0x0", - "logIndex": "0x3", - "removed": false - }, - { - "address": "0xec07b7b9313d2e5bd881c2111054c24c0adcaf71", - "topics": [ - "0x9d182ff993316b1c662980e27e42d42fae0bd158f1bed04e9988ec637fea8da4", - "0x0000000000000000000000001aa8c753ea542d6ccebc451d0d9cf7e8f2789835" - ], - "data": "0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000012a05f20000000000000000000000000000000000000000000000000000000005d21dba00000000000000000000000000000000000000000000000000000000174876e800000000000000000000000000000000000000000000000000000000174876e800", - "blockHash": "0x52f8f89537cab5c7cc212c1b72b7509eb129b4360469c20a03da5b118ca737f5", - "blockNumber": "0x739b0", - "blockTimestamp": "0x6aa8968f", - "transactionHash": "0x82e91e93384bbf8323e5419f1729c86595b72d92c3b6a9d9f4bb13ba97f522f5", - "transactionIndex": "0x0", - "logIndex": "0x4", - "removed": false - }, - { - "address": "0xec07b7b9313d2e5bd881c2111054c24c0adcaf71", - "topics": [ - "0x9d182ff993316b1c662980e27e42d42fae0bd158f1bed04e9988ec637fea8da4", - "0x0000000000000000000000000000000000000000000000000000000000000000" - ], - "data": "0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000056bc75e2d6310000000000000000000000000000000000000000000000000003635c9adc5dea00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010f0cf064dd59200000", - "blockHash": "0x52f8f89537cab5c7cc212c1b72b7509eb129b4360469c20a03da5b118ca737f5", - "blockNumber": "0x739b0", - "blockTimestamp": "0x6aa8968f", - "transactionHash": "0x82e91e93384bbf8323e5419f1729c86595b72d92c3b6a9d9f4bb13ba97f522f5", - "transactionIndex": "0x0", - "logIndex": "0x5", - "removed": false - }, - { - "address": "0xec07b7b9313d2e5bd881c2111054c24c0adcaf71", - "topics": [ - "0x1a734774792c149a91deb03650b2a2bc51de7bbab7262857ace6fb1699b43041", - "0x0000000000000000000000000000000000000000000000000000000000000001" - ], - "data": "0x000000000000000000000000000000000000000000000000000000006aa8968f0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000005460000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000000000000000015180000000000000000000000000000000000000000000000000000000000002a3000000000000000000000000000000000000000000000000000000000000093a8000000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000007d0000000000000000000000000000000000000000000000000000000000000138800000000000000000000000000000000000000000000000000000000000003e80000000000000000000000000000000000000000000000000000000000015180", - "blockHash": "0x52f8f89537cab5c7cc212c1b72b7509eb129b4360469c20a03da5b118ca737f5", - "blockNumber": "0x739b0", - "blockTimestamp": "0x6aa8968f", - "transactionHash": "0x82e91e93384bbf8323e5419f1729c86595b72d92c3b6a9d9f4bb13ba97f522f5", - "transactionIndex": "0x0", - "logIndex": "0x6", - "removed": false - }, - { - "address": "0xec07b7b9313d2e5bd881c2111054c24c0adcaf71", - "topics": [ - "0x4488062cfafbbb2c1d440f13796e120c4522761f74ecade440ee8f31f1bfbee0" - ], - "data": "0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003", - "blockHash": "0x52f8f89537cab5c7cc212c1b72b7509eb129b4360469c20a03da5b118ca737f5", - "blockNumber": "0x739b0", - "blockTimestamp": "0x6aa8968f", - "transactionHash": "0x82e91e93384bbf8323e5419f1729c86595b72d92c3b6a9d9f4bb13ba97f522f5", - "transactionIndex": "0x0", - "logIndex": "0x7", - "removed": false - } - ], - "logsBloom": "0x00100000000080080000000000000004000000000000000000000000000000000000040000000000000000000000004000000002011000000000000080040000000000000020000000000000000000800000000000040000000000000000800000000000020400000000000000000800080000000000040000000000000000000000000000000000000800000008000000000000000000002000000000000800000000000008000801000008000000000010000000002000002000000010000200000000000008000000000000000000000000000010000000000002000060000000000001000000001000000000000000000000000010000800000000000000", - "transactionHash": "0x82e91e93384bbf8323e5419f1729c86595b72d92c3b6a9d9f4bb13ba97f522f5", - "transactionIndex": "0x0", - "blockHash": "0x52f8f89537cab5c7cc212c1b72b7509eb129b4360469c20a03da5b118ca737f5", - "blockNumber": "0x739b0", - "gasUsed": "0xe176d", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "to": "0x7a9c532528482b93191a68c6311762936dab60d5", - "contractAddress": null - }, - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0x10a05", - "logs": [ - { - "address": "0x1aa8c753ea542d6ccebc451d0d9cf7e8f2789835", - "topics": [ - "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x000000000000000000000000ec07b7b9313d2e5bd881c2111054c24c0adcaf71" - ], - "data": "0x0000000000000000000000000000000000000000000000000000003a35294400", - "blockHash": "0xc7f1b69cbea7067c36483c347ce5f47a665c2f472ad8f2721d53ee2afc39d1d4", - "blockNumber": "0x739b5", - "blockTimestamp": "0x6aa896a0", - "transactionHash": "0xa062f737f24b4257cbafa20f44a7df8470c47e94a4f0a7211566aba87801136b", - "transactionIndex": "0x0", - "logIndex": "0x0", - "removed": false - } - ], - "logsBloom": "0x00000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000008000000000000000000000000000000000000000000000000020000000000000000000800000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002010000000000000000000000002000000000000000000000000000000000000000000000000000020000000000000000000001000000000000000000000000000000800000000000000", - "transactionHash": "0xa062f737f24b4257cbafa20f44a7df8470c47e94a4f0a7211566aba87801136b", - "transactionIndex": "0x0", - "blockHash": "0xc7f1b69cbea7067c36483c347ce5f47a665c2f472ad8f2721d53ee2afc39d1d4", - "blockNumber": "0x739b5", - "gasUsed": "0x10a05", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "to": "0x1aa8c753ea542d6ccebc451d0d9cf7e8f2789835", - "contractAddress": null - }, - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0xba7f", - "logs": [ - { - "address": "0xec07b7b9313d2e5bd881c2111054c24c0adcaf71", - "topics": [ - "0x5548c837ab068cf56a2c2479df0882a4922fd203edb7517321831d95078c5f62", - "0x00000000000000000000000001e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "0x0000000000000000000000000000000000000000000000000000000000000000" - ], - "data": "0x0000000000000000000000000000000000000000000000001bc16d674ec80000", - "blockHash": "0x27b63091da196f6ddffb926b03365a3c06a0574ff84fad8f8888b18303c08efc", - "blockNumber": "0x739bb", - "blockTimestamp": "0x6aa896b7", - "transactionHash": "0xcc23e3c8d6cca83d83770ec34b0d1bae0f69fe7298d114c813b2884268cc3558", - "transactionIndex": "0x0", - "logIndex": "0x0", - "removed": false - } - ], - "logsBloom": "0x00000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000004000000000001002000000000040000000000000000028000000000000000000000000000000000000000000000000000000000000020000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000", - "transactionHash": "0xcc23e3c8d6cca83d83770ec34b0d1bae0f69fe7298d114c813b2884268cc3558", - "transactionIndex": "0x0", - "blockHash": "0x27b63091da196f6ddffb926b03365a3c06a0574ff84fad8f8888b18303c08efc", - "blockNumber": "0x739bb", - "gasUsed": "0xba7f", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "to": "0xec07b7b9313d2e5bd881c2111054c24c0adcaf71", - "contractAddress": null - } - ], - "libraries": [ - "src/SkurPolicyLib.sol:SkurPolicyLib:0xd587B84371f2F55AB35CFE2f64787d6829B85a2D" - ], - "pending": [], - "returns": {}, - "timestamp": 1789433571992, - "chain": 9000, - "commit": null -} \ No newline at end of file diff --git a/contracts/broadcast/Deploy.s.sol/9000/run-1789443553946.json b/contracts/broadcast/Deploy.s.sol/9000/run-1789443553946.json deleted file mode 100644 index 65eac38..0000000 --- a/contracts/broadcast/Deploy.s.sol/9000/run-1789443553946.json +++ /dev/null @@ -1,432 +0,0 @@ -{ - "transactions": [ - { - "hash": "0x4cc1be49c4e66ca9efd9222231489d12e288735cc93d85ddf6282a8215540808", - "transactionType": "CREATE", - "contractName": "SkurPolicyLib", - "contractAddress": "0x689a4b4bb095cc9f2f9535eaf6d71e6dd0b55837", - "function": null, - "arguments": null, - "transaction": { - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "gas": "0xeb984", - "input": "0x60808060405234601957610c75908161001e823930815050f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c8063099fe59914610660578063194e5f5c1461018b5780631c71447d146100b957637309439114610045575f80fd5b6101403660031901126100b55761005b366109c0565b60a03660a31901126100b55761006f61082f565b60a4359081151582036100b5576020926100ab92825260c4358483015260e4356040830152610104356060830152610124356080830152610bf7565b6040519015158152f35b5f80fd5b60a03660031901126100b5576100ce366109c0565b60208101516040820151106101525760608101518015159182610144575b82610136575b50506100fa57005b6040516353e95a6760e01b81526020600482015260136024820152720e0cae4a8f09ac2f0407c40c8c2d2d8f29ac2f606b1b6044820152606490fd5b6080015110905081806100f2565b6080810151151592506100ec565b6040516353e95a6760e01b815260206004820152601060248201526f0d8deee9ac2f0407c40d0d2ced09ac2f60831b6044820152606490fd5b6102203660031901126100b5576101a13661084f565b60ff8151161561062b576020810160ff81511660ff835116116105f55760ff80604084015116915116116105bb5760ff6060820151161561057f5760a0810151151580610570575b6105365760e0810163ffffffff81511663ffffffff60c084015116116104f0576101a0820161ffff81511680151590816104dd575b506104a65761271061ffff825116118015610492575b61045d576101c083019061ffff825116801515918261044e575b50506104115761ffff612710915116116103db576101e0820161271061ffff825116116103a6575161ffff16151580610390575b6103595763ffffffff9051169063ffffffff61012082015116828111610351575b5063ffffffff61014082015116828111610349575b5063ffffffff6101008201511682811161033f575b5061016063ffffffff91015116610e10820180921161032b57106102ed57005b6040516353e95a6760e01b81526020600482015260156024820152741c1c9bdc1bdcd85b151d1b081d1bdbc81cda1bdc9d605a1b6044820152606490fd5b634e487b7160e01b5f52601160045260245ffd5b91506101606102cd565b9150826102b8565b9150826102a3565b6040516353e95a6760e01b815260206004820152600e60248201526d656e76656c6f706557696e646f7760901b6044820152606490fd5b50610e1063ffffffff6102008401511610610282565b6040516353e95a6760e01b815260206004820152600c60248201526b656e76656c6f70652062707360a01b6044820152606490fd5b6040516353e95a6760e01b815260206004820152600d60248201526c68617264426c6f636b2062707360981b6044820152606490fd5b6040516353e95a6760e01b81526020600482015260146024820152731a185c99109b1bd8dac80f0818dc9a5d1a58d85b60621b6044820152606490fd5b5161ffff16119050848061024e565b6040516353e95a6760e01b815260206004820152600c60248201526b6578706f737572652062707360a01b6044820152606490fd5b5061271061ffff6101808501511611610234565b6040516353e95a6760e01b815260206004820152600e60248201526d32bc3837b9bab9329037b93232b960911b6044820152606490fd5b905061ffff61018085015116118461021e565b6040516353e95a6760e01b815260206004820152601960248201527f64656c6179437269746963616c203c2064656c617948696768000000000000006044820152606490fd5b6040516353e95a6760e01b815260206004820152601160248201527019dd585c991a585b951a1c995cda1bdb19607a1b6044820152606490fd5b5060ff608082015116156101e9565b6040516353e95a6760e01b815260206004820152601360248201527219dbdd995c9b985b98d9551a1c995cda1bdb19606a1b6044820152606490fd5b6040516353e95a6760e01b8152602060048201526011602482015270185c1c1c9bdd985b1cd0dc9a5d1a58d85b607a1b6044820152606490fd5b6040516353e95a6760e01b815260206004820152600d60248201526c0c2e0e0e4deecc2d8e690d2ced609b1b6044820152606490fd5b6040516353e95a6760e01b815260206004820152600c60248201526b617070726f76616c734c6f7760a01b6044820152606490fd5b6104403660031901126100b5576106763661084f565b610220366102231901126100b55761068c6107fa565b6102243560ff811681036100b55781526102443560ff811681036100b55760208201526102643560ff811681036100b55760408201526102843560ff811681036100b55760608201526102a43560ff811681036100b55760808201526102c43580151581036100b55760a08201526102e43563ffffffff811681036100b55760c08201526103043563ffffffff811681036100b55760e08201526103243563ffffffff811681036100b5576101008201526103443563ffffffff811681036100b5576101208201526103643563ffffffff811681036100b5576101408201526103843563ffffffff811681036100b5576101608201526103a43561ffff811681036100b5576101808201526103c43561ffff811681036100b5576101a08201526103e43561ffff811681036100b5576101c08201526104043561ffff811681036100b5576101e0820152610424359063ffffffff821682036100b5576020926100ab92610200830152610a05565b60405190610220820182811067ffffffffffffffff82111761081b57604052565b634e487b7160e01b5f52604160045260245ffd5b6040519060a0820182811067ffffffffffffffff82111761081b57604052565b6102209060031901126100b5576108646107fa565b9060043560ff811681036100b557825260243560ff811681036100b557602083015260443560ff811681036100b557604083015260643560ff811681036100b557606083015260843560ff811681036100b557608083015260a43580151581036100b55760a083015260c43563ffffffff811681036100b55760c083015260e43563ffffffff811681036100b55760e08301526101043563ffffffff811681036100b5576101008301526101243563ffffffff811681036100b5576101208301526101443563ffffffff811681036100b5576101408301526101643563ffffffff811681036100b5576101608301526101843561ffff811681036100b5576101808301526101a43561ffff811681036100b5576101a08301526101c43561ffff811681036100b5576101c08301526101e43561ffff811681036100b5576101e08301526102043563ffffffff811681036100b557610200830152565b60a09060031901126100b5576109d461082f565b9060043580151581036100b55782526024356020830152604435604083015260643560608301526084356080830152565b60ff82511660ff82511611610be45760ff60208301511660ff60208301511611610be45760ff60408301511660ff60408301511611610be45760ff60608301511660ff60608301511611610be45760ff60808301511660ff60808301511611610be45760a0810151151580610beb575b610be45763ffffffff60c08301511663ffffffff60c08301511611610be45763ffffffff60e08301511663ffffffff60e08301511611610be45763ffffffff6101008301511663ffffffff6101008301511611610be45763ffffffff6101208301511663ffffffff6101208301511611610be45763ffffffff6101408301511663ffffffff6101408301511611610be45763ffffffff6101608301511663ffffffff6101608301511610610be457610b4061ffff6101808301511661ffff6101808501511690610c5e565b610be457610b6161ffff6101a08301511661ffff6101a08501511690610c5e565b610be457610b8261ffff6101c08301511661ffff6101c08501511690610c5e565b610be45761ffff6101e082015116610ba361ffff6101e08501511682610c5e565b610bdc5715159182610bbf575b5050610bba575f90565b600190565b63ffffffff80929350610200809201511692015116115f80610bb0565b505050600190565b5050600190565b5060a082015115610a75565b9081511580610c54575b610be4576020810151602083015110610be4576040810151604083015110610be457610c366060830151606083015190610c5e565b610be457608080610c4c93015191015190610c5e565b610bba575f90565b5080511515610c01565b908115610c6f578015610be4571190565b50505f9056", - "nonce": "0x6", - "chainId": "0x2328" - }, - "additionalContracts": [], - "isFixedGasLimit": false - }, - { - "hash": "0x01e4efadbdc2b96b4b204e4e6c28570e198b56ea842cced139cb9df885843c90", - "transactionType": "CREATE", - "contractName": "SkurTestUSD", - "contractAddress": "0xf4abedf17c3c8669d13c352a6002eae5b39d772b", - "function": null, - "arguments": null, - "transaction": { - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "to": null, - "gas": "0x98df4", - "value": "0x0", - "input": "0x60806040523461031757604080519081016001600160401b0381118282101761022a576040908152600d82526c14dadd5c8815195cdd081554d1609a1b602083015280519081016001600160401b0381118282101761022a5760405260048152631cd554d160e21b602082015281516001600160401b03811161022a57600354600181811c9116801561030d575b602082101461020c57601f81116102aa575b50602092601f821160011461024957928192935f9261023e575b50508160011b915f199060031b1c1916176003555b80516001600160401b03811161022a57600454600181811c91168015610220575b602082101461020c57601f81116101a9575b50602091601f8211600114610149579181925f9261013e575b50508160011b915f199060031b1c1916176004555b6040516106b5908161031c8239f35b015190505f8061011a565b601f1982169260045f52805f20915f5b85811061019157508360019510610179575b505050811b0160045561012f565b01515f1960f88460031b161c191690555f808061016b565b91926020600181928685015181550194019201610159565b60045f527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b601f830160051c81019160208410610202575b601f0160051c01905b8181106101f75750610101565b5f81556001016101ea565b90915081906101e1565b634e487b7160e01b5f52602260045260245ffd5b90607f16906100ef565b634e487b7160e01b5f52604160045260245ffd5b015190505f806100b9565b601f1982169360035f52805f20915f5b868110610292575083600195961061027a575b505050811b016003556100ce565b01515f1960f88460031b161c191690555f808061026c565b91926020600181928685015181550194019201610259565b60035f527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b601f830160051c81019160208410610303575b601f0160051c01905b8181106102f8575061009f565b5f81556001016102eb565b90915081906102e2565b90607f169061008d565b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c90816306fdde03146104e657508063095ea7b31461046457806318160ddd1461044757806323b872dd14610367578063313ce5671461034c57806340c10f191461028c57806370a082311461025557806395d89b411461013a578063a9059cbb14610109578063dd62ed3e146100b95763f0292a0314610095575f80fd5b346100b5575f3660031901126100b55760206040516509184e72a0008152f35b5f80fd5b346100b55760403660031901126100b5576100d26105df565b6100da6105f5565b6001600160a01b039182165f908152600160209081526040808320949093168252928352819020549051908152f35b346100b55760403660031901126100b55761012f6101256105df565b602435903361060b565b602060405160018152f35b346100b5575f3660031901126100b5576040515f6004548060011c9060018116801561024b575b6020831081146102375782855290811561021b57506001146101c6575b50819003601f01601f191681019067ffffffffffffffff8211818310176101b2576101ae829182604052826105b5565b0390f35b634e487b7160e01b5f52604160045260245ffd5b905060045f527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b5f905b8282106102055750602091508201018261017e565b60018160209254838588010152019101906101f0565b90506020925060ff191682840152151560051b8201018261017e565b634e487b7160e01b5f52602260045260245ffd5b91607f1691610161565b346100b55760203660031901126100b5576001600160a01b036102766105df565b165f525f602052602060405f2054604051908152f35b346100b55760403660031901126100b5576102a56105df565b602435906509184e72a000821161033d576001600160a01b031690811561032a57600254908082018092116103165760207fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef915f9360025584845283825260408420818154019055604051908152a3005b634e487b7160e01b5f52601160045260245ffd5b63ec442f0560e01b5f525f60045260245ffd5b638b01e07b60e01b5f5260045ffd5b346100b5575f3660031901126100b557602060405160068152f35b346100b55760603660031901126100b5576103806105df565b6103886105f5565b6001600160a01b0382165f8181526001602081815260408084203385529091529091205491936044359392909181016103c7575b5061012f935061060b565b83811061042c5784156104195733156104065761012f945f52600160205260405f2060018060a01b0333165f526020528360405f2091039055846103bc565b634a1406b160e11b5f525f60045260245ffd5b63e602df0560e01b5f525f60045260245ffd5b8390637dc7a0d960e11b5f523360045260245260445260645ffd5b346100b5575f3660031901126100b5576020600254604051908152f35b346100b55760403660031901126100b55761047d6105df565b602435903315610419576001600160a01b031690811561040657335f52600160205260405f20825f526020528060405f20556040519081527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560203392a3602060405160018152f35b346100b5575f3660031901126100b5575f6003548060011c906001811680156105ab575b6020831081146102375782855290811561021b57506001146105565750819003601f01601f191681019067ffffffffffffffff8211818310176101b2576101ae829182604052826105b5565b905060035f527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b5f905b8282106105955750602091508201018261017e565b6001816020925483858801015201910190610580565b91607f169161050a565b602060409281835280519182918282860152018484015e5f828201840152601f01601f1916010190565b600435906001600160a01b03821682036100b557565b602435906001600160a01b03821682036100b557565b6001600160a01b03169081156106a2576001600160a01b031691821561032a57815f525f60205260405f205481811061068957817fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef92602092855f525f84520360405f2055845f525f825260405f20818154019055604051908152a3565b8263391434e360e21b5f5260045260245260445260645ffd5b634b637e8f60e11b5f525f60045260245ffd", - "nonce": "0x7", - "chainId": "0x2328" - }, - "additionalContracts": [], - "isFixedGasLimit": false - }, - { - "hash": "0x776559273350f1c607cfb0584a7cd6b76db3b3a4290e4b12de7a8582dcd56424", - "transactionType": "CREATE", - "contractName": "SkurVault", - "contractAddress": "0xfd5202675c194fbfa682622f11b80167336abba5", - "function": null, - "arguments": null, - "transaction": { - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "to": null, - "gas": "0x61c679", - "value": "0x0", - "input": "0x6080806040523460255760015f55600160ff1981541617600155615749908161002a8239f35b5f80fdfe6080806040526004361015610062575b50361561001a575f80fd5b5f8052601160205260405f20610031348254612c7c565b90555f6040513481527f5548c837ab068cf56a2c2479df0882a4922fd203edb7517321831d95078c5f6260203392a3005b5f905f3560e01c9081630db026221461278357508063128e9be6146127605780632014055d146126b55780632358d5a81461266c57806328dca5ba14612642578063295a5212146126165780632d8a98d9146121f55780632de94807146121ba5780633a5d0d6f1461210a57806340e58ee514612072578063460fc01914611ea05780635305548114611e6757806354387ad714611e4357806358355ead14611e1c57806362812a3914611d6257806367e4ac2c14611ce457806368d525c714611b7957806378f3c95a14611a5b5780638ec1f4ca14611a3757806397feb926146119745780639d8b9a49146118ec5780639eab5253146117bf578063a15a2f8b14611786578063a2380d351461175d578063a8badaa5146116a8578063b314c57014611580578063b52f393e14611555578063b759f95414611535578063bacd2e9714611517578063bf81bf431461142b578063c0555a0a1461139b578063c65df7941461135c578063c75e0df21461120c578063c7f758a814610f26578063ce1e462614610e85578063d0ee62c714610939578063da35c6641461091b578063db02466414610574578063ea70b4af14610556578063ee5248941461026f5763fe0d94c10361000f573461026c57602036600319011261026c57600281541461025d576002815561025660043561385a565b6001815580f35b633ee5aeb560e01b8152600490fd5b80fd5b503461026c5761022036600319011261026c5761028a61486f565b73689a4b4bb095cc9f2f9535eaf6d71e6dd0b55837803b1561052a5760405163065397d760e21b815282906102c1600482016136b2565b818161022481865af4801561053657610541575b50506020610444916040519283809263099fe59960e01b825260025460ff8116600484015260ff8160081c16602484015260ff8160101c16604484015260ff8160181c16606484015260ff81871c16608484015260ff8160281c16151560a484015263ffffffff8160301c1660c484015263ffffffff8160501c1660e484015263ffffffff8160701c1661010484015263ffffffff8160901c1661012484015263ffffffff8160b01c1661014484015263ffffffff8160d01c1661016484015260f01c61018483015263ffffffff60035461ffff81166101a485015261ffff8160101c166101c485015261ffff81881c166101e485015260301c166102048301526103e361022483016136b2565b5af49081156105365782916104f8575b5060405190610404602083016136b2565b6102208252610415610240836129d7565b61041e8161495e565b9183600260ff60015460081c16610434816127cc565b1490816104f0575b816104e4575b506104d5579261045b6104cd9260209583806001614cd3565b8082526012855260408220600254815463ff00000090911664ffff0000001990911617602084901b64ff000000001617815590936104b7906104ae9063ffffffff905b166001600160401b034216612c2e565b6004830161368a565b604051916104c586846129d7565b825283614f18565b604051908152f35b6372b89e9760e11b8452600484fd5b9450600190505f610442565b83915061043c565b90506020813d60201161052e575b81610513602093836129d7565b8101031261052a5761052490612ba2565b5f6103f3565b5080fd5b3d9150610506565b6040513d84823e3d90fd5b8161054b916129d7565b61052a57815f6102d5565b503461026c578060031936011261026c576020600554604051908152f35b503461026c57604036600319011261026c5761058e6127a0565b906105976127b6565b61059f6148c5565b6001600160a01b031691821561090c576001600160a01b03811682526008602052604082205460ff16156108fd57828252600860205260ff6040832054166108ee5760ff60025460201c16156108b457604051926020840152602083526106076040846129d7565b61061260045461381e565b908160045561062260055461381e565b600590815582845260126020526040842080546bffffffffffffffffffff0000163360601b6001600160601b031916176101061781556002810180546001600160a01b0319166001600160a01b0390941693909317909255600382018490558451939082016001600160401b0385116108a05761069f81546135b4565b601f811161085b575b506020958690601f87116001146107f6579580916104cd969785926107eb575b50508160011b915f199060031b1c19161790555b60ff60281b19825416825560015463ffffffff60401b83549160301b169063ffffffff60401b191617825560046001600160401b03421692016001600160401b0383166001600160401b031982541617815561074363ffffffff60025460d01c1684612c2e565b815467ffffffffffffffff60801b191660809190911b67ffffffffffffffff60801b161790558381526012855260408120805463ff000000198116825560025464ffff0000001990911690871c60201b64ff0000000016178155916107c1906107b89063ffffffff60025460b01c1690612c2e565b6004840161368a565b50604051906107d16040836129d7565b60088252677265636f7665727960c01b8583015283614f18565b015190505f806106c8565b8284528184209190601f198816855b81811061084457509160019391896104cd999a941061082c575b505050811b0190556106dc565b01515f1960f88460031b161c191690555f808061081f565b92938a600181928786015181550195019301610805565b81835260208320601f870160051c81019160208810610896575b601f0160051c01905b81811061088b57506106a8565b83815560010161087e565b9091508190610875565b634e487b7160e01b82526041600452602482fd5b604051637417de8f60e01b815260206004820152601160248201527019dd585c991a585b951a1c995cda1bdb19607a1b6044820152606490fd5b630c8ce74760e11b8252600482fd5b63148fe22160e11b8252600482fd5b63d92e233d60e01b8252600482fd5b503461026c578060031936011261026c576020600454604051908152f35b503461026c57608036600319011261026c576109536127a0565b9061095c6127b6565b60443590606435906001600160401b038211610e815736602383011215610e81578160040135936001600160401b03851161026c57366024868501011161026c576109a561489a565b600260ff60015460081c166109b9816127cc565b14610e72576001600160a01b03821694851561090c578415610e63576109e0858489612d80565b9260018060a01b0388168352600d602052610a4560408420600460405191610a0783612970565b60ff81541615158352600181015460208401526002810154604084015260038101546060840152015460808201528761ffff60408801511691614c58565b868352600e602052604083209060ff8254166006811015610e4f5715610e40575b506001600160401b03421697610a8663ffffffff60a0870151168a612c2e565b915460ff81166006811015610e2c5760011480610e0e575b610dfa575b5060209860405198610ab58b8b6129d7565b858a52610ac360045461381e565b9889600455610ad360055461381e565b60059081558a885260128d526040882080546bffffffffffffffffffff0000163360601b6001600160601b031916176101001781556002810180546001600160a01b03191690941790935560038301919091558a519a908201906001600160401b038c11610de657908b8d979695949392610b4e83546135b4565b601f8111610d78575b50601f899211600114610d065792610bec928e6104cd9e9f600494610ccf9a9998978e92610cfb575b50508160011b915f199060031b1c19161790555b60ff60281b19815416815560015463ffffffff60401b82549160301b169063ffffffff60401b191617815501916001600160401b0381166001600160401b031984541617835563ffffffff60025460d01c1690612c2e565b8154906001600160401b0360801b9060801b16906001600160401b0360801b191617905589865260128552610ca560ff608060408920998051610c2e816127cc565b610c37816127cc565b8b5462ff000067ffff0000000000008c85015160301b169260101b169067ffff000000ff0000191617178b55610c88836060830151168c9063ff00000082549160181b169063ff0000001916179055565b0151895464ff000000001916911660201b64ff0000000016178855565b6001870180546001600160a01b0319166001600160a01b039092169190911790556004860161368a565b806024610cdb82612c61565b96610ce960405198896129d7565b82885201838701378401015283614f18565b015190505f80610b80565b828a52888a209190601f198f168b5b818110610d6157506104cd9e9f600494610ccf9a99989794610bec97948360019510610d49575b505050811b019055610b94565b01515f1960f88460031b161c191690555f8080610d3c565b92938b600181928786015181550195019301610d15565b838b9a929495969798999a52818b2091601f840160051c83019310610ddc575b918f99989796959493918f93601f0160051c01905b818110610dba5750610b57565b90925060019193949596979899508a815501918d918f99989796959493610dad565b9091508190610d98565b634e487b7160e01b88526041600452602488fd5b60481c6001600160401b031691505f610aa3565b506001600160401b0383166001600160401b038260481c1611610a9e565b634e487b7160e01b86526021600452602486fd5b610e4990614b3b565b5f610a66565b634e487b7160e01b85526021600452602485fd5b6365e52d5160e11b8252600482fd5b6372b89e9760e11b8152600490fd5b8380fd5b503461026c578060031936011261026c57610200604051610ea58161299f565b8281528260208201528260408201528260608201528260808201528260a08201528260c08201528260e08201528261010082015282610120820152826101408201528261016082015282610180820152826101a0820152826101c0820152826101e08201520152610220610f17612c89565b610f246040518092612887565bf35b503461026c57602036600319011261026c5760606101e0604051610f49816129bb565b83815283602082015283604082015283838201528360808201528360a08201528360c08201528360e08201528361010082015283610120820152836101408201528361016082015283610180820152836101a0820152836101c08201520152600435815260126020526040812060405190610fc3826129bb565b80549060ff82166007811015610e4f57835260ff8260081c166005811015610e4f576110b691600591602086015260ff8460101c16611001816127cc565b604086015260ff8460181c16606086015260ff8460201c16608086015260ff8460281c16151560a086015261ffff8460301c1660c086015263ffffffff8460401c1660e086015261010085019360601c845260018060a01b0360018201541661012086015260018060a01b0360028201541661014086015260038101546101608601526001600160401b036004820154818116610180880152818160401c166101a088015260801c166101c0860152016135ec565b6101e08301526040519260208452825160078110156111f857602085015260208301519060058210156111e4575083926111e0926101e09260408601526040820151611101816127cc565b606086015260ff606083015116608086015260ff60808301511660a086015260a0820151151560c086015261ffff60c08301511660e086015263ffffffff60e08301511661010086015260018060a01b0390511661012085015260018060a01b036101208201511661014085015260018060a01b03610140820151166101608501526101608101516101808501526001600160401b03610180820151166101a08501526001600160401b036101a0820151166101c08501526001600160401b036101c08201511682850152015161020080840152610220830190612863565b0390f35b634e487b7160e01b81526021600452602490fd5b634e487b7160e01b82526021600452602482fd5b503461026c57602036600319011261026c57600435600381101561052a5761123261486f565b61123b816127cc565b60ff60015460081c1661124d816127cc565b60ff8216908082101561134d57600290611266816127cc565b036113435761128063ffffffff60025460901c16926127cc565b60ff60025460201c1690604051906112996020836129d7565b84825284600260ff60015460081c166112b1816127cc565b14908161133a575b81611330575b5061132157916104b76104ae63ffffffff6112e5600189966104cd9860209b6005614cd3565b8086526012895260408620600254815463ff00000090911664ffff000000199091161760209690961b64ff00000000169590951785559661049e565b6372b89e9760e11b8552600485fd5b955050845f6112bf565b600191506112b9565b61128083926127cc565b63710a5e0f60e01b8452600484fd5b503461026c57602036600319011261026c5761138760406111e0926004358152601360205220612bbb565b604051918291602083526020830190612827565b503461026c57604036600319011261026c576004356113b86148c5565b6113c1816148dc565b6113ca82613276565b1561141c5761040061ff00198254161790556113e7600554612b51565b6005556040519060243582527fcf4be29f7d91e92478a19a3eab52f6161f4e4580fcd2f5f8715f191dc2647bbb60203393a380f35b63ef28671760e01b8352600483fd5b503461026c57602036600319011261026c5760043580825260126020526040822060ff815460081c166005811015611503576001036114f4576001600160401b03600482015460801c164211156114c05761030061ff0019825416179055611494600554612b51565b60055533907f74c34a008ce735d9fcf0bd03a9b238d212ad4c441c020661f4ffbb6442645b858380a380f35b60405163935f84bf60e01b815260206004820152600b60248201526a1b9bdd08195e1c1a5c995960aa1b6044820152606490fd5b633ee3282d60e11b8352600483fd5b634e487b7160e01b84526021600452602484fd5b503461026c578060031936011261026c576020600654604051908152f35b503461026c57602036600319011261026c57611552600435613396565b80f35b503461026c57602036600319011261026c5761138760406111e0926004358152601460205220612bbb565b503461026c57604036600319011261026c5761159a6127a0565b6024359060ff821691828103610e81576115b261486f565b6001600160a01b0382168015611699576115cb82614bfa565b8452600860205260ff60408520541690838214611667576115eb91614c26565b906115f58261495e565b92604051916116056020846129d7565b85835285600260ff60015460081c1661161d816127cc565b14908161165f575b81611653575b5061164457926020959261045b926104cd956003614cd3565b6372b89e9760e11b8652600486fd5b9650600190505f61162b565b859150611625565b60405163935f84bf60e01b81526020600482015260096024820152686e6f206368616e676560b81b6044820152606490fd5b63d92e233d60e01b8552600485fd5b503461026c57602036600319011261026c576116c26127a0565b6116ca61489a565b6001600160a01b038116801561174e578252600e60205260ff604083205416600681101561173a576116ff5761155290614b3b565b60405163935f84bf60e01b8152602060048201526012602482015271185b1c9958591e481c9959da5cdd195c995960721b6044820152606490fd5b634e487b7160e01b83526021600452602483fd5b63d92e233d60e01b8352600483fd5b503461026c57602036600319011261026c57602061177c600435613276565b6040519015158152f35b503461026c57602036600319011261026c576020906040906001600160a01b036117ae6127a0565b168152601083522054604051908152f35b503461026c578060031936011261026c57604051906007548083528260208101600784526020842092845b8181106118ca5750506117ff925003836129d7565b815161182361180d8261324b565b9161181b60405193846129d7565b80835261324b565b602082019290601f1901368437805b8451811015611872576001906001600160a01b036118508288613262565b51168352600860205260ff60408420541661186b8286613262565b5201611832565b5061188f9392916020604051958695604087526040870190612827565b918583038287015251918281520192915b8181106118ae575050500390f35b825160ff168452859450602093840193909201916001016118a0565b84546001600160a01b03168352600194850194879450602090930192016117ea565b503461026c57606036600319011261026c5760c061191c61190b6127a0565b6119136127b6565b60443591612d80565b63ffffffff60a0604051928051611932816127cc565b845261ffff602082015116602085015261ffff604082015116604085015260ff606082015116606085015260ff608082015116608085015201511660a0820152f35b503461026c57604036600319011261026c5761198e6127a0565b6001600160a01b0316602435811561174e578015611a28576119e06040516323b872dd60e01b6020820152336024820152306044820152826064820152606481526119da6084826129d7565b8361563e565b8183526011602052604083206119f7828254612c7c565b90556040519081527f5548c837ab068cf56a2c2479df0882a4922fd203edb7517321831d95078c5f6260203392a380f35b6365e52d5160e11b8352600483fd5b503461026c578060031936011261026c57602060ff600a5460101c16604051908152f35b503461026c57604036600319011261026c57611a756127a0565b6024356006811015611b7557611a8961486f565b6001600160a01b0382168015611b66578115611b28578352600e602052611ab78160ff604086205416614a45565b90611ac18261495e565b9260405191611ad16020846129d7565b85835285600260ff60015460081c16611ae9816127cc565b149081611b20575b81611b14575b5061164457926020959261045b9260ff6104cd9616906004614cd3565b9650600190505f611af7565b859150611af1565b60405163935f84bf60e01b8152602060048201526015602482015274757365204e455720746f2072652d70726f6261746560581b6044820152606490fd5b63d92e233d60e01b8452600484fd5b8280fd5b503461026c57602036600319011261026c5760a0906001600160401b03611b9e6127a0565b8283908492600180881b03811695868152600f60205260408120968152600d602052611c0960408220600460405191611bd683612970565b60ff8154161515835260018101546020840152600281015460408401526003810154606084015201546080820152614976565b929050875487611c1a818316612c0e565b164210611cd95782985b6003549061ffff8260201c169283611c5a575b505050505050604051958652602086015260408501526060840152166080820152f35b9598509396509194509260401c8716919060301c63ffffffff1687611c7f8285612c2e565b164210611cae57505050611c9f61271091611c9a85946149d3565b612c4e565b0491925b5f8080808080611c37565b9194509250612710611ccc611cd39360026003870154960154612c4e565b0493612c2e565b92611ca3565b600189015498611c24565b503461026c578060031936011261026c57604051600b8054808352908352909160208301917f0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01db9915b818110611d43576111e085611387818703826129d7565b82546001600160a01b0316845260209093019260019283019201611d2c565b503461026c57602036600319011261026c57604060a091611d816127a0565b611d896129f8565b50600180851b03168152600e6020522060405190611da682612970565b6001600160401b038154611dbd60ff821685612baf565b816020850193818360081c1685528160016040880192828660481c1684528260608a019660881c16865201549560808801968752611dff60405180995161281a565b511660208701525116604085015251166060830152516080820152f35b503461026c578060031936011261026c57602063ffffffff60015460101c16604051908152f35b503461026c578060031936011261026c57602060ff600a5460181c16604051908152f35b503461026c57602036600319011261026c576020906040906001600160a01b03611e8f6127a0565b168152601183522054604051908152f35b503461206e5760c036600319011261206e57611eba6127a0565b60a036602319011261206e57611ece61486f565b73689a4b4bb095cc9f2f9535eaf6d71e6dd0b55837803b1561206e57604051631c71447d60e01b8152611f0360048201612b71565b5f8160a481855af480156120635761204e575b5060018060a01b0382168352600d60205260206004610144604086206040519485938492637309439160e01b845260ff81541615158285015260018101546024850152600281015460448501526003810154606485015201546084830152611f8060a48301612b71565b5af4908115612043578391612009575b50604051611fa060208201612b71565b60a08152611faf60c0826129d7565b611fb88261495e565b9284600260ff60015460081c16611fce816127cc565b149081612001575b81611ff5575b50611321576104cd92859260209661045b936002614cd3565b9550600190505f611fdc565b849150611fd6565b90506020813d60201161203b575b81612024602093836129d7565b81010312611b755761203590612ba2565b5f611f90565b3d9150612017565b6040513d85823e3d90fd5b61205b9193505f906129d7565b5f915f611f16565b6040513d5f823e3d90fd5b5f80fd5b3461206e57602036600319011261206e5760043561208f816148dc565b805460601c331415806120f3575b6120e45761030061ff00198254161790556120b9600554612b51565b60055533907f74c34a008ce735d9fcf0bd03a9b238d212ad4c441c020661f4ffbb6442645b855f80a3005b6367909b1560e01b5f5260045ffd5b50335f526008602052600160405f2054161561209d565b3461206e57604036600319011261206e57600435600381101561206e5761212f614844565b612138816127cc565b60ff60015460081c1661214a816127cc565b60ff821611156121ab576001549060ff8260081c1691612169826127cc565b61ff001916600882901b61ff001617600155612184826127cc565b61218d816127cc565b6040519160243583525f5160206157295f395f51905f5260203394a4005b63216a142160e11b5f5260045ffd5b3461206e57602036600319011261206e576001600160a01b036121db6127a0565b165f526008602052602060ff60405f205416604051908152f35b3461206e576102a036600319011261206e576004356001600160401b03811161206e576122269036906004016127ea565b906024356001600160401b03811161206e576122469036906004016127ea565b90919061022036604319011261206e57610264356001600160401b03811161206e576122769036906004016127ea565b909261028435926001600160401b03841161206e573660238501121561206e578360040135956001600160401b03871161206e5736602460a089028701011161206e5760015460ff81166126085760019060ff1916176001558188148015906125fe575b6125cf575f5b88811061253e57505050505f5b8181106124a457856040516123018161299f565b60443560ff8116810361206e57815260643560ff8116810361206e57602082015260843560ff8116810361206e57604082015260a43560ff8116810361206e57606082015260c43560ff8116810361206e57608082015260e435801515810361206e5760a08201526101043563ffffffff8116810361206e5760c08201526101243563ffffffff8116810361206e5760e08201526101443563ffffffff8116810361206e576101008201526101643563ffffffff8116810361206e576101208201526101843563ffffffff8116810361206e576101408201526101a43563ffffffff8116810361206e576101608201526101c43561ffff8116810361206e576101808201526101e43561ffff8116810361206e576101a08201526102043561ffff8116810361206e576101c08201526102243561ffff8116810361206e576101e0820152610244359163ffffffff8316830361206e5761248a827f4488062cfafbbb2c1d440f13796e120c4522761f74ecade440ee8f31f1bfbee09461020060409501526145c5565b63ffffffff60015460101c169082519182526020820152a1005b6124b76124b2828487612b1f565b612b2f565b908581101561252a5760a08102840160a0602319823603011261206e576040516124e081612970565b602482013593841515850361206e5760a4612524936001968452604481013560208501526064810135604085015260848101356060850152013560808301526143f9565b016122ed565b634e487b7160e01b5f52603260045260245ffd5b6001600160a01b036125546124b2838c88612b1f565b165f52600860205260ff60405f2054166125c05760ff61257d612578838686612b1f565b612b43565b16156125b157806125ab6125976124b26001948d89612b1f565b6125a5612578848888612b1f565b9061426b565b016122e0565b633a767e1d60e21b5f5260045ffd5b630c8ce74760e11b5f5260045ffd5b60405163935f84bf60e01b81526020600482015260066024820152650d8cadccee8d60d31b6044820152606490fd5b50868414156122da565b62dc149f60e41b5f5260045ffd5b3461206e575f36600319011261206e57602060ff60015460081c166040519061263e816127cc565b8152f35b3461206e57602036600319011261206e576040612660600435612a22565b82519182526020820152f35b3461206e57604036600319011261206e576126856127b6565b6004355f52601560205260405f209060018060a01b03165f52602052602060ff60405f2054166040519015158152f35b3461206e57602036600319011261206e576126ce6127a0565b6126d66129f8565b5060018060a01b03165f52600d60205260a060405f206004604051916126fb83612970565b60ff8154161515835260018101546020840152600281015460408401526003810154606084015201546080820152610f246040518092608080918051151584526020810151602085015260408101516040850152606081015160608501520151910152565b3461206e575f36600319011261206e57602060ff600a5460081c16604051908152f35b3461206e575f36600319011261206e5760209060ff600a54168152f35b600435906001600160a01b038216820361206e57565b602435906001600160a01b038216820361206e57565b600311156127d657565b634e487b7160e01b5f52602160045260245ffd5b9181601f8401121561206e578235916001600160401b03831161206e576020808501948460051b01011161206e57565b9060068210156127d65752565b90602080835192838152019201905f5b8181106128445750505090565b82516001600160a01b0316845260209384019390920191600101612837565b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b63ffffffff610200809260ff815116855260ff602082015116602086015260ff604082015116604086015260ff606082015116606086015260ff608082015116608086015260a0810151151560a08601528260c08201511660c08601528260e08201511660e0860152826101008201511661010086015282610120820151166101208601528261014082015116610140860152826101608201511661016086015261ffff6101808201511661018086015261ffff6101a0820151166101a086015261ffff6101c0820151166101c086015261ffff6101e0820151166101e0860152015116910152565b60a081019081106001600160401b0382111761298b57604052565b634e487b7160e01b5f52604160045260245ffd5b61022081019081106001600160401b0382111761298b57604052565b61020081019081106001600160401b0382111761298b57604052565b90601f801991011681019081106001600160401b0382111761298b57604052565b60405190612a0582612970565b5f6080838281528260208201528260408201528260608201520152565b90815f52601260205260405f20825f52601360205260ff60405f2091541660078110156127d657612b17576002905b5f918154915f5b838110612acc5750505050915f52601460205260405f205f908054905f5b828110612a835750505090565b612a8d8183613315565b60018060a01b0391549060031b1c165f526008602052600860405f205416612ab8575b600101612a76565b92612ac460019161381e565b939050612ab0565b612ad68183613315565b60018060a01b0391549060031b1c165f52600860205260ff8360405f20541616612b03575b600101612a58565b93612b0f60019161381e565b949050612afb565b600190612a51565b919081101561252a5760051b0190565b356001600160a01b038116810361206e5790565b3560ff8116810361206e5790565b8015612b5d575f190190565b634e487b7160e01b5f52601160045260245ffd5b60243580151580910361206e578152604435602082015260643560408201526084356060820152608060a435910152565b5190811515820361206e57565b60068210156127d65752565b90604051918281549182825260208201905f5260205f20925f5b818110612bec575050612bea925003836129d7565b565b84546001600160a01b0316835260019485019487945060209093019201612bd5565b6001600160401b0362015180911601906001600160401b038211612b5d57565b906001600160401b03809116911601906001600160401b038211612b5d57565b81810292918115918404141715612b5d57565b6001600160401b03811161298b57601f01601f191660200190565b91908201809211612b5d57565b60405190612c968261299f565b8160025460ff8116825260ff8160081c16602083015260ff8160101c16604083015260ff8160181c16606083015260ff8160201c16608083015260ff8160281c16151560a083015263ffffffff8160301c1660c083015263ffffffff8160501c1660e083015263ffffffff8160701c1661010083015263ffffffff8160901c1661012083015263ffffffff8160b01c1661014083015263ffffffff8160d01c1661016083015260f01c61018082015261020063ffffffff60035461ffff81166101a085015261ffff8160101c166101c085015261ffff8160201c166101e085015260301c16910152565b92916040519160c083018381106001600160401b0382111761298b576040525f835260208301925f845260408101915f835260608201945f865260808301935f85525f60a0850152839860018060a01b038116805f52600d60205260405f209460405195612ded87612970565b60ff81541615918215885260018201549260208901938452600460028401549360408b01948552600381015460608c0152015460808a01526132385760018060a01b031693845f52600e60205260405f209760405195612e4c87612970565b60018a549a612e5e60ff8d168a612baf565b6001600160401b038c60081c1660208a01526001600160401b0360408a019c8d828260481c16905260881c1660608a015201546080880152865160068110156127d6576005146132265750612eb290614976565b9490505f52600f602052612ec960405f20916149d3565b92519151906001600160401b03612ee281835416612c0e565b16421061321c57505f9a5b85519560068710156127d6575160068110156127d6576001149889613208575b5060ff60015460081c1660025460f01c9161ffff6003541697604051976101608901928984106001600160401b0385111761298b57612f75936040528952602089019788526040890196875260608901958652608089015260a088019e8f5260c08801612baf565b60e086019915158a52612f87816127cc565b6101008601526101208501526101408401948552835190515f9283918311156131e3575050612fc161ffff916002809417945b5190615696565b94511680151590816131d4575b501561317d5750600860029117955b60c083015160068110156127d65760040361311c575050602060029517975b6080820151151590816130d8575b509360ff9860a0989794613074948b989463ffffffff986130a1575b6101006001910151613037816127cc565b613040816127cc565b14613085575b61ffff90811690915216905261305b816127cc565b808352613067816127cc565b61306f612c89565b614ab3565b939093169190950152169052169052565b9061ffff60406130968295976156f5565b961793505090613046565b92946130ac816127cc565b6001818b16106130ca5761010060806001925b971794915050613026565b5060016101006080826130bf565b6130ef915097969397959194929551855190612c7c565b918260011b9280840460021490151715612b5d5760808501519697939691959094909290911060ff61300a565b60c083015160068110156127d657613143575061313b610110916156f5565b951797612ffc565b60c0839a92979a015160068110156127d6576001149081613172575b5015612ffc57979461313b6010916156f5565b90505115155f61315f565b909561ffff6101208401511680151590816131c5575b5015612fdd5795906131a4816127cc565b600160ff8216106131bb576004905b911795612fdd565b50600460016131b3565b905061ffff851610155f613193565b905061ffff851610155f612fce565b939092935182116131fc575b61ffff91612fc191612fba565b600193508392506131ef565b516001600160401b0316421098505f612f0d565b600101549a612eed565b63325fbd1560e11b5f5260045260245ffd5b8363a6e7da2360e01b5f5260045260245ffd5b6001600160401b03811161298b5760051b60200190565b805182101561252a5760209160051b010190565b5f52601260205260405f205460ff8160081c1660058110156127d6576001036132f85760ff8160281c166132f25760ff811660078110156127d6576132f857600260ff8260101c166132c7816127cc565b146132f25760301c6010166132ed5760ff60015460081c166132e8816127cc565b151590565b600190565b50600190565b505f90565b60075481101561252a5760075f5260205f2001905f90565b805482101561252a575f5260205f2001905f90565b60075490600160401b82101561298b5761334f826001612bea94016007556007613315565b81546001600160a01b0393841660039290921b91821b9390911b1916919091179055565b90815491600160401b83101561298b578261334f916001612bea95018155613315565b61339f816148dc565b335f52600860205260405f205490600882166134bb575460ff1660078110156127d657600681146134a75761349c5760ff600280925b16161561348657505f81815260156020908152604080832033845290915290205460ff1661347757805f52601560205260405f2060018060a01b0333165f5260205260405f20600160ff19825416179055805f52601360205261343b3360405f20613373565b805f52601360205260405f2054906040519182527f2f0f51d2f12c71357e60d575d47e93e081dbaf32072245ab1e01540f0aadbf1a60203393a3565b63080fc0bd60e11b5f5260045ffd5b60ff9063e84c979760e01b5f521660045260245ffd5b60ff600180926133d5565b63e84c979760e01b5f52600860045260245ffd5b60ff91505460201c1615613563575f81815260156020908152604080832033845290915290205460ff1661347757805f52601560205260405f2060018060a01b0333165f5260205260405f20600160ff19825416179055805f5260146020526135273360405f20613373565b805f52601460205260405f2054906040519182527fb8c7de51f1e8e2b6cec2574c8731fba6de6e3e3002b2e454ebccdf32f591ca8660203393a3565b60405163935f84bf60e01b815260206004820152602260248201527f677561726469616e20636f6e6669726d6174696f6e206e6f7420726571756972604482015261195960f21b6064820152608490fd5b90600182811c921680156135e2575b60208310146135ce57565b634e487b7160e01b5f52602260045260245ffd5b91607f16916135c3565b9060405191825f8254926135ff846135b4565b80845293600181169081156136685750600114613624575b50612bea925003836129d7565b90505f9291925260205f20905f915b81831061364c575050906020612bea928201015f613617565b6020919350806001915483858901015201910190918492613633565b905060209250612bea94915060ff191682840152151560051b8201015f613617565b9067ffffffffffffffff60401b82549160401b169067ffffffffffffffff60401b1916179055565b60043560ff811680910361206e57815260243560ff811680910361206e57602082015260443560ff811680910361206e57604082015260643560ff811680910361206e57606082015260843560ff811680910361206e57608082015260a43580151580910361206e5760a082015260c43563ffffffff811680910361206e5760c082015260e43563ffffffff811680910361206e5760e08201526101043563ffffffff811680910361206e576101008201526101243563ffffffff811680910361206e576101208201526101443563ffffffff811680910361206e576101408201526101643563ffffffff811680910361206e576101608201526101843561ffff811680910361206e576101808201526101a43561ffff811680910361206e576101a08201526101c43561ffff811680910361206e576101c08201526101e43561ffff811680910361206e576101e0820152610204359063ffffffff821680920361206e576102000152565b5f198114612b5d5760010190565b519060ff8216820361206e57565b519063ffffffff8216820361206e57565b519061ffff8216820361206e57565b613863816148dc565b805460ff8116906007821015806127d657821591821561422b57335f526008602052600460405f20541615614218575b6001600160401b03600486015460401c1680421061420657506001549082600260ff8460081c166138c3816127cc565b1490816141d6575b506141c75760ff8160181c169160ff8260201c16936127d6578490816141aa575b5061416f575b506138fc86612a22565b919081811061415a57505081811061414557505080614132575b61020061ff001984541617835561392e600554612b51565b60055561393c60065461381e565b60065533847f9c85b616f29fca57a17eafe71cf9ff82ffef41766e2cf01ea7f8f7878dd3ec245f80a315613a56575060018060a01b03600182015416906002810191600360018060a01b0384541692019382828654927feb669e0a01fff40cbe45de851f42334fe70673136abfd91399651b7ae199a07b6020604051868152a481613a1e575050905491545f92839250829182916001600160a01b03165af13d15613a19573d6139eb81612c61565b906139f960405192836129d7565b81525f60203d92013e5b15613a0a57565b633d2cec6f60e21b5f5260045ffd5b613a03565b90919250612bea93506040519263a9059cbb60e01b60208501526024840152604483015260448252613a516064836129d7565b61563e565b60018103613bc05750613a6c91506005016135ec565b6102208180518101031261206e57612bea90613bb561022060405192613a918461299f565b613a9d6020820161382c565b8452613aab6040820161382c565b6020850152613abc6060820161382c565b6040850152613acd6080820161382c565b6060850152613ade60a0820161382c565b6080850152613aef60c08201612ba2565b60a0850152613b0060e0820161383a565b60c0850152613b12610100820161383a565b60e0850152613b24610120820161383a565b610100850152613b37610140820161383a565b610120850152613b4a610160820161383a565b610140850152613b5d610180820161383a565b610160850152613b706101a0820161384b565b610180850152613b836101c0820161384b565b6101a0850152613b966101e0820161384b565b6101c0850152613ba9610200820161384b565b6101e08501520161383a565b6102008201526145c5565b60028103613c3d575060028101546001600160a01b03169150613be5906005016135ec565b60a08180518101031261206e57612bea9160a060405192613c0584612970565b613c1160208201612ba2565b8452604081015160208501526060810151604085015260808101516060850152015160808301526143f9565b60038103613c7057506002810154600390910154613c68925060ff16906001600160a01b031661426b565b612bea61550f565b60048103613d8a5750905060ff600360018060a01b0360028401541692015416600681109081156127d657825f52600e60205260405f209160ff8354169060068210156127d6578115613d65575b60018314613d17575b156127d657613d14827f155590dadece0bf62c36a0e5cabcce51e1280ff0e8f6cc53c14d8900e0f43daf9460409460ff19825416179055613d0a8451809461281a565b602083019061281a565ba2565b613d60613d3863ffffffff60025460701c166001600160401b034216612c2e565b855467ffffffffffffffff60481b191660489190911b67ffffffffffffffff60481b16178555565b613cc7565b835468ffffffffffffffff0019164260081b68ffffffffffffffff0016178455613cbe565b600503613e325760039150015460ff8116613da4816127cc565b613dad816127cc565b60ff60015460081c16613dbf816127cc565b811015613e235760015461ff008060ff8360081c1694613dde856127cc565b60081b16169061ff00191617600155613df6826127cc565b613dff816127cc565b60405191640e4cad8c2f60db1b83525f5160206157295f395f51905f5260203394a4565b63710a5e0f60e01b5f5260045ffd5b60028101546001600160a01b031690613e4d906005016135ec565b60208180518101031261206e57602001516001600160a01b038116929083900361206e57815f52600860205260ff60405f205416801561412357835f52600860205260ff60405f2054166125c057821561411457613eaa5f614bfa565b825f5260086020528260ff60405f205416801580809161410d575b15614007575060405f5160206157095f395f51905f5291613ee58461332a565b600754845f526009602052825f20555b613eff5f826153c3565b613f095f8261540d565b613f135f82615461565b613f1d5f826154b7565b835f526008602052815f2060ff19815416905581519081525f6020820152a2613f46818561426b565b613f4e61550f565b60ff60015460081c16613f60816127cc565b15613f93575b60207f607a6564adeb3eec35d536d94fa031ad71407e7510a784e53f7bf36d44f3bc4191604051908152a4565b600154600881901c60ff169061ff00191661010017600155613fb4816127cc565b7f607a6564adeb3eec35d536d94fa031ad71407e7510a784e53f7bf36d44f3bc4191602091600160405191677265636f7665727960c01b83525f5160206157295f395f51905f52853394a4915050613f66565b1580614105575b614029575b60405f5160206157095f395f51905f5291613ef5565b905f52600960205260405f20545f198101818111612b5d576007545f198101908111612b5d578082036140c7575b5050506007549081156140b35760405f5160206157095f395f51905f529186935f190161409b614086826132fd565b81549060018060a01b039060031b1b19169055565b600755835f5260096020525f82812055915050614013565b634e487b7160e01b5f52603160045260245ffd5b61334f916140d76140f1926132fd565b905460039190911b1c6001600160a01b03169283916132fd565b5f52600960205260405f20555f8080614057565b50600161400e565b505f613ec5565b63d92e233d60e01b5f5260045ffd5b63148fe22160e11b5f5260045ffd5b61413b8361500b565b6139165750505050565b63d495223560e01b5f5260045260245260445ffd5b635ab4617560e01b5f5260045260245260445ffd5b61418260ff809260101c1661306f612c89565b5091168281116141a1575b5060ff16828111156138f25791505f6138f2565b915060ff61418d565b63ffffffff915060101c1663ffffffff8260401c1614155f6138ec565b6372b89e9760e11b5f5260045ffd5b90506127d6578384156141ea575b836138cb565b5060ff8160281c1680156141e457505f925060058514156141e4565b6384280dad60e01b5f5260045260245ffd5b63e84c979760e01b5f526004805260245ffd5b5f9150600684036142435761423e614844565b613893565b335f526008602052600560405f2054166138935763e84c979760e01b5f52600560045260245ffd5b6001600160a01b0381169182156141145761428581614bfa565b825f52600860205260ff60405f2054169182158080916143ed575b1561431f57509160ff6040926142c35f5160206157095f395f51905f529561332a565b600754865f526009602052845f20555b6142dd81846153c3565b6142e7818461540d565b6142f18184615461565b6142fb81846154b7565b855f526008602052835f2082821683198254161790558351928352166020820152a2565b905015806143e2575b614346575b5f5160206157095f395f51905f529160ff6040926142d3565b825f52600960205260405f20545f198101818111612b5d576007545f198101908111612b5d578082036143be575b5050506007549182156140b35760ff6040925f5160206157095f395f51905f52945f19016143a4614086826132fd565b600755855f5260096020525f84812055925050915061432d565b61334f916140d76143ce926132fd565b5f52600960205260405f20555f8080614374565b5060ff811615614328565b5060ff831615156142a0565b9073689a4b4bb095cc9f2f9535eaf6d71e6dd0b55837803b1561206e57604051631c71447d60e01b81525f8160a48183956144616004830189608080918051151584526020810151602085015260408101516040850152606081015160608501520151910152565b5af48015612063576145b2575b506001600160a01b038316808252600c602052604082205490939060ff1615614532575b507f9d182ff993316b1c662980e27e42d42fae0bd158f1bed04e9988ec637fea8da4916040828560a09452600d602052208151151560ff8019835416911617815560208201516001820155604082015160028201556060820151600382015560046080830151910155613d146040518092608080918051151584526020810151602085015260408101516040850152606081015160608501520151910152565b838252600c60205260408220805460ff19166001179055600b54600160401b81101561459e5760a0926145957f9d182ff993316b1c662980e27e42d42fae0bd158f1bed04e9988ec637fea8da4959361334f84600160409601600b55600b613315565b92505091614492565b634e487b7160e01b83526041600452602483fd5b6145be91505f906129d7565b5f5f61446e565b73689a4b4bb095cc9f2f9535eaf6d71e6dd0b55837803b1561206e5760405163065397d760e21b81525f816102248183956146036004830189612887565b5af4801561206357614831575b50815160208084015160408501516060860151608087015160a088015160c089015160e08a01516101008b01516101208c01516101408d01516101608e01516101808f015160089b909b1b61ff001660ff9d909d169c909c176010998a1b62ff0000161760189890981b63ff0000001697909717958a1b64ff00000000169590951793151560281b60ff60281b1693909317603092831b69ffffffff0000000000009081169190911760509290921b63ffffffff60501b169190911760709390931b63ffffffff60701b169290921760909390931b63ffffffff60901b169290921760b09390931b63ffffffff60b01b169290921760d09690961b63ffffffff60d01b169590951760f09390931b6001600160f01b031916929092176002556101a0860151600380546101c08901516101e08a01516102008b015169ffffffffffffffffffff199390931661ffff959095169490941790851b63ffff000016179290951b65ffff0000000016919091179390941b9091169190911790915560018054929183901c63ffffffff9081169091019190821161481d575063ffffffff7f1a734774792c149a91deb03650b2a2bc51de7bbab7262857ace6fb1699b430419265ffffffff00006102409360101b169065ffffffff0000191617806001556147f861550f565b614815604051956001600160401b03421687526020870190612887565b60101c1692a2565b634e487b7160e01b81526011600452602490fd5b61483d91505f906129d7565b5f5f614610565b335f526008602052600960405f2054161561485b57565b63e84c979760e01b5f52600960045260245ffd5b335f526008602052600160405f2054161561488657565b63e84c979760e01b5f52600160045260245ffd5b335f526008602052600360405f205416156148b157565b63e84c979760e01b5f52600360045260245ffd5b335f526008602052600860405f205416156134a757565b80158015614953575b614944575f52601260205260405f209060ff825460081c1660058110156127d657600103614935576001600160401b03600483015460801c16421161492657565b6328a7237960e01b5f5260045ffd5b633ee3282d60e11b5f5260045ffd5b6302eae9e560e51b5f5260045ffd5b5060045481116148e5565b156149725763ffffffff60025460901c1690565b5f90565b906080606083015192015190600160ff815460081c16614995816127cc565b1461499c57565b826149bb575b816149a957565b9060011c806149b85750600190565b90565b9160011c806149ce575060015b916149a2565b6149c8565b6001600160a01b03168015614a40576020602491604051928380926370a0823160e01b82523060048301525afa908115612063575f91614a11575090565b90506020813d602011614a38575b81614a2c602093836129d7565b8101031261206e575190565b3d9150614a1f565b504790565b906006811015806127d657600282148015614aa7575b614a9f576127d6576001149081614a76575b506132ed575f90565b905060068110156127d65760048114908115614a94575b505f614a6d565b60059150145f614a8d565b505050600190565b50505f60038214614a5b565b9190614abe816127cc565b8015614b2e57600190614ad0816127cc565b14614b135760ff6040830151169060a083015115155f14614b055763ffffffff60e060ff608086015116945b01511691929190565b63ffffffff60e05f94614afc565b9063ffffffff60c060ff6020840151169201511690915f9190565b50905160ff16905f908190565b6001600160a01b03165f818152600e60209081526040909120805468ffffffffffffffffff191642600881901b68ffffffffffffffff00169190911760011782557fa834c3aabec8913fbfd0591ec9c2a7494c727a364cc1f231c948c0f9564b5d0692916001600160401b0391614bec90614bc490841663ffffffff60025460701c1690612c2e565b825467ffffffffffffffff60481b191660489190911b67ffffffffffffffff60481b16178255565b5460481c16604051908152a2565b600f60ff8216116125b1576008811615159081614c19575b506125b157565b600791501615155f614c12565b60ff8119831616614c515760081615159081614c4557506132ed575f90565b6008915016155f614a6d565b5050600190565b614c6190614976565b509081151580614cca575b614cb557505061ffff60035460101c169081151580614ca8575b614c8e575050565b61ffff90630a8a293d60e01b5f521660045260245260445ffd5b508161ffff821611614c86565b634fa5781f60e11b5f5260045260245260445ffd5b50818111614c6c565b9290949391614ce360045461381e565b958660045586614cf460055461381e565b6005555f52601260205260405f209460078110156127d65785546bffffffffffffffffffffffff61ffff1990911660ff929092169190911761010017163360601b6001600160601b0319161785556002850180546001600160a01b0319166001600160a01b039092169190911790556003840155805160058401916001600160401b03821161298b578190614d8984546135b4565b601f8111614ec8575b50602090601f8311600114614e65575f92614e5a575b50508160011b915f199060031b1c19161790555b60ff60281b825491151560281b169060ff60281b191617815560015463ffffffff60401b82549160301b169063ffffffff60401b1916178155614e3260046001600160401b0342169201916001600160401b0381166001600160401b031984541617835563ffffffff60025460d01c1690612c2e565b815467ffffffffffffffff60801b191660809190911b67ffffffffffffffff60801b16179055565b015190505f80614da8565b5f8581528281209350601f198516905b818110614eb05750908460019594939210614e98575b505050811b019055614dbc565b01515f1960f88460031b161c191690555f8080614e8b565b92936020600181928786015181550195019301614e75565b909150835f5260205f20601f840160051c81019160208510614f0e575b90601f859493920160051c01905b818110614f005750614d92565b5f8155849350600101614ef3565b9091508190614ee5565b815460ff81169260018060a01b0360018201541660018060a01b0360028301541691600381015493600460ff8260101c169201549460078810156127d6577fcf5d862ed732f5aec4f98da191cd4e962a57abdc7bab078c436b0517229d388f95615006936001600160401b0392604051978897885260208801526040870152614fa0816127cc565b606086015261ffff8360301c16608086015260ff8360181c1660a086015260ff8360201c1660c0860152818160401c1660e086015260801c1661010084015260ff8160281c16151561012084015261016061014084015260601c96610160830190612863565b0390a4565b60018060a01b0360018201541690600381015491805f52600d60205260405f20916040519261503984612970565b600460ff8254161591821586526001810154602087015260028101546040870152600381015460608701520154608085015261538c57600201546001600160a01b03165f818152600e60205260409020805490939160ff82169060068210156127d65760058214613226575060011480615377575b61535857506150bc826149d3565b90818511615341576150e1906150dc6150d58488615696565b8783614c58565b614976565b919050825f52600f60205260405f20916001600160401b0361510581855416612c0e565b1642101561531c575b80151580615306575b6152e3575060035461ffff8160201c166151b5575b505060010161513c848254612c7c565b90555f52601060205260405f20615154838254612c7c565b905580549060016001600160401b038360881c1601906001600160401b038211612b5d5767ffffffffffffffff60881b1990921660889190911b67ffffffffffffffff60881b16178155600101805490916151ae91612c7c565b9055600190565b6151d86001600160401b039163ffffffff83865460401c169160301c1690612c2e565b164210156152bf575b506127106151fd600283015461ffff60035460201c1690612c4e565b0460038201908154816152108883612c7c565b1161522f57505090816152268660019454612c7c565b9055905f61512c565b9091507f72f5745158d621f0076c165a1d23a925d0b2f8c5b5fb643933649247142d156d945060609395925060405192835260208301526040820152a2600154600881901c60ff169061ff0019166102001760015561528d816127cc565b6002604051916e31b4b931bab4ba16b13932b0b5b2b960891b83525f5160206157295f395f51905f5260203394a45f90565b6152d26001600160401b0342168361368a565b60028201555f60038201555f6151e1565b6152f1866001850154612c7c565b6398b5494560e01b5f5260045260245260445ffd5b5080615316876001860154612c7c565b11615117565b825467ffffffffffffffff1916426001600160401b03161783555f600184015561510e565b508363cf47918160e01b5f5260045260245260445ffd5b6001600160401b03906384280dad60e01b5f5260481c1660045260245ffd5b506001600160401b038160481c1642106150ae565b5063a6e7da2360e01b5f5260045260245ffd5b60ff5f199116019060ff8211612b5d57565b60ff60019116019060ff8211612b5d57565b6001918216151591161515811461540a57156153f65760ff6153e881600a54166153b1565b1660ff19600a541617600a55565b60ff61540581600a541661539f565b6153e8565b50565b6002918216151591161515811461540a571561544b5761543460ff600a5460081c166153b1565b61ff00600a549160081b169061ff00191617600a55565b61545c60ff600a5460081c1661539f565b615434565b6004918216151591161515811461540a57156154a15761548860ff600a5460101c166153b1565b62ff0000600a549160101b169062ff0000191617600a55565b6154b260ff600a5460101c1661539f565b615488565b6008918216151591161515811461540a57156154f9576154de60ff600a5460181c166153b1565b63ff000000600a549160181b169063ff000000191617600a55565b61550a60ff600a5460181c1661539f565b6154de565b600a5460ff81166001811061560d57600160ff8360101c16106155d9576002549060ff8260181c161161559d5760ff8160101c1660ff8360081c16106155635760ff809160201c169160181c16106108b457565b604051637417de8f60e01b8152602060048201526011602482015270185c1c1c9bdd985b1cd0dc9a5d1a58d85b607a1b6044820152606490fd5b604051637417de8f60e01b815260206004820152601360248201527219dbdd995c9b985b98d9551a1c995cda1bdb19606a1b6044820152606490fd5b604051637417de8f60e01b815260206004820152600b60248201526a37379032bc32b1baba37b960a91b6044820152606490fd5b604051637417de8f60e01b815260206004820152600860248201526737379037bbb732b960c11b6044820152606490fd5b905f602091828151910182855af115612063575f513d61568d57506001600160a01b0381163b155b61566d5750565b635274afe760e01b5f9081526001600160a01b0391909116600452602490fd5b60011415615666565b9081156156ef5780158080156156e5575b6156dc57612710830292830461271003612b5d576156c85761ffff91041690565b634e487b7160e01b5f52601260045260245ffd5b50505061271090565b50818310156156a7565b50505f90565b6156fe816127cc565b156132ed5760029056fe5d3de0fa15f594659e83f792fff19b5062cbc430ea0e9cf465377cdfaab8845467ac1adbe98dcfa9ea51aab9f87f1ae3ca912bf37e541afd86007c5bde3e5311", - "nonce": "0x8", - "chainId": "0x2328" - }, - "additionalContracts": [], - "isFixedGasLimit": false - }, - { - "hash": "0x1595c05e9aebb398da3d64fe84e7a038f73ab1ddcb57a8e58da2a9abe2de72be", - "transactionType": "CREATE", - "contractName": "SkurFactory", - "contractAddress": "0x02929b28dd9ddf57b1615c70dccdca6b6eda0015", - "function": null, - "arguments": [ - "0xFd5202675c194FBFA682622F11b80167336abba5" - ], - "transaction": { - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "to": null, - "gas": "0xb2b99", - "value": "0x0", - "input": "0x60a03461008e57601f6109c938819003918201601f19168301916001600160401b038311848410176100925780849260209460405283398101031261008e57516001600160a01b038116810361008e57803b1561007f5760805260405161092290816100a782396080518181816101fc0152818161033401526107e90152f35b63c99547ad60e01b5f5260045ffd5b5f80fd5b634e487b7160e01b5f52604160045260245ffdfe6080806040526004361015610012575f80fd5b5f905f3560e01c908163089ec70e14610783575080635492a6ec1461022b5780635c60da1b146101e6578063652b9b41146101a75780636cc811f8146100e05780638c13a4531461008a5763a7c6a1001461006b575f80fd5b3461008757806003193601126100875760209054604051908152f35b80fd5b5034610087576020366003190112610087576004359080548210156100cc5780805260208082209092015460405160039290921b1c6001600160a01b03168152f35b634e487b7160e01b81526032600452602490fd5b5034610087576020366003190112610087576001600160a01b0361010261084c565b168152600260205260408120604051908160208254918281520190819285526020852090855b818110610188575050508261013e910383610893565b604051928392602084019060208552518091526040840192915b818110610166575050500390f35b82516001600160a01b0316845285945060209384019390920191600101610158565b82546001600160a01b0316845260209093019260019283019201610128565b50346100875760203660031901126100875760209060ff906040906001600160a01b036101d261084c565b168152600184522054166040519015158152f35b50346100875780600319360112610087576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b34610468576102c03660031901126104685760043560243567ffffffffffffffff811161046857610260903690600401610862565b919060443567ffffffffffffffff811161046857610282903690600401610862565b9390610220366063190112610468576102843567ffffffffffffffff8111610468576102b2903690600401610862565b6102a435929167ffffffffffffffff841161046857366023850112156104685783600401359267ffffffffffffffff84116104685736602460a0860287010111610468576040805133602082019081529181018a905261031f81606081015b03601f198101835282610893565b5190206e5af43d82803e903d91602b57fd5bf37f0000000000000000000000000000000000000000000000000000000000000000763d602d80600a3d3981f3363d3d373d3d3d363d7300000062ffffff8260881c16175f5260781b1760205260018060a01b0390603760095ff51698891561077457895f52600160205260405f20600160ff198254161790555f54600160401b811015610454578060016103c992015f555f6108b5565b81546001600160a01b0360039290921b91821b1916908c901b1790555f5b8781101561046c57600581901b8901356001600160a01b03811690819003610468575f52600260205260405f20908154600160401b81101561045457610435816001948f93860181556108b5565b819291549060031b91821b91858060a01b03901b1916179055016103e7565b634e487b7160e01b5f52604160045260245ffd5b5f80fd5b50899697895f54905f19820191821161076057604051908152897fc18dea5f6b30c3fd606e8e38832933e2fa57407dd77800aad0658505ee94df9860203393a4873b1561046857604051632d8a98d960e01b81526102a06004820152986020916104db916102a48c01916108de565b8981036003190160248b01528281520191905f905b80821061073b5750505060643560ff811680910361046857604488015260843560ff811680910361046857606488015260a43560ff811680910361046857608488015260c43560ff81168091036104685760a488015260e43560ff81168091036104685760c4880152610104358015158091036104685760e48801526101243563ffffffff8116809103610468576101048801526101443563ffffffff8116809103610468576101248801526101643563ffffffff8116809103610468576101448801526101843563ffffffff8116809103610468576101648801526101a43563ffffffff8116809103610468576101848801526101c43563ffffffff8116809103610468576101a48801526101e43561ffff8116809103610468576101c48801526102043561ffff8116809103610468576101e48801526102243561ffff8116809103610468576102048801526102443561ffff811680910361046857610224880152610264359063ffffffff82168092036104685761068d90889695946024946020946102448a0152600319898403016102648a01526108de565b85810360031901610284870152838152019201905f905b8082106106eb5750505090805f92038183855af19182156106e0576020926106d0575b50604051908152f35b5f6106da91610893565b5f6106c7565b6040513d5f823e3d90fd5b919350918335908115158092036104685760a081600193829352602087013560208201526040870135604082015260608701356060820152608087013560808201520194019201859392916106a4565b90919283359060ff8216809203610468576020816001938293520194019201906104f0565b634e487b7160e01b5f52601160045260245ffd5b63b06ebf3d60e01b5f5260045ffd5b346104685760403660031901126104685760209061079f61084c565b6001600160a01b03168282019081526024356020820152906107c48160408401610311565b519020604051903060388301526f5af43d82803e903d91602b57fd5bf3ff60248301527f00000000000000000000000000000000000000000000000000000000000000006014830152733d602d80600a3d3981f3363d3d373d3d3d363d73825260588201526037600c82012060788201526055604360018060a01b0392012016604051908152f35b600435906001600160a01b038216820361046857565b9181601f840112156104685782359167ffffffffffffffff8311610468576020808501948460051b01011161046857565b90601f8019910116810190811067ffffffffffffffff82111761045457604052565b80548210156108ca575f5260205f2001905f90565b634e487b7160e01b5f52603260045260245ffd5b916020908281520191905f905b8082106108f85750505090565b90919283359060018060a01b038216809203610468576020816001938293520194019201906108eb56000000000000000000000000fd5202675c194fbfa682622f11b80167336abba5", - "nonce": "0x9", - "chainId": "0x2328" - }, - "additionalContracts": [], - "isFixedGasLimit": false - }, - { - "hash": "0x9e5497669c9346a93768977e5941191bf439ad05b52c8966e0e6752d904cccb9", - "transactionType": "CALL", - "contractName": "SkurFactory", - "contractAddress": "0x02929b28dd9ddf57b1615c70dccdca6b6eda0015", - "function": "createVault(bytes32,address[],uint8[],(uint8,uint8,uint8,uint8,uint8,bool,uint32,uint32,uint32,uint32,uint32,uint32,uint16,uint16,uint16,uint16,uint32),address[],(bool,uint256,uint256,uint256,uint256)[])", - "arguments": [ - "0xfb983e9dbbaa3ef70fb8bb403a3f4b14671884646e3181a121177cf14b3664eb", - "[0x01e06CA25BF1e8711053D7Aa704A6B11eD7F4A22, 0x92e2349ADBDE041FB823552Ba99EbDba895a4A2d, 0xe90D2FE71BFeC93F730cF6b122675Da4437eC831]", - "[7, 2, 8]", - "(1, 2, 2, 1, 1, true, 21600, 86400, 86400, 86400, 172800, 604800, 500, 2000, 5000, 1000, 86400)", - "[0xF4AbedF17C3C8669D13C352a6002eAe5B39D772B, 0x0000000000000000000000000000000000000000]", - "[(true, 5000000000, 25000000000, 100000000000, 100000000000), (true, 100000000000000000000, 1000000000000000000000, 0, 5000000000000000000000)]" - ], - "transaction": { - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "to": "0x02929b28dd9ddf57b1615c70dccdca6b6eda0015", - "gas": "0x1376c0", - "value": "0x0", - "input": "0x5492a6ecfb983e9dbbaa3ef70fb8bb403a3f4b14671884646e3181a121177cf14b3664eb00000000000000000000000000000000000000000000000000000000000002c000000000000000000000000000000000000000000000000000000000000003400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000005460000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000000000000000015180000000000000000000000000000000000000000000000000000000000002a3000000000000000000000000000000000000000000000000000000000000093a8000000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000007d0000000000000000000000000000000000000000000000000000000000000138800000000000000000000000000000000000000000000000000000000000003e8000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000000000000003c00000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000000000000000000000000000000300000000000000000000000001e06ca25bf1e8711053d7aa704a6b11ed7f4a2200000000000000000000000092e2349adbde041fb823552ba99ebdba895a4a2d000000000000000000000000e90d2fe71bfec93f730cf6b122675da4437ec83100000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000007000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000002000000000000000000000000f4abedf17c3c8669d13c352a6002eae5b39d772b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000012a05f20000000000000000000000000000000000000000000000000000000005d21dba00000000000000000000000000000000000000000000000000000000174876e800000000000000000000000000000000000000000000000000000000174876e80000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000056bc75e2d6310000000000000000000000000000000000000000000000000003635c9adc5dea00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010f0cf064dd59200000", - "nonce": "0xa", - "chainId": "0x2328" - }, - "additionalContracts": [ - { - "transactionType": "CREATE2", - "contractName": null, - "address": "0xb2208ad86b44ce75427334a117ae27cb1b8bc4cc", - "initCode": "0x3d602d80600a3d3981f3363d3d373d3d3d363d73fd5202675c194fbfa682622f11b80167336abba55af43d82803e903d91602b57fd5bf3" - } - ], - "isFixedGasLimit": false - }, - { - "hash": "0x01f7aaee389357f7fd2d3f08d71a6310d706c3b76bc9c725f33011d51f2f5875", - "transactionType": "CALL", - "contractName": "SkurTestUSD", - "contractAddress": "0xf4abedf17c3c8669d13c352a6002eae5b39d772b", - "function": "mint(address,uint256)", - "arguments": [ - "0xb2208Ad86B44cE75427334A117Ae27CB1b8bc4cC", - "250000000000" - ], - "transaction": { - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "to": "0xf4abedf17c3c8669d13c352a6002eae5b39d772b", - "gas": "0x16f70", - "value": "0x0", - "input": "0x40c10f19000000000000000000000000b2208ad86b44ce75427334a117ae27cb1b8bc4cc0000000000000000000000000000000000000000000000000000003a35294400", - "nonce": "0xb", - "chainId": "0x2328" - }, - "additionalContracts": [], - "isFixedGasLimit": false - }, - { - "hash": "0xcbeaf5adb48944641de17375409ca6804fc4e1fd57c1bca6a98be6599f45066c", - "transactionType": "CALL", - "contractName": null, - "contractAddress": "0xb2208ad86b44ce75427334a117ae27cb1b8bc4cc", - "function": null, - "arguments": null, - "transaction": { - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "to": "0xb2208ad86b44ce75427334a117ae27cb1b8bc4cc", - "gas": "0x10197", - "value": "0x1bc16d674ec80000", - "input": "0x", - "nonce": "0xc", - "chainId": "0x2328" - }, - "additionalContracts": [], - "isFixedGasLimit": false - } - ], - "receipts": [ - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0xb53a1", - "logs": [], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "transactionHash": "0x4cc1be49c4e66ca9efd9222231489d12e288735cc93d85ddf6282a8215540808", - "transactionIndex": "0x0", - "blockHash": "0x3cd17c470dd491efd45c626a8cf00b2dd760fa631b03e2f47174bf69201a08c6", - "blockNumber": "0x74488", - "gasUsed": "0xb53a1", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "to": null, - "contractAddress": "0x689a4b4bb095cc9f2f9535eaf6d71e6dd0b55837" - }, - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0x75981", - "logs": [], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "transactionHash": "0x01e4efadbdc2b96b4b204e4e6c28570e198b56ea842cced139cb9df885843c90", - "transactionIndex": "0x0", - "blockHash": "0xb3e036c8c83a8959b06c00ae1d922cb7341ecd2624d0e416d0df0978d400b1e8", - "blockNumber": "0x7448a", - "gasUsed": "0x75981", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "to": null, - "contractAddress": "0xf4abedf17c3c8669d13c352a6002eae5b39d772b" - }, - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0x4b3636", - "logs": [], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "transactionHash": "0x776559273350f1c607cfb0584a7cd6b76db3b3a4290e4b12de7a8582dcd56424", - "transactionIndex": "0x0", - "blockHash": "0xf0b10a9fb69d4b418f43625f3e228f2aedde60d5cae86a212dcb3763df7aed2d", - "blockNumber": "0x7448c", - "gasUsed": "0x4b3636", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "to": null, - "contractAddress": "0xfd5202675c194fbfa682622f11b80167336abba5" - }, - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0x897b1", - "logs": [], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "transactionHash": "0x1595c05e9aebb398da3d64fe84e7a038f73ab1ddcb57a8e58da2a9abe2de72be", - "transactionIndex": "0x0", - "blockHash": "0x14c61b58039f70f06c91f2d650992a7100759a8788032f6d90de3da6ec264615", - "blockNumber": "0x7448e", - "gasUsed": "0x897b1", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "to": null, - "contractAddress": "0x02929b28dd9ddf57b1615c70dccdca6b6eda0015" - }, - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0xe176d", - "logs": [ - { - "address": "0x02929b28dd9ddf57b1615c70dccdca6b6eda0015", - "topics": [ - "0xc18dea5f6b30c3fd606e8e38832933e2fa57407dd77800aad0658505ee94df98", - "0x000000000000000000000000b2208ad86b44ce75427334a117ae27cb1b8bc4cc", - "0x00000000000000000000000001e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "0x0000000000000000000000000000000000000000000000000000000000000000" - ], - "data": "0xfb983e9dbbaa3ef70fb8bb403a3f4b14671884646e3181a121177cf14b3664eb", - "blockHash": "0xcd6ec99408c27dd73a683bf5c33b7c4b2cac121fe5d1b7ccec0c2258e8f072fc", - "blockNumber": "0x74490", - "blockTimestamp": "0x6aa8bdcf", - "transactionHash": "0x9e5497669c9346a93768977e5941191bf439ad05b52c8966e0e6752d904cccb9", - "transactionIndex": "0x0", - "logIndex": "0x0", - "removed": false - }, - { - "address": "0xb2208ad86b44ce75427334a117ae27cb1b8bc4cc", - "topics": [ - "0x5d3de0fa15f594659e83f792fff19b5062cbc430ea0e9cf465377cdfaab88454", - "0x00000000000000000000000001e06ca25bf1e8711053d7aa704a6b11ed7f4a22" - ], - "data": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007", - "blockHash": "0xcd6ec99408c27dd73a683bf5c33b7c4b2cac121fe5d1b7ccec0c2258e8f072fc", - "blockNumber": "0x74490", - "blockTimestamp": "0x6aa8bdcf", - "transactionHash": "0x9e5497669c9346a93768977e5941191bf439ad05b52c8966e0e6752d904cccb9", - "transactionIndex": "0x0", - "logIndex": "0x1", - "removed": false - }, - { - "address": "0xb2208ad86b44ce75427334a117ae27cb1b8bc4cc", - "topics": [ - "0x5d3de0fa15f594659e83f792fff19b5062cbc430ea0e9cf465377cdfaab88454", - "0x00000000000000000000000092e2349adbde041fb823552ba99ebdba895a4a2d" - ], - "data": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002", - "blockHash": "0xcd6ec99408c27dd73a683bf5c33b7c4b2cac121fe5d1b7ccec0c2258e8f072fc", - "blockNumber": "0x74490", - "blockTimestamp": "0x6aa8bdcf", - "transactionHash": "0x9e5497669c9346a93768977e5941191bf439ad05b52c8966e0e6752d904cccb9", - "transactionIndex": "0x0", - "logIndex": "0x2", - "removed": false - }, - { - "address": "0xb2208ad86b44ce75427334a117ae27cb1b8bc4cc", - "topics": [ - "0x5d3de0fa15f594659e83f792fff19b5062cbc430ea0e9cf465377cdfaab88454", - "0x000000000000000000000000e90d2fe71bfec93f730cf6b122675da4437ec831" - ], - "data": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008", - "blockHash": "0xcd6ec99408c27dd73a683bf5c33b7c4b2cac121fe5d1b7ccec0c2258e8f072fc", - "blockNumber": "0x74490", - "blockTimestamp": "0x6aa8bdcf", - "transactionHash": "0x9e5497669c9346a93768977e5941191bf439ad05b52c8966e0e6752d904cccb9", - "transactionIndex": "0x0", - "logIndex": "0x3", - "removed": false - }, - { - "address": "0xb2208ad86b44ce75427334a117ae27cb1b8bc4cc", - "topics": [ - "0x9d182ff993316b1c662980e27e42d42fae0bd158f1bed04e9988ec637fea8da4", - "0x000000000000000000000000f4abedf17c3c8669d13c352a6002eae5b39d772b" - ], - "data": "0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000012a05f20000000000000000000000000000000000000000000000000000000005d21dba00000000000000000000000000000000000000000000000000000000174876e800000000000000000000000000000000000000000000000000000000174876e800", - "blockHash": "0xcd6ec99408c27dd73a683bf5c33b7c4b2cac121fe5d1b7ccec0c2258e8f072fc", - "blockNumber": "0x74490", - "blockTimestamp": "0x6aa8bdcf", - "transactionHash": "0x9e5497669c9346a93768977e5941191bf439ad05b52c8966e0e6752d904cccb9", - "transactionIndex": "0x0", - "logIndex": "0x4", - "removed": false - }, - { - "address": "0xb2208ad86b44ce75427334a117ae27cb1b8bc4cc", - "topics": [ - "0x9d182ff993316b1c662980e27e42d42fae0bd158f1bed04e9988ec637fea8da4", - "0x0000000000000000000000000000000000000000000000000000000000000000" - ], - "data": "0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000056bc75e2d6310000000000000000000000000000000000000000000000000003635c9adc5dea00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010f0cf064dd59200000", - "blockHash": "0xcd6ec99408c27dd73a683bf5c33b7c4b2cac121fe5d1b7ccec0c2258e8f072fc", - "blockNumber": "0x74490", - "blockTimestamp": "0x6aa8bdcf", - "transactionHash": "0x9e5497669c9346a93768977e5941191bf439ad05b52c8966e0e6752d904cccb9", - "transactionIndex": "0x0", - "logIndex": "0x5", - "removed": false - }, - { - "address": "0xb2208ad86b44ce75427334a117ae27cb1b8bc4cc", - "topics": [ - "0x1a734774792c149a91deb03650b2a2bc51de7bbab7262857ace6fb1699b43041", - "0x0000000000000000000000000000000000000000000000000000000000000001" - ], - "data": "0x000000000000000000000000000000000000000000000000000000006aa8bdcf0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000005460000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000000000000000015180000000000000000000000000000000000000000000000000000000000002a3000000000000000000000000000000000000000000000000000000000000093a8000000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000007d0000000000000000000000000000000000000000000000000000000000000138800000000000000000000000000000000000000000000000000000000000003e80000000000000000000000000000000000000000000000000000000000015180", - "blockHash": "0xcd6ec99408c27dd73a683bf5c33b7c4b2cac121fe5d1b7ccec0c2258e8f072fc", - "blockNumber": "0x74490", - "blockTimestamp": "0x6aa8bdcf", - "transactionHash": "0x9e5497669c9346a93768977e5941191bf439ad05b52c8966e0e6752d904cccb9", - "transactionIndex": "0x0", - "logIndex": "0x6", - "removed": false - }, - { - "address": "0xb2208ad86b44ce75427334a117ae27cb1b8bc4cc", - "topics": [ - "0x4488062cfafbbb2c1d440f13796e120c4522761f74ecade440ee8f31f1bfbee0" - ], - "data": "0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003", - "blockHash": "0xcd6ec99408c27dd73a683bf5c33b7c4b2cac121fe5d1b7ccec0c2258e8f072fc", - "blockNumber": "0x74490", - "blockTimestamp": "0x6aa8bdcf", - "transactionHash": "0x9e5497669c9346a93768977e5941191bf439ad05b52c8966e0e6752d904cccb9", - "transactionIndex": "0x0", - "logIndex": "0x7", - "removed": false - } - ], - "logsBloom": "0x00100000000080080800000000000004000000000000000000000000000000000000040000000000000000200000004000004002010000000000000000040000000000000000000000000000000000800000000000040000000000000200000000000000020400000000000000000800080000000000040000000000000000000080000000000000000800000008000000000000000000000000000000000020000000000008000801000008000000000010000000080000002000000000000008000000000008000000000000000000000000000000000040002002000060000000000001000000000000000000000000000020000210000000000000000000", - "transactionHash": "0x9e5497669c9346a93768977e5941191bf439ad05b52c8966e0e6752d904cccb9", - "transactionIndex": "0x0", - "blockHash": "0xcd6ec99408c27dd73a683bf5c33b7c4b2cac121fe5d1b7ccec0c2258e8f072fc", - "blockNumber": "0x74490", - "gasUsed": "0xe176d", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "to": "0x02929b28dd9ddf57b1615c70dccdca6b6eda0015", - "contractAddress": null - }, - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0x10a05", - "logs": [ - { - "address": "0xf4abedf17c3c8669d13c352a6002eae5b39d772b", - "topics": [ - "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x000000000000000000000000b2208ad86b44ce75427334a117ae27cb1b8bc4cc" - ], - "data": "0x0000000000000000000000000000000000000000000000000000003a35294400", - "blockHash": "0x17d84ae556d56a5afead220e20d293bf75457ffb984d1fd157269dab7d121369", - "blockNumber": "0x74492", - "blockTimestamp": "0x6aa8bdd5", - "transactionHash": "0x01f7aaee389357f7fd2d3f08d71a6310d706c3b76bc9c725f33011d51f2f5875", - "transactionIndex": "0x0", - "logIndex": "0x0", - "removed": false - } - ], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000020000000000000040000800000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000002000000000000000000000000000000000000000000002000000020000000000000000000000000000000000000000020000200000000000000000000", - "transactionHash": "0x01f7aaee389357f7fd2d3f08d71a6310d706c3b76bc9c725f33011d51f2f5875", - "transactionIndex": "0x0", - "blockHash": "0x17d84ae556d56a5afead220e20d293bf75457ffb984d1fd157269dab7d121369", - "blockNumber": "0x74492", - "gasUsed": "0x10a05", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "to": "0xf4abedf17c3c8669d13c352a6002eae5b39d772b", - "contractAddress": null - }, - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0xba7f", - "logs": [ - { - "address": "0xb2208ad86b44ce75427334a117ae27cb1b8bc4cc", - "topics": [ - "0x5548c837ab068cf56a2c2479df0882a4922fd203edb7517321831d95078c5f62", - "0x00000000000000000000000001e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "0x0000000000000000000000000000000000000000000000000000000000000000" - ], - "data": "0x0000000000000000000000000000000000000000000000001bc16d674ec80000", - "blockHash": "0x5dac9f4a49db6e256d1baafa5a756cbe745fcc7c649a7d32ad8bb329e214886b", - "blockNumber": "0x74494", - "blockTimestamp": "0x6aa8bddc", - "transactionHash": "0xcbeaf5adb48944641de17375409ca6804fc4e1fd57c1bca6a98be6599f45066c", - "transactionIndex": "0x0", - "logIndex": "0x0", - "removed": false - } - ], - "logsBloom": "0x00000000000000080800000000000000000000000000000000000000000000000000000000000000000000000000004000000000000002000000000040000000000000000008000000000000000000000000000000000000000000000200000000000000020000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000", - "transactionHash": "0xcbeaf5adb48944641de17375409ca6804fc4e1fd57c1bca6a98be6599f45066c", - "transactionIndex": "0x0", - "blockHash": "0x5dac9f4a49db6e256d1baafa5a756cbe745fcc7c649a7d32ad8bb329e214886b", - "blockNumber": "0x74494", - "gasUsed": "0xba7f", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x01e06ca25bf1e8711053d7aa704a6b11ed7f4a22", - "to": "0xb2208ad86b44ce75427334a117ae27cb1b8bc4cc", - "contractAddress": null - } - ], - "libraries": [ - "src/SkurPolicyLib.sol:SkurPolicyLib:0x689a4b4Bb095cc9F2F9535Eaf6D71E6DD0B55837" - ], - "pending": [], - "returns": {}, - "timestamp": 1789443553946, - "chain": 9000, - "commit": null -} \ No newline at end of file diff --git a/contracts/broadcast/Deploy.s.sol/9000/run-1789464061730.json b/contracts/broadcast/Deploy.s.sol/9000/run-1789464061730.json deleted file mode 100644 index 0168ef5..0000000 --- a/contracts/broadcast/Deploy.s.sol/9000/run-1789464061730.json +++ /dev/null @@ -1,432 +0,0 @@ -{ - "transactions": [ - { - "hash": "0x5ec40a2bac9472f8b4d869940f032896d94240d509d887f4c34cd7b7691a2e76", - "transactionType": "CREATE", - "contractName": "SkurPolicyLib", - "contractAddress": "0x6baa1a24b55796d8346748ee3565e40d844ba576", - "function": null, - "arguments": null, - "transaction": { - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "gas": "0xeb984", - "input": "0x60808060405234601957610c75908161001e823930815050f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c8063099fe59914610660578063194e5f5c1461018b5780631c71447d146100b957637309439114610045575f80fd5b6101403660031901126100b55761005b366109c0565b60a03660a31901126100b55761006f61082f565b60a4359081151582036100b5576020926100ab92825260c4358483015260e4356040830152610104356060830152610124356080830152610bf7565b6040519015158152f35b5f80fd5b60a03660031901126100b5576100ce366109c0565b60208101516040820151106101525760608101518015159182610144575b82610136575b50506100fa57005b6040516353e95a6760e01b81526020600482015260136024820152720e0cae4a8f09ac2f0407c40c8c2d2d8f29ac2f606b1b6044820152606490fd5b6080015110905081806100f2565b6080810151151592506100ec565b6040516353e95a6760e01b815260206004820152601060248201526f0d8deee9ac2f0407c40d0d2ced09ac2f60831b6044820152606490fd5b6102203660031901126100b5576101a13661084f565b60ff8151161561062b576020810160ff81511660ff835116116105f55760ff80604084015116915116116105bb5760ff6060820151161561057f5760a0810151151580610570575b6105365760e0810163ffffffff81511663ffffffff60c084015116116104f0576101a0820161ffff81511680151590816104dd575b506104a65761271061ffff825116118015610492575b61045d576101c083019061ffff825116801515918261044e575b50506104115761ffff612710915116116103db576101e0820161271061ffff825116116103a6575161ffff16151580610390575b6103595763ffffffff9051169063ffffffff61012082015116828111610351575b5063ffffffff61014082015116828111610349575b5063ffffffff6101008201511682811161033f575b5061016063ffffffff91015116610e10820180921161032b57106102ed57005b6040516353e95a6760e01b81526020600482015260156024820152741c1c9bdc1bdcd85b151d1b081d1bdbc81cda1bdc9d605a1b6044820152606490fd5b634e487b7160e01b5f52601160045260245ffd5b91506101606102cd565b9150826102b8565b9150826102a3565b6040516353e95a6760e01b815260206004820152600e60248201526d656e76656c6f706557696e646f7760901b6044820152606490fd5b50610e1063ffffffff6102008401511610610282565b6040516353e95a6760e01b815260206004820152600c60248201526b656e76656c6f70652062707360a01b6044820152606490fd5b6040516353e95a6760e01b815260206004820152600d60248201526c68617264426c6f636b2062707360981b6044820152606490fd5b6040516353e95a6760e01b81526020600482015260146024820152731a185c99109b1bd8dac80f0818dc9a5d1a58d85b60621b6044820152606490fd5b5161ffff16119050848061024e565b6040516353e95a6760e01b815260206004820152600c60248201526b6578706f737572652062707360a01b6044820152606490fd5b5061271061ffff6101808501511611610234565b6040516353e95a6760e01b815260206004820152600e60248201526d32bc3837b9bab9329037b93232b960911b6044820152606490fd5b905061ffff61018085015116118461021e565b6040516353e95a6760e01b815260206004820152601960248201527f64656c6179437269746963616c203c2064656c617948696768000000000000006044820152606490fd5b6040516353e95a6760e01b815260206004820152601160248201527019dd585c991a585b951a1c995cda1bdb19607a1b6044820152606490fd5b5060ff608082015116156101e9565b6040516353e95a6760e01b815260206004820152601360248201527219dbdd995c9b985b98d9551a1c995cda1bdb19606a1b6044820152606490fd5b6040516353e95a6760e01b8152602060048201526011602482015270185c1c1c9bdd985b1cd0dc9a5d1a58d85b607a1b6044820152606490fd5b6040516353e95a6760e01b815260206004820152600d60248201526c0c2e0e0e4deecc2d8e690d2ced609b1b6044820152606490fd5b6040516353e95a6760e01b815260206004820152600c60248201526b617070726f76616c734c6f7760a01b6044820152606490fd5b6104403660031901126100b5576106763661084f565b610220366102231901126100b55761068c6107fa565b6102243560ff811681036100b55781526102443560ff811681036100b55760208201526102643560ff811681036100b55760408201526102843560ff811681036100b55760608201526102a43560ff811681036100b55760808201526102c43580151581036100b55760a08201526102e43563ffffffff811681036100b55760c08201526103043563ffffffff811681036100b55760e08201526103243563ffffffff811681036100b5576101008201526103443563ffffffff811681036100b5576101208201526103643563ffffffff811681036100b5576101408201526103843563ffffffff811681036100b5576101608201526103a43561ffff811681036100b5576101808201526103c43561ffff811681036100b5576101a08201526103e43561ffff811681036100b5576101c08201526104043561ffff811681036100b5576101e0820152610424359063ffffffff821682036100b5576020926100ab92610200830152610a05565b60405190610220820182811067ffffffffffffffff82111761081b57604052565b634e487b7160e01b5f52604160045260245ffd5b6040519060a0820182811067ffffffffffffffff82111761081b57604052565b6102209060031901126100b5576108646107fa565b9060043560ff811681036100b557825260243560ff811681036100b557602083015260443560ff811681036100b557604083015260643560ff811681036100b557606083015260843560ff811681036100b557608083015260a43580151581036100b55760a083015260c43563ffffffff811681036100b55760c083015260e43563ffffffff811681036100b55760e08301526101043563ffffffff811681036100b5576101008301526101243563ffffffff811681036100b5576101208301526101443563ffffffff811681036100b5576101408301526101643563ffffffff811681036100b5576101608301526101843561ffff811681036100b5576101808301526101a43561ffff811681036100b5576101a08301526101c43561ffff811681036100b5576101c08301526101e43561ffff811681036100b5576101e08301526102043563ffffffff811681036100b557610200830152565b60a09060031901126100b5576109d461082f565b9060043580151581036100b55782526024356020830152604435604083015260643560608301526084356080830152565b60ff82511660ff82511611610be45760ff60208301511660ff60208301511611610be45760ff60408301511660ff60408301511611610be45760ff60608301511660ff60608301511611610be45760ff60808301511660ff60808301511611610be45760a0810151151580610beb575b610be45763ffffffff60c08301511663ffffffff60c08301511611610be45763ffffffff60e08301511663ffffffff60e08301511611610be45763ffffffff6101008301511663ffffffff6101008301511611610be45763ffffffff6101208301511663ffffffff6101208301511611610be45763ffffffff6101408301511663ffffffff6101408301511611610be45763ffffffff6101608301511663ffffffff6101608301511610610be457610b4061ffff6101808301511661ffff6101808501511690610c5e565b610be457610b6161ffff6101a08301511661ffff6101a08501511690610c5e565b610be457610b8261ffff6101c08301511661ffff6101c08501511690610c5e565b610be45761ffff6101e082015116610ba361ffff6101e08501511682610c5e565b610bdc5715159182610bbf575b5050610bba575f90565b600190565b63ffffffff80929350610200809201511692015116115f80610bb0565b505050600190565b5050600190565b5060a082015115610a75565b9081511580610c54575b610be4576020810151602083015110610be4576040810151604083015110610be457610c366060830151606083015190610c5e565b610be457608080610c4c93015191015190610c5e565b610bba575f90565b5080511515610c01565b908115610c6f578015610be4571190565b50505f9056", - "nonce": "0x169", - "chainId": "0x2328" - }, - "additionalContracts": [], - "isFixedGasLimit": false - }, - { - "hash": "0x1837db1649c1303b313f6d8ba6cb8df1c0aa2f840fd10ce133ae96aa6d460935", - "transactionType": "CREATE", - "contractName": "SkurTestUSD", - "contractAddress": "0x3e41424adf6bb69b6271ef9b0541acd59a0d454a", - "function": null, - "arguments": null, - "transaction": { - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": null, - "gas": "0x98df4", - "value": "0x0", - "input": "0x60806040523461031757604080519081016001600160401b0381118282101761022a576040908152600d82526c14dadd5c8815195cdd081554d1609a1b602083015280519081016001600160401b0381118282101761022a5760405260048152631cd554d160e21b602082015281516001600160401b03811161022a57600354600181811c9116801561030d575b602082101461020c57601f81116102aa575b50602092601f821160011461024957928192935f9261023e575b50508160011b915f199060031b1c1916176003555b80516001600160401b03811161022a57600454600181811c91168015610220575b602082101461020c57601f81116101a9575b50602091601f8211600114610149579181925f9261013e575b50508160011b915f199060031b1c1916176004555b6040516106b5908161031c8239f35b015190505f8061011a565b601f1982169260045f52805f20915f5b85811061019157508360019510610179575b505050811b0160045561012f565b01515f1960f88460031b161c191690555f808061016b565b91926020600181928685015181550194019201610159565b60045f527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b601f830160051c81019160208410610202575b601f0160051c01905b8181106101f75750610101565b5f81556001016101ea565b90915081906101e1565b634e487b7160e01b5f52602260045260245ffd5b90607f16906100ef565b634e487b7160e01b5f52604160045260245ffd5b015190505f806100b9565b601f1982169360035f52805f20915f5b868110610292575083600195961061027a575b505050811b016003556100ce565b01515f1960f88460031b161c191690555f808061026c565b91926020600181928685015181550194019201610259565b60035f527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b601f830160051c81019160208410610303575b601f0160051c01905b8181106102f8575061009f565b5f81556001016102eb565b90915081906102e2565b90607f169061008d565b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c90816306fdde03146104e657508063095ea7b31461046457806318160ddd1461044757806323b872dd14610367578063313ce5671461034c57806340c10f191461028c57806370a082311461025557806395d89b411461013a578063a9059cbb14610109578063dd62ed3e146100b95763f0292a0314610095575f80fd5b346100b5575f3660031901126100b55760206040516509184e72a0008152f35b5f80fd5b346100b55760403660031901126100b5576100d26105df565b6100da6105f5565b6001600160a01b039182165f908152600160209081526040808320949093168252928352819020549051908152f35b346100b55760403660031901126100b55761012f6101256105df565b602435903361060b565b602060405160018152f35b346100b5575f3660031901126100b5576040515f6004548060011c9060018116801561024b575b6020831081146102375782855290811561021b57506001146101c6575b50819003601f01601f191681019067ffffffffffffffff8211818310176101b2576101ae829182604052826105b5565b0390f35b634e487b7160e01b5f52604160045260245ffd5b905060045f527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b5f905b8282106102055750602091508201018261017e565b60018160209254838588010152019101906101f0565b90506020925060ff191682840152151560051b8201018261017e565b634e487b7160e01b5f52602260045260245ffd5b91607f1691610161565b346100b55760203660031901126100b5576001600160a01b036102766105df565b165f525f602052602060405f2054604051908152f35b346100b55760403660031901126100b5576102a56105df565b602435906509184e72a000821161033d576001600160a01b031690811561032a57600254908082018092116103165760207fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef915f9360025584845283825260408420818154019055604051908152a3005b634e487b7160e01b5f52601160045260245ffd5b63ec442f0560e01b5f525f60045260245ffd5b638b01e07b60e01b5f5260045ffd5b346100b5575f3660031901126100b557602060405160068152f35b346100b55760603660031901126100b5576103806105df565b6103886105f5565b6001600160a01b0382165f8181526001602081815260408084203385529091529091205491936044359392909181016103c7575b5061012f935061060b565b83811061042c5784156104195733156104065761012f945f52600160205260405f2060018060a01b0333165f526020528360405f2091039055846103bc565b634a1406b160e11b5f525f60045260245ffd5b63e602df0560e01b5f525f60045260245ffd5b8390637dc7a0d960e11b5f523360045260245260445260645ffd5b346100b5575f3660031901126100b5576020600254604051908152f35b346100b55760403660031901126100b55761047d6105df565b602435903315610419576001600160a01b031690811561040657335f52600160205260405f20825f526020528060405f20556040519081527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560203392a3602060405160018152f35b346100b5575f3660031901126100b5575f6003548060011c906001811680156105ab575b6020831081146102375782855290811561021b57506001146105565750819003601f01601f191681019067ffffffffffffffff8211818310176101b2576101ae829182604052826105b5565b905060035f527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b5f905b8282106105955750602091508201018261017e565b6001816020925483858801015201910190610580565b91607f169161050a565b602060409281835280519182918282860152018484015e5f828201840152601f01601f1916010190565b600435906001600160a01b03821682036100b557565b602435906001600160a01b03821682036100b557565b6001600160a01b03169081156106a2576001600160a01b031691821561032a57815f525f60205260405f205481811061068957817fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef92602092855f525f84520360405f2055845f525f825260405f20818154019055604051908152a3565b8263391434e360e21b5f5260045260245260445260645ffd5b634b637e8f60e11b5f525f60045260245ffd", - "nonce": "0x16a", - "chainId": "0x2328" - }, - "additionalContracts": [], - "isFixedGasLimit": false - }, - { - "hash": "0x7009b1ae1b36af481edcaa8989b1243b9f1ff770649e7a5a27c0f6f522b28396", - "transactionType": "CREATE", - "contractName": "SkurVault", - "contractAddress": "0xe21c7ad3a56d187848f9915bb43f88820278dea3", - "function": null, - "arguments": null, - "transaction": { - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": null, - "gas": "0x61c679", - "value": "0x0", - "input": "0x6080806040523460255760015f55600160ff1981541617600155615749908161002a8239f35b5f80fdfe6080806040526004361015610062575b50361561001a575f80fd5b5f8052601160205260405f20610031348254612c7c565b90555f6040513481527f5548c837ab068cf56a2c2479df0882a4922fd203edb7517321831d95078c5f6260203392a3005b5f905f3560e01c9081630db026221461278357508063128e9be6146127605780632014055d146126b55780632358d5a81461266c57806328dca5ba14612642578063295a5212146126165780632d8a98d9146121f55780632de94807146121ba5780633a5d0d6f1461210a57806340e58ee514612072578063460fc01914611ea05780635305548114611e6757806354387ad714611e4357806358355ead14611e1c57806362812a3914611d6257806367e4ac2c14611ce457806368d525c714611b7957806378f3c95a14611a5b5780638ec1f4ca14611a3757806397feb926146119745780639d8b9a49146118ec5780639eab5253146117bf578063a15a2f8b14611786578063a2380d351461175d578063a8badaa5146116a8578063b314c57014611580578063b52f393e14611555578063b759f95414611535578063bacd2e9714611517578063bf81bf431461142b578063c0555a0a1461139b578063c65df7941461135c578063c75e0df21461120c578063c7f758a814610f26578063ce1e462614610e85578063d0ee62c714610939578063da35c6641461091b578063db02466414610574578063ea70b4af14610556578063ee5248941461026f5763fe0d94c10361000f573461026c57602036600319011261026c57600281541461025d576002815561025660043561385a565b6001815580f35b633ee5aeb560e01b8152600490fd5b80fd5b503461026c5761022036600319011261026c5761028a61486f565b736baa1a24b55796d8346748ee3565e40d844ba576803b1561052a5760405163065397d760e21b815282906102c1600482016136b2565b818161022481865af4801561053657610541575b50506020610444916040519283809263099fe59960e01b825260025460ff8116600484015260ff8160081c16602484015260ff8160101c16604484015260ff8160181c16606484015260ff81871c16608484015260ff8160281c16151560a484015263ffffffff8160301c1660c484015263ffffffff8160501c1660e484015263ffffffff8160701c1661010484015263ffffffff8160901c1661012484015263ffffffff8160b01c1661014484015263ffffffff8160d01c1661016484015260f01c61018483015263ffffffff60035461ffff81166101a485015261ffff8160101c166101c485015261ffff81881c166101e485015260301c166102048301526103e361022483016136b2565b5af49081156105365782916104f8575b5060405190610404602083016136b2565b6102208252610415610240836129d7565b61041e8161495e565b9183600260ff60015460081c16610434816127cc565b1490816104f0575b816104e4575b506104d5579261045b6104cd9260209583806001614cd3565b8082526012855260408220600254815463ff00000090911664ffff0000001990911617602084901b64ff000000001617815590936104b7906104ae9063ffffffff905b166001600160401b034216612c2e565b6004830161368a565b604051916104c586846129d7565b825283614f18565b604051908152f35b6372b89e9760e11b8452600484fd5b9450600190505f610442565b83915061043c565b90506020813d60201161052e575b81610513602093836129d7565b8101031261052a5761052490612ba2565b5f6103f3565b5080fd5b3d9150610506565b6040513d84823e3d90fd5b8161054b916129d7565b61052a57815f6102d5565b503461026c578060031936011261026c576020600554604051908152f35b503461026c57604036600319011261026c5761058e6127a0565b906105976127b6565b61059f6148c5565b6001600160a01b031691821561090c576001600160a01b03811682526008602052604082205460ff16156108fd57828252600860205260ff6040832054166108ee5760ff60025460201c16156108b457604051926020840152602083526106076040846129d7565b61061260045461381e565b908160045561062260055461381e565b600590815582845260126020526040842080546bffffffffffffffffffff0000163360601b6001600160601b031916176101061781556002810180546001600160a01b0319166001600160a01b0390941693909317909255600382018490558451939082016001600160401b0385116108a05761069f81546135b4565b601f811161085b575b506020958690601f87116001146107f6579580916104cd969785926107eb575b50508160011b915f199060031b1c19161790555b60ff60281b19825416825560015463ffffffff60401b83549160301b169063ffffffff60401b191617825560046001600160401b03421692016001600160401b0383166001600160401b031982541617815561074363ffffffff60025460d01c1684612c2e565b815467ffffffffffffffff60801b191660809190911b67ffffffffffffffff60801b161790558381526012855260408120805463ff000000198116825560025464ffff0000001990911690871c60201b64ff0000000016178155916107c1906107b89063ffffffff60025460b01c1690612c2e565b6004840161368a565b50604051906107d16040836129d7565b60088252677265636f7665727960c01b8583015283614f18565b015190505f806106c8565b8284528184209190601f198816855b81811061084457509160019391896104cd999a941061082c575b505050811b0190556106dc565b01515f1960f88460031b161c191690555f808061081f565b92938a600181928786015181550195019301610805565b81835260208320601f870160051c81019160208810610896575b601f0160051c01905b81811061088b57506106a8565b83815560010161087e565b9091508190610875565b634e487b7160e01b82526041600452602482fd5b604051637417de8f60e01b815260206004820152601160248201527019dd585c991a585b951a1c995cda1bdb19607a1b6044820152606490fd5b630c8ce74760e11b8252600482fd5b63148fe22160e11b8252600482fd5b63d92e233d60e01b8252600482fd5b503461026c578060031936011261026c576020600454604051908152f35b503461026c57608036600319011261026c576109536127a0565b9061095c6127b6565b60443590606435906001600160401b038211610e815736602383011215610e81578160040135936001600160401b03851161026c57366024868501011161026c576109a561489a565b600260ff60015460081c166109b9816127cc565b14610e72576001600160a01b03821694851561090c578415610e63576109e0858489612d80565b9260018060a01b0388168352600d602052610a4560408420600460405191610a0783612970565b60ff81541615158352600181015460208401526002810154604084015260038101546060840152015460808201528761ffff60408801511691614c58565b868352600e602052604083209060ff8254166006811015610e4f5715610e40575b506001600160401b03421697610a8663ffffffff60a0870151168a612c2e565b915460ff81166006811015610e2c5760011480610e0e575b610dfa575b5060209860405198610ab58b8b6129d7565b858a52610ac360045461381e565b9889600455610ad360055461381e565b60059081558a885260128d526040882080546bffffffffffffffffffff0000163360601b6001600160601b031916176101001781556002810180546001600160a01b03191690941790935560038301919091558a519a908201906001600160401b038c11610de657908b8d979695949392610b4e83546135b4565b601f8111610d78575b50601f899211600114610d065792610bec928e6104cd9e9f600494610ccf9a9998978e92610cfb575b50508160011b915f199060031b1c19161790555b60ff60281b19815416815560015463ffffffff60401b82549160301b169063ffffffff60401b191617815501916001600160401b0381166001600160401b031984541617835563ffffffff60025460d01c1690612c2e565b8154906001600160401b0360801b9060801b16906001600160401b0360801b191617905589865260128552610ca560ff608060408920998051610c2e816127cc565b610c37816127cc565b8b5462ff000067ffff0000000000008c85015160301b169260101b169067ffff000000ff0000191617178b55610c88836060830151168c9063ff00000082549160181b169063ff0000001916179055565b0151895464ff000000001916911660201b64ff0000000016178855565b6001870180546001600160a01b0319166001600160a01b039092169190911790556004860161368a565b806024610cdb82612c61565b96610ce960405198896129d7565b82885201838701378401015283614f18565b015190505f80610b80565b828a52888a209190601f198f168b5b818110610d6157506104cd9e9f600494610ccf9a99989794610bec97948360019510610d49575b505050811b019055610b94565b01515f1960f88460031b161c191690555f8080610d3c565b92938b600181928786015181550195019301610d15565b838b9a929495969798999a52818b2091601f840160051c83019310610ddc575b918f99989796959493918f93601f0160051c01905b818110610dba5750610b57565b90925060019193949596979899508a815501918d918f99989796959493610dad565b9091508190610d98565b634e487b7160e01b88526041600452602488fd5b60481c6001600160401b031691505f610aa3565b506001600160401b0383166001600160401b038260481c1611610a9e565b634e487b7160e01b86526021600452602486fd5b610e4990614b3b565b5f610a66565b634e487b7160e01b85526021600452602485fd5b6365e52d5160e11b8252600482fd5b6372b89e9760e11b8152600490fd5b8380fd5b503461026c578060031936011261026c57610200604051610ea58161299f565b8281528260208201528260408201528260608201528260808201528260a08201528260c08201528260e08201528261010082015282610120820152826101408201528261016082015282610180820152826101a0820152826101c0820152826101e08201520152610220610f17612c89565b610f246040518092612887565bf35b503461026c57602036600319011261026c5760606101e0604051610f49816129bb565b83815283602082015283604082015283838201528360808201528360a08201528360c08201528360e08201528361010082015283610120820152836101408201528361016082015283610180820152836101a0820152836101c08201520152600435815260126020526040812060405190610fc3826129bb565b80549060ff82166007811015610e4f57835260ff8260081c166005811015610e4f576110b691600591602086015260ff8460101c16611001816127cc565b604086015260ff8460181c16606086015260ff8460201c16608086015260ff8460281c16151560a086015261ffff8460301c1660c086015263ffffffff8460401c1660e086015261010085019360601c845260018060a01b0360018201541661012086015260018060a01b0360028201541661014086015260038101546101608601526001600160401b036004820154818116610180880152818160401c166101a088015260801c166101c0860152016135ec565b6101e08301526040519260208452825160078110156111f857602085015260208301519060058210156111e4575083926111e0926101e09260408601526040820151611101816127cc565b606086015260ff606083015116608086015260ff60808301511660a086015260a0820151151560c086015261ffff60c08301511660e086015263ffffffff60e08301511661010086015260018060a01b0390511661012085015260018060a01b036101208201511661014085015260018060a01b03610140820151166101608501526101608101516101808501526001600160401b03610180820151166101a08501526001600160401b036101a0820151166101c08501526001600160401b036101c08201511682850152015161020080840152610220830190612863565b0390f35b634e487b7160e01b81526021600452602490fd5b634e487b7160e01b82526021600452602482fd5b503461026c57602036600319011261026c57600435600381101561052a5761123261486f565b61123b816127cc565b60ff60015460081c1661124d816127cc565b60ff8216908082101561134d57600290611266816127cc565b036113435761128063ffffffff60025460901c16926127cc565b60ff60025460201c1690604051906112996020836129d7565b84825284600260ff60015460081c166112b1816127cc565b14908161133a575b81611330575b5061132157916104b76104ae63ffffffff6112e5600189966104cd9860209b6005614cd3565b8086526012895260408620600254815463ff00000090911664ffff000000199091161760209690961b64ff00000000169590951785559661049e565b6372b89e9760e11b8552600485fd5b955050845f6112bf565b600191506112b9565b61128083926127cc565b63710a5e0f60e01b8452600484fd5b503461026c57602036600319011261026c5761138760406111e0926004358152601360205220612bbb565b604051918291602083526020830190612827565b503461026c57604036600319011261026c576004356113b86148c5565b6113c1816148dc565b6113ca82613276565b1561141c5761040061ff00198254161790556113e7600554612b51565b6005556040519060243582527fcf4be29f7d91e92478a19a3eab52f6161f4e4580fcd2f5f8715f191dc2647bbb60203393a380f35b63ef28671760e01b8352600483fd5b503461026c57602036600319011261026c5760043580825260126020526040822060ff815460081c166005811015611503576001036114f4576001600160401b03600482015460801c164211156114c05761030061ff0019825416179055611494600554612b51565b60055533907f74c34a008ce735d9fcf0bd03a9b238d212ad4c441c020661f4ffbb6442645b858380a380f35b60405163935f84bf60e01b815260206004820152600b60248201526a1b9bdd08195e1c1a5c995960aa1b6044820152606490fd5b633ee3282d60e11b8352600483fd5b634e487b7160e01b84526021600452602484fd5b503461026c578060031936011261026c576020600654604051908152f35b503461026c57602036600319011261026c57611552600435613396565b80f35b503461026c57602036600319011261026c5761138760406111e0926004358152601460205220612bbb565b503461026c57604036600319011261026c5761159a6127a0565b6024359060ff821691828103610e81576115b261486f565b6001600160a01b0382168015611699576115cb82614bfa565b8452600860205260ff60408520541690838214611667576115eb91614c26565b906115f58261495e565b92604051916116056020846129d7565b85835285600260ff60015460081c1661161d816127cc565b14908161165f575b81611653575b5061164457926020959261045b926104cd956003614cd3565b6372b89e9760e11b8652600486fd5b9650600190505f61162b565b859150611625565b60405163935f84bf60e01b81526020600482015260096024820152686e6f206368616e676560b81b6044820152606490fd5b63d92e233d60e01b8552600485fd5b503461026c57602036600319011261026c576116c26127a0565b6116ca61489a565b6001600160a01b038116801561174e578252600e60205260ff604083205416600681101561173a576116ff5761155290614b3b565b60405163935f84bf60e01b8152602060048201526012602482015271185b1c9958591e481c9959da5cdd195c995960721b6044820152606490fd5b634e487b7160e01b83526021600452602483fd5b63d92e233d60e01b8352600483fd5b503461026c57602036600319011261026c57602061177c600435613276565b6040519015158152f35b503461026c57602036600319011261026c576020906040906001600160a01b036117ae6127a0565b168152601083522054604051908152f35b503461026c578060031936011261026c57604051906007548083528260208101600784526020842092845b8181106118ca5750506117ff925003836129d7565b815161182361180d8261324b565b9161181b60405193846129d7565b80835261324b565b602082019290601f1901368437805b8451811015611872576001906001600160a01b036118508288613262565b51168352600860205260ff60408420541661186b8286613262565b5201611832565b5061188f9392916020604051958695604087526040870190612827565b918583038287015251918281520192915b8181106118ae575050500390f35b825160ff168452859450602093840193909201916001016118a0565b84546001600160a01b03168352600194850194879450602090930192016117ea565b503461026c57606036600319011261026c5760c061191c61190b6127a0565b6119136127b6565b60443591612d80565b63ffffffff60a0604051928051611932816127cc565b845261ffff602082015116602085015261ffff604082015116604085015260ff606082015116606085015260ff608082015116608085015201511660a0820152f35b503461026c57604036600319011261026c5761198e6127a0565b6001600160a01b0316602435811561174e578015611a28576119e06040516323b872dd60e01b6020820152336024820152306044820152826064820152606481526119da6084826129d7565b8361563e565b8183526011602052604083206119f7828254612c7c565b90556040519081527f5548c837ab068cf56a2c2479df0882a4922fd203edb7517321831d95078c5f6260203392a380f35b6365e52d5160e11b8352600483fd5b503461026c578060031936011261026c57602060ff600a5460101c16604051908152f35b503461026c57604036600319011261026c57611a756127a0565b6024356006811015611b7557611a8961486f565b6001600160a01b0382168015611b66578115611b28578352600e602052611ab78160ff604086205416614a45565b90611ac18261495e565b9260405191611ad16020846129d7565b85835285600260ff60015460081c16611ae9816127cc565b149081611b20575b81611b14575b5061164457926020959261045b9260ff6104cd9616906004614cd3565b9650600190505f611af7565b859150611af1565b60405163935f84bf60e01b8152602060048201526015602482015274757365204e455720746f2072652d70726f6261746560581b6044820152606490fd5b63d92e233d60e01b8452600484fd5b8280fd5b503461026c57602036600319011261026c5760a0906001600160401b03611b9e6127a0565b8283908492600180881b03811695868152600f60205260408120968152600d602052611c0960408220600460405191611bd683612970565b60ff8154161515835260018101546020840152600281015460408401526003810154606084015201546080820152614976565b929050875487611c1a818316612c0e565b164210611cd95782985b6003549061ffff8260201c169283611c5a575b505050505050604051958652602086015260408501526060840152166080820152f35b9598509396509194509260401c8716919060301c63ffffffff1687611c7f8285612c2e565b164210611cae57505050611c9f61271091611c9a85946149d3565b612c4e565b0491925b5f8080808080611c37565b9194509250612710611ccc611cd39360026003870154960154612c4e565b0493612c2e565b92611ca3565b600189015498611c24565b503461026c578060031936011261026c57604051600b8054808352908352909160208301917f0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01db9915b818110611d43576111e085611387818703826129d7565b82546001600160a01b0316845260209093019260019283019201611d2c565b503461026c57602036600319011261026c57604060a091611d816127a0565b611d896129f8565b50600180851b03168152600e6020522060405190611da682612970565b6001600160401b038154611dbd60ff821685612baf565b816020850193818360081c1685528160016040880192828660481c1684528260608a019660881c16865201549560808801968752611dff60405180995161281a565b511660208701525116604085015251166060830152516080820152f35b503461026c578060031936011261026c57602063ffffffff60015460101c16604051908152f35b503461026c578060031936011261026c57602060ff600a5460181c16604051908152f35b503461026c57602036600319011261026c576020906040906001600160a01b03611e8f6127a0565b168152601183522054604051908152f35b503461206e5760c036600319011261206e57611eba6127a0565b60a036602319011261206e57611ece61486f565b736baa1a24b55796d8346748ee3565e40d844ba576803b1561206e57604051631c71447d60e01b8152611f0360048201612b71565b5f8160a481855af480156120635761204e575b5060018060a01b0382168352600d60205260206004610144604086206040519485938492637309439160e01b845260ff81541615158285015260018101546024850152600281015460448501526003810154606485015201546084830152611f8060a48301612b71565b5af4908115612043578391612009575b50604051611fa060208201612b71565b60a08152611faf60c0826129d7565b611fb88261495e565b9284600260ff60015460081c16611fce816127cc565b149081612001575b81611ff5575b50611321576104cd92859260209661045b936002614cd3565b9550600190505f611fdc565b849150611fd6565b90506020813d60201161203b575b81612024602093836129d7565b81010312611b755761203590612ba2565b5f611f90565b3d9150612017565b6040513d85823e3d90fd5b61205b9193505f906129d7565b5f915f611f16565b6040513d5f823e3d90fd5b5f80fd5b3461206e57602036600319011261206e5760043561208f816148dc565b805460601c331415806120f3575b6120e45761030061ff00198254161790556120b9600554612b51565b60055533907f74c34a008ce735d9fcf0bd03a9b238d212ad4c441c020661f4ffbb6442645b855f80a3005b6367909b1560e01b5f5260045ffd5b50335f526008602052600160405f2054161561209d565b3461206e57604036600319011261206e57600435600381101561206e5761212f614844565b612138816127cc565b60ff60015460081c1661214a816127cc565b60ff821611156121ab576001549060ff8260081c1691612169826127cc565b61ff001916600882901b61ff001617600155612184826127cc565b61218d816127cc565b6040519160243583525f5160206157295f395f51905f5260203394a4005b63216a142160e11b5f5260045ffd5b3461206e57602036600319011261206e576001600160a01b036121db6127a0565b165f526008602052602060ff60405f205416604051908152f35b3461206e576102a036600319011261206e576004356001600160401b03811161206e576122269036906004016127ea565b906024356001600160401b03811161206e576122469036906004016127ea565b90919061022036604319011261206e57610264356001600160401b03811161206e576122769036906004016127ea565b909261028435926001600160401b03841161206e573660238501121561206e578360040135956001600160401b03871161206e5736602460a089028701011161206e5760015460ff81166126085760019060ff1916176001558188148015906125fe575b6125cf575f5b88811061253e57505050505f5b8181106124a457856040516123018161299f565b60443560ff8116810361206e57815260643560ff8116810361206e57602082015260843560ff8116810361206e57604082015260a43560ff8116810361206e57606082015260c43560ff8116810361206e57608082015260e435801515810361206e5760a08201526101043563ffffffff8116810361206e5760c08201526101243563ffffffff8116810361206e5760e08201526101443563ffffffff8116810361206e576101008201526101643563ffffffff8116810361206e576101208201526101843563ffffffff8116810361206e576101408201526101a43563ffffffff8116810361206e576101608201526101c43561ffff8116810361206e576101808201526101e43561ffff8116810361206e576101a08201526102043561ffff8116810361206e576101c08201526102243561ffff8116810361206e576101e0820152610244359163ffffffff8316830361206e5761248a827f4488062cfafbbb2c1d440f13796e120c4522761f74ecade440ee8f31f1bfbee09461020060409501526145c5565b63ffffffff60015460101c169082519182526020820152a1005b6124b76124b2828487612b1f565b612b2f565b908581101561252a5760a08102840160a0602319823603011261206e576040516124e081612970565b602482013593841515850361206e5760a4612524936001968452604481013560208501526064810135604085015260848101356060850152013560808301526143f9565b016122ed565b634e487b7160e01b5f52603260045260245ffd5b6001600160a01b036125546124b2838c88612b1f565b165f52600860205260ff60405f2054166125c05760ff61257d612578838686612b1f565b612b43565b16156125b157806125ab6125976124b26001948d89612b1f565b6125a5612578848888612b1f565b9061426b565b016122e0565b633a767e1d60e21b5f5260045ffd5b630c8ce74760e11b5f5260045ffd5b60405163935f84bf60e01b81526020600482015260066024820152650d8cadccee8d60d31b6044820152606490fd5b50868414156122da565b62dc149f60e41b5f5260045ffd5b3461206e575f36600319011261206e57602060ff60015460081c166040519061263e816127cc565b8152f35b3461206e57602036600319011261206e576040612660600435612a22565b82519182526020820152f35b3461206e57604036600319011261206e576126856127b6565b6004355f52601560205260405f209060018060a01b03165f52602052602060ff60405f2054166040519015158152f35b3461206e57602036600319011261206e576126ce6127a0565b6126d66129f8565b5060018060a01b03165f52600d60205260a060405f206004604051916126fb83612970565b60ff8154161515835260018101546020840152600281015460408401526003810154606084015201546080820152610f246040518092608080918051151584526020810151602085015260408101516040850152606081015160608501520151910152565b3461206e575f36600319011261206e57602060ff600a5460081c16604051908152f35b3461206e575f36600319011261206e5760209060ff600a54168152f35b600435906001600160a01b038216820361206e57565b602435906001600160a01b038216820361206e57565b600311156127d657565b634e487b7160e01b5f52602160045260245ffd5b9181601f8401121561206e578235916001600160401b03831161206e576020808501948460051b01011161206e57565b9060068210156127d65752565b90602080835192838152019201905f5b8181106128445750505090565b82516001600160a01b0316845260209384019390920191600101612837565b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b63ffffffff610200809260ff815116855260ff602082015116602086015260ff604082015116604086015260ff606082015116606086015260ff608082015116608086015260a0810151151560a08601528260c08201511660c08601528260e08201511660e0860152826101008201511661010086015282610120820151166101208601528261014082015116610140860152826101608201511661016086015261ffff6101808201511661018086015261ffff6101a0820151166101a086015261ffff6101c0820151166101c086015261ffff6101e0820151166101e0860152015116910152565b60a081019081106001600160401b0382111761298b57604052565b634e487b7160e01b5f52604160045260245ffd5b61022081019081106001600160401b0382111761298b57604052565b61020081019081106001600160401b0382111761298b57604052565b90601f801991011681019081106001600160401b0382111761298b57604052565b60405190612a0582612970565b5f6080838281528260208201528260408201528260608201520152565b90815f52601260205260405f20825f52601360205260ff60405f2091541660078110156127d657612b17576002905b5f918154915f5b838110612acc5750505050915f52601460205260405f205f908054905f5b828110612a835750505090565b612a8d8183613315565b60018060a01b0391549060031b1c165f526008602052600860405f205416612ab8575b600101612a76565b92612ac460019161381e565b939050612ab0565b612ad68183613315565b60018060a01b0391549060031b1c165f52600860205260ff8360405f20541616612b03575b600101612a58565b93612b0f60019161381e565b949050612afb565b600190612a51565b919081101561252a5760051b0190565b356001600160a01b038116810361206e5790565b3560ff8116810361206e5790565b8015612b5d575f190190565b634e487b7160e01b5f52601160045260245ffd5b60243580151580910361206e578152604435602082015260643560408201526084356060820152608060a435910152565b5190811515820361206e57565b60068210156127d65752565b90604051918281549182825260208201905f5260205f20925f5b818110612bec575050612bea925003836129d7565b565b84546001600160a01b0316835260019485019487945060209093019201612bd5565b6001600160401b0362015180911601906001600160401b038211612b5d57565b906001600160401b03809116911601906001600160401b038211612b5d57565b81810292918115918404141715612b5d57565b6001600160401b03811161298b57601f01601f191660200190565b91908201809211612b5d57565b60405190612c968261299f565b8160025460ff8116825260ff8160081c16602083015260ff8160101c16604083015260ff8160181c16606083015260ff8160201c16608083015260ff8160281c16151560a083015263ffffffff8160301c1660c083015263ffffffff8160501c1660e083015263ffffffff8160701c1661010083015263ffffffff8160901c1661012083015263ffffffff8160b01c1661014083015263ffffffff8160d01c1661016083015260f01c61018082015261020063ffffffff60035461ffff81166101a085015261ffff8160101c166101c085015261ffff8160201c166101e085015260301c16910152565b92916040519160c083018381106001600160401b0382111761298b576040525f835260208301925f845260408101915f835260608201945f865260808301935f85525f60a0850152839860018060a01b038116805f52600d60205260405f209460405195612ded87612970565b60ff81541615918215885260018201549260208901938452600460028401549360408b01948552600381015460608c0152015460808a01526132385760018060a01b031693845f52600e60205260405f209760405195612e4c87612970565b60018a549a612e5e60ff8d168a612baf565b6001600160401b038c60081c1660208a01526001600160401b0360408a019c8d828260481c16905260881c1660608a015201546080880152865160068110156127d6576005146132265750612eb290614976565b9490505f52600f602052612ec960405f20916149d3565b92519151906001600160401b03612ee281835416612c0e565b16421061321c57505f9a5b85519560068710156127d6575160068110156127d6576001149889613208575b5060ff60015460081c1660025460f01c9161ffff6003541697604051976101608901928984106001600160401b0385111761298b57612f75936040528952602089019788526040890196875260608901958652608089015260a088019e8f5260c08801612baf565b60e086019915158a52612f87816127cc565b6101008601526101208501526101408401948552835190515f9283918311156131e3575050612fc161ffff916002809417945b5190615696565b94511680151590816131d4575b501561317d5750600860029117955b60c083015160068110156127d65760040361311c575050602060029517975b6080820151151590816130d8575b509360ff9860a0989794613074948b989463ffffffff986130a1575b6101006001910151613037816127cc565b613040816127cc565b14613085575b61ffff90811690915216905261305b816127cc565b808352613067816127cc565b61306f612c89565b614ab3565b939093169190950152169052169052565b9061ffff60406130968295976156f5565b961793505090613046565b92946130ac816127cc565b6001818b16106130ca5761010060806001925b971794915050613026565b5060016101006080826130bf565b6130ef915097969397959194929551855190612c7c565b918260011b9280840460021490151715612b5d5760808501519697939691959094909290911060ff61300a565b60c083015160068110156127d657613143575061313b610110916156f5565b951797612ffc565b60c0839a92979a015160068110156127d6576001149081613172575b5015612ffc57979461313b6010916156f5565b90505115155f61315f565b909561ffff6101208401511680151590816131c5575b5015612fdd5795906131a4816127cc565b600160ff8216106131bb576004905b911795612fdd565b50600460016131b3565b905061ffff851610155f613193565b905061ffff851610155f612fce565b939092935182116131fc575b61ffff91612fc191612fba565b600193508392506131ef565b516001600160401b0316421098505f612f0d565b600101549a612eed565b63325fbd1560e11b5f5260045260245ffd5b8363a6e7da2360e01b5f5260045260245ffd5b6001600160401b03811161298b5760051b60200190565b805182101561252a5760209160051b010190565b5f52601260205260405f205460ff8160081c1660058110156127d6576001036132f85760ff8160281c166132f25760ff811660078110156127d6576132f857600260ff8260101c166132c7816127cc565b146132f25760301c6010166132ed5760ff60015460081c166132e8816127cc565b151590565b600190565b50600190565b505f90565b60075481101561252a5760075f5260205f2001905f90565b805482101561252a575f5260205f2001905f90565b60075490600160401b82101561298b5761334f826001612bea94016007556007613315565b81546001600160a01b0393841660039290921b91821b9390911b1916919091179055565b90815491600160401b83101561298b578261334f916001612bea95018155613315565b61339f816148dc565b335f52600860205260405f205490600882166134bb575460ff1660078110156127d657600681146134a75761349c5760ff600280925b16161561348657505f81815260156020908152604080832033845290915290205460ff1661347757805f52601560205260405f2060018060a01b0333165f5260205260405f20600160ff19825416179055805f52601360205261343b3360405f20613373565b805f52601360205260405f2054906040519182527f2f0f51d2f12c71357e60d575d47e93e081dbaf32072245ab1e01540f0aadbf1a60203393a3565b63080fc0bd60e11b5f5260045ffd5b60ff9063e84c979760e01b5f521660045260245ffd5b60ff600180926133d5565b63e84c979760e01b5f52600860045260245ffd5b60ff91505460201c1615613563575f81815260156020908152604080832033845290915290205460ff1661347757805f52601560205260405f2060018060a01b0333165f5260205260405f20600160ff19825416179055805f5260146020526135273360405f20613373565b805f52601460205260405f2054906040519182527fb8c7de51f1e8e2b6cec2574c8731fba6de6e3e3002b2e454ebccdf32f591ca8660203393a3565b60405163935f84bf60e01b815260206004820152602260248201527f677561726469616e20636f6e6669726d6174696f6e206e6f7420726571756972604482015261195960f21b6064820152608490fd5b90600182811c921680156135e2575b60208310146135ce57565b634e487b7160e01b5f52602260045260245ffd5b91607f16916135c3565b9060405191825f8254926135ff846135b4565b80845293600181169081156136685750600114613624575b50612bea925003836129d7565b90505f9291925260205f20905f915b81831061364c575050906020612bea928201015f613617565b6020919350806001915483858901015201910190918492613633565b905060209250612bea94915060ff191682840152151560051b8201015f613617565b9067ffffffffffffffff60401b82549160401b169067ffffffffffffffff60401b1916179055565b60043560ff811680910361206e57815260243560ff811680910361206e57602082015260443560ff811680910361206e57604082015260643560ff811680910361206e57606082015260843560ff811680910361206e57608082015260a43580151580910361206e5760a082015260c43563ffffffff811680910361206e5760c082015260e43563ffffffff811680910361206e5760e08201526101043563ffffffff811680910361206e576101008201526101243563ffffffff811680910361206e576101208201526101443563ffffffff811680910361206e576101408201526101643563ffffffff811680910361206e576101608201526101843561ffff811680910361206e576101808201526101a43561ffff811680910361206e576101a08201526101c43561ffff811680910361206e576101c08201526101e43561ffff811680910361206e576101e0820152610204359063ffffffff821680920361206e576102000152565b5f198114612b5d5760010190565b519060ff8216820361206e57565b519063ffffffff8216820361206e57565b519061ffff8216820361206e57565b613863816148dc565b805460ff8116906007821015806127d657821591821561422b57335f526008602052600460405f20541615614218575b6001600160401b03600486015460401c1680421061420657506001549082600260ff8460081c166138c3816127cc565b1490816141d6575b506141c75760ff8160181c169160ff8260201c16936127d6578490816141aa575b5061416f575b506138fc86612a22565b919081811061415a57505081811061414557505080614132575b61020061ff001984541617835561392e600554612b51565b60055561393c60065461381e565b60065533847f9c85b616f29fca57a17eafe71cf9ff82ffef41766e2cf01ea7f8f7878dd3ec245f80a315613a56575060018060a01b03600182015416906002810191600360018060a01b0384541692019382828654927feb669e0a01fff40cbe45de851f42334fe70673136abfd91399651b7ae199a07b6020604051868152a481613a1e575050905491545f92839250829182916001600160a01b03165af13d15613a19573d6139eb81612c61565b906139f960405192836129d7565b81525f60203d92013e5b15613a0a57565b633d2cec6f60e21b5f5260045ffd5b613a03565b90919250612bea93506040519263a9059cbb60e01b60208501526024840152604483015260448252613a516064836129d7565b61563e565b60018103613bc05750613a6c91506005016135ec565b6102208180518101031261206e57612bea90613bb561022060405192613a918461299f565b613a9d6020820161382c565b8452613aab6040820161382c565b6020850152613abc6060820161382c565b6040850152613acd6080820161382c565b6060850152613ade60a0820161382c565b6080850152613aef60c08201612ba2565b60a0850152613b0060e0820161383a565b60c0850152613b12610100820161383a565b60e0850152613b24610120820161383a565b610100850152613b37610140820161383a565b610120850152613b4a610160820161383a565b610140850152613b5d610180820161383a565b610160850152613b706101a0820161384b565b610180850152613b836101c0820161384b565b6101a0850152613b966101e0820161384b565b6101c0850152613ba9610200820161384b565b6101e08501520161383a565b6102008201526145c5565b60028103613c3d575060028101546001600160a01b03169150613be5906005016135ec565b60a08180518101031261206e57612bea9160a060405192613c0584612970565b613c1160208201612ba2565b8452604081015160208501526060810151604085015260808101516060850152015160808301526143f9565b60038103613c7057506002810154600390910154613c68925060ff16906001600160a01b031661426b565b612bea61550f565b60048103613d8a5750905060ff600360018060a01b0360028401541692015416600681109081156127d657825f52600e60205260405f209160ff8354169060068210156127d6578115613d65575b60018314613d17575b156127d657613d14827f155590dadece0bf62c36a0e5cabcce51e1280ff0e8f6cc53c14d8900e0f43daf9460409460ff19825416179055613d0a8451809461281a565b602083019061281a565ba2565b613d60613d3863ffffffff60025460701c166001600160401b034216612c2e565b855467ffffffffffffffff60481b191660489190911b67ffffffffffffffff60481b16178555565b613cc7565b835468ffffffffffffffff0019164260081b68ffffffffffffffff0016178455613cbe565b600503613e325760039150015460ff8116613da4816127cc565b613dad816127cc565b60ff60015460081c16613dbf816127cc565b811015613e235760015461ff008060ff8360081c1694613dde856127cc565b60081b16169061ff00191617600155613df6826127cc565b613dff816127cc565b60405191640e4cad8c2f60db1b83525f5160206157295f395f51905f5260203394a4565b63710a5e0f60e01b5f5260045ffd5b60028101546001600160a01b031690613e4d906005016135ec565b60208180518101031261206e57602001516001600160a01b038116929083900361206e57815f52600860205260ff60405f205416801561412357835f52600860205260ff60405f2054166125c057821561411457613eaa5f614bfa565b825f5260086020528260ff60405f205416801580809161410d575b15614007575060405f5160206157095f395f51905f5291613ee58461332a565b600754845f526009602052825f20555b613eff5f826153c3565b613f095f8261540d565b613f135f82615461565b613f1d5f826154b7565b835f526008602052815f2060ff19815416905581519081525f6020820152a2613f46818561426b565b613f4e61550f565b60ff60015460081c16613f60816127cc565b15613f93575b60207f607a6564adeb3eec35d536d94fa031ad71407e7510a784e53f7bf36d44f3bc4191604051908152a4565b600154600881901c60ff169061ff00191661010017600155613fb4816127cc565b7f607a6564adeb3eec35d536d94fa031ad71407e7510a784e53f7bf36d44f3bc4191602091600160405191677265636f7665727960c01b83525f5160206157295f395f51905f52853394a4915050613f66565b1580614105575b614029575b60405f5160206157095f395f51905f5291613ef5565b905f52600960205260405f20545f198101818111612b5d576007545f198101908111612b5d578082036140c7575b5050506007549081156140b35760405f5160206157095f395f51905f529186935f190161409b614086826132fd565b81549060018060a01b039060031b1b19169055565b600755835f5260096020525f82812055915050614013565b634e487b7160e01b5f52603160045260245ffd5b61334f916140d76140f1926132fd565b905460039190911b1c6001600160a01b03169283916132fd565b5f52600960205260405f20555f8080614057565b50600161400e565b505f613ec5565b63d92e233d60e01b5f5260045ffd5b63148fe22160e11b5f5260045ffd5b61413b8361500b565b6139165750505050565b63d495223560e01b5f5260045260245260445ffd5b635ab4617560e01b5f5260045260245260445ffd5b61418260ff809260101c1661306f612c89565b5091168281116141a1575b5060ff16828111156138f25791505f6138f2565b915060ff61418d565b63ffffffff915060101c1663ffffffff8260401c1614155f6138ec565b6372b89e9760e11b5f5260045ffd5b90506127d6578384156141ea575b836138cb565b5060ff8160281c1680156141e457505f925060058514156141e4565b6384280dad60e01b5f5260045260245ffd5b63e84c979760e01b5f526004805260245ffd5b5f9150600684036142435761423e614844565b613893565b335f526008602052600560405f2054166138935763e84c979760e01b5f52600560045260245ffd5b6001600160a01b0381169182156141145761428581614bfa565b825f52600860205260ff60405f2054169182158080916143ed575b1561431f57509160ff6040926142c35f5160206157095f395f51905f529561332a565b600754865f526009602052845f20555b6142dd81846153c3565b6142e7818461540d565b6142f18184615461565b6142fb81846154b7565b855f526008602052835f2082821683198254161790558351928352166020820152a2565b905015806143e2575b614346575b5f5160206157095f395f51905f529160ff6040926142d3565b825f52600960205260405f20545f198101818111612b5d576007545f198101908111612b5d578082036143be575b5050506007549182156140b35760ff6040925f5160206157095f395f51905f52945f19016143a4614086826132fd565b600755855f5260096020525f84812055925050915061432d565b61334f916140d76143ce926132fd565b5f52600960205260405f20555f8080614374565b5060ff811615614328565b5060ff831615156142a0565b90736baa1a24b55796d8346748ee3565e40d844ba576803b1561206e57604051631c71447d60e01b81525f8160a48183956144616004830189608080918051151584526020810151602085015260408101516040850152606081015160608501520151910152565b5af48015612063576145b2575b506001600160a01b038316808252600c602052604082205490939060ff1615614532575b507f9d182ff993316b1c662980e27e42d42fae0bd158f1bed04e9988ec637fea8da4916040828560a09452600d602052208151151560ff8019835416911617815560208201516001820155604082015160028201556060820151600382015560046080830151910155613d146040518092608080918051151584526020810151602085015260408101516040850152606081015160608501520151910152565b838252600c60205260408220805460ff19166001179055600b54600160401b81101561459e5760a0926145957f9d182ff993316b1c662980e27e42d42fae0bd158f1bed04e9988ec637fea8da4959361334f84600160409601600b55600b613315565b92505091614492565b634e487b7160e01b83526041600452602483fd5b6145be91505f906129d7565b5f5f61446e565b736baa1a24b55796d8346748ee3565e40d844ba576803b1561206e5760405163065397d760e21b81525f816102248183956146036004830189612887565b5af4801561206357614831575b50815160208084015160408501516060860151608087015160a088015160c089015160e08a01516101008b01516101208c01516101408d01516101608e01516101808f015160089b909b1b61ff001660ff9d909d169c909c176010998a1b62ff0000161760189890981b63ff0000001697909717958a1b64ff00000000169590951793151560281b60ff60281b1693909317603092831b69ffffffff0000000000009081169190911760509290921b63ffffffff60501b169190911760709390931b63ffffffff60701b169290921760909390931b63ffffffff60901b169290921760b09390931b63ffffffff60b01b169290921760d09690961b63ffffffff60d01b169590951760f09390931b6001600160f01b031916929092176002556101a0860151600380546101c08901516101e08a01516102008b015169ffffffffffffffffffff199390931661ffff959095169490941790851b63ffff000016179290951b65ffff0000000016919091179390941b9091169190911790915560018054929183901c63ffffffff9081169091019190821161481d575063ffffffff7f1a734774792c149a91deb03650b2a2bc51de7bbab7262857ace6fb1699b430419265ffffffff00006102409360101b169065ffffffff0000191617806001556147f861550f565b614815604051956001600160401b03421687526020870190612887565b60101c1692a2565b634e487b7160e01b81526011600452602490fd5b61483d91505f906129d7565b5f5f614610565b335f526008602052600960405f2054161561485b57565b63e84c979760e01b5f52600960045260245ffd5b335f526008602052600160405f2054161561488657565b63e84c979760e01b5f52600160045260245ffd5b335f526008602052600360405f205416156148b157565b63e84c979760e01b5f52600360045260245ffd5b335f526008602052600860405f205416156134a757565b80158015614953575b614944575f52601260205260405f209060ff825460081c1660058110156127d657600103614935576001600160401b03600483015460801c16421161492657565b6328a7237960e01b5f5260045ffd5b633ee3282d60e11b5f5260045ffd5b6302eae9e560e51b5f5260045ffd5b5060045481116148e5565b156149725763ffffffff60025460901c1690565b5f90565b906080606083015192015190600160ff815460081c16614995816127cc565b1461499c57565b826149bb575b816149a957565b9060011c806149b85750600190565b90565b9160011c806149ce575060015b916149a2565b6149c8565b6001600160a01b03168015614a40576020602491604051928380926370a0823160e01b82523060048301525afa908115612063575f91614a11575090565b90506020813d602011614a38575b81614a2c602093836129d7565b8101031261206e575190565b3d9150614a1f565b504790565b906006811015806127d657600282148015614aa7575b614a9f576127d6576001149081614a76575b506132ed575f90565b905060068110156127d65760048114908115614a94575b505f614a6d565b60059150145f614a8d565b505050600190565b50505f60038214614a5b565b9190614abe816127cc565b8015614b2e57600190614ad0816127cc565b14614b135760ff6040830151169060a083015115155f14614b055763ffffffff60e060ff608086015116945b01511691929190565b63ffffffff60e05f94614afc565b9063ffffffff60c060ff6020840151169201511690915f9190565b50905160ff16905f908190565b6001600160a01b03165f818152600e60209081526040909120805468ffffffffffffffffff191642600881901b68ffffffffffffffff00169190911760011782557fa834c3aabec8913fbfd0591ec9c2a7494c727a364cc1f231c948c0f9564b5d0692916001600160401b0391614bec90614bc490841663ffffffff60025460701c1690612c2e565b825467ffffffffffffffff60481b191660489190911b67ffffffffffffffff60481b16178255565b5460481c16604051908152a2565b600f60ff8216116125b1576008811615159081614c19575b506125b157565b600791501615155f614c12565b60ff8119831616614c515760081615159081614c4557506132ed575f90565b6008915016155f614a6d565b5050600190565b614c6190614976565b509081151580614cca575b614cb557505061ffff60035460101c169081151580614ca8575b614c8e575050565b61ffff90630a8a293d60e01b5f521660045260245260445ffd5b508161ffff821611614c86565b634fa5781f60e11b5f5260045260245260445ffd5b50818111614c6c565b9290949391614ce360045461381e565b958660045586614cf460055461381e565b6005555f52601260205260405f209460078110156127d65785546bffffffffffffffffffffffff61ffff1990911660ff929092169190911761010017163360601b6001600160601b0319161785556002850180546001600160a01b0319166001600160a01b039092169190911790556003840155805160058401916001600160401b03821161298b578190614d8984546135b4565b601f8111614ec8575b50602090601f8311600114614e65575f92614e5a575b50508160011b915f199060031b1c19161790555b60ff60281b825491151560281b169060ff60281b191617815560015463ffffffff60401b82549160301b169063ffffffff60401b1916178155614e3260046001600160401b0342169201916001600160401b0381166001600160401b031984541617835563ffffffff60025460d01c1690612c2e565b815467ffffffffffffffff60801b191660809190911b67ffffffffffffffff60801b16179055565b015190505f80614da8565b5f8581528281209350601f198516905b818110614eb05750908460019594939210614e98575b505050811b019055614dbc565b01515f1960f88460031b161c191690555f8080614e8b565b92936020600181928786015181550195019301614e75565b909150835f5260205f20601f840160051c81019160208510614f0e575b90601f859493920160051c01905b818110614f005750614d92565b5f8155849350600101614ef3565b9091508190614ee5565b815460ff81169260018060a01b0360018201541660018060a01b0360028301541691600381015493600460ff8260101c169201549460078810156127d6577fcf5d862ed732f5aec4f98da191cd4e962a57abdc7bab078c436b0517229d388f95615006936001600160401b0392604051978897885260208801526040870152614fa0816127cc565b606086015261ffff8360301c16608086015260ff8360181c1660a086015260ff8360201c1660c0860152818160401c1660e086015260801c1661010084015260ff8160281c16151561012084015261016061014084015260601c96610160830190612863565b0390a4565b60018060a01b0360018201541690600381015491805f52600d60205260405f20916040519261503984612970565b600460ff8254161591821586526001810154602087015260028101546040870152600381015460608701520154608085015261538c57600201546001600160a01b03165f818152600e60205260409020805490939160ff82169060068210156127d65760058214613226575060011480615377575b61535857506150bc826149d3565b90818511615341576150e1906150dc6150d58488615696565b8783614c58565b614976565b919050825f52600f60205260405f20916001600160401b0361510581855416612c0e565b1642101561531c575b80151580615306575b6152e3575060035461ffff8160201c166151b5575b505060010161513c848254612c7c565b90555f52601060205260405f20615154838254612c7c565b905580549060016001600160401b038360881c1601906001600160401b038211612b5d5767ffffffffffffffff60881b1990921660889190911b67ffffffffffffffff60881b16178155600101805490916151ae91612c7c565b9055600190565b6151d86001600160401b039163ffffffff83865460401c169160301c1690612c2e565b164210156152bf575b506127106151fd600283015461ffff60035460201c1690612c4e565b0460038201908154816152108883612c7c565b1161522f57505090816152268660019454612c7c565b9055905f61512c565b9091507f72f5745158d621f0076c165a1d23a925d0b2f8c5b5fb643933649247142d156d945060609395925060405192835260208301526040820152a2600154600881901c60ff169061ff0019166102001760015561528d816127cc565b6002604051916e31b4b931bab4ba16b13932b0b5b2b960891b83525f5160206157295f395f51905f5260203394a45f90565b6152d26001600160401b0342168361368a565b60028201555f60038201555f6151e1565b6152f1866001850154612c7c565b6398b5494560e01b5f5260045260245260445ffd5b5080615316876001860154612c7c565b11615117565b825467ffffffffffffffff1916426001600160401b03161783555f600184015561510e565b508363cf47918160e01b5f5260045260245260445ffd5b6001600160401b03906384280dad60e01b5f5260481c1660045260245ffd5b506001600160401b038160481c1642106150ae565b5063a6e7da2360e01b5f5260045260245ffd5b60ff5f199116019060ff8211612b5d57565b60ff60019116019060ff8211612b5d57565b6001918216151591161515811461540a57156153f65760ff6153e881600a54166153b1565b1660ff19600a541617600a55565b60ff61540581600a541661539f565b6153e8565b50565b6002918216151591161515811461540a571561544b5761543460ff600a5460081c166153b1565b61ff00600a549160081b169061ff00191617600a55565b61545c60ff600a5460081c1661539f565b615434565b6004918216151591161515811461540a57156154a15761548860ff600a5460101c166153b1565b62ff0000600a549160101b169062ff0000191617600a55565b6154b260ff600a5460101c1661539f565b615488565b6008918216151591161515811461540a57156154f9576154de60ff600a5460181c166153b1565b63ff000000600a549160181b169063ff000000191617600a55565b61550a60ff600a5460181c1661539f565b6154de565b600a5460ff81166001811061560d57600160ff8360101c16106155d9576002549060ff8260181c161161559d5760ff8160101c1660ff8360081c16106155635760ff809160201c169160181c16106108b457565b604051637417de8f60e01b8152602060048201526011602482015270185c1c1c9bdd985b1cd0dc9a5d1a58d85b607a1b6044820152606490fd5b604051637417de8f60e01b815260206004820152601360248201527219dbdd995c9b985b98d9551a1c995cda1bdb19606a1b6044820152606490fd5b604051637417de8f60e01b815260206004820152600b60248201526a37379032bc32b1baba37b960a91b6044820152606490fd5b604051637417de8f60e01b815260206004820152600860248201526737379037bbb732b960c11b6044820152606490fd5b905f602091828151910182855af115612063575f513d61568d57506001600160a01b0381163b155b61566d5750565b635274afe760e01b5f9081526001600160a01b0391909116600452602490fd5b60011415615666565b9081156156ef5780158080156156e5575b6156dc57612710830292830461271003612b5d576156c85761ffff91041690565b634e487b7160e01b5f52601260045260245ffd5b50505061271090565b50818310156156a7565b50505f90565b6156fe816127cc565b156132ed5760029056fe5d3de0fa15f594659e83f792fff19b5062cbc430ea0e9cf465377cdfaab8845467ac1adbe98dcfa9ea51aab9f87f1ae3ca912bf37e541afd86007c5bde3e5311", - "nonce": "0x16b", - "chainId": "0x2328" - }, - "additionalContracts": [], - "isFixedGasLimit": false - }, - { - "hash": "0xb810c6065c9e9cfe719f60acbbb547d2cab7a766fc72b83a3183924ed7546342", - "transactionType": "CREATE", - "contractName": "SkurFactory", - "contractAddress": "0x018c12ba8b085da81e434b9e4001e07cf48764e9", - "function": null, - "arguments": [ - "0xE21C7ad3a56D187848F9915bb43f88820278DeA3" - ], - "transaction": { - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": null, - "gas": "0xb2b99", - "value": "0x0", - "input": "0x60a03461008e57601f6109c938819003918201601f19168301916001600160401b038311848410176100925780849260209460405283398101031261008e57516001600160a01b038116810361008e57803b1561007f5760805260405161092290816100a782396080518181816101fc0152818161033401526107e90152f35b63c99547ad60e01b5f5260045ffd5b5f80fd5b634e487b7160e01b5f52604160045260245ffdfe6080806040526004361015610012575f80fd5b5f905f3560e01c908163089ec70e14610783575080635492a6ec1461022b5780635c60da1b146101e6578063652b9b41146101a75780636cc811f8146100e05780638c13a4531461008a5763a7c6a1001461006b575f80fd5b3461008757806003193601126100875760209054604051908152f35b80fd5b5034610087576020366003190112610087576004359080548210156100cc5780805260208082209092015460405160039290921b1c6001600160a01b03168152f35b634e487b7160e01b81526032600452602490fd5b5034610087576020366003190112610087576001600160a01b0361010261084c565b168152600260205260408120604051908160208254918281520190819285526020852090855b818110610188575050508261013e910383610893565b604051928392602084019060208552518091526040840192915b818110610166575050500390f35b82516001600160a01b0316845285945060209384019390920191600101610158565b82546001600160a01b0316845260209093019260019283019201610128565b50346100875760203660031901126100875760209060ff906040906001600160a01b036101d261084c565b168152600184522054166040519015158152f35b50346100875780600319360112610087576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b34610468576102c03660031901126104685760043560243567ffffffffffffffff811161046857610260903690600401610862565b919060443567ffffffffffffffff811161046857610282903690600401610862565b9390610220366063190112610468576102843567ffffffffffffffff8111610468576102b2903690600401610862565b6102a435929167ffffffffffffffff841161046857366023850112156104685783600401359267ffffffffffffffff84116104685736602460a0860287010111610468576040805133602082019081529181018a905261031f81606081015b03601f198101835282610893565b5190206e5af43d82803e903d91602b57fd5bf37f0000000000000000000000000000000000000000000000000000000000000000763d602d80600a3d3981f3363d3d373d3d3d363d7300000062ffffff8260881c16175f5260781b1760205260018060a01b0390603760095ff51698891561077457895f52600160205260405f20600160ff198254161790555f54600160401b811015610454578060016103c992015f555f6108b5565b81546001600160a01b0360039290921b91821b1916908c901b1790555f5b8781101561046c57600581901b8901356001600160a01b03811690819003610468575f52600260205260405f20908154600160401b81101561045457610435816001948f93860181556108b5565b819291549060031b91821b91858060a01b03901b1916179055016103e7565b634e487b7160e01b5f52604160045260245ffd5b5f80fd5b50899697895f54905f19820191821161076057604051908152897fc18dea5f6b30c3fd606e8e38832933e2fa57407dd77800aad0658505ee94df9860203393a4873b1561046857604051632d8a98d960e01b81526102a06004820152986020916104db916102a48c01916108de565b8981036003190160248b01528281520191905f905b80821061073b5750505060643560ff811680910361046857604488015260843560ff811680910361046857606488015260a43560ff811680910361046857608488015260c43560ff81168091036104685760a488015260e43560ff81168091036104685760c4880152610104358015158091036104685760e48801526101243563ffffffff8116809103610468576101048801526101443563ffffffff8116809103610468576101248801526101643563ffffffff8116809103610468576101448801526101843563ffffffff8116809103610468576101648801526101a43563ffffffff8116809103610468576101848801526101c43563ffffffff8116809103610468576101a48801526101e43561ffff8116809103610468576101c48801526102043561ffff8116809103610468576101e48801526102243561ffff8116809103610468576102048801526102443561ffff811680910361046857610224880152610264359063ffffffff82168092036104685761068d90889695946024946020946102448a0152600319898403016102648a01526108de565b85810360031901610284870152838152019201905f905b8082106106eb5750505090805f92038183855af19182156106e0576020926106d0575b50604051908152f35b5f6106da91610893565b5f6106c7565b6040513d5f823e3d90fd5b919350918335908115158092036104685760a081600193829352602087013560208201526040870135604082015260608701356060820152608087013560808201520194019201859392916106a4565b90919283359060ff8216809203610468576020816001938293520194019201906104f0565b634e487b7160e01b5f52601160045260245ffd5b63b06ebf3d60e01b5f5260045ffd5b346104685760403660031901126104685760209061079f61084c565b6001600160a01b03168282019081526024356020820152906107c48160408401610311565b519020604051903060388301526f5af43d82803e903d91602b57fd5bf3ff60248301527f00000000000000000000000000000000000000000000000000000000000000006014830152733d602d80600a3d3981f3363d3d373d3d3d363d73825260588201526037600c82012060788201526055604360018060a01b0392012016604051908152f35b600435906001600160a01b038216820361046857565b9181601f840112156104685782359167ffffffffffffffff8311610468576020808501948460051b01011161046857565b90601f8019910116810190811067ffffffffffffffff82111761045457604052565b80548210156108ca575f5260205f2001905f90565b634e487b7160e01b5f52603260045260245ffd5b916020908281520191905f905b8082106108f85750505090565b90919283359060018060a01b038216809203610468576020816001938293520194019201906108eb56000000000000000000000000e21c7ad3a56d187848f9915bb43f88820278dea3", - "nonce": "0x16c", - "chainId": "0x2328" - }, - "additionalContracts": [], - "isFixedGasLimit": false - }, - { - "hash": "0xef612541e68ecf1ad41453e65fe9df94a329a416436f769df8774bde63c3446c", - "transactionType": "CALL", - "contractName": "SkurFactory", - "contractAddress": "0x018c12ba8b085da81e434b9e4001e07cf48764e9", - "function": "createVault(bytes32,address[],uint8[],(uint8,uint8,uint8,uint8,uint8,bool,uint32,uint32,uint32,uint32,uint32,uint32,uint16,uint16,uint16,uint16,uint32),address[],(bool,uint256,uint256,uint256,uint256)[])", - "arguments": [ - "0xfb983e9dbbaa3ef70fb8bb403a3f4b14671884646e3181a121177cf14b3664eb", - "[0x4FEA262782b5BE34B18cf24b6aC026215899EEc0, 0xB6685D046E34A3260b9098aA6073CD7867e2de1C, 0xCec10C48B8d37b6A083000173e3415Cc6f86974F]", - "[7, 2, 8]", - "(1, 2, 2, 1, 1, true, 21600, 86400, 86400, 86400, 172800, 604800, 500, 2000, 5000, 1000, 86400)", - "[0x3e41424ADF6Bb69B6271ef9B0541ACD59A0d454a, 0x0000000000000000000000000000000000000000]", - "[(true, 5000000000, 25000000000, 100000000000, 100000000000), (true, 100000000000000000000, 1000000000000000000000, 0, 5000000000000000000000)]" - ], - "transaction": { - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": "0x018c12ba8b085da81e434b9e4001e07cf48764e9", - "gas": "0x149bca", - "value": "0x0", - "input": "0x5492a6ecfb983e9dbbaa3ef70fb8bb403a3f4b14671884646e3181a121177cf14b3664eb00000000000000000000000000000000000000000000000000000000000002c000000000000000000000000000000000000000000000000000000000000003400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000005460000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000000000000000015180000000000000000000000000000000000000000000000000000000000002a3000000000000000000000000000000000000000000000000000000000000093a8000000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000007d0000000000000000000000000000000000000000000000000000000000000138800000000000000000000000000000000000000000000000000000000000003e8000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000000000000000000000000000000030000000000000000000000004fea262782b5be34b18cf24b6ac026215899eec0000000000000000000000000b6685d046e34a3260b9098aa6073cd7867e2de1c000000000000000000000000cec10c48b8d37b6a083000173e3415cc6f86974f000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000020000000000000000000000003e41424adf6bb69b6271ef9b0541acd59a0d454a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000012a05f20000000000000000000000000000000000000000000000000000000005d21dba00000000000000000000000000000000000000000000000000000000174876e800000000000000000000000000000000000000000000000000000000174876e80000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000056bc75e2d6310000000000000000000000000000000000000000000000000003635c9adc5dea00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010f0cf064dd59200000", - "nonce": "0x16d", - "chainId": "0x2328" - }, - "additionalContracts": [ - { - "transactionType": "CREATE2", - "contractName": null, - "address": "0xcc31c7474267ca5600c2d530ebd7657a09c042dc", - "initCode": "0x3d602d80600a3d3981f3363d3d373d3d3d363d73e21c7ad3a56d187848f9915bb43f88820278dea35af43d82803e903d91602b57fd5bf3" - } - ], - "isFixedGasLimit": false - }, - { - "hash": "0x2de910f0c6733ab9cf2edd1dba34f0d8cacde340a5262e5f32ab20a487b78db9", - "transactionType": "CALL", - "contractName": "SkurTestUSD", - "contractAddress": "0x3e41424adf6bb69b6271ef9b0541acd59a0d454a", - "function": "mint(address,uint256)", - "arguments": [ - "0xCC31c7474267ca5600c2D530ebD7657A09c042Dc", - "250000000000" - ], - "transaction": { - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": "0x3e41424adf6bb69b6271ef9b0541acd59a0d454a", - "gas": "0x184fc", - "value": "0x0", - "input": "0x40c10f19000000000000000000000000cc31c7474267ca5600c2d530ebd7657a09c042dc0000000000000000000000000000000000000000000000000000003a35294400", - "nonce": "0x16e", - "chainId": "0x2328" - }, - "additionalContracts": [], - "isFixedGasLimit": false - }, - { - "hash": "0x63e02bb87ddcb55dc346f13fed3a4f3648901a22e54269bbfa08c739280e1979", - "transactionType": "CALL", - "contractName": null, - "contractAddress": "0xcc31c7474267ca5600c2d530ebd7657a09c042dc", - "function": null, - "arguments": null, - "transaction": { - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": "0xcc31c7474267ca5600c2d530ebd7657a09c042dc", - "gas": "0x10197", - "value": "0x1bc16d674ec80000", - "input": "0x", - "nonce": "0x16f", - "chainId": "0x2328" - }, - "additionalContracts": [], - "isFixedGasLimit": false - } - ], - "receipts": [ - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0xb53a1", - "logs": [], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "transactionHash": "0x5ec40a2bac9472f8b4d869940f032896d94240d509d887f4c34cd7b7691a2e76", - "transactionIndex": "0x0", - "blockHash": "0x9d9d8e38f8fef0d8e3b84527b9ba363fbda49a4dee860358878444244f06bd6a", - "blockNumber": "0x75aa9", - "gasUsed": "0xb53a1", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": null, - "contractAddress": "0x6baa1a24b55796d8346748ee3565e40d844ba576" - }, - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0x75981", - "logs": [], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "transactionHash": "0x1837db1649c1303b313f6d8ba6cb8df1c0aa2f840fd10ce133ae96aa6d460935", - "transactionIndex": "0x0", - "blockHash": "0x3189719aed5d3b21870b8d30c87a6f3b154cf561256cdd92814321647f0eb991", - "blockNumber": "0x75aab", - "gasUsed": "0x75981", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": null, - "contractAddress": "0x3e41424adf6bb69b6271ef9b0541acd59a0d454a" - }, - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0x4b3636", - "logs": [], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "transactionHash": "0x7009b1ae1b36af481edcaa8989b1243b9f1ff770649e7a5a27c0f6f522b28396", - "transactionIndex": "0x0", - "blockHash": "0xb81f91fec023c346589f72def43142e29ba9ec463523519e5710a3e78943d0b8", - "blockNumber": "0x75aad", - "gasUsed": "0x4b3636", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": null, - "contractAddress": "0xe21c7ad3a56d187848f9915bb43f88820278dea3" - }, - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0x897b1", - "logs": [], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "transactionHash": "0xb810c6065c9e9cfe719f60acbbb547d2cab7a766fc72b83a3183924ed7546342", - "transactionIndex": "0x0", - "blockHash": "0x001cf2924909c2331e1dff4384be3331d5864e1a03266824264376ec85d239ce", - "blockNumber": "0x75aaf", - "gasUsed": "0x897b1", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": null, - "contractAddress": "0x018c12ba8b085da81e434b9e4001e07cf48764e9" - }, - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0xe1761", - "logs": [ - { - "address": "0x018c12ba8b085da81e434b9e4001e07cf48764e9", - "topics": [ - "0xc18dea5f6b30c3fd606e8e38832933e2fa57407dd77800aad0658505ee94df98", - "0x000000000000000000000000cc31c7474267ca5600c2d530ebd7657a09c042dc", - "0x0000000000000000000000004fea262782b5be34b18cf24b6ac026215899eec0", - "0x0000000000000000000000000000000000000000000000000000000000000000" - ], - "data": "0xfb983e9dbbaa3ef70fb8bb403a3f4b14671884646e3181a121177cf14b3664eb", - "blockHash": "0x83447b317c5cf5fcf9b1c286c6c091f71ce06206dd2f7149f42c3981cd4042c5", - "blockNumber": "0x75ab1", - "blockTimestamp": "0x6aa90de8", - "transactionHash": "0xef612541e68ecf1ad41453e65fe9df94a329a416436f769df8774bde63c3446c", - "transactionIndex": "0x0", - "logIndex": "0x0", - "removed": false - }, - { - "address": "0xcc31c7474267ca5600c2d530ebd7657a09c042dc", - "topics": [ - "0x5d3de0fa15f594659e83f792fff19b5062cbc430ea0e9cf465377cdfaab88454", - "0x0000000000000000000000004fea262782b5be34b18cf24b6ac026215899eec0" - ], - "data": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007", - "blockHash": "0x83447b317c5cf5fcf9b1c286c6c091f71ce06206dd2f7149f42c3981cd4042c5", - "blockNumber": "0x75ab1", - "blockTimestamp": "0x6aa90de8", - "transactionHash": "0xef612541e68ecf1ad41453e65fe9df94a329a416436f769df8774bde63c3446c", - "transactionIndex": "0x0", - "logIndex": "0x1", - "removed": false - }, - { - "address": "0xcc31c7474267ca5600c2d530ebd7657a09c042dc", - "topics": [ - "0x5d3de0fa15f594659e83f792fff19b5062cbc430ea0e9cf465377cdfaab88454", - "0x000000000000000000000000b6685d046e34a3260b9098aa6073cd7867e2de1c" - ], - "data": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002", - "blockHash": "0x83447b317c5cf5fcf9b1c286c6c091f71ce06206dd2f7149f42c3981cd4042c5", - "blockNumber": "0x75ab1", - "blockTimestamp": "0x6aa90de8", - "transactionHash": "0xef612541e68ecf1ad41453e65fe9df94a329a416436f769df8774bde63c3446c", - "transactionIndex": "0x0", - "logIndex": "0x2", - "removed": false - }, - { - "address": "0xcc31c7474267ca5600c2d530ebd7657a09c042dc", - "topics": [ - "0x5d3de0fa15f594659e83f792fff19b5062cbc430ea0e9cf465377cdfaab88454", - "0x000000000000000000000000cec10c48b8d37b6a083000173e3415cc6f86974f" - ], - "data": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008", - "blockHash": "0x83447b317c5cf5fcf9b1c286c6c091f71ce06206dd2f7149f42c3981cd4042c5", - "blockNumber": "0x75ab1", - "blockTimestamp": "0x6aa90de8", - "transactionHash": "0xef612541e68ecf1ad41453e65fe9df94a329a416436f769df8774bde63c3446c", - "transactionIndex": "0x0", - "logIndex": "0x3", - "removed": false - }, - { - "address": "0xcc31c7474267ca5600c2d530ebd7657a09c042dc", - "topics": [ - "0x9d182ff993316b1c662980e27e42d42fae0bd158f1bed04e9988ec637fea8da4", - "0x0000000000000000000000003e41424adf6bb69b6271ef9b0541acd59a0d454a" - ], - "data": "0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000012a05f20000000000000000000000000000000000000000000000000000000005d21dba00000000000000000000000000000000000000000000000000000000174876e800000000000000000000000000000000000000000000000000000000174876e800", - "blockHash": "0x83447b317c5cf5fcf9b1c286c6c091f71ce06206dd2f7149f42c3981cd4042c5", - "blockNumber": "0x75ab1", - "blockTimestamp": "0x6aa90de8", - "transactionHash": "0xef612541e68ecf1ad41453e65fe9df94a329a416436f769df8774bde63c3446c", - "transactionIndex": "0x0", - "logIndex": "0x4", - "removed": false - }, - { - "address": "0xcc31c7474267ca5600c2d530ebd7657a09c042dc", - "topics": [ - "0x9d182ff993316b1c662980e27e42d42fae0bd158f1bed04e9988ec637fea8da4", - "0x0000000000000000000000000000000000000000000000000000000000000000" - ], - "data": "0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000056bc75e2d6310000000000000000000000000000000000000000000000000003635c9adc5dea00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010f0cf064dd59200000", - "blockHash": "0x83447b317c5cf5fcf9b1c286c6c091f71ce06206dd2f7149f42c3981cd4042c5", - "blockNumber": "0x75ab1", - "blockTimestamp": "0x6aa90de8", - "transactionHash": "0xef612541e68ecf1ad41453e65fe9df94a329a416436f769df8774bde63c3446c", - "transactionIndex": "0x0", - "logIndex": "0x5", - "removed": false - }, - { - "address": "0xcc31c7474267ca5600c2d530ebd7657a09c042dc", - "topics": [ - "0x1a734774792c149a91deb03650b2a2bc51de7bbab7262857ace6fb1699b43041", - "0x0000000000000000000000000000000000000000000000000000000000000001" - ], - "data": "0x000000000000000000000000000000000000000000000000000000006aa90de80000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000005460000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000000000000000015180000000000000000000000000000000000000000000000000000000000002a3000000000000000000000000000000000000000000000000000000000000093a8000000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000007d0000000000000000000000000000000000000000000000000000000000000138800000000000000000000000000000000000000000000000000000000000003e80000000000000000000000000000000000000000000000000000000000015180", - "blockHash": "0x83447b317c5cf5fcf9b1c286c6c091f71ce06206dd2f7149f42c3981cd4042c5", - "blockNumber": "0x75ab1", - "blockTimestamp": "0x6aa90de8", - "transactionHash": "0xef612541e68ecf1ad41453e65fe9df94a329a416436f769df8774bde63c3446c", - "transactionIndex": "0x0", - "logIndex": "0x6", - "removed": false - }, - { - "address": "0xcc31c7474267ca5600c2d530ebd7657a09c042dc", - "topics": [ - "0x4488062cfafbbb2c1d440f13796e120c4522761f74ecade440ee8f31f1bfbee0" - ], - "data": "0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003", - "blockHash": "0x83447b317c5cf5fcf9b1c286c6c091f71ce06206dd2f7149f42c3981cd4042c5", - "blockNumber": "0x75ab1", - "blockTimestamp": "0x6aa90de8", - "transactionHash": "0xef612541e68ecf1ad41453e65fe9df94a329a416436f769df8774bde63c3446c", - "transactionIndex": "0x0", - "logIndex": "0x7", - "removed": false - } - ], - "logsBloom": "0x00100000000080000000000000000004080000000000000000000000000000000000040000020000000000040000000000000012010008000800080000040400000000040000000000000000000000800000000000040000000000000000000000000000020400000000000000000800080000000000000000000000000080020000800000080000000800000000000000002000000000000000000000000000000010800000000001000000000000000010000000008000000000000000000000000000000008000400000000000000000000000000000000000002000060000000000400000000000000000000000000000000000410000000400000000000", - "transactionHash": "0xef612541e68ecf1ad41453e65fe9df94a329a416436f769df8774bde63c3446c", - "transactionIndex": "0x0", - "blockHash": "0x83447b317c5cf5fcf9b1c286c6c091f71ce06206dd2f7149f42c3981cd4042c5", - "blockNumber": "0x75ab1", - "gasUsed": "0xe1761", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": "0x018c12ba8b085da81e434b9e4001e07cf48764e9", - "contractAddress": null - }, - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0x109f9", - "logs": [ - { - "address": "0x3e41424adf6bb69b6271ef9b0541acd59a0d454a", - "topics": [ - "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x000000000000000000000000cc31c7474267ca5600c2d530ebd7657a09c042dc" - ], - "data": "0x0000000000000000000000000000000000000000000000000000003a35294400", - "blockHash": "0x52755d37993fe83b34e2babbfbf9b31b43a63a86258b1363ad387056c4a51d1b", - "blockNumber": "0x75ab3", - "blockTimestamp": "0x6aa90dee", - "transactionHash": "0x2de910f0c6733ab9cf2edd1dba34f0d8cacde340a5262e5f32ab20a487b78db9", - "transactionIndex": "0x0", - "logIndex": "0x0", - "removed": false - } - ], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000010000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000020000000000000000000800000000000000000000000012000000004100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000400000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000", - "transactionHash": "0x2de910f0c6733ab9cf2edd1dba34f0d8cacde340a5262e5f32ab20a487b78db9", - "transactionIndex": "0x0", - "blockHash": "0x52755d37993fe83b34e2babbfbf9b31b43a63a86258b1363ad387056c4a51d1b", - "blockNumber": "0x75ab3", - "gasUsed": "0x109f9", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": "0x3e41424adf6bb69b6271ef9b0541acd59a0d454a", - "contractAddress": null - }, - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0xba7f", - "logs": [ - { - "address": "0xcc31c7474267ca5600c2d530ebd7657a09c042dc", - "topics": [ - "0x5548c837ab068cf56a2c2479df0882a4922fd203edb7517321831d95078c5f62", - "0x0000000000000000000000004fea262782b5be34b18cf24b6ac026215899eec0", - "0x0000000000000000000000000000000000000000000000000000000000000000" - ], - "data": "0x0000000000000000000000000000000000000000000000001bc16d674ec80000", - "blockHash": "0x0bc9c246a511471da7a287d656ab83e66fc9c22a798752f1f5815fe784a62a4a", - "blockNumber": "0x75ab5", - "blockTimestamp": "0x6aa90df7", - "transactionHash": "0x63e02bb87ddcb55dc346f13fed3a4f3648901a22e54269bbfa08c739280e1979", - "transactionIndex": "0x0", - "logIndex": "0x0", - "removed": false - } - ], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000002000000080040000000000000040008000000000000000000000000000000000000000000000000000000000000020000000000000000000800000000000000000000000000000080020000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000", - "transactionHash": "0x63e02bb87ddcb55dc346f13fed3a4f3648901a22e54269bbfa08c739280e1979", - "transactionIndex": "0x0", - "blockHash": "0x0bc9c246a511471da7a287d656ab83e66fc9c22a798752f1f5815fe784a62a4a", - "blockNumber": "0x75ab5", - "gasUsed": "0xba7f", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": "0xcc31c7474267ca5600c2d530ebd7657a09c042dc", - "contractAddress": null - } - ], - "libraries": [ - "src/SkurPolicyLib.sol:SkurPolicyLib:0x6bAA1A24B55796d8346748EE3565e40d844bA576" - ], - "pending": [], - "returns": {}, - "timestamp": 1789464061730, - "chain": 9000, - "commit": "d25692a" -} \ No newline at end of file diff --git a/contracts/broadcast/Deploy.s.sol/9000/run-latest.json b/contracts/broadcast/Deploy.s.sol/9000/run-latest.json deleted file mode 100644 index 0168ef5..0000000 --- a/contracts/broadcast/Deploy.s.sol/9000/run-latest.json +++ /dev/null @@ -1,432 +0,0 @@ -{ - "transactions": [ - { - "hash": "0x5ec40a2bac9472f8b4d869940f032896d94240d509d887f4c34cd7b7691a2e76", - "transactionType": "CREATE", - "contractName": "SkurPolicyLib", - "contractAddress": "0x6baa1a24b55796d8346748ee3565e40d844ba576", - "function": null, - "arguments": null, - "transaction": { - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "gas": "0xeb984", - "input": "0x60808060405234601957610c75908161001e823930815050f35b5f80fdfe60806040526004361015610011575f80fd5b5f3560e01c8063099fe59914610660578063194e5f5c1461018b5780631c71447d146100b957637309439114610045575f80fd5b6101403660031901126100b55761005b366109c0565b60a03660a31901126100b55761006f61082f565b60a4359081151582036100b5576020926100ab92825260c4358483015260e4356040830152610104356060830152610124356080830152610bf7565b6040519015158152f35b5f80fd5b60a03660031901126100b5576100ce366109c0565b60208101516040820151106101525760608101518015159182610144575b82610136575b50506100fa57005b6040516353e95a6760e01b81526020600482015260136024820152720e0cae4a8f09ac2f0407c40c8c2d2d8f29ac2f606b1b6044820152606490fd5b6080015110905081806100f2565b6080810151151592506100ec565b6040516353e95a6760e01b815260206004820152601060248201526f0d8deee9ac2f0407c40d0d2ced09ac2f60831b6044820152606490fd5b6102203660031901126100b5576101a13661084f565b60ff8151161561062b576020810160ff81511660ff835116116105f55760ff80604084015116915116116105bb5760ff6060820151161561057f5760a0810151151580610570575b6105365760e0810163ffffffff81511663ffffffff60c084015116116104f0576101a0820161ffff81511680151590816104dd575b506104a65761271061ffff825116118015610492575b61045d576101c083019061ffff825116801515918261044e575b50506104115761ffff612710915116116103db576101e0820161271061ffff825116116103a6575161ffff16151580610390575b6103595763ffffffff9051169063ffffffff61012082015116828111610351575b5063ffffffff61014082015116828111610349575b5063ffffffff6101008201511682811161033f575b5061016063ffffffff91015116610e10820180921161032b57106102ed57005b6040516353e95a6760e01b81526020600482015260156024820152741c1c9bdc1bdcd85b151d1b081d1bdbc81cda1bdc9d605a1b6044820152606490fd5b634e487b7160e01b5f52601160045260245ffd5b91506101606102cd565b9150826102b8565b9150826102a3565b6040516353e95a6760e01b815260206004820152600e60248201526d656e76656c6f706557696e646f7760901b6044820152606490fd5b50610e1063ffffffff6102008401511610610282565b6040516353e95a6760e01b815260206004820152600c60248201526b656e76656c6f70652062707360a01b6044820152606490fd5b6040516353e95a6760e01b815260206004820152600d60248201526c68617264426c6f636b2062707360981b6044820152606490fd5b6040516353e95a6760e01b81526020600482015260146024820152731a185c99109b1bd8dac80f0818dc9a5d1a58d85b60621b6044820152606490fd5b5161ffff16119050848061024e565b6040516353e95a6760e01b815260206004820152600c60248201526b6578706f737572652062707360a01b6044820152606490fd5b5061271061ffff6101808501511611610234565b6040516353e95a6760e01b815260206004820152600e60248201526d32bc3837b9bab9329037b93232b960911b6044820152606490fd5b905061ffff61018085015116118461021e565b6040516353e95a6760e01b815260206004820152601960248201527f64656c6179437269746963616c203c2064656c617948696768000000000000006044820152606490fd5b6040516353e95a6760e01b815260206004820152601160248201527019dd585c991a585b951a1c995cda1bdb19607a1b6044820152606490fd5b5060ff608082015116156101e9565b6040516353e95a6760e01b815260206004820152601360248201527219dbdd995c9b985b98d9551a1c995cda1bdb19606a1b6044820152606490fd5b6040516353e95a6760e01b8152602060048201526011602482015270185c1c1c9bdd985b1cd0dc9a5d1a58d85b607a1b6044820152606490fd5b6040516353e95a6760e01b815260206004820152600d60248201526c0c2e0e0e4deecc2d8e690d2ced609b1b6044820152606490fd5b6040516353e95a6760e01b815260206004820152600c60248201526b617070726f76616c734c6f7760a01b6044820152606490fd5b6104403660031901126100b5576106763661084f565b610220366102231901126100b55761068c6107fa565b6102243560ff811681036100b55781526102443560ff811681036100b55760208201526102643560ff811681036100b55760408201526102843560ff811681036100b55760608201526102a43560ff811681036100b55760808201526102c43580151581036100b55760a08201526102e43563ffffffff811681036100b55760c08201526103043563ffffffff811681036100b55760e08201526103243563ffffffff811681036100b5576101008201526103443563ffffffff811681036100b5576101208201526103643563ffffffff811681036100b5576101408201526103843563ffffffff811681036100b5576101608201526103a43561ffff811681036100b5576101808201526103c43561ffff811681036100b5576101a08201526103e43561ffff811681036100b5576101c08201526104043561ffff811681036100b5576101e0820152610424359063ffffffff821682036100b5576020926100ab92610200830152610a05565b60405190610220820182811067ffffffffffffffff82111761081b57604052565b634e487b7160e01b5f52604160045260245ffd5b6040519060a0820182811067ffffffffffffffff82111761081b57604052565b6102209060031901126100b5576108646107fa565b9060043560ff811681036100b557825260243560ff811681036100b557602083015260443560ff811681036100b557604083015260643560ff811681036100b557606083015260843560ff811681036100b557608083015260a43580151581036100b55760a083015260c43563ffffffff811681036100b55760c083015260e43563ffffffff811681036100b55760e08301526101043563ffffffff811681036100b5576101008301526101243563ffffffff811681036100b5576101208301526101443563ffffffff811681036100b5576101408301526101643563ffffffff811681036100b5576101608301526101843561ffff811681036100b5576101808301526101a43561ffff811681036100b5576101a08301526101c43561ffff811681036100b5576101c08301526101e43561ffff811681036100b5576101e08301526102043563ffffffff811681036100b557610200830152565b60a09060031901126100b5576109d461082f565b9060043580151581036100b55782526024356020830152604435604083015260643560608301526084356080830152565b60ff82511660ff82511611610be45760ff60208301511660ff60208301511611610be45760ff60408301511660ff60408301511611610be45760ff60608301511660ff60608301511611610be45760ff60808301511660ff60808301511611610be45760a0810151151580610beb575b610be45763ffffffff60c08301511663ffffffff60c08301511611610be45763ffffffff60e08301511663ffffffff60e08301511611610be45763ffffffff6101008301511663ffffffff6101008301511611610be45763ffffffff6101208301511663ffffffff6101208301511611610be45763ffffffff6101408301511663ffffffff6101408301511611610be45763ffffffff6101608301511663ffffffff6101608301511610610be457610b4061ffff6101808301511661ffff6101808501511690610c5e565b610be457610b6161ffff6101a08301511661ffff6101a08501511690610c5e565b610be457610b8261ffff6101c08301511661ffff6101c08501511690610c5e565b610be45761ffff6101e082015116610ba361ffff6101e08501511682610c5e565b610bdc5715159182610bbf575b5050610bba575f90565b600190565b63ffffffff80929350610200809201511692015116115f80610bb0565b505050600190565b5050600190565b5060a082015115610a75565b9081511580610c54575b610be4576020810151602083015110610be4576040810151604083015110610be457610c366060830151606083015190610c5e565b610be457608080610c4c93015191015190610c5e565b610bba575f90565b5080511515610c01565b908115610c6f578015610be4571190565b50505f9056", - "nonce": "0x169", - "chainId": "0x2328" - }, - "additionalContracts": [], - "isFixedGasLimit": false - }, - { - "hash": "0x1837db1649c1303b313f6d8ba6cb8df1c0aa2f840fd10ce133ae96aa6d460935", - "transactionType": "CREATE", - "contractName": "SkurTestUSD", - "contractAddress": "0x3e41424adf6bb69b6271ef9b0541acd59a0d454a", - "function": null, - "arguments": null, - "transaction": { - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": null, - "gas": "0x98df4", - "value": "0x0", - "input": "0x60806040523461031757604080519081016001600160401b0381118282101761022a576040908152600d82526c14dadd5c8815195cdd081554d1609a1b602083015280519081016001600160401b0381118282101761022a5760405260048152631cd554d160e21b602082015281516001600160401b03811161022a57600354600181811c9116801561030d575b602082101461020c57601f81116102aa575b50602092601f821160011461024957928192935f9261023e575b50508160011b915f199060031b1c1916176003555b80516001600160401b03811161022a57600454600181811c91168015610220575b602082101461020c57601f81116101a9575b50602091601f8211600114610149579181925f9261013e575b50508160011b915f199060031b1c1916176004555b6040516106b5908161031c8239f35b015190505f8061011a565b601f1982169260045f52805f20915f5b85811061019157508360019510610179575b505050811b0160045561012f565b01515f1960f88460031b161c191690555f808061016b565b91926020600181928685015181550194019201610159565b60045f527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b601f830160051c81019160208410610202575b601f0160051c01905b8181106101f75750610101565b5f81556001016101ea565b90915081906101e1565b634e487b7160e01b5f52602260045260245ffd5b90607f16906100ef565b634e487b7160e01b5f52604160045260245ffd5b015190505f806100b9565b601f1982169360035f52805f20915f5b868110610292575083600195961061027a575b505050811b016003556100ce565b01515f1960f88460031b161c191690555f808061026c565b91926020600181928685015181550194019201610259565b60035f527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b601f830160051c81019160208410610303575b601f0160051c01905b8181106102f8575061009f565b5f81556001016102eb565b90915081906102e2565b90607f169061008d565b5f80fdfe6080806040526004361015610012575f80fd5b5f3560e01c90816306fdde03146104e657508063095ea7b31461046457806318160ddd1461044757806323b872dd14610367578063313ce5671461034c57806340c10f191461028c57806370a082311461025557806395d89b411461013a578063a9059cbb14610109578063dd62ed3e146100b95763f0292a0314610095575f80fd5b346100b5575f3660031901126100b55760206040516509184e72a0008152f35b5f80fd5b346100b55760403660031901126100b5576100d26105df565b6100da6105f5565b6001600160a01b039182165f908152600160209081526040808320949093168252928352819020549051908152f35b346100b55760403660031901126100b55761012f6101256105df565b602435903361060b565b602060405160018152f35b346100b5575f3660031901126100b5576040515f6004548060011c9060018116801561024b575b6020831081146102375782855290811561021b57506001146101c6575b50819003601f01601f191681019067ffffffffffffffff8211818310176101b2576101ae829182604052826105b5565b0390f35b634e487b7160e01b5f52604160045260245ffd5b905060045f527f8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b5f905b8282106102055750602091508201018261017e565b60018160209254838588010152019101906101f0565b90506020925060ff191682840152151560051b8201018261017e565b634e487b7160e01b5f52602260045260245ffd5b91607f1691610161565b346100b55760203660031901126100b5576001600160a01b036102766105df565b165f525f602052602060405f2054604051908152f35b346100b55760403660031901126100b5576102a56105df565b602435906509184e72a000821161033d576001600160a01b031690811561032a57600254908082018092116103165760207fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef915f9360025584845283825260408420818154019055604051908152a3005b634e487b7160e01b5f52601160045260245ffd5b63ec442f0560e01b5f525f60045260245ffd5b638b01e07b60e01b5f5260045ffd5b346100b5575f3660031901126100b557602060405160068152f35b346100b55760603660031901126100b5576103806105df565b6103886105f5565b6001600160a01b0382165f8181526001602081815260408084203385529091529091205491936044359392909181016103c7575b5061012f935061060b565b83811061042c5784156104195733156104065761012f945f52600160205260405f2060018060a01b0333165f526020528360405f2091039055846103bc565b634a1406b160e11b5f525f60045260245ffd5b63e602df0560e01b5f525f60045260245ffd5b8390637dc7a0d960e11b5f523360045260245260445260645ffd5b346100b5575f3660031901126100b5576020600254604051908152f35b346100b55760403660031901126100b55761047d6105df565b602435903315610419576001600160a01b031690811561040657335f52600160205260405f20825f526020528060405f20556040519081527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560203392a3602060405160018152f35b346100b5575f3660031901126100b5575f6003548060011c906001811680156105ab575b6020831081146102375782855290811561021b57506001146105565750819003601f01601f191681019067ffffffffffffffff8211818310176101b2576101ae829182604052826105b5565b905060035f527fc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b5f905b8282106105955750602091508201018261017e565b6001816020925483858801015201910190610580565b91607f169161050a565b602060409281835280519182918282860152018484015e5f828201840152601f01601f1916010190565b600435906001600160a01b03821682036100b557565b602435906001600160a01b03821682036100b557565b6001600160a01b03169081156106a2576001600160a01b031691821561032a57815f525f60205260405f205481811061068957817fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef92602092855f525f84520360405f2055845f525f825260405f20818154019055604051908152a3565b8263391434e360e21b5f5260045260245260445260645ffd5b634b637e8f60e11b5f525f60045260245ffd", - "nonce": "0x16a", - "chainId": "0x2328" - }, - "additionalContracts": [], - "isFixedGasLimit": false - }, - { - "hash": "0x7009b1ae1b36af481edcaa8989b1243b9f1ff770649e7a5a27c0f6f522b28396", - "transactionType": "CREATE", - "contractName": "SkurVault", - "contractAddress": "0xe21c7ad3a56d187848f9915bb43f88820278dea3", - "function": null, - "arguments": null, - "transaction": { - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": null, - "gas": "0x61c679", - "value": "0x0", - "input": "0x6080806040523460255760015f55600160ff1981541617600155615749908161002a8239f35b5f80fdfe6080806040526004361015610062575b50361561001a575f80fd5b5f8052601160205260405f20610031348254612c7c565b90555f6040513481527f5548c837ab068cf56a2c2479df0882a4922fd203edb7517321831d95078c5f6260203392a3005b5f905f3560e01c9081630db026221461278357508063128e9be6146127605780632014055d146126b55780632358d5a81461266c57806328dca5ba14612642578063295a5212146126165780632d8a98d9146121f55780632de94807146121ba5780633a5d0d6f1461210a57806340e58ee514612072578063460fc01914611ea05780635305548114611e6757806354387ad714611e4357806358355ead14611e1c57806362812a3914611d6257806367e4ac2c14611ce457806368d525c714611b7957806378f3c95a14611a5b5780638ec1f4ca14611a3757806397feb926146119745780639d8b9a49146118ec5780639eab5253146117bf578063a15a2f8b14611786578063a2380d351461175d578063a8badaa5146116a8578063b314c57014611580578063b52f393e14611555578063b759f95414611535578063bacd2e9714611517578063bf81bf431461142b578063c0555a0a1461139b578063c65df7941461135c578063c75e0df21461120c578063c7f758a814610f26578063ce1e462614610e85578063d0ee62c714610939578063da35c6641461091b578063db02466414610574578063ea70b4af14610556578063ee5248941461026f5763fe0d94c10361000f573461026c57602036600319011261026c57600281541461025d576002815561025660043561385a565b6001815580f35b633ee5aeb560e01b8152600490fd5b80fd5b503461026c5761022036600319011261026c5761028a61486f565b736baa1a24b55796d8346748ee3565e40d844ba576803b1561052a5760405163065397d760e21b815282906102c1600482016136b2565b818161022481865af4801561053657610541575b50506020610444916040519283809263099fe59960e01b825260025460ff8116600484015260ff8160081c16602484015260ff8160101c16604484015260ff8160181c16606484015260ff81871c16608484015260ff8160281c16151560a484015263ffffffff8160301c1660c484015263ffffffff8160501c1660e484015263ffffffff8160701c1661010484015263ffffffff8160901c1661012484015263ffffffff8160b01c1661014484015263ffffffff8160d01c1661016484015260f01c61018483015263ffffffff60035461ffff81166101a485015261ffff8160101c166101c485015261ffff81881c166101e485015260301c166102048301526103e361022483016136b2565b5af49081156105365782916104f8575b5060405190610404602083016136b2565b6102208252610415610240836129d7565b61041e8161495e565b9183600260ff60015460081c16610434816127cc565b1490816104f0575b816104e4575b506104d5579261045b6104cd9260209583806001614cd3565b8082526012855260408220600254815463ff00000090911664ffff0000001990911617602084901b64ff000000001617815590936104b7906104ae9063ffffffff905b166001600160401b034216612c2e565b6004830161368a565b604051916104c586846129d7565b825283614f18565b604051908152f35b6372b89e9760e11b8452600484fd5b9450600190505f610442565b83915061043c565b90506020813d60201161052e575b81610513602093836129d7565b8101031261052a5761052490612ba2565b5f6103f3565b5080fd5b3d9150610506565b6040513d84823e3d90fd5b8161054b916129d7565b61052a57815f6102d5565b503461026c578060031936011261026c576020600554604051908152f35b503461026c57604036600319011261026c5761058e6127a0565b906105976127b6565b61059f6148c5565b6001600160a01b031691821561090c576001600160a01b03811682526008602052604082205460ff16156108fd57828252600860205260ff6040832054166108ee5760ff60025460201c16156108b457604051926020840152602083526106076040846129d7565b61061260045461381e565b908160045561062260055461381e565b600590815582845260126020526040842080546bffffffffffffffffffff0000163360601b6001600160601b031916176101061781556002810180546001600160a01b0319166001600160a01b0390941693909317909255600382018490558451939082016001600160401b0385116108a05761069f81546135b4565b601f811161085b575b506020958690601f87116001146107f6579580916104cd969785926107eb575b50508160011b915f199060031b1c19161790555b60ff60281b19825416825560015463ffffffff60401b83549160301b169063ffffffff60401b191617825560046001600160401b03421692016001600160401b0383166001600160401b031982541617815561074363ffffffff60025460d01c1684612c2e565b815467ffffffffffffffff60801b191660809190911b67ffffffffffffffff60801b161790558381526012855260408120805463ff000000198116825560025464ffff0000001990911690871c60201b64ff0000000016178155916107c1906107b89063ffffffff60025460b01c1690612c2e565b6004840161368a565b50604051906107d16040836129d7565b60088252677265636f7665727960c01b8583015283614f18565b015190505f806106c8565b8284528184209190601f198816855b81811061084457509160019391896104cd999a941061082c575b505050811b0190556106dc565b01515f1960f88460031b161c191690555f808061081f565b92938a600181928786015181550195019301610805565b81835260208320601f870160051c81019160208810610896575b601f0160051c01905b81811061088b57506106a8565b83815560010161087e565b9091508190610875565b634e487b7160e01b82526041600452602482fd5b604051637417de8f60e01b815260206004820152601160248201527019dd585c991a585b951a1c995cda1bdb19607a1b6044820152606490fd5b630c8ce74760e11b8252600482fd5b63148fe22160e11b8252600482fd5b63d92e233d60e01b8252600482fd5b503461026c578060031936011261026c576020600454604051908152f35b503461026c57608036600319011261026c576109536127a0565b9061095c6127b6565b60443590606435906001600160401b038211610e815736602383011215610e81578160040135936001600160401b03851161026c57366024868501011161026c576109a561489a565b600260ff60015460081c166109b9816127cc565b14610e72576001600160a01b03821694851561090c578415610e63576109e0858489612d80565b9260018060a01b0388168352600d602052610a4560408420600460405191610a0783612970565b60ff81541615158352600181015460208401526002810154604084015260038101546060840152015460808201528761ffff60408801511691614c58565b868352600e602052604083209060ff8254166006811015610e4f5715610e40575b506001600160401b03421697610a8663ffffffff60a0870151168a612c2e565b915460ff81166006811015610e2c5760011480610e0e575b610dfa575b5060209860405198610ab58b8b6129d7565b858a52610ac360045461381e565b9889600455610ad360055461381e565b60059081558a885260128d526040882080546bffffffffffffffffffff0000163360601b6001600160601b031916176101001781556002810180546001600160a01b03191690941790935560038301919091558a519a908201906001600160401b038c11610de657908b8d979695949392610b4e83546135b4565b601f8111610d78575b50601f899211600114610d065792610bec928e6104cd9e9f600494610ccf9a9998978e92610cfb575b50508160011b915f199060031b1c19161790555b60ff60281b19815416815560015463ffffffff60401b82549160301b169063ffffffff60401b191617815501916001600160401b0381166001600160401b031984541617835563ffffffff60025460d01c1690612c2e565b8154906001600160401b0360801b9060801b16906001600160401b0360801b191617905589865260128552610ca560ff608060408920998051610c2e816127cc565b610c37816127cc565b8b5462ff000067ffff0000000000008c85015160301b169260101b169067ffff000000ff0000191617178b55610c88836060830151168c9063ff00000082549160181b169063ff0000001916179055565b0151895464ff000000001916911660201b64ff0000000016178855565b6001870180546001600160a01b0319166001600160a01b039092169190911790556004860161368a565b806024610cdb82612c61565b96610ce960405198896129d7565b82885201838701378401015283614f18565b015190505f80610b80565b828a52888a209190601f198f168b5b818110610d6157506104cd9e9f600494610ccf9a99989794610bec97948360019510610d49575b505050811b019055610b94565b01515f1960f88460031b161c191690555f8080610d3c565b92938b600181928786015181550195019301610d15565b838b9a929495969798999a52818b2091601f840160051c83019310610ddc575b918f99989796959493918f93601f0160051c01905b818110610dba5750610b57565b90925060019193949596979899508a815501918d918f99989796959493610dad565b9091508190610d98565b634e487b7160e01b88526041600452602488fd5b60481c6001600160401b031691505f610aa3565b506001600160401b0383166001600160401b038260481c1611610a9e565b634e487b7160e01b86526021600452602486fd5b610e4990614b3b565b5f610a66565b634e487b7160e01b85526021600452602485fd5b6365e52d5160e11b8252600482fd5b6372b89e9760e11b8152600490fd5b8380fd5b503461026c578060031936011261026c57610200604051610ea58161299f565b8281528260208201528260408201528260608201528260808201528260a08201528260c08201528260e08201528261010082015282610120820152826101408201528261016082015282610180820152826101a0820152826101c0820152826101e08201520152610220610f17612c89565b610f246040518092612887565bf35b503461026c57602036600319011261026c5760606101e0604051610f49816129bb565b83815283602082015283604082015283838201528360808201528360a08201528360c08201528360e08201528361010082015283610120820152836101408201528361016082015283610180820152836101a0820152836101c08201520152600435815260126020526040812060405190610fc3826129bb565b80549060ff82166007811015610e4f57835260ff8260081c166005811015610e4f576110b691600591602086015260ff8460101c16611001816127cc565b604086015260ff8460181c16606086015260ff8460201c16608086015260ff8460281c16151560a086015261ffff8460301c1660c086015263ffffffff8460401c1660e086015261010085019360601c845260018060a01b0360018201541661012086015260018060a01b0360028201541661014086015260038101546101608601526001600160401b036004820154818116610180880152818160401c166101a088015260801c166101c0860152016135ec565b6101e08301526040519260208452825160078110156111f857602085015260208301519060058210156111e4575083926111e0926101e09260408601526040820151611101816127cc565b606086015260ff606083015116608086015260ff60808301511660a086015260a0820151151560c086015261ffff60c08301511660e086015263ffffffff60e08301511661010086015260018060a01b0390511661012085015260018060a01b036101208201511661014085015260018060a01b03610140820151166101608501526101608101516101808501526001600160401b03610180820151166101a08501526001600160401b036101a0820151166101c08501526001600160401b036101c08201511682850152015161020080840152610220830190612863565b0390f35b634e487b7160e01b81526021600452602490fd5b634e487b7160e01b82526021600452602482fd5b503461026c57602036600319011261026c57600435600381101561052a5761123261486f565b61123b816127cc565b60ff60015460081c1661124d816127cc565b60ff8216908082101561134d57600290611266816127cc565b036113435761128063ffffffff60025460901c16926127cc565b60ff60025460201c1690604051906112996020836129d7565b84825284600260ff60015460081c166112b1816127cc565b14908161133a575b81611330575b5061132157916104b76104ae63ffffffff6112e5600189966104cd9860209b6005614cd3565b8086526012895260408620600254815463ff00000090911664ffff000000199091161760209690961b64ff00000000169590951785559661049e565b6372b89e9760e11b8552600485fd5b955050845f6112bf565b600191506112b9565b61128083926127cc565b63710a5e0f60e01b8452600484fd5b503461026c57602036600319011261026c5761138760406111e0926004358152601360205220612bbb565b604051918291602083526020830190612827565b503461026c57604036600319011261026c576004356113b86148c5565b6113c1816148dc565b6113ca82613276565b1561141c5761040061ff00198254161790556113e7600554612b51565b6005556040519060243582527fcf4be29f7d91e92478a19a3eab52f6161f4e4580fcd2f5f8715f191dc2647bbb60203393a380f35b63ef28671760e01b8352600483fd5b503461026c57602036600319011261026c5760043580825260126020526040822060ff815460081c166005811015611503576001036114f4576001600160401b03600482015460801c164211156114c05761030061ff0019825416179055611494600554612b51565b60055533907f74c34a008ce735d9fcf0bd03a9b238d212ad4c441c020661f4ffbb6442645b858380a380f35b60405163935f84bf60e01b815260206004820152600b60248201526a1b9bdd08195e1c1a5c995960aa1b6044820152606490fd5b633ee3282d60e11b8352600483fd5b634e487b7160e01b84526021600452602484fd5b503461026c578060031936011261026c576020600654604051908152f35b503461026c57602036600319011261026c57611552600435613396565b80f35b503461026c57602036600319011261026c5761138760406111e0926004358152601460205220612bbb565b503461026c57604036600319011261026c5761159a6127a0565b6024359060ff821691828103610e81576115b261486f565b6001600160a01b0382168015611699576115cb82614bfa565b8452600860205260ff60408520541690838214611667576115eb91614c26565b906115f58261495e565b92604051916116056020846129d7565b85835285600260ff60015460081c1661161d816127cc565b14908161165f575b81611653575b5061164457926020959261045b926104cd956003614cd3565b6372b89e9760e11b8652600486fd5b9650600190505f61162b565b859150611625565b60405163935f84bf60e01b81526020600482015260096024820152686e6f206368616e676560b81b6044820152606490fd5b63d92e233d60e01b8552600485fd5b503461026c57602036600319011261026c576116c26127a0565b6116ca61489a565b6001600160a01b038116801561174e578252600e60205260ff604083205416600681101561173a576116ff5761155290614b3b565b60405163935f84bf60e01b8152602060048201526012602482015271185b1c9958591e481c9959da5cdd195c995960721b6044820152606490fd5b634e487b7160e01b83526021600452602483fd5b63d92e233d60e01b8352600483fd5b503461026c57602036600319011261026c57602061177c600435613276565b6040519015158152f35b503461026c57602036600319011261026c576020906040906001600160a01b036117ae6127a0565b168152601083522054604051908152f35b503461026c578060031936011261026c57604051906007548083528260208101600784526020842092845b8181106118ca5750506117ff925003836129d7565b815161182361180d8261324b565b9161181b60405193846129d7565b80835261324b565b602082019290601f1901368437805b8451811015611872576001906001600160a01b036118508288613262565b51168352600860205260ff60408420541661186b8286613262565b5201611832565b5061188f9392916020604051958695604087526040870190612827565b918583038287015251918281520192915b8181106118ae575050500390f35b825160ff168452859450602093840193909201916001016118a0565b84546001600160a01b03168352600194850194879450602090930192016117ea565b503461026c57606036600319011261026c5760c061191c61190b6127a0565b6119136127b6565b60443591612d80565b63ffffffff60a0604051928051611932816127cc565b845261ffff602082015116602085015261ffff604082015116604085015260ff606082015116606085015260ff608082015116608085015201511660a0820152f35b503461026c57604036600319011261026c5761198e6127a0565b6001600160a01b0316602435811561174e578015611a28576119e06040516323b872dd60e01b6020820152336024820152306044820152826064820152606481526119da6084826129d7565b8361563e565b8183526011602052604083206119f7828254612c7c565b90556040519081527f5548c837ab068cf56a2c2479df0882a4922fd203edb7517321831d95078c5f6260203392a380f35b6365e52d5160e11b8352600483fd5b503461026c578060031936011261026c57602060ff600a5460101c16604051908152f35b503461026c57604036600319011261026c57611a756127a0565b6024356006811015611b7557611a8961486f565b6001600160a01b0382168015611b66578115611b28578352600e602052611ab78160ff604086205416614a45565b90611ac18261495e565b9260405191611ad16020846129d7565b85835285600260ff60015460081c16611ae9816127cc565b149081611b20575b81611b14575b5061164457926020959261045b9260ff6104cd9616906004614cd3565b9650600190505f611af7565b859150611af1565b60405163935f84bf60e01b8152602060048201526015602482015274757365204e455720746f2072652d70726f6261746560581b6044820152606490fd5b63d92e233d60e01b8452600484fd5b8280fd5b503461026c57602036600319011261026c5760a0906001600160401b03611b9e6127a0565b8283908492600180881b03811695868152600f60205260408120968152600d602052611c0960408220600460405191611bd683612970565b60ff8154161515835260018101546020840152600281015460408401526003810154606084015201546080820152614976565b929050875487611c1a818316612c0e565b164210611cd95782985b6003549061ffff8260201c169283611c5a575b505050505050604051958652602086015260408501526060840152166080820152f35b9598509396509194509260401c8716919060301c63ffffffff1687611c7f8285612c2e565b164210611cae57505050611c9f61271091611c9a85946149d3565b612c4e565b0491925b5f8080808080611c37565b9194509250612710611ccc611cd39360026003870154960154612c4e565b0493612c2e565b92611ca3565b600189015498611c24565b503461026c578060031936011261026c57604051600b8054808352908352909160208301917f0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01db9915b818110611d43576111e085611387818703826129d7565b82546001600160a01b0316845260209093019260019283019201611d2c565b503461026c57602036600319011261026c57604060a091611d816127a0565b611d896129f8565b50600180851b03168152600e6020522060405190611da682612970565b6001600160401b038154611dbd60ff821685612baf565b816020850193818360081c1685528160016040880192828660481c1684528260608a019660881c16865201549560808801968752611dff60405180995161281a565b511660208701525116604085015251166060830152516080820152f35b503461026c578060031936011261026c57602063ffffffff60015460101c16604051908152f35b503461026c578060031936011261026c57602060ff600a5460181c16604051908152f35b503461026c57602036600319011261026c576020906040906001600160a01b03611e8f6127a0565b168152601183522054604051908152f35b503461206e5760c036600319011261206e57611eba6127a0565b60a036602319011261206e57611ece61486f565b736baa1a24b55796d8346748ee3565e40d844ba576803b1561206e57604051631c71447d60e01b8152611f0360048201612b71565b5f8160a481855af480156120635761204e575b5060018060a01b0382168352600d60205260206004610144604086206040519485938492637309439160e01b845260ff81541615158285015260018101546024850152600281015460448501526003810154606485015201546084830152611f8060a48301612b71565b5af4908115612043578391612009575b50604051611fa060208201612b71565b60a08152611faf60c0826129d7565b611fb88261495e565b9284600260ff60015460081c16611fce816127cc565b149081612001575b81611ff5575b50611321576104cd92859260209661045b936002614cd3565b9550600190505f611fdc565b849150611fd6565b90506020813d60201161203b575b81612024602093836129d7565b81010312611b755761203590612ba2565b5f611f90565b3d9150612017565b6040513d85823e3d90fd5b61205b9193505f906129d7565b5f915f611f16565b6040513d5f823e3d90fd5b5f80fd5b3461206e57602036600319011261206e5760043561208f816148dc565b805460601c331415806120f3575b6120e45761030061ff00198254161790556120b9600554612b51565b60055533907f74c34a008ce735d9fcf0bd03a9b238d212ad4c441c020661f4ffbb6442645b855f80a3005b6367909b1560e01b5f5260045ffd5b50335f526008602052600160405f2054161561209d565b3461206e57604036600319011261206e57600435600381101561206e5761212f614844565b612138816127cc565b60ff60015460081c1661214a816127cc565b60ff821611156121ab576001549060ff8260081c1691612169826127cc565b61ff001916600882901b61ff001617600155612184826127cc565b61218d816127cc565b6040519160243583525f5160206157295f395f51905f5260203394a4005b63216a142160e11b5f5260045ffd5b3461206e57602036600319011261206e576001600160a01b036121db6127a0565b165f526008602052602060ff60405f205416604051908152f35b3461206e576102a036600319011261206e576004356001600160401b03811161206e576122269036906004016127ea565b906024356001600160401b03811161206e576122469036906004016127ea565b90919061022036604319011261206e57610264356001600160401b03811161206e576122769036906004016127ea565b909261028435926001600160401b03841161206e573660238501121561206e578360040135956001600160401b03871161206e5736602460a089028701011161206e5760015460ff81166126085760019060ff1916176001558188148015906125fe575b6125cf575f5b88811061253e57505050505f5b8181106124a457856040516123018161299f565b60443560ff8116810361206e57815260643560ff8116810361206e57602082015260843560ff8116810361206e57604082015260a43560ff8116810361206e57606082015260c43560ff8116810361206e57608082015260e435801515810361206e5760a08201526101043563ffffffff8116810361206e5760c08201526101243563ffffffff8116810361206e5760e08201526101443563ffffffff8116810361206e576101008201526101643563ffffffff8116810361206e576101208201526101843563ffffffff8116810361206e576101408201526101a43563ffffffff8116810361206e576101608201526101c43561ffff8116810361206e576101808201526101e43561ffff8116810361206e576101a08201526102043561ffff8116810361206e576101c08201526102243561ffff8116810361206e576101e0820152610244359163ffffffff8316830361206e5761248a827f4488062cfafbbb2c1d440f13796e120c4522761f74ecade440ee8f31f1bfbee09461020060409501526145c5565b63ffffffff60015460101c169082519182526020820152a1005b6124b76124b2828487612b1f565b612b2f565b908581101561252a5760a08102840160a0602319823603011261206e576040516124e081612970565b602482013593841515850361206e5760a4612524936001968452604481013560208501526064810135604085015260848101356060850152013560808301526143f9565b016122ed565b634e487b7160e01b5f52603260045260245ffd5b6001600160a01b036125546124b2838c88612b1f565b165f52600860205260ff60405f2054166125c05760ff61257d612578838686612b1f565b612b43565b16156125b157806125ab6125976124b26001948d89612b1f565b6125a5612578848888612b1f565b9061426b565b016122e0565b633a767e1d60e21b5f5260045ffd5b630c8ce74760e11b5f5260045ffd5b60405163935f84bf60e01b81526020600482015260066024820152650d8cadccee8d60d31b6044820152606490fd5b50868414156122da565b62dc149f60e41b5f5260045ffd5b3461206e575f36600319011261206e57602060ff60015460081c166040519061263e816127cc565b8152f35b3461206e57602036600319011261206e576040612660600435612a22565b82519182526020820152f35b3461206e57604036600319011261206e576126856127b6565b6004355f52601560205260405f209060018060a01b03165f52602052602060ff60405f2054166040519015158152f35b3461206e57602036600319011261206e576126ce6127a0565b6126d66129f8565b5060018060a01b03165f52600d60205260a060405f206004604051916126fb83612970565b60ff8154161515835260018101546020840152600281015460408401526003810154606084015201546080820152610f246040518092608080918051151584526020810151602085015260408101516040850152606081015160608501520151910152565b3461206e575f36600319011261206e57602060ff600a5460081c16604051908152f35b3461206e575f36600319011261206e5760209060ff600a54168152f35b600435906001600160a01b038216820361206e57565b602435906001600160a01b038216820361206e57565b600311156127d657565b634e487b7160e01b5f52602160045260245ffd5b9181601f8401121561206e578235916001600160401b03831161206e576020808501948460051b01011161206e57565b9060068210156127d65752565b90602080835192838152019201905f5b8181106128445750505090565b82516001600160a01b0316845260209384019390920191600101612837565b805180835260209291819084018484015e5f828201840152601f01601f1916010190565b63ffffffff610200809260ff815116855260ff602082015116602086015260ff604082015116604086015260ff606082015116606086015260ff608082015116608086015260a0810151151560a08601528260c08201511660c08601528260e08201511660e0860152826101008201511661010086015282610120820151166101208601528261014082015116610140860152826101608201511661016086015261ffff6101808201511661018086015261ffff6101a0820151166101a086015261ffff6101c0820151166101c086015261ffff6101e0820151166101e0860152015116910152565b60a081019081106001600160401b0382111761298b57604052565b634e487b7160e01b5f52604160045260245ffd5b61022081019081106001600160401b0382111761298b57604052565b61020081019081106001600160401b0382111761298b57604052565b90601f801991011681019081106001600160401b0382111761298b57604052565b60405190612a0582612970565b5f6080838281528260208201528260408201528260608201520152565b90815f52601260205260405f20825f52601360205260ff60405f2091541660078110156127d657612b17576002905b5f918154915f5b838110612acc5750505050915f52601460205260405f205f908054905f5b828110612a835750505090565b612a8d8183613315565b60018060a01b0391549060031b1c165f526008602052600860405f205416612ab8575b600101612a76565b92612ac460019161381e565b939050612ab0565b612ad68183613315565b60018060a01b0391549060031b1c165f52600860205260ff8360405f20541616612b03575b600101612a58565b93612b0f60019161381e565b949050612afb565b600190612a51565b919081101561252a5760051b0190565b356001600160a01b038116810361206e5790565b3560ff8116810361206e5790565b8015612b5d575f190190565b634e487b7160e01b5f52601160045260245ffd5b60243580151580910361206e578152604435602082015260643560408201526084356060820152608060a435910152565b5190811515820361206e57565b60068210156127d65752565b90604051918281549182825260208201905f5260205f20925f5b818110612bec575050612bea925003836129d7565b565b84546001600160a01b0316835260019485019487945060209093019201612bd5565b6001600160401b0362015180911601906001600160401b038211612b5d57565b906001600160401b03809116911601906001600160401b038211612b5d57565b81810292918115918404141715612b5d57565b6001600160401b03811161298b57601f01601f191660200190565b91908201809211612b5d57565b60405190612c968261299f565b8160025460ff8116825260ff8160081c16602083015260ff8160101c16604083015260ff8160181c16606083015260ff8160201c16608083015260ff8160281c16151560a083015263ffffffff8160301c1660c083015263ffffffff8160501c1660e083015263ffffffff8160701c1661010083015263ffffffff8160901c1661012083015263ffffffff8160b01c1661014083015263ffffffff8160d01c1661016083015260f01c61018082015261020063ffffffff60035461ffff81166101a085015261ffff8160101c166101c085015261ffff8160201c166101e085015260301c16910152565b92916040519160c083018381106001600160401b0382111761298b576040525f835260208301925f845260408101915f835260608201945f865260808301935f85525f60a0850152839860018060a01b038116805f52600d60205260405f209460405195612ded87612970565b60ff81541615918215885260018201549260208901938452600460028401549360408b01948552600381015460608c0152015460808a01526132385760018060a01b031693845f52600e60205260405f209760405195612e4c87612970565b60018a549a612e5e60ff8d168a612baf565b6001600160401b038c60081c1660208a01526001600160401b0360408a019c8d828260481c16905260881c1660608a015201546080880152865160068110156127d6576005146132265750612eb290614976565b9490505f52600f602052612ec960405f20916149d3565b92519151906001600160401b03612ee281835416612c0e565b16421061321c57505f9a5b85519560068710156127d6575160068110156127d6576001149889613208575b5060ff60015460081c1660025460f01c9161ffff6003541697604051976101608901928984106001600160401b0385111761298b57612f75936040528952602089019788526040890196875260608901958652608089015260a088019e8f5260c08801612baf565b60e086019915158a52612f87816127cc565b6101008601526101208501526101408401948552835190515f9283918311156131e3575050612fc161ffff916002809417945b5190615696565b94511680151590816131d4575b501561317d5750600860029117955b60c083015160068110156127d65760040361311c575050602060029517975b6080820151151590816130d8575b509360ff9860a0989794613074948b989463ffffffff986130a1575b6101006001910151613037816127cc565b613040816127cc565b14613085575b61ffff90811690915216905261305b816127cc565b808352613067816127cc565b61306f612c89565b614ab3565b939093169190950152169052169052565b9061ffff60406130968295976156f5565b961793505090613046565b92946130ac816127cc565b6001818b16106130ca5761010060806001925b971794915050613026565b5060016101006080826130bf565b6130ef915097969397959194929551855190612c7c565b918260011b9280840460021490151715612b5d5760808501519697939691959094909290911060ff61300a565b60c083015160068110156127d657613143575061313b610110916156f5565b951797612ffc565b60c0839a92979a015160068110156127d6576001149081613172575b5015612ffc57979461313b6010916156f5565b90505115155f61315f565b909561ffff6101208401511680151590816131c5575b5015612fdd5795906131a4816127cc565b600160ff8216106131bb576004905b911795612fdd565b50600460016131b3565b905061ffff851610155f613193565b905061ffff851610155f612fce565b939092935182116131fc575b61ffff91612fc191612fba565b600193508392506131ef565b516001600160401b0316421098505f612f0d565b600101549a612eed565b63325fbd1560e11b5f5260045260245ffd5b8363a6e7da2360e01b5f5260045260245ffd5b6001600160401b03811161298b5760051b60200190565b805182101561252a5760209160051b010190565b5f52601260205260405f205460ff8160081c1660058110156127d6576001036132f85760ff8160281c166132f25760ff811660078110156127d6576132f857600260ff8260101c166132c7816127cc565b146132f25760301c6010166132ed5760ff60015460081c166132e8816127cc565b151590565b600190565b50600190565b505f90565b60075481101561252a5760075f5260205f2001905f90565b805482101561252a575f5260205f2001905f90565b60075490600160401b82101561298b5761334f826001612bea94016007556007613315565b81546001600160a01b0393841660039290921b91821b9390911b1916919091179055565b90815491600160401b83101561298b578261334f916001612bea95018155613315565b61339f816148dc565b335f52600860205260405f205490600882166134bb575460ff1660078110156127d657600681146134a75761349c5760ff600280925b16161561348657505f81815260156020908152604080832033845290915290205460ff1661347757805f52601560205260405f2060018060a01b0333165f5260205260405f20600160ff19825416179055805f52601360205261343b3360405f20613373565b805f52601360205260405f2054906040519182527f2f0f51d2f12c71357e60d575d47e93e081dbaf32072245ab1e01540f0aadbf1a60203393a3565b63080fc0bd60e11b5f5260045ffd5b60ff9063e84c979760e01b5f521660045260245ffd5b60ff600180926133d5565b63e84c979760e01b5f52600860045260245ffd5b60ff91505460201c1615613563575f81815260156020908152604080832033845290915290205460ff1661347757805f52601560205260405f2060018060a01b0333165f5260205260405f20600160ff19825416179055805f5260146020526135273360405f20613373565b805f52601460205260405f2054906040519182527fb8c7de51f1e8e2b6cec2574c8731fba6de6e3e3002b2e454ebccdf32f591ca8660203393a3565b60405163935f84bf60e01b815260206004820152602260248201527f677561726469616e20636f6e6669726d6174696f6e206e6f7420726571756972604482015261195960f21b6064820152608490fd5b90600182811c921680156135e2575b60208310146135ce57565b634e487b7160e01b5f52602260045260245ffd5b91607f16916135c3565b9060405191825f8254926135ff846135b4565b80845293600181169081156136685750600114613624575b50612bea925003836129d7565b90505f9291925260205f20905f915b81831061364c575050906020612bea928201015f613617565b6020919350806001915483858901015201910190918492613633565b905060209250612bea94915060ff191682840152151560051b8201015f613617565b9067ffffffffffffffff60401b82549160401b169067ffffffffffffffff60401b1916179055565b60043560ff811680910361206e57815260243560ff811680910361206e57602082015260443560ff811680910361206e57604082015260643560ff811680910361206e57606082015260843560ff811680910361206e57608082015260a43580151580910361206e5760a082015260c43563ffffffff811680910361206e5760c082015260e43563ffffffff811680910361206e5760e08201526101043563ffffffff811680910361206e576101008201526101243563ffffffff811680910361206e576101208201526101443563ffffffff811680910361206e576101408201526101643563ffffffff811680910361206e576101608201526101843561ffff811680910361206e576101808201526101a43561ffff811680910361206e576101a08201526101c43561ffff811680910361206e576101c08201526101e43561ffff811680910361206e576101e0820152610204359063ffffffff821680920361206e576102000152565b5f198114612b5d5760010190565b519060ff8216820361206e57565b519063ffffffff8216820361206e57565b519061ffff8216820361206e57565b613863816148dc565b805460ff8116906007821015806127d657821591821561422b57335f526008602052600460405f20541615614218575b6001600160401b03600486015460401c1680421061420657506001549082600260ff8460081c166138c3816127cc565b1490816141d6575b506141c75760ff8160181c169160ff8260201c16936127d6578490816141aa575b5061416f575b506138fc86612a22565b919081811061415a57505081811061414557505080614132575b61020061ff001984541617835561392e600554612b51565b60055561393c60065461381e565b60065533847f9c85b616f29fca57a17eafe71cf9ff82ffef41766e2cf01ea7f8f7878dd3ec245f80a315613a56575060018060a01b03600182015416906002810191600360018060a01b0384541692019382828654927feb669e0a01fff40cbe45de851f42334fe70673136abfd91399651b7ae199a07b6020604051868152a481613a1e575050905491545f92839250829182916001600160a01b03165af13d15613a19573d6139eb81612c61565b906139f960405192836129d7565b81525f60203d92013e5b15613a0a57565b633d2cec6f60e21b5f5260045ffd5b613a03565b90919250612bea93506040519263a9059cbb60e01b60208501526024840152604483015260448252613a516064836129d7565b61563e565b60018103613bc05750613a6c91506005016135ec565b6102208180518101031261206e57612bea90613bb561022060405192613a918461299f565b613a9d6020820161382c565b8452613aab6040820161382c565b6020850152613abc6060820161382c565b6040850152613acd6080820161382c565b6060850152613ade60a0820161382c565b6080850152613aef60c08201612ba2565b60a0850152613b0060e0820161383a565b60c0850152613b12610100820161383a565b60e0850152613b24610120820161383a565b610100850152613b37610140820161383a565b610120850152613b4a610160820161383a565b610140850152613b5d610180820161383a565b610160850152613b706101a0820161384b565b610180850152613b836101c0820161384b565b6101a0850152613b966101e0820161384b565b6101c0850152613ba9610200820161384b565b6101e08501520161383a565b6102008201526145c5565b60028103613c3d575060028101546001600160a01b03169150613be5906005016135ec565b60a08180518101031261206e57612bea9160a060405192613c0584612970565b613c1160208201612ba2565b8452604081015160208501526060810151604085015260808101516060850152015160808301526143f9565b60038103613c7057506002810154600390910154613c68925060ff16906001600160a01b031661426b565b612bea61550f565b60048103613d8a5750905060ff600360018060a01b0360028401541692015416600681109081156127d657825f52600e60205260405f209160ff8354169060068210156127d6578115613d65575b60018314613d17575b156127d657613d14827f155590dadece0bf62c36a0e5cabcce51e1280ff0e8f6cc53c14d8900e0f43daf9460409460ff19825416179055613d0a8451809461281a565b602083019061281a565ba2565b613d60613d3863ffffffff60025460701c166001600160401b034216612c2e565b855467ffffffffffffffff60481b191660489190911b67ffffffffffffffff60481b16178555565b613cc7565b835468ffffffffffffffff0019164260081b68ffffffffffffffff0016178455613cbe565b600503613e325760039150015460ff8116613da4816127cc565b613dad816127cc565b60ff60015460081c16613dbf816127cc565b811015613e235760015461ff008060ff8360081c1694613dde856127cc565b60081b16169061ff00191617600155613df6826127cc565b613dff816127cc565b60405191640e4cad8c2f60db1b83525f5160206157295f395f51905f5260203394a4565b63710a5e0f60e01b5f5260045ffd5b60028101546001600160a01b031690613e4d906005016135ec565b60208180518101031261206e57602001516001600160a01b038116929083900361206e57815f52600860205260ff60405f205416801561412357835f52600860205260ff60405f2054166125c057821561411457613eaa5f614bfa565b825f5260086020528260ff60405f205416801580809161410d575b15614007575060405f5160206157095f395f51905f5291613ee58461332a565b600754845f526009602052825f20555b613eff5f826153c3565b613f095f8261540d565b613f135f82615461565b613f1d5f826154b7565b835f526008602052815f2060ff19815416905581519081525f6020820152a2613f46818561426b565b613f4e61550f565b60ff60015460081c16613f60816127cc565b15613f93575b60207f607a6564adeb3eec35d536d94fa031ad71407e7510a784e53f7bf36d44f3bc4191604051908152a4565b600154600881901c60ff169061ff00191661010017600155613fb4816127cc565b7f607a6564adeb3eec35d536d94fa031ad71407e7510a784e53f7bf36d44f3bc4191602091600160405191677265636f7665727960c01b83525f5160206157295f395f51905f52853394a4915050613f66565b1580614105575b614029575b60405f5160206157095f395f51905f5291613ef5565b905f52600960205260405f20545f198101818111612b5d576007545f198101908111612b5d578082036140c7575b5050506007549081156140b35760405f5160206157095f395f51905f529186935f190161409b614086826132fd565b81549060018060a01b039060031b1b19169055565b600755835f5260096020525f82812055915050614013565b634e487b7160e01b5f52603160045260245ffd5b61334f916140d76140f1926132fd565b905460039190911b1c6001600160a01b03169283916132fd565b5f52600960205260405f20555f8080614057565b50600161400e565b505f613ec5565b63d92e233d60e01b5f5260045ffd5b63148fe22160e11b5f5260045ffd5b61413b8361500b565b6139165750505050565b63d495223560e01b5f5260045260245260445ffd5b635ab4617560e01b5f5260045260245260445ffd5b61418260ff809260101c1661306f612c89565b5091168281116141a1575b5060ff16828111156138f25791505f6138f2565b915060ff61418d565b63ffffffff915060101c1663ffffffff8260401c1614155f6138ec565b6372b89e9760e11b5f5260045ffd5b90506127d6578384156141ea575b836138cb565b5060ff8160281c1680156141e457505f925060058514156141e4565b6384280dad60e01b5f5260045260245ffd5b63e84c979760e01b5f526004805260245ffd5b5f9150600684036142435761423e614844565b613893565b335f526008602052600560405f2054166138935763e84c979760e01b5f52600560045260245ffd5b6001600160a01b0381169182156141145761428581614bfa565b825f52600860205260ff60405f2054169182158080916143ed575b1561431f57509160ff6040926142c35f5160206157095f395f51905f529561332a565b600754865f526009602052845f20555b6142dd81846153c3565b6142e7818461540d565b6142f18184615461565b6142fb81846154b7565b855f526008602052835f2082821683198254161790558351928352166020820152a2565b905015806143e2575b614346575b5f5160206157095f395f51905f529160ff6040926142d3565b825f52600960205260405f20545f198101818111612b5d576007545f198101908111612b5d578082036143be575b5050506007549182156140b35760ff6040925f5160206157095f395f51905f52945f19016143a4614086826132fd565b600755855f5260096020525f84812055925050915061432d565b61334f916140d76143ce926132fd565b5f52600960205260405f20555f8080614374565b5060ff811615614328565b5060ff831615156142a0565b90736baa1a24b55796d8346748ee3565e40d844ba576803b1561206e57604051631c71447d60e01b81525f8160a48183956144616004830189608080918051151584526020810151602085015260408101516040850152606081015160608501520151910152565b5af48015612063576145b2575b506001600160a01b038316808252600c602052604082205490939060ff1615614532575b507f9d182ff993316b1c662980e27e42d42fae0bd158f1bed04e9988ec637fea8da4916040828560a09452600d602052208151151560ff8019835416911617815560208201516001820155604082015160028201556060820151600382015560046080830151910155613d146040518092608080918051151584526020810151602085015260408101516040850152606081015160608501520151910152565b838252600c60205260408220805460ff19166001179055600b54600160401b81101561459e5760a0926145957f9d182ff993316b1c662980e27e42d42fae0bd158f1bed04e9988ec637fea8da4959361334f84600160409601600b55600b613315565b92505091614492565b634e487b7160e01b83526041600452602483fd5b6145be91505f906129d7565b5f5f61446e565b736baa1a24b55796d8346748ee3565e40d844ba576803b1561206e5760405163065397d760e21b81525f816102248183956146036004830189612887565b5af4801561206357614831575b50815160208084015160408501516060860151608087015160a088015160c089015160e08a01516101008b01516101208c01516101408d01516101608e01516101808f015160089b909b1b61ff001660ff9d909d169c909c176010998a1b62ff0000161760189890981b63ff0000001697909717958a1b64ff00000000169590951793151560281b60ff60281b1693909317603092831b69ffffffff0000000000009081169190911760509290921b63ffffffff60501b169190911760709390931b63ffffffff60701b169290921760909390931b63ffffffff60901b169290921760b09390931b63ffffffff60b01b169290921760d09690961b63ffffffff60d01b169590951760f09390931b6001600160f01b031916929092176002556101a0860151600380546101c08901516101e08a01516102008b015169ffffffffffffffffffff199390931661ffff959095169490941790851b63ffff000016179290951b65ffff0000000016919091179390941b9091169190911790915560018054929183901c63ffffffff9081169091019190821161481d575063ffffffff7f1a734774792c149a91deb03650b2a2bc51de7bbab7262857ace6fb1699b430419265ffffffff00006102409360101b169065ffffffff0000191617806001556147f861550f565b614815604051956001600160401b03421687526020870190612887565b60101c1692a2565b634e487b7160e01b81526011600452602490fd5b61483d91505f906129d7565b5f5f614610565b335f526008602052600960405f2054161561485b57565b63e84c979760e01b5f52600960045260245ffd5b335f526008602052600160405f2054161561488657565b63e84c979760e01b5f52600160045260245ffd5b335f526008602052600360405f205416156148b157565b63e84c979760e01b5f52600360045260245ffd5b335f526008602052600860405f205416156134a757565b80158015614953575b614944575f52601260205260405f209060ff825460081c1660058110156127d657600103614935576001600160401b03600483015460801c16421161492657565b6328a7237960e01b5f5260045ffd5b633ee3282d60e11b5f5260045ffd5b6302eae9e560e51b5f5260045ffd5b5060045481116148e5565b156149725763ffffffff60025460901c1690565b5f90565b906080606083015192015190600160ff815460081c16614995816127cc565b1461499c57565b826149bb575b816149a957565b9060011c806149b85750600190565b90565b9160011c806149ce575060015b916149a2565b6149c8565b6001600160a01b03168015614a40576020602491604051928380926370a0823160e01b82523060048301525afa908115612063575f91614a11575090565b90506020813d602011614a38575b81614a2c602093836129d7565b8101031261206e575190565b3d9150614a1f565b504790565b906006811015806127d657600282148015614aa7575b614a9f576127d6576001149081614a76575b506132ed575f90565b905060068110156127d65760048114908115614a94575b505f614a6d565b60059150145f614a8d565b505050600190565b50505f60038214614a5b565b9190614abe816127cc565b8015614b2e57600190614ad0816127cc565b14614b135760ff6040830151169060a083015115155f14614b055763ffffffff60e060ff608086015116945b01511691929190565b63ffffffff60e05f94614afc565b9063ffffffff60c060ff6020840151169201511690915f9190565b50905160ff16905f908190565b6001600160a01b03165f818152600e60209081526040909120805468ffffffffffffffffff191642600881901b68ffffffffffffffff00169190911760011782557fa834c3aabec8913fbfd0591ec9c2a7494c727a364cc1f231c948c0f9564b5d0692916001600160401b0391614bec90614bc490841663ffffffff60025460701c1690612c2e565b825467ffffffffffffffff60481b191660489190911b67ffffffffffffffff60481b16178255565b5460481c16604051908152a2565b600f60ff8216116125b1576008811615159081614c19575b506125b157565b600791501615155f614c12565b60ff8119831616614c515760081615159081614c4557506132ed575f90565b6008915016155f614a6d565b5050600190565b614c6190614976565b509081151580614cca575b614cb557505061ffff60035460101c169081151580614ca8575b614c8e575050565b61ffff90630a8a293d60e01b5f521660045260245260445ffd5b508161ffff821611614c86565b634fa5781f60e11b5f5260045260245260445ffd5b50818111614c6c565b9290949391614ce360045461381e565b958660045586614cf460055461381e565b6005555f52601260205260405f209460078110156127d65785546bffffffffffffffffffffffff61ffff1990911660ff929092169190911761010017163360601b6001600160601b0319161785556002850180546001600160a01b0319166001600160a01b039092169190911790556003840155805160058401916001600160401b03821161298b578190614d8984546135b4565b601f8111614ec8575b50602090601f8311600114614e65575f92614e5a575b50508160011b915f199060031b1c19161790555b60ff60281b825491151560281b169060ff60281b191617815560015463ffffffff60401b82549160301b169063ffffffff60401b1916178155614e3260046001600160401b0342169201916001600160401b0381166001600160401b031984541617835563ffffffff60025460d01c1690612c2e565b815467ffffffffffffffff60801b191660809190911b67ffffffffffffffff60801b16179055565b015190505f80614da8565b5f8581528281209350601f198516905b818110614eb05750908460019594939210614e98575b505050811b019055614dbc565b01515f1960f88460031b161c191690555f8080614e8b565b92936020600181928786015181550195019301614e75565b909150835f5260205f20601f840160051c81019160208510614f0e575b90601f859493920160051c01905b818110614f005750614d92565b5f8155849350600101614ef3565b9091508190614ee5565b815460ff81169260018060a01b0360018201541660018060a01b0360028301541691600381015493600460ff8260101c169201549460078810156127d6577fcf5d862ed732f5aec4f98da191cd4e962a57abdc7bab078c436b0517229d388f95615006936001600160401b0392604051978897885260208801526040870152614fa0816127cc565b606086015261ffff8360301c16608086015260ff8360181c1660a086015260ff8360201c1660c0860152818160401c1660e086015260801c1661010084015260ff8160281c16151561012084015261016061014084015260601c96610160830190612863565b0390a4565b60018060a01b0360018201541690600381015491805f52600d60205260405f20916040519261503984612970565b600460ff8254161591821586526001810154602087015260028101546040870152600381015460608701520154608085015261538c57600201546001600160a01b03165f818152600e60205260409020805490939160ff82169060068210156127d65760058214613226575060011480615377575b61535857506150bc826149d3565b90818511615341576150e1906150dc6150d58488615696565b8783614c58565b614976565b919050825f52600f60205260405f20916001600160401b0361510581855416612c0e565b1642101561531c575b80151580615306575b6152e3575060035461ffff8160201c166151b5575b505060010161513c848254612c7c565b90555f52601060205260405f20615154838254612c7c565b905580549060016001600160401b038360881c1601906001600160401b038211612b5d5767ffffffffffffffff60881b1990921660889190911b67ffffffffffffffff60881b16178155600101805490916151ae91612c7c565b9055600190565b6151d86001600160401b039163ffffffff83865460401c169160301c1690612c2e565b164210156152bf575b506127106151fd600283015461ffff60035460201c1690612c4e565b0460038201908154816152108883612c7c565b1161522f57505090816152268660019454612c7c565b9055905f61512c565b9091507f72f5745158d621f0076c165a1d23a925d0b2f8c5b5fb643933649247142d156d945060609395925060405192835260208301526040820152a2600154600881901c60ff169061ff0019166102001760015561528d816127cc565b6002604051916e31b4b931bab4ba16b13932b0b5b2b960891b83525f5160206157295f395f51905f5260203394a45f90565b6152d26001600160401b0342168361368a565b60028201555f60038201555f6151e1565b6152f1866001850154612c7c565b6398b5494560e01b5f5260045260245260445ffd5b5080615316876001860154612c7c565b11615117565b825467ffffffffffffffff1916426001600160401b03161783555f600184015561510e565b508363cf47918160e01b5f5260045260245260445ffd5b6001600160401b03906384280dad60e01b5f5260481c1660045260245ffd5b506001600160401b038160481c1642106150ae565b5063a6e7da2360e01b5f5260045260245ffd5b60ff5f199116019060ff8211612b5d57565b60ff60019116019060ff8211612b5d57565b6001918216151591161515811461540a57156153f65760ff6153e881600a54166153b1565b1660ff19600a541617600a55565b60ff61540581600a541661539f565b6153e8565b50565b6002918216151591161515811461540a571561544b5761543460ff600a5460081c166153b1565b61ff00600a549160081b169061ff00191617600a55565b61545c60ff600a5460081c1661539f565b615434565b6004918216151591161515811461540a57156154a15761548860ff600a5460101c166153b1565b62ff0000600a549160101b169062ff0000191617600a55565b6154b260ff600a5460101c1661539f565b615488565b6008918216151591161515811461540a57156154f9576154de60ff600a5460181c166153b1565b63ff000000600a549160181b169063ff000000191617600a55565b61550a60ff600a5460181c1661539f565b6154de565b600a5460ff81166001811061560d57600160ff8360101c16106155d9576002549060ff8260181c161161559d5760ff8160101c1660ff8360081c16106155635760ff809160201c169160181c16106108b457565b604051637417de8f60e01b8152602060048201526011602482015270185c1c1c9bdd985b1cd0dc9a5d1a58d85b607a1b6044820152606490fd5b604051637417de8f60e01b815260206004820152601360248201527219dbdd995c9b985b98d9551a1c995cda1bdb19606a1b6044820152606490fd5b604051637417de8f60e01b815260206004820152600b60248201526a37379032bc32b1baba37b960a91b6044820152606490fd5b604051637417de8f60e01b815260206004820152600860248201526737379037bbb732b960c11b6044820152606490fd5b905f602091828151910182855af115612063575f513d61568d57506001600160a01b0381163b155b61566d5750565b635274afe760e01b5f9081526001600160a01b0391909116600452602490fd5b60011415615666565b9081156156ef5780158080156156e5575b6156dc57612710830292830461271003612b5d576156c85761ffff91041690565b634e487b7160e01b5f52601260045260245ffd5b50505061271090565b50818310156156a7565b50505f90565b6156fe816127cc565b156132ed5760029056fe5d3de0fa15f594659e83f792fff19b5062cbc430ea0e9cf465377cdfaab8845467ac1adbe98dcfa9ea51aab9f87f1ae3ca912bf37e541afd86007c5bde3e5311", - "nonce": "0x16b", - "chainId": "0x2328" - }, - "additionalContracts": [], - "isFixedGasLimit": false - }, - { - "hash": "0xb810c6065c9e9cfe719f60acbbb547d2cab7a766fc72b83a3183924ed7546342", - "transactionType": "CREATE", - "contractName": "SkurFactory", - "contractAddress": "0x018c12ba8b085da81e434b9e4001e07cf48764e9", - "function": null, - "arguments": [ - "0xE21C7ad3a56D187848F9915bb43f88820278DeA3" - ], - "transaction": { - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": null, - "gas": "0xb2b99", - "value": "0x0", - "input": "0x60a03461008e57601f6109c938819003918201601f19168301916001600160401b038311848410176100925780849260209460405283398101031261008e57516001600160a01b038116810361008e57803b1561007f5760805260405161092290816100a782396080518181816101fc0152818161033401526107e90152f35b63c99547ad60e01b5f5260045ffd5b5f80fd5b634e487b7160e01b5f52604160045260245ffdfe6080806040526004361015610012575f80fd5b5f905f3560e01c908163089ec70e14610783575080635492a6ec1461022b5780635c60da1b146101e6578063652b9b41146101a75780636cc811f8146100e05780638c13a4531461008a5763a7c6a1001461006b575f80fd5b3461008757806003193601126100875760209054604051908152f35b80fd5b5034610087576020366003190112610087576004359080548210156100cc5780805260208082209092015460405160039290921b1c6001600160a01b03168152f35b634e487b7160e01b81526032600452602490fd5b5034610087576020366003190112610087576001600160a01b0361010261084c565b168152600260205260408120604051908160208254918281520190819285526020852090855b818110610188575050508261013e910383610893565b604051928392602084019060208552518091526040840192915b818110610166575050500390f35b82516001600160a01b0316845285945060209384019390920191600101610158565b82546001600160a01b0316845260209093019260019283019201610128565b50346100875760203660031901126100875760209060ff906040906001600160a01b036101d261084c565b168152600184522054166040519015158152f35b50346100875780600319360112610087576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b34610468576102c03660031901126104685760043560243567ffffffffffffffff811161046857610260903690600401610862565b919060443567ffffffffffffffff811161046857610282903690600401610862565b9390610220366063190112610468576102843567ffffffffffffffff8111610468576102b2903690600401610862565b6102a435929167ffffffffffffffff841161046857366023850112156104685783600401359267ffffffffffffffff84116104685736602460a0860287010111610468576040805133602082019081529181018a905261031f81606081015b03601f198101835282610893565b5190206e5af43d82803e903d91602b57fd5bf37f0000000000000000000000000000000000000000000000000000000000000000763d602d80600a3d3981f3363d3d373d3d3d363d7300000062ffffff8260881c16175f5260781b1760205260018060a01b0390603760095ff51698891561077457895f52600160205260405f20600160ff198254161790555f54600160401b811015610454578060016103c992015f555f6108b5565b81546001600160a01b0360039290921b91821b1916908c901b1790555f5b8781101561046c57600581901b8901356001600160a01b03811690819003610468575f52600260205260405f20908154600160401b81101561045457610435816001948f93860181556108b5565b819291549060031b91821b91858060a01b03901b1916179055016103e7565b634e487b7160e01b5f52604160045260245ffd5b5f80fd5b50899697895f54905f19820191821161076057604051908152897fc18dea5f6b30c3fd606e8e38832933e2fa57407dd77800aad0658505ee94df9860203393a4873b1561046857604051632d8a98d960e01b81526102a06004820152986020916104db916102a48c01916108de565b8981036003190160248b01528281520191905f905b80821061073b5750505060643560ff811680910361046857604488015260843560ff811680910361046857606488015260a43560ff811680910361046857608488015260c43560ff81168091036104685760a488015260e43560ff81168091036104685760c4880152610104358015158091036104685760e48801526101243563ffffffff8116809103610468576101048801526101443563ffffffff8116809103610468576101248801526101643563ffffffff8116809103610468576101448801526101843563ffffffff8116809103610468576101648801526101a43563ffffffff8116809103610468576101848801526101c43563ffffffff8116809103610468576101a48801526101e43561ffff8116809103610468576101c48801526102043561ffff8116809103610468576101e48801526102243561ffff8116809103610468576102048801526102443561ffff811680910361046857610224880152610264359063ffffffff82168092036104685761068d90889695946024946020946102448a0152600319898403016102648a01526108de565b85810360031901610284870152838152019201905f905b8082106106eb5750505090805f92038183855af19182156106e0576020926106d0575b50604051908152f35b5f6106da91610893565b5f6106c7565b6040513d5f823e3d90fd5b919350918335908115158092036104685760a081600193829352602087013560208201526040870135604082015260608701356060820152608087013560808201520194019201859392916106a4565b90919283359060ff8216809203610468576020816001938293520194019201906104f0565b634e487b7160e01b5f52601160045260245ffd5b63b06ebf3d60e01b5f5260045ffd5b346104685760403660031901126104685760209061079f61084c565b6001600160a01b03168282019081526024356020820152906107c48160408401610311565b519020604051903060388301526f5af43d82803e903d91602b57fd5bf3ff60248301527f00000000000000000000000000000000000000000000000000000000000000006014830152733d602d80600a3d3981f3363d3d373d3d3d363d73825260588201526037600c82012060788201526055604360018060a01b0392012016604051908152f35b600435906001600160a01b038216820361046857565b9181601f840112156104685782359167ffffffffffffffff8311610468576020808501948460051b01011161046857565b90601f8019910116810190811067ffffffffffffffff82111761045457604052565b80548210156108ca575f5260205f2001905f90565b634e487b7160e01b5f52603260045260245ffd5b916020908281520191905f905b8082106108f85750505090565b90919283359060018060a01b038216809203610468576020816001938293520194019201906108eb56000000000000000000000000e21c7ad3a56d187848f9915bb43f88820278dea3", - "nonce": "0x16c", - "chainId": "0x2328" - }, - "additionalContracts": [], - "isFixedGasLimit": false - }, - { - "hash": "0xef612541e68ecf1ad41453e65fe9df94a329a416436f769df8774bde63c3446c", - "transactionType": "CALL", - "contractName": "SkurFactory", - "contractAddress": "0x018c12ba8b085da81e434b9e4001e07cf48764e9", - "function": "createVault(bytes32,address[],uint8[],(uint8,uint8,uint8,uint8,uint8,bool,uint32,uint32,uint32,uint32,uint32,uint32,uint16,uint16,uint16,uint16,uint32),address[],(bool,uint256,uint256,uint256,uint256)[])", - "arguments": [ - "0xfb983e9dbbaa3ef70fb8bb403a3f4b14671884646e3181a121177cf14b3664eb", - "[0x4FEA262782b5BE34B18cf24b6aC026215899EEc0, 0xB6685D046E34A3260b9098aA6073CD7867e2de1C, 0xCec10C48B8d37b6A083000173e3415Cc6f86974F]", - "[7, 2, 8]", - "(1, 2, 2, 1, 1, true, 21600, 86400, 86400, 86400, 172800, 604800, 500, 2000, 5000, 1000, 86400)", - "[0x3e41424ADF6Bb69B6271ef9B0541ACD59A0d454a, 0x0000000000000000000000000000000000000000]", - "[(true, 5000000000, 25000000000, 100000000000, 100000000000), (true, 100000000000000000000, 1000000000000000000000, 0, 5000000000000000000000)]" - ], - "transaction": { - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": "0x018c12ba8b085da81e434b9e4001e07cf48764e9", - "gas": "0x149bca", - "value": "0x0", - "input": "0x5492a6ecfb983e9dbbaa3ef70fb8bb403a3f4b14671884646e3181a121177cf14b3664eb00000000000000000000000000000000000000000000000000000000000002c000000000000000000000000000000000000000000000000000000000000003400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000005460000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000000000000000015180000000000000000000000000000000000000000000000000000000000002a3000000000000000000000000000000000000000000000000000000000000093a8000000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000007d0000000000000000000000000000000000000000000000000000000000000138800000000000000000000000000000000000000000000000000000000000003e8000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000000000000000000000000000000030000000000000000000000004fea262782b5be34b18cf24b6ac026215899eec0000000000000000000000000b6685d046e34a3260b9098aa6073cd7867e2de1c000000000000000000000000cec10c48b8d37b6a083000173e3415cc6f86974f000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000070000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000020000000000000000000000003e41424adf6bb69b6271ef9b0541acd59a0d454a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000012a05f20000000000000000000000000000000000000000000000000000000005d21dba00000000000000000000000000000000000000000000000000000000174876e800000000000000000000000000000000000000000000000000000000174876e80000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000056bc75e2d6310000000000000000000000000000000000000000000000000003635c9adc5dea00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010f0cf064dd59200000", - "nonce": "0x16d", - "chainId": "0x2328" - }, - "additionalContracts": [ - { - "transactionType": "CREATE2", - "contractName": null, - "address": "0xcc31c7474267ca5600c2d530ebd7657a09c042dc", - "initCode": "0x3d602d80600a3d3981f3363d3d373d3d3d363d73e21c7ad3a56d187848f9915bb43f88820278dea35af43d82803e903d91602b57fd5bf3" - } - ], - "isFixedGasLimit": false - }, - { - "hash": "0x2de910f0c6733ab9cf2edd1dba34f0d8cacde340a5262e5f32ab20a487b78db9", - "transactionType": "CALL", - "contractName": "SkurTestUSD", - "contractAddress": "0x3e41424adf6bb69b6271ef9b0541acd59a0d454a", - "function": "mint(address,uint256)", - "arguments": [ - "0xCC31c7474267ca5600c2D530ebD7657A09c042Dc", - "250000000000" - ], - "transaction": { - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": "0x3e41424adf6bb69b6271ef9b0541acd59a0d454a", - "gas": "0x184fc", - "value": "0x0", - "input": "0x40c10f19000000000000000000000000cc31c7474267ca5600c2d530ebd7657a09c042dc0000000000000000000000000000000000000000000000000000003a35294400", - "nonce": "0x16e", - "chainId": "0x2328" - }, - "additionalContracts": [], - "isFixedGasLimit": false - }, - { - "hash": "0x63e02bb87ddcb55dc346f13fed3a4f3648901a22e54269bbfa08c739280e1979", - "transactionType": "CALL", - "contractName": null, - "contractAddress": "0xcc31c7474267ca5600c2d530ebd7657a09c042dc", - "function": null, - "arguments": null, - "transaction": { - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": "0xcc31c7474267ca5600c2d530ebd7657a09c042dc", - "gas": "0x10197", - "value": "0x1bc16d674ec80000", - "input": "0x", - "nonce": "0x16f", - "chainId": "0x2328" - }, - "additionalContracts": [], - "isFixedGasLimit": false - } - ], - "receipts": [ - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0xb53a1", - "logs": [], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "transactionHash": "0x5ec40a2bac9472f8b4d869940f032896d94240d509d887f4c34cd7b7691a2e76", - "transactionIndex": "0x0", - "blockHash": "0x9d9d8e38f8fef0d8e3b84527b9ba363fbda49a4dee860358878444244f06bd6a", - "blockNumber": "0x75aa9", - "gasUsed": "0xb53a1", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": null, - "contractAddress": "0x6baa1a24b55796d8346748ee3565e40d844ba576" - }, - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0x75981", - "logs": [], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "transactionHash": "0x1837db1649c1303b313f6d8ba6cb8df1c0aa2f840fd10ce133ae96aa6d460935", - "transactionIndex": "0x0", - "blockHash": "0x3189719aed5d3b21870b8d30c87a6f3b154cf561256cdd92814321647f0eb991", - "blockNumber": "0x75aab", - "gasUsed": "0x75981", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": null, - "contractAddress": "0x3e41424adf6bb69b6271ef9b0541acd59a0d454a" - }, - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0x4b3636", - "logs": [], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "transactionHash": "0x7009b1ae1b36af481edcaa8989b1243b9f1ff770649e7a5a27c0f6f522b28396", - "transactionIndex": "0x0", - "blockHash": "0xb81f91fec023c346589f72def43142e29ba9ec463523519e5710a3e78943d0b8", - "blockNumber": "0x75aad", - "gasUsed": "0x4b3636", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": null, - "contractAddress": "0xe21c7ad3a56d187848f9915bb43f88820278dea3" - }, - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0x897b1", - "logs": [], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "transactionHash": "0xb810c6065c9e9cfe719f60acbbb547d2cab7a766fc72b83a3183924ed7546342", - "transactionIndex": "0x0", - "blockHash": "0x001cf2924909c2331e1dff4384be3331d5864e1a03266824264376ec85d239ce", - "blockNumber": "0x75aaf", - "gasUsed": "0x897b1", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": null, - "contractAddress": "0x018c12ba8b085da81e434b9e4001e07cf48764e9" - }, - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0xe1761", - "logs": [ - { - "address": "0x018c12ba8b085da81e434b9e4001e07cf48764e9", - "topics": [ - "0xc18dea5f6b30c3fd606e8e38832933e2fa57407dd77800aad0658505ee94df98", - "0x000000000000000000000000cc31c7474267ca5600c2d530ebd7657a09c042dc", - "0x0000000000000000000000004fea262782b5be34b18cf24b6ac026215899eec0", - "0x0000000000000000000000000000000000000000000000000000000000000000" - ], - "data": "0xfb983e9dbbaa3ef70fb8bb403a3f4b14671884646e3181a121177cf14b3664eb", - "blockHash": "0x83447b317c5cf5fcf9b1c286c6c091f71ce06206dd2f7149f42c3981cd4042c5", - "blockNumber": "0x75ab1", - "blockTimestamp": "0x6aa90de8", - "transactionHash": "0xef612541e68ecf1ad41453e65fe9df94a329a416436f769df8774bde63c3446c", - "transactionIndex": "0x0", - "logIndex": "0x0", - "removed": false - }, - { - "address": "0xcc31c7474267ca5600c2d530ebd7657a09c042dc", - "topics": [ - "0x5d3de0fa15f594659e83f792fff19b5062cbc430ea0e9cf465377cdfaab88454", - "0x0000000000000000000000004fea262782b5be34b18cf24b6ac026215899eec0" - ], - "data": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007", - "blockHash": "0x83447b317c5cf5fcf9b1c286c6c091f71ce06206dd2f7149f42c3981cd4042c5", - "blockNumber": "0x75ab1", - "blockTimestamp": "0x6aa90de8", - "transactionHash": "0xef612541e68ecf1ad41453e65fe9df94a329a416436f769df8774bde63c3446c", - "transactionIndex": "0x0", - "logIndex": "0x1", - "removed": false - }, - { - "address": "0xcc31c7474267ca5600c2d530ebd7657a09c042dc", - "topics": [ - "0x5d3de0fa15f594659e83f792fff19b5062cbc430ea0e9cf465377cdfaab88454", - "0x000000000000000000000000b6685d046e34a3260b9098aa6073cd7867e2de1c" - ], - "data": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002", - "blockHash": "0x83447b317c5cf5fcf9b1c286c6c091f71ce06206dd2f7149f42c3981cd4042c5", - "blockNumber": "0x75ab1", - "blockTimestamp": "0x6aa90de8", - "transactionHash": "0xef612541e68ecf1ad41453e65fe9df94a329a416436f769df8774bde63c3446c", - "transactionIndex": "0x0", - "logIndex": "0x2", - "removed": false - }, - { - "address": "0xcc31c7474267ca5600c2d530ebd7657a09c042dc", - "topics": [ - "0x5d3de0fa15f594659e83f792fff19b5062cbc430ea0e9cf465377cdfaab88454", - "0x000000000000000000000000cec10c48b8d37b6a083000173e3415cc6f86974f" - ], - "data": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008", - "blockHash": "0x83447b317c5cf5fcf9b1c286c6c091f71ce06206dd2f7149f42c3981cd4042c5", - "blockNumber": "0x75ab1", - "blockTimestamp": "0x6aa90de8", - "transactionHash": "0xef612541e68ecf1ad41453e65fe9df94a329a416436f769df8774bde63c3446c", - "transactionIndex": "0x0", - "logIndex": "0x3", - "removed": false - }, - { - "address": "0xcc31c7474267ca5600c2d530ebd7657a09c042dc", - "topics": [ - "0x9d182ff993316b1c662980e27e42d42fae0bd158f1bed04e9988ec637fea8da4", - "0x0000000000000000000000003e41424adf6bb69b6271ef9b0541acd59a0d454a" - ], - "data": "0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000012a05f20000000000000000000000000000000000000000000000000000000005d21dba00000000000000000000000000000000000000000000000000000000174876e800000000000000000000000000000000000000000000000000000000174876e800", - "blockHash": "0x83447b317c5cf5fcf9b1c286c6c091f71ce06206dd2f7149f42c3981cd4042c5", - "blockNumber": "0x75ab1", - "blockTimestamp": "0x6aa90de8", - "transactionHash": "0xef612541e68ecf1ad41453e65fe9df94a329a416436f769df8774bde63c3446c", - "transactionIndex": "0x0", - "logIndex": "0x4", - "removed": false - }, - { - "address": "0xcc31c7474267ca5600c2d530ebd7657a09c042dc", - "topics": [ - "0x9d182ff993316b1c662980e27e42d42fae0bd158f1bed04e9988ec637fea8da4", - "0x0000000000000000000000000000000000000000000000000000000000000000" - ], - "data": "0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000056bc75e2d6310000000000000000000000000000000000000000000000000003635c9adc5dea00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010f0cf064dd59200000", - "blockHash": "0x83447b317c5cf5fcf9b1c286c6c091f71ce06206dd2f7149f42c3981cd4042c5", - "blockNumber": "0x75ab1", - "blockTimestamp": "0x6aa90de8", - "transactionHash": "0xef612541e68ecf1ad41453e65fe9df94a329a416436f769df8774bde63c3446c", - "transactionIndex": "0x0", - "logIndex": "0x5", - "removed": false - }, - { - "address": "0xcc31c7474267ca5600c2d530ebd7657a09c042dc", - "topics": [ - "0x1a734774792c149a91deb03650b2a2bc51de7bbab7262857ace6fb1699b43041", - "0x0000000000000000000000000000000000000000000000000000000000000001" - ], - "data": "0x000000000000000000000000000000000000000000000000000000006aa90de80000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000005460000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000000000000000015180000000000000000000000000000000000000000000000000000000000002a3000000000000000000000000000000000000000000000000000000000000093a8000000000000000000000000000000000000000000000000000000000000001f400000000000000000000000000000000000000000000000000000000000007d0000000000000000000000000000000000000000000000000000000000000138800000000000000000000000000000000000000000000000000000000000003e80000000000000000000000000000000000000000000000000000000000015180", - "blockHash": "0x83447b317c5cf5fcf9b1c286c6c091f71ce06206dd2f7149f42c3981cd4042c5", - "blockNumber": "0x75ab1", - "blockTimestamp": "0x6aa90de8", - "transactionHash": "0xef612541e68ecf1ad41453e65fe9df94a329a416436f769df8774bde63c3446c", - "transactionIndex": "0x0", - "logIndex": "0x6", - "removed": false - }, - { - "address": "0xcc31c7474267ca5600c2d530ebd7657a09c042dc", - "topics": [ - "0x4488062cfafbbb2c1d440f13796e120c4522761f74ecade440ee8f31f1bfbee0" - ], - "data": "0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000003", - "blockHash": "0x83447b317c5cf5fcf9b1c286c6c091f71ce06206dd2f7149f42c3981cd4042c5", - "blockNumber": "0x75ab1", - "blockTimestamp": "0x6aa90de8", - "transactionHash": "0xef612541e68ecf1ad41453e65fe9df94a329a416436f769df8774bde63c3446c", - "transactionIndex": "0x0", - "logIndex": "0x7", - "removed": false - } - ], - "logsBloom": "0x00100000000080000000000000000004080000000000000000000000000000000000040000020000000000040000000000000012010008000800080000040400000000040000000000000000000000800000000000040000000000000000000000000000020400000000000000000800080000000000000000000000000080020000800000080000000800000000000000002000000000000000000000000000000010800000000001000000000000000010000000008000000000000000000000000000000008000400000000000000000000000000000000000002000060000000000400000000000000000000000000000000000410000000400000000000", - "transactionHash": "0xef612541e68ecf1ad41453e65fe9df94a329a416436f769df8774bde63c3446c", - "transactionIndex": "0x0", - "blockHash": "0x83447b317c5cf5fcf9b1c286c6c091f71ce06206dd2f7149f42c3981cd4042c5", - "blockNumber": "0x75ab1", - "gasUsed": "0xe1761", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": "0x018c12ba8b085da81e434b9e4001e07cf48764e9", - "contractAddress": null - }, - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0x109f9", - "logs": [ - { - "address": "0x3e41424adf6bb69b6271ef9b0541acd59a0d454a", - "topics": [ - "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x000000000000000000000000cc31c7474267ca5600c2d530ebd7657a09c042dc" - ], - "data": "0x0000000000000000000000000000000000000000000000000000003a35294400", - "blockHash": "0x52755d37993fe83b34e2babbfbf9b31b43a63a86258b1363ad387056c4a51d1b", - "blockNumber": "0x75ab3", - "blockTimestamp": "0x6aa90dee", - "transactionHash": "0x2de910f0c6733ab9cf2edd1dba34f0d8cacde340a5262e5f32ab20a487b78db9", - "transactionIndex": "0x0", - "logIndex": "0x0", - "removed": false - } - ], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000010000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000020000000000000000000800000000000000000000000012000000004100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000400000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000", - "transactionHash": "0x2de910f0c6733ab9cf2edd1dba34f0d8cacde340a5262e5f32ab20a487b78db9", - "transactionIndex": "0x0", - "blockHash": "0x52755d37993fe83b34e2babbfbf9b31b43a63a86258b1363ad387056c4a51d1b", - "blockNumber": "0x75ab3", - "gasUsed": "0x109f9", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": "0x3e41424adf6bb69b6271ef9b0541acd59a0d454a", - "contractAddress": null - }, - { - "type": "0x2", - "status": "0x1", - "cumulativeGasUsed": "0xba7f", - "logs": [ - { - "address": "0xcc31c7474267ca5600c2d530ebd7657a09c042dc", - "topics": [ - "0x5548c837ab068cf56a2c2479df0882a4922fd203edb7517321831d95078c5f62", - "0x0000000000000000000000004fea262782b5be34b18cf24b6ac026215899eec0", - "0x0000000000000000000000000000000000000000000000000000000000000000" - ], - "data": "0x0000000000000000000000000000000000000000000000001bc16d674ec80000", - "blockHash": "0x0bc9c246a511471da7a287d656ab83e66fc9c22a798752f1f5815fe784a62a4a", - "blockNumber": "0x75ab5", - "blockTimestamp": "0x6aa90df7", - "transactionHash": "0x63e02bb87ddcb55dc346f13fed3a4f3648901a22e54269bbfa08c739280e1979", - "transactionIndex": "0x0", - "logIndex": "0x0", - "removed": false - } - ], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000002000000080040000000000000040008000000000000000000000000000000000000000000000000000000000000020000000000000000000800000000000000000000000000000080020000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000", - "transactionHash": "0x63e02bb87ddcb55dc346f13fed3a4f3648901a22e54269bbfa08c739280e1979", - "transactionIndex": "0x0", - "blockHash": "0x0bc9c246a511471da7a287d656ab83e66fc9c22a798752f1f5815fe784a62a4a", - "blockNumber": "0x75ab5", - "gasUsed": "0xba7f", - "effectiveGasPrice": "0x3b9aca01", - "from": "0x4fea262782b5be34b18cf24b6ac026215899eec0", - "to": "0xcc31c7474267ca5600c2d530ebd7657a09c042dc", - "contractAddress": null - } - ], - "libraries": [ - "src/SkurPolicyLib.sol:SkurPolicyLib:0x6bAA1A24B55796d8346748EE3565e40d844bA576" - ], - "pending": [], - "returns": {}, - "timestamp": 1789464061730, - "chain": 9000, - "commit": "d25692a" -} \ No newline at end of file diff --git a/contracts/deployments/9000.json b/contracts/deployments/9000.json deleted file mode 100644 index ec54f6a..0000000 --- a/contracts/deployments/9000.json +++ /dev/null @@ -1 +0,0 @@ -{"chainId":9000,"testUsd":"0x3e41424ADF6Bb69B6271ef9B0541ACD59A0d454a","factory":"0x018c12BA8b085da81E434b9e4001e07CF48764E9","vaultImplementation":"0xE21C7ad3a56D187848F9915bb43f88820278DeA3","demoVault":"0xCC31c7474267ca5600c2D530ebD7657A09c042Dc","deployer":"0x4FEA262782b5BE34B18cf24b6aC026215899EEc0","deployedAtBlock":481946} \ No newline at end of file diff --git a/contracts/foundry.lock b/contracts/foundry.lock deleted file mode 100644 index f302bde..0000000 --- a/contracts/foundry.lock +++ /dev/null @@ -1,14 +0,0 @@ -{ - "lib/forge-std": { - "tag": { - "name": "v1.16.2", - "rev": "bf647bd6046f2f7da30d0c2bf435e5c76a780c1b" - } - }, - "lib/openzeppelin-contracts": { - "tag": { - "name": "v5.1.0", - "rev": "69c8def5f222ff96f2b5beff05dfba996368aa79" - } - } -} \ No newline at end of file diff --git a/contracts/foundry.toml b/contracts/foundry.toml deleted file mode 100644 index 304e48e..0000000 --- a/contracts/foundry.toml +++ /dev/null @@ -1,44 +0,0 @@ -[profile.default] -src = "src" -out = "out" -libs = ["lib"] -test = "test" -script = "script" -solc_version = "0.8.28" -evm_version = "cancun" -optimizer = true -optimizer_runs = 200 -via_ir = true -bytecode_hash = "none" -cbor_metadata = false -fs_permissions = [{ access = "read-write", path = "./deployments" }] - -[profile.default.fuzz] -runs = 512 - -[profile.default.invariant] -runs = 64 -depth = 48 -fail_on_revert = false - -[profile.ci.fuzz] -runs = 4096 - -[profile.ci.invariant] -runs = 256 -depth = 96 - -# Every endpoint lives here and in web/src/config/chain.ts. The sslip.io hosts are -# devnet-only and will move to an Ark-owned domain for testnet/mainnet. -[rpc_endpoints] -ark_devnet = "https://evm.34.60.137.196.sslip.io" - -[etherscan] -ark_devnet = { key = "blockscout", url = "https://explorer-api.34.60.137.196.sslip.io/api", chain = 9000 } - -[fmt] -line_length = 120 -tab_width = 4 - -[lint] -exclude_lints = ["block-timestamp"] diff --git a/contracts/lib/forge-std b/contracts/lib/forge-std deleted file mode 160000 index bf647bd..0000000 --- a/contracts/lib/forge-std +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bf647bd6046f2f7da30d0c2bf435e5c76a780c1b diff --git a/contracts/lib/openzeppelin-contracts b/contracts/lib/openzeppelin-contracts deleted file mode 160000 index 69c8def..0000000 --- a/contracts/lib/openzeppelin-contracts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 69c8def5f222ff96f2b5beff05dfba996368aa79 diff --git a/contracts/script/Deploy.s.sol b/contracts/script/Deploy.s.sol deleted file mode 100644 index 9373a39..0000000 --- a/contracts/script/Deploy.s.sol +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.28; - -import {Script, console} from "forge-std/Script.sol"; -import "../src/SkurTypes.sol"; -import {SkurFactory} from "../src/SkurFactory.sol"; -import {SkurVault} from "../src/SkurVault.sol"; -import {SkurTestUSD} from "../src/SkurTestUSD.sol"; -import {SkurTemplates} from "./SkurTemplates.sol"; - -/// @notice Deploys the test stablecoin, the vault implementation (via the factory) and the factory. -/// Optionally creates a demo vault when DEMO_VAULT=true so the interface has something to read. -/// @dev forge script script/Deploy.s.sol --rpc-url ark_devnet --broadcast --private-key $PK -contract Deploy is Script { - function run() external { - uint256 pk = vm.envUint("PRIVATE_KEY"); - address deployer = vm.addr(pk); - bool demo = vm.envOr("DEMO_VAULT", false); - - vm.startBroadcast(pk); - SkurTestUSD usd = new SkurTestUSD(); - SkurVault implementation = new SkurVault(); - SkurFactory factory = new SkurFactory(address(implementation)); - - address vault; - if (demo) { - // Demo vault: deployer is owner+approver+executor; two extra signers and a guardian are derived from the key. - address approver2 = vm.addr(uint256(keccak256(abi.encode(pk, "approver2")))); - address guardian = vm.addr(uint256(keccak256(abi.encode(pk, "guardian")))); - - address[] memory members = new address[](3); - uint8[] memory roles = new uint8[](3); - members[0] = deployer; - roles[0] = ROLE_OWNER | ROLE_APPROVER | ROLE_EXECUTOR; - members[1] = approver2; - roles[1] = ROLE_APPROVER; - members[2] = guardian; - roles[2] = ROLE_GUARDIAN; - - (address[] memory assets, AssetLimits[] memory limits) = SkurTemplates.startupAssets(address(usd)); - Policy memory policy = SkurTemplates.startup(); - // One-owner demo: governance threshold must be satisfiable by the member set. - policy.governanceThreshold = 1; - - vault = factory.createVault(keccak256("skur-demo-vault"), members, roles, policy, assets, limits); - usd.mint(vault, 250_000 * 1e6); - (bool ok,) = vault.call{value: 2 ether}(""); - require(ok, "fund failed"); - } - vm.stopBroadcast(); - - console.log("SkurTestUSD:", address(usd)); - console.log("SkurFactory:", address(factory)); - console.log("SkurVault implementation:", factory.implementation()); - if (demo) console.log("Demo vault:", vault); - - string memory json = string.concat( - '{"chainId":', - vm.toString(block.chainid), - ',"testUsd":"', - vm.toString(address(usd)), - '","factory":"', - vm.toString(address(factory)), - '","vaultImplementation":"', - vm.toString(factory.implementation()), - '","demoVault":"', - vm.toString(vault), - '","deployer":"', - vm.toString(deployer), - '","deployedAtBlock":', - vm.toString(block.number), - "}" - ); - vm.writeFile(string.concat("deployments/", vm.toString(block.chainid), ".json"), json); - } -} diff --git a/contracts/script/SkurTemplates.sol b/contracts/script/SkurTemplates.sol deleted file mode 100644 index 2b24d7d..0000000 --- a/contracts/script/SkurTemplates.sol +++ /dev/null @@ -1,136 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.28; - -import "../src/SkurTypes.sol"; - -/// @title Skur policy templates -/// @notice The same six templates the interface offers, so tests and deploy scripts never design a policy -/// from first principles. Stablecoin amounts assume a 6-decimal token; native amounts assume 18 decimals. -/// @dev Mirror of web/src/lib/templates.ts. Keep both in sync; test/SkurTemplates.t.sol validates each one. -library SkurTemplates { - uint256 internal constant USD = 1e6; - uint256 internal constant KASH = 1e18; - - /// Startup: simple thresholds, new-recipient delay, one guardian. - function startup() internal pure returns (Policy memory p) { - p.approvalsLow = 1; - p.approvalsHigh = 2; - p.approvalsCritical = 2; - p.governanceThreshold = 2; - p.guardianThreshold = 1; - p.guardianRequiredCritical = true; - p.delayHigh = 6 hours; - p.delayCritical = 24 hours; - p.recipientActivationDelay = 24 hours; - p.policyChangeDelay = 24 hours; - p.recoveryDelay = 48 hours; - p.proposalTtl = 7 days; - p.highExposureBps = 500; // 5% - p.criticalExposureBps = 2000; // 20% - p.hardBlockExposureBps = 5000; // 50% - p.envelopeBps = 1000; // 10% of holdings per 24h - p.envelopeWindow = 24 hours; - } - - /// SME: tiered approvals, velocity limits, supplier trust. - function sme() internal pure returns (Policy memory p) { - p = startup(); - p.approvalsLow = 1; - p.approvalsHigh = 2; - p.approvalsCritical = 3; - p.governanceThreshold = 2; - p.delayHigh = 4 hours; - p.delayCritical = 24 hours; - p.envelopeBps = 500; // 5% per 24h - } - - /// Protocol / DAO: higher quorum, timelocks. - function dao() internal pure returns (Policy memory p) { - p = startup(); - p.approvalsLow = 2; - p.approvalsHigh = 3; - p.approvalsCritical = 4; - p.governanceThreshold = 3; - p.guardianThreshold = 2; - p.delayHigh = 24 hours; - p.delayCritical = 72 hours; - p.policyChangeDelay = 72 hours; - p.recoveryDelay = 7 days; - p.proposalTtl = 14 days; - p.highExposureBps = 200; // 2% - p.criticalExposureBps = 1000; // 10% - p.hardBlockExposureBps = 2500; - p.envelopeBps = 500; - } - - /// Fund: exposure limits, strong guardians, long critical delays. - function fund() internal pure returns (Policy memory p) { - p = startup(); - p.approvalsLow = 2; - p.approvalsHigh = 3; - p.approvalsCritical = 3; - p.governanceThreshold = 2; - p.guardianThreshold = 2; - p.delayHigh = 12 hours; - p.delayCritical = 72 hours; - p.recipientActivationDelay = 48 hours; - p.policyChangeDelay = 72 hours; - p.recoveryDelay = 7 days; - p.proposalTtl = 14 days; - p.highExposureBps = 100; // 1% - p.criticalExposureBps = 500; // 5% - p.hardBlockExposureBps = 2000; - p.envelopeBps = 300; // 3% per 24h - } - - /// Nonprofit: restricted recipients, approval separation, auditability. - function nonprofit() internal pure returns (Policy memory p) { - p = startup(); - p.approvalsLow = 2; - p.approvalsHigh = 2; - p.approvalsCritical = 3; - p.governanceThreshold = 2; - p.recipientActivationDelay = 72 hours; - p.delayHigh = 12 hours; - p.delayCritical = 48 hours; - p.envelopeBps = 500; - } - - /// Family office: conservative limits, cold guardian, recovery policy. - function familyOffice() internal pure returns (Policy memory p) { - p = startup(); - p.approvalsLow = 1; - p.approvalsHigh = 2; - p.approvalsCritical = 2; - p.governanceThreshold = 2; - p.guardianThreshold = 1; - p.delayHigh = 24 hours; - p.delayCritical = 72 hours; - p.recipientActivationDelay = 72 hours; - p.policyChangeDelay = 72 hours; - p.recoveryDelay = 14 days; - p.proposalTtl = 21 days; - p.highExposureBps = 100; - p.criticalExposureBps = 500; - p.hardBlockExposureBps = 1500; - p.envelopeBps = 200; // 2% per 24h - } - - /// Asset limits for the startup template: sUSD tiers plus a native KASH policy. - function startupAssets(address stable) - internal - pure - returns (address[] memory assets, AssetLimits[] memory limits) - { - assets = new address[](2); - limits = new AssetLimits[](2); - assets[0] = stable; - limits[0] = AssetLimits({ - approved: true, lowMax: 5_000 * USD, highMax: 25_000 * USD, perTxMax: 100_000 * USD, dailyMax: 100_000 * USD - }); - assets[1] = address(0); - limits[1] = AssetLimits({ - approved: true, lowMax: 100 * KASH, highMax: 1_000 * KASH, perTxMax: 0, dailyMax: 5_000 * KASH - }); - } -} diff --git a/contracts/src/ISkurVault.sol b/contracts/src/ISkurVault.sol deleted file mode 100644 index 0dd2c73..0000000 --- a/contracts/src/ISkurVault.sol +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.28; - -import "./SkurTypes.sol"; - -/// @title SkurVault events and errors -/// @notice Split out so tests, the factory and the interface ABI can import them without the implementation. -interface ISkurVault { - // ---------------------------------------------------------------- events - event VaultInitialized(uint32 policyVersion, uint256 memberCount); - event Deposit(address indexed from, address indexed asset, uint256 amount); - event MemberSet(address indexed member, uint8 previousRoles, uint8 roles); - event RecipientRegistered(address indexed recipient, uint64 activatesAt); - event RecipientTrustSet(address indexed recipient, Trust previous, Trust trust); - event AssetLimitsSet(address indexed asset, AssetLimits limits); - event PolicyActivated(uint32 indexed version, uint64 activatedAt, Policy policy); - event ModeChanged(Mode indexed previous, Mode indexed mode, address indexed by, bytes32 reason); - event CircuitBreakerTripped(address indexed asset, uint256 attempted, uint256 envelopeSpent, uint256 envelope); - - event ProposalCreated( - uint256 indexed id, - Kind indexed kind, - address indexed proposer, - address asset, - address target, - uint256 amount, - Tier tier, - uint16 riskReasons, - uint8 requiredApprovals, - uint8 requiredGuardians, - uint64 executableAfter, - uint64 expiresAt, - bool securityReducing, - string memo - ); - event ProposalApproved(uint256 indexed id, address indexed approver, uint256 approvals); - event ProposalGuardianConfirmed(uint256 indexed id, address indexed guardian, uint256 confirmations); - event ProposalExecuted(uint256 indexed id, address indexed executor); - event ProposalCancelled(uint256 indexed id, address indexed by); - event ProposalVetoed(uint256 indexed id, address indexed guardian, bytes32 reason); - event TransferExecuted(uint256 indexed id, address indexed asset, address indexed to, uint256 amount); - event RecoveryCompleted(uint256 indexed id, address indexed oldSigner, address indexed newSigner, uint8 roles); - - // ---------------------------------------------------------------- errors - error AlreadyInitialized(); - error NotAuthorized(uint8 requiredRole); - error ZeroAddress(); - error InvalidPolicy(string reason); - error InvalidRoles(); - error ThresholdUnsatisfiable(string reason); - error MemberExists(); - error NotMember(); - error AssetNotApproved(address asset); - error RecipientBlocked(address recipient); - error RecipientNotRegistered(); - error AmountZero(); - error PerTxLimitExceeded(uint256 amount, uint256 max); - error ExposureHardBlocked(uint16 exposureBps, uint16 maxBps); - error VelocityExceeded(uint256 wouldSpend, uint256 dailyMax); - error InsufficientBalance(uint256 amount, uint256 balance); - error VaultLocked(); - error UnknownProposal(); - error NotPending(); - error AlreadyApproved(); - error ProposalExpired(); - error Timelocked(uint64 executableAfter); - error InsufficientApprovals(uint256 have, uint256 need); - error InsufficientGuardians(uint256 have, uint256 need); - error NotVetoable(); - error NotCancellable(); - error ModeNotStricter(); - error ModeNotRelaxation(); - error NativeTransferFailed(); - error InvalidProposal(string reason); -} diff --git a/contracts/src/SkurFactory.sol b/contracts/src/SkurFactory.sol deleted file mode 100644 index 63bae21..0000000 --- a/contracts/src/SkurFactory.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.28; - -import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; -import "./SkurTypes.sol"; -import {SkurVault} from "./SkurVault.sol"; - -/// @title SkurFactory -/// @notice Creates and registers SkurVault instances as EIP-1167 minimal proxies over one immutable implementation. -/// @dev Vault logic is not upgradeable in V1. Migrating means creating a new vault and moving assets through -/// that vault's own policy. The factory holds no privileges over vaults it creates. -contract SkurFactory { - event VaultCreated(address indexed vault, address indexed creator, uint256 indexed index, bytes32 salt); - - error NotAVault(); - - address public immutable implementation; - address[] private _vaults; - mapping(address => bool) public isVault; - mapping(address => address[]) private _vaultsByMember; - - error NotImplementation(); - - /// @param impl A deployed SkurVault used as the logic contract for every clone. Deployed separately so - /// explorers index and verify it as a top-level contract. - constructor(address impl) { - if (impl.code.length == 0) revert NotImplementation(); - implementation = impl; - } - - /// @notice Deploy a vault. `salt` lets integrators precompute the address with `predictVaultAddress`. - function createVault( - bytes32 salt, - address[] calldata members, - uint8[] calldata roles, - Policy calldata policy, - address[] calldata assets, - AssetLimits[] calldata limits - ) external returns (address vault) { - bytes32 fullSalt = keccak256(abi.encode(msg.sender, salt)); - vault = Clones.cloneDeterministic(implementation, fullSalt); - isVault[vault] = true; - _vaults.push(vault); - for (uint256 i = 0; i < members.length; i++) { - _vaultsByMember[members[i]].push(vault); - } - emit VaultCreated(vault, msg.sender, _vaults.length - 1, salt); - SkurVault(payable(vault)).initialize(members, roles, policy, assets, limits); - } - - function predictVaultAddress(address creator, bytes32 salt) external view returns (address) { - return Clones.predictDeterministicAddress(implementation, keccak256(abi.encode(creator, salt)), address(this)); - } - - function vaultCount() external view returns (uint256) { - return _vaults.length; - } - - function vaultAt(uint256 index) external view returns (address) { - return _vaults[index]; - } - - /// @notice Vaults a member was part of at creation time. Later membership changes live on the vault itself. - function vaultsOf(address member) external view returns (address[] memory) { - return _vaultsByMember[member]; - } -} diff --git a/contracts/src/SkurPolicyLib.sol b/contracts/src/SkurPolicyLib.sol deleted file mode 100644 index bc6155a..0000000 --- a/contracts/src/SkurPolicyLib.sol +++ /dev/null @@ -1,93 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.28; - -import "./SkurTypes.sol"; -import {ISkurVault} from "./ISkurVault.sol"; - -/// @title Skur policy validation and security-reducing classification -/// @notice Pure rules that decide whether a policy is well-formed and whether a change weakens security. -/// @dev Deployed as an external library (linked) to keep SkurVault under the EIP-170 size limit. -/// The interface's policy editor mirrors `policyReduces` and `limitsReduce` in web/src/lib/policy.ts. -library SkurPolicyLib { - uint32 internal constant MIN_EXECUTION_WINDOW = 1 hours; - - function validatePolicy(Policy memory p) public pure { - if (p.approvalsLow == 0) revert ISkurVault.InvalidPolicy("approvalsLow"); - if (p.approvalsHigh < p.approvalsLow) revert ISkurVault.InvalidPolicy("approvalsHigh"); - if (p.approvalsCritical < p.approvalsHigh) revert ISkurVault.InvalidPolicy("approvalsCritical"); - if (p.governanceThreshold == 0) revert ISkurVault.InvalidPolicy("governanceThreshold"); - if (p.guardianRequiredCritical && p.guardianThreshold == 0) { - revert ISkurVault.InvalidPolicy("guardianThreshold"); - } - if (p.delayCritical < p.delayHigh) revert ISkurVault.InvalidPolicy("delayCritical < delayHigh"); - if (p.criticalExposureBps != 0 && p.highExposureBps > p.criticalExposureBps) { - revert ISkurVault.InvalidPolicy("exposure order"); - } - if (p.criticalExposureBps > BPS || p.highExposureBps > BPS) revert ISkurVault.InvalidPolicy("exposure bps"); - if (p.hardBlockExposureBps != 0 && p.hardBlockExposureBps < p.criticalExposureBps) { - revert ISkurVault.InvalidPolicy("hardBlock < critical"); - } - if (p.hardBlockExposureBps > BPS) revert ISkurVault.InvalidPolicy("hardBlock bps"); - if (p.envelopeBps > BPS) revert ISkurVault.InvalidPolicy("envelope bps"); - if (p.envelopeBps != 0 && p.envelopeWindow < MIN_EXECUTION_WINDOW) { - revert ISkurVault.InvalidPolicy("envelopeWindow"); - } - uint256 longest = p.delayCritical; - if (p.policyChangeDelay > longest) longest = p.policyChangeDelay; - if (p.recoveryDelay > longest) longest = p.recoveryDelay; - if (p.recipientActivationDelay > longest) longest = p.recipientActivationDelay; - if (p.proposalTtl < longest + MIN_EXECUTION_WINDOW) revert ISkurVault.InvalidPolicy("proposalTtl too short"); - } - - function validateLimits(AssetLimits memory l) public pure { - if (l.lowMax > l.highMax) revert ISkurVault.InvalidPolicy("lowMax > highMax"); - if (l.perTxMax != 0 && l.dailyMax != 0 && l.perTxMax > l.dailyMax) { - revert ISkurVault.InvalidPolicy("perTxMax > dailyMax"); - } - } - - /// @notice True when any field of `b` is weaker than `a`. Tightening and loosening in one change counts as loosening. - function policyReduces(Policy memory a, Policy memory b) public pure returns (bool) { - if (b.approvalsLow < a.approvalsLow) return true; - if (b.approvalsHigh < a.approvalsHigh) return true; - if (b.approvalsCritical < a.approvalsCritical) return true; - if (b.governanceThreshold < a.governanceThreshold) return true; - if (b.guardianThreshold < a.guardianThreshold) return true; - if (a.guardianRequiredCritical && !b.guardianRequiredCritical) return true; - if (b.delayHigh < a.delayHigh) return true; - if (b.delayCritical < a.delayCritical) return true; - if (b.recipientActivationDelay < a.recipientActivationDelay) return true; - if (b.policyChangeDelay < a.policyChangeDelay) return true; - if (b.recoveryDelay < a.recoveryDelay) return true; - if (b.proposalTtl > a.proposalTtl) return true; - if (thresholdLoosens(a.highExposureBps, b.highExposureBps)) return true; - if (thresholdLoosens(a.criticalExposureBps, b.criticalExposureBps)) return true; - if (thresholdLoosens(a.hardBlockExposureBps, b.hardBlockExposureBps)) return true; - if (thresholdLoosens(a.envelopeBps, b.envelopeBps)) return true; - if (a.envelopeBps != 0 && b.envelopeWindow < a.envelopeWindow) return true; - return false; - } - - function limitsReduce(AssetLimits memory a, AssetLimits memory b) public pure returns (bool) { - if (!a.approved && b.approved) return true; - if (b.lowMax > a.lowMax) return true; - if (b.highMax > a.highMax) return true; - if (capLoosens(a.perTxMax, b.perTxMax)) return true; - if (capLoosens(a.dailyMax, b.dailyMax)) return true; - return false; - } - - /// @dev "0 = disabled" thresholds: disabling, or raising a live threshold, loosens security. - function thresholdLoosens(uint256 a, uint256 b) internal pure returns (bool) { - if (a == 0) return false; - if (b == 0) return true; - return b > a; - } - - /// @dev "0 = unlimited" caps: enabling only tightens; disabling or raising loosens. - function capLoosens(uint256 a, uint256 b) internal pure returns (bool) { - if (a == 0) return false; - if (b == 0) return true; - return b > a; - } -} diff --git a/contracts/src/SkurRisk.sol b/contracts/src/SkurRisk.sol deleted file mode 100644 index e86987d..0000000 --- a/contracts/src/SkurRisk.sol +++ /dev/null @@ -1,105 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.28; - -import "./SkurTypes.sol"; - -/// @title Skur deterministic risk engine -/// @notice Pure classification of a proposed transfer into LOW / HIGH / CRITICAL from observable inputs. -/// @dev No storage, no randomness, no oracle. The TypeScript mirror in web/src/lib/risk.ts must stay identical; -/// test/SkurRisk.t.sol pins the vectors both implementations are checked against. -library SkurRisk { - struct Input { - uint256 amount; - uint256 assetBalance; // vault balance of the asset before the transfer - uint256 lowMax; - uint256 highMax; - uint256 dailyMax; // 0 = no cap - uint256 daySpent; // outflow already recorded in the current 24h bucket - Trust trust; - bool inProbation; // NEW recipient before activatesAt - Mode mode; - uint16 highExposureBps; - uint16 criticalExposureBps; - } - - /// @notice Exposure of a transfer as basis points of the current asset balance, capped at 100%. - function exposureBps(uint256 amount, uint256 balance) internal pure returns (uint16) { - if (amount == 0) return 0; - // casting to 'uint16' is safe because BPS is the constant 10_000, well inside uint16 - // forge-lint: disable-next-line(unsafe-typecast) - if (balance == 0 || amount >= balance) return uint16(BPS); - // casting to 'uint16' is safe because the branch above guarantees amount < balance, - // so the quotient is strictly below BPS (10_000) - // forge-lint: disable-next-line(unsafe-typecast) - return uint16((amount * BPS) / balance); - } - - /// @notice Deterministic tier + reasons. Never reverts; BLOCKED recipients are refused by the vault before this. - function classify(Input memory i) internal pure returns (Tier tier, uint16 reasons, uint16 exposure) { - tier = Tier.LOW; - - // 1. Amount tier - if (i.amount > i.highMax) { - tier = Tier.CRITICAL; - reasons |= REASON_AMOUNT_CRITICAL; - } else if (i.amount > i.lowMax) { - tier = Tier.HIGH; - reasons |= REASON_AMOUNT_HIGH; - } - - // 2. Treasury exposure (percentage of this asset's holdings) - exposure = exposureBps(i.amount, i.assetBalance); - if (i.criticalExposureBps != 0 && exposure >= i.criticalExposureBps) { - tier = Tier.CRITICAL; - reasons |= REASON_EXPOSURE_CRITICAL; - } else if (i.highExposureBps != 0 && exposure >= i.highExposureBps) { - tier = _atLeast(tier, Tier.HIGH); - reasons |= REASON_EXPOSURE_HIGH; - } - - // 3. Recipient trust - if (i.trust == Trust.RESTRICTED) { - tier = Tier.CRITICAL; - reasons |= REASON_RECIPIENT_RESTRICTED; - } else if (i.trust == Trust.UNKNOWN) { - // auto-registered on proposal creation; treated exactly like a NEW recipient in probation - tier = _bump(tier); - reasons |= REASON_RECIPIENT_UNKNOWN | REASON_RECIPIENT_PROBATION; - } else if (i.trust == Trust.NEW && i.inProbation) { - tier = _bump(tier); - reasons |= REASON_RECIPIENT_PROBATION; - } - - // 4. Velocity pressure: this transfer would push the 24h bucket past half of its cap - if (i.dailyMax != 0 && (i.daySpent + i.amount) * 2 > i.dailyMax) { - tier = _atLeast(tier, Tier.HIGH); - reasons |= REASON_VELOCITY_PRESSURE; - } - - // 5. Security posture - if (i.mode == Mode.ELEVATED) { - tier = _bump(tier); - reasons |= REASON_MODE_ELEVATED; - } - } - - /// @notice Requirements pinned to a proposal at creation for a given tier. - function requirements(Policy memory p, Tier tier) - internal - pure - returns (uint8 approvals, uint8 guardians, uint32 delay) - { - if (tier == Tier.LOW) return (p.approvalsLow, 0, 0); - if (tier == Tier.HIGH) return (p.approvalsHigh, 0, p.delayHigh); - return (p.approvalsCritical, p.guardianRequiredCritical ? p.guardianThreshold : 0, p.delayCritical); - } - - function _bump(Tier t) private pure returns (Tier) { - if (t == Tier.LOW) return Tier.HIGH; - return Tier.CRITICAL; - } - - function _atLeast(Tier t, Tier floor) private pure returns (Tier) { - return uint8(t) >= uint8(floor) ? t : floor; - } -} diff --git a/contracts/src/SkurTestUSD.sol b/contracts/src/SkurTestUSD.sol deleted file mode 100644 index b5d19a5..0000000 --- a/contracts/src/SkurTestUSD.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.28; - -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -/// @title Skur test stablecoin (devnet only) -/// @notice Ark devnet has no canonical USDC. This 6-decimal token stands in for the stablecoin that -/// exposure and velocity policies are denominated in. Anyone can mint on devnet. -/// @dev NEVER deploy this to a network with real value. Registering it as an approved asset on a vault -/// is itself a policy decision that goes through the policy-change firewall. -contract SkurTestUSD is ERC20 { - uint256 public constant MAX_MINT = 10_000_000 * 1e6; - - error MintTooLarge(); - - constructor() ERC20("Skur Test USD", "sUSD") {} - - function decimals() public pure override returns (uint8) { - return 6; - } - - function mint(address to, uint256 amount) external { - if (amount > MAX_MINT) revert MintTooLarge(); - _mint(to, amount); - } -} diff --git a/contracts/src/SkurTypes.sol b/contracts/src/SkurTypes.sol deleted file mode 100644 index 3437a0a..0000000 --- a/contracts/src/SkurTypes.sol +++ /dev/null @@ -1,141 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.28; - -// @title Skur shared types -// @notice Enums, structs and constants shared by SkurVault, SkurFactory, SkurRisk and tests. -// @dev Everything here is deterministic and auditable. No opaque scoring exists anywhere in V1. - -// Role bits. A member may hold several treasury roles, but GUARDIAN is exclusive: -// guardians never hold OWNER, APPROVER or EXECUTOR (independent control plane). -uint8 constant ROLE_OWNER = 1; -uint8 constant ROLE_APPROVER = 2; -uint8 constant ROLE_EXECUTOR = 4; -uint8 constant ROLE_GUARDIAN = 8; -uint8 constant ROLE_TREASURY_MASK = ROLE_OWNER | ROLE_APPROVER | ROLE_EXECUTOR; - -uint256 constant BPS = 10_000; - -// Risk reason flags (bitmask) so the interface can explain *why* a tier was assigned. -uint16 constant REASON_AMOUNT_HIGH = 1 << 0; -uint16 constant REASON_AMOUNT_CRITICAL = 1 << 1; -uint16 constant REASON_EXPOSURE_HIGH = 1 << 2; -uint16 constant REASON_EXPOSURE_CRITICAL = 1 << 3; -uint16 constant REASON_RECIPIENT_PROBATION = 1 << 4; -uint16 constant REASON_RECIPIENT_RESTRICTED = 1 << 5; -uint16 constant REASON_MODE_ELEVATED = 1 << 6; -uint16 constant REASON_VELOCITY_PRESSURE = 1 << 7; -uint16 constant REASON_RECIPIENT_UNKNOWN = 1 << 8; - -enum Mode { - NORMAL, - ELEVATED, - LOCKDOWN -} - -enum Tier { - LOW, - HIGH, - CRITICAL -} - -enum Trust { - UNKNOWN, // never seen by this vault - NEW, // registered, serving (or finished) activation delay - VERIFIED, - TRUSTED, - RESTRICTED, - BLOCKED -} - -enum Kind { - TRANSFER, // treasury plane: native KASH or approved ERC-20 out - POLICY_UPDATE, // governance plane - ASSET_LIMITS, // governance plane - MEMBER_SET, // governance plane - RECIPIENT_TRUST, // governance plane - MODE_RELAX, // governance plane + guardian confirmation - RECOVERY // guardian plane: replace a signer after a delay, owner-cancellable -} - -enum Status { - NONE, - PENDING, - EXECUTED, - CANCELLED, - VETOED -} - -// Vault-wide policy. Versioned; every activation is emitted onchain. -struct Policy { - uint8 approvalsLow; - uint8 approvalsHigh; - uint8 approvalsCritical; - uint8 governanceThreshold; // owner approvals for governance proposals - uint8 guardianThreshold; // guardian confirmations for critical transfers, mode relax, recovery - bool guardianRequiredCritical; - uint32 delayHigh; // seconds a HIGH transfer must wait after creation - uint32 delayCritical; // seconds a CRITICAL transfer must wait; doubles as the guardian veto window - uint32 recipientActivationDelay; // probation for NEW recipients - uint32 policyChangeDelay; // timelock for security-reducing governance changes - uint32 recoveryDelay; // timelock for guardian-initiated signer replacement - uint32 proposalTtl; // proposals expire this long after creation - uint16 highExposureBps; // amount / asset balance >= this => at least HIGH - uint16 criticalExposureBps; // >= this => CRITICAL - uint16 hardBlockExposureBps; // > this => refused outright (0 = disabled) - uint16 envelopeBps; // circuit breaker: max outflow per envelopeWindow as % of balance at window start (0 = off) - uint32 envelopeWindow; // seconds -} - -// Per-asset limits. address(0) is native KASH. Amounts are in the asset's own units. -struct AssetLimits { - bool approved; - uint256 lowMax; // amount <= lowMax => LOW - uint256 highMax; // amount <= highMax => HIGH, else CRITICAL - uint256 perTxMax; // hard cap per transfer (0 = none) - uint256 dailyMax; // hard cap per rolling 24h bucket (0 = none) -} - -struct Recipient { - Trust trust; - uint64 registeredAt; - uint64 activatesAt; - uint64 paymentCount; - uint256 totalPaid; -} - -struct Velocity { - uint64 dayStart; - uint64 envelopeStart; - uint256 daySpent; - uint256 envelopeBase; // asset balance snapshot at envelope window start - uint256 envelopeSpent; -} - -struct Proposal { - Kind kind; - Status status; - Tier tier; - uint8 requiredApprovals; - uint8 requiredGuardians; - bool securityReducing; - uint16 riskReasons; - uint32 policyVersion; - address proposer; - address asset; // TRANSFER only - address target; // recipient, member, old signer - uint256 amount; // TRANSFER only - uint64 createdAt; - uint64 executableAfter; - uint64 expiresAt; - bytes data; // kind-specific ABI payload -} - -// Result of the deterministic risk engine, also used for the interface preview. -struct RiskResult { - Tier tier; - uint16 reasons; - uint16 exposureBps; - uint8 requiredApprovals; - uint8 requiredGuardians; - uint32 delay; -} diff --git a/contracts/src/SkurVault.sol b/contracts/src/SkurVault.sol deleted file mode 100644 index 6455402..0000000 --- a/contracts/src/SkurVault.sol +++ /dev/null @@ -1,712 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.28; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; -import "./SkurTypes.sol"; -import {ISkurVault} from "./ISkurVault.sol"; -import {SkurRisk} from "./SkurRisk.sol"; -import {SkurPolicyLib} from "./SkurPolicyLib.sol"; - -/// @title SkurVault -/// @notice Self-custodial treasury vault with deterministic transaction-risk controls. -/// @dev A valid signature proves authorization; it does not prove that a transaction is safe. -/// Every control here is enforced onchain. No Skur server is in the execution path. -/// -/// Planes: -/// - Treasury plane (OWNER | APPROVER | EXECUTOR): propose, approve, execute transfers. -/// - Governance plane (OWNER): policy, limits, membership, recipient trust, mode relaxation. -/// - Guardian plane (GUARDIAN): freeze, veto, confirm critical actions, recovery. Never withdraws. -/// -/// Deployed behind EIP-1167 minimal proxies by SkurFactory; logic is immutable (no upgrade path in V1). -contract SkurVault is ISkurVault, ReentrancyGuard { - using SafeERC20 for IERC20; - - // ------------------------------------------------------------------ storage - bool private _initialized; - Mode public mode; - uint32 public policyVersion; - Policy private _policy; - - uint256 public proposalCount; - uint256 public pendingCount; // aggregate for invariants (never derived by iterating a mapping) - uint256 public executedCount; - - address[] private _members; - mapping(address => uint8) public rolesOf; - mapping(address => uint256) private _memberIndexPlusOne; - uint8 public ownerCount; - uint8 public approverCount; - uint8 public executorCount; - uint8 public guardianCount; - - address[] private _assets; - mapping(address => bool) private _assetListed; - mapping(address => AssetLimits) private _assetLimits; - mapping(address => Recipient) private _recipients; - mapping(address => Velocity) private _velocity; - mapping(address => uint256) public totalOutflow; // lifetime executed outflow per asset - mapping(address => uint256) public totalDeposited; // deposits seen through receive()/depositERC20 - - mapping(uint256 => Proposal) private _proposals; - mapping(uint256 => address[]) private _approvers; - mapping(uint256 => address[]) private _guardianConfirmers; - mapping(uint256 => mapping(address => bool)) public hasApproved; - - uint64 private constant DAY = 1 days; - - constructor() { - // The implementation itself is never a vault; only clones are initialized. - _initialized = true; - } - - // ------------------------------------------------------------------ initialization - function initialize( - address[] calldata members, - uint8[] calldata roles, - Policy calldata policy, - address[] calldata assets, - AssetLimits[] calldata limits - ) external { - if (_initialized) revert AlreadyInitialized(); - _initialized = true; - if (members.length != roles.length || assets.length != limits.length) revert InvalidProposal("length"); - for (uint256 i = 0; i < members.length; i++) { - if (rolesOf[members[i]] != 0) revert MemberExists(); - if (roles[i] == 0) revert InvalidRoles(); - _setMember(members[i], roles[i]); - } - for (uint256 i = 0; i < assets.length; i++) { - _setAssetLimits(assets[i], limits[i]); - } - _activatePolicy(policy); - emit VaultInitialized(policyVersion, members.length); - } - - // ------------------------------------------------------------------ deposits - receive() external payable { - totalDeposited[address(0)] += msg.value; - emit Deposit(msg.sender, address(0), msg.value); - } - - function depositERC20(address asset, uint256 amount) external { - if (asset == address(0)) revert ZeroAddress(); - if (amount == 0) revert AmountZero(); - IERC20(asset).safeTransferFrom(msg.sender, address(this), amount); - totalDeposited[asset] += amount; - emit Deposit(msg.sender, asset, amount); - } - - // ------------------------------------------------------------------ views - function getPolicy() external view returns (Policy memory) { - return _policy; - } - - function getAssetLimits(address asset) external view returns (AssetLimits memory) { - return _assetLimits[asset]; - } - - function getAssets() external view returns (address[] memory) { - return _assets; - } - - function getRecipient(address r) external view returns (Recipient memory) { - return _recipients[r]; - } - - function getMembers() external view returns (address[] memory addrs, uint8[] memory roles) { - addrs = _members; - roles = new uint8[](addrs.length); - for (uint256 i = 0; i < addrs.length; i++) { - roles[i] = rolesOf[addrs[i]]; - } - } - - function getProposal(uint256 id) external view returns (Proposal memory) { - return _proposals[id]; - } - - function getApprovers(uint256 id) external view returns (address[] memory) { - return _approvers[id]; - } - - function getGuardianConfirmers(uint256 id) external view returns (address[] memory) { - return _guardianConfirmers[id]; - } - - /// @notice Approvals and guardian confirmations that still count (removed signers are excluded). - function liveApprovals(uint256 id) public view returns (uint256 approvals, uint256 guardians) { - Proposal storage p = _proposals[id]; - approvals = _countLive(_approvers[id], p.kind == Kind.TRANSFER ? ROLE_APPROVER : ROLE_OWNER); - guardians = _countLive(_guardianConfirmers[id], ROLE_GUARDIAN); - } - - function isVetoable(uint256 id) public view returns (bool) { - Proposal storage p = _proposals[id]; - if (p.status != Status.PENDING) return false; - if (p.securityReducing) return true; - if (p.kind != Kind.TRANSFER) return false; - if (p.tier == Tier.CRITICAL) return true; - if (p.riskReasons & REASON_RECIPIENT_PROBATION != 0) return true; - return mode != Mode.NORMAL; - } - - /// @notice Current velocity accounting for an asset, with window rolls applied virtually. - function velocityOf(address asset) - external - view - returns (uint256 daySpent, uint256 dailyMax, uint256 envelopeSpent, uint256 envelope, uint64 envelopeResetsAt) - { - Velocity storage v = _velocity[asset]; - (, dailyMax) = _effectiveCaps(_assetLimits[asset]); - daySpent = block.timestamp >= v.dayStart + DAY ? 0 : v.daySpent; - if (_policy.envelopeBps != 0) { - if (block.timestamp >= v.envelopeStart + _policy.envelopeWindow) { - envelopeSpent = 0; - envelope = (_balance(asset) * _policy.envelopeBps) / BPS; - envelopeResetsAt = 0; - } else { - envelopeSpent = v.envelopeSpent; - envelope = (v.envelopeBase * _policy.envelopeBps) / BPS; - envelopeResetsAt = v.envelopeStart + _policy.envelopeWindow; - } - } - } - - /// @notice The deterministic risk preview the interface shows before a signature is requested. - function previewTransfer(address asset, address to, uint256 amount) public view returns (RiskResult memory r) { - AssetLimits memory l = _assetLimits[asset]; - if (!l.approved) revert AssetNotApproved(asset); - Recipient memory rec = _recipients[to]; - if (rec.trust == Trust.BLOCKED) revert RecipientBlocked(to); - (, uint256 dailyMax) = _effectiveCaps(l); - Velocity storage v = _velocity[asset]; - SkurRisk.Input memory i = SkurRisk.Input({ - amount: amount, - assetBalance: _balance(asset), - lowMax: l.lowMax, - highMax: l.highMax, - dailyMax: dailyMax, - daySpent: block.timestamp >= v.dayStart + DAY ? 0 : v.daySpent, - trust: rec.trust, - inProbation: rec.trust == Trust.NEW && block.timestamp < rec.activatesAt, - mode: mode, - highExposureBps: _policy.highExposureBps, - criticalExposureBps: _policy.criticalExposureBps - }); - (r.tier, r.reasons, r.exposureBps) = SkurRisk.classify(i); - (r.requiredApprovals, r.requiredGuardians, r.delay) = SkurRisk.requirements(_policy, r.tier); - } - - // ------------------------------------------------------------------ treasury plane - /// @notice Propose an outgoing transfer of native KASH (asset = address(0)) or an approved ERC-20. - function proposeTransfer(address asset, address to, uint256 amount, string calldata memo) - external - returns (uint256 id) - { - _requireRole(ROLE_OWNER | ROLE_APPROVER); - if (mode == Mode.LOCKDOWN) revert VaultLocked(); - if (to == address(0)) revert ZeroAddress(); - if (amount == 0) revert AmountZero(); - - RiskResult memory r = previewTransfer(asset, to, amount); - AssetLimits memory l = _assetLimits[asset]; - _checkStaticLimits(l, amount, r.exposureBps); - - Recipient storage rec = _recipients[to]; - if (rec.trust == Trust.UNKNOWN) _registerRecipient(to); - - uint64 executableAfter = uint64(block.timestamp) + r.delay; - // Invariant 9: a recipient in probation cannot receive funds before its activation time. - if (rec.trust == Trust.NEW && rec.activatesAt > executableAfter) executableAfter = rec.activatesAt; - - id = _create(Kind.TRANSFER, to, amount, "", false); - Proposal storage p = _proposals[id]; - p.tier = r.tier; - p.riskReasons = r.reasons; - p.requiredApprovals = r.requiredApprovals; - p.requiredGuardians = r.requiredGuardians; - p.asset = asset; - p.executableAfter = executableAfter; - _emitCreated(id, p, memo); - } - - /// @notice Pre-register a recipient so its activation delay starts before the first payment is proposed. - function registerRecipient(address r) external { - _requireRole(ROLE_OWNER | ROLE_APPROVER); - if (r == address(0)) revert ZeroAddress(); - if (_recipients[r].trust != Trust.UNKNOWN) revert InvalidProposal("already registered"); - _registerRecipient(r); - } - - // ------------------------------------------------------------------ governance plane - function proposePolicy(Policy calldata next) external returns (uint256 id) { - _requireRole(ROLE_OWNER); - SkurPolicyLib.validatePolicy(next); - bool reducing = SkurPolicyLib.policyReduces(_policy, next); - id = _createGovernance( - Kind.POLICY_UPDATE, address(0), 0, abi.encode(next), reducing, 0, _governanceDelay(reducing) - ); - } - - function proposeAssetLimits(address asset, AssetLimits calldata next) external returns (uint256 id) { - _requireRole(ROLE_OWNER); - SkurPolicyLib.validateLimits(next); - bool reducing = SkurPolicyLib.limitsReduce(_assetLimits[asset], next); - id = _createGovernance(Kind.ASSET_LIMITS, asset, 0, abi.encode(next), reducing, 0, _governanceDelay(reducing)); - } - - /// @notice Set a member's role bitmask (0 removes the member). Adding authority is security-reducing. - function proposeMember(address member, uint8 roles) external returns (uint256 id) { - _requireRole(ROLE_OWNER); - if (member == address(0)) revert ZeroAddress(); - _validateRoles(roles); - uint8 prev = rolesOf[member]; - if (prev == roles) revert InvalidProposal("no change"); - bool reducing = _memberChangeReduces(prev, roles); - id = _createGovernance(Kind.MEMBER_SET, member, roles, "", reducing, 0, _governanceDelay(reducing)); - } - - function proposeRecipientTrust(address recipient, Trust trust) external returns (uint256 id) { - _requireRole(ROLE_OWNER); - if (recipient == address(0)) revert ZeroAddress(); - if (trust == Trust.UNKNOWN) revert InvalidProposal("use NEW to re-probate"); - Trust prev = _recipients[recipient].trust; - bool reducing = _trustReduces(prev, trust); - id = _createGovernance( - Kind.RECIPIENT_TRUST, recipient, uint8(trust), "", reducing, 0, _governanceDelay(reducing) - ); - } - - /// @notice Leaving Lockdown or Elevated. Requires owner threshold + guardian confirmations, and a delay out of Lockdown. - function proposeModeRelax(Mode target) external returns (uint256 id) { - _requireRole(ROLE_OWNER); - if (uint8(target) >= uint8(mode)) revert ModeNotRelaxation(); - // Leaving Lockdown must be a stronger process than entering it: owner threshold, guardian - // confirmations and the policy-change timelock. Relaxing Elevated -> Normal skips only the timelock. - uint32 delay = mode == Mode.LOCKDOWN ? _policy.policyChangeDelay : 0; - id = _createGovernance(Kind.MODE_RELAX, address(0), uint8(target), "", true, _policy.guardianThreshold, delay); - } - - // ------------------------------------------------------------------ guardian plane - /// @notice Guardians propose replacing a lost or compromised signer. Delayed; owners can cancel during the delay. - function proposeRecovery(address oldSigner, address newSigner) external returns (uint256 id) { - _requireRole(ROLE_GUARDIAN); - if (newSigner == address(0)) revert ZeroAddress(); - if (rolesOf[oldSigner] == 0) revert NotMember(); - if (rolesOf[newSigner] != 0) revert MemberExists(); - if (_policy.guardianThreshold == 0) revert ThresholdUnsatisfiable("guardianThreshold"); - id = _create(Kind.RECOVERY, oldSigner, 0, abi.encode(newSigner), false); - Proposal storage p = _proposals[id]; - p.requiredApprovals = 0; - p.requiredGuardians = _policy.guardianThreshold; - p.executableAfter = uint64(block.timestamp) + _policy.recoveryDelay; - _emitCreated(id, p, "recovery"); - } - - /// @notice Owners and guardians can move the vault to a stricter mode immediately. - function raiseMode(Mode target, bytes32 reason) external { - _requireRole(ROLE_OWNER | ROLE_GUARDIAN); - if (uint8(target) <= uint8(mode)) revert ModeNotStricter(); - _setMode(target, reason); - } - - /// @notice A single guardian can cancel any vetoable proposal at any time before execution. - function veto(uint256 id, bytes32 reason) external { - _requireRole(ROLE_GUARDIAN); - Proposal storage p = _pending(id); - if (!isVetoable(id)) revert NotVetoable(); - p.status = Status.VETOED; - pendingCount--; - emit ProposalVetoed(id, msg.sender, reason); - } - - // ------------------------------------------------------------------ lifecycle - /// @notice Approve (treasury or governance) or confirm (guardian) a pending proposal. - function approve(uint256 id) external { - Proposal storage p = _pending(id); - uint8 r = rolesOf[msg.sender]; - if (r & ROLE_GUARDIAN != 0) { - if (p.requiredGuardians == 0) revert InvalidProposal("guardian confirmation not required"); - if (hasApproved[id][msg.sender]) revert AlreadyApproved(); - hasApproved[id][msg.sender] = true; - _guardianConfirmers[id].push(msg.sender); - emit ProposalGuardianConfirmed(id, msg.sender, _guardianConfirmers[id].length); - return; - } - if (p.kind == Kind.RECOVERY) revert NotAuthorized(ROLE_GUARDIAN); - uint8 need = p.kind == Kind.TRANSFER ? ROLE_APPROVER : ROLE_OWNER; - if (r & need == 0) revert NotAuthorized(need); - // Invariant 8: one live approval per signer per proposal, never replayed. - if (hasApproved[id][msg.sender]) revert AlreadyApproved(); - hasApproved[id][msg.sender] = true; - _approvers[id].push(msg.sender); - emit ProposalApproved(id, msg.sender, _approvers[id].length); - } - - /// @notice Proposer or any owner may cancel a pending proposal. - function cancel(uint256 id) external { - Proposal storage p = _pending(id); - if (msg.sender != p.proposer && rolesOf[msg.sender] & ROLE_OWNER == 0) revert NotCancellable(); - p.status = Status.CANCELLED; - pendingCount--; - emit ProposalCancelled(id, msg.sender); - } - - /// @notice Anyone may mark an expired pending proposal as cancelled to keep aggregates exact. - function expire(uint256 id) external { - Proposal storage p = _proposals[id]; - if (p.status != Status.PENDING) revert NotPending(); - if (block.timestamp <= p.expiresAt) revert InvalidProposal("not expired"); - p.status = Status.CANCELLED; - pendingCount--; - emit ProposalCancelled(id, msg.sender); - } - - /// @notice Execute once every pinned condition holds. Limits are re-checked here (lifecycle step 7). - function execute(uint256 id) external nonReentrant { - Proposal storage p = _pending(id); - Kind kind = p.kind; - - if (kind == Kind.TRANSFER) _requireRole(ROLE_EXECUTOR); - else if (kind == Kind.RECOVERY) _requireRole(ROLE_OWNER | ROLE_GUARDIAN); - else _requireRole(ROLE_OWNER | ROLE_EXECUTOR); - - if (block.timestamp < p.executableAfter) revert Timelocked(p.executableAfter); - // Invariant 3: Lockdown blocks outgoing execution and any security-reducing change. Tightening - // changes, recovery and the audited exit path (MODE_RELAX) stay available. - if (mode == Mode.LOCKDOWN && (kind == Kind.TRANSFER || (p.securityReducing && kind != Kind.MODE_RELAX))) { - revert VaultLocked(); - } - - // Requirements were pinned at creation; a policy tightened since then still applies (never weaker). - uint256 needApprovals = p.requiredApprovals; - uint256 needGuardians = p.requiredGuardians; - if (kind == Kind.TRANSFER && p.policyVersion != policyVersion) { - (uint8 a, uint8 g,) = SkurRisk.requirements(_policy, p.tier); - if (a > needApprovals) needApprovals = a; - if (g > needGuardians) needGuardians = g; - } - (uint256 haveApprovals, uint256 haveGuardians) = liveApprovals(id); - if (haveApprovals < needApprovals) revert InsufficientApprovals(haveApprovals, needApprovals); - if (haveGuardians < needGuardians) revert InsufficientGuardians(haveGuardians, needGuardians); - - if (kind == Kind.TRANSFER) { - // May trip the circuit breaker: the vault locks and the proposal stays pending. - if (!_checkTransfer(p)) return; - } - - // Effects before interactions (invariants 6 and 12). - p.status = Status.EXECUTED; - pendingCount--; - executedCount++; - emit ProposalExecuted(id, msg.sender); - - if (kind == Kind.TRANSFER) _settleTransfer(id, p); - else if (kind == Kind.POLICY_UPDATE) _activatePolicy(abi.decode(p.data, (Policy))); - else if (kind == Kind.ASSET_LIMITS) _setAssetLimits(p.target, abi.decode(p.data, (AssetLimits))); - else if (kind == Kind.MEMBER_SET) _applyMember(p.target, uint8(p.amount)); - else if (kind == Kind.RECIPIENT_TRUST) _setTrust(p.target, Trust(uint8(p.amount))); - else if (kind == Kind.MODE_RELAX) _applyRelax(Mode(uint8(p.amount))); - else _applyRecovery(id, p); - } - - // ------------------------------------------------------------------ internal: proposals - function _create(Kind kind, address target, uint256 amount, bytes memory data, bool reducing) - private - returns (uint256 id) - { - id = ++proposalCount; - pendingCount++; - Proposal storage p = _proposals[id]; - p.kind = kind; - p.status = Status.PENDING; - p.proposer = msg.sender; - p.target = target; - p.amount = amount; - p.data = data; - p.securityReducing = reducing; - p.policyVersion = policyVersion; - p.createdAt = uint64(block.timestamp); - p.expiresAt = uint64(block.timestamp) + _policy.proposalTtl; - } - - function _createGovernance( - Kind kind, - address target, - uint256 amount, - bytes memory data, - bool reducing, - uint8 guardians, - uint32 delay - ) private returns (uint256 id) { - // Policy-change firewall: Lockdown prevents policy relaxation (MODE_RELAX is the audited exit path). - if (mode == Mode.LOCKDOWN && reducing && kind != Kind.MODE_RELAX) revert VaultLocked(); - id = _create(kind, target, amount, data, reducing); - Proposal storage p = _proposals[id]; - p.requiredApprovals = _policy.governanceThreshold; - p.requiredGuardians = guardians; - p.executableAfter = uint64(block.timestamp) + delay; - _emitCreated(id, p, ""); - } - - /// @dev Security-reducing changes never activate immediately (invariant 4). - function _governanceDelay(bool reducing) private view returns (uint32) { - return reducing ? _policy.policyChangeDelay : 0; - } - - function _emitCreated(uint256 id, Proposal storage p, string memory memo) private { - emit ProposalCreated( - id, - p.kind, - p.proposer, - p.asset, - p.target, - p.amount, - p.tier, - p.riskReasons, - p.requiredApprovals, - p.requiredGuardians, - p.executableAfter, - p.expiresAt, - p.securityReducing, - memo - ); - } - - function _pending(uint256 id) private view returns (Proposal storage p) { - if (id == 0 || id > proposalCount) revert UnknownProposal(); - p = _proposals[id]; - if (p.status != Status.PENDING) revert NotPending(); - if (block.timestamp > p.expiresAt) revert ProposalExpired(); - } - - function _countLive(address[] storage list, uint8 role) private view returns (uint256 n) { - uint256 len = list.length; - for (uint256 i = 0; i < len; i++) { - if (rolesOf[list[i]] & role != 0) n++; - } - } - - // ------------------------------------------------------------------ internal: transfers - function _checkStaticLimits(AssetLimits memory l, uint256 amount, uint16 exposure) private view { - (uint256 perTxMax,) = _effectiveCaps(l); - if (perTxMax != 0 && amount > perTxMax) revert PerTxLimitExceeded(amount, perTxMax); - if (_policy.hardBlockExposureBps != 0 && exposure > _policy.hardBlockExposureBps) { - revert ExposureHardBlocked(exposure, _policy.hardBlockExposureBps); - } - } - - /// @dev Re-checks every limit at execution time. Returns false (and locks the vault) if the envelope would be breached. - function _checkTransfer(Proposal storage p) private returns (bool) { - address asset = p.asset; - uint256 amount = p.amount; - AssetLimits memory l = _assetLimits[asset]; - if (!l.approved) revert AssetNotApproved(asset); - - Recipient storage rec = _recipients[p.target]; - if (rec.trust == Trust.BLOCKED) revert RecipientBlocked(p.target); - if (rec.trust == Trust.NEW && block.timestamp < rec.activatesAt) revert Timelocked(rec.activatesAt); - - uint256 bal = _balance(asset); - if (amount > bal) revert InsufficientBalance(amount, bal); - _checkStaticLimits(l, amount, SkurRisk.exposureBps(amount, bal)); - - (, uint256 dailyMax) = _effectiveCaps(l); - Velocity storage v = _velocity[asset]; - if (block.timestamp >= v.dayStart + DAY) { - v.dayStart = uint64(block.timestamp); - v.daySpent = 0; - } - // Invariant 5: cumulative accounting, so a drain split into many transfers hits the same cap. - if (dailyMax != 0 && v.daySpent + amount > dailyMax) revert VelocityExceeded(v.daySpent + amount, dailyMax); - - if (_policy.envelopeBps != 0) { - if (block.timestamp >= v.envelopeStart + _policy.envelopeWindow) { - v.envelopeStart = uint64(block.timestamp); - v.envelopeBase = bal; - v.envelopeSpent = 0; - } - uint256 envelope = (v.envelopeBase * _policy.envelopeBps) / BPS; - if (v.envelopeSpent + amount > envelope) { - // Treasury circuit breaker: freeze instead of paying. Deposits, guardians and recovery keep working. - emit CircuitBreakerTripped(asset, amount, v.envelopeSpent, envelope); - _setMode(Mode.LOCKDOWN, "circuit-breaker"); - return false; - } - v.envelopeSpent += amount; - } - v.daySpent += amount; - totalOutflow[asset] += amount; - rec.paymentCount += 1; - rec.totalPaid += amount; - return true; - } - - function _settleTransfer(uint256 id, Proposal storage p) private { - emit TransferExecuted(id, p.asset, p.target, p.amount); - if (p.asset == address(0)) { - (bool ok,) = payable(p.target).call{value: p.amount}(""); - if (!ok) revert NativeTransferFailed(); - } else { - IERC20(p.asset).safeTransfer(p.target, p.amount); - } - } - - function _effectiveCaps(AssetLimits memory l) private view returns (uint256 perTxMax, uint256 dailyMax) { - perTxMax = l.perTxMax; - dailyMax = l.dailyMax; - if (mode == Mode.ELEVATED) { - // Elevated posture halves the caps. A configured cap is floored at 1 unit so it can never - // round down to 0, which would mean "unlimited". - if (perTxMax != 0) perTxMax = perTxMax / 2 == 0 ? 1 : perTxMax / 2; - if (dailyMax != 0) dailyMax = dailyMax / 2 == 0 ? 1 : dailyMax / 2; - } - } - - function _balance(address asset) private view returns (uint256) { - if (asset == address(0)) return address(this).balance; - return IERC20(asset).balanceOf(address(this)); - } - - // ------------------------------------------------------------------ internal: governance application - function _activatePolicy(Policy memory p) private { - SkurPolicyLib.validatePolicy(p); - _policy = p; - policyVersion += 1; - _validateCounts(); - emit PolicyActivated(policyVersion, uint64(block.timestamp), p); - } - - function _setAssetLimits(address asset, AssetLimits memory l) private { - SkurPolicyLib.validateLimits(l); - if (!_assetListed[asset]) { - _assetListed[asset] = true; - _assets.push(asset); - } - _assetLimits[asset] = l; - emit AssetLimitsSet(asset, l); - } - - function _applyMember(address member, uint8 roles) private { - _setMember(member, roles); - _validateCounts(); - } - - function _setMember(address member, uint8 roles) private { - if (member == address(0)) revert ZeroAddress(); - _validateRoles(roles); - uint8 prev = rolesOf[member]; - if (prev == 0 && roles != 0) { - _members.push(member); - _memberIndexPlusOne[member] = _members.length; - } else if (prev != 0 && roles == 0) { - uint256 idx = _memberIndexPlusOne[member] - 1; - uint256 last = _members.length - 1; - if (idx != last) { - address moved = _members[last]; - _members[idx] = moved; - _memberIndexPlusOne[moved] = idx + 1; - } - _members.pop(); - delete _memberIndexPlusOne[member]; - } - _adjustCount(ROLE_OWNER, prev, roles); - _adjustCount(ROLE_APPROVER, prev, roles); - _adjustCount(ROLE_EXECUTOR, prev, roles); - _adjustCount(ROLE_GUARDIAN, prev, roles); - rolesOf[member] = roles; - emit MemberSet(member, prev, roles); - } - - function _adjustCount(uint8 bit, uint8 prev, uint8 next) private { - bool had = prev & bit != 0; - bool has = next & bit != 0; - if (had == has) return; - if (bit == ROLE_OWNER) ownerCount = has ? ownerCount + 1 : ownerCount - 1; - else if (bit == ROLE_APPROVER) approverCount = has ? approverCount + 1 : approverCount - 1; - else if (bit == ROLE_EXECUTOR) executorCount = has ? executorCount + 1 : executorCount - 1; - else guardianCount = has ? guardianCount + 1 : guardianCount - 1; - } - - function _registerRecipient(address r) private { - Recipient storage rec = _recipients[r]; - rec.trust = Trust.NEW; - rec.registeredAt = uint64(block.timestamp); - rec.activatesAt = uint64(block.timestamp) + _policy.recipientActivationDelay; - emit RecipientRegistered(r, rec.activatesAt); - } - - function _setTrust(address r, Trust trust) private { - Recipient storage rec = _recipients[r]; - Trust prev = rec.trust; - if (prev == Trust.UNKNOWN) { - rec.registeredAt = uint64(block.timestamp); - } - if (trust == Trust.NEW) { - // (Re-)probation restarts the activation clock. - rec.activatesAt = uint64(block.timestamp) + _policy.recipientActivationDelay; - } - rec.trust = trust; - emit RecipientTrustSet(r, prev, trust); - } - - function _applyRelax(Mode target) private { - if (uint8(target) >= uint8(mode)) revert ModeNotRelaxation(); - _setMode(target, "relax"); - } - - function _applyRecovery(uint256 id, Proposal storage p) private { - address oldSigner = p.target; - address newSigner = abi.decode(p.data, (address)); - uint8 roles = rolesOf[oldSigner]; - if (roles == 0) revert NotMember(); - if (rolesOf[newSigner] != 0) revert MemberExists(); - // Invariant 10: identical roles, policy untouched, posture never relaxed. - _setMember(oldSigner, 0); - _setMember(newSigner, roles); - _validateCounts(); - if (mode == Mode.NORMAL) _setMode(Mode.ELEVATED, "recovery"); - emit RecoveryCompleted(id, oldSigner, newSigner, roles); - } - - function _setMode(Mode target, bytes32 reason) private { - Mode prev = mode; - mode = target; - emit ModeChanged(prev, target, msg.sender, reason); - } - - // ------------------------------------------------------------------ internal: validation - function _requireRole(uint8 role) private view { - if (rolesOf[msg.sender] & role == 0) revert NotAuthorized(role); - } - - function _validateRoles(uint8 roles) private pure { - if (roles > 15) revert InvalidRoles(); - // Invariant 2 precondition: guardian authority is never combined with treasury authority. - if (roles & ROLE_GUARDIAN != 0 && roles & ROLE_TREASURY_MASK != 0) revert InvalidRoles(); - } - - /// @dev Thresholds must remain satisfiable by the current member set (checked after every membership or policy change). - function _validateCounts() private view { - if (ownerCount < 1) revert ThresholdUnsatisfiable("no owner"); - if (executorCount < 1) revert ThresholdUnsatisfiable("no executor"); - if (ownerCount < _policy.governanceThreshold) revert ThresholdUnsatisfiable("governanceThreshold"); - if (approverCount < _policy.approvalsCritical) revert ThresholdUnsatisfiable("approvalsCritical"); - if (guardianCount < _policy.guardianThreshold) revert ThresholdUnsatisfiable("guardianThreshold"); - } - - // ------------------------------------------------------------------ internal: security-reducing classification - function _memberChangeReduces(uint8 prev, uint8 next) private pure returns (bool) { - if (next & ~prev != 0) return true; // any new authority - if (prev & ROLE_GUARDIAN != 0 && next & ROLE_GUARDIAN == 0) return true; // weakening the guardian layer - return false; - } - - function _trustReduces(Trust prev, Trust next) private pure returns (bool) { - if (next == Trust.VERIFIED || next == Trust.TRUSTED) return true; - if (next == Trust.NEW && (prev == Trust.RESTRICTED || prev == Trust.BLOCKED)) return true; - return false; - } -} diff --git a/contracts/test/SkurBase.t.sol b/contracts/test/SkurBase.t.sol deleted file mode 100644 index dfb6a33..0000000 --- a/contracts/test/SkurBase.t.sol +++ /dev/null @@ -1,122 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.28; - -import {Test} from "forge-std/Test.sol"; -import "../src/SkurTypes.sol"; -import {ISkurVault} from "../src/ISkurVault.sol"; -import {SkurVault} from "../src/SkurVault.sol"; -import {SkurFactory} from "../src/SkurFactory.sol"; -import {SkurTestUSD} from "../src/SkurTestUSD.sol"; -import {SkurTemplates} from "../script/SkurTemplates.sol"; - -/// @notice Shared fixture: a startup-template vault with 3 owners/approvers, 1 executor-only member, 2 guardians. -abstract contract SkurBase is Test { - uint256 internal constant USD = 1e6; - - SkurFactory internal factory; - SkurVault internal vault; - SkurTestUSD internal usd; - - address internal alice = makeAddr("alice"); // owner + approver + executor - address internal bob = makeAddr("bob"); // owner + approver - address internal carol = makeAddr("carol"); // owner + approver - address internal exec = makeAddr("exec"); // executor only - address internal g1 = makeAddr("guardian1"); - address internal g2 = makeAddr("guardian2"); - address internal supplier = makeAddr("supplier"); - address internal stranger = makeAddr("stranger"); - - function setUp() public virtual { - vm.warp(1_800_000_000); - usd = new SkurTestUSD(); - factory = new SkurFactory(address(new SkurVault())); - - (address[] memory members, uint8[] memory roles) = _defaultMembers(); - (address[] memory assets, AssetLimits[] memory limits) = SkurTemplates.startupAssets(address(usd)); - Policy memory policy = _defaultPolicy(); - - vault = SkurVault(payable(factory.createVault(bytes32("t"), members, roles, policy, assets, limits))); - - usd.mint(address(vault), 1_000_000 * USD); - vm.deal(address(this), 100_000 ether); - (bool ok,) = address(vault).call{value: 10_000 ether}(""); - require(ok); - - // supplier is a long-activated, VERIFIED recipient so LOW-tier tests are not escalated by probation - _setTrust(supplier, Trust.VERIFIED); - } - - function _defaultMembers() internal view returns (address[] memory members, uint8[] memory roles) { - members = new address[](6); - roles = new uint8[](6); - members[0] = alice; - roles[0] = ROLE_OWNER | ROLE_APPROVER | ROLE_EXECUTOR; - members[1] = bob; - roles[1] = ROLE_OWNER | ROLE_APPROVER; - members[2] = carol; - roles[2] = ROLE_OWNER | ROLE_APPROVER; - members[3] = exec; - roles[3] = ROLE_EXECUTOR; - members[4] = g1; - roles[4] = ROLE_GUARDIAN; - members[5] = g2; - roles[5] = ROLE_GUARDIAN; - } - - function _defaultPolicy() internal pure returns (Policy memory p) { - p = SkurTemplates.startup(); - } - - // ------------------------------------------------------------ helpers - function _setTrust(address r, Trust t) internal { - vm.prank(alice); - uint256 id = vault.proposeRecipientTrust(r, t); - _approveGovernance(id); - Proposal memory p = vault.getProposal(id); - if (block.timestamp < p.executableAfter) vm.warp(p.executableAfter); - vm.prank(alice); - vault.execute(id); - } - - function _approveGovernance(uint256 id) internal { - vm.prank(alice); - vault.approve(id); - vm.prank(bob); - vault.approve(id); - } - - function _propose(address asset, address to, uint256 amount) internal returns (uint256 id) { - vm.prank(alice); - id = vault.proposeTransfer(asset, to, amount, "test"); - } - - function _approveBy(uint256 id, address who) internal { - vm.prank(who); - vault.approve(id); - } - - function _executeBy(uint256 id, address who) internal { - vm.prank(who); - vault.execute(id); - } - - function _warpTo(uint256 id) internal { - Proposal memory p = vault.getProposal(id); - if (block.timestamp < p.executableAfter) vm.warp(p.executableAfter); - } - - /// @dev Fully approve (approvers + guardians as needed), wait, execute. - function _runTransfer(uint256 id) internal { - Proposal memory p = vault.getProposal(id); - address[3] memory approvers = [alice, bob, carol]; - for (uint256 i = 0; i < p.requiredApprovals; i++) { - _approveBy(id, approvers[i]); - } - address[2] memory guardians = [g1, g2]; - for (uint256 i = 0; i < p.requiredGuardians; i++) { - _approveBy(id, guardians[i]); - } - _warpTo(id); - _executeBy(id, exec); - } -} diff --git a/contracts/test/SkurFactory.t.sol b/contracts/test/SkurFactory.t.sol deleted file mode 100644 index e4eab19..0000000 --- a/contracts/test/SkurFactory.t.sol +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.28; - -import {SkurBase} from "./SkurBase.t.sol"; -import "../src/SkurTypes.sol"; -import {SkurVault} from "../src/SkurVault.sol"; -import {SkurTemplates} from "../script/SkurTemplates.sol"; - -contract SkurFactoryTest is SkurBase { - function test_registry() public view { - assertEq(factory.vaultCount(), 1); - assertEq(factory.vaultAt(0), address(vault)); - assertTrue(factory.isVault(address(vault))); - assertEq(factory.vaultsOf(alice).length, 1); - assertEq(factory.vaultsOf(g1)[0], address(vault)); - assertEq(factory.vaultsOf(stranger).length, 0); - } - - function test_predictAddress() public { - (address[] memory m, uint8[] memory r) = _defaultMembers(); - (address[] memory a, AssetLimits[] memory l) = SkurTemplates.startupAssets(address(usd)); - address predicted = factory.predictVaultAddress(address(this), bytes32("second")); - address created = factory.createVault(bytes32("second"), m, r, _defaultPolicy(), a, l); - assertEq(created, predicted); - assertEq(factory.vaultCount(), 2); - } - - function test_sameSaltDifferentCreator() public { - (address[] memory m, uint8[] memory r) = _defaultMembers(); - (address[] memory a, AssetLimits[] memory l) = SkurTemplates.startupAssets(address(usd)); - vm.prank(stranger); - address v2 = factory.createVault(bytes32("t"), m, r, _defaultPolicy(), a, l); - assertTrue(v2 != address(vault)); - } - - function test_factoryHasNoPowerOverVault() public { - vm.prank(address(factory)); - vm.expectRevert(); - vault.raiseMode(Mode.LOCKDOWN, ""); - } - - function test_vaultsAreIndependent() public { - (address[] memory m, uint8[] memory r) = _defaultMembers(); - (address[] memory a, AssetLimits[] memory l) = SkurTemplates.startupAssets(address(usd)); - SkurVault v2 = SkurVault(payable(factory.createVault(bytes32("v2"), m, r, _defaultPolicy(), a, l))); - vm.prank(g1); - v2.raiseMode(Mode.LOCKDOWN, "only v2"); - assertEq(uint8(vault.mode()), uint8(Mode.NORMAL)); - assertEq(uint8(v2.mode()), uint8(Mode.LOCKDOWN)); - } -} diff --git a/contracts/test/SkurInvariants.t.sol b/contracts/test/SkurInvariants.t.sol deleted file mode 100644 index fa8c7e3..0000000 --- a/contracts/test/SkurInvariants.t.sol +++ /dev/null @@ -1,275 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.28; - -import {Test} from "forge-std/Test.sol"; -import {SkurBase} from "./SkurBase.t.sol"; -import "../src/SkurTypes.sol"; -import {SkurVault} from "../src/SkurVault.sol"; -import {SkurTestUSD} from "../src/SkurTestUSD.sol"; - -/// @notice Random actor / random action handler. Reverts are expected and ignored; ghosts record what succeeded. -contract SkurHandler is Test { - SkurVault public vault; - SkurTestUSD public usd; - - address[] public actors; // members + strangers - address[] public guardians; - address[] public recipients; - - // ghost state - uint256 public ghostUsdOut; - uint256 public ghostNativeOut; - uint256 public ghostNativeIn; - uint256 public ghostUsdIn; - uint256 public ghostExecutions; - bool public violatedTimelock; - bool public violatedProbation; - bool public violatedLockdown; - bool public guardianMovedFunds; - bool public doubleExecution; - mapping(uint256 => bool) public executedOnce; - - constructor(SkurVault v, SkurTestUSD u, address[] memory members, address[] memory gs, address[] memory strangers) { - vault = v; - usd = u; - for (uint256 i = 0; i < members.length; i++) { - actors.push(members[i]); - } - for (uint256 i = 0; i < strangers.length; i++) { - actors.push(strangers[i]); - } - guardians = gs; - recipients.push(makeAddr("r1")); - recipients.push(makeAddr("r2")); - recipients.push(makeAddr("r3")); - recipients.push(strangers[0]); - } - - function _actor(uint256 seed) internal view returns (address) { - return actors[seed % actors.length]; - } - - function warp(uint256 secs) external { - vm.warp(block.timestamp + bound(secs, 1, 3 days)); - } - - function deposit(uint256 seed, uint256 amount) external { - amount = bound(amount, 1, 1_000 ether); - vm.deal(address(this), amount); - (bool ok,) = address(vault).call{value: amount}(""); - if (ok) ghostNativeIn += amount; - uint256 u = bound(seed, 1, 100_000 * 1e6); - usd.mint(address(vault), u); - ghostUsdIn += u; - } - - function propose(uint256 seed, uint256 amount, bool native) external { - address who = _actor(seed); - address to = recipients[seed % recipients.length]; - amount = native ? bound(amount, 1, 2_000 ether) : bound(amount, 1, 120_000 * 1e6); - vm.prank(who); - try vault.proposeTransfer(native ? address(0) : address(usd), to, amount, "fuzz") {} catch {} - } - - function approve(uint256 seed, uint256 id) external { - id = bound(id, 1, vault.proposalCount() == 0 ? 1 : vault.proposalCount()); - address who = _actor(seed); - vm.prank(who); - try vault.approve(id) {} catch {} - } - - function execute(uint256 seed, uint256 id) external { - id = bound(id, 1, vault.proposalCount() == 0 ? 1 : vault.proposalCount()); - address who = _actor(seed); - Proposal memory p = vault.getProposal(id); - Mode modeBefore = vault.mode(); - uint256 usdBefore = vault.totalOutflow(address(usd)); - uint256 natBefore = vault.totalOutflow(address(0)); - bool wasExecuted = p.status == Status.EXECUTED; - bool isGuardian = vault.rolesOf(who) & ROLE_GUARDIAN != 0; - - vm.prank(who); - try vault.execute(id) { - Proposal memory post = vault.getProposal(id); - if (post.status == Status.EXECUTED && !wasExecuted) { - ghostExecutions++; - if (executedOnce[id]) doubleExecution = true; - executedOnce[id] = true; - if (block.timestamp < p.executableAfter) violatedTimelock = true; - if (p.kind == Kind.TRANSFER) { - Recipient memory rec = vault.getRecipient(p.target); - if (rec.trust == Trust.NEW && block.timestamp < rec.activatesAt) violatedProbation = true; - if (modeBefore == Mode.LOCKDOWN) violatedLockdown = true; - if (p.asset == address(0)) ghostNativeOut += p.amount; - else ghostUsdOut += p.amount; - } - } else if (wasExecuted) { - doubleExecution = true; - } - } catch {} - if ( - isGuardian && (vault.totalOutflow(address(usd)) != usdBefore || vault.totalOutflow(address(0)) != natBefore) - ) { - guardianMovedFunds = true; - } - } - - function veto(uint256 seed, uint256 id) external { - id = bound(id, 1, vault.proposalCount() == 0 ? 1 : vault.proposalCount()); - vm.prank(guardians[seed % guardians.length]); - try vault.veto(id, "fuzz") {} catch {} - } - - function cancel(uint256 seed, uint256 id) external { - id = bound(id, 1, vault.proposalCount() == 0 ? 1 : vault.proposalCount()); - vm.prank(_actor(seed)); - try vault.cancel(id) {} catch {} - } - - function raiseMode(uint256 seed, uint8 m) external { - vm.prank(_actor(seed)); - try vault.raiseMode(Mode(bound(m, 1, 2)), "fuzz") {} catch {} - } - - function relax(uint256 seed, uint8 m) external { - address who = _actor(seed); - vm.prank(who); - try vault.proposeModeRelax(Mode(bound(m, 0, 1))) {} catch {} - } - - function registerRecipient(uint256 seed) external { - vm.prank(_actor(seed)); - try vault.registerRecipient(recipients[seed % recipients.length]) {} catch {} - } - - function expire(uint256 id) external { - id = bound(id, 1, vault.proposalCount() == 0 ? 1 : vault.proposalCount()); - try vault.expire(id) {} catch {} - } -} - -contract SkurInvariantTest is SkurBase { - SkurHandler handler; - - function setUp() public override { - super.setUp(); - address[] memory members = new address[](6); - members[0] = alice; - members[1] = bob; - members[2] = carol; - members[3] = exec; - members[4] = g1; - members[5] = g2; - address[] memory gs = new address[](2); - gs[0] = g1; - gs[1] = g2; - address[] memory strangers = new address[](2); - strangers[0] = stranger; - strangers[1] = makeAddr("stranger2"); - handler = new SkurHandler(vault, usd, members, gs, strangers); - targetContract(address(handler)); - } - - /// Invariant 11: balances equal deposits minus recorded outflow, per asset. - function invariant_nativeAccounting() public view { - assertEq(address(vault).balance, 10_000 ether + handler.ghostNativeIn() - vault.totalOutflow(address(0))); - assertEq(vault.totalOutflow(address(0)), handler.ghostNativeOut()); - } - - function invariant_erc20Accounting() public view { - assertEq( - usd.balanceOf(address(vault)), 1_000_000 * USD + handler.ghostUsdIn() - vault.totalOutflow(address(usd)) - ); - assertEq(vault.totalOutflow(address(usd)), handler.ghostUsdOut()); - } - - /// Invariant 6: no proposal executes twice. - function invariant_noDoubleExecution() public view { - assertFalse(handler.doubleExecution()); - // setUp executed one governance proposal before the handler existed - assertEq(vault.executedCount(), 1 + handler.ghostExecutions()); - } - - /// Invariant 1: never before the pinned executableAfter. - function invariant_timelockRespected() public view { - assertFalse(handler.violatedTimelock()); - } - - /// Invariant 9: never before recipient activation. - function invariant_probationRespected() public view { - assertFalse(handler.violatedProbation()); - } - - /// Invariant 3: no outgoing execution in lockdown. - function invariant_lockdownRespected() public view { - assertFalse(handler.violatedLockdown()); - } - - /// Invariant 2: guardian calls never move funds. - function invariant_guardiansNeverMoveFunds() public view { - assertFalse(handler.guardianMovedFunds()); - } - - /// Invariant 5: the 24h bucket never exceeds the effective cap. - function invariant_dailyCapNeverExceeded() public view { - (uint256 spent, uint256 cap,,,) = vault.velocityOf(address(usd)); - if (cap != 0) assertLe(spent, cap); - (spent, cap,,,) = vault.velocityOf(address(0)); - if (cap != 0) assertLe(spent, cap); - } - - /// Circuit breaker: envelope spend never exceeds the envelope. - function invariant_envelopeNeverExceeded() public view { - (,, uint256 eSpent, uint256 env,) = vault.velocityOf(address(usd)); - assertLe(eSpent, env); - (,, eSpent, env,) = vault.velocityOf(address(0)); - assertLe(eSpent, env); - } - - /// Role separation and count bookkeeping. - function invariant_rolesConsistent() public view { - (address[] memory m, uint8[] memory r) = vault.getMembers(); - uint256 o; - uint256 a; - uint256 e; - uint256 g; - for (uint256 i = 0; i < m.length; i++) { - assertEq(r[i], vault.rolesOf(m[i])); - assertTrue(r[i] != 0); - if (r[i] & ROLE_GUARDIAN != 0) assertEq(r[i] & ROLE_TREASURY_MASK, 0); - if (r[i] & ROLE_OWNER != 0) o++; - if (r[i] & ROLE_APPROVER != 0) a++; - if (r[i] & ROLE_EXECUTOR != 0) e++; - if (r[i] & ROLE_GUARDIAN != 0) g++; - } - assertEq(o, vault.ownerCount()); - assertEq(a, vault.approverCount()); - assertEq(e, vault.executorCount()); - assertEq(g, vault.guardianCount()); - } - - /// Aggregate counters match a bounded scan of proposal storage. - function invariant_pendingCountExact() public view { - uint256 n = vault.proposalCount(); - uint256 pending; - uint256 executed; - for (uint256 i = 1; i <= n; i++) { - Status s = vault.getProposal(i).status; - if (s == Status.PENDING) pending++; - if (s == Status.EXECUTED) executed++; - } - assertEq(pending, vault.pendingCount()); - assertEq(executed, vault.executedCount()); - } - - /// Pinned requirements never fall below the policy in force at creation (only tighten later). - function invariant_executedTransfersHadEnoughLiveApprovals() public view { - uint256 n = vault.proposalCount(); - for (uint256 i = 1; i <= n; i++) { - Proposal memory p = vault.getProposal(i); - if (p.status != Status.EXECUTED || p.kind != Kind.TRANSFER) continue; - assertGe(vault.getApprovers(i).length, p.requiredApprovals); - assertGe(vault.getGuardianConfirmers(i).length, p.requiredGuardians); - } - } -} diff --git a/contracts/test/SkurRisk.t.sol b/contracts/test/SkurRisk.t.sol deleted file mode 100644 index 4e13010..0000000 --- a/contracts/test/SkurRisk.t.sol +++ /dev/null @@ -1,202 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.28; - -import {Test} from "forge-std/Test.sol"; -import "../src/SkurTypes.sol"; -import {SkurRisk} from "../src/SkurRisk.sol"; - -/// @notice Pinned vectors for the risk engine. web/src/lib/risk.test.ts asserts the same table. -contract SkurRiskTest is Test { - uint256 constant USD = 1e6; - - function _base() internal pure returns (SkurRisk.Input memory i) { - i.amount = 1_000 * USD; - i.assetBalance = 1_000_000 * USD; - i.lowMax = 5_000 * USD; - i.highMax = 25_000 * USD; - i.dailyMax = 100_000 * USD; - i.daySpent = 0; - i.trust = Trust.VERIFIED; - i.inProbation = false; - i.mode = Mode.NORMAL; - i.highExposureBps = 500; - i.criticalExposureBps = 2000; - } - - function test_routineSupplierPaymentIsLow() public pure { - (Tier t, uint16 r, uint16 e) = SkurRisk.classify(_base()); - assertEq(uint8(t), uint8(Tier.LOW)); - assertEq(r, 0); - assertEq(e, 10); // 0.1% - } - - function test_amountAboveLowMaxIsHigh() public pure { - SkurRisk.Input memory i = _base(); - i.amount = 10_000 * USD; - (Tier t, uint16 r,) = SkurRisk.classify(i); - assertEq(uint8(t), uint8(Tier.HIGH)); - assertEq(r, REASON_AMOUNT_HIGH); - } - - function test_amountAboveHighMaxIsCritical() public pure { - SkurRisk.Input memory i = _base(); - i.amount = 30_000 * USD; - (Tier t, uint16 r,) = SkurRisk.classify(i); - assertEq(uint8(t), uint8(Tier.CRITICAL)); - assertEq(r, REASON_AMOUNT_CRITICAL); - } - - function test_exposureEscalatesSmallTreasury() public pure { - SkurRisk.Input memory i = _base(); - i.assetBalance = 50_000 * USD; - i.amount = 4_000 * USD; // 8% of holdings, under lowMax by amount - i.dailyMax = 0; - (Tier t, uint16 r, uint16 e) = SkurRisk.classify(i); - assertEq(e, 800); - assertEq(uint8(t), uint8(Tier.HIGH)); - assertEq(r, REASON_EXPOSURE_HIGH); - } - - function test_exposureCritical() public pure { - SkurRisk.Input memory i = _base(); - i.assetBalance = 20_000 * USD; - i.amount = 4_000 * USD; // 20% - i.dailyMax = 0; - (Tier t, uint16 r, uint16 e) = SkurRisk.classify(i); - assertEq(e, 2000); - assertEq(uint8(t), uint8(Tier.CRITICAL)); - assertEq(r, REASON_EXPOSURE_CRITICAL); - } - - function test_newRecipientBumpsOneTier() public pure { - SkurRisk.Input memory i = _base(); - i.trust = Trust.NEW; - i.inProbation = true; - (Tier t, uint16 r,) = SkurRisk.classify(i); - assertEq(uint8(t), uint8(Tier.HIGH)); - assertEq(r, REASON_RECIPIENT_PROBATION); - - i.amount = 10_000 * USD; - (t, r,) = SkurRisk.classify(i); - assertEq(uint8(t), uint8(Tier.CRITICAL)); - assertEq(r, REASON_AMOUNT_HIGH | REASON_RECIPIENT_PROBATION); - } - - function test_unknownRecipientTreatedAsProbation() public pure { - SkurRisk.Input memory i = _base(); - i.trust = Trust.UNKNOWN; - (Tier t, uint16 r,) = SkurRisk.classify(i); - assertEq(uint8(t), uint8(Tier.HIGH)); - assertEq(r, REASON_RECIPIENT_UNKNOWN | REASON_RECIPIENT_PROBATION); - } - - function test_activatedNewRecipientIsNormal() public pure { - SkurRisk.Input memory i = _base(); - i.trust = Trust.NEW; - i.inProbation = false; - (Tier t, uint16 r,) = SkurRisk.classify(i); - assertEq(uint8(t), uint8(Tier.LOW)); - assertEq(r, 0); - } - - function test_restrictedRecipientAlwaysCritical() public pure { - SkurRisk.Input memory i = _base(); - i.trust = Trust.RESTRICTED; - (Tier t, uint16 r,) = SkurRisk.classify(i); - assertEq(uint8(t), uint8(Tier.CRITICAL)); - assertEq(r, REASON_RECIPIENT_RESTRICTED); - } - - function test_velocityPressure() public pure { - SkurRisk.Input memory i = _base(); - i.daySpent = 49_500 * USD; // 49.5k spent + 1k => 50.5k > half of 100k - (Tier t, uint16 r,) = SkurRisk.classify(i); - assertEq(uint8(t), uint8(Tier.HIGH)); - assertEq(r, REASON_VELOCITY_PRESSURE); - } - - function test_elevatedModeBumps() public pure { - SkurRisk.Input memory i = _base(); - i.mode = Mode.ELEVATED; - (Tier t, uint16 r,) = SkurRisk.classify(i); - assertEq(uint8(t), uint8(Tier.HIGH)); - assertEq(r, REASON_MODE_ELEVATED); - } - - function test_worstCaseStacks() public pure { - SkurRisk.Input memory i = _base(); - i.amount = 30_000 * USD; - i.assetBalance = 100_000 * USD; - i.trust = Trust.UNKNOWN; - i.mode = Mode.ELEVATED; - (Tier t, uint16 r, uint16 e) = SkurRisk.classify(i); - assertEq(uint8(t), uint8(Tier.CRITICAL)); - assertEq(e, 3000); - assertEq( - r, - REASON_AMOUNT_CRITICAL | REASON_EXPOSURE_CRITICAL | REASON_RECIPIENT_UNKNOWN | REASON_RECIPIENT_PROBATION - | REASON_MODE_ELEVATED - ); - } - - function test_exposureBpsEdges() public pure { - assertEq(SkurRisk.exposureBps(0, 100), 0); - assertEq(SkurRisk.exposureBps(1, 0), 10_000); - assertEq(SkurRisk.exposureBps(100, 100), 10_000); - assertEq(SkurRisk.exposureBps(150, 100), 10_000); - assertEq(SkurRisk.exposureBps(1, 100), 100); - assertEq(SkurRisk.exposureBps(1, 30_000), 0); // rounds down - } - - /// @dev Tier is monotone in amount: more money never lowers the tier. - function testFuzz_tierMonotoneInAmount(uint128 a, uint128 b, uint128 balance) public pure { - vm.assume(a <= b); - SkurRisk.Input memory i = _base(); - i.assetBalance = balance; - i.dailyMax = 0; - i.amount = a; - (Tier ta,,) = SkurRisk.classify(i); - i.amount = b; - (Tier tb,,) = SkurRisk.classify(i); - assertLe(uint8(ta), uint8(tb)); - } - - /// @dev Elevated mode never yields a lower tier than normal mode for the same inputs. - function testFuzz_elevatedNeverLower(uint128 amount, uint128 balance, uint8 trustRaw) public pure { - SkurRisk.Input memory i = _base(); - i.amount = amount; - i.assetBalance = balance; - i.trust = Trust(bound(trustRaw, 0, 4)); // exclude BLOCKED (refused before classify) - i.inProbation = i.trust == Trust.NEW; - (Tier tn,,) = SkurRisk.classify(i); - i.mode = Mode.ELEVATED; - (Tier te,,) = SkurRisk.classify(i); - assertLe(uint8(tn), uint8(te)); - } - - function test_requirementsPerTier() public pure { - Policy memory p; - p.approvalsLow = 1; - p.approvalsHigh = 2; - p.approvalsCritical = 3; - p.guardianThreshold = 2; - p.guardianRequiredCritical = true; - p.delayHigh = 6 hours; - p.delayCritical = 24 hours; - (uint8 a, uint8 g, uint32 d) = SkurRisk.requirements(p, Tier.LOW); - assertEq(a, 1); - assertEq(g, 0); - assertEq(d, 0); - (a, g, d) = SkurRisk.requirements(p, Tier.HIGH); - assertEq(a, 2); - assertEq(g, 0); - assertEq(d, 6 hours); - (a, g, d) = SkurRisk.requirements(p, Tier.CRITICAL); - assertEq(a, 3); - assertEq(g, 2); - assertEq(d, 24 hours); - p.guardianRequiredCritical = false; - (, g,) = SkurRisk.requirements(p, Tier.CRITICAL); - assertEq(g, 0); - } -} diff --git a/contracts/test/SkurTemplates.t.sol b/contracts/test/SkurTemplates.t.sol deleted file mode 100644 index dc8076d..0000000 --- a/contracts/test/SkurTemplates.t.sol +++ /dev/null @@ -1,93 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.28; - -import {Test} from "forge-std/Test.sol"; -import "../src/SkurTypes.sol"; -import {SkurVault} from "../src/SkurVault.sol"; -import {SkurFactory} from "../src/SkurFactory.sol"; -import {SkurTestUSD} from "../src/SkurTestUSD.sol"; -import {SkurTemplates} from "../script/SkurTemplates.sol"; - -/// @notice Every shipped template must be accepted by the vault with a member set large enough to satisfy it. -contract SkurTemplatesTest is Test { - SkurFactory factory; - SkurTestUSD usd; - - function setUp() public { - factory = new SkurFactory(address(new SkurVault())); - usd = new SkurTestUSD(); - } - - function _members(uint8 signers, uint8 guardians) - internal - returns (address[] memory members, uint8[] memory roles) - { - members = new address[](signers + guardians); - roles = new uint8[](signers + guardians); - for (uint8 i = 0; i < signers; i++) { - members[i] = makeAddr(string.concat("signer", vm.toString(i))); - roles[i] = ROLE_OWNER | ROLE_APPROVER | ROLE_EXECUTOR; - } - for (uint8 i = 0; i < guardians; i++) { - members[signers + i] = makeAddr(string.concat("guardian", vm.toString(i))); - roles[signers + i] = ROLE_GUARDIAN; - } - } - - function _deploy(Policy memory p, uint8 signers, uint8 guardians) internal returns (SkurVault v) { - (address[] memory m, uint8[] memory r) = _members(signers, guardians); - (address[] memory a, AssetLimits[] memory l) = SkurTemplates.startupAssets(address(usd)); - v = SkurVault(payable(factory.createVault(bytes32(uint256(signers)), m, r, p, a, l))); - } - - function test_startup() public { - SkurVault v = _deploy(SkurTemplates.startup(), 2, 1); - assertEq(v.policyVersion(), 1); - } - - function test_sme() public { - _deploy(SkurTemplates.sme(), 3, 1); - } - - function test_dao() public { - _deploy(SkurTemplates.dao(), 4, 2); - } - - function test_fund() public { - _deploy(SkurTemplates.fund(), 3, 2); - } - - function test_nonprofit() public { - _deploy(SkurTemplates.nonprofit(), 3, 1); - } - - function test_familyOffice() public { - _deploy(SkurTemplates.familyOffice(), 2, 1); - } - - function test_templateRejectsInsufficientMembers() public { - (address[] memory m, uint8[] memory r) = _members(1, 0); - (address[] memory a, AssetLimits[] memory l) = SkurTemplates.startupAssets(address(usd)); - vm.expectRevert(); - factory.createVault(bytes32("x"), m, r, SkurTemplates.startup(), a, l); - } - - /// @dev Every template keeps the "leaving lockdown is stronger than entering" property: at least one guardian. - function test_everyTemplateHasGuardianLayer() public pure { - Policy[6] memory ps = [ - SkurTemplates.startup(), - SkurTemplates.sme(), - SkurTemplates.dao(), - SkurTemplates.fund(), - SkurTemplates.nonprofit(), - SkurTemplates.familyOffice() - ]; - for (uint256 i = 0; i < ps.length; i++) { - assertGe(ps[i].guardianThreshold, 1); - assertTrue(ps[i].guardianRequiredCritical); - assertGt(ps[i].policyChangeDelay, 0); - assertGt(ps[i].recipientActivationDelay, 0); - assertGt(ps[i].envelopeBps, 0); - } - } -} diff --git a/contracts/test/SkurVault.t.sol b/contracts/test/SkurVault.t.sol deleted file mode 100644 index fedf53b..0000000 --- a/contracts/test/SkurVault.t.sol +++ /dev/null @@ -1,821 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.28; - -import {SkurBase} from "./SkurBase.t.sol"; -import "../src/SkurTypes.sol"; -import {ISkurVault} from "../src/ISkurVault.sol"; -import {SkurVault} from "../src/SkurVault.sol"; -import {SkurTemplates} from "../script/SkurTemplates.sol"; -import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; - -/// @dev ERC-20 that returns nothing from transfer (USDT-style). Invariant 11 must still hold. -contract NoBoolToken { - string public name = "NoBool"; - mapping(address => uint256) public balanceOf; - - function mint(address to, uint256 a) external { - balanceOf[to] += a; - } - - function transfer(address to, uint256 a) external { - balanceOf[msg.sender] -= a; - balanceOf[to] += a; - } -} - -/// @dev Recipient that tries to re-enter execute() while receiving native funds. -contract Reenterer { - SkurVault immutable vault; - uint256 public targetId; - bool public reentered; - - constructor(SkurVault v) { - vault = v; - } - - function setTarget(uint256 id) external { - targetId = id; - } - - receive() external payable { - reentered = true; - vault.execute(targetId); - } -} - -contract SkurVaultTest is SkurBase { - // ------------------------------------------------------------------ setup & roles - function test_setupCounts() public view { - assertEq(vault.ownerCount(), 3); - assertEq(vault.approverCount(), 3); - assertEq(vault.executorCount(), 2); - assertEq(vault.guardianCount(), 2); - assertEq(vault.policyVersion(), 1); - assertEq(uint8(vault.mode()), uint8(Mode.NORMAL)); - (address[] memory m, uint8[] memory r) = vault.getMembers(); - assertEq(m.length, 6); - assertEq(r[4], ROLE_GUARDIAN); - } - - function test_implementationCannotBeInitialized() public { - SkurVault impl = SkurVault(payable(factory.implementation())); - (address[] memory m, uint8[] memory r) = _defaultMembers(); - (address[] memory a, AssetLimits[] memory l) = SkurTemplates.startupAssets(address(usd)); - vm.expectRevert(ISkurVault.AlreadyInitialized.selector); - impl.initialize(m, r, _defaultPolicy(), a, l); - vm.expectRevert(ISkurVault.AlreadyInitialized.selector); - vault.initialize(m, r, _defaultPolicy(), a, l); - } - - function test_guardianRolesAreExclusive() public { - vm.prank(alice); - vm.expectRevert(ISkurVault.InvalidRoles.selector); - vault.proposeMember(stranger, ROLE_GUARDIAN | ROLE_OWNER); - } - - // ------------------------------------------------------------------ LOW tier lifecycle - function test_lowTransferLifecycle() public { - uint256 before = usd.balanceOf(supplier); - uint256 id = _propose(address(usd), supplier, 1_000 * USD); - Proposal memory p = vault.getProposal(id); - assertEq(uint8(p.tier), uint8(Tier.LOW)); - assertEq(p.requiredApprovals, 1); - assertEq(p.requiredGuardians, 0); - assertEq(p.executableAfter, block.timestamp); - assertEq(vault.pendingCount(), 1); - - _approveBy(id, bob); - _executeBy(id, exec); - - assertEq(usd.balanceOf(supplier), before + 1_000 * USD); - assertEq(vault.totalOutflow(address(usd)), 1_000 * USD); - assertEq(uint8(vault.getProposal(id).status), uint8(Status.EXECUTED)); - assertEq(vault.pendingCount(), 0); - assertEq(vault.executedCount(), 2); // setUp executed one governance proposal - Recipient memory rec = vault.getRecipient(supplier); - assertEq(rec.paymentCount, 1); - assertEq(rec.totalPaid, 1_000 * USD); - } - - function test_nativeTransferLifecycle() public { - uint256 id = _propose(address(0), supplier, 10 ether); - _approveBy(id, bob); - _executeBy(id, exec); - assertEq(supplier.balance, 10 ether); - assertEq(vault.totalOutflow(address(0)), 10 ether); - } - - function test_cannotExecuteTwice() public { - uint256 id = _propose(address(usd), supplier, 1_000 * USD); - _approveBy(id, bob); - _executeBy(id, exec); - vm.prank(exec); - vm.expectRevert(ISkurVault.NotPending.selector); - vault.execute(id); - } - - function test_executeRequiresExecutor() public { - uint256 id = _propose(address(usd), supplier, 1_000 * USD); - _approveBy(id, bob); - vm.prank(bob); // owner + approver, but not executor - vm.expectRevert(abi.encodeWithSelector(ISkurVault.NotAuthorized.selector, ROLE_EXECUTOR)); - vault.execute(id); - } - - function test_approveRequiresApprover() public { - uint256 id = _propose(address(usd), supplier, 1_000 * USD); - vm.prank(exec); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.NotAuthorized.selector, ROLE_APPROVER)); - vault.approve(id); - vm.prank(stranger); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.NotAuthorized.selector, ROLE_APPROVER)); - vault.approve(id); - } - - function test_duplicateApprovalRejected() public { - uint256 id = _propose(address(usd), supplier, 10_000 * USD); - _approveBy(id, bob); - vm.prank(bob); - vm.expectRevert(ISkurVault.AlreadyApproved.selector); - vault.approve(id); - (uint256 a,) = vault.liveApprovals(id); - assertEq(a, 1); - } - - function test_insufficientApprovalsReverts() public { - uint256 id = _propose(address(usd), supplier, 1_000 * USD); - vm.prank(exec); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.InsufficientApprovals.selector, 0, 1)); - vault.execute(id); - } - - function test_strangerCannotPropose() public { - vm.prank(stranger); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.NotAuthorized.selector, ROLE_OWNER | ROLE_APPROVER)); - vault.proposeTransfer(address(usd), supplier, 1, ""); - } - - // ------------------------------------------------------------------ invariant 7: removed signers - function test_removedSignerApprovalStopsCounting() public { - uint256 id = _propose(address(usd), supplier, 1_000 * USD); - _approveBy(id, bob); - (uint256 live,) = vault.liveApprovals(id); - assertEq(live, 1); - - // Removing bob is security-tightening: immediate once the governance threshold approves. - vm.prank(alice); - uint256 gid = vault.proposeMember(bob, 0); - assertFalse(vault.getProposal(gid).securityReducing); - _approveBy(gid, alice); - _approveBy(gid, carol); - _executeBy(gid, alice); - assertEq(vault.rolesOf(bob), 0); - - (live,) = vault.liveApprovals(id); - assertEq(live, 0); - vm.prank(exec); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.InsufficientApprovals.selector, 0, 1)); - vault.execute(id); - - vm.prank(bob); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.NotAuthorized.selector, ROLE_APPROVER)); - vault.approve(id); - } - - // ------------------------------------------------------------------ HIGH / CRITICAL tiers - function test_highTierNeedsTwoApprovalsAndDelay() public { - uint256 id = _propose(address(usd), supplier, 10_000 * USD); - Proposal memory p = vault.getProposal(id); - assertEq(uint8(p.tier), uint8(Tier.HIGH)); - assertEq(p.requiredApprovals, 2); - assertEq(p.executableAfter, block.timestamp + 6 hours); - - _approveBy(id, bob); - _approveBy(id, carol); - vm.prank(exec); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.Timelocked.selector, p.executableAfter)); - vault.execute(id); - - vm.warp(p.executableAfter); - _executeBy(id, exec); - assertEq(usd.balanceOf(supplier), 10_000 * USD); - } - - function test_criticalTierNeedsGuardianAndIsVetoable() public { - uint256 id = _propose(address(usd), supplier, 30_000 * USD); - Proposal memory p = vault.getProposal(id); - assertEq(uint8(p.tier), uint8(Tier.CRITICAL)); - assertEq(p.requiredApprovals, 2); - assertEq(p.requiredGuardians, 1); - assertEq(p.executableAfter, block.timestamp + 24 hours); - assertTrue(vault.isVetoable(id)); - - _approveBy(id, bob); - _approveBy(id, carol); - vm.warp(p.executableAfter); - vm.prank(exec); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.InsufficientGuardians.selector, 0, 1)); - vault.execute(id); - - _approveBy(id, g1); - _executeBy(id, exec); - assertEq(usd.balanceOf(supplier), 30_000 * USD); - } - - function test_guardianVetoDuringWindow() public { - uint256 id = _propose(address(usd), supplier, 30_000 * USD); - _approveBy(id, bob); - _approveBy(id, carol); - _approveBy(id, g1); - vm.warp(block.timestamp + 12 hours); - vm.prank(g2); - vault.veto(id, "suspicious"); - assertEq(uint8(vault.getProposal(id).status), uint8(Status.VETOED)); - assertEq(vault.pendingCount(), 0); - - vm.warp(block.timestamp + 24 hours); - vm.prank(exec); - vm.expectRevert(ISkurVault.NotPending.selector); - vault.execute(id); - } - - function test_lowTierNotVetoableInNormalMode() public { - uint256 id = _propose(address(usd), supplier, 1_000 * USD); - assertFalse(vault.isVetoable(id)); - vm.prank(g1); - vm.expectRevert(ISkurVault.NotVetoable.selector); - vault.veto(id, "x"); - } - - function test_anyTransferVetoableOutsideNormalMode() public { - uint256 id = _propose(address(usd), supplier, 1_000 * USD); - vm.prank(g1); - vault.raiseMode(Mode.ELEVATED, "incident"); - assertTrue(vault.isVetoable(id)); - vm.prank(g1); - vault.veto(id, "incident"); - } - - function test_tightenedPolicyAppliesToPendingProposal() public { - uint256 id = _propose(address(usd), supplier, 10_000 * USD); // HIGH, needs 2 - _approveBy(id, bob); - _approveBy(id, carol); - - Policy memory p = _defaultPolicy(); - p.approvalsHigh = 3; - p.approvalsCritical = 3; - vm.prank(alice); - uint256 gid = vault.proposePolicy(p); - assertFalse(vault.getProposal(gid).securityReducing); - _approveGovernance(gid); - _executeBy(gid, alice); - assertEq(vault.policyVersion(), 2); - - _warpTo(id); - vm.prank(exec); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.InsufficientApprovals.selector, 2, 3)); - vault.execute(id); - _approveBy(id, alice); - _executeBy(id, exec); - } - - // ------------------------------------------------------------------ invariant 9: recipients - function test_newRecipientWaitsForActivation() public { - uint256 id = _propose(address(usd), stranger, 1_000 * USD); - Proposal memory p = vault.getProposal(id); - Recipient memory rec = vault.getRecipient(stranger); - assertEq(uint8(rec.trust), uint8(Trust.NEW)); - assertEq(rec.activatesAt, block.timestamp + 24 hours); - assertEq(uint8(p.tier), uint8(Tier.HIGH)); // LOW bumped by probation - assertTrue(p.riskReasons & REASON_RECIPIENT_PROBATION != 0); - assertEq(p.executableAfter, rec.activatesAt); // forced past delayHigh (6h) - assertTrue(vault.isVetoable(id)); - - _approveBy(id, bob); - _approveBy(id, carol); - vm.warp(block.timestamp + 6 hours); - vm.prank(exec); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.Timelocked.selector, rec.activatesAt)); - vault.execute(id); - - vm.warp(rec.activatesAt); - _executeBy(id, exec); - assertEq(usd.balanceOf(stranger), 1_000 * USD); - } - - function test_activatedRecipientGetsNormalPolicy() public { - vm.prank(alice); - vault.registerRecipient(stranger); - vm.warp(block.timestamp + 24 hours); - uint256 id = _propose(address(usd), stranger, 1_000 * USD); - assertEq(uint8(vault.getProposal(id).tier), uint8(Tier.LOW)); - } - - function test_reProbationRestartsClockAndBlocksPending() public { - uint256 id = _propose(address(usd), supplier, 1_000 * USD); - _approveBy(id, bob); - _setTrust(supplier, Trust.NEW); // tightening: immediate - Recipient memory rec = vault.getRecipient(supplier); - assertEq(rec.activatesAt, block.timestamp + 24 hours); - vm.prank(exec); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.Timelocked.selector, rec.activatesAt)); - vault.execute(id); - } - - function test_blockedRecipientRefusedAtCreationAndExecution() public { - uint256 id = _propose(address(usd), supplier, 1_000 * USD); - _approveBy(id, bob); - _setTrust(supplier, Trust.BLOCKED); - vm.prank(exec); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.RecipientBlocked.selector, supplier)); - vault.execute(id); - vm.prank(alice); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.RecipientBlocked.selector, supplier)); - vault.proposeTransfer(address(usd), supplier, 1, ""); - } - - function test_trustedIsSecurityReducingAndDelayed() public { - vm.prank(alice); - uint256 id = vault.proposeRecipientTrust(supplier, Trust.TRUSTED); - Proposal memory p = vault.getProposal(id); - assertTrue(p.securityReducing); - assertEq(p.executableAfter, block.timestamp + 24 hours); - assertTrue(vault.isVetoable(id)); - } - - function test_restrictedRecipientIsCritical() public { - _setTrust(supplier, Trust.RESTRICTED); - uint256 id = _propose(address(usd), supplier, 100 * USD); - assertEq(uint8(vault.getProposal(id).tier), uint8(Tier.CRITICAL)); - } - - // ------------------------------------------------------------------ limits - function test_perTxLimit() public { - vm.prank(alice); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.PerTxLimitExceeded.selector, 150_000 * USD, 100_000 * USD)); - vault.proposeTransfer(address(usd), supplier, 150_000 * USD, ""); - } - - function test_hardBlockExposure() public { - // 6,000 of 10,000 KASH = 60% > 50% hard block - vm.prank(alice); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.ExposureHardBlocked.selector, 6000, 5000)); - vault.proposeTransfer(address(0), supplier, 6_000 ether, ""); - } - - function test_assetNotApproved() public { - vm.prank(alice); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.AssetNotApproved.selector, address(0xBEEF))); - vault.proposeTransfer(address(0xBEEF), supplier, 1, ""); - } - - /// @dev Invariant 5: twenty-one LOW payments of 4,900 are created and approved at once (each alone is routine); - /// twenty execute (98k), the twenty-first breaches the 100k daily cap even though its pinned tier is LOW. - function test_splitDrainHitsDailyCap() public { - uint256[] memory ids = new uint256[](21); - for (uint256 i = 0; i < 21; i++) { - ids[i] = _propose(address(usd), supplier, 4_900 * USD); - assertEq(uint8(vault.getProposal(ids[i]).tier), uint8(Tier.LOW)); - _approveBy(ids[i], bob); - } - for (uint256 i = 0; i < 20; i++) { - _executeBy(ids[i], exec); - } - (uint256 spent, uint256 cap,,,) = vault.velocityOf(address(usd)); - assertEq(spent, 98_000 * USD); - assertEq(cap, 100_000 * USD); - - uint256 last = ids[20]; - vm.prank(exec); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.VelocityExceeded.selector, 102_900 * USD, 100_000 * USD)); - vault.execute(last); - - // next day the bucket resets - vm.warp(block.timestamp + 1 days); - _executeBy(last, exec); - assertEq(vault.totalOutflow(address(usd)), 102_900 * USD); - } - - function test_circuitBreakerTripsLockdownInsteadOfPaying() public { - // Native envelope: 10% of 10,000 KASH = 1,000 KASH per 24h. Daily cap is 5,000 so the envelope binds first. - uint256 a = _propose(address(0), supplier, 900 ether); - _runTransfer(a); - assertEq(supplier.balance, 900 ether); - - uint256 b = _propose(address(0), supplier, 200 ether); - Proposal memory p = vault.getProposal(b); - for (uint256 i = 0; i < p.requiredApprovals; i++) { - _approveBy(b, [alice, bob, carol][i]); - } - _warpTo(b); - vm.expectEmit(true, false, false, true); - emit ISkurVault.CircuitBreakerTripped(address(0), 200 ether, 900 ether, 1_000 ether); - _executeBy(b, exec); - - assertEq(uint8(vault.mode()), uint8(Mode.LOCKDOWN)); - assertEq(uint8(vault.getProposal(b).status), uint8(Status.PENDING)); - assertEq(supplier.balance, 900 ether); - assertEq(vault.totalOutflow(address(0)), 900 ether); - - vm.prank(exec); - vm.expectRevert(ISkurVault.VaultLocked.selector); - vault.execute(b); - } - - function test_elevatedHalvesCapsAndBumpsTier() public { - vm.prank(g1); - vault.raiseMode(Mode.ELEVATED, "phish"); - RiskResult memory r = vault.previewTransfer(address(usd), supplier, 1_000 * USD); - assertEq(uint8(r.tier), uint8(Tier.HIGH)); - assertTrue(r.reasons & REASON_MODE_ELEVATED != 0); - (, uint256 cap,,,) = vault.velocityOf(address(usd)); - assertEq(cap, 50_000 * USD); - vm.prank(alice); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.PerTxLimitExceeded.selector, 60_000 * USD, 50_000 * USD)); - vault.proposeTransfer(address(usd), supplier, 60_000 * USD, ""); - } - - // ------------------------------------------------------------------ modes - function test_raiseModeAuthority() public { - vm.prank(stranger); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.NotAuthorized.selector, ROLE_OWNER | ROLE_GUARDIAN)); - vault.raiseMode(Mode.LOCKDOWN, ""); - vm.prank(exec); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.NotAuthorized.selector, ROLE_OWNER | ROLE_GUARDIAN)); - vault.raiseMode(Mode.LOCKDOWN, ""); - vm.prank(g1); - vault.raiseMode(Mode.LOCKDOWN, "freeze"); - assertEq(uint8(vault.mode()), uint8(Mode.LOCKDOWN)); - vm.prank(alice); - vm.expectRevert(ISkurVault.ModeNotStricter.selector); - vault.raiseMode(Mode.ELEVATED, ""); - } - - function test_lockdownBlocksOutgoingButNotDepositsOrGuardians() public { - uint256 id = _propose(address(usd), supplier, 1_000 * USD); - _approveBy(id, bob); - vm.prank(g1); - vault.raiseMode(Mode.LOCKDOWN, "freeze"); - - vm.prank(exec); - vm.expectRevert(ISkurVault.VaultLocked.selector); - vault.execute(id); - vm.prank(alice); - vm.expectRevert(ISkurVault.VaultLocked.selector); - vault.proposeTransfer(address(usd), supplier, 1, ""); - - // deposits still work - (bool ok,) = address(vault).call{value: 1 ether}(""); - assertTrue(ok); - usd.mint(address(this), 5 * USD); - usd.approve(address(vault), 5 * USD); - vault.depositERC20(address(usd), 5 * USD); - - // guardians can still veto and propose recovery; policy can still be tightened - vm.prank(g1); - vault.veto(id, "x"); - vm.prank(g1); - vault.proposeRecovery(bob, stranger); - vm.prank(alice); - uint256 tid = vault.proposeRecipientTrust(supplier, Trust.BLOCKED); - _approveGovernance(tid); - _executeBy(tid, alice); - assertEq(uint8(vault.getRecipient(supplier).trust), uint8(Trust.BLOCKED)); - } - - function test_lockdownBlocksSecurityReducingChanges() public { - vm.prank(g1); - vault.raiseMode(Mode.LOCKDOWN, "freeze"); - Policy memory p = _defaultPolicy(); - p.approvalsHigh = 1; - vm.prank(alice); - vm.expectRevert(ISkurVault.VaultLocked.selector); - vault.proposePolicy(p); - vm.prank(alice); - vm.expectRevert(ISkurVault.VaultLocked.selector); - vault.proposeMember(stranger, ROLE_APPROVER); - } - - function test_leavingLockdownIsStrongerThanEntering() public { - vm.prank(g1); - vault.raiseMode(Mode.LOCKDOWN, "freeze"); - - vm.prank(alice); - uint256 id = vault.proposeModeRelax(Mode.ELEVATED); - Proposal memory p = vault.getProposal(id); - assertEq(p.requiredApprovals, 2); - assertEq(p.requiredGuardians, 1); - assertEq(p.executableAfter, block.timestamp + 24 hours); - assertTrue(vault.isVetoable(id)); - - _approveGovernance(id); - vm.warp(p.executableAfter); - vm.prank(alice); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.InsufficientGuardians.selector, 0, 1)); - vault.execute(id); - - _approveBy(id, g1); - _executeBy(id, alice); - assertEq(uint8(vault.mode()), uint8(Mode.ELEVATED)); - - // Elevated -> Normal: approvals + guardian, no timelock - vm.prank(alice); - uint256 id2 = vault.proposeModeRelax(Mode.NORMAL); - assertEq(vault.getProposal(id2).executableAfter, block.timestamp); - _approveGovernance(id2); - _approveBy(id2, g2); - _executeBy(id2, alice); - assertEq(uint8(vault.mode()), uint8(Mode.NORMAL)); - } - - function test_guardianCanVetoLockdownExit() public { - vm.prank(g1); - vault.raiseMode(Mode.LOCKDOWN, "freeze"); - vm.prank(alice); - uint256 id = vault.proposeModeRelax(Mode.NORMAL); - vm.prank(g2); - vault.veto(id, "not yet"); - assertEq(uint8(vault.mode()), uint8(Mode.LOCKDOWN)); - } - - // ------------------------------------------------------------------ invariant 4: policy firewall - function test_looseningPolicyIsDelayedAndVetoable() public { - Policy memory p = _defaultPolicy(); - p.approvalsHigh = 1; - vm.prank(alice); - uint256 id = vault.proposePolicy(p); - Proposal memory pr = vault.getProposal(id); - assertTrue(pr.securityReducing); - assertEq(pr.executableAfter, block.timestamp + 24 hours); - assertTrue(vault.isVetoable(id)); - - _approveGovernance(id); - vm.prank(alice); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.Timelocked.selector, pr.executableAfter)); - vault.execute(id); - - vm.warp(pr.executableAfter); - vm.expectEmit(true, false, false, false); - emit ISkurVault.PolicyActivated(2, uint64(block.timestamp), p); - _executeBy(id, alice); - assertEq(vault.getPolicy().approvalsHigh, 1); - } - - function test_mixedChangeCountsAsLoosening() public { - Policy memory p = _defaultPolicy(); - p.approvalsLow = 2; // tighter - p.delayCritical = 12 hours; // looser - vm.prank(alice); - uint256 id = vault.proposePolicy(p); - assertTrue(vault.getProposal(id).securityReducing); - } - - function test_invalidPolicyRejected() public { - Policy memory p = _defaultPolicy(); - p.approvalsCritical = 1; // < approvalsHigh - vm.prank(alice); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.InvalidPolicy.selector, "approvalsCritical")); - vault.proposePolicy(p); - p = _defaultPolicy(); - p.proposalTtl = 1 hours; - vm.prank(alice); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.InvalidPolicy.selector, "proposalTtl too short")); - vault.proposePolicy(p); - } - - function test_policyCannotExceedMemberSet() public { - Policy memory p = _defaultPolicy(); - p.approvalsCritical = 4; // only 3 approvers - vm.prank(alice); - uint256 id = vault.proposePolicy(p); - _approveGovernance(id); - vm.prank(alice); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.ThresholdUnsatisfiable.selector, "approvalsCritical")); - vault.execute(id); - } - - function test_approvingNewAssetIsDelayed() public { - AssetLimits memory l = AssetLimits({approved: true, lowMax: 1, highMax: 2, perTxMax: 0, dailyMax: 0}); - vm.prank(alice); - uint256 id = vault.proposeAssetLimits(address(0xBEEF), l); - assertTrue(vault.getProposal(id).securityReducing); - _approveGovernance(id); - _warpTo(id); - _executeBy(id, alice); - assertTrue(vault.getAssetLimits(address(0xBEEF)).approved); - assertEq(vault.getAssets().length, 3); - } - - function test_tighteningLimitsIsImmediate() public { - AssetLimits memory l = vault.getAssetLimits(address(usd)); - l.dailyMax = 50_000 * USD; - l.perTxMax = 50_000 * USD; - vm.prank(alice); - uint256 id = vault.proposeAssetLimits(address(usd), l); - assertFalse(vault.getProposal(id).securityReducing); - assertEq(vault.getProposal(id).executableAfter, block.timestamp); - } - - // ------------------------------------------------------------------ membership - function test_addingMemberIsDelayedRemovingIsImmediate() public { - vm.prank(alice); - uint256 add = vault.proposeMember(stranger, ROLE_APPROVER); - assertTrue(vault.getProposal(add).securityReducing); - _approveGovernance(add); - vm.prank(alice); - vm.expectRevert(); - vault.execute(add); - _warpTo(add); - _executeBy(add, alice); - assertEq(vault.rolesOf(stranger), ROLE_APPROVER); - assertEq(vault.approverCount(), 4); - - vm.prank(alice); - uint256 rm = vault.proposeMember(stranger, 0); - assertFalse(vault.getProposal(rm).securityReducing); - _approveGovernance(rm); - _executeBy(rm, alice); - assertEq(vault.rolesOf(stranger), 0); - (address[] memory m,) = vault.getMembers(); - assertEq(m.length, 6); - } - - function test_removingGuardianIsSecurityReducing() public { - vm.prank(alice); - uint256 id = vault.proposeMember(g2, 0); - assertTrue(vault.getProposal(id).securityReducing); - } - - function test_cannotDropBelowThresholds() public { - // remove both guardians -> guardianThreshold 1 unsatisfiable on the second removal - vm.prank(alice); - uint256 a = vault.proposeMember(g1, 0); - vm.prank(alice); - uint256 b = vault.proposeMember(g2, 0); - _approveGovernance(a); - _approveGovernance(b); - _warpTo(b); - _executeBy(a, alice); - vm.prank(alice); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.ThresholdUnsatisfiable.selector, "guardianThreshold")); - vault.execute(b); - } - - // ------------------------------------------------------------------ invariant 2: guardians never withdraw - function test_guardianCannotMoveFunds() public { - vm.startPrank(g1); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.NotAuthorized.selector, ROLE_OWNER | ROLE_APPROVER)); - vault.proposeTransfer(address(usd), g1, 1, ""); - vm.stopPrank(); - - uint256 id = _propose(address(usd), supplier, 1_000 * USD); - vm.prank(g1); - vm.expectRevert( - abi.encodeWithSelector(ISkurVault.InvalidProposal.selector, "guardian confirmation not required") - ); - vault.approve(id); - _approveBy(id, bob); - vm.prank(g1); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.NotAuthorized.selector, ROLE_EXECUTOR)); - vault.execute(id); - - // guardians cannot grant themselves treasury roles, and cannot run governance - vm.prank(g1); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.NotAuthorized.selector, ROLE_OWNER)); - vault.proposeMember(g1, ROLE_OWNER); - } - - // ------------------------------------------------------------------ recovery - function test_recoveryFlow() public { - address newBob = makeAddr("newBob"); - uint32 versionBefore = vault.policyVersion(); - - vm.prank(g1); - uint256 id = vault.proposeRecovery(bob, newBob); - Proposal memory p = vault.getProposal(id); - assertEq(p.requiredApprovals, 0); - assertEq(p.requiredGuardians, 1); - assertEq(p.executableAfter, block.timestamp + 48 hours); - - // owners cannot approve a recovery, guardians confirm it - vm.prank(alice); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.NotAuthorized.selector, ROLE_GUARDIAN)); - vault.approve(id); - _approveBy(id, g1); - - vm.prank(g1); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.Timelocked.selector, p.executableAfter)); - vault.execute(id); - - vm.warp(p.executableAfter); - _executeBy(id, g1); - assertEq(vault.rolesOf(bob), 0); - assertEq(vault.rolesOf(newBob), ROLE_OWNER | ROLE_APPROVER); - assertEq(uint8(vault.mode()), uint8(Mode.ELEVATED)); - assertEq(vault.policyVersion(), versionBefore); // invariant 10 - assertEq(vault.ownerCount(), 3); - } - - function test_ownerCanCancelRecoveryDuringDelay() public { - vm.prank(g1); - uint256 id = vault.proposeRecovery(bob, stranger); - vm.prank(carol); - vault.cancel(id); - assertEq(uint8(vault.getProposal(id).status), uint8(Status.CANCELLED)); - } - - function test_recoveryValidation() public { - vm.prank(g1); - vm.expectRevert(ISkurVault.NotMember.selector); - vault.proposeRecovery(stranger, makeAddr("x")); - vm.prank(g1); - vm.expectRevert(ISkurVault.MemberExists.selector); - vault.proposeRecovery(bob, carol); - vm.prank(alice); - vm.expectRevert(abi.encodeWithSelector(ISkurVault.NotAuthorized.selector, ROLE_GUARDIAN)); - vault.proposeRecovery(bob, stranger); - } - - // ------------------------------------------------------------------ expiry & cancel - function test_expiry() public { - uint256 id = _propose(address(usd), supplier, 1_000 * USD); - _approveBy(id, bob); - vm.warp(block.timestamp + 7 days + 1); - vm.prank(exec); - vm.expectRevert(ISkurVault.ProposalExpired.selector); - vault.execute(id); - vm.prank(stranger); - vault.expire(id); - assertEq(uint8(vault.getProposal(id).status), uint8(Status.CANCELLED)); - assertEq(vault.pendingCount(), 0); - } - - function test_cancelAuthority() public { - uint256 id = _propose(address(usd), supplier, 1_000 * USD); - vm.prank(exec); - vm.expectRevert(ISkurVault.NotCancellable.selector); - vault.cancel(id); - vm.prank(alice); // proposer - vault.cancel(id); - uint256 id2 = _propose(address(usd), supplier, 1_000 * USD); - vm.prank(carol); // owner - vault.cancel(id2); - assertEq(vault.pendingCount(), 0); - } - - // ------------------------------------------------------------------ invariants 11 & 12 - function test_nonStandardERC20Transfers() public { - NoBoolToken t = new NoBoolToken(); - t.mint(address(vault), 1_000_000); - AssetLimits memory l = AssetLimits({approved: true, lowMax: 10_000, highMax: 100_000, perTxMax: 0, dailyMax: 0}); - vm.prank(alice); - uint256 gid = vault.proposeAssetLimits(address(t), l); - _approveGovernance(gid); - _warpTo(gid); - _executeBy(gid, alice); - - uint256 id = _propose(address(t), supplier, 5_000); - _approveBy(id, bob); - _executeBy(id, exec); - assertEq(t.balanceOf(supplier), 5_000); - assertEq(vault.totalOutflow(address(t)), 5_000); - } - - function test_reentrancyBlocked() public { - Reenterer r = new Reenterer(vault); - vm.prank(alice); - vault.registerRecipient(address(r)); - vm.warp(block.timestamp + 24 hours); - uint256 a = _propose(address(0), address(r), 10 ether); - uint256 b = _propose(address(0), address(r), 10 ether); - _approveBy(a, bob); - _approveBy(b, bob); - r.setTarget(b); - vm.prank(exec); - vm.expectRevert(ISkurVault.NativeTransferFailed.selector); - vault.execute(a); - assertEq(vault.totalOutflow(address(0)), 0); - assertEq(uint8(vault.getProposal(a).status), uint8(Status.PENDING)); - } - - function test_previewMatchesPinnedRequirements() public { - RiskResult memory r = vault.previewTransfer(address(usd), supplier, 30_000 * USD); - uint256 id = _propose(address(usd), supplier, 30_000 * USD); - Proposal memory p = vault.getProposal(id); - assertEq(uint8(r.tier), uint8(p.tier)); - assertEq(r.reasons, p.riskReasons); - assertEq(r.requiredApprovals, p.requiredApprovals); - assertEq(r.requiredGuardians, p.requiredGuardians); - assertEq(p.executableAfter, block.timestamp + r.delay); - } - - function test_depositsTracked() public { - (bool ok,) = address(vault).call{value: 3 ether}(""); - assertTrue(ok); - assertEq(vault.totalDeposited(address(0)), 10_003 ether); - usd.mint(address(this), 7 * USD); - usd.approve(address(vault), 7 * USD); - vault.depositERC20(address(usd), 7 * USD); - assertEq(vault.totalDeposited(address(usd)), 7 * USD); - } -} diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md deleted file mode 100644 index 4e400b6..0000000 --- a/docs/DECISIONS.md +++ /dev/null @@ -1,75 +0,0 @@ -# Skur V1 design decisions - -Each entry resolves an open question from the product blueprint or records a choice the contracts now enforce. Changing any of these is a security decision and needs a matching invariant, test and policy-simulator scenario. - -## D1. Authoring language: Solidity, not Scriipture TypeScript - -See [scriipture-capability-report.md](scriipture-capability-report.md). Scriipture 0.1.0 cannot express events, enums, structs, ERC-20 interface calls or parameterised errors. Contracts are Solidity 0.8.28 with OpenZeppelin 5.1.0, built and tested with Foundry. - -## D2. Risk tiers: LOW / HIGH / CRITICAL, no MEDIUM - -Three tiers keep every escalation explainable in one sentence ("bumped one tier because the recipient is in probation"). Every escalation rule is a `+1 tier` or `at least X` step, so an intermediate tier would only add configuration surface without a new control. The reason bitmask on each proposal is how the interface explains the tier. - -## D3. Exposure is per asset, in that asset's units - -`exposureBps = amount / vault balance of the same asset`. No cross-asset valuation and therefore no oracle. Amount tiers and velocity caps are also per asset. The devnet stablecoin `SkurTestUSD` (6 decimals) stands in for USDC; registering any asset is itself a security-reducing governance change. - -## D4. Velocity accounting: anchored 24-hour buckets - -`daySpent` resets when 24 hours have passed since the bucket started. A bucket boundary lets an attacker spend up to two daily caps across 24 hours; the per-transaction cap, the circuit-breaker envelope and guardian veto bound that. Rolling windows were rejected for gas and because the invariant "bucket spend never exceeds cap" is directly checkable against a storage aggregate. - -## D5. Circuit breaker latches into Lockdown instead of paying - -The envelope is `envelopeBps` of the asset balance snapshotted at window start. An execution that would exceed it does **not** revert: it emits `CircuitBreakerTripped`, moves the vault to Lockdown and leaves the proposal pending. Reverting would let the attacker simply retry with a smaller amount; latching forces the strong exit process. The daily cap, by contrast, is a plain block (revert). - -## D6. Recipients in probation cannot receive funds before activation - -`executableAfter = max(now + tierDelay, activatesAt)`, and the tier is bumped. A brand-new address therefore always waits the activation delay, is escalated, and is guardian-vetoable. Unknown addresses are auto-registered at proposal time; pre-registering starts the clock early. - -## D7. Requirements are pinned at creation and can only tighten - -A proposal stores its tier, required approvals, guardian confirmations and `executableAfter`. At execution the policy in force is consulted again and the larger requirement wins. A policy that is later loosened never weakens an in-flight proposal; one that is tightened applies immediately. - -## D8. Approvals are recounted against live membership at execution - -Approver lists are stored per proposal and counted at execution against current roles. A removed signer's approval stops counting without any bookkeeping on removal. - -## D9. Guardian veto scope - -A single guardian can veto: any security-reducing governance proposal, any CRITICAL transfer, any transfer to a recipient in probation, and any transfer while the vault is not in Normal mode. Veto is always safe (it can only stop money moving), so it needs no quorum. Guardian *confirmation* (a positive act) uses `guardianThreshold`. - -## D10. Mode transitions - -Any owner or guardian can raise the mode immediately. Leaving Lockdown needs the governance threshold, `guardianThreshold` confirmations, the policy-change delay, and is vetoable. Leaving Elevated needs the same minus the delay. Tightening governance changes and recovery still execute in Lockdown; outgoing transfers and loosening changes do not. - -## D11. Recovery: guardian quorum, long delay, owner-cancellable - -Guardians propose replacing one signer with a new address that inherits exactly the same role bits. It executes after `recoveryDelay`, any owner can cancel during the delay, and the vault moves to Elevated when it completes. Policy version is untouched. This is symmetric: compromised guardians cannot take over without the real owners noticing for the whole delay, and compromised owners cannot replace guardians without going through the delayed, vetoable governance path. - -## D12. Guardian set composition - -Guardians hold no treasury role bits (enforced by `_validateRoles`). Veto is 1-of-N, confirmation and recovery are `guardianThreshold`-of-N. Templates ship with one guardian for small teams and two for DAO and fund profiles. A cold-storage guardian is a deployment choice, not a contract concept. - -## D13. Not upgradeable - -Vaults are EIP-1167 minimal proxies over one immutable implementation. There is no admin, no upgrade slot and the factory has no authority over vaults. Migration means creating a new vault and moving funds through the old vault's own policy. The implementation is deployed as a top-level contract and passed to the factory constructor, because the devnet Blockscout does not index contracts created inside another constructor and the implementation must be source-verified for the no-backend fallback to hold. - -## D14. Governance thresholds are counts, not weights - -`governanceThreshold` owner approvals, `approvalsLow/High/Critical` approver approvals. Weighted voting is out of V1 scope. - -## D15. Proposal expiry - -Every proposal expires `proposalTtl` after creation. Policy validation requires `proposalTtl >= longest delay + 1 hour` so every proposal has a real execution window. Anyone can call `expire` to keep the pending counter exact. - -## D16. Recipient payment history is recorded but not yet a risk input - -`paymentCount` and `totalPaid` are tracked and shown in the interface. Using them to relax or tighten tiers is deferred until a rule can be stated that is as explainable as the current ones. - -## D17. Endpoints live in two config files only - -`contracts/foundry.toml` (`[rpc_endpoints]`, `[etherscan]`) and `web/src/config/chain.ts`. Nothing else may inline an Ark host name, so the move from `sslip.io` to an Ark-owned domain is a two-file change. - -## D18. Reading the devnet: no JSON-RPC batching, windowed log scans, no indexer - -Measured on the Ark devnet RPC: a JSON-RPC batch is worked through almost serially (about 0.4 s per entry, 8.5 s for 20 calls), while 20 concurrent single requests return in about 3 s, and `eth_getLogs` refuses any range wider than 10,000 blocks. The interface therefore sends single requests, scans logs in 9,000-block windows with the last window ending at `latest`, caches proposals once they reach a terminal status, re-reads live approvals only for pending ones, and refetches a few more times after a write so a slow receipt still shows up. Full history without an indexer stays acceptable at devnet scale; a mainnet deployment should add one rather than widen these scans. diff --git a/docs/MIGRATION_EVM_TO_SUI.md b/docs/MIGRATION_EVM_TO_SUI.md new file mode 100644 index 0000000..d48ba4f --- /dev/null +++ b/docs/MIGRATION_EVM_TO_SUI.md @@ -0,0 +1,107 @@ +# Moving from EVM to Sui + +What came across, what changed shape because Sui is not an EVM chain, and what the old build is +still here for. + +## Why move at all + +The EVM build worked and was deployed to the Ark Constellation devnet. The reason to move is not +that the code was wrong; it is that the controls Skur exists to provide — approvals that scale with +risk, cumulative outflow accounting, a circuit breaker, a guardian plane — have no onchain +competitor on Sui, while on an EVM chain they sit next to a mature module ecosystem around Safe. +Sui is where this is infrastructure rather than a feature. + +Four things about Sui also make the design better rather than merely portable: + +- **A vault is one shared object.** The policy, the roster, the recipients and the proposals are + fields on it, so "what are the rules right now" is a single read. On the EVM they were storage + slots across a proxy, and the interface had to reconstruct them. +- **Programmable transaction blocks** let creation be atomic without a factory. `begin` hands back + a value with no abilities, so the same transaction must configure the assets and call `finish`. + There is no half-configured vault, and no EIP-1167 clone to deploy and verify. +- **`sui::clock`** gives millisecond time in the transaction. Timelocks are durations, not block + arithmetic. +- **Typed coins.** The treasury is keyed by `TypeName`, so one vault holds any coin type with its + own limits, day and envelope — no token allowlist to maintain, and no `SafeERC20` dance around + tokens that lie about their return values. + +## What carried over unchanged + +The security model. Every control in the EVM build is in the Move build, with the same semantics: + +- the four treasury roles and the exclusive guardian role; +- the three tiers, the six trust levels, the three security modes, the reason bitmask; +- all seventeen policy fields and every validation rule, including the awkward ones — the + proposal lifetime that must clear the longest delay by an hour, the hard block that cannot sit + below the critical exposure threshold; +- the loosening comparison, including the cases where a number going down is a weakening; +- anchored 24-hour buckets and the loss envelope; +- recipient activation delay, pinned-then-tighten-only requirements, live approval recount; +- guardian veto, guardian recovery with an owner cancel; +- the seven policy templates and the attack simulator. + +The TypeScript engines came across as the same algorithms: `risk`, `policy`, `templates`, +`maxLoss`, `posture`, `simulator`, `format`. They now live in [`sdk/src/engine/`](../sdk/src/engine/) +as one copy shared by every interface, instead of in `web/src/lib` with the mobile app reaching +into it through a path alias. + +## What changed shape + +| EVM | Sui | Why | +|---|---|---| +| `SkurFactory` + EIP-1167 clones | `vault::begin` → `setup_*` → `finish` in one PTB | A shared object does not need a proxy, and a hot potato makes the configuration atomic | +| `address` asset identifiers with an allowlist | `TypeName` keys into a `Bag` of `Balance` | The type system is the allowlist | +| `mapping` + arrays for members, recipients, proposals | `Table` and dynamic fields | Reads are per entry rather than per array scan, which is why the SDK lists names and fetches values | +| Durations in seconds | Durations in milliseconds | `sui::clock` reports milliseconds; converting at every comparison is how you get an off-by-1000 | +| `revert` on a tripped breaker, with the mode write lost | `execute_transfer` returns without aborting, so the Lockdown write commits | A latch that reverts with the transaction is not a latch. This is the one behavioural change, and it is a fix | +| `nonReentrant` | Nothing | Move has no dynamic dispatch into a caller; reentrancy is absent by construction | +| `SafeERC20` | `balance::split` | A `Balance` cannot lie about a transfer | +| Events read with `eth_getLogs`, in 9,000-block windows | `listEvents` over gRPC with ledger cursors | No block-range limit to work around; a cursor resumes exactly | +| wagmi + viem, Vite | `@mysten/dapp-kit-react` 2.x, Next.js | `@mysten/dapp-kit` 1.x is deprecated for being JSON-RPC only | +| Nothing | A `PROPOSER` role | The agent case: software that can request a payment and provably cannot take one | +| Nothing | A Node/PostgreSQL indexer and notification service | The EVM interface read the chain directly for everything, which is honest but slow past a few hundred proposals | + +### The one semantic change worth arguing about + +On the EVM, a transfer that would cross the loss envelope reverted. Reverting undid the state +change that put the vault into Lockdown, so the breaker tripped and the vault stayed open — an +attacker could keep trying, and every attempt cost them only gas. + +In Move the function returns instead of aborting: the proposal is marked `BLOCKED`, the vault is set +to Lockdown, a `BreakerTripped` event is emitted, and the transaction **succeeds** so all of that +persists. The money does not move. This is tested by +`crossing_the_loss_envelope_freezes_the_vault_instead_of_paying`, which asserts all three: the +status, the unchanged balance, and the Lockdown. + +## The old build is gone + +`contracts/`, `web/` and `mobile/` have been removed. Two implementations of one security model is +one more than anyone can keep honest, and keeping the EVM build alongside the Sui one meant every +claim on the site, in the docs and in the tests had to be read twice to know which chain it was +about. + +Nothing is lost. The EVM build is in this repository's history and on three branches that were +never deleted: + +| Branch | What it holds | +|---|---| +| `feat/skur-v1` | The original V1: Foundry contracts, the interface, the devnet deployment | +| `feat/safe-pattern-ui` | The rebuilt interface, plus the mobile app merged into it | +| `feat/mobile-app` | The Expo app on its own | + +```bash +git show feat/safe-pattern-ui:contracts/src/SkurVault.sol # read a file without checking out +git checkout feat/safe-pattern-ui # or bring the whole thing back +``` + +The deployed Ark Constellation devnet vault and its verified sources are unaffected by any of +this — they live on that chain, not in this repository. + +### What was not ported, and why + +- **The mobile app.** Sui wallet connection on a device is its own piece of work: the Wallet + Standard assumes a browser extension, and the mobile story goes through deep links to a wallet + app instead. It is a project, not a port, and it was not in the target stack. +- **The devnet test tokens.** `SkurTestToken` existed because the EVM devnet had no stablecoin. On + Sui the treasury is keyed by coin type, so any coin works without a stand-in. +- **The ABI export script.** Move has no ABI to export; the SDK's BCS layouts replace it. diff --git a/docs/SECURITY_MODEL.md b/docs/SECURITY_MODEL.md deleted file mode 100644 index 0e45853..0000000 --- a/docs/SECURITY_MODEL.md +++ /dev/null @@ -1,64 +0,0 @@ -# Skur V1 security model - -This is the contract-level view of the blueprint's security model: what each control is, where it is enforced, and which test proves it. The claim Skur makes is bounded: **independent, programmable controls that can limit or delay damage even when authorized credentials are compromised.** Nothing here makes signer compromise harmless. - -## Planes and roles - -| Plane | Role bits | Can | Cannot | -|---|---|---|---| -| Treasury | `OWNER(1)`, `APPROVER(2)`, `EXECUTOR(4)` | propose (owner/approver), approve (approver), execute (executor) transfers | veto, confirm critical actions, propose recovery | -| Governance | `OWNER(1)` | propose and approve policy, limits, membership, recipient trust, mode relaxation; cancel any proposal; raise mode | approve transfers unless also approver | -| Guardian | `GUARDIAN(8)` | raise mode, veto, confirm critical transfers and mode relaxation, propose and confirm recovery | hold any treasury bit, propose or approve transfers, execute transfers, run governance | - -Guardian exclusivity is enforced in `_validateRoles` and holds for every membership path, including recovery. - -## Transaction lifecycle as implemented - -1. `proposeTransfer` runs `previewTransfer` (deterministic tier + reasons), checks per-transaction cap and hard exposure block, auto-registers unknown recipients, and pins `tier`, `requiredApprovals`, `requiredGuardians`, `executableAfter` (at least the recipient's activation time) and `expiresAt`. -2. Approvers call `approve`; guardians call `approve` to confirm when `requiredGuardians > 0`. -3. `execute` (executor only) checks: not expired, timelock passed, not in Lockdown, live approvals under current roles, requirements re-derived from the current policy if it changed (only ever stricter), recipient not blocked and past activation, balance, per-transaction cap, hard exposure block, daily bucket, circuit-breaker envelope. -4. State and counters update, `ProposalExecuted` and `TransferExecuted` emit, then the external transfer happens (`call{value}` or `SafeERC20.safeTransfer`). - -## Invariants and where they are proven - -| # | Invariant | Enforced by | Proven by | -|---|---|---|---| -| 1 | No execution before all active conditions | `execute` ordering | `invariant_timelockRespected`, `test_highTierNeedsTwoApprovalsAndDelay`, `test_criticalTierNeedsGuardianAndIsVetoable` | -| 2 | Guardians cannot transfer treasury assets | role exclusivity, role gates on propose/approve/execute | `test_guardianCannotMoveFunds`, `invariant_guardiansNeverMoveFunds`, `invariant_rolesConsistent` | -| 3 | Lockdown blocks ordinary outgoing execution | `execute` lockdown gate, `proposeTransfer` gate | `test_lockdownBlocksOutgoingButNotDepositsOrGuardians`, `invariant_lockdownRespected` | -| 4 | Security-reducing policy changes never take immediate effect | `SkurPolicyLib.policyReduces` and friends, `_createGovernance` delay | `test_looseningPolicyIsDelayedAndVetoable`, `test_mixedChangeCountsAsLoosening`, `test_addingMemberIsDelayedRemovingIsImmediate`, `test_trustedIsSecurityReducingAndDelayed`, `test_approvingNewAssetIsDelayed` | -| 5 | Cumulative outflow cannot be bypassed by splitting | `daySpent` bucket and envelope in `_checkTransfer` | `test_splitDrainHitsDailyCap`, `invariant_dailyCapNeverExceeded`, `invariant_envelopeNeverExceeded` | -| 6 | No proposal executes twice | status set before interaction, `_pending` | `test_cannotExecuteTwice`, `invariant_noDoubleExecution`, `invariant_pendingCountExact` | -| 7 | Removed signers cannot approve new actions | role check in `approve`, live recount in `execute` | `test_removedSignerApprovalStopsCounting` | -| 8 | Approval counting cannot be duplicated or replayed | `hasApproved` per proposal per signer | `test_duplicateApprovalRejected`, `invariant_executedTransfersHadEnoughLiveApprovals` | -| 9 | New recipients cannot bypass activation delay | `executableAfter = max(.., activatesAt)` plus re-check at execution | `test_newRecipientWaitsForActivation`, `test_reProbationRestartsClockAndBlocksPending`, `invariant_probationRespected` | -| 10 | Recovery cannot silently weaken policy | `_applyRecovery` copies roles only, mode to Elevated, policy untouched | `test_recoveryFlow`, `test_recoveryValidation` | -| 11 | Native and ERC-20 accounting is correct | `totalOutflow`, `SafeERC20` | `invariant_nativeAccounting`, `invariant_erc20Accounting`, `test_nonStandardERC20Transfers` | -| 12 | External calls cannot re-enter an unsafe state | `nonReentrant`, effects before interactions | `test_reentrancyBlocked`, Slither reentrancy detectors (clean) | - -## Threats mapped to controls - -| Threat | What happens in the contract | -|---|---| -| One signer compromised | Cannot reach any tier's threshold alone unless `approvalsLow == 1`; even then LOW is capped by `lowMax`, the daily bucket and the envelope. | -| Enough signers compromised | HIGH/CRITICAL delays give guardians the veto window; CRITICAL needs guardian confirmation; envelope trips Lockdown on the first over-limit attempt. | -| Admin tries to weaken policy | Loosening waits `policyChangeDelay`, is vetoable, and is impossible while in Lockdown. | -| Drain split into many transfers | Daily bucket and envelope count cumulatively; pinned tiers do not matter at execution. | -| New malicious recipient | Forced wait until activation, one-tier bump, vetoable. | -| Guardian compromised | No withdrawal path exists; recovery is delayed and owner-cancellable; guardian can only freeze or veto. | -| Frontend compromised | Every requirement is computed onchain; the wallet shows the calldata; Blockscout-verified source lets any interface reproduce the reads. | -| Lost signer | Guardian recovery after `recoveryDelay`, owners can cancel. | -| Emergency | Any owner or guardian enters Lockdown instantly; leaving needs threshold + guardians + delay. | - -## Known limitations (state these to customers) - -- **Bucket boundary.** Anchored 24h buckets allow up to two daily caps across a boundary. The envelope and per-transaction cap bound this; the maximum-loss view in the interface accounts for it. -- **Exposure is per asset.** A vault holding several assets has several independent exposure calculations. There is no portfolio-level number without an oracle. -- **Circuit breaker is a latch, not a refund.** Everything executed before the trip stays paid. -- **Single-guardian vaults.** With `guardianThreshold = 1`, one guardian key both freezes and unfreezes (with owners). Templates for larger organisations ship with two. -- **Devnet stablecoin.** `SkurTestUSD` is mintable by anyone. It is a stand-in, not an asset. -- **No contract-call proposals in V1.** Only native and ERC-20 transfers exist, so "unlimited approval" and "malicious contract" threats are excluded by construction rather than analysed. - -## Static analysis - -`slither . --filter-paths "lib/|test/|script/" --exclude-informational --exclude-optimization` reports fourteen low findings: enum strict-equality false positives, one intentionally unused return, timestamp comparisons required by timelocks, and a benign write-after-call in the factory that was reordered. No medium or high findings. diff --git a/docs/SUI_LANDSCAPE.md b/docs/SUI_LANDSCAPE.md new file mode 100644 index 0000000..6eb690e --- /dev/null +++ b/docs/SUI_LANDSCAPE.md @@ -0,0 +1,130 @@ +# What already exists on Sui, and what it does not do + +Research pass: 2026-10-05. Purpose: decide what V1 has to be so that it is not a second +multisig, and so that the grant application can say precisely what is missing from the ecosystem +today. + +## The three things a Sui organization can use today + +### 1. Sui native multisig (protocol-level) + +Built into the protocol. A multisig address is a committee of up to **10 public keys**, each with +a weight, and a threshold. It can mix key schemes (Ed25519, ECDSA Secp256k1, Secp256r1), which is +genuinely useful — a Ledger, a passkey and a hot key can sit in the same committee. + +What it is: **signature aggregation**. The chain checks that the weights of the supplied +signatures reach the threshold, and then the transaction executes exactly as any other. + +What it cannot express, at all: + +- any dependence on **what** the transaction does — the same threshold clears a $10 invoice and + the entire treasury; +- any dependence on **who receives** it; +- any dependence on **what has already happened today**; +- any delay, any freeze, any veto, any recovery. + +There is also no onchain proposal object: coordination happens offchain, by passing partial +signatures around. There is nothing to query, index, or show a reviewer. + +### 2. account.tech (formerly Kraken) — Smart Account Framework + +The most serious piece of engineering in this space, and the real benchmark. A shared, programmable +account object with an **intent-based architecture**: an intent is created, resolved against the +account's `Config`, and then executed. + +Implemented: configurable ownership rules, **role-based permissions**, asset and object management, +**payments and vestings** (recurring transfer intents), currency control (locking a `TreasuryCap`), +package upgrade control (locking `UpgradeCap`s) with **an optional time-lock on upgrades**, and +dependency whitelisting so only approved packages can touch account state. + +Not implemented, by its own documentation: spending limits or transaction caps, recipient +allowlists or denylists, risk scoring, emergency freeze, social recovery. + +The shape of it matters: account.tech is an **extensibility** framework. Its security story is +"only approved packages may act, and the config decides who may act." That is a different axis +from ours. It makes a treasury *programmable*; it does not make a treasury *defended*. The upgrade +timelock is the only time-based control, and it protects code, not money. + +### 3. Maven, by the MSafe team + +The incumbent product: "the first and only multi-signature non-custodial digital assets management +solution on Sui," live on mainnet since day one. Multi-signature approval, **weighted voting power** +for co-managers, granular role assignment, and **co-signer account recovery**. + +Its published documentation does not describe spending limits, recipient allowlists, timelocks, +risk scoring or an emergency freeze. + +Maven is the closest thing to a Safe on Sui, and like Safe it is a *threshold* product. Safe's own +answer to policy is a module ecosystem bolted on afterwards; Maven does not have even that yet. + +### Also present, worth naming + +- **Streamflow** — multichain treasury management (Solana, Aptos, Sui) from one dashboard. Payment + streaming and vesting, not authorization policy. +- **MPCVault** — multi-chain MPC custody with one approval workflow and an audit log across + Ethereum, Bitcoin, Solana, TRON, TON, Aptos and Sui. Closest to us on *process* (approval + workflow, audit log), but it is **offchain policy over MPC custody**: the rules live in a + company's backend, not in a contract you can read. If their service is compromised or goes away, + the rules go with it. + +## The gap, stated as one sentence + +Every option on Sui today answers one question — *did enough of the right keys sign?* — and nothing +answers the question that actually decides whether a treasury survives a compromise: **given the +amount, the destination, the recent outflow and the vault's current posture, how much authorization +should this payment need, and should it be allowed to settle at all right now?** + +Concretely, nothing on Sui provides: + +| Control | Native multisig | account.tech | Maven | MPCVault | Skur | +|---|---|---|---|---|---| +| k-of-n threshold | yes | yes | yes | yes | yes | +| Roles beyond signer | no | yes | yes | yes | yes | +| Onchain proposal object | no | yes | yes | no | yes | +| Approvals that scale with amount | no | no | no | offchain | **yes** | +| Approvals that scale with share of treasury | no | no | no | no | **yes** | +| Per-transaction and 24-hour caps | no | no | no | offchain | **yes** | +| Cumulative accounting (split-proof) | no | no | no | unclear | **yes** | +| Recipient trust levels | no | no | no | allowlist | **yes** | +| New-recipient activation delay | no | no | no | no | **yes** | +| Risk-tiered timelocks | no | code upgrades only | no | no | **yes** | +| Loss-envelope circuit breaker | no | no | no | no | **yes** | +| Emergency freeze | no | no | no | yes (operator) | **yes (guardian)** | +| Guardian role with no spend path | no | no | no | no | **yes** | +| Tightening-only policy changes | no | no | no | no | **yes** | +| Guardian veto on weakening | no | no | no | no | **yes** | +| Signer recovery with delay + owner cancel | no | no | co-signers | yes (operator) | **yes** | +| Agent/automation role that cannot execute | no | no | no | no | **yes** | +| Rules enforced in Move, readable onchain | n/a | yes | yes | **no** | **yes** | + +## What this means for V1 + +1. **Do not compete on being a multisig.** Native multisig is free and in the protocol; Maven + already ships the product. Competing there is competing on UI. + +2. **Compete on the layer above it.** The pitch is a *programmable authorization and treasury + security layer*: the thing that decides how much authorization a payment needs and whether it + may settle now. A Skur vault can sit behind a native multisig or a Maven account as one of its + owners — we are not asking anyone to abandon the committee they already trust. + +3. **Interoperate with account.tech rather than duplicate it.** They own extensibility; we own + policy. A Skur policy object is a legitimate thing for an account.tech intent resolver to + consult, and that is a partnership conversation, not a competitive one. + +4. **The differentiators that no one can answer quickly** are the ones that require state over + time, not just a config: cumulative velocity accounting, the loss envelope measured against the + balance when the window opened, recipient probation, and tightening-only policy changes. Those + are the four to lead with. + +5. **Enforcement in Move is the honest claim against MPCVault.** Their controls are real but they + are somebody's backend. Ours are in a shared object anyone can read. That is the sentence for + the funding application. + +## Sources + +- https://docs.sui.io/develop/transactions/transaction-auth/multisig +- https://github.com/account-tech/move-framework +- https://github.com/account-tech +- https://doc.m-safe.io/sui +- https://sui.directory/project/maven/ +- https://blog.sui.io/write-multi-signature-multisig-move-contracts/ diff --git a/docs/SUI_POSITIONING.md b/docs/SUI_POSITIONING.md new file mode 100644 index 0000000..1b17831 --- /dev/null +++ b/docs/SUI_POSITIONING.md @@ -0,0 +1,196 @@ +# Skur: a programmable authorization and treasury security layer for Sui + +Positioning and funding narrative. Written for grant reviewers, RFP committees and prospective +investors — people who will read one page before deciding whether to read the code. + +--- + +## The one sentence + +**Skur decides how much authorization a payment needs, and whether it may settle right now, from +the amount, the destination, the recent outflow and the vault's own security posture — enforced in +Move, in a shared object anyone can read.** + +It is not a wallet. It is the authorization layer a treasury sits behind. + +## Why this is not a multisig pitch + +A multisig answers one question: *did k of n sign?* + +That question is necessary and insufficient. Every large onchain treasury loss of the last three +years happened with **valid signatures**. The keys were real, the threshold was met, the contract +did exactly what it was told. The failure was never the signature check. It was that nothing +between the signature and the money asked a second question: + +> Given the amount, the destination, what has already left today and what the organization knows +> right now, *should* this payment settle? + +Sui already has signature aggregation in the protocol — native multisig, up to ten keys, mixed +schemes, free. Building a nicer interface over it is a design project, not an infrastructure one. +The layer that is genuinely missing is the one that answers the second question. + +## What exists on Sui today, and what it does not do + +A detailed pass is in [`SUI_LANDSCAPE.md`](./SUI_LANDSCAPE.md). The short version: + +| | Native multisig | account.tech | Maven (MSafe) | MPCVault | **Skur** | +|---|---|---|---|---|---| +| k-of-n threshold | yes | yes | yes | yes | yes | +| Onchain proposal object | no | yes | yes | no | yes | +| Approvals that scale with amount | no | no | no | offchain | **yes** | +| Approvals that scale with share of treasury | no | no | no | no | **yes** | +| Cumulative, split-proof outflow accounting | no | no | no | unclear | **yes** | +| Recipient trust and a new-recipient delay | no | no | no | allowlist | **yes** | +| Risk-tiered timelocks | no | code upgrades only | no | no | **yes** | +| Loss-envelope circuit breaker | no | no | no | no | **yes** | +| Guardian role with no spend path | no | no | no | no | **yes** | +| Tightening-only policy changes with a veto | no | no | no | no | **yes** | +| Rules enforced in Move, readable onchain | n/a | yes | yes | **no** | **yes** | + +**account.tech** is the serious engineering in this space and the right benchmark. It is an +*extensibility* framework: an intent-based smart account where only approved packages may act. Its +one time-based control is an upgrade timelock, which protects code rather than money. It makes a +treasury programmable; it does not make a treasury defended. These are different axes, and we would +rather be a policy module it can consult than a competing account standard. + +**MPCVault** has real approval workflows and audit logs across chains — and they live in a +company's backend. Ours live in a shared object. That is the whole difference, and it is the +sentence we are willing to be judged on. + +## What the Move core actually enforces + +Six controls stand between a valid approval and the money. All of them are in +[`sui/sources/`](../sui/sources/), and every one has a test named after the attack it defends +against in [`sui/tests/`](../sui/tests/). + +1. **Risk-tiered authorization.** Five independent signals — the absolute amount against the + asset's own limits, the payment's share of holdings, the recipient's standing, the day's + outflow so far, and the vault's security mode — each able to raise the tier and none able to + lower it. The tier sets the approvals, the guardian sign-off and the wait. + +2. **Recipient trust with an activation delay.** An address the vault has never paid is scored + higher, every time. A newly registered one serves a delay before it can be paid at all. Raising + an address's trust is a governance action with a timelock; a guardian can restrict or block one + instantly, alone. + +3. **Cumulative velocity accounting.** Per-transaction and per-day caps, counted against what has + already left. Crossing half the day's allowance escalates every further payment, so a drain + split into twenty pieces meets a higher bar partway through instead of never. + +4. **A loss-envelope circuit breaker.** A share of the balance that may leave per window. The + payment that would cross it **is not made**: the vault latches into Lockdown and the transaction + still succeeds, because a latch that reverts with the transaction is not a latch. The envelope + is measured against the balance when the window *opened*, so a drain cannot shrink its own + denominator. + +5. **A guardian role with no path to the money.** The contract refuses to give a guardian any + treasury role, and refuses to give a treasury member the guardian role. A guardian can freeze, + veto, block a recipient and start a recovery. It can never propose, approve or execute a + payment. It is the second control plane an attacker has to breach and the one they cannot profit + from. + +6. **A policy-change firewall.** Any change that gives up a control is detected by comparing the + old and new policy field by field — including the cases that look like tightening and are not, + such as setting a threshold to zero to disable it, or extending a proposal's lifetime. A + weakening waits out the policy-change delay, can be vetoed by any guardian, and cannot be + proposed at all while the vault is in Lockdown. + +Plus the things that make the above hold under adversarial conditions: + +- **Requirements are pinned, then tighten-only.** At execution the payment is reclassified and the + *stricter* of the pinned and live requirements applies. Tightening the policy affects payments + already in flight; loosening it cannot rescue one. +- **Approvals are recounted against the live roster.** A signer removed after approving stops + counting. The tally is never trusted as a number frozen at approval time. +- **Deposits are never gated.** Even in Lockdown, even from a stranger. The controls exist to + govern money leaving; refusing money arriving would only strand it. + +## The agent case, which is where this is going + +An organization increasingly wants software to *request* payments — an invoice processor, a +rebalancer, an LLM agent reading a vendor inbox. Nobody sensibly wants software to *authorize* +them. + +Skur has a `PROPOSER` role that can open a proposal and do nothing else. It cannot approve, cannot +confirm, cannot execute, and cannot be a guardian. A fully compromised agent key produces a queue +of proposals that no human approved. That is the difference between an automation incident and a +loss, and no threshold product can express it, because to a threshold every key is the same kind of +key. + +## Why Sui, specifically + +This is not a port looking for a chain. + +- **A vault is a shared object**, not an account with a nonce. Its policy, its roster, its + recipients and its proposals are fields on one object, so "what are the rules right now" is a + single read and not an archaeology exercise across storage slots. +- **Programmable transaction blocks** make a hot-potato setup flow possible: creating a vault + hands back a value with no abilities, so the same transaction must configure the assets and + finish, and there is no window in which a half-configured vault is reachable. +- **`sui::clock`** gives millisecond time in the transaction, so timelocks are expressed directly + rather than inferred from block numbers. +- **Typed coins** mean the treasury is keyed by type. One vault holds SUI, stablecoins and any + other coin, each with its own limits, its own day and its own envelope — without an allowlist of + token addresses to maintain or get wrong. +- **Move's abilities** make the guardian separation structural rather than conventional. + +## V1 scope + +Done and under test: + +- the Move core: types, policy, risk engine, vault, with the seven proposal kinds and the full + lifecycle — open, approve or reject, confirm, veto, cancel, expire, execute; +- a TypeScript SDK on `@mysten/sui` v2 over **gRPC and GraphQL**; +- a Node/PostgreSQL indexer, notification service and advisory API; +- the chain-agnostic engines — risk classifier, policy rules, templates, maximum-loss calculation, + posture report and attack simulator — shared by every interface; +- seven starting policies, one per kind of organization: startup, operating team, protocol/DAO, + fund, payments company, nonprofit, family office. + +Explicitly not done, and we will not claim otherwise: + +- **no independent audit.** Do not put real value behind Skur until one is complete. Part of what + we are asking for is the audit. +- **no mainnet deployment.** +- **no oracle.** Exposure is per asset, as a share of that asset's own balance. A vault holding two + assets does not know their relative value, and we would rather state that than imply a + portfolio-wide number we cannot compute honestly. +- **anchored, not sliding, windows.** A day bucket anchors on its first spend. It is explained in + the security model rather than hidden. + +## On not building around JSON-RPC + +JSON-RPC was switched off on Sui Foundation mainnet full nodes in the week of 27 July 2026, with +full decommissioning — code removal included — scheduled for mid-October 2026. + +Nothing in this project depends on it, directly or transitively. That is why the frontend uses +`@mysten/dapp-kit-react` 2.x rather than `@mysten/dapp-kit` 1.x, which is deprecated precisely +because it is JSON-RPC only. The SDK's read layer is written against the transport-agnostic Core +API, so the same code runs over gRPC today and over anything that implements that contract +tomorrow. + +## Where the funding goes + +| Route | The ask | What it buys | +|---|---|---| +| **Sui Foundation RFP / grant** | Audit and testnet-to-mainnet hardening | An audited, public authorization layer any Sui organization can use, and the only onchain one with velocity and breaker controls | +| **Hackathon** | Proof under a deadline | The agent-proposer demo: an automated agent that can request payments and provably cannot take them | +| **Hydropower accelerator** | Go-to-market with real treasuries | Pilots with three to five Sui organizations; the policy templates come from what they actually need | +| **Academic Research Award** | Formal treatment of the policy algebra | A proof that "tightening-only" is sound: that no sequence of permitted changes reaches a weaker policy without the delay and the veto | +| **Strategic investment** | The product around the core | Mobile, notifications, the organization dashboard, and the integrations | + +The audit is the first ask and the honest one. Everything else is easier to fund after it. + +## What we want a reviewer to check + +Not the pitch. These: + +- [`sui/sources/vault.move`](../sui/sources/vault.move) — `execute_transfer` is where every control + is re-evaluated against live state. If that function is right, the product is right. +- [`sui/sources/policy.move`](../sui/sources/policy.move) — `reductions` is the policy firewall. The + interesting cases are the ones where a number going *down* is a loosening. +- [`sui/tests/vault_tests.move`](../sui/tests/vault_tests.move) — each test is named after the + attack. `crossing_the_loss_envelope_freezes_the_vault_instead_of_paying` is the one to read + first. +- [`docs/SUI_LANDSCAPE.md`](./SUI_LANDSCAPE.md) — the competitive pass, with what the incumbents do + and do not implement, by their own documentation. diff --git a/docs/SUI_SECURITY_MODEL.md b/docs/SUI_SECURITY_MODEL.md new file mode 100644 index 0000000..73fd18e --- /dev/null +++ b/docs/SUI_SECURITY_MODEL.md @@ -0,0 +1,135 @@ +# Skur security model (Sui / Move) + +What each control is, where it is enforced, and which test proves it. + +The claim is bounded and worth stating before anything else: **Skur adds independent, programmable +controls that can limit or delay damage even when authorized credentials are misused.** It does not +make signer compromise harmless. Anyone who reads this document and concludes otherwise has read it +wrong. + +Module paths are under [`sui/sources/`](../sui/sources/); tests under [`sui/tests/`](../sui/tests/). + +## Two control planes + +| Plane | Role bits | Can | Cannot | +|---|---|---|---| +| Treasury | `OWNER(1)`, `APPROVER(2)`, `EXECUTOR(4)`, `PROPOSER(16)` | open a payment (any treasury role), approve one (approver), execute one (executor), run governance (owner) | veto, confirm, hold the guardian role | +| Guardian | `GUARDIAN(8)` | raise the mode including straight to Lockdown, veto, confirm critical payments, restrict or block a recipient alone, open a recovery | hold any treasury bit, open a payment, approve one, execute one, run governance | + +The exclusivity is `types::roles_valid`, and every path that writes a role goes through it: +`begin`, `execute_member` and `execute_recovery` all call `apply_roles`, which asserts it again. +There is no path that installs a role without it. + +`PROPOSER` is the automation role. It satisfies `can_propose` and nothing else — not approval, not +confirmation, not execution, not governance. + +## The lifecycle, as implemented + +1. **`propose_transfer`** refuses a blocked recipient, an unapproved asset, a zero amount, an + amount over the per-transaction cap, an amount over what is left of the day, and anything at or + above the hard block. Then it classifies the payment through `risk::classify` and pins the tier, + the reasons, the exposure, the required approvals, the required guardian confirmations, + `executable_at` (the tier's delay, or the recipient's activation time, whichever is later) and + `expires_at`. +2. **`approve`** takes the approver role for a payment and the owner role for governance, and + records the approval against the address. **`reject`** takes the same role and records the + opposite, settling the proposal once as many signers have rejected it as it needed to be + approved. **`confirm`** takes the guardian role. +3. **`execute_transfer`** re-evaluates everything against live state — see below — and only then + writes the status, charges the windows, and moves the coin. + +Governance kinds share the same path through `open_governance` and `authorize_governance`, so there +is one place where a proposal becomes effective and one place to audit. + +## What execution re-checks + +This is the function to read: `execute_transfer` in [`vault.move`](../sui/sources/vault.move). + +| Re-checked | Why it cannot be checked only at proposal | +|---|---| +| The asset is still approved | Governance can revoke an asset while a payment is in flight | +| Balance covers the amount | The vault may have paid something else since | +| Recipient is not blocked, and has activated | A guardian can block an address after it was approved | +| Per-transaction and daily caps, under the current mode | The mode may have risen to Elevated, halving both | +| The hard block, against the live balance | A smaller balance makes the same amount a larger share | +| The tier, reclassified | Every input above may have moved | +| Approvals and confirmations, recounted against live roles | A signer may have been removed since approving | +| The timelock, against the live delay as well as the pinned one | The policy may have tightened | +| The loss envelope | Checked last, and charged in the same step as the payment | + +Requirements are **pinned, then tighten-only**: the stricter of the pinned and the live requirement +applies. Tightening the policy affects payments already in flight; loosening it cannot rescue one. + +The order of those checks is itself part of the design. The recipient's activation is checked before +the tier's timelock, so a payment to an address that is still in probation is refused *for that +reason* rather than with a generic "too early" — which is the difference between a signer +understanding what to do and a signer retrying. + +## Invariants, and the test that proves each + +| # | Invariant | Enforced by | Proven by | +|---|---|---|---| +| 1 | Nothing executes before every active condition is met | the ordered assertions in `execute_transfer` | `a_new_recipient_cannot_skip_activation`, `one_signer_cannot_clear_a_high_tier_payment` | +| 2 | A guardian cannot move treasury assets through guardian authority | `types::roles_valid`, role gates on propose/approve/execute | `a_guardian_cannot_open_a_payment`, `a_guardian_cannot_hold_a_treasury_role` | +| 3 | Lockdown stops outgoing execution and new payments | `assert_not_lockdown` in `propose_transfer` and `execute_transfer` | `lockdown_stops_new_payments_from_being_opened` | +| 4 | A security-reducing change never takes effect immediately | `policy::reductions`, `limit_reductions`, `trust_reduces`, and the delay in `open_governance` | `loosening_the_policy_has_to_wait`, `loosening_the_policy_succeeds_once_the_delay_has_run`, `a_guardian_can_veto_a_weakening`, `tightening_the_policy_takes_effect_as_soon_as_owners_agree` | +| 5 | Cumulative outflow cannot be bypassed by splitting | the day bucket and the envelope in `execute_transfer` | `splitting_a_drain_does_not_beat_the_daily_cap`, `velocity_pressure_escalates_mid_drain` | +| 6 | A proposal cannot execute twice | status written before the coin moves, `settle` decrements once | `a_payment_cannot_execute_twice` | +| 7 | A removed signer's approval stops counting | `count_with_role` against the live roster at execution | `a_removed_signers_approval_stops_counting` | +| 8 | An approval cannot be duplicated or replayed | one entry per address in a `VecSet`, checked before insert | `an_approval_cannot_be_replayed` | +| 9 | A new recipient cannot skip its activation delay | `executable_at = max(delay, activates_at)`, re-asserted at execution | `a_new_recipient_cannot_skip_activation` | +| 10 | Recovery cannot quietly weaken the vault | `execute_recovery` copies the roles it finds now, leaves the policy untouched, and raises the mode | `recovery_moves_the_roles_and_leaves_the_vault_elevated`, `recovery_cannot_skip_its_delay`, `any_owner_can_cancel_a_recovery` | +| 11 | Balance accounting reconciles | one `Balance` per type in a `Bag`; `balance::split` is the only outgoing path | `a_routine_payment_needs_one_approval_and_no_wait`, `the_daily_bucket_reopens_after_a_day` | +| 12 | The roster always satisfies the policy | `assert_counts_valid` after `finish`, `execute_member` and `execute_recovery` | `counts_must_satisfy_the_policy`, and the roster assertions in the recovery tests | +| 13 | The breaker's latch survives the attempt | `execute_transfer` returns without aborting, so the Lockdown write commits | `crossing_the_loss_envelope_freezes_the_vault_instead_of_paying` | +| 14 | Leaving a raised posture is never unilateral | `propose_mode_relax` needs owners, guardians and the policy-change delay | `leaving_lockdown_takes_owners_a_guardian_and_time`, `owners_alone_cannot_lift_a_freeze` | +| 15 | An automation key cannot authorize itself | `PROPOSER` satisfies only `can_propose` | `an_agent_cannot_approve_its_own_proposal`, `an_agent_cannot_execute`, `an_agent_cannot_reject`, `a_proposer_can_only_propose` | +| 16 | Rejecting takes as many signers as approving | `reject` counts rejections against live roles and compares to `req_approvals` | `one_rejection_does_not_turn_down_a_payment_that_needs_two`, `as_many_rejections_as_approvals_turns_a_payment_down`, `a_rejected_payment_cannot_execute` | + +Reentrancy is absent by construction rather than defended against: Move has no dynamic dispatch +into a caller, and the only outgoing value movement is `transfer::public_transfer` of a coin split +after the status has been written. + +## Threats, mapped to what the contract does + +| Threat | What happens | +|---|---| +| One signer compromised | Cannot reach any tier's threshold alone unless `approvals_low` is 1 — and even then the payment is bounded by the routine maximum, the per-transaction cap, the day's remaining allowance and the envelope | +| Enough signers compromised | The high and critical delays are a guardian's veto window; a critical payment needs a guardian; the envelope latches the vault on the first attempt that would cross it | +| An owner quorum weakens the policy | The change waits out `policy_change_delay`, any guardian can veto it, and it cannot be opened at all in Lockdown | +| A drain split into many payments | The day bucket and the envelope count cumulatively; crossing half the day's allowance escalates every further payment | +| A new malicious recipient | Escalated every time while unknown, forced to wait out activation once registered, and vetoable | +| A guardian key compromised | There is no withdrawal path. A recovery waits `recovery_delay` and any owner cancels it. A guardian can freeze, veto and block | +| An automation key compromised | A queue of proposals nobody approved | +| The interface compromised | Every requirement is computed on chain. `preview_transfer` is the same code `execute_transfer` runs, so a preview cannot promise what the vault would refuse, and any interface can reproduce the reads | +| A lost signer | Guardians open a recovery, owners can cancel it, the roles move unchanged, the vault comes out Elevated | +| An emergency | Any owner or guardian enters Lockdown instantly | + +## Known limitations + +State these to anyone evaluating the system. They are design positions, not oversights. + +- **No independent audit yet.** Do not put real value behind Skur until one is complete. +- **Anchored windows, not sliding ones.** A day bucket anchors on its first charge, so up to two + days' allowance can leave across a boundary. The envelope and the per-transaction cap bound it, + and the maximum-loss view says so explicitly rather than quietly assuming the best case. +- **Exposure is per asset.** A vault holding several coin types has several independent exposure + calculations and no portfolio-wide number. Producing one would need an oracle, and a wrong + portfolio number is worse than an honest per-asset one. +- **The breaker latches; it does not refund.** Everything that executed before the trip stays paid. + What it buys is that the trip happens *instead of* the payment that would have crossed the line, + not after it. +- **The envelope basis is the balance when the window opened.** That is deliberate — measuring + against the live balance would let a drain shrink its own denominator — but it means a large + deposit mid-window does not raise the window's allowance until the window rolls. +- **Single-guardian vaults.** With `guardian_threshold = 1`, one guardian key both freezes and (with + owners) unfreezes. The protocol, fund and larger templates ship with two. +- **A shared object serializes.** Every write to a vault goes through consensus on that object, so + one vault has a throughput ceiling. For a payments treasury doing thousands of payments an hour + that is a real constraint, and the answer is several vaults rather than pretending otherwise. +- **No arbitrary contract calls in V1.** Only coin transfers exist. "Unlimited approval" and + "malicious contract interaction" are excluded by construction rather than analysed — which is + also why decoding arbitrary calls is on the roadmap rather than claimed as done. +- **`register_recipient` does not auto-promote.** Paying an unknown address does not make it known; + only governance changes trust. A payment to a never-registered address is escalated every time, + forever, which is intended and occasionally surprising. diff --git a/docs/scriipture-capability-report.md b/docs/scriipture-capability-report.md deleted file mode 100644 index d40b49f..0000000 --- a/docs/scriipture-capability-report.md +++ /dev/null @@ -1,49 +0,0 @@ -# Scriipture 0.1.0 capability report - -**Date:** 2026-09-15 -**Scope:** Can Skur's V1 contracts be authored in Scriipture TypeScript, as the project brief assumes? -**Result:** No. The vault, factory and test stablecoin are authored in plain Solidity 0.8.28 and built with Foundry. The brief explicitly allows this fallback ("authoring that module in plain Solidity") and asks for the decision to be made before the state-machine design. This document records the evidence. - -## Method - -`scriipture@0.1.0` was installed into a scratch directory and its published artifacts were read directly: `types/index.d.ts`, `types/standards.d.ts`, `docs/details.md`, and the compiled parser, mapper, emitter and validator in `dist/index.js`. Claims below reference the bundle's own source comments (`// src/parser/parse.ts`, `// src/mapper/expressions.ts`, and so on). - -## What is present - -| Primitive the brief asked to verify | Status | Evidence | -|---|---|---| -| `msg.sender` inside function bodies | Present | `msg`, `block` globals are declared in `types/index.d.ts` and rewritten verbatim by `GLOBAL_OBJECT_REWRITES`. Per-role checks are expressible with `require(rolesOf.get(msg.sender) & ROLE != 0n, "...")`. | -| `block.timestamp` | Present | Same global rewrite. | -| `require` / `revert` | Present | `require(cond, "msg")` is rewritten into an auto-derived parameterless custom error by the `custom-errors` optimizer pass. `revert("...")` is emitted as a string revert. | -| Reentrancy guard | Present | `@nonReentrant` wires OpenZeppelin `ReentrancyGuard`. | -| Mappings | Present, no iteration | `Map` maps to `mapping`; `.get/.set/.has/.delete` only. | - -## What is missing - -| Primitive | Status | Evidence | Impact on Skur | -|---|---|---|---| -| **Events** | Absent | `parseClass` returns `events: []` unconditionally; no syntax populates it. `emit(...)` is exported as a function type but `emitCall` has no special case, so `emit(Foo, a)` is emitted literally as `emit(Foo, a)`, which is not Solidity. | Section 10 of the blueprint requires every policy version and activation time onchain; the interface reads events for history. | -| **Custom errors with parameters** | Absent | Only the auto-derived, parameterless errors from `require` strings exist (`errorByMsg` in the custom-errors pass). | Reverts such as `Timelocked(executableAfter)` and `InsufficientApprovals(have, need)` cannot be expressed. | -| **Enums** | Absent | `parseContractFiles` only visits `ts.isClassDeclaration` nodes; enum declarations are skipped. The docs additionally state string enums are rejected. | Modes, tiers, trust states and proposal kinds would have to be untyped `bigint` constants. | -| **Structs** | Absent | Only `PropertyDeclaration`, `MethodDeclaration` and `ConstructorDeclaration` class members are parsed. A custom type name is passed through `solidityType` verbatim with no declaration emitted. | `Proposal`, `Policy`, `AssetLimits`, `Recipient` would need to be split into parallel mappings. | -| **ERC-20 interface calls** | Absent | `STANDARD_IMPORTS` contains only `ERC20`, `ERC721`, `Ownable`, `ReentrancyGuard`, `Test`. There is no way to import `IERC20` or `SafeERC20`, and no `interface` declaration syntax. | Vault cannot call `transfer` on an approved token. A low-level `call` would be flagged by the validator and Slither. | -| **Inheritance beyond OZ bases** | Partial | `extends X` emits `is X` but only adds an import when `X` is in `STANDARD_IMPORTS`. | Cannot split the vault into modules. | -| **Invariants over aggregates** | Present in principle | `@invariant` methods become Forge invariant tests. | Usable, but moot if the contract cannot be written. | -| **Visibility control** | Undocumented | `external/public/internal/private` decorators exist in `FUNCTION_DECORATORS` but are not exported from `types/index.d.ts`. All state variables are emitted `public`. | Internal helpers would be public entry points unless the undocumented decorators are used. | - -## Escape hatches considered - -1. **`solidity\`...\`` tagged templates.** Any expression statement wrapped in this tag is emitted verbatim. Events could be emitted this way, but their *declarations* still cannot be, because the emitter only writes declarations from `contract.events`, which nothing populates. -2. **Plugin optimizer pass.** A plugin receives the `IRContract` and could push into `contract.events` and `contract.errors`. This would recover events and parameterless errors. It cannot recover enums, structs, interfaces or imports because the IR has no slot for them. -3. **Raw statements.** Unrecognised statements are emitted verbatim (`kind: "raw"`), but only inside function bodies. Contract-level declarations are unreachable. - -Combining all three would produce a TypeScript file that is mostly Solidity strings glued together by a 0.1.0 transpiler. The generated Solidity is what gets audited either way, and the brief itself notes that a 0.1.0 transpiler in the path of a treasury contract is part of the threat model. Plain Solidity is the smaller attack surface. - -## What Scriipture is still used for - -- `scriipture deploy --browser` can deploy any compiled artifact (it reads `out/artifacts/.json` and only needs bytecode plus ABI). It is documented in the README as an optional self-custodial deploy path for teams that want a browser-wallet signature instead of a keystore. -- `scriipture verify-source` targets only the Etherscan v2 multichain API (`ETHERSCAN_V2` in the CLI bundle). Ark devnet's explorer is Blockscout, so verification uses `forge verify-contract --verifier blockscout` instead. - -## Re-evaluation trigger - -Re-run this check when Scriipture publishes a version whose parser handles enum, struct, interface and event declarations, or documents a plugin parse hook. Until then, the TypeScript surface of Skur is the interface and the risk-engine mirror in `web/src/lib/risk.ts`, which is pinned to the Solidity engine by shared test vectors. diff --git a/mobile/.env.example b/mobile/.env.example new file mode 100644 index 0000000..3e43396 --- /dev/null +++ b/mobile/.env.example @@ -0,0 +1,9 @@ +# Which network, and the package published on it. +EXPO_PUBLIC_SKUR_NETWORK=testnet +EXPO_PUBLIC_SKUR_PACKAGE_ID=0x0 + +# A full node other than the public one. +# EXPO_PUBLIC_SKUR_GRPC_URL=https://fullnode.testnet.sui.io:443 + +# The indexer. Optional: every screen reads the chain directly without it. +# EXPO_PUBLIC_SKUR_API=http://localhost:8787 diff --git a/mobile/.gitignore b/mobile/.gitignore new file mode 100644 index 0000000..100dbea --- /dev/null +++ b/mobile/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +.expo/ +dist/ +web-build/ +ios/ +android/ +*.log +.env +.env.local diff --git a/mobile/App.tsx b/mobile/App.tsx new file mode 100644 index 0000000..9a73f3a --- /dev/null +++ b/mobile/App.tsx @@ -0,0 +1,449 @@ +import './src/lib/polyfills'; + +import { useFonts } from 'expo-font'; +import { DMMono_400Regular, DMMono_500Medium } from '@expo-google-fonts/dm-mono'; +import { DMSans_400Regular, DMSans_500Medium, DMSans_700Bold } from '@expo-google-fonts/dm-sans'; +import { SpaceGrotesk_600SemiBold, SpaceGrotesk_700Bold } from '@expo-google-fonts/space-grotesk'; +import { + createNavigationContainerRef, + DarkTheme, + DefaultTheme, + NavigationContainer, + useNavigation, + type NavigatorScreenParams, +} from '@react-navigation/native'; +import { createBottomTabNavigator, type BottomTabNavigationProp } from '@react-navigation/bottom-tabs'; +import { createNativeStackNavigator, type NativeStackScreenProps } from '@react-navigation/native-stack'; +import { QueryClient, useQueryClient } from '@tanstack/react-query'; +import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'; +import { StatusBar } from 'expo-status-bar'; +import * as Linking from 'expo-linking'; +import { useEffect, useRef, useState } from 'react'; +import { ActivityIndicator, View } from 'react-native'; +import { SafeAreaProvider } from 'react-native-safe-area-context'; + +import { ErrorBoundary } from './src/components/ErrorBoundary'; +import { Icon, OfflineBanner } from './src/components/ui'; +import { useOnline } from './src/hooks/useOnline'; +import { NETWORK } from './src/lib/config'; +import { parseLink } from './src/lib/links'; +import { useNotificationTaps } from './src/lib/notifications'; +import { persister } from './src/lib/persist'; +import { StoreProvider, useStore } from './src/state/store'; +import { ThemeProvider, useScheme, useTheme } from './src/state/theme'; +import { F } from './src/theme'; + +import { Launch } from './src/screens/onboarding/Launch'; +import { Tour } from './src/screens/onboarding/Tour'; +import { Start } from './src/screens/onboarding/Start'; +import { CreateAccount } from './src/screens/onboarding/CreateAccount'; +import { CreateOrg, type OrgDraft } from './src/screens/onboarding/CreateOrg'; +import { AddMembers, type MemberDraft } from './src/screens/onboarding/AddMembers'; +import { ConnectWallet } from './src/screens/onboarding/ConnectWallet'; + +import { ActivityLog } from './src/screens/ActivityLog'; +import { Dashboard } from './src/screens/Dashboard'; +import { Help } from './src/screens/Help'; +import { Home } from './src/screens/Home'; +import { More } from './src/screens/More'; +import { OpenVault } from './src/screens/OpenVault'; +import { Policies } from './src/screens/Policies'; +import { Receive } from './src/screens/Receive'; +import { Roles } from './src/screens/Roles'; +import { Security } from './src/screens/Security'; +import { Send } from './src/screens/Send'; +import { Settings } from './src/screens/Settings'; +import { Team } from './src/screens/Team'; +import { Transactions } from './src/screens/Transactions'; +import { TxDetail } from './src/screens/TxDetail'; + +/** + * Reads are cached to disk, so a cold start offline shows the vault as it last was — the offline + * banner states its age. Only vault reads persist: a transfer preview is a quote, and a stale + * quote shown as current is worse than none. + */ +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 5_000, + retry: 1, + refetchOnWindowFocus: false, + gcTime: 24 * 3_600_000, + }, + }, +}); + +const persistOptions = { + persister, + maxAge: 24 * 3_600_000, + dehydrateOptions: { + // Only settled vault reads. A query dehydrated while still pending is rehydrated as a + // promise that nothing will ever resolve — react-query rejects it on restore and logs a + // warning over the UI. + shouldDehydrateQuery: (q: { queryKey: readonly unknown[]; state: { status: string } }) => + q.queryKey[0] === 'vault' && q.state.status === 'success', + }, +}; + +/** Route params are serialisable by contract, so a proposal id travels as a string. */ +type ProposalsParams = { Queue: undefined; Proposal: { id: string }; New: undefined }; +type TeamParams = { Members: undefined; Roles: undefined }; +type MoreParams = { + Menu: undefined; + SecurityCenter: undefined; + Policies: undefined; + Logs: undefined; + ReceiveScreen: undefined; + SettingsScreen: undefined; + Help: undefined; +}; +type TabParams = { + Home: undefined; + Treasury: undefined; + Proposals: NavigatorScreenParams; + Team: NavigatorScreenParams; + More: NavigatorScreenParams; +}; + +const Tab = createBottomTabNavigator(); +const ProposalsNav = createNativeStackNavigator(); +const TeamNav = createNativeStackNavigator(); +const MoreNav = createNativeStackNavigator(); +const navRef = createNavigationContainerRef(); + +/** + * Stacks, not component swaps: Android's hardware back closes the inner screen instead of the + * app, iOS keeps its swipe-back, and a deep link has something to target. + */ +function ProposalsStack() { + return ( + + + {({ navigation }) => ( + navigation.navigate('Proposal', { id: id.toString() })} /> + )} + + + {({ route, navigation }: NativeStackScreenProps) => ( + navigation.goBack()} /> + )} + + + {({ navigation }) => navigation.navigate('Queue')} />} + + + ); +} + +function TeamStack() { + return ( + + + {({ navigation }) => navigation.navigate('Roles')} />} + + + {({ navigation }) => navigation.goBack()} />} + + + ); +} + +function MoreStack() { + return ( + + + {({ navigation }) => ( + + navigation.navigate( + screen === 'Security' + ? 'SecurityCenter' + : screen === 'Receive' + ? 'ReceiveScreen' + : screen === 'Settings' + ? 'SettingsScreen' + : screen, + ) + } + /> + )} + + + + {({ navigation }) => navigation.goBack()} />} + + + {({ navigation }) => navigation.goBack()} />} + + + + {({ navigation }) => ( + navigation.navigate('Help')} + onOpenSecurity={() => navigation.navigate('SecurityCenter')} + /> + )} + + + {({ navigation }) => navigation.goBack()} />} + + + ); +} + +function HomeTab() { + const navigation = useNavigation>(); + const { closeVault } = useStore(); + return ( + navigation.navigate('Proposals', { screen: 'Queue' })} + onOpenProposal={(id) => navigation.navigate('Proposals', { screen: 'Proposal', params: { id: id.toString() } })} + onOpenTreasury={() => navigation.navigate('Treasury')} + onSwitch={closeVault} + /> + ); +} + +function TreasuryTab() { + const navigation = useNavigation>(); + const { closeVault } = useStore(); + return ( + navigation.navigate('Proposals', { screen: 'Queue' })} + onSend={() => navigation.navigate('Proposals', { screen: 'New' })} + onRequest={() => navigation.navigate('More', { screen: 'ReceiveScreen' })} + onSwitch={closeVault} + /> + ); +} + +/** The banner sits above the tabs, with the age of what is on screen. */ +function ConnectionBanner() { + const online = useOnline(); + const { vaultId } = useStore(); + const qc = useQueryClient(); + if (online) return null; + const asOf = vaultId ? qc.getQueryState(['vault', NETWORK, vaultId])?.dataUpdatedAt : undefined; + return ; +} + +function Tabs() { + const C = useTheme(); + return ( + <> + + ({ + headerShown: false, + tabBarActiveTintColor: C.text, + tabBarInactiveTintColor: C.text3, + tabBarStyle: { backgroundColor: C.canvas, borderTopColor: C.border }, + tabBarLabelStyle: { fontFamily: F.bodyMedium, fontSize: 11 }, + tabBarIcon: ({ color }) => { + const name = + route.name === 'Home' + ? 'home' + : route.name === 'Treasury' + ? 'pie-chart' + : route.name === 'Proposals' + ? 'list' + : route.name === 'Team' + ? 'users' + : 'more-horizontal'; + return ; + }, + })} + > + + + + + + + + ); +} + +/** + * Deep links: skur://vault/0x…, skur://proposal/42, skur://vault/0x…/proposal/42. + * + * A link can arrive before the navigator mounts, before the store loads, or before any vault is + * open — tapping a notification cold-starts the app into exactly that state. So links queue, and + * the queue drains when everything the link needs is ready. + */ +function DeepLinks({ navReady }: { navReady: boolean }) { + const url = Linking.useURL(); + const { ready, address, vaultId, openVault } = useStore(); + const pending = useRef>(null); + const handled = useRef(null); + + useEffect(() => { + if (!url || url === handled.current) return; + const parsed = parseLink(url); + if (!parsed) return; + handled.current = url; + pending.current = parsed; + if (parsed.kind === 'vault') openVault(parsed.vaultId); + }, [url, openVault]); + + useEffect(() => { + const link = pending.current; + if (!link || !navReady || !ready || !address || !vaultId) return; + const proposalId = link.proposalId; + pending.current = null; + if (proposalId && navRef.isReady()) { + navRef.navigate('Proposals', { screen: 'Proposal', params: { id: proposalId } }); + } + }); + + return null; +} + +type OnboardStep = 'launch' | 'tour' | 'start' | 'account' | 'org' | 'members' | 'wallet'; + +/** + * Screens 1–7 as one flow. "Create" walks the whole path and ends with the vault created on + * chain; "Join" skips the organisation and lands on the key, then the vault picker. + */ +function Onboarding({ onDone }: { onDone: () => void }) { + const { address } = useStore(); + // A device that already holds a key skips the marketing and goes straight to the fork — but a + // key created mid-flow must NOT reset the flow, so this is the initial step only. + const [step, setStep] = useState(address ? 'start' : 'launch'); + const [org, setOrg] = useState(null); + const [members, setMembers] = useState([]); + const [joining, setJoining] = useState(false); + + switch (step) { + case 'launch': + return setStep('tour')} />; + case 'tour': + return setStep('start')} />; + case 'start': + return ( + { + setJoining(false); + setStep('account'); + }} + onJoin={() => { + setJoining(true); + setStep('account'); + }} + /> + ); + case 'account': + return ( + setStep('start')} + onDone={() => setStep(joining ? 'wallet' : 'org')} + onWallet={() => { + // "Continue with Wallet": straight to the key; the vault picker follows. + setJoining(true); + setStep('wallet'); + }} + /> + ); + case 'org': + return ( + setStep('account')} + onDone={(draft) => { + setOrg(draft); + setStep('members'); + }} + /> + ); + case 'members': + return ( + setStep('org')} + onDone={(list) => { + setMembers(list); + setStep('wallet'); + }} + /> + ); + case 'wallet': + return ( + setStep(joining ? 'account' : 'members')} + onDone={onDone} + /> + ); + } +} + +/** An introduction, a key to set up, a vault to pick, or the vault. */ +function Root() { + useNotificationTaps(); + const C = useTheme(); + const { dark } = useScheme(); + const { ready, onboarded, address, vaultId, finishOnboarding } = useStore(); + const [navReady, setNavReady] = useState(false); + + if (!ready) { + return ( + + + + ); + } + + const nav = { + ...(dark ? DarkTheme : DefaultTheme), + colors: { + ...(dark ? DarkTheme : DefaultTheme).colors, + background: C.canvas, + card: C.canvas, + border: C.border, + text: C.text, + primary: C.accent, + }, + }; + + return ( + setNavReady(true)}> + + + {address && vaultId ? ( + + ) : !onboarded || !address ? ( + // The flow owns the screen until it says it is done. Creating the key mid-flow flips + // `address`, and an address-based gate here would unmount the flow one step before it + // creates the organisation — which is exactly the bug this ordering exists to prevent. + + ) : ( + + )} + + ); +} + +export default function App() { + const [loaded] = useFonts({ + DMSans_400Regular, + DMSans_500Medium, + DMSans_700Bold, + DMMono_400Regular, + DMMono_500Medium, + SpaceGrotesk_600SemiBold, + SpaceGrotesk_700Bold, + }); + + return ( + + + + + {loaded ? : null} + + + + + ); +} diff --git a/mobile/README.md b/mobile/README.md new file mode 100644 index 0000000..c58be8f --- /dev/null +++ b/mobile/README.md @@ -0,0 +1,159 @@ +# Skur on iOS and Android + +Expo and React Native, reading the chain through `@mysten/sui` over gRPC and sharing the vault +logic with every other interface via [`@skur/sdk`](../sdk). + +## The key on the device + +This is the part to understand before using it. + +The app generates an Ed25519 key and writes it to the **iOS keychain or Android keystore**, not to +app storage. `requireAuthentication` means the OS will not release it without a successful +biometric or passcode check, and the key is unlocked **per transaction** — there is no session. +A cached signer is a signer that acts without a face, which on a treasury app is the thing you +were trying to avoid. + +What that does **not** give you: + +- **No protection on a rooted or jailbroken device.** The keystore's guarantees end there. +- **No backup.** Lose the phone and the key is gone — which is what the vault's guardian recovery + is for: guardians move your roles to a new key after a delay any owner can cancel. + +**A key on a phone is a hot key.** It is appropriate for an **approver** or an **executor**, whose +authority is bounded by the vault's policy — tiers, caps, the day's allowance, the loss envelope. +It is **not** appropriate for a **guardian**. A guardian's entire value is being an independent +control plane an attacker has to breach separately; keeping a guardian key on the same phone as an +approver key collapses the two planes the contract works to keep apart. The app says so where it +matters, and the contract will refuse to give one address both roles anyway. + +## Testing it + +```bash +npm test +``` + +Twenty-eight tests that render the real screens and press their real buttons. They exist in this +form because there is no way to send a touch to a device from a headless machine — no Simulator +GUI, no `idb_companion` — and because a render-and-press suite is repeatable in a way a +hand-driven simulator session is not. + +What they cover is the part of the interface that can actually hurt someone: **which action each +role is offered**. The contract refuses an action the caller has no role for, so an interface that +offers it is not merely unsafe, it is lying to the person holding the phone. So each case asserts +both halves — what appears, and what does not: + +- an approver is offered Approve and Reject, and not Execute; +- an executor the reverse; +- a guardian is never offered Approve on a payment, only Confirm and Veto; +- an **agent is offered nothing at all**, because proposing is the whole of its authority; +- a veto appears on a critical payment and on a weakening, and not on a routine one; +- approvals are counted against live roles, so a removed signer's approval shows as not counting; +- a disabled Execute does not fire when pressed. + +Two things about React Native Testing Library 14 that cost real time, recorded so they do not cost +it twice: **`render` and `fireEvent` are both asynchronous.** An un-awaited `fireEvent` does not +fail the test that fired it — it leaves React's work queue mid-flight, and the *next* test's render +resolves to an empty tree, which looks like a dozen unrelated failures. + +The action buttons carry `testID`s rather than being found by their labels, because the Execute +button's label changes with its state — "Not enough approvals yet" until it is ready — and that +state is exactly what is worth testing. + +## Running it + +From the repository root, `npm run ios` and `npm run android` work too. + +```bash +npm install +cp .env.example .env # point it at a network and a published package +npm start # Metro, for Expo Go or a dev build +npm run ios # prebuild, pods and a simulator run +npm run android +npm run typecheck +npm test +``` + +### If the iOS build dies in CMake + +``` +CMAKE_APPLE_ARCH_SYSROOTS: …sdk;…sdk +is not the same length as CMAKE_OSX_ARCHITECTURES: arm64 +``` + +Hermes builds itself through CMake and caches `CMAKE_APPLE_ARCH_SYSROOTS` as an **internal** +variable, which CMake never recomputes. A build that once configured for two architectures leaves +a two-entry list behind, and every later build fails against a one-entry `CMAKE_OSX_ARCHITECTURES` +no matter what you change. It is not a CMake version problem — 3.31 and 4.4 both fail the same way. + +```bash +rm -rf ios/Pods/hermes-engine/build ios/Pods/hermes-engine/build_host_hermesc +``` + +If `ios/` was generated by a different app — it is not committed, so it can be anything left on +disk — regenerate it rather than reusing it: + +```bash +npx expo prebuild --clean --platform ios +``` + +Every script builds [`../sdk`](../sdk) first, because the app consumes its compiled output and a +fresh clone would otherwise fail with an unhelpful resolver error. + +| Variable | What it does | +|---|---| +| `EXPO_PUBLIC_SKUR_NETWORK` | `mainnet`, `testnet`, `devnet` or `localnet` | +| `EXPO_PUBLIC_SKUR_PACKAGE_ID` | The published package | +| `EXPO_PUBLIC_SKUR_GRPC_URL` | A full node other than the public one | +| `EXPO_PUBLIC_SKUR_API` | The indexer. Optional — see below | + +`app.json`'s `extra` holds the committed defaults; `EXPO_PUBLIC_*` overrides them. + +## What needed solving for React Native + +Three things, all in [`src/lib/polyfills.ts`](src/lib/polyfills.ts) and +[`metro.config.js`](metro.config.js): + +- **Hermes has no CSPRNG behind `crypto.getRandomValues`**, which key generation needs. + `react-native-get-random-values` supplies it, and `assertCryptoReady` fails at startup rather + than mid-signature if it did not load. +- **The text codecs BCS needs** ship with recent React Native, but a silent absence would surface + much later as a corrupted transaction rather than a missing global, so there is a guarded UTF-8 + fallback. +- **The Sui SDK is ESM with an exports map and no CommonJS fallback**, and `@skur/sdk` resolves + through a symlink to a sibling directory. Metro needs `unstable_enablePackageExports`, `mjs` in + `sourceExts`, and the workspace in `watchFolders`. +- **`Intl.PluralRules` does not exist in Hermes**, and the Sui SDK builds an ordinal formatter with + it at **module scope** — `new Intl.PluralRules("en-US", { type: "ordinal" })`, used to say "1st + command" in an error message. On Hermes the constructor is `undefined`, so importing anything + from the SDK's client throws `undefined cannot be used as a constructor` before a line of app + code runs, with no usable stack. An English-ordinals polyfill fixes it, and `assertCryptoReady` + checks it at startup. + +Transport is **gRPC-web over fetch**, which is what React Native can actually do — there is no +HTTP/2 socket to reach for. JSON-RPC is not used anywhere: Sui switched it off on mainnet full +nodes in July 2026 and is removing it. + +## The indexer is optional + +Every screen reads the chain directly through the SDK. Setting `EXPO_PUBLIC_SKUR_API` adds one +thing — finding your vaults by address, since a vault does not know it is yours — and saves round +trips. Without it you open a vault by its id and everything else works. + +## Screens + +| Screen | What it does | +|---|---| +| Welcome, Set up signing | The pitch, then creating or importing the device key | +| Treasury | Posture, holdings, today's outflow per asset, what the policy enforces | +| Activity | The queue and the history, with one proposal per row | +| Proposal | Approve, reject, confirm, veto or execute — each gated on the role the contract requires | +| Pay | Open a payment, with the vault's own `preview_transfer` as the review | +| Receive | The vault address, and which assets it accepts | +| Security | The posture report, the maximum-loss numbers, and freezing the vault | +| Settings | Appearance, the device key, the network, closing the vault | + +Every action is gated on the role the device's key actually holds, so a signer is never offered +something the vault would refuse. The payment review is the chain's classification, simulated +live — the phone does not compute a tier, because an interface that computed its own answer could +promise something the vault would reject, and on a confirmation screen that is the one thing that +must never happen. diff --git a/mobile/app.json b/mobile/app.json new file mode 100644 index 0000000..05b93c2 --- /dev/null +++ b/mobile/app.json @@ -0,0 +1,73 @@ +{ + "expo": { + "name": "Skur", + "slug": "skur", + "version": "1.0.0", + "orientation": "portrait", + "scheme": "skur", + "icon": "./assets/icon.png", + "backgroundColor": "#000000", + "userInterfaceStyle": "automatic", + "newArchEnabled": true, + "ios": { + "supportsTablet": false, + "bundleIdentifier": "io.skur.app", + "usePrecompiledModules": false, + "infoPlist": { + "NSFaceIDUsageDescription": "Skur unlocks your signing key with Face ID so a payment cannot be approved without you.", + "NSCameraUsageDescription": "Skur uses the camera to scan a vault or recipient address." + } + }, + "android": { + "package": "io.skur.app", + "edgeToEdgeEnabled": true, + "adaptiveIcon": { + "foregroundImage": "./assets/android-icon-foreground.png", + "backgroundImage": "./assets/android-icon-background.png", + "monochromeImage": "./assets/android-icon-monochrome.png" + }, + "permissions": [ + "android.permission.USE_BIOMETRIC", + "android.permission.CAMERA" + ] + }, + "web": { + "favicon": "./assets/favicon.png" + }, + "plugins": [ + [ + "expo-splash-screen", + { + "image": "./assets/splash-icon.png", + "imageWidth": 160, + "resizeMode": "contain", + "backgroundColor": "#000000" + } + ], + [ + "expo-local-authentication", + { + "faceIDPermission": "Skur unlocks your signing key with Face ID so a payment cannot be approved without you." + } + ], + [ + "expo-camera", + { + "cameraPermission": "Skur uses the camera to scan a vault or recipient address." + } + ], + [ + "expo-notifications", + { + "icon": "./assets/android-icon-monochrome.png", + "color": "#ffd000" + } + ], + "expo-secure-store" + ], + "extra": { + "SKUR_NETWORK": "testnet", + "SKUR_PACKAGE_ID": "0x0" + } + } +} \ No newline at end of file diff --git a/mobile/assets/android-icon-background.png b/mobile/assets/android-icon-background.png new file mode 100644 index 0000000..3b82e19 Binary files /dev/null and b/mobile/assets/android-icon-background.png differ diff --git a/mobile/assets/android-icon-foreground.png b/mobile/assets/android-icon-foreground.png new file mode 100644 index 0000000..0cc17ce Binary files /dev/null and b/mobile/assets/android-icon-foreground.png differ diff --git a/mobile/assets/android-icon-monochrome.png b/mobile/assets/android-icon-monochrome.png new file mode 100644 index 0000000..d4e621f Binary files /dev/null and b/mobile/assets/android-icon-monochrome.png differ diff --git a/mobile/assets/favicon.png b/mobile/assets/favicon.png new file mode 100644 index 0000000..77bbd20 Binary files /dev/null and b/mobile/assets/favicon.png differ diff --git a/mobile/assets/icon.png b/mobile/assets/icon.png new file mode 100644 index 0000000..c42b768 Binary files /dev/null and b/mobile/assets/icon.png differ diff --git a/mobile/assets/mark.png b/mobile/assets/mark.png new file mode 100644 index 0000000..9aba193 Binary files /dev/null and b/mobile/assets/mark.png differ diff --git a/mobile/assets/splash-icon.png b/mobile/assets/splash-icon.png new file mode 100644 index 0000000..6d1eb45 Binary files /dev/null and b/mobile/assets/splash-icon.png differ diff --git a/mobile/index.ts b/mobile/index.ts new file mode 100644 index 0000000..5fd059f --- /dev/null +++ b/mobile/index.ts @@ -0,0 +1,4 @@ +import { registerRootComponent } from 'expo'; +import App from './App'; + +registerRootComponent(App); diff --git a/mobile/jest.config.js b/mobile/jest.config.js new file mode 100644 index 0000000..aa51fbb --- /dev/null +++ b/mobile/jest.config.js @@ -0,0 +1,37 @@ +/** + * Tests render the real screens and fire their real handlers. + * + * This is how the app's behaviour is verified: this environment has no Simulator GUI and no + * idb_companion, so there is no way to send a touch to a running device. Rendering the component + * tree and pressing through it is both closer to the code and repeatable, which a hand-driven + * simulator session is not. + */ +const preset = require('jest-expo/jest-preset'); + +module.exports = { + preset: 'jest-expo', + // The Sui SDK's gRPC entry is built from `.mjs` chunks, which the preset's `\.[jt]sx?$` pattern + // does not match — so Jest would hand raw ESM to a CommonJS runtime. The same gap Metro had. + // + // The preset's own transforms are kept rather than replaced: they are what handle React Native's + // Flow-typed sources, and dropping them fails on the first `value(id: TimeoutID)` it meets. + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'mjs', 'json', 'node'], + transform: { ...preset.transform, '^.+\\.mjs$': preset.transform['\\.[jt]sx?$'] }, + setupFilesAfterEnv: ['/test/setup.tsx'], + testMatch: ['/test/**/*.test.tsx'], + // The Sui SDK and its dependencies ship ESM, which Jest has to transform rather than skip. + transformIgnorePatterns: [ + 'node_modules/(?!((jest-)?react-native|@react-native(-community)?|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|@mysten/.*|@skur/.*|@protobuf-ts/.*|@noble/.*|@scure/.*|valibot|graphql|gql.tada|poseidon-lite))', + ], + moduleNameMapper: { + // The screens import the client for its type and its proxy; tests supply their own data. + '^../lib/client$': '/test/mocks/client.ts', + '^../../lib/client$': '/test/mocks/client.ts', + // @skur/sdk is ESM with an exports map holding only `import`, which Jest's CommonJS resolver + // does not read. Point at the build output and let Babel transform it like any source file. + '^@skur/sdk$': '/../sdk/dist/index.js', + // Babel's helpers are required from the transformed SDK, which sits outside this package and + // so cannot resolve them on its own. + '^@babel/runtime/(.*)$': '/node_modules/@babel/runtime/$1', + }, +}; diff --git a/mobile/metro.config.js b/mobile/metro.config.js new file mode 100644 index 0000000..150d534 --- /dev/null +++ b/mobile/metro.config.js @@ -0,0 +1,28 @@ +const { getDefaultConfig } = require('expo/metro-config'); +const path = require('node:path'); + +const projectRoot = __dirname; +const workspaceRoot = path.resolve(projectRoot, '..'); + +const config = getDefaultConfig(projectRoot); + +// `@skur/sdk` is a file: dependency, so it resolves through a symlink to a sibling directory. +// Metro has to be told to watch the workspace and to look there for modules, or an edit to the +// SDK is invisible until the bundler is restarted. +config.watchFolders = [workspaceRoot]; +config.resolver.nodeModulesPaths = [ + path.resolve(projectRoot, 'node_modules'), + path.resolve(workspaceRoot, 'node_modules'), +]; + +// The Sui SDK ships ESM with an exports map and no CommonJS fallback. +config.resolver.unstable_enablePackageExports = true; +config.resolver.unstable_conditionNames = ['react-native', 'import', 'require', 'default']; + +// Its gRPC entry is built from `.mjs` chunks that import each other by explicit path. Expo's +// default sourceExts does not include `mjs`, so Metro would resolve those files without applying +// the transform — the module loads, every export is undefined, and the first `new` on one fails +// with "undefined cannot be used as a constructor" from somewhere with no stack. +config.resolver.sourceExts = [...config.resolver.sourceExts, 'mjs']; + +module.exports = config; diff --git a/mobile/package-lock.json b/mobile/package-lock.json new file mode 100644 index 0000000..f0b58a9 --- /dev/null +++ b/mobile/package-lock.json @@ -0,0 +1,10613 @@ +{ + "name": "@skur/mobile", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@skur/mobile", + "version": "1.0.0", + "dependencies": { + "@expo-google-fonts/dm-mono": "^0.4.2", + "@expo-google-fonts/dm-sans": "^0.4.2", + "@expo-google-fonts/space-grotesk": "^0.4.1", + "@expo/vector-icons": "^15.0.2", + "@mysten/sui": "^2.34.0", + "@react-native-async-storage/async-storage": "2.2.0", + "@react-navigation/bottom-tabs": "^7.19.1", + "@react-navigation/native": "^7.4.1", + "@react-navigation/native-stack": "^7.19.1", + "@skur/sdk": "file:../sdk", + "@tanstack/query-async-storage-persister": "^5.104.1", + "@tanstack/react-query": "^5.102.8", + "@tanstack/react-query-persist-client": "^5.104.1", + "expo": "~57.0.23", + "expo-camera": "~57.0.5", + "expo-clipboard": "~57.0.2", + "expo-constants": "^57.0.20", + "expo-font": "~57.0.4", + "expo-haptics": "^57.0.3", + "expo-linking": "~57.0.10", + "expo-local-authentication": "~57.0.3", + "expo-network": "^57.0.2", + "expo-notifications": "~57.0.19", + "expo-screen-capture": "^57.0.3", + "expo-secure-store": "~57.0.4", + "expo-splash-screen": "^57.0.9", + "expo-status-bar": "~57.0.1", + "react": "19.2.3", + "react-native": "0.86.3", + "react-native-get-random-values": "~1.11.0", + "react-native-safe-area-context": "~5.7.0", + "react-native-screens": "~4.26.0", + "react-native-svg": "15.15.4" + }, + "devDependencies": { + "@testing-library/react-native": "^14.0.1", + "@types/jest": "^30.0.0", + "@types/react": "~19.2.2", + "jest": "^29.7.0", + "jest-expo": "^57.0.5", + "react-test-renderer": "^19.2.3", + "typescript": "~6.0.3" + } + }, + "../sdk": { + "name": "@skur/sdk", + "version": "0.1.0", + "dependencies": { + "@mysten/sui": "^2.34.0" + }, + "devDependencies": { + "@types/node": "^24.19.1", + "typescript": "~6.0.2", + "vitest": "^3.2.7" + } + }, + "node_modules/@0no-co/graphql.web": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.3.4.tgz", + "integrity": "sha512-imSwulOeDQodRy/olQmVEo2PiY6ntjkZ9eiGdw6lMYylh/tay9b7MusyJBmEnkL8GiKRKr6ltr+D42mY5bd8Bg==", + "license": "MIT", + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "graphql": { + "optional": true + } + } + }, + "node_modules/@0no-co/graphqlsp": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/@0no-co/graphqlsp/-/graphqlsp-1.17.5.tgz", + "integrity": "sha512-wsygfTO3pdkz4idDw8BxdMdgB9AB8nuJDJTUwYy87BiZEZRXS4bEfn4oNl9VtxTo6j2lzX/gED+IvOgxsSy2Jg==", + "license": "MIT", + "dependencies": { + "@gql.tada/internal": "^1.2.1", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "typescript": "^5.0.0 || ^6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.8", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-wrap-function": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-decorators": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-default-from": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-default-from": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-flow": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@expo-google-fonts/dm-mono": { + "version": "0.4.2", + "license": "MIT AND OFL-1.1" + }, + "node_modules/@expo-google-fonts/dm-sans": { + "version": "0.4.2", + "license": "MIT AND OFL-1.1" + }, + "node_modules/@expo-google-fonts/space-grotesk": { + "version": "0.4.1", + "license": "MIT AND OFL-1.1" + }, + "node_modules/@expo/code-signing-certificates": { + "version": "0.0.6", + "license": "MIT", + "dependencies": { + "node-forge": "^1.3.3" + } + }, + "node_modules/@expo/config": { + "version": "57.0.9", + "license": "MIT", + "dependencies": { + "@expo/config-plugins": "~57.0.9", + "@expo/config-types": "^57.0.2", + "@expo/json-file": "^11.0.1", + "@expo/require-utils": "^57.0.5", + "deepmerge": "^4.3.1", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "resolve-workspace-root": "^2.0.0", + "semver": "^7.6.0", + "slugify": "^1.3.4" + } + }, + "node_modules/@expo/config-plugins": { + "version": "57.0.9", + "license": "MIT", + "dependencies": { + "@expo/config-types": "^57.0.2", + "@expo/json-file": "~11.0.1", + "@expo/plist": "^0.8.1", + "@expo/require-utils": "^57.0.5", + "@expo/sdk-runtime-versions": "^1.0.0", + "chalk": "^4.1.2", + "debug": "^4.3.5", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "semver": "^7.5.4", + "slugify": "^1.6.6", + "xcode": "^3.0.1", + "xml2js": "0.6.0" + } + }, + "node_modules/@expo/config-types": { + "version": "57.0.2", + "license": "MIT" + }, + "node_modules/@expo/devcert": { + "version": "1.2.1", + "license": "MIT", + "dependencies": { + "@expo/sudo-prompt": "^9.3.1", + "debug": "^3.1.0" + } + }, + "node_modules/@expo/devcert/node_modules/debug": { + "version": "3.2.7", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@expo/devtools": { + "version": "57.0.1", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@expo/env": { + "version": "2.4.3", + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "debug": "^4.3.4", + "getenv": "^2.0.0" + }, + "engines": { + "node": ">=20.12.0" + } + }, + "node_modules/@expo/expo-modules-macros-plugin": { + "version": "0.6.1", + "license": "MIT" + }, + "node_modules/@expo/fingerprint": { + "version": "0.20.13", + "license": "MIT", + "dependencies": { + "@expo/env": "^2.4.3", + "@expo/spawn-async": "^1.8.0", + "arg": "^5.0.2", + "chalk": "^4.1.2", + "debug": "^4.3.4", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "ignore": "^5.3.1", + "minimatch": "^10.2.2", + "resolve-from": "^5.0.0", + "semver": "^7.6.0" + }, + "bin": { + "fingerprint": "bin/cli.js" + } + }, + "node_modules/@expo/image-utils": { + "version": "0.11.5", + "license": "MIT", + "dependencies": { + "@expo/require-utils": "^57.0.5", + "@expo/spawn-async": "^1.8.0", + "chalk": "^4.0.0", + "getenv": "^2.0.0", + "jimp-compact": "0.16.1", + "parse-png": "^2.1.0", + "semver": "^7.6.0" + } + }, + "node_modules/@expo/inline-modules": { + "version": "0.1.7", + "license": "MIT", + "dependencies": { + "@expo/config-plugins": "~57.0.9" + } + }, + "node_modules/@expo/json-file": { + "version": "11.0.1", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.20.0", + "json5": "^2.2.3" + } + }, + "node_modules/@expo/local-build-cache-provider": { + "version": "57.0.8", + "license": "MIT", + "dependencies": { + "@expo/config": "~57.0.9", + "chalk": "^4.1.2" + } + }, + "node_modules/@expo/metro": { + "version": "56.0.2", + "license": "MIT", + "dependencies": { + "metro": "0.84.5", + "metro-babel-transformer": "0.84.5", + "metro-cache": "0.84.5", + "metro-cache-key": "0.84.5", + "metro-config": "0.84.5", + "metro-core": "0.84.5", + "metro-file-map": "0.84.5", + "metro-minify-terser": "0.84.5", + "metro-resolver": "0.84.5", + "metro-runtime": "0.84.5", + "metro-source-map": "0.84.5", + "metro-symbolicate": "0.84.5", + "metro-transform-plugins": "0.84.5", + "metro-transform-worker": "0.84.5" + } + }, + "node_modules/@expo/metro-file-map": { + "version": "57.0.3", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "fb-watchman": "^2.0.2", + "invariant": "^2.2.4", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + } + }, + "node_modules/@expo/osascript": { + "version": "2.7.1", + "license": "MIT", + "dependencies": { + "@expo/spawn-async": "^1.8.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@expo/package-manager": { + "version": "1.13.1", + "license": "MIT", + "dependencies": { + "@expo/json-file": "^11.0.1", + "@expo/spawn-async": "^1.8.0", + "chalk": "^4.0.0", + "npm-package-arg": "^11.0.0", + "ora": "^3.4.0", + "resolve-workspace-root": "^2.0.0" + } + }, + "node_modules/@expo/plist": { + "version": "0.8.1", + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + } + }, + "node_modules/@expo/prebuild-config": { + "version": "57.0.16", + "license": "MIT", + "dependencies": { + "@expo/config": "~57.0.9", + "@expo/config-plugins": "~57.0.9", + "@expo/config-types": "^57.0.2", + "@expo/image-utils": "^0.11.5", + "@expo/json-file": "^11.0.1", + "@react-native/normalize-colors": "0.86.3", + "debug": "^4.3.1", + "expo-modules-autolinking": "~57.0.13", + "resolve-from": "^5.0.0", + "semver": "^7.6.0" + } + }, + "node_modules/@expo/require-utils": { + "version": "57.0.5", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.20.0", + "@babel/core": "^7.25.2", + "@babel/plugin-transform-modules-commonjs": "^7.24.8" + }, + "peerDependencies": { + "typescript": "^5.0.0 || ^5.0.0-0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@expo/schema-utils": { + "version": "57.0.2", + "license": "MIT" + }, + "node_modules/@expo/sdk-runtime-versions": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/@expo/spawn-async": { + "version": "1.8.0", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.6" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@expo/sudo-prompt": { + "version": "9.3.2", + "license": "MIT" + }, + "node_modules/@expo/vector-icons": { + "version": "15.1.1", + "license": "MIT", + "peerDependencies": { + "expo-font": ">=14.0.4", + "react": "*", + "react-native": "*" + } + }, + "node_modules/@expo/xcpretty": { + "version": "4.4.5", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/code-frame": "^7.20.0", + "chalk": "^4.1.0", + "js-yaml": "^4.1.0" + }, + "bin": { + "excpretty": "build/cli.js" + } + }, + "node_modules/@gql.tada/cli-utils": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@gql.tada/cli-utils/-/cli-utils-1.9.4.tgz", + "integrity": "sha512-xQ9M+2Xxjxhz9IJi+7IMU/aOsEtuSiSNb4OgDMBUC/8AcSpo2vDtx71Aodsaif4qSwuTb0YFVfDwC+ESqiZSHg==", + "license": "MIT", + "dependencies": { + "@0no-co/graphqlsp": "^1.17.3", + "@gql.tada/internal": "1.2.2", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" + }, + "peerDependencies": { + "@0no-co/graphqlsp": "^1.16.0", + "@gql.tada/svelte-support": "1.0.3", + "@gql.tada/vue-support": "1.0.3", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@gql.tada/svelte-support": { + "optional": true + }, + "@gql.tada/vue-support": { + "optional": true + } + } + }, + "node_modules/@gql.tada/internal": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@gql.tada/internal/-/internal-1.2.2.tgz", + "integrity": "sha512-4lZcElPP6MC8Ct8KN70LR2WQsHjbAsyAmTGG09VsOPhx738UFIYaL0S1XiI2pqq2tj/sDs/y3b9jvKoWGL7iuQ==", + "license": "MIT", + "dependencies": { + "@0no-co/graphql.web": "^1.3.1" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "license": "MIT", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@isaacs/ttlcache": { + "version": "1.4.1", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/console/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/create-cache-key-function": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", + "integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.5.0.tgz", + "integrity": "sha512-OsqBjHXCn8cadasoAZBP6nWYvMsRhpMzGXTpxJ5aO04NlbdhIz+FVe3q49l0AwVhsz/cEmIpBes6gAFl1/dWQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/environment/node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.5.2.tgz", + "integrity": "sha512-d/HOLSrJUlBIl6n3LzOa7cjW3xdePm5H5gQw2lixZ/0h157l9sw3mLblzFqnPcTbhDzhRQdBOX3A3KcG73nm5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.5.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect/node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect/node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers/node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.5.0.tgz", + "integrity": "sha512-9/2VUPitAjmBzbvDvqrxmvB7BzWsBW0WmkkojX1ODuxX1NLGxx9gfaZpHB0z8DtJ9uhGNmZG/VXBhf8uO0OV8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals/node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.5.0.tgz", + "integrity": "sha512-HdNQYSdRTEBNrginaqzQtTjG0HRMfrra/z6Ok7uL3S87vSlarIVohEsJsSj5edu3MiHoHjAkvPROz5ZjoKai+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.5.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern/node_modules/jest-regex-util": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.5.0.tgz", + "integrity": "sha512-Mg0WK7A6xRHLSA1udJ8y9f3lM0uUhFTBnLKzwPmqB9AylvpleJ6BLemR8K9dK27DY+cesDryoA7yLZCAHsPG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/react-is-18": { + "name": "react-is", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/react-is-19": { + "name": "react-is", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.3.0.tgz", + "integrity": "sha512-UpMYezM4v5/18F28aC66AEsjXIgE02kyEMH6yLdgLXu/UTfa1Ntwck/nNLrbqJsEXW7gPb0coNO9FQse9WTovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@jest/reporters/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mysten/bcs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@mysten/bcs/-/bcs-2.1.2.tgz", + "integrity": "sha512-fv6lyuZMTHN0xRlu4GYiV/UXUrdJDOHbXm1KfZYEU0sy/t+22yTrTVtWnAQv1Km7eyB3qHhwIrAG2aem4JMocw==", + "license": "Apache-2.0", + "dependencies": { + "@mysten/utils": "^0.4.1", + "@scure/base": "^2.3.0" + } + }, + "node_modules/@mysten/bcs/node_modules/@scure/base": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.4.0.tgz", + "integrity": "sha512-thZ1TuJwFwBblOhgsjDKvvGirBxNp+wSvY/DR6tJBJOTDhdAAcHJ8Vbr2eFnqaxeca4+t0i9KBf+uHYGWwZORg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@mysten/sui": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@mysten/sui/-/sui-2.34.0.tgz", + "integrity": "sha512-Qf/xt10YIJv3Yo0hQTU70xPgimFfw2mudBzI/dylGXZNuhuQVmkmxUm0fz49BFPOFvBxWc/MRFl204zwpwRnXw==", + "license": "Apache-2.0", + "dependencies": { + "@graphql-typed-document-node/core": "^3.2.0", + "@mysten/bcs": "^2.1.2", + "@mysten/utils": "^0.4.1", + "@noble/curves": "^2.3.0", + "@noble/hashes": "^2.3.0", + "@protobuf-ts/grpcweb-transport": "^2.11.1", + "@protobuf-ts/runtime": "^2.11.1", + "@protobuf-ts/runtime-rpc": "^2.11.1", + "@scure/base": "^2.3.0", + "@scure/bip32": "^2.3.0", + "@scure/bip39": "^2.3.0", + "gql.tada": "^1.11.3", + "graphql": "^17.0.2", + "poseidon-lite": "0.3.0", + "valibot": "^1.4.2" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@mysten/sui/node_modules/@noble/curves": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.4.0.tgz", + "integrity": "sha512-P4/62zrgfH33CneE3Dn4WhJVA22YUU0eR51wKIan4NVRvwsA0YnPTwWGpNbpuacSujmSFLvyzpyuR30+fbq2Ew==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.4.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@mysten/sui/node_modules/@noble/hashes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz", + "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@mysten/sui/node_modules/@scure/base": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.4.0.tgz", + "integrity": "sha512-thZ1TuJwFwBblOhgsjDKvvGirBxNp+wSvY/DR6tJBJOTDhdAAcHJ8Vbr2eFnqaxeca4+t0i9KBf+uHYGWwZORg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@mysten/sui/node_modules/@scure/bip32": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-2.4.0.tgz", + "integrity": "sha512-i3DS0CptAocyvqE4n3SUkpzeQK4vJMFwWLofTwRiiKo2aWojBOfyMCgfKw9HVpO6fSY5AK86sHS/Uzn8kK9Few==", + "license": "MIT", + "dependencies": { + "@noble/curves": "2.4.0", + "@noble/hashes": "2.4.0", + "@scure/base": "2.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@mysten/sui/node_modules/@scure/bip39": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.4.0.tgz", + "integrity": "sha512-82dxFbZUYboyOf0AXiydsQrFQ5Q4h9mX+O2UkE91ROYmsc0BKMGZLwDmy96Jpa2+vrtoxomjUhy1RPIgH/r2nA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@mysten/utils": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@mysten/utils/-/utils-0.4.1.tgz", + "integrity": "sha512-gffLJ84CbqG/CcOPuVlwXIRu37tFn0BtUx7CQtbLwJzWjUE7FU2Gq8BXWjzxImnvufcyLTaNWtCPG6wq22Zhog==", + "license": "Apache-2.0", + "dependencies": { + "@scure/base": "^2.3.0" + } + }, + "node_modules/@mysten/utils/node_modules/@scure/base": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.4.0.tgz", + "integrity": "sha512-thZ1TuJwFwBblOhgsjDKvvGirBxNp+wSvY/DR6tJBJOTDhdAAcHJ8Vbr2eFnqaxeca4+t0i9KBf+uHYGWwZORg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@protobuf-ts/grpcweb-transport": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/grpcweb-transport/-/grpcweb-transport-2.11.1.tgz", + "integrity": "sha512-1W4utDdvOB+RHMFQ0soL4JdnxjXV+ddeGIUg08DvZrA8Ms6k5NN6GBFU2oHZdTOcJVpPrDJ02RJlqtaoCMNBtw==", + "license": "Apache-2.0", + "dependencies": { + "@protobuf-ts/runtime": "^2.11.1", + "@protobuf-ts/runtime-rpc": "^2.11.1" + } + }, + "node_modules/@protobuf-ts/runtime": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime/-/runtime-2.11.1.tgz", + "integrity": "sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@protobuf-ts/runtime-rpc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime-rpc/-/runtime-rpc-2.11.1.tgz", + "integrity": "sha512-4CqqUmNA+/uMz00+d3CYKgElXO9VrEbucjnBFEjqI4GuDrEQ32MaI3q+9qPBvIGOlL4PmHXrzM32vBPWRhQKWQ==", + "license": "Apache-2.0", + "dependencies": { + "@protobuf-ts/runtime": "^2.11.1" + } + }, + "node_modules/@react-native-async-storage/async-storage": { + "version": "2.2.0", + "license": "MIT", + "dependencies": { + "merge-options": "^3.0.4" + }, + "peerDependencies": { + "react-native": "^0.0.0-0 || >=0.65 <1.0" + } + }, + "node_modules/@react-native/assets-registry": { + "version": "0.86.3", + "license": "MIT", + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@react-native/babel-plugin-codegen": { + "version": "0.86.3", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.0", + "@react-native/codegen": "0.86.3" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@react-native/codegen": { + "version": "0.86.3", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/parser": "^7.29.0", + "hermes-parser": "0.36.0", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "tinyglobby": "^0.2.15", + "yargs": "^17.6.2" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/codegen/node_modules/hermes-estree": { + "version": "0.36.0", + "license": "MIT" + }, + "node_modules/@react-native/codegen/node_modules/hermes-parser": { + "version": "0.36.0", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.36.0" + } + }, + "node_modules/@react-native/community-cli-plugin": { + "version": "0.86.3", + "license": "MIT", + "dependencies": { + "@react-native/dev-middleware": "0.86.3", + "debug": "^4.4.0", + "invariant": "^2.2.4", + "metro": "^0.84.3", + "metro-config": "^0.84.3", + "metro-core": "^0.84.3", + "semver": "^7.1.3" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + }, + "peerDependencies": { + "@react-native-community/cli": "*", + "@react-native/metro-config": "0.86.3" + }, + "peerDependenciesMeta": { + "@react-native-community/cli": { + "optional": true + }, + "@react-native/metro-config": { + "optional": true + } + } + }, + "node_modules/@react-native/debugger-frontend": { + "version": "0.86.3", + "license": "BSD-3-Clause", + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@react-native/debugger-shell": { + "version": "0.86.3", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.6", + "debug": "^4.4.0", + "fb-dotslash": "0.5.8" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@react-native/dev-middleware": { + "version": "0.86.3", + "license": "MIT", + "dependencies": { + "@isaacs/ttlcache": "^1.4.1", + "@react-native/debugger-frontend": "0.86.3", + "@react-native/debugger-shell": "0.86.3", + "chrome-launcher": "^0.15.2", + "chromium-edge-launcher": "^0.3.0", + "connect": "^3.6.5", + "debug": "^4.4.0", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "open": "^7.0.3", + "serve-static": "^1.16.2", + "ws": "^7.5.10" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@react-native/gradle-plugin": { + "version": "0.86.3", + "license": "MIT", + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@react-native/jest-preset": { + "version": "0.86.3", + "resolved": "https://registry.npmjs.org/@react-native/jest-preset/-/jest-preset-0.86.3.tgz", + "integrity": "sha512-oMmBwXUpTgJvGlcXQQPuiQLykoM8jsWYp6RwdJ2NWyIWzJcGp1rmSr3XV7istoIcUt0wdOVLtQs0A0nDm9ND+A==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/create-cache-key-function": "^29.7.0", + "@react-native/js-polyfills": "0.86.3", + "babel-jest": "^29.7.0", + "jest-environment-node": "^29.7.0", + "regenerator-runtime": "^0.13.2" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/@react-native/js-polyfills": { + "version": "0.86.3", + "license": "MIT", + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/@react-native/normalize-colors": { + "version": "0.86.3", + "license": "MIT" + }, + "node_modules/@react-native/virtualized-lists": { + "version": "0.86.3", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + }, + "peerDependencies": { + "@types/react": "^19.2.0", + "react": "*", + "react-native": "0.86.3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@react-navigation/bottom-tabs": { + "version": "7.19.1", + "license": "MIT", + "dependencies": { + "@react-navigation/elements": "^2.9.42", + "color": "^4.2.3", + "sf-symbols-typescript": "^2.1.0" + }, + "peerDependencies": { + "@react-navigation/native": "^7.4.1", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0", + "react-native-screens": ">= 4.0.0" + } + }, + "node_modules/@react-navigation/core": { + "version": "7.22.1", + "license": "MIT", + "dependencies": { + "@react-navigation/routers": "^7.6.4", + "escape-string-regexp": "^4.0.0", + "fast-deep-equal": "^3.1.3", + "nanoid": "^3.3.11", + "react-is": "^19.1.0", + "use-latest-callback": "^0.2.4", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "react": ">= 18.2.0" + } + }, + "node_modules/@react-navigation/core/node_modules/react-is": { + "version": "19.3.0", + "license": "MIT" + }, + "node_modules/@react-navigation/elements": { + "version": "2.9.42", + "license": "MIT", + "dependencies": { + "color": "^4.2.3", + "use-latest-callback": "^0.2.4", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@react-native-masked-view/masked-view": ">= 0.2.0", + "@react-navigation/native": "^7.4.1", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0" + }, + "peerDependenciesMeta": { + "@react-native-masked-view/masked-view": { + "optional": true + } + } + }, + "node_modules/@react-navigation/native": { + "version": "7.4.1", + "license": "MIT", + "dependencies": { + "@react-navigation/core": "^7.22.1", + "escape-string-regexp": "^4.0.0", + "fast-deep-equal": "^3.1.3", + "nanoid": "^3.3.11", + "standard-navigation": "^0.0.8", + "use-latest-callback": "^0.2.4" + }, + "peerDependencies": { + "react": ">= 18.2.0", + "react-native": "*" + } + }, + "node_modules/@react-navigation/native-stack": { + "version": "7.19.1", + "license": "MIT", + "dependencies": { + "@react-navigation/elements": "^2.9.42", + "color": "^4.2.3", + "sf-symbols-typescript": "^2.1.0", + "warn-once": "^0.1.1" + }, + "peerDependencies": { + "@react-navigation/native": "^7.4.1", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0", + "react-native-screens": ">= 4.0.0" + } + }, + "node_modules/@react-navigation/routers": { + "version": "7.6.4", + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.12", + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "devOptional": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "devOptional": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@skur/sdk": { + "resolved": "../sdk", + "link": true + }, + "node_modules/@tanstack/query-async-storage-persister": { + "version": "5.104.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-async-storage-persister/-/query-async-storage-persister-5.104.1.tgz", + "integrity": "sha512-wSs/HrangoSMVW3zLifawRT9lA+fbiilPQmaY+YhxBOM/VS9oqx+MQvROHpW+hKwDpdvFmhyBjWiyDhTHL1YXg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.104.1", + "@tanstack/query-persist-client-core": "5.104.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.104.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.104.1.tgz", + "integrity": "sha512-TiRghcrGkUTE+M4JBxvpPSpBhxvIzQairw+cksrHYGr+2uwC77yW1SY9nYxnYRYOppvucf8rEg6b5byoz4rTpw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-persist-client-core": { + "version": "5.104.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.104.1.tgz", + "integrity": "sha512-ONiadXqe56iNrw6RESqKILpTIVG9ZMI/DageTQnrBDt/nZ6WiP4G13iIiMwJEsiO12gQ3Gychx8tcFIPw4k25g==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.104.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.104.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.104.1.tgz", + "integrity": "sha512-Zz70EgjahNI7aCz8BZxM3vUGU44ycxEAzMuCUjLeA7SVhhOkg3ZgPjPJPvHjs6Rmky/dtmnu1WzjGHNHmhCoZQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.104.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-persist-client": { + "version": "5.104.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-5.104.1.tgz", + "integrity": "sha512-kU3YaqjkwEUzUUfPF0JVg56d6gJKWHaFVqOlQztwc4C2IT0P5sv/23orrSoQqjxKaUe6Ejw5IIQtA/G12uNmow==", + "license": "MIT", + "dependencies": { + "@tanstack/query-persist-client-core": "5.104.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.104.1", + "react": "^18 || ^19" + } + }, + "node_modules/@testing-library/react-native": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/react-native/-/react-native-14.0.1.tgz", + "integrity": "sha512-2r2e2y8SkUNRWKRXlUGqDYunB+AkbdXUPn49Bs5rpv9oxRb0K3USBVugbPbAKJjfj4w5Ba91NJ8LcQCZyopUZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-matcher-utils": "^30.4.1", + "picocolors": "^1.1.1", + "pretty-format": "^30.4.1", + "redent": "^3.0.0" + }, + "engines": { + "node": "^22.13.0 || >=24" + }, + "peerDependencies": { + "jest": ">=29.0.0", + "react": ">=19.0.0", + "react-native": ">=0.78", + "test-renderer": "^1.0.0" + }, + "peerDependenciesMeta": { + "jest": { + "optional": true + } + } + }, + "node_modules/@testing-library/react-native/node_modules/@jest/schemas": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.5.0.tgz", + "integrity": "sha512-/hunigyNpc4RCjC0VaW3f5RCUZVM2+WQ65qP7z083Gmvac7or2LI50XVNOtE4YPgBpV0yxYiAgorAPGniCoJmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@testing-library/react-native/node_modules/@sinclair/typebox": { + "version": "0.34.52", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz", + "integrity": "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react-native/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/react-native/node_modules/pretty-format": { + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz", + "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/emscripten": { + "version": "1.41.6", + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/jest/node_modules/@jest/schemas": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.5.0.tgz", + "integrity": "sha512-/hunigyNpc4RCjC0VaW3f5RCUZVM2+WQ65qP7z083Gmvac7or2LI50XVNOtE4YPgBpV0yxYiAgorAPGniCoJmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/@sinclair/typebox": { + "version": "0.34.52", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz", + "integrity": "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/node": { + "version": "22.20.3", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.18", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-reconciler": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.33.1.tgz", + "integrity": "sha512-0kF59ji2mKuHUd5wytHSpmORybMYVerJvq0K10pz9ZFoOlJ3e+w1LBjLshWpCrokeyQOioRodNbO4PH4ddvRfg==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.4.0", + "license": "ISC" + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.15", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/agent-cli-detector": { + "version": "0.1.7", + "license": "MIT", + "bin": { + "agent-cli-detector": "dist/cli.js" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/anser": { + "version": "1.4.10", + "license": "MIT" + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "license": "Python-2.0" + }, + "node_modules/asap": { + "version": "2.0.6", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "devOptional": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.17", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-react-compiler": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.0" + } + }, + "node_modules/babel-plugin-react-native-web": { + "version": "0.21.2", + "license": "MIT" + }, + "node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.36.0", + "license": "MIT", + "dependencies": { + "hermes-parser": "0.36.0" + } + }, + "node_modules/babel-plugin-syntax-hermes-parser/node_modules/hermes-estree": { + "version": "0.36.0", + "license": "MIT" + }, + "node_modules/babel-plugin-syntax-hermes-parser/node_modules/hermes-parser": { + "version": "0.36.0", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.36.0" + } + }, + "node_modules/babel-plugin-transform-flow-enums": { + "version": "0.0.2", + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-flow": "^7.12.1" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/badgin": { + "version": "1.2.3", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/barcode-detector": { + "version": "3.2.2", + "license": "MIT", + "dependencies": { + "zxing-wasm": "3.1.3" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.24", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/big-integer": { + "version": "1.6.52", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/bplist-creator": { + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "stream-buffers": "2.2.x" + } + }, + "node_modules/bplist-parser": { + "version": "0.3.1", + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.29.0", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.23", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.427", + "node-releases": "^2.0.55", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-launcher": { + "version": "0.15.2", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/chromium-edge-launcher": { + "version": "0.3.0", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0", + "mkdirp": "^1.0.4" + } + }, + "node_modules/ci-info": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-cursor": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "restore-cursor": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color": { + "version": "4.2.3", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.2", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "destroy": "1.2.0", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/connect": { + "version": "3.7.0", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/content-type": { + "version": "2.1.0", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.50.0", + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.7" + }, + "engines": { + "node": ">=6.4.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-tree/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "devOptional": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dnssd-advertise": { + "version": "1.1.6", + "license": "MIT" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.429", + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-ex/node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "devOptional": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/execa/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.5.2.tgz", + "integrity": "sha512-0LNuMvs8/5mRYhnCR4k+lGV7fqPMs6Bnksda4S3zsCUmdxBpAn4zU4NNzXdq1pMKe/H4W5t/zVIDSJ/H3e0vDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.5.2", + "@jest/get-type": "30.5.0", + "jest-matcher-utils": "30.5.2", + "jest-message-util": "30.5.1", + "jest-mock": "30.5.2", + "jest-util": "30.5.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/expect/node_modules/@jest/schemas": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.5.0.tgz", + "integrity": "sha512-/hunigyNpc4RCjC0VaW3f5RCUZVM2+WQ65qP7z083Gmvac7or2LI50XVNOtE4YPgBpV0yxYiAgorAPGniCoJmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/expect/node_modules/@jest/types": { + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.5.1.tgz", + "integrity": "sha512-LvVYn83nnXPl+Rg98nvcFgjx6nRMTArhSn6RAX/w3ELn54S8A42TYZvsCMdGUqTM8S0wyXbtlQdU6Hi6dykj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.5.0", + "@jest/schemas": "30.5.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/expect/node_modules/@sinclair/typebox": { + "version": "0.34.52", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz", + "integrity": "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==", + "dev": true, + "license": "MIT" + }, + "node_modules/expect/node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/expect/node_modules/jest-util": { + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.5.1.tgz", + "integrity": "sha512-yKuxmNy2rSbTXw+3SIPanJo+nV4/BS1p26v44IYBFMsswSQySfMMcPHErnOncda7i9HEz0q605rIhSTBVgrZTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.5.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/expect/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/expo": { + "version": "57.0.23", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.0", + "@expo/cli": "^57.0.25", + "@expo/config": "~57.0.9", + "@expo/config-plugins": "~57.0.9", + "@expo/devtools": "~57.0.1", + "@expo/dom-webview": "~57.0.1", + "@expo/fingerprint": "^0.20.13", + "@expo/local-build-cache-provider": "^57.0.8", + "@expo/log-box": "^57.0.4", + "@expo/metro": "~56.0.2", + "@expo/metro-config": "~57.0.12", + "@ungap/structured-clone": "^1.3.0", + "babel-preset-expo": "~57.0.12", + "expo-asset": "~57.0.17", + "expo-constants": "~57.0.18", + "expo-file-system": "~57.0.7", + "expo-font": "~57.0.4", + "expo-keep-awake": "~57.0.2", + "expo-modules-autolinking": "~57.0.13", + "expo-modules-core": "~57.0.18", + "pretty-format": "^29.7.0", + "react-refresh": "^0.14.2", + "whatwg-url-minimum": "^0.1.2" + }, + "bin": { + "expo": "bin/cli", + "expo-modules-autolinking": "bin/autolinking", + "fingerprint": "bin/fingerprint" + }, + "peerDependencies": { + "@expo/dom-webview": "*", + "@expo/metro-runtime": "*", + "react": "*", + "react-dom": "*", + "react-native": "*", + "react-native-web": "*", + "react-native-webview": "*" + }, + "peerDependenciesMeta": { + "@expo/dom-webview": { + "optional": true + }, + "@expo/metro-runtime": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native-web": { + "optional": true + }, + "react-native-webview": { + "optional": true + } + } + }, + "node_modules/expo-application": { + "version": "57.0.3", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-camera": { + "version": "57.0.5", + "license": "MIT", + "dependencies": { + "barcode-detector": "^3.0.0" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*", + "react-native-web": "*" + }, + "peerDependenciesMeta": { + "react-native-web": { + "optional": true + } + } + }, + "node_modules/expo-clipboard": { + "version": "57.0.2", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-constants": { + "version": "57.0.20", + "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-57.0.20.tgz", + "integrity": "sha512-WycPVvBgbluSa6yAh6DKnX646ZHrae0gV2ZLum5fioNkR+DiWyulq1+UfS++Jc6yfAh/l55NOirv6mt5cnQhSw==", + "license": "MIT", + "dependencies": { + "@expo/env": "~2.4.3" + }, + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, + "node_modules/expo-font": { + "version": "57.0.4", + "license": "MIT", + "dependencies": { + "fontfaceobserver": "^2.1.0" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-haptics": { + "version": "57.0.3", + "resolved": "https://registry.npmjs.org/expo-haptics/-/expo-haptics-57.0.3.tgz", + "integrity": "sha512-qucUXtwtJdtECKSQK5D6TQBc/0SxKj7s5wQ4mrfSxhZsHrS9a4rcxINl/gbruMaY9spTXStco32ST4pV0vXmxw==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-linking": { + "version": "57.0.10", + "license": "MIT", + "dependencies": { + "expo-constants": "~57.0.18", + "invariant": "^2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-local-authentication": { + "version": "57.0.3", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-modules-autolinking": { + "version": "57.0.13", + "license": "MIT", + "dependencies": { + "@expo/require-utils": "^57.0.5", + "@expo/spawn-async": "^1.8.0", + "chalk": "^4.1.0", + "commander": "^7.2.0" + }, + "bin": { + "expo-modules-autolinking": "bin/expo-modules-autolinking.js" + } + }, + "node_modules/expo-modules-core": { + "version": "57.0.18", + "license": "MIT", + "dependencies": { + "@expo/expo-modules-macros-plugin": "0.6.1", + "expo-modules-jsi": "~57.1.0", + "invariant": "^2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-worklets": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0" + }, + "peerDependenciesMeta": { + "react-native-worklets": { + "optional": true + } + } + }, + "node_modules/expo-modules-jsi": { + "version": "57.1.0", + "license": "MIT", + "peerDependencies": { + "react-native": "*" + } + }, + "node_modules/expo-network": { + "version": "57.0.2", + "resolved": "https://registry.npmjs.org/expo-network/-/expo-network-57.0.2.tgz", + "integrity": "sha512-BfK1kdftZcM6o9o1FAG7/y0l6vWX0n8td0M5lwkvaMeRXZ9LkRwOi9CQXwvsMTQGQiPwtQXgwiAze13cqjt75g==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*" + } + }, + "node_modules/expo-notifications": { + "version": "57.0.19", + "license": "MIT", + "dependencies": { + "@expo/image-utils": "^0.11.5", + "abort-controller": "^3.0.0", + "badgin": "^1.1.5", + "expo-application": "~57.0.3", + "expo-constants": "~57.0.18" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-screen-capture": { + "version": "57.0.3", + "resolved": "https://registry.npmjs.org/expo-screen-capture/-/expo-screen-capture-57.0.3.tgz", + "integrity": "sha512-EpTd6tBgX2nWKJUvYDIrnsC2xWGJnlOLqN48EzLwPTQlbbwDUeWwt+4RDZOyFkWCa67XiUbrzIEOJApNBVZSow==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*" + } + }, + "node_modules/expo-secure-store": { + "version": "57.0.4", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-server": { + "version": "57.0.3", + "license": "MIT", + "engines": { + "node": ">=20.16.0" + } + }, + "node_modules/expo-splash-screen": { + "version": "57.0.9", + "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-57.0.9.tgz", + "integrity": "sha512-7cKDn7uBwg0RkcmY015/5j9st4msWJ4zQiPmQdVZoppW3cFSbv05n7xhM6CjUU466Tx4VRTqDF21HuIHkmnEtA==", + "license": "MIT", + "dependencies": { + "@expo/config-plugins": "~57.0.9", + "@expo/image-utils": "^0.11.5", + "xml2js": "0.6.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-status-bar": { + "version": "57.0.1", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo/node_modules/@expo/cli": { + "version": "57.0.25", + "license": "MIT", + "dependencies": { + "@expo/code-signing-certificates": "^0.0.6", + "@expo/config": "~57.0.9", + "@expo/config-plugins": "~57.0.9", + "@expo/devcert": "^1.2.1", + "@expo/env": "~2.4.3", + "@expo/image-utils": "^0.11.5", + "@expo/inline-modules": "^0.1.7", + "@expo/json-file": "^11.0.1", + "@expo/log-box": "^57.0.4", + "@expo/metro": "~56.0.2", + "@expo/metro-config": "~57.0.12", + "@expo/metro-file-map": "^57.0.3", + "@expo/osascript": "^2.7.1", + "@expo/package-manager": "^1.13.1", + "@expo/plist": "^0.8.1", + "@expo/prebuild-config": "^57.0.16", + "@expo/require-utils": "^57.0.5", + "@expo/router-server": "^57.0.10", + "@expo/schema-utils": "^57.0.2", + "@expo/spawn-async": "^1.8.0", + "@expo/ws-tunnel": "^2.0.0", + "@expo/xcpretty": "^4.4.4", + "@react-native/dev-middleware": "0.86.3", + "accepts": "^1.3.8", + "agent-cli-detector": "0.1.7", + "arg": "^5.0.2", + "bplist-creator": "0.1.0", + "bplist-parser": "^0.3.1", + "chalk": "^4.0.0", + "ci-info": "^3.3.0", + "compression": "^1.7.4", + "connect": "^3.7.0", + "debug": "^4.3.4", + "dnssd-advertise": "^1.1.4", + "expo-server": "^57.0.3", + "fetch-nodeshim": "^0.4.10", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "lan-network": "^0.2.1", + "multitars": "^1.0.2", + "node-forge": "^1.3.3", + "npm-package-arg": "^11.0.0", + "ora": "^3.4.0", + "picomatch": "^4.0.4", + "pretty-format": "^29.7.0", + "progress": "^2.0.3", + "prompts": "^2.3.2", + "resolve-from": "^5.0.0", + "sandbox-cli-detector": "^0.2.0", + "semver": "^7.6.0", + "send": "^0.19.0", + "slugify": "^1.3.4", + "stacktrace-parser": "^0.1.10", + "structured-headers": "^0.4.1", + "terminal-link": "^2.1.1", + "toqr": "^0.1.1", + "wrap-ansi": "^7.0.0", + "ws": "^8.12.1", + "zod": "^3.25.76" + }, + "bin": { + "expo-internal": "main.js" + }, + "peerDependencies": { + "expo": "*", + "expo-router": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "expo-router": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/expo/node_modules/@expo/cli/node_modules/@expo/router-server": { + "version": "57.0.10", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "@expo/metro-runtime": "^57.0.15", + "expo": "*", + "expo-constants": "^57.0.18", + "expo-font": "^57.0.4", + "expo-router": "*", + "expo-server": "^57.0.3", + "react": "*", + "react-dom": "*", + "react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1" + }, + "peerDependenciesMeta": { + "@expo/metro-runtime": { + "optional": true + }, + "expo-router": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-server-dom-webpack": { + "optional": true + } + } + }, + "node_modules/expo/node_modules/@expo/dom-webview": { + "version": "57.0.1", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo/node_modules/@expo/log-box": { + "version": "57.0.4", + "license": "MIT", + "dependencies": { + "@expo/dom-webview": "^57.0.1", + "anser": "^1.4.9", + "stacktrace-parser": "^0.1.10" + }, + "peerDependencies": { + "@expo/dom-webview": "^57.0.1", + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo/node_modules/@expo/metro-config": { + "version": "57.0.12", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.20.0", + "@babel/core": "^7.20.0", + "@babel/generator": "^7.20.5", + "@expo/config": "~57.0.9", + "@expo/env": "~2.4.3", + "@expo/json-file": "~11.0.1", + "@expo/metro": "~56.0.2", + "@expo/require-utils": "^57.0.5", + "@expo/spawn-async": "^1.8.0", + "@jridgewell/gen-mapping": "^0.3.13", + "@jridgewell/remapping": "^2.3.5", + "@jridgewell/sourcemap-codec": "^1.5.5", + "browserslist": "^4.25.0", + "chalk": "^4.1.0", + "debug": "^4.3.2", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "hermes-parser": "^0.36.0", + "jsc-safe-url": "^0.2.4", + "lightningcss": "^1.30.1", + "picomatch": "^4.0.4", + "postcss": "^8.5.14", + "resolve-from": "^5.0.0" + }, + "peerDependencies": { + "expo": "*" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, + "node_modules/expo/node_modules/@expo/ws-tunnel": { + "version": "2.0.0", + "license": "MIT", + "peerDependencies": { + "ws": "^8.0.0" + } + }, + "node_modules/expo/node_modules/accepts": { + "version": "1.3.8", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expo/node_modules/babel-preset-expo": { + "version": "57.0.12", + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.20.5", + "@babel/helper-module-imports": "^7.25.9", + "@babel/plugin-proposal-decorators": "^7.12.9", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-class-static-block": "^7.27.1", + "@babel/plugin-transform-classes": "^7.25.4", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.28.6", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@babel/plugin-transform-react-pure-annotations": "^7.27.1", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.25.2", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/preset-typescript": "^7.23.0", + "@react-native/babel-plugin-codegen": "0.86.3", + "babel-plugin-react-compiler": "^1.0.0", + "babel-plugin-react-native-web": "~0.21.0", + "babel-plugin-syntax-hermes-parser": "^0.36.0", + "babel-plugin-transform-flow-enums": "^0.0.2", + "debug": "^4.3.4" + }, + "peerDependencies": { + "@babel/runtime": "^7.20.0", + "expo": "*", + "expo-widgets": "^57.0.19", + "react-refresh": ">=0.14.0 <1.0.0" + }, + "peerDependenciesMeta": { + "@babel/runtime": { + "optional": true + }, + "expo": { + "optional": true + }, + "expo-widgets": { + "optional": true + } + } + }, + "node_modules/expo/node_modules/ci-info": { + "version": "3.9.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/expo/node_modules/expo-asset": { + "version": "57.0.17", + "license": "MIT", + "dependencies": { + "@expo/image-utils": "^0.11.5", + "expo-constants": "~57.0.18" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo/node_modules/expo-file-system": { + "version": "57.0.7", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, + "node_modules/expo/node_modules/expo-keep-awake": { + "version": "57.0.2", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*" + } + }, + "node_modules/expo/node_modules/hermes-estree": { + "version": "0.36.1", + "license": "MIT" + }, + "node_modules/expo/node_modules/hermes-parser": { + "version": "0.36.1", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.36.1" + } + }, + "node_modules/expo/node_modules/mime-db": { + "version": "1.52.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expo/node_modules/mime-types": { + "version": "2.1.35", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expo/node_modules/negotiator": { + "version": "0.6.3", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expo/node_modules/picomatch": { + "version": "4.0.7", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/expo/node_modules/ws": { + "version": "8.21.3", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "license": "Apache-2.0" + }, + "node_modules/fast-base64-decode": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fb-dotslash": { + "version": "0.5.8", + "license": "(MIT OR Apache-2.0)", + "bin": { + "dotslash": "bin/dotslash" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fetch-nodeshim": { + "version": "0.4.10", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flow-enums-runtime": { + "version": "0.0.6", + "license": "MIT" + }, + "node_modules/fontfaceobserver": { + "version": "2.3.0", + "license": "BSD-2-Clause" + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/getenv": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gql.tada": { + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/gql.tada/-/gql.tada-1.11.4.tgz", + "integrity": "sha512-UQnhJUhSM0MV8cpcgjci/7ET8LnmHx9o9FtZqU4sH+ILiBVG5rnfXJzNdzTN+Y7MuCz6Pp3XE/VPEVqmO969jw==", + "license": "MIT", + "dependencies": { + "@0no-co/graphql.web": "^1.3.2", + "@0no-co/graphqlsp": "^1.17.3", + "@gql.tada/cli-utils": "1.9.4", + "@gql.tada/internal": "1.2.2" + }, + "bin": { + "gql-tada": "bin/cli.js", + "gql.tada": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "license": "ISC" + }, + "node_modules/graphql": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-17.0.2.tgz", + "integrity": "sha512-FRWbddMxfkjiB7z+aQDWIR+E34xo9I8c9mtK2RPv8PmMzKRvrdsreHL/Ui/TmwHJfhHChEtsFPyMHKI+xuarQQ==", + "license": "MIT", + "engines": { + "node": "^22.0.0 || ^24.0.0 || ^25.0.0 || >=26.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-compiler": { + "version": "250829098.0.17", + "license": "MIT" + }, + "node_modules/hermes-estree": { + "version": "0.35.0", + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.35.0", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.35.0" + } + }, + "node_modules/hosted-git-info": { + "version": "7.0.2", + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.4.3", + "license": "ISC" + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "devOptional": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC" + }, + "node_modules/invariant": { + "version": "2.2.4", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "devOptional": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jest-config/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-diff": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.5.2.tgz", + "integrity": "sha512-rBtHnI6BWTiWj3lM7fOLVfChx7R9B0u9VV3PRHawKs79x6ZjW1QoSrKenaigNGWlvhtoi2BqvDbABDie+Os3xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.5.0", + "@jest/get-type": "30.5.0", + "chalk": "^4.1.2", + "pretty-format": "30.5.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-diff/node_modules/@jest/schemas": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.5.0.tgz", + "integrity": "sha512-/hunigyNpc4RCjC0VaW3f5RCUZVM2+WQ65qP7z083Gmvac7or2LI50XVNOtE4YPgBpV0yxYiAgorAPGniCoJmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-diff/node_modules/@sinclair/typebox": { + "version": "0.34.52", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz", + "integrity": "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node/node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-expo": { + "version": "57.0.5", + "resolved": "https://registry.npmjs.org/jest-expo/-/jest-expo-57.0.5.tgz", + "integrity": "sha512-U08fwjaDBwnnbKnZcOA0WQbHfKqOWTCGku+IdCaekrLZmfbbZr3P1WqEsXHo6ntidQ3DmW+TT6G1aOp+ChYfKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/create-cache-key-function": "^29.2.1", + "@jest/globals": "^29.2.1", + "babel-jest": "^29.2.1", + "jest-environment-jsdom": "^29.2.1", + "jest-snapshot": "^29.2.1", + "jest-watch-select-projects": "^2.0.0", + "jest-watch-typeahead": "2.2.1", + "json5": "^2.2.3", + "lodash": "^4.17.19", + "react-test-renderer": "19.2.3", + "server-only": "^0.0.1", + "stacktrace-js": "^2.0.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "peerDependencies": { + "@react-native/jest-preset": "^0.86.3", + "expo": "*", + "react-native": "*", + "react-server-dom-webpack": "~19.0.4 || ~19.1.5 || ~19.2.4" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "react-server-dom-webpack": { + "optional": true + } + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.5.2.tgz", + "integrity": "sha512-lFkB365PTIHOhPzwrb9T6gvlDnPpOZ7/c3ewOKKB7bzztqjVrtlyRL2MkyfJXpyY2u2SKw001JKrTx14fBUFcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.5.0", + "chalk": "^4.1.2", + "jest-diff": "30.5.2", + "pretty-format": "30.5.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/@jest/schemas": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.5.0.tgz", + "integrity": "sha512-/hunigyNpc4RCjC0VaW3f5RCUZVM2+WQ65qP7z083Gmvac7or2LI50XVNOtE4YPgBpV0yxYiAgorAPGniCoJmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/@sinclair/typebox": { + "version": "0.34.52", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz", + "integrity": "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.5.1.tgz", + "integrity": "sha512-UdQlLdd9wL/Ys7xRErckqwD6wPlSZYueosSWuHc1r2ztGLwlgPvtSJq2+BPEgaEY13WLvfFbmhTj8pba0Sd1jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.5.1", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-util": "30.5.1", + "picomatch": "^4.0.3", + "pretty-format": "30.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/@jest/schemas": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.5.0.tgz", + "integrity": "sha512-/hunigyNpc4RCjC0VaW3f5RCUZVM2+WQ65qP7z083Gmvac7or2LI50XVNOtE4YPgBpV0yxYiAgorAPGniCoJmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/@jest/types": { + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.5.1.tgz", + "integrity": "sha512-LvVYn83nnXPl+Rg98nvcFgjx6nRMTArhSn6RAX/w3ELn54S8A42TYZvsCMdGUqTM8S0wyXbtlQdU6Hi6dykj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.5.0", + "@jest/schemas": "30.5.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/@sinclair/typebox": { + "version": "0.34.52", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz", + "integrity": "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/jest-util": { + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.5.1.tgz", + "integrity": "sha512-yKuxmNy2rSbTXw+3SIPanJo+nV4/BS1p26v44IYBFMsswSQySfMMcPHErnOncda7i9HEz0q605rIhSTBVgrZTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.5.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.5.2", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.5.2.tgz", + "integrity": "sha512-KTHb37Fhj8xY8qPFvTFaFJHcdsumQp+ExsUlgoX232Agmc706gagBs53+CYvulV7MVQo9AgXOUxsqzzVBoJlrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.5.2", + "@jest/types": "30.5.1", + "@types/node": "*", + "jest-util": "30.5.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock/node_modules/@jest/schemas": { + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.5.0.tgz", + "integrity": "sha512-/hunigyNpc4RCjC0VaW3f5RCUZVM2+WQ65qP7z083Gmvac7or2LI50XVNOtE4YPgBpV0yxYiAgorAPGniCoJmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock/node_modules/@jest/types": { + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.5.1.tgz", + "integrity": "sha512-LvVYn83nnXPl+Rg98nvcFgjx6nRMTArhSn6RAX/w3ELn54S8A42TYZvsCMdGUqTM8S0wyXbtlQdU6Hi6dykj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.5.0", + "@jest/schemas": "30.5.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock/node_modules/@sinclair/typebox": { + "version": "0.34.52", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz", + "integrity": "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-mock/node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-mock/node_modules/jest-util": { + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.5.1.tgz", + "integrity": "sha512-yKuxmNy2rSbTXw+3SIPanJo+nV4/BS1p26v44IYBFMsswSQySfMMcPHErnOncda7i9HEz0q605rIhSTBVgrZTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.5.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/ci-info": { + "version": "3.9.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watch-select-projects": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jest-watch-select-projects/-/jest-watch-select-projects-2.0.0.tgz", + "integrity": "sha512-j00nW4dXc2NiCW6znXgFLF9g8PJ0zP25cpQ1xRro/HU2GBfZQFZD0SoXnAlaoKkIY4MlfTMkKGbNXFpvCdjl1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.3.0", + "chalk": "^3.0.0", + "prompts": "^2.2.1" + } + }, + "node_modules/jest-watch-select-projects/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watch-typeahead": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-2.2.1.tgz", + "integrity": "sha512-jYpYmUnTzysmVnwq49TAxlmtOAwp8QIqvZyoofQFn8fiWhEDZj33ZXzg3JA4nGnzWFm1hbWf3ADpteUokvXgFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^6.0.0", + "chalk": "^4.0.0", + "jest-regex-util": "^29.0.0", + "jest-watcher": "^29.0.0", + "slash": "^5.0.0", + "string-length": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "jest": "^27.0.0 || ^28.0.0 || ^29.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/ansi-escapes": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", + "integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watch-typeahead/node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/jest-watch-typeahead/node_modules/char-regex": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-2.0.2.tgz", + "integrity": "sha512-cbGOjAptfM2LVmWhwRFHEKTPkLwNddVmuqYZQt895yXwAsWsXObCG+YN4DGQ/JBtT4GP1a1lPPdio2z413LmTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/jest-watch-typeahead/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watch-typeahead/node_modules/string-length": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz", + "integrity": "sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^2.0.0", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watch-typeahead/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jimp-compact": { + "version": "0.16.1", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsc-safe-url": { + "version": "0.2.4", + "license": "0BSD" + }, + "node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/jsdom/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jsdom/node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lan-network": { + "version": "0.2.1", + "license": "MIT", + "bin": { + "lan-network": "dist/lan-network-cli.js" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lighthouse-logger": { + "version": "1.4.2", + "license": "Apache-2.0", + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-logger/node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/lighthouse-logger/node_modules/ms": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "license": "MIT" + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "2.2.0", + "license": "MIT", + "dependencies": { + "chalk": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "3.2.1", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "2.4.2", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "1.9.3", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.3", + "license": "MIT" + }, + "node_modules/log-symbols/node_modules/escape-string-regexp": { + "version": "1.0.5", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "5.5.0", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/marky": { + "version": "1.3.0", + "license": "Apache-2.0" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "license": "CC0-1.0" + }, + "node_modules/memoize-one": { + "version": "5.2.1", + "license": "MIT" + }, + "node_modules/merge-options": { + "version": "3.0.4", + "license": "MIT", + "dependencies": { + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/metro": { + "version": "0.84.5", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/core": "^7.25.2", + "@babel/generator": "^7.29.1", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "accepts": "^2.0.0", + "ci-info": "^2.0.0", + "connect": "^3.6.5", + "debug": "^4.4.0", + "error-stack-parser": "^2.0.6", + "flow-enums-runtime": "^0.0.6", + "graceful-fs": "^4.2.4", + "hermes-parser": "0.35.0", + "invariant": "^2.2.4", + "jest-worker": "^29.7.0", + "jsc-safe-url": "^0.2.2", + "lodash.throttle": "^4.1.1", + "metro-babel-transformer": "0.84.5", + "metro-cache": "0.84.5", + "metro-cache-key": "0.84.5", + "metro-config": "0.84.5", + "metro-core": "0.84.5", + "metro-file-map": "0.84.5", + "metro-resolver": "0.84.5", + "metro-runtime": "0.84.5", + "metro-source-map": "0.84.5", + "metro-symbolicate": "0.84.5", + "metro-transform-plugins": "0.84.5", + "metro-transform-worker": "0.84.5", + "mime-types": "^3.0.1", + "nullthrows": "^1.1.1", + "serialize-error": "^2.1.0", + "source-map": "^0.5.6", + "throat": "^5.0.0", + "ws": "^7.5.10", + "yargs": "^17.6.2" + }, + "bin": { + "metro": "src/cli.js" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/metro-babel-transformer": { + "version": "0.84.5", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "hermes-parser": "0.35.0", + "metro-cache-key": "0.84.5", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/metro-cache": { + "version": "0.84.5", + "license": "MIT", + "dependencies": { + "exponential-backoff": "^3.1.1", + "flow-enums-runtime": "^0.0.6", + "https-proxy-agent": "^7.0.5", + "metro-core": "0.84.5" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/metro-cache-key": { + "version": "0.84.5", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/metro-config": { + "version": "0.84.5", + "license": "MIT", + "dependencies": { + "connect": "^3.6.5", + "flow-enums-runtime": "^0.0.6", + "jest-validate": "^29.7.0", + "metro": "0.84.5", + "metro-cache": "0.84.5", + "metro-core": "0.84.5", + "metro-runtime": "0.84.5", + "yaml": "^2.6.1" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/metro-core": { + "version": "0.84.5", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "lodash.throttle": "^4.1.1", + "metro-resolver": "0.84.5" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/metro-file-map": { + "version": "0.84.5", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fb-watchman": "^2.0.0", + "flow-enums-runtime": "^0.0.6", + "graceful-fs": "^4.2.4", + "invariant": "^2.2.4", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "nullthrows": "^1.1.1", + "walker": "^1.0.7" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/metro-minify-terser": { + "version": "0.84.5", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "terser": "^5.15.0" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/metro-resolver": { + "version": "0.84.5", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/metro-runtime": { + "version": "0.84.5", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/metro-source-map": { + "version": "0.84.5", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-symbolicate": "0.84.5", + "nullthrows": "^1.1.1", + "ob1": "0.84.5", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/metro-symbolicate": { + "version": "0.84.5", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-source-map": "0.84.5", + "nullthrows": "^1.1.1", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "bin": { + "metro-symbolicate": "src/index.js" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/metro-transform-plugins": { + "version": "0.84.5", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/generator": "^7.29.1", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "flow-enums-runtime": "^0.0.6", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/metro-transform-worker": { + "version": "0.84.5", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/generator": "^7.29.1", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "flow-enums-runtime": "^0.0.6", + "metro": "0.84.5", + "metro-babel-transformer": "0.84.5", + "metro-cache": "0.84.5", + "metro-cache-key": "0.84.5", + "metro-minify-terser": "0.84.5", + "metro-source-map": "0.84.5", + "metro-transform-plugins": "0.84.5", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-fn": { + "version": "1.2.0", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/multitars": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.1.0", + "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/node-forge": { + "version": "1.4.0", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.55", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-package-arg": { + "version": "11.0.3", + "license": "ISC", + "dependencies": { + "hosted-git-info": "^7.0.0", + "proc-log": "^4.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nullthrows": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/nwsapi": { + "version": "2.2.28", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.28.tgz", + "integrity": "sha512-IlVB7OS7qrOsVYlpnFIkETjMwT9jwvmocJmmM+GZU/PAB3uGi9Ezd7vcWhWBUnSc0ya4ppmQITOyP1ez9gg8cg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ob1": { + "version": "0.84.5", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "mimic-fn": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/open": { + "version": "7.4.2", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "3.4.0", + "license": "MIT", + "dependencies": { + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-spinners": "^2.0.0", + "log-symbols": "^2.2.0", + "strip-ansi": "^5.2.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "4.1.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ora/node_modules/ansi-styles": { + "version": "3.2.1", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "2.4.2", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ora/node_modules/color-convert": { + "version": "1.9.3", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/ora/node_modules/color-name": { + "version": "1.1.3", + "license": "MIT" + }, + "node_modules/ora/node_modules/escape-string-regexp": { + "version": "1.0.5", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/ora/node_modules/has-flag": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "5.2.0", + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ora/node_modules/supports-color": { + "version": "5.5.0", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-png": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "pngjs": "^3.3.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.2", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/plist": { + "version": "3.1.1", + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.9.10", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/plist/node_modules/@xmldom/xmldom": { + "version": "0.9.12", + "license": "MIT", + "engines": { + "node": ">=14.6" + } + }, + "node_modules/pngjs": { + "version": "3.4.0", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/poseidon-lite": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/poseidon-lite/-/poseidon-lite-0.3.0.tgz", + "integrity": "sha512-ilJj4MIve4uBEG7SrtPqUUNkvpJ/pLVbndxa0WvebcQqeIhe+h72JR4g0EvwchUzm9sOQDlOjiDNmRAgxNZl4A==", + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.28", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/proc-log": { + "version": "4.2.0", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise": { + "version": "8.3.0", + "license": "MIT", + "dependencies": { + "asap": "~2.0.6" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/react": { + "version": "19.2.3", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-devtools-core": { + "version": "6.1.5", + "license": "MIT", + "dependencies": { + "shell-quote": "^1.6.1", + "ws": "^7" + } + }, + "node_modules/react-freeze": { + "version": "1.0.4", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=17.0.0" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "license": "MIT" + }, + "node_modules/react-native": { + "version": "0.86.3", + "license": "MIT", + "dependencies": { + "@react-native/assets-registry": "0.86.3", + "@react-native/codegen": "0.86.3", + "@react-native/community-cli-plugin": "0.86.3", + "@react-native/gradle-plugin": "0.86.3", + "@react-native/js-polyfills": "0.86.3", + "@react-native/normalize-colors": "0.86.3", + "@react-native/virtualized-lists": "0.86.3", + "abort-controller": "^3.0.0", + "anser": "^1.4.9", + "ansi-regex": "^5.0.0", + "babel-plugin-syntax-hermes-parser": "0.36.0", + "base64-js": "^1.5.1", + "commander": "^12.0.0", + "flow-enums-runtime": "^0.0.6", + "hermes-compiler": "250829098.0.17", + "invariant": "^2.2.4", + "memoize-one": "^5.0.0", + "metro-runtime": "^0.84.3", + "metro-source-map": "^0.84.3", + "nullthrows": "^1.1.1", + "pretty-format": "^29.7.0", + "promise": "^8.3.0", + "react-devtools-core": "^6.1.5", + "react-refresh": "^0.14.0", + "regenerator-runtime": "^0.13.2", + "scheduler": "0.27.0", + "semver": "^7.1.3", + "stacktrace-parser": "^0.1.10", + "tinyglobby": "^0.2.15", + "whatwg-fetch": "^3.0.0", + "ws": "^7.5.10", + "yargs": "^17.6.2" + }, + "bin": { + "react-native": "cli.js" + }, + "engines": { + "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" + }, + "peerDependencies": { + "@react-native/jest-preset": "0.86.3", + "@types/react": "^19.1.1", + "react": "^19.2.3" + }, + "peerDependenciesMeta": { + "@react-native/jest-preset": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-native-get-random-values": { + "version": "1.11.0", + "license": "MIT", + "dependencies": { + "fast-base64-decode": "^1.0.0" + }, + "peerDependencies": { + "react-native": ">=0.56" + } + }, + "node_modules/react-native-safe-area-context": { + "version": "5.7.0", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-screens": { + "version": "4.26.2", + "license": "MIT", + "dependencies": { + "react-freeze": "^1.0.0", + "warn-once": "^0.1.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-svg": { + "version": "15.15.4", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.4.tgz", + "integrity": "sha512-boT/vIRgj6zZKBpfTPJJiYWMbZE9duBMOwPK6kCSTgxsS947IFMOq9OgIFkpWZTB7t229H24pDRkh3W9ZK/J1A==", + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "css-tree": "^1.1.3", + "warn-once": "0.1.1" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native/node_modules/commander": { + "version": "12.1.0", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-test-renderer": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-19.2.3.tgz", + "integrity": "sha512-TMR1LnSFiWZMJkCgNf5ATSvAheTT2NvKIwiVwdBPHxjBI7n/JbWd4gaZ16DVd9foAXdvDz+sB5yxZTwMjPRxpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "react-is": "^19.2.3", + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-test-renderer/node_modules/react-is": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.3.0.tgz", + "integrity": "sha512-UpMYezM4v5/18F28aC66AEsjXIgE02kyEMH6yLdgLXu/UTfa1Ntwck/nNLrbqJsEXW7gPb0coNO9FQse9WTovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "license": "MIT" + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.2", + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.12", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-workspace-root": { + "version": "2.0.1", + "license": "MIT" + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sandbox-cli-detector": { + "version": "0.2.0", + "license": "MIT", + "bin": { + "sandbox-cli-detector": "dist/cli.js" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/sax": { + "version": "1.6.1", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/on-finished": { + "version": "2.4.1", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-error": { + "version": "2.1.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/server-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", + "dev": true, + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "license": "ISC" + }, + "node_modules/sf-symbols-typescript": { + "version": "2.2.0", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.10.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "license": "ISC" + }, + "node_modules/simple-plist": { + "version": "1.3.1", + "license": "MIT", + "dependencies": { + "bplist-creator": "0.1.0", + "bplist-parser": "0.3.1", + "plist": "^3.0.5" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slugify": { + "version": "1.6.9", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "devOptional": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-generator": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", + "integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "license": "MIT" + }, + "node_modules/stacktrace-gps": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz", + "integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map": "0.5.6", + "stackframe": "^1.3.4" + } + }, + "node_modules/stacktrace-gps/node_modules/source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stacktrace-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", + "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "error-stack-parser": "^2.0.6", + "stack-generator": "^2.0.5", + "stacktrace-gps": "^3.0.4" + } + }, + "node_modules/stacktrace-parser": { + "version": "0.1.11", + "license": "MIT", + "dependencies": { + "type-fest": "^0.7.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/standard-navigation": { + "version": "0.0.8", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stream-buffers": { + "version": "2.2.0", + "license": "Unlicense", + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/structured-headers": { + "version": "0.4.1", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terminal-link": { + "version": "2.1.1", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.51.2", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "devOptional": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/test-renderer": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/test-renderer/-/test-renderer-1.3.0.tgz", + "integrity": "sha512-DXyHv7JrVmDc5dKpfVHFhJ0Evo/EFY3TqlNt8CR65ua3SCsI09K/mQDJmYUOnZz36GANYqjp0ApeRER/mOxktQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/react-reconciler": "~0.33.0", + "react-reconciler": "~0.34.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/test-renderer/node_modules/react-reconciler": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.34.0.tgz", + "integrity": "sha512-eVibvB1fc9j5Qf1KRkiM3GzRPBdjg2P6G6QV2f3P6Eb3OGMxRBhdg2rrUwjbV5JrQaJj8nOa8uajmmJA5tJWjQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.28.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/test-renderer/node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/throat": { + "version": "5.0.0", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.7", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toqr": { + "version": "0.1.1", + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.7.1", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.3", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/use-latest-callback": { + "version": "0.2.6", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.7.0", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "7.0.3", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/valibot": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.5.0.tgz", + "integrity": "sha512-nil6AkP2TChWL43Z5uJ6GTxX01CUA+g8LWUM+N/rB9NBbkUMaUsi9PUNzlUPgoKASgmx9f7eGOYpJ04/fSa6FQ==", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vlq": { + "version": "1.0.1", + "license": "MIT" + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/warn-once": { + "version": "0.1.1", + "license": "MIT" + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "license": "MIT" + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url-minimum": { + "version": "0.1.2", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "7.5.13", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xcode": { + "version": "3.0.1", + "license": "Apache-2.0", + "dependencies": { + "simple-plist": "^1.1.0", + "uuid": "^7.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/xml2js": { + "version": "0.6.0", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.9.1", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zxing-wasm": { + "version": "3.1.3", + "license": "MIT", + "dependencies": { + "@types/emscripten": "^1.41.5", + "type-fest": "^5.8.0" + }, + "peerDependencies": { + "@types/emscripten": ">=1.39.6" + } + }, + "node_modules/zxing-wasm/node_modules/type-fest": { + "version": "5.9.0", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/mobile/package.json b/mobile/package.json new file mode 100644 index 0000000..ec91070 --- /dev/null +++ b/mobile/package.json @@ -0,0 +1,58 @@ +{ + "name": "@skur/mobile", + "version": "1.0.0", + "main": "index.ts", + "private": true, + "scripts": { + "sdk": "npm --prefix ../sdk run --silent build", + "start": "npm run sdk && expo start", + "ios": "npm run sdk && expo run:ios", + "android": "npm run sdk && expo run:android", + "typecheck": "npm run sdk && tsc --noEmit", + "test": "npm run sdk && jest" + }, + "dependencies": { + "@expo-google-fonts/dm-mono": "^0.4.2", + "@expo-google-fonts/dm-sans": "^0.4.2", + "@expo-google-fonts/space-grotesk": "^0.4.1", + "@expo/vector-icons": "^15.0.2", + "@mysten/sui": "^2.34.0", + "@react-native-async-storage/async-storage": "2.2.0", + "@react-navigation/bottom-tabs": "^7.19.1", + "@react-navigation/native": "^7.4.1", + "@react-navigation/native-stack": "^7.19.1", + "@skur/sdk": "file:../sdk", + "@tanstack/query-async-storage-persister": "^5.104.1", + "@tanstack/react-query": "^5.102.8", + "@tanstack/react-query-persist-client": "^5.104.1", + "expo": "~57.0.23", + "expo-camera": "~57.0.5", + "expo-clipboard": "~57.0.2", + "expo-constants": "^57.0.20", + "expo-font": "~57.0.4", + "expo-haptics": "^57.0.3", + "expo-linking": "~57.0.10", + "expo-local-authentication": "~57.0.3", + "expo-network": "^57.0.2", + "expo-notifications": "~57.0.19", + "expo-screen-capture": "^57.0.3", + "expo-secure-store": "~57.0.4", + "expo-splash-screen": "^57.0.9", + "expo-status-bar": "~57.0.1", + "react": "19.2.3", + "react-native": "0.86.3", + "react-native-get-random-values": "~1.11.0", + "react-native-safe-area-context": "~5.7.0", + "react-native-screens": "~4.26.0", + "react-native-svg": "15.15.4" + }, + "devDependencies": { + "@testing-library/react-native": "^14.0.1", + "@types/jest": "^30.0.0", + "@types/react": "~19.2.2", + "jest": "^29.7.0", + "jest-expo": "^57.0.5", + "react-test-renderer": "^19.2.3", + "typescript": "~6.0.3" + } +} diff --git a/mobile/src/components/ErrorBoundary.tsx b/mobile/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..feff185 --- /dev/null +++ b/mobile/src/components/ErrorBoundary.tsx @@ -0,0 +1,58 @@ +import { Component, type ReactNode } from 'react'; +import { Pressable, Text, View } from 'react-native'; +import { DARK, F, R } from '../theme'; + +/** + * The last line. Without this, an uncaught render error is a white screen — no message, no way + * back, and on a treasury app a person staring at it not knowing whether their payment went + * through. + * + * It is a class component because error boundaries still have to be, it uses hard-coded dark + * palette values because the theme provider may be the thing that crashed, and it says what a + * person needs to hear first: nothing was signed by this crash. Signing only happens behind an + * explicit biometric prompt, so a render error cannot have moved money. + */ +type Props = { children: ReactNode }; +type State = { error: Error | null; key: number }; + +export class ErrorBoundary extends Component { + state: State = { error: null, key: 0 }; + + static getDerivedStateFromError(error: Error): Partial { + return { error }; + } + + componentDidCatch(error: Error, info: { componentStack?: string | null }) { + // The one place a stack is deliberately logged. Nothing sensitive renders into component + // stacks — keys never enter the React tree — and without this line a field crash is unfixable. + console.error('[skur] render crash:', error.message, info.componentStack ?? ''); + } + + render() { + if (!this.state.error) { + // The key remounts the whole subtree on restart, clearing whatever state crashed it. + return {this.props.children}; + } + return ( + + + Something broke in the app + + + Nothing was signed — signing only ever happens behind a biometric prompt you can see. The + vault and your key are unaffected. + + + {this.state.error.message} + + this.setState((s) => ({ error: null, key: s.key + 1 }))} + style={{ marginTop: 8, backgroundColor: DARK.accent, borderRadius: R.md, paddingVertical: 13, paddingHorizontal: 28 }} + > + Restart + + + ); + } +} diff --git a/mobile/src/components/Identicon.tsx b/mobile/src/components/Identicon.tsx new file mode 100644 index 0000000..e16598f --- /dev/null +++ b/mobile/src/components/Identicon.tsx @@ -0,0 +1,38 @@ +import { useMemo } from "react"; +import { useTheme } from "../state/theme"; +import { Text, View } from "react-native"; +import { F } from "../theme"; + +/** A deterministic blocky avatar, drawn with views. + * + * A Sui address is already the output of a hash, so its hex is used directly as the entropy — + * hashing it again would add a dependency and no information. */ +export function Identicon({ address, size = 40, badge }: { address: string; size?: number; badge?: string }) { + const C = useTheme(); + const { cells, fg, bg } = useMemo(() => { + const hash = address.toLowerCase().replace(/^0x/, '').padEnd(64, '0'); + const hue = parseInt(hash.slice(0, 4), 16) % 360; + const hue2 = (hue + 140) % 360; + const bits = hash.slice(4); + const cells: boolean[] = []; + for (let y = 0; y < 5; y++) { + const row: boolean[] = []; + for (let x = 0; x < 3; x++) row.push(parseInt(bits[(y * 3 + x) % bits.length], 16) % 3 !== 0); + cells.push(row[0], row[1], row[2], row[1], row[0]); + } + return { cells, fg: `hsl(${hue}, 70%, 45%)`, bg: `hsl(${hue2}, 60%, 88%)` }; + }, [address]); + const unit = size / 5; + return ( + + + {cells.map((on, i) => )} + + {badge ? ( + + {badge} + + ) : null} + + ); +} diff --git a/mobile/src/components/Scan.tsx b/mobile/src/components/Scan.tsx new file mode 100644 index 0000000..be5e168 --- /dev/null +++ b/mobile/src/components/Scan.tsx @@ -0,0 +1,80 @@ +import { useState } from 'react'; +import { Text, View } from 'react-native'; +import { CameraView, useCameraPermissions } from 'expo-camera'; +import { Button, Notice, Sheet } from './ui'; +import { useTheme } from '../state/theme'; +import { F, R } from '../theme'; + +/** + * The QR scanner behind the icon in the recipient field. Only a Sui address is accepted — + * a QR that decodes to anything else is said out loud rather than silently pasted. + */ +const ADDRESS = /0x[0-9a-fA-F]{40,64}/; + +export function ScanSheet({ + open, + onClose, + onAddress, +}: { + open: boolean; + onClose: () => void; + onAddress: (address: string) => void; +}) { + const C = useTheme(); + const [permission, requestPermission] = useCameraPermissions(); + const [rejected, setRejected] = useState(null); + + const handle = (data: string) => { + const match = ADDRESS.exec(data.trim()); + if (match) { + setRejected(null); + onAddress(match[0]); + onClose(); + } else { + setRejected(data.slice(0, 48)); + } + }; + + return ( + + + {!permission?.granted ? ( + + + Point the camera at a recipient's address QR. The camera is used for this and + nothing else. + + + {permission && !permission.granted && !permission.canAskAgain ? ( + + Camera access is off for Skur. Turn it on in the system settings to scan. + + ) : null} + + ) : ( + + + {open ? ( + handle(data)} + /> + ) : null} + + {rejected ? ( + That QR does not carry a Sui address: “{rejected}…” + ) : ( + + Looking for a QR… + + )} + + )} + + + ); +} diff --git a/mobile/src/components/TokenMark.tsx b/mobile/src/components/TokenMark.tsx new file mode 100644 index 0000000..5031e40 --- /dev/null +++ b/mobile/src/components/TokenMark.tsx @@ -0,0 +1,48 @@ +import { Text, View } from 'react-native'; +import { useTheme } from '../state/theme'; +import { F } from '../theme'; + +/** A coin type's struct name is what a person recognises; the rest is address noise. */ +export function coinSymbol(coinType: string): string { + return (coinType.split('::').pop() ?? coinType).toUpperCase(); +} + +const DECIMALS: Record = { SUI: 9, USDC: 6, USDT: 6, USDSUI: 6 }; + +export function coinDecimals(coinType: string): number { + return DECIMALS[coinSymbol(coinType)] ?? 9; +} + +/** Deterministic colour per symbol, so the same asset looks the same on every screen. */ +const TINTS: [string, string][] = [ + ['#4da2ff', '#0b1f36'], + ['#2775ca', '#06182e'], + ['#26a17b', '#05261c'], + ['#8b5cf6', '#1b0f33'], +]; + +export function TokenMark({ coinType, size = 32 }: { coinType: string; size?: number }) { + const C = useTheme(); + const symbol = coinSymbol(coinType); + let n = 0; + for (let i = 0; i < symbol.length; i++) n = (n + symbol.charCodeAt(i)) % TINTS.length; + const [bg, fg] = TINTS[n]; + return ( + + + {symbol.slice(0, 2)} + + + ); +} diff --git a/mobile/src/components/kit.tsx b/mobile/src/components/kit.tsx new file mode 100644 index 0000000..54884ea --- /dev/null +++ b/mobile/src/components/kit.tsx @@ -0,0 +1,375 @@ +import type { ReactNode } from 'react'; +import { Switch, Text, View } from 'react-native'; +import Svg, { Circle, G } from 'react-native-svg'; +import { Icon, type IconName } from './ui'; +import { useTheme } from '../state/theme'; +import { F, R } from '../theme'; + +/** + * The pieces the product screens are drawn with, beyond the base kit in ui.tsx: checklists, + * meters, bars, avatars, pager dots and timelines. Pure presentation — nothing here reads the + * chain or holds state. + */ + +/** A green-check feature or control line, used by the tour, the security center and sign review. */ +export function CheckItem({ + title, + detail, + ok = true, + icon, +}: { + title: string; + detail?: string; + ok?: boolean | 'warn'; + icon?: IconName; +}) { + const C = useTheme(); + const color = ok === true ? C.success : ok === 'warn' ? C.warning : C.error; + const name: IconName = icon ?? (ok === true ? 'check-circle' : ok === 'warn' ? 'alert-circle' : 'x-circle'); + return ( + + + + + + {title} + {detail ? ( + {detail} + ) : null} + + + ); +} + +/** A settings row with a switch on the right. */ +export function SwitchRow({ + title, + subtitle, + value, + onChange, + icon, +}: { + title: string; + subtitle?: string; + value: boolean; + onChange: (v: boolean) => void; + icon?: IconName; +}) { + const C = useTheme(); + return ( + + {icon ? : null} + + {title} + {subtitle ? {subtitle} : null} + + + + ); +} + +/** A labelled progress meter: posture, approvals collected, budget used. 0..1. */ +export function Meter({ value, label, trailing }: { value: number; label?: string; trailing?: string }) { + const C = useTheme(); + const clamped = Math.max(0, Math.min(1, value)); + return ( + + {label || trailing ? ( + + {label ? {label} : } + {trailing ? {trailing} : null} + + ) : null} + + + + + ); +} + +export type Slice = { label: string; value: number; color: string }; + +/** One stacked bar for the whole portfolio, with a legend — the honest cousin of a donut. */ +export function PortfolioBar({ slices }: { slices: Slice[] }) { + const C = useTheme(); + const total = slices.reduce((s, x) => s + x.value, 0); + if (total <= 0) return null; + return ( + + + {slices.map((s) => ( + + ))} + + + {slices.map((s) => ( + + + {s.label} + + {((s.value / total) * 100).toFixed(1)}% + + + ))} + + + ); +} + +/** Vertical bars, one per item, scaled to the largest. */ +export function BarChart({ items }: { items: { label: string; value: number }[] }) { + const C = useTheme(); + const max = Math.max(...items.map((i) => i.value), 1); + return ( + + {items.map((i) => ( + + + + {i.label} + + + ))} + + ); +} + +/** Initials in a circle, for people the chain only knows as addresses. */ +export function Avatar({ name, size = 36 }: { name: string; size?: number }) { + const C = useTheme(); + const initials = name + .split(/\s+/) + .filter(Boolean) + .slice(0, 2) + .map((w) => w[0]!.toUpperCase()) + .join(''); + return ( + + {initials || '•'} + + ); +} + +/** Pager dots for the tour. */ +export function Dots({ count, index }: { count: number; index: number }) { + const C = useTheme(); + return ( + + {Array.from({ length: count }).map((_, i) => ( + + ))} + + ); +} + +/** + * One step on an approval timeline. The rail is drawn by the items themselves so the line never + * outruns the content. + */ +export function TimelineStep({ + title, + subtitle, + state, + last = false, + trailing, +}: { + title: string; + subtitle?: ReactNode; + state: 'done' | 'active' | 'todo'; + last?: boolean; + trailing?: ReactNode; +}) { + const C = useTheme(); + const color = state === 'done' ? C.success : state === 'active' ? C.accent : C.text3; + return ( + + + + {state === 'done' ? : null} + + {!last ? : null} + + + + {title} + {trailing} + + {subtitle ? ( + {subtitle} + ) : null} + + + ); +} + +/** A big stat with a caption, for the dashboard's value and count cards. */ +export function Stat({ value, label, tone }: { value: string; label: string; tone?: 'accent' | 'plain' }) { + const C = useTheme(); + return ( + + + {value} + + {label} + + ); +} + +/** Two-to-three equal buttons acting as one choice — the reference's Send / Request toggle. */ +export function Segmented({ + value, + options, + onChange, +}: { + value: T; + options: Array<[T, string]>; + onChange: (v: T) => void; +}) { + const C = useTheme(); + return ( + + {options.map(([v, label]) => { + const on = v === value; + return ( + onChange(v)} + accessibilityRole="button" + accessibilityState={{ selected: on }} + style={{ + flex: 1, + textAlign: 'center', + paddingVertical: 13, + borderRadius: R.md, + overflow: 'hidden', + fontFamily: F.bodyBold, + fontSize: 15, + backgroundColor: on ? C.accent : C.card, + color: on ? C.onAccent : C.text2, + }} + > + {label} + + ); + })} + + ); +} + +/** + * The donut from the reference: segments by share, the total in the hole. Shares are by + * normalised units — this app carries no price oracle, and a chart that silently guessed at + * dollars would be a lie with a legend. + */ +export function Donut({ + slices, + centerTitle, + centerSub, + size = 200, +}: { + slices: Slice[]; + centerTitle: string; + centerSub?: string; + size?: number; +}) { + const C = useTheme(); + const total = slices.reduce((s, x) => s + x.value, 0); + const stroke = size * 0.13; + const r = (size - stroke) / 2; + const circumference = 2 * Math.PI * r; + const gap = slices.length > 1 ? circumference * 0.012 : 0; + let offset = 0; + return ( + + + + {total <= 0 ? ( + + ) : ( + slices.map((s) => { + const length = (s.value / total) * circumference - gap; + const el = ( + 1 ? 'round' : 'butt'} + strokeDasharray={`${Math.max(length, 1)} ${circumference}`} + strokeDashoffset={-offset} + /> + ); + offset += (s.value / total) * circumference; + return el; + }) + )} + + + + + {centerTitle} + + {centerSub ? ( + {centerSub} + ) : null} + + + ); +} diff --git a/mobile/src/components/ui.tsx b/mobile/src/components/ui.tsx new file mode 100644 index 0000000..e9aca46 --- /dev/null +++ b/mobile/src/components/ui.tsx @@ -0,0 +1,427 @@ +import { Feather } from "@expo/vector-icons"; +import * as Clipboard from "expo-clipboard"; +import { forwardRef, useState, type ComponentProps, type ReactNode } from "react"; +import { ActivityIndicator, Image, KeyboardAvoidingView, Modal, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View, type StyleProp, type TextInputProps, type ViewStyle } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { + MODE_LABELS, + Mode, + STATUS_LABELS, + Status, + TIER_LABELS, + Tier, + TRUST_LABELS, + Trust, + short, +} from "@skur/sdk"; +import type { TxState } from "../hooks/useTx"; +import { useTheme } from "../state/theme"; +import { F, R, type Palette } from "../theme"; + +export type IconName = ComponentProps["name"]; +export function Icon({ name, size = 18, color }: { name: IconName; size?: number; color?: string }) { + const C = useTheme(); + return ; +} + +export function Screen({ children, top, footer, refreshControl, padded = true, centered = false }: { children: ReactNode; top?: ReactNode; footer?: ReactNode; refreshControl?: ReactNode; padded?: boolean; centered?: boolean }) { + const C = useTheme(); const s = useStyles(); const insets = useSafeAreaInsets(); + return ( + // Without the avoiding view, the keyboard covers the footer — which on Send is the submit + // button, so the screen looks broken exactly when someone is mid-payment. Android resizes the + // window itself (adjustResize), so the behaviour only applies on iOS. + + {top ? {top} : null} + {/* `centered`: sparse screens (the launch, the fork, a lone empty state) sit in the + middle of the viewport instead of hugging the status bar with a void below. */} + {children} + {footer ? {footer} : null} + + ); +} + +export function TopBar({ left, center, right, title }: { left?: ReactNode; center?: ReactNode; right?: ReactNode; title?: string }) { + const s = useStyles(); + return ( + + {left}{title ? {title} : null} + {center ? {center} : null} + {right} + + ); +} + +export function IconButton({ name, onPress, dot, tone = "dark", label }: { name: IconName; onPress?: () => void; dot?: boolean; tone?: "dark" | "accent"; label?: string }) { + const C = useTheme(); const s = useStyles(); + return ( + [s.iconBtn, tone === "accent" && { backgroundColor: C.accent }, pressed && { opacity: 0.6 }]}> + + {dot ? : null} + + ); +} + +export function BackButton({ onPress }: { onPress: () => void }) { return ; } + +/** The web logo: a yellow tile with an S, then the wordmark. */ +export function Logo({ size = 28, wordmark = true }: { size?: number; wordmark?: boolean }) { + const C = useTheme(); + return ( + + {/* The mark itself, not a stand-in letter. Yellow on transparent, so it sits on either ground. */} + + {wordmark ? Skur : null} + + ); +} + +/** A card that opens on tap: the header carries the value, so a long screen reads as a short list. */ +export function Expandable({ title, summary, icon, tone = "dark", children, open, onToggle }: { title: string; summary?: string; icon?: IconName; tone?: Tone; children: ReactNode; open: boolean; onToggle: () => void }) { + const C = useTheme(); const s = useStyles(); + return ( + + [{ flexDirection: "row", alignItems: "center", gap: 12, padding: 16 }, pressed && { opacity: 0.6 }]}> + {icon ? : null} + + {title} + {summary ? {summary} : null} + + + + {open ? {children} : null} + + ); +} + +export function Card({ children, style, flush = false }: { children: ReactNode; style?: StyleProp; flush?: boolean }) { + const s = useStyles(); + return {children}; +} +export function SectionLabel({ children }: { children: ReactNode }) { const s = useStyles(); return {children}; } + +/** The list row every screen is built from: leading mark, title, value, chevron. */ +export function Row({ leading, title, subtitle, trailing, onPress, chevron = false, last = false }: { leading?: ReactNode; title: ReactNode; subtitle?: ReactNode; trailing?: ReactNode; onPress?: () => void; chevron?: boolean; last?: boolean }) { + const C = useTheme(); const s = useStyles(); + const body = ( + + {leading ? {leading} : null} + + {typeof title === "string" ? {title} : title} + {subtitle ? (typeof subtitle === "string" ? {subtitle} : subtitle) : null} + + {trailing} + {chevron ? : null} + + ); + return onPress ? pressed && { opacity: 0.6 }}>{body} : body; +} + +type Tone = "ok" | "warn" | "bad" | "info" | "neutral" | "accent" | "dark" | "success" | "error"; +function tones(C: Palette): Record { + return { + ok: { bg: C.successBg, fg: C.success }, success: { bg: C.successBg, fg: C.success }, + warn: { bg: C.warningBg, fg: C.warning }, bad: { bg: C.errorBg, fg: C.error }, error: { bg: C.errorBg, fg: C.error }, + info: { bg: C.infoBg, fg: C.info }, neutral: { bg: C.card2, fg: C.text2 }, dark: { bg: C.card2, fg: C.text }, + accent: { bg: C.accent, fg: C.onAccent }, + }; +} + +export function CircleIcon({ name, size = 40, tone = "dark", text }: { name?: IconName; size?: number; tone?: Tone; text?: string }) { + const C = useTheme(); const t = tones(C)[tone]; + return ( + + {text ? {text} : name ? : null} + + ); +} + +export function KV({ k, v, mono = false, last = false }: { k: string; v: ReactNode; mono?: boolean; last?: boolean }) { + const s = useStyles(); + return ( + + {k} + {typeof v === "string" || typeof v === "number" ? {v} : v} + + ); +} + +export function Badge({ tone = "neutral", children, icon }: { tone?: Tone; children: ReactNode; icon?: IconName }) { + const C = useTheme(); const s = useStyles(); const t = tones(C)[tone]; + return {icon ? : null}{children}; +} +export const TierBadge = ({ tier }: { tier: Tier }) => {TIER_LABELS[tier]}; +export const ModeBadge = ({ mode }: { mode: Mode }) => {MODE_LABELS[mode]}; +export const StatusBadge = ({ status }: { status: Status }) => {STATUS_LABELS[status]}; +export const TrustBadge = ({ trust }: { trust: Trust }) => {TRUST_LABELS[trust]}; + +export function Button({ children, onPress, kind = "primary", disabled, loading, icon, style, size = "md", testID }: { children: ReactNode; onPress?: () => void; kind?: "primary" | "secondary" | "danger" | "ghost"; disabled?: boolean; loading?: boolean; icon?: IconName; style?: StyleProp; size?: "md" | "sm"; testID?: string }) { + const C = useTheme(); const s = useStyles(); + const bg = kind === "primary" ? C.accent : kind === "danger" ? C.errorBg : kind === "secondary" ? C.card2 : "transparent"; + const fg = kind === "primary" ? C.onAccent : kind === "danger" ? C.error : C.text; + return ( + [s.btn, size === "sm" && { height: 36, paddingHorizontal: 14 }, { backgroundColor: bg, opacity: disabled ? 0.4 : pressed ? 0.8 : 1 }, style]}> + {loading ? : <>{icon ? : null}{children}} + + ); +} + +/** The sticky primary action: what you are about to sign, and with which key. */ +export function SignBar({ label, signerAddress, onPress, disabled, loading, hint }: { label: string; signerAddress?: string | null; onPress?: () => void; disabled?: boolean; loading?: boolean; hint?: string }) { + const C = useTheme(); const s = useStyles(); + return ( + + {hint ? {hint} : null} + [s.signbar, { opacity: disabled ? 0.45 : pressed ? 0.85 : 1 }]}> + {loading ? : } + {label} + {signerAddress ? {short(signerAddress)} : null} + + + + ); +} + +export function Field({ label, hint, children }: { label: string; hint?: string; children: ReactNode }) { + const s = useStyles(); + return ( + + {label} + {children} + {hint ? {hint} : null} + + ); +} + +export const Input = forwardRef(function Input(props, ref) { + const C = useTheme(); const s = useStyles(); + return ; +}); + +export function Tabs({ value, options, onChange }: { value: T; options: Array<[T, string]>; onChange: (v: T) => void }) { + const C = useTheme(); const s = useStyles(); + return ( + + {options.map(([v, label]) => ( + onChange(v)} style={[s.tab, value === v && { borderBottomColor: C.accent }]}>{label} + ))} + + ); +} + +export function Address({ value, full = false, style }: { value: string; full?: boolean; style?: object }) { + const C = useTheme(); const s = useStyles(); + const [copied, setCopied] = useState(false); + return ( + { await Clipboard.setStringAsync(value); setCopied(true); setTimeout(() => setCopied(false), 1200); }} hitSlop={8} style={{ flexDirection: "row", alignItems: "center", gap: 6 }}> + {full ? value : short(value)} + + + ); +} + +export function Notice({ tone = "info", children }: { tone?: Tone; children: ReactNode }) { + const C = useTheme(); const s = useStyles(); const t = tones(C)[tone]; + return {children}; +} + +/** + * A transaction, step by step. "Working…" tells a person nothing when the thing working is their + * money; which step it is on tells them what is left and roughly how long. + */ +const TX_STEPS: [TxState['phase'], string][] = [ + ['unlocking', 'Unlock the key'], + ['signing', 'Sign and send'], + ['waiting', 'Settle on the network'], +]; + +export function TxProgress({ state }: { state: TxState }) { + const C = useTheme(); + if (state.phase === 'idle') return null; + if (state.phase === 'cancelled') return Cancelled.; + if (state.phase === 'error') return {state.message}; + if (state.phase === 'done') { + return Executed · {short(state.digest, 6)}; + } + const current = TX_STEPS.findIndex(([phase]) => phase === state.phase); + return ( + + {TX_STEPS.map(([phase, label], i) => { + const done = i < current; + const active = i === current; + return ( + + {active ? ( + + ) : ( + + )} + + {label} + + + ); + })} + + ); +} + +/** Kept as the compact single-line form, for places a step list will not fit. */ +export function TxStatus({ state }: { state: TxState }) { + return ; +} + +/** + * A failed read, with the way out. "Error" with no retry makes the person restart the app, which + * works by accident and teaches them the app is flaky. + */ +export function ErrorState({ + title = 'Could not reach the vault', + detail, + onRetry, +}: { + title?: string; + detail?: string; + onRetry?: () => void; +}) { + const C = useTheme(); + return ( + + + {title} + {detail ? ( + + {detail} + + ) : null} + {onRetry ? ( + + ) : null} + + ); +} + +/** Placeholder blocks while a read is in flight, so the layout arrives before the numbers do. */ +export function Skeleton({ lines = 3 }: { lines?: number }) { + const C = useTheme(); + return ( + + {Array.from({ length: lines }, (_, i) => ( + + ))} + + ); +} + +/** + * Shown when the device believes it is offline. The data on screen is the cache, which is worth + * far more than a spinner — as long as its age is stated rather than implied. + */ +export function OfflineBanner({ asOf }: { asOf?: number }) { + const C = useTheme(); + const age = + asOf && asOf > 0 + ? ` — showing the vault as of ${new Date(asOf).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}` + : ''; + return ( + + + + Offline{age} + + + ); +} + +export function Empty({ icon = "inbox", children }: { icon?: IconName; children: ReactNode }) { + const C = useTheme(); + return {children}; +} + +/** Caution tape: the brand's mark, used only where the vault is not in Normal mode. */ +export function Tape({ height = 8 }: { height?: number }) { + const C = useTheme(); + return ( + + {Array.from({ length: 34 }).map((_, i) => )} + + ); +} + +export function Sheet({ open, onClose, title, children, action }: { open: boolean; onClose: () => void; title: string; children: ReactNode; action?: ReactNode }) { + const s = useStyles(); const insets = useSafeAreaInsets(); + return ( + + + + + + {title} + {action ? {action} : null} + + {children} + + + ); +} + +const makeStyles = (C: Palette) => StyleSheet.create({ + screen: { flex: 1, backgroundColor: C.canvas }, + topbar: { height: 52, paddingHorizontal: 16, flexDirection: "row", alignItems: "center", justifyContent: "space-between" }, + topTitle: { fontFamily: F.display, fontSize: 22, color: C.text, letterSpacing: -0.3 }, + iconBtn: { width: 40, height: 40, borderRadius: 20, backgroundColor: C.card, alignItems: "center", justifyContent: "center" }, + dot: { position: "absolute", top: 8, right: 8, width: 8, height: 8, borderRadius: 4, backgroundColor: C.accent, borderWidth: 1.5, borderColor: C.canvas }, + footer: { position: "absolute", left: 0, right: 0, bottom: 0, padding: 16, backgroundColor: C.canvas, borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: C.border }, + card: { backgroundColor: C.card, borderRadius: R.lg, padding: 16, marginBottom: 12 }, + sectionLabel: { fontFamily: F.mono, fontSize: 11, letterSpacing: 1.2, textTransform: "uppercase", color: C.text3, marginBottom: 8, marginTop: 8 }, + row: { flexDirection: "row", alignItems: "center", gap: 12, paddingVertical: 13, paddingHorizontal: 16, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: C.border }, + rowTitle: { fontFamily: F.bodyBold, fontSize: 15, color: C.text }, + rowSub: { fontFamily: F.body, fontSize: 13, color: C.text2 }, + kv: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", gap: 12, paddingVertical: 11, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: C.border }, + kvK: { fontFamily: F.body, fontSize: 14, color: C.text2 }, + kvV: { fontFamily: F.bodyMedium, fontSize: 14, color: C.text, textAlign: "right", flexShrink: 1 }, + badge: { flexDirection: "row", alignItems: "center", gap: 5, paddingHorizontal: 9, paddingVertical: 4, borderRadius: R.pill, alignSelf: "flex-start" }, + badgeText: { fontFamily: F.bodyBold, fontSize: 12 }, + btn: { height: 48, borderRadius: R.md, alignItems: "center", justifyContent: "center", paddingHorizontal: 18, flexDirection: "row", gap: 8 }, + btnText: { fontFamily: F.bodyBold, fontSize: 15 }, + signbar: { height: 56, borderRadius: R.lg, backgroundColor: C.accent, flexDirection: "row", alignItems: "center", gap: 10, paddingHorizontal: 18 }, + signbarText: { fontFamily: F.bodyBold, fontSize: 16, color: C.onAccent, flex: 1 }, + signerChip: { backgroundColor: "rgba(16,20,24,0.12)", borderRadius: R.pill, paddingHorizontal: 10, height: 28, justifyContent: "center" }, + signerChipText: { fontFamily: F.monoMedium, fontSize: 12, color: C.onAccent }, + label: { fontFamily: F.bodyMedium, fontSize: 13, color: C.text2, marginBottom: 6 }, + hint: { fontFamily: F.body, fontSize: 12, color: C.text3, marginTop: 6, lineHeight: 16 }, + input: { backgroundColor: C.card2, borderRadius: R.md, paddingHorizontal: 14, height: 48, color: C.text, fontFamily: F.body, fontSize: 15, borderWidth: 1, borderColor: C.border }, + tabs: { flexDirection: "row", borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: C.border, marginBottom: 4 }, + tab: { paddingVertical: 10, paddingHorizontal: 4, marginRight: 22, borderBottomWidth: 2, borderBottomColor: "transparent" }, + tabText: { fontFamily: F.bodyBold, fontSize: 15, color: C.text3 }, + addr: { fontFamily: F.mono, fontSize: 13, color: C.text2 }, + notice: { padding: 12, borderRadius: R.md, marginTop: 10 }, + sheetBackdrop: { flex: 1, backgroundColor: "rgba(0,0,0,0.5)" }, + sheet: { backgroundColor: C.card, borderTopLeftRadius: 22, borderTopRightRadius: 22, paddingHorizontal: 16, paddingTop: 8 }, + grabber: { width: 40, height: 4, borderRadius: 2, backgroundColor: C.border, alignSelf: "center", marginBottom: 14 }, + sheetTitle: { fontFamily: F.display, fontSize: 18, color: C.text }, +}); + +const cache = new Map>(); +/** Styles for the ground currently in use, built once per palette. */ +export function useStyles() { + const C = useTheme(); + if (!cache.has(C.canvas)) cache.set(C.canvas, makeStyles(C)); + return cache.get(C.canvas)!; +} diff --git a/mobile/src/hooks/useOnline.ts b/mobile/src/hooks/useOnline.ts new file mode 100644 index 0000000..861ecf2 --- /dev/null +++ b/mobile/src/hooks/useOnline.ts @@ -0,0 +1,15 @@ +import { useNetworkState } from 'expo-network'; + +/** + * Whether the device believes it can reach anything. + * + * "Believes" is the honest word: a connected radio with a dead backhaul still reports reachable, + * so this gates banners and copy, never correctness — a read that fails shows its own error with + * a retry, whatever this says. + */ +export function useOnline(): boolean { + const state = useNetworkState(); + // Unknown is treated as online, because a false offline banner on launch teaches people to + // ignore the banner. + return state.isInternetReachable !== false && state.isConnected !== false; +} diff --git a/mobile/src/hooks/useTx.ts b/mobile/src/hooks/useTx.ts new file mode 100644 index 0000000..2853050 --- /dev/null +++ b/mobile/src/hooks/useTx.ts @@ -0,0 +1,82 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { useCallback, useState } from 'react'; +import type { Transaction } from '@mysten/sui/transactions'; +import { describeFailure } from '@skur/sdk'; +import { hapticError, hapticSuccess } from '../lib/haptics'; +import { client } from '../lib/client'; +import { NETWORK } from '../lib/config'; +import { unlockSigner } from '../lib/keystore'; + +export type TxState = + | { phase: 'idle' } + | { phase: 'unlocking' } + | { phase: 'signing' } + | { phase: 'waiting'; digest: string } + | { phase: 'done'; digest: string } + | { phase: 'error'; message: string } + | { phase: 'cancelled' }; + +/** + * Sign and execute, with the key unlocked for exactly one action. + * + * The biometric prompt is per transaction, not per session. A cached signer is a signer that acts + * without a face, which on a treasury app is the whole thing you were trying to avoid. + */ +export function useTx(vaultId: string | null) { + const queryClient = useQueryClient(); + const [state, setState] = useState({ phase: 'idle' }); + + const run = useCallback( + async (build: () => Transaction, reason: string): Promise => { + try { + setState({ phase: 'unlocking' }); + const keypair = await unlockSigner(reason); + + setState({ phase: 'signing' }); + const result = await client.signAndExecuteTransaction({ + transaction: build(), + signer: keypair, + include: { effects: true }, + }); + + // A resolved promise means the network executed it, not that it succeeded: a Move abort + // comes back as FailedTransaction. + if (result.FailedTransaction) { + const message = describeFailure( + result.FailedTransaction.status.error?.message ?? 'the vault refused it', + ); + hapticError(); + setState({ phase: 'error', message }); + return null; + } + + const digest = result.Transaction.digest; + setState({ phase: 'waiting', digest }); + await client.core.waitForTransaction({ digest }); + + await queryClient.invalidateQueries({ queryKey: ['vault', NETWORK, vaultId] }); + await queryClient.invalidateQueries({ queryKey: ['preview', NETWORK, vaultId] }); + hapticSuccess(); + setState({ phase: 'done', digest }); + return digest; + } catch (e) { + const raw = e instanceof Error ? e.message : String(e); + // A declined fingerprint is not an error worth a red banner. + if (raw === 'cancelled') { + setState({ phase: 'cancelled' }); + return null; + } + hapticError(); + if (__DEV__) console.error('tx failed', e, e instanceof Error ? e.stack : ''); + setState({ phase: 'error', message: describeFailure(e) }); + return null; + } + }, + [queryClient, vaultId], + ); + + const reset = useCallback(() => setState({ phase: 'idle' }), []); + const busy = state.phase === 'unlocking' || state.phase === 'signing' || state.phase === 'waiting'; + + return { run, state, reset, busy }; +} diff --git a/mobile/src/hooks/useVault.ts b/mobile/src/hooks/useVault.ts new file mode 100644 index 0000000..7f0544c --- /dev/null +++ b/mobile/src/hooks/useVault.ts @@ -0,0 +1,65 @@ +import { useQuery } from '@tanstack/react-query'; +import { + fetchVaultView, + previewTransfer, + type TransferPreview, + type VaultView, +} from '@skur/sdk'; +import { client } from '../lib/client'; +import { NETWORK, PACKAGE_ID } from '../lib/config'; + +/** Everything one screen needs, read from the chain. */ +export function useVaultView(vaultId: string | null) { + return useQuery({ + queryKey: ['vault', NETWORK, vaultId], + enabled: Boolean(vaultId), + // A vault is a shared object, so anyone's write changes it. Short freshness, and a poll + // slow enough that the screen does not look like it is reloading constantly. + staleTime: 5_000, + refetchInterval: 20_000, + retry: 1, + queryFn: () => fetchVaultView(client, vaultId!), + }); +} + +/** + * What the vault itself will demand for this payment. + * + * Simulated against the chain's own `preview_transfer`, not computed here, so the confirmation + * screen cannot promise something execution would refuse. + */ +export function useTransferPreview(input: { + vaultId: string | null; + coinType: string; + amount: bigint; + recipient: string; + sender: string | null; + enabled: boolean; +}) { + return useQuery({ + queryKey: [ + 'preview', + NETWORK, + input.vaultId, + input.coinType, + input.amount.toString(), + input.recipient, + ], + enabled: + input.enabled && + Boolean(input.vaultId && input.sender && input.coinType) && + input.amount > 0n, + staleTime: 10_000, + refetchInterval: false, + retry: 0, + queryFn: () => + previewTransfer(client, { + packageId: PACKAGE_ID, + vaultId: input.vaultId!, + coinType: input.coinType, + amount: input.amount, + recipient: input.recipient, + sender: input.sender!, + }), + }); +} diff --git a/mobile/src/lib/client.ts b/mobile/src/lib/client.ts new file mode 100644 index 0000000..f78612b --- /dev/null +++ b/mobile/src/lib/client.ts @@ -0,0 +1,41 @@ +import './polyfills'; +import * as grpc from '@mysten/sui/grpc'; +import { GRPC_URL, NETWORK } from './config'; +import type { SuiGrpcClient } from '@mysten/sui/grpc'; + +/** + * One client for the app, built on first use rather than at import. + * + * Constructing it at module scope means any resolution problem in the SDK throws during module + * initialisation — before React mounts, with no component tree to show an error in, and no stack + * that names the cause. Lazily, a failure is a message on a screen. + * + * gRPC, not JSON-RPC: Sui switched JSON-RPC off on mainnet full nodes in July 2026 and is removing + * it. The transport is gRPC-web over fetch, which is what React Native can actually do — there is + * no HTTP/2 socket to reach for here. + */ +let instance: SuiGrpcClient | null = null; + +export function getClient(): SuiGrpcClient { + if (instance) return instance; + const Ctor = grpc.SuiGrpcClient; + if (typeof Ctor !== 'function') { + throw new Error( + `@mysten/sui/grpc did not provide SuiGrpcClient under this bundler. Got ${typeof Ctor}; the module exported: ${Object.keys(grpc).join(', ') || '(nothing)'}`, + ); + } + instance = new Ctor({ network: NETWORK, baseUrl: GRPC_URL }); + return instance; +} + +/** + * A proxy, so callers can keep writing `client.core.getObject(...)` while construction stays + * lazy. Every property access builds the client on first touch. + */ +export const client = new Proxy({} as SuiGrpcClient, { + get(_t, prop) { + const c = getClient() as unknown as Record; + const value = c[prop]; + return typeof value === 'function' ? value.bind(c) : value; + }, +}); diff --git a/mobile/src/lib/config.ts b/mobile/src/lib/config.ts new file mode 100644 index 0000000..585ea04 --- /dev/null +++ b/mobile/src/lib/config.ts @@ -0,0 +1,23 @@ +import Constants from 'expo-constants'; +import { GRPC_ENDPOINTS, type Network } from '@skur/sdk'; + +/** + * Where this build points. `EXPO_PUBLIC_*` wins, then app.json's `extra`, then the defaults — + * so a developer can point a build at their own publish without editing a committed file. + */ +const extra = (Constants.expoConfig?.extra ?? {}) as Record; + +function setting(key: string, fallback: string): string { + return process.env[`EXPO_PUBLIC_${key}`] ?? extra[key] ?? fallback; +} + +export const NETWORK = setting('SKUR_NETWORK', 'testnet') as Network; +export const PACKAGE_ID = setting('SKUR_PACKAGE_ID', '0x0'); +export const GRPC_URL = setting('SKUR_GRPC_URL', GRPC_ENDPOINTS[NETWORK]); +/** The indexer. Optional: every screen reads the chain directly without it. */ +export const API_URL = setting('SKUR_API', ''); + +export const CONFIGURED = PACKAGE_ID !== '0x0'; + +export const explorer = (kind: 'object' | 'tx', id: string) => + `https://suiscan.xyz/${NETWORK}/${kind}/${id}`; diff --git a/mobile/src/lib/haptics.ts b/mobile/src/lib/haptics.ts new file mode 100644 index 0000000..eda9ebc --- /dev/null +++ b/mobile/src/lib/haptics.ts @@ -0,0 +1,18 @@ +import * as Haptics from 'expo-haptics'; + +/** + * Touch feedback for the moments that matter: a transaction settling, a transaction refused. + * Wrapped so a platform without a haptic engine degrades to nothing instead of a rejection + * nobody handles. + */ +export function hapticSuccess() { + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => undefined); +} + +export function hapticError() { + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => undefined); +} + +export function hapticTap() { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => undefined); +} diff --git a/mobile/src/lib/keystore.ts b/mobile/src/lib/keystore.ts new file mode 100644 index 0000000..c19afe2 --- /dev/null +++ b/mobile/src/lib/keystore.ts @@ -0,0 +1,126 @@ +import * as LocalAuthentication from 'expo-local-authentication'; +import * as SecureStore from 'expo-secure-store'; +import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519'; +import { decodeSuiPrivateKey } from '@mysten/sui/cryptography'; + +/** + * The signing key, held in the device's secure element behind biometrics. + * + * Be clear about what this is. A key on a phone is a **hot key**: it is appropriate for an + * approver or an executor, who need to act often and whose authority is bounded by the vault's + * policy. It is **not** appropriate for a guardian, whose entire value is being an independent + * control plane an attacker has to breach separately — a guardian key belongs on hardware or on a + * machine that is not also reading email. The app says so where it matters, because a guardian + * key on the same phone as an approver key collapses the two planes the contract works to keep + * apart. + * + * What the device gives us: the secret is written to the iOS keychain or Android keystore, not to + * app storage, and `requireAuthentication` means the OS will not release it without a successful + * biometric or passcode check. What it does not give us: protection from a rooted or jailbroken + * device, and no recovery if the device is lost — which is what the vault's guardian recovery is + * for. + */ +const KEY = 'skur.signer.v1'; +/** + * The address, in its own entry without `requireAuthentication`. An address is public — screens + * show it constantly — and on Android any read of an authenticated keystore entry raises the + * system biometric prompt no matter what options the *read* passes. Keeping the address beside + * the secret meant a fingerprint prompt just to launch the app, and pulled the secret into JS + * memory to derive something that was never secret. + */ +const ADDR = 'skur.signer.addr.v1'; + +const OPTIONS: SecureStore.SecureStoreOptions = { + requireAuthentication: true, + keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY, + authenticationPrompt: 'Unlock your Skur signing key', +}; + +const ADDR_OPTIONS: SecureStore.SecureStoreOptions = { + keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY, +}; + +export type BiometricSupport = { + available: boolean; + enrolled: boolean; + /** What the device calls it, so the UI does not promise Face ID on a fingerprint phone. */ + label: string; +}; + +export async function biometricSupport(): Promise { + const [available, enrolled, types] = await Promise.all([ + LocalAuthentication.hasHardwareAsync(), + LocalAuthentication.isEnrolledAsync(), + LocalAuthentication.supportedAuthenticationTypesAsync(), + ]); + const label = types.includes(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION) + ? 'Face ID' + : types.includes(LocalAuthentication.AuthenticationType.FINGERPRINT) + ? 'fingerprint' + : 'device passcode'; + return { available, enrolled, label }; +} + +export async function hasSigner(): Promise { + // The address entry is the existence marker: it is not auth-gated, so asking whether a key + // exists never prompts for a face. + return Boolean(await signerAddress()); +} + +/** Generate a key and store it. Refuses to overwrite one that already exists. */ +export async function createSigner(): Promise { + if (await hasSigner()) throw new Error('a signing key already exists on this device'); + const keypair = Ed25519Keypair.generate(); + const address = keypair.toSuiAddress(); + await SecureStore.setItemAsync(KEY, keypair.getSecretKey(), OPTIONS); + await SecureStore.setItemAsync(ADDR, address, ADDR_OPTIONS); + return address; +} + +/** Import an existing key, so a signer already on a vault can use their phone. */ +export async function importSigner(secret: string): Promise { + const trimmed = secret.trim(); + // Validate before storing: a key that cannot be decoded is worse in the keychain than rejected. + const { secretKey } = decodeSuiPrivateKey(trimmed); + const keypair = Ed25519Keypair.fromSecretKey(secretKey); + const address = keypair.toSuiAddress(); + await SecureStore.setItemAsync(KEY, trimmed, OPTIONS); + await SecureStore.setItemAsync(ADDR, address, ADDR_OPTIONS); + return address; +} + +/** + * Unlock the key. Prompts for biometrics, and returns the keypair only for the duration of one + * action — nothing here caches it, because a cached signer is a signer that acts without a face. + */ +export async function unlockSigner(reason = 'Approve with your Skur key'): Promise { + // One prompt, raised by the OS for the key release itself. A separate LocalAuthentication + // check before the read meant two fingerprints per action — and the second one was the only + // one that actually protected anything. + let stored: string | null; + try { + stored = await SecureStore.getItemAsync(KEY, { ...OPTIONS, authenticationPrompt: reason }); + } catch { + // The OS prompt was dismissed or failed. A decision, not an error. + throw new Error('cancelled'); + } + if (!stored) throw new Error('no signing key on this device'); + const { secretKey } = decodeSuiPrivateKey(stored); + return Ed25519Keypair.fromSecretKey(secretKey); +} + +/** The address, read without unlocking — it is public, and screens need it constantly. */ +export async function signerAddress(): Promise { + return await SecureStore.getItemAsync(ADDR, ADDR_OPTIONS).catch(() => null); +} + +/** + * Forget the key. The vault does not care — the address stays a member until governance removes + * it, which is the point of recovery existing. + */ +export async function forgetSigner(): Promise { + await SecureStore.deleteItemAsync(KEY, { + keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY, + }).catch(() => undefined); + await SecureStore.deleteItemAsync(ADDR, ADDR_OPTIONS).catch(() => undefined); +} diff --git a/mobile/src/lib/links.ts b/mobile/src/lib/links.ts new file mode 100644 index 0000000..9675c47 --- /dev/null +++ b/mobile/src/lib/links.ts @@ -0,0 +1,43 @@ +/** + * Deep links the app answers to. + * + * skur://vault/0x… open that vault + * skur://proposal/42 open that proposal in the currently open vault + * skur://vault/0x…/proposal/42 open the vault, then the proposal + * + * Parsing is a pure function so it can be tested like one. The navigation side lives in App.tsx, + * because a link can arrive before the navigator is mounted or before a vault is open, and what + * to do then is a navigation decision, not a parsing one. + */ +export type ParsedLink = + | { kind: 'vault'; vaultId: string; proposalId?: string } + | { kind: 'proposal'; proposalId: string }; + +const VAULT_ID = /^0x[0-9a-fA-F]{10,64}$/; +const PROPOSAL_ID = /^\d{1,18}$/; + +export function parseLink(url: string): ParsedLink | null { + let path: string; + try { + // skur://vault/0x1 parses with 'vault' as the host, not the path — normalise both shapes, + // and expo dev-client URLs (exp+skur://expo-development-client/…) fall through to null. + const u = new URL(url); + if (!/^(skur|exp\+skur):$/.test(u.protocol)) return null; + path = `${u.host}${u.pathname}`; + } catch { + return null; + } + const parts = path.split('/').filter(Boolean); + + if (parts[0] === 'vault' && parts[1] && VAULT_ID.test(parts[1])) { + if (parts[2] === 'proposal' && parts[3] && PROPOSAL_ID.test(parts[3])) { + return { kind: 'vault', vaultId: parts[1], proposalId: parts[3] }; + } + if (parts.length === 2) return { kind: 'vault', vaultId: parts[1] }; + return null; + } + if (parts[0] === 'proposal' && parts[1] && PROPOSAL_ID.test(parts[1]) && parts.length === 2) { + return { kind: 'proposal', proposalId: parts[1] }; + } + return null; +} diff --git a/mobile/src/lib/notifications.ts b/mobile/src/lib/notifications.ts new file mode 100644 index 0000000..cd16bea --- /dev/null +++ b/mobile/src/lib/notifications.ts @@ -0,0 +1,75 @@ +import Constants from 'expo-constants'; +import * as Linking from 'expo-linking'; +import * as Notifications from 'expo-notifications'; +import { useEffect } from 'react'; +import { API_URL } from './config'; + +/** + * Push, end to end: the indexer watches the chain, decides who a thing concerns, and sends it to + * Expo's push service; the device registers its token as a subscription; a tap on the banner opens + * the exact proposal, through the same deep-link path a pasted URL takes. + * + * Remote push needs an EAS project id — Expo's service will not mint a token without one — so + * everything here degrades honestly: no project id, no indexer, or no permission each produce a + * stated reason in Settings rather than a silent nothing. + */ +Notifications.setNotificationHandler({ + handleNotification: async (n) => { + const severity = (n.request.content.data as { severity?: string } | null)?.severity; + return { + // An alert interrupts; information waits in the tray. + shouldShowBanner: true, + shouldShowList: true, + shouldPlaySound: severity === 'alert', + shouldSetBadge: false, + }; + }, +}); + +function projectId(): string | null { + const extra = Constants.expoConfig?.extra as { eas?: { projectId?: string } } | undefined; + return extra?.eas?.projectId ?? Constants.easConfig?.projectId ?? null; +} + +export type PushResult = { ok: true; token: string } | { ok: false; reason: string }; + +export async function enablePush(vaultId: string, address: string): Promise { + if (!API_URL) { + return { ok: false, reason: 'Push needs the indexer. Set EXPO_PUBLIC_SKUR_API.' }; + } + if (!projectId()) { + return { + ok: false, + reason: 'Push needs an EAS project id in app.json (extra.eas.projectId). Local and dev builds without one cannot receive remote notifications.', + }; + } + const perm = await Notifications.requestPermissionsAsync(); + if (!perm.granted) { + return { ok: false, reason: 'Notifications are off for Skur in system settings.' }; + } + const token = (await Notifications.getExpoPushTokenAsync({ projectId: projectId()! })).data; + const res = await fetch(`${API_URL}/subscriptions`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ vaultId, address, channel: 'push', endpoint: token, events: [] }), + }); + if (!res.ok) return { ok: false, reason: `The indexer refused the registration (${res.status}).` }; + return { ok: true, token }; +} + +/** + * Tapping a notification routes through the same deep-link path as a pasted URL, including the + * cold start — the response that launched the app is read back, because the listener was not + * alive to hear it. + */ +export function useNotificationTaps() { + useEffect(() => { + const open = (response: Notifications.NotificationResponse | null) => { + const url = (response?.notification.request.content.data as { url?: string } | null)?.url; + if (url && url.startsWith('skur://')) void Linking.openURL(url); + }; + void Notifications.getLastNotificationResponseAsync().then(open); + const sub = Notifications.addNotificationResponseReceivedListener(open); + return () => sub.remove(); + }, []); +} diff --git a/mobile/src/lib/persist.ts b/mobile/src/lib/persist.ts new file mode 100644 index 0000000..03b4daa --- /dev/null +++ b/mobile/src/lib/persist.ts @@ -0,0 +1,31 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'; + +/** + * The query cache survives a restart, so opening the app offline shows the vault as it last was — + * with its age stated — instead of a spinner that will never resolve. + * + * The serializer is the part that matters: vault data is full of bigints, and a plain + * JSON.stringify throws on the first one. Persistence would then fail silently on exactly the + * data it exists for, which is the kind of bug that passes every demo and fails the first user + * with a balance. + */ +const TAG = '__skur_bigint__'; + +export function serialize(value: unknown): string { + return JSON.stringify(value, (_k, v) => (typeof v === 'bigint' ? { [TAG]: v.toString() } : v)); +} + +export function deserialize(text: string): unknown { + return JSON.parse(text, (_k, v) => + v && typeof v === 'object' && TAG in v ? BigInt((v as Record)[TAG]) : v, + ); +} + +export const persister = createAsyncStoragePersister({ + storage: AsyncStorage, + key: 'skur.querycache.v1', + serialize, + deserialize: deserialize as (s: string) => never, + throttleTime: 2_000, +}); diff --git a/mobile/src/lib/polyfills.ts b/mobile/src/lib/polyfills.ts new file mode 100644 index 0000000..b808839 --- /dev/null +++ b/mobile/src/lib/polyfills.ts @@ -0,0 +1,248 @@ +/** + * Must be imported before anything that touches crypto or BCS. + * + * Hermes is not a browser and not Node. Two things the Sui SDK assumes are missing or partial: + * a CSPRNG behind `crypto.getRandomValues`, which key generation needs, and the text codecs that + * BCS uses for every string it serialises. + */ +import 'react-native-get-random-values'; + +// Hermes ships TextEncoder and TextDecoder on recent React Native, but the version is not +// something this app should depend on — and a silent absence would surface much later as a +// corrupted transaction rather than a missing global. UTF-8 only, which is all BCS uses. +if (typeof globalThis.TextEncoder === 'undefined') { + class MinimalTextEncoder { + readonly encoding = 'utf-8'; + encode(input = ''): Uint8Array { + const bytes: number[] = []; + for (let i = 0; i < input.length; i++) { + let code = input.charCodeAt(i); + if (code >= 0xd800 && code <= 0xdbff && i + 1 < input.length) { + const low = input.charCodeAt(i + 1); + if (low >= 0xdc00 && low <= 0xdfff) { + code = ((code - 0xd800) << 10) + (low - 0xdc00) + 0x10000; + i++; + } + } + if (code < 0x80) bytes.push(code); + else if (code < 0x800) bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f)); + else if (code < 0x10000) + bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f)); + else + bytes.push( + 0xf0 | (code >> 18), + 0x80 | ((code >> 12) & 0x3f), + 0x80 | ((code >> 6) & 0x3f), + 0x80 | (code & 0x3f), + ); + } + return new Uint8Array(bytes); + } + } + globalThis.TextEncoder = MinimalTextEncoder as unknown as typeof TextEncoder; +} + +if (typeof globalThis.TextDecoder === 'undefined') { + class MinimalTextDecoder { + readonly encoding = 'utf-8'; + decode(input?: Uint8Array): string { + if (!input) return ''; + let out = ''; + for (let i = 0; i < input.length; ) { + const b = input[i++]; + if (b < 0x80) out += String.fromCharCode(b); + else if (b < 0xe0) out += String.fromCharCode(((b & 0x1f) << 6) | (input[i++] & 0x3f)); + else if (b < 0xf0) + out += String.fromCharCode( + ((b & 0x0f) << 12) | ((input[i++] & 0x3f) << 6) | (input[i++] & 0x3f), + ); + else { + const cp = + (((b & 0x07) << 18) | + ((input[i++] & 0x3f) << 12) | + ((input[i++] & 0x3f) << 6) | + (input[i++] & 0x3f)) - + 0x10000; + out += String.fromCharCode(0xd800 + (cp >> 10), 0xdc00 + (cp & 0x3ff)); + } + } + return out; + } + } + globalThis.TextDecoder = MinimalTextDecoder as unknown as typeof TextDecoder; +} + +/** + * Hermes ships a partial `Intl`, and `PluralRules` is one of the pieces it leaves out. + * + * This matters more than it sounds. The Sui SDK builds an ordinal formatter at **module scope** — + * `new Intl.PluralRules("en-US", { type: "ordinal" })`, used to say "1st command" in an error — + * so on Hermes the constructor is `undefined` and importing anything from the SDK's client throws + * `undefined cannot be used as a constructor` before a line of app code runs. There is no stack + * worth reading, because it happens while the module graph is still loading. + * + * English only, which is all the SDK asks for, and only installed when it is genuinely absent. + */ +if (typeof (globalThis as { Intl?: { PluralRules?: unknown } }).Intl?.PluralRules === 'undefined') { + class MinimalPluralRules { + #ordinal: boolean; + + constructor(_locales?: string | string[], options?: { type?: string }) { + this.#ordinal = options?.type === 'ordinal'; + } + + select(n: number): string { + if (!this.#ordinal) return n === 1 ? 'one' : 'other'; + // en-US ordinals: 1st, 2nd, 3rd, 4th … and the 11th/12th/13th exceptions. + const tens = Math.abs(n) % 100; + const units = Math.abs(n) % 10; + if (units === 1 && tens !== 11) return 'one'; + if (units === 2 && tens !== 12) return 'two'; + if (units === 3 && tens !== 13) return 'few'; + return 'other'; + } + + resolvedOptions() { + return { locale: 'en-US', type: this.#ordinal ? 'ordinal' : 'cardinal' }; + } + + static supportedLocalesOf(locales?: string | string[]): string[] { + return typeof locales === 'string' ? [locales] : (locales ?? []); + } + } + + const intl = ((globalThis as { Intl?: Record }).Intl ??= {} as Record); + intl.PluralRules = MinimalPluralRules; +} + +/** Fail loudly at startup rather than mid-signature. */ +export function assertCryptoReady() { + if (typeof globalThis.crypto?.getRandomValues !== 'function') { + throw new Error('crypto.getRandomValues is unavailable; react-native-get-random-values did not load'); + } + if (new TextEncoder().encode('ß').length !== 2) { + throw new Error('TextEncoder is not producing UTF-8'); + } + // The SDK builds an ordinal formatter at module scope, so this has to work before it is imported. + const ordinals = new Intl.PluralRules('en-US', { type: 'ordinal' }); + if (ordinals.select(1) !== 'one' || ordinals.select(11) !== 'other') { + throw new Error('Intl.PluralRules is not producing English ordinals'); + } +} + +/** + * Hermes has neither `structuredClone` (used by the Sui SDK while building a transaction — the + * resolver splices cloned commands) nor `Promise.withResolvers` (used by its gRPC transport). + * Without the first, every `tx.build()` dies with "undefined is not a function" *after* the user + * has already passed the biometric prompt, which reads like a signing failure and is not. + * + * The clone below covers what the SDK actually clones — plain objects, arrays, strings, numbers, + * bytes — not the full HTML algorithm. Uint8Array is the one typed case that matters: transaction + * arguments carry BCS bytes, and JSON round-tripping would quietly turn them into objects. + */ +if (typeof (globalThis as { structuredClone?: unknown }).structuredClone !== 'function') { + const clone = (value: unknown, seen: WeakMap): unknown => { + if (value === null || typeof value !== 'object') return value; + const hit = seen.get(value); + if (hit) return hit; + if (value instanceof Uint8Array) return new Uint8Array(value); + if (value instanceof ArrayBuffer) return value.slice(0); + if (value instanceof Date) return new Date(value.getTime()); + if (value instanceof Map) { + const out = new Map(); + seen.set(value, out); + for (const [k, v] of value) out.set(clone(k, seen), clone(v, seen)); + return out; + } + if (value instanceof Set) { + const out = new Set(); + seen.set(value, out); + for (const v of value) out.add(clone(v, seen)); + return out; + } + if (Array.isArray(value)) { + const out: unknown[] = []; + seen.set(value, out); + for (const v of value) out.push(clone(v, seen)); + return out; + } + const out: Record = {}; + seen.set(value, out); + for (const [k, v] of Object.entries(value)) out[k] = clone(v, seen); + return out; + }; + (globalThis as { structuredClone?: unknown }).structuredClone = (value: unknown) => + clone(value, new WeakMap()); +} + +if (typeof (Promise as { withResolvers?: unknown }).withResolvers !== 'function') { + (Promise as { withResolvers?: unknown }).withResolvers = function withResolvers() { + let resolve!: (value: T | PromiseLike) => void; + let reject!: (reason?: unknown) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; + }; +} + +/** + * Hermes (via React Native) ships `AbortController` but not the static helpers on `AbortSignal`. + * The Sui client's `waitForTransaction` builds its deadline with `AbortSignal.timeout` and merges + * it with the caller's signal via `AbortSignal.any` — so without these, a payment executes on + * chain and then the app throws while *waiting to report it*, which is the worst possible place + * to fail. + */ +type AbortSignalStatics = { + timeout?: (ms: number) => AbortSignal; + any?: (signals: Iterable) => AbortSignal; + abort?: (reason?: unknown) => AbortSignal; +}; +const signalStatics = AbortSignal as unknown as AbortSignalStatics; + +if (typeof signalStatics.timeout !== 'function') { + signalStatics.timeout = (ms: number) => { + const controller = new AbortController(); + setTimeout(() => { + controller.abort(new Error(`signal timed out after ${ms}ms`)); + }, ms); + return controller.signal; + }; +} + +if (typeof signalStatics.any !== 'function') { + signalStatics.any = (signals: Iterable) => { + const controller = new AbortController(); + for (const signal of signals) { + if (signal.aborted) { + controller.abort(signal.reason); + break; + } + signal.addEventListener('abort', () => controller.abort(signal.reason), { once: true }); + } + return controller.signal; + }; +} + +if (typeof signalStatics.abort !== 'function') { + signalStatics.abort = (reason?: unknown) => { + const controller = new AbortController(); + controller.abort(reason); + return controller.signal; + }; +} + +/** + * React Native's own `AbortSignal` predates `throwIfAborted`. The Sui client calls it on every + * poll of `waitForTransaction`. + */ +type AbortSignalWithThrow = AbortSignal & { throwIfAborted?: () => void }; +const signalProto = AbortSignal.prototype as AbortSignalWithThrow; +if (typeof signalProto.throwIfAborted !== 'function') { + signalProto.throwIfAborted = function throwIfAborted(this: AbortSignal) { + if (this.aborted) { + throw (this as { reason?: unknown }).reason ?? new Error('the operation was aborted'); + } + }; +} diff --git a/mobile/src/lib/roles.ts b/mobile/src/lib/roles.ts new file mode 100644 index 0000000..b0d374f --- /dev/null +++ b/mobile/src/lib/roles.ts @@ -0,0 +1,64 @@ +import { Role } from '@skur/sdk'; + +/** + * The roles the app talks about, mapped to the bits the contract enforces. + * + * The contract's vocabulary is capabilities (owner, approver, executor, proposer, guardian); the + * product's vocabulary is job titles. This file is the only place the two meet, so a title can + * never quietly drift from what the key can actually do. + */ +export type RoleId = 'owner' | 'admin' | 'signer' | 'guardian' | 'viewer'; + +export type RoleSpec = { + id: RoleId; + title: string; + /** What this role may do, in the words the Roles & Permissions screen shows. */ + grants: string; + bits: number; +}; + +export const ROLE_SPECS: RoleSpec[] = [ + { + id: 'owner', + title: 'Owner', + grants: 'Full access to all features: governance, approvals, execution and policy.', + bits: Role.OWNER | Role.APPROVER | Role.EXECUTOR, + }, + { + id: 'admin', + title: 'Admin', + grants: 'Manage day-to-day money movement: approve and execute payments.', + bits: Role.APPROVER | Role.EXECUTOR, + }, + { + id: 'signer', + title: 'Signer', + grants: 'Approve transactions and proposals. Cannot execute or change policy.', + bits: Role.APPROVER, + }, + { + id: 'guardian', + title: 'Guardian', + grants: 'An independent brake: veto critical payments, freeze the vault, confirm recovery. Holds no treasury role by design.', + bits: Role.GUARDIAN, + }, + { + id: 'viewer', + title: 'Viewer', + grants: 'View-only access. A vault is a shared object, so viewers need no on-chain role at all.', + bits: 0, + }, +]; + +export const roleSpec = (id: RoleId): RoleSpec => ROLE_SPECS.find((r) => r.id === id)!; + +/** The closest title for a member's bits, for badges on the team list. */ +export function titleForBits(bits: number): string { + if (bits & Role.GUARDIAN) return 'Guardian'; + if (bits & Role.OWNER) return 'Owner'; + if ((bits & Role.APPROVER) !== 0 && (bits & Role.EXECUTOR) !== 0) return 'Admin'; + if (bits & Role.APPROVER) return 'Signer'; + if (bits & Role.EXECUTOR) return 'Executor'; + if (bits & Role.PROPOSER) return 'Agent'; + return 'Viewer'; +} diff --git a/mobile/src/screens/ActivityLog.tsx b/mobile/src/screens/ActivityLog.tsx new file mode 100644 index 0000000..5e87490 --- /dev/null +++ b/mobile/src/screens/ActivityLog.tsx @@ -0,0 +1,143 @@ +import { useMemo, useState } from 'react'; +import { RefreshControl, View } from 'react-native'; +import { KIND_LABELS, Kind, Status, fmtAmount, fmtRelative, short, type Proposal } from '@skur/sdk'; +import { + BackButton, + Card, + CircleIcon, + Empty, + ErrorState, + Row, + Screen, + Skeleton, + Tabs, + TopBar, + type IconName, +} from '../components/ui'; +import { coinDecimals, coinSymbol } from '../components/TokenMark'; +import { useTheme } from '../state/theme'; +import { useVaultView } from '../hooks/useVault'; +import { useStore } from '../state/store'; + +type Filter = 'all' | 'approvals' | 'system'; +type Entry = { key: string; icon: IconName; tone: 'ok' | 'warn' | 'bad' | 'info' | 'dark'; title: string; sub: string; at: number; kind: Filter }; + +/** + * Screen 19 — the audit trail, derived from the proposals the chain records. Approvals are listed + * by signer; "system" is everything that changes the vault itself rather than moving money. + */ +export function ActivityLog({ onBack }: { onBack: () => void }) { + const C = useTheme(); + const { vaultId } = useStore(); + const q = useVaultView(vaultId); + const [filter, setFilter] = useState('all'); + + const entries = useMemo(() => { + const out: Entry[] = []; + const system = new Set([Kind.POLICY_UPDATE, Kind.MEMBER_SET, Kind.ASSET_LIMITS, Kind.MODE_RELAX, Kind.RECOVERY, Kind.RECIPIENT_TRUST]); + for (const p of (q.data?.proposals ?? []) as Proposal[]) { + const isSystem = system.has(p.kind); + const what = + p.kind === Kind.TRANSFER && p.asset + ? `${fmtAmount(p.amount, coinDecimals(p.asset), coinSymbol(p.asset))}` + : KIND_LABELS[p.kind]; + out.push({ + key: `${p.id}-open`, + icon: isSystem ? 'settings' : 'arrow-up-right', + tone: 'info', + title: `${KIND_LABELS[p.kind]} opened`, + sub: `#${p.id} · ${what} · by ${short(p.proposer)} · ${fmtRelative(p.createdAt)}`, + at: p.createdAt, + kind: isSystem ? 'system' : 'all', + }); + for (const a of p.approvals) { + out.push({ + key: `${p.id}-ap-${a}`, + icon: 'check', + tone: 'ok', + title: `Approved by ${short(a)}`, + sub: `#${p.id} · ${what}`, + at: p.createdAt + 1, + kind: 'approvals', + }); + } + for (const r of p.rejections) { + out.push({ + key: `${p.id}-rj-${r}`, + icon: 'x', + tone: 'warn', + title: `Rejected by ${short(r)}`, + sub: `#${p.id} · ${what}`, + at: p.createdAt + 1, + kind: 'approvals', + }); + } + for (const g of p.confirmations) { + out.push({ + key: `${p.id}-cf-${g}`, + icon: 'shield', + tone: 'info', + title: `Guardian confirmed · ${short(g)}`, + sub: `#${p.id} · ${what}`, + at: p.createdAt + 1, + kind: 'approvals', + }); + } + if (p.status !== Status.PENDING) { + out.push({ + key: `${p.id}-end`, + icon: p.status === Status.EXECUTED ? 'check-circle' : p.status === Status.VETOED ? 'slash' : 'x-circle', + tone: p.status === Status.EXECUTED ? 'ok' : p.status === Status.VETOED ? 'bad' : 'warn', + title: + p.status === Status.EXECUTED + ? `${KIND_LABELS[p.kind]} executed` + : p.status === Status.VETOED + ? `${KIND_LABELS[p.kind]} vetoed` + : `${KIND_LABELS[p.kind]} closed`, + sub: `#${p.id} · ${what}`, + at: p.createdAt + 2, + kind: isSystem ? 'system' : 'all', + }); + } + } + return out.sort((a, b) => b.at - a.at); + }, [q.data]); + + const visible = entries.filter((e) => filter === 'all' || e.kind === filter); + + return ( + } title="Activity Logs" />} + refreshControl={ q.refetch()} tintColor={C.text2} />} + > + + + {q.isLoading ? : null} + {q.error ? q.refetch()} /> : null} + {!q.isLoading && visible.length === 0 ? ( + Nothing in this view yet. + ) : ( + + {visible.map((e, i) => ( + } + title={e.title} + subtitle={e.sub} + last={i === visible.length - 1} + /> + ))} + + )} + + + ); +} diff --git a/mobile/src/screens/Dashboard.tsx b/mobile/src/screens/Dashboard.tsx new file mode 100644 index 0000000..17c2cb5 --- /dev/null +++ b/mobile/src/screens/Dashboard.tsx @@ -0,0 +1,191 @@ +import { RefreshControl, Text, View } from 'react-native'; +import { KIND_LABELS, Kind, Mode, Status, fmtAmount, fmtRelative, type Proposal } from '@skur/sdk'; +import { + Card, + Empty, + ErrorState, + IconButton, + ModeBadge, + Notice, + Row, + Screen, + SectionLabel, + Skeleton, + StatusBadge, + Tape, + TierBadge, + TopBar, +} from '../components/ui'; +import { Stat } from '../components/kit'; +import { Identicon } from '../components/Identicon'; +import { TokenMark, coinDecimals, coinSymbol } from '../components/TokenMark'; +import { useTheme } from '../state/theme'; +import { F } from '../theme'; +import { useVaultView } from '../hooks/useVault'; +import { useStore } from '../state/store'; + +/** + * Screen 8 — the dashboard. The treasury's value, what is waiting, who is on it, and what just + * happened. No fiat figures: this app has no price oracle, and a made-up dollar number on a + * treasury screen is worse than none. + */ +export function Dashboard({ + onOpenProposals, + onOpenProposal, + onOpenTreasury, + onSwitch, +}: { + onOpenProposals: () => void; + onOpenProposal: (id: bigint) => void; + onOpenTreasury: () => void; + onSwitch: () => void; +}) { + const C = useTheme(); + const { vaultId } = useStore(); + const q = useVaultView(vaultId); + + if (q.isLoading) { + return ( + }> + + + ); + } + if (q.error || !q.data) { + const message = q.error instanceof Error ? q.error.message : ''; + const badId = /not found|deleted|invalid|parse/i.test(message); + return ( + } />}> + q.refetch()} + /> + + ); + } + + const v = q.data; + const pending = v.proposals.filter((x) => x.status === Status.PENDING); + const recent = [...v.proposals].sort((a, b) => b.createdAt - a.createdAt).slice(0, 3); + const primary = [...v.assets].sort((a, b) => + Number(b.balance / 10n ** BigInt(coinDecimals(b.coinType))) - Number(a.balance / 10n ** BigInt(coinDecimals(a.coinType))), + )[0]; + + const line = (p: Proposal) => + p.kind === Kind.TRANSFER && p.asset + ? fmtAmount(p.amount, coinDecimals(p.asset), coinSymbol(p.asset)) + : KIND_LABELS[p.kind]; + + return ( + } + title={v.vault.name} + right={} + /> + } + refreshControl={ + q.refetch()} tintColor={C.text2} /> + } + > + + {v.vault.mode !== Mode.NORMAL ? : null} + {v.vault.mode === Mode.LOCKDOWN ? ( + This vault is frozen. Nothing outgoing executes. Deposits still work. + ) : v.vault.mode === Mode.ELEVATED ? ( + Elevated: every payment is escalated one tier and the caps are halved. + ) : null} + + + + Total treasury value + + + + {primary ? fmtAmount(primary.balance, coinDecimals(primary.coinType)) : '0'} + + {' '} + {primary ? coinSymbol(primary.coinType) : ''} + + + + {v.assets.length === 1 + ? 'one asset approved' + : `across ${v.assets.length} assets — see Treasury for the breakdown`} + + + + + {v.assets.slice(0, 2).map((a) => ( + + ))} + + + + + + + + + + Recent activity + + View all + + + {recent.length === 0 ? ( + Nothing has happened yet. Fund the vault, or open the first payment. + ) : ( + + {recent.map((p, i) => ( + + ) : ( + + ) + } + title={line(p)} + subtitle={`${p.memo || KIND_LABELS[p.kind]} · ${fmtRelative(p.createdAt)}`} + trailing={ + p.status === Status.PENDING && p.kind === Kind.TRANSFER ? ( + + ) : ( + + ) + } + onPress={() => onOpenProposal(p.id)} + chevron + last={i === recent.length - 1} + /> + ))} + + )} + + + + } + title="Treasury" + subtitle="Balances, breakdown, send and request" + onPress={onOpenTreasury} + chevron + last + /> + + + + ); +} diff --git a/mobile/src/screens/Help.tsx b/mobile/src/screens/Help.tsx new file mode 100644 index 0000000..ec41547 --- /dev/null +++ b/mobile/src/screens/Help.tsx @@ -0,0 +1,92 @@ +import { useState } from 'react'; +import { Linking, Text, View } from 'react-native'; +import { BackButton, Button, Card, CircleIcon, Expandable, Row, Screen, SectionLabel, TopBar } from '../components/ui'; +import { useTheme } from '../state/theme'; +import { F } from '../theme'; + +const SUPPORT_EMAIL = 'support@skur.app'; +const DOCS_URL = 'https://github.com/Emmyhack/skur#readme'; +const ISSUES_URL = 'https://github.com/Emmyhack/skur/issues/new'; + +const FAQS: [string, string][] = [ + [ + 'What happens if I lose my phone?', + 'The key is gone with it — it never leaves the secure element. Your address is still a member of the vault, and guardian recovery can move your roles to a new key after a delay any owner can cancel. The treasury is never at the mercy of one device.', + ], + [ + 'Why does a payment sometimes wait?', + 'The vault scores every payment by amount, destination and today’s outflow. High-risk and critical payments carry a timelock so a theft in progress can be vetoed before it settles.', + ], + [ + 'Can the app move money on its own?', + 'No. Every rule is enforced by a Move object on Sui. The app proposes, approves and executes only what the vault itself permits — and only after the OS releases the key behind your biometric.', + ], + [ + 'Who can see the vault?', + 'Anyone. A vault is a shared object; balances, policy and proposals are public reads. What is gated is money leaving, never reading.', + ], +]; + +/** Screen 21 — help: answers first, humans second. */ +export function Help({ onBack }: { onBack: () => void }) { + const C = useTheme(); + const [open, setOpen] = useState(0); + return ( + } title="Help & Support" />} + footer={ + + } + > + + + + Need assistance? + We're here to help. + + + + FAQs · Common questions + {FAQS.map(([question, answer], i) => ( + setOpen(open === i ? null : i)} + > + {answer} + + ))} + + + + } + title="Documentation" + subtitle="Learn more about Skur" + onPress={() => void Linking.openURL(DOCS_URL)} + chevron + /> + } + title="Contact Support" + subtitle={SUPPORT_EMAIL} + onPress={() => void Linking.openURL(`mailto:${SUPPORT_EMAIL}`)} + chevron + /> + } + title="Report an Issue" + subtitle="Bug or feedback" + onPress={() => void Linking.openURL(ISSUES_URL)} + chevron + last + /> + + + + ); +} diff --git a/mobile/src/screens/Home.tsx b/mobile/src/screens/Home.tsx new file mode 100644 index 0000000..731a5b9 --- /dev/null +++ b/mobile/src/screens/Home.tsx @@ -0,0 +1,202 @@ +import { RefreshControl, Text, View } from 'react-native'; +import { Mode, describeReasons, fmtAmount, fmtDuration, type VaultView } from '@skur/sdk'; +import { + Address, + Button, + Card, + Empty, + ErrorState, + IconButton, + KV, + Notice, + Row, + Screen, + SectionLabel, + Skeleton, + Tape, + TopBar, +} from '../components/ui'; +import { BarChart, Donut, PortfolioBar } from '../components/kit'; +import { TokenMark, coinDecimals, coinSymbol } from '../components/TokenMark'; +import { useTheme } from '../state/theme'; +import { F } from '../theme'; +import { useVaultView } from '../hooks/useVault'; +import { useStore } from '../state/store'; + +/** + * Screen 9 — the treasury overview: total balance, the portfolio breakdown, send/request, and + * what the vault enforces. Shares are by normalised units, not price — this app carries no + * oracle, and a breakdown that silently guessed at dollars would be a lie with a legend. + */ +export function Home({ + onOpenQueue, + onSwitch, + onSend, + onRequest, +}: { + onOpenQueue?: () => void; + onSwitch: () => void; + onSend?: () => void; + onRequest?: () => void; +}) { + const C = useTheme(); + const { vaultId } = useStore(); + const q = useVaultView(vaultId); + + if (q.isLoading) { + return ( + }> + + + ); + } + if (q.error || !q.data) { + const message = q.error instanceof Error ? q.error.message : ''; + const badId = /not found|deleted|invalid|parse/i.test(message); + return ( + } />}> + q.refetch()} + /> + + ); + } + + const v: VaultView = q.data; + const p = v.vault.policy; + + const palette = [C.accent, C.info, C.success, C.warning, C.error]; + const normalized = v.assets.map((a, i) => ({ + label: coinSymbol(a.coinType), + value: Number(a.balance) / 10 ** coinDecimals(a.coinType), + color: palette[i % palette.length], + })); + const primary = [...v.assets].sort( + (a, b) => Number(b.balance) / 10 ** coinDecimals(b.coinType) - Number(a.balance) / 10 ** coinDecimals(a.coinType), + )[0]; + + return ( + } />} + refreshControl={ + q.refetch()} tintColor={C.text2} /> + } + > + + {v.vault.mode !== Mode.NORMAL ? : null} + + {v.vault.mode === Mode.LOCKDOWN ? ( + + This vault is frozen. Nothing outgoing executes and no payment can be opened. Deposits + still work. Reopening needs {p.governanceThreshold} owner + {p.governanceThreshold === 1 ? '' : 's'}, {p.guardianThreshold} guardian + {p.guardianThreshold === 1 ? '' : 's'} and {fmtDuration(p.policyChangeDelay)}. + + ) : v.vault.mode === Mode.ELEVATED ? ( + + Elevated. Every payment is escalated one tier and the caps are halved. + {v.vault.postureReasons ? ` ${describeReasons(v.vault.postureReasons).join('; ')}.` : ''} + + ) : null} + + + Total balance + + + + {normalized.length > 1 ? ( + + + + ) : null} + + + + + + + + + Holdings + {v.assets.length === 0 ? ( + No asset is approved for this vault yet. + ) : ( + + {v.assets.map((a, i) => { + const d = coinDecimals(a.coinType); + const daily = a.limits.dailyMax; + const spent = a.velocity?.daySpent ?? 0n; + return ( + } + title={coinSymbol(a.coinType)} + subtitle={ + daily === 0n + ? 'no daily cap' + : `${fmtAmount(spent, d)} of ${fmtAmount(daily, d)} used today` + } + trailing={ + + {fmtAmount(a.balance, d)} + + } + last={i === v.assets.length - 1} + /> + ); + })} + + )} + + + {normalized.length > 0 ? ( + + Asset breakdown + ({ label: n.label, value: n.value }))} /> + + ) : null} + + + What this vault enforces + + + + + + + + {onOpenQueue ? ( + + + + ) : null} + + + This vault +
+ + + + ); +} diff --git a/mobile/src/screens/More.tsx b/mobile/src/screens/More.tsx new file mode 100644 index 0000000..fa6f539 --- /dev/null +++ b/mobile/src/screens/More.tsx @@ -0,0 +1,64 @@ +import { View } from 'react-native'; +import { Card, CircleIcon, Row, Screen, TopBar } from '../components/ui'; + +/** The fifth tab: everything that is not money-in-motion, one tap deep. */ +export function More({ + onOpen, +}: { + onOpen: (screen: 'Security' | 'Policies' | 'Logs' | 'Receive' | 'Settings' | 'Help') => void; +}) { + return ( + }> + + + } + title="Security Center" + subtitle="Posture, the brake, maximum possible loss" + onPress={() => onOpen('Security')} + chevron + /> + } + title="Spending Policies" + subtitle="The rules every payment is scored against" + onPress={() => onOpen('Policies')} + chevron + /> + } + title="Activity Logs" + subtitle="Everything that happened, by whom" + onPress={() => onOpen('Logs')} + chevron + /> + } + title="Receive" + subtitle="The vault's address and accepted assets" + onPress={() => onOpen('Receive')} + chevron + last + /> + + + } + title="Settings" + subtitle="Profile, wallet, appearance, notifications" + onPress={() => onOpen('Settings')} + chevron + /> + } + title="Help & Support" + subtitle="FAQs, documentation, contact" + onPress={() => onOpen('Help')} + chevron + last + /> + + + + ); +} diff --git a/mobile/src/screens/OpenVault.tsx b/mobile/src/screens/OpenVault.tsx new file mode 100644 index 0000000..60471ce --- /dev/null +++ b/mobile/src/screens/OpenVault.tsx @@ -0,0 +1,102 @@ +import { useState } from 'react'; +import { RefreshControl, Text, View } from 'react-native'; +import { useQuery } from '@tanstack/react-query'; +import { MODE_LABELS, describeRoles } from '@skur/sdk'; +import { Address, Button, Card, Empty, Field, Input, Notice, Row, Screen, SectionLabel, TopBar } from '../components/ui'; +import { Identicon } from '../components/Identicon'; +import { useTheme } from '../state/theme'; +import { F } from '../theme'; +import { API_URL, CONFIGURED, NETWORK } from '../lib/config'; +import { useStore } from '../state/store'; + +/** Vaults the device's key holds a role in, via the indexer when one is configured. */ +function useMyVaults(address: string | null) { + return useQuery<{ vault_id: string; name: string; roles: number; mode: number }[]>({ + queryKey: ['my-vaults', NETWORK, address], + enabled: Boolean(address && API_URL), + retry: 1, + queryFn: async () => { + const res = await fetch(`${API_URL}/members/${address}/vaults`); + if (!res.ok) throw new Error(`the indexer returned ${res.status}`); + return (await res.json()).vaults; + }, + }); +} + +export function OpenVault() { + const C = useTheme(); + const { address, openVault } = useStore(); + const [manual, setManual] = useState(''); + const mine = useMyVaults(address); + // Exactly a normalised Sui object id. The looser shapes this used to accept — an EVM address + // is 40 hex chars — all name vaults that cannot exist on a Sui network. + const valid = /^0x[0-9a-fA-F]{64}$/.test(manual.trim()); + + return ( + } + refreshControl={ mine.refetch()} tintColor={C.text2} />} + > + + {!CONFIGURED ? ( + + No package is configured for {NETWORK}. Publish the Move package and set + EXPO_PUBLIC_SKUR_PACKAGE_ID. + + ) : null} + + + Yours + {!API_URL ? ( + + + Finding your vaults by address needs the indexer. Set EXPO_PUBLIC_SKUR_API, or open + a vault by its id below — reading one needs nothing but the chain. + + + ) : mine.isLoading ? ( + Looking… + ) : mine.error ? ( + The indexer is not reachable. You can still open a vault by id. + ) : mine.data && mine.data.length > 0 ? ( + + {mine.data.map((v, i) => ( + } + title={v.name} + subtitle={`${describeRoles(v.roles).join(' · ') || 'no role'} · ${MODE_LABELS[v.mode as 0 | 1 | 2]}`} + onPress={() => openVault(v.vault_id)} + chevron + last={i === mine.data!.length - 1} + /> + ))} + + ) : ( + + No vault on {NETWORK} lists this device's key as a signer yet. + {address ? '\n\nSend your address to an owner so they can add it.' : ''} + + )} + + + {address ? ( + + This device signs as +
+ + ) : null} + + + By id + + + + + + + + ); +} diff --git a/mobile/src/screens/Policies.tsx b/mobile/src/screens/Policies.tsx new file mode 100644 index 0000000..5bc5281 --- /dev/null +++ b/mobile/src/screens/Policies.tsx @@ -0,0 +1,200 @@ +import { useState } from 'react'; +import { Text, View } from 'react-native'; +import { Role, fmtAmount, fmtDuration, hasRole, tx as build, type Policy } from '@skur/sdk'; +import { + BackButton, + Button, + Card, + Field, + Input, + Notice, + Screen, + SectionLabel, + Sheet, + Skeleton, + TopBar, + TxStatus, +} from '../components/ui'; +import { CheckItem } from '../components/kit'; +import { CircleIcon, type IconName } from '../components/ui'; + +/** One policy band, drawn as the reference draws it: an icon chip, the band, what it demands. */ +function RuleCard({ icon, tone, title, detail }: { icon: IconName; tone: 'ok' | 'warn' | 'info' | 'dark'; title: string; detail: string }) { + const C = useTheme(); + return ( + + + + + {title} + {detail} + + + + ); +} +import { coinDecimals, coinSymbol } from '../components/TokenMark'; +import { useTheme } from '../state/theme'; +import { F } from '../theme'; +import { PACKAGE_ID } from '../lib/config'; +import { useTx } from '../hooks/useTx'; +import { useVaultView } from '../hooks/useVault'; +import { useStore } from '../state/store'; + +/** + * Screen 17 — the spending policy, shown as the rules it actually is: which band an amount falls + * into, what each band demands, and what unknown recipients wait. Editing routes through + * governance like every other change — the phone proposes, it never decrees. + */ +export function Policies({ onBack }: { onBack: () => void }) { + const C = useTheme(); + const { vaultId, address } = useStore(); + const q = useVaultView(vaultId); + const tx = useTx(vaultId); + const [editing, setEditing] = useState(false); + const [low, setLow] = useState(''); + const [high, setHigh] = useState(''); + const [critical, setCritical] = useState(''); + + if (q.isLoading || !q.data) { + return ( + } title="Spending Policies" />}> + + + ); + } + + const p = q.data.vault.policy; + const asset = q.data.assets[0]; + const d = asset ? coinDecimals(asset.coinType) : 9; + const sym = asset ? coinSymbol(asset.coinType) : ''; + const myRoles = address ? (q.data.members.find((m) => m.address === address)?.roles ?? 0) : 0; + const isOwner = hasRole(myRoles, Role.OWNER); + + const parsed = { + low: low === '' ? p.approvalsLow : Number(low), + high: high === '' ? p.approvalsHigh : Number(high), + critical: critical === '' ? p.approvalsCritical : Number(critical), + }; + const sane = + [parsed.low, parsed.high, parsed.critical].every((n) => Number.isInteger(n) && n >= 1 && n <= 20) && + parsed.low <= parsed.high && + parsed.high <= parsed.critical; + const changed = parsed.low !== p.approvalsLow || parsed.high !== p.approvalsHigh || parsed.critical !== p.approvalsCritical; + + const propose = () => { + const next: Policy = { ...p, approvalsLow: parsed.low, approvalsHigh: parsed.high, approvalsCritical: parsed.critical }; + void tx + .run(() => build.proposePolicy(PACKAGE_ID, { vaultId: vaultId!, policy: next }), 'Propose this policy change') + .then((digest) => { + if (digest) setEditing(false); + }); + }; + + return ( + } title="Spending Policies" />} + footer={ + isOwner ? ( + + + + + ) : null + } + > + + Policy rules + + + + + + + + + {asset ? ( + + Per-asset caps · {sym} + + + + ) : null} + + + These rules are enforced by the vault object itself, not by this app. Changing them is a + governance proposal: it collects {p.governanceThreshold} owner approval + {p.governanceThreshold === 1 ? '' : 's'}, waits {fmtDuration(p.policyChangeDelay)}, and any + guardian can veto a change that weakens the vault. + + + + setEditing(false)} title="Edit policies"> + + + + + + + + + + + {!sane ? ( + Each band must need at least one approval, and never fewer than the band below it. + ) : null} + + + Everything not shown here carries over unchanged. Loosening any rule waits{' '} + {fmtDuration(p.policyChangeDelay)} and can be vetoed. + + + + + ); +} diff --git a/mobile/src/screens/Receive.tsx b/mobile/src/screens/Receive.tsx new file mode 100644 index 0000000..34fa9b4 --- /dev/null +++ b/mobile/src/screens/Receive.tsx @@ -0,0 +1,64 @@ +import { Text, View } from 'react-native'; +import { fmtAmount } from '@skur/sdk'; +import { Address, Card, Empty, Notice, Row, Screen, SectionLabel, TopBar } from '../components/ui'; +import { TokenMark, coinDecimals, coinSymbol } from '../components/TokenMark'; +import { Identicon } from '../components/Identicon'; +import { useTheme } from '../state/theme'; +import { F } from '../theme'; +import { useVaultView } from '../hooks/useVault'; +import { useStore } from '../state/store'; + +export function Receive() { + const C = useTheme(); + const { vaultId } = useStore(); + const q = useVaultView(vaultId); + + return ( + }> + + + + + {q.data?.vault.name ?? 'Treasury'} + + + + + Vault address +
+ + + + Deposits are never gated — in any mode, from any address. The controls exist to govern + money leaving; refusing money arriving would only strand it. + + + + Assets this vault accepts + {(q.data?.assets ?? []).filter((a) => a.limits.approved).length === 0 ? ( + No asset is approved yet. + ) : ( + + {q.data!.assets + .filter((a) => a.limits.approved) + .map((a, i, arr) => ( + } + title={coinSymbol(a.coinType)} + subtitle={`holding ${fmtAmount(a.balance, coinDecimals(a.coinType))}`} + last={i === arr.length - 1} + /> + ))} + + )} + + + + Sending a coin type this vault has not approved will fail. Approving one is a governance + action. + + + + ); +} diff --git a/mobile/src/screens/Roles.tsx b/mobile/src/screens/Roles.tsx new file mode 100644 index 0000000..087577f --- /dev/null +++ b/mobile/src/screens/Roles.tsx @@ -0,0 +1,37 @@ +import { Text, View } from 'react-native'; +import { BackButton, Badge, Card, Screen, TopBar } from '../components/ui'; +import { CircleIcon } from '../components/ui'; +import { ROLE_SPECS } from '../lib/roles'; +import { useTheme } from '../state/theme'; +import { F } from '../theme'; + +const ICONS = { owner: 'key', admin: 'sliders', signer: 'edit-3', guardian: 'shield', viewer: 'eye' } as const; + +/** + * Screen 16 — what each role may do. These map one-to-one onto the capabilities the contract + * enforces, so nothing promised here can drift from what a key can actually sign. + */ +export function Roles({ onBack }: { onBack: () => void }) { + const C = useTheme(); + return ( + } title="Roles & Permissions" />}> + + {ROLE_SPECS.map((r) => ( + + + + {r.title} + {r.id === 'guardian' ? Separate plane : null} + + {r.grants} + + ))} + + Each role has specific permissions to ensure security and control. A guardian deliberately + holds no treasury role: its whole value is being a brake an attacker has to breach + separately, so the contract refuses to combine it with any of the others. + + + + ); +} diff --git a/mobile/src/screens/Security.tsx b/mobile/src/screens/Security.tsx new file mode 100644 index 0000000..fa65a85 --- /dev/null +++ b/mobile/src/screens/Security.tsx @@ -0,0 +1,202 @@ +import { useMemo } from 'react'; +import { Text, View } from 'react-native'; +import { + Mode, + Role, + computeMaxLoss, + fmtAmount, + fmtDuration, + hasRole, + postureItems, + tx as build, +} from '@skur/sdk'; +import { + Button, + Card, + CircleIcon, + Icon, + KV, + Notice, + Screen, + SectionLabel, + TopBar, + TxStatus, +} from '../components/ui'; +import { Meter } from '../components/kit'; +import { coinDecimals, coinSymbol } from '../components/TokenMark'; +import { useTheme } from '../state/theme'; +import { F } from '../theme'; +import { PACKAGE_ID } from '../lib/config'; +import { useTx } from '../hooks/useTx'; +import { useVaultView } from '../hooks/useVault'; +import { useStore } from '../state/store'; + +export function Security() { + const C = useTheme(); + const { vaultId, address } = useStore(); + const q = useVaultView(vaultId); + const tx = useTx(vaultId); + + const roles = address ? (q.data?.members.find((m) => m.address === address)?.roles ?? 0) : 0; + const canRaise = hasRole(roles, Role.GUARDIAN) || hasRole(roles, Role.OWNER); + + const posture = useMemo( + () => + q.data + ? postureItems({ + policy: q.data.vault.policy, + guardians: q.data.vault.guardianCount, + assets: q.data.assets.map((a) => ({ symbol: coinSymbol(a.coinType), limits: a.limits })), + }) + : [], + [q.data], + ); + + if (!q.data) { + return ( + }> + Reading… + + ); + } + + const v = q.data; + const p = v.vault.policy; + const holding = posture.filter((i) => i.ok === 'ok').length; + const allOk = holding === posture.length; + + return ( + } + footer={tx.state.phase !== 'idle' ? : null} + > + + + + + + {allOk ? 'Your assets are protected' : 'Posture has gaps'} + + + Enforced by the vault object, not by this app. + + + + + + What holds + + {posture.map((item, i) => ( + + + + + + {item.text} + + + ))} + + + Confirmed controls and obvious gaps. This is not a score and does not claim to measure + absolute security. + + + + + In an incident + {canRaise ? ( + + + + + Freezing is immediate and stops all outgoing execution. Deposits keep working. + Reopening needs {p.governanceThreshold} owner + {p.governanceThreshold === 1 ? '' : 's'}, {p.guardianThreshold} guardian + {p.guardianThreshold === 1 ? '' : 's'} and {fmtDuration(p.policyChangeDelay)} — + whoever froze it cannot lift it alone. + + + ) : ( + + Raising the posture takes an owner or a guardian. This device's key holds + neither. + + )} + + + + Security posture + + + {allOk ? 'Strong' : holding >= posture.length - 1 ? 'Good' : 'Needs attention'} + + + {holding}/{posture.length} controls + + + + + Counted from the checks above — not a score of absolute security, which nothing can + honestly claim to measure. + + + + + Maximum possible loss + {v.assets.map((a) => { + const r = computeMaxLoss(p, a.limits, a.balance, v.vault.mode); + const d = coinDecimals(a.coinType); + return ( + + + {coinSymbol(a.coinType)} + + + + + + ); + })} + + Assumes every signer a tier needs is compromised and cooperating, and that no guardian + acts. An upper bound under those assumptions, never a guarantee. A 24-hour bucket + anchors on its first payment, so up to two days' allowance can leave across a + boundary. + + + + + ); +} diff --git a/mobile/src/screens/Send.tsx b/mobile/src/screens/Send.tsx new file mode 100644 index 0000000..48fb73b --- /dev/null +++ b/mobile/src/screens/Send.tsx @@ -0,0 +1,277 @@ +import { useMemo, useState } from 'react'; +import { Text, View } from 'react-native'; +import { + Trust, + canPropose, + describeReasons, + fmtAmount, + fmtDuration, + parseAmount, + tx as build, +} from '@skur/sdk'; +import { + Address, + Button, + Card, + Field, + Input, + KV, + Notice, + Screen, + SectionLabel, + TierBadge, + TopBar, + TrustBadge, + TxStatus, +} from '../components/ui'; +import { Segmented } from '../components/kit'; +import { ScanSheet } from '../components/Scan'; +import { IconButton } from '../components/ui'; +import { coinDecimals, coinSymbol } from '../components/TokenMark'; +import { useTheme } from '../state/theme'; +import { F } from '../theme'; +import { PACKAGE_ID } from '../lib/config'; +import { useTransferPreview, useVaultView } from '../hooks/useVault'; +import { useTx } from '../hooks/useTx'; +import { useStore } from '../state/store'; + +/** + * Opening a payment. + * + * The review is the vault's own classification, simulated live. The phone does not compute it, so + * it cannot promise something the vault would refuse — which is the point of `preview_transfer` + * being the same code `execute_transfer` runs. + */ +export function Send({ onDone }: { onDone: () => void }) { + const C = useTheme(); + const { vaultId, address, book } = useStore(); + const q = useVaultView(vaultId); + const tx = useTx(vaultId); + + const approved = (q.data?.assets ?? []).filter((a) => a.limits.approved); + const [direction, setDirection] = useState<'send' | 'request'>('send'); + const [coinType, setCoinType] = useState(''); + const [to, setTo] = useState(''); + const [amountText, setAmountText] = useState(''); + const [memo, setMemo] = useState(''); + const [scanning, setScanning] = useState(false); + + const asset = approved.find((a) => a.coinType === coinType) ?? approved[0]; + const activeType = asset?.coinType ?? ''; + const decimals = activeType ? coinDecimals(activeType) : 9; + const amount = useMemo(() => parseAmount(amountText, decimals) ?? 0n, [amountText, decimals]); + const validTo = /^0x[0-9a-fA-F]{1,64}$/.test(to); + + const known = q.data?.recipients.find((r) => r.address === to); + const labelled = book.find((b) => b.address === to); + const roles = address ? (q.data?.members.find((m) => m.address === address)?.roles ?? 0) : 0; + + const preview = useTransferPreview({ + vaultId, + coinType: activeType, + amount, + recipient: to, + sender: address, + enabled: Boolean(activeType) && validTo && amount > 0n, + }); + + // Mirrors what `propose_transfer` would refuse, so the form says why before a prompt appears. + const problems: string[] = []; + if (asset && amount > 0n) { + if ((known?.trust ?? Trust.UNKNOWN) === Trust.BLOCKED) problems.push('This recipient is blocked.'); + if (asset.limits.perTxMax !== 0n && amount > asset.limits.perTxMax) + problems.push(`Above the per-payment cap of ${fmtAmount(asset.limits.perTxMax, decimals)}.`); + const spent = asset.velocity?.daySpent ?? 0n; + if (asset.limits.dailyMax !== 0n && spent + amount > asset.limits.dailyMax) + problems.push(`Above what is left of today (${fmtAmount(asset.limits.dailyMax - spent, decimals)}).`); + if (amount > asset.balance) problems.push(`The vault holds ${fmtAmount(asset.balance, decimals)}.`); + } + + const mayPropose = canPropose(roles); + const ready = mayPropose && validTo && amount > 0n && problems.length === 0 && Boolean(activeType); + + if (approved.length === 0) { + return ( + }> + + No asset is approved for this vault yet. An owner has to approve one through governance + before anything can be paid. + + + ); + } + + if (direction === 'request') { + return ( + }> + + + + Deposits are never gated — in any mode, from any address. Share this vault's address + and the funds land directly on it. + + + Vault address +
+ + + Sending a coin type this vault has not approved will fail. Approving one is a + governance action. + + + + ); + } + + return ( + } + footer={ + + + + + } + > + + + + Opening a payment is not approving it. The vault scores it first and then says what it + needs. + + + {approved.length > 1 ? ( + + Asset + + {approved.map((a) => ( + + ))} + + + ) : null} + + + + + setTo(t.trim())} placeholder="0x…" autoCapitalize="none" autoCorrect={false} /> + + setScanning(true)} /> + + + {validTo ? ( + + + + {known && known.paidCount > 0 + ? `paid ${known.paidCount} time${known.paidCount === 1 ? '' : 's'} before` + : 'never paid by this vault'} + + + ) : null} + + + + + + {amount > 0n ? ( + + = {amount.toString()} base units + + ) : null} + + + + + + {!mayPropose ? ( + + This device's key cannot open payments in this vault. That takes an owner, + approver, executor or proposer role. + + ) : null} + + {problems.length > 0 ? ( + {`The vault would refuse this. ${problems.join(' ')}`} + ) : null} + + {preview.data ? ( + + + What the vault will demand + + + + + + + {describeReasons(preview.data.reasons).length > 0 ? ( + + {describeReasons(preview.data.reasons).map((r) => ( + + • {r} + + ))} + + ) : null} + + ) : preview.isLoading ? ( + Asking the vault… + ) : null} + + + setScanning(false)} onAddress={setTo} /> + + ); +} diff --git a/mobile/src/screens/Settings.tsx b/mobile/src/screens/Settings.tsx new file mode 100644 index 0000000..9ea5d4d --- /dev/null +++ b/mobile/src/screens/Settings.tsx @@ -0,0 +1,239 @@ +import { useEffect, useState } from 'react'; +import { Alert, Text, View } from 'react-native'; +import Constants from 'expo-constants'; +import { + Address, + Button, + Card, + CircleIcon, + Field, + IconButton, + Input, + KV, + Notice, + Row, + Screen, + SectionLabel, + Sheet, + Tabs, + TopBar, +} from '../components/ui'; +import { Avatar, SwitchRow } from '../components/kit'; +import { useScheme, type Scheme } from '../state/theme'; +import { useTheme } from '../state/theme'; +import { F } from '../theme'; +import { API_URL, NETWORK, PACKAGE_ID } from '../lib/config'; +import { biometricSupport, forgetSigner, type BiometricSupport } from '../lib/keystore'; +import { enablePush, type PushResult } from '../lib/notifications'; +import { useStore } from '../state/store'; + +/** + * Screen 20 — settings: who you are, the organisation, the wallet on this device, security, + * notifications and appearance. Dark mode is a switch and a precise preference — the switch flips + * ground, the segmented control underneath can hand the choice back to the system. + */ +export function Settings({ onOpenHelp, onOpenSecurity }: { onOpenHelp?: () => void; onOpenSecurity?: () => void }) { + const C = useTheme(); + const { scheme, dark, setScheme } = useScheme(); + const { address, vaultId, profile, saveProfile, closeVault, refreshSigner } = useStore(); + const [bio, setBio] = useState(null); + const [push, setPush] = useState(null); + const [pushBusy, setPushBusy] = useState(false); + const [editing, setEditing] = useState(false); + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + + useEffect(() => { + void biometricSupport().then(setBio); + }, []); + + const forget = () => + Alert.alert( + 'Forget this key?', + 'The key is deleted from this device and cannot be recovered. Your address stays a member of the vault until governance removes it — guardians can move your roles to a new key, which is what recovery is for.', + [ + { text: 'Cancel', style: 'cancel' }, + { text: 'Forget it', style: 'destructive', onPress: () => void forgetSigner().then(refreshSigner) }, + ], + ); + + const version = Constants.expoConfig?.version ?? '1.0.0'; + + return ( + }> + + + } + title={profile?.name ?? 'No name set'} + subtitle={profile?.email || 'No email — only used for notifications'} + trailing={ + { + setName(profile?.name ?? ''); + setEmail(profile?.email ?? ''); + setEditing(true); + }} + /> + } + last + /> + + + {vaultId ? ( + + Organisation + + } title="Vault" subtitle={`${vaultId.slice(0, 18)}…`} /> + } + title="Close this vault" + subtitle="Keeps the key; just stops showing it" + onPress={closeVault} + last + /> + + + ) : null} + + + Wallets + + {address ? ( + <> + This device signs as +
+ + + + + ) : ( + No signing key on this device. + )} + + {address ? ( + + ) : null} + + + + Security + + } + title="Security Center" + subtitle="Posture, the brake, and maximum possible loss" + onPress={onOpenSecurity} + chevron + last + /> + + + This is a hot key: appropriate for an approver or an executor, whose authority is + bounded by the policy. A guardian key belongs somewhere this phone is not. + + + + {vaultId && address ? ( + + Notifications + + {push && !push.ok ? {push.reason} : null} + + ) : null} + + + Appearance + + setScheme(v ? 'dark' : 'light')} + icon="moon" + /> + + setScheme(s as Scheme)} + /> + + + + About + + + + + + + + } + title="Help & Support" + subtitle="FAQs, documentation, contact" + onPress={onOpenHelp} + chevron + last + /> + + + + + Skur has not been audited. Do not put real value behind it until an independent audit is + complete. + + + + setEditing(false)} title="Edit profile"> + + + + + + + + + + + + ); +} diff --git a/mobile/src/screens/Team.tsx b/mobile/src/screens/Team.tsx new file mode 100644 index 0000000..972a5e1 --- /dev/null +++ b/mobile/src/screens/Team.tsx @@ -0,0 +1,183 @@ +import { useState } from 'react'; +import { RefreshControl, Text, View } from 'react-native'; +import { Role, hasRole, short, tx as build } from '@skur/sdk'; +import { + Badge, + Button, + Card, + ErrorState, + Field, + IconButton, + Input, + Notice, + Row, + Screen, + SectionLabel, + Sheet, + Skeleton, + TopBar, + TxStatus, +} from '../components/ui'; +import { Identicon } from '../components/Identicon'; +import { ROLE_SPECS, roleSpec, titleForBits, type RoleId } from '../lib/roles'; +import { useTheme } from '../state/theme'; +import { F } from '../theme'; +import { PACKAGE_ID } from '../lib/config'; +import { useTx } from '../hooks/useTx'; +import { useVaultView } from '../hooks/useVault'; +import { useStore } from '../state/store'; + +const ADDRESS = /^0x[0-9a-fA-F]{64}$/; + +/** + * Screen 15 — the team. The roster is the chain's, not a local list, and an invitation is a + * governance proposal: it collects owner approvals like any other change to who can move money. + */ +export function Team({ onOpenRoles }: { onOpenRoles: () => void }) { + const C = useTheme(); + const { vaultId, address, profile } = useStore(); + const q = useVaultView(vaultId); + const tx = useTx(vaultId); + + const [inviting, setInviting] = useState(false); + const [invitee, setInvitee] = useState(''); + const [role, setRole] = useState('signer'); + + const myRoles = address ? (q.data?.members.find((m) => m.address === address)?.roles ?? 0) : 0; + const isOwner = hasRole(myRoles, Role.OWNER); + const trimmed = invitee.trim(); + const valid = ADDRESS.test(trimmed); + const already = q.data?.members.some((m) => m.address.toLowerCase() === trimmed.toLowerCase()) ?? false; + + if (q.isLoading) { + return ( + }> + + + ); + } + if (q.error || !q.data) { + return ( + }> + q.refetch()} /> + + ); + } + + const members = q.data.members; + + return ( + } + /> + } + refreshControl={ q.refetch()} tintColor={C.text2} />} + footer={ + isOwner ? ( + + + + + ) : null + } + > + + + {members.length} member{members.length === 1 ? '' : 's'} + + + {members.map((m, i) => { + const me = m.address === address; + const title = titleForBits(m.roles); + return ( + } + title={me && profile?.name ? `${profile.name} (You)` : me ? `${short(m.address)} (You)` : short(m.address)} + subtitle={me ? undefined : short(m.address) === m.address ? undefined : undefined} + trailing={ + {title} + } + last={i === members.length - 1} + /> + ); + })} + + + + + + + {!isOwner ? ( + + Changing the roster takes an owner. Membership is a governance action, approved like any + other change to who can move money. + + ) : ( + + An invitation opens a proposal. It executes once {q.data.vault.policy.governanceThreshold} owner + {q.data.vault.policy.governanceThreshold === 1 ? ' has' : 's have'} approved it. + + )} + + + setInviting(false)} title="Invite a member"> + + + + + + Role + + {ROLE_SPECS.filter((r) => r.id !== 'viewer').map((r) => ( + + ))} + + + {roleSpec(role).grants} + + + {already ? That address is already a member. : null} + + + + + ); +} diff --git a/mobile/src/screens/Transactions.tsx b/mobile/src/screens/Transactions.tsx new file mode 100644 index 0000000..38dd969 --- /dev/null +++ b/mobile/src/screens/Transactions.tsx @@ -0,0 +1,162 @@ +import { useState } from 'react'; +import { RefreshControl, Text, View } from 'react-native'; +import { KIND_LABELS, Kind, Status, fmtAmount, fmtRelative, short, type Proposal } from '@skur/sdk'; +import { + Address, + Card, + Empty, + ErrorState, + Notice, + Row, + Screen, + SectionLabel, + Skeleton, + StatusBadge, + Tabs, + TierBadge, + TopBar, +} from '../components/ui'; +import { TokenMark, coinDecimals, coinSymbol } from '../components/TokenMark'; +import { Identicon } from '../components/Identicon'; +import { useTheme } from '../state/theme'; +import { F } from '../theme'; +import { useVaultView } from '../hooks/useVault'; +import { useStore } from '../state/store'; + +type Filter = 'all' | 'sent' | 'received'; + +/** + * Screen 10 — the transaction list. "Sent" is every transfer that executed; "Received" is honest + * about what the chain does: deposits land directly on the vault object, not through proposals, + * so there is no per-deposit feed here — only where to send and what is accepted. + */ +export function Transactions({ onOpen }: { onOpen: (id: bigint) => void }) { + const C = useTheme(); + const { vaultId } = useStore(); + const q = useVaultView(vaultId); + const [filter, setFilter] = useState('all'); + + const all = q.data?.proposals ?? []; + const pending = all.filter((p) => p.status === Status.PENDING); + const settled = all.filter((p) => p.status !== Status.PENDING); + const sent = all.filter((p) => p.kind === Kind.TRANSFER && p.status === Status.EXECUTED); + + const line = (p: Proposal) => + p.kind === Kind.TRANSFER && p.asset + ? fmtAmount(p.amount, coinDecimals(p.asset), coinSymbol(p.asset)) + : KIND_LABELS[p.kind]; + + // "To: Supplier A" when the vault has a label for the recipient; the address otherwise. + const toLine = (p: Proposal) => { + if (p.kind !== Kind.TRANSFER) return p.memo || KIND_LABELS[p.kind]; + const known = q.data?.recipients.find((r) => r.address === p.recipient); + const who = known?.label || short(p.recipient); + return p.memo ? `To: ${who} · ${p.memo}` : `To: ${who}`; + }; + + const item = (p: Proposal, i: number, arr: Proposal[]) => ( + : + } + title={line(p)} + subtitle={ + p.status === Status.PENDING ? ( + `${toLine(p)} · ${p.approvals.length}/${p.reqApprovals} approvals` + ) : ( + + {`${toLine(p)} · ${fmtRelative(p.createdAt)}`} + + ) + } + trailing={ + p.status === Status.PENDING && p.kind === Kind.TRANSFER ? : + } + onPress={() => onOpen(p.id)} + chevron + last={i === arr.length - 1} + /> + ); + + return ( + } + refreshControl={ + q.refetch()} tintColor={C.text2} /> + } + > + + + + {q.isLoading ? : null} + {q.error ? q.refetch()} /> : null} + + {filter === 'all' ? ( + <> + + Awaiting action + {pending.length === 0 ? ( + Nothing is waiting. + ) : ( + {pending.map((p, i) => item(p, i, pending))} + )} + + + Settled + {settled.length === 0 ? ( + Nothing has settled yet. + ) : ( + {settled.map((p, i) => item(p, i, settled))} + )} + + + ) : null} + + {filter === 'sent' ? ( + sent.length === 0 ? ( + No payment has executed yet. + ) : ( + {sent.map((p, i) => item(p, i, sent))} + ) + ) : null} + + {filter === 'received' ? ( + + + Deposits land directly on the vault — in any mode, from any address — so they do not + appear as proposals. Share the address below to receive. + + + Vault address +
+ + {(q.data?.assets ?? []).filter((a) => a.limits.approved).length > 0 ? ( + + {q.data!.assets + .filter((a) => a.limits.approved) + .map((a, i, arr) => ( + } + title={coinSymbol(a.coinType)} + subtitle={`holding ${fmtAmount(a.balance, coinDecimals(a.coinType))}`} + last={i === arr.length - 1} + /> + ))} + + ) : null} + + ) : null} + + + ); +} diff --git a/mobile/src/screens/TxDetail.tsx b/mobile/src/screens/TxDetail.tsx new file mode 100644 index 0000000..70cc719 --- /dev/null +++ b/mobile/src/screens/TxDetail.tsx @@ -0,0 +1,433 @@ +import { useState } from 'react'; +import { Text, View } from 'react-native'; +import { + KIND_LABELS, + Kind, + Role, + Status, + Tier, + Trust, + describeReasons, + fmtAmount, + fmtDuration, + fmtRelative, + fmtTimestamp, + hasRole, + policyReductions, + short, + tx as build, +} from '@skur/sdk'; +import { + Address, + BackButton, + Button, + Card, + KV, + Notice, + Screen, + SectionLabel, + StatusBadge, + TierBadge, + TopBar, + TxStatus, + Sheet, +} from '../components/ui'; +import { CheckItem, Meter, TimelineStep } from '../components/kit'; +import { coinDecimals, coinSymbol } from '../components/TokenMark'; +import { Identicon } from '../components/Identicon'; +import { useTheme } from '../state/theme'; +import { F } from '../theme'; +import { PACKAGE_ID } from '../lib/config'; +import { useTx } from '../hooks/useTx'; +import { useTransferPreview, useVaultView } from '../hooks/useVault'; +import { useStore } from '../state/store'; + +/** + * Screens 12–14 in one place: the details of a proposal, its approval flow as a timeline, the + * review-before-signing checks, and the actions this device's key is actually allowed to take. + * + * Every button is gated on the role the contract requires, so a signer is never offered something + * the vault would refuse. The requirements shown are the ones pinned when the proposal was opened; + * at execution the vault re-derives them and applies whichever is stricter, which is why a payment + * can end up needing more than this says and never less. + */ +export function TxDetail({ id, onBack }: { id: bigint; onBack: () => void }) { + const C = useTheme(); + const { vaultId, address } = useStore(); + const q = useVaultView(vaultId); + const tx = useTx(vaultId); + const [simulating, setSimulating] = useState(false); + + const p = q.data?.proposals.find((x) => x.id === id); + const roles = address ? (q.data?.members.find((m) => m.address === address)?.roles ?? 0) : 0; + + const isTransfer = p?.kind === Kind.TRANSFER; + // The simulation re-runs the vault's own preview for this exact payment, on demand. + const preview = useTransferPreview({ + vaultId, + coinType: p?.asset ?? '', + amount: p?.amount ?? 0n, + recipient: p?.recipient ?? '', + sender: address, + enabled: simulating && Boolean(isTransfer && p?.asset), + }); + + if (!p || !q.data) { + return ( + } title="Proposal" />}> + Reading… + + ); + } + + const voteRole = isTransfer ? Role.APPROVER : Role.OWNER; + const canVote = hasRole(roles, voteRole); + const canExecute = isTransfer ? hasRole(roles, Role.EXECUTOR) : roles !== 0; + const isGuardian = hasRole(roles, Role.GUARDIAN); + + const live = (set: string[], role: number) => + set.filter((a) => hasRole(q.data!.members.find((m) => m.address === a)?.roles ?? 0, role)).length; + const liveApprovals = live(p.approvals, Role.APPROVER); + const liveConfirmations = live(p.confirmations, Role.GUARDIAN); + const liveRejections = live(p.rejections, voteRole); + + const voted = address ? p.approvals.includes(address) : false; + const rejected = address ? p.rejections.includes(address) : false; + const confirmed = address ? p.confirmations.includes(address) : false; + + const timeMet = Date.now() >= p.executableAt; + const ready = liveApprovals >= p.reqApprovals && liveConfirmations >= p.reqGuardians && timeMet; + const open = p.status === Status.PENDING; + + const vetoable = + p.reductionMask !== 0 || + (isTransfer && p.tier === Tier.CRITICAL) || + p.kind === Kind.RECOVERY || + p.kind === Kind.MODE_RELAX; + + const reasons = describeReasons(p.reasons); + const reductions = p.newPolicy ? policyReductions(q.data.vault.policy, p.newPolicy) : []; + + // Screen 14's checks, computed from chain state rather than asserted. + const recipientRecord = q.data.recipients.find((r) => r.address === p.recipient); + const trust = recipientRecord?.trust ?? Trust.UNKNOWN; + const asset = p.asset ? q.data.assets.find((a) => a.coinType === p.asset) : undefined; + const withinPerTx = !asset || asset.limits.perTxMax === 0n || p.amount <= asset.limits.perTxMax; + const recipientKnown = trust !== Trust.UNKNOWN && trust !== Trust.BLOCKED; + const noRedFlags = trust !== Trust.BLOCKED && p.reductionMask === 0; + + const executeTx = () => { + if (isTransfer && p.asset) + return build.executeTransfer(PACKAGE_ID, { vaultId: vaultId!, coinType: p.asset, proposalId: id }); + if (p.kind === Kind.POLICY_UPDATE) return build.executePolicy(PACKAGE_ID, vaultId!, id); + if (p.kind === Kind.MEMBER_SET) return build.executeMember(PACKAGE_ID, vaultId!, id); + if (p.kind === Kind.RECIPIENT_TRUST) return build.executeRecipientTrust(PACKAGE_ID, vaultId!, id); + if (p.kind === Kind.MODE_RELAX) return build.executeModeRelax(PACKAGE_ID, vaultId!, id); + if (p.kind === Kind.RECOVERY) return build.executeRecovery(PACKAGE_ID, vaultId!, id); + if (p.kind === Kind.ASSET_LIMITS && p.asset) + return build.executeAssetLimits(PACKAGE_ID, { vaultId: vaultId!, coinType: p.asset, proposalId: id }); + throw new Error('nothing to execute for this kind'); + }; + + // Screen 13 — the approval flow, as it stands on chain right now. + const flow: { title: string; subtitle: string; state: 'done' | 'active' | 'todo' }[] = [ + { + title: 'Proposed', + subtitle: `by ${short(p.proposer)} · ${fmtRelative(p.createdAt)}`, + state: 'done', + }, + ...p.approvals.map((a) => ({ + title: 'Approved', + subtitle: short(a), + state: 'done' as const, + })), + ...Array.from({ length: Math.max(0, p.reqApprovals - liveApprovals) }).map((_, i) => ({ + title: 'Approval', + subtitle: i === 0 && canVote && !voted ? 'Yours, if you give it' : 'Awaiting a signer', + state: (i === 0 ? 'active' : 'todo') as 'active' | 'todo', + })), + ...(p.reqGuardians > 0 + ? [ + { + title: 'Guardian confirmation', + subtitle: `${liveConfirmations} of ${p.reqGuardians}`, + state: (liveConfirmations >= p.reqGuardians ? 'done' : 'todo') as 'done' | 'todo', + }, + ] + : []), + { + title: p.status === Status.EXECUTED ? 'Executed' : 'Execution', + subtitle: + p.status === Status.EXECUTED + ? 'Settled on chain' + : !timeMet + ? `Waits until ${fmtTimestamp(p.executableAt)}` + : ready + ? 'Ready now' + : 'After the approvals above', + state: p.status === Status.EXECUTED ? 'done' : ready && open ? 'active' : 'todo', + }, + ]; + + return ( + } title={`#${id} · ${KIND_LABELS[p.kind]}`} />} + footer={ + open ? ( + + + {canVote ? ( + + + + + ) : null} + {isGuardian && (p.reqGuardians > 0 || vetoable) ? ( + + ) : null} + {isGuardian && vetoable ? ( + + ) : null} + {canExecute ? ( + + ) : null} + + ) : null + } + > + + + {isTransfer ? : null} + + {p.reductionMask !== 0 ? ( + weakens the vault + ) : null} + + + {isTransfer && p.asset ? ( + + Leaves the vault + + {fmtAmount(p.amount, coinDecimals(p.asset), coinSymbol(p.asset))} + + + To +
+ + {p.memo ? ( + {p.memo} + ) : null} + + ) : null} + + {isTransfer && p.asset ? ( + + Details + + + + {p.memo ? : null} + } last /> + {open ? ( + + + + ) : null} + + ) : null} + + {p.kind === Kind.RECOVERY ? ( + + Replace a lost signer + } /> + } last /> + + The roles move across unchanged and the policy is untouched. Any owner can cancel this + before it executes. + + + ) : null} + + {reasons.length > 0 ? ( + + Scored this way because + {reasons.map((r) => ( + + • {r} + + ))} + + ) : null} + + {reductions.length > 0 ? ( + + This change gives up: {reductions.join('; ')}. It waits{' '} + {fmtDuration(q.data.vault.policy.policyChangeDelay)} and any guardian can stop it. + + ) : null} + + {open && isTransfer && (canVote || canExecute || isGuardian) ? ( + + Review before signing + + + + + + + + ) : null} + + + Approval flow + + {flow.map((step, i) => ( + + ))} + + + + + What it needs + + {p.approvals.length > 0 ? ( + + {p.approvals.slice(0, 6).map((a) => ( + + ))} + {p.approvals.length > 6 ? ( + + +{p.approvals.length - 6} + + ) : null} + + ) : null} + {p.reqGuardians > 0 ? ( + + ) : null} + {liveRejections > 0 ? : null} + + + } last /> + + + {!canVote && !canExecute && !isGuardian ? ( + + This device's key holds no role that can act on this proposal. You can read it. + + ) : null} + + + setSimulating(false)} title="Simulation"> + + + The vault's own preview, re-run against its state right now — the same code + execution will run. + + {preview.isLoading ? ( + Asking the vault… + ) : preview.data ? ( + + + It would demand + + + + + + + + ) : preview.error ? ( + + The vault refused the simulation: {preview.error instanceof Error ? preview.error.message : 'it would not execute as-is.'} + + ) : null} + + + + ); +} diff --git a/mobile/src/screens/onboarding/AddMembers.tsx b/mobile/src/screens/onboarding/AddMembers.tsx new file mode 100644 index 0000000..8391a52 --- /dev/null +++ b/mobile/src/screens/onboarding/AddMembers.tsx @@ -0,0 +1,161 @@ +import { useState } from 'react'; +import { Text, View } from 'react-native'; +import { + Badge, + BackButton, + Button, + Card, + Field, + IconButton, + Input, + Notice, + Row, + Screen, + SectionLabel, + Sheet, + TopBar, +} from '../../components/ui'; +import { Avatar } from '../../components/kit'; +import { Identicon } from '../../components/Identicon'; +import { short } from '@skur/sdk'; +import { ROLE_SPECS, roleSpec, type RoleId } from '../../lib/roles'; +import { useTheme } from '../../state/theme'; +import { F } from '../../theme'; +import { useStore } from '../../state/store'; + +export type MemberDraft = { address: string; role: RoleId }; + +const ADDRESS = /^0x[0-9a-fA-F]{64}$/; + +/** + * Screen 6 — the roster. You are the first owner; everyone else is an address and a role. The + * contract writes this at creation, so what is promised here is exactly what will hold. + */ +export function AddMembers({ + members, + onBack, + onDone, +}: { + members: MemberDraft[]; + onBack: () => void; + onDone: (members: MemberDraft[]) => void; +}) { + const C = useTheme(); + const { profile } = useStore(); + const [list, setList] = useState(members); + const [adding, setAdding] = useState(false); + const [address, setAddress] = useState(''); + const [role, setRole] = useState('signer'); + + const trimmed = address.trim(); + const valid = ADDRESS.test(trimmed); + const duplicate = list.some((m) => m.address.toLowerCase() === trimmed.toLowerCase()); + const guardians = list.filter((m) => m.role === 'guardian').length; + + const add = () => { + setList([...list, { address: trimmed, role }]); + setAddress(''); + setRole('signer'); + setAdding(false); + }; + + return ( + } title="Add Members" />} + footer={ + + } + > + + + Invite your team members and set their roles. + + + + } + title={`${profile?.name ?? 'You'} (You)`} + subtitle="Key created in the next step" + trailing={Owner} + last={list.length === 0} + /> + {list.map((m, i) => ( + } + title={short(m.address)} + subtitle={roleSpec(m.role).grants} + trailing={ + + {roleSpec(m.role).title} + setList(list.filter((x) => x.address !== m.address))} + /> + + } + last={i === list.length - 1} + /> + ))} + + + + + {guardians === 0 ? ( + + No guardian yet. A guardian is an independent key that can veto a theft in progress — + without one, the vault is only as safe as its signers. You can also add one later + through governance. + + ) : null} + + + Members are written on chain at creation. Viewers need no role — a vault is a shared + object anyone can read. + + + + setAdding(false)} title="Add a member"> + + + + + + Role + + {ROLE_SPECS.filter((r) => r.id !== 'viewer' && r.id !== 'owner').map((r) => ( + + ))} + + + {roleSpec(role).grants} + + + {duplicate ? That address is already on the roster. : null} + + + + + ); +} diff --git a/mobile/src/screens/onboarding/ConnectWallet.tsx b/mobile/src/screens/onboarding/ConnectWallet.tsx new file mode 100644 index 0000000..2d9c7f2 --- /dev/null +++ b/mobile/src/screens/onboarding/ConnectWallet.tsx @@ -0,0 +1,313 @@ +import { useEffect, useState } from 'react'; +import { Linking, Text, View } from 'react-native'; +import { usePreventScreenCapture } from 'expo-screen-capture'; +import { Role, templateById, tx as build } from '@skur/sdk'; +import { + Address, + BackButton, + Badge, + Button, + Card, + Field, + Input, + Notice, + Row, + Screen, + SectionLabel, + Sheet, + TopBar, + TxProgress, +} from '../../components/ui'; +import { CheckItem } from '../../components/kit'; +import { Icon } from '../../components/ui'; +import { useTheme } from '../../state/theme'; +import { F, R } from '../../theme'; +import { client } from '../../lib/client'; +import { NETWORK, PACKAGE_ID } from '../../lib/config'; +import { biometricSupport, createSigner, importSigner, unlockSigner, type BiometricSupport } from '../../lib/keystore'; +import { describeFailure } from '@skur/sdk'; +import { hapticError, hapticSuccess } from '../../lib/haptics'; +import { roleSpec } from '../../lib/roles'; +import { useStore } from '../../state/store'; +import type { OrgDraft } from './CreateOrg'; +import type { MemberDraft } from './AddMembers'; +import type { TxState } from '../../hooks/useTx'; + +/** + * Screen 7 — the signer. + * + * The real wallet on a phone is the phone: an Ed25519 key in the secure element, released one + * action at a time behind a biometric. The external wallets people know from the desktop are + * listed so the choice is visible, and marked honestly until wallet-to-wallet connections ship. + */ +const EXTERNAL_WALLETS = ['Slush', 'Suiet', 'Backpack', 'Ledger', 'Nightly', 'OKX Wallet'] as const; + +export function ConnectWallet({ + draft, + members, + onBack, + onDone, +}: { + /** Present when this run is creating an organisation; null when joining one. */ + draft: OrgDraft | null; + members: MemberDraft[]; + onBack: () => void; + onDone: () => void; +}) { + usePreventScreenCapture(); + const C = useTheme(); + const { address, refreshSigner, openVault, saveProfile, profile } = useStore(); + const [bio, setBio] = useState(null); + const [importing, setImporting] = useState(false); + const [secret, setSecret] = useState(''); + const [error, setError] = useState(null); + const [busy, setBusy] = useState(false); + const [state, setState] = useState({ phase: 'idle' }); + + useEffect(() => { + void biometricSupport().then(setBio); + }, []); + + const makeKey = async (how: 'create' | 'import') => { + setError(null); + setBusy(true); + try { + if (how === 'create') await createSigner(); + else await importSigner(secret); + await refreshSigner(); + setImporting(false); + setSecret(''); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + } finally { + setBusy(false); + } + }; + + /** Build and execute the creation, and open the vault the chain hands back. */ + const createOrganisation = async () => { + if (!draft || !address) return; + setState({ phase: 'unlocking' }); + try { + const keypair = await unlockSigner(`Create ${draft.name} on Sui`); + setState({ phase: 'signing' }); + + const template = templateById('startup'); + const roster = [ + { address, roles: Role.OWNER | Role.APPROVER | Role.EXECUTOR }, + ...members + .filter((m) => roleSpec(m.role).bits !== 0) + .map((m) => ({ address: m.address, roles: roleSpec(m.role).bits })), + ]; + // The template assumes a team; the roster is what actually exists. Every threshold is + // clamped to the people who can meet it — the contract refuses anything else, rightly: a + // 2-of-1 vault is a locked box. Growing the team later raises these through governance. + const owners = roster.filter((m) => m.roles & Role.OWNER).length; + const approvers = roster.filter((m) => m.roles & Role.APPROVER).length; + const guardians = roster.filter((m) => m.roles & Role.GUARDIAN).length; + const policy = { + ...template.policy, + governanceThreshold: Math.min(template.policy.governanceThreshold, owners), + approvalsLow: Math.min(template.policy.approvalsLow, approvers), + approvalsHigh: Math.min(template.policy.approvalsHigh, approvers), + approvalsCritical: Math.min(template.policy.approvalsCritical, approvers), + guardianThreshold: Math.min(template.policy.guardianThreshold, Math.max(guardians, 1)), + guardianRequiredCritical: template.policy.guardianRequiredCritical && guardians > 0, + }; + const tx = build.createVault(PACKAGE_ID, { + name: draft.name, + policy, + members: roster, + assets: [{ coinType: '0x2::sui::SUI', limits: template.native }], + }); + + const result = await client.signAndExecuteTransaction({ + transaction: tx, + signer: keypair, + include: { effects: true }, + }); + if (result.FailedTransaction) { + throw new Error(result.FailedTransaction.status.error?.message ?? 'the network refused it'); + } + const digest = result.Transaction.digest; + setState({ phase: 'waiting', digest }); + await client.core.waitForTransaction({ digest }); + + const created = (result.Transaction.effects?.changedObjects ?? []).find( + (c) => c.outputOwner?.$kind === 'Shared' && c.idOperation === 'Created', + ); + const vaultId = created && 'objectId' in created ? (created as { objectId: string }).objectId : null; + if (!vaultId) throw new Error('the vault was created but its id could not be read — open it by id'); + + hapticSuccess(); + setState({ phase: 'done', digest }); + if (profile) saveProfile(profile); // keep as-is; descriptions stay local + openVault(vaultId); + onDone(); + } catch (e) { + const raw = e instanceof Error ? e.message : String(e); + if (raw === 'cancelled') { + setState({ phase: 'cancelled' }); + return; + } + hapticError(); + const funds = /gas|coin|balance|insufficient/i.test(raw); + setState({ + phase: 'error', + message: funds + ? `This device's address has no SUI to pay for creation on ${NETWORK}. Fund it and try again.` + : describeFailure(e), + }); + } + }; + + const creating = state.phase === 'unlocking' || state.phase === 'signing' || state.phase === 'waiting'; + + return ( + } title="Connect Wallet" />} + footer={ + address ? ( + + {state.phase !== 'idle' ? : null} + {draft ? ( + + ) : ( + + )} + + ) : undefined + } + > + + + Choose a wallet to connect to Skur. + + + {address ? ( + + + + This device + + Connected + + + Signs as +
+ + ) : ( + + + + + This device (recommended) + + + + A new Ed25519 key, generated here and written to the secure element. It never leaves + the phone and is released for one action at a time, behind{' '} + {bio?.enrolled ? bio.label : 'your screen lock'}. + + {bio && !bio.enrolled ? ( + + No {bio.available ? bio.label : 'screen lock'} is set up on this device. Set one up + first — without it the key would sit unprotected. + + ) : null} + + + + + {error ? {error} : null} + + )} + + + External wallets + + {EXTERNAL_WALLETS.map((w, i) => ( + + {w[0]} + + } + title={w} + trailing={Soon} + last={i === EXTERNAL_WALLETS.length - 1} + /> + ))} + + + Wallet-to-wallet connections are not wired up yet. The device key above is live today + and holds the same roles any wallet would. + + + + {!address ? ( + + + + ) : null} + + void Linking.openURL('mailto:support@skur.app')} + accessibilityRole="link" + style={{ fontFamily: F.bodyMedium, fontSize: 14, color: C.info, textAlign: 'center', paddingVertical: 6 }} + > + Need help? + + + + setImporting(false)} title="Import a key"> + + + + + + {error ? {error} : null} + + + + ); +} diff --git a/mobile/src/screens/onboarding/CreateAccount.tsx b/mobile/src/screens/onboarding/CreateAccount.tsx new file mode 100644 index 0000000..3a37ce4 --- /dev/null +++ b/mobile/src/screens/onboarding/CreateAccount.tsx @@ -0,0 +1,110 @@ +import { useState } from 'react'; +import { Pressable, Text, View } from 'react-native'; +import { BackButton, Button, Field, Icon, Input, Notice, Screen, TopBar } from '../../components/ui'; +import { useTheme } from '../../state/theme'; +import { F, R } from '../../theme'; +import { useStore } from '../../state/store'; + +/** + * Screen 4 — who you are. + * + * There is deliberately no password and no federated login here. Skur's credential is the signing + * key in the phone's secure element, created on the next step and released by the OS behind a + * biometric — a password would be a second, weaker door into the same house. The name and email + * stay on this device: the chain identifies members by address, and the backend only ever sees + * what notification registration sends it. + */ +export function CreateAccount({ onBack, onDone, onWallet }: { onBack: () => void; onDone: () => void; onWallet: () => void }) { + const C = useTheme(); + const { profile, saveProfile } = useStore(); + const [name, setName] = useState(profile?.name ?? ''); + const [email, setEmail] = useState(profile?.email ?? ''); + const [agreed, setAgreed] = useState(false); + + const emailOk = email === '' || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); + const ready = name.trim().length > 0 && emailOk && agreed; + + return ( + } title="Create Account" />} + footer={ + + } + > + + + Enter your details to get started. + + + + + + + + + + {!emailOk ? That does not look like an email address. : null} + + setAgreed(!agreed)} + accessibilityRole="checkbox" + accessibilityState={{ checked: agreed }} + style={{ flexDirection: 'row', gap: 10, alignItems: 'center' }} + > + + {agreed ? : null} + + + I agree to the Terms of Service and Privacy Policy. + + + + + No password. Your signing key — created on the next step, held in this phone's + secure element — is the credential. + + + + + or + + + + + + Skip the profile and go straight to your signing key. + + + + ); +} diff --git a/mobile/src/screens/onboarding/CreateOrg.tsx b/mobile/src/screens/onboarding/CreateOrg.tsx new file mode 100644 index 0000000..c202985 --- /dev/null +++ b/mobile/src/screens/onboarding/CreateOrg.tsx @@ -0,0 +1,72 @@ +import { useState } from 'react'; +import { Text, View } from 'react-native'; +import { BackButton, Button, Card, Field, Input, KV, Screen, SectionLabel, TopBar } from '../../components/ui'; +import { useTheme } from '../../state/theme'; +import { F } from '../../theme'; +import { NETWORK } from '../../lib/config'; + +export type OrgDraft = { name: string; description: string }; + +/** + * Screen 5 — name the organisation. The blockchain row is fixed by the build: which Sui network + * this app points at is an environment decision, not something to mis-tap during setup. + */ +export function CreateOrg({ + draft, + onBack, + onDone, +}: { + draft: OrgDraft | null; + onBack: () => void; + onDone: (draft: OrgDraft) => void; +}) { + const C = useTheme(); + const [name, setName] = useState(draft?.name ?? ''); + const [description, setDescription] = useState(draft?.description ?? ''); + const ready = name.trim().length >= 2; + + return ( + } title="Create Organisation" />} + footer={ + + } + > + + + Set up your team or business on Skur. + + + + + + + + + + + + Blockchain + + + + + + + ); +} diff --git a/mobile/src/screens/onboarding/Launch.tsx b/mobile/src/screens/onboarding/Launch.tsx new file mode 100644 index 0000000..bfa3d0e --- /dev/null +++ b/mobile/src/screens/onboarding/Launch.tsx @@ -0,0 +1,46 @@ +import { Text, View } from 'react-native'; +import { Button, Logo, Screen } from '../../components/ui'; +import { useTheme } from '../../state/theme'; +import { F } from '../../theme'; +import { NETWORK } from '../../lib/config'; + +/** Screen 1 — the front door. */ +export function Launch({ onStart }: { onStart: () => void }) { + const C = useTheme(); + return ( + + Get started + + } + > + + + + Skur + + Secure. Shared. On Sui. + + + + The mobile treasury security layer for teams, DAOs and businesses building on Sui. + + + Sui · {NETWORK} · not audited + + + + ); +} diff --git a/mobile/src/screens/onboarding/Start.tsx b/mobile/src/screens/onboarding/Start.tsx new file mode 100644 index 0000000..3f14c90 --- /dev/null +++ b/mobile/src/screens/onboarding/Start.tsx @@ -0,0 +1,48 @@ +import { Text, View } from 'react-native'; +import { Button, Logo, Screen } from '../../components/ui'; +import { useTheme } from '../../state/theme'; +import { F, R } from '../../theme'; + +/** Screen 3 — create a new organisation, or join one that already exists. */ +export function Start({ onCreate, onJoin }: { onCreate: () => void; onJoin: () => void }) { + const C = useTheme(); + return ( + + + + + } + > + + + Welcome to Skur! + + + Let's set up your secure organisation and get you started. + + + + + + + ); +} diff --git a/mobile/src/screens/onboarding/Tour.tsx b/mobile/src/screens/onboarding/Tour.tsx new file mode 100644 index 0000000..84a26b9 --- /dev/null +++ b/mobile/src/screens/onboarding/Tour.tsx @@ -0,0 +1,70 @@ +import { useState } from 'react'; +import { Text, View } from 'react-native'; +import { Button, Card, Screen } from '../../components/ui'; +import { CheckItem, Dots } from '../../components/kit'; +import { useTheme } from '../../state/theme'; +import { F } from '../../theme'; + +/** + * Screen 2 — what the product does, in four controls. One page of checks plus two pages of + * depth, so "Next" has somewhere to go and the dots are not decoration. + */ +const PAGES: { heading: string; sub: string; items: [string, string][] }[] = [ + { + heading: 'Your Treasury,\nUnder Control.', + sub: 'Four controls stand between a stolen key and your money.', + items: [ + ['Multi-Signature Security', 'Require multiple approvals'], + ['Spending Policies', 'Set limits and rules'], + ['Transaction Simulation', 'Check before you sign'], + ['Team Permissions', 'Manage roles and access'], + ], + }, + { + heading: 'Approvals that\nfollow the risk.', + sub: 'The amount, the destination and today’s outflow decide how much sign-off a payment needs.', + items: [ + ['Risk-tiered approvals', 'Routine, high and critical each demand their own quorum'], + ['Timelocks on big moves', 'Critical payments wait, so a theft can be vetoed'], + ['A loss envelope', 'A hard ceiling on what can leave in a day'], + ], + }, + { + heading: 'Enforced on chain,\nnot promised.', + sub: 'Every rule lives in a Move object anyone can read. The app cannot bend them, and neither can an attacker with one key.', + items: [ + ['Guardians', 'An independent brake that can veto or freeze'], + ['Key on your phone', 'In the secure element, behind your fingerprint or face'], + ['Recovery built in', 'Lose the phone, keep the treasury'], + ], + }, +]; + +export function Tour({ onDone }: { onDone: () => void }) { + const C = useTheme(); + const [page, setPage] = useState(0); + const p = PAGES[page]; + const last = page === PAGES.length - 1; + return ( + + + + + } + > + + {p.heading} + {p.sub} + + {p.items.map(([title, detail]) => ( + + ))} + + + + ); +} diff --git a/mobile/src/state/store.tsx b/mobile/src/state/store.tsx new file mode 100644 index 0000000..7022611 --- /dev/null +++ b/mobile/src/state/store.tsx @@ -0,0 +1,170 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, + type ReactNode, +} from 'react'; +import { hasSigner, signerAddress } from '../lib/keystore'; + +/** + * What the app remembers between launches: which vault is open, the addresses you have labelled, + * and whether onboarding is done. None of it is authoritative — the vault is, and all of this is + * rebuilt from the chain on open. + */ +export type Labelled = { address: string; label: string }; + +/** Who is holding the phone, for the Settings header and notification registrations. Local only. */ +export type Profile = { name: string; email: string }; + +type Store = { + ready: boolean; + /** Null until a key exists on the device. */ + address: string | null; + vaultId: string | null; + onboarded: boolean; + book: Labelled[]; + profile: Profile | null; + saveProfile: (p: Profile) => void; + openVault: (id: string) => void; + closeVault: () => void; + finishOnboarding: () => void; + label: (address: string, label: string) => void; + unlabel: (address: string) => void; + /** Re-read the device key, after creating, importing or forgetting one. */ + refreshSigner: () => Promise; +}; + +const Ctx = createContext(null); +const K = { + vault: 'skur.vault', + onboarded: 'skur.onboarded', + book: 'skur.book', + profile: 'skur.profile', +} as const; + +/** + * A normalised Sui object id: 0x and exactly 32 bytes of hex. Storage is validated on load + * because this app's storage keys are older than its chain — an earlier build wrote an EVM + * address (40 hex chars) under the same key, and restoring it silently points every screen at a + * vault that cannot exist. + */ +const SUI_ID = /^0x[0-9a-fA-F]{64}$/; + +export function StoreProvider({ children }: { children: ReactNode }) { + const [ready, setReady] = useState(false); + const [address, setAddress] = useState(null); + const [vaultId, setVaultId] = useState(null); + const [onboarded, setOnboarded] = useState(false); + const [book, setBook] = useState([]); + const [profile, setProfile] = useState(null); + + const refreshSigner = useCallback(async () => { + setAddress((await hasSigner()) ? await signerAddress() : null); + }, []); + + useEffect(() => { + void (async () => { + const [v, o, b, pr] = await Promise.all([ + AsyncStorage.getItem(K.vault), + AsyncStorage.getItem(K.onboarded), + AsyncStorage.getItem(K.book), + AsyncStorage.getItem(K.profile), + ]); + try { + if (pr) { + const parsed = JSON.parse(pr) as Profile; + if (parsed && typeof parsed.name === 'string' && typeof parsed.email === 'string') { + setProfile(parsed); + } + } + } catch { + // A corrupted profile is cosmetic; the key and the vault are what matter. + } + if (v && SUI_ID.test(v)) { + setVaultId(v); + } else if (v) { + // Residue from before, or hand-edited: drop it rather than show a vault that can't load. + void AsyncStorage.removeItem(K.vault); + } + setOnboarded(o === '1'); + try { + setBook(b ? (JSON.parse(b) as Labelled[]) : []); + } catch { + // A corrupted address book is an inconvenience, not a reason to fail to start. + setBook([]); + } + await refreshSigner(); + setReady(true); + })(); + }, [refreshSigner]); + + const openVault = useCallback((id: string) => { + setVaultId(id); + void AsyncStorage.setItem(K.vault, id); + }, []); + + const closeVault = useCallback(() => { + setVaultId(null); + void AsyncStorage.removeItem(K.vault); + }, []); + + const saveProfile = useCallback((p: Profile) => { + setProfile(p); + void AsyncStorage.setItem(K.profile, JSON.stringify(p)); + }, []); + + const finishOnboarding = useCallback(() => { + setOnboarded(true); + void AsyncStorage.setItem(K.onboarded, '1'); + }, []); + + const persistBook = useCallback((next: Labelled[]) => { + setBook(next); + void AsyncStorage.setItem(K.book, JSON.stringify(next)); + }, []); + + const label = useCallback( + (addr: string, text: string) => + persistBook([ + ...book.filter((e) => e.address !== addr), + { address: addr, label: text }, + ]), + [book, persistBook], + ); + + const unlabel = useCallback( + (addr: string) => persistBook(book.filter((e) => e.address !== addr)), + [book, persistBook], + ); + + const value = useMemo( + () => ({ + ready, + address, + vaultId, + onboarded, + book, + profile, + saveProfile, + openVault, + closeVault, + finishOnboarding, + label, + unlabel, + refreshSigner, + }), + [ready, address, vaultId, onboarded, book, profile, saveProfile, openVault, closeVault, finishOnboarding, label, unlabel, refreshSigner], + ); + + return {children}; +} + +export function useStore(): Store { + const s = useContext(Ctx); + if (!s) throw new Error('StoreProvider missing'); + return s; +} diff --git a/mobile/src/state/theme.tsx b/mobile/src/state/theme.tsx new file mode 100644 index 0000000..62f7b97 --- /dev/null +++ b/mobile/src/state/theme.tsx @@ -0,0 +1,31 @@ +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from "react"; +import { useColorScheme } from "react-native"; +import { DARK, LIGHT, type Palette } from "../theme"; + +export type Scheme = "light" | "dark" | "system"; +const Ctx = createContext<{ C: Palette; scheme: Scheme; dark: boolean; setScheme: (s: Scheme) => void } | null>(null); +const KEY = "skur.theme"; + +/** One ground for the whole app, onboarding included, switchable in Settings and remembered. */ +export function ThemeProvider({ children }: { children: ReactNode }) { + const system = useColorScheme(); + const [scheme, setSchemeState] = useState("system"); + useEffect(() => { void AsyncStorage.getItem(KEY).then((v) => { if (v === "light" || v === "dark" || v === "system") setSchemeState(v); }); }, []); + const setScheme = useCallback((s: Scheme) => { setSchemeState(s); void AsyncStorage.setItem(KEY, s); }, []); + const dark = scheme === "system" ? system === "dark" : scheme === "dark"; + const value = useMemo(() => ({ C: dark ? DARK : LIGHT, scheme, dark, setScheme }), [dark, scheme, setScheme]); + return {children}; +} + +export function useTheme(): Palette { + const t = useContext(Ctx); + if (!t) throw new Error("ThemeProvider missing"); + return t.C; +} + +export function useScheme() { + const t = useContext(Ctx); + if (!t) throw new Error("ThemeProvider missing"); + return t; +} diff --git a/mobile/src/theme.ts b/mobile/src/theme.ts new file mode 100644 index 0000000..ec21780 --- /dev/null +++ b/mobile/src/theme.ts @@ -0,0 +1,39 @@ +/** Skur's two grounds. Same tokens in both, so a screen never needs to know which one is on. */ +export type Palette = { + canvas: string; card: string; card2: string; border: string; + text: string; text2: string; text3: string; + accent: string; accentBg: string; onAccent: string; + success: string; successBg: string; warning: string; warningBg: string; + error: string; errorBg: string; info: string; infoBg: string; + shadowOpacity: number; +}; + +export const LIGHT: Palette = { + canvas: "#ffffff", card: "#f6f7f9", card2: "#eceef2", border: "#e4e7ec", + text: "#101418", text2: "#667085", text3: "#98a2b3", + accent: "#ffd000", accentBg: "#fff6cc", onAccent: "#101418", + success: "#11875a", successBg: "#e6f6ec", warning: "#a86a00", warningBg: "#fff3d6", + error: "#b42318", errorBg: "#ffe4e2", info: "#1971c2", infoBg: "#e7f1ff", + shadowOpacity: 0.08, +}; + +export const DARK: Palette = { + canvas: "#212529", card: "#2b3035", card2: "#343a40", border: "#495057", + text: "#f8f9fa", text2: "#adb5bd", text3: "#6c757d", + accent: "#ffd000", accentBg: "#3d3400", onAccent: "#212529", + success: "#75b798", successBg: "#1b4332", warning: "#ffa94d", warningBg: "#4d2e05", + error: "#ea868f", errorBg: "#4a1c22", info: "#6ea8fe", infoBg: "#1b3a5c", + shadowOpacity: 0.4, +}; + +export const F = { + display: "SpaceGrotesk_700Bold", + displayMedium: "SpaceGrotesk_600SemiBold", + body: "DMSans_400Regular", + bodyMedium: "DMSans_500Medium", + bodyBold: "DMSans_700Bold", + mono: "DMMono_400Regular", + monoMedium: "DMMono_500Medium", +} as const; + +export const R = { sm: 8, md: 12, lg: 16, pill: 999 } as const; diff --git a/mobile/test/design.test.tsx b/mobile/test/design.test.tsx new file mode 100644 index 0000000..56219a9 --- /dev/null +++ b/mobile/test/design.test.tsx @@ -0,0 +1,176 @@ +import { fireEvent } from '@testing-library/react-native'; +import { Role, Status } from '@skur/sdk'; +import { renderScreen } from './harness'; +import { ME, VAULT, proposal, view } from './fixtures'; + +/** + * The product screens from the design pass: onboarding, dashboard, team, policies, logs, help. + * Same contract as the other suites — what appears is what the chain would honour. + */ +const mockStore = jest.fn(); +const mockView = jest.fn(); +const mockRun = jest.fn(async () => 'digest'); + +jest.mock('../src/state/store', () => ({ useStore: () => mockStore() })); +jest.mock('../src/hooks/useVault', () => ({ + useVaultView: () => mockView(), + useTransferPreview: () => ({ data: undefined, isLoading: false, error: null }), +})); +jest.mock('../src/hooks/useTx', () => ({ + useTx: () => ({ run: mockRun, state: { phase: 'idle' }, reset: jest.fn(), busy: false }), +})); + +import { Launch } from '../src/screens/onboarding/Launch'; +import { Tour } from '../src/screens/onboarding/Tour'; +import { Start } from '../src/screens/onboarding/Start'; +import { AddMembers } from '../src/screens/onboarding/AddMembers'; +import { ActivityLog } from '../src/screens/ActivityLog'; +import { Dashboard } from '../src/screens/Dashboard'; +import { Help } from '../src/screens/Help'; +import { Policies } from '../src/screens/Policies'; +import { Roles } from '../src/screens/Roles'; +import { Team } from '../src/screens/Team'; + +function seed(roles: number, over = {}) { + mockStore.mockReturnValue({ + vaultId: VAULT, + address: ME, + book: [], + ready: true, + profile: { name: 'Olajumoke Emmanuel', email: 'tayo@company.com' }, + }); + mockView.mockReturnValue({ + data: view(roles, over), + isLoading: false, + error: null, + refetch: jest.fn(), + isRefetching: false, + }); +} + +afterEach(() => jest.clearAllMocks()); + +describe('the front door', () => { + it('names the product and where it runs', async () => { + seed(Role.OWNER); + const r = await renderScreen(); + expect(r.queryByText('Skur')).not.toBeNull(); + expect(r.queryByText('Secure. Shared. On Sui.')).not.toBeNull(); + }); + + it('walks the tour and only then continues', async () => { + const done = jest.fn(); + const r = await renderScreen(); + expect(r.queryByText('Multi-Signature Security')).not.toBeNull(); + expect(r.queryByText('Transaction Simulation')).not.toBeNull(); + await fireEvent.press(r.getByTestId('tour-next')); + expect(done).not.toHaveBeenCalled(); + await fireEvent.press(r.getByTestId('tour-next')); + await fireEvent.press(r.getByTestId('tour-next')); + expect(done).toHaveBeenCalledTimes(1); + }); + + it('offers create and join as different paths', async () => { + const create = jest.fn(); + const join = jest.fn(); + const r = await renderScreen(); + await fireEvent.press(r.getByTestId('create-org')); + await fireEvent.press(r.getByTestId('join-org')); + expect(create).toHaveBeenCalled(); + expect(join).toHaveBeenCalled(); + }); +}); + +describe('building the roster', () => { + it('warns when no guardian is on it', async () => { + seed(Role.OWNER); + const r = await renderScreen(); + expect(r.queryByText(/No guardian yet/)).not.toBeNull(); + }); + + it('lists you first, as the owner', async () => { + seed(Role.OWNER); + const r = await renderScreen(); + expect(r.queryByText(/Olajumoke Emmanuel \(You\)/)).not.toBeNull(); + expect(r.queryByText('Owner')).not.toBeNull(); + }); +}); + +describe('the dashboard', () => { + it('counts what is pending and who is on the vault', async () => { + seed(Role.APPROVER); + const r = await renderScreen( + , + ); + expect(r.queryByText('Pending approvals')).not.toBeNull(); + expect(r.queryByText('Active members')).not.toBeNull(); + expect(r.queryByText('Total treasury value')).not.toBeNull(); + }); + + it('never shows a fiat figure it cannot know', async () => { + seed(Role.APPROVER); + const r = await renderScreen( + , + ); + expect(r.queryAllByText(/\$/).length).toBe(0); + }); +}); + +describe('the team', () => { + it('offers Invite Member to an owner', async () => { + seed(Role.OWNER); + const r = await renderScreen(); + expect(r.queryByTestId('invite-member')).not.toBeNull(); + }); + + it('does not offer it to a signer, and says why', async () => { + seed(Role.APPROVER); + const r = await renderScreen(); + expect(r.queryByTestId('invite-member')).toBeNull(); + expect(r.queryByText(/takes an owner/)).not.toBeNull(); + }); + + it('marks the guardian as a separate plane in the role guide', async () => { + const r = await renderScreen(); + expect(r.queryByText('Guardian')).not.toBeNull(); + expect(r.queryByText(/refuses to combine/)).not.toBeNull(); + expect(r.queryByText('Viewer')).not.toBeNull(); + }); +}); + +describe('spending policies', () => { + it('shows the bands with the vault’s own numbers', async () => { + seed(Role.OWNER); + const r = await renderScreen(); + // Fixture policy: 1 / 2 / 3 approvals, unknown-recipient timelock. + expect(r.queryByText(/1 approval required, no wait/)).not.toBeNull(); + expect(r.queryByText('Unknown recipients')).not.toBeNull(); + }); + + it('offers editing only to an owner', async () => { + seed(Role.APPROVER); + const r = await renderScreen(); + expect(r.queryByTestId('edit-policies')).toBeNull(); + }); +}); + +describe('activity logs', () => { + it('derives entries from what actually happened', async () => { + seed(Role.APPROVER, { + proposals: [proposal({ approvals: [ME], status: Status.EXECUTED })], + }); + const r = await renderScreen(); + expect(r.queryByText(/Payment opened/)).not.toBeNull(); + expect(r.queryByText(/Approved by/)).not.toBeNull(); + expect(r.queryByText(/Payment executed/)).not.toBeNull(); + }); +}); + +describe('help', () => { + it('answers the lost-phone question without hiding the loss', async () => { + const r = await renderScreen(); + expect(r.queryByText('What happens if I lose my phone?')).not.toBeNull(); + expect(r.queryByText(/The key is gone with it/)).not.toBeNull(); + expect(r.queryByText('Chat with Support')).not.toBeNull(); + }); +}); diff --git a/mobile/test/fixtures.ts b/mobile/test/fixtures.ts new file mode 100644 index 0000000..ef8cd7e --- /dev/null +++ b/mobile/test/fixtures.ts @@ -0,0 +1,121 @@ +import { + Kind, + Mode, + Role, + Status, + Tier, + type Policy, + type Proposal, + type VaultView, +} from '@skur/sdk'; + +export const ME = `0x${'11'.repeat(32)}`; +export const OTHER = `0x${'22'.repeat(32)}`; +export const BOB = `0x${'33'.repeat(32)}`; +export const SUI = '0x2::sui::SUI'; +export const VAULT = `0x${'ab'.repeat(32)}`; + +const HOUR = 3_600_000; +const DAY = 86_400_000; + +export const policy: Policy = { + approvalsLow: 1, + approvalsHigh: 2, + approvalsCritical: 3, + governanceThreshold: 2, + guardianThreshold: 1, + guardianRequiredCritical: true, + delayHigh: HOUR, + delayCritical: DAY, + recipientActivationDelay: 12 * HOUR, + policyChangeDelay: 2 * DAY, + recoveryDelay: 3 * DAY, + proposalTtl: 30 * DAY, + highExposureBps: 1_000, + criticalExposureBps: 2_500, + hardBlockExposureBps: 9_000, + envelopeBps: 3_000, + envelopeWindow: DAY, +}; + +export function proposal(over: Partial = {}): Proposal { + const now = Date.now(); + return { + id: 1n, + kind: Kind.TRANSFER, + status: Status.PENDING, + proposer: ME, + createdAt: now - HOUR, + executableAt: now - 1_000, // executable by default; tests override to test the timelock + expiresAt: now + 10 * DAY, + reqApprovals: 2, + reqGuardians: 0, + tier: Tier.HIGH, + reasons: 1, + exposureBps: 500, + policyVersion: 1n, + reductionMask: 0, + approvals: [], + confirmations: [], + rejections: [], + asset: SUI, + amount: 5_000_000_000n, + recipient: BOB, + memo: 'invoice 1182', + newPolicy: null, + newLimits: null, + member: `0x${'00'.repeat(32)}`, + memberPrev: `0x${'00'.repeat(32)}`, + memberRoles: 0, + trustLevel: 0, + targetMode: 0, + ...over, + }; +} + +/** A vault where the device's key holds exactly `roles`. */ +export function view(roles: number, over: Partial = {}): VaultView { + return { + vault: { + id: VAULT, + name: 'Skur Demo Treasury', + policy, + policyVersion: 1n, + mode: Mode.NORMAL, + postureReasons: 0, + ownerCount: 2, + approverCount: 3, + executorCount: 2, + guardianCount: 1, + nextProposal: 2n, + pendingCount: 1, + createdAt: Date.now() - 30 * DAY, + tables: { members: '0x1', limits: '0x2', funds: '0x3', velocity: '0x4', recipients: '0x5', proposals: '0x6' }, + sizes: { members: 3, limits: 1, funds: 1, velocity: 1, recipients: 1, proposals: 1 }, + ...over.vault, + }, + members: [ + { address: ME, roles }, + { address: OTHER, roles: Role.OWNER | Role.APPROVER | Role.EXECUTOR }, + ], + assets: [ + { + coinType: SUI, + limits: { + approved: true, + lowMax: 1_000_000_000n, + highMax: 10_000_000_000n, + perTxMax: 50_000_000_000n, + dailyMax: 100_000_000_000n, + }, + balance: 1_000_000_000_000n, + velocity: { dayAnchor: Date.now(), daySpent: 0n, envelopeAnchor: Date.now(), envelopeSpent: 0n, envelopeBasis: 1_000_000_000_000n }, + }, + ], + recipients: [ + { address: BOB, trust: 2, registeredAt: Date.now() - DAY, activatesAt: Date.now() - HOUR, paidCount: 3, lastPaid: Date.now() - HOUR, label: 'Payroll' }, + ], + proposals: [proposal()], + ...over, + }; +} diff --git a/mobile/test/harness.tsx b/mobile/test/harness.tsx new file mode 100644 index 0000000..8b052eb --- /dev/null +++ b/mobile/test/harness.tsx @@ -0,0 +1,31 @@ +import type { ReactElement } from 'react'; +import { cleanup, render } from '@testing-library/react-native'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { SafeAreaProvider } from 'react-native-safe-area-context'; +import { ThemeProvider } from '../src/state/theme'; + +/** + * The providers every screen assumes, and nothing else. + * + * `render` is asynchronous in React Native Testing Library 14 — it returns a promise, not the + * queries. Forgetting to await it leaves `screen` insisting nothing has been rendered. + */ +export async function renderScreen(ui: ReactElement) { + // Tear down anything still mounted before mounting the next tree. `render` is asynchronous in + // RNTL 14, and the automatic cleanup that runs after a test can land in the middle of the next + // one's render — which unmounts the tree the test is about to query and leaves it empty. + await cleanup(); + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + return await render( + + + {ui} + + , + ); +} diff --git a/mobile/test/lib.test.tsx b/mobile/test/lib.test.tsx new file mode 100644 index 0000000..1e00340 --- /dev/null +++ b/mobile/test/lib.test.tsx @@ -0,0 +1,163 @@ +import { Text } from 'react-native'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { parseLink } from '../src/lib/links'; +import { StoreProvider, useStore } from '../src/state/store'; +import { deserialize, serialize } from '../src/lib/persist'; +import { ErrorBoundary } from '../src/components/ErrorBoundary'; +import { OfflineBanner, TxProgress } from '../src/components/ui'; +import { renderScreen } from './harness'; +import { act, fireEvent, render } from '@testing-library/react-native'; + +describe('the links the app answers to', () => { + it('parses a vault link', () => { + expect(parseLink('skur://vault/0xabc123def456')).toEqual({ kind: 'vault', vaultId: '0xabc123def456' }); + }); + + it('parses a proposal in the open vault', () => { + expect(parseLink('skur://proposal/42')).toEqual({ kind: 'proposal', proposalId: '42' }); + }); + + it('parses a vault and a proposal together — the shape a push notification carries', () => { + expect(parseLink('skur://vault/0xabc123def456/proposal/7')).toEqual({ + kind: 'vault', + vaultId: '0xabc123def456', + proposalId: '7', + }); + }); + + it('ignores the dev client’s own URLs instead of treating them as navigation', () => { + expect(parseLink('exp+skur://expo-development-client/?url=http%3A%2F%2Flocalhost')).toBeNull(); + }); + + it('refuses what only looks like a link', () => { + expect(parseLink('skur://vault/not-an-id')).toBeNull(); + expect(parseLink('skur://proposal/42/extra')).toBeNull(); + expect(parseLink('skur://vault/0xabc/proposal/NaN')).toBeNull(); + expect(parseLink('https://vault/0xabc123def456')).toBeNull(); + expect(parseLink('')).toBeNull(); + }); +}); + +describe('the cache that survives a restart', () => { + it('round-trips bigints, which plain JSON would throw on', () => { + const data = { balance: 1_000_000_000_000n, nested: [{ amount: 42n }], name: 'Treasury', n: 7 }; + expect(deserialize(serialize(data))).toEqual(data); + }); + + it('leaves everything else exactly as it was', () => { + const data = { s: 'text', b: true, x: null, list: [1, 2, 3] }; + expect(deserialize(serialize(data))).toEqual(data); + }); +}); + +describe('the last line between a crash and a white screen', () => { + const Bomb = () => { + throw new Error('exploded in render'); + }; + + it('shows what happened and that nothing was signed', async () => { + // The boundary logs the crash; the test does not need to also print it. + const quiet = jest.spyOn(console, 'error').mockImplementation(() => undefined); + const r = await render( + + + , + ); + expect(r.queryByText(/Something broke in the app/)).not.toBeNull(); + expect(r.queryByText(/Nothing was signed/)).not.toBeNull(); + expect(r.queryByText(/exploded in render/)).not.toBeNull(); + quiet.mockRestore(); + }); + + it('restart remounts and recovers when the cause was transient', async () => { + const quiet = jest.spyOn(console, 'error').mockImplementation(() => undefined); + let shouldThrow = true; + const Flaky = () => { + if (shouldThrow) throw new Error('transient'); + return recovered; + }; + const r = await render( + + + , + ); + shouldThrow = false; + await fireEvent.press(r.getByText('Restart')); + expect(r.queryByText('recovered')).not.toBeNull(); + quiet.mockRestore(); + }); +}); + +describe('a transaction, step by step', () => { + it('is silent when nothing is happening', async () => { + // The harness providers render wrapper views, so the tree is not null — but nothing in it + // should say anything. + const r = await renderScreen(); + expect(r.queryByText(/./)).toBeNull(); + }); + + it('names the step it is on', async () => { + const r = await renderScreen(); + expect(r.queryByText('Sign and send')).not.toBeNull(); + expect(r.queryByText('Settle on the network')).not.toBeNull(); + }); + + it('a declined fingerprint is a decision, not an error', async () => { + const r = await renderScreen(); + expect(r.queryByText('Cancelled.')).not.toBeNull(); + }); + + it('an error is the contract’s sentence, not a code', async () => { + const r = await renderScreen( + , + ); + expect(r.queryByText(/activation delay/)).not.toBeNull(); + }); +}); + +describe('being offline', () => { + it('states the age of what is on screen', async () => { + const r = await renderScreen(); + expect(r.queryByText(/Offline — showing the vault as of 9:30/)).not.toBeNull(); + }); + + it('says only that it is offline when there is nothing cached to date', async () => { + const r = await renderScreen(); + expect(r.queryByText('Offline')).not.toBeNull(); + }); +}); + +describe('restoring the open vault', () => { + const EVM = '0xD3f026024e015e1eC841a88D983883d4a017ED52'; // 40 hex — an EVM address + const SUI = '0x' + 'ab'.repeat(32); // 64 hex — a normalised Sui object id + + async function restoredVaultId(stored: string | null): Promise { + if (stored === null) await AsyncStorage.removeItem('skur.vault'); + else await AsyncStorage.setItem('skur.vault', stored); + let seen: string | null = null; + function Probe() { + const s = useStore(); + if (s.ready) seen = s.vaultId; + return null; + } + await renderScreen( + + + , + ); + // The store loads asynchronously; flush until ready. + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)); + }); + return seen; + } + + it('restores a Sui object id', async () => { + expect(await restoredVaultId(SUI)).toBe(SUI); + }); + + it('discards an EVM address left by an earlier build', async () => { + expect(await restoredVaultId(EVM)).toBeNull(); + expect(await AsyncStorage.getItem('skur.vault')).toBeNull(); + }); +}); diff --git a/mobile/test/mocks/client.ts b/mobile/test/mocks/client.ts new file mode 100644 index 0000000..cc630e3 --- /dev/null +++ b/mobile/test/mocks/client.ts @@ -0,0 +1,5 @@ +/** No network in tests. Every read is supplied by the fixtures instead. */ +export const client = {} as never; +export function getClient() { + throw new Error('tests must not reach the network'); +} diff --git a/mobile/test/roles.test.tsx b/mobile/test/roles.test.tsx new file mode 100644 index 0000000..5a32f17 --- /dev/null +++ b/mobile/test/roles.test.tsx @@ -0,0 +1,158 @@ +import { Role, Status, Tier } from '@skur/sdk'; +import { renderScreen } from './harness'; +import { BOB, ME, VAULT, proposal, view } from './fixtures'; + +/** + * What a signer is offered, by role. + * + * This is the security-critical part of the interface. The contract refuses an action the caller + * has no role for, so an interface that offers it is not unsafe — it is worse than that, it is + * lying to the person holding the phone. Each case below asserts both halves: what appears, and + * what does not. + */ +const mockStore = jest.fn(); +const mockView = jest.fn(); +const mockRun = jest.fn(async () => 'digest'); + +jest.mock('../src/state/store', () => ({ useStore: () => mockStore() })); +jest.mock('../src/hooks/useVault', () => ({ + useVaultView: () => mockView(), + useTransferPreview: () => ({ data: undefined, isLoading: false, error: null }), +})); +jest.mock('../src/hooks/useTx', () => ({ + useTx: () => ({ run: mockRun, state: { phase: 'idle' }, reset: jest.fn(), busy: false }), +})); + +import { TxDetail } from '../src/screens/TxDetail'; + +/** Renders the screen for a device key holding exactly `roles`, and returns a `has` predicate. */ +async function show(roles: number, over = {}) { + mockStore.mockReturnValue({ vaultId: VAULT, address: ME, book: [], ready: true }); + mockView.mockReturnValue({ data: view(roles, over), isLoading: false, error: null, refetch: jest.fn() }); + const r = await renderScreen(); + // queryAllByText: a fact may legitimately appear twice — once in the review-before-signing + // card and once in the what-it-needs card. The assertion is "the screen says it", not "once". + return Object.assign((label: string | RegExp) => r.queryAllByText(label).length > 0, { + action: (id: string) => r.queryByTestId(`action-${id}`) !== null, + enabled: (id: string) => { + const node = r.queryByTestId(`action-${id}`); + return node !== null && node.props.accessibilityState?.disabled !== true; + }, + }); +} + +describe('a payment, by the role the device holds', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('offers an approver Approve and Reject, and not Execute', async () => { + const has = await show(Role.APPROVER); + expect(has.action('approve')).toBe(true); + expect(has.action('reject')).toBe(true); + expect(has.action('execute')).toBe(false); + }); + + it('offers an executor Execute, and not Approve', async () => { + const has = await show(Role.EXECUTOR); + expect(has.action('execute')).toBe(true); + expect(has.action('approve')).toBe(false); + expect(has.action('reject')).toBe(false); + }); + + it('offers both to someone holding both roles', async () => { + const has = await show(Role.APPROVER | Role.EXECUTOR); + expect(has.action('approve')).toBe(true); + expect(has.action('execute')).toBe(true); + }); + + it('never offers a guardian Approve on a payment', async () => { + // A guardian holds no treasury role at all — the contract refuses to give it one — so the + // only things it can do here are confirm and veto. + const has = await show(Role.GUARDIAN, { proposals: [proposal({ tier: Tier.CRITICAL, reqGuardians: 1 })] }); + expect(has.action('approve')).toBe(false); + expect(has.action('reject')).toBe(false); + expect(has.action('execute')).toBe(false); + expect(has.action('confirm')).toBe(true); + expect(has.action('veto')).toBe(true); + }); + + it('offers an agent nothing at all', async () => { + // A proposer can open a payment and that is the whole of its authority. If a compromised + // agent key could be nudged into approving from this screen, the role would be pointless. + const has = await show(Role.PROPOSER); + expect(has.action('approve')).toBe(false); + expect(has.action('reject')).toBe(false); + expect(has.action('execute')).toBe(false); + expect(has.action('veto')).toBe(false); + expect(has(/holds no role that can act/)).toBe(true); + }); + + it('tells a stranger they can read it and nothing more', async () => { + const has = await show(0); + expect(has(/holds no role that can act/)).toBe(true); + }); + + it('does not offer a veto on a routine payment', async () => { + // Vetoable means critical, security-reducing, a recovery or a relaxation. A routine payment + // is none of those, and showing the button would promise something the vault would refuse. + const has = await show(Role.GUARDIAN, { proposals: [proposal({ tier: Tier.LOW, reqGuardians: 0 })] }); + expect(has.action('veto')).toBe(false); + }); + + it('offers a veto on a change that weakens the vault', async () => { + const has = await show(Role.GUARDIAN, { proposals: [proposal({ reductionMask: 1 })] }); + expect(has.action('veto')).toBe(true); + }); +}); + +describe('what the screen says it still needs', () => { + afterEach(() => jest.clearAllMocks()); + + it('counts only approvals from addresses that still hold the role', async () => { + // The vault recounts at execution, so a removed signer's approval stops counting. The screen + // has to say the same thing, or someone waits for an execution that cannot happen. + const stranger = `0x${'99'.repeat(32)}`; + const has = await show(Role.EXECUTOR, { + proposals: [proposal({ approvals: [ME, stranger], reqApprovals: 2 })], + }); + // ME holds EXECUTOR here, not APPROVER, and the stranger is not a member at all — so zero of + // the two recorded approvals count. + expect(has(/0 of 2/)).toBe(true); + expect(has(/2 no longer count/)).toBe(true); + }); + + it('will not let an executor execute before the waiting period', async () => { + const has = await show(Role.EXECUTOR, { + proposals: [proposal({ executableAt: Date.now() + 3_600_000, approvals: [], reqApprovals: 0 })], + }); + expect(has(/Waits until/)).toBe(true); + // Offered, but not pressable — the vault would refuse it. + expect(has.enabled('execute')).toBe(false); + }); + + it('says a critical payment still needs its guardian', async () => { + const has = await show(Role.EXECUTOR, { + proposals: [proposal({ tier: Tier.CRITICAL, reqGuardians: 1, confirmations: [] })], + }); + expect(has('0 of 1')).toBe(true); + }); +}); + +describe('a settled proposal', () => { + afterEach(() => jest.clearAllMocks()); + + it('offers no actions once it has executed', async () => { + const has = await show(Role.APPROVER | Role.EXECUTOR | Role.GUARDIAN, { + proposals: [proposal({ status: Status.EXECUTED })], + }); + expect(has.action('approve')).toBe(false); + expect(has.action('execute')).toBe(false); + expect(has.action('veto')).toBe(false); + }); + + it('says plainly when the breaker refused it', async () => { + const has = await show(Role.APPROVER, { proposals: [proposal({ status: Status.BLOCKED })] }); + expect(has(/Refused by the circuit breaker/)).toBe(true); + }); +}); diff --git a/mobile/test/screens.test.tsx b/mobile/test/screens.test.tsx new file mode 100644 index 0000000..fb49e66 --- /dev/null +++ b/mobile/test/screens.test.tsx @@ -0,0 +1,171 @@ +import { Mode, Role, Tier } from '@skur/sdk'; +import { fireEvent } from '@testing-library/react-native'; + +// `fireEvent` is asynchronous in React Native Testing Library 14, like `render`. Leaving one +// un-awaited does not fail the test that fired it — it leaves React's work queue mid-flight, and +// the next test's render resolves to an empty tree. Every call here is awaited for that reason. +import { renderScreen } from './harness'; +import { BOB, ME, VAULT, policy, proposal, view } from './fixtures'; + +/** + * The rest of the screens: what they show, what they refuse, and what they actually do when + * pressed. The last part matters most — a button that renders correctly and calls the wrong + * builder is worse than one that does not render. + */ +const mockStore = jest.fn(); +const mockView = jest.fn(); +const mockPreview = jest.fn(); +// Typed with the hook's real signature, so the assertions on what it was called with are checked +// rather than inferred from an empty tuple. +const mockRun = jest.fn, [() => unknown, string]>(async () => 'digest'); + +jest.mock('../src/state/store', () => ({ useStore: () => mockStore() })); +jest.mock('../src/hooks/useVault', () => ({ + useVaultView: () => mockView(), + useTransferPreview: () => mockPreview(), +})); +jest.mock('../src/hooks/useTx', () => ({ + useTx: () => ({ run: mockRun, state: { phase: 'idle' }, reset: jest.fn(), busy: false }), +})); + +import { Home } from '../src/screens/Home'; +import { Security } from '../src/screens/Security'; +import { Send } from '../src/screens/Send'; +import { TxDetail } from '../src/screens/TxDetail'; + +function seed(roles: number, over = {}) { + mockStore.mockReturnValue({ vaultId: VAULT, address: ME, book: [], ready: true }); + mockView.mockReturnValue({ data: view(roles, over), isLoading: false, error: null, refetch: jest.fn(), isRefetching: false }); + mockPreview.mockReturnValue({ data: undefined, isLoading: false, error: null }); +} + +afterEach(() => jest.clearAllMocks()); + + +describe('the treasury screen', () => { + it('says plainly when the vault is frozen, and what it takes to reopen', async () => { + seed(Role.APPROVER, { vault: { ...view(Role.APPROVER).vault, mode: Mode.LOCKDOWN } }); + const r = await renderScreen(); + expect(r.queryByText(/This vault is frozen/)).not.toBeNull(); + expect(r.queryByText(/Deposits still work/)).not.toBeNull(); + }); + + it('shows what the policy enforces, not a generic summary', async () => { + seed(Role.APPROVER); + const r = await renderScreen(); + expect(r.queryByText(/1 approval, no wait/)).not.toBeNull(); + expect(r.queryByText(/3 approvals \+ 1 guardian/)).not.toBeNull(); + }); +}); + +describe('opening a payment', () => { + it('shows the vault’s own numbers rather than computing its own', async () => { + seed(Role.APPROVER); + // The chain says critical, three approvals, a day's wait. The screen must say exactly that, + // even though the amount alone would not imply it. + mockPreview.mockReturnValue({ + data: { tier: Tier.CRITICAL, reasons: 256, exposureBps: 2600, reqApprovals: 3, reqGuardians: 1, delay: 86_400_000 }, + isLoading: false, + error: null, + }); + const r = await renderScreen(); + await fireEvent.changeText(r.getByPlaceholderText('0x…'), BOB); + await fireEvent.changeText(r.getByPlaceholderText('0.00'), '1'); + expect(r.queryByText('Critical')).not.toBeNull(); + expect(r.queryByText('3')).not.toBeNull(); + expect(r.queryByText(/never paid this address/)).not.toBeNull(); + }); + + it('refuses an amount over the per-payment cap, and says why', async () => { + seed(Role.APPROVER); + const r = await renderScreen(); + await fireEvent.changeText(r.getByPlaceholderText('0x…'), BOB); + // The cap is 50 SUI; ask for 60. + await fireEvent.changeText(r.getByPlaceholderText('0.00'), '60'); + expect(r.queryByText(/The vault would refuse this/)).not.toBeNull(); + expect(r.queryByText(/per-payment cap/)).not.toBeNull(); + }); + + it('refuses more than the vault holds', async () => { + seed(Role.APPROVER); + const r = await renderScreen(); + await fireEvent.changeText(r.getByPlaceholderText('0x…'), BOB); + await fireEvent.changeText(r.getByPlaceholderText('0.00'), '999999'); + expect(r.queryByText(/The vault would refuse this/)).not.toBeNull(); + }); + + it('tells a guardian it cannot open payments at all', async () => { + seed(Role.GUARDIAN); + const r = await renderScreen(); + expect(r.queryByText(/cannot open payments in this vault/)).not.toBeNull(); + }); + + it('lets an agent open one, because proposing is the whole of its authority', async () => { + seed(Role.PROPOSER); + const r = await renderScreen(); + expect(r.queryByText(/cannot open payments in this vault/)).toBeNull(); + }); +}); + +describe('the security screen', () => { + it('offers the brake to a guardian', async () => { + seed(Role.GUARDIAN); + const r = await renderScreen(); + expect(r.queryByText('Freeze the vault')).not.toBeNull(); + expect(r.queryByText('Raise to Elevated')).not.toBeNull(); + }); + + it('does not offer it to a plain approver', async () => { + seed(Role.APPROVER); + const r = await renderScreen(); + expect(r.queryByText('Freeze the vault')).toBeNull(); + expect(r.queryByText(/takes an owner or a guardian/)).not.toBeNull(); + }); + + it('flags a vault with no guardian as bad, not as a warning', async () => { + const base = view(Role.OWNER); + seed(Role.OWNER, { vault: { ...base.vault, guardianCount: 0 } }); + const r = await renderScreen(); + expect(r.queryByText(/No guardian: nobody independent/)).not.toBeNull(); + }); + + it('states the assumptions behind the maximum-loss numbers', async () => { + seed(Role.OWNER); + const r = await renderScreen(); + expect(r.queryByText(/never a guarantee/)).not.toBeNull(); + }); +}); + +describe('what a press actually does', () => { + it('approving builds an approve transaction for this proposal', async () => { + seed(Role.APPROVER); + const r = await renderScreen(); + await fireEvent.press(r.getByTestId('action-approve')); + expect(mockRun).toHaveBeenCalledTimes(1); + // The hook is handed a builder and a reason; the reason is what the biometric prompt shows. + expect(mockRun.mock.calls[0][1]).toMatch(/Approve/); + }); + + it('rejecting is a different action, not a differently-labelled approve', async () => { + seed(Role.APPROVER); + const r = await renderScreen(); + await fireEvent.press(r.getByTestId('action-reject')); + expect(mockRun).toHaveBeenCalledTimes(1); + expect(mockRun.mock.calls[0][1]).toMatch(/Reject/); + }); + + it('a guardian’s veto asks to veto', async () => { + seed(Role.GUARDIAN, { proposals: [proposal({ tier: Tier.CRITICAL, reqGuardians: 1 })] }); + const r = await renderScreen(); + await fireEvent.press(r.getByTestId('action-veto')); + expect(mockRun.mock.calls[0][1]).toMatch(/Veto/); + }); + + it('does nothing when the action is disabled', async () => { + // Executable in an hour, so the button is present and must not fire. + seed(Role.EXECUTOR, { proposals: [proposal({ executableAt: Date.now() + 3_600_000 })] }); + const r = await renderScreen(); + await fireEvent.press(r.getByTestId('action-execute')); + expect(mockRun).not.toHaveBeenCalled(); + }); +}); diff --git a/mobile/test/setup.tsx b/mobile/test/setup.tsx new file mode 100644 index 0000000..6f33a9e --- /dev/null +++ b/mobile/test/setup.tsx @@ -0,0 +1,74 @@ + +/** + * Native modules the screens touch. Each mock is the smallest thing that lets the component tree + * render — the point of these tests is the app's own logic, not Expo's. + */ +jest.mock('expo-font', () => ({ useFonts: () => [true, null], isLoaded: () => true })); + +jest.mock('expo-constants', () => ({ + __esModule: true, + default: { expoConfig: { extra: { SKUR_NETWORK: 'testnet', SKUR_PACKAGE_ID: '0xpkg' } } }, +})); + +jest.mock('@react-native-async-storage/async-storage', () => { + const store = new Map(); + return { + __esModule: true, + default: { + getItem: jest.fn(async (k: string) => store.get(k) ?? null), + setItem: jest.fn(async (k: string, v: string) => void store.set(k, v)), + removeItem: jest.fn(async (k: string) => void store.delete(k)), + }, + }; +}); + +jest.mock('expo-clipboard', () => ({ setStringAsync: jest.fn(async () => true) })); + +jest.mock('expo-secure-store', () => ({ + WHEN_UNLOCKED_THIS_DEVICE_ONLY: 'whenUnlocked', + getItemAsync: jest.fn(async () => null), + setItemAsync: jest.fn(async () => undefined), + deleteItemAsync: jest.fn(async () => undefined), +})); + +jest.mock('expo-local-authentication', () => ({ + AuthenticationType: { FINGERPRINT: 1, FACIAL_RECOGNITION: 2 }, + hasHardwareAsync: jest.fn(async () => true), + isEnrolledAsync: jest.fn(async () => true), + supportedAuthenticationTypesAsync: jest.fn(async () => [2]), + authenticateAsync: jest.fn(async () => ({ success: true })), +})); + +// Silence the act() noise from react-query's background refetches; the tests await what matters. +const warn = console.warn; +console.warn = (...args: unknown[]) => { + if (typeof args[0] === 'string' && args[0].includes('not wrapped in act')) return; + warn(...args); +}; + +jest.mock('expo-haptics', () => ({ + notificationAsync: jest.fn(async () => undefined), + impactAsync: jest.fn(async () => undefined), + NotificationFeedbackType: { Success: 'success', Error: 'error' }, + ImpactFeedbackStyle: { Light: 'light' }, +})); + +jest.mock('expo-screen-capture', () => ({ usePreventScreenCapture: jest.fn() })); + +jest.mock('expo-network', () => ({ + useNetworkState: jest.fn(() => ({ isConnected: true, isInternetReachable: true })), +})); + +jest.mock('expo-notifications', () => ({ + setNotificationHandler: jest.fn(), + requestPermissionsAsync: jest.fn(async () => ({ granted: true })), + getExpoPushTokenAsync: jest.fn(async () => ({ data: 'ExponentPushToken[test]' })), + getLastNotificationResponseAsync: jest.fn(async () => null), + addNotificationResponseReceivedListener: jest.fn(() => ({ remove: jest.fn() })), +})); + +jest.mock('expo-linking', () => ({ + useURL: jest.fn(() => null), + openURL: jest.fn(async () => true), + createURL: jest.fn((p: string) => `skur://${p}`), +})); diff --git a/mobile/tsconfig.json b/mobile/tsconfig.json new file mode 100644 index 0000000..41d21d2 --- /dev/null +++ b/mobile/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "expo/tsconfig.base", + "compilerOptions": { + "strict": true, + "noEmit": true, + "jsx": "react-jsx", + "moduleResolution": "bundler", + "module": "esnext", + "skipLibCheck": true, + "types": [ + "jest", + "node" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules", + "ios", + "android", + ".expo" + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 893c621..bbe3ffd 100644 --- a/package.json +++ b/package.json @@ -2,15 +2,30 @@ "name": "skur", "private": true, "version": "0.1.0", - "description": "Programmable treasury security for onchain businesses.", + "description": "A programmable authorization and treasury security layer for Sui organizations.", "scripts": { - "contracts:build": "cd contracts && forge build", - "contracts:test": "cd contracts && forge test", - "contracts:slither": "cd contracts && slither . --filter-paths \"lib/|test/|script/\" --exclude-informational --exclude-optimization", - "abi": "python3 scripts/export-abi.py", - "web:dev": "cd web && npm run dev", - "web:build": "cd web && npm run build", - "web:test": "cd web && npm test", - "test": "npm run contracts:test && npm run web:test" + "android": "npm run mobile:android", + "app:build": "cd app && npm run build", + "app:dev": "cd app && npm run dev", + "dev": "npm run app:dev", + "indexer": "npm run server:dev", + "ios": "npm run mobile:ios", + "mobile:android": "cd mobile && npm run android", + "mobile:ios": "cd mobile && npm run ios", + "mobile:start": "cd mobile && npm start", + "move:build": "cd sui && sui move build", + "move:publish": "cd sui && scripts/publish.sh", + "move:test": "cd sui && sui move test", + "sdk:build": "cd sdk && npm run build", + "sdk:test": "cd sdk && npm test", + "server:dev": "cd server && npm run dev", + "server:migrate": "cd server && npm run migrate", + "start": "npm run mobile:start", + "sui:e2e": "cd sdk && npm run e2e", + "sui:seed": "cd sdk && npm run seed", + "test": "npm run move:test && npm run sdk:test", + "typecheck": "cd sdk && npm run typecheck && cd ../server && npm run typecheck && cd ../app && npm run typecheck && cd ../mobile && npm run typecheck", + "verify": "scripts/verify.sh", + "web": "npm run app:dev" } -} +} \ No newline at end of file diff --git a/scripts/export-abi.py b/scripts/export-abi.py deleted file mode 100755 index 1ae94f5..0000000 --- a/scripts/export-abi.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python3 -"""Copy contract ABIs from the Foundry build into the web app as typed constants.""" -import json, os, sys -root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -out = os.path.join(root, 'contracts', 'out') -targets = {'SkurVault': 'SkurVault.sol/SkurVault.json', 'SkurFactory': 'SkurFactory.sol/SkurFactory.json', 'SkurTestUSD': 'SkurTestUSD.sol/SkurTestUSD.json'} -os.makedirs(os.path.join(root, 'web', 'src', 'abi'), exist_ok=True) -for name, rel in targets.items(): - abi = json.load(open(os.path.join(out, rel)))['abi'] - with open(os.path.join(root, 'web', 'src', 'abi', f'{name}.ts'), 'w') as f: - f.write(f'// Generated from contracts/out/{rel} by scripts/export-abi.py. Do not edit.\n') - f.write(f'export const {name}Abi = {json.dumps(abi, indent=2)} as const;\n') - print(name, len(abi), 'entries') diff --git a/scripts/verify.sh b/scripts/verify.sh new file mode 100755 index 0000000..fb91741 --- /dev/null +++ b/scripts/verify.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Everything CI would run, in one command, so a red check for reasons outside the code does not +# leave you without a signal. Mirrors .github/workflows/sui.yml job for job. +# +set -uo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PASS=0; FAIL=0 +step() { + printf '\n\033[1m── %s\033[0m\n' "$1"; shift + if "$@"; then PASS=$((PASS+1)); printf ' \033[32mok\033[0m\n' + else FAIL=$((FAIL+1)); printf ' \033[31mFAILED\033[0m\n'; fi +} + +have() { command -v "$1" >/dev/null 2>&1; } + +if have sui; then + step "Move · build (must be warning-free)" bash -c " + cd '$ROOT/sui' + out=\$(sui move build 2>&1 | sed 's/\x1b\[[0-9;]*m//g') + echo \"\$out\" | grep -qE '^(warning|error)' && { echo \"\$out\" | grep -E '^(warning|error)' -A 4; exit 1; } + exit 0" + step "Move · 75 tests" bash -c "cd '$ROOT/sui' && sui move test 2>&1 | tail -1 | grep -q 'Test result: OK'" +else + printf '\n\033[33m── Move · skipped: the sui CLI is not on PATH\033[0m\n' + printf ' get it from https://github.com/MystenLabs/sui/releases\n' +fi + +step "SDK · typecheck" bash -c "cd '$ROOT/sdk' && npm run --silent typecheck" +step "SDK · build" bash -c "cd '$ROOT/sdk' && npm run --silent build" +step "SDK · tests" bash -c "cd '$ROOT/sdk' && npm run --silent test" + +step "Backend · typecheck" bash -c "cd '$ROOT/server' && DATABASE_URL=unused SKUR_PACKAGE_ID=0x0 npm run --silent typecheck" +step "Backend · tests" bash -c "cd '$ROOT/server' && npm run --silent test" + +# The migrations job needs a database. Skipped rather than failed when there is none, because a +# missing Postgres is not a broken migration. +if have pg_isready && pg_isready -q 2>/dev/null; then + step "Backend · migrations against a real Postgres" bash -c " + createdb skur_verify 2>/dev/null + cd '$ROOT/server' && DATABASE_URL=postgres://localhost:5432/skur_verify SKUR_PACKAGE_ID=0x0 \ + node --experimental-strip-types src/migrate.ts >/dev/null + dropdb skur_verify 2>/dev/null; exit 0" +else + printf '\n\033[33m── Migrations · skipped: no Postgres is running\033[0m\n' +fi + +step "Interface · typecheck" bash -c "cd '$ROOT/app' && npm run --silent typecheck" +step "Interface · build" bash -c "cd '$ROOT/app' && NEXT_TELEMETRY_DISABLED=1 npm run --silent build >/dev/null" + +step "Mobile · typecheck" bash -c "cd '$ROOT/mobile' && npm run --silent typecheck" +# The screens are rendered and pressed here. This environment has no Simulator GUI and no +# idb_companion, so there is no way to send a touch to a device — and a render-and-press suite is +# repeatable in a way a hand-driven simulator session never is. +step "Mobile · 28 screen tests" bash -c "cd '$ROOT/mobile' && npm run --silent test" +# Bundling is the check that matters for mobile: it proves the Sui SDK and its ESM exports resolve +# under Metro and Hermes, which no typecheck can tell you. +step "Mobile · bundles for iOS" bash -c "cd '$ROOT/mobile' && npx expo export --platform ios --output-dir \"\$(mktemp -d)\" >/dev/null 2>&1" + +printf '\n\033[1m%s passed, %s failed\033[0m\n' "$PASS" "$FAIL" +[ "$FAIL" -eq 0 ] || exit 1 diff --git a/sdk/package-lock.json b/sdk/package-lock.json new file mode 100644 index 0000000..5715373 --- /dev/null +++ b/sdk/package-lock.json @@ -0,0 +1,1936 @@ +{ + "name": "@skur/sdk", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@skur/sdk", + "version": "0.1.0", + "dependencies": { + "@mysten/sui": "^2.34.0" + }, + "devDependencies": { + "@types/node": "^24.19.1", + "typescript": "~6.0.2", + "vitest": "^3.2.7" + } + }, + "node_modules/@0no-co/graphql.web": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.3.4.tgz", + "integrity": "sha512-imSwulOeDQodRy/olQmVEo2PiY6ntjkZ9eiGdw6lMYylh/tay9b7MusyJBmEnkL8GiKRKr6ltr+D42mY5bd8Bg==", + "license": "MIT", + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "graphql": { + "optional": true + } + } + }, + "node_modules/@0no-co/graphqlsp": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/@0no-co/graphqlsp/-/graphqlsp-1.17.5.tgz", + "integrity": "sha512-wsygfTO3pdkz4idDw8BxdMdgB9AB8nuJDJTUwYy87BiZEZRXS4bEfn4oNl9VtxTo6j2lzX/gED+IvOgxsSy2Jg==", + "license": "MIT", + "dependencies": { + "@gql.tada/internal": "^1.2.1", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "typescript": "^5.0.0 || ^6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@gql.tada/cli-utils": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@gql.tada/cli-utils/-/cli-utils-1.9.4.tgz", + "integrity": "sha512-xQ9M+2Xxjxhz9IJi+7IMU/aOsEtuSiSNb4OgDMBUC/8AcSpo2vDtx71Aodsaif4qSwuTb0YFVfDwC+ESqiZSHg==", + "license": "MIT", + "dependencies": { + "@0no-co/graphqlsp": "^1.17.3", + "@gql.tada/internal": "1.2.2", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" + }, + "peerDependencies": { + "@0no-co/graphqlsp": "^1.16.0", + "@gql.tada/svelte-support": "1.0.3", + "@gql.tada/vue-support": "1.0.3", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@gql.tada/svelte-support": { + "optional": true + }, + "@gql.tada/vue-support": { + "optional": true + } + } + }, + "node_modules/@gql.tada/internal": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@gql.tada/internal/-/internal-1.2.2.tgz", + "integrity": "sha512-4lZcElPP6MC8Ct8KN70LR2WQsHjbAsyAmTGG09VsOPhx738UFIYaL0S1XiI2pqq2tj/sDs/y3b9jvKoWGL7iuQ==", + "license": "MIT", + "dependencies": { + "@0no-co/graphql.web": "^1.3.1" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "license": "MIT", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@mysten/bcs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@mysten/bcs/-/bcs-2.1.2.tgz", + "integrity": "sha512-fv6lyuZMTHN0xRlu4GYiV/UXUrdJDOHbXm1KfZYEU0sy/t+22yTrTVtWnAQv1Km7eyB3qHhwIrAG2aem4JMocw==", + "license": "Apache-2.0", + "dependencies": { + "@mysten/utils": "^0.4.1", + "@scure/base": "^2.3.0" + } + }, + "node_modules/@mysten/sui": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@mysten/sui/-/sui-2.34.0.tgz", + "integrity": "sha512-Qf/xt10YIJv3Yo0hQTU70xPgimFfw2mudBzI/dylGXZNuhuQVmkmxUm0fz49BFPOFvBxWc/MRFl204zwpwRnXw==", + "license": "Apache-2.0", + "dependencies": { + "@graphql-typed-document-node/core": "^3.2.0", + "@mysten/bcs": "^2.1.2", + "@mysten/utils": "^0.4.1", + "@noble/curves": "^2.3.0", + "@noble/hashes": "^2.3.0", + "@protobuf-ts/grpcweb-transport": "^2.11.1", + "@protobuf-ts/runtime": "^2.11.1", + "@protobuf-ts/runtime-rpc": "^2.11.1", + "@scure/base": "^2.3.0", + "@scure/bip32": "^2.3.0", + "@scure/bip39": "^2.3.0", + "gql.tada": "^1.11.3", + "graphql": "^17.0.2", + "poseidon-lite": "0.3.0", + "valibot": "^1.4.2" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@mysten/utils": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@mysten/utils/-/utils-0.4.1.tgz", + "integrity": "sha512-gffLJ84CbqG/CcOPuVlwXIRu37tFn0BtUx7CQtbLwJzWjUE7FU2Gq8BXWjzxImnvufcyLTaNWtCPG6wq22Zhog==", + "license": "Apache-2.0", + "dependencies": { + "@scure/base": "^2.3.0" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@noble/curves": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.4.0.tgz", + "integrity": "sha512-P4/62zrgfH33CneE3Dn4WhJVA22YUU0eR51wKIan4NVRvwsA0YnPTwWGpNbpuacSujmSFLvyzpyuR30+fbq2Ew==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.4.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz", + "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@protobuf-ts/grpcweb-transport": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/grpcweb-transport/-/grpcweb-transport-2.11.1.tgz", + "integrity": "sha512-1W4utDdvOB+RHMFQ0soL4JdnxjXV+ddeGIUg08DvZrA8Ms6k5NN6GBFU2oHZdTOcJVpPrDJ02RJlqtaoCMNBtw==", + "license": "Apache-2.0", + "dependencies": { + "@protobuf-ts/runtime": "^2.11.1", + "@protobuf-ts/runtime-rpc": "^2.11.1" + } + }, + "node_modules/@protobuf-ts/runtime": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime/-/runtime-2.11.1.tgz", + "integrity": "sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@protobuf-ts/runtime-rpc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime-rpc/-/runtime-rpc-2.11.1.tgz", + "integrity": "sha512-4CqqUmNA+/uMz00+d3CYKgElXO9VrEbucjnBFEjqI4GuDrEQ32MaI3q+9qPBvIGOlL4PmHXrzM32vBPWRhQKWQ==", + "license": "Apache-2.0", + "dependencies": { + "@protobuf-ts/runtime": "^2.11.1" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.0.tgz", + "integrity": "sha512-FLLpX941CD/7aBvX3GqLGBM9OvY5PGXTryPJkG5r5/OuBpZpV5k45Uo3RxAvqd35tsyc3hH1bhtz921IKzoIzg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.0.tgz", + "integrity": "sha512-iI32XM/jnDYJsvx5ai/za/7gWO9XoSeFFtU06/ZCaRgyilfJOjaopD3nOpk2WrTCGOyeDt4hleUiGOAnd5tHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.0.tgz", + "integrity": "sha512-dRBZlbt2sEtF8Mn4MODThze9fIPdDoOhVSlXMAKy5dQbOYK9pOOoPY4Fat3OTtr0+8zbJo5TTZwlsfxY/kDNJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.0.tgz", + "integrity": "sha512-Y3whrJOMucfkhgNkvcM93M6j8C53iEAMmPMYD99C6G1kUy4ELigLWk9ar2/4aCBSRgsql0zdlOemLgW2m5GZgQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.0.tgz", + "integrity": "sha512-S6S1GEzPjFF6IONYJX3AKt/799+8RgBgCcwRnUo+Ep1IvBxUbw5M51iU0TDdK2NWWVnSvCQ8yi6Em0fGASMOng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.0.tgz", + "integrity": "sha512-Ed1DPoFENkXj4IGzSB12SesV40iP6LxCCrbTIkSGIQBpOOxpBM+0kj62lmaeIHfMWTDoddqTOtqc7s8LYfn+Pw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.0.tgz", + "integrity": "sha512-J/6xhutJPtFyIkmHdf4olOdHHKH5kOJNumojdWn+Elv5ICEYTVOa362Vw27TxsK0BLzb1le3EDlO3UbvFK1lnQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.0.tgz", + "integrity": "sha512-G4L/Nnx59d2LvDoV/P4S5lZ7oVLT6hodOHMacqjiq2LL2gts86KbkEBTYL53+fBt0odhf7XPIbIATGoYiJctxg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.0.tgz", + "integrity": "sha512-zF16pvJDEUksGYRbfnXne7pdklAyj2XDmJeYKgP93oq0cUyX9Y8O7lUo1R+IY1GagP/YBvCEDCe7eEcAKpw//g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.0.tgz", + "integrity": "sha512-nIggWn8n+w12+R5lsV+PYYoUY8vuObxPAQqJI7WES+9v0qspYuAHoZMfYM0yLxlipasT5BY9ht5W8t5c/rSCkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.0.tgz", + "integrity": "sha512-WLOTiHPzszP0dxIMsHpFeDSOSB5I49Mj8K4+yKE3H1X/D0JRG5XZ5jrctJLMjnUc/LUzaKiwq6EHWNJ8pup8PA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.0.tgz", + "integrity": "sha512-35RK9aRhh7g7Pmeu5gQ01aCvrhXzj9LoBE6xtUcEH1ns+nyTaSzxSpADmBQINu4QiTDGvmQYwo1j5hlg1rRXFw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.0.tgz", + "integrity": "sha512-RqTfnpoTjuWfPAmqK5p2edt2AMCDJWm6oTn7sKfmyx+6tRga0ip05SJ977o6udTcIiDeF/CJMUfkaN4gtWt6Aw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.0.tgz", + "integrity": "sha512-RQjZPigjxFvFZDijG6rjlSXxhRaTf9zLYVpGK+lVcga64oMqP1GbgVqCb46eHTTceSIgp89Ja0y5aPe3GFBYHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.0.tgz", + "integrity": "sha512-tjL58mgQcTRnDRpeky+Sth1MKQeYi2Ct6r3Luq9E5aTSjnM/i/JbozqZkKqp9thM5+oDSZxo+05f9kdQedfTGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.0.tgz", + "integrity": "sha512-ictoRyVaoZ8C67myHef+8hhCa6IplXJJ/xUCkVCxziV4zxMZh+jvhkhGsPBLqj1S72fnqD0QH5zL4lcaa4X7DQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.0.tgz", + "integrity": "sha512-8aVvZ2hfr9RLFzBQkQNmYeHLDja8fLeQAmLt757zdKN/B+8L809KlLJKctQjS22wZotptV8G6o7fVPuAs8S1tQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.0.tgz", + "integrity": "sha512-2dEF8GAcDKwshUfydD+GhosppNyBzhVUHkdFF3CXd3FpOkzj/RZzF5aoEtzklujklT5qFVRU2GK/cFeLaTNKVg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.0.tgz", + "integrity": "sha512-3XHTvoo2hKkh0MiOfS7SkhHjMaaWF7P4F3drocZZaho/Z8w7h/jpnuBHm91BOQLfvrKHs1yR+zir4E8RI3XzTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.0.tgz", + "integrity": "sha512-HKtkzodL4a3KFWDXDoO61cRynAWTR3LnAE0oGtfG9lcNFpNuVYUFOByxDgNqEvic9rvaQbAj+SFwTEWDptRaIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.0.tgz", + "integrity": "sha512-JBsxqiY16uyyCujtoC3s7zL+SrYyNElptD7K4V+KUoJ2V8/XG3hPWpheK+SuPwNh1cikJvIZmIdsrYXvD6PLJw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.0.tgz", + "integrity": "sha512-S5Qrijh37qhnUYQ0ay8WN3LuWeKjow7wTUyCOPMZtnXiBXYXdsZETI6ZLfYnQAKovltckPLBFdNeD61P2rJc7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.0.tgz", + "integrity": "sha512-zbZ6iMmLFEsCtTdc2/y1F/cDFVNNCPF7Ae8PCT/wdPE//hGwYp6r+MyCLMFX+kOeIe09Pz6ASfnnfpSTN1FJQQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.0.tgz", + "integrity": "sha512-L6Hw69oNpuYaOr1zCG30v0/DqKYCSRwodPGwIcVZ/6f8SOzq9rxtL/xNVI5MO4wjli+Ko0ErK9uHJL3mNPt8wQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.0.tgz", + "integrity": "sha512-nl+CTWIgOGszQmUz/kanfgrfFZ4qiwxKIfT/ryl3N3Ewk5tqizmYtNXiqb7Bu/fDtNgu1kyxdFiAUJNur3iysg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@scure/base": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.4.0.tgz", + "integrity": "sha512-thZ1TuJwFwBblOhgsjDKvvGirBxNp+wSvY/DR6tJBJOTDhdAAcHJ8Vbr2eFnqaxeca4+t0i9KBf+uHYGWwZORg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-2.4.0.tgz", + "integrity": "sha512-i3DS0CptAocyvqE4n3SUkpzeQK4vJMFwWLofTwRiiKo2aWojBOfyMCgfKw9HVpO6fSY5AK86sHS/Uzn8kK9Few==", + "license": "MIT", + "dependencies": { + "@noble/curves": "2.4.0", + "@noble/hashes": "2.4.0", + "@scure/base": "2.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.4.0.tgz", + "integrity": "sha512-82dxFbZUYboyOf0AXiydsQrFQ5Q4h9mX+O2UkE91ROYmsc0BKMGZLwDmy96Jpa2+vrtoxomjUhy1RPIgH/r2nA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.1.tgz", + "integrity": "sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gql.tada": { + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/gql.tada/-/gql.tada-1.11.4.tgz", + "integrity": "sha512-UQnhJUhSM0MV8cpcgjci/7ET8LnmHx9o9FtZqU4sH+ILiBVG5rnfXJzNdzTN+Y7MuCz6Pp3XE/VPEVqmO969jw==", + "license": "MIT", + "dependencies": { + "@0no-co/graphql.web": "^1.3.2", + "@0no-co/graphqlsp": "^1.17.3", + "@gql.tada/cli-utils": "1.9.4", + "@gql.tada/internal": "1.2.2" + }, + "bin": { + "gql-tada": "bin/cli.js", + "gql.tada": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/graphql": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-17.0.2.tgz", + "integrity": "sha512-FRWbddMxfkjiB7z+aQDWIR+E34xo9I8c9mtK2RPv8PmMzKRvrdsreHL/Ui/TmwHJfhHChEtsFPyMHKI+xuarQQ==", + "license": "MIT", + "engines": { + "node": "^22.0.0 || ^24.0.0 || ^25.0.0 || >=26.0.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/poseidon-lite": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/poseidon-lite/-/poseidon-lite-0.3.0.tgz", + "integrity": "sha512-ilJj4MIve4uBEG7SrtPqUUNkvpJ/pLVbndxa0WvebcQqeIhe+h72JR4g0EvwchUzm9sOQDlOjiDNmRAgxNZl4A==", + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.0.tgz", + "integrity": "sha512-gjA/kFeDfffII8COoPsvLPvE16hWDTEc1+0QKQNKUkuV8iJjqsC4xXs8/FzuL0v9EV39kuxTbAgcFoTRyy2TGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.64.0", + "@rollup/rollup-android-arm64": "4.64.0", + "@rollup/rollup-darwin-arm64": "4.64.0", + "@rollup/rollup-darwin-x64": "4.64.0", + "@rollup/rollup-freebsd-arm64": "4.64.0", + "@rollup/rollup-freebsd-x64": "4.64.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.64.0", + "@rollup/rollup-linux-arm-musleabihf": "4.64.0", + "@rollup/rollup-linux-arm64-gnu": "4.64.0", + "@rollup/rollup-linux-arm64-musl": "4.64.0", + "@rollup/rollup-linux-loong64-gnu": "4.64.0", + "@rollup/rollup-linux-loong64-musl": "4.64.0", + "@rollup/rollup-linux-ppc64-gnu": "4.64.0", + "@rollup/rollup-linux-ppc64-musl": "4.64.0", + "@rollup/rollup-linux-riscv64-gnu": "4.64.0", + "@rollup/rollup-linux-riscv64-musl": "4.64.0", + "@rollup/rollup-linux-s390x-gnu": "4.64.0", + "@rollup/rollup-linux-x64-gnu": "4.64.0", + "@rollup/rollup-linux-x64-musl": "4.64.0", + "@rollup/rollup-openbsd-x64": "4.64.0", + "@rollup/rollup-openharmony-arm64": "4.64.0", + "@rollup/rollup-win32-arm64-msvc": "4.64.0", + "@rollup/rollup-win32-ia32-msvc": "4.64.0", + "@rollup/rollup-win32-x64-gnu": "4.64.0", + "@rollup/rollup-win32-x64-msvc": "4.64.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", + "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/valibot": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.5.0.tgz", + "integrity": "sha512-nil6AkP2TChWL43Z5uJ6GTxX01CUA+g8LWUM+N/rB9NBbkUMaUsi9PUNzlUPgoKASgmx9f7eGOYpJ04/fSa6FQ==", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/sdk/package.json b/sdk/package.json new file mode 100644 index 0000000..480ed38 --- /dev/null +++ b/sdk/package.json @@ -0,0 +1,39 @@ +{ + "name": "@skur/sdk", + "private": true, + "version": "0.1.0", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./engine": { + "types": "./dist/engine/index.d.ts", + "import": "./dist/engine/index.js" + }, + "./src/*": "./src/*" + }, + "scripts": { + "build": "tsc -p tsconfig.build.json", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "prepare": "tsc -p tsconfig.build.json", + "seed": "node --experimental-strip-types scripts/seed.ts", + "e2e": "node --experimental-strip-types scripts/e2e.ts" + }, + "dependencies": { + "@mysten/sui": "^2.34.0" + }, + "devDependencies": { + "@types/node": "^24.19.1", + "typescript": "~6.0.2", + "vitest": "^3.2.7" + }, + "files": [ + "dist", + "src" + ] +} \ No newline at end of file diff --git a/sdk/scripts/e2e.ts b/sdk/scripts/e2e.ts new file mode 100644 index 0000000..8a24801 --- /dev/null +++ b/sdk/scripts/e2e.ts @@ -0,0 +1,277 @@ +/** + * Drive a real vault on a real network, end to end, and check that the chain agrees with the SDK + * at every step. + * + * SUI_PRIVATE_KEY=suiprivkey1... \ + * SKUR_NETWORK=testnet \ + * SKUR_PACKAGE_ID=0x... \ + * node --experimental-strip-types scripts/e2e.ts + * + * This is the test the unit suites cannot be: it proves the BCS layouts match the Move structs, + * that the PTB builders produce transactions the vault accepts, that `preview_transfer` decodes, + * and that the events decode back into the shapes the indexer projects. One wrong field offset and + * this fails loudly where a unit test would pass. + * + * It creates its own vault with a single signer holding every treasury role, so it needs no + * coordination — and therefore it exercises the routine path rather than the multi-signer one. + */ +import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519'; +import { decodeSuiPrivateKey } from '@mysten/sui/cryptography'; +import { SUI_TYPE_ARG } from '@mysten/sui/utils'; +import type { Transaction } from '@mysten/sui/transactions'; +import { + HOUR, + Reason, + Role, + Status, + Tier, + describeFailure, + describeReasons, + fetchVaultView, + grpcClient, + listVaultEvents, + previewTransfer, + templateById, + tx as build, + type Network, +} from '../dist/index.js'; + +const network = (process.env.SKUR_NETWORK ?? 'testnet') as Network; +const packageId = process.env.SKUR_PACKAGE_ID; +const secret = process.env.SUI_PRIVATE_KEY; +if (!packageId || packageId === '0x0') throw new Error('SKUR_PACKAGE_ID is not set'); +if (!secret) throw new Error('SUI_PRIVATE_KEY is not set (use a throwaway key)'); + +const keypair = Ed25519Keypair.fromSecretKey(decodeSuiPrivateKey(secret).secretKey); +const me = keypair.toSuiAddress(); +const client = grpcClient(network); + +let failures = 0; +function check(label: string, ok: boolean, detail = '') { + console.log(`${ok ? ' ok ' : ' FAIL '} ${label}${detail ? ` — ${detail}` : ''}`); + if (!ok) failures++; +} + +async function send(transaction: Transaction, label: string) { + const result = await client.signAndExecuteTransaction({ + transaction, + signer: keypair, + include: { effects: true, objectTypes: true }, + }); + const settled = result.Transaction ?? result.FailedTransaction; + if (!settled?.status.success) { + throw new Error(`${label}: ${describeFailure(settled?.status.error?.message ?? label)}`); + } + await client.waitForTransaction({ digest: settled.digest }); + return result; +} + +async function main() { + console.log(`end to end on ${network} as ${me}\n`); + + // A policy this one signer can actually satisfy, with a one-minute activation delay so the + // probation path is exercised rather than skipped. + const template = templateById('startup'); + const policy = { + ...template.policy, + approvalsLow: 1, + approvalsHigh: 1, + approvalsCritical: 1, + governanceThreshold: 1, + guardianThreshold: 0, + guardianRequiredCritical: false, + delayHigh: 0, + delayCritical: 30_000, + recipientActivationDelay: 60_000, + policyChangeDelay: 60_000, + recoveryDelay: 60_000, + proposalTtl: 2 * HOUR, + }; + + // Named, because the event checks later compare against it. + const members = [{ address: me, roles: Role.OWNER | Role.APPROVER | Role.EXECUTOR }]; + + console.log('1. create'); + const created = await send( + build.createVault(packageId!, { + name: 'Skur end-to-end', + policy, + members, + assets: [ + { + coinType: SUI_TYPE_ARG, + limits: { + approved: true, + lowMax: 100_000_000n, // 0.1 SUI stays routine + highMax: 500_000_000n, + perTxMax: 0n, + dailyMax: 0n, + }, + }, + ], + recipients: [], + }), + 'create', + ); + const types = created.Transaction?.objectTypes ?? {}; + const vaultId = created.Transaction?.effects?.changedObjects.find( + (o) => o.idOperation === 'Created' && types[o.objectId]?.endsWith('::vault::Vault'), + )?.objectId; + if (!vaultId) throw new Error('no vault in the effects'); + console.log(` vault ${vaultId}`); + + console.log('2. read it back'); + let view = await fetchVaultView(client, vaultId); + check('the name round-tripped through BCS', view.vault.name === 'Skur end-to-end', view.vault.name); + check('the policy round-tripped', view.vault.policy.recipientActivationDelay === 60_000, String(view.vault.policy.recipientActivationDelay)); + check('the roster is there', view.members.length === 1 && view.members[0].address === me); + check('the roles are right', view.members[0]?.roles === (Role.OWNER | Role.APPROVER | Role.EXECUTOR)); + check('SUI is approved', view.assets.some((a) => a.limits.approved)); + check('the counters are right', view.vault.ownerCount === 1 && view.vault.pendingCount === 0); + + console.log('3. deposit 0.2 SUI'); + await send( + build.deposit(packageId!, { vaultId, coinType: SUI_TYPE_ARG, amount: 200_000_000n }), + 'deposit', + ); + view = await fetchVaultView(client, vaultId); + const sui = view.assets.find((a) => a.coinType === SUI_TYPE_ARG || a.coinType.endsWith('::sui::SUI')); + check('the balance is in the bag', sui?.balance === 200_000_000n, String(sui?.balance)); + + console.log('4. preview, to an address the vault has never paid'); + const stranger = Ed25519Keypair.generate().toSuiAddress(); + const unknown = await previewTransfer(client, { + packageId: packageId!, + vaultId, + coinType: SUI_TYPE_ARG, + amount: 10_000_000n, + recipient: stranger, + sender: me, + }); + check('an unknown recipient is escalated', unknown.tier >= Tier.HIGH, `tier ${unknown.tier}`); + check('for being unknown, specifically', (unknown.reasons & Reason.RECIPIENT_UNKNOWN) !== 0, describeReasons(unknown.reasons).join('; ')); + check( + 'and the chain mapped the tier to this policy', + unknown.reqApprovals === policy.approvalsCritical && + unknown.reqGuardians === 0 && + unknown.delay === policy.delayCritical, + `${unknown.reqApprovals} approvals, ${unknown.reqGuardians} guardians, ${unknown.delay}ms`, + ); + + console.log('5. preview, to ourselves (a registered recipient)'); + await send( + build.registerRecipient(packageId!, { vaultId, recipient: me, label: 'Operations' }), + 'register', + ); + const probation = await previewTransfer(client, { + packageId: packageId!, + vaultId, + coinType: SUI_TYPE_ARG, + amount: 10_000_000n, + recipient: me, + sender: me, + }); + check('a registered recipient still waits out probation', (probation.reasons & Reason.RECIPIENT_PROBATION) !== 0, describeReasons(probation.reasons).join('; ')); + check('and is no longer flagged as never paid', (probation.reasons & Reason.RECIPIENT_UNKNOWN) === 0); + check('so it is still escalated', probation.tier >= Tier.HIGH, `tier ${probation.tier}`); + + console.log('6. open a payment and clear it'); + const opened = await send( + build.proposeTransfer(packageId!, { + vaultId, + coinType: SUI_TYPE_ARG, + amount: 10_000_000n, + recipient: me, + memo: 'end to end', + }), + 'propose', + ); + view = await fetchVaultView(client, vaultId); + const proposal = view.proposals[0]; + check('the proposal decoded', Boolean(proposal), proposal ? `#${proposal.id}` : 'none'); + check('its memo survived', proposal?.memo === 'end to end', proposal?.memo); + check('its amount survived', proposal?.amount === 10_000_000n, String(proposal?.amount)); + check('it is pending', proposal?.status === Status.PENDING); + check('the preview and the proposal agree on the tier', proposal?.tier === probation.tier); + check('the vault counted it', view.vault.pendingCount === 1); + + await send(build.approve(packageId!, vaultId, proposal!.id), 'approve'); + view = await fetchVaultView(client, vaultId); + check('the approval was recorded against the address', view.proposals[0]?.approvals.includes(me)); + + console.log('7. the activation delay actually holds'); + try { + await send( + build.executeTransfer(packageId!, { vaultId, coinType: SUI_TYPE_ARG, proposalId: proposal!.id }), + 'execute-early', + ); + check('executing before activation is refused', false, 'it went through'); + } catch (e) { + const reason = describeFailure(e); + check( + 'executing before activation is refused, and says why', + reason === 'The recipient is still in its activation delay.', + reason, + ); + } + + console.log(' waiting out the 60s activation delay'); + await new Promise((r) => setTimeout(r, 66_000)); + + await send( + build.executeTransfer(packageId!, { vaultId, coinType: SUI_TYPE_ARG, proposalId: proposal!.id }), + 'execute', + ); + view = await fetchVaultView(client, vaultId); + check('it executed', view.proposals[0]?.status === Status.EXECUTED); + check('the balance moved', view.assets.find((a) => a.coinType.endsWith('::sui::SUI'))?.balance === 190_000_000n); + check('the pending count came back down', view.vault.pendingCount === 0); + check('the recipient was credited', view.recipients.find((r) => r.address === me)?.paidCount === 1); + + console.log('8. probation has now expired, so a small payment is routine again'); + const routine = await previewTransfer(client, { + packageId: packageId!, + vaultId, + coinType: SUI_TYPE_ARG, + amount: 1_000n, // far below every threshold, to isolate the recipient signal + recipient: me, + sender: me, + }); + check('the tier is back to routine', routine.tier === Tier.LOW, `tier ${routine.tier}`); + check('with no reasons at all', routine.reasons === 0, describeReasons(routine.reasons).join('; ')); + check('one approval and no wait', routine.reqApprovals === policy.approvalsLow && routine.delay === 0); + + console.log('9. the events decode'); + const { events } = await listVaultEvents(client, { packageId: packageId!, vaultId, limit: 50 }); + const names = new Set(events.map((e) => e.event.name)); + for (const expected of ['VaultCreated', 'Deposited', 'ProposalOpened', 'Approved', 'RecipientRegistered', 'MemberChanged', 'Executed']) { + check(`${expected} decoded`, names.has(expected as never)); + } + // The roster has to be rebuildable from the log alone: VaultCreated carries counts, not + // addresses, and a Move table cannot be iterated, so if the founding members are not announced + // then nothing downstream can ever learn who they were. + const founding = events.filter( + (e) => e.event.name === 'MemberChanged' && e.event.proposal === 0n, + ); + check('the founding roster is in the event log', founding.length === members.length, `${founding.length} of ${members.length}`); + check( + 'with the roles it was created with', + founding.some( + (e) => e.event.name === 'MemberChanged' && e.event.member === me && e.event.rolesAfter === members[0].roles, + ), + ); + const executed = events.find((e) => e.event.name === 'Executed'); + check( + 'the Executed event carries the amount and the remaining balance', + executed?.event.name === 'Executed' && executed.event.amount === 10_000_000n && executed.event.balanceAfter === 190_000_000n, + ); + + console.log(`\n${failures === 0 ? 'all checks passed' : `${failures} check(s) failed`}`); + console.log(`vault: https://suiscan.xyz/${network}/object/${vaultId}`); + if (failures > 0) process.exit(1); +} + +main().catch((e) => { + console.error(`\n${e instanceof Error ? e.message : e}`); + process.exit(1); +}); diff --git a/sdk/scripts/seed.ts b/sdk/scripts/seed.ts new file mode 100644 index 0000000..32fd199 --- /dev/null +++ b/sdk/scripts/seed.ts @@ -0,0 +1,129 @@ +/** + * Create a demonstration vault and fund it, so an evaluator can open a real one rather than read + * about it. + * + * SUI_PRIVATE_KEY=suiprivkey1... \ + * SKUR_NETWORK=testnet \ + * SKUR_PACKAGE_ID=0x... \ + * node --experimental-strip-types scripts/seed.ts + * + * The roster is deliberately realistic: two owners, a separate approver, a guardian that holds no + * treasury role, and an agent that can only propose. Addresses other than the deployer's are + * derived placeholders unless given — the point is to show the shape, and the deployer keeps the + * roles needed to drive a demo alone. + */ +import { readFile, writeFile } from 'node:fs/promises'; +import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519'; +import { SUI_TYPE_ARG } from '@mysten/sui/utils'; +import { decodeSuiPrivateKey } from '@mysten/sui/cryptography'; +import { grpcClient, Role, templateById, tx as build, type Network } from '../dist/index.js'; + +const network = (process.env.SKUR_NETWORK ?? 'testnet') as Network; +const packageId = process.env.SKUR_PACKAGE_ID; +const secret = process.env.SUI_PRIVATE_KEY; + +if (!packageId) throw new Error('SKUR_PACKAGE_ID is not set'); +if (!secret) throw new Error('SUI_PRIVATE_KEY is not set (use a throwaway key for a demo)'); + +const { secretKey } = decodeSuiPrivateKey(secret); +const keypair = Ed25519Keypair.fromSecretKey(secretKey); +const me = keypair.toSuiAddress(); +const client = grpcClient(network); + +const extra = (process.env.SKUR_SIGNERS ?? '').split(',').map((s) => s.trim()).filter(Boolean); +const [owner2 = me, approver = me, guardian, agent] = extra; + +const template = templateById('startup'); + +// A guardian must hold no treasury role, so a demo without a second key cannot have one. Saying +// so is better than quietly creating a vault with a guardian that is also an owner, which the +// contract would refuse anyway. +const members = [ + { address: me, roles: Role.OWNER | Role.APPROVER | Role.EXECUTOR }, + ...(owner2 !== me ? [{ address: owner2, roles: Role.OWNER | Role.APPROVER }] : []), + ...(approver !== me && approver !== owner2 ? [{ address: approver, roles: Role.APPROVER }] : []), + ...(guardian ? [{ address: guardian, roles: Role.GUARDIAN }] : []), + ...(agent ? [{ address: agent, roles: Role.PROPOSER }] : []), +]; + +// The template's policy assumes a roster this demo may not have, so the thresholds are brought +// down to what these members can actually satisfy. A vault whose threshold nobody can reach is a +// frozen vault, and the contract refuses to create one. +const owners = members.filter((m) => (m.roles & Role.OWNER) !== 0).length; +const approvers = members.filter((m) => (m.roles & Role.APPROVER) !== 0).length; +const guardians = members.filter((m) => (m.roles & Role.GUARDIAN) !== 0).length; +const policy = { + ...template.policy, + governanceThreshold: Math.min(template.policy.governanceThreshold, owners), + approvalsLow: Math.min(template.policy.approvalsLow, approvers), + approvalsHigh: Math.min(template.policy.approvalsHigh, approvers), + approvalsCritical: Math.min(template.policy.approvalsCritical, approvers), + guardianThreshold: Math.min(template.policy.guardianThreshold, guardians), + guardianRequiredCritical: guardians > 0 && template.policy.guardianRequiredCritical, +}; + +async function main() { + console.log(`seeding on ${network} as ${me}`); + + const transaction = build.createVault(packageId!, { + name: 'Skur Demo Treasury', + policy, + members, + assets: [{ coinType: SUI_TYPE_ARG, limits: template.native }], + recipients: [{ address: me, label: 'Operations' }], + }); + + const result = await client.signAndExecuteTransaction({ + transaction, + signer: keypair, + include: { effects: true, objectTypes: true }, + }); + await client.waitForTransaction({ result }); + + const settled = result.Transaction ?? result.FailedTransaction; + if (!settled?.status.success) { + throw new Error(`vault creation failed: ${JSON.stringify(settled?.status)}`); + } + + // `objectTypes` maps each changed object id to its type, which is how the new shared Vault is + // identified without guessing from ownership. + const types = result.Transaction?.objectTypes ?? {}; + const vaultId = settled.effects?.changedObjects.find( + (o) => o.idOperation === 'Created' && types[o.objectId]?.endsWith('::vault::Vault'), + )?.objectId; + if (!vaultId) throw new Error('the vault object id was not in the effects'); + console.log(`vault: ${vaultId}`); + + const fund = build.deposit(packageId!, { + vaultId, + coinType: SUI_TYPE_ARG, + amount: 1_000_000_000n, // 1 SUI + }); + const funded = await client.signAndExecuteTransaction({ + transaction: fund, + signer: keypair, + include: { effects: true }, + }); + await client.waitForTransaction({ result: funded }); + console.log('funded with 1 SUI'); + + const path = new URL(`../../sui/deployments/${network}.json`, import.meta.url).pathname; + try { + const file = JSON.parse(await readFile(path, 'utf8')); + file.vaults = [ + ...(file.vaults ?? []).filter((v: { id: string }) => v.id !== vaultId), + { id: vaultId, name: 'Skur Demo Treasury' }, + ]; + await writeFile(path, `${JSON.stringify(file, null, 2)}\n`); + console.log(`recorded in ${path}`); + } catch { + console.log(`could not update ${path}; record the vault id by hand`); + } + + console.log(`explorer: https://suiscan.xyz/${network}/object/${vaultId}`); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/sdk/src/bcs.ts b/sdk/src/bcs.ts new file mode 100644 index 0000000..0e979e7 --- /dev/null +++ b/sdk/src/bcs.ts @@ -0,0 +1,126 @@ +import { bcs } from '@mysten/sui/bcs'; + +/** + * BCS layouts for the Move structs, in declaration order. BCS is positional, so these must match + * `sui/sources/*.move` field for field — reordering a field in Move without reordering it here + * silently misreads every vault. + * + * Parsed over the `content` bytes from `getObject({ include: { content: true } })`, which is the + * stable representation. The `json` include is explicitly documented as not guaranteed across + * clients, so nothing here uses it. + */ + +/** `sui::object::UID` is a single 32-byte id on the wire. */ +const UID = bcs.Address; + +/** `sui::table::Table` and `sui::bag::Bag` both serialise as a handle plus a count. */ +const Handle = bcs.struct('Handle', { id: UID, size: bcs.u64() }); + +const TypeNameBcs = bcs.struct('TypeName', { name: bcs.string() }); + +const VecSetAddress = bcs.struct('VecSet
', { + contents: bcs.vector(bcs.Address), +}); + +export const PolicyBcs = bcs.struct('Policy', { + approvals_low: bcs.u8(), + approvals_high: bcs.u8(), + approvals_critical: bcs.u8(), + governance_threshold: bcs.u8(), + guardian_threshold: bcs.u8(), + guardian_required_critical: bcs.bool(), + delay_high: bcs.u64(), + delay_critical: bcs.u64(), + recipient_activation_delay: bcs.u64(), + policy_change_delay: bcs.u64(), + recovery_delay: bcs.u64(), + proposal_ttl: bcs.u64(), + high_exposure_bps: bcs.u64(), + critical_exposure_bps: bcs.u64(), + hard_block_exposure_bps: bcs.u64(), + envelope_bps: bcs.u64(), + envelope_window: bcs.u64(), +}); + +export const AssetLimitsBcs = bcs.struct('AssetLimits', { + approved: bcs.bool(), + low_max: bcs.u64(), + high_max: bcs.u64(), + per_tx_max: bcs.u64(), + daily_max: bcs.u64(), +}); + +export const VelocityBcs = bcs.struct('Velocity', { + day_anchor: bcs.u64(), + day_spent: bcs.u64(), + envelope_anchor: bcs.u64(), + envelope_spent: bcs.u64(), + envelope_basis: bcs.u64(), +}); + +export const RecipientBcs = bcs.struct('Recipient', { + trust: bcs.u8(), + registered_at: bcs.u64(), + activates_at: bcs.u64(), + paid_count: bcs.u64(), + last_paid: bcs.u64(), + label: bcs.string(), +}); + +export const ProposalBcs = bcs.struct('Proposal', { + id: bcs.u64(), + kind: bcs.u8(), + status: bcs.u8(), + proposer: bcs.Address, + created_at: bcs.u64(), + executable_at: bcs.u64(), + expires_at: bcs.u64(), + req_approvals: bcs.u8(), + req_guardians: bcs.u8(), + tier: bcs.u8(), + reasons: bcs.u16(), + exposure_bps: bcs.u64(), + policy_version: bcs.u64(), + reduction_mask: bcs.u32(), + approvals: VecSetAddress, + confirmations: VecSetAddress, + rejections: VecSetAddress, + asset: bcs.option(TypeNameBcs), + amount: bcs.u64(), + recipient: bcs.Address, + memo: bcs.string(), + new_policy: bcs.option(PolicyBcs), + new_limits: bcs.option(AssetLimitsBcs), + member: bcs.Address, + member_prev: bcs.Address, + member_roles: bcs.u8(), + trust_level: bcs.u8(), + target_mode: bcs.u8(), +}); + +export const VaultBcs = bcs.struct('Vault', { + id: UID, + name: bcs.string(), + policy: PolicyBcs, + policy_version: bcs.u64(), + mode: bcs.u8(), + posture_reasons: bcs.u16(), + members: Handle, + owner_count: bcs.u64(), + approver_count: bcs.u64(), + executor_count: bcs.u64(), + guardian_count: bcs.u64(), + limits: Handle, + funds: Handle, + velocity: Handle, + recipients: Handle, + proposals: Handle, + next_proposal: bcs.u64(), + pending_count: bcs.u64(), + created_at: bcs.u64(), +}); + +/** `sui::balance::Balance` is a single u64 on the wire. */ +export const BalanceBcs = bcs.struct('Balance', { value: bcs.u64() }); + +export { bcs }; diff --git a/sdk/src/engine/format.ts b/sdk/src/engine/format.ts new file mode 100644 index 0000000..72a422a --- /dev/null +++ b/sdk/src/engine/format.ts @@ -0,0 +1,80 @@ +/** + * Formatting for amounts, durations and shares. No chain dependency: the EVM build imported these + * from viem, which is why they are implemented here instead. + * + * Durations are milliseconds throughout, matching `sui::clock`. + */ + +export function formatUnits(value: bigint, decimals: number): string { + const neg = value < 0n; + const abs = neg ? -value : value; + const base = 10n ** BigInt(decimals); + const int = abs / base; + const frac = abs % base; + const fracText = decimals === 0 ? '' : frac.toString().padStart(decimals, '0').replace(/0+$/, ''); + return `${neg ? '-' : ''}${int}${fracText ? `.${fracText}` : ''}`; +} + +export function parseUnits(text: string, decimals: number): bigint | null { + const cleaned = text.replace(/[, _]/g, '').trim(); + if (!cleaned || !/^-?\d*(\.\d*)?$/.test(cleaned)) return null; + const neg = cleaned.startsWith('-'); + const body = neg ? cleaned.slice(1) : cleaned; + const [int = '0', frac = ''] = body.split('.'); + if (frac.length > decimals) return null; + const scaled = BigInt(int || '0') * 10n ** BigInt(decimals) + BigInt((frac || '0').padEnd(decimals, '0') || '0'); + return neg ? -scaled : scaled; +} + +export function short(addr: string, n = 4): string { + if (!addr) return ''; + if (addr.length <= 2 * n + 3) return addr; + return `${addr.slice(0, 2 + n)}…${addr.slice(-n)}`; +} + +export function fmtAmount(value: bigint, decimals: number, symbol?: string, maxFrac = 2): string { + const s = formatUnits(value, decimals); + const [int, frac = ''] = s.split('.'); + const intFmt = Number(int).toLocaleString('en-US'); + const fracTrim = frac.slice(0, maxFrac).replace(/0+$/, ''); + const out = fracTrim ? `${intFmt}.${fracTrim}` : intFmt; + return symbol ? `${out} ${symbol}` : out; +} + +export function parseAmount(text: string, decimals: number): bigint | null { + const v = parseUnits(text, decimals); + return v === null || v < 0n ? null : v; +} + +export function fmtBps(bps: number): string { + return `${(bps / 100).toFixed(bps % 100 === 0 ? 0 : 2)}%`; +} + +/** Milliseconds to something a person reads. */ +export function fmtDuration(ms: number): string { + if (ms <= 0) return 'none'; + const totalMinutes = Math.floor(ms / 60_000); + const d = Math.floor(totalMinutes / 1440); + const h = Math.floor((totalMinutes % 1440) / 60); + const m = totalMinutes % 60; + const parts: string[] = []; + if (d) parts.push(`${d}d`); + if (h) parts.push(`${h}h`); + if (m && !d) parts.push(`${m}m`); + return parts.join(' ') || `${Math.round(ms / 1000)}s`; +} + +export function fmtTimestamp(ms: number | bigint): string { + const n = Number(ms); + if (!n) return '—'; + return new Date(n).toLocaleString('en-US', { dateStyle: 'medium', timeStyle: 'short' }); +} + +export function fmtRelative(ms: number | bigint, now = Date.now()): string { + const n = Number(ms); + if (!n) return '—'; + const diff = n - now; + if (Math.abs(diff) < 60_000) return diff >= 0 ? 'now' : 'just now'; + const abs = fmtDuration(Math.abs(diff)); + return diff > 0 ? `in ${abs}` : `${abs} ago`; +} diff --git a/sdk/src/engine/index.ts b/sdk/src/engine/index.ts new file mode 100644 index 0000000..f237c10 --- /dev/null +++ b/sdk/src/engine/index.ts @@ -0,0 +1,17 @@ +/** + * The chain-agnostic core. These are the engines that make a Skur vault more than a threshold: + * the risk classifier, the policy rules and what counts as weakening them, the organization + * templates, the maximum-loss calculation, the posture report and the attack simulator. + * + * Every one of them mirrors a Move module and is pinned against the Move tests. They exist in + * TypeScript so an interface can preview a policy that is not deployed yet; for a live vault the + * chain is always authoritative. + */ +export * from './format.js'; +export * from './maxLoss.js'; +export * from './policy.js'; +export * from './policyFields.js'; +export * from './posture.js'; +export * from './risk.js'; +export * from './simulator.js'; +export * from './templates.js'; diff --git a/sdk/src/engine/maxLoss.ts b/sdk/src/engine/maxLoss.ts new file mode 100644 index 0000000..007596c --- /dev/null +++ b/sdk/src/engine/maxLoss.ts @@ -0,0 +1,112 @@ +import { DAY, Mode, type AssetLimits, type Policy } from '../types.js'; +import { effectiveCaps } from './risk.js'; +import { fmtDuration } from './format.js'; + +/** + * Maximum possible loss per asset, turned from a policy into numbers a finance team can act on. + * + * Conservative by construction: it assumes every signer a tier needs is compromised and + * cooperating, and that the attacker picks amounts optimally. It is an upper bound under the + * stated assumptions and never a guarantee — the assumptions are returned alongside the numbers + * so they cannot be quoted without them. + */ +export type MaxLoss = { + /** The most that can leave with no delay at all. */ + immediate: bigint; + /** The most that can leave within a day if no guardian acts. */ + day: bigint; + criticalDelay: number; + highDelay: number; + /** Which control produced each number. */ + immediateBinding: string; + dayBinding: string; + assumptions: string[]; +}; + +function min(...xs: bigint[]): bigint { + return xs.reduce((a, b) => (b < a ? b : a)); +} + +export function computeMaxLoss( + policy: Policy, + limits: AssetLimits, + balance: bigint, + mode: Mode, +): MaxLoss { + const caps = effectiveCaps(limits.perTxMax, limits.dailyMax, mode); + const assumptions = [ + 'Enough approvers to satisfy each tier are compromised and cooperate.', + 'Executors are compromised or colluding.', + 'No guardian vetoes or freezes the vault during any delay window.', + 'The attacker pays addresses that are already activated, not in probation.', + 'Limits are per asset: each asset has its own caps and its own envelope.', + ]; + + // Immediate: the routine tier needs no delay, so an attacker repeats it until a cumulative + // control binds. In Elevated there is no undelayed tier at all. + let immediate = 0n; + let immediateBinding = 'no undelayed tier'; + if (mode === Mode.NORMAL) { + const exposureLow = + policy.highExposureBps === 0 + ? balance + : (balance * BigInt(Math.max(policy.highExposureBps - 1, 0))) / 10_000n; + const perTransfer = min(limits.lowMax, caps.perTxMax || limits.lowMax, exposureLow); + if (perTransfer > 0n) { + immediate = balance; + immediateBinding = 'the entire balance (no daily cap and no envelope)'; + const envelope = + policy.envelopeBps === 0 ? 0n : (balance * BigInt(policy.envelopeBps)) / 10_000n; + if (caps.dailyMax !== 0n && caps.dailyMax < immediate) { + immediate = caps.dailyMax; + immediateBinding = 'the daily cap'; + } + if (envelope !== 0n && envelope < immediate) { + immediate = envelope; + immediateBinding = `the loss envelope per ${fmtDuration(policy.envelopeWindow)}`; + } + } else { + immediateBinding = 'the routine tier is disabled by the limits'; + } + } + + // Within a day: the high tier is also reachable if its delay is under a day. Either way the + // attacker can straddle a bucket boundary and get two windows' worth. + const envelope = policy.envelopeBps === 0 ? 0n : (balance * BigInt(policy.envelopeBps)) / 10_000n; + const perTx = caps.perTxMax === 0n ? balance : caps.perTxMax; + const highCeiling = + policy.criticalExposureBps === 0 + ? balance + : (balance * BigInt(Math.max(policy.criticalExposureBps - 1, 0))) / 10_000n; + const candidates: [bigint, string][] = [[balance, 'the entire balance']]; + if (caps.dailyMax !== 0n) + candidates.push([caps.dailyMax * 2n, 'two daily buckets across a boundary']); + if (envelope !== 0n) + candidates.push([envelope * 2n, 'two envelope windows across a boundary']); + if (policy.delayHigh >= DAY) { + candidates.push([immediate * 2n, 'the routine tier only, across two buckets']); + } else { + candidates.push([ + min(perTx, highCeiling) === 0n ? 0n : balance, + 'the high tier, reachable within a day', + ]); + } + let [day, dayBinding] = candidates.reduce((a, b) => (b[0] < a[0] ? b : a)); + + if (mode === Mode.LOCKDOWN) { + immediate = 0n; + day = 0n; + immediateBinding = 'the vault is in Lockdown'; + dayBinding = 'the vault is in Lockdown'; + } + + return { + immediate, + day, + criticalDelay: policy.delayCritical, + highDelay: policy.delayHigh, + immediateBinding, + dayBinding, + assumptions, + }; +} diff --git a/sdk/src/engine/policy.ts b/sdk/src/engine/policy.ts new file mode 100644 index 0000000..e4bd9a8 --- /dev/null +++ b/sdk/src/engine/policy.ts @@ -0,0 +1,131 @@ +import { BPS, HOUR, Trust, type AssetLimits, type Policy } from '../types.js'; +import { POLICY_ERRORS } from '../errors.js'; + +/** + * Mirror of `sui/sources/policy.move`. The codes are the Move abort codes, so a draft validated + * here fails for the same reason and with the same number as it would on chain. + */ +const MIN_EXECUTION_WINDOW = HOUR; + +export const OK = 0; + +export function validatePolicyCode(p: Policy): number { + if (p.approvalsLow === 0) return 1; + if (p.approvalsHigh < p.approvalsLow) return 2; + if (p.approvalsCritical < p.approvalsHigh) return 3; + if (p.governanceThreshold === 0) return 4; + if (p.guardianRequiredCritical && p.guardianThreshold === 0) return 5; + if (p.delayCritical < p.delayHigh) return 6; + if (p.criticalExposureBps !== 0 && p.highExposureBps > p.criticalExposureBps) return 7; + if (p.criticalExposureBps > Number(BPS) || p.highExposureBps > Number(BPS)) return 8; + if (p.hardBlockExposureBps !== 0 && p.hardBlockExposureBps < p.criticalExposureBps) return 9; + if (p.hardBlockExposureBps > Number(BPS)) return 10; + if (p.envelopeBps > Number(BPS)) return 11; + if (p.envelopeBps !== 0 && p.envelopeWindow < MIN_EXECUTION_WINDOW) return 12; + if (p.proposalTtl < longestDelay(p) + MIN_EXECUTION_WINDOW) return 13; + return OK; +} + +export function longestDelay(p: Policy): number { + return Math.max(p.delayCritical, p.policyChangeDelay, p.recoveryDelay, p.recipientActivationDelay); +} + +/** Every failing rule, not just the first, so an editor can mark all of them at once. */ +export function validatePolicy(p: Policy): string[] { + const errs: string[] = []; + const push = (code: number) => errs.push(POLICY_ERRORS[code]); + if (p.approvalsLow === 0) push(1); + if (p.approvalsHigh < p.approvalsLow) push(2); + if (p.approvalsCritical < p.approvalsHigh) push(3); + if (p.governanceThreshold === 0) push(4); + if (p.guardianRequiredCritical && p.guardianThreshold === 0) push(5); + if (p.delayCritical < p.delayHigh) push(6); + if (p.criticalExposureBps !== 0 && p.highExposureBps > p.criticalExposureBps) push(7); + if (p.criticalExposureBps > Number(BPS) || p.highExposureBps > Number(BPS)) push(8); + if (p.hardBlockExposureBps !== 0 && p.hardBlockExposureBps < p.criticalExposureBps) push(9); + if (p.hardBlockExposureBps > Number(BPS)) push(10); + if (p.envelopeBps > Number(BPS)) push(11); + if (p.envelopeBps !== 0 && p.envelopeWindow < MIN_EXECUTION_WINDOW) push(12); + if (p.proposalTtl < longestDelay(p) + MIN_EXECUTION_WINDOW) push(13); + return errs; +} + +export function validateLimits(l: AssetLimits): string[] { + const errs: string[] = []; + if (l.lowMax > l.highMax) errs.push(POLICY_ERRORS[20]); + if (l.perTxMax !== 0n && l.dailyMax !== 0n && l.perTxMax > l.dailyMax) errs.push(POLICY_ERRORS[21]); + return errs; +} + +export function validateCounts( + p: Policy, + owners: number, + approvers: number, + executors: number, + guardians: number, +): string[] { + const errs: string[] = []; + if (owners < 1) errs.push(POLICY_ERRORS[30]); + if (executors < 1) errs.push(POLICY_ERRORS[31]); + if (owners < p.governanceThreshold) + errs.push(`The governance threshold (${p.governanceThreshold}) exceeds the number of owners (${owners}).`); + if (approvers < p.approvalsCritical) + errs.push(`Critical approvals (${p.approvalsCritical}) exceed the number of approvers (${approvers}).`); + if (guardians < p.guardianThreshold) + errs.push(`The guardian threshold (${p.guardianThreshold}) exceeds the number of guardians (${guardians}).`); + return errs; +} + +/** Where zero means "disabled", going to zero is the loosest move available. */ +function thresholdLoosens(a: number | bigint, b: number | bigint): boolean { + if (a === 0 || a === 0n) return false; + if (b === 0 || b === 0n) return true; + return b > a; +} + +/** The fields `b` gives up relative to `a`. Empty means the change only tightens. */ +export function policyReductions(a: Policy, b: Policy): string[] { + const out: string[] = []; + if (b.approvalsLow < a.approvalsLow) out.push('fewer approvals for routine payments'); + if (b.approvalsHigh < a.approvalsHigh) out.push('fewer approvals for high-risk payments'); + if (b.approvalsCritical < a.approvalsCritical) out.push('fewer approvals for critical payments'); + if (b.governanceThreshold < a.governanceThreshold) out.push('a lower governance threshold'); + if (b.guardianThreshold < a.guardianThreshold) out.push('fewer guardian confirmations'); + if (a.guardianRequiredCritical && !b.guardianRequiredCritical) + out.push('no guardian required for critical payments'); + if (b.delayHigh < a.delayHigh) out.push('a shorter high-risk delay'); + if (b.delayCritical < a.delayCritical) out.push('a shorter critical delay'); + if (b.recipientActivationDelay < a.recipientActivationDelay) + out.push('a shorter new-recipient delay'); + if (b.policyChangeDelay < a.policyChangeDelay) out.push('a shorter policy-change delay'); + if (b.recoveryDelay < a.recoveryDelay) out.push('a shorter recovery delay'); + if (b.proposalTtl > a.proposalTtl) out.push('a longer proposal lifetime'); + if (thresholdLoosens(a.highExposureBps, b.highExposureBps)) + out.push('a higher exposure threshold before escalation'); + if (thresholdLoosens(a.criticalExposureBps, b.criticalExposureBps)) + out.push('a higher exposure threshold before critical'); + if (thresholdLoosens(a.hardBlockExposureBps, b.hardBlockExposureBps)) + out.push('a weaker exposure hard block'); + if (thresholdLoosens(a.envelopeBps, b.envelopeBps)) out.push('a larger loss envelope'); + if (a.envelopeBps !== 0 && b.envelopeWindow < a.envelopeWindow) + out.push('a shorter envelope window'); + return out; +} + +export function limitReductions(a: AssetLimits, b: AssetLimits): string[] { + const out: string[] = []; + if (!a.approved && b.approved) out.push('approving a new asset'); + if (b.lowMax > a.lowMax) out.push('a higher routine maximum'); + if (b.highMax > a.highMax) out.push('a higher high-tier maximum'); + if (thresholdLoosens(a.perTxMax, b.perTxMax)) out.push('a weaker per-transaction cap'); + if (thresholdLoosens(a.dailyMax, b.dailyMax)) out.push('a weaker daily cap'); + return out; +} + +/** Raising trust, or lifting a restriction, is security-reducing; tightening is not. */ +export function trustReduces(from: Trust, to: Trust): boolean { + const heldBack = from === Trust.RESTRICTED || from === Trust.BLOCKED || from === Trust.UNKNOWN; + if (heldBack && to !== Trust.RESTRICTED && to !== Trust.BLOCKED) return true; + if (to === Trust.BLOCKED || to === Trust.RESTRICTED) return false; + return to > from; +} diff --git a/sdk/src/engine/policyFields.ts b/sdk/src/engine/policyFields.ts new file mode 100644 index 0000000..3a2adfb --- /dev/null +++ b/sdk/src/engine/policyFields.ts @@ -0,0 +1,60 @@ +import { HOUR, type Policy } from '../types.js'; + +/** Field metadata for the policy editor, shared by every interface. */ +export type Unit = 'count' | 'hours' | 'bps' | 'bool'; +export type PolicyField = { + key: keyof Policy; + label: string; + hint: string; + unit: Unit; + max?: number; +}; + +/** Editors show hours; the policy stores milliseconds. */ +export const HOUR_MS = HOUR; +export const toHours = (ms: number) => ms / HOUR; +export const fromHours = (h: number) => Math.round(h * HOUR); + +export const POLICY_GROUPS: { title: string; sub: string; fields: PolicyField[] }[] = [ + { + title: 'Approvals', + sub: 'How many confirmations each tier needs. Guardians confirm critical payments in addition to approvers.', + fields: [ + { key: 'approvalsLow', label: 'Routine', hint: 'Payments under the routine maximum.', unit: 'count' }, + { key: 'approvalsHigh', label: 'High risk', hint: 'Larger payments, new recipients, elevated posture.', unit: 'count' }, + { key: 'approvalsCritical', label: 'Critical', hint: 'Very large or high-exposure payments.', unit: 'count' }, + { key: 'guardianRequiredCritical', label: 'Guardian required for critical', hint: 'An independent sign-off on the largest payments.', unit: 'bool' }, + { key: 'guardianThreshold', label: 'Guardian confirmations', hint: 'For critical payments, leaving Lockdown, and recovery.', unit: 'count' }, + { key: 'governanceThreshold', label: 'Owner approvals for governance', hint: 'Policy, limits, signers, trust and mode changes.', unit: 'count' }, + ], + }, + { + title: 'Delays', + sub: 'A delay gives guardians a veto window and gives the organization time to notice.', + fields: [ + { key: 'delayHigh', label: 'High-risk delay', hint: 'Between full approval and execution.', unit: 'hours' }, + { key: 'delayCritical', label: 'Critical delay', hint: 'Also the guardian veto window.', unit: 'hours' }, + { key: 'recipientActivationDelay', label: 'New-recipient activation', hint: 'A never-paid address cannot be paid sooner.', unit: 'hours' }, + { key: 'policyChangeDelay', label: 'Security-reducing change delay', hint: 'Any loosening waits this long and can be vetoed.', unit: 'hours' }, + { key: 'recoveryDelay', label: 'Recovery delay', hint: 'Owners can cancel a recovery during it.', unit: 'hours' }, + { key: 'proposalTtl', label: 'Proposal lifetime', hint: 'Must exceed the longest delay by at least an hour.', unit: 'hours' }, + ], + }, + { + title: 'Exposure', + sub: "Share of the asset's holdings a single payment may move before it is escalated or refused.", + fields: [ + { key: 'highExposureBps', label: 'Escalate to high risk at', hint: 'Percent of holdings.', unit: 'bps', max: 100 }, + { key: 'criticalExposureBps', label: 'Escalate to critical at', hint: '', unit: 'bps', max: 100 }, + { key: 'hardBlockExposureBps', label: 'Refuse above', hint: '0 disables the hard block.', unit: 'bps', max: 100 }, + ], + }, + { + title: 'Circuit breaker', + sub: 'Cumulative outflow per window, as a share of the balance when the window opened. The payment that would cross it is not made; the vault enters Lockdown.', + fields: [ + { key: 'envelopeBps', label: 'Loss envelope', hint: '0 disables the breaker.', unit: 'bps', max: 100 }, + { key: 'envelopeWindow', label: 'Envelope window', hint: 'At least one hour.', unit: 'hours' }, + ], + }, +]; diff --git a/sdk/src/engine/posture.ts b/sdk/src/engine/posture.ts new file mode 100644 index 0000000..99f848a --- /dev/null +++ b/sdk/src/engine/posture.ts @@ -0,0 +1,78 @@ +import { DAY, type AssetLimits, type Policy } from '../types.js'; +import { fmtDuration } from './format.js'; + +/** + * Plain-language posture checks. This reports confirmed controls and obvious weaknesses; it does + * not claim to measure absolute security, and it deliberately produces no score. + */ +export type PostureItem = { ok: 'ok' | 'warn' | 'bad'; text: string }; + +export type PostureInput = { + policy: Policy; + guardians: number; + assets: { symbol: string; limits: AssetLimits }[]; +}; + +export function postureItems({ policy: p, guardians: g, assets }: PostureInput): PostureItem[] { + const everyAssetCapped = assets.length > 0 && assets.every((a) => a.limits.dailyMax > 0n); + return [ + { + ok: g > 0 ? 'ok' : 'bad', + text: g > 0 + ? `${g} independent guardian${g === 1 ? '' : 's'} configured` + : 'No guardian: nobody independent can veto or freeze the vault', + }, + { + ok: p.guardianRequiredCritical ? 'ok' : 'warn', + text: p.guardianRequiredCritical + ? 'Critical payments need guardian confirmation' + : 'Critical payments do not need a guardian', + }, + { + ok: p.delayCritical >= DAY ? 'ok' : 'warn', + text: `Critical payments wait ${fmtDuration(p.delayCritical)}`, + }, + { + ok: p.recipientActivationDelay > 0 ? 'ok' : 'bad', + text: p.recipientActivationDelay > 0 + ? `New recipients wait ${fmtDuration(p.recipientActivationDelay)}` + : 'New recipients can be paid immediately', + }, + { + ok: everyAssetCapped ? 'ok' : 'warn', + text: everyAssetCapped + ? 'A 24-hour outflow cap on every approved asset' + : 'Some approved assets have no 24-hour cap', + }, + { + ok: p.envelopeBps > 0 ? 'ok' : 'warn', + text: p.envelopeBps > 0 + ? `Circuit breaker at ${p.envelopeBps / 100}% per ${fmtDuration(p.envelopeWindow)}` + : 'No loss envelope configured', + }, + { + ok: p.policyChangeDelay > 0 ? 'ok' : 'bad', + text: p.policyChangeDelay > 0 + ? `Weakening the policy waits ${fmtDuration(p.policyChangeDelay)} and can be vetoed` + : 'The policy can be weakened immediately', + }, + { + ok: p.recoveryDelay > 0 && g > 0 ? 'ok' : 'warn', + text: p.recoveryDelay > 0 && g > 0 + ? `Signer recovery available after ${fmtDuration(p.recoveryDelay)}` + : 'No usable recovery path', + }, + { + ok: p.approvalsLow >= 2 ? 'ok' : 'warn', + text: p.approvalsLow >= 2 + ? 'Routine payments need two approvals' + : 'Routine payments need a single approval', + }, + { + ok: p.hardBlockExposureBps > 0 ? 'ok' : 'warn', + text: p.hardBlockExposureBps > 0 + ? `No single payment may exceed ${p.hardBlockExposureBps / 100}% of an asset` + : 'No hard block: a single payment could take the whole balance if approved', + }, + ]; +} diff --git a/web/src/lib/risk.ts b/sdk/src/engine/risk.ts similarity index 61% rename from web/src/lib/risk.ts rename to sdk/src/engine/risk.ts index 67a5c2d..61b59f6 100644 --- a/web/src/lib/risk.ts +++ b/sdk/src/engine/risk.ts @@ -1,25 +1,12 @@ -import { - BPS, - Mode, - REASON_AMOUNT_CRITICAL, - REASON_AMOUNT_HIGH, - REASON_EXPOSURE_CRITICAL, - REASON_EXPOSURE_HIGH, - REASON_MODE_ELEVATED, - REASON_RECIPIENT_PROBATION, - REASON_RECIPIENT_RESTRICTED, - REASON_RECIPIENT_UNKNOWN, - REASON_VELOCITY_PRESSURE, - Tier, - Trust, - type Policy, -} from "./types"; +import { BPS, Mode, Reason, Tier, Trust, type Policy } from '../types.js'; /** - * TypeScript mirror of contracts/src/SkurRisk.sol. - * Used only for the policy simulator and for previewing policies that are not deployed yet. - * For a live vault the interface calls `previewTransfer` on the contract, which is authoritative. - * test/SkurRisk.t.sol and risk.test.ts pin the same vectors. + * Mirror of `sui/sources/risk.move`. Used to preview a transfer against a policy that is not + * deployed yet, and to drive the simulator. + * + * For a live vault, prefer `previewTransfer` from `../vault`, which simulates the chain's own + * `preview_transfer` so the answer is the contract's and not this file's. These vectors are pinned + * against the Move tests in `sui/tests/engine_tests.move`. */ export type RiskInput = { amount: bigint; @@ -52,51 +39,54 @@ function atLeast(t: Tier, floor: Tier): Tier { } export function classify(i: RiskInput): RiskOutput { - let tier = Tier.LOW; + let tier: Tier = Tier.LOW; let reasons = 0; if (i.amount > i.highMax) { tier = Tier.CRITICAL; - reasons |= REASON_AMOUNT_CRITICAL; + reasons |= Reason.AMOUNT_CRITICAL; } else if (i.amount > i.lowMax) { tier = Tier.HIGH; - reasons |= REASON_AMOUNT_HIGH; + reasons |= Reason.AMOUNT_HIGH; } const exposure = exposureBps(i.amount, i.assetBalance); if (i.criticalExposureBps !== 0 && exposure >= i.criticalExposureBps) { tier = Tier.CRITICAL; - reasons |= REASON_EXPOSURE_CRITICAL; + reasons |= Reason.EXPOSURE_CRITICAL; } else if (i.highExposureBps !== 0 && exposure >= i.highExposureBps) { tier = atLeast(tier, Tier.HIGH); - reasons |= REASON_EXPOSURE_HIGH; + reasons |= Reason.EXPOSURE_HIGH; } if (i.trust === Trust.RESTRICTED) { tier = Tier.CRITICAL; - reasons |= REASON_RECIPIENT_RESTRICTED; + reasons |= Reason.RECIPIENT_RESTRICTED; } else if (i.trust === Trust.UNKNOWN) { tier = bump(tier); - reasons |= REASON_RECIPIENT_UNKNOWN | REASON_RECIPIENT_PROBATION; + reasons |= Reason.RECIPIENT_UNKNOWN | Reason.RECIPIENT_PROBATION; } else if (i.trust === Trust.NEW && i.inProbation) { tier = bump(tier); - reasons |= REASON_RECIPIENT_PROBATION; + reasons |= Reason.RECIPIENT_PROBATION; } if (i.dailyMax !== 0n && (i.daySpent + i.amount) * 2n > i.dailyMax) { tier = atLeast(tier, Tier.HIGH); - reasons |= REASON_VELOCITY_PRESSURE; + reasons |= Reason.VELOCITY_PRESSURE; } if (i.mode === Mode.ELEVATED) { tier = bump(tier); - reasons |= REASON_MODE_ELEVATED; + reasons |= Reason.MODE_ELEVATED; } return { tier, reasons, exposureBps: exposure }; } -export function requirements(p: Policy, tier: Tier): { approvals: number; guardians: number; delay: number } { +export function requirements( + p: Policy, + tier: Tier, +): { approvals: number; guardians: number; delay: number } { if (tier === Tier.LOW) return { approvals: p.approvalsLow, guardians: 0, delay: 0 }; if (tier === Tier.HIGH) return { approvals: p.approvalsHigh, guardians: 0, delay: p.delayHigh }; return { @@ -106,8 +96,12 @@ export function requirements(p: Policy, tier: Tier): { approvals: number; guardi }; } -/** Effective caps under the current mode (Elevated halves them, floored at 1 unit). */ -export function effectiveCaps(perTxMax: bigint, dailyMax: bigint, mode: Mode): { perTxMax: bigint; dailyMax: bigint } { +/** Elevated halves the caps, floored at one unit. No cap stays no cap. */ +export function effectiveCaps( + perTxMax: bigint, + dailyMax: bigint, + mode: Mode, +): { perTxMax: bigint; dailyMax: bigint } { if (mode !== Mode.ELEVATED) return { perTxMax, dailyMax }; const half = (x: bigint) => (x === 0n ? 0n : x / 2n === 0n ? 1n : x / 2n); return { perTxMax: half(perTxMax), dailyMax: half(dailyMax) }; diff --git a/sdk/src/engine/simulator.ts b/sdk/src/engine/simulator.ts new file mode 100644 index 0000000..32dd96e --- /dev/null +++ b/sdk/src/engine/simulator.ts @@ -0,0 +1,266 @@ +import { Mode, TIER_LABELS, Tier, Trust, type AssetLimits, type Policy } from '../types.js'; +import { classify, effectiveCaps, requirements } from './risk.js'; +import { fmtDuration } from './format.js'; + +/** + * Runs a policy against the attacks that actually happen, before the policy is live. Every verdict + * is the deterministic risk engine plus the vault's own limit checks — nothing here is heuristic, + * and nothing here is a score. + */ +export type Verdict = 'blocked' | 'delayed' | 'escalated' | 'allowed' | 'impossible'; + +export type ScenarioResult = { + id: string; + title: string; + narrative: string; + verdict: Verdict; + detail: string[]; +}; + +export type SimContext = { + policy: Policy; + limits: AssetLimits; + balance: bigint; + decimals: number; + symbol: string; + approverCount: number; + guardianCount: number; +}; + +function fmt(v: bigint, ctx: SimContext): string { + const s = Number(v) / 10 ** ctx.decimals; + return `${s.toLocaleString('en-US', { maximumFractionDigits: 2 })} ${ctx.symbol}`; +} + +function single( + ctx: SimContext, + amount: bigint, + trust: Trust, + inProbation: boolean, + mode: Mode, + compromised: number, +): { verdict: Verdict; detail: string[] } { + const caps = effectiveCaps(ctx.limits.perTxMax, ctx.limits.dailyMax, mode); + const detail: string[] = []; + if (mode === Mode.LOCKDOWN) + return { verdict: 'blocked', detail: ['The vault is in Lockdown: nothing outgoing executes.'] }; + if (trust === Trust.BLOCKED) return { verdict: 'blocked', detail: ['The recipient is blocked.'] }; + if (caps.perTxMax !== 0n && amount > caps.perTxMax) + return { + verdict: 'blocked', + detail: [ + `The per-transaction cap is ${fmt(caps.perTxMax, ctx)}, so the proposal is refused when it is opened.`, + ], + }; + + const r = classify({ + amount, + assetBalance: ctx.balance, + lowMax: ctx.limits.lowMax, + highMax: ctx.limits.highMax, + dailyMax: caps.dailyMax, + daySpent: 0n, + trust, + inProbation, + mode, + highExposureBps: ctx.policy.highExposureBps, + criticalExposureBps: ctx.policy.criticalExposureBps, + }); + + if (ctx.policy.hardBlockExposureBps !== 0 && r.exposureBps >= ctx.policy.hardBlockExposureBps) + return { + verdict: 'blocked', + detail: [ + `Exposure of ${(r.exposureBps / 100).toFixed(1)}% is at or above the hard block of ${ctx.policy.hardBlockExposureBps / 100}%, which no amount of approval can clear.`, + ], + }; + if (caps.dailyMax !== 0n && amount > caps.dailyMax) + return { + verdict: 'blocked', + detail: [`The daily cap is ${fmt(caps.dailyMax, ctx)}, so this cannot be opened today.`], + }; + + const envelope = + ctx.policy.envelopeBps === 0 ? 0n : (ctx.balance * BigInt(ctx.policy.envelopeBps)) / 10_000n; + if (envelope !== 0n && amount > envelope) + return { + verdict: 'blocked', + detail: [ + `The loss envelope is ${fmt(envelope, ctx)} per ${fmtDuration(ctx.policy.envelopeWindow)}. The breaker trips and the vault enters Lockdown instead of paying.`, + ], + }; + + const req = requirements(ctx.policy, r.tier); + detail.push( + `Risk tier: ${TIER_LABELS[r.tier]}, at ${(r.exposureBps / 100).toFixed(2)}% of holdings.`, + ); + detail.push( + `Needs ${req.approvals} approval${req.approvals === 1 ? '' : 's'}${ + req.guardians + ? ` and ${req.guardians} guardian confirmation${req.guardians === 1 ? '' : 's'}` + : '' + }${req.delay ? `, then waits ${fmtDuration(req.delay)}` : ', with no delay'}.`, + ); + + let delay = req.delay; + if (inProbation) { + delay = Math.max(delay, ctx.policy.recipientActivationDelay); + detail.push( + `The recipient's activation delay forces a wait of at least ${fmtDuration(ctx.policy.recipientActivationDelay)}.`, + ); + } + if (r.tier === Tier.CRITICAL || inProbation || mode !== Mode.NORMAL) + detail.push('Any guardian can veto it before it executes.'); + + if (req.guardians > ctx.guardianCount) + return { + verdict: 'impossible', + detail: [...detail, 'More guardian confirmations are required than there are guardians.'], + }; + if (compromised < req.approvals) { + detail.push( + `The attacker holds ${compromised} approver key${compromised === 1 ? '' : 's'}, fewer than required.`, + ); + return { verdict: 'blocked', detail }; + } + if (req.guardians > 0) { + detail.push('Guardian confirmation is an independent control the attacker does not hold.'); + return { verdict: 'blocked', detail }; + } + if (delay > 0) return { verdict: 'delayed', detail }; + if (r.tier > Tier.LOW) return { verdict: 'escalated', detail }; + return { verdict: 'allowed', detail }; +} + +export function runScenarios(ctx: SimContext): ScenarioResult[] { + const out: ScenarioResult[] = []; + const pct = (bps: number) => (ctx.balance * BigInt(bps)) / 10_000n; + const unit = 10n ** BigInt(ctx.decimals); + + { + const amount = 100_000n * unit; + const r = single(ctx, amount, Trust.UNKNOWN, true, Mode.NORMAL, 1); + out.push({ + id: 'single-signer-new-address', + title: 'One finance signer compromised', + narrative: `An attacker with a single approver key opens ${fmt(amount, ctx)} to an address the vault has never paid.`, + ...r, + }); + } + { + const amount = pct(4_000); + const r = single(ctx, amount, Trust.VERIFIED, false, Mode.NORMAL, 3); + out.push({ + id: 'three-signers-40pct', + title: 'Three signers compromised, 40% of the treasury', + narrative: `An attacker with three approver keys opens ${fmt(amount, ctx)} — 40% of holdings — to a recipient the vault has already verified.`, + ...r, + }); + } + { + const each = 9_900n * unit; + const total = each * 20n; + const caps = effectiveCaps(ctx.limits.perTxMax, ctx.limits.dailyMax, Mode.NORMAL); + const envelope = ctx.policy.envelopeBps === 0 ? 0n : pct(ctx.policy.envelopeBps); + const detail: string[] = []; + let verdict: Verdict = 'allowed'; + const tierEach = classify({ + amount: each, + assetBalance: ctx.balance, + lowMax: ctx.limits.lowMax, + highMax: ctx.limits.highMax, + dailyMax: caps.dailyMax, + daySpent: 0n, + trust: Trust.VERIFIED, + inProbation: false, + mode: Mode.NORMAL, + highExposureBps: ctx.policy.highExposureBps, + criticalExposureBps: ctx.policy.criticalExposureBps, + }); + detail.push(`On its own each payment is ${TIER_LABELS[tierEach.tier]} tier.`); + if (caps.dailyMax !== 0n && total > caps.dailyMax) { + const n = Number(caps.dailyMax / each); + detail.push( + `The daily cap of ${fmt(caps.dailyMax, ctx)} stops execution after ${n} payment${n === 1 ? '' : 's'}, at ${fmt(each * BigInt(n), ctx)}.`, + ); + verdict = 'blocked'; + } + if (envelope !== 0n && total > envelope) { + const n = Number(envelope / each); + detail.push( + `The loss envelope of ${fmt(envelope, ctx)} trips the breaker after ${n} payment${n === 1 ? '' : 's'}, and the vault latches into Lockdown.`, + ); + verdict = 'blocked'; + } + if (caps.dailyMax !== 0n && each * 2n > caps.dailyMax / 2n) { + detail.push("Once half the day's allowance is gone, every further payment is scored high risk."); + if (verdict === 'allowed') verdict = 'escalated'; + } + if (verdict === 'allowed') + detail.push( + 'No cumulative control is configured, so the split drain would succeed. Set a daily cap or a loss envelope.', + ); + out.push({ + id: 'split-drain', + title: 'A drain split into twenty payments', + narrative: `Twenty payments of ${fmt(each, ctx)}, ${fmt(total, ctx)} in total, inside one day.`, + verdict, + detail, + }); + } + { + const detail = [ + `Lowering any threshold, delay or cap is a security-reducing change: it waits ${fmtDuration(ctx.policy.policyChangeDelay)} and any guardian can veto it.`, + `It needs ${ctx.policy.governanceThreshold} owner approval${ctx.policy.governanceThreshold === 1 ? '' : 's'}, and cannot be opened at all while the vault is in Lockdown.`, + 'Until it takes effect, payments already in flight keep the requirements pinned when they were opened.', + ]; + const verdict: Verdict = ctx.policy.policyChangeDelay > 0 ? 'delayed' : 'allowed'; + if (verdict === 'allowed') + detail.push( + 'The policy-change delay is zero, so a compromised owner quorum could weaken the vault at once. Set a delay.', + ); + out.push({ + id: 'policy-weaken', + title: 'An attacker weakens the policy first', + narrative: 'A compromised owner quorum lowers approvals and caps before trying to drain the vault.', + verdict, + detail, + }); + } + { + out.push({ + id: 'guardian-compromised', + title: 'A guardian key is compromised', + narrative: 'An attacker holding a guardian key tries to take funds.', + verdict: 'impossible', + detail: [ + 'A guardian holds no treasury role: it cannot open, approve or execute a payment, and the contract refuses to give a guardian any treasury role.', + `The worst it can do is freeze the vault, or open a recovery that waits ${fmtDuration(ctx.policy.recoveryDelay)} and that any owner can cancel.`, + ], + }); + } + { + const r = single(ctx, ctx.limits.lowMax, Trust.VERIFIED, false, Mode.ELEVATED, 2); + out.push({ + id: 'elevated-routine', + title: 'A routine payment during an incident', + narrative: `The vault is in Elevated mode and a ${fmt(ctx.limits.lowMax, ctx)} routine payment is opened.`, + ...r, + }); + } + { + const detail = [ + 'An agent holds the proposer role only. It can open a payment and nothing else.', + `Whatever it opens still needs ${ctx.policy.approvalsLow} approval${ctx.policy.approvalsLow === 1 ? '' : 's'} from a human approver for a routine amount, and more above it.`, + 'A compromised agent is a queue full of proposals nobody approves, not a withdrawal.', + ]; + out.push({ + id: 'agent-compromised', + title: 'An automated agent is compromised', + narrative: 'An attacker takes over the key an automation uses to request payments.', + verdict: 'blocked', + detail, + }); + } + return out; +} diff --git a/sdk/src/engine/templates.ts b/sdk/src/engine/templates.ts new file mode 100644 index 0000000..8fb6fa3 --- /dev/null +++ b/sdk/src/engine/templates.ts @@ -0,0 +1,255 @@ +import { DAY, HOUR, type AssetLimits, type Policy } from '../types.js'; + +/** + * Starting policies, one per kind of organization. Nobody designs seventeen fields from first + * principles; they pick the closest fit and tighten from there, which is why every template is a + * complete and valid policy rather than a partial preset. + * + * Amounts assume a 6-decimal stablecoin (USDC on Sui) and 9-decimal SUI. + */ +export const USDC = 1_000_000n; +export const SUI = 1_000_000_000n; + +export type TemplateId = + | 'startup' + | 'team' + | 'protocol' + | 'fund' + | 'payments' + | 'nonprofit' + | 'familyOffice'; + +export type Template = { + id: TemplateId; + name: string; + tagline: string; + /** Who this is for, in the words that organization would use. */ + audience: string; + minSigners: number; + minGuardians: number; + policy: Policy; + stable: AssetLimits; + native: AssetLimits; +}; + +const startupPolicy: Policy = { + approvalsLow: 1, + approvalsHigh: 2, + approvalsCritical: 2, + governanceThreshold: 2, + guardianThreshold: 1, + guardianRequiredCritical: true, + delayHigh: 6 * HOUR, + delayCritical: 24 * HOUR, + recipientActivationDelay: 24 * HOUR, + policyChangeDelay: 24 * HOUR, + recoveryDelay: 48 * HOUR, + proposalTtl: 7 * DAY, + highExposureBps: 500, + criticalExposureBps: 2_000, + hardBlockExposureBps: 5_000, + envelopeBps: 1_000, + envelopeWindow: 24 * HOUR, +}; + +const startupStable: AssetLimits = { + approved: true, + lowMax: 5_000n * USDC, + highMax: 25_000n * USDC, + perTxMax: 100_000n * USDC, + dailyMax: 100_000n * USDC, +}; + +const startupNative: AssetLimits = { + approved: true, + lowMax: 100n * SUI, + highMax: 1_000n * SUI, + perTxMax: 0n, + dailyMax: 5_000n * SUI, +}; + +export const TEMPLATES: Template[] = [ + { + id: 'startup', + name: 'Startup', + tagline: 'Simple thresholds, a new-recipient delay, one guardian.', + audience: 'A small team paying contractors and vendors out of a seed round.', + minSigners: 2, + minGuardians: 1, + policy: startupPolicy, + stable: startupStable, + native: startupNative, + }, + { + id: 'team', + name: 'Operating team', + tagline: 'Tiered approvals, velocity limits, supplier trust.', + audience: 'A company with a finance function and recurring suppliers.', + minSigners: 3, + minGuardians: 1, + policy: { ...startupPolicy, approvalsCritical: 3, delayHigh: 4 * HOUR, envelopeBps: 500 }, + stable: { + ...startupStable, + lowMax: 10_000n * USDC, + highMax: 50_000n * USDC, + perTxMax: 150_000n * USDC, + dailyMax: 150_000n * USDC, + }, + native: startupNative, + }, + { + id: 'protocol', + name: 'Protocol / DAO', + tagline: 'Higher quorum, long timelocks, two guardians.', + audience: 'A protocol treasury answerable to tokenholders, where every move is public.', + minSigners: 4, + minGuardians: 2, + policy: { + ...startupPolicy, + approvalsLow: 2, + approvalsHigh: 3, + approvalsCritical: 4, + governanceThreshold: 3, + guardianThreshold: 2, + delayHigh: 24 * HOUR, + delayCritical: 72 * HOUR, + policyChangeDelay: 72 * HOUR, + recoveryDelay: 7 * DAY, + proposalTtl: 14 * DAY, + highExposureBps: 200, + criticalExposureBps: 1_000, + hardBlockExposureBps: 2_500, + envelopeBps: 500, + }, + stable: { + ...startupStable, + lowMax: 25_000n * USDC, + highMax: 100_000n * USDC, + perTxMax: 500_000n * USDC, + dailyMax: 500_000n * USDC, + }, + native: { ...startupNative, lowMax: 1_000n * SUI, highMax: 10_000n * SUI, dailyMax: 50_000n * SUI }, + }, + { + id: 'fund', + name: 'Fund', + tagline: 'Tight exposure limits, strong guardians, long critical delays.', + audience: 'A fund managing outside capital, where a single bad transfer is existential.', + minSigners: 3, + minGuardians: 2, + policy: { + ...startupPolicy, + approvalsLow: 2, + approvalsHigh: 3, + approvalsCritical: 3, + guardianThreshold: 2, + delayHigh: 12 * HOUR, + delayCritical: 72 * HOUR, + recipientActivationDelay: 48 * HOUR, + policyChangeDelay: 72 * HOUR, + recoveryDelay: 7 * DAY, + proposalTtl: 14 * DAY, + highExposureBps: 100, + criticalExposureBps: 500, + hardBlockExposureBps: 2_000, + envelopeBps: 300, + }, + stable: { + ...startupStable, + lowMax: 50_000n * USDC, + highMax: 250_000n * USDC, + perTxMax: 1_000_000n * USDC, + dailyMax: 1_000_000n * USDC, + }, + native: { ...startupNative, lowMax: 1_000n * SUI, highMax: 10_000n * SUI, dailyMax: 50_000n * SUI }, + }, + { + id: 'payments', + name: 'Payments company', + tagline: 'High routine throughput, a tight envelope, an agent proposer.', + audience: + 'A business paying many small amounts automatically, where volume is normal and a spike is not.', + minSigners: 3, + minGuardians: 1, + policy: { + ...startupPolicy, + approvalsLow: 1, + approvalsHigh: 2, + approvalsCritical: 3, + delayHigh: 1 * HOUR, + delayCritical: 12 * HOUR, + recipientActivationDelay: 6 * HOUR, + policyChangeDelay: 48 * HOUR, + proposalTtl: 3 * DAY, + highExposureBps: 100, + criticalExposureBps: 500, + hardBlockExposureBps: 1_500, + // The envelope, not the per-payment caps, is what defends a high-volume treasury. + envelopeBps: 200, + envelopeWindow: 6 * HOUR, + }, + stable: { + ...startupStable, + lowMax: 2_500n * USDC, + highMax: 25_000n * USDC, + perTxMax: 25_000n * USDC, + dailyMax: 500_000n * USDC, + }, + native: startupNative, + }, + { + id: 'nonprofit', + name: 'Nonprofit', + tagline: 'Restricted recipients, separation of duties, a full audit trail.', + audience: 'An organization spending donated money that has to account for all of it.', + minSigners: 3, + minGuardians: 1, + policy: { + ...startupPolicy, + approvalsLow: 2, + approvalsHigh: 2, + approvalsCritical: 3, + recipientActivationDelay: 72 * HOUR, + delayHigh: 12 * HOUR, + delayCritical: 48 * HOUR, + envelopeBps: 500, + }, + stable: startupStable, + native: startupNative, + }, + { + id: 'familyOffice', + name: 'Family office', + tagline: 'Conservative limits, a cold guardian, a long recovery path.', + audience: 'Long-term holdings that move rarely, where availability matters less than safety.', + minSigners: 2, + minGuardians: 1, + policy: { + ...startupPolicy, + delayHigh: 24 * HOUR, + delayCritical: 72 * HOUR, + recipientActivationDelay: 72 * HOUR, + policyChangeDelay: 72 * HOUR, + recoveryDelay: 14 * DAY, + proposalTtl: 21 * DAY, + highExposureBps: 100, + criticalExposureBps: 500, + hardBlockExposureBps: 1_500, + envelopeBps: 200, + }, + stable: { + ...startupStable, + lowMax: 10_000n * USDC, + highMax: 50_000n * USDC, + perTxMax: 250_000n * USDC, + dailyMax: 250_000n * USDC, + }, + native: startupNative, + }, +]; + +export function templateById(id: TemplateId): Template { + const t = TEMPLATES.find((x) => x.id === id); + if (!t) throw new Error(`unknown template ${id}`); + return t; +} diff --git a/sdk/src/errors.ts b/sdk/src/errors.ts new file mode 100644 index 0000000..2f97267 --- /dev/null +++ b/sdk/src/errors.ts @@ -0,0 +1,123 @@ +/** + * Move abort codes, mapped to the sentence a person should read. The numbers are the constants in + * `sui/sources/policy.move` and `sui/sources/vault.move`; a change there is a change here. + * + * A Sui abort arrives as a `MoveAbort` in the execution status, carrying the module and the code, + * so an interface can be specific about why a transaction was refused rather than saying "failed". + */ +export const POLICY_ERRORS: Record = { + 1: 'Routine transfers must need at least one approval.', + 2: 'High-risk transfers cannot need fewer approvals than routine ones.', + 3: 'Critical transfers cannot need fewer approvals than high-risk ones.', + 4: 'Governance must need at least one owner.', + 5: 'Guardian confirmation is required but no guardian confirmations were set.', + 6: 'The critical delay cannot be shorter than the high-risk delay.', + 7: 'The high exposure threshold cannot exceed the critical one.', + 8: 'Exposure thresholds cannot exceed 100%.', + 9: 'The hard block cannot sit below the critical exposure threshold.', + 10: 'The hard block cannot exceed 100%.', + 11: 'The loss envelope cannot exceed 100%.', + 12: 'The envelope window must be at least one hour.', + 13: 'Proposal lifetime must exceed the longest delay by at least an hour.', + 20: 'The routine maximum cannot exceed the high-tier maximum.', + 21: 'The per-transaction cap cannot exceed the daily cap.', + 30: 'A vault needs at least one owner.', + 31: 'A vault needs at least one executor.', + 32: 'The governance threshold exceeds the number of owners.', + 33: 'Critical approvals exceed the number of approvers.', + 34: 'The guardian threshold exceeds the number of guardians.', +}; + +export const VAULT_ERRORS: Record = { + 100: 'That address is not a member of this vault.', + 101: 'Only an owner can do that.', + 102: 'Only an approver can approve a payment.', + 103: 'Only an executor can execute.', + 104: 'Only a guardian can do that.', + 105: 'That role cannot open proposals.', + 106: 'Those roles are not a valid combination.', + 107: 'The vault is in Lockdown.', + 108: 'That asset is not approved for this vault.', + 109: 'No such proposal.', + 110: 'That proposal is no longer pending.', + 111: 'That proposal is of a different kind.', + 112: 'That proposal is for a different asset.', + 113: 'The waiting period has not elapsed yet.', + 114: 'That proposal has expired.', + 115: 'That proposal has not expired yet.', + 116: 'Not enough approvals from current signers.', + 117: 'Not enough guardian confirmations.', + 118: 'The recipient is blocked.', + 119: 'The recipient is still in its activation delay.', + 120: 'Above the per-transaction cap for this asset.', + 121: "Above today's remaining allowance for this asset.", + 122: 'Above the hard block: no amount of approval can clear this.', + 123: 'The vault does not hold enough of that asset.', + 124: 'The amount must be greater than zero.', + 125: 'That proposal is not one a guardian can veto.', + 126: 'That address is already registered.', + 128: 'That is not a valid trust level.', + 129: 'That is not a valid security mode.', + 130: 'That is not a relaxation of the current mode.', + 131: 'Only the proposer or an owner can cancel.', + 133: 'A guardian cannot hold a treasury role.', + 134: 'That asset was already configured.', + 135: 'The member list and the role list are different lengths.', + 136: 'A guardian cannot nominate themselves as a replacement signer.', + 137: 'No such member.', + 138: 'That signer has already voted on this proposal.', + 139: 'Those roles are already set.', +}; + +export function describeAbort(module: string | undefined, code: number | bigint): string { + const n = Number(code); + if (module === 'policy' && POLICY_ERRORS[n]) return POLICY_ERRORS[n]; + if (module === 'vault' && VAULT_ERRORS[n]) return VAULT_ERRORS[n]; + return POLICY_ERRORS[n] ?? VAULT_ERRORS[n] ?? `The vault refused the transaction (code ${n}).`; +} + +/** + * Pull the abort code out of a failed execution, whatever shape the message arrives in. + * + * The shapes differ by where the failure happened. A transaction rejected during resolution reads + * `MoveAbort in 1st command, abort code: 119, in '0x…'`; one that executed and aborted reads + * `MoveAbort(MoveLocation { … name: Identifier("vault") … }, 119) in command 0`. + * + * Scanning loosely for the first run of digits is what a careless version of this does, and it is + * wrong in a way that matters: in the first shape it finds the `1` of `1st command` and reports + * code 1, so a signer told "the recipient is still in its activation delay" is instead told + * "routine transfers must need at least one approval". Both patterns below are anchored on text + * that only ever precedes the real code. + */ +function abortCode(text: string): number | null { + const labelled = /abort\s*code:?\s*(\d+)/i.exec(text); + if (labelled) return Number(labelled[1]); + const call = /MoveAbort\([\s\S]*,\s*(\d+)\s*\)/.exec(text); + if (call) return Number(call[1]); + return null; +} + +function abortModule(text: string): string | undefined { + const identifier = /name:\s*Identifier\(\s*"(\w+)"\s*\)/.exec(text); + if (identifier) return identifier[1]; + const qualified = /0x[0-9a-fA-F]+::(\w+)::/.exec(text); + if (qualified) return qualified[1]; + return undefined; +} + +/** Turn a failure into the sentence the contract means by it. */ +export function describeFailure(error: unknown): string { + const text = error instanceof Error ? error.message : String(error); + const code = abortCode(text); + if (code !== null) return describeAbort(abortModule(text), code); + if (/InsufficientGas|GasBalanceTooLow|InsufficientCoinBalance/.test(text)) { + return 'Not enough SUI to pay for gas.'; + } + if (/ObjectNotFound|ObjectDeleted/.test(text)) { + return 'That object no longer exists on this network.'; + } + if (/ObjectVersionUnavailable|ObjectLockConflict/.test(text)) { + return 'The vault was changed by someone else a moment ago. Read it again and retry.'; + } + return text; +} diff --git a/sdk/src/events.ts b/sdk/src/events.ts new file mode 100644 index 0000000..d2968d6 --- /dev/null +++ b/sdk/src/events.ts @@ -0,0 +1,457 @@ +import type { ClientWithCoreApi } from '@mysten/sui/client'; +import { bcs } from '@mysten/sui/bcs'; +import { AssetLimitsBcs, PolicyBcs } from './bcs.js'; +import { moveType } from './network.js'; +import type { Kind, Mode, Status, Tier, Trust } from './types.js'; + +/** + * The vault's events, which are the whole history. Every state change emits one, so an indexer or + * a notification service never has to diff object versions to know what happened. + * + * Parsed from `event.bcs`, not `event.json`: the SDK documents the JSON shape as varying between + * transports, and an audit trail cannot depend on which endpoint read it. + */ + +const UID = bcs.Address; +const OptionTypeName = bcs.option(bcs.struct('TypeName', { name: bcs.string() })); + +function typeName(v: { name: string } | null): string | null { + if (!v) return null; + return v.name.startsWith('0x') ? v.name : `0x${v.name}`; +} + +export const EVENT_NAMES = [ + 'VaultCreated', + 'Deposited', + 'ProposalOpened', + 'Approved', + 'Rejected', + 'Confirmed', + 'Settled', + 'Executed', + 'BreakerTripped', + 'ModeChanged', + 'PolicyChanged', + 'LimitsChanged', + 'MemberChanged', + 'RecipientRegistered', + 'RecipientTrustChanged', + 'Recovered', +] as const; +export type EventName = (typeof EVENT_NAMES)[number]; + +const VaultCreatedBcs = bcs.struct('VaultCreated', { + vault: UID, + name: bcs.string(), + creator: bcs.Address, + owner_count: bcs.u64(), + guardian_count: bcs.u64(), + at: bcs.u64(), +}); + +const DepositedBcs = bcs.struct('Deposited', { + vault: UID, + asset: bcs.struct('TypeName', { name: bcs.string() }), + amount: bcs.u64(), + balance: bcs.u64(), + from: bcs.Address, + at: bcs.u64(), +}); + +const ProposalOpenedBcs = bcs.struct('ProposalOpened', { + vault: UID, + proposal: bcs.u64(), + kind: bcs.u8(), + proposer: bcs.Address, + asset: OptionTypeName, + amount: bcs.u64(), + recipient: bcs.Address, + tier: bcs.u8(), + reasons: bcs.u16(), + exposure_bps: bcs.u64(), + req_approvals: bcs.u8(), + req_guardians: bcs.u8(), + executable_at: bcs.u64(), + expires_at: bcs.u64(), + reduction_mask: bcs.u32(), + memo: bcs.string(), + at: bcs.u64(), +}); + +const ApprovedBcs = bcs.struct('Approved', { + vault: UID, + proposal: bcs.u64(), + approver: bcs.Address, + approvals: bcs.u64(), + req_approvals: bcs.u8(), + at: bcs.u64(), +}); + +const RejectedBcs = bcs.struct('Rejected', { + vault: UID, + proposal: bcs.u64(), + rejecter: bcs.Address, + rejections: bcs.u64(), + req_approvals: bcs.u8(), + settled: bcs.bool(), + at: bcs.u64(), +}); + +const ConfirmedBcs = bcs.struct('Confirmed', { + vault: UID, + proposal: bcs.u64(), + guardian: bcs.Address, + confirmations: bcs.u64(), + req_guardians: bcs.u8(), + at: bcs.u64(), +}); + +const SettledBcs = bcs.struct('Settled', { + vault: UID, + proposal: bcs.u64(), + kind: bcs.u8(), + status: bcs.u8(), + actor: bcs.Address, + at: bcs.u64(), +}); + +const ExecutedBcs = bcs.struct('Executed', { + vault: UID, + proposal: bcs.u64(), + kind: bcs.u8(), + asset: OptionTypeName, + amount: bcs.u64(), + recipient: bcs.Address, + executor: bcs.Address, + tier: bcs.u8(), + balance_after: bcs.u64(), + day_spent: bcs.u64(), + at: bcs.u64(), +}); + +const BreakerTrippedBcs = bcs.struct('BreakerTripped', { + vault: UID, + proposal: bcs.u64(), + asset: bcs.struct('TypeName', { name: bcs.string() }), + amount: bcs.u64(), + envelope_spent: bcs.u64(), + envelope_limit: bcs.u64(), + envelope_basis: bcs.u64(), + at: bcs.u64(), +}); + +const ModeChangedBcs = bcs.struct('ModeChanged', { + vault: UID, + from: bcs.u8(), + to: bcs.u8(), + actor: bcs.Address, + reasons: bcs.u16(), + at: bcs.u64(), +}); + +const PolicyChangedBcs = bcs.struct('PolicyChanged', { + vault: UID, + proposal: bcs.u64(), + version: bcs.u64(), + reduction_mask: bcs.u32(), + at: bcs.u64(), +}); + +const LimitsChangedBcs = bcs.struct('LimitsChanged', { + vault: UID, + proposal: bcs.u64(), + asset: bcs.struct('TypeName', { name: bcs.string() }), + approved: bcs.bool(), + low_max: bcs.u64(), + high_max: bcs.u64(), + per_tx_max: bcs.u64(), + daily_max: bcs.u64(), + reduction_mask: bcs.u32(), + at: bcs.u64(), +}); + +const MemberChangedBcs = bcs.struct('MemberChanged', { + vault: UID, + proposal: bcs.u64(), + member: bcs.Address, + roles_before: bcs.u8(), + roles_after: bcs.u8(), + at: bcs.u64(), +}); + +const RecipientRegisteredBcs = bcs.struct('RecipientRegistered', { + vault: UID, + recipient: bcs.Address, + trust: bcs.u8(), + activates_at: bcs.u64(), + label: bcs.string(), + actor: bcs.Address, + at: bcs.u64(), +}); + +const RecipientTrustChangedBcs = bcs.struct('RecipientTrustChanged', { + vault: UID, + proposal: bcs.u64(), + recipient: bcs.Address, + from: bcs.u8(), + to: bcs.u8(), + actor: bcs.Address, + at: bcs.u64(), +}); + +const RecoveredBcs = bcs.struct('Recovered', { + vault: UID, + proposal: bcs.u64(), + lost: bcs.Address, + replacement: bcs.Address, + roles: bcs.u8(), + at: bcs.u64(), +}); + +export type SkurEvent = + | { name: 'VaultCreated'; vault: string; vaultName: string; creator: string; ownerCount: number; guardianCount: number; at: number } + | { name: 'Deposited'; vault: string; asset: string; amount: bigint; balance: bigint; from: string; at: number } + | { + name: 'ProposalOpened'; + vault: string; + proposal: bigint; + kind: Kind; + proposer: string; + asset: string | null; + amount: bigint; + recipient: string; + tier: Tier; + reasons: number; + exposureBps: number; + reqApprovals: number; + reqGuardians: number; + executableAt: number; + expiresAt: number; + reductionMask: number; + memo: string; + at: number; + } + | { name: 'Approved'; vault: string; proposal: bigint; approver: string; approvals: number; reqApprovals: number; at: number } + | { + name: 'Rejected'; + vault: string; + proposal: bigint; + rejecter: string; + rejections: number; + reqApprovals: number; + settled: boolean; + at: number; + } + | { name: 'Confirmed'; vault: string; proposal: bigint; guardian: string; confirmations: number; reqGuardians: number; at: number } + | { name: 'Settled'; vault: string; proposal: bigint; kind: Kind; status: Status; actor: string; at: number } + | { + name: 'Executed'; + vault: string; + proposal: bigint; + kind: Kind; + asset: string | null; + amount: bigint; + recipient: string; + executor: string; + tier: Tier; + balanceAfter: bigint; + daySpent: bigint; + at: number; + } + | { + name: 'BreakerTripped'; + vault: string; + proposal: bigint; + asset: string; + amount: bigint; + envelopeSpent: bigint; + envelopeLimit: bigint; + envelopeBasis: bigint; + at: number; + } + | { name: 'ModeChanged'; vault: string; from: Mode; to: Mode; actor: string; reasons: number; at: number } + | { name: 'PolicyChanged'; vault: string; proposal: bigint; version: bigint; reductionMask: number; at: number } + | { + name: 'LimitsChanged'; + vault: string; + proposal: bigint; + asset: string; + approved: boolean; + lowMax: bigint; + highMax: bigint; + perTxMax: bigint; + dailyMax: bigint; + reductionMask: number; + at: number; + } + | { name: 'MemberChanged'; vault: string; proposal: bigint; member: string; rolesBefore: number; rolesAfter: number; at: number } + | { name: 'RecipientRegistered'; vault: string; recipient: string; trust: Trust; activatesAt: number; label: string; actor: string; at: number } + | { name: 'RecipientTrustChanged'; vault: string; proposal: bigint; recipient: string; from: Trust; to: Trust; actor: string; at: number } + | { name: 'Recovered'; vault: string; proposal: bigint; lost: string; replacement: string; roles: number; at: number }; + +/** Ledger position, so an indexer can resume exactly where it stopped. */ +export type EventPosition = { + transactionDigest: string; + eventIndex: number; + checkpoint?: string; + timestampMs?: number; +}; + +export type DecodedEvent = { event: SkurEvent; position: EventPosition }; + +const n = (v: string | number | bigint) => Number(v); +const b = (v: string | number | bigint) => BigInt(v); + +/** Decode one Move event into a tagged union, or null if it is not one of ours. */ +export function decodeEvent(eventType: string, bytes: Uint8Array): SkurEvent | null { + const name = eventType.split('::').pop() as EventName | undefined; + switch (name) { + case 'VaultCreated': { + const e = VaultCreatedBcs.parse(bytes); + return { name, vault: e.vault, vaultName: e.name, creator: e.creator, ownerCount: n(e.owner_count), guardianCount: n(e.guardian_count), at: n(e.at) }; + } + case 'Deposited': { + const e = DepositedBcs.parse(bytes); + return { name, vault: e.vault, asset: typeName(e.asset)!, amount: b(e.amount), balance: b(e.balance), from: e.from, at: n(e.at) }; + } + case 'ProposalOpened': { + const e = ProposalOpenedBcs.parse(bytes); + return { + name, + vault: e.vault, + proposal: b(e.proposal), + kind: n(e.kind) as Kind, + proposer: e.proposer, + asset: typeName(e.asset), + amount: b(e.amount), + recipient: e.recipient, + tier: n(e.tier) as Tier, + reasons: n(e.reasons), + exposureBps: n(e.exposure_bps), + reqApprovals: n(e.req_approvals), + reqGuardians: n(e.req_guardians), + executableAt: n(e.executable_at), + expiresAt: n(e.expires_at), + reductionMask: n(e.reduction_mask), + memo: e.memo, + at: n(e.at), + }; + } + case 'Approved': { + const e = ApprovedBcs.parse(bytes); + return { name, vault: e.vault, proposal: b(e.proposal), approver: e.approver, approvals: n(e.approvals), reqApprovals: n(e.req_approvals), at: n(e.at) }; + } + case 'Rejected': { + const e = RejectedBcs.parse(bytes); + return { name, vault: e.vault, proposal: b(e.proposal), rejecter: e.rejecter, rejections: n(e.rejections), reqApprovals: n(e.req_approvals), settled: e.settled, at: n(e.at) }; + } + case 'Confirmed': { + const e = ConfirmedBcs.parse(bytes); + return { name, vault: e.vault, proposal: b(e.proposal), guardian: e.guardian, confirmations: n(e.confirmations), reqGuardians: n(e.req_guardians), at: n(e.at) }; + } + case 'Settled': { + const e = SettledBcs.parse(bytes); + return { name, vault: e.vault, proposal: b(e.proposal), kind: n(e.kind) as Kind, status: n(e.status) as Status, actor: e.actor, at: n(e.at) }; + } + case 'Executed': { + const e = ExecutedBcs.parse(bytes); + return { name, vault: e.vault, proposal: b(e.proposal), kind: n(e.kind) as Kind, asset: typeName(e.asset), amount: b(e.amount), recipient: e.recipient, executor: e.executor, tier: n(e.tier) as Tier, balanceAfter: b(e.balance_after), daySpent: b(e.day_spent), at: n(e.at) }; + } + case 'BreakerTripped': { + const e = BreakerTrippedBcs.parse(bytes); + return { name, vault: e.vault, proposal: b(e.proposal), asset: typeName(e.asset)!, amount: b(e.amount), envelopeSpent: b(e.envelope_spent), envelopeLimit: b(e.envelope_limit), envelopeBasis: b(e.envelope_basis), at: n(e.at) }; + } + case 'ModeChanged': { + const e = ModeChangedBcs.parse(bytes); + return { name, vault: e.vault, from: n(e.from) as Mode, to: n(e.to) as Mode, actor: e.actor, reasons: n(e.reasons), at: n(e.at) }; + } + case 'PolicyChanged': { + const e = PolicyChangedBcs.parse(bytes); + return { name, vault: e.vault, proposal: b(e.proposal), version: b(e.version), reductionMask: n(e.reduction_mask), at: n(e.at) }; + } + case 'LimitsChanged': { + const e = LimitsChangedBcs.parse(bytes); + return { name, vault: e.vault, proposal: b(e.proposal), asset: typeName(e.asset)!, approved: e.approved, lowMax: b(e.low_max), highMax: b(e.high_max), perTxMax: b(e.per_tx_max), dailyMax: b(e.daily_max), reductionMask: n(e.reduction_mask), at: n(e.at) }; + } + case 'MemberChanged': { + const e = MemberChangedBcs.parse(bytes); + return { name, vault: e.vault, proposal: b(e.proposal), member: e.member, rolesBefore: n(e.roles_before), rolesAfter: n(e.roles_after), at: n(e.at) }; + } + case 'RecipientRegistered': { + const e = RecipientRegisteredBcs.parse(bytes); + return { name, vault: e.vault, recipient: e.recipient, trust: n(e.trust) as Trust, activatesAt: n(e.activates_at), label: e.label, actor: e.actor, at: n(e.at) }; + } + case 'RecipientTrustChanged': { + const e = RecipientTrustChangedBcs.parse(bytes); + return { name, vault: e.vault, proposal: b(e.proposal), recipient: e.recipient, from: n(e.from) as Trust, to: n(e.to) as Trust, actor: e.actor, at: n(e.at) }; + } + case 'Recovered': { + const e = RecoveredBcs.parse(bytes); + return { name, vault: e.vault, proposal: b(e.proposal), lost: e.lost, replacement: e.replacement, roles: n(e.roles), at: n(e.at) }; + } + default: + return null; + } +} + +/** + * Page through the package's events. The filter is `emitModule`, which is one predicate and so + * works on every transport; events for other vaults are dropped client-side when `vaultId` is + * given, because a module-wide filter is the finest the portable API offers. + */ +export async function listVaultEvents( + client: ClientWithCoreApi, + input: { + packageId: string; + vaultId?: string; + limit?: number; + /** Walk forwards from here. Use for indexing and for polling what is new. */ + after?: string | null; + /** Walk backwards from here. Use for an interface showing newest first. */ + before?: string | null; + order?: 'ascending' | 'descending'; + }, +): Promise<{ + events: DecodedEvent[]; + startCursor: string | null; + endCursor: string | null; + hasNextPage: boolean; +}> { + const page = await client.core.listEvents({ + filter: { emitModule: `${input.packageId}::vault` }, + limit: input.limit ?? 50, + ...(input.after != null ? { after: input.after } : {}), + ...(input.before != null ? { before: input.before } : {}), + ...(input.after == null && input.before == null + ? { order: input.order ?? 'descending' } + : {}), + }); + const events: DecodedEvent[] = []; + for (const e of page.events) { + const decoded = decodeEvent(e.eventType, e.bcs); + if (!decoded) continue; + if (input.vaultId && decoded.vault !== input.vaultId) continue; + events.push({ + event: decoded, + position: { + transactionDigest: e.transactionDigest, + eventIndex: e.eventIndex, + checkpoint: e.checkpoint ?? undefined, + timestampMs: decoded.at, + }, + }); + } + return { + events, + startCursor: page.startCursor, + endCursor: page.endCursor, + hasNextPage: page.hasNextPage, + }; +} + +/** The Move type of one of our events, for a precise `eventType` filter. */ +export function eventType(packageId: string, name: EventName): string { + return moveType(packageId, 'vault', name); +} + +export { PolicyBcs, AssetLimitsBcs }; diff --git a/sdk/src/index.ts b/sdk/src/index.ts new file mode 100644 index 0000000..0afe8be --- /dev/null +++ b/sdk/src/index.ts @@ -0,0 +1,14 @@ +/** + * The Skur SDK: everything an interface, a backend or a script needs to read and drive a vault on + * Sui. + * + * Transport is gRPC or GraphQL. JSON-RPC is not used anywhere, directly or transitively. + */ +export * from './network.js'; +export * from './types.js'; +export * from './errors.js'; +export * from './events.js'; +export * from './vault.js'; +export * as tx from './tx.js'; +export * from './engine/index.js'; +export { bcs } from '@mysten/sui/bcs'; diff --git a/sdk/src/network.ts b/sdk/src/network.ts new file mode 100644 index 0000000..78095ee --- /dev/null +++ b/sdk/src/network.ts @@ -0,0 +1,71 @@ +import { SuiGrpcClient } from '@mysten/sui/grpc'; +import { SuiGraphQLClient } from '@mysten/sui/graphql'; +import type { ClientWithCoreApi } from '@mysten/sui/client'; + +/** + * Transport policy for this project: gRPC first, GraphQL where a query needs shapes gRPC's + * unified methods do not carry. + * + * JSON-RPC is deliberately absent. It was switched off on Sui Foundation mainnet full nodes in + * the week of 27 July 2026 and is scheduled for full removal, code included, in mid-October 2026. + * Nothing here may depend on it, including transitively: `@mysten/dapp-kit` 1.x is JSON-RPC only + * and is why the frontend uses `@mysten/dapp-kit-react` 2.x instead. + */ +export type Network = 'mainnet' | 'testnet' | 'devnet' | 'localnet'; + +export const GRPC_ENDPOINTS: Record = { + mainnet: 'https://fullnode.mainnet.sui.io:443', + testnet: 'https://fullnode.testnet.sui.io:443', + devnet: 'https://fullnode.devnet.sui.io:443', + localnet: 'http://127.0.0.1:9000', +}; + +export const GRAPHQL_ENDPOINTS: Record = { + mainnet: 'https://sui-mainnet.mystenlabs.com/graphql', + testnet: 'https://sui-testnet.mystenlabs.com/graphql', + devnet: 'https://sui-devnet.mystenlabs.com/graphql', + localnet: 'http://127.0.0.1:9125/graphql', +}; + +/** The shared `Clock` every time-dependent entry function reads. */ +export const CLOCK_ID = '0x6'; + +export function grpcClient(network: Network, baseUrl = GRPC_ENDPOINTS[network]) { + return new SuiGrpcClient({ network, baseUrl }); +} + +export function graphqlClient(network: Network, url = GRAPHQL_ENDPOINTS[network]) { + return new SuiGraphQLClient({ network, url }); +} + +/** + * Where the package and its vaults live. One object per environment, written by the deploy + * script, so nothing in the app hardcodes an address. + */ +export type Deployment = { + network: Network; + /** The published `skur` package. */ + packageId: string; + /** Vaults the deploy script created, for demos and for the explorer links in the docs. */ + vaults: { id: string; name: string }[]; + publishedAtDigest?: string; + publishedAtCheckpoint?: string; +}; + +export type SkurConfig = { + deployment: Deployment; + client: ClientWithCoreApi; +}; + +/** Fully qualified Move type for a struct in the deployed package. */ +export function moveType(packageId: string, module: string, name: string) { + return `${packageId}::${module}::${name}`; +} + +export function moveTarget( + packageId: string, + module: string, + fn: string, +): `${string}::${string}::${string}` { + return `${packageId}::${module}::${fn}`; +} diff --git a/sdk/src/tx.ts b/sdk/src/tx.ts new file mode 100644 index 0000000..d50e0eb --- /dev/null +++ b/sdk/src/tx.ts @@ -0,0 +1,384 @@ +import { Transaction, type TransactionObjectArgument } from '@mysten/sui/transactions'; +import { SUI_TYPE_ARG } from '@mysten/sui/utils'; +import { CLOCK_ID, moveTarget } from './network.js'; +import type { AssetLimits, Mode, Policy, Trust } from './types.js'; + +/** + * Programmable transaction blocks for every vault action. + * + * Two things here are worth knowing. First, a policy is built on chain by calling + * `policy::new(...)` inside the same block, so the seventeen fields are validated by the module + * that owns them rather than packed into bytes by this file. Second, creating a vault is one + * block: `begin` hands back a value with no abilities, so the same transaction has to configure + * the assets and call `finish`, and there is no moment when a half-configured vault exists. + */ + +export type Builder = { tx: Transaction; packageId: string }; + +function target(pkg: string, module: string, fn: string) { + return moveTarget(pkg, module, fn); +} + +/** Build the `Policy` struct inside the block. */ +export function policyArg(b: Builder, p: Policy): TransactionObjectArgument { + const { tx, packageId } = b; + return tx.moveCall({ + target: target(packageId, 'policy', 'new'), + arguments: [ + tx.pure.u8(p.approvalsLow), + tx.pure.u8(p.approvalsHigh), + tx.pure.u8(p.approvalsCritical), + tx.pure.u8(p.governanceThreshold), + tx.pure.u8(p.guardianThreshold), + tx.pure.bool(p.guardianRequiredCritical), + tx.pure.u64(p.delayHigh), + tx.pure.u64(p.delayCritical), + tx.pure.u64(p.recipientActivationDelay), + tx.pure.u64(p.policyChangeDelay), + tx.pure.u64(p.recoveryDelay), + tx.pure.u64(p.proposalTtl), + tx.pure.u64(p.highExposureBps), + tx.pure.u64(p.criticalExposureBps), + tx.pure.u64(p.hardBlockExposureBps), + tx.pure.u64(p.envelopeBps), + tx.pure.u64(p.envelopeWindow), + ], + }); +} + +export type MemberSeed = { address: string; roles: number }; +export type AssetSeed = { coinType: string; limits: AssetLimits }; +export type RecipientSeed = { address: string; label: string }; + +/** `begin` → `setup_asset`* → `setup_recipient`* → `finish`, in one block. */ +export function createVault( + packageId: string, + input: { + name: string; + policy: Policy; + members: MemberSeed[]; + assets: AssetSeed[]; + recipients?: RecipientSeed[]; + }, +): Transaction { + const tx = new Transaction(); + const b = { tx, packageId }; + const setup = tx.moveCall({ + target: target(packageId, 'vault', 'begin'), + arguments: [ + tx.pure.string(input.name), + policyArg(b, input.policy), + tx.pure.vector('address', input.members.map((m) => m.address)), + tx.pure.vector('u8', input.members.map((m) => m.roles)), + tx.object(CLOCK_ID), + ], + }); + + for (const a of input.assets) { + tx.moveCall({ + target: target(packageId, 'vault', 'setup_asset'), + typeArguments: [a.coinType], + arguments: [ + setup, + tx.pure.bool(a.limits.approved), + tx.pure.u64(a.limits.lowMax), + tx.pure.u64(a.limits.highMax), + tx.pure.u64(a.limits.perTxMax), + tx.pure.u64(a.limits.dailyMax), + ], + }); + } + for (const r of input.recipients ?? []) { + tx.moveCall({ + target: target(packageId, 'vault', 'setup_recipient'), + arguments: [setup, tx.pure.address(r.address), tx.pure.string(r.label)], + }); + } + tx.moveCall({ target: target(packageId, 'vault', 'finish'), arguments: [setup] }); + return tx; +} + +/** + * Fund a vault. SUI comes out of the gas coin, which is the only coin a PTB always has; any other + * asset is merged from the coin objects the caller holds and then split to the exact amount, so a + * deposit never has to match a coin's balance. + */ +export function deposit( + packageId: string, + input: { vaultId: string; coinType: string; amount: bigint; coinObjectIds?: string[] }, +): Transaction { + const tx = new Transaction(); + let coin: TransactionObjectArgument; + if (input.coinType === SUI_TYPE_ARG || input.coinType === '0x2::sui::SUI') { + [coin] = tx.splitCoins(tx.gas, [tx.pure.u64(input.amount)]); + } else { + const ids = input.coinObjectIds ?? []; + if (ids.length === 0) throw new Error('coinObjectIds is required for a non-SUI deposit'); + const primary = tx.object(ids[0]); + if (ids.length > 1) tx.mergeCoins(primary, ids.slice(1).map((id) => tx.object(id))); + [coin] = tx.splitCoins(primary, [tx.pure.u64(input.amount)]); + } + tx.moveCall({ + target: target(packageId, 'vault', 'deposit'), + typeArguments: [input.coinType], + arguments: [tx.object(input.vaultId), coin, tx.object(CLOCK_ID)], + }); + return tx; +} + +export function proposeTransfer( + packageId: string, + input: { + vaultId: string; + coinType: string; + amount: bigint; + recipient: string; + memo?: string; + }, +): Transaction { + const tx = new Transaction(); + tx.moveCall({ + target: target(packageId, 'vault', 'propose_transfer'), + typeArguments: [input.coinType], + arguments: [ + tx.object(input.vaultId), + tx.pure.u64(input.amount), + tx.pure.address(input.recipient), + tx.pure.string(input.memo ?? ''), + tx.object(CLOCK_ID), + ], + }); + return tx; +} + +/** `approve`, `reject`, `confirm`, `veto`, `cancel` and `expire` all take the same arguments. */ +function proposalAction(packageId: string, fn: string, vaultId: string, proposalId: bigint) { + const tx = new Transaction(); + tx.moveCall({ + target: target(packageId, 'vault', fn), + arguments: [tx.object(vaultId), tx.pure.u64(proposalId), tx.object(CLOCK_ID)], + }); + return tx; +} + +export const approve = (pkg: string, vaultId: string, id: bigint) => + proposalAction(pkg, 'approve', vaultId, id); +export const reject = (pkg: string, vaultId: string, id: bigint) => + proposalAction(pkg, 'reject', vaultId, id); +export const confirm = (pkg: string, vaultId: string, id: bigint) => + proposalAction(pkg, 'confirm', vaultId, id); +export const veto = (pkg: string, vaultId: string, id: bigint) => + proposalAction(pkg, 'veto', vaultId, id); +export const cancel = (pkg: string, vaultId: string, id: bigint) => + proposalAction(pkg, 'cancel', vaultId, id); +export const expire = (pkg: string, vaultId: string, id: bigint) => + proposalAction(pkg, 'expire', vaultId, id); +export const executePolicy = (pkg: string, vaultId: string, id: bigint) => + proposalAction(pkg, 'execute_policy', vaultId, id); +export const executeMember = (pkg: string, vaultId: string, id: bigint) => + proposalAction(pkg, 'execute_member', vaultId, id); +export const executeRecipientTrust = (pkg: string, vaultId: string, id: bigint) => + proposalAction(pkg, 'execute_recipient_trust', vaultId, id); +export const executeModeRelax = (pkg: string, vaultId: string, id: bigint) => + proposalAction(pkg, 'execute_mode_relax', vaultId, id); +export const executeRecovery = (pkg: string, vaultId: string, id: bigint) => + proposalAction(pkg, 'execute_recovery', vaultId, id); + +export function executeTransfer( + packageId: string, + input: { vaultId: string; coinType: string; proposalId: bigint }, +): Transaction { + const tx = new Transaction(); + tx.moveCall({ + target: target(packageId, 'vault', 'execute_transfer'), + typeArguments: [input.coinType], + arguments: [ + tx.object(input.vaultId), + tx.pure.u64(input.proposalId), + tx.object(CLOCK_ID), + ], + }); + return tx; +} + +export function registerRecipient( + packageId: string, + input: { vaultId: string; recipient: string; label: string }, +): Transaction { + const tx = new Transaction(); + tx.moveCall({ + target: target(packageId, 'vault', 'register_recipient'), + arguments: [ + tx.object(input.vaultId), + tx.pure.address(input.recipient), + tx.pure.string(input.label), + tx.object(CLOCK_ID), + ], + }); + return tx; +} + +/** A guardian restricting or blocking a destination, immediately and alone. */ +export function guardianRestrict( + packageId: string, + input: { vaultId: string; recipient: string; trust: Trust }, +): Transaction { + const tx = new Transaction(); + tx.moveCall({ + target: target(packageId, 'vault', 'guardian_restrict'), + arguments: [ + tx.object(input.vaultId), + tx.pure.address(input.recipient), + tx.pure.u8(input.trust), + tx.object(CLOCK_ID), + ], + }); + return tx; +} + +export function raiseMode( + packageId: string, + input: { vaultId: string; mode: Mode; reasons?: number }, +): Transaction { + const tx = new Transaction(); + tx.moveCall({ + target: target(packageId, 'vault', 'raise_mode'), + arguments: [ + tx.object(input.vaultId), + tx.pure.u8(input.mode), + tx.pure.u16(input.reasons ?? 0), + tx.object(CLOCK_ID), + ], + }); + return tx; +} + +export function proposePolicy( + packageId: string, + input: { vaultId: string; policy: Policy }, +): Transaction { + const tx = new Transaction(); + const b = { tx, packageId }; + tx.moveCall({ + target: target(packageId, 'vault', 'propose_policy'), + arguments: [tx.object(input.vaultId), policyArg(b, input.policy), tx.object(CLOCK_ID)], + }); + return tx; +} + +export function proposeAssetLimits( + packageId: string, + input: { vaultId: string; coinType: string; limits: AssetLimits }, +): Transaction { + const tx = new Transaction(); + tx.moveCall({ + target: target(packageId, 'vault', 'propose_asset_limits'), + typeArguments: [input.coinType], + arguments: [ + tx.object(input.vaultId), + tx.pure.bool(input.limits.approved), + tx.pure.u64(input.limits.lowMax), + tx.pure.u64(input.limits.highMax), + tx.pure.u64(input.limits.perTxMax), + tx.pure.u64(input.limits.dailyMax), + tx.object(CLOCK_ID), + ], + }); + return tx; +} + +export function executeAssetLimits( + packageId: string, + input: { vaultId: string; coinType: string; proposalId: bigint }, +): Transaction { + const tx = new Transaction(); + tx.moveCall({ + target: target(packageId, 'vault', 'execute_asset_limits'), + typeArguments: [input.coinType], + arguments: [tx.object(input.vaultId), tx.pure.u64(input.proposalId), tx.object(CLOCK_ID)], + }); + return tx; +} + +/** `roles` of 0 removes the member. */ +export function proposeMember( + packageId: string, + input: { vaultId: string; member: string; roles: number }, +): Transaction { + const tx = new Transaction(); + tx.moveCall({ + target: target(packageId, 'vault', 'propose_member'), + arguments: [ + tx.object(input.vaultId), + tx.pure.address(input.member), + tx.pure.u8(input.roles), + tx.object(CLOCK_ID), + ], + }); + return tx; +} + +export function proposeRecipientTrust( + packageId: string, + input: { vaultId: string; recipient: string; trust: Trust }, +): Transaction { + const tx = new Transaction(); + tx.moveCall({ + target: target(packageId, 'vault', 'propose_recipient_trust'), + arguments: [ + tx.object(input.vaultId), + tx.pure.address(input.recipient), + tx.pure.u8(input.trust), + tx.object(CLOCK_ID), + ], + }); + return tx; +} + +export function proposeModeRelax( + packageId: string, + input: { vaultId: string; mode: Mode }, +): Transaction { + const tx = new Transaction(); + tx.moveCall({ + target: target(packageId, 'vault', 'propose_mode_relax'), + arguments: [tx.object(input.vaultId), tx.pure.u8(input.mode), tx.object(CLOCK_ID)], + }); + return tx; +} + +export function proposeRecovery( + packageId: string, + input: { vaultId: string; lost: string; replacement: string }, +): Transaction { + const tx = new Transaction(); + tx.moveCall({ + target: target(packageId, 'vault', 'propose_recovery'), + arguments: [ + tx.object(input.vaultId), + tx.pure.address(input.lost), + tx.pure.address(input.replacement), + tx.object(CLOCK_ID), + ], + }); + return tx; +} + +/** The block used to read `preview_transfer` back through a simulation. */ +export function previewTransferTx( + packageId: string, + input: { vaultId: string; coinType: string; amount: bigint; recipient: string }, +): Transaction { + const tx = new Transaction(); + tx.moveCall({ + target: target(packageId, 'vault', 'preview_transfer'), + typeArguments: [input.coinType], + arguments: [ + tx.object(input.vaultId), + tx.pure.u64(input.amount), + tx.pure.address(input.recipient), + tx.object(CLOCK_ID), + ], + }); + return tx; +} diff --git a/sdk/src/types.ts b/sdk/src/types.ts new file mode 100644 index 0000000..c1e13b2 --- /dev/null +++ b/sdk/src/types.ts @@ -0,0 +1,292 @@ +/** + * The vocabulary, kept identical to `sui/sources/types.move`. Anything that disagrees with the + * chain is a bug in this file, not in the contract. + * + * Durations are milliseconds everywhere, because that is what `sui::clock` reports. Amounts are + * `bigint` in the smallest unit of their coin type. + */ + +export const Role = { + OWNER: 1, + APPROVER: 2, + EXECUTOR: 4, + GUARDIAN: 8, + /** An automated agent: may open proposals and nothing else. */ + PROPOSER: 16, +} as const; +export type Role = (typeof Role)[keyof typeof Role]; + +export const ROLE_TREASURY_MASK = Role.OWNER | Role.APPROVER | Role.EXECUTOR | Role.PROPOSER; +export const ROLE_ALL = 31; + +export const ROLE_LABELS: Record = { + [Role.OWNER]: 'Owner', + [Role.APPROVER]: 'Approver', + [Role.EXECUTOR]: 'Executor', + [Role.GUARDIAN]: 'Guardian', + [Role.PROPOSER]: 'Proposer', +}; + +export function hasRole(bits: number, role: Role | number): boolean { + return (bits & role) === role; +} + +export function hasAnyRole(bits: number, mask: number): boolean { + return (bits & mask) !== 0; +} + +export function canPropose(bits: number): boolean { + return hasAnyRole(bits, ROLE_TREASURY_MASK); +} + +/** A guardian is the second control plane and may hold no treasury role. */ +export function rolesValid(bits: number): boolean { + if (bits === 0 || bits > ROLE_ALL) return false; + if (hasRole(bits, Role.GUARDIAN) && (bits & ROLE_TREASURY_MASK) !== 0) return false; + return true; +} + +export function describeRoles(bits: number): string[] { + return Object.values(Role) + .filter((r) => hasRole(bits, r)) + .map((r) => ROLE_LABELS[r]); +} + +export const Tier = { LOW: 0, HIGH: 1, CRITICAL: 2 } as const; +export type Tier = (typeof Tier)[keyof typeof Tier]; +export const TIER_LABELS: Record = { + [Tier.LOW]: 'Routine', + [Tier.HIGH]: 'High risk', + [Tier.CRITICAL]: 'Critical', +}; + +export const Trust = { + UNKNOWN: 0, + NEW: 1, + VERIFIED: 2, + TRUSTED: 3, + RESTRICTED: 4, + BLOCKED: 5, +} as const; +export type Trust = (typeof Trust)[keyof typeof Trust]; +export const TRUST_LABELS: Record = { + [Trust.UNKNOWN]: 'Never paid', + [Trust.NEW]: 'New', + [Trust.VERIFIED]: 'Verified', + [Trust.TRUSTED]: 'Trusted', + [Trust.RESTRICTED]: 'Restricted', + [Trust.BLOCKED]: 'Blocked', +}; + +export const Mode = { NORMAL: 0, ELEVATED: 1, LOCKDOWN: 2 } as const; +export type Mode = (typeof Mode)[keyof typeof Mode]; +export const MODE_LABELS: Record = { + [Mode.NORMAL]: 'Normal', + [Mode.ELEVATED]: 'Elevated', + [Mode.LOCKDOWN]: 'Lockdown', +}; + +export const Kind = { + TRANSFER: 0, + POLICY_UPDATE: 1, + ASSET_LIMITS: 2, + MEMBER_SET: 3, + RECIPIENT_TRUST: 4, + MODE_RELAX: 5, + RECOVERY: 6, +} as const; +export type Kind = (typeof Kind)[keyof typeof Kind]; +export const KIND_LABELS: Record = { + [Kind.TRANSFER]: 'Payment', + [Kind.POLICY_UPDATE]: 'Policy change', + [Kind.ASSET_LIMITS]: 'Asset limits', + [Kind.MEMBER_SET]: 'Signer change', + [Kind.RECIPIENT_TRUST]: 'Recipient trust', + [Kind.MODE_RELAX]: 'Lower security mode', + [Kind.RECOVERY]: 'Signer recovery', +}; + +export const Status = { + NONE: 0, + PENDING: 1, + EXECUTED: 2, + CANCELLED: 3, + VETOED: 4, + EXPIRED: 5, + /** Refused by the circuit breaker. The vault latched into Lockdown instead of paying. */ + BLOCKED: 6, + /** Turned down by the signers: as many rejections as it needed approvals. */ + REJECTED: 7, +} as const; +export type Status = (typeof Status)[keyof typeof Status]; +export const STATUS_LABELS: Record = { + [Status.NONE]: 'Unknown', + [Status.PENDING]: 'Awaiting approval', + [Status.EXECUTED]: 'Executed', + [Status.CANCELLED]: 'Cancelled', + [Status.VETOED]: 'Vetoed', + [Status.EXPIRED]: 'Expired', + [Status.BLOCKED]: 'Refused by the circuit breaker', + [Status.REJECTED]: 'Rejected', +}; + +export const Reason = { + AMOUNT_HIGH: 1, + AMOUNT_CRITICAL: 2, + EXPOSURE_HIGH: 4, + EXPOSURE_CRITICAL: 8, + RECIPIENT_PROBATION: 16, + RECIPIENT_RESTRICTED: 32, + MODE_ELEVATED: 64, + VELOCITY_PRESSURE: 128, + RECIPIENT_UNKNOWN: 256, + ENVELOPE_PRESSURE: 512, +} as const; +export type Reason = (typeof Reason)[keyof typeof Reason]; + +/** Plain-language explanations, in the order a reviewer should read them. */ +export const REASON_TEXT: [Reason, string][] = [ + [Reason.AMOUNT_CRITICAL, 'Above the high-tier maximum for this asset'], + [Reason.AMOUNT_HIGH, 'Above the routine maximum for this asset'], + [Reason.EXPOSURE_CRITICAL, 'A critical share of what the vault holds'], + [Reason.EXPOSURE_HIGH, 'A large share of what the vault holds'], + [Reason.RECIPIENT_RESTRICTED, 'The recipient is restricted'], + [Reason.RECIPIENT_UNKNOWN, 'The vault has never paid this address'], + [Reason.RECIPIENT_PROBATION, 'The recipient is still in its activation delay'], + [Reason.VELOCITY_PRESSURE, "Past half of today's allowance for this asset"], + [Reason.ENVELOPE_PRESSURE, 'Against the loss envelope for the window'], + [Reason.MODE_ELEVATED, 'The vault is in Elevated mode'], +]; + +export function describeReasons(mask: number): string[] { + return REASON_TEXT.filter(([bit]) => (mask & bit) !== 0).map(([, text]) => text); +} + +export const BPS = 10_000n; +export const SECOND = 1_000; +export const MINUTE = 60 * SECOND; +export const HOUR = 60 * MINUTE; +export const DAY = 24 * HOUR; + +/** The seventeen fields. Durations in milliseconds, exposures in basis points. */ +export type Policy = { + approvalsLow: number; + approvalsHigh: number; + approvalsCritical: number; + governanceThreshold: number; + guardianThreshold: number; + guardianRequiredCritical: boolean; + delayHigh: number; + delayCritical: number; + recipientActivationDelay: number; + policyChangeDelay: number; + recoveryDelay: number; + proposalTtl: number; + highExposureBps: number; + criticalExposureBps: number; + hardBlockExposureBps: number; + envelopeBps: number; + envelopeWindow: number; +}; + +export type AssetLimits = { + approved: boolean; + lowMax: bigint; + highMax: bigint; + perTxMax: bigint; + dailyMax: bigint; +}; + +export type Velocity = { + dayAnchor: number; + daySpent: bigint; + envelopeAnchor: number; + envelopeSpent: bigint; + /** The balance when the window opened; the envelope is measured against this, not the live + * balance, so a drain cannot shrink its own denominator. */ + envelopeBasis: bigint; +}; + +export type RecipientRecord = { + address: string; + trust: Trust; + registeredAt: number; + activatesAt: number; + paidCount: number; + lastPaid: number; + label: string; +}; + +export type Proposal = { + id: bigint; + kind: Kind; + status: Status; + proposer: string; + createdAt: number; + executableAt: number; + expiresAt: number; + reqApprovals: number; + reqGuardians: number; + tier: Tier; + reasons: number; + exposureBps: number; + policyVersion: bigint; + reductionMask: number; + approvals: string[]; + confirmations: string[]; + rejections: string[]; + asset: string | null; + amount: bigint; + recipient: string; + memo: string; + newPolicy: Policy | null; + newLimits: AssetLimits | null; + member: string; + memberPrev: string; + memberRoles: number; + trustLevel: Trust; + targetMode: Mode; +}; + +export type VaultSummary = { + id: string; + name: string; + policy: Policy; + policyVersion: bigint; + mode: Mode; + postureReasons: number; + ownerCount: number; + approverCount: number; + executorCount: number; + guardianCount: number; + nextProposal: bigint; + pendingCount: number; + createdAt: number; + /** Table and Bag object ids, needed to read the collections by dynamic field. */ + tables: { + members: string; + limits: string; + funds: string; + velocity: string; + recipients: string; + proposals: string; + }; + sizes: { + members: number; + limits: number; + funds: number; + velocity: number; + recipients: number; + proposals: number; + }; +}; + +/** What `preview_transfer` returns, in order. */ +export type TransferPreview = { + tier: Tier; + reasons: number; + exposureBps: number; + reqApprovals: number; + reqGuardians: number; + delay: number; +}; diff --git a/sdk/src/vault.ts b/sdk/src/vault.ts new file mode 100644 index 0000000..cf0c4a7 --- /dev/null +++ b/sdk/src/vault.ts @@ -0,0 +1,367 @@ +import type { ClientWithCoreApi } from '@mysten/sui/client'; +import { bcs } from '@mysten/sui/bcs'; +import { + AssetLimitsBcs, + PolicyBcs, + ProposalBcs, + RecipientBcs, + VaultBcs, + VelocityBcs, +} from './bcs.js'; +import { previewTransferTx } from './tx.js'; +import { + Kind, + Mode, + Status, + Tier, + Trust, + type AssetLimits, + type Policy, + type Proposal, + type RecipientRecord, + type TransferPreview, + type VaultSummary, + type Velocity, +} from './types.js'; + +/** + * Reading a vault. + * + * Written against `ClientWithCoreApi` rather than a concrete client, so the same code works over + * gRPC and GraphQL. The vault's collections are Move `Table`s and a `Bag`, which are dynamic + * fields on their own handles: list the names, then fetch the values. + */ + +const CONCURRENCY = 16; + +async function mapLimited(items: A[], fn: (a: A) => Promise, limit = CONCURRENCY) { + const out: B[] = new Array(items.length); + let next = 0; + const workers = Array.from({ length: Math.min(limit, items.length) }, async () => { + while (true) { + const i = next++; + if (i >= items.length) return; + out[i] = await fn(items[i]); + } + }); + await Promise.all(workers); + return out; +} + +function num(v: string | number | bigint): number { + return Number(v); +} + +function big(v: string | number | bigint): bigint { + return BigInt(v); +} + +function toPolicy(raw: ReturnType): Policy { + return { + approvalsLow: raw.approvals_low, + approvalsHigh: raw.approvals_high, + approvalsCritical: raw.approvals_critical, + governanceThreshold: raw.governance_threshold, + guardianThreshold: raw.guardian_threshold, + guardianRequiredCritical: raw.guardian_required_critical, + delayHigh: num(raw.delay_high), + delayCritical: num(raw.delay_critical), + recipientActivationDelay: num(raw.recipient_activation_delay), + policyChangeDelay: num(raw.policy_change_delay), + recoveryDelay: num(raw.recovery_delay), + proposalTtl: num(raw.proposal_ttl), + highExposureBps: num(raw.high_exposure_bps), + criticalExposureBps: num(raw.critical_exposure_bps), + hardBlockExposureBps: num(raw.hard_block_exposure_bps), + envelopeBps: num(raw.envelope_bps), + envelopeWindow: num(raw.envelope_window), + }; +} + +function toLimits(raw: ReturnType): AssetLimits { + return { + approved: raw.approved, + lowMax: big(raw.low_max), + highMax: big(raw.high_max), + perTxMax: big(raw.per_tx_max), + dailyMax: big(raw.daily_max), + }; +} + +export async function fetchVault( + client: ClientWithCoreApi, + vaultId: string, +): Promise { + const { object } = await client.core.getObject({ objectId: vaultId, include: { content: true } }); + const v = VaultBcs.parse(object.content); + return { + id: vaultId, + name: v.name, + policy: toPolicy(v.policy), + policyVersion: big(v.policy_version), + mode: num(v.mode) as Mode, + postureReasons: num(v.posture_reasons), + ownerCount: num(v.owner_count), + approverCount: num(v.approver_count), + executorCount: num(v.executor_count), + guardianCount: num(v.guardian_count), + nextProposal: big(v.next_proposal), + pendingCount: num(v.pending_count), + createdAt: num(v.created_at), + tables: { + members: v.members.id, + limits: v.limits.id, + funds: v.funds.id, + velocity: v.velocity.id, + recipients: v.recipients.id, + proposals: v.proposals.id, + }, + sizes: { + members: num(v.members.size), + limits: num(v.limits.size), + funds: num(v.funds.size), + velocity: num(v.velocity.size), + recipients: num(v.recipients.size), + proposals: num(v.proposals.size), + }, + }; +} + +/** Every dynamic field name under a handle, paged to the end. */ +async function listNames(client: ClientWithCoreApi, parentId: string) { + const names: { type: string; bcs: Uint8Array }[] = []; + let page = await client.core.listDynamicFields({ parentId, limit: 50 }); + for (;;) { + for (const f of page.dynamicFields) names.push(f.name); + if (!page.hasNextPage) break; + page = await client.core.listDynamicFields({ parentId, cursor: page.cursor, limit: 50 }); + } + return names; +} + +async function readEntries( + client: ClientWithCoreApi, + parentId: string, + parse: (bytes: Uint8Array) => V, +) { + const names = await listNames(client, parentId); + return mapLimited(names, async (name) => { + const { dynamicField } = await client.core.getDynamicField({ parentId, name }); + return { name, value: parse(dynamicField.value.bcs) }; + }); +} + +export async function fetchMembers( + client: ClientWithCoreApi, + vault: VaultSummary, +): Promise<{ address: string; roles: number }[]> { + const entries = await readEntries(client, vault.tables.members, (b) => bcs.u8().parse(b)); + return entries.map((e) => ({ + address: bcs.Address.parse(e.name.bcs), + roles: e.value, + })); +} + +/** The `TypeName` a `Table` or the funds `Bag` is keyed by. */ +function parseTypeNameKey(bytes: Uint8Array): string { + const name = bcs.struct('TypeName', { name: bcs.string() }).parse(bytes).name; + // Move reports a type name without the leading `0x`; restore it so the string matches the + // coin type an interface or an RPC filter would use. + return name.startsWith('0x') ? name : `0x${name}`; +} + +export async function fetchLimits( + client: ClientWithCoreApi, + vault: VaultSummary, +): Promise<{ coinType: string; limits: AssetLimits }[]> { + const entries = await readEntries(client, vault.tables.limits, (b) => + toLimits(AssetLimitsBcs.parse(b)), + ); + return entries.map((e) => ({ coinType: parseTypeNameKey(e.name.bcs), limits: e.value })); +} + +export async function fetchBalances( + client: ClientWithCoreApi, + vault: VaultSummary, +): Promise<{ coinType: string; balance: bigint }[]> { + const entries = await readEntries(client, vault.tables.funds, (b) => big(bcs.u64().parse(b))); + return entries.map((e) => ({ coinType: parseTypeNameKey(e.name.bcs), balance: e.value })); +} + +export async function fetchVelocity( + client: ClientWithCoreApi, + vault: VaultSummary, +): Promise<{ coinType: string; velocity: Velocity }[]> { + const entries = await readEntries(client, vault.tables.velocity, (b) => { + const raw = VelocityBcs.parse(b); + return { + dayAnchor: num(raw.day_anchor), + daySpent: big(raw.day_spent), + envelopeAnchor: num(raw.envelope_anchor), + envelopeSpent: big(raw.envelope_spent), + envelopeBasis: big(raw.envelope_basis), + } satisfies Velocity; + }); + return entries.map((e) => ({ coinType: parseTypeNameKey(e.name.bcs), velocity: e.value })); +} + +export async function fetchRecipients( + client: ClientWithCoreApi, + vault: VaultSummary, +): Promise { + const entries = await readEntries(client, vault.tables.recipients, (b) => RecipientBcs.parse(b)); + return entries.map((e) => ({ + address: bcs.Address.parse(e.name.bcs), + trust: num(e.value.trust) as Trust, + registeredAt: num(e.value.registered_at), + activatesAt: num(e.value.activates_at), + paidCount: num(e.value.paid_count), + lastPaid: num(e.value.last_paid), + label: e.value.label, + })); +} + +function toProposal(raw: ReturnType): Proposal { + return { + id: big(raw.id), + kind: num(raw.kind) as Kind, + status: num(raw.status) as Status, + proposer: raw.proposer, + createdAt: num(raw.created_at), + executableAt: num(raw.executable_at), + expiresAt: num(raw.expires_at), + reqApprovals: raw.req_approvals, + reqGuardians: raw.req_guardians, + tier: num(raw.tier) as Tier, + reasons: num(raw.reasons), + exposureBps: num(raw.exposure_bps), + policyVersion: big(raw.policy_version), + reductionMask: num(raw.reduction_mask), + approvals: raw.approvals.contents, + confirmations: raw.confirmations.contents, + rejections: raw.rejections.contents, + asset: raw.asset ? (raw.asset.name.startsWith('0x') ? raw.asset.name : `0x${raw.asset.name}`) : null, + amount: big(raw.amount), + recipient: raw.recipient, + memo: raw.memo, + newPolicy: raw.new_policy ? toPolicy(raw.new_policy) : null, + newLimits: raw.new_limits ? toLimits(raw.new_limits) : null, + member: raw.member, + memberPrev: raw.member_prev, + memberRoles: raw.member_roles, + trustLevel: num(raw.trust_level) as Trust, + targetMode: num(raw.target_mode) as Mode, + }; +} + +/** One proposal by its number. */ +export async function fetchProposal( + client: ClientWithCoreApi, + vault: VaultSummary, + id: bigint, +): Promise { + try { + const { dynamicField } = await client.core.getDynamicField({ + parentId: vault.tables.proposals, + name: { type: 'u64', bcs: bcs.u64().serialize(id).toBytes() }, + }); + return toProposal(ProposalBcs.parse(dynamicField.value.bcs)); + } catch { + return null; + } +} + +/** + * The most recent proposals, newest first. Proposal numbers are sequential from 1, so this walks + * backwards from the counter instead of listing the whole table — a vault with ten thousand + * settled payments should not have to read all of them to show the queue. + */ +export async function fetchRecentProposals( + client: ClientWithCoreApi, + vault: VaultSummary, + count = 25, +): Promise { + const last = vault.nextProposal - 1n; + const ids: bigint[] = []; + for (let i = last; i > 0n && ids.length < count; i--) ids.push(i); + const got = await mapLimited(ids, (id) => fetchProposal(client, vault, id)); + return got.filter((p): p is Proposal => p !== null); +} + +/** + * What the vault itself says a transfer would need, read by simulating its own + * `preview_transfer`. The interface never computes this for a live vault: if the preview and the + * execution could disagree, the preview is worthless. + */ +export async function previewTransfer( + client: ClientWithCoreApi, + input: { + packageId: string; + vaultId: string; + coinType: string; + amount: bigint; + recipient: string; + sender: string; + }, +): Promise { + const tx = previewTransferTx(input.packageId, input); + tx.setSender(input.sender); + // `commandResults` carries each command's return values as BCS bytes. The transaction is never + // signed or submitted — this is a read dressed as a transaction, which is the only way to call a + // Move function and get its return values back. + const result = await client.core.simulateTransaction({ + transaction: tx, + include: { commandResults: true }, + }); + const values = result.commandResults?.[0]?.returnValues; + if (!values || values.length < 6) { + throw new Error('preview_transfer returned no values; is the package id correct?'); + } + return { + tier: num(bcs.u8().parse(values[0].bcs)) as Tier, + reasons: num(bcs.u16().parse(values[1].bcs)), + exposureBps: num(bcs.u64().parse(values[2].bcs)), + reqApprovals: num(bcs.u8().parse(values[3].bcs)), + reqGuardians: num(bcs.u8().parse(values[4].bcs)), + delay: num(bcs.u64().parse(values[5].bcs)), + }; +} + +/** Everything an interface needs for one screen, in as few round trips as the transport allows. */ +export type VaultView = { + vault: VaultSummary; + members: { address: string; roles: number }[]; + assets: { coinType: string; limits: AssetLimits; balance: bigint; velocity: Velocity | null }[]; + recipients: RecipientRecord[]; + proposals: Proposal[]; +}; + +export async function fetchVaultView( + client: ClientWithCoreApi, + vaultId: string, + proposalCount = 25, +): Promise { + const vault = await fetchVault(client, vaultId); + const [members, limits, balances, velocity, recipients, proposals] = await Promise.all([ + fetchMembers(client, vault), + fetchLimits(client, vault), + fetchBalances(client, vault), + fetchVelocity(client, vault), + fetchRecipients(client, vault), + fetchRecentProposals(client, vault, proposalCount), + ]); + const balanceOf = new Map(balances.map((b) => [b.coinType, b.balance])); + const velocityOf = new Map(velocity.map((v) => [v.coinType, v.velocity])); + return { + vault, + members, + recipients, + proposals, + assets: limits.map((l) => ({ + coinType: l.coinType, + limits: l.limits, + balance: balanceOf.get(l.coinType) ?? 0n, + velocity: velocityOf.get(l.coinType) ?? null, + })), + }; +} diff --git a/sdk/test/bcs.test.ts b/sdk/test/bcs.test.ts new file mode 100644 index 0000000..1acce09 --- /dev/null +++ b/sdk/test/bcs.test.ts @@ -0,0 +1,199 @@ +import { describe, expect, it } from 'vitest'; +import { + AssetLimitsBcs, + PolicyBcs, + ProposalBcs, + RecipientBcs, + VaultBcs, + VelocityBcs, + bcs, +} from '../src/bcs'; + +/** + * BCS is positional and silent: a field in the wrong place does not error, it reads the next + * field's bytes and produces a plausible wrong number. These round-trips will not catch a layout + * that disagrees with Move — only a deployment can do that — but they do catch the mistakes that + * happen while editing this file: a width changed, a field dropped, two fields transposed to the + * same width. + * + * Every value below is distinct, so a transposition shows up as a wrong value rather than a + * coincidence. + */ +const policy = { + approvals_low: 1, + approvals_high: 2, + approvals_critical: 3, + governance_threshold: 4, + guardian_threshold: 5, + guardian_required_critical: true, + delay_high: '3600000', + delay_critical: '86400000', + recipient_activation_delay: '43200000', + policy_change_delay: '172800000', + recovery_delay: '259200000', + proposal_ttl: '2592000000', + high_exposure_bps: '1000', + critical_exposure_bps: '2500', + hard_block_exposure_bps: '9000', + envelope_bps: '3000', + envelope_window: '86400000', +}; + +const limits = { + approved: true, + low_max: '1000', + high_max: '10000', + per_tx_max: '50000', + daily_max: '100000', +}; + +const A1 = `0x${'11'.repeat(32)}`; +const A2 = `0x${'22'.repeat(32)}`; +const A3 = `0x${'33'.repeat(32)}`; + +describe('BCS round-trips', () => { + it('preserves every policy field', () => { + const parsed = PolicyBcs.parse(PolicyBcs.serialize(policy).toBytes()); + expect(parsed).toEqual(policy); + }); + + it('is fixed width for a policy: five u8, a bool, and eleven u64', () => { + // 5 + 1 + 11 * 8 = 94. A changed width shows up here before it shows up as a wrong number. + expect(PolicyBcs.serialize(policy).toBytes()).toHaveLength(94); + }); + + it('preserves the asset limits', () => { + const parsed = AssetLimitsBcs.parse(AssetLimitsBcs.serialize(limits).toBytes()); + expect(parsed).toEqual(limits); + expect(AssetLimitsBcs.serialize(limits).toBytes()).toHaveLength(1 + 4 * 8); + }); + + it('preserves the velocity windows, which are all the same width and so the easiest to transpose', () => { + const velocity = { + day_anchor: '1', + day_spent: '2', + envelope_anchor: '3', + envelope_spent: '4', + envelope_basis: '5', + }; + expect(VelocityBcs.parse(VelocityBcs.serialize(velocity).toBytes())).toEqual(velocity); + }); + + it('preserves a recipient, including its label', () => { + const recipient = { + trust: 2, + registered_at: '1700000000000', + activates_at: '1700043200000', + paid_count: '7', + last_paid: '1700086400000', + label: 'Payroll — ACME GmbH', + }; + expect(RecipientBcs.parse(RecipientBcs.serialize(recipient).toBytes())).toEqual(recipient); + }); + + it('preserves a transfer proposal with both payload options empty', () => { + const proposal = { + id: '9', + kind: 0, + status: 1, + proposer: A1, + created_at: '1700000000000', + executable_at: '1700003600000', + expires_at: '1702592000000', + req_approvals: 2, + req_guardians: 1, + tier: 2, + reasons: 321, + exposure_bps: '2500', + policy_version: '3', + reduction_mask: 70000, + approvals: { contents: [A1, A2] }, + confirmations: { contents: [A3] }, + rejections: { contents: [] }, + asset: { name: '0000000000000000000000000000000000000000000000000000000000000002::sui::SUI' }, + amount: '1234567890', + recipient: A2, + memo: 'invoice 1182', + new_policy: null, + new_limits: null, + member: A3, + member_prev: A1, + member_roles: 7, + trust_level: 3, + target_mode: 1, + }; + expect(ProposalBcs.parse(ProposalBcs.serialize(proposal).toBytes())).toEqual(proposal); + }); + + it('preserves a governance proposal carrying a policy and limits', () => { + const proposal = { + id: '10', + kind: 1, + status: 1, + proposer: A1, + created_at: '1', + executable_at: '2', + expires_at: '3', + req_approvals: 2, + req_guardians: 0, + tier: 0, + reasons: 0, + exposure_bps: '0', + policy_version: '1', + reduction_mask: 2048, + approvals: { contents: [] }, + confirmations: { contents: [] }, + rejections: { contents: [A2] }, + asset: null, + amount: '0', + recipient: `0x${'00'.repeat(32)}`, + memo: 'policy', + new_policy: policy, + new_limits: limits, + member: `0x${'00'.repeat(32)}`, + member_prev: `0x${'00'.repeat(32)}`, + member_roles: 0, + trust_level: 0, + target_mode: 0, + }; + const parsed = ProposalBcs.parse(ProposalBcs.serialize(proposal).toBytes()); + expect(parsed).toEqual(proposal); + expect(parsed.new_policy?.envelope_bps).toBe('3000'); + }); + + it('preserves a vault, keeping the six table handles distinct and in order', () => { + const handle = (n: string, size: string) => ({ id: `0x${n.repeat(32)}`, size }); + const vault = { + id: `0x${'ab'.repeat(32)}`, + name: 'Skur Demo Treasury', + policy, + policy_version: '4', + mode: 1, + posture_reasons: 576, + members: handle('01', '6'), + owner_count: '2', + approver_count: '4', + executor_count: '2', + guardian_count: '1', + limits: handle('02', '2'), + funds: handle('03', '2'), + velocity: handle('04', '2'), + recipients: handle('05', '9'), + proposals: handle('06', '40'), + next_proposal: '41', + pending_count: '3', + created_at: '1700000000000', + }; + const parsed = VaultBcs.parse(VaultBcs.serialize(vault).toBytes()); + expect(parsed).toEqual(vault); + // The handles are the part a transposition would quietly swap, and reading the wrong table is + // the kind of bug that looks like missing data rather than a crash. + expect(parsed.members.id).toBe(`0x${'01'.repeat(32)}`); + expect(parsed.proposals.id).toBe(`0x${'06'.repeat(32)}`); + expect(parsed.proposals.size).toBe('40'); + }); + + it('reads a Balance as a bare u64, which is how it is stored in the bag', () => { + expect(bcs.u64().parse(bcs.u64().serialize('250000000000').toBytes())).toBe('250000000000'); + }); +}); diff --git a/sdk/test/engine.test.ts b/sdk/test/engine.test.ts new file mode 100644 index 0000000..1ead3c6 --- /dev/null +++ b/sdk/test/engine.test.ts @@ -0,0 +1,370 @@ +import { describe, expect, it } from 'vitest'; +import { + classify, + computeMaxLoss, + effectiveCaps, + exposureBps, + fmtDuration, + limitReductions, + parseAmount, + policyReductions, + postureItems, + requirements, + runScenarios, + TEMPLATES, + trustReduces, + validateCounts, + validateLimits, + validatePolicy, + validatePolicyCode, +} from '../src/engine'; +import { DAY, HOUR, Mode, Reason, Tier, Trust, type Policy } from '../src/types'; + +/** + * These are the same vectors as `sui/tests/engine_tests.move`. Both suites have to agree, because + * the only thing worse than a wrong tier is two components disagreeing about it. + */ +const base: Policy = { + approvalsLow: 1, + approvalsHigh: 2, + approvalsCritical: 3, + governanceThreshold: 2, + guardianThreshold: 1, + guardianRequiredCritical: true, + delayHigh: HOUR, + delayCritical: DAY, + recipientActivationDelay: 12 * HOUR, + policyChangeDelay: 2 * DAY, + recoveryDelay: 3 * DAY, + proposalTtl: 30 * DAY, + highExposureBps: 1_000, + criticalExposureBps: 2_500, + hardBlockExposureBps: 9_000, + envelopeBps: 3_000, + envelopeWindow: DAY, +}; + +const input = ( + amount: bigint, + balance: bigint, + trust: Trust, + probation = false, + mode: Mode = Mode.NORMAL, +) => ({ + amount, + assetBalance: balance, + lowMax: 1_000n, + highMax: 10_000n, + dailyMax: 0n, + daySpent: 0n, + trust, + inProbation: probation, + mode, + highExposureBps: 1_000, + criticalExposureBps: 2_500, +}); + +describe('policy validation', () => { + it('accepts the base policy', () => { + expect(validatePolicyCode(base)).toBe(0); + expect(validatePolicy(base)).toEqual([]); + }); + + it('returns the same code the contract aborts with', () => { + expect(validatePolicyCode({ ...base, approvalsLow: 0 })).toBe(1); + expect(validatePolicyCode({ ...base, approvalsHigh: 0, approvalsLow: 1 })).toBe(2); + expect(validatePolicyCode({ ...base, approvalsCritical: 1 })).toBe(3); + expect(validatePolicyCode({ ...base, governanceThreshold: 0 })).toBe(4); + expect(validatePolicyCode({ ...base, guardianThreshold: 0 })).toBe(5); + expect(validatePolicyCode({ ...base, delayCritical: 0 })).toBe(6); + expect(validatePolicyCode({ ...base, highExposureBps: 3_000 })).toBe(7); + expect(validatePolicyCode({ ...base, hardBlockExposureBps: 2_000 })).toBe(9); + expect(validatePolicyCode({ ...base, envelopeWindow: 60_000 })).toBe(12); + expect(validatePolicyCode({ ...base, proposalTtl: DAY })).toBe(13); + }); + + it('requires the lifetime to clear the longest delay by an hour', () => { + const p = { ...base, delayCritical: DAY, policyChangeDelay: 0, recoveryDelay: 0, recipientActivationDelay: 0 }; + expect(validatePolicyCode({ ...p, proposalTtl: DAY + HOUR })).toBe(0); + expect(validatePolicyCode({ ...p, proposalTtl: DAY + HOUR - 1 })).toBe(13); + }); + + it('checks the roster against the policy', () => { + expect(validateCounts(base, 2, 3, 1, 1)).toEqual([]); + expect(validateCounts(base, 0, 3, 1, 1)).toHaveLength(2); // no owner, and below threshold + expect(validateCounts(base, 2, 3, 0, 1)).toHaveLength(1); + expect(validateCounts(base, 2, 2, 1, 1)).toHaveLength(1); + expect(validateCounts(base, 2, 3, 1, 0)).toHaveLength(1); + }); + + it('checks limits for coherence, treating zero as no cap', () => { + expect(validateLimits({ approved: true, lowMax: 10n, highMax: 5n, perTxMax: 0n, dailyMax: 0n })).toHaveLength(1); + expect(validateLimits({ approved: true, lowMax: 1n, highMax: 5n, perTxMax: 100n, dailyMax: 50n })).toHaveLength(1); + expect(validateLimits({ approved: true, lowMax: 1n, highMax: 5n, perTxMax: 100n, dailyMax: 0n })).toEqual([]); + }); +}); + +describe('what counts as weakening', () => { + it('reports nothing for a change that only tightens', () => { + expect( + policyReductions(base, { + ...base, + approvalsLow: 2, + delayHigh: 2 * HOUR, + highExposureBps: 500, + envelopeBps: 2_000, + }), + ).toEqual([]); + }); + + it('catches fewer approvals', () => { + expect(policyReductions(base, { ...base, approvalsHigh: 1 })).toHaveLength(1); + }); + + it('catches dropping the guardian requirement', () => { + expect(policyReductions(base, { ...base, guardianRequiredCritical: false })).toEqual([ + 'no guardian required for critical payments', + ]); + }); + + it('treats disabling a threshold as the loosest move, not the tightest', () => { + expect(policyReductions(base, { ...base, envelopeBps: 0 })).toEqual(['a larger loss envelope']); + expect(policyReductions(base, { ...base, hardBlockExposureBps: 0 })).toEqual([ + 'a weaker exposure hard block', + ]); + }); + + it('treats a longer proposal lifetime as weakening', () => { + expect(policyReductions(base, { ...base, proposalTtl: 60 * DAY })).toEqual([ + 'a longer proposal lifetime', + ]); + }); + + it('catches weakened caps and newly approved assets', () => { + const a = { approved: true, lowMax: 100n, highMax: 1_000n, perTxMax: 500n, dailyMax: 5_000n }; + expect(limitReductions(a, { ...a, perTxMax: 0n })).toEqual(['a weaker per-transaction cap']); + expect(limitReductions(a, { ...a, perTxMax: 400n })).toEqual([]); + expect(limitReductions({ ...a, approved: false }, a)).toContain('approving a new asset'); + }); + + it('treats lifting a restriction as weakening and imposing one as not', () => { + expect(trustReduces(Trust.RESTRICTED, Trust.VERIFIED)).toBe(true); + expect(trustReduces(Trust.NEW, Trust.TRUSTED)).toBe(true); + expect(trustReduces(Trust.TRUSTED, Trust.BLOCKED)).toBe(false); + expect(trustReduces(Trust.VERIFIED, Trust.NEW)).toBe(false); + }); +}); + +describe('risk classification', () => { + it('computes exposure as a share of holdings', () => { + expect(exposureBps(0n, 1_000n)).toBe(0); + expect(exposureBps(100n, 1_000n)).toBe(1_000); + expect(exposureBps(1_000n, 1_000n)).toBe(10_000); + expect(exposureBps(1_500n, 1_000n)).toBe(10_000); + expect(exposureBps(5n, 0n)).toBe(10_000); + }); + + it('survives a treasury large enough to overflow 64-bit intermediate maths', () => { + const big = 1_000_000_000_000_000_000n; + expect(exposureBps(big / 4n, big)).toBe(2_500); + }); + + it('escalates on amount', () => { + expect(classify(input(500n, 1_000_000n, Trust.VERIFIED)).tier).toBe(Tier.LOW); + expect(classify(input(5_000n, 1_000_000n, Trust.VERIFIED)).tier).toBe(Tier.HIGH); + expect(classify(input(50_000n, 10_000_000n, Trust.VERIFIED)).tier).toBe(Tier.CRITICAL); + }); + + it('escalates on share of the treasury alone', () => { + const o = classify(input(500n, 4_000n, Trust.VERIFIED)); + expect(o.tier).toBe(Tier.HIGH); + expect(o.reasons & Reason.EXPOSURE_HIGH).toBeTruthy(); + expect(classify(input(900n, 3_000n, Trust.VERIFIED)).tier).toBe(Tier.CRITICAL); + }); + + it('escalates an address the vault has never paid', () => { + const o = classify(input(500n, 1_000_000n, Trust.UNKNOWN)); + expect(o.tier).toBe(Tier.HIGH); + expect(o.reasons & Reason.RECIPIENT_UNKNOWN).toBeTruthy(); + expect(classify(input(5_000n, 1_000_000n, Trust.UNKNOWN)).tier).toBe(Tier.CRITICAL); + }); + + it('escalates during probation and stops afterwards', () => { + expect(classify(input(500n, 1_000_000n, Trust.NEW, true)).tier).toBe(Tier.HIGH); + expect(classify(input(500n, 1_000_000n, Trust.NEW, false)).tier).toBe(Tier.LOW); + }); + + it('makes a restricted recipient critical at any amount', () => { + expect(classify(input(1n, 1_000_000_000n, Trust.RESTRICTED)).tier).toBe(Tier.CRITICAL); + }); + + it('escalates halfway through the day rather than at the end', () => { + const quiet = { ...input(100n, 1_000_000n, Trust.VERIFIED), dailyMax: 10_000n, daySpent: 0n }; + expect(classify(quiet).tier).toBe(Tier.LOW); + const pressured = { ...quiet, daySpent: 5_000n }; + const o = classify(pressured); + expect(o.tier).toBe(Tier.HIGH); + expect(o.reasons & Reason.VELOCITY_PRESSURE).toBeTruthy(); + }); + + it('escalates everything in Elevated mode', () => { + expect(classify(input(500n, 1_000_000n, Trust.VERIFIED, false, Mode.ELEVATED)).tier).toBe(Tier.HIGH); + expect(classify(input(5_000n, 1_000_000n, Trust.VERIFIED, false, Mode.ELEVATED)).tier).toBe(Tier.CRITICAL); + }); + + it('records every reason, not only the loudest', () => { + const o = classify({ + ...input(5_000n, 20_000n, Trust.UNKNOWN, true, Mode.ELEVATED), + dailyMax: 10_000n, + daySpent: 6_000n, + }); + expect(o.tier).toBe(Tier.CRITICAL); + for (const bit of [ + Reason.AMOUNT_HIGH, + Reason.EXPOSURE_CRITICAL, + Reason.RECIPIENT_UNKNOWN, + Reason.VELOCITY_PRESSURE, + Reason.MODE_ELEVATED, + ]) { + expect(o.reasons & bit).toBeTruthy(); + } + }); + + it('maps a tier to its requirements', () => { + expect(requirements(base, Tier.LOW)).toEqual({ approvals: 1, guardians: 0, delay: 0 }); + expect(requirements(base, Tier.HIGH)).toEqual({ approvals: 2, guardians: 0, delay: HOUR }); + expect(requirements(base, Tier.CRITICAL)).toEqual({ approvals: 3, guardians: 1, delay: DAY }); + }); + + it('halves the caps in Elevated but never to zero, and leaves "no cap" alone', () => { + expect(effectiveCaps(1_000n, 10_000n, Mode.ELEVATED)).toEqual({ perTxMax: 500n, dailyMax: 5_000n }); + expect(effectiveCaps(1n, 1n, Mode.ELEVATED)).toEqual({ perTxMax: 1n, dailyMax: 1n }); + expect(effectiveCaps(0n, 0n, Mode.ELEVATED)).toEqual({ perTxMax: 0n, dailyMax: 0n }); + expect(effectiveCaps(1_000n, 10_000n, Mode.NORMAL)).toEqual({ perTxMax: 1_000n, dailyMax: 10_000n }); + }); +}); + +describe('templates', () => { + it('ships only valid policies', () => { + for (const t of TEMPLATES) { + expect(validatePolicy(t.policy), t.name).toEqual([]); + expect(validateLimits(t.stable), t.name).toEqual([]); + expect(validateLimits(t.native), t.name).toEqual([]); + } + }); + + it('ships rosters each template can actually satisfy', () => { + for (const t of TEMPLATES) { + expect( + validateCounts(t.policy, t.minSigners, t.minSigners, t.minSigners, t.minGuardians), + t.name, + ).toEqual([]); + } + }); + + it('covers every kind of organization the product is for', () => { + expect(TEMPLATES.map((t) => t.id)).toEqual([ + 'startup', + 'team', + 'protocol', + 'fund', + 'payments', + 'nonprofit', + 'familyOffice', + ]); + }); +}); + +describe('maximum loss', () => { + const limits = { approved: true, lowMax: 5_000n, highMax: 25_000n, perTxMax: 0n, dailyMax: 100_000n }; + + it('is bounded by the tightest cumulative control', () => { + const r = computeMaxLoss(base, limits, 1_000_000n, Mode.NORMAL); + // The envelope is 30% of 1,000,000 = 300,000, so the 100,000 daily cap binds first. + expect(r.immediate).toBe(100_000n); + expect(r.immediateBinding).toBe('the daily cap'); + }); + + it('is zero in Lockdown', () => { + const r = computeMaxLoss(base, limits, 1_000_000n, Mode.LOCKDOWN); + expect(r.immediate).toBe(0n); + expect(r.day).toBe(0n); + }); + + it('always states its assumptions', () => { + expect(computeMaxLoss(base, limits, 1_000_000n, Mode.NORMAL).assumptions.length).toBeGreaterThan(3); + }); +}); + +describe('the simulator', () => { + const ctx = { + policy: base, + limits: { approved: true, lowMax: 5_000_000_000n, highMax: 25_000_000_000n, perTxMax: 0n, dailyMax: 100_000_000_000n }, + balance: 1_000_000_000_000n, + decimals: 6, + symbol: 'USDC', + approverCount: 4, + guardianCount: 2, + }; + + it('stops one compromised signer paying a new address', () => { + const r = runScenarios(ctx).find((s) => s.id === 'single-signer-new-address')!; + expect(r.verdict).toBe('blocked'); + }); + + it('calls a compromised guardian impossible, because it holds no spend path', () => { + const r = runScenarios(ctx).find((s) => s.id === 'guardian-compromised')!; + expect(r.verdict).toBe('impossible'); + }); + + it('stops a split drain on the cumulative controls', () => { + const r = runScenarios(ctx).find((s) => s.id === 'split-drain')!; + expect(r.verdict).toBe('blocked'); + expect(r.detail.join(' ')).toMatch(/envelope|daily cap/); + }); + + it('warns when nothing cumulative is configured', () => { + const open = { + ...ctx, + policy: { ...base, envelopeBps: 0 }, + limits: { ...ctx.limits, dailyMax: 0n }, + }; + const r = runScenarios(open).find((s) => s.id === 'split-drain')!; + expect(r.verdict).toBe('allowed'); + expect(r.detail.join(' ')).toMatch(/Set a daily cap or a loss envelope/); + }); + + it('shows a compromised agent cannot pay itself', () => { + const r = runScenarios(ctx).find((s) => s.id === 'agent-compromised')!; + expect(r.verdict).toBe('blocked'); + }); +}); + +describe('posture', () => { + it('flags a vault with no guardian as bad, not merely a warning', () => { + const items = postureItems({ policy: base, guardians: 0, assets: [] }); + expect(items[0].ok).toBe('bad'); + }); + + it('flags an instantly weakenable policy', () => { + const items = postureItems({ policy: { ...base, policyChangeDelay: 0 }, guardians: 1, assets: [] }); + expect(items.some((i) => i.ok === 'bad' && /weakened immediately/.test(i.text))).toBe(true); + }); +}); + +describe('formatting', () => { + it('renders durations from milliseconds', () => { + expect(fmtDuration(0)).toBe('none'); + expect(fmtDuration(HOUR)).toBe('1h'); + expect(fmtDuration(DAY + 6 * HOUR)).toBe('1d 6h'); + expect(fmtDuration(90 * 60_000)).toBe('1h 30m'); + }); + + it('parses amounts without losing precision', () => { + expect(parseAmount('1.5', 9)).toBe(1_500_000_000n); + expect(parseAmount('0.000001', 6)).toBe(1n); + expect(parseAmount('1,000', 6)).toBe(1_000_000_000n); + expect(parseAmount('1.0000001', 6)).toBeNull(); + expect(parseAmount('abc', 6)).toBeNull(); + }); +}); diff --git a/sdk/test/errors.test.ts b/sdk/test/errors.test.ts new file mode 100644 index 0000000..dda8775 --- /dev/null +++ b/sdk/test/errors.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from 'vitest'; +import { describeAbort, describeFailure } from '../src/errors'; + +/** + * Every string here is a shape a real node produced, not one I imagined. The first test is the + * reason this file exists: the original parser scanned for the first run of digits and found the + * `1` in `1st command`, so an abort with code 119 was reported as code 1 — and a signer whose + * payment was held by a recipient's activation delay was told their policy needed more approvals. + * + * A local end-to-end run caught it. No unit test written against imagined shapes would have. + */ +describe('reading an abort code out of a failure', () => { + it('does not mistake the ordinal in "1st command" for the code', () => { + const real = + "Transaction resolution failed: MoveAbort in 1st command, abort code: 119, in '0x5ee47804bf9511f46cb26d9de652642fa1f7473d2ba6bb54169d9aaf453d180f'"; + expect(describeFailure(real)).toBe('The recipient is still in its activation delay.'); + }); + + it('reads the labelled form at any command index', () => { + expect( + describeFailure("MoveAbort in 3rd command, abort code: 120, in '0xabc'"), + ).toBe('Above the per-transaction cap for this asset.'); + expect( + describeFailure("MoveAbort in 2nd command, abort code: 13, in '0xabc::policy::assert_valid'"), + ).toBe('Proposal lifetime must exceed the longest delay by at least an hour.'); + }); + + it('reads the executed form, where the code is the second argument', () => { + const executed = + 'MoveAbort(MoveLocation { module: ModuleId { address: 5ee47804, name: Identifier("vault") }, function: 12, instruction: 60, function_name: Some("execute_transfer") }, 116) in command 0'; + expect(describeFailure(executed)).toBe('Not enough approvals from current signers.'); + }); + + it('resolves the module from either shape', () => { + const identifier = + 'MoveAbort(MoveLocation { module: ModuleId { address: 1, name: Identifier("policy") }, function: 1, instruction: 2 }, 5) in command 0'; + expect(describeFailure(identifier)).toBe( + 'Guardian confirmation is required but no guardian confirmations were set.', + ); + expect( + describeFailure("MoveAbort in 1st command, abort code: 107, in '0xabc::vault::propose_transfer'"), + ).toBe('The vault is in Lockdown.'); + }); + + it('covers every code the Move modules can raise', () => { + // The ranges are disjoint by design — policy aborts below 100, the vault at 100 and above — + // so a code resolves to one sentence whether or not the module came through. + for (const code of [1, 13, 20, 30, 34]) { + expect(describeAbort(undefined, code)).not.toMatch(/^The vault refused/); + } + for (const code of [100, 119, 122, 139]) { + expect(describeAbort(undefined, code)).not.toMatch(/^The vault refused/); + } + }); + + it('says so plainly for a code it does not know', () => { + expect(describeFailure('MoveAbort in 1st command, abort code: 9999, in \'0xabc\'')).toMatch( + /code 9999/, + ); + }); + + it('recognises the failures that are not aborts at all', () => { + expect(describeFailure('InsufficientGas: balance too low')).toMatch(/gas/i); + expect(describeFailure('ObjectNotFound: 0xabc')).toMatch(/no longer exists/); + expect(describeFailure('ObjectVersionUnavailable for consensus object 0xabc')).toMatch( + /changed by someone else/, + ); + }); + + it('passes an unrecognised message through rather than inventing one', () => { + expect(describeFailure('something nobody anticipated')).toBe('something nobody anticipated'); + }); +}); diff --git a/sdk/tsconfig.build.json b/sdk/tsconfig.build.json new file mode 100644 index 0000000..cf12b4a --- /dev/null +++ b/sdk/tsconfig.build.json @@ -0,0 +1,21 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src", + "module": "NodeNext", + "moduleResolution": "nodenext", + "allowImportingTsExtensions": false + }, + "include": [ + "src" + ], + "exclude": [ + "scripts", + "test" + ] +} \ No newline at end of file diff --git a/sdk/tsconfig.json b/sdk/tsconfig.json new file mode 100644 index 0000000..2eaf5ec --- /dev/null +++ b/sdk/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2023", + "DOM" + ], + "strict": true, + "noUncheckedIndexedAccess": false, + "noEmit": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "allowImportingTsExtensions": true, + "types": [ + "node" + ] + }, + "include": [ + "src", + "scripts", + "test" + ] +} \ No newline at end of file diff --git a/server/.env.example b/server/.env.example new file mode 100644 index 0000000..84ea356 --- /dev/null +++ b/server/.env.example @@ -0,0 +1,14 @@ +# Postgres +DATABASE_URL=postgres://localhost:5432/skur + +# Which network to index, and the published package on it. +SKUR_NETWORK=testnet +SKUR_PACKAGE_ID=0x0 + +# Optional: a gRPC endpoint other than the public full node. +# SKUR_GRPC_URL=https://fullnode.testnet.sui.io:443 + +# How often the indexer asks for new events, in milliseconds. +INDEX_INTERVAL_MS=4000 + +PORT=8787 diff --git a/server/migrations/001_init.sql b/server/migrations/001_init.sql new file mode 100644 index 0000000..e45f280 --- /dev/null +++ b/server/migrations/001_init.sql @@ -0,0 +1,182 @@ +-- Skur indexer and notification store. +-- +-- Nothing here is authoritative. The chain is. Every row is a projection of an event the vault +-- emitted, kept so that an interface can answer "show me the last 500 payments" without walking +-- the ledger, and so that a notification can be sent once rather than every time someone polls. +-- +-- Two consequences are designed for: the indexer may see the same event twice (so every insert is +-- idempotent on its ledger position), and it may be rebuilt from zero (so no table holds state +-- that cannot be derived from events). + +CREATE TABLE IF NOT EXISTS networks ( + name TEXT PRIMARY KEY, + package_id TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- Where the indexer has read to. One row per network, advanced only after a batch commits. +CREATE TABLE IF NOT EXISTS indexer_cursor ( + network TEXT PRIMARY KEY REFERENCES networks(name) ON DELETE CASCADE, + cursor TEXT, + last_event_at TIMESTAMPTZ, + events_seen BIGINT NOT NULL DEFAULT 0, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE IF NOT EXISTS vaults ( + network TEXT NOT NULL REFERENCES networks(name) ON DELETE CASCADE, + vault_id TEXT NOT NULL, + name TEXT NOT NULL, + creator TEXT NOT NULL, + mode SMALLINT NOT NULL DEFAULT 0, + posture_reasons INTEGER NOT NULL DEFAULT 0, + policy_version BIGINT NOT NULL DEFAULT 1, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (network, vault_id) +); + +-- The raw event log, which is the only table the projections are derived from. +CREATE TABLE IF NOT EXISTS events ( + network TEXT NOT NULL REFERENCES networks(name) ON DELETE CASCADE, + tx_digest TEXT NOT NULL, + event_index INTEGER NOT NULL, + vault_id TEXT NOT NULL, + name TEXT NOT NULL, + checkpoint BIGINT, + occurred_at TIMESTAMPTZ NOT NULL, + payload JSONB NOT NULL, + PRIMARY KEY (network, tx_digest, event_index) +); + +CREATE INDEX IF NOT EXISTS events_vault_time ON events (network, vault_id, occurred_at DESC); +CREATE INDEX IF NOT EXISTS events_name ON events (network, name, occurred_at DESC); + +CREATE TABLE IF NOT EXISTS members ( + network TEXT NOT NULL, + vault_id TEXT NOT NULL, + address TEXT NOT NULL, + roles SMALLINT NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (network, vault_id, address) +); + +CREATE INDEX IF NOT EXISTS members_by_address ON members (network, address); + +CREATE TABLE IF NOT EXISTS assets ( + network TEXT NOT NULL, + vault_id TEXT NOT NULL, + coin_type TEXT NOT NULL, + approved BOOLEAN NOT NULL DEFAULT false, + low_max NUMERIC(39,0) NOT NULL DEFAULT 0, + high_max NUMERIC(39,0) NOT NULL DEFAULT 0, + per_tx_max NUMERIC(39,0) NOT NULL DEFAULT 0, + daily_max NUMERIC(39,0) NOT NULL DEFAULT 0, + balance NUMERIC(39,0) NOT NULL DEFAULT 0, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (network, vault_id, coin_type) +); + +CREATE TABLE IF NOT EXISTS proposals ( + network TEXT NOT NULL, + vault_id TEXT NOT NULL, + proposal_id BIGINT NOT NULL, + kind SMALLINT NOT NULL, + status SMALLINT NOT NULL, + proposer TEXT NOT NULL, + coin_type TEXT, + amount NUMERIC(39,0) NOT NULL DEFAULT 0, + recipient TEXT, + memo TEXT NOT NULL DEFAULT '', + tier SMALLINT NOT NULL DEFAULT 0, + reasons INTEGER NOT NULL DEFAULT 0, + exposure_bps INTEGER NOT NULL DEFAULT 0, + req_approvals SMALLINT NOT NULL DEFAULT 0, + req_guardians SMALLINT NOT NULL DEFAULT 0, + approvals INTEGER NOT NULL DEFAULT 0, + confirmations INTEGER NOT NULL DEFAULT 0, + reduction_mask INTEGER NOT NULL DEFAULT 0, + opened_at TIMESTAMPTZ NOT NULL, + executable_at TIMESTAMPTZ NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + settled_at TIMESTAMPTZ, + PRIMARY KEY (network, vault_id, proposal_id) +); + +CREATE INDEX IF NOT EXISTS proposals_pending ON proposals (network, vault_id, status, executable_at); +CREATE INDEX IF NOT EXISTS proposals_recipient ON proposals (network, recipient) WHERE recipient IS NOT NULL; + +CREATE TABLE IF NOT EXISTS proposal_votes ( + network TEXT NOT NULL, + vault_id TEXT NOT NULL, + proposal_id BIGINT NOT NULL, + voter TEXT NOT NULL, + kind TEXT NOT NULL CHECK (kind IN ('approval', 'confirmation')), + voted_at TIMESTAMPTZ NOT NULL, + PRIMARY KEY (network, vault_id, proposal_id, voter, kind) +); + +CREATE TABLE IF NOT EXISTS recipients ( + network TEXT NOT NULL, + vault_id TEXT NOT NULL, + address TEXT NOT NULL, + label TEXT NOT NULL DEFAULT '', + trust SMALLINT NOT NULL DEFAULT 0, + activates_at TIMESTAMPTZ, + paid_count INTEGER NOT NULL DEFAULT 0, + total_paid NUMERIC(39,0) NOT NULL DEFAULT 0, + first_seen TIMESTAMPTZ NOT NULL DEFAULT now(), + last_paid TIMESTAMPTZ, + PRIMARY KEY (network, vault_id, address) +); + +-- Cross-vault reputation: how many distinct vaults have paid this address, and for how long. +-- This is the one genuinely off-chain signal, and it is advisory only. No contract reads it. +CREATE TABLE IF NOT EXISTS recipient_reputation ( + network TEXT NOT NULL, + address TEXT NOT NULL, + vault_count INTEGER NOT NULL DEFAULT 0, + payment_count INTEGER NOT NULL DEFAULT 0, + first_seen TIMESTAMPTZ NOT NULL, + last_seen TIMESTAMPTZ NOT NULL, + blocked_by INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (network, address) +); + +-- Who gets told what. A subscription is per address and per vault, because a guardian wants the +-- vetoable things and an approver wants the queue. +CREATE TABLE IF NOT EXISTS subscriptions ( + id BIGSERIAL PRIMARY KEY, + network TEXT NOT NULL, + vault_id TEXT NOT NULL, + address TEXT NOT NULL, + channel TEXT NOT NULL CHECK (channel IN ('webhook', 'email', 'log')), + endpoint TEXT NOT NULL, + events TEXT[] NOT NULL DEFAULT '{}', + secret TEXT, + active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE (network, vault_id, address, channel, endpoint) +); + +CREATE TABLE IF NOT EXISTS notifications ( + id BIGSERIAL PRIMARY KEY, + subscription_id BIGINT NOT NULL REFERENCES subscriptions(id) ON DELETE CASCADE, + network TEXT NOT NULL, + vault_id TEXT NOT NULL, + -- The event that caused it, so a replayed event cannot send a second notification. + tx_digest TEXT NOT NULL, + event_index INTEGER NOT NULL, + rule TEXT NOT NULL, + title TEXT NOT NULL, + body TEXT NOT NULL, + severity TEXT NOT NULL CHECK (severity IN ('info', 'action', 'alert')), + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + delivered_at TIMESTAMPTZ, + attempts INTEGER NOT NULL DEFAULT 0, + last_error TEXT, + UNIQUE (subscription_id, tx_digest, event_index, rule) +); + +CREATE INDEX IF NOT EXISTS notifications_undelivered + ON notifications (created_at) WHERE delivered_at IS NULL; diff --git a/server/migrations/002_rejections.sql b/server/migrations/002_rejections.sql new file mode 100644 index 0000000..a71dde3 --- /dev/null +++ b/server/migrations/002_rejections.sql @@ -0,0 +1,10 @@ +-- Rejections: a proposal can now be turned down by as many signers as it needed to be approved, +-- rather than only cancelled by an owner or left to expire. +ALTER TABLE proposals ADD COLUMN IF NOT EXISTS rejections INTEGER NOT NULL DEFAULT 0; + +-- `proposal_votes.kind` gains a third value. The constraint is replaced rather than widened in +-- place, because Postgres has no ALTER CONSTRAINT for a CHECK. +ALTER TABLE proposal_votes DROP CONSTRAINT IF EXISTS proposal_votes_kind_check; +ALTER TABLE proposal_votes + ADD CONSTRAINT proposal_votes_kind_check + CHECK (kind IN ('approval', 'confirmation', 'rejection')); diff --git a/server/migrations/003_push.sql b/server/migrations/003_push.sql new file mode 100644 index 0000000..5beb6e4 --- /dev/null +++ b/server/migrations/003_push.sql @@ -0,0 +1,10 @@ +-- Device push, delivered through Expo's push service. The endpoint is the device's Expo push +-- token; the delivery loop refuses anything that does not look like one. +ALTER TABLE subscriptions DROP CONSTRAINT IF EXISTS subscriptions_channel_check; +ALTER TABLE subscriptions + ADD CONSTRAINT subscriptions_channel_check + CHECK (channel IN ('webhook', 'email', 'log', 'push')); + +-- Every notification carries the deep link that opens the thing it is about, so tapping one lands +-- on the proposal rather than on the home screen with homework. +ALTER TABLE notifications ADD COLUMN IF NOT EXISTS link TEXT; diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000..1c8872a --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,2795 @@ +{ + "name": "@skur/server", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@skur/server", + "version": "0.1.0", + "dependencies": { + "@skur/sdk": "file:../sdk", + "express": "^5.1.0", + "pg": "^8.16.3" + }, + "devDependencies": { + "@types/express": "^5.0.3", + "@types/node": "^24.13.3", + "@types/pg": "^8.15.5", + "typescript": "~6.0.2", + "vitest": "^3.2.7" + } + }, + "../sdk": { + "name": "@skur/sdk", + "version": "0.1.0", + "dependencies": { + "@mysten/sui": "^2.34.0" + }, + "devDependencies": { + "typescript": "~6.0.2", + "vitest": "^3.2.7" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.64.0.tgz", + "integrity": "sha512-FLLpX941CD/7aBvX3GqLGBM9OvY5PGXTryPJkG5r5/OuBpZpV5k45Uo3RxAvqd35tsyc3hH1bhtz921IKzoIzg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.64.0.tgz", + "integrity": "sha512-iI32XM/jnDYJsvx5ai/za/7gWO9XoSeFFtU06/ZCaRgyilfJOjaopD3nOpk2WrTCGOyeDt4hleUiGOAnd5tHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.64.0.tgz", + "integrity": "sha512-dRBZlbt2sEtF8Mn4MODThze9fIPdDoOhVSlXMAKy5dQbOYK9pOOoPY4Fat3OTtr0+8zbJo5TTZwlsfxY/kDNJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.64.0.tgz", + "integrity": "sha512-Y3whrJOMucfkhgNkvcM93M6j8C53iEAMmPMYD99C6G1kUy4ELigLWk9ar2/4aCBSRgsql0zdlOemLgW2m5GZgQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.64.0.tgz", + "integrity": "sha512-S6S1GEzPjFF6IONYJX3AKt/799+8RgBgCcwRnUo+Ep1IvBxUbw5M51iU0TDdK2NWWVnSvCQ8yi6Em0fGASMOng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.64.0.tgz", + "integrity": "sha512-Ed1DPoFENkXj4IGzSB12SesV40iP6LxCCrbTIkSGIQBpOOxpBM+0kj62lmaeIHfMWTDoddqTOtqc7s8LYfn+Pw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.64.0.tgz", + "integrity": "sha512-J/6xhutJPtFyIkmHdf4olOdHHKH5kOJNumojdWn+Elv5ICEYTVOa362Vw27TxsK0BLzb1le3EDlO3UbvFK1lnQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.64.0.tgz", + "integrity": "sha512-G4L/Nnx59d2LvDoV/P4S5lZ7oVLT6hodOHMacqjiq2LL2gts86KbkEBTYL53+fBt0odhf7XPIbIATGoYiJctxg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.64.0.tgz", + "integrity": "sha512-zF16pvJDEUksGYRbfnXne7pdklAyj2XDmJeYKgP93oq0cUyX9Y8O7lUo1R+IY1GagP/YBvCEDCe7eEcAKpw//g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.64.0.tgz", + "integrity": "sha512-nIggWn8n+w12+R5lsV+PYYoUY8vuObxPAQqJI7WES+9v0qspYuAHoZMfYM0yLxlipasT5BY9ht5W8t5c/rSCkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.64.0.tgz", + "integrity": "sha512-WLOTiHPzszP0dxIMsHpFeDSOSB5I49Mj8K4+yKE3H1X/D0JRG5XZ5jrctJLMjnUc/LUzaKiwq6EHWNJ8pup8PA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.64.0.tgz", + "integrity": "sha512-35RK9aRhh7g7Pmeu5gQ01aCvrhXzj9LoBE6xtUcEH1ns+nyTaSzxSpADmBQINu4QiTDGvmQYwo1j5hlg1rRXFw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.64.0.tgz", + "integrity": "sha512-RqTfnpoTjuWfPAmqK5p2edt2AMCDJWm6oTn7sKfmyx+6tRga0ip05SJ977o6udTcIiDeF/CJMUfkaN4gtWt6Aw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.64.0.tgz", + "integrity": "sha512-RQjZPigjxFvFZDijG6rjlSXxhRaTf9zLYVpGK+lVcga64oMqP1GbgVqCb46eHTTceSIgp89Ja0y5aPe3GFBYHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.64.0.tgz", + "integrity": "sha512-tjL58mgQcTRnDRpeky+Sth1MKQeYi2Ct6r3Luq9E5aTSjnM/i/JbozqZkKqp9thM5+oDSZxo+05f9kdQedfTGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.64.0.tgz", + "integrity": "sha512-ictoRyVaoZ8C67myHef+8hhCa6IplXJJ/xUCkVCxziV4zxMZh+jvhkhGsPBLqj1S72fnqD0QH5zL4lcaa4X7DQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.64.0.tgz", + "integrity": "sha512-8aVvZ2hfr9RLFzBQkQNmYeHLDja8fLeQAmLt757zdKN/B+8L809KlLJKctQjS22wZotptV8G6o7fVPuAs8S1tQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.64.0.tgz", + "integrity": "sha512-2dEF8GAcDKwshUfydD+GhosppNyBzhVUHkdFF3CXd3FpOkzj/RZzF5aoEtzklujklT5qFVRU2GK/cFeLaTNKVg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.64.0.tgz", + "integrity": "sha512-3XHTvoo2hKkh0MiOfS7SkhHjMaaWF7P4F3drocZZaho/Z8w7h/jpnuBHm91BOQLfvrKHs1yR+zir4E8RI3XzTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.64.0.tgz", + "integrity": "sha512-HKtkzodL4a3KFWDXDoO61cRynAWTR3LnAE0oGtfG9lcNFpNuVYUFOByxDgNqEvic9rvaQbAj+SFwTEWDptRaIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.64.0.tgz", + "integrity": "sha512-JBsxqiY16uyyCujtoC3s7zL+SrYyNElptD7K4V+KUoJ2V8/XG3hPWpheK+SuPwNh1cikJvIZmIdsrYXvD6PLJw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.64.0.tgz", + "integrity": "sha512-S5Qrijh37qhnUYQ0ay8WN3LuWeKjow7wTUyCOPMZtnXiBXYXdsZETI6ZLfYnQAKovltckPLBFdNeD61P2rJc7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.64.0.tgz", + "integrity": "sha512-zbZ6iMmLFEsCtTdc2/y1F/cDFVNNCPF7Ae8PCT/wdPE//hGwYp6r+MyCLMFX+kOeIe09Pz6ASfnnfpSTN1FJQQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.64.0.tgz", + "integrity": "sha512-L6Hw69oNpuYaOr1zCG30v0/DqKYCSRwodPGwIcVZ/6f8SOzq9rxtL/xNVI5MO4wjli+Ko0ErK9uHJL3mNPt8wQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.64.0.tgz", + "integrity": "sha512-nl+CTWIgOGszQmUz/kanfgrfFZ4qiwxKIfT/ryl3N3Ewk5tqizmYtNXiqb7Bu/fDtNgu1kyxdFiAUJNur3iysg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@skur/sdk": { + "resolved": "../sdk", + "link": true + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz", + "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.1.tgz", + "integrity": "sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/pg": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.23.1.tgz", + "integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pg": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.1.tgz", + "integrity": "sha512-aL96AHANtWjPLDOLqnhx+ngp9+UK7ETEU8VJrDCGvsSSi/mGLcWYsS6Herg7lmaBJe4uwrfqsa7gTEFaSizDoQ==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.1", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.1", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.1.tgz", + "integrity": "sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.1.tgz", + "integrity": "sha512-qR3kGNPBLpCNtz0evbKA0Y/MRFXwSSdT+pTJvYp/bXTcReZbvX1kzF0IyTc1QnxqF7AZbOeBhNL8R5mYQZV/MA==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.1.tgz", + "integrity": "sha512-p9VOFMiHB/ZbJATetbg+99PxssTVSQRnyuPSQ67mN1+1KBOjZaZ83ZQzltnxPhJwSsC3nwVjJ10DVJlerbFzLg==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rollup": { + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.64.0.tgz", + "integrity": "sha512-gjA/kFeDfffII8COoPsvLPvE16hWDTEc1+0QKQNKUkuV8iJjqsC4xXs8/FzuL0v9EV39kuxTbAgcFoTRyy2TGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.64.0", + "@rollup/rollup-android-arm64": "4.64.0", + "@rollup/rollup-darwin-arm64": "4.64.0", + "@rollup/rollup-darwin-x64": "4.64.0", + "@rollup/rollup-freebsd-arm64": "4.64.0", + "@rollup/rollup-freebsd-x64": "4.64.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.64.0", + "@rollup/rollup-linux-arm-musleabihf": "4.64.0", + "@rollup/rollup-linux-arm64-gnu": "4.64.0", + "@rollup/rollup-linux-arm64-musl": "4.64.0", + "@rollup/rollup-linux-loong64-gnu": "4.64.0", + "@rollup/rollup-linux-loong64-musl": "4.64.0", + "@rollup/rollup-linux-ppc64-gnu": "4.64.0", + "@rollup/rollup-linux-ppc64-musl": "4.64.0", + "@rollup/rollup-linux-riscv64-gnu": "4.64.0", + "@rollup/rollup-linux-riscv64-musl": "4.64.0", + "@rollup/rollup-linux-s390x-gnu": "4.64.0", + "@rollup/rollup-linux-x64-gnu": "4.64.0", + "@rollup/rollup-linux-x64-musl": "4.64.0", + "@rollup/rollup-openbsd-x64": "4.64.0", + "@rollup/rollup-openharmony-arm64": "4.64.0", + "@rollup/rollup-win32-arm64-msvc": "4.64.0", + "@rollup/rollup-win32-ia32-msvc": "4.64.0", + "@rollup/rollup-win32-x64-gnu": "4.64.0", + "@rollup/rollup-win32-x64-msvc": "4.64.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", + "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..6b7103e --- /dev/null +++ b/server/package.json @@ -0,0 +1,27 @@ +{ + "name": "@skur/server", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "npm run sdk && node --experimental-strip-types --watch src/main.ts", + "start": "npm run sdk && node --experimental-strip-types src/main.ts", + "migrate": "node --experimental-strip-types src/migrate.ts", + "index": "npm run sdk && node --experimental-strip-types src/indexer.ts", + "typecheck": "npm run sdk && tsc --noEmit", + "test": "npm run sdk && vitest run", + "sdk": "npm --prefix ../sdk run --silent build" + }, + "dependencies": { + "@skur/sdk": "file:../sdk", + "express": "^5.1.0", + "pg": "^8.16.3" + }, + "devDependencies": { + "@types/express": "^5.0.3", + "@types/node": "^24.13.3", + "@types/pg": "^8.15.5", + "typescript": "~6.0.2", + "vitest": "^3.2.7" + } +} \ No newline at end of file diff --git a/server/src/advisory.ts b/server/src/advisory.ts new file mode 100644 index 0000000..e2f59b0 --- /dev/null +++ b/server/src/advisory.ts @@ -0,0 +1,217 @@ +import { + DAY, + Mode, + Status, + Trust, + computeMaxLoss, + fmtDuration, + postureItems, + type AssetLimits, + type Policy, +} from '@skur/sdk'; +import { fromNumeric, pool } from './db.ts'; + +/** + * The off-chain policy services. + * + * These are advisory by design. Nothing here can approve, delay or refuse a payment — the vault + * does that, from its own state, and a backend that could override it would be the single point of + * failure the whole product exists to remove. What a backend can do is see across vaults and + * across time, which no single vault object can, and tell a human something useful before they + * sign. + */ + +export type RecipientAdvisory = { + address: string; + /** Trust as the vault records it. */ + trust: Trust; + knownForMs: number; + paymentsFromThisVault: number; + /** How many other vaults on this network have paid it. */ + otherVaults: number; + /** How many vaults have restricted or blocked it. */ + restrictedBy: number; + flags: string[]; + /** Short sentence for a confirmation screen. */ + summary: string; +}; + +export async function recipientAdvisory( + network: string, + vaultId: string, + address: string, +): Promise { + const { rows: local } = await pool.query<{ + trust: number; + paid_count: number; + first_seen: Date; + activates_at: Date | null; + }>( + `SELECT trust, paid_count, first_seen, activates_at + FROM recipients WHERE network = $1 AND vault_id = $2 AND address = $3`, + [network, vaultId, address], + ); + const { rows: rep } = await pool.query<{ + vault_count: number; + payment_count: number; + first_seen: Date; + blocked_by: number; + }>( + `SELECT vault_count, payment_count, first_seen, blocked_by + FROM recipient_reputation WHERE network = $1 AND address = $2`, + [network, address], + ); + + const r = local[0]; + const g = rep[0]; + const now = Date.now(); + const knownForMs = r ? now - r.first_seen.getTime() : g ? now - g.first_seen.getTime() : 0; + const trust = (r?.trust ?? Trust.UNKNOWN) as Trust; + const paymentsFromThisVault = r?.paid_count ?? 0; + const otherVaults = Math.max((g?.vault_count ?? 0) - (paymentsFromThisVault > 0 ? 1 : 0), 0); + const restrictedBy = g?.blocked_by ?? 0; + + const flags: string[] = []; + if (trust === Trust.UNKNOWN) flags.push('This vault has never paid this address.'); + if (trust === Trust.BLOCKED) flags.push('This address is blocked by this vault.'); + if (trust === Trust.RESTRICTED) flags.push('This address is restricted by this vault.'); + if (r?.activates_at && r.activates_at.getTime() > now) { + flags.push(`Still activating, for another ${fmtDuration(r.activates_at.getTime() - now)}.`); + } + if (restrictedBy > 0) { + flags.push( + `${restrictedBy} vault${restrictedBy === 1 ? '' : 's'} on this network have restricted or blocked it.`, + ); + } + if (knownForMs > 0 && knownForMs < DAY) flags.push('First seen less than a day ago.'); + if (paymentsFromThisVault === 0 && otherVaults > 0) { + flags.push(`Paid by ${otherVaults} other vault${otherVaults === 1 ? '' : 's'}, but not by this one.`); + } + + const summary = flags.length + ? flags[0] + : paymentsFromThisVault > 0 + ? `Paid ${paymentsFromThisVault} time${paymentsFromThisVault === 1 ? '' : 's'} before, first seen ${fmtDuration(knownForMs)} ago.` + : 'Nothing known about this address.'; + + return { + address, + trust, + knownForMs, + paymentsFromThisVault, + otherVaults, + restrictedBy, + flags, + summary, + }; +} + +/** + * Outflow against the envelope, from the indexed history rather than the chain, so a dashboard + * can show the trend without a round trip per asset. + */ +export type OutflowWindow = { + coinType: string; + spentInWindow: bigint; + envelopeLimit: bigint; + /** 0 to 10,000. */ + usedBps: number; + dailyCap: bigint; + spentToday: bigint; +}; + +export async function outflow( + network: string, + vaultId: string, + policy: Policy, +): Promise { + const windowStart = new Date(Date.now() - (policy.envelopeWindow || DAY)); + const dayStart = new Date(Date.now() - DAY); + const { rows } = await pool.query<{ + coin_type: string; + balance: string; + daily_max: string; + in_window: string | null; + today: string | null; + }>( + `SELECT a.coin_type, + a.balance, + a.daily_max, + (SELECT sum(p.amount) FROM proposals p + WHERE p.network = a.network AND p.vault_id = a.vault_id + AND p.coin_type = a.coin_type AND p.status = $5 AND p.settled_at >= $3) AS in_window, + (SELECT sum(p.amount) FROM proposals p + WHERE p.network = a.network AND p.vault_id = a.vault_id + AND p.coin_type = a.coin_type AND p.status = $5 AND p.settled_at >= $4) AS today + FROM assets a + WHERE a.network = $1 AND a.vault_id = $2 AND a.approved`, + [network, vaultId, windowStart, dayStart, Status.EXECUTED], + ); + + return rows.map((row) => { + const balance = fromNumeric(row.balance); + const spent = fromNumeric(row.in_window); + // Measured against the balance plus what already left, which approximates the basis the + // contract pinned when the window opened. The contract's own number is authoritative. + const basis = balance + spent; + const limit = policy.envelopeBps === 0 ? 0n : (basis * BigInt(policy.envelopeBps)) / 10_000n; + return { + coinType: row.coin_type, + spentInWindow: spent, + envelopeLimit: limit, + usedBps: limit === 0n ? 0 : Number((spent * 10_000n) / limit), + dailyCap: fromNumeric(row.daily_max), + spentToday: fromNumeric(row.today), + }; + }); +} + +/** The posture report and the maximum-loss numbers, computed from indexed state. */ +export async function riskReport(network: string, vaultId: string, policy: Policy) { + const { rows } = await pool.query<{ + coin_type: string; + approved: boolean; + low_max: string; + high_max: string; + per_tx_max: string; + daily_max: string; + balance: string; + }>( + `SELECT coin_type, approved, low_max, high_max, per_tx_max, daily_max, balance + FROM assets WHERE network = $1 AND vault_id = $2`, + [network, vaultId], + ); + const { rows: vrows } = await pool.query<{ mode: number; guardians: string }>( + `SELECT v.mode, + (SELECT count(*) FROM members m + WHERE m.network = v.network AND m.vault_id = v.vault_id AND (m.roles & 8) <> 0) AS guardians + FROM vaults v WHERE v.network = $1 AND v.vault_id = $2`, + [network, vaultId], + ); + const mode = (vrows[0]?.mode ?? Mode.NORMAL) as Mode; + const guardians = Number(vrows[0]?.guardians ?? 0); + + const assets = rows.map((r) => ({ + coinType: r.coin_type, + limits: { + approved: r.approved, + lowMax: fromNumeric(r.low_max), + highMax: fromNumeric(r.high_max), + perTxMax: fromNumeric(r.per_tx_max), + dailyMax: fromNumeric(r.daily_max), + } satisfies AssetLimits, + balance: fromNumeric(r.balance), + })); + + return { + posture: postureItems({ + policy, + guardians, + assets: assets.map((a) => ({ symbol: a.coinType, limits: a.limits })), + }), + maxLoss: assets.map((a) => ({ + coinType: a.coinType, + ...computeMaxLoss(policy, a.limits, a.balance, mode), + })), + }; +} diff --git a/server/src/api.ts b/server/src/api.ts new file mode 100644 index 0000000..16a1bf8 --- /dev/null +++ b/server/src/api.ts @@ -0,0 +1,237 @@ +import express, { type Request, type Response } from 'express'; +import { fetchVault, grpcClient, Status } from '@skur/sdk'; +import { config } from './config.ts'; +import { fromNumeric, pool } from './db.ts'; +import { describeWait } from './notify.ts'; +import { outflow, recipientAdvisory, riskReport } from './advisory.ts'; + +/** + * The read API. + * + * Deliberately read-only and deliberately not required: every endpoint here answers a question + * faster than the chain can, and none of them answers one the chain cannot. An interface that + * loses this service shows a slower dashboard; it does not lose the ability to move money, read + * the policy or approve a payment. That is the test every endpoint had to pass. + */ +const client = grpcClient(config.network, config.grpcUrl); + +/** bigint does not survive JSON.stringify, and silently throwing on it is worse than a string. */ +function send(res: Response, body: unknown) { + res.type('application/json').send( + JSON.stringify(body, (_, v) => (typeof v === 'bigint' ? v.toString() : v)), + ); +} + +/** Express 5 types a route parameter as possibly repeated; these routes take exactly one. */ +function param(req: Request, name: string): string { + const v = req.params[name]; + return Array.isArray(v) ? (v[0] ?? '') : (v ?? ''); +} + +function wrap(fn: (req: Request, res: Response) => Promise) { + return (req: Request, res: Response) => { + fn(req, res).catch((e: unknown) => { + const message = e instanceof Error ? e.message : String(e); + res.status(500); + send(res, { error: message }); + }); + }; +} + +export function makeApi() { + const app = express(); + app.use(express.json()); + + // Everything this API serves is public chain state read back out of the indexer; the browser + // is a first-class client. Without these headers every fetch from the web app dies in CORS + // preflight and the UI degrades to "the indexer is not reachable". + app.use((req, res, next) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + if (req.method === 'OPTIONS') { + res.status(204).end(); + return; + } + next(); + }); + + app.get('/health', wrap(async (_req, res) => { + const { rows } = await pool.query<{ cursor: string | null; events_seen: string; last_event_at: Date | null }>( + `SELECT cursor, events_seen, last_event_at FROM indexer_cursor WHERE network = $1`, + [config.network], + ); + send(res, { + ok: true, + network: config.network, + packageId: config.packageId, + indexer: rows[0] ?? null, + }); + })); + + app.get('/vaults', wrap(async (_req, res) => { + const { rows } = await pool.query( + `SELECT v.vault_id, v.name, v.mode, v.policy_version, v.created_at, + (SELECT count(*) FROM members m WHERE m.network = v.network AND m.vault_id = v.vault_id) AS members, + (SELECT count(*) FROM proposals p + WHERE p.network = v.network AND p.vault_id = v.vault_id AND p.status = $2) AS pending + FROM vaults v WHERE v.network = $1 ORDER BY v.created_at DESC`, + [config.network, Status.PENDING], + ); + send(res, { vaults: rows }); + })); + + /** Vaults an address is a member of, which is how an interface finds "my vaults". */ + app.get('/members/:address/vaults', wrap(async (req, res) => { + const { rows } = await pool.query( + `SELECT m.vault_id, m.roles, v.name, v.mode + FROM members m JOIN vaults v + ON v.network = m.network AND v.vault_id = m.vault_id + WHERE m.network = $1 AND m.address = $2 + ORDER BY v.created_at DESC`, + [config.network, param(req, 'address')], + ); + send(res, { vaults: rows }); + })); + + app.get('/vaults/:id', wrap(async (req, res) => { + // The policy is read from the chain, never from the projection: it is the thing that decides, + // and a stale copy of it is worse than no copy. + const onchain = await fetchVault(client, param(req, 'id')); + const { rows: members } = await pool.query( + `SELECT address, roles FROM members WHERE network = $1 AND vault_id = $2 ORDER BY roles DESC`, + [config.network, param(req, 'id')], + ); + const { rows: assets } = await pool.query( + `SELECT coin_type, approved, low_max, high_max, per_tx_max, daily_max, balance + FROM assets WHERE network = $1 AND vault_id = $2 ORDER BY coin_type`, + [config.network, param(req, 'id')], + ); + send(res, { vault: onchain, members, assets }); + })); + + app.get('/vaults/:id/proposals', wrap(async (req, res) => { + const limit = Math.min(Number(req.query.limit ?? 50), 200); + const status = req.query.status === undefined ? null : Number(req.query.status); + const { rows } = await pool.query( + `SELECT * FROM proposals + WHERE network = $1 AND vault_id = $2 AND ($3::int IS NULL OR status = $3) + ORDER BY proposal_id DESC LIMIT $4`, + [config.network, param(req, 'id'), status, limit], + ); + send(res, { + proposals: rows.map((p) => ({ + ...p, + amount: fromNumeric(p.amount as string), + wait: p.status === Status.PENDING ? describeWait(new Date(p.executable_at as string).getTime()) : null, + })), + }); + })); + + /** The queue, from the point of view of one signer: what is waiting on them specifically. */ + app.get('/vaults/:id/queue/:address', wrap(async (req, res) => { + const { rows } = await pool.query( + `SELECT p.*, + EXISTS (SELECT 1 FROM proposal_votes v + WHERE v.network = p.network AND v.vault_id = p.vault_id + AND v.proposal_id = p.proposal_id AND v.voter = $3) AS voted, + coalesce((SELECT m.roles FROM members m + WHERE m.network = p.network AND m.vault_id = p.vault_id + AND m.address = $3), 0) AS roles + FROM proposals p + WHERE p.network = $1 AND p.vault_id = $2 AND p.status = $4 + ORDER BY p.opened_at DESC`, + [config.network, param(req, 'id'), param(req, 'address'), Status.PENDING], + ); + send(res, { + proposals: rows.map((p) => ({ + ...p, + amount: fromNumeric(p.amount as string), + needsYou: + !p.voted && + ((p.kind === 0 && (Number(p.roles) & 2) !== 0) || + (p.kind !== 0 && (Number(p.roles) & 1) !== 0) || + (Number(p.req_guardians) > 0 && (Number(p.roles) & 8) !== 0)), + })), + }); + })); + + app.get('/vaults/:id/events', wrap(async (req, res) => { + const limit = Math.min(Number(req.query.limit ?? 100), 500); + const { rows } = await pool.query( + `SELECT tx_digest, event_index, name, occurred_at, payload + FROM events WHERE network = $1 AND vault_id = $2 + ORDER BY occurred_at DESC, event_index DESC LIMIT $3`, + [config.network, param(req, 'id'), limit], + ); + send(res, { events: rows }); + })); + + app.get('/vaults/:id/recipients', wrap(async (req, res) => { + const { rows } = await pool.query( + `SELECT address, label, trust, activates_at, paid_count, total_paid, last_paid + FROM recipients WHERE network = $1 AND vault_id = $2 ORDER BY last_paid DESC NULLS LAST`, + [config.network, param(req, 'id')], + ); + send(res, { + recipients: rows.map((r) => ({ ...r, total_paid: fromNumeric(r.total_paid as string) })), + }); + })); + + /** The advisory a confirmation screen should show before anyone signs. */ + app.get('/vaults/:id/recipients/:address/advisory', wrap(async (req, res) => { + send(res, await recipientAdvisory(config.network, param(req, 'id'), param(req, 'address'))); + })); + + app.get('/vaults/:id/outflow', wrap(async (req, res) => { + const vault = await fetchVault(client, param(req, 'id')); + send(res, { outflow: await outflow(config.network, param(req, 'id'), vault.policy) }); + })); + + app.get('/vaults/:id/risk', wrap(async (req, res) => { + const vault = await fetchVault(client, param(req, 'id')); + send(res, await riskReport(config.network, param(req, 'id'), vault.policy)); + })); + + /** Subscribe an address to notifications for a vault. */ + app.post('/subscriptions', wrap(async (req, res) => { + const { vaultId, address, channel, endpoint, events, secret } = req.body ?? {}; + if (!vaultId || !address || !channel || !endpoint) { + res.status(400); + send(res, { error: 'vaultId, address, channel and endpoint are required' }); + return; + } + if (!['webhook', 'email', 'log', 'push'].includes(channel)) { + res.status(400); + send(res, { error: 'channel must be webhook, email, log or push' }); + return; + } + const { rows } = await pool.query( + `INSERT INTO subscriptions (network, vault_id, address, channel, endpoint, events, secret) + VALUES ($1,$2,$3,$4,$5,$6,$7) + ON CONFLICT (network, vault_id, address, channel, endpoint) DO UPDATE + SET events = EXCLUDED.events, secret = EXCLUDED.secret, active = true + RETURNING id`, + [config.network, vaultId, address, channel, endpoint, events ?? [], secret ?? null], + ); + send(res, { id: rows[0].id }); + })); + + app.delete('/subscriptions/:id', wrap(async (req, res) => { + await pool.query(`UPDATE subscriptions SET active = false WHERE id = $1`, [param(req, 'id')]); + send(res, { ok: true }); + })); + + app.get('/subscriptions/:address/notifications', wrap(async (req, res) => { + const { rows } = await pool.query( + `SELECT n.id, n.vault_id, n.rule, n.title, n.body, n.severity, n.created_at, n.delivered_at + FROM notifications n JOIN subscriptions s ON s.id = n.subscription_id + WHERE s.network = $1 AND s.address = $2 + ORDER BY n.created_at DESC LIMIT 100`, + [config.network, param(req, 'address')], + ); + send(res, { notifications: rows }); + })); + + return app; +} diff --git a/server/src/config.ts b/server/src/config.ts new file mode 100644 index 0000000..cd2feab --- /dev/null +++ b/server/src/config.ts @@ -0,0 +1,18 @@ +import type { Network } from '@skur/sdk'; + +function required(name: string): string { + const v = process.env[name]; + if (!v) throw new Error(`${name} is not set; copy .env.example and fill it in`); + return v; +} + +export const config = { + databaseUrl: required('DATABASE_URL'), + network: (process.env.SKUR_NETWORK ?? 'testnet') as Network, + packageId: required('SKUR_PACKAGE_ID'), + grpcUrl: process.env.SKUR_GRPC_URL, + indexIntervalMs: Number(process.env.INDEX_INTERVAL_MS ?? 4000), + port: Number(process.env.PORT ?? 8787), + /** Page size for each pass of the indexer. */ + batchSize: Number(process.env.INDEX_BATCH ?? 50), +}; diff --git a/server/src/db.ts b/server/src/db.ts new file mode 100644 index 0000000..b40659d --- /dev/null +++ b/server/src/db.ts @@ -0,0 +1,35 @@ +import { Pool, type PoolClient } from 'pg'; +import { config } from './config.ts'; + +export const pool = new Pool({ connectionString: config.databaseUrl, max: 10 }); + +/** Run a unit of work in one transaction, rolling back on any throw. */ +export async function withTx(fn: (c: PoolClient) => Promise): Promise { + const c = await pool.connect(); + try { + await c.query('BEGIN'); + const out = await fn(c); + await c.query('COMMIT'); + return out; + } catch (e) { + await c.query('ROLLBACK'); + throw e; + } finally { + c.release(); + } +} + +/** Postgres has no u64, and amounts can exceed a double. NUMERIC in, bigint out. */ +export function toNumeric(v: bigint): string { + return v.toString(); +} + +export function fromNumeric(v: string | number | null | undefined): bigint { + if (v === null || v === undefined) return 0n; + return BigInt(String(v).split('.')[0]); +} + +/** Move timestamps are milliseconds since the epoch. */ +export function toTimestamp(ms: number): Date { + return new Date(ms); +} diff --git a/server/src/indexer.ts b/server/src/indexer.ts new file mode 100644 index 0000000..ccafbc6 --- /dev/null +++ b/server/src/indexer.ts @@ -0,0 +1,113 @@ +import { grpcClient, listVaultEvents } from '@skur/sdk'; +import { config } from './config.ts'; +import { pool, withTx } from './db.ts'; +import { ensureVault, project } from './project.ts'; +import { deliverPending, queueNotifications } from './notify.ts'; + +/** + * The indexer. + * + * It walks the package's events forwards from a stored cursor, projects each one, queues whatever + * notifications it implies, and only then advances the cursor — so a crash re-reads a batch rather + * than skipping it, and every write is idempotent on the event's ledger position to make that + * safe. + * + * Transport is gRPC. `hasNextPage` can be true with an empty page because a gRPC server bounds how + * much ledger one request scans, so the loop keeps going while the server says there is more + * rather than stopping on an empty result. + */ +const client = grpcClient(config.network, config.grpcUrl); + +async function readCursor(): Promise { + const { rows } = await pool.query<{ cursor: string | null }>( + `SELECT cursor FROM indexer_cursor WHERE network = $1`, + [config.network], + ); + if (rows.length === 0) { + await pool.query(`INSERT INTO indexer_cursor (network) VALUES ($1)`, [config.network]); + return null; + } + return rows[0].cursor; +} + +export async function indexOnce(): Promise<{ events: number; notifications: number; done: boolean }> { + const cursor = await readCursor(); + const page = await listVaultEvents(client, { + packageId: config.packageId, + limit: config.batchSize, + // No cursor yet means the start of the package's history, read oldest first. + ...(cursor ? { after: cursor } : { order: 'ascending' as const }), + }); + + if (page.events.length === 0) { + if (page.hasNextPage && page.endCursor) { + // A scan-limited page: nothing matched in the range read, but moving the cursor makes + // progress rather than reading the same empty range forever. + await pool.query( + `UPDATE indexer_cursor SET cursor = $2, updated_at = now() WHERE network = $1`, + [config.network, page.endCursor], + ); + } + return { events: 0, notifications: 0, done: !page.hasNextPage }; + } + + const notifications = await withTx(async (c) => { + let queued = 0; + for (const decoded of page.events) { + await ensureVault(c, config.network, decoded.event); + // `events` is the idempotency gate: if the row is already there, this event has been + // applied and its counters must not be moved again. + const seen = await c.query( + `SELECT 1 FROM events WHERE network = $1 AND tx_digest = $2 AND event_index = $3`, + [config.network, decoded.position.transactionDigest, decoded.position.eventIndex], + ); + if (seen.rowCount) continue; + await project(c, config.network, decoded); + queued += await queueNotifications(c, config.network, decoded); + } + const last = page.events[page.events.length - 1]; + await c.query( + `UPDATE indexer_cursor + SET cursor = $2, last_event_at = $3, events_seen = events_seen + $4, updated_at = now() + WHERE network = $1`, + [ + config.network, + page.endCursor, + new Date(last.event.at), + page.events.length, + ], + ); + return queued; + }); + + return { events: page.events.length, notifications, done: !page.hasNextPage }; +} + +/** Catch up to the tip, then poll. */ +export async function runIndexer(signal?: AbortSignal) { + console.log(`indexing ${config.packageId} on ${config.network}`); + while (!signal?.aborted) { + try { + let done = false; + while (!done && !signal?.aborted) { + const r = await indexOnce(); + done = r.done; + if (r.events) { + console.log(`+${r.events} events, ${r.notifications} notifications queued`); + } + } + const sent = await deliverPending(); + if (sent) console.log(`delivered ${sent} notifications`); + } catch (e) { + console.error('index pass failed:', e instanceof Error ? e.message : e); + } + await new Promise((r) => setTimeout(r, config.indexIntervalMs)); + } +} + +if (import.meta.url === `file://${process.argv[1]}`) { + const ac = new AbortController(); + process.on('SIGINT', () => ac.abort()); + process.on('SIGTERM', () => ac.abort()); + runIndexer(ac.signal).then(() => pool.end()); +} diff --git a/server/src/main.ts b/server/src/main.ts new file mode 100644 index 0000000..9f2955c --- /dev/null +++ b/server/src/main.ts @@ -0,0 +1,25 @@ +import { config } from './config.ts'; +import { pool } from './db.ts'; +import { makeApi } from './api.ts'; +import { runIndexer } from './indexer.ts'; + +/** + * One process runs both the indexer and the read API. They share a pool and a cursor, and keeping + * them together means a deployment cannot end up with an API serving a store nothing is filling. + * Split them when there is a reason to; `npm run index` already runs the indexer alone. + */ +const ac = new AbortController(); +const server = makeApi().listen(config.port, () => { + console.log(`api on :${config.port} (${config.network})`); +}); + +runIndexer(ac.signal); + +async function shutdown() { + ac.abort(); + server.close(); + await pool.end(); + process.exit(0); +} +process.on('SIGINT', shutdown); +process.on('SIGTERM', shutdown); diff --git a/server/src/migrate.ts b/server/src/migrate.ts new file mode 100644 index 0000000..e331def --- /dev/null +++ b/server/src/migrate.ts @@ -0,0 +1,49 @@ +import { readdir, readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { config } from './config.ts'; +import { pool, withTx } from './db.ts'; + +/** + * Migrations are plain SQL, applied in filename order, recorded by name. There is no down path: + * a projection store is rebuilt from events rather than migrated backwards. + */ +const dir = new URL('../migrations/', import.meta.url).pathname; + +async function main() { + await pool.query(` + CREATE TABLE IF NOT EXISTS schema_migrations ( + name TEXT PRIMARY KEY, + applied_at TIMESTAMPTZ NOT NULL DEFAULT now() + )`); + + const files = (await readdir(dir)).filter((f) => f.endsWith('.sql')).sort(); + const { rows } = await pool.query<{ name: string }>('SELECT name FROM schema_migrations'); + const done = new Set(rows.map((r) => r.name)); + + for (const file of files) { + if (done.has(file)) continue; + const sql = await readFile(join(dir, file), 'utf8'); + await withTx(async (c) => { + await c.query(sql); + await c.query('INSERT INTO schema_migrations (name) VALUES ($1)', [file]); + }); + console.log(`applied ${file}`); + } + + await pool.query( + `INSERT INTO networks (name, package_id) VALUES ($1, $2) + ON CONFLICT (name) DO UPDATE SET package_id = EXCLUDED.package_id`, + [config.network, config.packageId], + ); + await pool.query( + `INSERT INTO indexer_cursor (network) VALUES ($1) ON CONFLICT (network) DO NOTHING`, + [config.network], + ); + console.log(`ready: ${config.network} at ${config.packageId}`); + await pool.end(); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/server/src/notify.ts b/server/src/notify.ts new file mode 100644 index 0000000..175cacf --- /dev/null +++ b/server/src/notify.ts @@ -0,0 +1,147 @@ +import { createHmac } from 'node:crypto'; +import type { PoolClient } from 'pg'; +import { fmtDuration, type DecodedEvent } from '@skur/sdk'; +import { pool } from './db.ts'; +import { buildExpoPushMessage, rulesFor } from './rules.ts'; + +const EXPO_PUSH_URL = 'https://exp.host/--/api/v2/push/send'; + +/** + * Queue notifications for an event. The unique constraint on + * `(subscription, tx_digest, event_index, rule)` is what makes a replayed event silent. + */ +export async function queueNotifications( + c: PoolClient, + network: string, + decoded: DecodedEvent, +): Promise { + const rules = rulesFor(decoded); + if (rules.length === 0) return 0; + const { event, position } = decoded; + let queued = 0; + + for (const r of rules) { + const { rows } = await c.query<{ id: string }>( + `SELECT s.id + FROM subscriptions s + LEFT JOIN members m + ON m.network = s.network AND m.vault_id = s.vault_id AND m.address = s.address + WHERE s.active + AND s.network = $1 + AND s.vault_id = $2 + AND ($3 = 0 OR (coalesce(m.roles, 0) & $3) <> 0) + AND (cardinality(s.events) = 0 OR $4 = ANY(s.events))`, + [network, event.vault, r.roles, r.rule], + ); + for (const row of rows) { + const res = await c.query( + `INSERT INTO notifications ( + subscription_id, network, vault_id, tx_digest, event_index, rule, title, body, severity, link + ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) + ON CONFLICT DO NOTHING`, + [ + row.id, + network, + event.vault, + position.transactionDigest, + position.eventIndex, + r.rule, + r.title, + r.body, + r.severity, + r.link ?? `skur://vault/${event.vault}`, + ], + ); + queued += res.rowCount ?? 0; + } + } + return queued; +} + +/** + * Deliver what is queued. A webhook is signed with the subscription's secret so the receiver can + * tell a real notification from anything else that finds the URL. + */ +export async function deliverPending(limit = 25): Promise { + const { rows } = await pool.query<{ + id: string; + channel: string; + endpoint: string; + secret: string | null; + vault_id: string; + rule: string; + title: string; + body: string; + severity: string; + link: string | null; + attempts: number; + }>( + `SELECT n.id, s.channel, s.endpoint, s.secret, n.vault_id, n.rule, n.title, n.body, + n.severity, n.link, n.attempts + FROM notifications n + JOIN subscriptions s ON s.id = n.subscription_id + WHERE n.delivered_at IS NULL AND n.attempts < 6 AND s.active + ORDER BY n.created_at + LIMIT $1`, + [limit], + ); + + let sent = 0; + for (const n of rows) { + const payload = JSON.stringify({ + vault: n.vault_id, + rule: n.rule, + title: n.title, + body: n.body, + severity: n.severity, + link: n.link, + }); + try { + if (n.channel === 'webhook') { + const headers: Record = { 'content-type': 'application/json' }; + if (n.secret) { + headers['x-skur-signature'] = createHmac('sha256', n.secret).update(payload).digest('hex'); + } + const res = await fetch(n.endpoint, { method: 'POST', headers, body: payload }); + if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); + } else if (n.channel === 'push') { + const res = await fetch(EXPO_PUSH_URL, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify([buildExpoPushMessage(n)]), + }); + if (!res.ok) throw new Error(`expo push: ${res.status} ${res.statusText}`); + const body = (await res.json()) as { data?: { status: string; message?: string }[] }; + const ticket = body.data?.[0]; + if (ticket && ticket.status !== 'ok') { + throw new Error(`expo push ticket: ${ticket.message ?? ticket.status}`); + } + } else if (n.channel === 'log') { + console.log(`[${n.severity}] ${n.title} — ${n.body}`); + } else { + // Email needs a provider. Rather than pretend, the row is left for whoever wires one up. + throw new Error(`channel ${n.channel} has no transport configured`); + } + await pool.query( + `UPDATE notifications SET delivered_at = now(), attempts = attempts + 1 WHERE id = $1`, + [n.id], + ); + sent++; + } catch (e) { + await pool.query( + `UPDATE notifications SET attempts = attempts + 1, last_error = $2 WHERE id = $1`, + [n.id, e instanceof Error ? e.message : String(e)], + ); + } + } + return sent; +} + +/** Used by the API to describe a waiting proposal in words rather than timestamps. */ +export function describeWait(executableAt: number, now = Date.now()): string { + const left = executableAt - now; + if (left <= 0) return 'executable now'; + return `executable in ${fmtDuration(left)}`; +} + + diff --git a/server/src/project.ts b/server/src/project.ts new file mode 100644 index 0000000..4848cc0 --- /dev/null +++ b/server/src/project.ts @@ -0,0 +1,310 @@ +import type { PoolClient } from 'pg'; +import { Status, type DecodedEvent, type SkurEvent } from '@skur/sdk'; +import { toNumeric, toTimestamp } from './db.ts'; + +/** + * Apply one event to the projections. + * + * Every statement is an upsert keyed on the chain's own identity for the row, so replaying the + * ledger converges on the same state rather than doubling anything. The one place that needs care + * is a counter: `paid_count` and `total_paid` are accumulated, so they are only ever touched from + * inside the idempotency guard in `ingest`, which has already established that this exact event + * has not been applied before. + */ +export async function project( + c: PoolClient, + network: string, + { event, position }: DecodedEvent, +): Promise { + const at = toTimestamp(event.at); + const vault = event.vault; + const key = [network, vault]; + + switch (event.name) { + case 'VaultCreated': + await c.query( + `INSERT INTO vaults (network, vault_id, name, creator, created_at) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (network, vault_id) DO UPDATE + SET name = EXCLUDED.name, creator = EXCLUDED.creator`, + [...key, event.vaultName, event.creator, at], + ); + break; + + case 'Deposited': + await c.query( + `INSERT INTO assets (network, vault_id, coin_type, balance, updated_at) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (network, vault_id, coin_type) DO UPDATE + SET balance = EXCLUDED.balance, updated_at = EXCLUDED.updated_at`, + [...key, event.asset, toNumeric(event.balance), at], + ); + break; + + case 'ProposalOpened': + await c.query( + `INSERT INTO proposals ( + network, vault_id, proposal_id, kind, status, proposer, coin_type, amount, recipient, + memo, tier, reasons, exposure_bps, req_approvals, req_guardians, reduction_mask, + opened_at, executable_at, expires_at + ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19) + ON CONFLICT (network, vault_id, proposal_id) DO UPDATE SET + kind = EXCLUDED.kind, status = EXCLUDED.status, proposer = EXCLUDED.proposer, + coin_type = EXCLUDED.coin_type, amount = EXCLUDED.amount, + recipient = EXCLUDED.recipient, memo = EXCLUDED.memo, tier = EXCLUDED.tier, + reasons = EXCLUDED.reasons, exposure_bps = EXCLUDED.exposure_bps, + req_approvals = EXCLUDED.req_approvals, req_guardians = EXCLUDED.req_guardians, + reduction_mask = EXCLUDED.reduction_mask, opened_at = EXCLUDED.opened_at, + executable_at = EXCLUDED.executable_at, expires_at = EXCLUDED.expires_at`, + [ + ...key, + event.proposal.toString(), + event.kind, + Status.PENDING, + event.proposer, + event.asset, + toNumeric(event.amount), + event.recipient === '0x0000000000000000000000000000000000000000000000000000000000000000' + ? null + : event.recipient, + event.memo, + event.tier, + event.reasons, + event.exposureBps, + event.reqApprovals, + event.reqGuardians, + event.reductionMask, + at, + toTimestamp(event.executableAt), + toTimestamp(event.expiresAt), + ], + ); + break; + + case 'Approved': + await c.query( + `INSERT INTO proposal_votes (network, vault_id, proposal_id, voter, kind, voted_at) + VALUES ($1,$2,$3,$4,'approval',$5) + ON CONFLICT DO NOTHING`, + [...key, event.proposal.toString(), event.approver, at], + ); + await c.query( + `UPDATE proposals SET approvals = $4 + WHERE network = $1 AND vault_id = $2 AND proposal_id = $3`, + [...key, event.proposal.toString(), event.approvals], + ); + break; + + case 'Rejected': + await c.query( + `INSERT INTO proposal_votes (network, vault_id, proposal_id, voter, kind, voted_at) + VALUES ($1,$2,$3,$4,'rejection',$5) + ON CONFLICT DO NOTHING`, + [...key, event.proposal.toString(), event.rejecter, at], + ); + await c.query( + `UPDATE proposals SET rejections = $4 + WHERE network = $1 AND vault_id = $2 AND proposal_id = $3`, + [...key, event.proposal.toString(), event.rejections], + ); + break; + + case 'Confirmed': + await c.query( + `INSERT INTO proposal_votes (network, vault_id, proposal_id, voter, kind, voted_at) + VALUES ($1,$2,$3,$4,'confirmation',$5) + ON CONFLICT DO NOTHING`, + [...key, event.proposal.toString(), event.guardian, at], + ); + await c.query( + `UPDATE proposals SET confirmations = $4 + WHERE network = $1 AND vault_id = $2 AND proposal_id = $3`, + [...key, event.proposal.toString(), event.confirmations], + ); + break; + + case 'Settled': + await c.query( + `UPDATE proposals SET status = $4, settled_at = $5 + WHERE network = $1 AND vault_id = $2 AND proposal_id = $3`, + [...key, event.proposal.toString(), event.status, at], + ); + break; + + case 'Executed': { + await c.query( + `UPDATE proposals SET status = $4, settled_at = $5, tier = $6 + WHERE network = $1 AND vault_id = $2 AND proposal_id = $3`, + [...key, event.proposal.toString(), Status.EXECUTED, at, event.tier], + ); + if (event.asset) { + await c.query( + `INSERT INTO assets (network, vault_id, coin_type, balance, updated_at) + VALUES ($1,$2,$3,$4,$5) + ON CONFLICT (network, vault_id, coin_type) DO UPDATE + SET balance = EXCLUDED.balance, updated_at = EXCLUDED.updated_at`, + [...key, event.asset, toNumeric(event.balanceAfter), at], + ); + } + if (event.recipient) { + await c.query( + `INSERT INTO recipients (network, vault_id, address, paid_count, total_paid, first_seen, last_paid) + VALUES ($1,$2,$3,1,$4,$5,$5) + ON CONFLICT (network, vault_id, address) DO UPDATE SET + paid_count = recipients.paid_count + 1, + total_paid = recipients.total_paid + EXCLUDED.total_paid, + last_paid = EXCLUDED.last_paid`, + [...key, event.recipient, toNumeric(event.amount), at], + ); + await c.query( + `INSERT INTO recipient_reputation (network, address, vault_count, payment_count, first_seen, last_seen) + VALUES ($1,$2,1,1,$3,$3) + ON CONFLICT (network, address) DO UPDATE SET + payment_count = recipient_reputation.payment_count + 1, + last_seen = EXCLUDED.last_seen, + vault_count = ( + SELECT count(DISTINCT vault_id) FROM recipients + WHERE network = $1 AND address = $2 AND paid_count > 0 + )`, + [network, event.recipient, at], + ); + } + break; + } + + case 'BreakerTripped': + await c.query( + `UPDATE proposals SET status = $4, settled_at = $5 + WHERE network = $1 AND vault_id = $2 AND proposal_id = $3`, + [...key, event.proposal.toString(), Status.BLOCKED, at], + ); + break; + + case 'ModeChanged': + await c.query( + `UPDATE vaults SET mode = $3, posture_reasons = $4, updated_at = $5 + WHERE network = $1 AND vault_id = $2`, + [...key, event.to, event.reasons, at], + ); + break; + + case 'PolicyChanged': + await c.query( + `UPDATE vaults SET policy_version = $3, updated_at = $4 + WHERE network = $1 AND vault_id = $2`, + [...key, event.version.toString(), at], + ); + break; + + case 'LimitsChanged': + await c.query( + `INSERT INTO assets ( + network, vault_id, coin_type, approved, low_max, high_max, per_tx_max, daily_max, updated_at + ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) + ON CONFLICT (network, vault_id, coin_type) DO UPDATE SET + approved = EXCLUDED.approved, low_max = EXCLUDED.low_max, + high_max = EXCLUDED.high_max, per_tx_max = EXCLUDED.per_tx_max, + daily_max = EXCLUDED.daily_max, updated_at = EXCLUDED.updated_at`, + [ + ...key, + event.asset, + event.approved, + toNumeric(event.lowMax), + toNumeric(event.highMax), + toNumeric(event.perTxMax), + toNumeric(event.dailyMax), + at, + ], + ); + break; + + case 'MemberChanged': + if (event.rolesAfter === 0) { + await c.query( + `DELETE FROM members WHERE network = $1 AND vault_id = $2 AND address = $3`, + [...key, event.member], + ); + } else { + await c.query( + `INSERT INTO members (network, vault_id, address, roles, updated_at) + VALUES ($1,$2,$3,$4,$5) + ON CONFLICT (network, vault_id, address) DO UPDATE + SET roles = EXCLUDED.roles, updated_at = EXCLUDED.updated_at`, + [...key, event.member, event.rolesAfter, at], + ); + } + break; + + case 'RecipientRegistered': + await c.query( + `INSERT INTO recipients (network, vault_id, address, label, trust, activates_at, first_seen) + VALUES ($1,$2,$3,$4,$5,$6,$7) + ON CONFLICT (network, vault_id, address) DO UPDATE SET + label = EXCLUDED.label, trust = EXCLUDED.trust, activates_at = EXCLUDED.activates_at`, + [...key, event.recipient, event.label, event.trust, toTimestamp(event.activatesAt), at], + ); + break; + + case 'RecipientTrustChanged': + await c.query( + `INSERT INTO recipients (network, vault_id, address, trust, first_seen) + VALUES ($1,$2,$3,$4,$5) + ON CONFLICT (network, vault_id, address) DO UPDATE SET trust = EXCLUDED.trust`, + [...key, event.recipient, event.to, at], + ); + // A blocked destination is the strongest signal the system produces about an address, so it + // is counted across vaults even though no contract reads it. + await c.query( + `INSERT INTO recipient_reputation (network, address, first_seen, last_seen, blocked_by) + VALUES ($1,$2,$3,$3,$4) + ON CONFLICT (network, address) DO UPDATE SET + last_seen = EXCLUDED.last_seen, + blocked_by = ( + SELECT count(*) FROM recipients + WHERE network = $1 AND address = $2 AND trust IN (4, 5) + )`, + [network, event.recipient, at, event.to >= 4 ? 1 : 0], + ); + break; + + case 'Recovered': + await c.query( + `DELETE FROM members WHERE network = $1 AND vault_id = $2 AND address = $3`, + [...key, event.lost], + ); + await c.query( + `INSERT INTO members (network, vault_id, address, roles, updated_at) + VALUES ($1,$2,$3,$4,$5) + ON CONFLICT (network, vault_id, address) DO UPDATE + SET roles = EXCLUDED.roles, updated_at = EXCLUDED.updated_at`, + [...key, event.replacement, event.roles, at], + ); + break; + } + + await c.query( + `INSERT INTO events (network, tx_digest, event_index, vault_id, name, checkpoint, occurred_at, payload) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8) + ON CONFLICT (network, tx_digest, event_index) DO NOTHING`, + [ + network, + position.transactionDigest, + position.eventIndex, + vault, + event.name, + position.checkpoint ?? null, + at, + JSON.stringify(event, (_, v) => (typeof v === 'bigint' ? v.toString() : v)), + ], + ); +} + +/** A `VaultCreated` row may be missing if indexing started mid-history; make one so FKs hold. */ +export async function ensureVault(c: PoolClient, network: string, event: SkurEvent) { + await c.query( + `INSERT INTO vaults (network, vault_id, name, creator, created_at) + VALUES ($1,$2,'(discovered)','0x0',$3) + ON CONFLICT (network, vault_id) DO NOTHING`, + [network, event.vault, toTimestamp(event.at)], + ); +} diff --git a/server/src/rules.ts b/server/src/rules.ts new file mode 100644 index 0000000..86c3c77 --- /dev/null +++ b/server/src/rules.ts @@ -0,0 +1,292 @@ +import { + KIND_LABELS, + MODE_LABELS, + Kind, + Mode, + Role, + Status, + TIER_LABELS, + Tier, + Trust, + describeReasons, + short, + type DecodedEvent, +} from '@skur/sdk'; + +/** + * Who needs to be told, and why. + * + * The rule is the point of the whole service: an approver should hear about the queue, an + * executor about what is ready, and a guardian about the things only a guardian can stop. Sending + * everything to everyone is the same as sending nothing, because nobody reads the eleventh alert. + * + * `roles` is a bitmask of who the message is for; zero means every member of the vault. + */ +type Rule = { + rule: string; + title: string; + body: string; + severity: 'info' | 'action' | 'alert'; + roles: number; + /** The deep link that opens what this is about. Defaults to the vault itself. */ + link?: string; +}; + +/** skur://vault/0x…[/proposal/n] — the scheme the mobile app answers to. */ +export function linkTo(vault: string, proposal?: bigint): string { + return proposal === undefined ? `skur://vault/${vault}` : `skur://vault/${vault}/proposal/${proposal}`; +} + +const fmtAmount = (v: bigint) => v.toLocaleString('en-US'); + +export function rulesFor({ event }: DecodedEvent): Rule[] { + switch (event.name) { + case 'ProposalOpened': { + const out: Rule[] = []; + const who = event.kind === Kind.TRANSFER ? Role.APPROVER : Role.OWNER; + const label = KIND_LABELS[event.kind] ?? 'Proposal'; + + if (event.kind === Kind.TRANSFER) { + const reasons = describeReasons(event.reasons); + out.push({ + rule: 'transfer.opened', + title: `${TIER_LABELS[event.tier]} payment awaiting approval`, + body: [ + `${fmtAmount(event.amount)} to ${short(event.recipient)}${event.memo ? ` — ${event.memo}` : ''}.`, + `Needs ${event.reqApprovals} approval${event.reqApprovals === 1 ? '' : 's'}${ + event.reqGuardians ? ` and ${event.reqGuardians} guardian confirmation${event.reqGuardians === 1 ? '' : 's'}` : '' + }.`, + reasons.length ? `Scored ${TIER_LABELS[event.tier]} because: ${reasons.join('; ')}.` : '', + ] + .filter(Boolean) + .join(' '), + severity: 'action', + roles: who, + link: linkTo(event.vault, event.proposal), + }); + if (event.tier === Tier.CRITICAL) { + out.push({ + rule: 'transfer.critical', + title: 'A critical payment is open and can be vetoed', + body: `${fmtAmount(event.amount)} to ${short(event.recipient)}, at ${(event.exposureBps / 100).toFixed(1)}% of holdings. You can veto it until it executes.`, + severity: 'alert', + roles: Role.GUARDIAN, + link: linkTo(event.vault, event.proposal), + }); + } + } else { + out.push({ + rule: 'governance.opened', + title: `${label} awaiting owner approval`, + body: `Opened by ${short(event.proposer)}.`, + severity: 'action', + roles: who, + link: linkTo(event.vault, event.proposal), + }); + } + + // The one notification that matters most: somebody is trying to make the vault weaker. + if (event.reductionMask !== 0) { + out.push({ + rule: 'governance.weakening', + title: `${label} would weaken this vault`, + body: `It cannot take effect before ${new Date(event.executableAt).toUTCString()}, and any guardian can veto it until then.`, + severity: 'alert', + roles: Role.GUARDIAN | Role.OWNER, + link: linkTo(event.vault, event.proposal), + }); + } + if (event.kind === Kind.RECOVERY) { + out.push({ + rule: 'recovery.opened', + title: 'A signer recovery has been proposed', + body: `Proposed by ${short(event.proposer)}. Any owner can cancel it before it executes.`, + severity: 'alert', + roles: Role.OWNER, + link: linkTo(event.vault, event.proposal), + }); + } + return out; + } + + case 'Approved': + // Only the approval that completes the set is worth an interruption. + if (event.approvals < event.reqApprovals) return []; + return [ + { + rule: 'proposal.ready', + title: 'A payment has all its approvals', + body: `Proposal #${event.proposal} is fully approved. It executes once its waiting period has elapsed.`, + severity: 'action', + roles: Role.EXECUTOR, + link: linkTo(event.vault, event.proposal), + }, + ]; + + case 'Rejected': + // A single rejection is a vote in progress, not news. The one that settles it is. + if (!event.settled) return []; + return [ + { + rule: 'proposal.rejected', + title: 'A proposal was rejected', + body: `Proposal #${event.proposal} was turned down by ${event.rejections} signer${event.rejections === 1 ? '' : 's'}, which is as many as it needed to be approved.`, + severity: 'info', + roles: 0, + }, + ]; + + case 'BreakerTripped': + return [ + { + rule: 'breaker.tripped', + title: 'The circuit breaker stopped a payment and froze the vault', + body: `${fmtAmount(event.amount)} would have taken the window past its loss envelope of ${fmtAmount(event.envelopeLimit)} against a basis of ${fmtAmount(event.envelopeBasis)}. The money did not move. The vault is now in Lockdown and needs owners, a guardian and time to reopen.`, + severity: 'alert', + roles: 0, + }, + ]; + + case 'ModeChanged': + if (event.to === Mode.NORMAL) { + return [ + { + rule: 'mode.normal', + title: 'The vault is back to normal', + body: `Lowered from ${MODE_LABELS[event.from]} by ${short(event.actor)}.`, + severity: 'info', + roles: 0, + }, + ]; + } + return [ + { + rule: 'mode.raised', + title: `The vault is now in ${MODE_LABELS[event.to]}`, + body: [ + `Raised from ${MODE_LABELS[event.from]} by ${short(event.actor)}.`, + event.to === Mode.LOCKDOWN + ? 'Nothing outgoing executes until owners and a guardian agree to reopen it.' + : 'Caps are halved and every payment is escalated one tier.', + describeReasons(event.reasons).join('; '), + ] + .filter(Boolean) + .join(' '), + severity: 'alert', + roles: 0, + }, + ]; + + case 'Settled': + if (event.status === Status.VETOED) { + return [ + { + rule: 'proposal.vetoed', + title: 'A guardian vetoed a proposal', + body: `Proposal #${event.proposal} was vetoed by ${short(event.actor)}. The vault moved to Elevated.`, + severity: 'alert', + roles: 0, + }, + ]; + } + if (event.status === Status.EXPIRED) { + return [ + { + rule: 'proposal.expired', + title: 'A proposal expired before it executed', + body: `Proposal #${event.proposal} (${KIND_LABELS[event.kind]}) ran out of time. Its approvals no longer count for anything.`, + severity: 'info', + roles: Role.APPROVER | Role.OWNER, + }, + ]; + } + return []; + + case 'Executed': + if (event.kind !== Kind.TRANSFER) return []; + return [ + { + rule: 'transfer.executed', + title: 'A payment went out', + body: `${fmtAmount(event.amount)} to ${short(event.recipient)}, executed by ${short(event.executor)}. ${fmtAmount(event.balanceAfter)} remains.`, + severity: 'info', + roles: 0, + }, + ]; + + case 'RecipientTrustChanged': + if (event.to !== Trust.BLOCKED && event.to !== Trust.RESTRICTED) return []; + return [ + { + rule: 'recipient.restricted', + title: `A recipient was ${event.to === Trust.BLOCKED ? 'blocked' : 'restricted'}`, + body: `${short(event.recipient)} was set by ${short(event.actor)}. Payments to it are ${event.to === Trust.BLOCKED ? 'refused' : 'treated as critical'} from now on.`, + severity: 'alert', + roles: 0, + }, + ]; + + case 'Recovered': + return [ + { + rule: 'recovery.executed', + title: 'A signer was replaced', + body: `${short(event.lost)} was replaced by ${short(event.replacement)} with the same roles. The policy was not changed, and the vault is now Elevated.`, + severity: 'alert', + roles: 0, + }, + ]; + + case 'PolicyChanged': + return [ + { + rule: 'policy.changed', + title: `The policy is now at version ${event.version}`, + body: event.reductionMask === 0 + ? 'The change only tightened the vault.' + : 'The change gave up one or more controls. It served its delay and was not vetoed.', + severity: event.reductionMask === 0 ? 'info' : 'alert', + roles: 0, + }, + ]; + + default: + return []; + } +} + +/** What Expo's push service accepts, built where a test can look at it. */ +export type ExpoPushMessage = { + to: string; + title: string; + body: string; + sound: 'default'; + priority: 'high' | 'default'; + data: { url: string }; +}; + +const EXPO_TOKEN = /^(ExponentPushToken|ExpoPushToken)\[[^\]]+\]$/; + +export function buildExpoPushMessage(n: { + endpoint: string; + title: string; + body: string; + severity: string; + link: string | null; + vault_id: string; +}): ExpoPushMessage { + if (!EXPO_TOKEN.test(n.endpoint)) { + // Refused here rather than by Expo, so the row records a reason instead of burning its six + // attempts on a 400. + throw new Error(`not an Expo push token: ${n.endpoint.slice(0, 24)}…`); + } + return { + to: n.endpoint, + title: n.title, + body: n.body, + sound: 'default', + // An alert is worth waking a phone for; information is not. + priority: n.severity === 'alert' ? 'high' : 'default', + data: { url: n.link ?? `skur://vault/${n.vault_id}` }, + }; +} diff --git a/server/test/notify.test.ts b/server/test/notify.test.ts new file mode 100644 index 0000000..b8d1da5 --- /dev/null +++ b/server/test/notify.test.ts @@ -0,0 +1,276 @@ +import { describe, expect, it } from 'vitest'; +import { Kind, Mode, Role, Status, Tier, Trust, type DecodedEvent } from '@skur/sdk'; +import { buildExpoPushMessage, linkTo, rulesFor } from '../src/rules.ts'; + +/** + * The rules decide who gets interrupted. Getting this wrong in either direction is a real failure: + * too little and a guardian misses the one thing only they can stop; too much and nobody reads the + * eleventh alert. + */ +const position = { transactionDigest: 'DIGEST', eventIndex: 0 }; + +function wrap(event: DecodedEvent['event']): DecodedEvent { + return { event, position }; +} + +const openedBase = { + name: 'ProposalOpened' as const, + vault: '0xV', + proposal: 7n, + kind: Kind.TRANSFER, + proposer: '0xP', + asset: '0x2::sui::SUI', + amount: 1_000n, + recipient: '0xR', + tier: Tier.LOW, + reasons: 0, + exposureBps: 10, + reqApprovals: 1, + reqGuardians: 0, + executableAt: 1, + expiresAt: 2, + reductionMask: 0, + memo: 'invoice', + at: 1, +}; + +describe('who hears about a new payment', () => { + it('tells approvers, and nobody else', () => { + const rules = rulesFor(wrap(openedBase)); + expect(rules).toHaveLength(1); + expect(rules[0].rule).toBe('transfer.opened'); + expect(rules[0].roles).toBe(Role.APPROVER); + expect(rules[0].severity).toBe('action'); + }); + + it('also tells guardians when the payment is critical, because only they can veto it', () => { + const rules = rulesFor(wrap({ ...openedBase, tier: Tier.CRITICAL, reqGuardians: 1 })); + const guardian = rules.find((r) => r.rule === 'transfer.critical'); + expect(guardian).toBeDefined(); + expect(guardian!.roles).toBe(Role.GUARDIAN); + expect(guardian!.severity).toBe('alert'); + }); + + it('explains why the tier landed where it did', () => { + const rules = rulesFor( + wrap({ ...openedBase, tier: Tier.HIGH, reasons: 1 | 256, reqApprovals: 2 }), + ); + expect(rules[0].body).toMatch(/never paid this address/); + expect(rules[0].body).toMatch(/2 approvals/); + }); + + it('routes a governance proposal to owners instead', () => { + const rules = rulesFor(wrap({ ...openedBase, kind: Kind.POLICY_UPDATE })); + expect(rules[0].rule).toBe('governance.opened'); + expect(rules[0].roles).toBe(Role.OWNER); + }); + + it('raises an alert to guardians and owners when a change would weaken the vault', () => { + const rules = rulesFor( + wrap({ ...openedBase, kind: Kind.POLICY_UPDATE, reductionMask: 1 }), + ); + const weakening = rules.find((r) => r.rule === 'governance.weakening'); + expect(weakening).toBeDefined(); + expect(weakening!.roles).toBe(Role.GUARDIAN | Role.OWNER); + expect(weakening!.severity).toBe('alert'); + expect(weakening!.body).toMatch(/veto/); + }); + + it('tells owners about a recovery, because any one of them can cancel it', () => { + const rules = rulesFor(wrap({ ...openedBase, kind: Kind.RECOVERY, reductionMask: 1 })); + expect(rules.some((r) => r.rule === 'recovery.opened' && r.roles === Role.OWNER)).toBe(true); + }); +}); + +describe('approvals', () => { + it('says nothing until the set is complete', () => { + expect( + rulesFor( + wrap({ + name: 'Approved', + vault: '0xV', + proposal: 7n, + approver: '0xA', + approvals: 1, + reqApprovals: 3, + at: 1, + }), + ), + ).toEqual([]); + }); + + it('tells executors once it is complete', () => { + const rules = rulesFor( + wrap({ + name: 'Approved', + vault: '0xV', + proposal: 7n, + approver: '0xA', + approvals: 3, + reqApprovals: 3, + at: 1, + }), + ); + expect(rules).toHaveLength(1); + expect(rules[0].roles).toBe(Role.EXECUTOR); + }); +}); + +describe('rejections', () => { + const base = { + name: 'Rejected' as const, + vault: '0xV', + proposal: 7n, + rejecter: '0xA', + rejections: 1, + reqApprovals: 2, + settled: false, + at: 1, + }; + + it('stays quiet on a rejection that has not settled anything', () => { + expect(rulesFor(wrap(base))).toEqual([]); + }); + + it('tells the vault once the rejection carries', () => { + const rules = rulesFor(wrap({ ...base, rejections: 2, settled: true })); + expect(rules).toHaveLength(1); + expect(rules[0].rule).toBe('proposal.rejected'); + expect(rules[0].roles).toBe(0); + expect(rules[0].body).toMatch(/2 signers/); + }); +}); + +describe('the things everyone hears about', () => { + it('tells the whole vault when the breaker trips, and says the money did not move', () => { + const rules = rulesFor( + wrap({ + name: 'BreakerTripped', + vault: '0xV', + proposal: 9n, + asset: '0x2::sui::SUI', + amount: 200_000n, + envelopeSpent: 0n, + envelopeLimit: 100_000n, + envelopeBasis: 1_000_000n, + at: 1, + }), + ); + expect(rules[0].roles).toBe(0); + expect(rules[0].severity).toBe('alert'); + expect(rules[0].body).toMatch(/did not move/); + expect(rules[0].body).toMatch(/Lockdown/); + }); + + it('distinguishes a freeze from a thaw', () => { + const raised = rulesFor( + wrap({ name: 'ModeChanged', vault: '0xV', from: Mode.NORMAL, to: Mode.LOCKDOWN, actor: '0xG', reasons: 0, at: 1 }), + ); + expect(raised[0].severity).toBe('alert'); + expect(raised[0].body).toMatch(/until owners and a guardian/); + + const lowered = rulesFor( + wrap({ name: 'ModeChanged', vault: '0xV', from: Mode.ELEVATED, to: Mode.NORMAL, actor: '0xO', reasons: 0, at: 1 }), + ); + expect(lowered[0].severity).toBe('info'); + }); + + it('alerts on a veto and merely notes an expiry', () => { + const vetoed = rulesFor( + wrap({ name: 'Settled', vault: '0xV', proposal: 1n, kind: Kind.TRANSFER, status: Status.VETOED, actor: '0xG', at: 1 }), + ); + expect(vetoed[0].severity).toBe('alert'); + + const expired = rulesFor( + wrap({ name: 'Settled', vault: '0xV', proposal: 1n, kind: Kind.TRANSFER, status: Status.EXPIRED, actor: '0xA', at: 1 }), + ); + expect(expired[0].severity).toBe('info'); + + const cancelled = rulesFor( + wrap({ name: 'Settled', vault: '0xV', proposal: 1n, kind: Kind.TRANSFER, status: Status.CANCELLED, actor: '0xA', at: 1 }), + ); + expect(cancelled).toEqual([]); + }); + + it('alerts when a recipient is blocked and stays quiet when one is merely verified', () => { + const blocked = rulesFor( + wrap({ name: 'RecipientTrustChanged', vault: '0xV', proposal: 0n, recipient: '0xR', from: Trust.NEW, to: Trust.BLOCKED, actor: '0xG', at: 1 }), + ); + expect(blocked[0].severity).toBe('alert'); + + const verified = rulesFor( + wrap({ name: 'RecipientTrustChanged', vault: '0xV', proposal: 1n, recipient: '0xR', from: Trust.NEW, to: Trust.VERIFIED, actor: '0xO', at: 1 }), + ); + expect(verified).toEqual([]); + }); + + it('marks a tightening policy change as information and a weakening as an alert', () => { + const tightened = rulesFor( + wrap({ name: 'PolicyChanged', vault: '0xV', proposal: 1n, version: 2n, reductionMask: 0, at: 1 }), + ); + expect(tightened[0].severity).toBe('info'); + + const weakened = rulesFor( + wrap({ name: 'PolicyChanged', vault: '0xV', proposal: 1n, version: 3n, reductionMask: 64, at: 1 }), + ); + expect(weakened[0].severity).toBe('alert'); + }); + + it('does not notify on a deposit, because incoming money is not an event anyone must act on', () => { + expect( + rulesFor( + wrap({ name: 'Deposited', vault: '0xV', asset: '0x2::sui::SUI', amount: 1n, balance: 1n, from: '0xX', at: 1 }), + ), + ).toEqual([]); + }); +}); + + +describe('push delivery to a device', () => { + const queued = { + endpoint: 'ExponentPushToken[abc123]', + title: 'A critical payment is open and can be vetoed', + body: 'Body.', + severity: 'alert', + link: 'skur://vault/0xV/proposal/7', + vault_id: '0xV', + }; + + it('builds what Expo accepts, with the deep link in the data', () => { + const m = buildExpoPushMessage(queued); + expect(m.to).toBe('ExponentPushToken[abc123]'); + expect(m.data.url).toBe('skur://vault/0xV/proposal/7'); + expect(m.sound).toBe('default'); + }); + + it('wakes a phone for an alert and not for information', () => { + expect(buildExpoPushMessage(queued).priority).toBe('high'); + expect(buildExpoPushMessage({ ...queued, severity: 'info' }).priority).toBe('default'); + }); + + it('refuses an endpoint that is not an Expo token, before Expo has to', () => { + expect(() => buildExpoPushMessage({ ...queued, endpoint: 'https://example.com/hook' })).toThrow( + /not an Expo push token/, + ); + }); + + it('falls back to the vault link when a rule carried none', () => { + expect(buildExpoPushMessage({ ...queued, link: null }).data.url).toBe('skur://vault/0xV'); + }); +}); + +describe('the links the rules carry', () => { + it('a payment notification opens that payment', () => { + const rules = rulesFor( + wrap({ ...openedBase, tier: Tier.CRITICAL, reqGuardians: 1 }), + ); + for (const r of rules) { + expect(r.link).toBe('skur://vault/0xV/proposal/7'); + } + }); + + it('linkTo composes the scheme the app parses', () => { + expect(linkTo('0xabc')).toBe('skur://vault/0xabc'); + expect(linkTo('0xabc', 42n)).toBe('skur://vault/0xabc/proposal/42'); + }); +}); diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..fc39497 --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "NodeNext", + "moduleResolution": "nodenext", + "lib": ["ES2023"], + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowImportingTsExtensions": true, + "types": ["node"] + }, + "include": ["src"] +} diff --git a/sui/.gitignore b/sui/.gitignore new file mode 100644 index 0000000..b2e48ff --- /dev/null +++ b/sui/.gitignore @@ -0,0 +1,6 @@ +build/ +.env + +# `sui client test-publish` writes an ephemeral publication record per environment. It is a local +# artifact of a throwaway network, unlike Published.toml, which is committed. +Pub.*.toml diff --git a/sui/Move.toml b/sui/Move.toml new file mode 100644 index 0000000..4dbe7d2 --- /dev/null +++ b/sui/Move.toml @@ -0,0 +1,23 @@ +[package] +name = "skur" +edition = "2024" +license = "MIT" +authors = ["Olajumoke Emmanuel"] + +# The Sui framework is an implicit dependency, resolved from the toolchain that builds this +# package. That keeps the build hermetic and pins the framework to the CLI you deploy with. +[dependencies] + +# `mainnet` and `testnet` are system environments: they are built in and must not be redeclared +# here. Publishing to either works with no `[environments]` section at all, and the resulting +# address lands in Published.toml, which is committed so other packages can depend on this one. +# +# A local network or a devnet needs an entry, because their chain identifiers are not fixed — +# devnet's changes every time the network is wiped. Get the current one with +# `sui client chain-identifier` and add it: +# +# [environments] +# local = "9f2b7fec" +# +# Or skip the bookkeeping entirely and use `sui client test-publish`, which publishes +# ephemerally against whatever network is active. diff --git a/sui/README.md b/sui/README.md new file mode 100644 index 0000000..1cb2728 --- /dev/null +++ b/sui/README.md @@ -0,0 +1,115 @@ +# The Skur Move package + +Four modules. The first three are pure and the fourth holds the state, which is deliberate: the +rules can be read, tested and argued about without reasoning about the object they act on. + +| Module | Lines | What it is | +|---|---|---| +| [`types.move`](sources/types.move) | ~145 | The vocabulary: role bits, risk tiers, trust levels, security modes, proposal kinds, statuses and the reason bitmask. No state, no logic beyond `roles_valid` | +| [`policy.move`](sources/policy.move) | ~340 | The seventeen policy fields, the per-asset limits, every validation rule, and `reductions` — the comparison that decides whether a change weakens the vault | +| [`risk.move`](sources/risk.move) | ~170 | The classifier. Five signals in, a tier, a reason mask and an exposure out. Pure and total | +| [`vault.move`](sources/vault.move) | ~1,700 | The shared object, the proposal lifecycle and every enforcement point | + +## Reading order, for a review + +1. **`risk::classify`** — five signals, each able to raise the tier and none able to lower it. If + this is wrong, every number the product shows is wrong. +2. **`policy::reductions`** — the policy firewall. The interesting cases are the ones where a + number going *down* is a loosening: `threshold_loosens` exists because a threshold of zero means + "disabled", so moving to zero is the loosest move available and moving away from it can only + tighten. +3. **`vault::execute_transfer`** — the last gate, and the function that matters most. Everything is + re-evaluated against live state, the stricter of the pinned and live requirements applies, and + the circuit breaker is checked last and charged in the same step as the payment. +4. **`vault::begin` / `finish`** — creation as a hot potato. `VaultSetup` has no abilities, so the + transaction that starts a vault must also finish it. + +## The one unusual control flow + +`execute_transfer` does not abort when the loss envelope would be crossed. It marks the proposal +`BLOCKED`, sets the vault to `LOCKDOWN`, emits `BreakerTripped`, and **returns** — so the +transaction succeeds and those writes persist. The coin is never split and never sent. + +Aborting would be the obvious thing and it would be wrong: the abort would roll back the Lockdown +along with everything else, leaving the vault open and the attacker free to try again for the price +of gas. `crossing_the_loss_envelope_freezes_the_vault_instead_of_paying` asserts all three +outcomes — the status, the unchanged balance, and the Lockdown. + +## Tests + +```bash +sui move build # clean, with no warnings +sui move test # 75 tests: 35 engine vectors, 40 end to end +sui move test engine_ # the pure vectors +sui move test vault_ # the end-to-end suite +sui move test crossing_the_loss # one test by name +``` + +Verified against `sui 1.80.1-671ba71e69c7`. + +Note for the test suite: `#[expected_failure]` needs `location = skur::vault` on any test that +asserts a code the vault raises. Without it the attribute expects the abort to originate in the +test module, and the test fails with the right code and the wrong module. + +[`tests/engine_tests.move`](tests/engine_tests.move) pins the classifier and the policy rules. The +same vectors are pinned in TypeScript in [`../sdk/test/engine.test.ts`](../sdk/test/engine.test.ts); +both suites have to agree, because the only thing worse than a wrong tier is two components +disagreeing about it. + +[`tests/vault_tests.move`](tests/vault_tests.move) is named after the attacks rather than the +functions. `a_removed_signers_approval_stops_counting`, `splitting_a_drain_does_not_beat_the_daily_cap` +and `an_agent_cannot_execute` say what they defend; `test_execute_transfer_3` would not. + +## Conventions + +- **Durations are milliseconds**, because that is what `sui::clock` reports. Converting at each + comparison is how an off-by-one-thousand gets in. +- **Amounts are `u64`** in the coin's smallest unit, and any intermediate that could overflow — + exposure, the velocity comparison, the envelope limit — is computed in `u128`. +- **Validation returns a code, and a separate function aborts with it.** An interface can dry-run a + draft policy and show the precise reason; the vault asserts with the same number. +- **Zero means "no limit"** for every cap and threshold. This is load-bearing: it is why + `threshold_loosens` cannot be a plain `>`. +- **Effects before interactions.** The proposal's status is written before any coin moves. Move has + no dynamic dispatch into a caller, so reentrancy is absent by construction rather than defended + against, but the ordering is kept anyway because it costs nothing and survives a refactor. + +## Verifying against a real chain + +The unit suites cannot prove that the BCS layouts match these structs, that the PTB builders +produce transactions the vault accepts, or that the events decode back into the shapes the indexer +projects. A wrong field offset passes every unit test and then reads a plausible wrong number off +the chain. + +```bash +scripts/verify-local.sh +``` + +Starts a throwaway local network, publishes ephemerally, and runs +[`../sdk/scripts/e2e.ts`](../sdk/scripts/e2e.ts) against it: create, read back, deposit, preview, +open, approve, prove the activation delay refuses an early execution, execute, prove probation +expires, and decode every event. Nothing touches a public network or your `~/.sui`. + +This found a real bug the unit tests could not: the SDK's abort-code parser was scanning for the +first run of digits, so `MoveAbort in 1st command, abort code: 119` reported code **1**, and a +signer held up by a recipient's activation delay was told their policy needed more approvals. + +## Publishing + +```bash +scripts/publish.sh testnet +``` + +Builds, runs the tests, publishes, and writes [`deployments/testnet.json`](deployments/). It will +not publish a package whose tests fail. + +Two things about the package-management model, because it changed and the old shape silently fails: + +- **`mainnet` and `testnet` are system environments.** They are built in and must *not* be declared + in `[environments]` — doing so fails with "cannot override default environments". Any other + network needs an entry with its chain identifier from `sui client chain-identifier`, and devnet's + changes every time it is wiped. +- **`Published.toml` records the published address** and is committed, so other packages can depend + on this one. `published-at` and `[addresses]` in `Move.toml` are the old system and are gone. + `Move.lock` pins the framework revision the package was built against and is also committed — + reproducing a build means reproducing that pin. diff --git a/sui/deployments/README.md b/sui/deployments/README.md new file mode 100644 index 0000000..e26fb54 --- /dev/null +++ b/sui/deployments/README.md @@ -0,0 +1,22 @@ +# Deployments + +One file per network, written by [`../scripts/publish.sh`](../scripts/publish.sh). This is the only +place a package address is configured: the SDK, the interface and the backend all read it from here +or from an environment variable that overrides it. + +```json +{ + "network": "testnet", + "packageId": "0x…", + "publishedAtDigest": "…", + "deployer": "0x…", + "publishedAt": "2026-10-05T00:00:00Z", + "vaults": [{ "id": "0x…", "name": "Skur Demo Treasury" }] +} +``` + +`vaults` is for demonstration vaults worth linking to from the docs. It is not a registry: a vault +is a shared object and works whether or not it is listed here. + +Run `npm run sync-deployments` in [`../../app`](../../app) after publishing, to copy these into the +interface's bundle. diff --git a/sui/scripts/publish.sh b/sui/scripts/publish.sh new file mode 100755 index 0000000..e7d918b --- /dev/null +++ b/sui/scripts/publish.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# Publish the Skur package and record where it landed. +# +# Usage: scripts/publish.sh [testnet|mainnet|] +# +# `mainnet` and `testnet` are system environments and work with no configuration. Any other +# environment — a devnet, a local network — has to be declared in Move.toml with its chain +# identifier, because those identifiers are not fixed. `sui client test-publish --build-env +# testnet` publishes ephemerally instead, which is what the local verification uses. +# +# The authoritative record of the published address is Published.toml, written by the CLI and +# committed to source control so other packages can depend on this one. This script additionally +# writes deployments/.json, which is the single place the interface, the SDK and the +# backend read an address from. +set -euo pipefail + +NETWORK="${1:-testnet}" +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OUT="$HERE/deployments/$NETWORK.json" + +command -v sui >/dev/null || { echo "the sui CLI is not on PATH" >&2; exit 1; } + +echo "building" +sui move build --path "$HERE" + +# Publishing a package whose tests fail is the one mistake this script will not let you make. +echo "testing before publishing" +sui move test --path "$HERE" + +echo "switching to $NETWORK" +sui client switch --env "$NETWORK" >/dev/null + +ADDRESS="$(sui client active-address)" +CHAIN="$(sui client chain-identifier 2>/dev/null | awk '/Hex:/ {print $2}')" +echo "publishing as $ADDRESS on chain $CHAIN" + +RESULT="$(sui client publish "$HERE" --gas-budget 2000000000 --json)" + +PACKAGE_ID="$(printf '%s' "$RESULT" | python3 -c ' +import json, sys +d = json.load(sys.stdin) +for c in d.get("objectChanges", []): + if c.get("type") == "published": + print(c["packageId"]); break +')" +DIGEST="$(printf '%s' "$RESULT" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("digest",""))')" +STATUS="$(printf '%s' "$RESULT" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("effects",{}).get("status",{}).get("status",""))')" + +[ "$STATUS" = "success" ] || { echo "publish did not succeed: $STATUS" >&2; exit 1; } +[ -n "$PACKAGE_ID" ] || { echo "could not find the published package id in the output" >&2; exit 1; } + +python3 - "$OUT" "$NETWORK" "$PACKAGE_ID" "$DIGEST" "$ADDRESS" "$CHAIN" <<'PY' +import json, sys, datetime, os +out, network, package, digest, deployer, chain = sys.argv[1:7] +existing = {} +if os.path.exists(out): + try: + existing = json.load(open(out)) + except Exception: + pass +json.dump({ + "network": network, + "chainIdentifier": chain, + "packageId": package, + "publishedAtDigest": digest, + "deployer": deployer, + "publishedAt": datetime.datetime.now(datetime.UTC).isoformat(), + # Demonstration vaults survive a republish; they are keyed to the old package only in that + # their proposals were opened against it, and they keep working. + "vaults": existing.get("vaults", []), +}, open(out, "w"), indent=2) +print(f"wrote {out}") +PY + +echo +echo "package: $PACKAGE_ID" +echo "record: $HERE/Published.toml (commit this)" +echo "explorer: https://suiscan.xyz/$NETWORK/object/$PACKAGE_ID" +echo +echo "next:" +echo " cd ../app && npm run sync-deployments" +echo " cd ../sdk && SUI_PRIVATE_KEY=... SKUR_NETWORK=$NETWORK SKUR_PACKAGE_ID=$PACKAGE_ID npm run e2e" diff --git a/sui/scripts/test.sh b/sui/scripts/test.sh new file mode 100755 index 0000000..8185daa --- /dev/null +++ b/sui/scripts/test.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Run the Move test suite, with coverage when the CLI was built with it. +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +command -v sui >/dev/null || { echo "the sui CLI is not on PATH" >&2; exit 1; } +sui move test --path "$HERE" "$@" diff --git a/sui/scripts/verify-local.sh b/sui/scripts/verify-local.sh new file mode 100755 index 0000000..4d5092c --- /dev/null +++ b/sui/scripts/verify-local.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Verify the whole stack against a throwaway local network: publish the package, then run the +# end-to-end script against it. +# +# This is the check the unit suites cannot be. It proves the BCS layouts match the Move structs, +# that the PTB builders produce transactions the vault accepts, that preview_transfer decodes, and +# that the events decode back into the shapes the indexer projects. Nothing here touches a public +# network or your ~/.sui config. +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +WORK="${SKUR_LOCAL_DIR:-/tmp/skur-localnet}" +export SUI_CONFIG_DIR="$WORK/suiconfig" +mkdir -p "$SUI_CONFIG_DIR" + +command -v sui >/dev/null || { echo "the sui CLI is not on PATH" >&2; exit 1; } + +cleanup() { [ -n "${NODE_PID:-}" ] && kill "$NODE_PID" 2>/dev/null || true; } +trap cleanup EXIT + +echo "starting a local network" +sui start --force-regenesis --with-faucet --fullnode-rpc-port 9000 > "$WORK/node.log" 2>&1 & +NODE_PID=$! +for _ in $(seq 1 60); do + sleep 2 + curl -s -m 2 -o /dev/null http://127.0.0.1:9000 2>/dev/null && break +done + +sui client new-env --alias skur-local --rpc http://127.0.0.1:9000 >/dev/null 2>&1 || true +sui client switch --env skur-local >/dev/null +ADDR="$(sui client active-address)" +sui client faucet --url http://127.0.0.1:9123/gas >/dev/null +sleep 5 + +echo "publishing ephemerally" +# The ephemeral publication record remembers that this package is already published for this +# environment, and refuses a second publish. The network it refers to was thrown away at the end +# of the last run, so the record is stale by definition — clear it. +rm -f "$HERE"/Pub.*.toml + +# `test-publish` needs a build environment because the local chain identifier is not declared in +# Move.toml, and it should not be: it changes on every regenesis. +RESULT="$(sui client test-publish --build-env testnet "$HERE" --gas-budget 2000000000 --json)" +PACKAGE_ID="$(printf '%s' "$RESULT" | python3 -c ' +import json, sys +d = json.load(sys.stdin) +print(next((c["packageId"] for c in d.get("objectChanges", []) if c.get("type") == "published"), "")) +')" +[ -n "$PACKAGE_ID" ] || { echo "publish failed" >&2; exit 1; } +echo "package $PACKAGE_ID" + +KEY="$(sui keytool export --key-identity "$ADDR" --json 2>/dev/null | python3 -c ' +import json, sys +d = json.load(sys.stdin) +print(d.get("exportedPrivateKey") or d.get("key", {}).get("exportedPrivateKey") or "") +')" +[ -n "$KEY" ] || { echo "could not export the throwaway key" >&2; exit 1; } + +echo "running the end-to-end script" +cd "$HERE/../sdk" +npm run --silent build +SKUR_NETWORK=localnet SKUR_PACKAGE_ID="$PACKAGE_ID" SUI_PRIVATE_KEY="$KEY" \ + node --experimental-strip-types scripts/e2e.ts diff --git a/sui/sources/policy.move b/sui/sources/policy.move new file mode 100644 index 0000000..48654fe --- /dev/null +++ b/sui/sources/policy.move @@ -0,0 +1,341 @@ +/// The policy: seventeen fields that decide how much authorization a payment needs, how long it +/// waits, and how much can leave in a window. Plus the two things a policy module must be able to +/// answer: is this policy internally coherent, and does this change weaken the vault? +/// +/// Durations are milliseconds, because that is the unit `sui::clock` reports. The TypeScript +/// engine keeps the same fields in seconds and converts once, at the SDK boundary. +module skur::policy; + +use skur::types; + +// ---------------------------------------------------------------- validation failure codes +// Returned (not aborted) by `validate`, so an interface can dry-run a draft policy and show the +// precise reason. `assert_valid` turns a non-zero code into an abort with the same number. +const E_OK: u64 = 0; +const E_APPROVALS_LOW_ZERO: u64 = 1; +const E_APPROVALS_HIGH_BELOW_LOW: u64 = 2; +const E_APPROVALS_CRITICAL_BELOW_HIGH: u64 = 3; +const E_GOVERNANCE_ZERO: u64 = 4; +const E_GUARDIAN_REQUIRED_WITHOUT_THRESHOLD: u64 = 5; +const E_CRITICAL_DELAY_BELOW_HIGH: u64 = 6; +const E_HIGH_EXPOSURE_ABOVE_CRITICAL: u64 = 7; +const E_EXPOSURE_ABOVE_FULL: u64 = 8; +const E_HARD_BLOCK_BELOW_CRITICAL: u64 = 9; +const E_HARD_BLOCK_ABOVE_FULL: u64 = 10; +const E_ENVELOPE_ABOVE_FULL: u64 = 11; +const E_ENVELOPE_WINDOW_TOO_SHORT: u64 = 12; +const E_TTL_BELOW_LONGEST_DELAY: u64 = 13; + +const E_LIMITS_LOW_ABOVE_HIGH: u64 = 20; +const E_LIMITS_PER_TX_ABOVE_DAILY: u64 = 21; + +const E_NO_OWNER: u64 = 30; +const E_NO_EXECUTOR: u64 = 31; +const E_GOVERNANCE_EXCEEDS_OWNERS: u64 = 32; +const E_CRITICAL_EXCEEDS_APPROVERS: u64 = 33; +const E_GUARDIAN_THRESHOLD_EXCEEDS_GUARDIANS: u64 = 34; + +public fun e_ok(): u64 { E_OK } + +/// A proposal must stay executable for at least an hour after its delay elapses, otherwise a +/// policy could be written whose transfers expire before anyone can execute them. +const MIN_EXECUTION_WINDOW_MS: u64 = 3_600_000; + +public fun min_execution_window_ms(): u64 { MIN_EXECUTION_WINDOW_MS } + +// ---------------------------------------------------------------- the policy +public struct Policy has copy, drop, store { + /// Approvals required at each risk tier. Monotonic: low <= high <= critical. + approvals_low: u8, + approvals_high: u8, + approvals_critical: u8, + /// Owner approvals required for any governance action (policy, members, limits, trust). + governance_threshold: u8, + /// Guardian confirmations required when a guardian is in the loop. + guardian_threshold: u8, + /// Whether a critical transfer needs guardian confirmation on top of approver approvals. + guardian_required_critical: bool, + /// Timelocks, in milliseconds. + delay_high: u64, + delay_critical: u64, + recipient_activation_delay: u64, + policy_change_delay: u64, + recovery_delay: u64, + /// How long a proposal remains executable before it expires. + proposal_ttl: u64, + /// Share of the asset's balance, in basis points, that escalates a transfer. + /// Zero disables the threshold. + high_exposure_bps: u64, + critical_exposure_bps: u64, + /// Share of balance that is refused outright, whatever the approvals. Zero disables it. + hard_block_exposure_bps: u64, + /// The circuit breaker: how much of the balance may leave per window before the vault latches + /// into Lockdown. Zero disables the breaker. + envelope_bps: u64, + envelope_window: u64, +} + +public fun new( + approvals_low: u8, + approvals_high: u8, + approvals_critical: u8, + governance_threshold: u8, + guardian_threshold: u8, + guardian_required_critical: bool, + delay_high: u64, + delay_critical: u64, + recipient_activation_delay: u64, + policy_change_delay: u64, + recovery_delay: u64, + proposal_ttl: u64, + high_exposure_bps: u64, + critical_exposure_bps: u64, + hard_block_exposure_bps: u64, + envelope_bps: u64, + envelope_window: u64, +): Policy { + Policy { + approvals_low, + approvals_high, + approvals_critical, + governance_threshold, + guardian_threshold, + guardian_required_critical, + delay_high, + delay_critical, + recipient_activation_delay, + policy_change_delay, + recovery_delay, + proposal_ttl, + high_exposure_bps, + critical_exposure_bps, + hard_block_exposure_bps, + envelope_bps, + envelope_window, + } +} + +public fun approvals_low(p: &Policy): u8 { p.approvals_low } +public fun approvals_high(p: &Policy): u8 { p.approvals_high } +public fun approvals_critical(p: &Policy): u8 { p.approvals_critical } +public fun governance_threshold(p: &Policy): u8 { p.governance_threshold } +public fun guardian_threshold(p: &Policy): u8 { p.guardian_threshold } +public fun guardian_required_critical(p: &Policy): bool { p.guardian_required_critical } +public fun delay_high(p: &Policy): u64 { p.delay_high } +public fun delay_critical(p: &Policy): u64 { p.delay_critical } +public fun recipient_activation_delay(p: &Policy): u64 { p.recipient_activation_delay } +public fun policy_change_delay(p: &Policy): u64 { p.policy_change_delay } +public fun recovery_delay(p: &Policy): u64 { p.recovery_delay } +public fun proposal_ttl(p: &Policy): u64 { p.proposal_ttl } +public fun high_exposure_bps(p: &Policy): u64 { p.high_exposure_bps } +public fun critical_exposure_bps(p: &Policy): u64 { p.critical_exposure_bps } +public fun hard_block_exposure_bps(p: &Policy): u64 { p.hard_block_exposure_bps } +public fun envelope_bps(p: &Policy): u64 { p.envelope_bps } +public fun envelope_window(p: &Policy): u64 { p.envelope_window } + +// ---------------------------------------------------------------- per-asset limits +public struct AssetLimits has copy, drop, store { + /// An asset must be approved before it can be proposed at all. + approved: bool, + /// Above `low_max` a transfer is at least high tier; above `high_max` it is critical. + low_max: u64, + high_max: u64, + /// Absolute caps. Zero means no cap. + per_tx_max: u64, + daily_max: u64, +} + +public fun new_limits( + approved: bool, + low_max: u64, + high_max: u64, + per_tx_max: u64, + daily_max: u64, +): AssetLimits { + AssetLimits { approved, low_max, high_max, per_tx_max, daily_max } +} + +public fun limits_approved(l: &AssetLimits): bool { l.approved } +public fun limits_low_max(l: &AssetLimits): u64 { l.low_max } +public fun limits_high_max(l: &AssetLimits): u64 { l.high_max } +public fun limits_per_tx_max(l: &AssetLimits): u64 { l.per_tx_max } +public fun limits_daily_max(l: &AssetLimits): u64 { l.daily_max } + +// ---------------------------------------------------------------- validation +/// Returns `E_OK` when the policy is coherent, otherwise the first failure's code. +public fun validate(p: &Policy): u64 { + let bps = types::bps(); + if (p.approvals_low == 0) return E_APPROVALS_LOW_ZERO; + if (p.approvals_high < p.approvals_low) return E_APPROVALS_HIGH_BELOW_LOW; + if (p.approvals_critical < p.approvals_high) return E_APPROVALS_CRITICAL_BELOW_HIGH; + if (p.governance_threshold == 0) return E_GOVERNANCE_ZERO; + if (p.guardian_required_critical && p.guardian_threshold == 0) { + return E_GUARDIAN_REQUIRED_WITHOUT_THRESHOLD + }; + if (p.delay_critical < p.delay_high) return E_CRITICAL_DELAY_BELOW_HIGH; + if (p.critical_exposure_bps != 0 && p.high_exposure_bps > p.critical_exposure_bps) { + return E_HIGH_EXPOSURE_ABOVE_CRITICAL + }; + if (p.critical_exposure_bps > bps || p.high_exposure_bps > bps) return E_EXPOSURE_ABOVE_FULL; + if (p.hard_block_exposure_bps != 0 && p.hard_block_exposure_bps < p.critical_exposure_bps) { + return E_HARD_BLOCK_BELOW_CRITICAL + }; + if (p.hard_block_exposure_bps > bps) return E_HARD_BLOCK_ABOVE_FULL; + if (p.envelope_bps > bps) return E_ENVELOPE_ABOVE_FULL; + if (p.envelope_bps != 0 && p.envelope_window < MIN_EXECUTION_WINDOW_MS) { + return E_ENVELOPE_WINDOW_TOO_SHORT + }; + if (p.proposal_ttl < longest_delay(p) + MIN_EXECUTION_WINDOW_MS) { + return E_TTL_BELOW_LONGEST_DELAY + }; + E_OK +} + +public fun longest_delay(p: &Policy): u64 { + let mut longest = p.delay_critical; + if (p.policy_change_delay > longest) longest = p.policy_change_delay; + if (p.recovery_delay > longest) longest = p.recovery_delay; + if (p.recipient_activation_delay > longest) longest = p.recipient_activation_delay; + longest +} + +public fun assert_valid(p: &Policy) { + let code = validate(p); + assert!(code == E_OK, code); +} + +public fun validate_limits(l: &AssetLimits): u64 { + if (l.low_max > l.high_max) return E_LIMITS_LOW_ABOVE_HIGH; + if (l.per_tx_max != 0 && l.daily_max != 0 && l.per_tx_max > l.daily_max) { + return E_LIMITS_PER_TX_ABOVE_DAILY + }; + E_OK +} + +public fun assert_limits_valid(l: &AssetLimits) { + let code = validate_limits(l); + assert!(code == E_OK, code); +} + +/// A policy is only meaningful against a real roster: a threshold nobody can reach is a frozen +/// vault, so the counts are validated together with the policy on every change to either. +public fun validate_counts( + p: &Policy, + owners: u64, + approvers: u64, + executors: u64, + guardians: u64, +): u64 { + if (owners < 1) return E_NO_OWNER; + if (executors < 1) return E_NO_EXECUTOR; + if (owners < (p.governance_threshold as u64)) return E_GOVERNANCE_EXCEEDS_OWNERS; + if (approvers < (p.approvals_critical as u64)) return E_CRITICAL_EXCEEDS_APPROVERS; + if (guardians < (p.guardian_threshold as u64)) { + return E_GUARDIAN_THRESHOLD_EXCEEDS_GUARDIANS + }; + E_OK +} + +public fun assert_counts_valid( + p: &Policy, + owners: u64, + approvers: u64, + executors: u64, + guardians: u64, +) { + let code = validate_counts(p, owners, approvers, executors, guardians); + assert!(code == E_OK, code); +} + +// ---------------------------------------------------------------- what weakened +// One bit per field that can be loosened. The vault records the mask on the proposal, so a signer +// sees exactly which controls a policy change gives up, and a non-zero mask routes the change +// through the policy-change delay with a guardian veto. +const R_APPROVALS_LOW: u32 = 1; +const R_APPROVALS_HIGH: u32 = 1 << 1; +const R_APPROVALS_CRITICAL: u32 = 1 << 2; +const R_GOVERNANCE: u32 = 1 << 3; +const R_GUARDIAN_THRESHOLD: u32 = 1 << 4; +const R_GUARDIAN_NOT_REQUIRED: u32 = 1 << 5; +const R_DELAY_HIGH: u32 = 1 << 6; +const R_DELAY_CRITICAL: u32 = 1 << 7; +const R_ACTIVATION_DELAY: u32 = 1 << 8; +const R_POLICY_CHANGE_DELAY: u32 = 1 << 9; +const R_RECOVERY_DELAY: u32 = 1 << 10; +const R_PROPOSAL_TTL: u32 = 1 << 11; +const R_HIGH_EXPOSURE: u32 = 1 << 12; +const R_CRITICAL_EXPOSURE: u32 = 1 << 13; +const R_HARD_BLOCK: u32 = 1 << 14; +const R_ENVELOPE: u32 = 1 << 15; +const R_ENVELOPE_WINDOW: u32 = 1 << 16; + +const R_ASSET_NEWLY_APPROVED: u32 = 1; +const R_LOW_MAX: u32 = 1 << 1; +const R_HIGH_MAX: u32 = 1 << 2; +const R_PER_TX_CAP: u32 = 1 << 3; +const R_DAILY_CAP: u32 = 1 << 4; + +public fun r_approvals_low(): u32 { R_APPROVALS_LOW } +public fun r_guardian_not_required(): u32 { R_GUARDIAN_NOT_REQUIRED } +public fun r_envelope(): u32 { R_ENVELOPE } +public fun r_asset_newly_approved(): u32 { R_ASSET_NEWLY_APPROVED } +public fun r_daily_cap(): u32 { R_DAILY_CAP } + +/// For a threshold where zero means "disabled", moving to zero is the loosest possible move and +/// moving away from zero can only tighten. +fun threshold_loosens(a: u64, b: u64): bool { + if (a == 0) return false; + if (b == 0) return true; + b > a +} + +/// Which controls `b` gives up relative to `a`. Zero means the change only tightens. +public fun reductions(a: &Policy, b: &Policy): u32 { + let mut m: u32 = 0; + if (b.approvals_low < a.approvals_low) m = m | R_APPROVALS_LOW; + if (b.approvals_high < a.approvals_high) m = m | R_APPROVALS_HIGH; + if (b.approvals_critical < a.approvals_critical) m = m | R_APPROVALS_CRITICAL; + if (b.governance_threshold < a.governance_threshold) m = m | R_GOVERNANCE; + if (b.guardian_threshold < a.guardian_threshold) m = m | R_GUARDIAN_THRESHOLD; + if (a.guardian_required_critical && !b.guardian_required_critical) { + m = m | R_GUARDIAN_NOT_REQUIRED + }; + if (b.delay_high < a.delay_high) m = m | R_DELAY_HIGH; + if (b.delay_critical < a.delay_critical) m = m | R_DELAY_CRITICAL; + if (b.recipient_activation_delay < a.recipient_activation_delay) m = m | R_ACTIVATION_DELAY; + if (b.policy_change_delay < a.policy_change_delay) m = m | R_POLICY_CHANGE_DELAY; + if (b.recovery_delay < a.recovery_delay) m = m | R_RECOVERY_DELAY; + // A longer lifetime keeps a stale approval set alive for longer, so it loosens. + if (b.proposal_ttl > a.proposal_ttl) m = m | R_PROPOSAL_TTL; + if (threshold_loosens(a.high_exposure_bps, b.high_exposure_bps)) m = m | R_HIGH_EXPOSURE; + if (threshold_loosens(a.critical_exposure_bps, b.critical_exposure_bps)) { + m = m | R_CRITICAL_EXPOSURE + }; + if (threshold_loosens(a.hard_block_exposure_bps, b.hard_block_exposure_bps)) { + m = m | R_HARD_BLOCK + }; + if (threshold_loosens(a.envelope_bps, b.envelope_bps)) m = m | R_ENVELOPE; + if (a.envelope_bps != 0 && b.envelope_window < a.envelope_window) m = m | R_ENVELOPE_WINDOW; + m +} + +public fun limit_reductions(a: &AssetLimits, b: &AssetLimits): u32 { + let mut m: u32 = 0; + if (!a.approved && b.approved) m = m | R_ASSET_NEWLY_APPROVED; + if (b.low_max > a.low_max) m = m | R_LOW_MAX; + if (b.high_max > a.high_max) m = m | R_HIGH_MAX; + if (threshold_loosens(a.per_tx_max, b.per_tx_max)) m = m | R_PER_TX_CAP; + if (threshold_loosens(a.daily_max, b.daily_max)) m = m | R_DAILY_CAP; + m +} + +/// Raising a recipient's trust, or lifting a restriction, is a security-reducing move; lowering it +/// is always allowed to take effect at once. +public fun trust_reduces(from: u8, to: u8): bool { + let restricted = types::trust_restricted(); + let blocked = types::trust_blocked(); + let from_held_back = from == restricted || from == blocked || from == types::trust_unknown(); + if (from_held_back && to != restricted && to != blocked) return true; + if (to == blocked || to == restricted) return false; + to > from +} diff --git a/sui/sources/risk.move b/sui/sources/risk.move new file mode 100644 index 0000000..d0bd760 --- /dev/null +++ b/sui/sources/risk.move @@ -0,0 +1,168 @@ +/// The risk engine. Given a payment and the state of the vault around it, decide what tier it is, +/// why, and therefore how much authorization it needs. +/// +/// This is deterministic and pure: the same function the interface dry-runs to preview a transfer +/// is the function the vault calls when it executes one, so a preview can never promise something +/// the vault would refuse. +module skur::risk; + +use skur::policy::Policy; +use skur::types; + +/// Everything the classifier reads. Collected into one struct so the vault, the simulator and the +/// tests all pass the same shape. +public struct RiskInput has copy, drop { + amount: u64, + /// The asset's balance in the vault *before* this transfer. + asset_balance: u64, + low_max: u64, + high_max: u64, + daily_max: u64, + /// Already executed out of this asset in the live 24-hour bucket. + day_spent: u64, + trust: u8, + /// The recipient's activation delay has not elapsed yet. + in_probation: bool, + mode: u8, + high_exposure_bps: u64, + critical_exposure_bps: u64, +} + +public fun new_input( + amount: u64, + asset_balance: u64, + low_max: u64, + high_max: u64, + daily_max: u64, + day_spent: u64, + trust: u8, + in_probation: bool, + mode: u8, + high_exposure_bps: u64, + critical_exposure_bps: u64, +): RiskInput { + RiskInput { + amount, + asset_balance, + low_max, + high_max, + daily_max, + day_spent, + trust, + in_probation, + mode, + high_exposure_bps, + critical_exposure_bps, + } +} + +public struct RiskOutput has copy, drop { + tier: u8, + /// Bitmask of `types::reason_*`: every reason the tier landed where it did. + reasons: u16, + exposure_bps: u64, +} + +public fun tier(o: &RiskOutput): u8 { o.tier } +public fun reasons(o: &RiskOutput): u16 { o.reasons } +public fun exposure(o: &RiskOutput): u64 { o.exposure_bps } + +/// A transfer's share of the asset's holdings, in basis points. Taking everything, or taking +/// anything from an empty balance, is full exposure. +public fun exposure_bps(amount: u64, balance: u64): u64 { + let bps = types::bps(); + if (amount == 0) return 0; + if (balance == 0 || amount >= balance) return bps; + (((amount as u128) * (bps as u128)) / (balance as u128)) as u64 +} + +/// One step up the ladder. Already critical stays critical. +fun bump(t: u8): u8 { + if (t == types::tier_low()) types::tier_high() else types::tier_critical() +} + +fun at_least(t: u8, floor: u8): u8 { + if (t >= floor) t else floor +} + +/// Five independent signals, each able to raise the tier and none able to lower it. +public fun classify(i: &RiskInput): RiskOutput { + let low = types::tier_low(); + let high = types::tier_high(); + let critical = types::tier_critical(); + + let mut tier = low; + let mut reasons: u16 = 0; + + // 1. Absolute size, against this asset's own limits. + if (i.amount > i.high_max) { + tier = critical; + reasons = reasons | types::reason_amount_critical(); + } else if (i.amount > i.low_max) { + tier = high; + reasons = reasons | types::reason_amount_high(); + }; + + // 2. Share of the treasury. A small absolute amount out of a small balance still hurts. + let exposure = exposure_bps(i.amount, i.asset_balance); + if (i.critical_exposure_bps != 0 && exposure >= i.critical_exposure_bps) { + tier = critical; + reasons = reasons | types::reason_exposure_critical(); + } else if (i.high_exposure_bps != 0 && exposure >= i.high_exposure_bps) { + tier = at_least(tier, high); + reasons = reasons | types::reason_exposure_high(); + }; + + // 3. Who is receiving it. An address the vault has never paid is the signature of a drain. + if (i.trust == types::trust_restricted()) { + tier = critical; + reasons = reasons | types::reason_recipient_restricted(); + } else if (i.trust == types::trust_unknown()) { + tier = bump(tier); + reasons = reasons | types::reason_recipient_unknown() | types::reason_recipient_probation(); + } else if (i.trust == types::trust_new() && i.in_probation) { + tier = bump(tier); + reasons = reasons | types::reason_recipient_probation(); + }; + + // 4. Velocity. Crossing half the day's allowance is enough to escalate, so a drain split into + // pieces runs into a higher bar partway through instead of at the very end. + if (i.daily_max != 0 + && ((i.day_spent as u128) + (i.amount as u128)) * 2 > (i.daily_max as u128)) { + tier = at_least(tier, high); + reasons = reasons | types::reason_velocity_pressure(); + }; + + // 5. The vault's own posture. Elevated means something already looked wrong. + if (i.mode == types::mode_elevated()) { + tier = bump(tier); + reasons = reasons | types::reason_mode_elevated(); + }; + + RiskOutput { tier, reasons, exposure_bps: exposure } +} + +/// What a tier costs: approvals, guardian confirmations, and the wait before execution. +public fun requirements(p: &Policy, tier: u8): (u8, u8, u64) { + if (tier == types::tier_low()) { + (p.approvals_low(), 0, 0) + } else if (tier == types::tier_high()) { + (p.approvals_high(), 0, p.delay_high()) + } else { + let guardians = if (p.guardian_required_critical()) p.guardian_threshold() else 0; + (p.approvals_critical(), guardians, p.delay_critical()) + } +} + +/// Elevated halves the caps, never below one unit, so a vault that has already seen something +/// wrong keeps paying salaries but cannot be emptied at the usual rate. +public fun effective_caps(per_tx_max: u64, daily_max: u64, mode: u8): (u64, u64) { + if (mode != types::mode_elevated()) return (per_tx_max, daily_max); + (half(per_tx_max), half(daily_max)) +} + +fun half(x: u64): u64 { + if (x == 0) return 0; + let h = x / 2; + if (h == 0) 1 else h +} diff --git a/sui/sources/types.move b/sui/sources/types.move new file mode 100644 index 0000000..b38a692 --- /dev/null +++ b/sui/sources/types.move @@ -0,0 +1,147 @@ +/// Shared vocabulary for the Skur authorization layer: roles, risk tiers, recipient trust, +/// security modes, proposal kinds and the reasons a payment was escalated. +/// +/// These are the same values the TypeScript engine and the interfaces use, so a tier shown to a +/// signer is the tier the chain computed. +module skur::types; + +// ---------------------------------------------------------------- roles (a bitmask per member) +const ROLE_OWNER: u8 = 1; +const ROLE_APPROVER: u8 = 2; +const ROLE_EXECUTOR: u8 = 4; +const ROLE_GUARDIAN: u8 = 8; +/// A proposer may open proposals and nothing else. It is the role an automated agent holds: it +/// can ask the treasury to pay, it cannot approve, confirm or execute, and so it can never move +/// money on its own however thoroughly it is compromised. +const ROLE_PROPOSER: u8 = 16; +const ROLE_TREASURY_MASK: u8 = 23; // owner | approver | executor | proposer +/// Any role that is allowed to open a proposal. +const ROLE_CAN_PROPOSE: u8 = 23; +const ROLE_ALL: u8 = 31; + +public fun role_owner(): u8 { ROLE_OWNER } +public fun role_approver(): u8 { ROLE_APPROVER } +public fun role_executor(): u8 { ROLE_EXECUTOR } +public fun role_guardian(): u8 { ROLE_GUARDIAN } +public fun role_proposer(): u8 { ROLE_PROPOSER } +public fun role_treasury_mask(): u8 { ROLE_TREASURY_MASK } + +public fun has_role(bits: u8, role: u8): bool { bits & role == role } + +/// True when the member holds at least one of the roles in `mask`. +public fun has_any(bits: u8, mask: u8): bool { bits & mask != 0 } + +public fun can_propose(bits: u8): bool { has_any(bits, ROLE_CAN_PROPOSE) } + +/// A guardian is the second control plane: it may hold no treasury role, and no treasury member +/// may hold it. That is what stops one stolen credential from both proposing and vetoing. +public fun roles_valid(bits: u8): bool { + if (bits == 0) return false; + if (bits > ROLE_ALL) return false; + if (has_role(bits, ROLE_GUARDIAN) && (bits & ROLE_TREASURY_MASK) != 0) return false; + true +} + +// ---------------------------------------------------------------- risk tiers +const TIER_LOW: u8 = 0; +const TIER_HIGH: u8 = 1; +const TIER_CRITICAL: u8 = 2; + +public fun tier_low(): u8 { TIER_LOW } +public fun tier_high(): u8 { TIER_HIGH } +public fun tier_critical(): u8 { TIER_CRITICAL } + +// ---------------------------------------------------------------- recipient trust +const TRUST_UNKNOWN: u8 = 0; +const TRUST_NEW: u8 = 1; +const TRUST_VERIFIED: u8 = 2; +const TRUST_TRUSTED: u8 = 3; +const TRUST_RESTRICTED: u8 = 4; +const TRUST_BLOCKED: u8 = 5; + +public fun trust_unknown(): u8 { TRUST_UNKNOWN } +public fun trust_new(): u8 { TRUST_NEW } +public fun trust_verified(): u8 { TRUST_VERIFIED } +public fun trust_trusted(): u8 { TRUST_TRUSTED } +public fun trust_restricted(): u8 { TRUST_RESTRICTED } +public fun trust_blocked(): u8 { TRUST_BLOCKED } +public fun trust_valid(t: u8): bool { t <= TRUST_BLOCKED } + +// ---------------------------------------------------------------- security modes +const MODE_NORMAL: u8 = 0; +const MODE_ELEVATED: u8 = 1; +const MODE_LOCKDOWN: u8 = 2; + +public fun mode_normal(): u8 { MODE_NORMAL } +public fun mode_elevated(): u8 { MODE_ELEVATED } +public fun mode_lockdown(): u8 { MODE_LOCKDOWN } + +// ---------------------------------------------------------------- proposal kinds +const KIND_TRANSFER: u8 = 0; +const KIND_POLICY_UPDATE: u8 = 1; +const KIND_ASSET_LIMITS: u8 = 2; +const KIND_MEMBER_SET: u8 = 3; +const KIND_RECIPIENT_TRUST: u8 = 4; +const KIND_MODE_RELAX: u8 = 5; +const KIND_RECOVERY: u8 = 6; + +public fun kind_transfer(): u8 { KIND_TRANSFER } +public fun kind_policy_update(): u8 { KIND_POLICY_UPDATE } +public fun kind_asset_limits(): u8 { KIND_ASSET_LIMITS } +public fun kind_member_set(): u8 { KIND_MEMBER_SET } +public fun kind_recipient_trust(): u8 { KIND_RECIPIENT_TRUST } +public fun kind_mode_relax(): u8 { KIND_MODE_RELAX } +public fun kind_recovery(): u8 { KIND_RECOVERY } + +// ---------------------------------------------------------------- proposal status +const STATUS_PENDING: u8 = 1; +const STATUS_EXECUTED: u8 = 2; +const STATUS_CANCELLED: u8 = 3; +const STATUS_VETOED: u8 = 4; +const STATUS_EXPIRED: u8 = 5; +/// Refused at execution by a control that must survive the attempt: the circuit breaker. The +/// transaction still succeeds, because the latch it sets has to persist. +const STATUS_BLOCKED: u8 = 6; +/// Turned down by the signers. It takes as many rejections as the proposal needed approvals, so +/// rejecting is exactly as hard as approving and a single signer cannot block the queue. +const STATUS_REJECTED: u8 = 7; + +public fun status_pending(): u8 { STATUS_PENDING } +public fun status_executed(): u8 { STATUS_EXECUTED } +public fun status_cancelled(): u8 { STATUS_CANCELLED } +public fun status_vetoed(): u8 { STATUS_VETOED } +public fun status_expired(): u8 { STATUS_EXPIRED } +public fun status_blocked(): u8 { STATUS_BLOCKED } +public fun status_rejected(): u8 { STATUS_REJECTED } + +// ---------------------------------------------------------------- why a payment was escalated +const REASON_AMOUNT_HIGH: u16 = 1; +const REASON_AMOUNT_CRITICAL: u16 = 2; +const REASON_EXPOSURE_HIGH: u16 = 4; +const REASON_EXPOSURE_CRITICAL: u16 = 8; +const REASON_RECIPIENT_PROBATION: u16 = 16; +const REASON_RECIPIENT_RESTRICTED: u16 = 32; +const REASON_MODE_ELEVATED: u16 = 64; +const REASON_VELOCITY_PRESSURE: u16 = 128; +const REASON_RECIPIENT_UNKNOWN: u16 = 256; +const REASON_ENVELOPE_PRESSURE: u16 = 512; + +public fun reason_amount_high(): u16 { REASON_AMOUNT_HIGH } +public fun reason_amount_critical(): u16 { REASON_AMOUNT_CRITICAL } +public fun reason_exposure_high(): u16 { REASON_EXPOSURE_HIGH } +public fun reason_exposure_critical(): u16 { REASON_EXPOSURE_CRITICAL } +public fun reason_recipient_probation(): u16 { REASON_RECIPIENT_PROBATION } +public fun reason_recipient_restricted(): u16 { REASON_RECIPIENT_RESTRICTED } +public fun reason_mode_elevated(): u16 { REASON_MODE_ELEVATED } +public fun reason_velocity_pressure(): u16 { REASON_VELOCITY_PRESSURE } +public fun reason_recipient_unknown(): u16 { REASON_RECIPIENT_UNKNOWN } +public fun reason_envelope_pressure(): u16 { REASON_ENVELOPE_PRESSURE } + +/// Basis points denominator, and one hour in milliseconds (Sui clocks are millisecond-based). +const BPS: u64 = 10_000; +const HOUR_MS: u64 = 3_600_000; +const DAY_MS: u64 = 86_400_000; + +public fun bps(): u64 { BPS } +public fun hour_ms(): u64 { HOUR_MS } +public fun day_ms(): u64 { DAY_MS } diff --git a/sui/sources/vault.move b/sui/sources/vault.move new file mode 100644 index 0000000..7ce5f20 --- /dev/null +++ b/sui/sources/vault.move @@ -0,0 +1,1713 @@ +/// The Skur vault: a shared object that holds a treasury and will only let it move when the +/// policy, the recipient's standing, the day's outflow, the loss envelope and the vault's own +/// security posture all agree. +/// +/// A plain multisig answers one question — did k of n sign? A Skur vault answers a harder one: +/// given everything that has happened recently, *should* this payment go out right now, and if so +/// how much authorization does it need? Every control below is enforced here, in Move, not in an +/// interface or a backend. +module skur::vault; + +use std::string::{Self, String}; +use std::type_name::{Self, TypeName}; +use sui::bag::{Self, Bag}; +use sui::balance::{Self, Balance}; +use sui::clock::{Self, Clock}; +use sui::coin::{Self, Coin}; +use sui::event; +use sui::table::{Self, Table}; +use sui::vec_set::{Self, VecSet}; +use skur::policy::{Self, Policy, AssetLimits}; +use skur::risk; +use skur::types; + +// ---------------------------------------------------------------- errors +const E_NOT_MEMBER: u64 = 100; +const E_NOT_OWNER: u64 = 101; +const E_NOT_APPROVER: u64 = 102; +const E_NOT_EXECUTOR: u64 = 103; +const E_NOT_GUARDIAN: u64 = 104; +const E_CANNOT_PROPOSE: u64 = 105; +const E_INVALID_ROLES: u64 = 106; +const E_LOCKDOWN: u64 = 107; +const E_ASSET_NOT_APPROVED: u64 = 108; +const E_UNKNOWN_PROPOSAL: u64 = 109; +const E_NOT_PENDING: u64 = 110; +const E_WRONG_KIND: u64 = 111; +const E_WRONG_ASSET: u64 = 112; +const E_TOO_EARLY: u64 = 113; +const E_EXPIRED: u64 = 114; +const E_NOT_EXPIRED: u64 = 115; +const E_NOT_ENOUGH_APPROVALS: u64 = 116; +const E_NOT_ENOUGH_GUARDIANS: u64 = 117; +const E_RECIPIENT_BLOCKED: u64 = 118; +const E_RECIPIENT_IN_PROBATION: u64 = 119; +const E_PER_TX_CAP: u64 = 120; +const E_DAILY_CAP: u64 = 121; +const E_HARD_BLOCK: u64 = 122; +const E_INSUFFICIENT_BALANCE: u64 = 123; +const E_ZERO_AMOUNT: u64 = 124; +const E_NOT_VETOABLE: u64 = 125; +const E_ALREADY_REGISTERED: u64 = 126; +const E_INVALID_TRUST: u64 = 128; +const E_INVALID_MODE: u64 = 129; +const E_NOT_A_RELAXATION: u64 = 130; +const E_NOT_PROPOSER_OR_OWNER: u64 = 131; +const E_GUARDIAN_HAS_TREASURY_ROLE: u64 = 133; +const E_ASSET_ALREADY_SET_UP: u64 = 134; +const E_ROSTER_MISMATCH: u64 = 135; +const E_SELF_RECOVERY: u64 = 136; +const E_NO_SUCH_MEMBER: u64 = 137; + +// ---------------------------------------------------------------- state +/// Cumulative outflow, counted per asset in anchored windows. +/// +/// The envelope is measured against `envelope_basis`, the balance when the window opened, not the +/// live balance. Measuring against the live balance would let a drain shrink its own denominator +/// and keep every later transfer under the same percentage. +public struct Velocity has copy, drop, store { + day_anchor: u64, + day_spent: u64, + envelope_anchor: u64, + envelope_spent: u64, + envelope_basis: u64, +} + +/// A payment destination and how much the vault trusts it. +public struct Recipient has copy, drop, store { + trust: u8, + registered_at: u64, + /// Until this moment the recipient is in probation and every payment is escalated. + activates_at: u64, + paid_count: u64, + last_paid: u64, + label: String, +} + +/// One pending or settled request. Every kind shares the same lifecycle — propose, approve, +/// optionally confirm, wait, execute — so there is exactly one path through which anything +/// changes, and exactly one place where the final checks run. +public struct Proposal has store { + id: u64, + kind: u8, + status: u8, + proposer: address, + created_at: u64, + /// Not executable before this moment: the tier's timelock, and for a new recipient, their + /// activation time, whichever is later. + executable_at: u64, + expires_at: u64, + /// Requirements pinned when the proposal was opened. At execution the live requirements are + /// recomputed and the stricter of the two applies, so tightening the policy affects proposals + /// already in flight while loosening it cannot rescue one. + req_approvals: u8, + req_guardians: u8, + tier: u8, + reasons: u16, + exposure_bps: u64, + policy_version: u64, + /// Which controls this proposal would give up. Non-zero routes it through the policy-change + /// delay and makes it vetoable. + reduction_mask: u32, + approvals: VecSet
, + confirmations: VecSet
, + /// Signers who have turned it down. Settled once this reaches `req_approvals`. + rejections: VecSet
, + // transfer payload + asset: Option, + amount: u64, + recipient: address, + memo: String, + // governance payloads + new_policy: Option, + new_limits: Option, + member: address, + member_prev: address, + member_roles: u8, + trust_level: u8, + target_mode: u8, +} + +public struct Vault has key { + id: UID, + name: String, + policy: Policy, + /// Bumped on every policy change, so a proposal can tell whether the rules moved under it. + policy_version: u64, + mode: u8, + /// Why the vault last escalated its posture; a bitmask of `types::reason_*`. + posture_reasons: u16, + members: Table, + owner_count: u64, + approver_count: u64, + executor_count: u64, + guardian_count: u64, + limits: Table, + /// `TypeName` -> `Balance`. One vault holds every asset the organization approved. + funds: Bag, + velocity: Table, + recipients: Table, + proposals: Table, + next_proposal: u64, + pending_count: u64, + created_at: u64, +} + +// ---------------------------------------------------------------- events +public struct VaultCreated has copy, drop { + vault: ID, + name: String, + creator: address, + owner_count: u64, + guardian_count: u64, + at: u64, +} + +public struct Deposited has copy, drop { + vault: ID, + asset: TypeName, + amount: u64, + balance: u64, + from: address, + at: u64, +} + +public struct ProposalOpened has copy, drop { + vault: ID, + proposal: u64, + kind: u8, + proposer: address, + asset: Option, + amount: u64, + recipient: address, + tier: u8, + reasons: u16, + exposure_bps: u64, + req_approvals: u8, + req_guardians: u8, + executable_at: u64, + expires_at: u64, + reduction_mask: u32, + memo: String, + at: u64, +} + +public struct Approved has copy, drop { + vault: ID, + proposal: u64, + approver: address, + approvals: u64, + req_approvals: u8, + at: u64, +} + +public struct Rejected has copy, drop { + vault: ID, + proposal: u64, + rejecter: address, + rejections: u64, + req_approvals: u8, + settled: bool, + at: u64, +} + +public struct Confirmed has copy, drop { + vault: ID, + proposal: u64, + guardian: address, + confirmations: u64, + req_guardians: u8, + at: u64, +} + +public struct Settled has copy, drop { + vault: ID, + proposal: u64, + kind: u8, + status: u8, + actor: address, + at: u64, +} + +public struct Executed has copy, drop { + vault: ID, + proposal: u64, + kind: u8, + asset: Option, + amount: u64, + recipient: address, + executor: address, + tier: u8, + balance_after: u64, + day_spent: u64, + at: u64, +} + +/// The breaker refused a transfer that was otherwise fully authorized. +public struct BreakerTripped has copy, drop { + vault: ID, + proposal: u64, + asset: TypeName, + amount: u64, + envelope_spent: u64, + envelope_limit: u64, + envelope_basis: u64, + at: u64, +} + +public struct ModeChanged has copy, drop { + vault: ID, + from: u8, + to: u8, + actor: address, + reasons: u16, + at: u64, +} + +public struct PolicyChanged has copy, drop { + vault: ID, + proposal: u64, + version: u64, + reduction_mask: u32, + at: u64, +} + +public struct LimitsChanged has copy, drop { + vault: ID, + proposal: u64, + asset: TypeName, + approved: bool, + low_max: u64, + high_max: u64, + per_tx_max: u64, + daily_max: u64, + reduction_mask: u32, + at: u64, +} + +public struct MemberChanged has copy, drop { + vault: ID, + proposal: u64, + member: address, + roles_before: u8, + roles_after: u8, + at: u64, +} + +public struct RecipientRegistered has copy, drop { + vault: ID, + recipient: address, + trust: u8, + activates_at: u64, + label: String, + actor: address, + at: u64, +} + +public struct RecipientTrustChanged has copy, drop { + vault: ID, + proposal: u64, + recipient: address, + from: u8, + to: u8, + actor: address, + at: u64, +} + +public struct Recovered has copy, drop { + vault: ID, + proposal: u64, + lost: address, + replacement: address, + roles: u8, + at: u64, +} + +// ---------------------------------------------------------------- creation +/// A vault under construction. It has no abilities, so the transaction that opened it must also +/// finish it: `begin`, then any number of `setup_asset` / `setup_recipient` calls, then `finish`, +/// all in one programmable transaction block. There is no window in which a half-configured +/// vault is reachable by anyone. +public struct VaultSetup { + vault: Vault, +} + +public fun begin( + name: String, + policy: Policy, + members: vector
, + roles: vector, + clock: &Clock, + ctx: &mut TxContext, +): VaultSetup { + assert!(members.length() == roles.length(), E_ROSTER_MISMATCH); + policy.assert_valid(); + let now = clock::timestamp_ms(clock); + + let mut vault = Vault { + id: object::new(ctx), + name, + policy, + policy_version: 1, + mode: types::mode_normal(), + posture_reasons: 0, + members: table::new(ctx), + owner_count: 0, + approver_count: 0, + executor_count: 0, + guardian_count: 0, + limits: table::new(ctx), + funds: bag::new(ctx), + velocity: table::new(ctx), + recipients: table::new(ctx), + proposals: table::new(ctx), + next_proposal: 1, + pending_count: 0, + created_at: now, + }; + + let vault_id = object::id(&vault); + let mut i = 0; + while (i < members.length()) { + let who = members[i]; + let bits = roles[i]; + assert!(types::roles_valid(bits), E_INVALID_ROLES); + assert!(!vault.members.contains(who), E_ALREADY_REGISTERED); + vault.members.add(who, bits); + tally(&mut vault, bits, true); + // The founding roster is announced member by member, like every later change. + // + // Without this the roster is the one piece of vault state that cannot be rebuilt from the + // event log: `VaultCreated` carries counts, not addresses, and a Move table cannot be + // iterated, so nothing downstream could ever learn who the original signers were. + event::emit(MemberChanged { + vault: vault_id, + proposal: 0, + member: who, + roles_before: 0, + roles_after: bits, + at: now, + }); + i = i + 1; + }; + + VaultSetup { vault } +} + +/// Approve an asset the vault may hold and pay out, with its tier boundaries and caps. +public fun setup_asset( + s: &mut VaultSetup, + approved: bool, + low_max: u64, + high_max: u64, + per_tx_max: u64, + daily_max: u64, +) { + let key = type_name::with_defining_ids(); + assert!(!s.vault.limits.contains(key), E_ASSET_ALREADY_SET_UP); + let limits = policy::new_limits(approved, low_max, high_max, per_tx_max, daily_max); + limits.assert_limits_valid(); + let created_at = s.vault.created_at; + s.vault.limits.add(key, limits); + s.vault.velocity.add(key, Velocity { + day_anchor: created_at, + day_spent: 0, + envelope_anchor: created_at, + envelope_spent: 0, + envelope_basis: 0, + }); +} + +/// Seed the address book. Recipients added at setup still serve their activation delay, so a +/// vault cannot be created with a pre-trusted drain address that is immediately cheap to pay. +public fun setup_recipient(s: &mut VaultSetup, who: address, label: String) { + assert!(!s.vault.recipients.contains(who), E_ALREADY_REGISTERED); + let registered_at = s.vault.created_at; + let activates_at = registered_at + s.vault.policy.recipient_activation_delay(); + s.vault.recipients.add(who, Recipient { + trust: types::trust_new(), + registered_at, + activates_at, + paid_count: 0, + last_paid: 0, + label, + }); +} + +/// Validate the finished roster against the policy and publish the vault as a shared object. +public fun finish(s: VaultSetup, ctx: &TxContext) { + let VaultSetup { vault } = s; + vault.policy.assert_counts_valid( + vault.owner_count, + vault.approver_count, + vault.executor_count, + vault.guardian_count, + ); + event::emit(VaultCreated { + vault: object::id(&vault), + name: vault.name, + creator: ctx.sender(), + owner_count: vault.owner_count, + guardian_count: vault.guardian_count, + at: vault.created_at, + }); + transfer::share_object(vault); +} + +// ---------------------------------------------------------------- roster bookkeeping +fun tally(v: &mut Vault, bits: u8, add: bool) { + if (types::has_role(bits, types::role_owner())) { + v.owner_count = if (add) v.owner_count + 1 else v.owner_count - 1; + }; + if (types::has_role(bits, types::role_approver())) { + v.approver_count = if (add) v.approver_count + 1 else v.approver_count - 1; + }; + if (types::has_role(bits, types::role_executor())) { + v.executor_count = if (add) v.executor_count + 1 else v.executor_count - 1; + }; + if (types::has_role(bits, types::role_guardian())) { + v.guardian_count = if (add) v.guardian_count + 1 else v.guardian_count - 1; + }; +} + +fun roles_of(v: &Vault, who: address): u8 { + if (v.members.contains(who)) *v.members.borrow(who) else 0 +} + +fun assert_role(v: &Vault, who: address, role: u8, code: u64) { + assert!(types::has_role(roles_of(v, who), role), code); +} + +fun assert_not_lockdown(v: &Vault) { + assert!(v.mode != types::mode_lockdown(), E_LOCKDOWN); +} + +// ---------------------------------------------------------------- read access +public fun name(v: &Vault): String { v.name } +public fun policy(v: &Vault): &Policy { &v.policy } +public fun policy_version(v: &Vault): u64 { v.policy_version } +public fun mode(v: &Vault): u8 { v.mode } +public fun posture_reasons(v: &Vault): u16 { v.posture_reasons } +public fun owner_count(v: &Vault): u64 { v.owner_count } +public fun approver_count(v: &Vault): u64 { v.approver_count } +public fun executor_count(v: &Vault): u64 { v.executor_count } +public fun guardian_count(v: &Vault): u64 { v.guardian_count } +public fun pending_count(v: &Vault): u64 { v.pending_count } +public fun proposal_count(v: &Vault): u64 { v.next_proposal - 1 } +public fun member_roles(v: &Vault, who: address): u8 { roles_of(v, who) } + +public fun balance_of(v: &Vault): u64 { + let key = type_name::with_defining_ids(); + if (!bag::contains(&v.funds, key)) return 0; + balance::value(bag::borrow>(&v.funds, key)) +} + +public fun limits_of(v: &Vault): AssetLimits { + let key = type_name::with_defining_ids(); + if (!v.limits.contains(key)) { + policy::new_limits(false, 0, 0, 0, 0) + } else { + *v.limits.borrow(key) + } +} + +public fun recipient_trust(v: &Vault, who: address): u8 { + if (!v.recipients.contains(who)) return types::trust_unknown(); + v.recipients.borrow(who).trust +} + +public fun recipient_activates_at(v: &Vault, who: address): u64 { + if (!v.recipients.contains(who)) return 0; + v.recipients.borrow(who).activates_at +} + +/// Day spent and envelope spent for an asset, after rolling any window that has already closed. +public fun outflow_of(v: &Vault, clock: &Clock): (u64, u64) { + let key = type_name::with_defining_ids(); + if (!v.velocity.contains(key)) return (0, 0); + let vel = *v.velocity.borrow(key); + let now = clock::timestamp_ms(clock); + let day = if (now >= vel.day_anchor + types::day_ms()) 0 else vel.day_spent; + let window = v.policy.envelope_window(); + let env = if (window != 0 && now >= vel.envelope_anchor + window) 0 else vel.envelope_spent; + (day, env) +} + +public fun proposal_status(v: &Vault, id: u64): u8 { + if (!v.proposals.contains(id)) return 0; + v.proposals.borrow(id).status +} + +public fun proposal_approvals(v: &Vault, id: u64): u64 { + assert!(v.proposals.contains(id), E_UNKNOWN_PROPOSAL); + v.proposals.borrow(id).approvals.length() +} + +public fun proposal_confirmations(v: &Vault, id: u64): u64 { + assert!(v.proposals.contains(id), E_UNKNOWN_PROPOSAL); + v.proposals.borrow(id).confirmations.length() +} + +public fun proposal_rejections(v: &Vault, id: u64): u64 { + assert!(v.proposals.contains(id), E_UNKNOWN_PROPOSAL); + v.proposals.borrow(id).rejections.length() +} + +/// Approvals that would still count right now: a removed signer's approval is not one. +public fun live_approvals(v: &Vault, id: u64): u64 { + assert!(v.proposals.contains(id), E_UNKNOWN_PROPOSAL); + count_live(v, &v.proposals.borrow(id).approvals, types::role_approver()) +} + +public fun live_confirmations(v: &Vault, id: u64): u64 { + assert!(v.proposals.contains(id), E_UNKNOWN_PROPOSAL); + count_live(v, &v.proposals.borrow(id).confirmations, types::role_guardian()) +} + +fun count_live(v: &Vault, set: &VecSet
, role: u8): u64 { + let keys = set.keys(); + let mut n = 0; + let mut i = 0; + while (i < keys.length()) { + if (types::has_role(roles_of(v, keys[i]), role)) n = n + 1; + i = i + 1; + }; + n +} + +// ---------------------------------------------------------------- deposits +/// Anyone may fund an approved asset. Deposits are never gated: the controls exist to govern money +/// leaving, and refusing incoming funds would only strand them. +public fun deposit(v: &mut Vault, c: Coin, clock: &Clock, ctx: &TxContext) { + let key = type_name::with_defining_ids(); + assert!(v.limits.contains(key) && v.limits.borrow(key).limits_approved(), E_ASSET_NOT_APPROVED); + let amount = c.value(); + assert!(amount > 0, E_ZERO_AMOUNT); + let incoming = c.into_balance(); + if (bag::contains(&v.funds, key)) { + balance::join(bag::borrow_mut>(&mut v.funds, key), incoming); + } else { + bag::add(&mut v.funds, key, incoming); + }; + event::emit(Deposited { + vault: object::id(v), + asset: key, + amount, + balance: balance_of(v), + from: ctx.sender(), + at: clock::timestamp_ms(clock), + }); +} + +// ---------------------------------------------------------------- recipients +/// Any owner may add a recipient at the restrictive default: trust `NEW`, serving an activation +/// delay. Raising trust beyond that is a governance action with a timelock. +public fun register_recipient( + v: &mut Vault, + who: address, + label: String, + clock: &Clock, + ctx: &TxContext, +) { + assert_not_lockdown(v); + assert_role(v, ctx.sender(), types::role_owner(), E_NOT_OWNER); + assert!(!v.recipients.contains(who), E_ALREADY_REGISTERED); + let now = clock::timestamp_ms(clock); + let activates_at = now + v.policy.recipient_activation_delay(); + v.recipients.add(who, Recipient { + trust: types::trust_new(), + registered_at: now, + activates_at, + paid_count: 0, + last_paid: 0, + label, + }); + event::emit(RecipientRegistered { + vault: object::id(v), + recipient: who, + trust: types::trust_new(), + activates_at, + label, + actor: ctx.sender(), + at: now, + }); +} + +/// A guardian may block or restrict a recipient immediately and alone. This is the one unilateral +/// power in the system and it can only ever stop money, never move it. +public fun guardian_restrict( + v: &mut Vault, + who: address, + to: u8, + clock: &Clock, + ctx: &TxContext, +) { + assert_role(v, ctx.sender(), types::role_guardian(), E_NOT_GUARDIAN); + assert!(to == types::trust_restricted() || to == types::trust_blocked(), E_INVALID_TRUST); + let now = clock::timestamp_ms(clock); + let from = if (v.recipients.contains(who)) { + let r = v.recipients.borrow_mut(who); + let before = r.trust; + r.trust = to; + before + } else { + v.recipients.add(who, Recipient { + trust: to, + registered_at: now, + activates_at: now, + paid_count: 0, + last_paid: 0, + label: string::utf8(b""), + }); + types::trust_unknown() + }; + event::emit(RecipientTrustChanged { + vault: object::id(v), + proposal: 0, + recipient: who, + from, + to, + actor: ctx.sender(), + at: now, + }); +} + +// ---------------------------------------------------------------- security modes +/// Any owner or guardian may raise the posture at once; a guardian may go straight to Lockdown. +/// Lowering it is never unilateral — see `propose_mode_relax`. +public fun raise_mode(v: &mut Vault, to: u8, reasons: u16, clock: &Clock, ctx: &TxContext) { + let bits = roles_of(v, ctx.sender()); + assert!(bits != 0, E_NOT_MEMBER); + assert!(to == types::mode_elevated() || to == types::mode_lockdown(), E_INVALID_MODE); + assert!(to > v.mode, E_NOT_A_RELAXATION); + if (to == types::mode_lockdown()) { + assert!( + types::has_role(bits, types::role_guardian()) + || types::has_role(bits, types::role_owner()), + E_NOT_GUARDIAN, + ); + } else { + assert!(types::has_any(bits, types::role_treasury_mask() | types::role_guardian()), E_NOT_MEMBER); + }; + set_mode(v, to, reasons, ctx.sender(), clock); +} + +fun set_mode(v: &mut Vault, to: u8, reasons: u16, actor: address, clock: &Clock) { + let from = v.mode; + if (from == to) return; + v.mode = to; + v.posture_reasons = if (to == types::mode_normal()) 0 else v.posture_reasons | reasons; + event::emit(ModeChanged { + vault: object::id(v), + from, + to, + actor, + reasons, + at: clock::timestamp_ms(clock), + }); +} + +const E_ALREADY_VOTED: u64 = 138; +const E_NOT_A_MEMBER_CHANGE: u64 = 139; + +// ---------------------------------------------------------------- proposal plumbing +fun count_with_role(v: &Vault, keys: &vector
, role: u8): u64 { + let mut n = 0; + let mut i = 0; + while (i < keys.length()) { + if (types::has_role(roles_of(v, keys[i]), role)) n = n + 1; + i = i + 1; + }; + n +} + +fun blank( + id: u64, + kind: u8, + proposer: address, + now: u64, + executable_at: u64, + expires_at: u64, + req_approvals: u8, + req_guardians: u8, + policy_version: u64, + reduction_mask: u32, +): Proposal { + Proposal { + id, + kind, + status: types::status_pending(), + proposer, + created_at: now, + executable_at, + expires_at, + req_approvals, + req_guardians, + tier: types::tier_low(), + reasons: 0, + exposure_bps: 0, + policy_version, + reduction_mask, + approvals: vec_set::empty(), + confirmations: vec_set::empty(), + rejections: vec_set::empty(), + asset: option::none(), + amount: 0, + recipient: @0x0, + memo: string::utf8(b""), + new_policy: option::none(), + new_limits: option::none(), + member: @0x0, + member_prev: @0x0, + member_roles: 0, + trust_level: 0, + target_mode: 0, + } +} + +fun larger(a: u8, b: u8): u8 { if (a > b) a else b } +fun later(a: u64, b: u64): u64 { if (a > b) a else b } + +/// Common checks for anyone acting on a live proposal. +fun assert_live(v: &Vault, id: u64, now: u64) { + assert!(v.proposals.contains(id), E_UNKNOWN_PROPOSAL); + let p = v.proposals.borrow(id); + assert!(p.status == types::status_pending(), E_NOT_PENDING); + assert!(now < p.expires_at, E_EXPIRED); +} + +fun settle(v: &mut Vault, id: u64, status: u8, actor: address, now: u64) { + let kind = { + let p = v.proposals.borrow_mut(id); + p.status = status; + p.kind + }; + v.pending_count = v.pending_count - 1; + event::emit(Settled { vault: object::id(v), proposal: id, kind, status, actor, at: now }); +} + +// ---------------------------------------------------------------- opening a transfer +/// Score a payment and open it for approval. Everything that can refuse the payment outright is +/// checked here *and* again at execution, because the state it reads can change in between. +public fun propose_transfer( + v: &mut Vault, + amount: u64, + recipient: address, + memo: String, + clock: &Clock, + ctx: &mut TxContext, +): u64 { + assert_not_lockdown(v); + let sender = ctx.sender(); + assert!(types::can_propose(roles_of(v, sender)), E_CANNOT_PROPOSE); + assert!(amount > 0, E_ZERO_AMOUNT); + + let key = type_name::with_defining_ids(); + assert!(v.limits.contains(key) && v.limits.borrow(key).limits_approved(), E_ASSET_NOT_APPROVED); + + let now = clock::timestamp_ms(clock); + let limits = limits_of(v); + let asset_balance = balance_of(v); + let trust = recipient_trust(v, recipient); + assert!(trust != types::trust_blocked(), E_RECIPIENT_BLOCKED); + + let (per_tx, daily) = risk::effective_caps( + limits.limits_per_tx_max(), + limits.limits_daily_max(), + v.mode, + ); + assert!(per_tx == 0 || amount <= per_tx, E_PER_TX_CAP); + let (day_spent, _) = outflow_of(v, clock); + assert!(daily == 0 || day_spent + amount <= daily, E_DAILY_CAP); + + let hard_block = v.policy.hard_block_exposure_bps(); + let exposure = risk::exposure_bps(amount, asset_balance); + assert!(hard_block == 0 || exposure < hard_block, E_HARD_BLOCK); + + let activates_at = recipient_activates_at(v, recipient); + let input = risk::new_input( + amount, + asset_balance, + limits.limits_low_max(), + limits.limits_high_max(), + daily, + day_spent, + trust, + now < activates_at, + v.mode, + v.policy.high_exposure_bps(), + v.policy.critical_exposure_bps(), + ); + let assessment = risk::classify(&input); + let (req_approvals, req_guardians, delay) = risk::requirements(&v.policy, assessment.tier()); + + // A new recipient's activation delay is a floor on execution, not a separate wait: the two + // run concurrently and the later one governs. + let executable_at = later(now + delay, activates_at); + let expires_at = now + v.policy.proposal_ttl(); + + let id = v.next_proposal; + v.next_proposal = id + 1; + let mut p = blank( + id, + types::kind_transfer(), + sender, + now, + executable_at, + expires_at, + req_approvals, + req_guardians, + v.policy_version, + 0, + ); + p.asset = option::some(key); + p.amount = amount; + p.recipient = recipient; + p.memo = memo; + p.tier = assessment.tier(); + p.reasons = assessment.reasons(); + p.exposure_bps = assessment.exposure(); + v.proposals.add(id, p); + v.pending_count = v.pending_count + 1; + + event::emit(ProposalOpened { + vault: object::id(v), + proposal: id, + kind: types::kind_transfer(), + proposer: sender, + asset: option::some(key), + amount, + recipient, + tier: assessment.tier(), + reasons: assessment.reasons(), + exposure_bps: assessment.exposure(), + req_approvals, + req_guardians, + executable_at, + expires_at, + reduction_mask: 0, + memo, + at: now, + }); + id +} + +/// What a transfer would cost before anyone commits to it. Pure, and identical to the logic +/// `propose_transfer` and `execute_transfer` run, so a preview cannot overpromise. +public fun preview_transfer( + v: &Vault, + amount: u64, + recipient: address, + clock: &Clock, +): (u8, u16, u64, u8, u8, u64) { + let limits = limits_of(v); + let (day_spent, _) = outflow_of(v, clock); + let now = clock::timestamp_ms(clock); + let (_, daily) = risk::effective_caps( + limits.limits_per_tx_max(), + limits.limits_daily_max(), + v.mode, + ); + let input = risk::new_input( + amount, + balance_of(v), + limits.limits_low_max(), + limits.limits_high_max(), + daily, + day_spent, + recipient_trust(v, recipient), + now < recipient_activates_at(v, recipient), + v.mode, + v.policy.high_exposure_bps(), + v.policy.critical_exposure_bps(), + ); + let a = risk::classify(&input); + let (req_approvals, req_guardians, delay) = risk::requirements(&v.policy, a.tier()); + (a.tier(), a.reasons(), a.exposure(), req_approvals, req_guardians, delay) +} + +// ---------------------------------------------------------------- voting +/// Approvers approve payments; owners approve governance. One signer, one approval, and the +/// approval is recorded against the address so a removed signer's vote stops counting. +public fun approve(v: &mut Vault, id: u64, clock: &Clock, ctx: &TxContext) { + let now = clock::timestamp_ms(clock); + assert_live(v, id, now); + let sender = ctx.sender(); + let kind = v.proposals.borrow(id).kind; + let role = if (kind == types::kind_transfer()) { + types::role_approver() + } else { + types::role_owner() + }; + let code = if (kind == types::kind_transfer()) E_NOT_APPROVER else E_NOT_OWNER; + assert_role(v, sender, role, code); + + let (count, req) = { + let p = v.proposals.borrow_mut(id); + assert!(!p.approvals.contains(&sender), E_ALREADY_VOTED); + p.approvals.insert(sender); + (p.approvals.length(), p.req_approvals) + }; + event::emit(Approved { + vault: object::id(v), + proposal: id, + approver: sender, + approvals: count, + req_approvals: req, + at: now, + }); +} + +/// Turn a proposal down. +/// +/// Takes the same role that approving it would, and it takes as many rejections as the proposal +/// needed approvals — so rejecting is exactly as hard as approving, and one signer cannot block +/// the queue on their own. Without this the only ways to clear a bad proposal are an owner +/// cancelling it or waiting out its lifetime. +public fun reject(v: &mut Vault, id: u64, clock: &Clock, ctx: &TxContext) { + let now = clock::timestamp_ms(clock); + assert_live(v, id, now); + let sender = ctx.sender(); + let kind = v.proposals.borrow(id).kind; + let role = if (kind == types::kind_transfer()) { + types::role_approver() + } else { + types::role_owner() + }; + let code = if (kind == types::kind_transfer()) E_NOT_APPROVER else E_NOT_OWNER; + assert_role(v, sender, role, code); + + let (keys, req) = { + let p = v.proposals.borrow_mut(id); + assert!(!p.rejections.contains(&sender), E_ALREADY_VOTED); + p.rejections.insert(sender); + (*p.rejections.keys(), p.req_approvals) + }; + // Counted against live roles, for the same reason approvals are. + // + // A recovery pins zero approvals, because what it needs is guardian confirmations. Flooring + // the threshold at one keeps rejection meaningful there instead of settling on an empty set — + // which matches the documented rule that any owner can stop a recovery. + let threshold = if (req == 0) 1 else (req as u64); + let live = count_with_role(v, &keys, role); + let settled = live >= threshold; + event::emit(Rejected { + vault: object::id(v), + proposal: id, + rejecter: sender, + rejections: live, + req_approvals: req, + settled, + at: now, + }); + if (settled) settle(v, id, types::status_rejected(), sender, now); +} + +/// A guardian's positive signature, required for critical transfers, recovery and leaving a +/// raised posture. +public fun confirm(v: &mut Vault, id: u64, clock: &Clock, ctx: &TxContext) { + let now = clock::timestamp_ms(clock); + assert_live(v, id, now); + let sender = ctx.sender(); + assert_role(v, sender, types::role_guardian(), E_NOT_GUARDIAN); + let (count, req) = { + let p = v.proposals.borrow_mut(id); + assert!(!p.confirmations.contains(&sender), E_ALREADY_VOTED); + p.confirmations.insert(sender); + (p.confirmations.length(), p.req_guardians) + }; + event::emit(Confirmed { + vault: object::id(v), + proposal: id, + guardian: sender, + confirmations: count, + req_guardians: req, + at: now, + }); +} + +/// A guardian stops a critical transfer, any weakening of the vault, a recovery or a relaxation — +/// alone, and without ever gaining a way to move funds. Vetoing also raises the posture, because +/// a guardian reaching for the brake is itself a signal. +public fun veto(v: &mut Vault, id: u64, clock: &Clock, ctx: &TxContext) { + let now = clock::timestamp_ms(clock); + assert_live(v, id, now); + let sender = ctx.sender(); + assert_role(v, sender, types::role_guardian(), E_NOT_GUARDIAN); + { + let p = v.proposals.borrow(id); + let vetoable = p.reduction_mask != 0 + || (p.kind == types::kind_transfer() && p.tier == types::tier_critical()) + || p.kind == types::kind_recovery() + || p.kind == types::kind_mode_relax(); + assert!(vetoable, E_NOT_VETOABLE); + }; + settle(v, id, types::status_vetoed(), sender, now); + if (v.mode == types::mode_normal()) { + set_mode(v, types::mode_elevated(), types::reason_mode_elevated(), sender, clock); + }; +} + +/// The proposer withdraws their own request; any owner can withdraw anyone's. +public fun cancel(v: &mut Vault, id: u64, clock: &Clock, ctx: &TxContext) { + let now = clock::timestamp_ms(clock); + assert_live(v, id, now); + let sender = ctx.sender(); + let is_proposer = v.proposals.borrow(id).proposer == sender; + assert!( + is_proposer || types::has_role(roles_of(v, sender), types::role_owner()), + E_NOT_PROPOSER_OR_OWNER, + ); + settle(v, id, types::status_cancelled(), sender, now); +} + +/// Anyone may retire a proposal whose lifetime has run out, so a stale approval set cannot sit +/// waiting to be reused. +public fun expire(v: &mut Vault, id: u64, clock: &Clock, ctx: &TxContext) { + assert!(v.proposals.contains(id), E_UNKNOWN_PROPOSAL); + let now = clock::timestamp_ms(clock); + { + let p = v.proposals.borrow(id); + assert!(p.status == types::status_pending(), E_NOT_PENDING); + assert!(now >= p.expires_at, E_NOT_EXPIRED); + }; + settle(v, id, types::status_expired(), ctx.sender(), now); +} + +// ---------------------------------------------------------------- executing a transfer +/// The last gate. Every condition is re-evaluated against the state as it is *now*, not as it was +/// when the proposal was opened: +/// +/// * the approval set is recounted against live roles, so a removed signer no longer counts; +/// * the payment is reclassified, and the stricter of the pinned and live requirements applies; +/// * the recipient's trust and activation are re-read; +/// * the caps and the hard block are re-applied against the live balance and the live day; +/// * the loss envelope is checked last, and if the payment would cross it the vault latches into +/// Lockdown instead of paying. +public fun execute_transfer(v: &mut Vault, id: u64, clock: &Clock, ctx: &mut TxContext) { + let sender = ctx.sender(); + assert_role(v, sender, types::role_executor(), E_NOT_EXECUTOR); + assert_not_lockdown(v); + assert!(v.proposals.contains(id), E_UNKNOWN_PROPOSAL); + let now = clock::timestamp_ms(clock); + let key = type_name::with_defining_ids(); + + let ( + kind, + status, + created_at, + executable_at, + expires_at, + pin_approvals, + pin_guardians, + amount, + recipient, + asset, + ) = { + let p = v.proposals.borrow(id); + ( + p.kind, + p.status, + p.created_at, + p.executable_at, + p.expires_at, + p.req_approvals, + p.req_guardians, + p.amount, + p.recipient, + p.asset, + ) + }; + assert!(kind == types::kind_transfer(), E_WRONG_KIND); + assert!(status == types::status_pending(), E_NOT_PENDING); + assert!(asset.is_some() && *asset.borrow() == key, E_WRONG_ASSET); + assert!(now < expires_at, E_EXPIRED); + + // The asset must still be approved: revoking approval stops payments already in flight. + assert!(v.limits.contains(key) && v.limits.borrow(key).limits_approved(), E_ASSET_NOT_APPROVED); + + let asset_balance = balance_of(v); + assert!(asset_balance >= amount, E_INSUFFICIENT_BALANCE); + + let trust = recipient_trust(v, recipient); + assert!(trust != types::trust_blocked(), E_RECIPIENT_BLOCKED); + let activates_at = recipient_activates_at(v, recipient); + assert!(now >= activates_at, E_RECIPIENT_IN_PROBATION); + + let limits = limits_of(v); + let (per_tx, daily) = risk::effective_caps( + limits.limits_per_tx_max(), + limits.limits_daily_max(), + v.mode, + ); + assert!(per_tx == 0 || amount <= per_tx, E_PER_TX_CAP); + let (day_spent, _) = outflow_of(v, clock); + assert!(daily == 0 || day_spent + amount <= daily, E_DAILY_CAP); + + let hard_block = v.policy.hard_block_exposure_bps(); + let exposure = risk::exposure_bps(amount, asset_balance); + assert!(hard_block == 0 || exposure < hard_block, E_HARD_BLOCK); + + // Reclassify, then take the stricter of what was pinned and what the rules now demand. + let input = risk::new_input( + amount, + asset_balance, + limits.limits_low_max(), + limits.limits_high_max(), + daily, + day_spent, + trust, + false, + v.mode, + v.policy.high_exposure_bps(), + v.policy.critical_exposure_bps(), + ); + let assessment = risk::classify(&input); + let (live_approvals_req, live_guardians_req, live_delay) = + risk::requirements(&v.policy, assessment.tier()); + let need_approvals = larger(pin_approvals, live_approvals_req); + let need_guardians = larger(pin_guardians, live_guardians_req); + assert!(now >= later(executable_at, created_at + live_delay), E_TOO_EARLY); + + let approval_keys = *v.proposals.borrow(id).approvals.keys(); + let confirm_keys = *v.proposals.borrow(id).confirmations.keys(); + assert!( + count_with_role(v, &approval_keys, types::role_approver()) >= (need_approvals as u64), + E_NOT_ENOUGH_APPROVALS, + ); + assert!( + count_with_role(v, &confirm_keys, types::role_guardian()) >= (need_guardians as u64), + E_NOT_ENOUGH_GUARDIANS, + ); + + // The circuit breaker, checked last and charged atomically with the payment. + let envelope_bps = v.policy.envelope_bps(); + let window = v.policy.envelope_window(); + let (tripped, envelope_spent, envelope_limit, envelope_basis, day_after) = { + let vel = v.velocity.borrow_mut(key); + if (now >= vel.day_anchor + types::day_ms()) { + vel.day_anchor = now; + vel.day_spent = 0; + }; + if (window != 0 && now >= vel.envelope_anchor + window) { + vel.envelope_anchor = now; + vel.envelope_spent = 0; + vel.envelope_basis = asset_balance; + }; + // The first charge of the vault's life opens the window against today's balance. + if (vel.envelope_basis == 0) vel.envelope_basis = asset_balance; + + let basis = vel.envelope_basis; + let spent = vel.envelope_spent; + let limit = if (envelope_bps == 0) { + 0 + } else { + (((basis as u128) * (envelope_bps as u128)) / (types::bps() as u128)) as u64 + }; + if (envelope_bps != 0 && (spent as u128) + (amount as u128) > (limit as u128)) { + (true, spent, limit, basis, 0) + } else { + vel.envelope_spent = spent + amount; + vel.day_spent = vel.day_spent + amount; + (false, vel.envelope_spent, limit, basis, vel.day_spent) + } + }; + + if (tripped) { + // Not an abort: the latch has to survive the attempt, so the transaction succeeds, the + // money stays, and the vault is frozen for everyone until owners and guardians agree to + // reopen it. + settle(v, id, types::status_blocked(), sender, now); + event::emit(BreakerTripped { + vault: object::id(v), + proposal: id, + asset: key, + amount, + envelope_spent, + envelope_limit, + envelope_basis, + at: now, + }); + set_mode( + v, + types::mode_lockdown(), + types::reason_envelope_pressure() | types::reason_velocity_pressure(), + sender, + clock, + ); + return + }; + + // Effects before the transfer: the proposal is spent before any value moves. + { + let p = v.proposals.borrow_mut(id); + p.status = types::status_executed(); + p.tier = assessment.tier(); + p.reasons = assessment.reasons(); + p.exposure_bps = exposure; + }; + v.pending_count = v.pending_count - 1; + if (v.recipients.contains(recipient)) { + let r = v.recipients.borrow_mut(recipient); + r.paid_count = r.paid_count + 1; + r.last_paid = now; + }; + + let paid = balance::split(bag::borrow_mut>(&mut v.funds, key), amount); + transfer::public_transfer(coin::from_balance(paid, ctx), recipient); + + event::emit(Executed { + vault: object::id(v), + proposal: id, + kind: types::kind_transfer(), + asset: option::some(key), + amount, + recipient, + executor: sender, + tier: assessment.tier(), + balance_after: balance_of(v), + day_spent: day_after, + at: now, + }); +} + +// ---------------------------------------------------------------- governance +/// Governance proposals share one authorization rule: owner approvals up to the governance +/// threshold, and if the change gives anything up, the policy-change delay plus a guardian veto +/// window on top. A change that only tightens the vault takes effect as soon as owners agree. +fun open_governance( + v: &mut Vault, + kind: u8, + reduction_mask: u32, + delay: u64, + now: u64, + sender: address, +): u64 { + assert_role(v, sender, types::role_owner(), E_NOT_OWNER); + // A vault in Lockdown cannot be talked into weakening itself. + if (reduction_mask != 0) assert_not_lockdown(v); + let id = v.next_proposal; + v.next_proposal = id + 1; + let p = blank( + id, + kind, + sender, + now, + now + delay, + now + v.policy.proposal_ttl(), + v.policy.governance_threshold(), + 0, + v.policy_version, + reduction_mask, + ); + v.proposals.add(id, p); + v.pending_count = v.pending_count + 1; + id +} + +fun emit_opened(v: &Vault, id: u64, kind: u8, sender: address, mask: u32, memo: String) { + let p = v.proposals.borrow(id); + event::emit(ProposalOpened { + vault: object::id(v), + proposal: id, + kind, + proposer: sender, + asset: p.asset, + amount: p.amount, + recipient: p.recipient, + tier: types::tier_low(), + reasons: 0, + exposure_bps: 0, + req_approvals: p.req_approvals, + req_guardians: p.req_guardians, + executable_at: p.executable_at, + expires_at: p.expires_at, + reduction_mask: mask, + memo, + at: p.created_at, + }); +} + +/// Shared final check for every governance kind. +fun authorize_governance(v: &Vault, id: u64, kind: u8, now: u64): u32 { + let p = v.proposals.borrow(id); + assert!(p.kind == kind, E_WRONG_KIND); + assert!(p.status == types::status_pending(), E_NOT_PENDING); + assert!(now < p.expires_at, E_EXPIRED); + assert!(now >= p.executable_at, E_TOO_EARLY); + let keys = *p.approvals.keys(); + assert!( + count_with_role(v, &keys, types::role_owner()) + >= (v.policy.governance_threshold() as u64), + E_NOT_ENOUGH_APPROVALS, + ); + p.reduction_mask +} + +// --- the policy itself +public fun propose_policy( + v: &mut Vault, + next: Policy, + clock: &Clock, + ctx: &TxContext, +): u64 { + next.assert_valid(); + next.assert_counts_valid(v.owner_count, v.approver_count, v.executor_count, v.guardian_count); + let mask = policy::reductions(&v.policy, &next); + let now = clock::timestamp_ms(clock); + let delay = if (mask == 0) 0 else v.policy.policy_change_delay(); + let id = open_governance(v, types::kind_policy_update(), mask, delay, now, ctx.sender()); + v.proposals.borrow_mut(id).new_policy = option::some(next); + emit_opened(v, id, types::kind_policy_update(), ctx.sender(), mask, string::utf8(b"policy")); + id +} + +public fun execute_policy(v: &mut Vault, id: u64, clock: &Clock, ctx: &TxContext) { + let now = clock::timestamp_ms(clock); + let mask = authorize_governance(v, id, types::kind_policy_update(), now); + let next = *v.proposals.borrow(id).new_policy.borrow(); + // Re-validated against the roster as it stands now, not as it stood when proposed. + next.assert_valid(); + next.assert_counts_valid(v.owner_count, v.approver_count, v.executor_count, v.guardian_count); + v.policy = next; + v.policy_version = v.policy_version + 1; + settle(v, id, types::status_executed(), ctx.sender(), now); + event::emit(PolicyChanged { + vault: object::id(v), + proposal: id, + version: v.policy_version, + reduction_mask: mask, + at: now, + }); +} + +// --- per-asset limits +public fun propose_asset_limits( + v: &mut Vault, + approved: bool, + low_max: u64, + high_max: u64, + per_tx_max: u64, + daily_max: u64, + clock: &Clock, + ctx: &TxContext, +): u64 { + let next = policy::new_limits(approved, low_max, high_max, per_tx_max, daily_max); + next.assert_limits_valid(); + let key = type_name::with_defining_ids(); + let current = limits_of(v); + let mask = policy::limit_reductions(¤t, &next); + let now = clock::timestamp_ms(clock); + let delay = if (mask == 0) 0 else v.policy.policy_change_delay(); + let id = open_governance(v, types::kind_asset_limits(), mask, delay, now, ctx.sender()); + { + let p = v.proposals.borrow_mut(id); + p.new_limits = option::some(next); + p.asset = option::some(key); + }; + emit_opened(v, id, types::kind_asset_limits(), ctx.sender(), mask, string::utf8(b"limits")); + id +} + +public fun execute_asset_limits(v: &mut Vault, id: u64, clock: &Clock, ctx: &TxContext) { + let now = clock::timestamp_ms(clock); + let key = type_name::with_defining_ids(); + let mask = authorize_governance(v, id, types::kind_asset_limits(), now); + let next = { + let p = v.proposals.borrow(id); + assert!(p.asset.is_some() && *p.asset.borrow() == key, E_WRONG_ASSET); + *p.new_limits.borrow() + }; + if (v.limits.contains(key)) { + *v.limits.borrow_mut(key) = next; + } else { + v.limits.add(key, next); + v.velocity.add(key, Velocity { + day_anchor: now, + day_spent: 0, + envelope_anchor: now, + envelope_spent: 0, + envelope_basis: 0, + }); + }; + settle(v, id, types::status_executed(), ctx.sender(), now); + event::emit(LimitsChanged { + vault: object::id(v), + proposal: id, + asset: key, + approved: next.limits_approved(), + low_max: next.limits_low_max(), + high_max: next.limits_high_max(), + per_tx_max: next.limits_per_tx_max(), + daily_max: next.limits_daily_max(), + reduction_mask: mask, + at: now, + }); +} + +// --- the roster +/// Set a member's roles. Zero removes them. Granting any role an address did not already hold is +/// treated as security-reducing: a new key is a new way in, so it waits and can be vetoed. +public fun propose_member( + v: &mut Vault, + who: address, + roles: u8, + clock: &Clock, + ctx: &TxContext, +): u64 { + assert!(roles == 0 || types::roles_valid(roles), E_INVALID_ROLES); + let before = roles_of(v, who); + assert!(before != roles, E_NOT_A_MEMBER_CHANGE); + let granted = roles & (before ^ 255); + let mask: u32 = if (granted != 0) 1 else 0; + let now = clock::timestamp_ms(clock); + let delay = if (mask == 0) 0 else v.policy.policy_change_delay(); + let id = open_governance(v, types::kind_member_set(), mask, delay, now, ctx.sender()); + { + let p = v.proposals.borrow_mut(id); + p.member = who; + p.member_roles = roles; + }; + emit_opened(v, id, types::kind_member_set(), ctx.sender(), mask, string::utf8(b"member")); + id +} + +public fun execute_member(v: &mut Vault, id: u64, clock: &Clock, ctx: &TxContext) { + let now = clock::timestamp_ms(clock); + let _ = authorize_governance(v, id, types::kind_member_set(), now); + let (who, roles) = { + let p = v.proposals.borrow(id); + (p.member, p.member_roles) + }; + let before = apply_roles(v, who, roles); + // The roster must still satisfy the policy after the change, so a vault can never be left + // with a threshold nobody can reach. + v.policy.assert_counts_valid( + v.owner_count, + v.approver_count, + v.executor_count, + v.guardian_count, + ); + settle(v, id, types::status_executed(), ctx.sender(), now); + event::emit(MemberChanged { + vault: object::id(v), + proposal: id, + member: who, + roles_before: before, + roles_after: roles, + at: now, + }); +} + +fun apply_roles(v: &mut Vault, who: address, roles: u8): u8 { + let before = roles_of(v, who); + if (before != 0) { + tally(v, before, false); + v.members.remove(who); + }; + if (roles != 0) { + assert!(types::roles_valid(roles), E_GUARDIAN_HAS_TREASURY_ROLE); + v.members.add(who, roles); + tally(v, roles, true); + }; + before +} + +// --- recipient trust +public fun propose_recipient_trust( + v: &mut Vault, + who: address, + to: u8, + clock: &Clock, + ctx: &TxContext, +): u64 { + assert!(types::trust_valid(to), E_INVALID_TRUST); + let from = recipient_trust(v, who); + let mask: u32 = if (policy::trust_reduces(from, to)) 1 else 0; + let now = clock::timestamp_ms(clock); + let delay = if (mask == 0) 0 else v.policy.policy_change_delay(); + let id = open_governance(v, types::kind_recipient_trust(), mask, delay, now, ctx.sender()); + { + let p = v.proposals.borrow_mut(id); + p.recipient = who; + p.trust_level = to; + }; + emit_opened(v, id, types::kind_recipient_trust(), ctx.sender(), mask, string::utf8(b"trust")); + id +} + +public fun execute_recipient_trust(v: &mut Vault, id: u64, clock: &Clock, ctx: &TxContext) { + let now = clock::timestamp_ms(clock); + let _ = authorize_governance(v, id, types::kind_recipient_trust(), now); + let (who, to) = { + let p = v.proposals.borrow(id); + (p.recipient, p.trust_level) + }; + let from = if (v.recipients.contains(who)) { + let r = v.recipients.borrow_mut(who); + let before = r.trust; + r.trust = to; + before + } else { + v.recipients.add(who, Recipient { + trust: to, + registered_at: now, + activates_at: now + v.policy.recipient_activation_delay(), + paid_count: 0, + last_paid: 0, + label: string::utf8(b""), + }); + types::trust_unknown() + }; + settle(v, id, types::status_executed(), ctx.sender(), now); + event::emit(RecipientTrustChanged { + vault: object::id(v), + proposal: id, + recipient: who, + from, + to, + actor: ctx.sender(), + at: now, + }); +} + +// --- leaving a raised posture +/// Lowering the posture takes owners, guardians and time, in that order. Nothing about a frozen +/// vault can be undone by whoever froze it alone. +public fun propose_mode_relax( + v: &mut Vault, + to: u8, + clock: &Clock, + ctx: &TxContext, +): u64 { + assert!(to < v.mode, E_NOT_A_RELAXATION); + let now = clock::timestamp_ms(clock); + let sender = ctx.sender(); + assert_role(v, sender, types::role_owner(), E_NOT_OWNER); + let id = v.next_proposal; + v.next_proposal = id + 1; + let mut p = blank( + id, + types::kind_mode_relax(), + sender, + now, + now + v.policy.policy_change_delay(), + now + v.policy.proposal_ttl(), + v.policy.governance_threshold(), + v.policy.guardian_threshold(), + v.policy_version, + 1, + ); + p.target_mode = to; + v.proposals.add(id, p); + v.pending_count = v.pending_count + 1; + emit_opened(v, id, types::kind_mode_relax(), sender, 1, string::utf8(b"relax")); + id +} + +public fun execute_mode_relax(v: &mut Vault, id: u64, clock: &Clock, ctx: &TxContext) { + let now = clock::timestamp_ms(clock); + let (to, approval_keys, confirm_keys) = { + let p = v.proposals.borrow(id); + assert!(p.kind == types::kind_mode_relax(), E_WRONG_KIND); + assert!(p.status == types::status_pending(), E_NOT_PENDING); + assert!(now < p.expires_at, E_EXPIRED); + assert!(now >= p.executable_at, E_TOO_EARLY); + (p.target_mode, *p.approvals.keys(), *p.confirmations.keys()) + }; + assert!( + count_with_role(v, &approval_keys, types::role_owner()) + >= (v.policy.governance_threshold() as u64), + E_NOT_ENOUGH_APPROVALS, + ); + assert!( + count_with_role(v, &confirm_keys, types::role_guardian()) + >= (v.policy.guardian_threshold() as u64), + E_NOT_ENOUGH_GUARDIANS, + ); + assert!(to < v.mode, E_NOT_A_RELAXATION); + settle(v, id, types::status_executed(), ctx.sender(), now); + set_mode(v, to, 0, ctx.sender(), clock); +} + +// --- recovery +/// Replace a signer whose key is gone. Guardians propose it, the recovery delay runs, any owner +/// can cancel it, the roles move across unchanged, the policy is untouched, and the vault comes +/// out of it in Elevated — because a recovery means something went wrong. +public fun propose_recovery( + v: &mut Vault, + lost: address, + replacement: address, + clock: &Clock, + ctx: &TxContext, +): u64 { + let sender = ctx.sender(); + assert_role(v, sender, types::role_guardian(), E_NOT_GUARDIAN); + assert!(replacement != sender, E_SELF_RECOVERY); + assert!(roles_of(v, lost) != 0, E_NO_SUCH_MEMBER); + assert!(roles_of(v, replacement) == 0, E_ALREADY_REGISTERED); + let now = clock::timestamp_ms(clock); + let id = v.next_proposal; + v.next_proposal = id + 1; + let mut p = blank( + id, + types::kind_recovery(), + sender, + now, + now + v.policy.recovery_delay(), + now + v.policy.proposal_ttl(), + 0, + v.policy.guardian_threshold(), + v.policy_version, + 1, + ); + p.member_prev = lost; + p.member = replacement; + p.member_roles = roles_of(v, lost); + v.proposals.add(id, p); + v.pending_count = v.pending_count + 1; + emit_opened(v, id, types::kind_recovery(), sender, 1, string::utf8(b"recovery")); + id +} + +public fun execute_recovery(v: &mut Vault, id: u64, clock: &Clock, ctx: &TxContext) { + let now = clock::timestamp_ms(clock); + let (lost, replacement, confirm_keys) = { + let p = v.proposals.borrow(id); + assert!(p.kind == types::kind_recovery(), E_WRONG_KIND); + assert!(p.status == types::status_pending(), E_NOT_PENDING); + assert!(now < p.expires_at, E_EXPIRED); + assert!(now >= p.executable_at, E_TOO_EARLY); + (p.member_prev, p.member, *p.confirmations.keys()) + }; + assert!( + count_with_role(v, &confirm_keys, types::role_guardian()) + >= (v.policy.guardian_threshold() as u64), + E_NOT_ENOUGH_GUARDIANS, + ); + // The roles are whatever the lost signer holds at this moment, not what they held when the + // recovery was proposed, so a recovery cannot be used to hand over roles acquired since. + let roles = roles_of(v, lost); + assert!(roles != 0, E_NO_SUCH_MEMBER); + assert!(roles_of(v, replacement) == 0, E_ALREADY_REGISTERED); + apply_roles(v, lost, 0); + apply_roles(v, replacement, roles); + v.policy.assert_counts_valid( + v.owner_count, + v.approver_count, + v.executor_count, + v.guardian_count, + ); + settle(v, id, types::status_executed(), ctx.sender(), now); + event::emit(Recovered { + vault: object::id(v), + proposal: id, + lost, + replacement, + roles, + at: now, + }); + if (v.mode == types::mode_normal()) { + set_mode(v, types::mode_elevated(), types::reason_mode_elevated(), ctx.sender(), clock); + }; +} diff --git a/sui/tests/engine_tests.move b/sui/tests/engine_tests.move new file mode 100644 index 0000000..c4e16b9 --- /dev/null +++ b/sui/tests/engine_tests.move @@ -0,0 +1,369 @@ +/// Vectors for the pure engines. These pin the numbers the interfaces and the simulator rely on: +/// if a tier moves here, it moves everywhere, and this suite says so. +#[test_only] +module skur::engine_tests; + +use skur::policy; +use skur::risk; +use skur::types; + +const HOUR: u64 = 3_600_000; +const DAY: u64 = 86_400_000; + +fun base(): policy::Policy { + policy::new( + 1, 2, 3, // approvals low / high / critical + 2, // governance threshold + 1, // guardian threshold + true, // guardian required for critical + HOUR, // delay high + DAY, // delay critical + 12 * HOUR, // recipient activation + 2 * DAY, // policy change + 3 * DAY, // recovery + 30 * DAY, // proposal ttl + 1_000, // high exposure 10% + 2_500, // critical exposure 25% + 9_000, // hard block 90% + 3_000, // envelope 30% + DAY, // envelope window + ) +} + +fun input(amount: u64, balance: u64, trust: u8, probation: bool, mode: u8): risk::RiskInput { + risk::new_input(amount, balance, 1_000, 10_000, 0, 0, trust, probation, mode, 1_000, 2_500) +} + +// ---------------------------------------------------------------- policy validation +#[test] +fun base_policy_is_valid() { + assert!(policy::validate(&base()) == policy::e_ok(), 0); +} + +#[test] +fun rejects_zero_low_approvals() { + let p = policy::new(0, 2, 3, 2, 1, true, HOUR, DAY, 0, 0, 0, 30 * DAY, 0, 0, 0, 0, 0); + assert!(policy::validate(&p) == 1, 0); +} + +#[test] +fun rejects_non_monotonic_approvals() { + let p = policy::new(3, 2, 3, 2, 1, true, HOUR, DAY, 0, 0, 0, 30 * DAY, 0, 0, 0, 0, 0); + assert!(policy::validate(&p) == 2, 0); + let q = policy::new(1, 3, 2, 2, 1, true, HOUR, DAY, 0, 0, 0, 30 * DAY, 0, 0, 0, 0, 0); + assert!(policy::validate(&q) == 3, 0); +} + +#[test] +fun rejects_guardian_required_without_guardians() { + let p = policy::new(1, 2, 3, 2, 0, true, HOUR, DAY, 0, 0, 0, 30 * DAY, 0, 0, 0, 0, 0); + assert!(policy::validate(&p) == 5, 0); +} + +#[test] +fun rejects_critical_delay_below_high() { + let p = policy::new(1, 2, 3, 2, 1, true, DAY, HOUR, 0, 0, 0, 30 * DAY, 0, 0, 0, 0, 0); + assert!(policy::validate(&p) == 6, 0); +} + +#[test] +fun rejects_high_exposure_above_critical() { + let p = policy::new(1, 2, 3, 2, 1, true, HOUR, DAY, 0, 0, 0, 30 * DAY, 3_000, 2_500, 0, 0, 0); + assert!(policy::validate(&p) == 7, 0); +} + +#[test] +fun rejects_hard_block_below_critical() { + let p = policy::new(1, 2, 3, 2, 1, true, HOUR, DAY, 0, 0, 0, 30 * DAY, 1_000, 2_500, 2_000, 0, 0); + assert!(policy::validate(&p) == 9, 0); +} + +#[test] +fun rejects_envelope_window_under_an_hour() { + let p = policy::new(1, 2, 3, 2, 1, true, HOUR, DAY, 0, 0, 0, 30 * DAY, 0, 0, 0, 3_000, 60_000); + assert!(policy::validate(&p) == 12, 0); +} + +/// A policy whose transfers would expire before their own timelock elapses is unusable, and the +/// vault refuses to hold one. +#[test] +fun rejects_ttl_shorter_than_the_longest_delay() { + let p = policy::new(1, 2, 3, 2, 1, true, HOUR, DAY, 0, 0, 0, DAY, 0, 0, 0, 0, 0); + assert!(policy::validate(&p) == 13, 0); +} + +#[test] +fun ttl_must_clear_the_longest_delay_by_an_hour() { + let exact = policy::new(1, 2, 3, 2, 1, true, HOUR, DAY, 0, 0, 0, DAY + HOUR, 0, 0, 0, 0, 0); + assert!(policy::validate(&exact) == policy::e_ok(), 0); + let short = policy::new(1, 2, 3, 2, 1, true, HOUR, DAY, 0, 0, 0, DAY + HOUR - 1, 0, 0, 0, 0, 0); + assert!(policy::validate(&short) == 13, 1); +} + +#[test] +fun counts_must_satisfy_the_policy() { + let p = base(); + assert!(policy::validate_counts(&p, 2, 3, 1, 1) == policy::e_ok(), 0); + assert!(policy::validate_counts(&p, 0, 3, 1, 1) == 30, 1); // no owner + assert!(policy::validate_counts(&p, 2, 3, 0, 1) == 31, 2); // no executor + assert!(policy::validate_counts(&p, 1, 3, 1, 1) == 32, 3); // governance > owners + assert!(policy::validate_counts(&p, 2, 2, 1, 1) == 33, 4); // critical > approvers + assert!(policy::validate_counts(&p, 2, 3, 1, 0) == 34, 5); // guardian threshold > guardians +} + +#[test] +fun limits_must_be_coherent() { + assert!(policy::validate_limits(&policy::new_limits(true, 10, 5, 0, 0)) == 20, 0); + assert!(policy::validate_limits(&policy::new_limits(true, 1, 5, 100, 50)) == 21, 1); + // Zero means "no cap", so a zero daily cap cannot be below a per-transaction cap. + assert!( + policy::validate_limits(&policy::new_limits(true, 1, 5, 100, 0)) == policy::e_ok(), + 2, + ); +} + +// ---------------------------------------------------------------- what counts as weakening +#[test] +fun tightening_reports_no_reduction() { + let a = base(); + let b = policy::new( + 2, 3, 4, 3, 2, true, 2 * HOUR, 2 * DAY, DAY, 3 * DAY, 4 * DAY, 30 * DAY, + 500, 1_500, 8_000, 2_000, DAY, + ); + assert!(policy::reductions(&a, &b) == 0, 0); +} + +#[test] +fun fewer_approvals_is_a_reduction() { + let a = base(); + let b = policy::new( + 1, 1, 3, 2, 1, true, HOUR, DAY, 12 * HOUR, 2 * DAY, 3 * DAY, 30 * DAY, + 1_000, 2_500, 9_000, 3_000, DAY, + ); + assert!(policy::reductions(&a, &b) != 0, 0); +} + +#[test] +fun dropping_the_guardian_requirement_is_a_reduction() { + let a = base(); + let b = policy::new( + 1, 2, 3, 2, 1, false, HOUR, DAY, 12 * HOUR, 2 * DAY, 3 * DAY, 30 * DAY, + 1_000, 2_500, 9_000, 3_000, DAY, + ); + assert!(policy::reductions(&a, &b) == policy::r_guardian_not_required(), 0); +} + +/// Disabling a threshold by setting it to zero is the loosest move available, and must not be +/// mistaken for tightening just because the number went down. +#[test] +fun zeroing_the_envelope_is_a_reduction() { + let a = base(); + let b = policy::new( + 1, 2, 3, 2, 1, true, HOUR, DAY, 12 * HOUR, 2 * DAY, 3 * DAY, 30 * DAY, + 1_000, 2_500, 9_000, 0, DAY, + ); + assert!(policy::reductions(&a, &b) == policy::r_envelope(), 0); +} + +#[test] +fun a_longer_proposal_lifetime_is_a_reduction() { + let a = base(); + let b = policy::new( + 1, 2, 3, 2, 1, true, HOUR, DAY, 12 * HOUR, 2 * DAY, 3 * DAY, 60 * DAY, + 1_000, 2_500, 9_000, 3_000, DAY, + ); + assert!(policy::reductions(&a, &b) != 0, 0); +} + +#[test] +fun weakening_a_cap_to_zero_is_a_reduction() { + let a = policy::new_limits(true, 100, 1_000, 500, 5_000); + let b = policy::new_limits(true, 100, 1_000, 0, 5_000); + assert!(policy::limit_reductions(&a, &b) != 0, 0); + let c = policy::new_limits(true, 100, 1_000, 400, 5_000); + assert!(policy::limit_reductions(&a, &c) == 0, 1); +} + +#[test] +fun approving_a_new_asset_is_a_reduction() { + let a = policy::new_limits(false, 0, 0, 0, 0); + let b = policy::new_limits(true, 100, 1_000, 0, 0); + assert!(policy::limit_reductions(&a, &b) & policy::r_asset_newly_approved() != 0, 0); +} + +#[test] +fun lifting_a_restriction_reduces_but_imposing_one_does_not() { + assert!(policy::trust_reduces(types::trust_restricted(), types::trust_verified()), 0); + assert!(policy::trust_reduces(types::trust_new(), types::trust_trusted()), 1); + assert!(!policy::trust_reduces(types::trust_trusted(), types::trust_blocked()), 2); + assert!(!policy::trust_reduces(types::trust_verified(), types::trust_new()), 3); +} + +// ---------------------------------------------------------------- roles +#[test] +fun a_guardian_cannot_hold_a_treasury_role() { + assert!(types::roles_valid(types::role_guardian()), 0); + assert!(types::roles_valid(types::role_owner() | types::role_approver()), 1); + assert!(!types::roles_valid(types::role_guardian() | types::role_owner()), 2); + assert!(!types::roles_valid(types::role_guardian() | types::role_proposer()), 3); + assert!(!types::roles_valid(0), 4); +} + +#[test] +fun a_proposer_can_only_propose() { + let agent = types::role_proposer(); + assert!(types::can_propose(agent), 0); + assert!(!types::has_role(agent, types::role_approver()), 1); + assert!(!types::has_role(agent, types::role_executor()), 2); + assert!(!types::has_role(agent, types::role_owner()), 3); +} + +// ---------------------------------------------------------------- risk classification +#[test] +fun exposure_is_a_share_of_holdings() { + assert!(risk::exposure_bps(0, 1_000) == 0, 0); + assert!(risk::exposure_bps(100, 1_000) == 1_000, 1); + assert!(risk::exposure_bps(1_000, 1_000) == types::bps(), 2); + assert!(risk::exposure_bps(1_500, 1_000) == types::bps(), 3); + // Paying out of an empty balance is full exposure, not a division by zero. + assert!(risk::exposure_bps(5, 0) == types::bps(), 4); +} + +/// Exposure is computed in 128-bit space, so a treasury large enough to overflow a naive +/// `amount * 10_000` still classifies correctly. +#[test] +fun exposure_survives_a_large_treasury() { + let big = 1_000_000_000_000_000_000; // 1e18 + assert!(risk::exposure_bps(big / 4, big) == 2_500, 0); +} + +#[test] +fun amount_sets_the_tier() { + let low = risk::classify(&input(500, 1_000_000, types::trust_verified(), false, types::mode_normal())); + assert!(low.tier() == types::tier_low(), 0); + assert!(low.reasons() == 0, 1); + + let high = risk::classify(&input(5_000, 1_000_000, types::trust_verified(), false, types::mode_normal())); + assert!(high.tier() == types::tier_high(), 2); + assert!(high.reasons() & types::reason_amount_high() != 0, 3); + + let crit = risk::classify(&input(50_000, 10_000_000, types::trust_verified(), false, types::mode_normal())); + assert!(crit.tier() == types::tier_critical(), 4); + assert!(crit.reasons() & types::reason_amount_critical() != 0, 5); +} + +/// A small payment out of a small treasury is still a big payment. +#[test] +fun share_of_treasury_escalates_on_its_own() { + let o = risk::classify(&input(500, 4_000, types::trust_verified(), false, types::mode_normal())); + assert!(o.tier() == types::tier_high(), 0); + assert!(o.reasons() & types::reason_exposure_high() != 0, 1); + + let c = risk::classify(&input(900, 3_000, types::trust_verified(), false, types::mode_normal())); + assert!(c.tier() == types::tier_critical(), 2); + assert!(c.reasons() & types::reason_exposure_critical() != 0, 3); +} + +#[test] +fun an_unknown_recipient_escalates_one_step() { + let o = risk::classify(&input(500, 1_000_000, types::trust_unknown(), false, types::mode_normal())); + assert!(o.tier() == types::tier_high(), 0); + assert!(o.reasons() & types::reason_recipient_unknown() != 0, 1); + + // Already high on amount, an unknown recipient takes it to critical. + let c = risk::classify(&input(5_000, 1_000_000, types::trust_unknown(), false, types::mode_normal())); + assert!(c.tier() == types::tier_critical(), 2); +} + +#[test] +fun probation_escalates_only_until_it_elapses() { + let during = risk::classify(&input(500, 1_000_000, types::trust_new(), true, types::mode_normal())); + assert!(during.tier() == types::tier_high(), 0); + assert!(during.reasons() & types::reason_recipient_probation() != 0, 1); + + let after = risk::classify(&input(500, 1_000_000, types::trust_new(), false, types::mode_normal())); + assert!(after.tier() == types::tier_low(), 2); +} + +#[test] +fun a_restricted_recipient_is_always_critical() { + let o = risk::classify(&input(1, 1_000_000_000, types::trust_restricted(), false, types::mode_normal())); + assert!(o.tier() == types::tier_critical(), 0); + assert!(o.reasons() & types::reason_recipient_restricted() != 0, 1); +} + +#[test] +fun trusted_recipients_are_never_discounted_below_the_amount_tier() { + let o = risk::classify(&input(50_000, 10_000_000, types::trust_trusted(), false, types::mode_normal())); + assert!(o.tier() == types::tier_critical(), 0); +} + +/// Crossing half the day's allowance escalates, so splitting a drain hits the higher bar partway +/// through rather than never. +#[test] +fun velocity_pressure_escalates_mid_drain() { + let quiet = risk::new_input( + 100, 1_000_000, 1_000, 10_000, 10_000, 0, + types::trust_verified(), false, types::mode_normal(), 1_000, 2_500, + ); + assert!(risk::classify(&quiet).tier() == types::tier_low(), 0); + + let pressured = risk::new_input( + 100, 1_000_000, 1_000, 10_000, 10_000, 5_000, + types::trust_verified(), false, types::mode_normal(), 1_000, 2_500, + ); + let o = risk::classify(&pressured); + assert!(o.tier() == types::tier_high(), 1); + assert!(o.reasons() & types::reason_velocity_pressure() != 0, 2); +} + +#[test] +fun elevated_mode_escalates_everything() { + let o = risk::classify(&input(500, 1_000_000, types::trust_verified(), false, types::mode_elevated())); + assert!(o.tier() == types::tier_high(), 0); + assert!(o.reasons() & types::reason_mode_elevated() != 0, 1); + + let c = risk::classify(&input(5_000, 1_000_000, types::trust_verified(), false, types::mode_elevated())); + assert!(c.tier() == types::tier_critical(), 2); +} + +/// Several signals at once, each recorded, so a reviewer sees every reason and not just the +/// loudest one. +#[test] +fun reasons_accumulate() { + let i = risk::new_input( + 5_000, 20_000, 1_000, 10_000, 10_000, 6_000, + types::trust_unknown(), true, types::mode_elevated(), 1_000, 2_500, + ); + let o = risk::classify(&i); + assert!(o.tier() == types::tier_critical(), 0); + assert!(o.reasons() & types::reason_amount_high() != 0, 1); + assert!(o.reasons() & types::reason_exposure_critical() != 0, 2); + assert!(o.reasons() & types::reason_recipient_unknown() != 0, 3); + assert!(o.reasons() & types::reason_velocity_pressure() != 0, 4); + assert!(o.reasons() & types::reason_mode_elevated() != 0, 5); +} + +#[test] +fun requirements_follow_the_tier() { + let p = base(); + let (a, g, d) = risk::requirements(&p, types::tier_low()); + assert!(a == 1 && g == 0 && d == 0, 0); + let (a2, g2, d2) = risk::requirements(&p, types::tier_high()); + assert!(a2 == 2 && g2 == 0 && d2 == HOUR, 1); + let (a3, g3, d3) = risk::requirements(&p, types::tier_critical()); + assert!(a3 == 3 && g3 == 1 && d3 == DAY, 2); +} + +#[test] +fun elevated_halves_the_caps_but_never_to_zero() { + let (p, d) = risk::effective_caps(1_000, 10_000, types::mode_elevated()); + assert!(p == 500 && d == 5_000, 0); + let (p2, d2) = risk::effective_caps(1, 1, types::mode_elevated()); + assert!(p2 == 1 && d2 == 1, 1); + // No cap stays no cap; halving zero would silently freeze the asset. + let (p3, d3) = risk::effective_caps(0, 0, types::mode_elevated()); + assert!(p3 == 0 && d3 == 0, 2); + let (p4, d4) = risk::effective_caps(1_000, 10_000, types::mode_normal()); + assert!(p4 == 1_000 && d4 == 10_000, 3); +} diff --git a/sui/tests/vault_tests.move b/sui/tests/vault_tests.move new file mode 100644 index 0000000..7fff611 --- /dev/null +++ b/sui/tests/vault_tests.move @@ -0,0 +1,1072 @@ +/// End-to-end tests for the vault. Each one is written as the attack or the mistake it defends +/// against, because that is the only reason the control exists. +#[test_only] +module skur::vault_tests; + +use std::string; +use sui::clock::{Self, Clock}; +use sui::coin::{Self, Coin}; +use sui::test_scenario::{Self as ts, Scenario}; +use skur::policy::{Self, Policy}; +use skur::types; +use skur::vault::{Self, Vault}; + +/// A stand-in asset. `Coin` behaves exactly like any other coin type for the vault's +/// purposes, which is the point of keying the treasury by type. +public struct USDC has drop {} + +const OWNER: address = @0xA1; +const OWNER2: address = @0xA2; +const APPROVER: address = @0xA3; +const APPROVER2: address = @0xA4; +const GUARD: address = @0xB1; +const AGENT: address = @0xC1; +const BOB: address = @0xD1; +const EVE: address = @0xD2; +const NEWKEY: address = @0xE1; + +const HOUR: u64 = 3_600_000; +const DAY: u64 = 86_400_000; +const T0: u64 = 1_700_000_000_000; + +const TREASURY: u64 = 1_000_000; + +// abort codes under test +const E_LOCKDOWN: u64 = 107; +const E_NOT_PENDING: u64 = 110; +const E_TOO_EARLY: u64 = 113; +const E_RECIPIENT_IN_PROBATION: u64 = 119; +const E_EXPIRED: u64 = 114; +const E_NOT_ENOUGH_APPROVALS: u64 = 116; +const E_NOT_ENOUGH_GUARDIANS: u64 = 117; +const E_RECIPIENT_BLOCKED: u64 = 118; +const E_PER_TX_CAP: u64 = 120; +const E_DAILY_CAP: u64 = 121; +const E_HARD_BLOCK: u64 = 122; +const E_NOT_APPROVER: u64 = 102; +const E_NOT_EXECUTOR: u64 = 103; +const E_CANNOT_PROPOSE: u64 = 105; +const E_ALREADY_VOTED: u64 = 138; + +// ---------------------------------------------------------------- fixtures +fun make_policy(envelope_bps: u64, hard_block: u64, delay_high: u64): Policy { + policy::new( + 1, 2, 3, // approvals low / high / critical + 2, // governance threshold + 1, // guardian threshold + true, // guardian required for critical + delay_high, + DAY, // critical delay + 12 * HOUR, // recipient activation + 2 * DAY, // policy change + 3 * DAY, // recovery + 30 * DAY, // proposal lifetime + 1_000, // high exposure 10% + 2_500, // critical exposure 25% + hard_block, + envelope_bps, + DAY, // envelope window + ) +} + +fun base_policy(): Policy { make_policy(3_000, 9_000, HOUR) } + +fun boot(sc: &mut Scenario, p: Policy, per_tx: u64, daily: u64, clock: &Clock) { + ts::next_tx(sc, OWNER); + let owner = types::role_owner() | types::role_approver() | types::role_executor(); + let mut s = vault::begin( + string::utf8(b"Skur Treasury"), + p, + vector[OWNER, OWNER2, APPROVER, APPROVER2, GUARD, AGENT], + vector[ + owner, + owner, + types::role_approver(), + types::role_approver(), + types::role_guardian(), + types::role_proposer(), + ], + clock, + sc.ctx(), + ); + vault::setup_asset(&mut s, true, 1_000, 10_000, per_tx, daily); + vault::setup_recipient(&mut s, BOB, string::utf8(b"Payroll")); + vault::finish(s, sc.ctx()); +} + +fun fund(sc: &mut Scenario, clock: &Clock, amount: u64) { + ts::next_tx(sc, OWNER); + let mut v = ts::take_shared(sc); + let c = coin::mint_for_testing(amount, sc.ctx()); + vault::deposit(&mut v, c, clock, sc.ctx()); + ts::return_shared(v); +} + +fun propose(sc: &mut Scenario, who: address, amount: u64, to: address, clock: &Clock): u64 { + ts::next_tx(sc, who); + let mut v = ts::take_shared(sc); + let id = vault::propose_transfer( + &mut v, + amount, + to, + string::utf8(b"invoice"), + clock, + sc.ctx(), + ); + ts::return_shared(v); + id +} + +fun approve(sc: &mut Scenario, who: address, id: u64, clock: &Clock) { + ts::next_tx(sc, who); + let mut v = ts::take_shared(sc); + vault::approve(&mut v, id, clock, sc.ctx()); + ts::return_shared(v); +} + +fun reject(sc: &mut Scenario, who: address, id: u64, clock: &Clock) { + ts::next_tx(sc, who); + let mut v = ts::take_shared(sc); + vault::reject(&mut v, id, clock, sc.ctx()); + ts::return_shared(v); +} + +fun confirm(sc: &mut Scenario, who: address, id: u64, clock: &Clock) { + ts::next_tx(sc, who); + let mut v = ts::take_shared(sc); + vault::confirm(&mut v, id, clock, sc.ctx()); + ts::return_shared(v); +} + +fun execute(sc: &mut Scenario, who: address, id: u64, clock: &Clock) { + ts::next_tx(sc, who); + let mut v = ts::take_shared(sc); + vault::execute_transfer(&mut v, id, clock, sc.ctx()); + ts::return_shared(v); +} + +fun balance(sc: &mut Scenario) : u64 { + ts::next_tx(sc, OWNER); + let v = ts::take_shared(sc); + let b = vault::balance_of(&v); + ts::return_shared(v); + b +} + +fun status(sc: &mut Scenario, id: u64): u8 { + ts::next_tx(sc, OWNER); + let v = ts::take_shared(sc); + let s = vault::proposal_status(&v, id); + ts::return_shared(v); + s +} + +fun current_mode(sc: &mut Scenario): u8 { + ts::next_tx(sc, OWNER); + let v = ts::take_shared(sc); + let m = vault::mode(&v); + ts::return_shared(v); + m +} + +fun tick(clock: &mut Clock, by: u64) { + let now = clock::timestamp_ms(clock); + clock::set_for_testing(clock, now + by); +} + +/// Past the activation delay of the recipient seeded at setup, so routine payments are routine. +fun settled_clock(sc: &mut Scenario): Clock { + let mut clock = clock::create_for_testing(sc.ctx()); + clock::set_for_testing(&mut clock, T0); + clock +} + +// ---------------------------------------------------------------- the ordinary case +#[test] +fun a_routine_payment_needs_one_approval_and_no_wait() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); // the payroll address has finished activating + + let id = propose(&mut sc, OWNER, 500, BOB, &clock); + approve(&mut sc, OWNER, id, &clock); + execute(&mut sc, OWNER, id, &clock); + + assert!(status(&mut sc, id) == types::status_executed(), 0); + assert!(balance(&mut sc) == TREASURY - 500, 1); + + ts::next_tx(&mut sc, BOB); + let paid = ts::take_from_sender>(&sc); + assert!(coin::value(&paid) == 500, 2); + ts::return_to_sender(&sc, paid); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +#[test] +fun a_preview_matches_what_the_vault_will_demand() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + + ts::next_tx(&mut sc, OWNER); + let v = ts::take_shared(&sc); + // 5_000 is above the routine maximum, so it is high tier: two approvals and an hour's wait. + let (tier, reasons, _, approvals, guardians, delay) = + vault::preview_transfer(&v, 5_000, BOB, &clock); + assert!(tier == types::tier_high(), 0); + assert!(reasons & types::reason_amount_high() != 0, 1); + assert!(approvals == 2 && guardians == 0 && delay == HOUR, 2); + ts::return_shared(v); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +#[test] +fun an_unknown_recipient_is_escalated_without_being_registered() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + + ts::next_tx(&mut sc, OWNER); + let v = ts::take_shared(&sc); + let (tier, reasons, _, approvals, _, _) = + vault::preview_transfer(&v, 500, EVE, &clock); + assert!(tier == types::tier_high(), 0); + assert!(reasons & types::reason_recipient_unknown() != 0, 1); + assert!(approvals == 2, 2); + ts::return_shared(v); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +// ---------------------------------------------------------------- stolen credentials +/// One stolen approver key cannot clear a high-tier payment, whatever it signs. +#[test] +#[expected_failure(abort_code = E_NOT_ENOUGH_APPROVALS, location = skur::vault)] +fun one_signer_cannot_clear_a_high_tier_payment() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + + let id = propose(&mut sc, OWNER, 5_000, BOB, &clock); + approve(&mut sc, OWNER, id, &clock); + tick(&mut clock, 2 * HOUR); + execute(&mut sc, OWNER, id, &clock); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +/// A signer removed after approving stops counting. The approval set is recounted against the +/// live roster at execution, not trusted as a tally frozen at approval time. +#[test] +#[expected_failure(abort_code = E_NOT_ENOUGH_APPROVALS, location = skur::vault)] +fun a_removed_signers_approval_stops_counting() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + + let id = propose(&mut sc, OWNER, 5_000, BOB, &clock); + approve(&mut sc, OWNER, id, &clock); + approve(&mut sc, APPROVER, id, &clock); + + // Owners remove the approver. Removing a role grants nothing, so it takes effect at once. + ts::next_tx(&mut sc, OWNER); + let mut v = ts::take_shared(&sc); + let gid = vault::propose_member(&mut v, APPROVER, 0, &clock, sc.ctx()); + ts::return_shared(v); + approve(&mut sc, OWNER, gid, &clock); + approve(&mut sc, OWNER2, gid, &clock); + ts::next_tx(&mut sc, OWNER); + let mut v2 = ts::take_shared(&sc); + vault::execute_member(&mut v2, gid, &clock, sc.ctx()); + assert!(vault::member_roles(&v2, APPROVER) == 0, 0); + ts::return_shared(v2); + + tick(&mut clock, 2 * HOUR); + execute(&mut sc, OWNER, id, &clock); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +#[test] +#[expected_failure(abort_code = E_ALREADY_VOTED, location = skur::vault)] +fun an_approval_cannot_be_replayed() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + + let id = propose(&mut sc, OWNER, 5_000, BOB, &clock); + approve(&mut sc, OWNER, id, &clock); + approve(&mut sc, OWNER, id, &clock); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +#[test] +#[expected_failure(abort_code = E_NOT_PENDING, location = skur::vault)] +fun a_payment_cannot_execute_twice() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + + let id = propose(&mut sc, OWNER, 500, BOB, &clock); + approve(&mut sc, OWNER, id, &clock); + execute(&mut sc, OWNER, id, &clock); + execute(&mut sc, OWNER, id, &clock); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +// ---------------------------------------------------------------- turning a proposal down +/// Rejecting is exactly as hard as approving, so one signer cannot block the queue. +#[test] +fun one_rejection_does_not_turn_down_a_payment_that_needs_two() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + + let id = propose(&mut sc, OWNER, 5_000, BOB, &clock); + reject(&mut sc, OWNER, id, &clock); + assert!(status(&mut sc, id) == types::status_pending(), 0); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +/// And once it has as many rejections as it needed approvals, it is settled and the money stays. +#[test] +fun as_many_rejections_as_approvals_turns_a_payment_down() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + + let id = propose(&mut sc, OWNER, 5_000, BOB, &clock); + reject(&mut sc, OWNER, id, &clock); + reject(&mut sc, APPROVER, id, &clock); + + assert!(status(&mut sc, id) == types::status_rejected(), 0); + assert!(balance(&mut sc) == TREASURY, 1); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +#[test] +#[expected_failure(abort_code = E_NOT_PENDING, location = skur::vault)] +fun a_rejected_payment_cannot_execute() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + + let id = propose(&mut sc, OWNER, 500, BOB, &clock); + approve(&mut sc, OWNER, id, &clock); + reject(&mut sc, OWNER2, id, &clock); // routine needs one, so one rejection settles it + execute(&mut sc, OWNER, id, &clock); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +#[test] +#[expected_failure(abort_code = E_ALREADY_VOTED, location = skur::vault)] +fun a_rejection_cannot_be_replayed() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + + let id = propose(&mut sc, OWNER, 5_000, BOB, &clock); + reject(&mut sc, OWNER, id, &clock); + reject(&mut sc, OWNER, id, &clock); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +/// An agent can ask, and it cannot answer — including answering no. +#[test] +#[expected_failure(abort_code = E_NOT_APPROVER, location = skur::vault)] +fun an_agent_cannot_reject() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + + let id = propose(&mut sc, AGENT, 500, BOB, &clock); + reject(&mut sc, AGENT, id, &clock); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +/// A recovery pins no approvals, so the rejection threshold has to be floored at one rather than +/// settling on an empty set. +#[test] +fun one_owner_can_reject_a_recovery() { + let mut sc = ts::begin(OWNER); + let clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + + ts::next_tx(&mut sc, GUARD); + let mut v = ts::take_shared(&sc); + let id = vault::propose_recovery(&mut v, APPROVER, NEWKEY, &clock, sc.ctx()); + ts::return_shared(v); + + reject(&mut sc, OWNER, id, &clock); + assert!(status(&mut sc, id) == types::status_rejected(), 0); + // The roster is untouched. + ts::next_tx(&mut sc, OWNER); + let v2 = ts::take_shared(&sc); + assert!(vault::member_roles(&v2, APPROVER) == types::role_approver(), 1); + assert!(vault::member_roles(&v2, NEWKEY) == 0, 2); + ts::return_shared(v2); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +// ---------------------------------------------------------------- recipients +/// A brand-new destination cannot be paid until its activation delay has run, however many +/// signatures it collects. +#[test] +#[expected_failure(abort_code = E_RECIPIENT_IN_PROBATION, location = skur::vault)] +fun a_new_recipient_cannot_skip_activation() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + fund(&mut sc, &clock, TREASURY); + + let id = propose(&mut sc, OWNER, 500, BOB, &clock); + approve(&mut sc, OWNER, id, &clock); + approve(&mut sc, APPROVER, id, &clock); + tick(&mut clock, 2 * HOUR); // past the tier delay, nowhere near activation + execute(&mut sc, OWNER, id, &clock); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +/// A guardian can block a destination while a payment to it is already approved and waiting. +#[test] +#[expected_failure(abort_code = E_RECIPIENT_BLOCKED, location = skur::vault)] +fun a_guardian_can_block_a_recipient_mid_flight() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + + let id = propose(&mut sc, OWNER, 500, BOB, &clock); + approve(&mut sc, OWNER, id, &clock); + + ts::next_tx(&mut sc, GUARD); + let mut v = ts::take_shared(&sc); + vault::guardian_restrict(&mut v, BOB, types::trust_blocked(), &clock, sc.ctx()); + ts::return_shared(v); + + execute(&mut sc, OWNER, id, &clock); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +#[test] +fun registering_a_recipient_starts_it_at_the_restrictive_default() { + let mut sc = ts::begin(OWNER); + let clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + + ts::next_tx(&mut sc, OWNER); + let mut v = ts::take_shared(&sc); + vault::register_recipient(&mut v, EVE, string::utf8(b"Vendor"), &clock, sc.ctx()); + assert!(vault::recipient_trust(&v, EVE) == types::trust_new(), 0); + assert!(vault::recipient_activates_at(&v, EVE) == T0 + 12 * HOUR, 1); + ts::return_shared(v); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +// ---------------------------------------------------------------- caps +#[test] +#[expected_failure(abort_code = E_PER_TX_CAP, location = skur::vault)] +fun the_per_transaction_cap_refuses_at_proposal_time() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 1_000, 100_000, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + propose(&mut sc, OWNER, 2_000, BOB, &clock); + clock::destroy_for_testing(clock); + ts::end(sc); +} + +/// Splitting a withdrawal into pieces does not beat the daily cap: the day's outflow is counted +/// cumulatively, and the last piece is refused. +#[test] +#[expected_failure(abort_code = E_DAILY_CAP, location = skur::vault)] +fun splitting_a_drain_does_not_beat_the_daily_cap() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 0, 1_200, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + + let a = propose(&mut sc, OWNER, 600, BOB, &clock); + approve(&mut sc, OWNER, a, &clock); + execute(&mut sc, OWNER, a, &clock); + + // Half the day's allowance is now gone, so the next piece is escalated and waits an hour. + let b = propose(&mut sc, OWNER, 600, BOB, &clock); + approve(&mut sc, OWNER, b, &clock); + approve(&mut sc, APPROVER, b, &clock); + tick(&mut clock, 2 * HOUR); + execute(&mut sc, OWNER, b, &clock); + assert!(balance(&mut sc) == TREASURY - 1_200, 0); + + propose(&mut sc, OWNER, 1, BOB, &clock); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +#[test] +fun the_daily_bucket_reopens_after_a_day() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 0, 1_200, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + + let a = propose(&mut sc, OWNER, 1_000, BOB, &clock); + approve(&mut sc, OWNER, a, &clock); + approve(&mut sc, APPROVER, a, &clock); + tick(&mut clock, 2 * HOUR); + execute(&mut sc, OWNER, a, &clock); + + tick(&mut clock, DAY + HOUR); + let b = propose(&mut sc, OWNER, 1_000, BOB, &clock); + approve(&mut sc, OWNER, b, &clock); + approve(&mut sc, APPROVER, b, &clock); + tick(&mut clock, 2 * HOUR); + execute(&mut sc, OWNER, b, &clock); + assert!(balance(&mut sc) == TREASURY - 2_000, 0); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +/// Above the hard block no amount of authorization is enough. It is the one control with no +/// override path at all. +#[test] +#[expected_failure(abort_code = E_HARD_BLOCK, location = skur::vault)] +fun the_hard_block_cannot_be_approved_around() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 0, 0, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + propose(&mut sc, OWNER, 950_000, BOB, &clock); + clock::destroy_for_testing(clock); + ts::end(sc); +} + +// ---------------------------------------------------------------- the circuit breaker +/// The envelope is the last gate. A payment that would cross it is not made, the money stays +/// where it is, and the vault latches into Lockdown — which has to persist, so the transaction +/// succeeds rather than reverting. +#[test] +fun crossing_the_loss_envelope_freezes_the_vault_instead_of_paying() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, make_policy(1_000, 9_000, HOUR), 0, 0, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + + // 200_000 is 20% of the treasury; the envelope allows 10% per day. + let id = propose(&mut sc, OWNER, 200_000, BOB, &clock); + approve(&mut sc, OWNER, id, &clock); + approve(&mut sc, OWNER2, id, &clock); + approve(&mut sc, APPROVER, id, &clock); + confirm(&mut sc, GUARD, id, &clock); + tick(&mut clock, DAY + HOUR); + execute(&mut sc, OWNER, id, &clock); + + assert!(status(&mut sc, id) == types::status_blocked(), 0); + assert!(balance(&mut sc) == TREASURY, 1); + assert!(current_mode(&mut sc) == types::mode_lockdown(), 2); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +#[test] +#[expected_failure(abort_code = E_LOCKDOWN, location = skur::vault)] +fun lockdown_stops_new_payments_from_being_opened() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, make_policy(1_000, 9_000, HOUR), 0, 0, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + + let id = propose(&mut sc, OWNER, 200_000, BOB, &clock); + approve(&mut sc, OWNER, id, &clock); + approve(&mut sc, OWNER2, id, &clock); + approve(&mut sc, APPROVER, id, &clock); + confirm(&mut sc, GUARD, id, &clock); + tick(&mut clock, DAY + HOUR); + execute(&mut sc, OWNER, id, &clock); + + propose(&mut sc, OWNER, 100, BOB, &clock); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +/// Leaving Lockdown takes owners, a guardian and time, in that order. Whoever caused the freeze +/// cannot lift it alone. +#[test] +fun leaving_lockdown_takes_owners_a_guardian_and_time() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, make_policy(1_000, 9_000, HOUR), 0, 0, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + + let id = propose(&mut sc, OWNER, 200_000, BOB, &clock); + approve(&mut sc, OWNER, id, &clock); + approve(&mut sc, OWNER2, id, &clock); + approve(&mut sc, APPROVER, id, &clock); + confirm(&mut sc, GUARD, id, &clock); + tick(&mut clock, DAY + HOUR); + execute(&mut sc, OWNER, id, &clock); + assert!(current_mode(&mut sc) == types::mode_lockdown(), 0); + + ts::next_tx(&mut sc, OWNER); + let mut v = ts::take_shared(&sc); + let rid = vault::propose_mode_relax(&mut v, types::mode_normal(), &clock, sc.ctx()); + ts::return_shared(v); + approve(&mut sc, OWNER, rid, &clock); + approve(&mut sc, OWNER2, rid, &clock); + confirm(&mut sc, GUARD, rid, &clock); + + tick(&mut clock, 2 * DAY + HOUR); + ts::next_tx(&mut sc, OWNER); + let mut v2 = ts::take_shared(&sc); + vault::execute_mode_relax(&mut v2, rid, &clock, sc.ctx()); + assert!(vault::mode(&v2) == types::mode_normal(), 1); + assert!(vault::posture_reasons(&v2) == 0, 2); + ts::return_shared(v2); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +/// Every owner can agree and the freeze still holds: a guardian has to sign it off. +#[test] +#[expected_failure(abort_code = E_NOT_ENOUGH_GUARDIANS, location = skur::vault)] +fun owners_alone_cannot_lift_a_freeze() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + fund(&mut sc, &clock, TREASURY); + + ts::next_tx(&mut sc, GUARD); + let mut v = ts::take_shared(&sc); + vault::raise_mode(&mut v, types::mode_lockdown(), 0, &clock, sc.ctx()); + ts::return_shared(v); + + ts::next_tx(&mut sc, OWNER); + let mut v2 = ts::take_shared(&sc); + let rid = vault::propose_mode_relax(&mut v2, types::mode_normal(), &clock, sc.ctx()); + ts::return_shared(v2); + + approve(&mut sc, OWNER, rid, &clock); + approve(&mut sc, OWNER2, rid, &clock); + + tick(&mut clock, 2 * DAY + HOUR); + ts::next_tx(&mut sc, OWNER); + let mut v3 = ts::take_shared(&sc); + vault::execute_mode_relax(&mut v3, rid, &clock, sc.ctx()); + ts::return_shared(v3); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +// ---------------------------------------------------------------- guardians +#[test] +fun a_guardian_veto_stops_a_critical_payment_and_raises_the_posture() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + + let id = propose(&mut sc, OWNER, 20_000, BOB, &clock); + ts::next_tx(&mut sc, GUARD); + let mut v = ts::take_shared(&sc); + vault::veto(&mut v, id, &clock, sc.ctx()); + ts::return_shared(v); + + assert!(status(&mut sc, id) == types::status_vetoed(), 0); + assert!(current_mode(&mut sc) == types::mode_elevated(), 1); + assert!(balance(&mut sc) == TREASURY, 2); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +/// A guardian has no treasury role and so no way to open a payment, let alone execute one. +#[test] +#[expected_failure(abort_code = E_CANNOT_PROPOSE, location = skur::vault)] +fun a_guardian_cannot_open_a_payment() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + propose(&mut sc, GUARD, 100, BOB, &clock); + clock::destroy_for_testing(clock); + ts::end(sc); +} + +// ---------------------------------------------------------------- the agent proposer +/// An automated agent holds the proposer role: it can ask the treasury to pay, and that is all. +#[test] +fun an_agent_can_open_a_payment_that_people_still_have_to_clear() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + + let id = propose(&mut sc, AGENT, 500, BOB, &clock); + approve(&mut sc, OWNER, id, &clock); + execute(&mut sc, OWNER, id, &clock); + assert!(status(&mut sc, id) == types::status_executed(), 0); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +#[test] +#[expected_failure(abort_code = E_NOT_APPROVER, location = skur::vault)] +fun an_agent_cannot_approve_its_own_proposal() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + let id = propose(&mut sc, AGENT, 500, BOB, &clock); + approve(&mut sc, AGENT, id, &clock); + clock::destroy_for_testing(clock); + ts::end(sc); +} + +#[test] +#[expected_failure(abort_code = E_NOT_EXECUTOR, location = skur::vault)] +fun an_agent_cannot_execute() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + let id = propose(&mut sc, AGENT, 500, BOB, &clock); + approve(&mut sc, OWNER, id, &clock); + execute(&mut sc, AGENT, id, &clock); + clock::destroy_for_testing(clock); + ts::end(sc); +} + +// ---------------------------------------------------------------- the policy firewall +#[test] +fun tightening_the_policy_takes_effect_as_soon_as_owners_agree() { + let mut sc = ts::begin(OWNER); + let clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + + let tighter = make_policy(2_000, 8_000, 2 * HOUR); + ts::next_tx(&mut sc, OWNER); + let mut v = ts::take_shared(&sc); + let id = vault::propose_policy(&mut v, tighter, &clock, sc.ctx()); + ts::return_shared(v); + approve(&mut sc, OWNER, id, &clock); + approve(&mut sc, OWNER2, id, &clock); + + ts::next_tx(&mut sc, OWNER); + let mut v2 = ts::take_shared(&sc); + vault::execute_policy(&mut v2, id, &clock, sc.ctx()); + assert!(vault::policy_version(&v2) == 2, 0); + assert!(policy::delay_high(vault::policy(&v2)) == 2 * HOUR, 1); + ts::return_shared(v2); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +/// Weakening the vault waits out the policy-change delay, even with every owner's signature. +#[test] +#[expected_failure(abort_code = E_TOO_EARLY, location = skur::vault)] +fun loosening_the_policy_has_to_wait() { + let mut sc = ts::begin(OWNER); + let clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + + let looser = make_policy(3_000, 9_000, HOUR / 2); + ts::next_tx(&mut sc, OWNER); + let mut v = ts::take_shared(&sc); + let id = vault::propose_policy(&mut v, looser, &clock, sc.ctx()); + ts::return_shared(v); + approve(&mut sc, OWNER, id, &clock); + approve(&mut sc, OWNER2, id, &clock); + + ts::next_tx(&mut sc, OWNER); + let mut v2 = ts::take_shared(&sc); + vault::execute_policy(&mut v2, id, &clock, sc.ctx()); + ts::return_shared(v2); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +#[test] +fun loosening_the_policy_succeeds_once_the_delay_has_run() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + + let looser = make_policy(3_000, 9_000, HOUR / 2); + ts::next_tx(&mut sc, OWNER); + let mut v = ts::take_shared(&sc); + let id = vault::propose_policy(&mut v, looser, &clock, sc.ctx()); + ts::return_shared(v); + approve(&mut sc, OWNER, id, &clock); + approve(&mut sc, OWNER2, id, &clock); + + tick(&mut clock, 2 * DAY + HOUR); + ts::next_tx(&mut sc, OWNER); + let mut v2 = ts::take_shared(&sc); + vault::execute_policy(&mut v2, id, &clock, sc.ctx()); + assert!(policy::delay_high(vault::policy(&v2)) == HOUR / 2, 0); + ts::return_shared(v2); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +/// And a guardian can stop it outright inside that window. +#[test] +fun a_guardian_can_veto_a_weakening() { + let mut sc = ts::begin(OWNER); + let clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + + let looser = make_policy(3_000, 9_000, HOUR / 2); + ts::next_tx(&mut sc, OWNER); + let mut v = ts::take_shared(&sc); + let id = vault::propose_policy(&mut v, looser, &clock, sc.ctx()); + ts::return_shared(v); + approve(&mut sc, OWNER, id, &clock); + approve(&mut sc, OWNER2, id, &clock); + + ts::next_tx(&mut sc, GUARD); + let mut v2 = ts::take_shared(&sc); + vault::veto(&mut v2, id, &clock, sc.ctx()); + ts::return_shared(v2); + assert!(status(&mut sc, id) == types::status_vetoed(), 0); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +// ---------------------------------------------------------------- recovery +/// A lost key is replaced by guardians, after a delay, with the same roles and nothing else +/// changed — and the vault comes out of it in a raised posture. +#[test] +fun recovery_moves_the_roles_and_leaves_the_vault_elevated() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + + ts::next_tx(&mut sc, GUARD); + let mut v = ts::take_shared(&sc); + let id = vault::propose_recovery(&mut v, APPROVER, NEWKEY, &clock, sc.ctx()); + ts::return_shared(v); + confirm(&mut sc, GUARD, id, &clock); + + tick(&mut clock, 3 * DAY + HOUR); + ts::next_tx(&mut sc, OWNER); + let mut v2 = ts::take_shared(&sc); + vault::execute_recovery(&mut v2, id, &clock, sc.ctx()); + assert!(vault::member_roles(&v2, APPROVER) == 0, 0); + assert!(vault::member_roles(&v2, NEWKEY) == types::role_approver(), 1); + assert!(vault::policy_version(&v2) == 1, 2); // the policy is untouched + assert!(vault::mode(&v2) == types::mode_elevated(), 3); + ts::return_shared(v2); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +#[test] +#[expected_failure(abort_code = E_TOO_EARLY, location = skur::vault)] +fun recovery_cannot_skip_its_delay() { + let mut sc = ts::begin(OWNER); + let clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + + ts::next_tx(&mut sc, GUARD); + let mut v = ts::take_shared(&sc); + let id = vault::propose_recovery(&mut v, APPROVER, NEWKEY, &clock, sc.ctx()); + ts::return_shared(v); + confirm(&mut sc, GUARD, id, &clock); + + ts::next_tx(&mut sc, OWNER); + let mut v2 = ts::take_shared(&sc); + vault::execute_recovery(&mut v2, id, &clock, sc.ctx()); + ts::return_shared(v2); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +/// A compromised guardian key cannot quietly install a new signer: any owner cancels it. +#[test] +fun any_owner_can_cancel_a_recovery() { + let mut sc = ts::begin(OWNER); + let clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + + ts::next_tx(&mut sc, GUARD); + let mut v = ts::take_shared(&sc); + let id = vault::propose_recovery(&mut v, APPROVER, NEWKEY, &clock, sc.ctx()); + ts::return_shared(v); + + ts::next_tx(&mut sc, OWNER2); + let mut v2 = ts::take_shared(&sc); + vault::cancel(&mut v2, id, &clock, sc.ctx()); + ts::return_shared(v2); + assert!(status(&mut sc, id) == types::status_cancelled(), 0); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +// ---------------------------------------------------------------- lifetimes +#[test] +fun a_proposal_expires_and_cannot_be_revived() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + + let id = propose(&mut sc, OWNER, 500, BOB, &clock); + approve(&mut sc, OWNER, id, &clock); + tick(&mut clock, 31 * DAY); + + ts::next_tx(&mut sc, EVE); + let mut v = ts::take_shared(&sc); + vault::expire(&mut v, id, &clock, sc.ctx()); + ts::return_shared(v); + assert!(status(&mut sc, id) == types::status_expired(), 0); + assert!(balance(&mut sc) == TREASURY, 1); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +#[test] +#[expected_failure(abort_code = E_EXPIRED, location = skur::vault)] +fun an_expired_proposal_cannot_execute() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + + let id = propose(&mut sc, OWNER, 500, BOB, &clock); + approve(&mut sc, OWNER, id, &clock); + tick(&mut clock, 31 * DAY); + execute(&mut sc, OWNER, id, &clock); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +// ---------------------------------------------------------------- posture +/// Elevated halves the caps and escalates every tier, so a vault that has seen something wrong +/// keeps running but at a lower rate. +#[test] +#[expected_failure(abort_code = E_PER_TX_CAP, location = skur::vault)] +fun elevated_halves_the_caps() { + let mut sc = ts::begin(OWNER); + let mut clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 1_000, 100_000, &clock); + fund(&mut sc, &clock, TREASURY); + tick(&mut clock, 13 * HOUR); + + ts::next_tx(&mut sc, GUARD); + let mut v = ts::take_shared(&sc); + vault::raise_mode(&mut v, types::mode_elevated(), 0, &clock, sc.ctx()); + ts::return_shared(v); + + propose(&mut sc, OWNER, 800, BOB, &clock); + + clock::destroy_for_testing(clock); + ts::end(sc); +} + +#[test] +fun deposits_are_never_gated() { + let mut sc = ts::begin(OWNER); + let clock = settled_clock(&mut sc); + boot(&mut sc, base_policy(), 50_000, 100_000, &clock); + + ts::next_tx(&mut sc, GUARD); + let mut v = ts::take_shared(&sc); + vault::raise_mode(&mut v, types::mode_lockdown(), 0, &clock, sc.ctx()); + ts::return_shared(v); + + // Even frozen, and even from a stranger, money can come in. + ts::next_tx(&mut sc, EVE); + let mut v2 = ts::take_shared(&sc); + let c = coin::mint_for_testing(42, sc.ctx()); + vault::deposit(&mut v2, c, &clock, sc.ctx()); + assert!(vault::balance_of(&v2) == 42, 0); + ts::return_shared(v2); + + clock::destroy_for_testing(clock); + ts::end(sc); +} diff --git a/web/.gitignore b/web/.gitignore deleted file mode 100644 index a547bf3..0000000 --- a/web/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/web/.oxlintrc.json b/web/.oxlintrc.json deleted file mode 100644 index 6fa991d..0000000 --- a/web/.oxlintrc.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "./node_modules/oxlint/configuration_schema.json", - "plugins": ["react", "typescript", "oxc"], - "rules": { - "react/rules-of-hooks": "error", - "react/only-export-components": ["warn", { "allowConstantExport": true }] - } -} diff --git a/web/README.md b/web/README.md deleted file mode 100644 index d6af7e3..0000000 --- a/web/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) - -## React Compiler - -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). - -## Expanding the Oxlint configuration - -If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`: - -```json -{ - "$schema": "./node_modules/oxlint/configuration_schema.json", - "plugins": ["react", "typescript", "oxc"], - "options": { - "typeAware": true - }, - "rules": { - "react/rules-of-hooks": "error", - "react/only-export-components": ["warn", { "allowConstantExport": true }] - } -} -``` - -See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories. diff --git a/web/index.html b/web/index.html deleted file mode 100644 index 5717bc8..0000000 --- a/web/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Skur · Treasury security - - -
- - - diff --git a/web/package-lock.json b/web/package-lock.json deleted file mode 100644 index 493a0ec..0000000 --- a/web/package-lock.json +++ /dev/null @@ -1,9434 +0,0 @@ -{ - "name": "web", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "web", - "version": "0.0.0", - "dependencies": { - "@tanstack/react-query": "^5.102.8", - "react": "^19.2.8", - "react-dom": "^19.2.8", - "viem": "^2.56.5", - "wagmi": "^2.19.5" - }, - "devDependencies": { - "@types/node": "^24.13.3", - "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.7", - "@vitejs/plugin-react": "^6.1.1", - "oxlint": "^1.81.0", - "typescript": "~6.0.2", - "vite": "^8.3.0", - "vitest": "^3.2.7" - } - }, - "node_modules/@adraffy/ens-normalize": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", - "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", - "license": "MIT" - }, - "node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@base-org/account": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@base-org/account/-/account-2.4.0.tgz", - "integrity": "sha512-A4Umpi8B9/pqR78D1Yoze4xHyQaujioVRqqO3d6xuDFw9VRtjg6tK3bPlwE0aW+nVH/ntllCpPa2PbI8Rnjcug==", - "license": "Apache-2.0", - "dependencies": { - "@coinbase/cdp-sdk": "^1.0.0", - "@noble/hashes": "1.4.0", - "clsx": "1.2.1", - "eventemitter3": "5.0.1", - "idb-keyval": "6.2.1", - "ox": "0.6.9", - "preact": "10.24.2", - "viem": "^2.31.7", - "zustand": "5.0.3" - } - }, - "node_modules/@base-org/account/node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@base-org/account/node_modules/ox": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.9.tgz", - "integrity": "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.10.1", - "@noble/curves": "^1.6.0", - "@noble/hashes": "^1.5.0", - "@scure/bip32": "^1.5.0", - "@scure/bip39": "^1.4.0", - "abitype": "^1.0.6", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@base-org/account/node_modules/ox/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@coinbase/cdp-sdk": { - "version": "1.56.0", - "resolved": "https://registry.npmjs.org/@coinbase/cdp-sdk/-/cdp-sdk-1.56.0.tgz", - "integrity": "sha512-Fwq77OUHRfSihdi/IB3sp2eVplv9UQjiXoZfpU0mYmxD42CRbVINQT9PVrBbn6fxqS9Vlc4XdaB166IPBVfXkA==", - "license": "MIT", - "dependencies": { - "@solana-program/system": "^0.10.0", - "@solana-program/token": "^0.9.0", - "@solana/kit": "^5.5.1", - "abitype": "1.0.6", - "axios": "^1.18.0", - "axios-retry": "^4.5.0", - "bs58": "^6.0.0", - "jose": "^6.2.0", - "md5": "^2.3.0", - "uncrypto": "^0.1.3", - "viem": "^2.47.0", - "zod": "^3.25.76" - }, - "peerDependencies": { - "@x402/core": "^2.25.0", - "@x402/evm": "^2.25.0", - "@x402/extensions": "^2.25.0", - "@x402/svm": "^2.25.0" - }, - "peerDependenciesMeta": { - "@x402/core": { - "optional": true - }, - "@x402/evm": { - "optional": true - }, - "@x402/extensions": { - "optional": true - }, - "@x402/svm": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana-program/system": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@solana-program/system/-/system-0.10.0.tgz", - "integrity": "sha512-Go+LOEZmqmNlfr+Gjy5ZWAdY5HbYzk2RBewD9QinEU/bBSzpFfzqDRT55JjFRBGJUvMgf3C2vfXEGT4i8DSI4g==", - "license": "Apache-2.0", - "peerDependencies": { - "@solana/kit": "^5.0" - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana-program/token": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@solana-program/token/-/token-0.9.0.tgz", - "integrity": "sha512-vnZxndd4ED4Fc56sw93cWZ2djEeeOFxtaPS8SPf5+a+JZjKA/EnKqzbE1y04FuMhIVrLERQ8uR8H2h72eZzlsA==", - "license": "Apache-2.0", - "peerDependencies": { - "@solana/kit": "^5.0" - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/accounts": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/accounts/-/accounts-5.5.1.tgz", - "integrity": "sha512-TfOY9xixg5rizABuLVuZ9XI2x2tmWUC/OoN556xwfDlhBHBjKfszicYYOyD6nbFmwTGYarCmyGIdteXxTXIdhQ==", - "license": "MIT", - "dependencies": { - "@solana/addresses": "5.5.1", - "@solana/codecs-core": "5.5.1", - "@solana/codecs-strings": "5.5.1", - "@solana/errors": "5.5.1", - "@solana/rpc-spec": "5.5.1", - "@solana/rpc-types": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/addresses": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/addresses/-/addresses-5.5.1.tgz", - "integrity": "sha512-5xoah3Q9G30HQghu/9BiHLb5pzlPKRC3zydQDmE3O9H//WfayxTFppsUDCL6FjYUHqj/wzK6CWHySglc2RkpdA==", - "license": "MIT", - "dependencies": { - "@solana/assertions": "5.5.1", - "@solana/codecs-core": "5.5.1", - "@solana/codecs-strings": "5.5.1", - "@solana/errors": "5.5.1", - "@solana/nominal-types": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/assertions": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/assertions/-/assertions-5.5.1.tgz", - "integrity": "sha512-YTCSWAlGwSlVPnWtWLm3ukz81wH4j2YaCveK+TjpvUU88hTy6fmUqxi0+hvAMAe4zKXpJyj3Az7BrLJRxbIm4Q==", - "license": "MIT", - "dependencies": { - "@solana/errors": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/codecs": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/codecs/-/codecs-5.5.1.tgz", - "integrity": "sha512-Vea29nJub/bXjfzEV7ZZQ/PWr1pYLZo3z0qW0LQL37uKKVzVFRQlwetd7INk3YtTD3xm9WUYr7bCvYUk3uKy2g==", - "license": "MIT", - "dependencies": { - "@solana/codecs-core": "5.5.1", - "@solana/codecs-data-structures": "5.5.1", - "@solana/codecs-numbers": "5.5.1", - "@solana/codecs-strings": "5.5.1", - "@solana/options": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/codecs-core": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-5.5.1.tgz", - "integrity": "sha512-TgBt//bbKBct0t6/MpA8ElaOA3sa8eYVvR7LGslCZ84WiAwwjCY0lW/lOYsFHJQzwREMdUyuEyy5YWBKtdh8Rw==", - "license": "MIT", - "dependencies": { - "@solana/errors": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/codecs-data-structures": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/codecs-data-structures/-/codecs-data-structures-5.5.1.tgz", - "integrity": "sha512-97bJWGyUY9WvBz3mX1UV3YPWGDTez6btCfD0ip3UVEXJbItVuUiOkzcO5iFDUtQT5riKT6xC+Mzl+0nO76gd0w==", - "license": "MIT", - "dependencies": { - "@solana/codecs-core": "5.5.1", - "@solana/codecs-numbers": "5.5.1", - "@solana/errors": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/codecs-numbers": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-5.5.1.tgz", - "integrity": "sha512-rllMIZAHqmtvC0HO/dc/21wDuWaD0B8Ryv8o+YtsICQBuiL/0U4AGwH7Pi5GNFySYk0/crSuwfIqQFtmxNSPFw==", - "license": "MIT", - "dependencies": { - "@solana/codecs-core": "5.5.1", - "@solana/errors": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/codecs-strings": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/codecs-strings/-/codecs-strings-5.5.1.tgz", - "integrity": "sha512-7klX4AhfHYA+uKKC/nxRGP2MntbYQCR3N6+v7bk1W/rSxYuhNmt+FN8aoThSZtWIKwN6BEyR1167ka8Co1+E7A==", - "license": "MIT", - "dependencies": { - "@solana/codecs-core": "5.5.1", - "@solana/codecs-numbers": "5.5.1", - "@solana/errors": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "fastestsmallesttextencoderdecoder": "^1.0.22", - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "fastestsmallesttextencoderdecoder": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/errors": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-5.5.1.tgz", - "integrity": "sha512-vFO3p+S7HoyyrcAectnXbdsMfwUzY2zYFUc2DEe5BwpiE9J1IAxPBGjOWO6hL1bbYdBrlmjNx8DXCslqS+Kcmg==", - "license": "MIT", - "dependencies": { - "chalk": "5.6.2", - "commander": "14.0.2" - }, - "bin": { - "errors": "bin/cli.mjs" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/fast-stable-stringify": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/fast-stable-stringify/-/fast-stable-stringify-5.5.1.tgz", - "integrity": "sha512-Ni7s2FN33zTzhTFgRjEbOVFO+UAmK8qi3Iu0/GRFYK4jN696OjKHnboSQH/EacQ+yGqS54bfxf409wU5dsLLCw==", - "license": "MIT", - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/functional": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/functional/-/functional-5.5.1.tgz", - "integrity": "sha512-tTHoJcEQq3gQx5qsdsDJ0LEJeFzwNpXD80xApW9o/PPoCNimI3SALkZl+zNW8VnxRrV3l3yYvfHWBKe/X3WG3w==", - "license": "MIT", - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/instruction-plans": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/instruction-plans/-/instruction-plans-5.5.1.tgz", - "integrity": "sha512-7z3CB7YMcFKuVvgcnNY8bY6IsZ8LG61Iytbz7HpNVGX2u1RthOs1tRW8luTzSG1MPL0Ox7afyAVMYeFqSPHnaQ==", - "license": "MIT", - "dependencies": { - "@solana/errors": "5.5.1", - "@solana/instructions": "5.5.1", - "@solana/keys": "5.5.1", - "@solana/promises": "5.5.1", - "@solana/transaction-messages": "5.5.1", - "@solana/transactions": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/instructions": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/instructions/-/instructions-5.5.1.tgz", - "integrity": "sha512-h0G1CG6S+gUUSt0eo6rOtsaXRBwCq1+Js2a+Ps9Bzk9q7YHNFA75/X0NWugWLgC92waRp66hrjMTiYYnLBoWOQ==", - "license": "MIT", - "dependencies": { - "@solana/codecs-core": "5.5.1", - "@solana/errors": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/keys": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/keys/-/keys-5.5.1.tgz", - "integrity": "sha512-KRD61cL7CRL+b4r/eB9dEoVxIf/2EJ1Pm1DmRYhtSUAJD2dJ5Xw8QFuehobOGm9URqQ7gaQl+Fkc1qvDlsWqKg==", - "license": "MIT", - "dependencies": { - "@solana/assertions": "5.5.1", - "@solana/codecs-core": "5.5.1", - "@solana/codecs-strings": "5.5.1", - "@solana/errors": "5.5.1", - "@solana/nominal-types": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/kit": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/kit/-/kit-5.5.1.tgz", - "integrity": "sha512-irKUGiV2yRoyf+4eGQ/ZeCRxa43yjFEL1DUI5B0DkcfZw3cr0VJtVJnrG8OtVF01vT0OUfYOcUn6zJW5TROHvQ==", - "license": "MIT", - "dependencies": { - "@solana/accounts": "5.5.1", - "@solana/addresses": "5.5.1", - "@solana/codecs": "5.5.1", - "@solana/errors": "5.5.1", - "@solana/functional": "5.5.1", - "@solana/instruction-plans": "5.5.1", - "@solana/instructions": "5.5.1", - "@solana/keys": "5.5.1", - "@solana/offchain-messages": "5.5.1", - "@solana/plugin-core": "5.5.1", - "@solana/programs": "5.5.1", - "@solana/rpc": "5.5.1", - "@solana/rpc-api": "5.5.1", - "@solana/rpc-parsed-types": "5.5.1", - "@solana/rpc-spec-types": "5.5.1", - "@solana/rpc-subscriptions": "5.5.1", - "@solana/rpc-types": "5.5.1", - "@solana/signers": "5.5.1", - "@solana/sysvars": "5.5.1", - "@solana/transaction-confirmation": "5.5.1", - "@solana/transaction-messages": "5.5.1", - "@solana/transactions": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/nominal-types": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/nominal-types/-/nominal-types-5.5.1.tgz", - "integrity": "sha512-I1ImR+kfrLFxN5z22UDiTWLdRZeKtU0J/pkWkO8qm/8WxveiwdIv4hooi8pb6JnlR4mSrWhq0pCIOxDYrL9GIQ==", - "license": "MIT", - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/offchain-messages": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/offchain-messages/-/offchain-messages-5.5.1.tgz", - "integrity": "sha512-g+xHH95prTU+KujtbOzj8wn+C7ZNoiLhf3hj6nYq3MTyxOXtBEysguc97jJveUZG0K97aIKG6xVUlMutg5yxhw==", - "license": "MIT", - "dependencies": { - "@solana/addresses": "5.5.1", - "@solana/codecs-core": "5.5.1", - "@solana/codecs-data-structures": "5.5.1", - "@solana/codecs-numbers": "5.5.1", - "@solana/codecs-strings": "5.5.1", - "@solana/errors": "5.5.1", - "@solana/keys": "5.5.1", - "@solana/nominal-types": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/options": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/options/-/options-5.5.1.tgz", - "integrity": "sha512-eo971c9iLNLmk+yOFyo7yKIJzJ/zou6uKpy6mBuyb/thKtS/haiKIc3VLhyTXty3OH2PW8yOlORJnv4DexJB8A==", - "license": "MIT", - "dependencies": { - "@solana/codecs-core": "5.5.1", - "@solana/codecs-data-structures": "5.5.1", - "@solana/codecs-numbers": "5.5.1", - "@solana/codecs-strings": "5.5.1", - "@solana/errors": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/plugin-core": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/plugin-core/-/plugin-core-5.5.1.tgz", - "integrity": "sha512-VUZl30lDQFJeiSyNfzU1EjYt2QZvoBFKEwjn1lilUJw7KgqD5z7mbV7diJhT+dLFs36i0OsjXvq5kSygn8YJ3A==", - "license": "MIT", - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/programs": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/programs/-/programs-5.5.1.tgz", - "integrity": "sha512-7U9kn0Jsx1NuBLn5HRTFYh78MV4XN145Yc3WP/q5BlqAVNlMoU9coG5IUTJIG847TUqC1lRto3Dnpwm6T4YRpA==", - "license": "MIT", - "dependencies": { - "@solana/addresses": "5.5.1", - "@solana/errors": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/promises": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/promises/-/promises-5.5.1.tgz", - "integrity": "sha512-T9lfuUYkGykJmppEcssNiCf6yiYQxJkhiLPP+pyAc2z84/7r3UVIb2tNJk4A9sucS66pzJnVHZKcZVGUUp6wzA==", - "license": "MIT", - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/rpc": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/rpc/-/rpc-5.5.1.tgz", - "integrity": "sha512-ku8zTUMrkCWci66PRIBC+1mXepEnZH/q1f3ck0kJZ95a06bOTl5KU7HeXWtskkyefzARJ5zvCs54AD5nxjQJ+A==", - "license": "MIT", - "dependencies": { - "@solana/errors": "5.5.1", - "@solana/fast-stable-stringify": "5.5.1", - "@solana/functional": "5.5.1", - "@solana/rpc-api": "5.5.1", - "@solana/rpc-spec": "5.5.1", - "@solana/rpc-spec-types": "5.5.1", - "@solana/rpc-transformers": "5.5.1", - "@solana/rpc-transport-http": "5.5.1", - "@solana/rpc-types": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/rpc-api": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/rpc-api/-/rpc-api-5.5.1.tgz", - "integrity": "sha512-XWOQQPhKl06Vj0xi3RYHAc6oEQd8B82okYJ04K7N0Vvy3J4PN2cxeK7klwkjgavdcN9EVkYCChm2ADAtnztKnA==", - "license": "MIT", - "dependencies": { - "@solana/addresses": "5.5.1", - "@solana/codecs-core": "5.5.1", - "@solana/codecs-strings": "5.5.1", - "@solana/errors": "5.5.1", - "@solana/keys": "5.5.1", - "@solana/rpc-parsed-types": "5.5.1", - "@solana/rpc-spec": "5.5.1", - "@solana/rpc-transformers": "5.5.1", - "@solana/rpc-types": "5.5.1", - "@solana/transaction-messages": "5.5.1", - "@solana/transactions": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/rpc-parsed-types": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/rpc-parsed-types/-/rpc-parsed-types-5.5.1.tgz", - "integrity": "sha512-HEi3G2nZqGEsa3vX6U0FrXLaqnUCg4SKIUrOe8CezD+cSFbRTOn3rCLrUmJrhVyXlHoQVaRO9mmeovk31jWxJg==", - "license": "MIT", - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/rpc-spec": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/rpc-spec/-/rpc-spec-5.5.1.tgz", - "integrity": "sha512-m3LX2bChm3E3by4mQrH4YwCAFY57QBzuUSWqlUw7ChuZ+oLLOq7b2czi4i6L4Vna67j3eCmB3e+4tqy1j5wy7Q==", - "license": "MIT", - "dependencies": { - "@solana/errors": "5.5.1", - "@solana/rpc-spec-types": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/rpc-spec-types": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/rpc-spec-types/-/rpc-spec-types-5.5.1.tgz", - "integrity": "sha512-6OFKtRpIEJQs8Jb2C4OO8KyP2h2Hy1MFhatMAoXA+0Ik8S3H+CicIuMZvGZ91mIu/tXicuOOsNNLu3HAkrakrw==", - "license": "MIT", - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/rpc-subscriptions": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions/-/rpc-subscriptions-5.5.1.tgz", - "integrity": "sha512-CTMy5bt/6mDh4tc6vUJms9EcuZj3xvK0/xq8IQ90rhkpYvate91RjBP+egvjgSayUg9yucU9vNuUpEjz4spM7w==", - "license": "MIT", - "dependencies": { - "@solana/errors": "5.5.1", - "@solana/fast-stable-stringify": "5.5.1", - "@solana/functional": "5.5.1", - "@solana/promises": "5.5.1", - "@solana/rpc-spec-types": "5.5.1", - "@solana/rpc-subscriptions-api": "5.5.1", - "@solana/rpc-subscriptions-channel-websocket": "5.5.1", - "@solana/rpc-subscriptions-spec": "5.5.1", - "@solana/rpc-transformers": "5.5.1", - "@solana/rpc-types": "5.5.1", - "@solana/subscribable": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/rpc-subscriptions-api": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions-api/-/rpc-subscriptions-api-5.5.1.tgz", - "integrity": "sha512-5Oi7k+GdeS8xR2ly1iuSFkAv6CZqwG0Z6b1QZKbEgxadE1XGSDrhM2cn59l+bqCozUWCqh4c/A2znU/qQjROlw==", - "license": "MIT", - "dependencies": { - "@solana/addresses": "5.5.1", - "@solana/keys": "5.5.1", - "@solana/rpc-subscriptions-spec": "5.5.1", - "@solana/rpc-transformers": "5.5.1", - "@solana/rpc-types": "5.5.1", - "@solana/transaction-messages": "5.5.1", - "@solana/transactions": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/rpc-subscriptions-channel-websocket": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions-channel-websocket/-/rpc-subscriptions-channel-websocket-5.5.1.tgz", - "integrity": "sha512-7tGfBBrYY8TrngOyxSHoCU5shy86iA9SRMRrPSyBhEaZRAk6dnbdpmUTez7gtdVo0BCvh9nzQtUycKWSS7PnFQ==", - "license": "MIT", - "dependencies": { - "@solana/errors": "5.5.1", - "@solana/functional": "5.5.1", - "@solana/rpc-subscriptions-spec": "5.5.1", - "@solana/subscribable": "5.5.1", - "ws": "^8.19.0" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/rpc-subscriptions-spec": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/rpc-subscriptions-spec/-/rpc-subscriptions-spec-5.5.1.tgz", - "integrity": "sha512-iq+rGq5fMKP3/mKHPNB6MC8IbVW41KGZg83Us/+LE3AWOTWV1WT20KT2iH1F1ik9roi42COv/TpoZZvhKj45XQ==", - "license": "MIT", - "dependencies": { - "@solana/errors": "5.5.1", - "@solana/promises": "5.5.1", - "@solana/rpc-spec-types": "5.5.1", - "@solana/subscribable": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/rpc-transformers": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/rpc-transformers/-/rpc-transformers-5.5.1.tgz", - "integrity": "sha512-OsWqLCQdcrRJKvHiMmwFhp9noNZ4FARuMkHT5us3ustDLXaxOjF0gfqZLnMkulSLcKt7TGXqMhBV+HCo7z5M8Q==", - "license": "MIT", - "dependencies": { - "@solana/errors": "5.5.1", - "@solana/functional": "5.5.1", - "@solana/nominal-types": "5.5.1", - "@solana/rpc-spec-types": "5.5.1", - "@solana/rpc-types": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/rpc-transport-http": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/rpc-transport-http/-/rpc-transport-http-5.5.1.tgz", - "integrity": "sha512-yv8GoVSHqEV0kUJEIhkdOVkR2SvJ6yoWC51cJn2rSV7plr6huLGe0JgujCmB7uZhhaLbcbP3zxXxu9sOjsi7Fg==", - "license": "MIT", - "dependencies": { - "@solana/errors": "5.5.1", - "@solana/rpc-spec": "5.5.1", - "@solana/rpc-spec-types": "5.5.1", - "undici-types": "^7.19.2" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/rpc-types": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/rpc-types/-/rpc-types-5.5.1.tgz", - "integrity": "sha512-bibTFQ7PbHJJjGJPmfYC2I+/5CRFS4O2p9WwbFraX1Keeel+nRrt/NBXIy8veP5AEn2sVJIyJPpWBRpCx1oATA==", - "license": "MIT", - "dependencies": { - "@solana/addresses": "5.5.1", - "@solana/codecs-core": "5.5.1", - "@solana/codecs-numbers": "5.5.1", - "@solana/codecs-strings": "5.5.1", - "@solana/errors": "5.5.1", - "@solana/nominal-types": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/signers": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/signers/-/signers-5.5.1.tgz", - "integrity": "sha512-FY0IVaBT2kCAze55vEieR6hag4coqcuJ31Aw3hqRH7mv6sV8oqwuJmUrx+uFwOp1gwd5OEAzlv6N4hOOple4sQ==", - "license": "MIT", - "dependencies": { - "@solana/addresses": "5.5.1", - "@solana/codecs-core": "5.5.1", - "@solana/errors": "5.5.1", - "@solana/instructions": "5.5.1", - "@solana/keys": "5.5.1", - "@solana/nominal-types": "5.5.1", - "@solana/offchain-messages": "5.5.1", - "@solana/transaction-messages": "5.5.1", - "@solana/transactions": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/subscribable": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/subscribable/-/subscribable-5.5.1.tgz", - "integrity": "sha512-9K0PsynFq0CsmK1CDi5Y2vUIJpCqkgSS5yfDN0eKPgHqEptLEaia09Kaxc90cSZDZU5mKY/zv1NBmB6Aro9zQQ==", - "license": "MIT", - "dependencies": { - "@solana/errors": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/sysvars": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/sysvars/-/sysvars-5.5.1.tgz", - "integrity": "sha512-k3Quq87Mm+geGUu1GWv6knPk0ALsfY6EKSJGw9xUJDHzY/RkYSBnh0RiOrUhtFm2TDNjOailg8/m0VHmi3reFA==", - "license": "MIT", - "dependencies": { - "@solana/accounts": "5.5.1", - "@solana/codecs": "5.5.1", - "@solana/errors": "5.5.1", - "@solana/rpc-types": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/transaction-confirmation": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/transaction-confirmation/-/transaction-confirmation-5.5.1.tgz", - "integrity": "sha512-j4mKlYPHEyu+OD7MBt3jRoX4ScFgkhZC6H65on4Fux6LMScgivPJlwnKoZMnsgxFgWds0pl+BYzSiALDsXlYtw==", - "license": "MIT", - "dependencies": { - "@solana/addresses": "5.5.1", - "@solana/codecs-strings": "5.5.1", - "@solana/errors": "5.5.1", - "@solana/keys": "5.5.1", - "@solana/promises": "5.5.1", - "@solana/rpc": "5.5.1", - "@solana/rpc-subscriptions": "5.5.1", - "@solana/rpc-types": "5.5.1", - "@solana/transaction-messages": "5.5.1", - "@solana/transactions": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/transaction-messages": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/transaction-messages/-/transaction-messages-5.5.1.tgz", - "integrity": "sha512-aXyhMCEaAp3M/4fP0akwBBQkFPr4pfwoC5CLDq999r/FUwDax2RE/h4Ic7h2Xk+JdcUwsb+rLq85Y52hq84XvQ==", - "license": "MIT", - "dependencies": { - "@solana/addresses": "5.5.1", - "@solana/codecs-core": "5.5.1", - "@solana/codecs-data-structures": "5.5.1", - "@solana/codecs-numbers": "5.5.1", - "@solana/errors": "5.5.1", - "@solana/functional": "5.5.1", - "@solana/instructions": "5.5.1", - "@solana/nominal-types": "5.5.1", - "@solana/rpc-types": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/@solana/transactions": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/@solana/transactions/-/transactions-5.5.1.tgz", - "integrity": "sha512-8hHtDxtqalZ157pnx6p8k10D7J/KY/biLzfgh9R09VNLLY3Fqi7kJvJCr7M2ik3oRll56pxhraAGCC9yIT6eOA==", - "license": "MIT", - "dependencies": { - "@solana/addresses": "5.5.1", - "@solana/codecs-core": "5.5.1", - "@solana/codecs-data-structures": "5.5.1", - "@solana/codecs-numbers": "5.5.1", - "@solana/codecs-strings": "5.5.1", - "@solana/errors": "5.5.1", - "@solana/functional": "5.5.1", - "@solana/instructions": "5.5.1", - "@solana/keys": "5.5.1", - "@solana/nominal-types": "5.5.1", - "@solana/rpc-types": "5.5.1", - "@solana/transaction-messages": "5.5.1" - }, - "engines": { - "node": ">=20.18.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/abitype": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.6.tgz", - "integrity": "sha512-MMSqYh4+C/aVqI2RQaWqbvI4Kxo5cQV40WQ4QFtDnNzCkqChm8MuENhElmynZlO0qUy/ObkEUaXtKqYnx1Kp3A==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/@coinbase/cdp-sdk/node_modules/undici-types": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.29.1.tgz", - "integrity": "sha512-2xO3p6gANwOmhSFwNy2MMYISbHTtM+K3bY0bqpD1gOGpUPIe+yFXCobxbz0s2jm7E0faMxIFcZUcCKa+N6Ozlg==", - "license": "MIT" - }, - "node_modules/@coinbase/wallet-sdk": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/@coinbase/wallet-sdk/-/wallet-sdk-4.3.6.tgz", - "integrity": "sha512-4q8BNG1ViL4mSAAvPAtpwlOs1gpC+67eQtgIwNvT3xyeyFFd+guwkc8bcX5rTmQhXpqnhzC4f0obACbP9CqMSA==", - "license": "Apache-2.0", - "dependencies": { - "@noble/hashes": "1.4.0", - "clsx": "1.2.1", - "eventemitter3": "5.0.1", - "idb-keyval": "6.2.1", - "ox": "0.6.9", - "preact": "10.24.2", - "viem": "^2.27.2", - "zustand": "5.0.3" - } - }, - "node_modules/@coinbase/wallet-sdk/node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@coinbase/wallet-sdk/node_modules/ox": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.9.tgz", - "integrity": "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.10.1", - "@noble/curves": "^1.6.0", - "@noble/hashes": "^1.5.0", - "@scure/bip32": "^1.5.0", - "@scure/bip39": "^1.4.0", - "abitype": "^1.0.6", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@coinbase/wallet-sdk/node_modules/ox/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@ecies/ciphers": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.6.tgz", - "integrity": "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g==", - "license": "MIT", - "engines": { - "bun": ">=1", - "deno": ">=2.7.10", - "node": ">=16" - }, - "peerDependencies": { - "@noble/ciphers": "^1.0.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", - "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", - "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", - "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", - "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", - "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", - "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", - "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", - "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", - "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", - "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", - "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", - "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", - "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", - "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", - "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", - "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", - "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", - "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", - "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", - "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", - "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", - "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", - "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", - "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", - "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", - "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@ethereumjs/common": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-3.2.0.tgz", - "integrity": "sha512-pksvzI0VyLgmuEF2FA/JR/4/y6hcPq8OUail3/AvycBaW1d5VSauOZzqGvJ3RTmR4MU35lWE8KseKOsEhrFRBA==", - "license": "MIT", - "dependencies": { - "@ethereumjs/util": "^8.1.0", - "crc-32": "^1.2.0" - } - }, - "node_modules/@ethereumjs/rlp": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-4.0.1.tgz", - "integrity": "sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==", - "license": "MPL-2.0", - "bin": { - "rlp": "bin/rlp" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@ethereumjs/tx": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/tx/-/tx-4.2.0.tgz", - "integrity": "sha512-1nc6VO4jtFd172BbSnTnDQVr9IYBFl1y4xPzZdtkrkKIncBCkdbgfdRV+MiTkJYAtTxvV12GRZLqBFT1PNK6Yw==", - "license": "MPL-2.0", - "dependencies": { - "@ethereumjs/common": "^3.2.0", - "@ethereumjs/rlp": "^4.0.1", - "@ethereumjs/util": "^8.1.0", - "ethereum-cryptography": "^2.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@ethereumjs/util": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/util/-/util-8.1.0.tgz", - "integrity": "sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==", - "license": "MPL-2.0", - "dependencies": { - "@ethereumjs/rlp": "^4.0.1", - "ethereum-cryptography": "^2.0.0", - "micro-ftch": "^0.3.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@gemini-wallet/core": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@gemini-wallet/core/-/core-0.3.2.tgz", - "integrity": "sha512-Z4aHi3ECFf5oWYWM3F1rW83GJfB9OvhBYPTmb5q+VyK3uvzvS48lwo+jwh2eOoCRWEuT/crpb9Vwp2QaS5JqgQ==", - "license": "MIT", - "dependencies": { - "@metamask/rpc-errors": "7.0.2", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "viem": ">=2.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", - "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@lit-labs/ssr-dom-shim": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.6.0.tgz", - "integrity": "sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@lit/reactive-element": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", - "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.5.0" - } - }, - "node_modules/@metamask/eth-json-rpc-provider": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@metamask/eth-json-rpc-provider/-/eth-json-rpc-provider-1.0.1.tgz", - "integrity": "sha512-whiUMPlAOrVGmX8aKYVPvlKyG4CpQXiNNyt74vE1xb5sPvmx5oA7B/kOi/JdBvhGQq97U1/AVdXEdk2zkP8qyA==", - "dependencies": { - "@metamask/json-rpc-engine": "^7.0.0", - "@metamask/safe-event-emitter": "^3.0.0", - "@metamask/utils": "^5.0.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@metamask/eth-json-rpc-provider/node_modules/@metamask/json-rpc-engine": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@metamask/json-rpc-engine/-/json-rpc-engine-7.3.3.tgz", - "integrity": "sha512-dwZPq8wx9yV3IX2caLi9q9xZBw2XeIoYqdyihDDDpuHVCEiqadJLwqM3zy+uwf6F1QYQ65A8aOMQg1Uw7LMLNg==", - "license": "ISC", - "dependencies": { - "@metamask/rpc-errors": "^6.2.1", - "@metamask/safe-event-emitter": "^3.0.0", - "@metamask/utils": "^8.3.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/eth-json-rpc-provider/node_modules/@metamask/json-rpc-engine/node_modules/@metamask/utils": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-8.5.0.tgz", - "integrity": "sha512-I6bkduevXb72TIM9q2LRO63JSsF9EXduh3sBr9oybNX2hNNpr/j1tEjXrsG0Uabm4MJ1xkGAQEMwifvKZIkyxQ==", - "license": "ISC", - "dependencies": { - "@ethereumjs/tx": "^4.2.0", - "@metamask/superstruct": "^3.0.0", - "@noble/hashes": "^1.3.1", - "@scure/base": "^1.1.3", - "@types/debug": "^4.1.7", - "debug": "^4.3.4", - "pony-cause": "^2.1.10", - "semver": "^7.5.4", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/eth-json-rpc-provider/node_modules/@metamask/rpc-errors": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@metamask/rpc-errors/-/rpc-errors-6.4.0.tgz", - "integrity": "sha512-1ugFO1UoirU2esS3juZanS/Fo8C8XYocCuBpfZI5N7ECtoG+zu0wF+uWZASik6CkO6w9n/Iebt4iI4pT0vptpg==", - "license": "MIT", - "dependencies": { - "@metamask/utils": "^9.0.0", - "fast-safe-stringify": "^2.0.6" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/eth-json-rpc-provider/node_modules/@metamask/rpc-errors/node_modules/@metamask/utils": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-9.3.0.tgz", - "integrity": "sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==", - "license": "ISC", - "dependencies": { - "@ethereumjs/tx": "^4.2.0", - "@metamask/superstruct": "^3.1.0", - "@noble/hashes": "^1.3.1", - "@scure/base": "^1.1.3", - "@types/debug": "^4.1.7", - "debug": "^4.3.4", - "pony-cause": "^2.1.10", - "semver": "^7.5.4", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/eth-json-rpc-provider/node_modules/@metamask/utils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-5.0.2.tgz", - "integrity": "sha512-yfmE79bRQtnMzarnKfX7AEJBwFTxvTyw3nBQlu/5rmGXrjAeAMltoGxO62TFurxrQAFMNa/fEjIHNvungZp0+g==", - "license": "ISC", - "dependencies": { - "@ethereumjs/tx": "^4.1.2", - "@types/debug": "^4.1.7", - "debug": "^4.3.4", - "semver": "^7.3.8", - "superstruct": "^1.0.3" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@metamask/eth-json-rpc-provider/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@metamask/json-rpc-engine": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@metamask/json-rpc-engine/-/json-rpc-engine-8.0.2.tgz", - "integrity": "sha512-IoQPmql8q7ABLruW7i4EYVHWUbF74yrp63bRuXV5Zf9BQwcn5H9Ww1eLtROYvI1bUXwOiHZ6qT5CWTrDc/t/AA==", - "license": "ISC", - "dependencies": { - "@metamask/rpc-errors": "^6.2.1", - "@metamask/safe-event-emitter": "^3.0.0", - "@metamask/utils": "^8.3.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/json-rpc-engine/node_modules/@metamask/rpc-errors": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@metamask/rpc-errors/-/rpc-errors-6.4.0.tgz", - "integrity": "sha512-1ugFO1UoirU2esS3juZanS/Fo8C8XYocCuBpfZI5N7ECtoG+zu0wF+uWZASik6CkO6w9n/Iebt4iI4pT0vptpg==", - "license": "MIT", - "dependencies": { - "@metamask/utils": "^9.0.0", - "fast-safe-stringify": "^2.0.6" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/json-rpc-engine/node_modules/@metamask/rpc-errors/node_modules/@metamask/utils": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-9.3.0.tgz", - "integrity": "sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==", - "license": "ISC", - "dependencies": { - "@ethereumjs/tx": "^4.2.0", - "@metamask/superstruct": "^3.1.0", - "@noble/hashes": "^1.3.1", - "@scure/base": "^1.1.3", - "@types/debug": "^4.1.7", - "debug": "^4.3.4", - "pony-cause": "^2.1.10", - "semver": "^7.5.4", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/json-rpc-engine/node_modules/@metamask/utils": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-8.5.0.tgz", - "integrity": "sha512-I6bkduevXb72TIM9q2LRO63JSsF9EXduh3sBr9oybNX2hNNpr/j1tEjXrsG0Uabm4MJ1xkGAQEMwifvKZIkyxQ==", - "license": "ISC", - "dependencies": { - "@ethereumjs/tx": "^4.2.0", - "@metamask/superstruct": "^3.0.0", - "@noble/hashes": "^1.3.1", - "@scure/base": "^1.1.3", - "@types/debug": "^4.1.7", - "debug": "^4.3.4", - "pony-cause": "^2.1.10", - "semver": "^7.5.4", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/json-rpc-engine/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@metamask/json-rpc-middleware-stream": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@metamask/json-rpc-middleware-stream/-/json-rpc-middleware-stream-7.0.2.tgz", - "integrity": "sha512-yUdzsJK04Ev98Ck4D7lmRNQ8FPioXYhEUZOMS01LXW8qTvPGiRVXmVltj2p4wrLkh0vW7u6nv0mNl5xzC5Qmfg==", - "license": "ISC", - "dependencies": { - "@metamask/json-rpc-engine": "^8.0.2", - "@metamask/safe-event-emitter": "^3.0.0", - "@metamask/utils": "^8.3.0", - "readable-stream": "^3.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/json-rpc-middleware-stream/node_modules/@metamask/utils": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-8.5.0.tgz", - "integrity": "sha512-I6bkduevXb72TIM9q2LRO63JSsF9EXduh3sBr9oybNX2hNNpr/j1tEjXrsG0Uabm4MJ1xkGAQEMwifvKZIkyxQ==", - "license": "ISC", - "dependencies": { - "@ethereumjs/tx": "^4.2.0", - "@metamask/superstruct": "^3.0.0", - "@noble/hashes": "^1.3.1", - "@scure/base": "^1.1.3", - "@types/debug": "^4.1.7", - "debug": "^4.3.4", - "pony-cause": "^2.1.10", - "semver": "^7.5.4", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/json-rpc-middleware-stream/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@metamask/object-multiplex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@metamask/object-multiplex/-/object-multiplex-2.1.0.tgz", - "integrity": "sha512-4vKIiv0DQxljcXwfpnbsXcfa5glMj5Zg9mqn4xpIWqkv6uJ2ma5/GtUfLFSxhlxnR8asRMv8dDmWya1Tc1sDFA==", - "license": "ISC", - "dependencies": { - "once": "^1.4.0", - "readable-stream": "^3.6.2" - }, - "engines": { - "node": "^16.20 || ^18.16 || >=20" - } - }, - "node_modules/@metamask/onboarding": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@metamask/onboarding/-/onboarding-1.0.1.tgz", - "integrity": "sha512-FqHhAsCI+Vacx2qa5mAFcWNSrTcVGMNjzxVgaX8ECSny/BJ9/vgXP9V7WF/8vb9DltPeQkxr+Fnfmm6GHfmdTQ==", - "license": "MIT", - "dependencies": { - "bowser": "^2.9.0" - } - }, - "node_modules/@metamask/providers": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/@metamask/providers/-/providers-16.1.0.tgz", - "integrity": "sha512-znVCvux30+3SaUwcUGaSf+pUckzT5ukPRpcBmy+muBLC0yaWnBcvDqGfcsw6CBIenUdFrVoAFa8B6jsuCY/a+g==", - "license": "MIT", - "dependencies": { - "@metamask/json-rpc-engine": "^8.0.1", - "@metamask/json-rpc-middleware-stream": "^7.0.1", - "@metamask/object-multiplex": "^2.0.0", - "@metamask/rpc-errors": "^6.2.1", - "@metamask/safe-event-emitter": "^3.1.1", - "@metamask/utils": "^8.3.0", - "detect-browser": "^5.2.0", - "extension-port-stream": "^3.0.0", - "fast-deep-equal": "^3.1.3", - "is-stream": "^2.0.0", - "readable-stream": "^3.6.2", - "webextension-polyfill": "^0.10.0" - }, - "engines": { - "node": "^18.18 || >=20" - } - }, - "node_modules/@metamask/providers/node_modules/@metamask/rpc-errors": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@metamask/rpc-errors/-/rpc-errors-6.4.0.tgz", - "integrity": "sha512-1ugFO1UoirU2esS3juZanS/Fo8C8XYocCuBpfZI5N7ECtoG+zu0wF+uWZASik6CkO6w9n/Iebt4iI4pT0vptpg==", - "license": "MIT", - "dependencies": { - "@metamask/utils": "^9.0.0", - "fast-safe-stringify": "^2.0.6" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/providers/node_modules/@metamask/rpc-errors/node_modules/@metamask/utils": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-9.3.0.tgz", - "integrity": "sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==", - "license": "ISC", - "dependencies": { - "@ethereumjs/tx": "^4.2.0", - "@metamask/superstruct": "^3.1.0", - "@noble/hashes": "^1.3.1", - "@scure/base": "^1.1.3", - "@types/debug": "^4.1.7", - "debug": "^4.3.4", - "pony-cause": "^2.1.10", - "semver": "^7.5.4", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/providers/node_modules/@metamask/utils": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-8.5.0.tgz", - "integrity": "sha512-I6bkduevXb72TIM9q2LRO63JSsF9EXduh3sBr9oybNX2hNNpr/j1tEjXrsG0Uabm4MJ1xkGAQEMwifvKZIkyxQ==", - "license": "ISC", - "dependencies": { - "@ethereumjs/tx": "^4.2.0", - "@metamask/superstruct": "^3.0.0", - "@noble/hashes": "^1.3.1", - "@scure/base": "^1.1.3", - "@types/debug": "^4.1.7", - "debug": "^4.3.4", - "pony-cause": "^2.1.10", - "semver": "^7.5.4", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/providers/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@metamask/rpc-errors": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@metamask/rpc-errors/-/rpc-errors-7.0.2.tgz", - "integrity": "sha512-YYYHsVYd46XwY2QZzpGeU4PSdRhHdxnzkB8piWGvJW2xbikZ3R+epAYEL4q/K8bh9JPTucsUdwRFnACor1aOYw==", - "license": "MIT", - "dependencies": { - "@metamask/utils": "^11.0.1", - "fast-safe-stringify": "^2.0.6" - }, - "engines": { - "node": "^18.20 || ^20.17 || >=22" - } - }, - "node_modules/@metamask/safe-event-emitter": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@metamask/safe-event-emitter/-/safe-event-emitter-3.1.3.tgz", - "integrity": "sha512-Pnl74ukgoOkSItBRmZ9nnhdBbRtBI1ZvpeYjKNgxjVzDWa/AtruKZc+7LjfZ0iO8iYqh7Ts3bPs2Gu7M2hSf7g==", - "license": "ISC", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@metamask/scure-bip39": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@metamask/scure-bip39/-/scure-bip39-2.1.1.tgz", - "integrity": "sha512-1K8aBsAqr6+8jWhguVl06n8e+zjV9sUnys+5PLyVU4mb8LbulQ60F6cq7iQys3xX/yCwKt1+7c7j2nuTEpW+ZQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.3.2", - "@scure/base": "~1.1.3" - }, - "engines": { - "node": "^16.20 || ^18.16 || >=20" - } - }, - "node_modules/@metamask/scure-bip39/node_modules/@noble/hashes": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", - "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@metamask/scure-bip39/node_modules/@scure/base": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", - "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@metamask/sdk": { - "version": "0.33.1", - "resolved": "https://registry.npmjs.org/@metamask/sdk/-/sdk-0.33.1.tgz", - "integrity": "sha512-1mcOQVGr9rSrVcbKPNVzbZ8eCl1K0FATsYH3WJ/MH4WcZDWGECWrXJPNMZoEAkLxWiMe8jOQBumg2pmcDa9zpQ==", - "deprecated": "No longer maintained, superseded by https://docs.metamask.io/metamask-connect", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@metamask/onboarding": "^1.0.1", - "@metamask/providers": "16.1.0", - "@metamask/sdk-analytics": "0.0.5", - "@metamask/sdk-communication-layer": "0.33.1", - "@metamask/sdk-install-modal-web": "0.32.1", - "@paulmillr/qr": "^0.2.1", - "bowser": "^2.9.0", - "cross-fetch": "^4.0.0", - "debug": "4.3.4", - "eciesjs": "^0.4.11", - "eth-rpc-errors": "^4.0.3", - "eventemitter2": "^6.4.9", - "obj-multiplex": "^1.0.0", - "pump": "^3.0.0", - "readable-stream": "^3.6.2", - "socket.io-client": "^4.5.1", - "tslib": "^2.6.0", - "util": "^0.12.4", - "uuid": "^8.3.2" - } - }, - "node_modules/@metamask/sdk-analytics": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@metamask/sdk-analytics/-/sdk-analytics-0.0.5.tgz", - "integrity": "sha512-fDah+keS1RjSUlC8GmYXvx6Y26s3Ax1U9hGpWb6GSY5SAdmTSIqp2CvYy6yW0WgLhnYhW+6xERuD0eVqV63QIQ==", - "deprecated": "No longer maintained, superseded by @metamask/connect-analytics", - "license": "MIT", - "dependencies": { - "openapi-fetch": "^0.13.5" - } - }, - "node_modules/@metamask/sdk-communication-layer": { - "version": "0.33.1", - "resolved": "https://registry.npmjs.org/@metamask/sdk-communication-layer/-/sdk-communication-layer-0.33.1.tgz", - "integrity": "sha512-0bI9hkysxcfbZ/lk0T2+aKVo1j0ynQVTuB3sJ5ssPWlz+Z3VwveCkP1O7EVu1tsVVCb0YV5WxK9zmURu2FIiaA==", - "deprecated": "No longer maintained, superseded by https://docs.metamask.io/metamask-connect", - "dependencies": { - "@metamask/sdk-analytics": "0.0.5", - "bufferutil": "^4.0.8", - "date-fns": "^2.29.3", - "debug": "4.3.4", - "utf-8-validate": "^5.0.2", - "uuid": "^8.3.2" - }, - "peerDependencies": { - "cross-fetch": "^4.0.0", - "eciesjs": "*", - "eventemitter2": "^6.4.9", - "readable-stream": "^3.6.2", - "socket.io-client": "^4.5.1" - } - }, - "node_modules/@metamask/sdk-install-modal-web": { - "version": "0.32.1", - "resolved": "https://registry.npmjs.org/@metamask/sdk-install-modal-web/-/sdk-install-modal-web-0.32.1.tgz", - "integrity": "sha512-MGmAo6qSjf1tuYXhCu2EZLftq+DSt5Z7fsIKr2P+lDgdTPWgLfZB1tJKzNcwKKOdf6q9Qmmxn7lJuI/gq5LrKw==", - "deprecated": "No longer maintained, superseded by https://docs.metamask.io/metamask-connect", - "dependencies": { - "@paulmillr/qr": "^0.2.1" - } - }, - "node_modules/@metamask/superstruct": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@metamask/superstruct/-/superstruct-3.4.1.tgz", - "integrity": "sha512-caTaaBUcwBGbUNf3r0uT48upX4nECRbKhQ9pPOfW4sIkfcIUUDV4S9DZxq/5fuNPVt5KWpyd5xIIz0sP+iWLlg==", - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@metamask/utils": { - "version": "11.12.1", - "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-11.12.1.tgz", - "integrity": "sha512-X97J5AEcaKDTIAfKb4X93ZTLfvOahzDVadlveQW+7OZ7yhXjMxXoFmhX2d9UKv48iS2Dxq9kbGJ1lUfphu9ebQ==", - "license": "ISC", - "dependencies": { - "@ethereumjs/tx": "^4.2.0", - "@metamask/scure-bip39": "^2.0.3", - "@metamask/superstruct": "^3.1.0", - "@noble/hashes": "^1.3.1", - "@scure/base": "^1.1.3", - "@types/debug": "^4.1.7", - "@types/lodash": "^4.17.20", - "debug": "^4.3.4", - "lodash": "^4.17.21", - "pony-cause": "^2.1.10", - "semver": "^7.5.4", - "uuid": "^9.0.1" - }, - "engines": { - "node": "^18.18 || ^20.14 || >=22" - } - }, - "node_modules/@metamask/utils/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/@napi-rs/lzma-linux-x64-gnu": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", - "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^22.20 || ^24.12 || >=25" - } - }, - "node_modules/@noble/ciphers": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", - "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/curves": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", - "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.149.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz", - "integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/oxc-project" - } - }, - "node_modules/@oxlint/binding-android-arm-eabi": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.83.0.tgz", - "integrity": "sha512-0yGY24EwsLk5YDe6F+VkmZyRHSwJDALa3nIrPpq7FXmp2lV2d0TzvBCGeZk+wgiULRGr5blhyr4QMp5KCXJUqA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-android-arm64": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.83.0.tgz", - "integrity": "sha512-hHfJ0vc17A4iUjH5p9BsTUPYbYRNxGpvD2lbu1aBRk54bzNIx9o5TtYF39QPZcV95DagZd+4DEAw2RH3G2ZsMg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-darwin-arm64": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.83.0.tgz", - "integrity": "sha512-hsOjYjszLb/3zym/TkzUMPAoQlTJcuzSyEPOAyA+skXJIX9M0o+4JfOtqopX/Vf4hSLrJ98j0nvFo23gzk8auQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-darwin-x64": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.83.0.tgz", - "integrity": "sha512-mjh5oH2EA+wl5yRJYT9K9G61O2zFlpuv+yf2JwZOi0+dq2FnTUtm1h8i+5Ik0fXPWIu/k84I1psZR9aQsLAnyA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-freebsd-x64": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.83.0.tgz", - "integrity": "sha512-fNHr64/YaO8YssuoDVC8+F4Uk5enR86q5uxfHkQrjAPs1dbAILOrD2uaud+J7MO8Fx774g44ERLD0IGIvZE48w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-linux-arm-gnueabihf": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.83.0.tgz", - "integrity": "sha512-Qpwy3zzAwMj+8/lyYItHmkSMwbkprFNWTK7jPYDOxSyxEhaSLOWYUTCMkjF334J8/WD0nznCCsoBbIH6hpsuIw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-linux-arm-musleabihf": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.83.0.tgz", - "integrity": "sha512-s+BirYLFq7JL2k9sP0XI3ZXJ9dYvJ8sX3jLCLoag7tt+zrSHpZxP0jqznfL+Gdgwu7ay0dYgGYJXrQvq3iWloA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-linux-arm64-gnu": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.83.0.tgz", - "integrity": "sha512-7lihXt3vKr+GIyapNbHrnFHm/biiW30le6Zv/DExbAFPF6YwCQXVFlONPFehxs0CpGO4CBfYPM9rdDT+XMoIlg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-linux-arm64-musl": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.83.0.tgz", - "integrity": "sha512-q63JalLYVkZiZvls1z3PPUnpmQluOMXp0khqQMznCeAPLGydfNY8JhvuA4WlK57JfrvikU8wB5lPVveqpIXvew==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-linux-ppc64-gnu": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.83.0.tgz", - "integrity": "sha512-krQmDF+dRbxvdqVPV88ZuOoPPu8X5BuqDA8Hd+qcS4YMRQCb+nexA57DazgGsc/rGdKBe3QmV0mnv0bdpW/p5g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-linux-riscv64-gnu": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.83.0.tgz", - "integrity": "sha512-MmOl8Y6txEAXZU1RG8Rr264jQ6D7VPmqFsU/45x/FeWsGe32hklTqGrLE6UxHzp5Rjt0wP+20tY8YXKgSFB3mw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-linux-riscv64-musl": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.83.0.tgz", - "integrity": "sha512-u1rMymh0W3JZkq370kzQsYPULGWqhE09pZRqnZvUSoYaI9pVO5yVX+iYIslmWuEgwuzH9YAaOsScJiobWCHoOw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-linux-s390x-gnu": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.83.0.tgz", - "integrity": "sha512-y0zK3HNwGysu7rqtE+BQG/d0bx5gh/KwlOtghN8oWeK1KcWzeaLqtZrbm8owqdma1lFyrce/hTO5ismuNu+INQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-linux-x64-gnu": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.83.0.tgz", - "integrity": "sha512-rS5gM0NgD7ngmuJmbIehsidtrOwKkLFwCQbKEeb9KuyQrrWNq5Zkn0uV6AYdXOMJ0grrWEiLwBuvMxt8w5vsNw==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-linux-x64-musl": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.83.0.tgz", - "integrity": "sha512-W2IH4EtpcPaWcvNGCA95YoDg4vxqE/ZiPCi3arrxEEpsK7+JQN9WYwrlYFx9pcdP6KPXqRqkv3zdQPHcx7b6YQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-openharmony-arm64": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.83.0.tgz", - "integrity": "sha512-6LyKkUyoajssTPLlZmDbZIbu4IZ5B4bGuRUnBgCGpEvHP3FQMaYITncHA/unPUo7q+Z+pIu2HhdkQ+8d1SG7iA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-win32-arm64-msvc": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.83.0.tgz", - "integrity": "sha512-Uz/fObEtF0jmNJQJ8CGRBKfefYstS0/wjD3s6IGzP8nUwsJykHQJBiN3npHwKiGRGn/vvBEgNr4B3cCzmmatvg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-win32-ia32-msvc": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.83.0.tgz", - "integrity": "sha512-u7XcvPW6Bk58tY5iWs2ESb0vJjoE/kuSpHxopbwp/p3ZtWVQXZ6wor5w3ssVTHOqd/v8b+QdhSFWQ4grEUNWpA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-win32-x64-msvc": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.83.0.tgz", - "integrity": "sha512-LZRubd7ph13QmAg4fFecTYVZkiYbROR2Htaxh/ufWRkDhPOm2wrwaEYR89e0YpPFD3dqBrPoxS7myBw5hmYA7Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@paulmillr/qr": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@paulmillr/qr/-/qr-0.2.1.tgz", - "integrity": "sha512-IHnV6A+zxU7XwmKFinmYjUcwlyK9+xkG3/s9KcQhI9BjQKycrJ1JRO+FbNYPwZiPKW3je/DR0k7w8/gLa5eaxQ==", - "deprecated": "Switch to \"qr\" (new package name) for security updates: npm install qr", - "license": "(MIT OR Apache-2.0)", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit/-/appkit-1.7.8.tgz", - "integrity": "sha512-51kTleozhA618T1UvMghkhKfaPcc9JlKwLJ5uV+riHyvSoWPKPRIa5A6M1Wano5puNyW0s3fwywhyqTHSilkaA==", - "license": "Apache-2.0", - "dependencies": { - "@reown/appkit-common": "1.7.8", - "@reown/appkit-controllers": "1.7.8", - "@reown/appkit-pay": "1.7.8", - "@reown/appkit-polyfills": "1.7.8", - "@reown/appkit-scaffold-ui": "1.7.8", - "@reown/appkit-ui": "1.7.8", - "@reown/appkit-utils": "1.7.8", - "@reown/appkit-wallet": "1.7.8", - "@walletconnect/types": "2.21.0", - "@walletconnect/universal-provider": "2.21.0", - "bs58": "6.0.0", - "valtio": "1.13.2", - "viem": ">=2.29.0" - } - }, - "node_modules/@reown/appkit-common": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit-common/-/appkit-common-1.7.8.tgz", - "integrity": "sha512-ridIhc/x6JOp7KbDdwGKY4zwf8/iK8EYBl+HtWrruutSLwZyVi5P8WaZa+8iajL6LcDcDF7LoyLwMTym7SRuwQ==", - "license": "Apache-2.0", - "dependencies": { - "big.js": "6.2.2", - "dayjs": "1.11.13", - "viem": ">=2.29.0" - } - }, - "node_modules/@reown/appkit-controllers": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit-controllers/-/appkit-controllers-1.7.8.tgz", - "integrity": "sha512-IdXlJlivrlj6m63VsGLsjtPHHsTWvKGVzWIP1fXZHVqmK+rZCBDjCi9j267Rb9/nYRGHWBtlFQhO8dK35WfeDA==", - "license": "Apache-2.0", - "dependencies": { - "@reown/appkit-common": "1.7.8", - "@reown/appkit-wallet": "1.7.8", - "@walletconnect/universal-provider": "2.21.0", - "valtio": "1.13.2", - "viem": ">=2.29.0" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@noble/ciphers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.2.1.tgz", - "integrity": "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@noble/curves": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", - "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.7.1" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@noble/hashes": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", - "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@scure/bip32": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", - "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.8.1", - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@scure/bip39": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", - "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.4" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@walletconnect/core": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.21.0.tgz", - "integrity": "sha512-o6R7Ua4myxR8aRUAJ1z3gT9nM+jd2B2mfamu6arzy1Cc6vi10fIwFWb6vg3bC8xJ6o9H3n/cN5TOW3aA9Y1XVw==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/jsonrpc-ws-connection": "1.0.16", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "@walletconnect/window-getters": "1.0.1", - "es-toolkit": "1.33.0", - "events": "3.3.0", - "uint8arrays": "3.1.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@walletconnect/sign-client": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.21.0.tgz", - "integrity": "sha512-z7h+PeLa5Au2R591d/8ZlziE0stJvdzP9jNFzFolf2RG/OiXulgFKum8PrIyXy+Rg2q95U9nRVUF9fWcn78yBA==", - "deprecated": "Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/core": "2.21.0", - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/logger": "2.1.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@walletconnect/types": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.21.0.tgz", - "integrity": "sha512-ll+9upzqt95ZBWcfkOszXZkfnpbJJ2CmxMfGgE5GmhdxxxCcO5bGhXkI+x8OpiS555RJ/v/sXJYMSOLkmu4fFw==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@walletconnect/universal-provider": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/universal-provider/-/universal-provider-2.21.0.tgz", - "integrity": "sha512-mtUQvewt+X0VBQay/xOJBvxsB3Xsm1lTwFjZ6WUwSOTR1X+FNb71hSApnV5kbsdDIpYPXeQUbGt2se1n5E5UBg==", - "deprecated": "Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/jsonrpc-http-connection": "1.0.8", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/sign-client": "2.21.0", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "es-toolkit": "1.33.0", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@walletconnect/utils": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.21.0.tgz", - "integrity": "sha512-zfHLiUoBrQ8rP57HTPXW7rQMnYxYI4gT9yTACxVW6LhIFROTF6/ytm5SKNoIvi4a5nX5dfXG4D9XwQUCu8Ilig==", - "license": "Apache-2.0", - "dependencies": { - "@noble/ciphers": "1.2.1", - "@noble/curves": "1.8.1", - "@noble/hashes": "1.7.1", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/window-getters": "1.0.1", - "@walletconnect/window-metadata": "1.0.1", - "bs58": "6.0.0", - "detect-browser": "5.3.0", - "query-string": "7.1.3", - "uint8arrays": "3.1.0", - "viem": "2.23.2" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/@walletconnect/utils/node_modules/viem": { - "version": "2.23.2", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.23.2.tgz", - "integrity": "sha512-NVmW/E0c5crMOtbEAqMF0e3NmvQykFXhLOc/CkLIXOlzHSA6KXVz3CYVmaKqBF8/xtjsjHAGjdJN3Ru1kFJLaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@noble/curves": "1.8.1", - "@noble/hashes": "1.7.1", - "@scure/bip32": "1.6.2", - "@scure/bip39": "1.5.4", - "abitype": "1.0.8", - "isows": "1.0.6", - "ox": "0.6.7", - "ws": "8.18.0" - }, - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@reown/appkit-controllers/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/@reown/appkit-controllers/node_modules/isows": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz", - "integrity": "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/@reown/appkit-controllers/node_modules/ox": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.7.tgz", - "integrity": "sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.10.1", - "@noble/curves": "^1.6.0", - "@noble/hashes": "^1.5.0", - "@scure/bip32": "^1.5.0", - "@scure/bip39": "^1.4.0", - "abitype": "^1.0.6", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@reown/appkit-controllers/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@reown/appkit-pay": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit-pay/-/appkit-pay-1.7.8.tgz", - "integrity": "sha512-OSGQ+QJkXx0FEEjlpQqIhT8zGJKOoHzVnyy/0QFrl3WrQTjCzg0L6+i91Ad5Iy1zb6V5JjqtfIFpRVRWN4M3pw==", - "license": "Apache-2.0", - "dependencies": { - "@reown/appkit-common": "1.7.8", - "@reown/appkit-controllers": "1.7.8", - "@reown/appkit-ui": "1.7.8", - "@reown/appkit-utils": "1.7.8", - "lit": "3.3.0", - "valtio": "1.13.2" - } - }, - "node_modules/@reown/appkit-polyfills": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit-polyfills/-/appkit-polyfills-1.7.8.tgz", - "integrity": "sha512-W/kq786dcHHAuJ3IV2prRLEgD/2iOey4ueMHf1sIFjhhCGMynMkhsOhQMUH0tzodPqUgAC494z4bpIDYjwWXaA==", - "license": "Apache-2.0", - "dependencies": { - "buffer": "6.0.3" - } - }, - "node_modules/@reown/appkit-scaffold-ui": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit-scaffold-ui/-/appkit-scaffold-ui-1.7.8.tgz", - "integrity": "sha512-RCeHhAwOrIgcvHwYlNWMcIDibdI91waaoEYBGw71inE0kDB8uZbE7tE6DAXJmDkvl0qPh+DqlC4QbJLF1FVYdQ==", - "license": "Apache-2.0", - "dependencies": { - "@reown/appkit-common": "1.7.8", - "@reown/appkit-controllers": "1.7.8", - "@reown/appkit-ui": "1.7.8", - "@reown/appkit-utils": "1.7.8", - "@reown/appkit-wallet": "1.7.8", - "lit": "3.3.0" - } - }, - "node_modules/@reown/appkit-ui": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit-ui/-/appkit-ui-1.7.8.tgz", - "integrity": "sha512-1hjCKjf6FLMFzrulhl0Y9Vb9Fu4royE+SXCPSWh4VhZhWqlzUFc7kutnZKx8XZFVQH4pbBvY62SpRC93gqoHow==", - "license": "Apache-2.0", - "dependencies": { - "@reown/appkit-common": "1.7.8", - "@reown/appkit-controllers": "1.7.8", - "@reown/appkit-wallet": "1.7.8", - "lit": "3.3.0", - "qrcode": "1.5.3" - } - }, - "node_modules/@reown/appkit-utils": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit-utils/-/appkit-utils-1.7.8.tgz", - "integrity": "sha512-8X7UvmE8GiaoitCwNoB86pttHgQtzy4ryHZM9kQpvjQ0ULpiER44t1qpVLXNM4X35O0v18W0Dk60DnYRMH2WRw==", - "license": "Apache-2.0", - "dependencies": { - "@reown/appkit-common": "1.7.8", - "@reown/appkit-controllers": "1.7.8", - "@reown/appkit-polyfills": "1.7.8", - "@reown/appkit-wallet": "1.7.8", - "@walletconnect/logger": "2.1.2", - "@walletconnect/universal-provider": "2.21.0", - "valtio": "1.13.2", - "viem": ">=2.29.0" - }, - "peerDependencies": { - "valtio": "1.13.2" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@noble/ciphers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.2.1.tgz", - "integrity": "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@noble/curves": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", - "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.7.1" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@noble/hashes": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", - "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@scure/bip32": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", - "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.8.1", - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@scure/bip39": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", - "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.4" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@walletconnect/core": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.21.0.tgz", - "integrity": "sha512-o6R7Ua4myxR8aRUAJ1z3gT9nM+jd2B2mfamu6arzy1Cc6vi10fIwFWb6vg3bC8xJ6o9H3n/cN5TOW3aA9Y1XVw==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/jsonrpc-ws-connection": "1.0.16", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "@walletconnect/window-getters": "1.0.1", - "es-toolkit": "1.33.0", - "events": "3.3.0", - "uint8arrays": "3.1.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@walletconnect/sign-client": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.21.0.tgz", - "integrity": "sha512-z7h+PeLa5Au2R591d/8ZlziE0stJvdzP9jNFzFolf2RG/OiXulgFKum8PrIyXy+Rg2q95U9nRVUF9fWcn78yBA==", - "deprecated": "Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/core": "2.21.0", - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/logger": "2.1.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@walletconnect/types": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.21.0.tgz", - "integrity": "sha512-ll+9upzqt95ZBWcfkOszXZkfnpbJJ2CmxMfGgE5GmhdxxxCcO5bGhXkI+x8OpiS555RJ/v/sXJYMSOLkmu4fFw==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@walletconnect/universal-provider": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/universal-provider/-/universal-provider-2.21.0.tgz", - "integrity": "sha512-mtUQvewt+X0VBQay/xOJBvxsB3Xsm1lTwFjZ6WUwSOTR1X+FNb71hSApnV5kbsdDIpYPXeQUbGt2se1n5E5UBg==", - "deprecated": "Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/jsonrpc-http-connection": "1.0.8", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/sign-client": "2.21.0", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "es-toolkit": "1.33.0", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@walletconnect/utils": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.21.0.tgz", - "integrity": "sha512-zfHLiUoBrQ8rP57HTPXW7rQMnYxYI4gT9yTACxVW6LhIFROTF6/ytm5SKNoIvi4a5nX5dfXG4D9XwQUCu8Ilig==", - "license": "Apache-2.0", - "dependencies": { - "@noble/ciphers": "1.2.1", - "@noble/curves": "1.8.1", - "@noble/hashes": "1.7.1", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/window-getters": "1.0.1", - "@walletconnect/window-metadata": "1.0.1", - "bs58": "6.0.0", - "detect-browser": "5.3.0", - "query-string": "7.1.3", - "uint8arrays": "3.1.0", - "viem": "2.23.2" - } - }, - "node_modules/@reown/appkit-utils/node_modules/@walletconnect/utils/node_modules/viem": { - "version": "2.23.2", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.23.2.tgz", - "integrity": "sha512-NVmW/E0c5crMOtbEAqMF0e3NmvQykFXhLOc/CkLIXOlzHSA6KXVz3CYVmaKqBF8/xtjsjHAGjdJN3Ru1kFJLaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@noble/curves": "1.8.1", - "@noble/hashes": "1.7.1", - "@scure/bip32": "1.6.2", - "@scure/bip39": "1.5.4", - "abitype": "1.0.8", - "isows": "1.0.6", - "ox": "0.6.7", - "ws": "8.18.0" - }, - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@reown/appkit-utils/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/@reown/appkit-utils/node_modules/isows": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz", - "integrity": "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/@reown/appkit-utils/node_modules/ox": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.7.tgz", - "integrity": "sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.10.1", - "@noble/curves": "^1.6.0", - "@noble/hashes": "^1.5.0", - "@scure/bip32": "^1.5.0", - "@scure/bip39": "^1.4.0", - "abitype": "^1.0.6", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@reown/appkit-utils/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@reown/appkit-wallet": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/@reown/appkit-wallet/-/appkit-wallet-1.7.8.tgz", - "integrity": "sha512-kspz32EwHIOT/eg/ZQbFPxgXq0B/olDOj3YMu7gvLEFz4xyOFd/wgzxxAXkp5LbG4Cp++s/elh79rVNmVFdB9A==", - "license": "Apache-2.0", - "dependencies": { - "@reown/appkit-common": "1.7.8", - "@reown/appkit-polyfills": "1.7.8", - "@walletconnect/logger": "2.1.2", - "zod": "3.22.4" - } - }, - "node_modules/@reown/appkit-wallet/node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/@reown/appkit/node_modules/@noble/ciphers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.2.1.tgz", - "integrity": "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit/node_modules/@noble/curves": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", - "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.7.1" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit/node_modules/@noble/hashes": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", - "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit/node_modules/@scure/bip32": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", - "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.8.1", - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit/node_modules/@scure/bip39": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", - "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.4" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit/node_modules/@walletconnect/core": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.21.0.tgz", - "integrity": "sha512-o6R7Ua4myxR8aRUAJ1z3gT9nM+jd2B2mfamu6arzy1Cc6vi10fIwFWb6vg3bC8xJ6o9H3n/cN5TOW3aA9Y1XVw==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/jsonrpc-ws-connection": "1.0.16", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "@walletconnect/window-getters": "1.0.1", - "es-toolkit": "1.33.0", - "events": "3.3.0", - "uint8arrays": "3.1.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@reown/appkit/node_modules/@walletconnect/sign-client": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.21.0.tgz", - "integrity": "sha512-z7h+PeLa5Au2R591d/8ZlziE0stJvdzP9jNFzFolf2RG/OiXulgFKum8PrIyXy+Rg2q95U9nRVUF9fWcn78yBA==", - "deprecated": "Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/core": "2.21.0", - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/logger": "2.1.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit/node_modules/@walletconnect/types": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.21.0.tgz", - "integrity": "sha512-ll+9upzqt95ZBWcfkOszXZkfnpbJJ2CmxMfGgE5GmhdxxxCcO5bGhXkI+x8OpiS555RJ/v/sXJYMSOLkmu4fFw==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit/node_modules/@walletconnect/universal-provider": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/universal-provider/-/universal-provider-2.21.0.tgz", - "integrity": "sha512-mtUQvewt+X0VBQay/xOJBvxsB3Xsm1lTwFjZ6WUwSOTR1X+FNb71hSApnV5kbsdDIpYPXeQUbGt2se1n5E5UBg==", - "deprecated": "Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/jsonrpc-http-connection": "1.0.8", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/sign-client": "2.21.0", - "@walletconnect/types": "2.21.0", - "@walletconnect/utils": "2.21.0", - "es-toolkit": "1.33.0", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit/node_modules/@walletconnect/utils": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.21.0.tgz", - "integrity": "sha512-zfHLiUoBrQ8rP57HTPXW7rQMnYxYI4gT9yTACxVW6LhIFROTF6/ytm5SKNoIvi4a5nX5dfXG4D9XwQUCu8Ilig==", - "license": "Apache-2.0", - "dependencies": { - "@noble/ciphers": "1.2.1", - "@noble/curves": "1.8.1", - "@noble/hashes": "1.7.1", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.0", - "@walletconnect/window-getters": "1.0.1", - "@walletconnect/window-metadata": "1.0.1", - "bs58": "6.0.0", - "detect-browser": "5.3.0", - "query-string": "7.1.3", - "uint8arrays": "3.1.0", - "viem": "2.23.2" - } - }, - "node_modules/@reown/appkit/node_modules/@walletconnect/utils/node_modules/viem": { - "version": "2.23.2", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.23.2.tgz", - "integrity": "sha512-NVmW/E0c5crMOtbEAqMF0e3NmvQykFXhLOc/CkLIXOlzHSA6KXVz3CYVmaKqBF8/xtjsjHAGjdJN3Ru1kFJLaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@noble/curves": "1.8.1", - "@noble/hashes": "1.7.1", - "@scure/bip32": "1.6.2", - "@scure/bip39": "1.5.4", - "abitype": "1.0.8", - "isows": "1.0.6", - "ox": "0.6.7", - "ws": "8.18.0" - }, - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@reown/appkit/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/@reown/appkit/node_modules/isows": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz", - "integrity": "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/@reown/appkit/node_modules/ox": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.7.tgz", - "integrity": "sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.10.1", - "@noble/curves": "^1.6.0", - "@noble/hashes": "^1.5.0", - "@scure/bip32": "^1.5.0", - "@scure/bip39": "^1.4.0", - "abitype": "^1.0.6", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@reown/appkit/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz", - "integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz", - "integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz", - "integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz", - "integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz", - "integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz", - "integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz", - "integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz", - "integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz", - "integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz", - "integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz", - "integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz", - "integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz", - "integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz", - "integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz", - "integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.3.tgz", - "integrity": "sha512-w3Jnvi1ocaVm/c7yVPpfB98XeSRBMyzp6njL5MVVbGyXjpmUkN+s6Hp4t0PqhGCCaI1ZHMKXt/w0lA1RCaLVcw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.3.tgz", - "integrity": "sha512-uI/ESiaIbbRYAEhzy8PCUWDp1hB0bjAqM06mW9flOoNO4Q8DQpeoREhBR5Hegfl+wpXiguyJv6XSPzEN7OxyHQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.3.tgz", - "integrity": "sha512-oxhrd1jmXLwWZ83eQYDXxuqRdkqkzrjR3JobKeuUyfdNZo11FuQIvqEOZhyIT7OBHxXoGslDDjN0cQcM6T0TqQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.3.tgz", - "integrity": "sha512-7/YiIMghVE8DrxKvNdorAaJVdriOFgOIpdStnPx8ppx5zfTwC3jBCSEAIzB7JD5404m65THl6H93UTTVUvypmg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.3.tgz", - "integrity": "sha512-GXFZRRoMAytaI5z6N3Zhfw0WL18Q0M8r95D5hlC4GqE/lGk8pbSJNUBoOWDfbm6dTciqHj2nU87tI5f6XhQiOg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.3.tgz", - "integrity": "sha512-77W+8X3ddYgPxUpB8nZFQs2Mq+wc4HVlcSRtApXLjYBcnPMkttrSnU8VwKQjeWYhMsITHFs5cWBQ8vz1Q+5RHQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.3.tgz", - "integrity": "sha512-FVkwK+iUC+mq+GipVK46rRVticfAPtvPUNlqlGXUDxdVk/UGjQiiiUVPUrEXdSpU2ufU0XxLGyTqDtBidDOVmg==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.3.tgz", - "integrity": "sha512-+aGU1t3398yQOVj1Bz8o3e+KtswxAPvO+mtxtNdfXYMkXIHu7XhhkCD7/DEH9q8tF8uhDnMWvfpUKI8y1sZJsg==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.3.tgz", - "integrity": "sha512-cR0kjpRXR2KJ2oQK8E2KTPtphs+b9hZ8IhTZubNryt/RsqgdOZBQ2Zq0q5UedtiIi0rs3jVhJh55RE1ZHUVGUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.3.tgz", - "integrity": "sha512-y1RYi4Q3/9ByVWSSt9kX2ustE0B7kFYbJ6zZdVZVyqopZs3yhCTwRfrjIX4vezUJInma/Gs6BOFDJg7yZmJ0IQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.3.tgz", - "integrity": "sha512-DNhEA5viIj3Z5bZLE4z4oV8N5ozWqDwyt7T6KG7VdLDJ0nW+rNOYlphBl4/3HQkK75qipPLsVOfStHHOwN9WSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.3.tgz", - "integrity": "sha512-17gQCqrIpXBX2Cmi9/TygnVOqGbzsba/iaqcYSL8FY7lNugg+7AiYNs5c5nKWD+NRQha36Sa0CqkJqH4XVHwnQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.3.tgz", - "integrity": "sha512-6LwVnZRIyINpdku/yOcI8Tm9YqLmhHK5emmlOOnW9tO0SYEm1FmKPcsSAGp0NBlqR2P04xaND4jvN6sTHqhq8A==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.3.tgz", - "integrity": "sha512-xMUqkTXlEUtI/p5AAukMwBRr1enU3efsTeF+bskeFfk8t1C9rcC8sLREcZXmTfAXEbvRdJVSonVJez3TMlbR3w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.3.tgz", - "integrity": "sha512-S3E94co9F9WRRqEaUoQZ38K1gCz6KiM+nL7/3ijq7fDGF3OznjS5TasgYITlvl27GQKtu4lOAOsr5MFwkijvOA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.3.tgz", - "integrity": "sha512-1QtRDwG42x5BJI3s9mxu5rEjDnfbSnk20HQ9/ylTAYnSwYwxMVb+Vgu34wzzTQ7ogqBybebgQNUDAvZVQ38DbA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.3.tgz", - "integrity": "sha512-BQhejF6ZXOpxbngiNTP12GCGQeaDVL2QXGeBVViKIYzFHM5RKxTxwUMB1fr1BeNFphFMpnRqC5QSXFSa4z6UQw==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.3.tgz", - "integrity": "sha512-SXagRwnI2Wlwlitllu59UK/nGVbD1CKPcNqDplHwIC4BqJcpXFjD32d1R/RbuISa95HdQrZM3/7v4bKiowFaLA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.3.tgz", - "integrity": "sha512-2IPozoEALRCziGqE8O9KMK60PMu5TS1huv4fwoeCexj+WjmcwFtX9CTOVbfXCUqcELAubEwRFPYlzb/WvwY2HQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.3.tgz", - "integrity": "sha512-AoxqosUHT9IX54hFn2TiN6A7d6ZKTtE6pd2bqWtqkkNJ6HJGaU6FRouGX8L1O7R/ZwsnCnpQrHzb4pDEx+UHRQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.3.tgz", - "integrity": "sha512-d+CaftKgmkFBzCwezMqqy1d0QNNYugqLCMcYVQWBy5SS2YfeMP8Q8ripkgx9O8IyBXXLHrJ+aaCV4U96usv6Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.3.tgz", - "integrity": "sha512-xXlDF6nR1eOuXbdDy5Hl5fmtY7teUDevF/k0O7IPoZe4Tpmdv+lgdE5JRsnhQtt37ql9P0VF2kAN9a0OCZdo+Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.3.tgz", - "integrity": "sha512-YtXAgLN+JP7Ay6qG3eWhc7IHMQPzLc8r3uvhAvlJIoCz/4Q32+Bl9Fmnywidh8v1GOIMmymjovfqY9ETAtysvA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.3.tgz", - "integrity": "sha512-WuWtSJRNo549vzcfZyEgfqb6zeSgn1F+UE5kQ+BCjzz0W4MGCjntUHkZVc1VRuAM7+ULaSyhiPxD1spyewFvkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.3.tgz", - "integrity": "sha512-+lIKX7O0+IGe7WuhATaAMMeT7B76vfhXH/l9wLQL+nvyhbw2ohYCKIdWL56JfDu75CWt5oKRP4QFH/jkMtBquA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@safe-global/safe-apps-provider": { - "version": "0.18.6", - "resolved": "https://registry.npmjs.org/@safe-global/safe-apps-provider/-/safe-apps-provider-0.18.6.tgz", - "integrity": "sha512-4LhMmjPWlIO8TTDC2AwLk44XKXaK6hfBTWyljDm0HQ6TWlOEijVWNrt2s3OCVMSxlXAcEzYfqyu1daHZooTC2Q==", - "license": "MIT", - "dependencies": { - "@safe-global/safe-apps-sdk": "^9.1.0", - "events": "^3.3.0" - } - }, - "node_modules/@safe-global/safe-apps-sdk": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/@safe-global/safe-apps-sdk/-/safe-apps-sdk-9.1.0.tgz", - "integrity": "sha512-N5p/ulfnnA2Pi2M3YeWjULeWbjo7ei22JwU/IXnhoHzKq3pYCN6ynL9mJBOlvDVv892EgLPCWCOwQk/uBT2v0Q==", - "license": "MIT", - "dependencies": { - "@safe-global/safe-gateway-typescript-sdk": "^3.5.3", - "viem": "^2.1.1" - } - }, - "node_modules/@safe-global/safe-gateway-typescript-sdk": { - "version": "3.23.1", - "resolved": "https://registry.npmjs.org/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.23.1.tgz", - "integrity": "sha512-6ORQfwtEJYpalCeVO21L4XXGSdbEMfyp2hEv6cP82afKXSwvse6d3sdelgaPWUxHIsFRkWvHDdzh8IyyKHZKxw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/@scure/base": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", - "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip32": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", - "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.9.0", - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@scure/bip39": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", - "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "license": "MIT" - }, - "node_modules/@tanstack/query-core": { - "version": "5.102.8", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.102.8.tgz", - "integrity": "sha512-ZNjkJ33CqvPNec/6lZBnHqLc3EVGPZ9ySLhYahU9TcuRFdmwXewuj0c4hwSWcGHqEUwcSrKeZ+oGcvPBqXcQcg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "5.102.8", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.102.8.tgz", - "integrity": "sha512-TYBea4OuXWD7MhaSHq069TWbFe7rcwWN6kzT7JF0OKi1K6c1gTv2IzD6A6ExJsCMozdkqBWeuIUZmu4KQg0O5A==", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.102.8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18 || ^19" - } - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/debug": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", - "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/lodash": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", - "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", - "license": "MIT" - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "24.13.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.4.tgz", - "integrity": "sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.18.0" - } - }, - "node_modules/@types/react": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", - "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", - "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.3.0" - } - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT" - }, - "node_modules/@vitejs/plugin-react": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz", - "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "^1.0.1" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", - "babel-plugin-react-compiler": "^1.0.0", - "oxc-transform-react": "^0.145.0", - "vite": "^8.0.0" - }, - "peerDependenciesMeta": { - "@rolldown/plugin-babel": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - }, - "oxc-transform-react": { - "optional": true - } - } - }, - "node_modules/@vitest/expect": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", - "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.7", - "@vitest/utils": "3.2.7", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", - "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", - "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "3.2.7", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", - "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.7", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", - "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", - "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.7", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@wagmi/connectors": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@wagmi/connectors/-/connectors-6.2.0.tgz", - "integrity": "sha512-2NfkbqhNWdjfibb4abRMrn7u6rPjEGolMfApXss6HCDVt9AW2oVC6k8Q5FouzpJezElxLJSagWz9FW1zaRlanA==", - "license": "MIT", - "dependencies": { - "@base-org/account": "2.4.0", - "@coinbase/wallet-sdk": "4.3.6", - "@gemini-wallet/core": "0.3.2", - "@metamask/sdk": "0.33.1", - "@safe-global/safe-apps-provider": "0.18.6", - "@safe-global/safe-apps-sdk": "9.1.0", - "@walletconnect/ethereum-provider": "2.21.1", - "cbw-sdk": "npm:@coinbase/wallet-sdk@3.9.3", - "porto": "0.2.35" - }, - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "@wagmi/core": "2.22.1", - "typescript": ">=5.0.4", - "viem": "2.x" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@wagmi/core": { - "version": "2.22.1", - "resolved": "https://registry.npmjs.org/@wagmi/core/-/core-2.22.1.tgz", - "integrity": "sha512-cG/xwQWsBEcKgRTkQVhH29cbpbs/TdcUJVFXCyri3ZknxhMyGv0YEjTcrNpRgt2SaswL1KrvslSNYKKo+5YEAg==", - "license": "MIT", - "dependencies": { - "eventemitter3": "5.0.1", - "mipd": "0.0.7", - "zustand": "5.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "@tanstack/query-core": ">=5.0.0", - "typescript": ">=5.0.4", - "viem": "2.x" - }, - "peerDependenciesMeta": { - "@tanstack/query-core": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/@wagmi/core/node_modules/zustand": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.0.tgz", - "integrity": "sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==", - "license": "MIT", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } - } - }, - "node_modules/@walletconnect/core": { - "version": "2.21.1", - "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.21.1.tgz", - "integrity": "sha512-Tp4MHJYcdWD846PH//2r+Mu4wz1/ZU/fr9av1UWFiaYQ2t2TPLDiZxjLw54AAEpMqlEHemwCgiRiAmjR1NDdTQ==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/jsonrpc-ws-connection": "1.0.16", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.1", - "@walletconnect/utils": "2.21.1", - "@walletconnect/window-getters": "1.0.1", - "es-toolkit": "1.33.0", - "events": "3.3.0", - "uint8arrays": "3.1.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@walletconnect/environment": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@walletconnect/environment/-/environment-1.0.1.tgz", - "integrity": "sha512-T426LLZtHj8e8rYnKfzsw1aG6+M0BT1ZxayMdv/p8yM0MU+eJDISqNY3/bccxRr4LrF9csq02Rhqt08Ibl0VRg==", - "license": "MIT", - "dependencies": { - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/environment/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/ethereum-provider": { - "version": "2.21.1", - "resolved": "https://registry.npmjs.org/@walletconnect/ethereum-provider/-/ethereum-provider-2.21.1.tgz", - "integrity": "sha512-SSlIG6QEVxClgl1s0LMk4xr2wg4eT3Zn/Hb81IocyqNSGfXpjtawWxKxiC5/9Z95f1INyBD6MctJbL/R1oBwIw==", - "deprecated": "Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases", - "license": "Apache-2.0", - "dependencies": { - "@reown/appkit": "1.7.8", - "@walletconnect/jsonrpc-http-connection": "1.0.8", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/sign-client": "2.21.1", - "@walletconnect/types": "2.21.1", - "@walletconnect/universal-provider": "2.21.1", - "@walletconnect/utils": "2.21.1", - "events": "3.3.0" - } - }, - "node_modules/@walletconnect/events": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@walletconnect/events/-/events-1.0.1.tgz", - "integrity": "sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ==", - "license": "MIT", - "dependencies": { - "keyvaluestorage-interface": "^1.0.0", - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/events/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/heartbeat": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@walletconnect/heartbeat/-/heartbeat-1.2.2.tgz", - "integrity": "sha512-uASiRmC5MwhuRuf05vq4AT48Pq8RMi876zV8rr8cV969uTOzWdB/k+Lj5yI2PBtB1bGQisGen7MM1GcZlQTBXw==", - "license": "MIT", - "dependencies": { - "@walletconnect/events": "^1.0.1", - "@walletconnect/time": "^1.0.2", - "events": "^3.3.0" - } - }, - "node_modules/@walletconnect/jsonrpc-http-connection": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-http-connection/-/jsonrpc-http-connection-1.0.8.tgz", - "integrity": "sha512-+B7cRuaxijLeFDJUq5hAzNyef3e3tBDIxyaCNmFtjwnod5AGis3RToNqzFU33vpVcxFhofkpE7Cx+5MYejbMGw==", - "license": "MIT", - "dependencies": { - "@walletconnect/jsonrpc-utils": "^1.0.6", - "@walletconnect/safe-json": "^1.0.1", - "cross-fetch": "^3.1.4", - "events": "^3.3.0" - } - }, - "node_modules/@walletconnect/jsonrpc-http-connection/node_modules/cross-fetch": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", - "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.7.0" - } - }, - "node_modules/@walletconnect/jsonrpc-provider": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-provider/-/jsonrpc-provider-1.0.14.tgz", - "integrity": "sha512-rtsNY1XqHvWj0EtITNeuf8PHMvlCLiS3EjQL+WOkxEOA4KPxsohFnBDeyPYiNm4ZvkQdLnece36opYidmtbmow==", - "license": "MIT", - "dependencies": { - "@walletconnect/jsonrpc-utils": "^1.0.8", - "@walletconnect/safe-json": "^1.0.2", - "events": "^3.3.0" - } - }, - "node_modules/@walletconnect/jsonrpc-types": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-types/-/jsonrpc-types-1.0.4.tgz", - "integrity": "sha512-P6679fG/M+wuWg9TY8mh6xFSdYnFyFjwFelxyISxMDrlbXokorEVXYOxiqEbrU3x1BmBoCAJJ+vtEaEoMlpCBQ==", - "license": "MIT", - "dependencies": { - "events": "^3.3.0", - "keyvaluestorage-interface": "^1.0.0" - } - }, - "node_modules/@walletconnect/jsonrpc-utils": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-utils/-/jsonrpc-utils-1.0.8.tgz", - "integrity": "sha512-vdeb03bD8VzJUL6ZtzRYsFMq1eZQcM3EAzT0a3st59dyLfJ0wq+tKMpmGH7HlB7waD858UWgfIcudbPFsbzVdw==", - "license": "MIT", - "dependencies": { - "@walletconnect/environment": "^1.0.1", - "@walletconnect/jsonrpc-types": "^1.0.3", - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/jsonrpc-utils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/jsonrpc-ws-connection": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-ws-connection/-/jsonrpc-ws-connection-1.0.16.tgz", - "integrity": "sha512-G81JmsMqh5nJheE1mPst1W0WfVv0SG3N7JggwLLGnI7iuDZJq8cRJvQwLGKHn5H1WTW7DEPCo00zz5w62AbL3Q==", - "license": "MIT", - "dependencies": { - "@walletconnect/jsonrpc-utils": "^1.0.6", - "@walletconnect/safe-json": "^1.0.2", - "events": "^3.3.0", - "ws": "^7.5.1" - } - }, - "node_modules/@walletconnect/jsonrpc-ws-connection/node_modules/ws": { - "version": "7.5.13", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", - "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@walletconnect/keyvaluestorage": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", - "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", - "license": "MIT", - "dependencies": { - "@walletconnect/safe-json": "^1.0.1", - "idb-keyval": "^6.2.1", - "unstorage": "^1.9.0" - }, - "peerDependencies": { - "@react-native-async-storage/async-storage": "1.x" - }, - "peerDependenciesMeta": { - "@react-native-async-storage/async-storage": { - "optional": true - } - } - }, - "node_modules/@walletconnect/logger": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@walletconnect/logger/-/logger-2.1.2.tgz", - "integrity": "sha512-aAb28I3S6pYXZHQm5ESB+V6rDqIYfsnHaQyzFbwUUBFY4H0OXx/YtTl8lvhUNhMMfb9UxbwEBS253TlXUYJWSw==", - "license": "MIT", - "dependencies": { - "@walletconnect/safe-json": "^1.0.2", - "pino": "7.11.0" - } - }, - "node_modules/@walletconnect/relay-api": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@walletconnect/relay-api/-/relay-api-1.0.11.tgz", - "integrity": "sha512-tLPErkze/HmC9aCmdZOhtVmYZq1wKfWTJtygQHoWtgg722Jd4homo54Cs4ak2RUFUZIGO2RsOpIcWipaua5D5Q==", - "license": "MIT", - "dependencies": { - "@walletconnect/jsonrpc-types": "^1.0.2" - } - }, - "node_modules/@walletconnect/relay-auth": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@walletconnect/relay-auth/-/relay-auth-1.1.0.tgz", - "integrity": "sha512-qFw+a9uRz26jRCDgL7Q5TA9qYIgcNY8jpJzI1zAWNZ8i7mQjaijRnWFKsCHAU9CyGjvt6RKrRXyFtFOpWTVmCQ==", - "license": "MIT", - "dependencies": { - "@noble/curves": "1.8.0", - "@noble/hashes": "1.7.0", - "@walletconnect/safe-json": "^1.0.1", - "@walletconnect/time": "^1.0.2", - "uint8arrays": "^3.0.0" - } - }, - "node_modules/@walletconnect/relay-auth/node_modules/@noble/curves": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.0.tgz", - "integrity": "sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.7.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/relay-auth/node_modules/@noble/hashes": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.0.tgz", - "integrity": "sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/safe-json": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@walletconnect/safe-json/-/safe-json-1.0.2.tgz", - "integrity": "sha512-Ogb7I27kZ3LPC3ibn8ldyUr5544t3/STow9+lzz7Sfo808YD7SBWk7SAsdBFlYgP2zDRy2hS3sKRcuSRM0OTmA==", - "license": "MIT", - "dependencies": { - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/safe-json/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/sign-client": { - "version": "2.21.1", - "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.21.1.tgz", - "integrity": "sha512-QaXzmPsMnKGV6tc4UcdnQVNOz4zyXgarvdIQibJ4L3EmLat73r5ZVl4c0cCOcoaV7rgM9Wbphgu5E/7jNcd3Zg==", - "deprecated": "Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/core": "2.21.1", - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/logger": "2.1.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.1", - "@walletconnect/utils": "2.21.1", - "events": "3.3.0" - } - }, - "node_modules/@walletconnect/time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@walletconnect/time/-/time-1.0.2.tgz", - "integrity": "sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==", - "license": "MIT", - "dependencies": { - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/time/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/types": { - "version": "2.21.1", - "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.21.1.tgz", - "integrity": "sha512-UeefNadqP6IyfwWC1Yi7ux+ljbP2R66PLfDrDm8izmvlPmYlqRerJWJvYO4t0Vvr9wrG4Ko7E0c4M7FaPKT/sQ==", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "events": "3.3.0" - } - }, - "node_modules/@walletconnect/universal-provider": { - "version": "2.21.1", - "resolved": "https://registry.npmjs.org/@walletconnect/universal-provider/-/universal-provider-2.21.1.tgz", - "integrity": "sha512-Wjx9G8gUHVMnYfxtasC9poGm8QMiPCpXpbbLFT+iPoQskDDly8BwueWnqKs4Mx2SdIAWAwuXeZ5ojk5qQOxJJg==", - "deprecated": "Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases", - "license": "Apache-2.0", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/jsonrpc-http-connection": "1.0.8", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "2.1.2", - "@walletconnect/sign-client": "2.21.1", - "@walletconnect/types": "2.21.1", - "@walletconnect/utils": "2.21.1", - "es-toolkit": "1.33.0", - "events": "3.3.0" - } - }, - "node_modules/@walletconnect/utils": { - "version": "2.21.1", - "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.21.1.tgz", - "integrity": "sha512-VPZvTcrNQCkbGOjFRbC24mm/pzbRMUq2DSQoiHlhh0X1U7ZhuIrzVtAoKsrzu6rqjz0EEtGxCr3K1TGRqDG4NA==", - "license": "Apache-2.0", - "dependencies": { - "@noble/ciphers": "1.2.1", - "@noble/curves": "1.8.1", - "@noble/hashes": "1.7.1", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.21.1", - "@walletconnect/window-getters": "1.0.1", - "@walletconnect/window-metadata": "1.0.1", - "bs58": "6.0.0", - "detect-browser": "5.3.0", - "query-string": "7.1.3", - "uint8arrays": "3.1.0", - "viem": "2.23.2" - } - }, - "node_modules/@walletconnect/utils/node_modules/@noble/ciphers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.2.1.tgz", - "integrity": "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/utils/node_modules/@noble/curves": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", - "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.7.1" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/utils/node_modules/@noble/hashes": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", - "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/utils/node_modules/@scure/bip32": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.6.2.tgz", - "integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.8.1", - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/utils/node_modules/@scure/bip39": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.5.4.tgz", - "integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.7.1", - "@scure/base": "~1.2.4" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/utils/node_modules/abitype": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", - "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/@walletconnect/utils/node_modules/isows": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.6.tgz", - "integrity": "sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/@walletconnect/utils/node_modules/ox": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.6.7.tgz", - "integrity": "sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.10.1", - "@noble/curves": "^1.6.0", - "@noble/hashes": "^1.5.0", - "@scure/bip32": "^1.5.0", - "@scure/bip39": "^1.4.0", - "abitype": "^1.0.6", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@walletconnect/utils/node_modules/viem": { - "version": "2.23.2", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.23.2.tgz", - "integrity": "sha512-NVmW/E0c5crMOtbEAqMF0e3NmvQykFXhLOc/CkLIXOlzHSA6KXVz3CYVmaKqBF8/xtjsjHAGjdJN3Ru1kFJLaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@noble/curves": "1.8.1", - "@noble/hashes": "1.7.1", - "@scure/bip32": "1.6.2", - "@scure/bip39": "1.5.4", - "abitype": "1.0.8", - "isows": "1.0.6", - "ox": "0.6.7", - "ws": "8.18.0" - }, - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@walletconnect/utils/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@walletconnect/window-getters": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@walletconnect/window-getters/-/window-getters-1.0.1.tgz", - "integrity": "sha512-vHp+HqzGxORPAN8gY03qnbTMnhqIwjeRJNOMOAzePRg4xVEEE2WvYsI9G2NMjOknA8hnuYbU3/hwLcKbjhc8+Q==", - "license": "MIT", - "dependencies": { - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/window-getters/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/@walletconnect/window-metadata": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@walletconnect/window-metadata/-/window-metadata-1.0.1.tgz", - "integrity": "sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==", - "license": "MIT", - "dependencies": { - "@walletconnect/window-getters": "^1.0.1", - "tslib": "1.14.1" - } - }, - "node_modules/@walletconnect/window-metadata/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/abitype": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", - "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3.22.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/async-mutex": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.2.6.tgz", - "integrity": "sha512-Hs4R+4SPgamu6rSGW8C7cV9gaWUKEHykfzCCvIRuaVv636Ju10ZdeUbvb4TBEW0INuq2DHZqXbK4Nd3yG4RaRw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axios": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", - "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.16.0", - "form-data": "^4.0.6", - "https-proxy-agent": "^5.0.1", - "proxy-from-env": "^2.1.0" - } - }, - "node_modules/axios-retry": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", - "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", - "license": "Apache-2.0", - "dependencies": { - "is-retry-allowed": "^2.2.0" - }, - "peerDependencies": { - "axios": "0.x || 1.x" - } - }, - "node_modules/base-x": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", - "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/big.js": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-6.2.2.tgz", - "integrity": "sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==", - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/bigjs" - } - }, - "node_modules/bn.js": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.5.tgz", - "integrity": "sha512-Vq886eXykuP5E6HcKSSStP3bJgrE6In5WKxVUvJ8XGpWWYs2xZHWqUwzCtGgEtBcxyd57KBFDPFoUfNzdaHCNg==", - "license": "MIT" - }, - "node_modules/bowser": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", - "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", - "license": "MIT" - }, - "node_modules/bs58": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", - "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", - "license": "MIT", - "dependencies": { - "base-x": "^5.0.0" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/bufferutil": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", - "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", - "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "get-intrinsic": "^1.3.0", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/cbw-sdk": { - "name": "@coinbase/wallet-sdk", - "version": "3.9.3", - "resolved": "https://registry.npmjs.org/@coinbase/wallet-sdk/-/wallet-sdk-3.9.3.tgz", - "integrity": "sha512-N/A2DRIf0Y3PHc1XAMvbBUu4zisna6qAdqABMZwBMNEfWrXpAwx16pZGkYCLGE+Rvv1edbcB2LYDRnACNcmCiw==", - "license": "Apache-2.0", - "dependencies": { - "bn.js": "^5.2.1", - "buffer": "^6.0.3", - "clsx": "^1.2.1", - "eth-block-tracker": "^7.1.0", - "eth-json-rpc-filters": "^6.0.0", - "eventemitter3": "^5.0.1", - "keccak": "^3.0.3", - "preact": "^10.16.0", - "sha.js": "^2.4.11" - } - }, - "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, - "node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "license": "MIT", - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", - "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/cookie-es": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz", - "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, - "node_modules/crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "license": "Apache-2.0", - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/cross-fetch": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz", - "integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.7.0" - } - }, - "node_modules/crossws": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", - "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", - "license": "MIT", - "dependencies": { - "uncrypto": "^0.1.3" - } - }, - "node_modules/crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.21.0" - }, - "engines": { - "node": ">=0.11" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" - } - }, - "node_modules/dayjs": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", - "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/defu": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", - "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", - "license": "MIT" - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/derive-valtio": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/derive-valtio/-/derive-valtio-0.1.0.tgz", - "integrity": "sha512-OCg2UsLbXK7GmmpzMXhYkdO64vhJ1ROUUGaTFyHjVwEdMEcTTRj7W1TxLbSBxdY8QLBPCcp66MTyaSy0RpO17A==", - "license": "MIT", - "peerDependencies": { - "valtio": "*" - } - }, - "node_modules/destr": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", - "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", - "license": "MIT" - }, - "node_modules/detect-browser": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/detect-browser/-/detect-browser-5.3.0.tgz", - "integrity": "sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==", - "license": "MIT" - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/dijkstrajs": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", - "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", - "license": "MIT" - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/duplexify": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", - "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.2" - } - }, - "node_modules/eciesjs": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.18.tgz", - "integrity": "sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==", - "license": "MIT", - "dependencies": { - "@ecies/ciphers": "^0.2.5", - "@noble/ciphers": "^1.3.0", - "@noble/curves": "^1.9.7", - "@noble/hashes": "^1.8.0" - }, - "engines": { - "bun": ">=1", - "deno": ">=2", - "node": ">=16" - } - }, - "node_modules/eciesjs/node_modules/@noble/curves": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", - "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/encode-utf8": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", - "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==", - "license": "MIT" - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/engine.io-client": { - "version": "6.6.6", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz", - "integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.4.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.21.0", - "xmlhttprequest-ssl": "~2.1.1" - } - }, - "node_modules/engine.io-client/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io-client/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-toolkit": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.33.0.tgz", - "integrity": "sha512-X13Q/ZSc+vsO1q600bvNK4bxgXMkHcf//RxCmYDaRY5DAcT+eoXjY5hoAPGMdRnWQjvyLEcyauG3b6hz76LNqg==", - "license": "MIT", - "workspaces": [ - "docs", - "benchmarks" - ] - }, - "node_modules/esbuild": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", - "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.2", - "@esbuild/android-arm": "0.28.2", - "@esbuild/android-arm64": "0.28.2", - "@esbuild/android-x64": "0.28.2", - "@esbuild/darwin-arm64": "0.28.2", - "@esbuild/darwin-x64": "0.28.2", - "@esbuild/freebsd-arm64": "0.28.2", - "@esbuild/freebsd-x64": "0.28.2", - "@esbuild/linux-arm": "0.28.2", - "@esbuild/linux-arm64": "0.28.2", - "@esbuild/linux-ia32": "0.28.2", - "@esbuild/linux-loong64": "0.28.2", - "@esbuild/linux-mips64el": "0.28.2", - "@esbuild/linux-ppc64": "0.28.2", - "@esbuild/linux-riscv64": "0.28.2", - "@esbuild/linux-s390x": "0.28.2", - "@esbuild/linux-x64": "0.28.2", - "@esbuild/netbsd-arm64": "0.28.2", - "@esbuild/netbsd-x64": "0.28.2", - "@esbuild/openbsd-arm64": "0.28.2", - "@esbuild/openbsd-x64": "0.28.2", - "@esbuild/openharmony-arm64": "0.28.2", - "@esbuild/sunos-x64": "0.28.2", - "@esbuild/win32-arm64": "0.28.2", - "@esbuild/win32-ia32": "0.28.2", - "@esbuild/win32-x64": "0.28.2" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/eth-block-tracker": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/eth-block-tracker/-/eth-block-tracker-7.1.0.tgz", - "integrity": "sha512-8YdplnuE1IK4xfqpf4iU7oBxnOYAc35934o083G8ao+8WM8QQtt/mVlAY6yIAdY1eMeLqg4Z//PZjJGmWGPMRg==", - "license": "MIT", - "dependencies": { - "@metamask/eth-json-rpc-provider": "^1.0.0", - "@metamask/safe-event-emitter": "^3.0.0", - "@metamask/utils": "^5.0.1", - "json-rpc-random-id": "^1.0.1", - "pify": "^3.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/eth-block-tracker/node_modules/@metamask/utils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@metamask/utils/-/utils-5.0.2.tgz", - "integrity": "sha512-yfmE79bRQtnMzarnKfX7AEJBwFTxvTyw3nBQlu/5rmGXrjAeAMltoGxO62TFurxrQAFMNa/fEjIHNvungZp0+g==", - "license": "ISC", - "dependencies": { - "@ethereumjs/tx": "^4.1.2", - "@types/debug": "^4.1.7", - "debug": "^4.3.4", - "semver": "^7.3.8", - "superstruct": "^1.0.3" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/eth-json-rpc-filters": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/eth-json-rpc-filters/-/eth-json-rpc-filters-6.0.1.tgz", - "integrity": "sha512-ITJTvqoCw6OVMLs7pI8f4gG92n/St6x80ACtHodeS+IXmO0w+t1T5OOzfSt7KLSMLRkVUoexV7tztLgDxg+iig==", - "license": "ISC", - "dependencies": { - "@metamask/safe-event-emitter": "^3.0.0", - "async-mutex": "^0.2.6", - "eth-query": "^2.1.2", - "json-rpc-engine": "^6.1.0", - "pify": "^5.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/eth-json-rpc-filters/node_modules/pify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", - "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eth-query": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/eth-query/-/eth-query-2.1.2.tgz", - "integrity": "sha512-srES0ZcvwkR/wd5OQBRA1bIJMww1skfGS0s8wlwK3/oNP4+wnds60krvu5R1QbpRQjMmpG5OMIWro5s7gvDPsA==", - "license": "ISC", - "dependencies": { - "json-rpc-random-id": "^1.0.0", - "xtend": "^4.0.1" - } - }, - "node_modules/eth-rpc-errors": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eth-rpc-errors/-/eth-rpc-errors-4.0.3.tgz", - "integrity": "sha512-Z3ymjopaoft7JDoxZcEb3pwdGh7yiYMhOwm2doUt6ASXlMavpNlK6Cre0+IMl2VSGyEU9rkiperQhp5iRxn5Pg==", - "license": "MIT", - "dependencies": { - "fast-safe-stringify": "^2.0.6" - } - }, - "node_modules/ethereum-cryptography": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz", - "integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==", - "license": "MIT", - "dependencies": { - "@noble/curves": "1.4.2", - "@noble/hashes": "1.4.0", - "@scure/bip32": "1.4.0", - "@scure/bip39": "1.3.0" - } - }, - "node_modules/ethereum-cryptography/node_modules/@noble/curves": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", - "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.4.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ethereum-cryptography/node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ethereum-cryptography/node_modules/@scure/base": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", - "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ethereum-cryptography/node_modules/@scure/bip32": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", - "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.4.0", - "@noble/hashes": "~1.4.0", - "@scure/base": "~1.1.6" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ethereum-cryptography/node_modules/@scure/bip39": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", - "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.4.0", - "@scure/base": "~1.1.6" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/eventemitter2": { - "version": "6.4.9", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", - "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==", - "license": "MIT" - }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "license": "MIT" - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/expect-type": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", - "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/extension-port-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/extension-port-stream/-/extension-port-stream-3.0.0.tgz", - "integrity": "sha512-an2S5quJMiy5bnZKEf6AkfH/7r8CzHvhchU40gxN+OM6HPhe7Z9T1FUychcf2M9PpPOO0Hf7BAEfJkw2TDIBDw==", - "license": "ISC", - "dependencies": { - "readable-stream": "^3.6.2 || ^4.4.2", - "webextension-polyfill": ">=0.10.0 <1.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-redact": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", - "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/filter-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", - "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/form-data": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", - "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.4", - "mime-types": "^2.1.35" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/generator-function": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/h3": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", - "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==", - "license": "MIT", - "dependencies": { - "cookie-es": "^1.2.3", - "crossws": "^0.3.5", - "defu": "^6.1.6", - "destr": "^2.0.5", - "iron-webcrypto": "^1.2.1", - "node-mock-http": "^1.0.4", - "radix3": "^1.1.2", - "ufo": "^1.6.3", - "uncrypto": "^0.1.3" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hono": { - "version": "4.13.7", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.7.tgz", - "integrity": "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==", - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/idb-keyval": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz", - "integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==", - "license": "Apache-2.0" - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/iron-webcrypto": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", - "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/brc-dd" - } - }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "license": "MIT" - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.4", - "generator-function": "^2.0.0", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-retry-allowed": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", - "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "license": "MIT" - }, - "node_modules/isows": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", - "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, - "node_modules/jose": { - "version": "6.2.12", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", - "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-rpc-engine": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/json-rpc-engine/-/json-rpc-engine-6.1.0.tgz", - "integrity": "sha512-NEdLrtrq1jUZyfjkr9OCz9EzCNhnRyWtt1PAnvnhwy6e8XETS0Dtc+ZNCO2gvuAoKsIn2+vCSowXTYE4CkgnAQ==", - "license": "ISC", - "dependencies": { - "@metamask/safe-event-emitter": "^2.0.0", - "eth-rpc-errors": "^4.0.2" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/json-rpc-engine/node_modules/@metamask/safe-event-emitter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@metamask/safe-event-emitter/-/safe-event-emitter-2.0.0.tgz", - "integrity": "sha512-/kSXhY692qiV1MXu6EeOZvg5nECLclxNXcKCxJ3cXQgYuRymRHpdx/t7JXfsK+JLjwA1e1c1/SBrlQYpusC29Q==", - "license": "ISC" - }, - "node_modules/json-rpc-random-id": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-rpc-random-id/-/json-rpc-random-id-1.0.1.tgz", - "integrity": "sha512-RJ9YYNCkhVDBuP4zN5BBtYAzEl03yq/jIIsyif0JY9qyJuQQZNeDK7anAPKKlyEtLSj2s8h6hNh2F8zO5q7ScA==", - "license": "ISC" - }, - "node_modules/keccak": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.4.tgz", - "integrity": "sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^2.0.0", - "node-gyp-build": "^4.2.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/keyvaluestorage-interface": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/keyvaluestorage-interface/-/keyvaluestorage-interface-1.0.0.tgz", - "integrity": "sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g==", - "license": "MIT" - }, - "node_modules/lightningcss": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", - "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.33.0", - "lightningcss-darwin-arm64": "1.33.0", - "lightningcss-darwin-x64": "1.33.0", - "lightningcss-freebsd-x64": "1.33.0", - "lightningcss-linux-arm-gnueabihf": "1.33.0", - "lightningcss-linux-arm64-gnu": "1.33.0", - "lightningcss-linux-arm64-musl": "1.33.0", - "lightningcss-linux-x64-gnu": "1.33.0", - "lightningcss-linux-x64-musl": "1.33.0", - "lightningcss-win32-arm64-msvc": "1.33.0", - "lightningcss-win32-x64-msvc": "1.33.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", - "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", - "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", - "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", - "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", - "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", - "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", - "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", - "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", - "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", - "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", - "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lit": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.0.tgz", - "integrity": "sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit/reactive-element": "^2.1.0", - "lit-element": "^4.2.0", - "lit-html": "^3.3.0" - } - }, - "node_modules/lit-element": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", - "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.5.0", - "@lit/reactive-element": "^2.1.0", - "lit-html": "^3.3.0" - } - }, - "node_modules/lit-html": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.3.tgz", - "integrity": "sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA==", - "license": "BSD-3-Clause", - "dependencies": { - "@types/trusted-types": "^2.0.2" - } - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" - }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/md5": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", - "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "license": "BSD-3-Clause", - "dependencies": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" - } - }, - "node_modules/micro-ftch": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/micro-ftch/-/micro-ftch-0.3.1.tgz", - "integrity": "sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==", - "license": "MIT" - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mipd": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mipd/-/mipd-0.0.7.tgz", - "integrity": "sha512-aAPZPNDQ3uMTdKbuO2YmAw2TxLHO0moa4YKAyETM/DTj5FloZo+a+8tU+iv4GmW+sOxKLSRwcSFuczk+Cpt6fg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wagmi-dev" - } - ], - "license": "MIT", - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "license": "MIT" - }, - "node_modules/multiformats": { - "version": "9.9.0", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", - "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", - "license": "(Apache-2.0 AND MIT)" - }, - "node_modules/nanoid": { - "version": "3.3.19", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", - "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-addon-api": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", - "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==", - "license": "MIT" - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch-native": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", - "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", - "license": "MIT" - }, - "node_modules/node-gyp-build": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "license": "MIT", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/node-mock-http": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.5.tgz", - "integrity": "sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw==", - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/obj-multiplex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/obj-multiplex/-/obj-multiplex-1.0.0.tgz", - "integrity": "sha512-0GNJAOsHoBHeNTvl5Vt6IWnpUEcc3uSRxzBri7EDyIcMgYvnY2JL2qdeV5zTMjWQX5OHcD5amcW2HFfDh0gjIA==", - "license": "ISC", - "dependencies": { - "end-of-stream": "^1.4.0", - "once": "^1.4.0", - "readable-stream": "^2.3.3" - } - }, - "node_modules/obj-multiplex/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/obj-multiplex/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/obj-multiplex/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/obj-multiplex/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/ofetch": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", - "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", - "license": "MIT", - "dependencies": { - "destr": "^2.0.5", - "node-fetch-native": "^1.6.7", - "ufo": "^1.6.1" - } - }, - "node_modules/on-exit-leak-free": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz", - "integrity": "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==", - "license": "MIT" - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/openapi-fetch": { - "version": "0.13.8", - "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.13.8.tgz", - "integrity": "sha512-yJ4QKRyNxE44baQ9mY5+r/kAzZ8yXMemtNAOFwOzRXJscdjSxxzWSNlyBAr+o5JjkUw9Lc3W7OIoca0cY3PYnQ==", - "license": "MIT", - "dependencies": { - "openapi-typescript-helpers": "^0.0.15" - } - }, - "node_modules/openapi-typescript-helpers": { - "version": "0.0.15", - "resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.15.tgz", - "integrity": "sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==", - "license": "MIT" - }, - "node_modules/ox": { - "version": "0.14.44", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.44.tgz", - "integrity": "sha512-O54qXXHEk4ySamMSyBpI0AeBegS5frxU6+LA6Jb9Sf6kMOxcTNaxYmwZfpOu22DIQsdKfgQxHq8EsAGaoBQScA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.11.0", - "@noble/ciphers": "^1.3.0", - "@noble/curves": "1.9.1", - "@noble/hashes": "^1.8.0", - "@scure/bip32": "^1.7.0", - "@scure/bip39": "^1.6.0", - "abitype": "^1.2.3", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/oxlint": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.83.0.tgz", - "integrity": "sha512-cyDzSzaw3uzP0TeCeq3lLRPPoaUxkbB4ZOXj+kn+5r+BX9V+4bNVGk9lxer+WrgcpebH4JxLlJ3KQjveVztOLQ==", - "dev": true, - "license": "MIT", - "bin": { - "oxlint": "bin/oxlint" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/sponsors/oxc-project" - }, - "optionalDependencies": { - "@oxlint/binding-android-arm-eabi": "1.83.0", - "@oxlint/binding-android-arm64": "1.83.0", - "@oxlint/binding-darwin-arm64": "1.83.0", - "@oxlint/binding-darwin-x64": "1.83.0", - "@oxlint/binding-freebsd-x64": "1.83.0", - "@oxlint/binding-linux-arm-gnueabihf": "1.83.0", - "@oxlint/binding-linux-arm-musleabihf": "1.83.0", - "@oxlint/binding-linux-arm64-gnu": "1.83.0", - "@oxlint/binding-linux-arm64-musl": "1.83.0", - "@oxlint/binding-linux-ppc64-gnu": "1.83.0", - "@oxlint/binding-linux-riscv64-gnu": "1.83.0", - "@oxlint/binding-linux-riscv64-musl": "1.83.0", - "@oxlint/binding-linux-s390x-gnu": "1.83.0", - "@oxlint/binding-linux-x64-gnu": "1.83.0", - "@oxlint/binding-linux-x64-musl": "1.83.0", - "@oxlint/binding-openharmony-arm64": "1.83.0", - "@oxlint/binding-win32-arm64-msvc": "1.83.0", - "@oxlint/binding-win32-ia32-msvc": "1.83.0", - "@oxlint/binding-win32-x64-msvc": "1.83.0" - }, - "peerDependencies": { - "oxlint-tsgolint": ">=7.0.2001", - "vite-plus": "*" - }, - "peerDependenciesMeta": { - "oxlint-tsgolint": { - "optional": true - }, - "vite-plus": { - "optional": true - } - } - }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/pino": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-7.11.0.tgz", - "integrity": "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==", - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0", - "fast-redact": "^3.0.0", - "on-exit-leak-free": "^0.2.0", - "pino-abstract-transport": "v0.5.0", - "pino-std-serializers": "^4.0.0", - "process-warning": "^1.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.1.0", - "safe-stable-stringify": "^2.1.0", - "sonic-boom": "^2.2.1", - "thread-stream": "^0.15.1" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-0.5.0.tgz", - "integrity": "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==", - "license": "MIT", - "dependencies": { - "duplexify": "^4.1.2", - "split2": "^4.0.0" - } - }, - "node_modules/pino-std-serializers": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz", - "integrity": "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==", - "license": "MIT" - }, - "node_modules/pngjs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/pony-cause": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-2.1.11.tgz", - "integrity": "sha512-M7LhCsdNbNgiLYiP4WjsfLUuFmCfnjdF6jKe2R9NKl4WFN+HZPGHJZ9lnLP7f9ZnKe3U9nuWD0szirmj+migUg==", - "license": "0BSD", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/porto": { - "version": "0.2.35", - "resolved": "https://registry.npmjs.org/porto/-/porto-0.2.35.tgz", - "integrity": "sha512-gu9FfjjvvYBgQXUHWTp6n3wkTxVtEcqFotM7i3GEZeoQbvLGbssAicCz6hFZ8+xggrJWwi/RLmbwNra50SMmUQ==", - "license": "MIT", - "dependencies": { - "hono": "^4.10.3", - "idb-keyval": "^6.2.1", - "mipd": "^0.0.7", - "ox": "^0.9.6", - "zod": "^4.1.5", - "zustand": "^5.0.1" - }, - "bin": { - "porto": "dist/cli/bin/index.js" - }, - "peerDependencies": { - "@tanstack/react-query": ">=5.59.0", - "@wagmi/core": ">=2.16.3", - "expo-auth-session": ">=7.0.8", - "expo-crypto": ">=15.0.7", - "expo-web-browser": ">=15.0.8", - "react": ">=18", - "react-native": ">=0.81.4", - "typescript": ">=5.4.0", - "viem": ">=2.37.0", - "wagmi": ">=2.0.0" - }, - "peerDependenciesMeta": { - "@tanstack/react-query": { - "optional": true - }, - "expo-auth-session": { - "optional": true - }, - "expo-crypto": { - "optional": true - }, - "expo-web-browser": { - "optional": true - }, - "react": { - "optional": true - }, - "react-native": { - "optional": true - }, - "typescript": { - "optional": true - }, - "wagmi": { - "optional": true - } - } - }, - "node_modules/porto/node_modules/ox": { - "version": "0.9.17", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.9.17.tgz", - "integrity": "sha512-rKAnhzhRU3Xh3hiko+i1ZxywZ55eWQzeS/Q4HRKLx2PqfHOolisZHErSsJVipGlmQKHW5qwOED/GighEw9dbLg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.11.0", - "@noble/ciphers": "^1.3.0", - "@noble/curves": "1.9.1", - "@noble/hashes": "^1.8.0", - "@scure/bip32": "^1.7.0", - "@scure/bip39": "^1.6.0", - "abitype": "^1.0.9", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/porto/node_modules/zod": { - "version": "4.6.5", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", - "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.5.28", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", - "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.18", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/preact": { - "version": "10.24.2", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.2.tgz", - "integrity": "sha512-1cSoF0aCC8uaARATfrlz4VCBqE8LwZwRfLgkxJOQwAlQt6ayTmi0D9OF7nXid1POI5SZidFuG9CnlXbDfLqY/Q==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, - "node_modules/process-warning": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", - "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==", - "license": "MIT" - }, - "node_modules/proxy-compare": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.6.0.tgz", - "integrity": "sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw==", - "license": "MIT" - }, - "node_modules/proxy-from-env": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", - "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/qrcode": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", - "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", - "license": "MIT", - "dependencies": { - "dijkstrajs": "^1.0.1", - "encode-utf8": "^1.0.3", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "qrcode": "bin/qrcode" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/query-string": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", - "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", - "license": "MIT", - "dependencies": { - "decode-uri-component": "^0.2.2", - "filter-obj": "^1.1.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", - "license": "MIT" - }, - "node_modules/radix3": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", - "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", - "license": "MIT" - }, - "node_modules/react": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", - "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", - "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.28.0" - }, - "peerDependencies": { - "react": "^19.3.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", - "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/real-require": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.1.0.tgz", - "integrity": "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "license": "ISC" - }, - "node_modules/rolldown": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz", - "integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.149.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.8", - "@rolldown/binding-android-arm64": "1.2.8", - "@rolldown/binding-darwin-arm64": "1.2.8", - "@rolldown/binding-darwin-x64": "1.2.8", - "@rolldown/binding-freebsd-x64": "1.2.8", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", - "@rolldown/binding-linux-arm64-gnu": "1.2.8", - "@rolldown/binding-linux-arm64-musl": "1.2.8", - "@rolldown/binding-linux-ppc64-gnu": "1.2.8", - "@rolldown/binding-linux-s390x-gnu": "1.2.8", - "@rolldown/binding-linux-x64-gnu": "1.2.8", - "@rolldown/binding-linux-x64-musl": "1.2.8", - "@rolldown/binding-openharmony-arm64": "1.2.8", - "@rolldown/binding-win32-arm64-msvc": "1.2.8", - "@rolldown/binding-win32-x64-msvc": "1.2.8" - } - }, - "node_modules/rollup": { - "version": "4.63.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.3.tgz", - "integrity": "sha512-1i2XreiAoMMXuPGD6Msj2xWrMMkHojNRKivInxGQcg7/1KuPuYlfUutLyh4drnOxUTHX9cHI4wFoat8D/NKaBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.9" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@napi-rs/lzma-linux-x64-gnu": "1.5.1", - "@rollup/rollup-android-arm-eabi": "4.63.3", - "@rollup/rollup-android-arm64": "4.63.3", - "@rollup/rollup-darwin-arm64": "4.63.3", - "@rollup/rollup-darwin-x64": "4.63.3", - "@rollup/rollup-freebsd-arm64": "4.63.3", - "@rollup/rollup-freebsd-x64": "4.63.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.63.3", - "@rollup/rollup-linux-arm-musleabihf": "4.63.3", - "@rollup/rollup-linux-arm64-gnu": "4.63.3", - "@rollup/rollup-linux-arm64-musl": "4.63.3", - "@rollup/rollup-linux-loong64-gnu": "4.63.3", - "@rollup/rollup-linux-loong64-musl": "4.63.3", - "@rollup/rollup-linux-ppc64-gnu": "4.63.3", - "@rollup/rollup-linux-ppc64-musl": "4.63.3", - "@rollup/rollup-linux-riscv64-gnu": "4.63.3", - "@rollup/rollup-linux-riscv64-musl": "4.63.3", - "@rollup/rollup-linux-s390x-gnu": "4.63.3", - "@rollup/rollup-linux-x64-gnu": "4.63.3", - "@rollup/rollup-linux-x64-musl": "4.63.3", - "@rollup/rollup-openbsd-x64": "4.63.3", - "@rollup/rollup-openharmony-arm64": "4.63.3", - "@rollup/rollup-win32-arm64-msvc": "4.63.3", - "@rollup/rollup-win32-ia32-msvc": "4.63.3", - "@rollup/rollup-win32-x64-gnu": "4.63.3", - "@rollup/rollup-win32-x64-msvc": "4.63.3", - "fsevents": "~2.3.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/scheduler": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", - "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC" - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/sha.js": { - "version": "2.4.12", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", - "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", - "license": "(MIT AND BSD-3-Clause)", - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1", - "to-buffer": "^1.2.0" - }, - "bin": { - "sha.js": "bin.js" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/socket.io-client": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", - "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.4.1", - "engine.io-client": "~6.6.1", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-client/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-client/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/socket.io-parser": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz", - "integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.4.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-parser/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/sonic-boom": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.8.0.tgz", - "integrity": "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==", - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split-on-first": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, - "license": "MIT" - }, - "node_modules/stream-shift": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", - "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", - "license": "MIT" - }, - "node_modules/strict-uri-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", - "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/superstruct": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.4.tgz", - "integrity": "sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/thread-stream": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-0.15.2.tgz", - "integrity": "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==", - "license": "MIT", - "dependencies": { - "real-require": "^0.1.0" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", - "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/to-buffer": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", - "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", - "license": "MIT", - "dependencies": { - "isarray": "^2.0.5", - "safe-buffer": "^5.2.1", - "typed-array-buffer": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/ufo": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", - "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", - "license": "MIT" - }, - "node_modules/uint8arrays": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.0.tgz", - "integrity": "sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog==", - "license": "MIT", - "dependencies": { - "multiformats": "^9.4.2" - } - }, - "node_modules/uncrypto": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", - "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/unstorage": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", - "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", - "license": "MIT", - "dependencies": { - "anymatch": "^3.1.3", - "chokidar": "^5.0.0", - "destr": "^2.0.5", - "h3": "^1.15.10", - "lru-cache": "^11.2.7", - "node-fetch-native": "^1.6.7", - "ofetch": "^1.5.1", - "ufo": "^1.6.3" - }, - "peerDependencies": { - "@azure/app-configuration": "^1.8.0", - "@azure/cosmos": "^4.2.0", - "@azure/data-tables": "^13.3.0", - "@azure/identity": "^4.6.0", - "@azure/keyvault-secrets": "^4.9.0", - "@azure/storage-blob": "^12.26.0", - "@capacitor/preferences": "^6 || ^7 || ^8", - "@deno/kv": ">=0.9.0", - "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", - "@planetscale/database": "^1.19.0", - "@upstash/redis": "^1.34.3", - "@vercel/blob": ">=0.27.1", - "@vercel/functions": "^2.2.12 || ^3.0.0", - "@vercel/kv": "^1 || ^2 || ^3", - "aws4fetch": "^1.0.20", - "db0": ">=0.2.1", - "idb-keyval": "^6.2.1", - "ioredis": "^5.4.2", - "uploadthing": "^7.4.4" - }, - "peerDependenciesMeta": { - "@azure/app-configuration": { - "optional": true - }, - "@azure/cosmos": { - "optional": true - }, - "@azure/data-tables": { - "optional": true - }, - "@azure/identity": { - "optional": true - }, - "@azure/keyvault-secrets": { - "optional": true - }, - "@azure/storage-blob": { - "optional": true - }, - "@capacitor/preferences": { - "optional": true - }, - "@deno/kv": { - "optional": true - }, - "@netlify/blobs": { - "optional": true - }, - "@planetscale/database": { - "optional": true - }, - "@upstash/redis": { - "optional": true - }, - "@vercel/blob": { - "optional": true - }, - "@vercel/functions": { - "optional": true - }, - "@vercel/kv": { - "optional": true - }, - "aws4fetch": { - "optional": true - }, - "db0": { - "optional": true - }, - "idb-keyval": { - "optional": true - }, - "ioredis": { - "optional": true - }, - "uploadthing": { - "optional": true - } - } - }, - "node_modules/use-sync-external-store": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", - "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/utf-8-validate": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/valtio": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/valtio/-/valtio-1.13.2.tgz", - "integrity": "sha512-Qik0o+DSy741TmkqmRfjq+0xpZBXi/Y6+fXZLn0xNF1z/waFMbE3rkivv5Zcf9RrMUp6zswf2J7sbh2KBlba5A==", - "license": "MIT", - "dependencies": { - "derive-valtio": "0.1.0", - "proxy-compare": "2.6.0", - "use-sync-external-store": "1.2.0" - }, - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "react": { - "optional": true - } - } - }, - "node_modules/valtio/node_modules/use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/viem": { - "version": "2.56.5", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.56.5.tgz", - "integrity": "sha512-GuEf/oee0PHgy4D6JIA8u0rY1g+sM5nF+p7HyMHO3vjlHC5G/ljfrgnUOnrES9m5Ny/P3RwWCHjDuN2T6HUWzQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@noble/curves": "1.9.1", - "@noble/hashes": "1.8.0", - "@scure/bip32": "1.7.0", - "@scure/bip39": "1.6.0", - "abitype": "1.2.3", - "isows": "1.0.7", - "ox": "0.14.44", - "ws": "8.21.0" - }, - "peerDependencies": { - "typescript": ">=5.0.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/vite": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", - "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "lightningcss": "^1.33.0", - "picomatch": "^4.0.7", - "postcss": "^8.5.28", - "rolldown": "~1.2.6", - "tinyglobby": "^0.2.17" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.7.1", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite-node/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/vite-node/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite-node/node_modules/vite": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", - "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.27.0 || ^0.28.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vitest": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", - "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.7", - "@vitest/mocker": "3.2.7", - "@vitest/pretty-format": "^3.2.7", - "@vitest/runner": "3.2.7", - "@vitest/snapshot": "3.2.7", - "@vitest/spy": "3.2.7", - "@vitest/utils": "3.2.7", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", - "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.7", - "@vitest/ui": "3.2.7", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/debug": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/@vitest/mocker": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", - "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "3.2.7", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/vitest/node_modules/vite": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", - "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.27.0 || ^0.28.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/wagmi": { - "version": "2.19.5", - "resolved": "https://registry.npmjs.org/wagmi/-/wagmi-2.19.5.tgz", - "integrity": "sha512-RQUfKMv6U+EcSNNGiPbdkDtJwtuFxZWLmvDiQmjjBgkuPulUwDJsKhi7gjynzJdsx2yDqhHCXkKsbbfbIsHfcQ==", - "license": "MIT", - "dependencies": { - "@wagmi/connectors": "6.2.0", - "@wagmi/core": "2.22.1", - "use-sync-external-store": "1.4.0" - }, - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "@tanstack/react-query": ">=5.0.0", - "react": ">=18", - "typescript": ">=5.0.4", - "viem": "2.x" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/webextension-polyfill": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/webextension-polyfill/-/webextension-polyfill-0.10.0.tgz", - "integrity": "sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g==", - "license": "MPL-2.0" - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "license": "ISC" - }, - "node_modules/which-typed-array": { - "version": "1.1.22", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", - "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", - "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "license": "ISC" - }, - "node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "license": "MIT", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zustand": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", - "integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==", - "license": "MIT", - "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } - } - } - } -} diff --git a/web/package.json b/web/package.json deleted file mode 100644 index 6b36f7f..0000000 --- a/web/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "web", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "lint": "oxlint", - "test": "vitest run", - "preview": "vite preview" - }, - "dependencies": { - "@tanstack/react-query": "^5.102.8", - "react": "^19.2.8", - "react-dom": "^19.2.8", - "viem": "^2.56.5", - "wagmi": "^2.19.5" - }, - "devDependencies": { - "@types/node": "^24.13.3", - "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.7", - "@vitejs/plugin-react": "^6.1.1", - "oxlint": "^1.81.0", - "typescript": "~6.0.2", - "vite": "^8.3.0", - "vitest": "^3.2.7" - } -} diff --git a/web/public/favicon.svg b/web/public/favicon.svg deleted file mode 100644 index 3d571db..0000000 --- a/web/public/favicon.svg +++ /dev/null @@ -1 +0,0 @@ -S diff --git a/web/public/icons.svg b/web/public/icons.svg deleted file mode 100644 index e952219..0000000 --- a/web/public/icons.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/src/App.tsx b/web/src/App.tsx deleted file mode 100644 index 5144dc1..0000000 --- a/web/src/App.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { useEffect, useState } from "react"; -import { AppShell, type AppPage } from "./components/AppShell"; -import { Skeleton } from "./components/Reveal"; -import { TxFlow } from "./components/TxFlow"; -import { Empty } from "./components/ui"; -import { useInvalidateVault, useVault } from "./hooks/useVault"; -import { AddressBook } from "./pages/AddressBook"; -import { Assets } from "./pages/Assets"; -import { CreateVault } from "./pages/CreateVault"; -import { Landing } from "./pages/Landing"; -import { Members } from "./pages/Members"; -import { Overview } from "./pages/Overview"; -import { Policies } from "./pages/Policies"; -import { Product } from "./pages/Product"; -import { Security } from "./pages/Security"; -import { SecurityPage } from "./pages/SecurityPage"; -import { Settings } from "./pages/Settings"; -import { Simulator } from "./pages/Simulator"; -import { Solutions } from "./pages/Solutions"; -import { Transactions } from "./pages/Transactions"; -import { Welcome } from "./pages/Welcome"; -import { ThemeContext, useThemeState } from "./state/theme"; -import { useVaultAddress } from "./state/vaultAddress"; - -/** - * Routes, patterned on safe.global -> app.safe.global: - * #/ #/product #/solutions #/security marketing - * #/welcome #/new-vault onboarding - * #/app/ the vault app - */ -type Route = { kind: "landing" } | { kind: "product" } | { kind: "solutions" } | { kind: "securitypage" } | { kind: "welcome" } | { kind: "create" } | { kind: "app"; page: AppPage }; - -const APP_PAGES: AppPage[] = ["overview", "assets", "transactions", "addressbook", "members", "settings", "policies", "security", "simulator"]; -const LEGACY: Record = { home: "overview", recipients: "addressbook" }; - -function readRoute(): Route { - const raw = window.location.hash.replace(/^#\/?/, "").split("?")[0].split("#")[0]; - if (raw === "" || raw === "/") return { kind: "landing" }; - if (raw === "product") return { kind: "product" }; - if (raw === "solutions") return { kind: "solutions" }; - if (raw === "security") return { kind: "securitypage" }; - if (raw === "welcome") return { kind: "welcome" }; - if (raw === "new-vault") return { kind: "create" }; - const seg = raw.replace(/^app\//, ""); - const page = (LEGACY[seg] ?? seg) as AppPage; - if (APP_PAGES.includes(page)) return { kind: "app", page }; - return { kind: "landing" }; -} - -function go(route: Route) { - const hash = route.kind === "landing" ? "#/" : route.kind === "product" ? "#/product" : route.kind === "solutions" ? "#/solutions" : route.kind === "securitypage" ? "#/security" : route.kind === "welcome" ? "#/welcome" : route.kind === "create" ? "#/new-vault" : `#/app/${route.page}`; - if (window.location.hash !== hash) window.location.hash = hash; - window.scrollTo({ top: 0 }); -} - -function LoadingVault() { - return ( -
-
-
-
-
-
-
-
-
-
- ); -} - -export default function App() { - const themeState = useThemeState(); - const [route, setRoute] = useState(readRoute); - const [vaultAddress, setVaultAddress] = useVaultAddress(); - const { data: vault, error, isLoading } = useVault(route.kind === "app" ? vaultAddress : null); - const invalidate = useInvalidateVault(vaultAddress); - const [flow, setFlow] = useState<{ open: boolean; asset?: `0x${string}` }>({ open: false }); - - useEffect(() => { - const onHash = () => setRoute(readRoute()); - window.addEventListener("hashchange", onHash); - return () => window.removeEventListener("hashchange", onHash); - }, []); - - const marketing = route.kind === "landing" || route.kind === "product" || route.kind === "solutions" || route.kind === "securitypage"; - useEffect(() => { - document.documentElement.setAttribute("data-theme", marketing ? "light" : themeState.theme); - }, [marketing, themeState.theme]); - - const navigate = (page: AppPage) => go({ kind: "app", page }); - const openFlow = (asset?: `0x${string}`) => setFlow({ open: true, asset }); - const openVault = (a: `0x${string}`) => { setVaultAddress(a); go({ kind: "app", page: "overview" }); }; - const launch = () => go({ kind: "welcome" }); - - if (route.kind === "landing") return ; - if (route.kind === "product") return ; - if (route.kind === "solutions") return ; - if (route.kind === "securitypage") return ; - - return ( - - {route.kind === "welcome" && go({ kind: "create" })} onLanding={() => go({ kind: "landing" })} />} - {route.kind === "create" && go({ kind: "welcome" })} />} - {route.kind === "app" && ( - <> - openFlow()} onSwitchVault={launch}> -
- {route.page === "settings" ? ( - - ) : route.page === "simulator" ? ( - - ) : !vaultAddress ? ( - No vault selected. . - ) : error ? ( -
Could not read the vault: {(error as Error).message}
- ) : isLoading || !vault ? ( - - ) : route.page === "overview" ? ( - openFlow()} /> - ) : route.page === "assets" ? ( - openFlow(a)} onNavigate={navigate} /> - ) : route.page === "transactions" ? ( - openFlow()} /> - ) : route.page === "addressbook" ? ( - - ) : route.page === "members" ? ( - - ) : route.page === "policies" ? ( - - ) : ( - - )} -
-
- {flow.open && vault && setFlow({ open: false })} onChanged={() => { invalidate(); navigate("transactions"); }} />} - - )} -
- ); -} diff --git a/web/src/abi/SkurFactory.ts b/web/src/abi/SkurFactory.ts deleted file mode 100644 index ff1da47..0000000 --- a/web/src/abi/SkurFactory.ts +++ /dev/null @@ -1,341 +0,0 @@ -// Generated from contracts/out/SkurFactory.sol/SkurFactory.json by scripts/export-abi.py. Do not edit. -export const SkurFactoryAbi = [ - { - "type": "constructor", - "inputs": [ - { - "name": "impl", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "createVault", - "inputs": [ - { - "name": "salt", - "type": "bytes32", - "internalType": "bytes32" - }, - { - "name": "members", - "type": "address[]", - "internalType": "address[]" - }, - { - "name": "roles", - "type": "uint8[]", - "internalType": "uint8[]" - }, - { - "name": "policy", - "type": "tuple", - "internalType": "struct Policy", - "components": [ - { - "name": "approvalsLow", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "approvalsHigh", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "approvalsCritical", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "governanceThreshold", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "guardianThreshold", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "guardianRequiredCritical", - "type": "bool", - "internalType": "bool" - }, - { - "name": "delayHigh", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "delayCritical", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "recipientActivationDelay", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "policyChangeDelay", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "recoveryDelay", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "proposalTtl", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "highExposureBps", - "type": "uint16", - "internalType": "uint16" - }, - { - "name": "criticalExposureBps", - "type": "uint16", - "internalType": "uint16" - }, - { - "name": "hardBlockExposureBps", - "type": "uint16", - "internalType": "uint16" - }, - { - "name": "envelopeBps", - "type": "uint16", - "internalType": "uint16" - }, - { - "name": "envelopeWindow", - "type": "uint32", - "internalType": "uint32" - } - ] - }, - { - "name": "assets", - "type": "address[]", - "internalType": "address[]" - }, - { - "name": "limits", - "type": "tuple[]", - "internalType": "struct AssetLimits[]", - "components": [ - { - "name": "approved", - "type": "bool", - "internalType": "bool" - }, - { - "name": "lowMax", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "highMax", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "perTxMax", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "dailyMax", - "type": "uint256", - "internalType": "uint256" - } - ] - } - ], - "outputs": [ - { - "name": "vault", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "implementation", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "isVault", - "inputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "predictVaultAddress", - "inputs": [ - { - "name": "creator", - "type": "address", - "internalType": "address" - }, - { - "name": "salt", - "type": "bytes32", - "internalType": "bytes32" - } - ], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "vaultAt", - "inputs": [ - { - "name": "index", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "vaultCount", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "vaultsOf", - "inputs": [ - { - "name": "member", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "address[]", - "internalType": "address[]" - } - ], - "stateMutability": "view" - }, - { - "type": "event", - "name": "VaultCreated", - "inputs": [ - { - "name": "vault", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "creator", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "index", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "salt", - "type": "bytes32", - "indexed": false, - "internalType": "bytes32" - } - ], - "anonymous": false - }, - { - "type": "error", - "name": "FailedDeployment", - "inputs": [] - }, - { - "type": "error", - "name": "InsufficientBalance", - "inputs": [ - { - "name": "balance", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "needed", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "type": "error", - "name": "NotAVault", - "inputs": [] - }, - { - "type": "error", - "name": "NotImplementation", - "inputs": [] - } -] as const; diff --git a/web/src/abi/SkurTestUSD.ts b/web/src/abi/SkurTestUSD.ts deleted file mode 100644 index e391def..0000000 --- a/web/src/abi/SkurTestUSD.ts +++ /dev/null @@ -1,352 +0,0 @@ -// Generated from contracts/out/SkurTestUSD.sol/SkurTestUSD.json by scripts/export-abi.py. Do not edit. -export const SkurTestUSDAbi = [ - { - "type": "constructor", - "inputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "MAX_MINT", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "allowance", - "inputs": [ - { - "name": "owner", - "type": "address", - "internalType": "address" - }, - { - "name": "spender", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "approve", - "inputs": [ - { - "name": "spender", - "type": "address", - "internalType": "address" - }, - { - "name": "value", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "balanceOf", - "inputs": [ - { - "name": "account", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "decimals", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint8", - "internalType": "uint8" - } - ], - "stateMutability": "pure" - }, - { - "type": "function", - "name": "mint", - "inputs": [ - { - "name": "to", - "type": "address", - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "name", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "string", - "internalType": "string" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "symbol", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "string", - "internalType": "string" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "totalSupply", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "transfer", - "inputs": [ - { - "name": "to", - "type": "address", - "internalType": "address" - }, - { - "name": "value", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "transferFrom", - "inputs": [ - { - "name": "from", - "type": "address", - "internalType": "address" - }, - { - "name": "to", - "type": "address", - "internalType": "address" - }, - { - "name": "value", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "event", - "name": "Approval", - "inputs": [ - { - "name": "owner", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "spender", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "value", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "Transfer", - "inputs": [ - { - "name": "from", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "to", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "value", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - }, - { - "type": "error", - "name": "ERC20InsufficientAllowance", - "inputs": [ - { - "name": "spender", - "type": "address", - "internalType": "address" - }, - { - "name": "allowance", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "needed", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "type": "error", - "name": "ERC20InsufficientBalance", - "inputs": [ - { - "name": "sender", - "type": "address", - "internalType": "address" - }, - { - "name": "balance", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "needed", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "type": "error", - "name": "ERC20InvalidApprover", - "inputs": [ - { - "name": "approver", - "type": "address", - "internalType": "address" - } - ] - }, - { - "type": "error", - "name": "ERC20InvalidReceiver", - "inputs": [ - { - "name": "receiver", - "type": "address", - "internalType": "address" - } - ] - }, - { - "type": "error", - "name": "ERC20InvalidSender", - "inputs": [ - { - "name": "sender", - "type": "address", - "internalType": "address" - } - ] - }, - { - "type": "error", - "name": "ERC20InvalidSpender", - "inputs": [ - { - "name": "spender", - "type": "address", - "internalType": "address" - } - ] - }, - { - "type": "error", - "name": "MintTooLarge", - "inputs": [] - } -] as const; diff --git a/web/src/abi/SkurVault.ts b/web/src/abi/SkurVault.ts deleted file mode 100644 index 22e2060..0000000 --- a/web/src/abi/SkurVault.ts +++ /dev/null @@ -1,2136 +0,0 @@ -// Generated from contracts/out/SkurVault.sol/SkurVault.json by scripts/export-abi.py. Do not edit. -export const SkurVaultAbi = [ - { - "type": "constructor", - "inputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "receive", - "stateMutability": "payable" - }, - { - "type": "function", - "name": "approve", - "inputs": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "approverCount", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint8", - "internalType": "uint8" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "cancel", - "inputs": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "depositERC20", - "inputs": [ - { - "name": "asset", - "type": "address", - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "execute", - "inputs": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "executedCount", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "executorCount", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint8", - "internalType": "uint8" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "expire", - "inputs": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "getApprovers", - "inputs": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "address[]", - "internalType": "address[]" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getAssetLimits", - "inputs": [ - { - "name": "asset", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "tuple", - "internalType": "struct AssetLimits", - "components": [ - { - "name": "approved", - "type": "bool", - "internalType": "bool" - }, - { - "name": "lowMax", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "highMax", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "perTxMax", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "dailyMax", - "type": "uint256", - "internalType": "uint256" - } - ] - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getAssets", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address[]", - "internalType": "address[]" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getGuardianConfirmers", - "inputs": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "address[]", - "internalType": "address[]" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getMembers", - "inputs": [], - "outputs": [ - { - "name": "addrs", - "type": "address[]", - "internalType": "address[]" - }, - { - "name": "roles", - "type": "uint8[]", - "internalType": "uint8[]" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getPolicy", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "tuple", - "internalType": "struct Policy", - "components": [ - { - "name": "approvalsLow", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "approvalsHigh", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "approvalsCritical", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "governanceThreshold", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "guardianThreshold", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "guardianRequiredCritical", - "type": "bool", - "internalType": "bool" - }, - { - "name": "delayHigh", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "delayCritical", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "recipientActivationDelay", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "policyChangeDelay", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "recoveryDelay", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "proposalTtl", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "highExposureBps", - "type": "uint16", - "internalType": "uint16" - }, - { - "name": "criticalExposureBps", - "type": "uint16", - "internalType": "uint16" - }, - { - "name": "hardBlockExposureBps", - "type": "uint16", - "internalType": "uint16" - }, - { - "name": "envelopeBps", - "type": "uint16", - "internalType": "uint16" - }, - { - "name": "envelopeWindow", - "type": "uint32", - "internalType": "uint32" - } - ] - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getProposal", - "inputs": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "tuple", - "internalType": "struct Proposal", - "components": [ - { - "name": "kind", - "type": "uint8", - "internalType": "enum Kind" - }, - { - "name": "status", - "type": "uint8", - "internalType": "enum Status" - }, - { - "name": "tier", - "type": "uint8", - "internalType": "enum Tier" - }, - { - "name": "requiredApprovals", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "requiredGuardians", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "securityReducing", - "type": "bool", - "internalType": "bool" - }, - { - "name": "riskReasons", - "type": "uint16", - "internalType": "uint16" - }, - { - "name": "policyVersion", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "proposer", - "type": "address", - "internalType": "address" - }, - { - "name": "asset", - "type": "address", - "internalType": "address" - }, - { - "name": "target", - "type": "address", - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "createdAt", - "type": "uint64", - "internalType": "uint64" - }, - { - "name": "executableAfter", - "type": "uint64", - "internalType": "uint64" - }, - { - "name": "expiresAt", - "type": "uint64", - "internalType": "uint64" - }, - { - "name": "data", - "type": "bytes", - "internalType": "bytes" - } - ] - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getRecipient", - "inputs": [ - { - "name": "r", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "tuple", - "internalType": "struct Recipient", - "components": [ - { - "name": "trust", - "type": "uint8", - "internalType": "enum Trust" - }, - { - "name": "registeredAt", - "type": "uint64", - "internalType": "uint64" - }, - { - "name": "activatesAt", - "type": "uint64", - "internalType": "uint64" - }, - { - "name": "paymentCount", - "type": "uint64", - "internalType": "uint64" - }, - { - "name": "totalPaid", - "type": "uint256", - "internalType": "uint256" - } - ] - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "guardianCount", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint8", - "internalType": "uint8" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "hasApproved", - "inputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "initialize", - "inputs": [ - { - "name": "members", - "type": "address[]", - "internalType": "address[]" - }, - { - "name": "roles", - "type": "uint8[]", - "internalType": "uint8[]" - }, - { - "name": "policy", - "type": "tuple", - "internalType": "struct Policy", - "components": [ - { - "name": "approvalsLow", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "approvalsHigh", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "approvalsCritical", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "governanceThreshold", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "guardianThreshold", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "guardianRequiredCritical", - "type": "bool", - "internalType": "bool" - }, - { - "name": "delayHigh", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "delayCritical", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "recipientActivationDelay", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "policyChangeDelay", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "recoveryDelay", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "proposalTtl", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "highExposureBps", - "type": "uint16", - "internalType": "uint16" - }, - { - "name": "criticalExposureBps", - "type": "uint16", - "internalType": "uint16" - }, - { - "name": "hardBlockExposureBps", - "type": "uint16", - "internalType": "uint16" - }, - { - "name": "envelopeBps", - "type": "uint16", - "internalType": "uint16" - }, - { - "name": "envelopeWindow", - "type": "uint32", - "internalType": "uint32" - } - ] - }, - { - "name": "assets", - "type": "address[]", - "internalType": "address[]" - }, - { - "name": "limits", - "type": "tuple[]", - "internalType": "struct AssetLimits[]", - "components": [ - { - "name": "approved", - "type": "bool", - "internalType": "bool" - }, - { - "name": "lowMax", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "highMax", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "perTxMax", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "dailyMax", - "type": "uint256", - "internalType": "uint256" - } - ] - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "isVetoable", - "inputs": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "liveApprovals", - "inputs": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "approvals", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "guardians", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "mode", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint8", - "internalType": "enum Mode" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "ownerCount", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint8", - "internalType": "uint8" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "pendingCount", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "policyVersion", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint32", - "internalType": "uint32" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "previewTransfer", - "inputs": [ - { - "name": "asset", - "type": "address", - "internalType": "address" - }, - { - "name": "to", - "type": "address", - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "r", - "type": "tuple", - "internalType": "struct RiskResult", - "components": [ - { - "name": "tier", - "type": "uint8", - "internalType": "enum Tier" - }, - { - "name": "reasons", - "type": "uint16", - "internalType": "uint16" - }, - { - "name": "exposureBps", - "type": "uint16", - "internalType": "uint16" - }, - { - "name": "requiredApprovals", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "requiredGuardians", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "delay", - "type": "uint32", - "internalType": "uint32" - } - ] - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "proposalCount", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "proposeAssetLimits", - "inputs": [ - { - "name": "asset", - "type": "address", - "internalType": "address" - }, - { - "name": "next", - "type": "tuple", - "internalType": "struct AssetLimits", - "components": [ - { - "name": "approved", - "type": "bool", - "internalType": "bool" - }, - { - "name": "lowMax", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "highMax", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "perTxMax", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "dailyMax", - "type": "uint256", - "internalType": "uint256" - } - ] - } - ], - "outputs": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "proposeMember", - "inputs": [ - { - "name": "member", - "type": "address", - "internalType": "address" - }, - { - "name": "roles", - "type": "uint8", - "internalType": "uint8" - } - ], - "outputs": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "proposeModeRelax", - "inputs": [ - { - "name": "target", - "type": "uint8", - "internalType": "enum Mode" - } - ], - "outputs": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "proposePolicy", - "inputs": [ - { - "name": "next", - "type": "tuple", - "internalType": "struct Policy", - "components": [ - { - "name": "approvalsLow", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "approvalsHigh", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "approvalsCritical", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "governanceThreshold", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "guardianThreshold", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "guardianRequiredCritical", - "type": "bool", - "internalType": "bool" - }, - { - "name": "delayHigh", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "delayCritical", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "recipientActivationDelay", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "policyChangeDelay", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "recoveryDelay", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "proposalTtl", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "highExposureBps", - "type": "uint16", - "internalType": "uint16" - }, - { - "name": "criticalExposureBps", - "type": "uint16", - "internalType": "uint16" - }, - { - "name": "hardBlockExposureBps", - "type": "uint16", - "internalType": "uint16" - }, - { - "name": "envelopeBps", - "type": "uint16", - "internalType": "uint16" - }, - { - "name": "envelopeWindow", - "type": "uint32", - "internalType": "uint32" - } - ] - } - ], - "outputs": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "proposeRecipientTrust", - "inputs": [ - { - "name": "recipient", - "type": "address", - "internalType": "address" - }, - { - "name": "trust", - "type": "uint8", - "internalType": "enum Trust" - } - ], - "outputs": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "proposeRecovery", - "inputs": [ - { - "name": "oldSigner", - "type": "address", - "internalType": "address" - }, - { - "name": "newSigner", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "proposeTransfer", - "inputs": [ - { - "name": "asset", - "type": "address", - "internalType": "address" - }, - { - "name": "to", - "type": "address", - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "memo", - "type": "string", - "internalType": "string" - } - ], - "outputs": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "raiseMode", - "inputs": [ - { - "name": "target", - "type": "uint8", - "internalType": "enum Mode" - }, - { - "name": "reason", - "type": "bytes32", - "internalType": "bytes32" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "registerRecipient", - "inputs": [ - { - "name": "r", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "rolesOf", - "inputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "uint8", - "internalType": "uint8" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "totalDeposited", - "inputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "totalOutflow", - "inputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "velocityOf", - "inputs": [ - { - "name": "asset", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "daySpent", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "dailyMax", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "envelopeSpent", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "envelope", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "envelopeResetsAt", - "type": "uint64", - "internalType": "uint64" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "veto", - "inputs": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "reason", - "type": "bytes32", - "internalType": "bytes32" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "event", - "name": "AssetLimitsSet", - "inputs": [ - { - "name": "asset", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "limits", - "type": "tuple", - "indexed": false, - "internalType": "struct AssetLimits", - "components": [ - { - "name": "approved", - "type": "bool", - "internalType": "bool" - }, - { - "name": "lowMax", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "highMax", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "perTxMax", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "dailyMax", - "type": "uint256", - "internalType": "uint256" - } - ] - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "CircuitBreakerTripped", - "inputs": [ - { - "name": "asset", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "attempted", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - }, - { - "name": "envelopeSpent", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - }, - { - "name": "envelope", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "Deposit", - "inputs": [ - { - "name": "from", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "asset", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "MemberSet", - "inputs": [ - { - "name": "member", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "previousRoles", - "type": "uint8", - "indexed": false, - "internalType": "uint8" - }, - { - "name": "roles", - "type": "uint8", - "indexed": false, - "internalType": "uint8" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "ModeChanged", - "inputs": [ - { - "name": "previous", - "type": "uint8", - "indexed": true, - "internalType": "enum Mode" - }, - { - "name": "mode", - "type": "uint8", - "indexed": true, - "internalType": "enum Mode" - }, - { - "name": "by", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "reason", - "type": "bytes32", - "indexed": false, - "internalType": "bytes32" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "PolicyActivated", - "inputs": [ - { - "name": "version", - "type": "uint32", - "indexed": true, - "internalType": "uint32" - }, - { - "name": "activatedAt", - "type": "uint64", - "indexed": false, - "internalType": "uint64" - }, - { - "name": "policy", - "type": "tuple", - "indexed": false, - "internalType": "struct Policy", - "components": [ - { - "name": "approvalsLow", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "approvalsHigh", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "approvalsCritical", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "governanceThreshold", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "guardianThreshold", - "type": "uint8", - "internalType": "uint8" - }, - { - "name": "guardianRequiredCritical", - "type": "bool", - "internalType": "bool" - }, - { - "name": "delayHigh", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "delayCritical", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "recipientActivationDelay", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "policyChangeDelay", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "recoveryDelay", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "proposalTtl", - "type": "uint32", - "internalType": "uint32" - }, - { - "name": "highExposureBps", - "type": "uint16", - "internalType": "uint16" - }, - { - "name": "criticalExposureBps", - "type": "uint16", - "internalType": "uint16" - }, - { - "name": "hardBlockExposureBps", - "type": "uint16", - "internalType": "uint16" - }, - { - "name": "envelopeBps", - "type": "uint16", - "internalType": "uint16" - }, - { - "name": "envelopeWindow", - "type": "uint32", - "internalType": "uint32" - } - ] - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "ProposalApproved", - "inputs": [ - { - "name": "id", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "approver", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "approvals", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "ProposalCancelled", - "inputs": [ - { - "name": "id", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "by", - "type": "address", - "indexed": true, - "internalType": "address" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "ProposalCreated", - "inputs": [ - { - "name": "id", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "kind", - "type": "uint8", - "indexed": true, - "internalType": "enum Kind" - }, - { - "name": "proposer", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "asset", - "type": "address", - "indexed": false, - "internalType": "address" - }, - { - "name": "target", - "type": "address", - "indexed": false, - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - }, - { - "name": "tier", - "type": "uint8", - "indexed": false, - "internalType": "enum Tier" - }, - { - "name": "riskReasons", - "type": "uint16", - "indexed": false, - "internalType": "uint16" - }, - { - "name": "requiredApprovals", - "type": "uint8", - "indexed": false, - "internalType": "uint8" - }, - { - "name": "requiredGuardians", - "type": "uint8", - "indexed": false, - "internalType": "uint8" - }, - { - "name": "executableAfter", - "type": "uint64", - "indexed": false, - "internalType": "uint64" - }, - { - "name": "expiresAt", - "type": "uint64", - "indexed": false, - "internalType": "uint64" - }, - { - "name": "securityReducing", - "type": "bool", - "indexed": false, - "internalType": "bool" - }, - { - "name": "memo", - "type": "string", - "indexed": false, - "internalType": "string" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "ProposalExecuted", - "inputs": [ - { - "name": "id", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "executor", - "type": "address", - "indexed": true, - "internalType": "address" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "ProposalGuardianConfirmed", - "inputs": [ - { - "name": "id", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "guardian", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "confirmations", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "ProposalVetoed", - "inputs": [ - { - "name": "id", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "guardian", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "reason", - "type": "bytes32", - "indexed": false, - "internalType": "bytes32" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "RecipientRegistered", - "inputs": [ - { - "name": "recipient", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "activatesAt", - "type": "uint64", - "indexed": false, - "internalType": "uint64" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "RecipientTrustSet", - "inputs": [ - { - "name": "recipient", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "previous", - "type": "uint8", - "indexed": false, - "internalType": "enum Trust" - }, - { - "name": "trust", - "type": "uint8", - "indexed": false, - "internalType": "enum Trust" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "RecoveryCompleted", - "inputs": [ - { - "name": "id", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "oldSigner", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "newSigner", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "roles", - "type": "uint8", - "indexed": false, - "internalType": "uint8" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "TransferExecuted", - "inputs": [ - { - "name": "id", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "asset", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "to", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "amount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "VaultInitialized", - "inputs": [ - { - "name": "policyVersion", - "type": "uint32", - "indexed": false, - "internalType": "uint32" - }, - { - "name": "memberCount", - "type": "uint256", - "indexed": false, - "internalType": "uint256" - } - ], - "anonymous": false - }, - { - "type": "error", - "name": "AlreadyApproved", - "inputs": [] - }, - { - "type": "error", - "name": "AlreadyInitialized", - "inputs": [] - }, - { - "type": "error", - "name": "AmountZero", - "inputs": [] - }, - { - "type": "error", - "name": "AssetNotApproved", - "inputs": [ - { - "name": "asset", - "type": "address", - "internalType": "address" - } - ] - }, - { - "type": "error", - "name": "ExposureHardBlocked", - "inputs": [ - { - "name": "exposureBps", - "type": "uint16", - "internalType": "uint16" - }, - { - "name": "maxBps", - "type": "uint16", - "internalType": "uint16" - } - ] - }, - { - "type": "error", - "name": "InsufficientApprovals", - "inputs": [ - { - "name": "have", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "need", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "type": "error", - "name": "InsufficientBalance", - "inputs": [ - { - "name": "amount", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "balance", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "type": "error", - "name": "InsufficientGuardians", - "inputs": [ - { - "name": "have", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "need", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "type": "error", - "name": "InvalidPolicy", - "inputs": [ - { - "name": "reason", - "type": "string", - "internalType": "string" - } - ] - }, - { - "type": "error", - "name": "InvalidProposal", - "inputs": [ - { - "name": "reason", - "type": "string", - "internalType": "string" - } - ] - }, - { - "type": "error", - "name": "InvalidRoles", - "inputs": [] - }, - { - "type": "error", - "name": "MemberExists", - "inputs": [] - }, - { - "type": "error", - "name": "ModeNotRelaxation", - "inputs": [] - }, - { - "type": "error", - "name": "ModeNotStricter", - "inputs": [] - }, - { - "type": "error", - "name": "NativeTransferFailed", - "inputs": [] - }, - { - "type": "error", - "name": "NotAuthorized", - "inputs": [ - { - "name": "requiredRole", - "type": "uint8", - "internalType": "uint8" - } - ] - }, - { - "type": "error", - "name": "NotCancellable", - "inputs": [] - }, - { - "type": "error", - "name": "NotMember", - "inputs": [] - }, - { - "type": "error", - "name": "NotPending", - "inputs": [] - }, - { - "type": "error", - "name": "NotVetoable", - "inputs": [] - }, - { - "type": "error", - "name": "PerTxLimitExceeded", - "inputs": [ - { - "name": "amount", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "max", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "type": "error", - "name": "ProposalExpired", - "inputs": [] - }, - { - "type": "error", - "name": "RecipientBlocked", - "inputs": [ - { - "name": "recipient", - "type": "address", - "internalType": "address" - } - ] - }, - { - "type": "error", - "name": "RecipientNotRegistered", - "inputs": [] - }, - { - "type": "error", - "name": "ReentrancyGuardReentrantCall", - "inputs": [] - }, - { - "type": "error", - "name": "SafeERC20FailedOperation", - "inputs": [ - { - "name": "token", - "type": "address", - "internalType": "address" - } - ] - }, - { - "type": "error", - "name": "ThresholdUnsatisfiable", - "inputs": [ - { - "name": "reason", - "type": "string", - "internalType": "string" - } - ] - }, - { - "type": "error", - "name": "Timelocked", - "inputs": [ - { - "name": "executableAfter", - "type": "uint64", - "internalType": "uint64" - } - ] - }, - { - "type": "error", - "name": "UnknownProposal", - "inputs": [] - }, - { - "type": "error", - "name": "VaultLocked", - "inputs": [] - }, - { - "type": "error", - "name": "VelocityExceeded", - "inputs": [ - { - "name": "wouldSpend", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "dailyMax", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "type": "error", - "name": "ZeroAddress", - "inputs": [] - } -] as const; diff --git a/web/src/assets/hero.png b/web/src/assets/hero.png deleted file mode 100644 index 02251f4..0000000 Binary files a/web/src/assets/hero.png and /dev/null differ diff --git a/web/src/assets/vite.svg b/web/src/assets/vite.svg deleted file mode 100644 index 5101b67..0000000 --- a/web/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ -Vite diff --git a/web/src/components/AppShell.tsx b/web/src/components/AppShell.tsx deleted file mode 100644 index f9d281d..0000000 --- a/web/src/components/AppShell.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { useEffect, useState, type ReactNode } from "react"; -import { useAccount, useConnect, useDisconnect, useSwitchChain } from "wagmi"; -import { arkDevnet, explorerAddress, NATIVE_ASSET } from "../config/chain"; -import { fmtAmount } from "../lib/format"; -import { Mode, roleNames, Status } from "../lib/types"; -import { useTheme } from "../state/theme"; -import { useVaultLabel } from "../state/vaultLabel"; -import { Button, ModeBadge } from "./ui"; -import { Identicon } from "./Identicon"; -import { IconApps, IconAssets, IconBook, IconChevron, IconCollapse, IconDocs, IconExternal, IconHelp, IconLab, IconMoon, IconOverview, IconPlus, IconPolicy, IconSettings, IconShield, IconSun, IconTx, IconUsers, IconWallet } from "./icons"; -import type { VaultData } from "../hooks/useVault"; - -export type AppPage = "overview" | "assets" | "transactions" | "addressbook" | "members" | "settings" | "policies" | "security" | "simulator"; - -const MAIN_NAV: Array<[AppPage, string, ReactNode]> = [ - ["overview", "Overview", ], - ["assets", "Assets", ], - ["transactions", "Transactions", ], - ["addressbook", "Address book", ], - ["members", "Members", ], - ["settings", "Settings", ], -]; -const SECURITY_NAV: Array<[AppPage, string, ReactNode]> = [ - ["policies", "Policies", ], - ["security", "Security", ], - ["simulator", "Simulator", ], -]; - -/** Safe{Wallet} application layout: full-height sidebar with the Home pill, content column with the account pill row. */ -export function AppShell({ page, onNavigate, onHome, vault, vaultAddress, onNewTransaction, onSwitchVault, children }: { page: AppPage; onNavigate: (p: AppPage) => void; onHome: () => void; vault?: VaultData; vaultAddress: `0x${string}` | null; onNewTransaction: () => void; onSwitchVault: () => void; children: ReactNode }) { - const [collapsed, setCollapsed] = useState(() => { try { return localStorage.getItem("skur.sidebar") === "collapsed"; } catch { return false; } }); - useEffect(() => { try { localStorage.setItem("skur.sidebar", collapsed ? "collapsed" : "open"); } catch { /* ignore */ } }, [collapsed]); - const { address, isConnected, chainId } = useAccount(); - const { connectors, connect, isPending } = useConnect(); - const { disconnect } = useDisconnect(); - const { switchChain } = useSwitchChain(); - const { theme, toggle } = useTheme(); - const [label] = useVaultLabel(vaultAddress); - const me = vault?.members.find((m) => m.address.toLowerCase() === address?.toLowerCase()); - const wrongChain = isConnected && chainId !== arkDevnet.id; - const pending = vault?.proposals.filter((p) => p.status === Status.PENDING).length ?? 0; - const stable = vault?.assets.find((a) => a.address !== NATIVE_ASSET); - const native = vault?.assets.find((a) => a.address === NATIVE_ASSET); - const canPropose = Boolean((me?.roles ?? 0) & 3) && vault?.mode !== Mode.LOCKDOWN; - - return ( -
- - -
-
-
-
- {vaultAddress ? ( - - ) : ( - - )} -
- - {vaultAddress && ( - - )} - {wrongChain ? ( - - ) : isConnected && address ? ( - - ) : ( - connectors.slice(0, 1).map((c) => ( - - )) - )} -
-
- {children} -
-
-
- ); -} - -export const AppsIcon = IconApps; diff --git a/web/src/components/Identicon.tsx b/web/src/components/Identicon.tsx deleted file mode 100644 index 0b55c83..0000000 --- a/web/src/components/Identicon.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { useMemo } from "react"; -import { keccak256, stringToHex } from "viem"; - -/** Deterministic blocky identicon for an address, in the spirit of the avatar Safe shows beside every account. */ -export function Identicon({ address, size = 40 }: { address: string; size?: number }) { - const { cells, fg, bg } = useMemo(() => { - const hash = keccak256(stringToHex(address.toLowerCase())).slice(2); - const hue = parseInt(hash.slice(0, 4), 16) % 360; - const hue2 = (hue + 140) % 360; - const bits = hash.slice(4); - const cells: boolean[] = []; - // 5x5 grid, mirrored horizontally; 15 bits drive the left half + centre column - for (let y = 0; y < 5; y++) { - const row: boolean[] = []; - for (let x = 0; x < 3; x++) { - const nibble = parseInt(bits[(y * 3 + x) % bits.length], 16); - row.push(nibble % 3 !== 0); - } - cells.push(row[0], row[1], row[2], row[1], row[0]); - } - return { cells, fg: `hsl(${hue} 70% 45%)`, bg: `hsl(${hue2} 60% 88%)` }; - }, [address]); - const unit = size / 5; - return ( - - - {cells.map((on, i) => - on ? : null, - )} - - ); -} diff --git a/web/src/components/MarketingShell.tsx b/web/src/components/MarketingShell.tsx deleted file mode 100644 index 2348a9f..0000000 --- a/web/src/components/MarketingShell.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import type { ReactNode } from "react"; -import { useScrolled } from "../lib/motion"; -import { IconChevronRight } from "./icons"; - -export type MarketingPage = "landing" | "product" | "solutions" | "security"; - -/** Nav, closing CTA and footer shared by every marketing page. */ -export function MarketingShell({ page, onLaunch, children, hideCta = false }: { page: MarketingPage; onLaunch: () => void; children: ReactNode; hideCta?: boolean }) { - const scrolled = useScrolled(); - return ( -
-
-
- SSkur - -
- Ark devnet · 9000 - -
-
-
- {children} - {!hideCta && ( -
-
-
Start here
-

Put a firewall in front of
your treasury

-

Self-custodial, enforced onchain, and readable by the people who sign.

-
- Read the docs - -
-
-
- )} -
- -
- ); -} - -/** Hero used by the product, solutions and security pages. */ -export function SubHero({ eyebrow, title, sub, onLaunch, children }: { eyebrow: string; title: ReactNode; sub: string; onLaunch: () => void; children?: ReactNode }) { - return ( - <> -
-
- {[[0, 130], [1310, 0], [1180, 260], [130, 390], [910, 390]].map(([x, y], i) => ( - - ))} -
-
{eyebrow}
-

{title}

-

{sub}

-
- - {children} -
-
-
-
- - ); -} diff --git a/web/src/components/Mockup.tsx b/web/src/components/Mockup.tsx deleted file mode 100644 index 69a507e..0000000 --- a/web/src/components/Mockup.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/** Static miniature of the app used as artwork on the landing page, the way safe.global shows its dashboard. */ -export function AppMockup({ variant = "overview" }: { variant?: "overview" | "queue" | "policy" }) { - return ( -
-
-
S  Home
-
+ New transaction
- {["Overview", "Assets", "Transactions", "Address book", "Members", "Settings"].map((n, i) => ( -
{n}
- ))} -
Security
- {["Policies", "Security", "Simulator"].map((n) => ( -
{n}
- ))} -
-
-
- - Main treasury
ark:0xCC31…42Dc
- 2/3 · 250,000 sUSD · Normal -
- {variant === "overview" && ( - <> -
-
Total balance
-
250,000.00 sUSD
- ↗ Send⌗ Receive -
Top assets
-
$ sUSD250,000
-
K KASH2
-
-
-
Pending transactions
-
Send 12,000 sUSD1 out of 2
-
Send 900 sUSD✓ 1 out of 1
-
Update policy⏱ 1d timelock
-
- - )} - {variant === "queue" && ( -
-
Queue
-
↗ Send 12,000 sUSD to 0x4F…a1c2High risk · 1 out of 2
-
↗ Send 60,000 sUSD to 0x9B…77e0Critical · guardian · 1d
-
⚙ Lower approvals to 1Security-reducing · vetoable
-
- )} - {variant === "policy" && ( -
-
Policy simulator
-
One finance signer compromisedBlocked
-
Drain split into twenty transfersBlocked
-
Attacker weakens policy, then withdrawsDelayed 1d
-
Guardian key compromisedNot possible
-
- )} -
-
- ); -} diff --git a/web/src/components/NetworkGuard.tsx b/web/src/components/NetworkGuard.tsx deleted file mode 100644 index 0637f43..0000000 --- a/web/src/components/NetworkGuard.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useAccount, useSwitchChain } from "wagmi"; -import { arkDevnet } from "../config/chain"; -import { Button } from "./ui"; - -/** Shows a switch button while the connected wallet is on a chain other than the Ark devnet. */ -export function NetworkGuard() { - const { isConnected, chainId } = useAccount(); - const { switchChain, isPending } = useSwitchChain(); - if (!isConnected || chainId === arkDevnet.id) return null; - return ( - - ); -} diff --git a/web/src/components/RiskReview.tsx b/web/src/components/RiskReview.tsx deleted file mode 100644 index eae3b87..0000000 --- a/web/src/components/RiskReview.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { fmtAmount, fmtBps, fmtDuration, short } from "../lib/format"; -import { explainReasons, Tier, TIER_LABEL, Trust, TRUST_LABEL, type AssetMeta } from "../lib/types"; -import { TierBadge } from "./ui"; - -export type ReviewInput = { - asset: AssetMeta; - to: `0x${string}`; - toLabel?: string; - amount: bigint; - memo: string; - tier: Tier; - reasons: number; - exposureBps: number; - requiredApprovals: number; - requiredGuardians: number; - delay: number; - trust: Trust; - activatesAt?: bigint; - vetoable: boolean; -}; - -/** Human-readable intent shown before a wallet signature is requested. */ -export function RiskReview({ r }: { r: ReviewInput }) { - const reasons = explainReasons(r.reasons); - const nowSec = Math.floor(Date.now() / 1000); - const probationWait = r.activatesAt && Number(r.activatesAt) > nowSec + r.delay ? Number(r.activatesAt) - nowSec : 0; - const effectiveDelay = Math.max(r.delay, probationWait); - const remaining = r.asset.balance - r.amount; - return ( -
-
- Action - - Send {fmtAmount(r.amount, r.asset.decimals, r.asset.symbol)} to {r.toLabel ? `${r.toLabel} (${short(r.to)})` : short(r.to)} - -
- {r.memo && ( -
- Purpose - {r.memo} -
- )} -
- Treasury impact - - {fmtBps(r.exposureBps)} of {r.asset.symbol} holdings · {fmtAmount(remaining < 0n ? 0n : remaining, r.asset.decimals, r.asset.symbol)} remains - -
-
- Recipient - {TRUST_LABEL[r.trust]}{r.trust === Trust.UNKNOWN ? " · will be registered as New" : ""} -
-
- Risk tier - -
-
- Security requirement - - {TIER_LABEL[r.tier]} · {r.requiredApprovals} approval{r.requiredApprovals === 1 ? "" : "s"} - {r.requiredGuardians ? ` + ${r.requiredGuardians} guardian confirmation${r.requiredGuardians === 1 ? "" : "s"}` : ""} - {effectiveDelay ? ` · executable after ${fmtDuration(effectiveDelay)}` : " · no delay"} - -
- {r.vetoable && ( -
- Guardian veto - Any guardian can cancel this at any point before it executes. -
- )} - {reasons.length > 0 && ( -
- Why this tier -
    - {reasons.map((t) => ( -
  • {t}
  • - ))} -
-
- )} -
- ); -} diff --git a/web/src/components/TxFlow.tsx b/web/src/components/TxFlow.tsx deleted file mode 100644 index f3096a4..0000000 --- a/web/src/components/TxFlow.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { useEffect, useState } from "react"; -import { usePublicClient } from "wagmi"; -import { isAddress } from "viem"; -import { SkurVaultAbi } from "../abi/SkurVault"; -import { describeError, useTx } from "../hooks/useTx"; -import type { VaultData } from "../hooks/useVault"; -import { fmtAmount, parseAmount } from "../lib/format"; -import { Mode, REASON_RECIPIENT_PROBATION, Tier, Trust } from "../lib/types"; -import { RiskReview, type ReviewInput } from "./RiskReview"; -import { Button, Field, Modal, TokenIcon, TxStatus } from "./ui"; - -/** - * "New transaction" flow, patterned on Safe's send-tokens dialog: details, then a review step that is - * populated by the vault's own previewTransfer before the wallet is ever asked to sign. - */ -export function TxFlow({ vault, onClose, onChanged, presetAsset }: { vault: VaultData; onClose: () => void; onChanged: () => void; presetAsset?: `0x${string}` }) { - const client = usePublicClient(); - const approved = vault.assets.filter((a) => a.limits.approved); - const [step, setStep] = useState<1 | 2>(1); - const [asset, setAsset] = useState(presetAsset ?? approved[0]?.address ?? ""); - const [to, setTo] = useState(""); - const [amountText, setAmountText] = useState(""); - const [memo, setMemo] = useState(""); - const [preview, setPreview] = useState(null); - const [previewError, setPreviewError] = useState(null); - const [loadingPreview, setLoadingPreview] = useState(false); - const tx = useTx(() => { - onChanged(); - }); - - const meta = approved.find((a) => a.address === asset); - const amount = meta ? parseAmount(amountText, meta.decimals) : null; - const validTo = isAddress(to); - const detailsOk = Boolean(meta && amount && amount > 0n && validTo); - - useEffect(() => { - if (step !== 2 || !client || !meta || !validTo || !amount) return; - let cancelled = false; - setPreview(null); - setPreviewError(null); - setLoadingPreview(true); - (async () => { - try { - const [r, rec] = await Promise.all([ - client.readContract({ address: vault.address, abi: SkurVaultAbi, functionName: "previewTransfer", args: [meta.address, to as `0x${string}`, amount] }), - client.readContract({ address: vault.address, abi: SkurVaultAbi, functionName: "getRecipient", args: [to as `0x${string}`] }), - ]); - if (cancelled) return; - const tier = Number(r.tier) as Tier; - const reasons = Number(r.reasons); - const trust = Number(rec.trust) as Trust; - setPreview({ - asset: meta, - to: to as `0x${string}`, - amount, - memo, - tier, - reasons, - exposureBps: Number(r.exposureBps), - requiredApprovals: Number(r.requiredApprovals), - requiredGuardians: Number(r.requiredGuardians), - delay: Number(r.delay), - trust, - activatesAt: trust === Trust.UNKNOWN ? BigInt(Math.floor(Date.now() / 1000) + vault.policy.recipientActivationDelay) : rec.activatesAt, - vetoable: tier === Tier.CRITICAL || Boolean(reasons & REASON_RECIPIENT_PROBATION) || vault.mode !== Mode.NORMAL, - }); - } catch (e) { - if (!cancelled) setPreviewError(describeError(e)); - } finally { - if (!cancelled) setLoadingPreview(false); - } - })(); - return () => { - cancelled = true; - }; - }, [step, client, meta, to, amount, memo, validTo, vault]); - - const submit = () => { - if (!meta || !amount || !validTo) return; - tx.send({ address: vault.address, abi: SkurVaultAbi, functionName: "proposeTransfer", args: [meta.address, to as `0x${string}`, amount, memo] }); - }; - const done = tx.state.phase === "done"; - - const steps = ( - <> - 1 Details - · - 2 Review - - ); - - return ( - - - - - ) : done ? ( - <> - - - - ) : ( - <> - - - - ) - } - > - {step === 1 ? ( - <> - - setTo(e.target.value.trim())} placeholder="0x…" list="known-recipients" autoFocus /> - - {vault.recipients.map((r) => ( - - -
- - - - - setAmountText(e.target.value)} placeholder="0.00" inputMode="decimal" /> - -
- - setMemo(e.target.value)} placeholder="Invoice 1042, design retainer" maxLength={140} /> - - - ) : ( - <> - {meta && amount && ( -
- -
-
{fmtAmount(amount, meta.decimals, meta.symbol)}
-
to {to}
-
-
- )} - {loadingPreview &&
Asking the vault how it scores this payment…
} - {previewError &&
{previewError}
} - {preview && } - - - )} -
- ); -} diff --git a/web/src/components/TxRow.tsx b/web/src/components/TxRow.tsx deleted file mode 100644 index c84b7ab..0000000 --- a/web/src/components/TxRow.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import { useState } from "react"; -import { useAccount } from "wagmi"; -import { decodeAbiParameters, stringToHex } from "viem"; -import { SkurVaultAbi } from "../abi/SkurVault"; -import { useTx } from "../hooks/useTx"; -import type { ProposalView, VaultData } from "../hooks/useVault"; -import { fmtAmount, fmtBps, fmtDate, fmtRelative, nowSec, short } from "../lib/format"; -import { explainReasons, Kind, KIND_LABEL, Mode, MODE_LABEL, ROLE_APPROVER, ROLE_EXECUTOR, ROLE_GUARDIAN, ROLE_OWNER, roleNames, Status, Trust, TRUST_LABEL } from "../lib/types"; -import { Address, Badge, Button, ConfirmPill, TierBadge, TokenIcon, TxStatus } from "./ui"; -import { IconChevron, IconLock, IconPolicy, IconSend, IconShield, IconUsers } from "./icons"; - -function describe(p: ProposalView, vault: VaultData): { kind: string; amount?: string; symbol?: string; detail: string } { - const asset = vault.assets.find((a) => a.address.toLowerCase() === p.asset.toLowerCase()); - switch (p.kind) { - case Kind.TRANSFER: - return { kind: "Send", amount: asset ? `-${fmtAmount(p.amount, asset.decimals)} ${asset.symbol}` : String(p.amount), symbol: asset?.symbol, detail: `to ${short(p.target)}` }; - case Kind.POLICY_UPDATE: - return { kind: "Policy update", detail: p.securityReducing ? "loosens at least one control" : "tightens or keeps every control" }; - case Kind.ASSET_LIMITS: - return { kind: "Asset limits", detail: `${asset?.symbol ?? short(p.target)} · ${p.securityReducing ? "loosens a limit or approves an asset" : "tightens limits"}` }; - case Kind.MEMBER_SET: { - const roles = Number(p.amount); - return { kind: roles === 0 ? "Remove member" : "Set roles", detail: `${short(p.target)}${roles ? ` → ${roleNames(roles).join(" + ")}` : ""}` }; - } - case Kind.RECIPIENT_TRUST: - return { kind: "Recipient trust", detail: `${short(p.target)} → ${TRUST_LABEL[Number(p.amount) as Trust]}` }; - case Kind.MODE_RELAX: - return { kind: "Lower security mode", detail: `to ${MODE_LABEL[Number(p.amount) as Mode]} · guardians must confirm` }; - case Kind.RECOVERY: { - let newSigner = "?"; - try { - newSigner = decodeAbiParameters([{ type: "address" }], p.data)[0]; - } catch { - /* ignore */ - } - return { kind: "Recovery", detail: `${short(p.target)} → ${short(newSigner)}` }; - } - } -} - -function KindIcon({ kind }: { kind: Kind }) { - if (kind === Kind.TRANSFER) return ; - if (kind === Kind.RECOVERY || kind === Kind.MODE_RELAX) return ; - if (kind === Kind.MEMBER_SET) return ; - return ; -} - -/** One transaction as Safe renders it: a rounded card row (type, amount, time, status) that expands into details and a timeline. */ -export function TxRow({ p, vault, onChanged, compact = false }: { p: ProposalView; vault: VaultData; onChanged: () => void; compact?: boolean }) { - const [open, setOpen] = useState(false); - const { address } = useAccount(); - const tx = useTx(onChanged); - const me = vault.members.find((m) => m.address.toLowerCase() === address?.toLowerCase()); - const roles = me?.roles ?? 0; - const now = nowSec(); - const d = describe(p, vault); - const asset = vault.assets.find((a) => a.address.toLowerCase() === p.asset.toLowerCase()); - const pending = p.status === Status.PENDING; - const expired = pending && Number(p.expiresAt) < now; - const timelocked = Number(p.executableAfter) > now; - const alreadyApproved = address ? [...p.approvers, ...p.guardianConfirmers].some((a) => a.toLowerCase() === address.toLowerCase()) : false; - - const canApprove = pending && !expired && !alreadyApproved && (roles & ROLE_GUARDIAN ? p.requiredGuardians > 0 : p.kind === Kind.RECOVERY ? false : p.kind === Kind.TRANSFER ? Boolean(roles & ROLE_APPROVER) : Boolean(roles & ROLE_OWNER)); - const executeRole = p.kind === Kind.TRANSFER ? ROLE_EXECUTOR : p.kind === Kind.RECOVERY ? ROLE_OWNER | ROLE_GUARDIAN : ROLE_OWNER | ROLE_EXECUTOR; - const approvalsMet = p.liveApprovals >= p.requiredApprovals && p.liveGuardians >= p.requiredGuardians; - const lockedOut = vault.mode === Mode.LOCKDOWN && (p.kind === Kind.TRANSFER || (p.securityReducing && p.kind !== Kind.MODE_RELAX)); - const canExecute = pending && !expired && Boolean(roles & executeRole) && approvalsMet && !timelocked && !lockedOut; - const canVeto = pending && !expired && Boolean(roles & ROLE_GUARDIAN) && p.vetoable; - const canCancel = pending && !expired && (Boolean(roles & ROLE_OWNER) || p.proposer.toLowerCase() === address?.toLowerCase()); - const call = (functionName: string, args: readonly unknown[] = [p.id]) => tx.send({ address: vault.address, abi: SkurVaultAbi, functionName, args }); - const reasons = explainReasons(p.riskReasons); - const need = p.requiredApprovals + p.requiredGuardians; - const have = Math.min(p.liveApprovals, p.requiredApprovals) + Math.min(p.liveGuardians, p.requiredGuardians); - const stage: "confirm" | "wait" | "execute" | "final" = !pending ? "final" : !approvalsMet ? "confirm" : timelocked || lockedOut ? "wait" : "execute"; - - const status = pending && !expired ? ( - - ) : ( - - {p.status === Status.EXECUTED ? "Success" : p.status === Status.VETOED ? "Vetoed" : expired ? "Expired" : "Cancelled"} - - ); - - return ( -
-
setOpen((o) => !o)} style={compact ? { gridTemplateColumns: "24px 1fr auto 24px", padding: "14px 12px" } : undefined}> - -
- {d.kind} - {compact && d.amount && · {d.amount}} - {compact &&
{d.detail}
} -
- {!compact && ( -
- {d.symbol && } - {d.amount ?? d.detail} - {d.amount && {d.detail}} -
- )} - {!compact &&
{fmtDate(p.createdAt).split(",").slice(-1)[0]?.trim() || fmtRelative(p.createdAt)}
} -
{status}
- -
- - {open && ( -
-
-
- {KIND_LABEL[p.kind]} #{String(p.id)} - {p.kind === Kind.TRANSFER && } - {p.securityReducing && Security-reducing} - {pending && !expired && ( - - {stage === "execute" ? "Ready to execute" : stage === "wait" ? (lockedOut ? "Locked down" : `Executable ${fmtRelative(p.executableAfter)}`) : "Needs confirmations"} - - )} -
-
- {p.kind === Kind.TRANSFER && ( - <> -
Recipient
-
Amount
{asset ? fmtAmount(p.amount, asset.decimals, asset.symbol) : String(p.amount)}
- {asset && asset.balance > 0n &&
Treasury impact
{fmtBps(Number((p.amount * 10_000n) / (asset.balance + (p.status === Status.EXECUTED ? p.amount : 0n))))} of {asset.symbol}
} - - )} - {p.kind !== Kind.TRANSFER && p.target !== "0x0000000000000000000000000000000000000000" &&
Target
} - {p.memo &&
Purpose
{p.memo}
} -
Proposed by
-
Requirement
{p.requiredApprovals} approval{p.requiredApprovals === 1 ? "" : "s"}{p.requiredGuardians ? ` + ${p.requiredGuardians} guardian` : ""}{Number(p.executableAfter) > Number(p.createdAt) ? ` · not before ${fmtDate(p.executableAfter)}` : ""}
-
Expires
{fmtDate(p.expiresAt)}
- {p.vetoable && pending &&
Guardian veto
open until execution
} -
- {reasons.length > 0 && p.kind === Kind.TRANSFER && ( - <> -
Why this tier
-
    {reasons.map((r) =>
  • {r}
  • )}
- - )} - {pending && ( -
- {canApprove && } - {Boolean(roles & executeRole) && !expired && ( - - )} - {canVeto && } - {canCancel && !expired && } - {expired && } -
- )} - -
-
    -
  • Created
    {fmtDate(p.createdAt)} by {short(p.proposer)}
  • -
  • = p.requiredApprovals ? "done" : stage === "confirm" ? "active" : ""}> -
    Confirmations ({Math.min(p.liveApprovals, p.requiredApprovals)} of {p.requiredApprovals})
    -
    - {p.approvers.length === 0 && No confirmations yet} - {p.approvers.map((a) => ✓
    )} -
    -
  • - {p.requiredGuardians > 0 && ( -
  • = p.requiredGuardians ? "done" : stage === "confirm" ? "active" : ""}> -
    Guardian confirmations ({Math.min(p.liveGuardians, p.requiredGuardians)} of {p.requiredGuardians})
    -
    - {p.guardianConfirmers.length === 0 && Independent sign-off required} - {p.guardianConfirmers.map((a) => ✓
    )} -
    -
  • - )} - {Number(p.executableAfter) > Number(p.createdAt) && ( -
  • -
    Timelock
    -
    {timelocked ? `Executable ${fmtRelative(p.executableAfter)}` : `Passed ${fmtDate(p.executableAfter)}`}
    -
  • - )} -
  • -
    {p.status === Status.EXECUTED ? "Executed" : p.status === Status.VETOED ? "Vetoed by a guardian" : p.status === Status.CANCELLED ? "Cancelled" : "Execute"}
    -
    {pending ? (stage === "execute" ? "Any executor can execute now" : "Waiting on the steps above") : ""}
    -
  • -
-
- )} -
- ); -} diff --git a/web/src/components/ui.tsx b/web/src/components/ui.tsx deleted file mode 100644 index 1362339..0000000 --- a/web/src/components/ui.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import { useEffect, useState, type ReactNode } from "react"; -import { explorerAddress, explorerTx } from "../config/chain"; -import { short } from "../lib/format"; -import { Mode, MODE_LABEL, Status, STATUS_LABEL, Tier, TIER_LABEL, Trust, TRUST_LABEL } from "../lib/types"; -import type { TxState } from "../hooks/useTx"; -import { IconCheck, IconClose, IconCopy, IconExternal } from "./icons"; - -export function Card({ title, subtitle, actions, children, className = "", flush = false }: { title?: ReactNode; subtitle?: ReactNode; actions?: ReactNode; children: ReactNode; className?: string; flush?: boolean }) { - return ( -
- {(title || actions) && ( -
-
- {title &&

{title}

} - {subtitle &&

{subtitle}

} -
- {actions &&
{actions}
} -
- )} - {children} -
- ); -} - -/** Two-column section used by Safe's settings pages: label column on the left, content on the right. */ -export function Section({ title, children }: { title: ReactNode; children: ReactNode }) { - return ( -
-

{title}

-
{children}
-
- ); -} - -export type Tone = "neutral" | "ok" | "warn" | "bad" | "info" | "review" | "brand"; - -export function Badge({ children, tone = "neutral" }: { children: ReactNode; tone?: Tone }) { - return {children}; -} - -/** "1 out of 2" confirmation pill, the way Safe shows signature progress. */ -export function ConfirmPill({ have, need }: { have: number; need: number }) { - const done = have >= need; - return ( - - {done ? : null} - {have} out of {need} - - ); -} - -export function TierBadge({ tier }: { tier: Tier }) { - const tone: Tone = tier === Tier.LOW ? "ok" : tier === Tier.HIGH ? "warn" : "bad"; - return {TIER_LABEL[tier]} risk; -} -export function ModeBadge({ mode }: { mode: Mode }) { - const tone: Tone = mode === Mode.NORMAL ? "ok" : mode === Mode.ELEVATED ? "warn" : "bad"; - return {MODE_LABEL[mode]}; -} -export function StatusBadge({ status }: { status: Status }) { - const tone: Tone = status === Status.EXECUTED ? "ok" : status === Status.PENDING ? "review" : status === Status.VETOED ? "bad" : "neutral"; - return {STATUS_LABEL[status]}; -} -export function TrustBadge({ trust }: { trust: Trust }) { - const tone: Tone = trust === Trust.TRUSTED || trust === Trust.VERIFIED ? "ok" : trust === Trust.NEW ? "info" : trust === Trust.UNKNOWN ? "neutral" : "bad"; - return {TRUST_LABEL[trust]}; -} - -export function CopyButton({ value, small = true }: { value: string; small?: boolean }) { - const [ok, setOk] = useState(false); - useEffect(() => { - if (!ok) return; - const t = setTimeout(() => setOk(false), 1200); - return () => clearTimeout(t); - }, [ok]); - return ( - - ); -} - -export function Address({ value, label, full = false, copy = false, prefix }: { value: string; label?: string; full?: boolean; copy?: boolean; prefix?: string }) { - return ( - - {label ? {label} : null} - - {prefix ? {prefix}: : null}{full ? value : short(value)} - - {copy && } - - ); -} - -export function TxLink({ hash }: { hash: string }) { - return ( - - {short(hash, 6)} - - - ); -} - -export type ButtonKind = "primary" | "secondary" | "dark" | "danger" | "ghost" | "white" | "black"; - -export function Button({ children, onClick, kind = "primary", disabled, type = "button", title, size, block, icon, className = "" }: { children: ReactNode; onClick?: () => void; kind?: ButtonKind; disabled?: boolean; type?: "button" | "submit"; title?: string; size?: "sm" | "lg"; block?: boolean; icon?: ReactNode; className?: string }) { - return ( - - ); -} - -export function Field({ label, children, hint }: { label: ReactNode; children: ReactNode; hint?: ReactNode }) { - return ( - - ); -} - -export function TxStatus({ state }: { state: TxState }) { - if (state.phase === "idle") return null; - if (state.phase === "error") return
{state.message}
; - if (state.phase === "done") return
Confirmed onchain.
; - if (state.phase === "mining") return
Waiting for the block…
; - return
{state.phase === "simulating" ? "Checking the transaction against the vault policy…" : "Confirm in your wallet…"}
; -} - -export function Empty({ children, icon }: { children: ReactNode; icon?: ReactNode }) { - if (icon) { - return ( -
-
{icon}
-
{children}
-
- ); - } - return
{children}
; -} - -export function KV({ rows }: { rows: Array<[ReactNode, ReactNode]> }) { - return ( -
- {rows.map(([k, v], i) => ( -
-
{k}
-
{v}
-
- ))} -
- ); -} - -export function Tabs({ value, onChange, items }: { value: T; onChange: (t: T) => void; items: Array<{ id: T; label: ReactNode; count?: number }> }) { - return ( -
- {items.map((it) => ( - - ))} -
- ); -} - -export function Switch({ on, onChange, label }: { on: boolean; onChange: (v: boolean) => void; label?: ReactNode }) { - return ( - onChange(!on)} role="switch" aria-checked={on}> - - {label && {label}} - - ); -} - -export function TokenIcon({ symbol, size = 32 }: { symbol: string; size?: number }) { - const s = symbol.toUpperCase(); - const stable = s.includes("USD"); - const native = s === "KASH"; - return ( - - {stable ? "$" : s.slice(0, 1)} - - ); -} - -export function Modal({ title, onClose, children, footer, steps }: { title: ReactNode; onClose: () => void; children: ReactNode; footer?: ReactNode; steps?: ReactNode }) { - useEffect(() => { - const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose(); - window.addEventListener("keydown", onKey); - return () => window.removeEventListener("keydown", onKey); - }, [onClose]); - return ( -
e.target === e.currentTarget && onClose()}> -
-
-
-

{title}

- {steps &&
{steps}
} -
- -
-
{children}
- {footer &&
{footer}
} -
-
- ); -} diff --git a/web/src/config/chain.ts b/web/src/config/chain.ts deleted file mode 100644 index cf97a63..0000000 --- a/web/src/config/chain.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { defineChain } from "viem"; -import deployments from "./deployments.json"; - -/** - * The single place the Ark Constellation endpoints live in the interface. - * The sslip.io hosts are devnet-only and move to an Ark-owned domain for testnet/mainnet. - * The other copy is contracts/foundry.toml. - */ -export const ARK_DEVNET_RPC = "https://evm.34.60.137.196.sslip.io"; -export const ARK_DEVNET_WS = "wss://evm-ws.34.60.137.196.sslip.io"; -export const ARK_DEVNET_EXPLORER = "https://explorer.34.60.137.196.sslip.io"; -export const ARK_DEVNET_FAUCET = "https://faucet.34.60.137.196.sslip.io/"; - -export const arkDevnet = defineChain({ - id: 9000, - name: "Ark Constellation Devnet", - nativeCurrency: { name: "KASH", symbol: "KASH", decimals: 18 }, - rpcUrls: { - default: { http: [ARK_DEVNET_RPC], webSocket: [ARK_DEVNET_WS] }, - }, - blockExplorers: { - default: { name: "Blockscout", url: ARK_DEVNET_EXPLORER }, - }, - testnet: true, -}); - -export type Deployments = { - chainId: number; - testUsd: `0x${string}`; - factory: `0x${string}`; - vaultImplementation: `0x${string}`; - demoVault: `0x${string}`; - deployer: `0x${string}`; - deployedAtBlock: number; -}; - -export const DEPLOYMENTS = deployments as Deployments; - -export const NATIVE_ASSET = "0x0000000000000000000000000000000000000000" as const; - -export function explorerAddress(addr: string): string { - return `${ARK_DEVNET_EXPLORER}/address/${addr}`; -} - -export function explorerTx(hash: string): string { - return `${ARK_DEVNET_EXPLORER}/tx/${hash}`; -} diff --git a/web/src/config/deployments.json b/web/src/config/deployments.json deleted file mode 100644 index ec54f6a..0000000 --- a/web/src/config/deployments.json +++ /dev/null @@ -1 +0,0 @@ -{"chainId":9000,"testUsd":"0x3e41424ADF6Bb69B6271ef9B0541ACD59A0d454a","factory":"0x018c12BA8b085da81E434b9e4001e07CF48764E9","vaultImplementation":"0xE21C7ad3a56D187848F9915bb43f88820278DeA3","demoVault":"0xCC31c7474267ca5600c2D530ebD7657A09c042Dc","deployer":"0x4FEA262782b5BE34B18cf24b6aC026215899EEc0","deployedAtBlock":481946} \ No newline at end of file diff --git a/web/src/hooks/useTx.ts b/web/src/hooks/useTx.ts deleted file mode 100644 index 4a486cf..0000000 --- a/web/src/hooks/useTx.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { useCallback, useState } from "react"; -import { useAccount, usePublicClient, useSwitchChain, useWalletClient } from "wagmi"; -import { getWalletClient } from "wagmi/actions"; -import { wagmiConfig } from "../wagmi"; -import { BaseError, ContractFunctionRevertedError, type Abi } from "viem"; -import { arkDevnet } from "../config/chain"; - -export type TxState = - | { phase: "idle" } - | { phase: "simulating" } - | { phase: "signing" } - | { phase: "mining"; hash: `0x${string}` } - | { phase: "done"; hash: `0x${string}` } - | { phase: "error"; message: string }; - -/** - * Simulate, sign, wait. Simulation runs first so a policy refusal surfaces as a readable custom error - * before the wallet ever asks for a signature (human-readable intent before signing). - */ -export function useTx(onDone?: () => void) { - const { data: walletForChain } = useWalletClient({ chainId: arkDevnet.id }); - const { isConnected, chainId } = useAccount(); - const { switchChainAsync } = useSwitchChain(); - const client = usePublicClient(); - const [state, setState] = useState({ phase: "idle" }); - - const send = useCallback( - async (args: { address: `0x${string}`; abi: Abi; functionName: string; args?: readonly unknown[]; value?: bigint }) => { - if (!client || !isConnected) { - setState({ phase: "error", message: "Connect a wallet to continue." }); - return; - } - let wallet = walletForChain; - // A connected wallet on another network yields no client for this chain: ask it to switch first. - if (!wallet || chainId !== arkDevnet.id) { - try { - setState({ phase: "signing" }); - await switchChainAsync({ chainId: arkDevnet.id }); - wallet = await getWalletClient(wagmiConfig, { chainId: arkDevnet.id }); - } catch { - setState({ phase: "error", message: `Your wallet is on another network. Switch it to Ark Constellation devnet (chain ${arkDevnet.id}) and try again.` }); - return; - } - } - try { - setState({ phase: "simulating" }); - const { request } = await client.simulateContract({ - ...args, - account: wallet.account, - chain: arkDevnet, - } as Parameters[0]); - setState({ phase: "signing" }); - const hash = await wallet.writeContract(request); - setState({ phase: "mining", hash }); - await client.waitForTransactionReceipt({ hash }); - setState({ phase: "done", hash }); - onDone?.(); - } catch (e) { - setState({ phase: "error", message: describeError(e) }); - } - }, - [walletForChain, client, isConnected, chainId, switchChainAsync, onDone], - ); - - const reset = useCallback(() => setState({ phase: "idle" }), []); - return { state, send, reset, busy: state.phase === "simulating" || state.phase === "signing" || state.phase === "mining" }; -} - -const ERROR_TEXT: Record string> = { - NotAuthorized: (a) => `Your wallet does not hold the required role (${roleText(Number(a[0]))}).`, - VaultLocked: () => "The vault is in Lockdown. Outgoing payments and security-reducing changes are blocked until it is lowered.", - Timelocked: (a) => `This cannot execute before ${new Date(Number(a[0]) * 1000).toLocaleString()}.`, - InsufficientApprovals: (a) => `${a[0]} of the required ${a[1]} approvals are in place.`, - InsufficientGuardians: (a) => `${a[0]} of the required ${a[1]} guardian confirmations are in place.`, - PerTxLimitExceeded: () => "This amount is above the per-transaction cap for the asset.", - VelocityExceeded: () => "This would push today's outflow past the 24-hour cap for the asset.", - ExposureHardBlocked: (a) => `This would move ${Number(a[0]) / 100}% of holdings; the policy refuses anything above ${Number(a[1]) / 100}%.`, - RecipientBlocked: () => "This recipient is blocked by policy.", - AssetNotApproved: () => "This asset is not on the vault's approved list.", - AlreadyApproved: () => "You have already confirmed this proposal.", - NotPending: () => "This proposal is no longer open.", - ProposalExpired: () => "This proposal has expired.", - NotVetoable: () => "Guardians cannot veto this proposal: it is neither critical, probationary nor security-reducing.", - ModeNotStricter: () => "The mode can only be raised directly. Lowering it goes through governance.", - ModeNotRelaxation: () => "That mode is not lower than the current one.", - InvalidPolicy: (a) => `Invalid policy: ${String(a[0])}.`, - ThresholdUnsatisfiable: (a) => `Thresholds would be unsatisfiable: ${String(a[0])}.`, - InvalidProposal: (a) => `Invalid proposal: ${String(a[0])}.`, - InvalidRoles: () => "Guardians cannot hold treasury roles, and roles must be valid.", - MemberExists: () => "That address is already a member.", - NotMember: () => "That address is not a member.", - InsufficientBalance: () => "The vault does not hold enough of this asset right now.", - AmountZero: () => "Amount must be greater than zero.", - ZeroAddress: () => "Address cannot be empty.", - NotCancellable: () => "Only the proposer or an owner can cancel this.", -}; - -function roleText(bits: number): string { - const n: string[] = []; - if (bits & 1) n.push("Owner"); - if (bits & 2) n.push("Approver"); - if (bits & 4) n.push("Executor"); - if (bits & 8) n.push("Guardian"); - return n.join(" or ") || "unknown"; -} - -export function describeError(e: unknown): string { - if (e instanceof BaseError) { - const revert = e.walk((err) => err instanceof ContractFunctionRevertedError) as ContractFunctionRevertedError | null; - if (revert?.data) { - const name = revert.data.errorName; - const f = ERROR_TEXT[name]; - if (f) return f(revert.data.args ?? []); - return `${name}(${(revert.data.args ?? []).map(String).join(", ")})`; - } - return e.shortMessage; - } - return e instanceof Error ? e.message : String(e); -} diff --git a/web/src/hooks/useVault.ts b/web/src/hooks/useVault.ts deleted file mode 100644 index 2a24a54..0000000 --- a/web/src/hooks/useVault.ts +++ /dev/null @@ -1,319 +0,0 @@ -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { usePublicClient } from "wagmi"; -import { erc20Abi, parseAbiItem, type PublicClient, type AbiEvent, type ReadContractReturnType } from "viem"; -import { SkurVaultAbi } from "../abi/SkurVault"; -import { DEPLOYMENTS, NATIVE_ASSET } from "../config/chain"; -import { policyFromContract } from "../lib/policy"; -import { - Kind, - Mode, - Status, - Trust, - type AssetLimits, - type AssetMeta, - type Policy, - type Proposal, - type Recipient, -} from "../lib/types"; - -export type Member = { address: `0x${string}`; roles: number }; - -export type ProposalView = Proposal & { - id: bigint; - liveApprovals: number; - liveGuardians: number; - vetoable: boolean; - approvers: `0x${string}`[]; - guardianConfirmers: `0x${string}`[]; - memo: string; -}; - -export type VelocityView = { - daySpent: bigint; - dailyMax: bigint; - envelopeSpent: bigint; - envelope: bigint; - envelopeResetsAt: bigint; -}; - -export type RecipientView = Recipient & { address: `0x${string}` }; - -/** Fast path: everything needed to render balances, policy, members and limits, read concurrently. */ -export type VaultCore = { - address: `0x${string}`; - mode: Mode; - policyVersion: number; - policy: Policy; - members: Member[]; - counts: { owners: number; approvers: number; executors: number; guardians: number }; - assets: AssetMeta[]; - velocity: Record; - proposalCount: number; - /** Head block at the time of the core read; the activity scan uses it so it needs no extra round trip. */ - blockNumber: bigint; - pendingCount: number; - executedCount: number; -}; - -/** Slow path: proposals, recipients and event history. Depends on the log queries, which the devnet serves slowly. */ -export type VaultActivity = { - proposals: ProposalView[]; - recipients: RecipientView[]; - /** True when the event scan failed; proposals still come from contract state but memos and histories are missing. */ - logsFailed: boolean; - policyHistory: Array<{ version: number; activatedAt: bigint; txHash: `0x${string}` }>; - modeHistory: Array<{ previous: Mode; mode: Mode; by: `0x${string}`; reason: string; txHash: `0x${string}`; block: bigint }>; -}; - -export type VaultData = VaultCore & VaultActivity & { activityLoading: boolean; fetchedAt: number }; - -const MAX_PROPOSALS = 60; - -/** Proposals in a terminal status never change again, so their reads are kept for the session. */ -type ContractProposal = ReadContractReturnType; -type ProposalRaw = { id: bigint; p: ContractProposal; live: readonly [number, number] | readonly [bigint, bigint]; vetoable: boolean; approvers: readonly `0x${string}`[]; guardianConfirmers: readonly `0x${string}`[] }; -const terminalCache = new Map(); - -const proposalCreatedEvent = parseAbiItem( - "event ProposalCreated(uint256 indexed id, uint8 indexed kind, address indexed proposer, address asset, address target, uint256 amount, uint8 tier, uint16 riskReasons, uint8 requiredApprovals, uint8 requiredGuardians, uint64 executableAfter, uint64 expiresAt, bool securityReducing, string memo)", -); -const recipientRegisteredEvent = parseAbiItem("event RecipientRegistered(address indexed recipient, uint64 activatesAt)"); -const recipientTrustEvent = parseAbiItem("event RecipientTrustSet(address indexed recipient, uint8 previous, uint8 trust)"); -const policyActivatedEvent = parseAbiItem( - "event PolicyActivated(uint32 indexed version, uint64 activatedAt, (uint8 approvalsLow, uint8 approvalsHigh, uint8 approvalsCritical, uint8 governanceThreshold, uint8 guardianThreshold, bool guardianRequiredCritical, uint32 delayHigh, uint32 delayCritical, uint32 recipientActivationDelay, uint32 policyChangeDelay, uint32 recoveryDelay, uint32 proposalTtl, uint16 highExposureBps, uint16 criticalExposureBps, uint16 hardBlockExposureBps, uint16 envelopeBps, uint32 envelopeWindow) policy)", -); -const modeChangedEvent = parseAbiItem("event ModeChanged(uint8 indexed previous, uint8 indexed mode, address indexed by, bytes32 reason)"); - -function bytes32ToString(hex: `0x${string}`): string { - const bytes = hex.slice(2).match(/.{2}/g) ?? []; - let out = ""; - for (const b of bytes) { - const c = parseInt(b, 16); - if (c === 0) break; - out += String.fromCharCode(c); - } - return out; -} - -async function fetchCore(client: PublicClient, address: `0x${string}`): Promise { - const vault = { address, abi: SkurVaultAbi } as const; - const [modeRaw, policyVersion, policyRaw, membersRaw, assetsRaw, proposalCountRaw, pendingCountRaw, executedCountRaw, nativeBalance, blockNumber] = - await Promise.all([ - client.readContract({ ...vault, functionName: "mode" }), - client.readContract({ ...vault, functionName: "policyVersion" }), - client.readContract({ ...vault, functionName: "getPolicy" }), - client.readContract({ ...vault, functionName: "getMembers" }), - client.readContract({ ...vault, functionName: "getAssets" }), - client.readContract({ ...vault, functionName: "proposalCount" }), - client.readContract({ ...vault, functionName: "pendingCount" }), - client.readContract({ ...vault, functionName: "executedCount" }), - client.getBalance({ address }), - client.getBlockNumber(), - ]); - - const members: Member[] = membersRaw[0].map((a, i) => ({ address: a, roles: Number(membersRaw[1][i]) })); - const counts = members.reduce( - (acc, m) => ({ - owners: acc.owners + (m.roles & 1 ? 1 : 0), - approvers: acc.approvers + (m.roles & 2 ? 1 : 0), - executors: acc.executors + (m.roles & 4 ? 1 : 0), - guardians: acc.guardians + (m.roles & 8 ? 1 : 0), - }), - { owners: 0, approvers: 0, executors: 0, guardians: 0 }, - ); - - // Second wave of concurrent reads: per-asset limits, velocity and ERC-20 metadata. - const perAsset = await Promise.all( - assetsRaw.map(async (asset) => { - const [limitsRaw, v, symbol, decimals, balance] = await Promise.all([ - client.readContract({ ...vault, functionName: "getAssetLimits", args: [asset] }), - client.readContract({ ...vault, functionName: "velocityOf", args: [asset] }), - asset === NATIVE_ASSET ? Promise.resolve("KASH") : client.readContract({ address: asset, abi: erc20Abi, functionName: "symbol" }).catch(() => "TOKEN"), - asset === NATIVE_ASSET ? Promise.resolve(18) : client.readContract({ address: asset, abi: erc20Abi, functionName: "decimals" }).catch(() => 18), - asset === NATIVE_ASSET ? Promise.resolve(nativeBalance) : client.readContract({ address: asset, abi: erc20Abi, functionName: "balanceOf", args: [address] }).catch(() => 0n), - ]); - const limits: AssetLimits = { approved: limitsRaw.approved, lowMax: limitsRaw.lowMax, highMax: limitsRaw.highMax, perTxMax: limitsRaw.perTxMax, dailyMax: limitsRaw.dailyMax }; - const meta: AssetMeta = { address: asset, symbol, decimals: Number(decimals), limits, balance }; - const velocity: VelocityView = { daySpent: v[0], dailyMax: v[1], envelopeSpent: v[2], envelope: v[3], envelopeResetsAt: v[4] }; - return { meta, velocity }; - }), - ); - const velocity: Record = {}; - for (const { meta, velocity: v } of perAsset) velocity[meta.address.toLowerCase()] = v; - - return { - address, - mode: Number(modeRaw) as Mode, - policyVersion: Number(policyVersion), - policy: policyFromContract(policyRaw as unknown as Record), - members, - counts, - assets: perAsset.map((x) => x.meta), - velocity, - proposalCount: Number(proposalCountRaw), - blockNumber, - pendingCount: Number(pendingCountRaw), - executedCount: Number(executedCountRaw), - }; -} - -/** The devnet RPC caps eth_getLogs at 10,000 blocks per call, so the scan runs in parallel windows. */ -const LOG_WINDOW = 9_000n; -type RawLog = { eventName?: string; args?: unknown; transactionHash: `0x${string}`; blockNumber: bigint }; -async function getLogsChunked(client: PublicClient, address: `0x${string}`, events: readonly AbiEvent[], fromBlock: bigint, head: bigint): Promise<{ logs: RawLog[]; failed: boolean }> { - try { - // `head` comes from the core read and may be a few blocks stale, so the last window runs to "latest" - // rather than to `head`; that keeps an event mined between the two reads from being missed. - const windows: Array<[bigint, bigint | "latest"]> = []; - for (let a = fromBlock; a <= head; a += LOG_WINDOW + 1n) windows.push([a, a + LOG_WINDOW > head ? head : a + LOG_WINDOW]); - if (windows.length === 0) windows.push([fromBlock, "latest"]); - else windows[windows.length - 1][1] = "latest"; - const chunks = await Promise.all(windows.map(([from, to]) => client.getLogs({ address, events, fromBlock: from, toBlock: to }) as unknown as Promise)); - return { logs: chunks.flat(), failed: false }; - } catch { - return { logs: [], failed: true }; - } -} - -async function fetchActivity(client: PublicClient, address: `0x${string}`, proposalCount: number, head: bigint): Promise { - const vault = { address, abi: SkurVaultAbi } as const; - const fromBlock = BigInt(DEPLOYMENTS.deployedAtBlock || 0); - - const firstId = Math.max(1, proposalCount - MAX_PROPOSALS + 1); - const ids: bigint[] = []; - for (let i = proposalCount; i >= firstId; i--) ids.push(BigInt(i)); - - // One log query for every event we index; the devnet serves each getLogs call slowly, so five - // separate queries were the long pole of the page load. - const [{ logs: allLogs, failed: logsFailed }, proposalsRaw] = await Promise.all([ - getLogsChunked(client, address, [proposalCreatedEvent, recipientRegisteredEvent, recipientTrustEvent, policyActivatedEvent, modeChangedEvent], fromBlock, head), - Promise.all( - ids.map(async (id): Promise => { - const cached = terminalCache.get(`${address}:${id}`); - if (cached) return cached; - const p = await client.readContract({ ...vault, functionName: "getProposal", args: [id] }); - const pending = Number(p.status) === Status.PENDING; - const [live, vetoable, approvers, guardianConfirmers] = await Promise.all([ - pending ? client.readContract({ ...vault, functionName: "liveApprovals", args: [id] }) : ([0, 0] as const), - pending ? client.readContract({ ...vault, functionName: "isVetoable", args: [id] }) : false, - client.readContract({ ...vault, functionName: "getApprovers", args: [id] }), - client.readContract({ ...vault, functionName: "getGuardianConfirmers", args: [id] }), - ]); - const raw: ProposalRaw = { id, p, live, vetoable, approvers, guardianConfirmers }; - if (!pending) terminalCache.set(`${address}:${id}`, raw); - return raw; - }), - ), - ]); - // viem types the args of a multi-event query as a union; narrow per event name explicitly. - type LogWith = { args: A; transactionHash: `0x${string}`; blockNumber: bigint }; - const byName = (name: string): LogWith[] => allLogs.filter((l) => l.eventName === name) as unknown as LogWith[]; - const createdLogs = byName<{ id?: bigint; memo?: string }>("ProposalCreated"); - const registeredLogs = byName<{ recipient?: `0x${string}` }>("RecipientRegistered"); - const trustLogs = byName<{ recipient?: `0x${string}` }>("RecipientTrustSet"); - const policyLogs = byName<{ version?: number; activatedAt?: bigint }>("PolicyActivated"); - const modeLogs = byName<{ previous?: number; mode?: number; by?: `0x${string}`; reason?: `0x${string}` }>("ModeChanged"); - - const memos = new Map(); - for (const l of createdLogs) memos.set(String(l.args.id), l.args.memo ?? ""); - - const proposals: ProposalView[] = proposalsRaw.map(({ id, p, live, vetoable, approvers, guardianConfirmers }) => ({ - id, - kind: Number(p.kind) as Kind, - status: Number(p.status) as Status, - tier: Number(p.tier), - requiredApprovals: Number(p.requiredApprovals), - requiredGuardians: Number(p.requiredGuardians), - securityReducing: p.securityReducing, - riskReasons: Number(p.riskReasons), - policyVersion: Number(p.policyVersion), - proposer: p.proposer, - asset: p.asset, - target: p.target, - amount: p.amount, - createdAt: p.createdAt, - executableAfter: p.executableAfter, - expiresAt: p.expiresAt, - data: p.data, - liveApprovals: Number(live[0]), - liveGuardians: Number(live[1]), - vetoable, - approvers: [...approvers], - guardianConfirmers: [...guardianConfirmers], - memo: memos.get(String(id)) ?? "", - })); - - const recipientSet = new Set(); - for (const l of registeredLogs) if (l.args.recipient) recipientSet.add(l.args.recipient); - for (const l of trustLogs) if (l.args.recipient) recipientSet.add(l.args.recipient); - for (const p of proposals) if (p.kind === Kind.TRANSFER) recipientSet.add(p.target); - const recipients: RecipientView[] = await Promise.all( - [...recipientSet].map(async (r) => { - const rec = await client.readContract({ ...vault, functionName: "getRecipient", args: [r as `0x${string}`] }); - return { address: r as `0x${string}`, trust: Number(rec.trust) as Trust, registeredAt: rec.registeredAt, activatesAt: rec.activatesAt, paymentCount: rec.paymentCount, totalPaid: rec.totalPaid }; - }), - ); - recipients.sort((a, b) => Number(b.registeredAt - a.registeredAt)); - - return { - proposals, - recipients, - logsFailed, - policyHistory: policyLogs.map((l) => ({ version: Number(l.args.version), activatedAt: l.args.activatedAt ?? 0n, txHash: l.transactionHash })), - modeHistory: modeLogs.map((l) => ({ - previous: Number(l.args.previous) as Mode, - mode: Number(l.args.mode) as Mode, - by: l.args.by as `0x${string}`, - reason: bytes32ToString(l.args.reason as `0x${string}`), - txHash: l.transactionHash, - block: l.blockNumber, - })), - }; -} - -export function vaultQueryKey(address: `0x${string}` | null) { - return ["vault", address] as const; -} - -const EMPTY_ACTIVITY: VaultActivity = { proposals: [], recipients: [], logsFailed: false, policyHistory: [], modeHistory: [] }; - -/** - * Two queries so the interface paints as soon as balances and policy arrive, then fills in the - * queue and history. Both refresh on an interval and are invalidated together after a write. - */ -export function useVault(address: `0x${string}` | null): { data: VaultData | undefined; error: unknown; isLoading: boolean } { - const client = usePublicClient(); - const core = useQuery({ - queryKey: [...vaultQueryKey(address), "core"], - queryFn: () => fetchCore(client as PublicClient, address as `0x${string}`), - enabled: Boolean(address && client), - refetchInterval: 20_000, - staleTime: 10_000, - }); - const activity = useQuery({ - queryKey: [...vaultQueryKey(address), "activity", core.data?.proposalCount ?? -1], - queryFn: () => fetchActivity(client as PublicClient, address as `0x${string}`, core.data?.proposalCount ?? 0, core.data?.blockNumber ?? 0n), - enabled: Boolean(address && client && core.data), - refetchInterval: 30_000, - staleTime: 15_000, - placeholderData: (prev) => prev, - }); - const data: VaultData | undefined = core.data - ? { ...core.data, ...(activity.data ?? EMPTY_ACTIVITY), activityLoading: !activity.data, fetchedAt: core.dataUpdatedAt } - : undefined; - return { data, error: core.error ?? activity.error, isLoading: core.isLoading }; -} - -/** - * Refresh after a write. The devnet RPC sits behind replicas that can lag a mined receipt by several - * seconds, so one refetch straight after the receipt often reads the old state; a few bounded - * follow-ups catch up without waiting for the regular interval. - */ -export function useInvalidateVault(address: `0x${string}` | null) { - const qc = useQueryClient(); - return () => { - const key = vaultQueryKey(address); - void qc.invalidateQueries({ queryKey: key }); - for (const ms of [6_000, 15_000, 30_000]) setTimeout(() => void qc.invalidateQueries({ queryKey: key }), ms); - }; -} diff --git a/web/src/lib/format.ts b/web/src/lib/format.ts deleted file mode 100644 index fd45e11..0000000 --- a/web/src/lib/format.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { formatUnits, parseUnits } from "viem"; - -export function short(addr: string, n = 4): string { - if (!addr) return ""; - return `${addr.slice(0, 2 + n)}…${addr.slice(-n)}`; -} - -export function fmtAmount(value: bigint, decimals: number, symbol?: string, maxFrac = 2): string { - const s = formatUnits(value, decimals); - const [int, frac = ""] = s.split("."); - const intFmt = Number(int).toLocaleString("en-US"); - const fracTrim = frac.slice(0, maxFrac).replace(/0+$/, ""); - const out = fracTrim ? `${intFmt}.${fracTrim}` : intFmt; - return symbol ? `${out} ${symbol}` : out; -} - -export function parseAmount(text: string, decimals: number): bigint | null { - try { - const cleaned = text.replace(/,/g, "").trim(); - if (!cleaned) return null; - return parseUnits(cleaned, decimals); - } catch { - return null; - } -} - -export function fmtBps(bps: number): string { - return `${(bps / 100).toFixed(bps % 100 === 0 ? 0 : 2)}%`; -} - -export function fmtDuration(seconds: number): string { - if (seconds <= 0) return "none"; - const d = Math.floor(seconds / 86400); - const h = Math.floor((seconds % 86400) / 3600); - const m = Math.floor((seconds % 3600) / 60); - const parts: string[] = []; - if (d) parts.push(`${d}d`); - if (h) parts.push(`${h}h`); - if (m && !d) parts.push(`${m}m`); - return parts.join(" ") || `${seconds}s`; -} - -export function fmtDate(ts: bigint | number): string { - const n = Number(ts); - if (!n) return "—"; - return new Date(n * 1000).toLocaleString("en-US", { dateStyle: "medium", timeStyle: "short" }); -} - -export function fmtRelative(ts: bigint | number, now = Date.now() / 1000): string { - const n = Number(ts); - if (!n) return "—"; - const diff = n - now; - if (Math.abs(diff) < 60) return diff >= 0 ? "now" : "just now"; - const abs = fmtDuration(Math.abs(Math.round(diff))); - return diff > 0 ? `in ${abs}` : `${abs} ago`; -} - -export function nowSec(): number { - return Math.floor(Date.now() / 1000); -} diff --git a/web/src/lib/maxLoss.ts b/web/src/lib/maxLoss.ts deleted file mode 100644 index 2e19360..0000000 --- a/web/src/lib/maxLoss.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Mode, Tier, type AssetLimits, type Policy } from "./types"; -import { effectiveCaps } from "./risk"; - -/** - * Maximum possible loss, per asset, translated from a policy into numbers a finance team understands. - * Conservative by construction: it assumes every signer needed for a tier is compromised and that the - * attacker chooses amounts optimally. It is an upper bound under the stated assumptions, never a guarantee. - */ -export type MaxLoss = { - /** Largest amount that can leave without any delay (LOW-tier transfers, repeated until a cap binds). */ - immediate: bigint; - /** Amount that can leave within 24h if only approvers are compromised and no guardian acts. */ - day: bigint; - /** Seconds a CRITICAL transfer must wait, i.e. how long guardians have to veto the largest transfers. */ - criticalDelay: number; - highDelay: number; - /** Which control produced each number. */ - immediateBinding: string; - dayBinding: string; - assumptions: string[]; -}; - -export function computeMaxLoss(policy: Policy, limits: AssetLimits, balance: bigint, mode: Mode): MaxLoss { - const caps = effectiveCaps(limits.perTxMax, limits.dailyMax, mode); - const assumptions = [ - "Enough approvers to satisfy each tier are compromised and cooperate.", - "Executors are compromised or collude.", - "No guardian vetoes or freezes during the delay windows.", - "Recipients used by the attacker are already activated (not in probation).", - "Limits are per asset; each asset has its own envelope and caps.", - ]; - - // --- immediate: LOW-tier transfers need no delay. Each is at most lowMax (or exposure-bounded). - // An attacker repeats them until the daily cap or envelope binds. In Elevated mode LOW is impossible. - let immediate = 0n; - let immediateBinding = "no undelayed tier"; - if (mode === Mode.NORMAL) { - const exposureLow = policy.highExposureBps === 0 ? balance : (balance * BigInt(policy.highExposureBps - 1)) / 10_000n; - const perTransfer = min(limits.lowMax, caps.perTxMax || limits.lowMax, exposureLow); - if (perTransfer > 0n) { - immediate = balance; - immediateBinding = "entire balance (no daily cap or envelope)"; - const envelope = policy.envelopeBps === 0 ? 0n : (balance * BigInt(policy.envelopeBps)) / 10_000n; - if (caps.dailyMax !== 0n && caps.dailyMax < immediate) { - immediate = caps.dailyMax; - immediateBinding = "daily cap"; - } - if (envelope !== 0n && envelope < immediate) { - immediate = envelope; - immediateBinding = "loss envelope"; - } - } else { - immediate = 0n; - immediateBinding = "routine tier disabled by limits"; - } - } - - // --- 24h: approvers can also push HIGH-tier transfers if delayHigh < 24h. Still bounded by caps. - let day = 0n; - let dayBinding = "no path"; - const highReachable = policy.delayHigh < 86_400; - const envelope = policy.envelopeBps === 0 ? 0n : (balance * BigInt(policy.envelopeBps)) / 10_000n; - const perTx = caps.perTxMax === 0n ? balance : caps.perTxMax; - const highCeiling = policy.criticalExposureBps === 0 ? balance : (balance * BigInt(policy.criticalExposureBps - 1)) / 10_000n; - const candidates: Array<[bigint, string]> = []; - candidates.push([balance, "entire balance"]); - if (caps.dailyMax !== 0n) candidates.push([caps.dailyMax * 2n, "two daily buckets across a boundary"]); - if (envelope !== 0n) candidates.push([envelope * 2n, "two envelope windows across a boundary"]); - if (!highReachable) { - // Only LOW transfers fit in 24h; each is at most lowMax and the tier is bounded by exposure. - candidates.push([immediate * 2n, "routine tier only, two buckets"]); - } else { - candidates.push([min(perTx, highCeiling) === 0n ? 0n : balance, "high tier reachable within 24h"]); - } - [day, dayBinding] = candidates.reduce((a, b) => (b[0] < a[0] ? b : a)); - if (mode === Mode.LOCKDOWN) { - immediate = 0n; - day = 0n; - immediateBinding = "vault is in Lockdown"; - dayBinding = "vault is in Lockdown"; - } - return { - immediate, - day, - criticalDelay: policy.delayCritical, - highDelay: policy.delayHigh, - immediateBinding, - dayBinding, - assumptions, - }; -} - -function min(...xs: bigint[]): bigint { - return xs.reduce((a, b) => (b < a ? b : a)); -} - -export { Tier }; diff --git a/web/src/lib/policy.ts b/web/src/lib/policy.ts deleted file mode 100644 index 2612cd2..0000000 --- a/web/src/lib/policy.ts +++ /dev/null @@ -1,131 +0,0 @@ -import type { AssetLimits, Policy } from "./types"; - -/** Mirror of contracts/src/SkurPolicyLib.sol. The contract decides; this only previews. */ -const MIN_EXECUTION_WINDOW = 3600; -const BPS = 10_000; - -export function validatePolicy(p: Policy): string[] { - const errs: string[] = []; - if (p.approvalsLow === 0) errs.push("Low-tier approvals must be at least 1."); - if (p.approvalsHigh < p.approvalsLow) errs.push("High-tier approvals cannot be lower than low-tier approvals."); - if (p.approvalsCritical < p.approvalsHigh) errs.push("Critical-tier approvals cannot be lower than high-tier approvals."); - if (p.governanceThreshold === 0) errs.push("Governance threshold must be at least 1."); - if (p.guardianRequiredCritical && p.guardianThreshold === 0) errs.push("Guardian confirmation is required but the guardian threshold is 0."); - if (p.delayCritical < p.delayHigh) errs.push("Critical delay cannot be shorter than high delay."); - if (p.criticalExposureBps !== 0 && p.highExposureBps > p.criticalExposureBps) errs.push("High exposure threshold must not exceed the critical one."); - if (p.criticalExposureBps > BPS || p.highExposureBps > BPS) errs.push("Exposure thresholds must be at most 100%."); - if (p.hardBlockExposureBps !== 0 && p.hardBlockExposureBps < p.criticalExposureBps) errs.push("Hard block must not be below the critical exposure threshold."); - if (p.hardBlockExposureBps > BPS) errs.push("Hard block must be at most 100%."); - if (p.envelopeBps > BPS) errs.push("Envelope must be at most 100%."); - if (p.envelopeBps !== 0 && p.envelopeWindow < MIN_EXECUTION_WINDOW) errs.push("Envelope window must be at least one hour."); - const longest = Math.max(p.delayCritical, p.policyChangeDelay, p.recoveryDelay, p.recipientActivationDelay); - if (p.proposalTtl < longest + MIN_EXECUTION_WINDOW) errs.push("Proposal lifetime must exceed the longest delay by at least one hour."); - return errs; -} - -export function validateLimits(l: AssetLimits): string[] { - const errs: string[] = []; - if (l.lowMax > l.highMax) errs.push("Routine maximum cannot exceed the high maximum."); - if (l.perTxMax !== 0n && l.dailyMax !== 0n && l.perTxMax > l.dailyMax) errs.push("Per-transaction cap cannot exceed the daily cap."); - return errs; -} - -export function validateCounts(p: Policy, owners: number, approvers: number, executors: number, guardians: number): string[] { - const errs: string[] = []; - if (owners < 1) errs.push("At least one owner is required."); - if (executors < 1) errs.push("At least one executor is required."); - if (owners < p.governanceThreshold) errs.push(`Governance threshold (${p.governanceThreshold}) exceeds the number of owners (${owners}).`); - if (approvers < p.approvalsCritical) errs.push(`Critical approvals (${p.approvalsCritical}) exceed the number of approvers (${approvers}).`); - if (guardians < p.guardianThreshold) errs.push(`Guardian threshold (${p.guardianThreshold}) exceeds the number of guardians (${guardians}).`); - return errs; -} - -function thresholdLoosens(a: number, b: number): boolean { - if (a === 0) return false; - if (b === 0) return true; - return b > a; -} - -function capLoosens(a: bigint, b: bigint): boolean { - if (a === 0n) return false; - if (b === 0n) return true; - return b > a; -} - -/** Returns the list of fields that weaken security (empty means the change is tightening-only). */ -export function policyReductions(a: Policy, b: Policy): string[] { - const out: string[] = []; - if (b.approvalsLow < a.approvalsLow) out.push("fewer approvals for routine transfers"); - if (b.approvalsHigh < a.approvalsHigh) out.push("fewer approvals for high-risk transfers"); - if (b.approvalsCritical < a.approvalsCritical) out.push("fewer approvals for critical transfers"); - if (b.governanceThreshold < a.governanceThreshold) out.push("lower governance threshold"); - if (b.guardianThreshold < a.guardianThreshold) out.push("fewer guardian confirmations"); - if (a.guardianRequiredCritical && !b.guardianRequiredCritical) out.push("guardian no longer required for critical transfers"); - if (b.delayHigh < a.delayHigh) out.push("shorter high-risk delay"); - if (b.delayCritical < a.delayCritical) out.push("shorter critical delay"); - if (b.recipientActivationDelay < a.recipientActivationDelay) out.push("shorter new-recipient delay"); - if (b.policyChangeDelay < a.policyChangeDelay) out.push("shorter policy-change delay"); - if (b.recoveryDelay < a.recoveryDelay) out.push("shorter recovery delay"); - if (b.proposalTtl > a.proposalTtl) out.push("longer proposal lifetime"); - if (thresholdLoosens(a.highExposureBps, b.highExposureBps)) out.push("higher exposure threshold for escalation"); - if (thresholdLoosens(a.criticalExposureBps, b.criticalExposureBps)) out.push("higher exposure threshold for critical"); - if (thresholdLoosens(a.hardBlockExposureBps, b.hardBlockExposureBps)) out.push("weaker exposure hard block"); - if (thresholdLoosens(a.envelopeBps, b.envelopeBps)) out.push("larger loss envelope"); - if (a.envelopeBps !== 0 && b.envelopeWindow < a.envelopeWindow) out.push("shorter envelope window"); - return out; -} - -export function limitReductions(a: AssetLimits, b: AssetLimits): string[] { - const out: string[] = []; - if (!a.approved && b.approved) out.push("approving a new asset"); - if (b.lowMax > a.lowMax) out.push("higher routine maximum"); - if (b.highMax > a.highMax) out.push("higher high-tier maximum"); - if (capLoosens(a.perTxMax, b.perTxMax)) out.push("weaker per-transaction cap"); - if (capLoosens(a.dailyMax, b.dailyMax)) out.push("weaker daily cap"); - return out; -} - -export function policyToContract(p: Policy) { - return { - approvalsLow: p.approvalsLow, - approvalsHigh: p.approvalsHigh, - approvalsCritical: p.approvalsCritical, - governanceThreshold: p.governanceThreshold, - guardianThreshold: p.guardianThreshold, - guardianRequiredCritical: p.guardianRequiredCritical, - delayHigh: p.delayHigh, - delayCritical: p.delayCritical, - recipientActivationDelay: p.recipientActivationDelay, - policyChangeDelay: p.policyChangeDelay, - recoveryDelay: p.recoveryDelay, - proposalTtl: p.proposalTtl, - highExposureBps: p.highExposureBps, - criticalExposureBps: p.criticalExposureBps, - hardBlockExposureBps: p.hardBlockExposureBps, - envelopeBps: p.envelopeBps, - envelopeWindow: p.envelopeWindow, - } as const; -} - -export function policyFromContract(raw: Record): Policy { - const n = (k: string) => Number(raw[k] ?? 0); - return { - approvalsLow: n("approvalsLow"), - approvalsHigh: n("approvalsHigh"), - approvalsCritical: n("approvalsCritical"), - governanceThreshold: n("governanceThreshold"), - guardianThreshold: n("guardianThreshold"), - guardianRequiredCritical: Boolean(raw.guardianRequiredCritical), - delayHigh: n("delayHigh"), - delayCritical: n("delayCritical"), - recipientActivationDelay: n("recipientActivationDelay"), - policyChangeDelay: n("policyChangeDelay"), - recoveryDelay: n("recoveryDelay"), - proposalTtl: n("proposalTtl"), - highExposureBps: n("highExposureBps"), - criticalExposureBps: n("criticalExposureBps"), - hardBlockExposureBps: n("hardBlockExposureBps"), - envelopeBps: n("envelopeBps"), - envelopeWindow: n("envelopeWindow"), - }; -} diff --git a/web/src/lib/risk.test.ts b/web/src/lib/risk.test.ts deleted file mode 100644 index f08b61a..0000000 --- a/web/src/lib/risk.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { classify, exposureBps, requirements, type RiskInput } from "./risk"; -import { - Mode, - REASON_AMOUNT_CRITICAL, - REASON_AMOUNT_HIGH, - REASON_EXPOSURE_CRITICAL, - REASON_EXPOSURE_HIGH, - REASON_MODE_ELEVATED, - REASON_RECIPIENT_PROBATION, - REASON_RECIPIENT_RESTRICTED, - REASON_RECIPIENT_UNKNOWN, - REASON_VELOCITY_PRESSURE, - Tier, - Trust, -} from "./types"; - -const USD = 1_000_000n; - -function base(): RiskInput { - return { - amount: 1_000n * USD, - assetBalance: 1_000_000n * USD, - lowMax: 5_000n * USD, - highMax: 25_000n * USD, - dailyMax: 100_000n * USD, - daySpent: 0n, - trust: Trust.VERIFIED, - inProbation: false, - mode: Mode.NORMAL, - highExposureBps: 500, - criticalExposureBps: 2000, - }; -} - -/** Same vectors as contracts/test/SkurRisk.t.sol. */ -describe("risk engine mirror", () => { - it("routine supplier payment is LOW", () => { - const r = classify(base()); - expect(r).toEqual({ tier: Tier.LOW, reasons: 0, exposureBps: 10 }); - }); - - it("amount above lowMax is HIGH", () => { - const r = classify({ ...base(), amount: 10_000n * USD }); - expect(r.tier).toBe(Tier.HIGH); - expect(r.reasons).toBe(REASON_AMOUNT_HIGH); - }); - - it("amount above highMax is CRITICAL", () => { - const r = classify({ ...base(), amount: 30_000n * USD }); - expect(r.tier).toBe(Tier.CRITICAL); - expect(r.reasons).toBe(REASON_AMOUNT_CRITICAL); - }); - - it("exposure escalates a small treasury", () => { - const r = classify({ ...base(), assetBalance: 50_000n * USD, amount: 4_000n * USD, dailyMax: 0n }); - expect(r).toEqual({ tier: Tier.HIGH, reasons: REASON_EXPOSURE_HIGH, exposureBps: 800 }); - }); - - it("exposure critical", () => { - const r = classify({ ...base(), assetBalance: 20_000n * USD, amount: 4_000n * USD, dailyMax: 0n }); - expect(r).toEqual({ tier: Tier.CRITICAL, reasons: REASON_EXPOSURE_CRITICAL, exposureBps: 2000 }); - }); - - it("new recipient bumps one tier", () => { - let r = classify({ ...base(), trust: Trust.NEW, inProbation: true }); - expect(r.tier).toBe(Tier.HIGH); - expect(r.reasons).toBe(REASON_RECIPIENT_PROBATION); - r = classify({ ...base(), trust: Trust.NEW, inProbation: true, amount: 10_000n * USD }); - expect(r.tier).toBe(Tier.CRITICAL); - expect(r.reasons).toBe(REASON_AMOUNT_HIGH | REASON_RECIPIENT_PROBATION); - }); - - it("unknown recipient treated as probation", () => { - const r = classify({ ...base(), trust: Trust.UNKNOWN }); - expect(r.tier).toBe(Tier.HIGH); - expect(r.reasons).toBe(REASON_RECIPIENT_UNKNOWN | REASON_RECIPIENT_PROBATION); - }); - - it("activated new recipient is normal", () => { - const r = classify({ ...base(), trust: Trust.NEW, inProbation: false }); - expect(r).toMatchObject({ tier: Tier.LOW, reasons: 0 }); - }); - - it("restricted recipient always critical", () => { - const r = classify({ ...base(), trust: Trust.RESTRICTED }); - expect(r).toMatchObject({ tier: Tier.CRITICAL, reasons: REASON_RECIPIENT_RESTRICTED }); - }); - - it("velocity pressure", () => { - const r = classify({ ...base(), daySpent: 49_500n * USD }); - expect(r).toMatchObject({ tier: Tier.HIGH, reasons: REASON_VELOCITY_PRESSURE }); - }); - - it("elevated mode bumps", () => { - const r = classify({ ...base(), mode: Mode.ELEVATED }); - expect(r).toMatchObject({ tier: Tier.HIGH, reasons: REASON_MODE_ELEVATED }); - }); - - it("worst case stacks", () => { - const r = classify({ - ...base(), - amount: 30_000n * USD, - assetBalance: 100_000n * USD, - trust: Trust.UNKNOWN, - mode: Mode.ELEVATED, - }); - expect(r.tier).toBe(Tier.CRITICAL); - expect(r.exposureBps).toBe(3000); - expect(r.reasons).toBe( - REASON_AMOUNT_CRITICAL | - REASON_EXPOSURE_CRITICAL | - REASON_RECIPIENT_UNKNOWN | - REASON_RECIPIENT_PROBATION | - REASON_MODE_ELEVATED, - ); - }); - - it("exposure edges", () => { - expect(exposureBps(0n, 100n)).toBe(0); - expect(exposureBps(1n, 0n)).toBe(10_000); - expect(exposureBps(100n, 100n)).toBe(10_000); - expect(exposureBps(150n, 100n)).toBe(10_000); - expect(exposureBps(1n, 100n)).toBe(100); - expect(exposureBps(1n, 30_000n)).toBe(0); - }); - - it("requirements per tier", () => { - const p = { - approvalsLow: 1, - approvalsHigh: 2, - approvalsCritical: 3, - governanceThreshold: 2, - guardianThreshold: 2, - guardianRequiredCritical: true, - delayHigh: 21600, - delayCritical: 86400, - recipientActivationDelay: 0, - policyChangeDelay: 0, - recoveryDelay: 0, - proposalTtl: 0, - highExposureBps: 0, - criticalExposureBps: 0, - hardBlockExposureBps: 0, - envelopeBps: 0, - envelopeWindow: 0, - }; - expect(requirements(p, Tier.LOW)).toEqual({ approvals: 1, guardians: 0, delay: 0 }); - expect(requirements(p, Tier.HIGH)).toEqual({ approvals: 2, guardians: 0, delay: 21600 }); - expect(requirements(p, Tier.CRITICAL)).toEqual({ approvals: 3, guardians: 2, delay: 86400 }); - expect(requirements({ ...p, guardianRequiredCritical: false }, Tier.CRITICAL).guardians).toBe(0); - }); -}); diff --git a/web/src/lib/simulator.ts b/web/src/lib/simulator.ts deleted file mode 100644 index b6486bf..0000000 --- a/web/src/lib/simulator.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { classify, effectiveCaps, requirements } from "./risk"; -import { Mode, Tier, Trust, type AssetLimits, type Policy } from "./types"; -import { fmtDuration } from "./format"; - -/** - * Policy simulator: runs a configured policy against the blueprint's attack scenarios before it is - * activated. Everything here is the deterministic risk engine plus the vault's limit checks; nothing is - * heuristic. Verdicts describe what the contract would do, in business language. - */ -export type Verdict = "blocked" | "delayed" | "escalated" | "allowed" | "impossible"; - -export type ScenarioResult = { - id: string; - title: string; - narrative: string; - verdict: Verdict; - detail: string[]; -}; - -export type SimContext = { - policy: Policy; - limits: AssetLimits; - balance: bigint; - decimals: number; - symbol: string; - approverCount: number; - guardianCount: number; -}; - -function fmt(v: bigint, ctx: SimContext): string { - const s = Number(v) / 10 ** ctx.decimals; - return `${s.toLocaleString("en-US", { maximumFractionDigits: 2 })} ${ctx.symbol}`; -} - -function single(ctx: SimContext, amount: bigint, trust: Trust, inProbation: boolean, mode: Mode, compromised: number) { - const caps = effectiveCaps(ctx.limits.perTxMax, ctx.limits.dailyMax, mode); - const detail: string[] = []; - if (mode === Mode.LOCKDOWN) return { verdict: "blocked" as Verdict, detail: ["Vault is in Lockdown: no outgoing execution."] }; - if (trust === Trust.BLOCKED) return { verdict: "blocked" as Verdict, detail: ["Recipient is blocked."] }; - if (caps.perTxMax !== 0n && amount > caps.perTxMax) { - return { verdict: "blocked" as Verdict, detail: [`Per-transaction cap is ${fmt(caps.perTxMax, ctx)}; the proposal is refused at creation.`] }; - } - const r = classify({ - amount, - assetBalance: ctx.balance, - lowMax: ctx.limits.lowMax, - highMax: ctx.limits.highMax, - dailyMax: caps.dailyMax, - daySpent: 0n, - trust, - inProbation, - mode, - highExposureBps: ctx.policy.highExposureBps, - criticalExposureBps: ctx.policy.criticalExposureBps, - }); - if (ctx.policy.hardBlockExposureBps !== 0 && r.exposureBps > ctx.policy.hardBlockExposureBps) { - return { verdict: "blocked" as Verdict, detail: [`Exposure ${(r.exposureBps / 100).toFixed(1)}% exceeds the hard block of ${ctx.policy.hardBlockExposureBps / 100}%.`] }; - } - if (caps.dailyMax !== 0n && amount > caps.dailyMax) { - return { verdict: "blocked" as Verdict, detail: [`Daily cap is ${fmt(caps.dailyMax, ctx)}; execution would revert.`] }; - } - const envelope = ctx.policy.envelopeBps === 0 ? 0n : (ctx.balance * BigInt(ctx.policy.envelopeBps)) / 10_000n; - if (envelope !== 0n && amount > envelope) { - return { verdict: "blocked" as Verdict, detail: [`Loss envelope is ${fmt(envelope, ctx)} per ${fmtDuration(ctx.policy.envelopeWindow)}; the circuit breaker trips and the vault enters Lockdown instead of paying.`] }; - } - const req = requirements(ctx.policy, r.tier); - detail.push(`Risk tier: ${Tier[r.tier]} (exposure ${(r.exposureBps / 100).toFixed(2)}%).`); - detail.push(`Needs ${req.approvals} approval${req.approvals === 1 ? "" : "s"}${req.guardians ? ` and ${req.guardians} guardian confirmation${req.guardians === 1 ? "" : "s"}` : ""}${req.delay ? `, then waits ${fmtDuration(req.delay)}` : ", no delay"}.`); - let delay = req.delay; - if (inProbation) { - delay = Math.max(delay, ctx.policy.recipientActivationDelay); - detail.push(`Recipient activation delay forces a wait of at least ${fmtDuration(ctx.policy.recipientActivationDelay)}.`); - } - const vetoable = r.tier === Tier.CRITICAL || inProbation || mode !== Mode.NORMAL; - if (vetoable) detail.push("Any guardian can veto before execution."); - if (req.guardians > ctx.guardianCount) { - return { verdict: "impossible" as Verdict, detail: [...detail, "More guardian confirmations are required than guardians exist."] }; - } - if (compromised < req.approvals) { - detail.push(`Attacker controls ${compromised} approver${compromised === 1 ? "" : "s"}, fewer than required.`); - return { verdict: "blocked" as Verdict, detail }; - } - if (req.guardians > 0) { - detail.push("Guardian confirmation is an independent control the attacker does not hold."); - return { verdict: "blocked" as Verdict, detail }; - } - if (delay > 0) return { verdict: "delayed" as Verdict, detail }; - if (r.tier > Tier.LOW) return { verdict: "escalated" as Verdict, detail }; - return { verdict: "allowed" as Verdict, detail }; -} - -export function runScenarios(ctx: SimContext): ScenarioResult[] { - const out: ScenarioResult[] = []; - const pct = (bps: number) => (ctx.balance * BigInt(bps)) / 10_000n; - const unit = 10n ** BigInt(ctx.decimals); - - // 1. Finance signer compromised, $100k to a new address - { - const amount = 100_000n * unit; - const r = single(ctx, amount, Trust.UNKNOWN, true, Mode.NORMAL, 1); - out.push({ - id: "single-signer-new-address", - title: "One finance signer compromised", - narrative: `An attacker holding a single approver key proposes ${fmt(amount, ctx)} to an address the vault has never paid.`, - verdict: r.verdict, - detail: r.detail, - }); - } - // 2. Three signers compromised, 40% of treasury - { - const amount = pct(4000); - const r = single(ctx, amount, Trust.VERIFIED, false, Mode.NORMAL, 3); - out.push({ - id: "three-signers-40pct", - title: "Three signers compromised, 40% withdrawal", - narrative: `An attacker holding three approver keys proposes ${fmt(amount, ctx)}, 40% of holdings, to a verified recipient.`, - verdict: r.verdict, - detail: r.detail, - }); - } - // 3. Split drain: 20 x 9,900 - { - const each = 9_900n * unit; - const total = each * 20n; - const caps = effectiveCaps(ctx.limits.perTxMax, ctx.limits.dailyMax, Mode.NORMAL); - const envelope = ctx.policy.envelopeBps === 0 ? 0n : pct(ctx.policy.envelopeBps); - const detail: string[] = []; - let verdict: Verdict = "allowed"; - const tierEach = classify({ - amount: each, - assetBalance: ctx.balance, - lowMax: ctx.limits.lowMax, - highMax: ctx.limits.highMax, - dailyMax: caps.dailyMax, - daySpent: 0n, - trust: Trust.VERIFIED, - inProbation: false, - mode: Mode.NORMAL, - highExposureBps: ctx.policy.highExposureBps, - criticalExposureBps: ctx.policy.criticalExposureBps, - }); - detail.push(`Each transfer alone is ${Tier[tierEach.tier]} tier.`); - if (caps.dailyMax !== 0n && total > caps.dailyMax) { - const n = Number(caps.dailyMax / each); - detail.push(`Daily cap ${fmt(caps.dailyMax, ctx)} stops execution after ${n} transfer${n === 1 ? "" : "s"} (${fmt(each * BigInt(n), ctx)}).`); - verdict = "blocked"; - } - if (envelope !== 0n && total > envelope) { - const n = Number(envelope / each); - detail.push(`Loss envelope ${fmt(envelope, ctx)} trips the circuit breaker after ${n} transfer${n === 1 ? "" : "s"}; the vault enters Lockdown.`); - verdict = "blocked"; - } - if (caps.dailyMax !== 0n && (each * 2n) > caps.dailyMax / 2n) { - detail.push("Once half the daily cap is spent, every further payment is scored HIGH."); - if (verdict === "allowed") verdict = "escalated"; - } - if (verdict === "allowed") detail.push("No cumulative cap is configured, so the split drain would succeed. Set a daily cap or a loss envelope."); - out.push({ - id: "split-drain", - title: "Drain split into twenty transfers", - narrative: `Twenty payments of ${fmt(each, ctx)}, ${fmt(total, ctx)} in total, inside one day.`, - verdict, - detail, - }); - } - // 4. Attacker weakens policy then withdraws - { - const detail: string[] = []; - detail.push(`Lowering any threshold, delay or cap is a security-reducing change: it waits ${fmtDuration(ctx.policy.policyChangeDelay)} and any guardian can veto it.`); - detail.push(`It needs ${ctx.policy.governanceThreshold} owner approval${ctx.policy.governanceThreshold === 1 ? "" : "s"}, and cannot be created at all while the vault is in Lockdown.`); - detail.push("Until it activates, in-flight proposals keep their pinned requirements."); - const verdict: Verdict = ctx.policy.policyChangeDelay > 0 ? "delayed" : "allowed"; - if (verdict === "allowed") detail.push("The policy-change delay is 0, so a compromised owner quorum could loosen the policy at once. Set a delay."); - out.push({ - id: "policy-weaken", - title: "Attacker weakens the policy, then withdraws", - narrative: "A compromised owner quorum lowers approvals and caps before trying to drain the vault.", - verdict, - detail, - }); - } - // 5. Guardian compromised - { - out.push({ - id: "guardian-compromised", - title: "Guardian key compromised", - narrative: "An attacker holding a guardian key tries to withdraw.", - verdict: "impossible", - detail: [ - "Guardians hold no treasury role: they cannot propose, approve or execute payments, and the contract refuses to give them treasury roles.", - `The worst a guardian can do is freeze the vault or propose a recovery that waits ${fmtDuration(ctx.policy.recoveryDelay)} and any owner can cancel.`, - ], - }); - } - // 6. Elevated posture check: routine payment during an incident - { - const amount = ctx.limits.lowMax; - const r = single(ctx, amount, Trust.VERIFIED, false, Mode.ELEVATED, 2); - out.push({ - id: "elevated-routine", - title: "Routine payment during an incident", - narrative: `The vault is in Elevated mode and a ${fmt(amount, ctx)} routine payment is proposed.`, - verdict: r.verdict, - detail: r.detail, - }); - } - return out; -} diff --git a/web/src/lib/templates.ts b/web/src/lib/templates.ts deleted file mode 100644 index 266b284..0000000 --- a/web/src/lib/templates.ts +++ /dev/null @@ -1,178 +0,0 @@ -import type { AssetLimits, Policy } from "./types"; - -/** Mirror of contracts/script/SkurTemplates.sol. Amounts assume a 6-decimal stablecoin and 18-decimal KASH. */ -const H = 3600; -const D = 86400; -export const USD = 1_000_000n; -export const KASH = 10n ** 18n; - -export type TemplateId = "startup" | "sme" | "dao" | "fund" | "nonprofit" | "familyOffice"; - -export type Template = { - id: TemplateId; - name: string; - tagline: string; - minSigners: number; - minGuardians: number; - policy: Policy; - stable: AssetLimits; - native: AssetLimits; -}; - -const startupPolicy: Policy = { - approvalsLow: 1, - approvalsHigh: 2, - approvalsCritical: 2, - governanceThreshold: 2, - guardianThreshold: 1, - guardianRequiredCritical: true, - delayHigh: 6 * H, - delayCritical: 24 * H, - recipientActivationDelay: 24 * H, - policyChangeDelay: 24 * H, - recoveryDelay: 48 * H, - proposalTtl: 7 * D, - highExposureBps: 500, - criticalExposureBps: 2000, - hardBlockExposureBps: 5000, - envelopeBps: 1000, - envelopeWindow: 24 * H, -}; - -const startupStable: AssetLimits = { - approved: true, - lowMax: 5_000n * USD, - highMax: 25_000n * USD, - perTxMax: 100_000n * USD, - dailyMax: 100_000n * USD, -}; - -const startupNative: AssetLimits = { - approved: true, - lowMax: 100n * KASH, - highMax: 1_000n * KASH, - perTxMax: 0n, - dailyMax: 5_000n * KASH, -}; - -export const TEMPLATES: Template[] = [ - { - id: "startup", - name: "Startup", - tagline: "Simple thresholds, new-recipient delay, one guardian.", - minSigners: 2, - minGuardians: 1, - policy: startupPolicy, - stable: startupStable, - native: startupNative, - }, - { - id: "sme", - name: "SME", - tagline: "Tiered approvals, velocity limits, supplier trust.", - minSigners: 3, - minGuardians: 1, - policy: { ...startupPolicy, approvalsCritical: 3, delayHigh: 4 * H, envelopeBps: 500 }, - stable: { ...startupStable, lowMax: 10_000n * USD, highMax: 50_000n * USD, perTxMax: 150_000n * USD, dailyMax: 150_000n * USD }, - native: startupNative, - }, - { - id: "dao", - name: "Protocol / DAO", - tagline: "Higher quorum, contract-call controls, timelocks.", - minSigners: 4, - minGuardians: 2, - policy: { - ...startupPolicy, - approvalsLow: 2, - approvalsHigh: 3, - approvalsCritical: 4, - governanceThreshold: 3, - guardianThreshold: 2, - delayHigh: 24 * H, - delayCritical: 72 * H, - policyChangeDelay: 72 * H, - recoveryDelay: 7 * D, - proposalTtl: 14 * D, - highExposureBps: 200, - criticalExposureBps: 1000, - hardBlockExposureBps: 2500, - envelopeBps: 500, - }, - stable: { ...startupStable, lowMax: 25_000n * USD, highMax: 100_000n * USD, perTxMax: 500_000n * USD, dailyMax: 500_000n * USD }, - native: { ...startupNative, lowMax: 1_000n * KASH, highMax: 10_000n * KASH, dailyMax: 50_000n * KASH }, - }, - { - id: "fund", - name: "Fund", - tagline: "Exposure limits, strong guardians, long critical delays.", - minSigners: 3, - minGuardians: 2, - policy: { - ...startupPolicy, - approvalsLow: 2, - approvalsHigh: 3, - approvalsCritical: 3, - guardianThreshold: 2, - delayHigh: 12 * H, - delayCritical: 72 * H, - recipientActivationDelay: 48 * H, - policyChangeDelay: 72 * H, - recoveryDelay: 7 * D, - proposalTtl: 14 * D, - highExposureBps: 100, - criticalExposureBps: 500, - hardBlockExposureBps: 2000, - envelopeBps: 300, - }, - stable: { ...startupStable, lowMax: 50_000n * USD, highMax: 250_000n * USD, perTxMax: 1_000_000n * USD, dailyMax: 1_000_000n * USD }, - native: { ...startupNative, lowMax: 1_000n * KASH, highMax: 10_000n * KASH, dailyMax: 50_000n * KASH }, - }, - { - id: "nonprofit", - name: "Nonprofit", - tagline: "Restricted recipients, approval separation, auditability.", - minSigners: 3, - minGuardians: 1, - policy: { - ...startupPolicy, - approvalsLow: 2, - approvalsHigh: 2, - approvalsCritical: 3, - recipientActivationDelay: 72 * H, - delayHigh: 12 * H, - delayCritical: 48 * H, - envelopeBps: 500, - }, - stable: startupStable, - native: startupNative, - }, - { - id: "familyOffice", - name: "Family Office", - tagline: "Conservative limits, cold guardian, recovery policy.", - minSigners: 2, - minGuardians: 1, - policy: { - ...startupPolicy, - delayHigh: 24 * H, - delayCritical: 72 * H, - recipientActivationDelay: 72 * H, - policyChangeDelay: 72 * H, - recoveryDelay: 14 * D, - proposalTtl: 21 * D, - highExposureBps: 100, - criticalExposureBps: 500, - hardBlockExposureBps: 1500, - envelopeBps: 200, - }, - stable: { ...startupStable, lowMax: 10_000n * USD, highMax: 50_000n * USD, perTxMax: 250_000n * USD, dailyMax: 250_000n * USD }, - native: startupNative, - }, -]; - -export function templateById(id: TemplateId): Template { - const t = TEMPLATES.find((x) => x.id === id); - if (!t) throw new Error(`unknown template ${id}`); - return t; -} diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts deleted file mode 100644 index 93a6ff8..0000000 --- a/web/src/lib/types.ts +++ /dev/null @@ -1,193 +0,0 @@ -/** Mirrors contracts/src/SkurTypes.sol. Keep in sync; the contract is authoritative. */ - -export const ROLE_OWNER = 1; -export const ROLE_APPROVER = 2; -export const ROLE_EXECUTOR = 4; -export const ROLE_GUARDIAN = 8; -export const ROLE_TREASURY_MASK = ROLE_OWNER | ROLE_APPROVER | ROLE_EXECUTOR; - -export const BPS = 10_000n; - -export const REASON_AMOUNT_HIGH = 1 << 0; -export const REASON_AMOUNT_CRITICAL = 1 << 1; -export const REASON_EXPOSURE_HIGH = 1 << 2; -export const REASON_EXPOSURE_CRITICAL = 1 << 3; -export const REASON_RECIPIENT_PROBATION = 1 << 4; -export const REASON_RECIPIENT_RESTRICTED = 1 << 5; -export const REASON_MODE_ELEVATED = 1 << 6; -export const REASON_VELOCITY_PRESSURE = 1 << 7; -export const REASON_RECIPIENT_UNKNOWN = 1 << 8; - -export enum Mode { - NORMAL = 0, - ELEVATED = 1, - LOCKDOWN = 2, -} - -export enum Tier { - LOW = 0, - HIGH = 1, - CRITICAL = 2, -} - -export enum Trust { - UNKNOWN = 0, - NEW = 1, - VERIFIED = 2, - TRUSTED = 3, - RESTRICTED = 4, - BLOCKED = 5, -} - -export enum Kind { - TRANSFER = 0, - POLICY_UPDATE = 1, - ASSET_LIMITS = 2, - MEMBER_SET = 3, - RECIPIENT_TRUST = 4, - MODE_RELAX = 5, - RECOVERY = 6, -} - -export enum Status { - NONE = 0, - PENDING = 1, - EXECUTED = 2, - CANCELLED = 3, - VETOED = 4, -} - -export type Policy = { - approvalsLow: number; - approvalsHigh: number; - approvalsCritical: number; - governanceThreshold: number; - guardianThreshold: number; - guardianRequiredCritical: boolean; - delayHigh: number; - delayCritical: number; - recipientActivationDelay: number; - policyChangeDelay: number; - recoveryDelay: number; - proposalTtl: number; - highExposureBps: number; - criticalExposureBps: number; - hardBlockExposureBps: number; - envelopeBps: number; - envelopeWindow: number; -}; - -export type AssetLimits = { - approved: boolean; - lowMax: bigint; - highMax: bigint; - perTxMax: bigint; - dailyMax: bigint; -}; - -export type Recipient = { - trust: Trust; - registeredAt: bigint; - activatesAt: bigint; - paymentCount: bigint; - totalPaid: bigint; -}; - -export type Proposal = { - kind: Kind; - status: Status; - tier: Tier; - requiredApprovals: number; - requiredGuardians: number; - securityReducing: boolean; - riskReasons: number; - policyVersion: number; - proposer: `0x${string}`; - asset: `0x${string}`; - target: `0x${string}`; - amount: bigint; - createdAt: bigint; - executableAfter: bigint; - expiresAt: bigint; - data: `0x${string}`; -}; - -export type AssetMeta = { - address: `0x${string}`; - symbol: string; - decimals: number; - limits: AssetLimits; - balance: bigint; -}; - -export const MODE_LABEL: Record = { - [Mode.NORMAL]: "Normal", - [Mode.ELEVATED]: "Elevated", - [Mode.LOCKDOWN]: "Lockdown", -}; - -export const TIER_LABEL: Record = { - [Tier.LOW]: "Low", - [Tier.HIGH]: "High", - [Tier.CRITICAL]: "Critical", -}; - -export const TRUST_LABEL: Record = { - [Trust.UNKNOWN]: "Unknown", - [Trust.NEW]: "New", - [Trust.VERIFIED]: "Verified", - [Trust.TRUSTED]: "Trusted", - [Trust.RESTRICTED]: "Restricted", - [Trust.BLOCKED]: "Blocked", -}; - -export const KIND_LABEL: Record = { - [Kind.TRANSFER]: "Transfer", - [Kind.POLICY_UPDATE]: "Policy update", - [Kind.ASSET_LIMITS]: "Asset limits", - [Kind.MEMBER_SET]: "Membership change", - [Kind.RECIPIENT_TRUST]: "Recipient trust", - [Kind.MODE_RELAX]: "Lower security mode", - [Kind.RECOVERY]: "Signer recovery", -}; - -export const STATUS_LABEL: Record = { - [Status.NONE]: "None", - [Status.PENDING]: "Pending", - [Status.EXECUTED]: "Executed", - [Status.CANCELLED]: "Cancelled", - [Status.VETOED]: "Vetoed", -}; - -export function roleNames(bits: number): string[] { - const out: string[] = []; - if (bits & ROLE_OWNER) out.push("Owner"); - if (bits & ROLE_APPROVER) out.push("Approver"); - if (bits & ROLE_EXECUTOR) out.push("Executor"); - if (bits & ROLE_GUARDIAN) out.push("Guardian"); - return out; -} - -export const REASON_TEXT: Array<[number, string]> = [ - [REASON_AMOUNT_CRITICAL, "the amount is above the critical threshold for this asset"], - [REASON_AMOUNT_HIGH, "the amount is above the routine threshold for this asset"], - [REASON_EXPOSURE_CRITICAL, "it would move a critical share of the vault's holdings"], - [REASON_EXPOSURE_HIGH, "it would move a significant share of the vault's holdings"], - [REASON_RECIPIENT_RESTRICTED, "the recipient is restricted by policy"], - [REASON_RECIPIENT_UNKNOWN, "this vault has never paid the recipient before"], - [REASON_RECIPIENT_PROBATION, "the recipient is still within its activation delay"], - [REASON_VELOCITY_PRESSURE, "more than half of today's daily cap would be used"], - [REASON_MODE_ELEVATED, "the vault is in Elevated mode"], -]; - -export function explainReasons(mask: number): string[] { - const seen = new Set(); - const out: string[] = []; - for (const [bit, text] of REASON_TEXT) { - if (mask & bit && !seen.has(text)) { - seen.add(text); - out.push(text); - } - } - return out; -} diff --git a/web/src/main.tsx b/web/src/main.tsx deleted file mode 100644 index 1497151..0000000 --- a/web/src/main.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { StrictMode } from "react"; -import { createRoot } from "react-dom/client"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { WagmiProvider } from "wagmi"; -import App from "./App"; -import { wagmiConfig } from "./wagmi"; -import "./styles.css"; - -const queryClient = new QueryClient(); -if (import.meta.env.DEV) (window as unknown as { __qc: QueryClient }).__qc = queryClient; - -createRoot(document.getElementById("root")!).render( - - - - - - - , -); diff --git a/web/src/pages/AddressBook.tsx b/web/src/pages/AddressBook.tsx deleted file mode 100644 index 590bba0..0000000 --- a/web/src/pages/AddressBook.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { useState } from "react"; -import { useAccount } from "wagmi"; -import { isAddress } from "viem"; -import { SkurVaultAbi } from "../abi/SkurVault"; -import { Address, Button, Empty, Field, Modal, TrustBadge, TxStatus } from "../components/ui"; -import { Identicon } from "../components/Identicon"; -import { IconBook, IconPlus } from "../components/icons"; -import { useTx } from "../hooks/useTx"; -import type { VaultData } from "../hooks/useVault"; -import { fmtDate, fmtDuration, fmtRelative, nowSec } from "../lib/format"; -import { ROLE_APPROVER, ROLE_OWNER, Trust, TRUST_LABEL } from "../lib/types"; - -/** app.safe.global/address-book: "New entry" action row, then the table. Entries here are onchain recipient records. */ -export function AddressBook({ vault, onChanged }: { vault: VaultData; onChanged: () => void }) { - const { address } = useAccount(); - const me = vault.members.find((m) => m.address.toLowerCase() === address?.toLowerCase()); - const roles = me?.roles ?? 0; - const tx = useTx(() => { onChanged(); setDialog(null); }); - const [dialog, setDialog] = useState(null); - const [target, setTarget] = useState(""); - const [trust, setTrust] = useState(Trust.VERIFIED); - const now = nowSec(); - - const openTrust = (addr: string, current: Trust) => { - setTarget(addr); - setTrust(current === Trust.UNKNOWN || current === Trust.NEW ? Trust.VERIFIED : current); - setDialog("trust"); - }; - - return ( - <> -
- - - A new recipient waits {fmtDuration(vault.policy.recipientActivationDelay)} before normal policy applies to it. -
- -
- {vault.activityLoading ? ( - Reading recipients from the chain… - ) : vault.recipients.length === 0 ? ( - }>No recipients yet. Register one to start its activation clock. - ) : ( -
- - - - - {vault.recipients.map((r) => { - const probation = r.trust === Trust.NEW && Number(r.activatesAt) > now; - return ( - - - - - - - - - ); - })} - -
RecipientTrustActivationPaymentsRegistered
{r.trust === Trust.NEW ? (probation ? `activates ${fmtRelative(r.activatesAt)}` : "activated") : "—"}{String(r.paymentCount)}{fmtDate(r.registeredAt)}
- )} -
- - {dialog === "register" && ( - setDialog(null)} footer={<>}> - - setTarget(e.target.value.trim())} placeholder="0x…" autoFocus /> - - - - )} - {dialog === "trust" && ( - setDialog(null)} footer={<>}> - setTarget(e.target.value.trim())} placeholder="0x…" /> - - - - - - )} - - ); -} diff --git a/web/src/pages/Assets.tsx b/web/src/pages/Assets.tsx deleted file mode 100644 index a688fd5..0000000 --- a/web/src/pages/Assets.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { useState } from "react"; -import { NATIVE_ASSET } from "../config/chain"; -import type { VaultData } from "../hooks/useVault"; -import { fmtAmount, fmtRelative } from "../lib/format"; -import { Mode } from "../lib/types"; -import { Address, Button, TokenIcon } from "../components/ui"; -import { IconSend } from "../components/icons"; -import type { AppPage } from "../components/AppShell"; - -/** app.safe.global/balances: Tokens tab, total assets value, table with per-row actions. */ -export function Assets({ vault, onSend, onNavigate }: { vault: VaultData; onSend: (asset: `0x${string}`) => void; onNavigate: (p: AppPage) => void }) { - const [tab, setTab] = useState<"tokens" | "limits">("tokens"); - const canSend = vault.mode !== Mode.LOCKDOWN; - const stable = vault.assets.find((a) => a.address !== NATIVE_ASSET) ?? vault.assets[0]; - - return ( - <> -
- - -
- -
-
-
Total assets value
-
{stable ? fmtAmount(stable.balance, stable.decimals, stable.symbol) : "—"}
-
-
- Read from the chain {fmtRelative(Math.floor(vault.fetchedAt / 1000))} - -
-
- -
- {tab === "tokens" ? ( - - - - - - - - - - - - {vault.assets.map((a) => { - const v = vault.velocity[a.address.toLowerCase()]; - return ( - - - - - - - - ); - })} - -
AssetBalanceShare of holdingsToday's outflowActions
-
- -
-
{a.symbol === "KASH" ? "KASH" : a.symbol === "sUSD" ? "Skur Test USD" : a.symbol}
-
{a.address === NATIVE_ASSET ? "native token" :
}
-
-
-
{fmtAmount(a.balance, a.decimals)} {a.symbol}{a.limits.approved ? "approved" : "not approved"}{v && v.dailyMax ? `${fmtAmount(v.daySpent, a.decimals)} / ${fmtAmount(v.dailyMax, a.decimals)}` : v ? fmtAmount(v.daySpent, a.decimals) : "—"} - -
- ) : ( - - - - - - - - - - - - - {vault.assets.map((a) => { - const v = vault.velocity[a.address.toLowerCase()]; - return ( - - - - - - - - - ); - })} - -
AssetRoutine up toHigh up toPer transactionDaily capLoss envelope
{a.symbol}
{fmtAmount(a.limits.lowMax, a.decimals)}{fmtAmount(a.limits.highMax, a.decimals)}{a.limits.perTxMax ? fmtAmount(a.limits.perTxMax, a.decimals) : "no cap"}{a.limits.dailyMax ? fmtAmount(a.limits.dailyMax, a.decimals) : "no cap"}{v && v.envelope ? `${fmtAmount(v.envelopeSpent, a.decimals)} / ${fmtAmount(v.envelope, a.decimals)}${v.envelopeResetsAt > 0n ? ` · resets ${fmtRelative(v.envelopeResetsAt)}` : ""}` : "off"}
- )} -
- Payments above the routine threshold need more approvals. Above the high threshold they also need a guardian and a delay. Exposure is measured per asset; V1 has no price oracle. -
-
- - ); -} diff --git a/web/src/pages/CreateVault.tsx b/web/src/pages/CreateVault.tsx deleted file mode 100644 index 81f3e72..0000000 --- a/web/src/pages/CreateVault.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import { useMemo, useState } from "react"; -import { useAccount, useConnect, useDisconnect, usePublicClient } from "wagmi"; -import { isAddress, keccak256, stringToHex } from "viem"; -import { SkurFactoryAbi } from "../abi/SkurFactory"; -import { Button, Field, KV, TxStatus } from "../components/ui"; -import { Identicon } from "../components/Identicon"; -import { NetworkGuard } from "../components/NetworkGuard"; -import { IconWallet } from "../components/icons"; -import { useTx } from "../hooks/useTx"; -import { DEPLOYMENTS, NATIVE_ASSET } from "../config/chain"; -import { fmtAmount, fmtDuration } from "../lib/format"; -import { policyToContract, validateCounts, validatePolicy } from "../lib/policy"; -import { TEMPLATES, type TemplateId } from "../lib/templates"; -import { ROLE_APPROVER, ROLE_EXECUTOR, ROLE_GUARDIAN, ROLE_OWNER } from "../lib/types"; -import { useVaultLabel } from "../state/vaultLabel"; - -type MemberRow = { address: string; roles: number }; - -/** app.safe.global/new-safe/create: numbered step card on the left, account preview on the right. */ -export function CreateVault({ onCreated, onCancel }: { onCreated: (a: `0x${string}`) => void; onCancel: () => void }) { - const { address, isConnected } = useAccount(); - const { connectors, connect, isPending } = useConnect(); - const { disconnect } = useDisconnect(); - const client = usePublicClient(); - const [step, setStep] = useState<1 | 2 | 3>(1); - const [name, setName] = useState(""); - const [templateId, setTemplateId] = useState("startup"); - const template = TEMPLATES.find((t) => t.id === templateId)!; - const [members, setMembers] = useState([{ address: address ?? "", roles: ROLE_OWNER | ROLE_APPROVER | ROLE_EXECUTOR }, { address: "", roles: ROLE_OWNER | ROLE_APPROVER }, { address: "", roles: ROLE_GUARDIAN }]); - const salt = useMemo(() => `vault-${Date.now()}`, []); - const predicted = useMemo(() => (address ? keccak256(stringToHex(salt)) : null), [address, salt]); - const [, setLabel] = useVaultLabel(null); - const tx = useTx(async () => { - if (!client || !address || !predicted) return; - const v = await client.readContract({ address: DEPLOYMENTS.factory, abi: SkurFactoryAbi, functionName: "predictVaultAddress", args: [address, predicted] }); - try { - const labels = JSON.parse(window.localStorage.getItem("skur.vaultLabels") ?? "{}"); - labels[v.toLowerCase()] = name || "Treasury vault"; - window.localStorage.setItem("skur.vaultLabels", JSON.stringify(labels)); - } catch { - /* ignore */ - } - setLabel(name); - onCreated(v); - }); - - const counts = members.reduce( - (a, m) => ({ owners: a.owners + (m.roles & 1 ? 1 : 0), approvers: a.approvers + (m.roles & 2 ? 1 : 0), executors: a.executors + (m.roles & 4 ? 1 : 0), guardians: a.guardians + (m.roles & 8 ? 1 : 0) }), - { owners: 0, approvers: 0, executors: 0, guardians: 0 }, - ); - const errors = useMemo(() => { - const e = [...validatePolicy(template.policy), ...validateCounts(template.policy, counts.owners, counts.approvers, counts.executors, counts.guardians)]; - const addrs = members.map((m) => m.address.toLowerCase()); - if (members.some((m) => !isAddress(m.address))) e.push("Every member needs a valid address."); - if (new Set(addrs).size !== addrs.length) e.push("Duplicate member address."); - if (members.some((m) => m.roles === 0)) e.push("Every member needs at least one role."); - return e; - }, [template, counts, members]); - - const setRow = (i: number, patch: Partial) => setMembers((ms) => ms.map((m, j) => (j === i ? { ...m, ...patch } : m))); - const toggleRole = (i: number, b: number) => - setRow(i, { roles: b === ROLE_GUARDIAN ? (members[i].roles & ROLE_GUARDIAN ? 0 : ROLE_GUARDIAN) : ((members[i].roles & b ? members[i].roles & ~b : members[i].roles | b) & ~ROLE_GUARDIAN) }); - - const create = () => { - if (!predicted) return; - tx.send({ - address: DEPLOYMENTS.factory, - abi: SkurFactoryAbi, - functionName: "createVault", - args: [predicted, members.map((m) => m.address as `0x${string}`), members.map((m) => m.roles), policyToContract(template.policy), [DEPLOYMENTS.testUsd, NATIVE_ASSET], [template.stable, template.native]], - }); - }; - - const titles = ["Name and template", "Signers and guardians", "Review and sign"]; - const descs = ["Name the vault and choose the policy it starts with. Every number can be changed later through governance.", "Owners govern, approvers confirm payments, executors execute. Guardians can only freeze, veto and recover, and the contract will not let them hold a treasury role.", "This is the policy the vault will enforce from its first block. Check it, then sign once."]; - - return ( -
-
- -
- - {isConnected && address ? ( - - ) : ( - connectors.slice(0, 1).map((c) => ) - )} -
-
-
-

Create new Skur vault

-
-
-
-
- {step} -
-

{titles[step - 1]}

-

{descs[step - 1]}

-
-
-
- {step === 1 && ( - <> - - setName(e.target.value)} placeholder="Main treasury" maxLength={40} /> - -
Policy template
-

Pick the profile closest to your organisation. Every value can be tuned under Policies afterwards.

-
- {TEMPLATES.map((t) => ( - - ))} -
- {!isConnected && ( -
-
-
No wallet connected
-
Creating a vault is a transaction your wallet signs.
-
- {connectors.slice(0, 1).map((c) => )} -
-
-
- )} - - )} - {step === 2 && ( - <> - - - - - - {members.map((m, i) => ( - - - {[ROLE_OWNER, ROLE_APPROVER, ROLE_EXECUTOR, ROLE_GUARDIAN].map((b) => ( - - ))} - - - ))} - -
MemberOwnerApproverExecutorGuardian
-
- {isAddress(m.address) ? : ?} - setRow(i, { address: e.target.value.trim() })} placeholder="0x…" style={{ height: 40 }} /> -
-
toggleRole(i, b)} />
-
- -
-

- {counts.owners} owners · {counts.approvers} approvers · {counts.executors} executors · {counts.guardians} guardians. The {template.name} template needs {template.minSigners}+ signers and {template.minGuardians}+ guardian{template.minGuardians === 1 ? "" : "s"}. -

- {errors.length > 0 &&
{errors.map((e) =>
{e}
)}
} - - )} - {step === 3 && ( - <> - -

The vault is a minimal proxy over the verified implementation. The factory that creates it keeps no authority over it; only the members listed above can act.

- - - )} -
-
- - {step < 3 ? ( - - ) : ( - - )} -
-
- - -
-
-
- ); -} diff --git a/web/src/pages/Landing.tsx b/web/src/pages/Landing.tsx deleted file mode 100644 index 613d09c..0000000 --- a/web/src/pages/Landing.tsx +++ /dev/null @@ -1,279 +0,0 @@ -import { useEffect } from "react"; -import { MarketingShell } from "../components/MarketingShell"; -import { AppMockup } from "../components/Mockup"; -import { ScoreTicker } from "../components/Ticker"; -import { Reveal } from "../components/Reveal"; -import { IconArrowRight, IconCheck, IconChevronRight } from "../components/icons"; -import { useCountUp } from "../lib/motion"; - -function Stat({ n, label }: { n: number; label: string }) { - const { ref, value } = useCountUp(n, 1400); - return ( -
- {value} -
{label}
-
- ); -} - -const FAQ: Array<[string, string]> = [ - ["What is Skur?", "A self-custodial treasury vault whose security requirements rise and fall with the risk of each transaction. It keeps multisignature approval and adds onchain rules about how much can move, where it can go and how long risky payments wait."], - ["How is that different from a multisig?", "A multisig asks one question: did enough people sign? Skur asks a second one: should this transaction be allowed right now? Amount tiers, recipient trust, daily caps, a loss envelope, guardian veto and a firewall around policy changes all sit between a valid approval and the money."], - ["Does Skur ever hold my funds or keys?", "No. The vault contract holds the assets and your wallets hold the keys. No Skur server is in the execution path. If this website disappears, the vault and its policy are still there and still reachable through the verified contracts."], - ["What happens if one of our signers is compromised?", "Alone, a stolen key cannot reach any tier's threshold unless you set that tier to one. Even then it is bounded by the routine cap, the daily cap and the loss envelope. If several keys are stolen, the larger payments still wait out a delay any guardian can use to veto, and the envelope freezes the vault on the first over-limit attempt."], - ["What can a guardian do?", "Freeze the vault, veto critical or security-reducing proposals, confirm the largest transfers and propose replacing a lost signer after a delay. Guardians hold no treasury role, so a stolen guardian key cannot move a single token."], - ["Which network does it run on?", "Skur V1 runs on the Ark Constellation devnet, chain 9000. Every endpoint lives in one configuration module, so testnet and mainnet are a configuration change rather than a rewrite."], - ["Has it been audited?", "Not yet. The contracts are verified on the explorer, covered by 95 tests including stateful invariant tests, and clean under static analysis. Do not put real value behind Skur before an independent audit."], -]; - -const HERO_CELLS: Array<[number, number]> = [[0, 130], [130, 260], [1180, 0], [1310, 0], [1310, 130], [910, 390], [1050, 260], [520, 0]]; - -/** Public landing page. */ -export function Landing({ onLaunch }: { onLaunch: () => void }) { - useEffect(() => { - const m = window.location.hash.match(/#\/#(.+)$/); - if (m) setTimeout(() => document.getElementById(m[1])?.scrollIntoView({ behavior: "smooth" }), 50); - }, []); - const words = "Treasury security that".split(" "); - - return ( - -
- -
-
- {HERO_CELLS.map(([x, y], i) => )} -
-
12 invariants enforced by the vault, not by a server
-

- {words.map((w, i) => ( - {w}{" "} - ))} - - {["assumes", "compromise"].map((w, i) => ( - {w}{i === 0 ? " " : ""} - ))} - -

-

An approval proves someone was authorised. Skur decides whether the payment is safe: how much can move, where it can go, and how long the risky ones wait, even when your signers are compromised.

-
- - See how it works -
-
Self-custodialArk Constellation devnet · 9000Verified source
-
-
-
-
-
-
-
- -
-
- -

Made for teams that hold real money onchain

-

Startups, DAOs, funds, nonprofits and family offices that need more than a signature count between a stolen key and the treasury.

-
-
-
- {[...Array(2)].flatMap((_, k) => ["Startups", "Protocols & DAOs", "Venture funds", "Nonprofits", "Family offices", "Finance teams", "Onchain service providers", "Grant programmes"].map((t) => ( - {t} - )))} -
-
-
-
- -
-
- -
01 / Controls
-

Your treasury, under control

-

Every rule below is contract code, not a setting on our server. If Skur disappeared tomorrow, your vault and its policy would not.

-
- -
-

Risk-tiered approvals

-

The vault scores every payment from its amount, its share of holdings, who receives it, what already left today and the current security mode. The score sets how many approvals it needs, whether a guardian must sign, and how long it waits.

-
-
- {["1,000 sUSD to a known supplier", "12,000 sUSD to a new address", "60,000 sUSD, 24% of holdings"].map((t) => {t})} -
-
S
-
- Low · 1 approval · executes now - High · 2 approvals · waits 1 day - Critical · 2 + a guardian · 1 day · vetoable -
-
-
-
-

Recipient trust

-

Recipients are security objects, not strings. A never-seen address serves an activation delay and is treated as higher risk until it has. Owners can verify, trust, restrict or block anyone.

-
- {[["New", "activates in 23h", "#1b3a5c", "#cfe2ff", "0x7c4f…41e2"], ["Verified", "normal policy", "#1b4332", "#d1e7dd", "0x3a91…c0de"], ["Restricted", "always critical", "#4d3a05", "#fff3cd", "0xe2b7…9f10"], ["Blocked", "refused", "#4a1c22", "#f8d7da", "0x0d55…77aa"]].map(([s, d, bg, fg, addr]) => ( -
{addr}{s} · {d}
- ))} -
-
-
-

Velocity limits and a circuit breaker

-

Outflow is counted cumulatively, so a drain split into twenty small transfers hits the same wall as one large one. Cross the loss envelope and the vault freezes instead of paying.

-
-
Left the vault today98,000 of 100,000 sUSD
-
-
Loss envelope, 24 hours25,000 sUSD, then Lockdown
-
-
-
-
-

Independent guardians

-

A second control plane that can freeze, veto, confirm and recover, and that can never withdraw. Stealing one role is not enough.

-
-
Treasury signers
Propose · Approve · Execute
-
Guardians
Freeze · Veto · Recover
never withdraw
-
-
-
-

Policy simulator

-

Run your policy against the attacks that actually happen, before you activate it. Verdicts come from the same rules the contract enforces, so the simulator cannot promise what the vault will refuse.

-
-
- -
-
- -
-
- -
02 / Guarantees
-

Security a finance team can read

-

Every control fits in one sentence, and every one is checked at execution, not only at approval.

-
- -

No single point of failure

Thresholds scale with risk, and guardians hold no treasury role. One stolen key, or one compromised role, cannot empty the vault.

-

Enforced onchain

Limits, delays, vetoes and modes live in the vault contract. Requirements are pinned when a proposal is created and checked again when it executes.

-

Open source and verified

Source is verified on the Ark explorer and the repository builds to byte-identical bytecode. Twelve invariants are proven with fuzz and stateful tests.

-

Recovery without a backdoor

Guardians can replace a lost signer after a delay any owner can cancel. Recovery never touches the policy and leaves the vault in Elevated mode.

-
- - Verified with - Foundry fuzz and invariant tests - Slither - Blockscout source verification - OpenZeppelin 5 - -
-
- -
-
- -
-
03 / Policy firewall
-

A firewall around the policy itself

-

Changing the rules is as dangerous as moving the money. An attacker who can lower a threshold never needs to touch the withdrawal path.

-
    -
  • Loosening any control waits; it never applies instantly
  • -
  • Any guardian can veto the change
  • -
  • Lockdown refuses it outright
  • -
-
-
-
-
-
- -
-
-
04 / Compared

Not every multisig is the same

- - - - - - - - - - - - -
SkurPlain multisigCustodial policy engine
AuthorizationThreshold follows transaction riskFixed N-of-MConfigurable, off-chain
Enough signers compromisedCaps, delays, guardian veto and a circuit breakerFunds goneDepends on the provider
Policy tamperingDelayed, vetoable, blocked in LockdownImmediateProvider-controlled
CustodySelf-custodial, no server in the pathSelf-custodialProvider holds keys or approvals
TransparencyEvery rule and every version onchainOnchainLimited
-
-
-
- -
- -
"Control not only who can move treasury funds, but how much can move, where it can go, and what happens when risk changes."
- The Skur thesis -
- - - -
-
-
- -
-
- -
05 / Who it is for
-

Two roles. Two kinds of power.

-

Finance teams move the money. Guardians hold the brake. Neither can do the other's job.

-
- -
-

For finance teams

-

Tiered approvals, trusted recipients, daily caps and a plain-language review before every signature.

- How teams use Skur -
-
-
-

For guardians

-

Freeze the vault, veto the risky and the security-reducing, confirm the largest transfers, recover a lost signer. Never touch a coin.

- What a guardian can do -
-
-
-
-
- -
-
- -
06 / Lifecycle
-

How a payment moves

-

Requirements are fixed at step one. Limits are checked again at step four.

-
- -
1

Proposed and scored

The vault reads the amount, its share of holdings, the recipient's trust, today's outflow and the security mode, then pins the tier and its requirements to the proposal.

-
2

Reviewed in plain language

Approvers see what leaves, what remains, who receives it and why the tier was assigned, before their wallet opens.

-
3

Confirmed, or vetoed

Approvals are recounted against live membership. Any guardian can veto a critical, probationary or security-reducing proposal at any time.

-
4

Executed only if still safe

Caps, exposure, recipient state and the loss envelope are checked again at execution. Over the envelope, the vault freezes instead of paying.

-
-
-
- -
-
-
07 / Questions

Questions, answered plainly

- -
- {FAQ.map(([q, a]) => ( -
- {q}+ -
{a}
-
- ))} -
-
-
-
- - ); -} diff --git a/web/src/pages/Members.tsx b/web/src/pages/Members.tsx deleted file mode 100644 index cc48f37..0000000 --- a/web/src/pages/Members.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { useState } from "react"; -import { useAccount } from "wagmi"; -import { isAddress } from "viem"; -import { SkurVaultAbi } from "../abi/SkurVault"; -import { Badge, Button, CopyButton, Field, Modal, Section, TxStatus } from "../components/ui"; -import { Identicon } from "../components/Identicon"; -import { IconPlus } from "../components/icons"; -import { useTx } from "../hooks/useTx"; -import type { VaultData } from "../hooks/useVault"; -import { fmtDuration } from "../lib/format"; -import { ROLE_APPROVER, ROLE_EXECUTOR, ROLE_GUARDIAN, ROLE_OWNER, roleNames } from "../lib/types"; - -/** Mirrors Safe's Settings › Setup: "Members" and "Required confirmations" as label/content sections. */ -export function Members({ vault, onChanged }: { vault: VaultData; onChanged: () => void }) { - const { address } = useAccount(); - const me = vault.members.find((m) => m.address.toLowerCase() === address?.toLowerCase()); - const isOwner = Boolean((me?.roles ?? 0) & ROLE_OWNER); - const tx = useTx(() => { onChanged(); setDialog(false); }); - const [dialog, setDialog] = useState(false); - const [target, setTarget] = useState(""); - const [bits, setBits] = useState(ROLE_APPROVER); - const signers = vault.members.filter((m) => !(m.roles & ROLE_GUARDIAN)); - const guardians = vault.members.filter((m) => m.roles & ROLE_GUARDIAN); - const existing = vault.members.find((m) => m.address.toLowerCase() === target.toLowerCase()); - const adds = existing ? bits & ~existing.roles : bits; - const reducing = adds !== 0 || Boolean(existing && existing.roles & ROLE_GUARDIAN && !(bits & ROLE_GUARDIAN)); - const toggle = (b: number) => setBits((x) => (b === ROLE_GUARDIAN ? (x & ROLE_GUARDIAN ? 0 : ROLE_GUARDIAN) : ((x & b ? x & ~b : x | b) & ~ROLE_GUARDIAN))); - const edit = (addr: string, roles: number) => { setTarget(addr); setBits(roles); setDialog(true); }; - - const Row = ({ m }: { m: { address: `0x${string}`; roles: number } }) => ( -
- - ark:{m.address} - - {roleNames(m.roles).map((r) => {r})} - - - - -
- ); - - return ( - <> -
-

Signers

-

Owners govern, approvers confirm payments, executors execute once every condition holds. A member may hold several of these roles.

-
- -
- {signers.map((m) => )} - -

Guardians

-

Guardians can freeze the vault, veto risky or security-reducing proposals, confirm critical transfers and start a recovery. The contract will not let them hold a treasury role, so they can never move funds.

- {guardians.length === 0 ?
No guardian configured. Nobody independent can veto a proposal or freeze the vault.
: guardians.map((m) => )} -
- -
-

A routine transfer requires the confirmation of:

-
{vault.policy.approvalsLow} out of {vault.counts.approvers} approvers.
-
-
High-risk transfer
{vault.policy.approvalsHigh} of {vault.counts.approvers} approvers, then {fmtDuration(vault.policy.delayHigh)}
-
Critical transfer
{vault.policy.approvalsCritical} of {vault.counts.approvers} approvers{vault.policy.guardianRequiredCritical ? ` + ${vault.policy.guardianThreshold} of ${vault.counts.guardians} guardians` : ""}, then {fmtDuration(vault.policy.delayCritical)}
-
Governance change
{vault.policy.governanceThreshold} of {vault.counts.owners} owners{" "}(+ {fmtDuration(vault.policy.policyChangeDelay)} if security-reducing)
-
Recovery
{vault.policy.guardianThreshold} of {vault.counts.guardians} guardians, then {fmtDuration(vault.policy.recoveryDelay)}; any owner can cancel
-
-

Thresholds are part of the policy. Lowering any of them waits {fmtDuration(vault.policy.policyChangeDelay)}, and any guardian can veto the change.

-
- - {dialog && ( - setDialog(false)} footer={<>}> - setTarget(e.target.value.trim())} placeholder="0x…" /> -
Roles (none = remove)
-
- {[[ROLE_OWNER, "Owner"], [ROLE_APPROVER, "Approver"], [ROLE_EXECUTOR, "Executor"], [ROLE_GUARDIAN, "Guardian"]].map(([b, label]) => ( - - ))} -
- {isAddress(target) && ( -
- {bits === 0 ? "Removing a member takes effect as soon as owners approve." : reducing ? `Adding authority or weakening the guardian layer loosens security: it waits ${fmtDuration(vault.policy.policyChangeDelay)} and any guardian can veto it.` : "Reducing a member's authority takes effect as soon as owners approve."} -
- )} - -
- )} - - ); -} diff --git a/web/src/pages/Overview.tsx b/web/src/pages/Overview.tsx deleted file mode 100644 index 51e751d..0000000 --- a/web/src/pages/Overview.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { useState } from "react"; -import { NATIVE_ASSET } from "../config/chain"; -import type { VaultData } from "../hooks/useVault"; -import { fmtAmount, fmtDuration } from "../lib/format"; -import { Mode, Status } from "../lib/types"; -import { TxRow } from "../components/TxRow"; -import { Skeleton } from "../components/Reveal"; -import { Address, Button, Card, Empty, TokenIcon } from "../components/ui"; -import { IconArrowRight, IconBank, IconClock, IconGuard, IconLab, IconReceive, IconSend, IconShield } from "../components/icons"; -import { postureItems } from "./Security"; -import type { AppPage } from "../components/AppShell"; - -function splitAmount(s: string): [string, string] { - const [int, frac] = s.split("."); - return [int, frac ? `.${frac}` : ".00"]; -} - -/** app.safe.global/home: Total balance card, promo banner, Top assets, "Explore what's possible", Pending transactions. */ -export function Overview({ vault, onChanged, onNavigate, onNewTransaction }: { vault: VaultData; onChanged: () => void; onNavigate: (p: AppPage) => void; onNewTransaction: () => void }) { - const [showReceive, setShowReceive] = useState(false); - const [bannerClosed, setBannerClosed] = useState(false); - const pending = vault.proposals.filter((p) => p.status === Status.PENDING); - const stable = vault.assets.find((a) => a.address !== NATIVE_ASSET) ?? vault.assets[0]; - const native = vault.assets.find((a) => a.address === NATIVE_ASSET); - const items = postureItems(vault); - const weakest = items.find((i) => i.ok === "bad") ?? items.find((i) => i.ok === "warn"); - const [intPart, decPart] = stable ? splitAmount(fmtAmount(stable.balance, stable.decimals, undefined, 2)) : ["0", ".00"]; - const canPropose = vault.mode !== Mode.LOCKDOWN; - - return ( -
-
- -
-
-
Total balance
-
- {intPart} - {decPart} {stable?.symbol} -
-
{native ? `+ ${fmtAmount(native.balance, native.decimals, native.symbol)}` : ""} · policy v{vault.policyVersion} · {vault.members.length} members
-
-
- - - -
-
- {showReceive && ( -
-
Send KASH or any approved token to
. Deposits are accepted in every mode, including Lockdown.
-
- )} -
- - {weakest && !bannerClosed && ( -
- -
-

{weakest.ok === "bad" ? "Strengthen your vault before funds arrive" : "Your vault is in good shape"}

-

{weakest.text}. {weakest.ok === "bad" ? "Fix it under Policies or Members." : "Everything else is confirmed; the details are under Security."}

- -
- -
- )} - - onNavigate("assets")}>View all ›}> -
    - {vault.assets.map((a) => { - const v = vault.velocity[a.address.toLowerCase()]; - return ( -
  • - -
    -
    {a.symbol === "KASH" ? "KASH" : a.symbol === "sUSD" ? "Skur Test USD" : a.symbol}
    -
    {fmtAmount(a.balance, a.decimals)} {a.symbol}
    -
    -
    -
    {fmtAmount(a.balance, a.decimals)}
    -
    {v && v.dailyMax ? `${fmtAmount(v.daySpent, a.decimals)} of ${fmtAmount(v.dailyMax, a.decimals)} today` : "no daily cap"}
    -
    -
  • - ); - })} -
-
- - -
- - - - -
-
-
- -
- 0 ? : undefined}> - {vault.activityLoading ? ( -
- - - -
- ) : pending.length === 0 ? ( - Nothing waiting for a signature - ) : ( -
- {pending.slice(0, 5).map((p) => )} -
- )} -
- onNavigate("policies")}>Edit ›}> -
-
Approvals
Low {vault.policy.approvalsLow} · High {vault.policy.approvalsHigh} · Critical {vault.policy.approvalsCritical}{vault.policy.guardianRequiredCritical ? ` + ${vault.policy.guardianThreshold} guardian` : ""}
-
Delays
High {fmtDuration(vault.policy.delayHigh)} · Critical {fmtDuration(vault.policy.delayCritical)}
-
New recipients wait
{fmtDuration(vault.policy.recipientActivationDelay)}
-
Circuit breaker
{vault.policy.envelopeBps ? `${vault.policy.envelopeBps / 100}% per ${fmtDuration(vault.policy.envelopeWindow)}` : "off"}
-
-
-
-
- ); -} diff --git a/web/src/pages/Policies.tsx b/web/src/pages/Policies.tsx deleted file mode 100644 index e8f280a..0000000 --- a/web/src/pages/Policies.tsx +++ /dev/null @@ -1,240 +0,0 @@ -import { useMemo, useRef, useState } from "react"; -import { useAccount } from "wagmi"; -import { SkurVaultAbi } from "../abi/SkurVault"; -import { Badge, Button, Section, TxLink, TxStatus } from "../components/ui"; -import { TokenIcon } from "../components/ui"; -import { useTx } from "../hooks/useTx"; -import type { VaultData } from "../hooks/useVault"; -import { fmtAmount, fmtBps, fmtDate, fmtDuration, parseAmount } from "../lib/format"; -import { limitReductions, policyReductions, policyToContract, validateCounts, validateLimits, validatePolicy } from "../lib/policy"; -import { TEMPLATES, type TemplateId } from "../lib/templates"; -import { ROLE_OWNER, type AssetLimits, type Policy } from "../lib/types"; - -const H = 3600; -type Unit = "count" | "hours" | "bps" | "bool"; -type F = { key: keyof Policy; label: string; hint: string; unit: Unit; max?: number }; - -const GROUPS: Array<{ title: string; sub: string; fields: F[] }> = [ - { - title: "Approvals", - sub: "How many confirmations each tier needs. Guardians confirm critical transfers in addition to approvers.", - fields: [ - { key: "approvalsLow", label: "Low tier", hint: "Routine payments under the routine threshold.", unit: "count" }, - { key: "approvalsHigh", label: "High tier", hint: "Larger payments, new recipients, elevated posture.", unit: "count" }, - { key: "approvalsCritical", label: "Critical tier", hint: "Very large or high-exposure payments.", unit: "count" }, - { key: "guardianRequiredCritical", label: "Guardian required for Critical", hint: "An independent sign-off on the largest transfers.", unit: "bool" }, - { key: "guardianThreshold", label: "Guardian confirmations", hint: "For critical transfers, leaving Lockdown and recovery.", unit: "count" }, - { key: "governanceThreshold", label: "Owner approvals for governance", hint: "Policy, limits, members, trust, mode changes.", unit: "count" }, - ], - }, - { - title: "Delays", - sub: "A delay gives guardians a veto window and gives the organisation time to notice.", - fields: [ - { key: "delayHigh", label: "High-tier delay", hint: "Between full approval and execution.", unit: "hours" }, - { key: "delayCritical", label: "Critical delay", hint: "Also the guardian veto window.", unit: "hours" }, - { key: "recipientActivationDelay", label: "New-recipient activation", hint: "A never-seen address cannot be paid sooner.", unit: "hours" }, - { key: "policyChangeDelay", label: "Security-reducing change delay", hint: "Any loosening waits this long and can be vetoed.", unit: "hours" }, - { key: "recoveryDelay", label: "Recovery delay", hint: "Owners can cancel a recovery during it.", unit: "hours" }, - { key: "proposalTtl", label: "Proposal lifetime", hint: "Must exceed the longest delay by at least 1h.", unit: "hours" }, - ], - }, - { - title: "Exposure", - sub: "Share of the asset's holdings that a single payment may move before it is escalated or refused.", - fields: [ - { key: "highExposureBps", label: "Escalate to High at", hint: "Percent of holdings.", unit: "bps", max: 100 }, - { key: "criticalExposureBps", label: "Escalate to Critical at", hint: "", unit: "bps", max: 100 }, - { key: "hardBlockExposureBps", label: "Refuse above", hint: "0 disables the hard block.", unit: "bps", max: 100 }, - ], - }, - { - title: "Circuit breaker", - sub: "Cumulative outflow per window as a share of holdings. The transfer that would exceed it is not paid; the vault enters Lockdown.", - fields: [ - { key: "envelopeBps", label: "Loss envelope", hint: "0 disables the breaker.", unit: "bps", max: 100 }, - { key: "envelopeWindow", label: "Envelope window", hint: "At least 1 hour.", unit: "hours" }, - ], - }, -]; - -function Control({ f, value, onChange }: { f: F; value: number | boolean; onChange: (v: number | boolean) => void }) { - if (f.unit === "bool") { - return ( - - ); - } - if (f.unit === "count") { - const n = Number(value); - return ( - - - onChange(Math.max(0, Number(e.target.value)))} /> - - - ); - } - if (f.unit === "hours") { - return ( - - onChange(Math.round(Number(e.target.value) * H))} /> - hours - - ); - } - return ( - - onChange(Math.round(Number(e.target.value) * 100))} /> - % - - ); -} - -/** Policy editor laid out like Safe's settings: a label column, then grouped controls with live validation. */ -export function Policies({ vault, onChanged }: { vault: VaultData; onChanged: () => void }) { - const { address } = useAccount(); - const me = vault.members.find((m) => m.address.toLowerCase() === address?.toLowerCase()); - const isOwner = Boolean((me?.roles ?? 0) & ROLE_OWNER); - const [draft, setDraft] = useState(vault.policy); - // When a new version activates while the editor is untouched, follow the live policy instead of showing the stale one. - const lastPolicyRef = useRef(vault.policy); - const [seenVersion, setSeenVersion] = useState(vault.policyVersion); - if (vault.policyVersion !== seenVersion) { - setSeenVersion(vault.policyVersion); - if (JSON.stringify(draft) === JSON.stringify(lastPolicyRef.current)) setDraft(vault.policy); - lastPolicyRef.current = vault.policy; - } - const [template, setTemplate] = useState(null); - const tx = useTx(() => { onChanged(); }); - const errors = useMemo(() => [...validatePolicy(draft), ...validateCounts(draft, vault.counts.owners, vault.counts.approvers, vault.counts.executors, vault.counts.guardians)], [draft, vault.counts]); - const reductions = useMemo(() => policyReductions(vault.policy, draft), [vault.policy, draft]); - const changed = JSON.stringify(draft) !== JSON.stringify(vault.policy); - const set = (k: keyof Policy, v: number | boolean) => setDraft((d) => ({ ...d, [k]: v })); - const changedKeys = new Set((Object.keys(draft) as Array).filter((k) => draft[k] !== vault.policy[k])); - - return ( - <> -
-
v{vault.policyVersion}
Active policy version
-
{vault.policy.approvalsLow}·{vault.policy.approvalsHigh}·{vault.policy.approvalsCritical}
Approvals low · high · critical
-
{fmtDuration(vault.policy.delayCritical)}
Critical delay & veto window
-
{vault.policy.envelopeBps ? fmtBps(vault.policy.envelopeBps) : "off"}
Loss envelope per {fmtDuration(vault.policy.envelopeWindow)}
-
- -
TemplatesStart from a profile instead of designing a policy from scratch. Choosing one only fills the editor; nothing is proposed until you say so.}> -
- {TEMPLATES.map((t) => ( - - ))} -
-
- - {GROUPS.map((g) => ( -
{g.title}{g.sub}}> - {g.fields.map((f) => ( -
-
-
{f.label}
- {f.hint &&
{f.hint}
} - {changedKeys.has(f.key) &&
was {f.unit === "bool" ? (vault.policy[f.key] ? "Yes" : "No") : f.unit === "hours" ? fmtDuration(Number(vault.policy[f.key])) : f.unit === "bps" ? fmtBps(Number(vault.policy[f.key])) : String(vault.policy[f.key])}
} -
- set(f.key, v)} /> -
- ))} -
- ))} - -
ProposeThe checks here mirror the contract's own validation. The contract has the final say.}> - {errors.length > 0 &&
{errors.map((e) =>
{e}
)}
} - {changed && errors.length === 0 && ( -
-
- {reductions.length ? ( - <>Security-reducing change. It waits {fmtDuration(vault.policy.policyChangeDelay)} after {vault.policy.governanceThreshold} owner approval{vault.policy.governanceThreshold === 1 ? "" : "s"} and any guardian can veto it: {reductions.join("; ")}. - ) : ( - <>Tightening-only change. It activates as soon as {vault.policy.governanceThreshold} owner{vault.policy.governanceThreshold === 1 ? "" : "s"} approve. - )} -
-
- )} - {!changed &&

Nothing has changed yet. Edit a value above or pick a template.

} -
- - - {!isOwner && Connect an owner wallet to propose.} -
- -
- - - -
HistoryEvery activated policy version is recorded onchain.}> - {vault.policyHistory.length === 0 ? ( -

No policy versions recorded yet.

- ) : ( -
- {[...vault.policyHistory].reverse().map((h) => ( -
Version {h.version}
{fmtDate(h.activatedAt)} ·
- ))} -
- )} -
- - ); -} - -function AssetLimitsSection({ vault, onChanged, isOwner }: { vault: VaultData; onChanged: () => void; isOwner: boolean }) { - const tx = useTx(onChanged); - const [assetAddr, setAssetAddr] = useState(vault.assets[0]?.address ?? ""); - const meta = vault.assets.find((a) => a.address === assetAddr); - const decimals = meta?.decimals ?? 18; - const fresh = (m?: typeof meta) => ({ - approved: m?.limits.approved ?? true, - lowMax: m ? fmtAmount(m.limits.lowMax, m.decimals, undefined, 6) : "", - highMax: m ? fmtAmount(m.limits.highMax, m.decimals, undefined, 6) : "", - perTxMax: m ? fmtAmount(m.limits.perTxMax, m.decimals, undefined, 6) : "0", - dailyMax: m ? fmtAmount(m.limits.dailyMax, m.decimals, undefined, 6) : "0", - }); - const [text, setText] = useState(() => fresh(meta)); - const pick = (addr: string) => { setAssetAddr(addr); setText(fresh(vault.assets.find((a) => a.address === addr))); }; - const parsed: AssetLimits | null = (() => { - const l = parseAmount(text.lowMax || "0", decimals), h = parseAmount(text.highMax || "0", decimals), p = parseAmount(text.perTxMax || "0", decimals), d = parseAmount(text.dailyMax || "0", decimals); - if (l === null || h === null || p === null || d === null) return null; - return { approved: text.approved, lowMax: l, highMax: h, perTxMax: p, dailyMax: d }; - })(); - const errors = parsed ? validateLimits(parsed) : ["Enter valid amounts."]; - const current = meta?.limits ?? { approved: false, lowMax: 0n, highMax: 0n, perTxMax: 0n, dailyMax: 0n }; - const reductions = parsed ? limitReductions(current, parsed) : []; - - return ( -
Asset limitsTiers and caps for each asset, in that asset's own units. Approving a new asset loosens security and goes through the delayed path.}> -
- {vault.assets.map((a) => ( - - ))} - pick(e.target.value.trim())} placeholder="or paste a token address" style={{ maxWidth: 320, height: 40 }} /> -
-
Approved
Unapproved assets cannot be paid out.
-
Routine up to
At or below this amount a payment is Low tier.
setText((t) => ({ ...t, lowMax: e.target.value }))} />
-
High up to
Above this a payment is Critical.
setText((t) => ({ ...t, highMax: e.target.value }))} />
-
Per-transaction cap
0 = no cap.
setText((t) => ({ ...t, perTxMax: e.target.value }))} />
-
Daily cap
Cumulative per 24h bucket. 0 = no cap.
setText((t) => ({ ...t, dailyMax: e.target.value }))} />
- {errors.length > 0 ?
{errors.join(" ")}
: ( -
{reductions.length ? <>Security-reducing: {reductions.join("; ")}. Delayed {fmtDuration(vault.policy.policyChangeDelay)} and vetoable. : <>Tightening-only: applies once owners approve.}
- )} -
- - {reductions.length > 0 && Delayed {fmtDuration(vault.policy.policyChangeDelay)}} -
- -
- ); -} diff --git a/web/src/pages/Product.tsx b/web/src/pages/Product.tsx deleted file mode 100644 index 24f1e9b..0000000 --- a/web/src/pages/Product.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { MarketingShell, SubHero } from "../components/MarketingShell"; -import { AppMockup } from "../components/Mockup"; -import { Reveal } from "../components/Reveal"; -import { IconBank, IconBook, IconClock, IconGuard, IconLab, IconLayers, IconShield, IconTrend, IconTx } from "../components/icons"; - -export function Product({ onLaunch }: { onLaunch: () => void }) { - return ( - - Multisig approvals,
plus a firewall} sub="Keep the N-of-M you already trust. Add the controls that decide whether a valid approval should move money right now." onLaunch={onLaunch}> - Read the security model -
- -
-
- -

Six controls between an approval and the money

-

Each one is contract code you can read on the explorer. The interface never shows a rule the vault does not enforce.

-
- -

Risk-tiered approvals

Amount, share of holdings, recipient trust, today's outflow and the security mode set the tier. The tier sets the approvals, the guardian sign-off and the wait.

-

Recipient trust

A never-seen address waits out an activation delay and is treated as higher risk until then. Owners can verify, trust, restrict or block any recipient through governance.

-

Velocity limits

Per-transaction and 24-hour caps, counted cumulatively. Splitting a payment into pieces changes nothing.

-

Circuit breaker

A loss envelope per window. The transfer that would cross it is not paid; the vault enters Lockdown instead, and leaving Lockdown takes owners, guardians and time.

-

Guardians

An independent role that can freeze, veto, confirm critical transfers and recover a lost signer, and that has no path to withdraw.

-

Policy-change firewall

Loosening any control waits for a delay, can be vetoed by a guardian, and cannot even be proposed while the vault is in Lockdown.

-
-
-
- -
-
- -

Know exactly what you are signing

-

Before the wallet opens, the vault itself describes the payment: what leaves, what remains, who receives it, why it was scored the way it was, and what it needs to execute.

-
- -
-

A review written by the contract

-

The preview is the vault's own classification, read live. The interface cannot promise something the vault would later refuse.

-
    -
  • Amount, treasury impact and remaining balance
  • -
  • Every reason the tier was assigned
  • -
  • Approvals, guardian sign-off and delay, exactly as pinned
  • -
-
-
-
-
-
- -
-
- -

Test the policy before it is live

-

The simulator runs the attacks that actually happen against your policy, using the same deterministic rules the contract applies.

-
- -

Attack scenarios

One stolen key paying a new address. Three stolen keys taking 40%. A drain split into twenty transfers. A policy weakened before a withdrawal. A stolen guardian key.

-

Maximum possible loss

One conservative number per asset: what could leave with no delay, what could leave within a day, and how long the largest transfers wait. Assumptions stated, never a guarantee.

-

Posture indicator

Confirmed controls and obvious weaknesses, in plain words. It does not claim to measure absolute security.

-
- -
-
-
-
-
- ); -} diff --git a/web/src/pages/Security.tsx b/web/src/pages/Security.tsx deleted file mode 100644 index 1877cd0..0000000 --- a/web/src/pages/Security.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { useState } from "react"; -import { useAccount, usePublicClient } from "wagmi"; -import { isAddress, stringToHex } from "viem"; -import { useQuery } from "@tanstack/react-query"; -import { SkurVaultAbi } from "../abi/SkurVault"; -import { Address, Badge, Button, CopyButton, Field, KV, Section, TxLink, TxStatus } from "../components/ui"; -import { Identicon } from "../components/Identicon"; -import { useTx } from "../hooks/useTx"; -import type { VaultData } from "../hooks/useVault"; -import { fmtAmount, fmtDuration } from "../lib/format"; -import { computeMaxLoss } from "../lib/maxLoss"; -import { Mode, MODE_LABEL, ROLE_GUARDIAN, ROLE_OWNER } from "../lib/types"; - -export function Security({ vault, onChanged }: { vault: VaultData; onChanged: () => void }) { - const { address } = useAccount(); - const me = vault.members.find((m) => m.address.toLowerCase() === address?.toLowerCase()); - const roles = me?.roles ?? 0; - const tx = useTx(onChanged); - const [reason, setReason] = useState(""); - const [oldSigner, setOldSigner] = useState(""); - const [newSigner, setNewSigner] = useState(""); - const guardians = vault.members.filter((m) => m.roles & ROLE_GUARDIAN); - const call = (functionName: string, args: readonly unknown[]) => tx.send({ address: vault.address, abi: SkurVaultAbi, functionName, args }); - const r32 = stringToHex(reason.slice(0, 31) || "manual", { size: 32 }); - const items = postureItems(vault); - const okCount = items.filter((i) => i.ok === "ok").length; - const pct = Math.round((okCount / items.length) * 100); - - return ( - <> -
-
-
-
{pct}%
-
Posture
{okCount} of {items.length} controls confirmed
-
-
-
{MODE_LABEL[vault.mode]}
Security mode
-
{guardians.length}
Guardian{guardians.length === 1 ? "" : "s"} · {vault.policy.guardianThreshold} to confirm
-
{fmtDuration(vault.policy.delayCritical)}
Critical veto window
-
- -
Security modeAny owner or guardian can raise the mode at once. Lowering it needs owners and guardians together, and leaving Lockdown also waits out the policy-change delay.}> -
- {[ - [Mode.NORMAL, "Normal", "Routine policy, standard approvals."], - [Mode.ELEVATED, "Elevated", "Every transfer one tier higher; per-transaction and daily caps halved."], - [Mode.LOCKDOWN, "Lockdown", "No outgoing execution, no loosening. Deposits, tightening, recovery and guardians keep working."], - ].map(([m, name, desc]) => ( -
- {vault.mode === m && Current} -
{name as string}
-
{desc as string}
-
- ))} -
-
- - setReason(e.target.value)} placeholder="phishing incident" maxLength={31} /> - -
- {vault.mode < Mode.ELEVATED && } - {vault.mode < Mode.LOCKDOWN && } - {vault.mode > Mode.NORMAL && } - {!(roles & (ROLE_OWNER | ROLE_GUARDIAN)) && Connect an owner or guardian wallet to change the mode.} -
- -
- -
GuardiansAn independent control plane. Guardians can freeze, veto, confirm and recover, and they can never move funds.}> - {guardians.length === 0 ? ( -
No guardian configured. Critical transfers cannot be confirmed and nobody independent can veto or freeze.
- ) : ( - guardians.map((g) => ( -
- - ark:{g.address} - - Guardian -
- )) - )} -
-
Veto
any single guardian, any time before execution
-
Critical confirmation
{vault.policy.guardianRequiredCritical ? `${vault.policy.guardianThreshold} of ${guardians.length}` : "not required"}
-
Leaving Lockdown
{vault.policy.guardianThreshold} of {guardians.length} plus {vault.policy.governanceThreshold} owner{vault.policy.governanceThreshold === 1 ? "" : "s"}, after {fmtDuration(vault.policy.policyChangeDelay)}
-
-
- -
RecoveryReplace a lost or compromised signer with a new address holding the same roles. It executes after {fmtDuration(vault.policy.recoveryDelay)}, any owner can cancel it, and the vault moves to Elevated when it completes.}> -
- - setOldSigner(e.target.value.trim())} placeholder="0x…" list="members" /> - {vault.members.map((m) => - - setNewSigner(e.target.value.trim())} placeholder="0x…" /> -
-
- - {!(roles & ROLE_GUARDIAN) && Only guardians can start a recovery.} -
-
- -
Maximum possible lossA conservative upper bound for each asset under the stated assumptions. A bound, never a guarantee.}> - {vault.assets.map((a) => { - const l = computeMaxLoss(vault.policy, a.limits, a.balance, vault.mode); - const share = a.balance > 0n ? Number((l.immediate * 100n) / a.balance) : 0; - return ( -
-
-
{share}%
-
-
{fmtAmount(l.immediate, a.decimals, a.symbol)} could leave with no delay
-
bounded by {l.immediateBinding}
-
-
- -
- ); - })} -
    {computeMaxLoss(vault.policy, vault.assets[0]?.limits ?? { approved: false, lowMax: 0n, highMax: 0n, perTxMax: 0n, dailyMax: 0n }, 0n, vault.mode).assumptions.map((s) =>
  • {s}
  • )}
-
- -
Security postureConfirmed controls and obvious weaknesses, in plain words. Not a measure of absolute security.}> - -
- -
Mode historyEvery mode change is recorded onchain with its reason.}> - {vault.modeHistory.length === 0 ?

The mode has never changed.

: ( -
- {[...vault.modeHistory].reverse().map((h, i) => ( -
{MODE_LABEL[h.previous]} → {MODE_LABEL[h.mode]}{h.reason ? ` · ${h.reason}` : ""}
·
- ))} -
- )} -
- - ); -} - -export type PostureItem = { ok: "ok" | "warn" | "bad"; text: string }; - -export function postureItems(vault: VaultData): PostureItem[] { - const p = vault.policy; - const g = vault.counts.guardians; - return [ - { ok: g > 0 ? "ok" : "bad", text: g > 0 ? `${g} independent guardian${g === 1 ? "" : "s"} configured` : "No guardian: nobody independent can veto or freeze" }, - { ok: p.guardianRequiredCritical ? "ok" : "warn", text: p.guardianRequiredCritical ? "Critical transfers need guardian confirmation" : "Critical transfers do not need a guardian" }, - { ok: p.delayCritical >= 24 * 3600 ? "ok" : "warn", text: `Critical delay ${fmtDuration(p.delayCritical)}` }, - { ok: p.recipientActivationDelay > 0 ? "ok" : "bad", text: p.recipientActivationDelay > 0 ? `New recipients wait ${fmtDuration(p.recipientActivationDelay)}` : "New recipients can be paid immediately" }, - { ok: vault.assets.every((a) => a.limits.dailyMax > 0n) ? "ok" : "warn", text: vault.assets.every((a) => a.limits.dailyMax > 0n) ? "24h outflow cap on every asset" : "Some assets have no 24h cap" }, - { ok: p.envelopeBps > 0 ? "ok" : "warn", text: p.envelopeBps > 0 ? `Circuit breaker at ${p.envelopeBps / 100}% per ${fmtDuration(p.envelopeWindow)}` : "No loss envelope configured" }, - { ok: p.policyChangeDelay > 0 ? "ok" : "bad", text: p.policyChangeDelay > 0 ? `Security-reducing changes wait ${fmtDuration(p.policyChangeDelay)}` : "Policy can be weakened instantly" }, - { ok: p.recoveryDelay > 0 && g > 0 ? "ok" : "warn", text: `Recovery configured (${fmtDuration(p.recoveryDelay)} delay)` }, - { ok: p.approvalsLow >= 2 ? "ok" : "warn", text: p.approvalsLow >= 2 ? "Routine payments need two approvals" : "Routine payments need a single approval" }, - ]; -} - -export function Posture({ vault, compact = false }: { vault: VaultData; compact?: boolean }) { - const client = usePublicClient(); - const signers = vault.members.filter((m) => !(m.roles & ROLE_GUARDIAN)); - const { data: codeMap } = useQuery({ - queryKey: ["codes", vault.address, signers.map((s) => s.address).join(",")], - queryFn: async () => { - const out: Record = {}; - await Promise.all(signers.map(async (s) => { const code = await client!.getCode({ address: s.address }).catch(() => undefined); out[s.address.toLowerCase()] = Boolean(code && code !== "0x"); })); - return out; - }, - enabled: Boolean(client), - staleTime: 60_000, - }); - const eoa = codeMap ? signers.filter((s) => !codeMap[s.address.toLowerCase()]).length : null; - const items = postureItems(vault); - if (eoa !== null) items.push({ ok: eoa === 0 ? "ok" : "warn", text: eoa === 0 ? "All signers are smart-contract accounts" : `${eoa} of ${signers.length} signers are externally-owned keys` }); - return ( -
    - {items.map((i) => ( -
  • {i.text}
  • - ))} -
- ); -} diff --git a/web/src/pages/SecurityPage.tsx b/web/src/pages/SecurityPage.tsx deleted file mode 100644 index 36f36d7..0000000 --- a/web/src/pages/SecurityPage.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { MarketingShell, SubHero } from "../components/MarketingShell"; -import { Reveal } from "../components/Reveal"; -import { IconCheck } from "../components/icons"; -import { DEPLOYMENTS } from "../config/chain"; - -const INVARIANTS = [ - ["No proposal executes before every active condition is met", "execution ordering · timelock · live recount"], - ["A guardian cannot move treasury assets through guardian authority alone", "role exclusivity · no withdrawal path"], - ["Lockdown blocks ordinary outgoing execution", "mode gate at execution"], - ["Security-reducing policy changes never take effect immediately", "policyReduces · timelock · veto"], - ["Cumulative outflow accounting cannot be bypassed by splitting", "24-hour bucket · loss envelope"], - ["A proposal cannot execute twice", "status written before any external call"], - ["Removed signers cannot approve new actions", "approvals recounted against live roles"], - ["Approvals cannot be duplicated or replayed", "one approval per signer per proposal"], - ["New recipients cannot skip their activation delay", "executable time ≥ activation · re-checked"], - ["Recovery cannot quietly weaken the policy", "same roles · policy untouched · Elevated after"], - ["Native and ERC-20 accounting reconciles", "SafeERC20 · totalOutflow matches balances"], - ["External calls cannot re-enter an unsafe state", "nonReentrant · effects before interactions"], -]; - -export function SecurityPage({ onLaunch }: { onLaunch: () => void }) { - return ( - - Built for the day
a key is stolen} sub="A stolen key, a compromised laptop or a rogue signer should be a bad day, not the end of the treasury. This page is what holds, and what has not been proven yet." onLaunch={onLaunch}> - Verified source -
- -
-
- -

What we claim, and what we do not

-

Skur does not make signer compromise harmless. It adds independent, programmable controls that limit or delay the damage when authorised credentials are misused.

-
- -
1

One signer compromised

Cannot reach any tier's threshold alone unless you set that tier to one, and even then is bounded by the routine cap, the daily cap and the loss envelope.

-
2

Enough signers compromised

High and critical delays give guardians a veto window, critical transfers need a guardian, and the envelope freezes the vault on the first over-limit attempt.

-
3

An admin weakens the policy

Loosening waits for the policy-change delay, can be vetoed by any guardian, and cannot be proposed at all while the vault is in Lockdown.

-
4

A guardian is compromised

There is no withdrawal path. Recovery is delayed and any owner can cancel it. A guardian can only freeze or veto.

-
-
-
- -
-
- -

Twelve invariants, each with a test

-

Every control is enforced by the contract and proven by a unit, fuzz or stateful invariant test. The mapping is public in the security model.

-
- - {INVARIANTS.map(([t, how], i) => ( -
-
{i + 1}
-

{t}

-

{how}

-
- ))} -
-
-
- -
-
- -

Verification, honestly stated

-

What has been done, and what has not.

-
- -

95 contract tests

A unit test per control, fuzzed monotonicity properties, and a stateful handler that drives random actors against the vault.

-

Static analysis

Slither reports nothing above informational. The linter is clean. Every narrowing cast is bounded and documented in place.

-

Verified and reproducible

Sources are verified on Blockscout, and the repository builds to byte-identical runtime bytecode.

-
–

Independent audit

Not yet. Do not put real value behind Skur until one has been completed.

-
–

Private pilot

Not yet. The devnet deployment exists for evaluation.

-

Known limitations, listed

Anchored 24-hour buckets, per-asset exposure without an oracle, a breaker that latches rather than refunds, single-guardian vaults. All in the model.

-
-
-
-
- ); -} diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx deleted file mode 100644 index d988973..0000000 --- a/web/src/pages/Settings.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { useState } from "react"; -import { Address, Button, Field, Section, Switch } from "../components/ui"; -import { Identicon } from "../components/Identicon"; -import { ARK_DEVNET_EXPLORER, ARK_DEVNET_FAUCET, ARK_DEVNET_RPC, ARK_DEVNET_WS, DEPLOYMENTS } from "../config/chain"; -import type { VaultData } from "../hooks/useVault"; -import { useTheme } from "../state/theme"; -import { useVaultLabel } from "../state/vaultLabel"; - -type Tab = "setup" | "appearance" | "environment"; - -/** app.safe.global/settings: horizontal tabs, then label/content sections. */ -export function Settings({ vault, vaultAddress, onSwitchVault }: { vault?: VaultData; vaultAddress: `0x${string}` | null; onSwitchVault: () => void }) { - const [tab, setTab] = useState("setup"); - const { theme, toggle } = useTheme(); - const [label, setLabel] = useVaultLabel(vaultAddress); - - return ( - <> -
- {(["setup", "appearance", "environment"] as Tab[]).map((t) => ( - - ))} -
- - {tab === "setup" && ( - <> -
- {vaultAddress ? ( - <> -
- -
-
{label}
-
-
-
- - setLabel(e.target.value)} maxLength={40} /> - -
-
Policy version
v{vault?.policyVersion ?? "—"}
-
Members
{vault ? `${vault.members.length} (${vault.counts.guardians} guardians)` : "—"}
-
Proposals
{vault ? `${vault.proposalCount} total · ${vault.pendingCount} pending · ${vault.executedCount} executed` : "—"}
-
Implementation
-
Factory
-
-
- - ) : ( - - )} -
-
-
Skur V1 · immutable
-

Every vault is a minimal proxy over one verified implementation, with no admin and no upgrade path. Moving to a new version means creating a new vault and transferring funds under this vault's own policy.

- View source ↗ -
- - )} - - {tab === "appearance" && ( -
-

Theme

-

Light is the default. Your choice is kept in this browser.

- -
- )} - - {tab === "environment" && ( -
-

If this interface ever disappears, the vault stays reachable through these endpoints and the verified contracts on the explorer.

-
-
Chain
Ark Constellation devnet · 9000 · KASH
-
RPC
{ARK_DEVNET_RPC}
-
WebSocket
{ARK_DEVNET_WS}
- - -
Test stablecoin (sUSD)
-
-
- )} - - ); -} diff --git a/web/src/pages/Simulator.tsx b/web/src/pages/Simulator.tsx deleted file mode 100644 index 432c1b6..0000000 --- a/web/src/pages/Simulator.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { useMemo, useState } from "react"; -import { Badge, Section } from "../components/ui"; -import { Reveal } from "../components/Reveal"; -import type { VaultData } from "../hooks/useVault"; -import { fmtAmount, parseAmount } from "../lib/format"; -import { runScenarios, type Verdict } from "../lib/simulator"; -import { TEMPLATES, type TemplateId } from "../lib/templates"; -import type { Policy } from "../lib/types"; - -const TONE: Record = { blocked: "ok", impossible: "ok", delayed: "info", escalated: "warn", allowed: "bad" }; -const TEXT: Record = { blocked: "Blocked", impossible: "Not possible", delayed: "Delayed", escalated: "Escalated", allowed: "Would succeed" }; - -/** Policy simulator: run a configured policy against the blueprint's attack scenarios. */ -export function Simulator({ vault }: { vault: VaultData | undefined }) { - const [source, setSource] = useState<"vault" | TemplateId>(vault ? "vault" : "startup"); - const [balanceText, setBalanceText] = useState("1,000,000"); - const [approvers, setApprovers] = useState(3); - const [guardians, setGuardians] = useState(1); - const stable = vault?.assets.find((a) => a.address !== "0x0000000000000000000000000000000000000000") ?? vault?.assets[0]; - const decimals = stable?.decimals ?? 6; - const symbol = stable?.symbol ?? "USD"; - const { policy, limits } = useMemo(() => { - if (source === "vault" && vault && stable) return { policy: vault.policy as Policy, limits: stable.limits }; - const t = TEMPLATES.find((x) => x.id === source) ?? TEMPLATES[0]; - return { policy: t.policy, limits: t.stable }; - }, [source, vault, stable]); - const balance = source === "vault" && stable ? stable.balance : (parseAmount(balanceText, decimals) ?? 0n); - const approverCount = source === "vault" && vault ? vault.counts.approvers : approvers; - const guardianCount = source === "vault" && vault ? vault.counts.guardians : guardians; - const results = useMemo(() => runScenarios({ policy, limits, balance, decimals, symbol, approverCount, guardianCount }), [policy, limits, balance, decimals, symbol, approverCount, guardianCount]); - const stopped = results.filter((r) => r.verdict !== "allowed").length; - const pct = Math.round((stopped / results.length) * 100); - - return ( - <> -
-
-
-
{stopped}/{results.length}
-
Scenarios stopped or delayed
verdicts use the contract's own rules
-
-
-
{results.filter((r) => r.verdict === "blocked" || r.verdict === "impossible").length}
Blocked outright
-
{results.filter((r) => r.verdict === "delayed").length}
Delayed with a veto window
-
r.verdict === "allowed") ? "" : ""}`} style={{ color: results.some((r) => r.verdict === "allowed") ? "var(--error)" : undefined }}>{results.filter((r) => r.verdict === "allowed").length}
Would succeed
-
- -
Configuration under testPick the live policy or a template, then adjust the treasury size and team to see how the verdicts move.}> -
-
- Policy - -
- {source === "vault" && stable ? ( -
Treasury balance
- ) : ( - <> -
Treasury balance ({symbol}) setBalanceText(e.target.value)} />
-
Approvers setApprovers(Number(e.target.value))} />
-
Guardians setGuardians(Number(e.target.value))} />
- - )} -
-
- - - {results.map((r) => ( -
-
- {r.title} - {TEXT[r.verdict]} -
-

{r.narrative}

-
    {r.detail.map((d, i) =>
  • {d}
  • )}
-
- ))} -
-

Blocked and Not possible mean the contract refuses. Delayed means guardians get a veto window. Escalated means more approvals are needed but nothing waits. Would succeed is a gap to close before the policy goes live.

- - ); -} diff --git a/web/src/pages/Solutions.tsx b/web/src/pages/Solutions.tsx deleted file mode 100644 index a6b40fd..0000000 --- a/web/src/pages/Solutions.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { MarketingShell, SubHero } from "../components/MarketingShell"; -import { AppMockup } from "../components/Mockup"; -import { Reveal } from "../components/Reveal"; -import { IconArrowRight, IconBank, IconClock, IconGuard, IconLayers, IconShield, IconUsers } from "../components/icons"; -import { TEMPLATES } from "../lib/templates"; -import { fmtDuration } from "../lib/format"; - -export function Solutions({ onLaunch }: { onLaunch: () => void }) { - return ( - - Treasury control
for onchain teams} sub="Approvals that scale with risk, recipients that earn trust, and guardians who can stop a bad day without ever being able to cause one." onLaunch={onLaunch} /> - -
-
- -

Three steps to a protected treasury

-

Nobody designs a policy from first principles. Pick a template, name the people, sign once.

-
- -

Name signers and guardians

Owners govern, approvers confirm payments, executors execute. Guardians hold the brake and nothing else; the contract will not let them hold both.

-

Pick a policy template

Six profiles, from Startup to Family Office, set the tiers, delays, caps and loss envelope. Every number can be tuned later through governance.

-

Operate behind a firewall

Every payment is scored and reviewed in plain language. Every loosening of the policy waits, and any guardian can stop it.

-
-
-
- -
-
- -

One vault, six starting points

-

Each template is a complete, valid policy. Start with the closest fit and tighten from there.

-
- - {TEMPLATES.map((t) => ( -
-
-

{t.name}

-

{t.tagline}

-

- Critical transfers wait {fmtDuration(t.policy.delayCritical)} · new recipients wait {fmtDuration(t.policy.recipientActivationDelay)} · loss envelope {t.policy.envelopeBps / 100}% a day -

-
- ))} -
-
-
- -
-
- -

For guardians

-

A cold key in a drawer, a board member, a security firm: anyone who should be able to stop money but must never be able to move it.

-
- -
-

Hold the brake, not the wheel

-

The contract refuses to give a guardian any treasury role. That makes guardians the second control plane an attacker has to breach, and the one they cannot profit from.

-
    -
  • Freeze the vault the moment something looks wrong
  • -
  • Veto critical and security-reducing proposals
  • -
  • Replace a lost signer after a delay owners can cancel
  • -
-
-
-
-
-
- -
-
- -

No custody. No fees on assets. No lock-in.

-

Skur never holds a key and never takes a percentage. The vault is an immutable contract you can reach with any compatible interface.

-
- -
-

Self-custodial by construction

-

The vault contract holds the assets. Your wallets hold the keys. The factory that created the vault has no authority over it.

- Read the security model -
-
-

Reachable without Skur

-

The contracts are verified on the Ark explorer and every endpoint lives in one configuration file. If this site disappears, your vault does not.

- Open the explorer -
-
-
-
-
-
- ); -} diff --git a/web/src/pages/Transactions.tsx b/web/src/pages/Transactions.tsx deleted file mode 100644 index c925752..0000000 --- a/web/src/pages/Transactions.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { useMemo, useState } from "react"; -import { TxRow } from "../components/TxRow"; -import { Button, Empty } from "../components/ui"; -import { IconTx } from "../components/icons"; -import type { ProposalView, VaultData } from "../hooks/useVault"; -import { Kind, Mode, Status } from "../lib/types"; - -type Tab = "queue" | "history"; -type Filter = "all" | "transfers" | "governance"; - -function dayLabel(ts: bigint): string { - return new Date(Number(ts) * 1000).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }).toUpperCase(); -} - -function groupByDay(list: ProposalView[]): Array<[string, ProposalView[]]> { - const out: Array<[string, ProposalView[]]> = []; - for (const p of list) { - const label = dayLabel(p.createdAt); - const last = out[out.length - 1]; - if (last && last[0] === label) last[1].push(p); - else out.push([label, [p]]); - } - return out; -} - -/** app.safe.global/transactions: Queue / History tabs, date-grouped card rows. */ -export function Transactions({ vault, onChanged, onNewTransaction }: { vault: VaultData; onChanged: () => void; onNewTransaction: () => void }) { - const [tab, setTab] = useState("queue"); - const [filter, setFilter] = useState("all"); - const queue = useMemo(() => vault.proposals.filter((p) => p.status === Status.PENDING), [vault.proposals]); - const history = useMemo(() => vault.proposals.filter((p) => p.status !== Status.PENDING), [vault.proposals]); - const list = (tab === "queue" ? queue : history).filter((p) => (filter === "all" ? true : filter === "transfers" ? p.kind === Kind.TRANSFER : p.kind !== Kind.TRANSFER)); - const groups = groupByDay(list); - - return ( - <> -
-
- - -
-
- - -
-
- - {vault.logsFailed &&
The chain's event log could not be read, so purposes and history details are missing. Proposals below come straight from contract state.
} - {vault.activityLoading ? ( - Reading proposals and history from the chain… - ) : list.length === 0 ? ( -
- }>{tab === "queue" ? "Proposals waiting for confirmation or execution will appear here" : "Executed, cancelled and vetoed proposals will appear here"} -
- ) : ( - groups.map(([label, items]) => ( -
-
{label}
- {items.map((p) => )} -
- )) - )} - {tab === "queue" && queue.length > 0 &&

Governance proposals need owner confirmations. Security-reducing ones also wait out their timelock, and any guardian can veto them.

} - - ); -} diff --git a/web/src/pages/Welcome.tsx b/web/src/pages/Welcome.tsx deleted file mode 100644 index 5fce065..0000000 --- a/web/src/pages/Welcome.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { useState } from "react"; -import { useAccount, useConnect, useDisconnect, usePublicClient } from "wagmi"; -import { isAddress } from "viem"; -import { SkurFactoryAbi } from "../abi/SkurFactory"; -import { DEPLOYMENTS } from "../config/chain"; -import { Button, Field } from "../components/ui"; -import { NetworkGuard } from "../components/NetworkGuard"; -import { Identicon } from "../components/Identicon"; -import { IconArrowRight, IconWallet } from "../components/icons"; -import { short } from "../lib/format"; - -/** app.safe.global/welcome: a centred "Get started" card on a dark canvas. */ -export function Welcome({ onOpen, onCreate, onLanding }: { onOpen: (a: `0x${string}`) => void; onCreate: () => void; onLanding: () => void }) { - const { address, isConnected } = useAccount(); - const { connectors, connect, isPending } = useConnect(); - const { disconnect } = useDisconnect(); - const client = usePublicClient(); - const [tab, setTab] = useState<"accounts" | "watch">("accounts"); - const [watch, setWatch] = useState(""); - const [mine, setMine] = useState<`0x${string}`[] | null>(null); - - const loadMine = async () => { - if (!client || !address) return; - const v = await client.readContract({ address: DEPLOYMENTS.factory, abi: SkurFactoryAbi, functionName: "vaultsOf", args: [address] }).catch(() => []); - setMine([...v]); - }; - - return ( -
-
- -
- - {isConnected && address ? ( - - ) : ( - connectors.slice(0, 1).map((c) => ( - - )) - )} -
-
- -
-
- - -
-
- New Skur V1 is live on the Ark Constellation devnet -
- - {tab === "accounts" ? ( -
-
SSkur
-

Get started

-

Connect a wallet to create a vault, or open one you belong to.

- {isConnected && address ? ( - <> - -
OR
- {mine === null ? ( - - ) : mine.length === 0 ? ( -

This wallet is not a member of any vault yet. You can still open any vault by address.

- ) : ( -
- {mine.map((v) => ( - - ))} -
- )} - {DEPLOYMENTS.demoVault && ( -

- or open the -

- )} - - ) : ( - <> - {connectors.slice(0, 1).map((c) => ( - - ))} -
OR
- - - )} -
- ) : ( -
-
SSkur
-

Watch a vault

-

Read any vault's balances, policy and queue. No wallet needed.

- - setWatch(e.target.value.trim())} placeholder="0x…" style={{ textAlign: "left" }} /> - - - {DEPLOYMENTS.demoVault && ( -

- or open the -

- )} -
- )} -

You interact directly with verified contracts on the Ark Constellation devnet. Skur never holds keys or funds.

-
- -
- Skur V1 · Ark devnet - GitHub - Security model - Explorer - Faucet -
-
- ); -} diff --git a/web/src/state/theme.ts b/web/src/state/theme.ts deleted file mode 100644 index 024ee9e..0000000 --- a/web/src/state/theme.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { createContext, useCallback, useContext, useEffect, useState } from "react"; - -export type Theme = "light" | "dark"; -const KEY = "skur.theme"; - -/** Dark by default, like the Safe app. */ -function initial(): Theme { - try { - const stored = window.localStorage.getItem(KEY); - if (stored === "light" || stored === "dark") return stored; - } catch { - /* ignore */ - } - return "light"; -} - -export function useThemeState(): { theme: Theme; toggle: () => void } { - const [theme, setTheme] = useState(initial); - useEffect(() => { - document.documentElement.setAttribute("data-theme", theme); - try { - window.localStorage.setItem(KEY, theme); - } catch { - /* ignore */ - } - }, [theme]); - const toggle = useCallback(() => setTheme((t) => (t === "light" ? "dark" : "light")), []); - return { theme, toggle }; -} - -export const ThemeContext = createContext<{ theme: Theme; toggle: () => void }>({ theme: "dark", toggle: () => {} }); -export const useTheme = () => useContext(ThemeContext); diff --git a/web/src/state/vaultAddress.ts b/web/src/state/vaultAddress.ts deleted file mode 100644 index d7389d6..0000000 --- a/web/src/state/vaultAddress.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; -import { isAddress } from "viem"; -import { DEPLOYMENTS } from "../config/chain"; - -const KEY = "skur.vault"; - -function readInitial(): `0x${string}` | null { - try { - const fromHash = new URLSearchParams(window.location.hash.split("?")[1] ?? "").get("vault"); - if (fromHash && isAddress(fromHash)) return fromHash as `0x${string}`; - const stored = window.localStorage.getItem(KEY); - if (stored && isAddress(stored)) return stored as `0x${string}`; - } catch { - /* storage unavailable */ - } - if (DEPLOYMENTS.demoVault && DEPLOYMENTS.demoVault !== "0x0000000000000000000000000000000000000000") { - return DEPLOYMENTS.demoVault; - } - return null; -} - -/** The selected vault. Persisted per browser; the contracts are the source of truth for everything else. */ -export function useVaultAddress(): [`0x${string}` | null, (a: `0x${string}` | null) => void] { - const [addr, setAddr] = useState<`0x${string}` | null>(readInitial); - useEffect(() => { - try { - if (addr) window.localStorage.setItem(KEY, addr); - else window.localStorage.removeItem(KEY); - } catch { - /* ignore */ - } - }, [addr]); - const set = useCallback((a: `0x${string}` | null) => setAddr(a), []); - return [addr, set]; -} diff --git a/web/src/state/vaultLabel.ts b/web/src/state/vaultLabel.ts deleted file mode 100644 index eec26ef..0000000 --- a/web/src/state/vaultLabel.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; - -const KEY = "skur.vaultLabels"; - -function readAll(): Record { - try { - return JSON.parse(window.localStorage.getItem(KEY) ?? "{}"); - } catch { - return {}; - } -} - -/** A private, browser-local display name for a vault. Never leaves the device; the chain only knows the address. */ -export function useVaultLabel(address: string | null): [string, (label: string) => void] { - const [labels, setLabels] = useState>(readAll); - useEffect(() => { - try { - window.localStorage.setItem(KEY, JSON.stringify(labels)); - } catch { - /* ignore */ - } - }, [labels]); - const key = address?.toLowerCase() ?? ""; - const set = useCallback((label: string) => setLabels((l) => ({ ...l, [key]: label })), [key]); - return [labels[key] ?? "Treasury vault", set]; -} diff --git a/web/src/styles.css b/web/src/styles.css deleted file mode 100644 index 6e8d415..0000000 --- a/web/src/styles.css +++ /dev/null @@ -1,721 +0,0 @@ -/* Skur. Identity: charcoal #212529 with #FFD000 used the way hazard tape and a highlighter are used: sparingly, to mark what matters. - Space Grotesk for display, DM Sans for text, DM Mono for numbers and labels. Sharp 4-8px radii, hairlines, numbered sections. - Light: charcoal primary, yellow accent on white. Dark: #212529 canvas, #2b3035 panels, yellow primary with charcoal text. - Motion: entrance reveals, staggered lists, live scorer, tape that runs in Lockdown, reduced-motion aware. */ -@import url("https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700;800&family=DM+Mono:wght@400;500&family=Space+Grotesk:wght@500;600;700&display=swap"); - -:root { - --text: #212529; - --text-2: #6c757d; - --text-3: #adb5bd; - --canvas: #f1f3f5; - --panel: #ffffff; - --panel-2: #e9ecef; - --panel-3: #f8f9fa; - --border: #dee2e6; - --border-2: #adb5bd; - --brand: #212529; - --brand-hover: #343a40; - --brand-contrast: #ffffff; - --brand-bg: #e9ecef; - --brand-bg-2: #dee2e6; - --accent: #ffd000; - --accent-hover: #e6bb00; - --accent-contrast: #212529; - --accent-bg: #fff6c2; - --accent-bg-2: #ffec85; - --accent-soft: rgba(255, 208, 0, 0.35); - --success: #198754; - --success-bg: #d1e7dd; - --success-text: #0f5132; - --warning: #e5850b; - --warning-bg: #fff3cd; - --warning-text: #664d03; - --error: #dc3545; - --error-bg: #f8d7da; - --error-text: #842029; - --info: #0a58ca; - --info-bg: #cfe2ff; - --info-text: #052c65; - --review: #c2410c; - --review-bg: #ffe8d6; - --radius: 8px; - --radius-sm: 6px; - --sidebar: 236px; - --ease: cubic-bezier(0.22, 1, 0.36, 1); - --shadow-1: 0 1px 2px rgba(33, 37, 41, 0.06), 0 0 0 1px rgba(33, 37, 41, 0.04); - --shadow-2: 0 12px 32px rgba(33, 37, 41, 0.12); -} -[data-theme="dark"] { - --text: #f8f9fa; - --text-2: #adb5bd; - --text-3: #6c757d; - --canvas: #212529; - --panel: #2b3035; - --panel-2: #343a40; - --panel-3: #30353a; - --border: #343a40; - --border-2: #6c757d; - --brand: #ffd000; - --brand-hover: #ffdc33; - --brand-contrast: #212529; - --brand-bg: #3d3400; - --brand-bg-2: #4f4400; - --accent: #ffd000; - --accent-hover: #ffdc33; - --accent-contrast: #212529; - --accent-bg: #3d3400; - --accent-bg-2: #4f4400; - --accent-soft: rgba(255, 208, 0, 0.3); - --success: #75b798; - --success-bg: #1b4332; - --success-text: #d1e7dd; - --warning: #ffa94d; - --warning-bg: #4d2e05; - --warning-text: #fff3cd; - --error: #ea868f; - --error-bg: #4a1c22; - --error-text: #f8d7da; - --info: #6ea8fe; - --info-bg: #1b3a5c; - --info-text: #cfe2ff; - --review: #ffa94d; - --review-bg: #4a2a0b; - --shadow-1: 0 0 0 1px rgba(255, 255, 255, 0.04); - --shadow-2: 0 12px 32px rgba(0, 0, 0, 0.4); -} - -* { box-sizing: border-box; } -html, body { margin: 0; } -html { scroll-behavior: smooth; } -body { font-family: "DM Sans", system-ui, sans-serif; font-size: 14px; line-height: 20px; color: var(--text); background: var(--canvas); -webkit-font-smoothing: antialiased; transition: background 0.25s var(--ease), color 0.25s var(--ease); } -h1, h2, h3, h4 { margin: 0; font-weight: 700; letter-spacing: -0.01em; } -h1 { font-size: 32px; line-height: 36px; } -h2 { font-size: 27px; line-height: 34px; } -h3 { font-size: 20px; line-height: 26px; } -h4 { font-size: 16px; line-height: 22px; } -p { margin: 0; } -a { color: inherit; text-decoration: none; } -code, .mono { font-family: "DM Mono", ui-monospace, Menlo, monospace; font-size: 12.5px; } -button, input, select, textarea { font-family: inherit; } -h1, h2, h3, .display, .balance-card .amount, .scorecard .v, .stats .v { font-family: "Space Grotesk", "DM Sans", system-ui, sans-serif; } -.muted { color: var(--text-2); } -.faint { color: var(--text-3); } -.small { font-size: 12px; line-height: 16px; } -.caption { font-size: 12px; line-height: 16px; color: var(--text-2); } -.overline { font-size: 11px; line-height: 14px; letter-spacing: 0.08em; text-transform: uppercase; font-weight: 700; color: var(--text-2); } -.strong { font-weight: 700; } -.num { font-variant-numeric: tabular-nums; } -.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.accent { color: var(--text); background: var(--accent); padding: 0 4px; } -[data-theme="dark"] .accent { color: var(--accent); background: none; padding: 0; } - -/* ================================================================== motion */ -@keyframes rise { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: none; } } -@keyframes fade { from { opacity: 0; } to { opacity: 1; } } -@keyframes pop { from { opacity: 0; transform: scale(0.96) translateY(8px); } to { opacity: 1; transform: none; } } -@keyframes shimmer { from { background-position: -600px 0; } to { background-position: 600px 0; } } -@keyframes cell { 0%, 100% { opacity: 0.15; } 50% { opacity: 0.6; } } -@keyframes marquee { from { transform: translateX(0); } to { transform: translateX(-50%); } } -@keyframes pulse-ring { 0% { box-shadow: 0 0 0 0 var(--accent-soft); } 70% { box-shadow: 0 0 0 14px transparent; } 100% { box-shadow: 0 0 0 0 transparent; } } -@keyframes spin { to { transform: rotate(360deg); } } -@keyframes float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } } -@keyframes grow { from { transform: scaleX(0); } to { transform: scaleX(1); } } - -.reveal { opacity: 0; transform: translateY(24px); transition: opacity 0.7s var(--ease), transform 0.7s var(--ease); } -.reveal.in { opacity: 1; transform: none; } -.reveal-stagger > * { opacity: 0; transform: translateY(16px); transition: opacity 0.6s var(--ease), transform 0.6s var(--ease); } -.reveal-stagger.in > * { opacity: 1; transform: none; } -.reveal-stagger.in > *:nth-child(1) { transition-delay: 0.05s; } -.reveal-stagger.in > *:nth-child(2) { transition-delay: 0.12s; } -.reveal-stagger.in > *:nth-child(3) { transition-delay: 0.19s; } -.reveal-stagger.in > *:nth-child(4) { transition-delay: 0.26s; } -.reveal-stagger.in > *:nth-child(5) { transition-delay: 0.33s; } -.reveal-stagger.in > *:nth-child(6) { transition-delay: 0.40s; } -.reveal-stagger.in > *:nth-child(n+7) { transition-delay: 0.47s; } -.enter { animation: rise 0.5s var(--ease) both; } -.enter-2 { animation: rise 0.5s var(--ease) 0.08s both; } -.enter-3 { animation: rise 0.5s var(--ease) 0.16s both; } -.enter-4 { animation: rise 0.5s var(--ease) 0.24s both; } -.page-enter { animation: fade 0.35s var(--ease) both; } -.page-enter > * { animation: rise 0.45s var(--ease) both; } -.page-enter > *:nth-child(2) { animation-delay: 0.06s; } -.page-enter > *:nth-child(3) { animation-delay: 0.12s; } -.page-enter > *:nth-child(4) { animation-delay: 0.18s; } -.lift { transition: transform 0.25s var(--ease), box-shadow 0.25s var(--ease), border-color 0.25s var(--ease); } -.lift:hover { transform: translateY(-3px); box-shadow: var(--shadow-2); } -.skeleton { position: relative; overflow: hidden; background: var(--panel-2); border-radius: 6px; color: transparent !important; } -.skeleton::after { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.35), transparent); background-size: 600px 100%; animation: shimmer 1.4s linear infinite; } -[data-theme="dark"] .skeleton::after { background: linear-gradient(90deg, transparent, rgba(255,255,255,0.08), transparent); background-size: 600px 100%; } -.spinner { width: 16px; height: 16px; border-radius: 50%; border: 2px solid var(--border-2); border-top-color: var(--brand); animation: spin 0.8s linear infinite; display: inline-block; vertical-align: middle; } -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; scroll-behavior: auto !important; } - .reveal, .reveal-stagger > * { opacity: 1; transform: none; } -} - -/* ================================================================== buttons */ -.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 40px; padding: 0 20px; border-radius: var(--radius-sm); border: 1px solid transparent; font-size: 14px; font-weight: 700; cursor: pointer; white-space: nowrap; transition: background 0.15s var(--ease), border-color 0.15s var(--ease), transform 0.15s var(--ease), box-shadow 0.15s var(--ease); } -.btn:active:not(:disabled) { transform: scale(0.98); } -.btn:disabled { opacity: 0.4; cursor: not-allowed; } -.btn-primary { background: var(--brand); color: var(--brand-contrast); } -.btn-primary:hover:not(:disabled) { background: var(--brand-hover); box-shadow: var(--shadow-2); } -.btn-secondary { background: transparent; color: var(--text); border-color: var(--border-2); } -.btn-secondary:hover:not(:disabled) { background: var(--panel-2); } -.btn-dark { background: var(--panel-2); color: var(--text); } -.btn-dark:hover:not(:disabled) { background: var(--brand-bg-2); } -.btn-danger { background: transparent; color: var(--error); border-color: var(--error); } -.btn-danger:hover:not(:disabled) { background: var(--error-bg); } -.btn-ghost { background: transparent; color: var(--text); } -.btn-ghost:hover:not(:disabled) { background: var(--panel-2); } -.btn-accent { background: var(--accent); color: var(--accent-contrast); } -.btn-accent:hover:not(:disabled) { background: var(--accent-hover); box-shadow: 0 10px 28px rgba(255,208,0,0.35); } -.btn-white { background: #fff; color: #212529; border-color: #dee2e6; } -.btn-white:hover:not(:disabled) { background: #f1f3f5; } -.btn-black { background: #212529; color: #fff; } -.btn-black:hover:not(:disabled) { background: #343a40; } -.btn-sm { height: 32px; padding: 0 12px; font-size: 13px; } -.btn-lg { height: 52px; padding: 0 28px; font-size: 16px; } -.btn-block { width: 100%; } -.icon-btn { width: 36px; height: 36px; display: inline-grid; place-items: center; border-radius: 999px; border: 0; background: var(--panel-2); color: var(--text-2); cursor: pointer; transition: background 0.15s, color 0.15s, transform 0.15s; } -.icon-btn:hover { color: var(--text); background: var(--brand-bg-2); } -.icon-btn:active { transform: scale(0.94); } -.icon-btn.small { width: 28px; height: 28px; background: transparent; } -.icon-btn.small:hover { background: var(--panel-2); } - -/* ================================================================== app shell */ -.shell { display: flex; min-height: 100vh; } -.shell.collapsed { --sidebar: 68px; } -.shell.collapsed .sidebar { padding: 12px 8px 16px; } -.shell.collapsed .lbl, .shell.collapsed .nav-group, .shell.collapsed .promo, .shell.collapsed .nav .count, .shell.collapsed .tag-new { display: none; } -.shell.collapsed .home-pill { padding: 0 8px; } -.shell.collapsed .sidebar-top { flex-direction: column; gap: 6px; } -.shell.collapsed .nav button { justify-content: center; padding: 0; } -.shell.collapsed .nav button.active::before { left: -6px; } -.shell.collapsed .newtx .btn { font-size: 0; gap: 0; padding: 0; } -.shell.collapsed .newtx .btn svg { width: 16px; height: 16px; } -.sidebar { transition: width 0.25s var(--ease), padding 0.25s var(--ease); } -.sidebar { width: var(--sidebar); flex: none; background: var(--panel); border-right: 1px solid var(--border); padding: 12px 10px 16px; display: flex; flex-direction: column; gap: 4px; position: sticky; top: 0; height: 100vh; overflow-y: auto; } -.home-pill { display: inline-flex; align-items: center; gap: 10px; height: 40px; padding: 0 14px 0 8px; border-radius: 6px; font-family: "Space Grotesk", sans-serif; background: var(--accent-bg); color: var(--text); font-weight: 700; cursor: pointer; border: 0; align-self: flex-start; transition: background 0.15s; } -.home-pill:hover { background: var(--accent-bg-2); } -.home-pill .mark { width: 24px; height: 24px; border-radius: 4px; background: var(--accent); color: var(--accent-contrast); display: grid; place-items: center; font-weight: 800; font-size: 13px; } -.sidebar-top { display: flex; align-items: center; justify-content: space-between; padding: 4px 4px 8px; } -.newtx { margin: 4px 0 16px; } -.newtx .btn { width: 100%; justify-content: center; border-color: var(--border-2); background: transparent; color: var(--text-2); font-weight: 500; } -.newtx .btn:not(:disabled) { color: var(--text); border-color: var(--accent); } -.newtx .btn:hover:not(:disabled) { background: var(--accent); color: var(--accent-contrast); } -.nav { display: flex; flex-direction: column; gap: 2px; position: relative; } -.nav button { display: flex; align-items: center; gap: 12px; width: 100%; text-align: left; height: 38px; padding: 0 12px; border-radius: var(--radius-sm); border: 0; background: transparent; color: var(--text); font-size: 14px; font-weight: 500; cursor: pointer; position: relative; transition: background 0.15s, color 0.15s; } -.nav button:hover { background: var(--panel-2); } -.nav button.active { background: var(--accent-bg); font-weight: 700; } -.nav button.active::before { content: ""; position: absolute; left: -10px; top: 8px; bottom: 8px; width: 3px; border-radius: 2px; background: var(--accent); animation: grow 0.3s var(--ease); transform-origin: center; } -.nav button .ico { width: 18px; height: 18px; color: var(--text-2); display: inline-grid; place-items: center; transition: color 0.15s, transform 0.2s var(--ease); } -.nav button:hover .ico { transform: translateX(2px); } -.nav button.active .ico { color: var(--text); } -[data-theme="dark"] .nav button.active .ico { color: var(--accent); } -.nav .count { margin-left: auto; font-size: 11px; font-weight: 700; padding: 1px 7px; border-radius: 999px; background: var(--review-bg); color: var(--review); } -.nav-group { font-size: 12px; color: var(--text-2); padding: 14px 12px 6px; } -.sidebar-bottom { margin-top: auto; display: flex; flex-direction: column; gap: 2px; } -.promo { border-radius: 10px; padding: 16px; background: linear-gradient(180deg, var(--brand-bg) 0%, var(--panel) 100%); margin: 0 4px 12px; animation: pop 0.4s var(--ease); } -.promo .tag { display: inline-block; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 999px; background: var(--accent); color: var(--accent-contrast); margin-bottom: 8px; } -.promo h4 { font-size: 14px; margin-bottom: 4px; } -.promo p { font-size: 12px; color: var(--text-2); margin-bottom: 10px; } -.tag-new { display: inline-flex; align-items: center; height: 18px; padding: 0 7px; border-radius: 999px; background: var(--accent); color: var(--accent-contrast); font-size: 11px; font-weight: 700; } - -.main { flex: 1; min-width: 0; padding: 20px 24px 48px; } -.main-inner { max-width: 1160px; } -.header-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 24px; flex-wrap: wrap; animation: fade 0.4s var(--ease); } -.acct-pill { display: inline-flex; align-items: center; gap: 12px; height: 56px; padding: 0 16px 0 8px; border-radius: 10px; background: var(--panel); box-shadow: var(--shadow-1); cursor: pointer; border: 0; color: var(--text); text-align: left; max-width: 100%; transition: box-shadow 0.2s; } -.acct-pill:hover { box-shadow: var(--shadow-2); } -.acct-pill .name { font-size: 14px; font-weight: 700; } -.acct-pill .addr { font-family: "DM Mono", ui-monospace, monospace; font-size: 11.5px; color: var(--text-2); } -.acct-pill .addr b { color: var(--text); font-weight: 600; } -.acct-pill .meta { display: inline-flex; align-items: center; gap: 12px; margin-left: 8px; padding-left: 12px; border-left: 1px solid var(--border); font-size: 14px; } -.acct-pill .meta .chip-s { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 6px; background: var(--panel-2); font-size: 12px; color: var(--text-2); } -.tools-pill { display: inline-flex; align-items: center; gap: 6px; height: 56px; padding: 0 10px; border-radius: 10px; background: var(--panel); box-shadow: var(--shadow-1); } -.tools-pill .btn { height: 36px; border-radius: 6px; } -.tools-pill .btn-secondary { border-color: var(--border-2); color: var(--text-2); } -.tools-pill .btn-secondary.on { color: var(--text); } - -/* ================================================================== cards, lists, tables */ -.card { background: var(--panel); border-radius: 10px; padding: 24px; margin-bottom: 24px; box-shadow: var(--shadow-1); } -.card.flush { padding: 0; overflow: hidden; } -.card.flush .card-head { padding: 20px 24px 8px; } -.card-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; } -.card-head h4 { font-size: 16px; } -.card-head .sub { color: var(--text-2); font-size: 14px; margin-top: 4px; } -.card-actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; } -.link { color: var(--text); font-weight: 500; cursor: pointer; background: none; border: 0; padding: 0; font-size: 14px; display: inline-flex; align-items: center; gap: 4px; } -.link:hover { text-decoration: underline; } -.link.accent { color: var(--text); font-weight: 700; box-shadow: inset 0 -6px 0 var(--accent); } -[data-theme="dark"] .link.accent { color: var(--accent); box-shadow: none; } -.grid { display: grid; gap: 24px; } -.grid-2 { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); } -.grid-3 { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); } -.overview-grid { display: grid; grid-template-columns: minmax(0, 2fr) minmax(320px, 1fr); gap: 24px; align-items: start; } -@media (max-width: 1000px) { .overview-grid { grid-template-columns: 1fr; } } - -.balance-card .label { font-size: 15px; font-weight: 500; margin-bottom: 4px; color: var(--text-2); } -.balance-card .amount { font-size: 42px; line-height: 46px; font-weight: 800; letter-spacing: -0.03em; } -.balance-card .amount .dec { color: var(--text-3); font-weight: 600; } -.balance-card .actions { display: flex; gap: 8px; flex-wrap: wrap; } -.balance-card .actions .btn { background: var(--panel-2); color: var(--text); border: 0; height: 38px; font-weight: 600; } -.balance-card .actions .btn.hi { background: var(--brand); color: var(--brand-contrast); } -.balance-card .actions .btn:hover:not(:disabled) { background: var(--brand-bg-2); } -.balance-card .actions .btn.hi:hover:not(:disabled) { background: var(--brand-hover); } -.balance-row { display: flex; justify-content: space-between; align-items: flex-end; gap: 16px; flex-wrap: wrap; } - -.banner { border-radius: 10px; padding: 20px 24px; display: flex; gap: 20px; align-items: center; margin-bottom: 24px; background: linear-gradient(90deg, #dee2e6 0%, #e9ecef 60%, #f1f3f5 100%); color: #212529; position: relative; overflow: hidden; } -[data-theme="dark"] .banner { background: linear-gradient(90deg, #343a40 0%, #2b3035 70%); color: #f8f9fa; } -.banner .glyph { width: 52px; height: 52px; border-radius: 50%; background: var(--accent); color: var(--accent-contrast); display: grid; place-items: center; flex: none; animation: pulse-ring 2.4s ease-out infinite; } -.banner h4 { font-size: 18px; margin-bottom: 4px; } -.banner p { color: var(--text-2); margin-bottom: 8px; } -.banner .cta { font-weight: 700; color: var(--text); display: inline-flex; align-items: center; gap: 6px; cursor: pointer; background: none; border: 0; padding: 0; font-size: 14px; } -.banner .cta svg { transition: transform 0.2s var(--ease); } -.banner .cta:hover svg { transform: translateX(4px); } -.banner.warn { background: linear-gradient(90deg, #ffe5b4 0%, #fff3cd 70%); color: #212529; } -[data-theme="dark"] .banner.warn { background: linear-gradient(90deg, #4d3a05 0%, #2b3035 70%); color: #f8f9fa; } -.banner.warn .glyph { background: var(--warning); color: #212529; } -.banner .close { margin-left: auto; align-self: flex-start; background: none; border: 0; cursor: pointer; color: inherit; opacity: 0.6; } -.banner .close:hover { opacity: 1; } - -.list { list-style: none; margin: 0; padding: 0; } -.list > li { display: flex; align-items: center; gap: 14px; padding: 14px 24px; border-top: 1px solid var(--border); transition: background 0.15s; } -.list > li:hover { background: var(--panel-3); } -.list > li:first-child { border-top: 0; } -.list .grow { flex: 1; min-width: 0; } -.list .title { font-weight: 700; } -.list .sub { font-size: 12px; color: var(--text-2); } -.list .right { text-align: right; } -.token-ico { width: 32px; height: 32px; border-radius: 50%; display: inline-grid; place-items: center; flex: none; background: var(--panel-2); color: var(--text); font-weight: 700; font-size: 12px; } -.token-ico.stable { background: #2775ca; color: #fff; } -.token-ico.native { background: var(--accent); color: var(--accent-contrast); } -table { width: 100%; border-collapse: collapse; } -th, td { text-align: left; padding: 14px 24px; border-top: 1px solid var(--border); vertical-align: middle; } -thead th { border-top: 0; color: var(--text-2); font-weight: 500; font-size: 13px; } -tbody tr { transition: background 0.15s; } -tbody tr:hover { background: var(--panel-3); } -td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; } -.card:not(.flush) table th, .card:not(.flush) table td { padding-left: 0; padding-right: 0; } -.explore { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 12px; } -.explore button { border: 1px solid var(--border); border-radius: 10px; background: var(--panel-3); color: var(--text); padding: 20px 16px; text-align: left; font-weight: 700; font-size: 15px; cursor: pointer; display: flex; flex-direction: column; gap: 20px; transition: transform 0.25s var(--ease), border-color 0.25s, box-shadow 0.25s; } -.explore button:hover { border-color: var(--accent); transform: translateY(-3px); box-shadow: var(--shadow-2); } -.explore .ico { width: 56px; height: 56px; color: var(--text); transition: transform 0.3s var(--ease); } -[data-theme="dark"] .explore .ico { color: var(--accent); } -.explore button { position: relative; overflow: hidden; } -.explore button::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 4px; background: var(--accent); transform: scaleX(0); transform-origin: left; transition: transform 0.3s var(--ease); } -.explore button:hover::after { transform: scaleX(1); } -.explore button:hover .ico { transform: scale(1.08) rotate(-4deg); } - -/* ================================================================== pills */ -.pill { display: inline-flex; align-items: center; gap: 6px; height: 24px; padding: 0 10px; border-radius: 6px; font-size: 12px; font-weight: 700; white-space: nowrap; } -.pill-neutral { background: var(--panel-2); color: var(--text-2); } -.pill-ok { background: var(--success-bg); color: var(--success-text); } -.pill-warn { background: var(--warning-bg); color: var(--warning-text); } -.pill-bad { background: var(--error-bg); color: var(--error-text); } -.pill-info { background: var(--info-bg); color: var(--info-text); } -.pill-review { background: var(--review-bg); color: var(--review); } -[data-theme="light"] .pill-review, :root:not([data-theme="dark"]) .pill-review { color: #7a5c00; } -.pill-brand { background: var(--accent); color: var(--accent-contrast); } -.pill svg { width: 12px; height: 12px; } - -/* ================================================================== tabs */ -.tabs { display: flex; gap: 8px; border-bottom: 1px solid var(--border); margin-bottom: 20px; overflow-x: auto; } -.tabs button { background: none; border: 0; padding: 10px 16px; font-size: 14px; font-weight: 700; cursor: pointer; color: var(--text-2); border-bottom: 2px solid transparent; margin-bottom: -1px; display: inline-flex; gap: 8px; align-items: center; white-space: nowrap; transition: color 0.15s, border-color 0.15s; } -.tabs button:hover { color: var(--text); } -.tabs button.active { color: var(--text); border-bottom-color: var(--accent); } -.tabs-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; border-bottom: 1px solid var(--border); margin-bottom: 20px; } -.tabs-row .tabs { margin-bottom: 0; border-bottom: 0; } - -/* ================================================================== transactions */ -.date-label { font-size: 12px; font-weight: 500; color: var(--text-2); letter-spacing: 0.06em; text-transform: uppercase; margin: 20px 0 10px; } -.txcard { background: var(--panel); border-radius: 10px; margin-bottom: 8px; box-shadow: var(--shadow-1); animation: rise 0.4s var(--ease) both; } -.txcard .row { display: grid; grid-template-columns: 24px minmax(120px, 1fr) minmax(0, 1.4fr) 110px auto 24px; align-items: center; gap: 16px; padding: 18px 20px; cursor: pointer; border-radius: 10px; transition: background 0.15s; } -.txcard .row:hover { background: var(--panel-3); } -.txcard .kind { font-weight: 600; } -.txcard .amt { display: inline-flex; align-items: center; gap: 10px; font-weight: 700; } -.txcard .when { color: var(--text-2); font-size: 13px; } -.txcard .status { text-align: right; font-weight: 700; } -.status-success { color: var(--success); } -.status-cancel { color: var(--text-2); } -.status-veto { color: var(--error); } -.txcard .detail { padding: 8px 20px 20px 60px; border-top: 1px solid var(--border); display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(260px, 1fr); gap: 24px; animation: rise 0.3s var(--ease); } -@media (max-width: 900px) { .txcard .row { grid-template-columns: 24px 1fr auto 24px; } .txcard .hide-sm { display: none; } .txcard .detail { grid-template-columns: 1fr; padding-left: 20px; } } -.chev { transition: transform 0.2s var(--ease); color: var(--text-2); } -.chev.open { transform: rotate(180deg); } -.timeline { list-style: none; margin: 0; padding: 0; } -.timeline li { position: relative; padding: 0 0 16px 24px; } -.timeline li::before { content: ""; position: absolute; left: 4px; top: 5px; width: 10px; height: 10px; border-radius: 50%; background: var(--border-2); transition: background 0.3s; } -.timeline li.done::before { background: var(--accent); } -.timeline li.active::before { background: var(--accent); box-shadow: 0 0 0 4px var(--accent-bg); animation: pulse-ring 2s ease-out infinite; } -.timeline li:not(:last-child)::after { content: ""; position: absolute; left: 8px; top: 16px; bottom: 0; width: 2px; background: var(--border); } -.timeline .t-title { font-weight: 700; } -.timeline .t-sub { font-size: 12px; color: var(--text-2); } -.timeline .signers { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; font-size: 12px; } -.tx-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 16px; } -.empty-illus { padding: 64px 24px; text-align: center; color: var(--text-2); animation: fade 0.5s var(--ease); } -.empty-illus .glyph { width: 96px; height: 96px; border-radius: 50%; background: var(--panel-2); display: grid; place-items: center; margin: 0 auto 16px; color: var(--accent); animation: float 4s ease-in-out infinite; } - -/* ================================================================== sections (settings-style) */ -.section { background: var(--panel); border-radius: 10px; padding: 28px 32px; margin-bottom: 20px; display: grid; grid-template-columns: 240px minmax(0, 1fr); gap: 24px; box-shadow: var(--shadow-1); } -.section > h3 { font-size: 20px; } -.section > h3 small { display: block; font-size: 13px; font-weight: 400; color: var(--text-2); margin-top: 6px; line-height: 18px; } -.section .body { min-width: 0; } -.section .body h4 { margin-bottom: 8px; } -.section .body p.desc { color: var(--text-2); margin-bottom: 16px; font-size: 15px; } -@media (max-width: 860px) { .section { grid-template-columns: 1fr; } } -.member-row { display: flex; align-items: center; gap: 14px; padding: 12px 0; border-top: 1px solid var(--border); } -.member-row:first-of-type { border-top: 0; } -.member-row .addr { font-family: "DM Mono", ui-monospace, monospace; font-size: 13px; } -.member-row .addr b { font-family: "DM Sans", sans-serif; font-weight: 700; } -.member-row .roles { display: flex; gap: 6px; flex-wrap: wrap; } -.member-row .tools { margin-left: auto; display: flex; gap: 4px; } -.kv { display: grid; grid-template-columns: 1fr; margin: 0; } -.kv div { display: flex; justify-content: space-between; gap: 16px; padding: 10px 0; border-top: 1px solid var(--border); font-size: 14px; } -.kv div:first-child { border-top: 0; } -.kv dt { color: var(--text-2); } -.kv dd { margin: 0; text-align: right; font-weight: 500; } -.addr { display: inline-flex; align-items: center; gap: 6px; color: var(--text); } -.addr code { font-size: 12.5px; } -.addr:hover code { text-decoration: underline; } - -/* policy editor controls */ -.pfield { display: grid; grid-template-columns: minmax(0, 1fr) 160px; gap: 16px; align-items: center; padding: 14px 0; border-top: 1px solid var(--border); } -.pfield:first-child { border-top: 0; } -.pfield .lbl { font-weight: 600; } -.pfield .hint { font-size: 12px; color: var(--text-2); margin-top: 2px; } -.pfield input, .pfield select { height: 40px; } -.pfield .unit { display: flex; align-items: center; gap: 8px; } -.pfield .unit span { color: var(--text-2); font-size: 12px; white-space: nowrap; } -.pfield.changed .lbl::after { content: " •"; color: var(--warning); } -.stepper { display: inline-flex; align-items: center; border: 1px solid var(--border-2); border-radius: 8px; overflow: hidden; height: 40px; } -.stepper button { width: 36px; height: 100%; border: 0; background: var(--panel-2); color: var(--text); cursor: pointer; font-size: 16px; } -.stepper button:hover { background: var(--brand-bg-2); } -.stepper input { border: 0; width: 64px; text-align: center; height: 100%; border-radius: 0; } -.mode-hero { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; } -.mode-hero > div { border: 1px solid var(--border); border-radius: 10px; padding: 18px; position: relative; transition: border-color 0.25s, transform 0.25s var(--ease); } -.mode-hero > div.on { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); transform: translateY(-2px); } -.mode-hero .m { font-size: 18px; font-weight: 800; margin-bottom: 4px; } -.mode-hero .d { color: var(--text-2); font-size: 13px; } -.mode-hero .cur { position: absolute; top: 12px; right: 12px; } -@media (max-width: 860px) { .mode-hero { grid-template-columns: 1fr; } } -.gauge { display: flex; align-items: center; gap: 16px; } -.gauge .ring { width: 72px; height: 72px; border-radius: 50%; display: grid; place-items: center; font-weight: 800; font-size: 18px; background: conic-gradient(var(--accent) calc(var(--pct) * 1%), var(--panel-2) 0); position: relative; } -.gauge .ring::after { content: ""; position: absolute; inset: 8px; border-radius: 50%; background: var(--panel); } -.gauge .ring span { position: relative; z-index: 1; font-size: 15px; } -.scorecard { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 20px; } -.scorecard > div { background: var(--panel); border-radius: 10px; padding: 16px 18px; box-shadow: var(--shadow-1); } -.scorecard .v { font-size: 26px; font-weight: 800; letter-spacing: -0.02em; } -.scorecard .l { font-size: 12px; color: var(--text-2); } - -/* ================================================================== forms */ -.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; } -.field-label { font-size: 14px; font-weight: 700; } -.field-hint { font-size: 12px; color: var(--text-2); } -input, select, textarea { height: 48px; padding: 0 14px; border: 1px solid var(--border-2); border-radius: var(--radius-sm); background: var(--panel); color: var(--text); width: 100%; font-size: 14px; transition: border-color 0.15s, box-shadow 0.15s; } -select option { background: var(--panel); color: var(--text); } -input[type="checkbox"] { height: auto; width: auto; accent-color: var(--accent); } -input[type="range"] { height: auto; padding: 0; border: 0; accent-color: var(--accent); } -input:focus, select:focus, textarea:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } -input:read-only { color: var(--text-2); } -.row { display: flex; gap: 16px; flex-wrap: wrap; align-items: flex-end; } -.row > .field { flex: 1; min-width: 180px; } -.inline { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } -.stack { display: flex; flex-direction: column; gap: 8px; } -.notice { padding: 14px 16px; border-radius: var(--radius-sm); margin: 12px 0; font-size: 14px; display: flex; gap: 10px; align-items: flex-start; animation: rise 0.3s var(--ease); } -.notice-ok { background: var(--success-bg); color: var(--success-text); } -.notice-bad { background: var(--error-bg); color: var(--error-text); } -.notice-warn { background: var(--warning-bg); color: var(--warning-text); } -.notice-info { background: var(--info-bg); color: var(--info-text); } -.empty { padding: 40px 24px; text-align: center; color: var(--text-2); } -.switch { display: inline-flex; align-items: center; gap: 10px; cursor: pointer; } -.switch .track { width: 40px; height: 22px; border-radius: 999px; background: var(--border-2); position: relative; transition: background 0.2s; } -.switch .track::after { content: ""; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: #fff; transition: transform 0.2s var(--ease); } -.switch.on .track { background: var(--accent); } -.switch.on .track::after { transform: translateX(18px); } -.segmented { display: inline-flex; padding: 4px; border-radius: 999px; background: var(--panel); gap: 4px; box-shadow: var(--shadow-1); } -.segmented button { border: 0; background: transparent; color: var(--text-2); font-weight: 700; padding: 10px 28px; border-radius: 999px; cursor: pointer; transition: background 0.2s, color 0.2s; } -.segmented button.active { background: var(--brand); color: var(--brand-contrast); } -.review { border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--panel-3); padding: 4px 16px; } -.review-line { display: flex; justify-content: space-between; gap: 16px; padding: 10px 0; border-top: 1px solid var(--border); } -.review-line:first-child { border-top: 0; } -.review-line > span:first-child { color: var(--text-2); flex: none; } -.review-line > *:last-child { text-align: right; } -.reasons { margin: 8px 0 0; padding-left: 18px; color: var(--text-2); } -.bar { height: 6px; background: var(--panel-2); border-radius: 3px; overflow: hidden; margin-top: 8px; } -.bar > i { display: block; height: 100%; background: var(--accent); transform-origin: left; animation: grow 0.8s var(--ease); } -.bar.warn > i { background: var(--warning); } -.bar.bad > i { background: var(--error); } -.templates { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; } -.template { border: 1px solid var(--border); border-radius: 10px; padding: 16px; cursor: pointer; background: var(--panel-3); text-align: left; color: var(--text); transition: border-color 0.2s, transform 0.2s var(--ease), box-shadow 0.2s; } -.template:hover { border-color: var(--border-2); transform: translateY(-2px); } -.template.active { border-color: var(--accent); background: var(--accent-bg); box-shadow: 0 0 0 3px var(--accent-soft); } -.template strong { display: block; margin-bottom: 4px; } -.scenario { border-radius: 10px; padding: 20px 24px; margin-bottom: 12px; background: var(--panel); border-left: 4px solid var(--border-2); box-shadow: var(--shadow-1); transition: transform 0.25s var(--ease), box-shadow 0.25s; } -.scenario:hover { transform: translateX(4px); box-shadow: var(--shadow-2); } -.scenario.blocked, .scenario.impossible { border-left-color: var(--success); } -.scenario.delayed { border-left-color: var(--info); } -.scenario.escalated { border-left-color: var(--warning); } -.scenario.allowed { border-left-color: var(--error); } -.scenario ul { margin: 8px 0 0; padding-left: 18px; color: var(--text-2); } -.posture { list-style: none; margin: 0; padding: 0; } -.posture li { display: flex; gap: 10px; align-items: baseline; padding: 6px 0; } -.posture .dot { width: 8px; height: 8px; border-radius: 50%; flex: none; background: var(--text-3); } -.posture .dot.ok { background: var(--success); } -.posture .dot.warn { background: var(--warning); } -.posture .dot.bad { background: var(--error); } -.assumptions { font-size: 12px; color: var(--text-2); margin: 12px 0 0; padding-left: 18px; } - -/* modal */ -.modal-backdrop { position: fixed; inset: 0; background: rgba(33, 37, 41, 0.55); backdrop-filter: blur(4px); z-index: 50; display: flex; align-items: flex-start; justify-content: center; padding: 48px 16px; overflow-y: auto; animation: fade 0.2s var(--ease); } -.modal { width: 100%; max-width: 640px; background: var(--panel); border-radius: 14px; box-shadow: 0 24px 64px rgba(0,0,0,0.35); animation: pop 0.3s var(--ease); } -.modal-head { display: flex; align-items: center; justify-content: space-between; padding: 20px 24px; border-bottom: 1px solid var(--border); } -.modal-body { padding: 24px; } -.modal-foot { display: flex; justify-content: space-between; gap: 8px; padding: 16px 24px; border-top: 1px solid var(--border); } -.steps { display: flex; gap: 8px; align-items: center; font-size: 12px; color: var(--text-2); } -.steps .step { display: inline-flex; align-items: center; gap: 6px; } -.steps .n { width: 20px; height: 20px; border-radius: 50%; display: inline-grid; place-items: center; background: var(--panel-2); font-weight: 700; font-size: 11px; transition: background 0.2s, color 0.2s; } -.steps .step.on .n { background: var(--accent); color: var(--accent-contrast); } -.steps .step.on { color: var(--text); font-weight: 700; } - -/* ================================================================== welcome / create */ -.entry { min-height: 100vh; background: radial-gradient(60% 40% at 50% 28%, var(--panel-2) 0%, var(--canvas) 60%); display: flex; flex-direction: column; } -.entry-top { display: flex; justify-content: space-between; align-items: center; padding: 16px 24px; animation: fade 0.4s var(--ease); } -.entry-top .logo { display: inline-flex; align-items: center; gap: 8px; font-weight: 800; font-size: 18px; } -.entry-top .logo .mark { width: 28px; height: 28px; border-radius: 7px; background: var(--accent); color: var(--accent-contrast); display: grid; place-items: center; font-weight: 800; } -.entry-center { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 40px 16px; gap: 16px; } -.entry-card { width: 100%; max-width: 440px; background: var(--panel); border-radius: 16px; padding: 32px; text-align: center; box-shadow: var(--shadow-2); animation: pop 0.45s var(--ease) both; } -.entry-card h2 { font-size: 24px; margin: 12px 0 8px; } -.entry-card p { color: var(--text-2); margin-bottom: 20px; } -.entry-card .or { display: flex; align-items: center; gap: 12px; color: var(--text-2); font-size: 12px; margin: 16px 0; } -.entry-card .or::before, .entry-card .or::after { content: ""; flex: 1; height: 1px; background: var(--border-2); } -.entry-note { max-width: 440px; width: 100%; background: var(--panel); border-radius: 10px; padding: 10px 16px; display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 14px; box-shadow: var(--shadow-1); animation: rise 0.4s var(--ease) 0.1s both; } -.entry-foot { display: flex; gap: 16px; justify-content: center; padding: 24px; color: var(--text-2); font-size: 13px; flex-wrap: wrap; } -.entry-foot a { color: var(--text); font-weight: 700; box-shadow: inset 0 -6px 0 var(--accent); } -[data-theme="dark"] .entry-foot a { color: var(--accent); box-shadow: none; } -.wordmark-lg { font-size: 28px; font-weight: 800; letter-spacing: -0.02em; display: inline-flex; align-items: center; gap: 10px; justify-content: center; } -.wordmark-lg .brace { color: var(--text-2); font-weight: 500; } -.create { max-width: 1160px; margin: 0 auto; padding: 24px; } -.create-grid { display: grid; grid-template-columns: minmax(0, 1fr) 360px; gap: 24px; align-items: start; } -@media (max-width: 960px) { .create-grid { grid-template-columns: 1fr; } } -.stepcard { background: var(--panel); border-radius: 10px; overflow: hidden; box-shadow: var(--shadow-1); animation: rise 0.45s var(--ease); } -.stepcard .progress { height: 4px; background: var(--border); } -.stepcard .progress i { display: block; height: 100%; background: var(--accent); transition: width 0.5s var(--ease); } -.stepcard .step-head { display: flex; gap: 16px; align-items: flex-start; padding: 24px 28px; border-bottom: 1px solid var(--border); } -.stepcard .step-head .n { width: 22px; height: 22px; border-radius: 50%; background: var(--accent); color: var(--accent-contrast); display: grid; place-items: center; font-weight: 800; font-size: 12px; flex: none; margin-top: 2px; } -.stepcard .step-head h3 { font-size: 20px; } -.stepcard .step-head p { color: var(--text-2); margin-top: 2px; } -.stepcard .step-body { padding: 28px; animation: fade 0.3s var(--ease); } -.stepcard .step-foot { display: flex; justify-content: space-between; padding: 20px 28px; border-top: 1px solid var(--border); } -.preview { background: var(--panel); border-radius: 10px; padding: 24px; text-align: center; box-shadow: var(--shadow-1); animation: rise 0.45s var(--ease) 0.1s both; position: sticky; top: 24px; } -.preview h4 { font-size: 18px; margin: 8px 0 16px; } -.preview .kv div { text-align: left; } - -/* ================================================================== marketing site (safe.global) */ -.land { --lt: #212529; --ls: #6c757d; font-size: 16px; line-height: 24px; color: #212529; background: #fff; overflow-x: hidden; } -.land .wrap { max-width: 1216px; margin: 0 auto; padding: 0 24px; } -.land-nav { position: sticky; top: 0; z-index: 30; background: rgba(255,255,255,0.85); backdrop-filter: blur(12px); border-bottom: 1px solid transparent; transition: border-color 0.2s, box-shadow 0.2s; } -.land-nav.scrolled { border-bottom-color: #e9ecef; box-shadow: 0 4px 24px rgba(33,37,41,0.06); } -.land-nav .nav-right { display: flex; align-items: center; gap: 14px; } -.netchip { display: inline-flex; align-items: center; gap: 8px; font-family: "DM Mono", monospace; font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: #6c757d; border: 1px solid #dee2e6; padding: 7px 10px; border-radius: 4px; } -.netchip::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: #ffd000; box-shadow: 0 0 0 3px rgba(255,208,0,0.25); } -.land-nav .wrap { height: 72px; display: flex; align-items: center; justify-content: space-between; gap: 24px; } -.land-nav .logo { display: inline-flex; align-items: center; gap: 10px; font-weight: 700; font-size: 22px; letter-spacing: -0.02em; font-family: "Space Grotesk", sans-serif; } -.land-nav .logo .mark { width: 28px; height: 28px; border-radius: 4px; background: #ffd000; color: #212529; display: grid; place-items: center; font-weight: 800; font-size: 14px; } -.land-nav .logo .brace { color: #6c757d; font-weight: 500; } -.land-nav nav { display: flex; gap: 32px; } -.land-nav nav a { color: #6c757d; font-weight: 500; position: relative; z-index: 0; transition: color 0.15s; } -.land-nav nav a::after { content: ""; position: absolute; left: -4px; right: -4px; bottom: 1px; height: 9px; z-index: -1; background: #ffd000; transform: scaleX(0); transform-origin: left; transition: transform 0.25s var(--ease); } -.land-nav nav a:hover, .land-nav nav a.on { color: #212529; } -.land-nav nav a:hover::after, .land-nav nav a.on::after { transform: scaleX(1); } -.land .announce { background: #212529; color: #fff; text-align: center; padding: 12px 24px; font-size: 14px; } -.land .announce a { font-weight: 500; } -.land .announce a span { color: #ffd000; } -.land .announce a span { display: inline-block; transition: transform 0.2s var(--ease); } -.land .announce a:hover span { transform: translateX(4px); } -.hero { position: relative; overflow: hidden; background: #f8f9fa; padding: 104px 0 0; text-align: center; } -.hero.sub-hero { padding: 88px 0 72px; } -.hero .grid-bg { position: absolute; inset: 0; background-image: linear-gradient(#e9ecef 1px, transparent 1px), linear-gradient(90deg, #e9ecef 1px, transparent 1px); background-size: 131px 131px; } -.hero .cell { position: absolute; width: 130px; height: 130px; background: linear-gradient(180deg, rgba(255,208,0,0) 0%, rgba(255,208,0,0.55) 100%); animation: cell 6s ease-in-out infinite; } -.hero .wrap { position: relative; } -.hero .eyebrow { font-family: "DM Mono", monospace; font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase; color: #6c757d; margin-bottom: 24px; animation: rise 0.6s var(--ease) both; display: flex; align-items: center; justify-content: center; gap: 12px; } -.hero .eyebrow b { background: #ffd000; color: #212529; padding: 3px 7px; font-weight: 500; } -.hero h1 { font-size: 84px; line-height: 84px; letter-spacing: -0.035em; font-weight: 700; max-width: 960px; margin: 0 auto 24px; animation: rise 0.7s var(--ease) 0.08s both; } -.hero h1 .w { display: inline-block; } -.hero .sub { font-size: 19px; line-height: 28px; color: #6c757d; max-width: 600px; margin: 0 auto 32px; animation: rise 0.7s var(--ease) 0.18s both; } -.hero .cta-row { animation: rise 0.7s var(--ease) 0.26s both; display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; } -.hero .mockwrap { position: relative; max-width: 960px; margin: 72px auto 0; text-align: left; animation: rise 0.9s var(--ease) 0.35s both; } -.hero .mock { border-radius: 8px 8px 0 0; box-shadow: 0 -8px 60px rgba(33,37,41,0.18); overflow: hidden; border: 1px solid #dee2e6; border-bottom: 0; } -.hero .float { position: absolute; right: -56px; bottom: 36px; width: 400px; animation: rise 0.9s var(--ease) 0.7s both; } -.hero .float .ticker { box-shadow: 0 30px 80px rgba(33,37,41,0.35), 0 0 0 1px rgba(255,255,255,0.06); } -@media (max-width: 1120px) { .hero .float { position: static; width: auto; margin: 24px 0 0; } .hero .mock { border-bottom: 1px solid #dee2e6; border-radius: 8px; } } -@media (max-width: 900px) { .hero h1 { font-size: 48px; line-height: 50px; } } -.land .light { background: #f8f9fa; padding: 96px 0; } -.land .white { background: #fff; padding: 96px 0; } -.land .dark { background: #212529; color: #fff; padding: 96px 0; } -.land h2 { font-size: 44px; line-height: 48px; letter-spacing: -0.03em; text-align: left; margin-bottom: 12px; font-weight: 700; max-width: 760px; } -.land .lead { text-align: left; color: #adb5bd; font-size: 17px; max-width: 640px; margin: 0 0 56px; } -.land .center h2, .land .center .lead { text-align: center; margin-left: auto; margin-right: auto; } -.land .light .lead, .land .white .lead { color: #6c757d; } -.marquee { overflow: hidden; mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent); } -.marquee .track { display: flex; gap: 12px; width: max-content; animation: marquee 28s linear infinite; } -.marquee .track:hover { animation-play-state: paused; } -.marquee .chip-l { padding: 12px 22px; border-radius: 6px; font-family: "Space Grotesk", sans-serif; border: 1px solid #dee2e6; background: #fff; font-weight: 600; white-space: nowrap; color: #212529; } -.bento { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } -.bento .big { grid-column: 1 / -1; } -.bcard { background: #2b3035; border: 1px solid #343a40; border-radius: 8px; padding: 32px; min-height: 340px; background-image: radial-gradient(60% 40% at 50% 0%, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0) 100%); transition: transform 0.35s var(--ease), border-color 0.35s, box-shadow 0.35s; } -.bcard:hover { transform: translateY(-4px); border-color: #ffd000; box-shadow: 0 24px 60px rgba(0,0,0,0.35); } -.bcard h3 { font-size: 24px; margin-bottom: 8px; } -.bcard p { color: #adb5bd; margin-bottom: 24px; } -.bcard.center { text-align: center; } -.bcard .art { margin-top: 8px; } -@media (max-width: 860px) { .bento { grid-template-columns: 1fr; } } -.dashed { display: grid; grid-template-columns: 1fr 1fr; max-width: 900px; margin: 0 auto; } -.dashed > div { padding: 40px 32px 40px 48px; border-left: 1px dashed #495057; border-top: 1px dashed #495057; } -.dashed > div:nth-child(-n+2) { border-top: 0; } -.dashed > div:nth-child(odd) { border-left: 0; } -.dashed h4 { font-size: 18px; margin-bottom: 8px; } -.dashed p { color: #adb5bd; } -.badges { display: flex; gap: 40px; justify-content: center; align-items: center; flex-wrap: wrap; padding: 40px 0; border-top: 1px solid #343a40; color: #adb5bd; font-weight: 700; font-size: 18px; } -.badges .lbl { font-weight: 400; font-size: 14px; } -.shield { background: #2b3035; border: 1px solid #343a40; border-radius: 8px; padding: 48px; display: grid; grid-template-columns: 1fr 1.2fr; gap: 40px; align-items: center; overflow: hidden; } -.shield h2 { text-align: left; font-size: 40px; margin-bottom: 16px; } -.shield p { color: #adb5bd; margin-bottom: 32px; max-width: 320px; } -.shield ul { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 16px; font-size: 20px; font-weight: 700; } -.shield ul li { display: flex; gap: 12px; align-items: center; } -.shield ul li svg { color: #212529; background: #ffd000; border-radius: 4px; padding: 3px; width: 26px; height: 26px; } -.shield .tilt { transform: perspective(1200px) rotateY(-12deg) rotateX(4deg) rotate(6deg) scale(1.05); transform-origin: left center; border-radius: 10px; overflow: hidden; box-shadow: 0 30px 80px rgba(0,0,0,0.5); transition: transform 0.6s var(--ease); } -.shield:hover .tilt { transform: perspective(1200px) rotateY(-6deg) rotateX(2deg) rotate(3deg) scale(1.03); } -@media (max-width: 900px) { .shield { grid-template-columns: 1fr; } .shield .tilt { transform: none; } } -.compare { background: #2b3035; border: 1px solid #343a40; border-radius: 8px; padding: 12px; max-width: 1000px; margin: 0 auto; overflow-x: auto; } -.compare table { border-collapse: separate; border-spacing: 8px 0; } -.compare th, .compare td { border: 0; padding: 20px 20px; vertical-align: top; background: #343a40; border-radius: 0; font-size: 15px; color: #adb5bd; } -.compare th { font-weight: 500; font-size: 16px; } -.compare td:first-child { color: #fff; font-weight: 500; } -.compare .hl { background: #ffd000; color: #212529 !important; font-weight: 700; } -.compare th.hl { border-radius: 8px 8px 0 0; } -.compare tr:last-child td.hl { border-radius: 0 0 8px 8px; } -.quote { text-align: center; max-width: 960px; margin: 0 auto; } -.quote blockquote { font-size: 36px; line-height: 44px; font-style: italic; color: #dee2e6; margin: 0 0 32px; font-weight: 400; letter-spacing: -0.01em; } -.quote cite { font-style: normal; font-weight: 700; font-size: 20px; } -.stats { display: flex; gap: 96px; justify-content: center; flex-wrap: wrap; margin-top: 56px; } -.stats .v { font-size: 64px; line-height: 68px; color: #ffd000; font-weight: 700; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; } -.stats .l { color: #adb5bd; margin-top: 8px; font-size: 14px; } -.audiences { display: grid; grid-template-columns: 1fr 2fr; gap: 20px; } -.acard { background: #212529; color: #fff; border-radius: 8px; padding: 28px; min-height: 420px; transition: transform 0.35s var(--ease), box-shadow 0.35s; } -.acard:hover { transform: translateY(-4px); box-shadow: 0 24px 60px rgba(33,37,41,0.3); } -.acard h3 { font-size: 22px; margin-bottom: 8px; } -.acard p { color: #adb5bd; margin-bottom: 20px; max-width: 320px; } -.acard a, .acard .link { color: #ffd000; font-weight: 700; } -.acard .art { margin-top: 24px; } -@media (max-width: 860px) { .audiences { grid-template-columns: 1fr; } } -.steps4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; } -.steps4 > div { background: #fff; border: 1px solid #e9ecef; border-radius: 8px; padding: 28px; transition: transform 0.3s var(--ease), box-shadow 0.3s; } -.steps4 > div:hover { transform: translateY(-4px); box-shadow: 0 16px 40px rgba(33,37,41,0.1); } -.steps4 .n { width: 32px; height: 32px; border-radius: 4px; background: #ffd000; color: #212529; font-family: "DM Mono", monospace; display: grid; place-items: center; font-weight: 800; margin-bottom: 20px; } -.steps4 h4 { font-size: 18px; margin-bottom: 8px; } -.steps4 p { color: #6c757d; } -@media (max-width: 900px) { .steps4 { grid-template-columns: 1fr 1fr; } } -.feat6 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; } -.feat6 > div { background: #fff; border: 1px solid #e9ecef; border-radius: 8px; padding: 28px; transition: transform 0.3s var(--ease), box-shadow 0.3s; } -.feat6 > div:hover { transform: translateY(-4px); box-shadow: 0 16px 40px rgba(33,37,41,0.1); } -.feat6 .ico { width: 44px; height: 44px; border-radius: 10px; background: #212529; color: #fff; display: grid; place-items: center; margin-bottom: 20px; } -.feat6 h4 { font-size: 18px; margin-bottom: 8px; } -.feat6 p { color: #6c757d; } -.dark .feat6 > div { background: #2b3035; border-color: #343a40; } -.dark .feat6 .ico { background: #ffd000; color: #212529; } -.dark .feat6 p { color: #adb5bd; } -@media (max-width: 900px) { .feat6 { grid-template-columns: 1fr; } } -.faq { max-width: 770px; margin: 0 auto; } -.faq details { border-bottom: 1px solid #e9ecef; } -.faq summary { list-style: none; cursor: pointer; display: flex; justify-content: space-between; align-items: center; padding: 20px 0; font-weight: 700; font-size: 17px; } -.faq summary::-webkit-details-marker { display: none; } -.faq summary .plus { width: 24px; height: 24px; border-radius: 4px; border: 1.5px solid #6c757d; display: grid; place-items: center; color: #6c757d; font-weight: 400; transition: transform 0.3s var(--ease), background 0.2s, color 0.2s; } -.faq details[open] summary .plus { transform: rotate(45deg); background: #ffd000; color: #212529; border-color: #ffd000; } -.faq .ans { padding: 0 0 20px; color: #6c757d; max-width: 680px; animation: rise 0.3s var(--ease); } -.cta { position: relative; overflow: hidden; background: #fff; background-image: radial-gradient(#d9dde2 1.2px, transparent 1.2px); background-size: 26px 26px; padding: 120px 0; text-align: center; } -.cta h2 { font-size: 56px; line-height: 60px; text-align: center; margin: 0 auto 12px; } -.cta h2 span { color: #6c757d; } -.cta .sub { color: #6c757d; font-size: 18px; margin: 20px 0 32px; } -.land-footer { background: #212529; color: #fff; padding: 72px 0 0; overflow: hidden; } -.land-footer .cols { display: grid; grid-template-columns: 2fr repeat(5, 1fr); gap: 24px; padding-bottom: 56px; border-bottom: 1px solid #343a40; } -.land-footer h5 { margin: 0 0 16px; font-size: 14px; color: #adb5bd; font-weight: 700; } -.land-footer ul { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 12px; font-size: 15px; } -.land-footer ul a { transition: color 0.15s; } -.land-footer ul a:hover { color: #adb5bd; } -.land-footer .legal { display: flex; justify-content: space-between; padding: 32px 0; color: #6c757d; font-size: 14px; } -.land-footer .giant { font-size: 30vw; line-height: 0.8; font-weight: 800; letter-spacing: -0.05em; color: transparent; -webkit-text-stroke: 1.5px #ffd000; text-align: left; margin-top: 24px; user-select: none; } -@media (max-width: 900px) { .land-footer .cols { grid-template-columns: 1fr 1fr; } .land-nav nav { display: none; } } - -/* mini app mockup used on the marketing pages */ -.mock-app { background: #212529; color: #fff; font-size: 12px; display: grid; grid-template-columns: 180px 1fr; min-height: 420px; } -.mock-app .sb { background: #2b3035; padding: 12px; border-right: 1px solid #343a40; } -.mock-app .sb .pill { height: 26px; border-radius: 4px; background: #3d3400; padding: 0 10px; font-weight: 700; margin-bottom: 8px; } -.mock-app .sb .ntx { border: 1px solid #6c757d; border-radius: 6px; padding: 6px; color: #adb5bd; text-align: center; margin-bottom: 12px; } -.mock-app .sb .it { padding: 6px 8px; color: #fff; border-radius: 6px; } -.mock-app .sb .it.on { background: #3d3400; font-weight: 700; } -.mock-app .ct { padding: 14px; display: grid; gap: 12px; grid-template-columns: 1.7fr 1fr; align-items: start; } -.mock-app .acct { grid-column: 1 / -1; background: #2b3035; border-radius: 10px; padding: 8px 12px; display: flex; align-items: center; gap: 10px; } -.mock-app .pn { background: #2b3035; border-radius: 10px; padding: 14px; } -.mock-app .big { font-size: 26px; font-weight: 800; letter-spacing: -0.02em; } -.mock-app .big span { color: #6c757d; } -.mock-app .mb { display: inline-block; background: #343a40; border-radius: 6px; padding: 4px 10px; margin-right: 6px; margin-top: 8px; } -.mock-app .li { display: flex; justify-content: space-between; padding: 8px 0; border-top: 1px solid #343a40; } -.mock-app .ok { color: #75b798; font-weight: 700; } -.mock-app .rv { color: #ffcd39; font-weight: 700; } -.mock-app .ident { width: 24px; height: 24px; border-radius: 50%; background: conic-gradient(#ffd000, #6c757d, #adb5bd, #ffd000); } - -@media (max-width: 860px) { - .shell { flex-direction: column; } - .sidebar { width: auto; height: auto; position: static; flex-direction: row; flex-wrap: wrap; align-items: center; gap: 6px; padding: 10px 12px; } - .newtx { margin: 0; flex: 1 1 100%; } - .nav { flex-direction: row; flex-wrap: wrap; } - .nav button.active::before { display: none; } - .nav-group, .sidebar-bottom { display: none; } - .main { padding: 16px; } -} - -/* ================================================================== identity: tape, section index, marker, live scorer, mode strip */ -.tape { height: 10px; background: repeating-linear-gradient(-45deg, #ffd000 0 16px, #212529 16px 32px); background-size: 45.25px 100%; } -.tape.thin { height: 6px; } -.tape.run { animation: tape 0.9s linear infinite; } -@keyframes tape { to { background-position: 45.25px 0; } } -.secno { display: flex; align-items: center; gap: 12px; font-family: "DM Mono", monospace; font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase; color: #6c757d; margin-bottom: 18px; } -.secno::before { content: ""; width: 28px; height: 3px; background: #ffd000; flex: none; } -.dark .secno { color: #adb5bd; } -.mark-hl { background-image: linear-gradient(180deg, transparent 60%, #ffd000 60%, #ffd000 92%, transparent 92%); background-repeat: no-repeat; background-size: 0% 100%; -webkit-box-decoration-break: clone; box-decoration-break: clone; animation: hlin 0.8s var(--ease) 0.7s forwards; } -@keyframes hlin { to { background-size: 100% 100%; } } -.hero-meta { margin-top: 32px; display: flex; gap: 24px; flex-wrap: wrap; justify-content: center; font-family: "DM Mono", monospace; font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: #6c757d; animation: rise 0.7s var(--ease) 0.4s both; } -.hero-meta span { display: inline-flex; align-items: center; gap: 8px; } -.hero-meta span::before { content: ""; width: 8px; height: 3px; background: #ffd000; } - -.ticker { background: #212529; color: #fff; border-radius: 8px; box-shadow: 0 30px 80px rgba(33,37,41,0.28); overflow: hidden; font-size: 14px; } -.ticker .head { display: flex; justify-content: space-between; padding: 14px 18px; border-bottom: 1px solid #343a40; font-family: "DM Mono", monospace; font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase; color: #adb5bd; } -.ticker .head b { color: #ffd000; font-weight: 500; } -.ticker .body { padding: 20px 18px 16px; display: grid; gap: 14px; } -.ticker .row { display: flex; justify-content: space-between; gap: 12px; align-items: baseline; } -.ticker .row > span:first-child { color: #adb5bd; font-size: 13px; } -.ticker .amt { font-family: "Space Grotesk", sans-serif; font-size: 30px; font-weight: 700; letter-spacing: -0.02em; } -.ticker .to { font-family: "DM Mono", monospace; font-size: 12.5px; color: #dee2e6; } -.ticker .prog { height: 6px; background: #343a40; border-radius: 3px; overflow: hidden; } -.ticker .prog i { display: block; height: 100%; background: #ffd000; width: 0; } -.ticker .prog.on i { animation: growx 1.1s var(--ease) both; } -.ticker .prog.done i { width: 100%; } -@keyframes growx { from { width: 0; } to { width: 100%; } } -.ticker .verdict { display: flex; align-items: center; gap: 10px; min-height: 34px; flex-wrap: wrap; } -.ticker .tier { font-family: "DM Mono", monospace; font-size: 11px; letter-spacing: 0.1em; padding: 7px 10px; border-radius: 4px; font-weight: 500; color: #212529; animation: pop 0.3s var(--ease) both; } -.ticker .tier.low { background: #ffd000; } -.ticker .tier.high { background: #ffa94d; } -.ticker .tier.critical { background: #ea868f; } -.ticker .tier.blocked { background: #fff; } -.ticker .scoring { color: #adb5bd; font-family: "DM Mono", monospace; font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; } -.ticker .why { list-style: none; padding: 0; margin: 0; display: grid; gap: 7px; color: #dee2e6; font-size: 13px; min-height: 66px; } -.ticker .why li { animation: rise 0.4s var(--ease) both; padding-left: 18px; position: relative; } -.ticker .why li::before { content: ""; position: absolute; left: 0; top: 8px; width: 9px; height: 3px; background: #ffd000; } -.ticker .foot { padding: 12px 18px; border-top: 1px solid #343a40; font-size: 12px; color: #adb5bd; display: flex; justify-content: space-between; gap: 12px; } -.ticker .dots { display: flex; gap: 6px; } -.ticker .dots i { width: 18px; height: 3px; background: #495057; } -.ticker .dots i.on { background: #ffd000; } -.mode-strip { height: 6px; margin: -20px -24px 16px; background: transparent; transition: background 0.3s; } -.mode-strip.elevated { background: #ffd000; } -.mode-strip.tape { background: repeating-linear-gradient(-45deg, #ffd000 0 16px, #212529 16px 32px); background-size: 45.25px 100%; } -@media (prefers-reduced-motion: reduce) { .tape.run, .mark-hl, .ticker .prog.on i, .ticker .why li, .ticker .tier { animation: none; } .mark-hl { background-size: 100% 100%; } .ticker .prog.on i { width: 100%; } } diff --git a/web/src/wagmi.ts b/web/src/wagmi.ts deleted file mode 100644 index 50bb26b..0000000 --- a/web/src/wagmi.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { createConfig, http } from "wagmi"; -import { injected } from "wagmi/connectors"; -import { arkDevnet } from "./config/chain"; - -export const wagmiConfig = createConfig({ - chains: [arkDevnet], - connectors: [injected()], - transports: { - // The devnet RPC has no Multicall3 and works through JSON-RPC batches almost serially (about 0.4s - // per entry), while concurrent single requests come back three times faster. So: no batching. - [arkDevnet.id]: http(undefined, { retryCount: 2 }), - }, -}); - -declare module "wagmi" { - interface Register { - config: typeof wagmiConfig; - } -} diff --git a/web/tsconfig.app.json b/web/tsconfig.app.json deleted file mode 100644 index 4971bc8..0000000 --- a/web/tsconfig.app.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "es2023", - "lib": ["ES2023", "DOM"], - "module": "esnext", - "types": ["vite/client"], - "allowArbitraryExtensions": true, - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", - - /* Linting */ - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src"] -} diff --git a/web/tsconfig.json b/web/tsconfig.json deleted file mode 100644 index 1ffef60..0000000 --- a/web/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] -} diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json deleted file mode 100644 index 8455dcb..0000000 --- a/web/tsconfig.node.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "target": "es2023", - "lib": ["ES2023"], - "types": ["node"], - "skipLibCheck": true, - - /* Bundler mode */ - "module": "nodenext", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - - /* Linting */ - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["vite.config.ts"] -} diff --git a/web/vite.config.ts b/web/vite.config.ts deleted file mode 100644 index 9982072..0000000 --- a/web/vite.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import react from '@vitejs/plugin-react' -import { defineConfig } from 'vite' - -// https://vite.dev/config/ -export default defineConfig({ - plugins: [react()], -})