diff --git a/.changeset/fast-pets-heal.md b/.changeset/fast-pets-heal.md new file mode 100644 index 00000000..4c413615 --- /dev/null +++ b/.changeset/fast-pets-heal.md @@ -0,0 +1,5 @@ +--- +"@oaknetwork/contracts": major +--- + +Added contract for sdk diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d835738..40915663 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,12 +41,21 @@ jobs: run: pnpm -r --workspace-concurrency=Infinity build - name: Run tests with coverage (enforces 100% threshold) - run: pnpm -r --workspace-concurrency=Infinity --filter=!@oaknetwork/contracts test --coverage + run: pnpm -r --workspace-concurrency=Infinity test --coverage env: CI: true CLIENT_ID: ${{ secrets.CLIENT_ID }} CLIENT_SECRET: ${{ secrets.CLIENT_SECRET }} OAK_ENVIRONMENT: sandbox + RPC_URL: ${{ secrets.RPC_URL }} + PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} + ALL_OR_NOTHING_ADDRESS: ${{ secrets.ALL_OR_NOTHING_ADDRESS }} + CAMPAIGN_INFO_FACTORY_ADDRESS: ${{ secrets.CAMPAIGN_INFO_FACTORY_ADDRESS }} + GLOBAL_PARAMS_ADDRESS: ${{ secrets.GLOBAL_PARAMS_ADDRESS }} + TREASURY_FACTORY_ADDRESS: ${{ secrets.TREASURY_FACTORY_ADDRESS }} + CAMPAIGN_INFO_ADDRESS: ${{ secrets.CAMPAIGN_INFO_ADDRESS }} + PAYMENT_TREASURY_ADDRESS: ${{ secrets.PAYMENT_TREASURY_ADDRESS }} + KEEP_WHATS_RAISED_ADDRESS: ${{ secrets.KEEP_WHATS_RAISED_ADDRESS }} - name: Upload coverage reports uses: actions/upload-artifact@v4 @@ -58,4 +67,4 @@ jobs: retention-days: 30 - name: Run lint - run: pnpm -r --workspace-concurrency=Infinity --filter=!@oaknetwork/contracts lint + run: pnpm -r --workspace-concurrency=Infinity lint diff --git a/.gitignore b/.gitignore index 9568e9ee..ecf40f3c 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,7 @@ typings/ dist/ build/ lib/ +!packages/contracts/src/lib/ lib-cov/ esm/ cjs/ diff --git a/CLAUDE.md b/CLAUDE.md index d9bc19b2..adbfdc3a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # Oak SDK - AI Development Guidelines -**Last Updated:** February 2026 +**Last Updated:** March 2026 **Status:** Pre-launch SDK (March 2026) This document provides strict rules and standards for AI assistants (Claude Code, Cursor, etc.) working on the Oak SDK codebase. Following these guidelines is **mandatory** to maintain code quality, security, and architectural consistency. @@ -9,20 +9,23 @@ This document provides strict rules and standards for AI assistants (Claude Code ## Table of Contents -1. [Architecture Principles](#architecture-principles) -2. [Code Standards](#code-standards) -3. [Security Rules](#security-rules) -4. [Testing Requirements](#testing-requirements) -5. [Anti-Patterns](#anti-patterns) -6. [Refactoring Guidelines](#refactoring-guidelines) -7. [Git Workflow](#git-workflow) -8. [Performance](#performance) -9. [Type System Rules](#type-system-rules) -10. [Documentation](#documentation) +1. [Architecture Principles (Payments SDK)](#architecture-principles-payments-sdk) +2. [Architecture Principles (Contracts SDK)](#architecture-principles-contracts-sdk) +3. [Code Standards](#code-standards) +4. [Security Rules](#security-rules) +5. [Testing Requirements](#testing-requirements) +6. [Anti-Patterns](#anti-patterns) +7. [Refactoring Guidelines](#refactoring-guidelines) +8. [Git Workflow](#git-workflow) +9. [Performance](#performance) +10. [Type System Rules](#type-system-rules) +11. [Documentation](#documentation) --- -## Architecture Principles +## Architecture Principles (Payments SDK) + +> **Scope:** This section applies to the **payments SDK** (`packages/payments-sdk`). For the contracts package (`packages/contracts`), see [Architecture Principles (Contracts SDK)](#architecture-principles-contracts-sdk) below. ### Core Patterns (DO NOT BREAK) @@ -79,6 +82,189 @@ export class CustomerService { --- +## Architecture Principles (Contracts SDK) + +> **This section is specific to `packages/contracts`.** The contracts package interacts with on-chain smart contracts via **viem**, not REST APIs. Its patterns differ significantly from the payments SDK above. + +### Architecture Overview + +``` +packages/contracts/src/ +├── client/ # createOakContractsClient, config resolution, types +├── contracts/ # Per-protocol contract entities (kebab-case directories) +│ ├── global-params/ +│ ├── campaign-info-factory/ +│ ├── campaign-info/ +│ ├── treasury-factory/ +│ ├── payment-treasury/ +│ ├── all-or-nothing/ +│ ├── keep-whats-raised/ +│ └── item-registry/ +├── types/ # Cross-contract structs + SDK params (no logic) +├── utils/ # Pure helpers (account guards, chain, hash, hex, time) +├── constants/ # Chain IDs, encoding, fees, registry +├── errors/ # Typed revert error classes + parseContractError +├── metrics/ # Platform/campaign/treasury reporting +├── lib/ # Thin viem re-exports + provider helpers +└── scripts/ # ABI generation/checking (dev tooling) +``` + +### Client Factory + +`createOakContractsClient(config)` is the entry point. It resolves viem clients and returns **entity factory methods**: + +```typescript +const oak = createOakContractsClient({ + chainId: CHAIN_IDS.CELO_TESTNET_SEPOLIA, + rpcUrl: process.env.RPC_URL, + privateKey: process.env.PRIVATE_KEY as `0x${string}`, +}); + +const globalParams = oak.globalParams(GLOBAL_PARAMS_ADDRESS); +const feePercent = await globalParams.getProtocolFeePercent(); +``` + +Client config supports three modes: +- **Read-only simple:** `{ chainId, rpcUrl }` -- writes/simulations throw +- **Simple:** `{ chainId, rpcUrl, privateKey }` -- standard usage +- **Full:** `{ chain, provider, signer }` -- BYO viem clients + +### Entity Composition Pattern + +Each contract directory contains `abi.ts`, `reads.ts`, `writes.ts`, `simulate.ts`, `events.ts`, `types.ts`, and `index.ts`. The entity factory composes them: + +```typescript +// ✅ CORRECT - Entity factory pattern +export function createGlobalParamsEntity( + address: Address, + publicClient: PublicClient, + walletClient: WalletClient | null, + chain: Chain, +): GlobalParamsEntity { + return { + ...createGlobalParamsReads(address, publicClient), + ...createGlobalParamsWrites(address, walletClient, chain), + simulate: createGlobalParamsSimulate(address, publicClient, walletClient, chain), + events: createGlobalParamsEvents(address, publicClient), + }; +} +``` + +Entity types combine reads + writes + nested simulate/events: + +```typescript +export type GlobalParamsEntity = GlobalParamsReads & GlobalParamsWrites & { + simulate: GlobalParamsSimulate; + events: GlobalParamsEvents; +}; +``` + +### Error Handling (Throws, NOT Result) + +The contracts package **throws errors** instead of returning `Result`. This is correct for on-chain interactions. + +```typescript +// ✅ CORRECT for contracts - throw errors +export function requireSigner(walletClient: WalletClient | null): WalletClient { + if (walletClient === null) { + throw new Error("No signer configured."); + } + return walletClient; +} + +// ❌ WRONG for contracts - do NOT use Result type +function requireSigner(walletClient: WalletClient | null): Result { + if (walletClient === null) return err(new Error("No signer")); + return ok(walletClient); +} +``` + +On-chain revert errors are parsed into typed classes: + +- `parseContractError(error)` -- decodes revert data into a typed error class +- `getRevertData(error)` -- extracts raw revert bytes from a caught error +- `simulateWithErrorDecode(...)` -- simulates a transaction and decodes any revert +- `getRecoveryHint(error)` -- returns a human-readable recovery suggestion + +Each contract has its own typed error classes in `src/errors/contracts/` (e.g., `GlobalParamsUnauthorizedError`, `AllOrNothingNotClaimableError`). + +### Signer Model + +Three levels of signer resolution, from most specific to least: + +1. **Per-call** (`CallSignerOptions`): `entity.enlistPlatform(..., { signer: walletClient })` +2. **Per-entity** (`EntitySignerOptions`): `oak.globalParams(address, { signer: walletClient })` +3. **Client-level**: `createOakContractsClient({ privateKey: "0x..." })` + +Write and simulate methods **MUST** use `requireSigner` and `requireAccount` guards before calling `writeContract` or `simulateContract`. + +### File and Naming Conventions + +| Item | Convention | Example | +|------|-----------|---------| +| Contract directories | kebab-case | `all-or-nothing/`, `payment-treasury/` | +| Factory functions | `create` | `createGlobalParamsReads`, `createAllOrNothingEntity` | +| Type interfaces | PascalCase + suffix | `GlobalParamsReads`, `AllOrNothingTreasuryEntity` | +| ABI files | `abi.ts` per contract | `src/contracts/global-params/abi.ts` | +| Struct types | `src/types/structs.ts` | On-chain struct mirrors, no logic | +| SDK params | `src/types/params.ts` | SDK input types, no logic | + +### Subpath Exports + +| Import path | Contents | +|-------------|----------| +| `@oaknetwork/contracts` | Client, utils, types, lib re-exports, constants, errors | +| `@oaknetwork/contracts/contracts` | Individual `create*Entity` factories | +| `@oaknetwork/contracts/client` | `createOakContractsClient` + client types | +| `@oaknetwork/contracts/utils` | Pure helper functions | +| `@oaknetwork/contracts/errors` | Error classes + parsing utilities | +| `@oaknetwork/contracts/metrics` | Reporting helpers (**NOT** re-exported from root) | + +### Testing (Contracts-Specific) + +#### Coverage Thresholds + +- `pnpm test` enforces **100% coverage** globally (branches, functions, lines, statements) +- `pnpm test:integration` relaxes thresholds via `--coverageThreshold='{}'` +- ABI files (`abi.ts`) and barrel `index.ts` files are excluded from coverage collection + +#### Unit Tests + +Use the Hardhat `#0` private key and dummy addresses from `__tests__/setup/constant.ts`. No live RPC required: + +```typescript +import { HARDHAT_PRIVATE_KEY, DUMMY_RPC_URL, DUMMY_ADDRESS } from "../setup/constant"; + +const client = createOakContractsClient({ + chainId: CHAIN_IDS.CELO_TESTNET_SEPOLIA, + rpcUrl: DUMMY_RPC_URL, + privateKey: HARDHAT_PRIVATE_KEY, +}); +``` + +#### Integration Tests + +Require a `.env` file with `RPC_URL`, `PRIVATE_KEY`, and deployed contract addresses. Use `loadTestConfig()` from `__tests__/setup/config.ts`, which throws with a clear message if any required env var is missing. + +### Contracts Anti-Patterns + +1. ❌ Using `Result` in contracts code -- use throws + typed errors +2. ❌ Using `withAuth` or `httpClient` -- contracts use viem clients directly +3. ❌ Inlining ABI arrays in read/write files -- always isolate in `abi.ts` +4. ❌ Skipping `requireSigner`/`requireAccount` guards in write methods +5. ❌ Putting logic in `src/types/` files -- types only, no runtime code +6. ❌ Adding runtime dependencies beyond `viem` -- keep the dep footprint minimal + +### Build and Dependencies + +- **Runtime dependency:** `viem` only (`^2.23.0`) +- **Build tool:** tsup (ESM only, `dts: true`, no splitting, clean) +- **TypeScript:** strict mode, `moduleResolution: "bundler"` +- **Module type:** ESM (`"type": "module"`) +- **Published files:** `dist/` only + +--- + ## Code Standards ### TypeScript Strict Mode @@ -714,16 +900,29 @@ export function verifyWebhookSignature( ## Common Mistakes to Avoid +### Shared (All Packages) + 1. ❌ Using `any` instead of `unknown` -2. ❌ Not multiplying OAuth `expires_in` by 1000 -3. ❌ Silent test skips with `console.warn` + `return` -4. ❌ Exposing `clientSecret` in public config -5. ❌ Hardcoding URLs instead of using `buildUrl` -6. ❌ Duplicating token-fetch logic instead of using `withAuth` -7. ❌ Putting test tools in `dependencies` instead of `devDependencies` -8. ❌ Creating zero-value wrapper functions -9. ❌ Using `ReturnType` instead of named interfaces -10. ❌ Not handling both success and error paths in tests +2. ❌ Silent test skips with `console.warn` + `return` +3. ❌ Putting test tools in `dependencies` instead of `devDependencies` +4. ❌ Creating zero-value wrapper functions +5. ❌ Using `ReturnType` instead of named interfaces +6. ❌ Not handling both success and error paths in tests + +### Payments SDK Only + +7. ❌ Not multiplying OAuth `expires_in` by 1000 +8. ❌ Exposing `clientSecret` in public config +9. ❌ Hardcoding URLs instead of using `buildUrl` +10. ❌ Duplicating token-fetch logic instead of using `withAuth` + +### Contracts Package Only + +11. ❌ Using `Result` instead of throws + typed errors +12. ❌ Using `withAuth` or `httpClient` instead of viem clients +13. ❌ Inlining ABI arrays instead of isolating in `abi.ts` +14. ❌ Skipping `requireSigner`/`requireAccount` guards in write methods +15. ❌ Adding runtime dependencies beyond `viem` --- diff --git a/packages/contracts/.env.example b/packages/contracts/.env.example new file mode 100644 index 00000000..53cc418e --- /dev/null +++ b/packages/contracts/.env.example @@ -0,0 +1,14 @@ +# Celo Sepolia testnet RPC +RPC_URL=https://forno.celo-sepolia.celo-testnet.org + +# 0x-prefixed private key with funds on the testnet +PRIVATE_KEY=0x... + +# Deployed contract addresses on Celo Sepolia +GLOBAL_PARAMS_ADDRESS=0x6c563C19Ec838d83854185F5cecc2c9d3b097F2D +CAMPAIGN_INFO_FACTORY_ADDRESS=0x1C9456c11753E7C189555b65D17A70e128bd3d74 +TREASURY_FACTORY_ADDRESS=0xC8083f1190aD25A7FeFdc5fE3dd2FB8142e3f6eF +CAMPAIGN_INFO_ADDRESS=0x... +PAYMENT_TREASURY_ADDRESS=0x... +ALL_OR_NOTHING_ADDRESS=0x... +KEEP_WHATS_RAISED_ADDRESS=0x... diff --git a/packages/contracts/README.md b/packages/contracts/README.md new file mode 100644 index 00000000..650a2772 --- /dev/null +++ b/packages/contracts/README.md @@ -0,0 +1,649 @@ +# Oak Contracts SDK + +TypeScript SDK for interacting with Oak Network smart contracts. Provides a type-safe client with full read/write access to all Oak protocol contracts. + +> Full Documentation: [oaknetwork.org/docs/contracts-sdk/overview](https://oaknetwork.org/docs/contracts-sdk/overview) + +## Prerequisites + +> **You need deployed contract addresses to use this SDK.** + +> The SDK interacts with Oak Network smart contracts that must already be deployed on-chain. To get your contract addresses and sandbox environment access, contact our team at **support@oaknetwork.org**. + +## Installation + +```bash +pnpm add @oaknetwork/contracts +``` + +```bash +npm install @oaknetwork/contracts +``` + +```bash +yarn add @oaknetwork/contracts +``` + +**Requirements:** Node.js 18+, TypeScript 5.x recommended. + +### Supported Chain IDs + +```typescript +import { CHAIN_IDS } from "@oaknetwork/contracts"; + +CHAIN_IDS.ETHEREUM_MAINNET; // 1 +CHAIN_IDS.CELO_MAINNET; // 42220 +CHAIN_IDS.ETHEREUM_TESTNET_SEPOLIA; // 11155111 +CHAIN_IDS.ETHEREUM_TESTNET_GOERLI; // 5 +CHAIN_IDS.CELO_TESTNET_SEPOLIA; // 11142220 +``` + +> See the full installation documentation here: [oaknetwork.org/docs/contracts-sdk/installation](https://oaknetwork.org/docs/contracts-sdk/installation) + +## Quick Start + +### Create a client + +```typescript +import { createOakContractsClient, CHAIN_IDS } from "@oaknetwork/contracts"; + +const oak = createOakContractsClient({ + chainId: CHAIN_IDS.CELO_TESTNET_SEPOLIA, + rpcUrl: "https://forno.celo-sepolia.celo-testnet.org", + privateKey: "0x...", +}); +``` + +See the full [Quickstart](https://oaknetwork.org/docs/contracts-sdk/quickstart) guide for a step-by-step walkthrough. + +## Client Configuration + +Four config/signer patterns are supported. Mix and match as needed. + +### Pattern 1 — Simple (chainId + rpcUrl + privateKey) + +Full read/write access using a raw private key. Suitable for backend services and scripts. + +```typescript +const oak = createOakContractsClient({ + chainId: CHAIN_IDS.CELO_TESTNET_SEPOLIA, + rpcUrl: "https://forno.celo-sepolia.celo-testnet.org", + privateKey: "0x...", // 0x-prefixed 32-byte hex string +}); + +const gp = oak.globalParams("0x..."); +const admin = await gp.getProtocolAdminAddress(); // read +await gp.enlistPlatform(hash, adminAddr, fee, adapter); // write — uses client key +``` + +### Pattern 2 — Read-only (chainId + rpcUrl, no privateKey) + +No private key required. All read methods work normally; write methods throw `"No signer configured"` immediately — no RPC call is made. + +```typescript +const oak = createOakContractsClient({ + chainId: CHAIN_IDS.CELO_TESTNET_SEPOLIA, + rpcUrl: "https://forno.celo-sepolia.celo-testnet.org", +}); + +const gp = oak.globalParams("0x..."); +const admin = await gp.getProtocolAdminAddress(); // reads work fine +await gp.transferOwnership("0x..."); // throws "No signer configured" +``` + +### Pattern 3 — Per-entity signer override + +Supply a signer when creating an entity. Every write/simulate call on that entity uses the provided signer — no need to pass it again per call. Designed for browser wallets (MetaMask, Privy, etc.) where the signer is resolved after the client is created. + +```typescript +import { + createOakContractsClient, + createWallet, + CHAIN_IDS, +} from "@oaknetwork/contracts"; + +const oak = createOakContractsClient({ + chainId: CHAIN_IDS.CELO_TESTNET_SEPOLIA, + rpcUrl: "https://forno.celo-sepolia.celo-testnet.org", +}); + +// Resolve signer after wallet connect +const signer = createWallet(privateKey, rpcUrl, oak.config.chain); +// or: const signer = await getSigner(window.ethereum, oak.config.chain); + +// All write/simulate calls on gp automatically use signer +const gp = oak.globalParams("0x...", { signer }); +const admin = await gp.getProtocolAdminAddress(); // read +await gp.simulate.enlistPlatform(hash, addr, fee, adapter); // simulate — uses signer +await gp.enlistPlatform(hash, addr, fee, adapter); // write — uses signer +``` + +### Pattern 4 — Per-call signer override + +Supply a different signer for a single write or simulate call. The entity itself has no fixed signer; the override is passed as the last optional argument. Useful when different operations on the same contract require different signers (e.g. multi-sig flows, role switching). + +```typescript +const gp = oak.globalParams("0x..."); // no entity-level signer + +// Read — no signer needed +const admin = await gp.getProtocolAdminAddress(); + +// Write/simulate — inject signer only for this one call +await gp.simulate.enlistPlatform(hash, addr, fee, adapter, { signer }); +await gp.enlistPlatform(hash, addr, fee, adapter, { signer }); + +// Different call, different signer +await gp.transferOwnership(newOwner, { signer: anotherWallet }); + +// No override → throws "No signer configured" +await gp.delistPlatform(hash); // throws if no client/entity signer set +``` + +### Pattern 5 — Full (bring your own clients) + +Pass pre-built viem `PublicClient` and `WalletClient` directly. Useful for advanced configurations (custom transports, account abstraction, etc.). + +```typescript +import { + createOakContractsClient, + createPublicClient, + createWalletClient, + http, + getChainFromId, + CHAIN_IDS, +} from "@oaknetwork/contracts"; + +const chain = getChainFromId(CHAIN_IDS.CELO_TESTNET_SEPOLIA); +const provider = createPublicClient({ chain, transport: http(RPC_URL) }); +const signer = createWalletClient({ account, chain, transport: http(RPC_URL) }); + +const oak = createOakContractsClient({ chain, provider, signer }); +``` + +### Signer resolution priority + +When a write or simulate method is called, the signer is resolved in this order: + +1. **Per-call** `options.signer` — highest priority +2. **Per-entity** `signer` passed to the entity factory method +3. **Client-level** `walletClient` from `createOakContractsClient` +4. **Throws** `"No signer configured"` if none of the above is set + +> For a detailed step-by-step guide, please refer to the complete [Client Configuration](https://oaknetwork.org/docs/contracts-sdk/client) documentation. + +## Contract Entities + +### GlobalParams + +Protocol-wide configuration registry. Manages platform listings, fee settings, token currencies, line item types, and a general-purpose key-value registry. + +```typescript +const gp = oak.globalParams("0x..."); + +// Reads +const admin = await gp.getProtocolAdminAddress(); +const fee = await gp.getProtocolFeePercent(); // bigint bps (e.g. 100 = 1%) +const count = await gp.getNumberOfListedPlatforms(); +const isListed = await gp.checkIfPlatformIsListed(platformHash); +const platAdmin = await gp.getPlatformAdminAddress(platformHash); +const platFee = await gp.getPlatformFeePercent(platformHash); +const delay = await gp.getPlatformClaimDelay(platformHash); +const adapter = await gp.getPlatformAdapter(platformHash); +const tokens = await gp.getTokensForCurrency(currency); // Address[] +const lineItem = await gp.getPlatformLineItemType(platformHash, typeId); +const value = await gp.getFromRegistry(key); + +// Writes +await gp.enlistPlatform(platformHash, adminAddress, feePercent, adapterAddress); +await gp.delistPlatform(platformHash); +await gp.updatePlatformAdminAddress(platformHash, newAdmin); +await gp.updatePlatformClaimDelay(platformHash, delaySeconds); +await gp.updateProtocolAdminAddress(newAdmin); +await gp.updateProtocolFeePercent(newFeePercent); +await gp.setPlatformAdapter(platformHash, adapterAddress); +await gp.setPlatformLineItemType( + platformHash, + typeId, + label, + countsTowardGoal, + applyProtocolFee, + canRefund, + instantTransfer, +); +await gp.removePlatformLineItemType(platformHash, typeId); +await gp.addTokenToCurrency(currency, tokenAddress); +await gp.removeTokenFromCurrency(currency, tokenAddress); +await gp.addPlatformData(platformHash, platformDataKey); +await gp.removePlatformData(platformHash, platformDataKey); +await gp.addToRegistry(key, value); +await gp.transferOwnership(newOwner); +``` + +> For complete details on the Global Params contract entity, please visit the following link: [Global Params](https://oaknetwork.org/docs/contracts-sdk/global-params). + +--- + +### CampaignInfoFactory + +Deploys new CampaignInfo contracts. Each campaign gets its own on-chain CampaignInfo instance with its own address, NFT collection, and configuration. + +```typescript +import { + createOakContractsClient, + keccak256, + toHex, + getCurrentTimestamp, + addDays, + CHAIN_IDS, +} from "@oaknetwork/contracts"; + +const factory = oak.campaignInfoFactory("0x..."); + +const PLATFORM_HASH = keccak256(toHex("my-platform")); +const CURRENCY = toHex("USD", { size: 32 }); +const identifierHash = keccak256(toHex("my-campaign-slug")); +const now = getCurrentTimestamp(); + +// Reads +const infoAddress = await factory.identifierToCampaignInfo(identifierHash); +const isValid = await factory.isValidCampaignInfo(infoAddress); + +// Writes +const txHash = await factory.createCampaign({ + creator: "0x...", + identifierHash, + selectedPlatformHash: [PLATFORM_HASH], + campaignData: { + launchTime: now + 3_600n, // 1 hour from now + deadline: addDays(now, 30), // 30 days from now + goalAmount: 1_000_000n, + currency: CURRENCY, + }, + nftName: "My Campaign NFT", + nftSymbol: "MCN", + nftImageURI: "https://example.com/nft.png", + contractURI: "https://example.com/contract.json", +}); + +const receipt = await oak.waitForReceipt(txHash); +const campaignAddress = await factory.identifierToCampaignInfo(identifierHash); +``` + +> For complete details on the Campaign Info Factory contract entity, please visit the following link: [Campaign Info Factory](https://oaknetwork.org/docs/contracts-sdk/campaign-info-factory). + +--- + +### CampaignInfo + +Per-campaign configuration and state. Each campaign deployed via the CampaignInfoFactory gets its own CampaignInfo contract that tracks funding progress, accepted tokens, platform settings, and NFT pledge records. + +```typescript +const ci = oak.campaignInfo("0x..."); + +// Reads +const launchTime = await ci.getLaunchTime(); +const deadline = await ci.getDeadline(); +const goalAmount = await ci.getGoalAmount(); +const currency = await ci.getCampaignCurrency(); +const totalRaised = await ci.getTotalRaisedAmount(); +const available = await ci.getTotalAvailableRaisedAmount(); +const isLocked = await ci.isLocked(); +const isCancelled = await ci.cancelled(); +const config = await ci.getCampaignConfig(); +const tokens = await ci.getAcceptedTokens(); + +// Writes +await ci.updateDeadline(newDeadline); +await ci.updateGoalAmount(newGoal); +await ci.pauseCampaign(message); +await ci.unpauseCampaign(message); +await ci.cancelCampaign(message); +``` + +> For complete details on the Campaign Info contract entity, please visit the following link: [Campaign Info](https://oaknetwork.org/docs/contracts-sdk/campaign-info). + +--- + +### TreasuryFactory + +Deploys treasury contracts for a given CampaignInfo. Manages treasury implementations that platforms can register, approve, and deploy. + +```typescript +const tf = oak.treasuryFactory("0x..."); + +// Deploy +const txHash = await tf.deploy(platformHash, infoAddress, implementationId); + +// Implementation management +await tf.registerTreasuryImplementation( + platformHash, + implementationId, + implAddress, +); +await tf.approveTreasuryImplementation(platformHash, implementationId); +await tf.disapproveTreasuryImplementation(implAddress); +await tf.removeTreasuryImplementation(platformHash, implementationId); +``` + +> For complete details on the Treasury Factory contract entity, please visit the following link: [Treasury Factory](https://oaknetwork.org/docs/contracts-sdk/treasury-factory). + +--- + +### PaymentTreasury + +Handles fiat-style payments via a payment gateway. Manages payment creation, confirmation, refunds, fee disbursement, and fund withdrawal for campaigns. + +> **Two treasury variants, one SDK method.** The `paymentTreasury()` method works with both on-chain implementations: +> +> | Variant | Description | +> | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +> | **PaymentTreasury** | Standard payment treasury with no time restrictions. Payments can be created, confirmed, and refunded at any time while the treasury is active. | +> | **TimeConstrainedPaymentTreasury** | Time-constrained variant that enforces launch-time and deadline windows on-chain. Payments can only be created within the campaign window (launch → deadline + buffer). Refunds, withdrawals, and fee disbursements are only available after launch. | +> +> Both contracts share the same ABI and the same SDK interface. Time enforcement is handled entirely on-chain — simply pass the deployed contract address regardless of which variant was deployed: + +```typescript +// Works for both PaymentTreasury and TimeConstrainedPaymentTreasury +const pt = oak.paymentTreasury("0x..."); + +// Reads +const raised = await pt.getRaisedAmount(); +const refunded = await pt.getRefundedAmount(); +const payment = await pt.getPaymentData(paymentId); + +// Writes +const txHash = await pt.createPayment( + paymentId, + buyerId, + itemId, + paymentToken, + amount, + expiration, + lineItems, + externalFees, +); +await pt.confirmPayment(paymentId, buyerAddress); +await pt.claimRefund(paymentId, refundAddress); +await pt.claimRefundSelf(paymentId); +await pt.disburseFees(); +await pt.withdraw(); +await pt.pauseTreasury(message); +await pt.unpauseTreasury(message); +await pt.cancelTreasury(message); +``` + +> **Note:** When using a `TimeConstrainedPaymentTreasury`, calls made outside the allowed time window will revert on-chain. For example, `createPayment()` will revert if called before launch or after the deadline + buffer period. + +> For complete details on the Payment Treasury contract entity, please visit the following link: [Payment Treasury](https://oaknetwork.org/docs/contracts-sdk/payment-treasury). + +--- + +### AllOrNothing Treasury + +Crowdfunding treasury where funds are only released if the campaign goal is met. If the goal is not reached, backers can claim full refunds. Includes ERC-721 pledge NFTs. + +```typescript +const aon = oak.allOrNothingTreasury("0x..."); + +// Reads +const raised = await aon.getRaisedAmount(); +const reward = await aon.getReward(rewardName); + +// Writes +await aon.addRewards(rewardNames, rewards); +await aon.pledgeForAReward(backer, pledgeToken, shippingFee, rewardNames); +await aon.pledgeWithoutAReward(backer, pledgeToken, pledgeAmount); +await aon.claimRefund(tokenId); +await aon.disburseFees(); +await aon.withdraw(); +await aon.pauseTreasury(message); +await aon.unpauseTreasury(message); +await aon.cancelTreasury(message); + +// ERC-721 +const owner = await aon.ownerOf(tokenId); +const uri = await aon.tokenURI(tokenId); +await aon.safeTransferFrom(from, to, tokenId); +``` + +> For complete details on the AllOrNothing Treasury contract entity, please visit the following link: [AllOrNothing Treasury](https://oaknetwork.org/docs/contracts-sdk/all-or-nothing). + +--- + +### KeepWhatsRaised Treasury + +Crowdfunding treasury where the creator keeps all funds raised regardless of whether the goal is met. Includes configurable fee structures, withdrawal delays, and ERC-721 pledge NFTs. + +```typescript +const kwr = oak.keepWhatsRaisedTreasury("0x..."); + +// Reads +const raised = await kwr.getRaisedAmount(); +const available = await kwr.getAvailableRaisedAmount(); +const reward = await kwr.getReward(rewardName); + +// Writes +await kwr.configureTreasury(config, campaignData, feeKeys, feeValues); +await kwr.addRewards(rewardNames, rewards); +await kwr.pledgeForAReward(pledgeId, backer, token, tip, rewardNames); +await kwr.pledgeWithoutAReward(pledgeId, backer, token, amount, tip); +await kwr.approveWithdrawal(); +await kwr.claimFund(); +await kwr.claimTip(); +await kwr.claimRefund(tokenId); +await kwr.disburseFees(); +await kwr.withdraw(token, amount); +await kwr.pauseTreasury(message); +await kwr.unpauseTreasury(message); +await kwr.cancelTreasury(message); +``` + +> For complete details on the KeepWhatsRaised Treasury contract entity, please visit the following link: [KeepWhatsRaised Treasury](https://oaknetwork.org/docs/contracts-sdk/keep-whats-raised). + +--- + +### ItemRegistry + +Manages items available for purchase in campaigns. Items represent physical goods with dimensions, weight, and category metadata. + +```typescript +const ir = oak.itemRegistry("0x..."); + +// Read +const item = await ir.getItem(ownerAddress, itemId); + +// Writes +await ir.addItem(itemId, item); +await ir.addItemsBatch(itemIds, items); +``` + +> For complete details on the Item Registry contract entity, please visit the following link: [Item Registry](https://oaknetwork.org/docs/contracts-sdk/item-registry). + +--- + +## Error Handling + +Contract calls can revert with on-chain errors. The SDK decodes raw revert data into typed error classes with decoded arguments and human-readable recovery hints. + +### Decoding revert errors: + +```typescript +import { parseContractError, getRevertData } from "@oaknetwork/contracts"; + +function handleError(err) { + // If the error is already a typed SDK error (thrown by simulate methods) + if (typeof err?.recoveryHint === "string") { + console.error("Reverted:", err.name); + console.error("Args:", err.args); + console.error("Hint:", err.recoveryHint); + return; + } + // Otherwise extract raw revert hex from the viem error chain and decode it + const revertData = getRevertData(err); + const parsed = parseContractError(revertData ?? ""); + if (parsed) { + console.error("Reverted:", parsed.name); + console.error("Args:", parsed.args); + if (parsed.recoveryHint) console.error("Hint:", parsed.recoveryHint); + return; + } + console.error("Unknown error:", err.message); +} + +try { + const txHash = await factory.createCampaign({ ... }); +} catch (err) { + handleError(err); +} +``` + +> See the full error handling guidelines here: [Error handling](https://oaknetwork.org/docs/contracts-sdk/error-handling) + +--- + +## Utility Functions + +The SDK exports pure utility functions and constants that have no client dependency. Import them from @oaknetwork/contracts or @oaknetwork/contracts/utils. + +```typescript +import { + keccak256, + id, + toHex, + stringToHex, + parseEther, + formatEther, + parseUnits, + isAddress, + getAddress, + getCurrentTimestamp, + addDays, + getChainFromId, + createJsonRpcProvider, + createWallet, + createBrowserProvider, + getSigner, + CHAIN_IDS, + BPS_DENOMINATOR, + BYTES32_ZERO, + DATA_REGISTRY_KEYS, + scopedToPlatform, +} from "@oaknetwork/contracts"; + +// Hash a string to bytes32 +const platformHash = keccak256(toHex("my-platform")); + +// Encode string to fixed bytes32 +const currency = toHex("USD", { size: 32 }); + +// Timestamp helpers +const now = getCurrentTimestamp(); // bigint seconds +const deadline = addDays(now, 30); // 30 days from now + +// Fee calculations (fees are in basis points, 10_000 = 100%) +const feeAmount = (raisedAmount * platformFee) / BPS_DENOMINATOR; + +// Browser wallet (frontend) +const chain = getChainFromId(CHAIN_IDS.CELO_TESTNET_SEPOLIA); +const provider = createBrowserProvider(window.ethereum, chain); +const signer = await getSigner(window.ethereum, chain); +``` + +For complete guidelines on utility functions, please refer to the following link: [Utility Functions](https://oaknetwork.org/docs/contracts-sdk/utilities). + +--- + +## Exported Entry Points + +| Entry point | Contents | +| --------------------------------- | ------------------------------------------- | +| `@oaknetwork/contracts` | Everything — client, types, utils, errors | +| `@oaknetwork/contracts/utils` | Utility functions only (no client) | +| `@oaknetwork/contracts/contracts` | Contract entity factories only | +| `@oaknetwork/contracts/client` | `createOakContractsClient` only | +| `@oaknetwork/contracts/errors` | Error classes and `parseContractError` only | + +--- + +## Local Development & Testing + +### Install dependencies + +```bash +pnpm install +``` + +### Build + +```bash +pnpm build +``` + +**Do not** use npm or yarn. The repository enforces pnpm >= 10.0.0. + +### Running tests + +```bash +pnpm test:unit # Unit tests +pnpm test:integration # Integration tests (requires credentials) +pnpm test:all # All tests with coverage +pnpm test:watch # Watch mode +``` + +--- + +## Changesets Workflow + +We use [Changesets](https://github.com/changesets/changesets) to manage versions and changelogs: + +1. After making changes, run `pnpm changeset` +2. Select impact (**Major** / **Minor** / **Patch**) for affected packages +3. Commit the generated file in `.changeset/` +4. CI automatically calculates versions, generates changelogs, and creates a release PR + +--- + +## Development Guidelines + +See [CLAUDE.md](../../CLAUDE.md) for coding standards including architecture principles, security rules, testing requirements, and anti-patterns. + +--- + +### Code review checklist + +- [ ] `pnpm build` succeeds +- [ ] `pnpm test` passes with >90% coverage +- [ ] `pnpm lint` has no errors +- [ ] Changeset created with `pnpm changeset` +- [ ] Documentation updated if needed + +--- + +## Documentation + +- [Full docs](https://oaknetwork.org/docs/contracts-sdk/overview) — oaknetwork.org/docs/sdk/overview +- [Quickstart](https://oaknetwork.org/docs/contracts-sdk/quickstart) — oaknetwork.org/docs/sdk/quickstart +- [Monorepo README](../../README.md) — README.md +- [Changelog](./CHANGELOG.md) — CHANGELOG.md + +--- + +## License + +[MIT](../../LICENSE) + +## Security + +[Security Policy](../../SECURITY.md) + +--- + +## Links + +- [Oak Network](https://oaknetwork.org) +- [Documentation](https://oaknetwork.org/docs/contracts-sdk/overview) +- [GitHub](https://github.com/oak-network/sdk) +- [Issues](https://github.com/oak-network/sdk/issues) +- [npm](https://www.npmjs.com/package/@oaknetwork/contracts) + +Questions? [Open an issue](https://github.com/oak-network/sdk/issues) or contact **support@oaknetwork.org** diff --git a/packages/contracts/__tests__/index.test.ts b/packages/contracts/__tests__/index.test.ts deleted file mode 100644 index 86a5088d..00000000 --- a/packages/contracts/__tests__/index.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { placeholder } from '../src/index'; - -describe('Contracts', () => { - it('should have placeholder function', () => { - expect(placeholder).toBeDefined(); - expect(typeof placeholder).toBe('function'); - placeholder(); // Ensure it's callable - }); - - // Add your contract tests here as you add code -}); diff --git a/packages/contracts/__tests__/integration/all-or-nothing.test.ts b/packages/contracts/__tests__/integration/all-or-nothing.test.ts new file mode 100644 index 00000000..32c78e90 --- /dev/null +++ b/packages/contracts/__tests__/integration/all-or-nothing.test.ts @@ -0,0 +1,62 @@ +import { getTestClient, getTestConfig } from "../setup/test-client"; +import { BYTES32_ZERO } from "../../src/constants/encoding"; + +const cfg = getTestConfig(); +const client = getTestClient(); +const aon = client.allOrNothingTreasury(cfg.addresses.allOrNothing); +const ZERO_ADDR = "0x0000000000000000000000000000000000000001" as const; + +describe("AllOrNothing — reads (may revert on uninitialized implementation)", () => { + it("getRaisedAmount", async () => { try { expect(typeof (await aon.getRaisedAmount())).toBe("bigint"); } catch { /* implementation revert */ } }); + it("getLifetimeRaisedAmount", async () => { try { expect(typeof (await aon.getLifetimeRaisedAmount())).toBe("bigint"); } catch { /* implementation revert */ } }); + it("getRefundedAmount", async () => { try { expect(typeof (await aon.getRefundedAmount())).toBe("bigint"); } catch { /* implementation revert */ } }); + it("getReward", async () => { try { expect(await aon.getReward(BYTES32_ZERO)).toBeDefined(); } catch { /* implementation revert */ } }); + it("getPlatformHash", async () => { try { expect(await aon.getPlatformHash()).toMatch(/^0x/); } catch { /* implementation revert */ } }); + it("getPlatformFeePercent", async () => { try { expect(typeof (await aon.getPlatformFeePercent())).toBe("bigint"); } catch { /* implementation revert */ } }); + it("paused", async () => { try { expect(typeof (await aon.paused())).toBe("boolean"); } catch { /* implementation revert */ } }); + it("cancelled", async () => { try { expect(typeof (await aon.cancelled())).toBe("boolean"); } catch { /* implementation revert */ } }); + it("balanceOf", async () => { try { expect(typeof (await aon.balanceOf(ZERO_ADDR))).toBe("bigint"); } catch { /* implementation revert */ } }); + it("ownerOf (may revert)", async () => { try { await aon.ownerOf(0n); } catch { /* expected */ } }); + it("tokenURI (may revert)", async () => { try { await aon.tokenURI(0n); } catch { /* expected */ } }); + it("name", async () => { try { expect(typeof (await aon.name())).toBe("string"); } catch { /* implementation revert */ } }); + it("symbol", async () => { try { expect(typeof (await aon.symbol())).toBe("string"); } catch { /* implementation revert */ } }); + it("getApproved (may revert)", async () => { try { await aon.getApproved(0n); } catch { /* expected */ } }); + it("isApprovedForAll", async () => { try { expect(typeof (await aon.isApprovedForAll(ZERO_ADDR, ZERO_ADDR))).toBe("boolean"); } catch { /* implementation revert */ } }); + it("supportsInterface", async () => { try { expect(typeof (await aon.supportsInterface("0x80ac58cd"))).toBe("boolean"); } catch { /* implementation revert */ } }); +}); + +describe("AllOrNothing — writes (may revert)", () => { + it("pauseTreasury", async () => { try { await aon.pauseTreasury(BYTES32_ZERO); } catch { /* expected */ } }); + it("unpauseTreasury", async () => { try { await aon.unpauseTreasury(BYTES32_ZERO); } catch { /* expected */ } }); + it("cancelTreasury", async () => { try { await aon.cancelTreasury(BYTES32_ZERO); } catch { /* expected */ } }); + it("addRewards", async () => { + try { + await aon.addRewards([BYTES32_ZERO], [{ rewardValue: 100n, isRewardTier: false, itemId: [], itemValue: [], itemQuantity: [] }]); + } catch { /* expected */ } + }); + it("removeReward", async () => { try { await aon.removeReward(BYTES32_ZERO); } catch { /* expected */ } }); + it("pledgeForAReward", async () => { try { await aon.pledgeForAReward(ZERO_ADDR, ZERO_ADDR, 0n, [BYTES32_ZERO]); } catch { /* expected */ } }); + it("pledgeWithoutAReward", async () => { try { await aon.pledgeWithoutAReward(ZERO_ADDR, ZERO_ADDR, 100n); } catch { /* expected */ } }); + it("claimRefund", async () => { try { await aon.claimRefund(0n); } catch { /* expected */ } }); + it("disburseFees", async () => { try { await aon.disburseFees(); } catch { /* expected */ } }); + it("withdraw", async () => { try { await aon.withdraw(); } catch { /* expected */ } }); + it("burn", async () => { try { await aon.burn(0n); } catch { /* expected */ } }); + it("approve", async () => { try { await aon.approve(ZERO_ADDR, 0n); } catch { /* expected */ } }); + it("setApprovalForAll", async () => { try { await aon.setApprovalForAll(ZERO_ADDR, true); } catch { /* expected */ } }); + it("safeTransferFrom", async () => { try { await aon.safeTransferFrom(ZERO_ADDR, ZERO_ADDR, 0n); } catch { /* expected */ } }); + it("transferFrom", async () => { try { await aon.transferFrom(ZERO_ADDR, ZERO_ADDR, 0n); } catch { /* expected */ } }); +}); + +describe("AllOrNothing — simulate (may throw)", () => { + it("simulate.pledgeForAReward", async () => { try { await aon.simulate.pledgeForAReward(ZERO_ADDR, ZERO_ADDR, 0n, [BYTES32_ZERO]); } catch { /* expected */ } }); + it("simulate.pledgeWithoutAReward", async () => { try { await aon.simulate.pledgeWithoutAReward(ZERO_ADDR, ZERO_ADDR, 100n); } catch { /* expected */ } }); + it("simulate.claimRefund", async () => { try { await aon.simulate.claimRefund(0n); } catch { /* expected */ } }); + it("simulate.disburseFees", async () => { try { await aon.simulate.disburseFees(); } catch { /* expected */ } }); + it("simulate.withdraw", async () => { try { await aon.simulate.withdraw(); } catch { /* expected */ } }); +}); + +describe("AllOrNothing — events", () => { + it("events is an empty object", () => { + expect(aon.events).toEqual({}); + }); +}); diff --git a/packages/contracts/__tests__/integration/campaign-info-factory.test.ts b/packages/contracts/__tests__/integration/campaign-info-factory.test.ts new file mode 100644 index 00000000..ba635656 --- /dev/null +++ b/packages/contracts/__tests__/integration/campaign-info-factory.test.ts @@ -0,0 +1,132 @@ +import { getTestClient, getTestConfig } from "../setup/test-client"; +import { BYTES32_ZERO } from "../../src/constants/encoding"; +import type { CreateCampaignParams } from "../../src/types/params"; + +const cfg = getTestConfig(); +const client = getTestClient(); +const cif = client.campaignInfoFactory(cfg.addresses.campaignInfoFactory); + +describe("CampaignInfoFactory — reads", () => { + it("identifierToCampaignInfo returns an address", async () => { + const addr = await cif.identifierToCampaignInfo(BYTES32_ZERO); + expect(addr).toMatch(/^0x[0-9a-fA-F]{40}$/); + }); + + it("isValidCampaignInfo returns a boolean", async () => { + const valid = await cif.isValidCampaignInfo( + "0x0000000000000000000000000000000000000001", + ); + expect(typeof valid).toBe("boolean"); + }); + + it("owner returns an address", async () => { + const addr = await cif.owner(); + expect(addr).toMatch(/^0x[0-9a-fA-F]{40}$/); + }); +}); + +describe("CampaignInfoFactory — writes (may revert)", () => { + const dummyParams: CreateCampaignParams = { + creator: "0x0000000000000000000000000000000000000001", + identifierHash: BYTES32_ZERO, + selectedPlatformHash: [BYTES32_ZERO], + campaignData: { + launchTime: 9999999999n, + deadline: 9999999999n, + goalAmount: 1000n, + currency: BYTES32_ZERO, + }, + nftName: "Test", + nftSymbol: "TST", + nftImageURI: "https://example.com/image.png", + contractURI: "https://example.com/contract.json", + }; + + it("createCampaign executes", async () => { + try { + await cif.createCampaign(dummyParams); + } catch { + /* revert expected */ + } + }); + + it("createCampaign with optional keys executes", async () => { + try { + await cif.createCampaign({ + ...dummyParams, + platformDataKey: [BYTES32_ZERO], + platformDataValue: [BYTES32_ZERO], + }); + } catch { + /* revert expected */ + } + }); + + it("updateImplementation executes", async () => { + try { + await cif.updateImplementation( + "0x0000000000000000000000000000000000000001", + ); + } catch { + /* revert expected */ + } + }); + + it("transferOwnership executes", async () => { + try { + await cif.transferOwnership("0x0000000000000000000000000000000000000001"); + } catch { + /* revert expected */ + } + }); + + it("renounceOwnership executes", async () => { + try { + await cif.renounceOwnership(); + } catch { + /* revert expected */ + } + }); +}); + +describe("CampaignInfoFactory — simulate (may throw)", () => { + const dummyParams: CreateCampaignParams = { + creator: "0x0000000000000000000000000000000000000001", + identifierHash: BYTES32_ZERO, + selectedPlatformHash: [BYTES32_ZERO], + campaignData: { + launchTime: 9999999999n, + deadline: 9999999999n, + goalAmount: 1000n, + currency: BYTES32_ZERO, + }, + nftName: "Test", + nftSymbol: "TST", + nftImageURI: "https://example.com/image.png", + contractURI: "https://example.com/contract.json", + }; + + it("simulate.createCampaign", async () => { + try { + await cif.simulate.createCampaign(dummyParams); + } catch { + /* expected */ + } + }); + + it("simulate.updateImplementation", async () => { + try { + await cif.simulate.updateImplementation( + "0x0000000000000000000000000000000000000001", + ); + } catch { + /* expected */ + } + }); +}); + +describe("CampaignInfoFactory — events", () => { + it("events is an empty object", () => { + expect(cif.events).toEqual({}); + }); +}); diff --git a/packages/contracts/__tests__/integration/campaign-info.test.ts b/packages/contracts/__tests__/integration/campaign-info.test.ts new file mode 100644 index 00000000..4cae9d0b --- /dev/null +++ b/packages/contracts/__tests__/integration/campaign-info.test.ts @@ -0,0 +1,133 @@ +import { getTestClient, getTestConfig } from "../setup/test-client"; +import { BYTES32_ZERO } from "../../src/constants/encoding"; + +const cfg = getTestConfig(); +const client = getTestClient(); +const ci = client.campaignInfo(cfg.addresses.campaignInfo); +const ZERO_ADDR = "0x0000000000000000000000000000000000000001" as const; + +describe("CampaignInfo — reads (may revert on uninitialized implementation)", () => { + it("getLaunchTime", async () => { + try { expect(typeof (await ci.getLaunchTime())).toBe("bigint"); } catch { /* implementation revert */ } + }); + it("getDeadline", async () => { + try { expect(typeof (await ci.getDeadline())).toBe("bigint"); } catch { /* implementation revert */ } + }); + it("getGoalAmount", async () => { + try { expect(typeof (await ci.getGoalAmount())).toBe("bigint"); } catch { /* implementation revert */ } + }); + it("getCampaignCurrency", async () => { + try { expect(await ci.getCampaignCurrency()).toMatch(/^0x/); } catch { /* implementation revert */ } + }); + it("getIdentifierHash", async () => { + try { expect(await ci.getIdentifierHash()).toMatch(/^0x/); } catch { /* implementation revert */ } + }); + it("checkIfPlatformSelected", async () => { + try { expect(typeof (await ci.checkIfPlatformSelected(BYTES32_ZERO))).toBe("boolean"); } catch { /* implementation revert */ } + }); + it("checkIfPlatformApproved", async () => { + try { expect(typeof (await ci.checkIfPlatformApproved(BYTES32_ZERO))).toBe("boolean"); } catch { /* implementation revert */ } + }); + it("getPlatformAdminAddress", async () => { + try { expect(await ci.getPlatformAdminAddress(BYTES32_ZERO)).toMatch(/^0x/); } catch { /* implementation revert */ } + }); + it("getPlatformData", async () => { + try { expect(await ci.getPlatformData(BYTES32_ZERO)).toMatch(/^0x/); } catch { /* implementation revert */ } + }); + it("getPlatformFeePercent", async () => { + try { expect(typeof (await ci.getPlatformFeePercent(BYTES32_ZERO))).toBe("bigint"); } catch { /* implementation revert */ } + }); + it("getPlatformClaimDelay", async () => { + try { expect(typeof (await ci.getPlatformClaimDelay(BYTES32_ZERO))).toBe("bigint"); } catch { /* implementation revert */ } + }); + it("getProtocolAdminAddress", async () => { + try { expect(await ci.getProtocolAdminAddress()).toMatch(/^0x/); } catch { /* implementation revert */ } + }); + it("getProtocolFeePercent", async () => { + try { expect(typeof (await ci.getProtocolFeePercent())).toBe("bigint"); } catch { /* implementation revert */ } + }); + it("getAcceptedTokens", async () => { + try { expect(Array.isArray(await ci.getAcceptedTokens())).toBe(true); } catch { /* implementation revert */ } + }); + it("isTokenAccepted", async () => { + try { expect(typeof (await ci.isTokenAccepted(ZERO_ADDR))).toBe("boolean"); } catch { /* implementation revert */ } + }); + it("getTotalRaisedAmount", async () => { + try { expect(typeof (await ci.getTotalRaisedAmount())).toBe("bigint"); } catch { /* implementation revert */ } + }); + it("getTotalLifetimeRaisedAmount", async () => { + try { expect(typeof (await ci.getTotalLifetimeRaisedAmount())).toBe("bigint"); } catch { /* implementation revert */ } + }); + it("getTotalRefundedAmount", async () => { + try { expect(typeof (await ci.getTotalRefundedAmount())).toBe("bigint"); } catch { /* implementation revert */ } + }); + it("getTotalAvailableRaisedAmount", async () => { + try { expect(typeof (await ci.getTotalAvailableRaisedAmount())).toBe("bigint"); } catch { /* implementation revert */ } + }); + it("getTotalCancelledAmount", async () => { + try { expect(typeof (await ci.getTotalCancelledAmount())).toBe("bigint"); } catch { /* implementation revert */ } + }); + it("getTotalExpectedAmount", async () => { + try { expect(typeof (await ci.getTotalExpectedAmount())).toBe("bigint"); } catch { /* implementation revert */ } + }); + it("getDataFromRegistry", async () => { + try { expect(await ci.getDataFromRegistry(BYTES32_ZERO)).toMatch(/^0x/); } catch { /* implementation revert */ } + }); + it("getBufferTime", async () => { + try { expect(typeof (await ci.getBufferTime())).toBe("bigint"); } catch { /* implementation revert */ } + }); + it("getLineItemType", async () => { + try { expect(await ci.getLineItemType(BYTES32_ZERO, BYTES32_ZERO)).toBeDefined(); } catch { /* implementation revert */ } + }); + it("getCampaignConfig", async () => { + try { expect(await ci.getCampaignConfig()).toBeDefined(); } catch { /* implementation revert */ } + }); + it("getApprovedPlatformHashes", async () => { + try { expect(Array.isArray(await ci.getApprovedPlatformHashes())).toBe(true); } catch { /* implementation revert */ } + }); + it("isLocked", async () => { + try { expect(typeof (await ci.isLocked())).toBe("boolean"); } catch { /* implementation revert */ } + }); + it("cancelled", async () => { + try { expect(typeof (await ci.cancelled())).toBe("boolean"); } catch { /* implementation revert */ } + }); + it("owner", async () => { + try { expect(await ci.owner()).toMatch(/^0x/); } catch { /* implementation revert */ } + }); + it("paused", async () => { + try { expect(typeof (await ci.paused())).toBe("boolean"); } catch { /* implementation revert */ } + }); +}); + +describe("CampaignInfo — writes (may revert)", () => { + it("updateDeadline", async () => { try { await ci.updateDeadline(9999999999n); } catch { /* expected */ } }); + it("updateGoalAmount", async () => { try { await ci.updateGoalAmount(1000n); } catch { /* expected */ } }); + it("updateLaunchTime", async () => { try { await ci.updateLaunchTime(9999999999n); } catch { /* expected */ } }); + it("updateSelectedPlatform", async () => { try { await ci.updateSelectedPlatform(BYTES32_ZERO, true, [], []); } catch { /* expected */ } }); + it("setImageURI", async () => { try { await ci.setImageURI("https://example.com/img.png"); } catch { /* expected */ } }); + it("updateContractURI", async () => { try { await ci.updateContractURI("https://example.com/c.json"); } catch { /* expected */ } }); + it("mintNFTForPledge", async () => { try { await ci.mintNFTForPledge(ZERO_ADDR, BYTES32_ZERO, ZERO_ADDR, 100n, 0n, 0n); } catch { /* expected */ } }); + it("burn", async () => { try { await ci.burn(0n); } catch { /* expected */ } }); + it("pauseCampaign", async () => { try { await ci.pauseCampaign(BYTES32_ZERO); } catch { /* expected */ } }); + it("unpauseCampaign", async () => { try { await ci.unpauseCampaign(BYTES32_ZERO); } catch { /* expected */ } }); + it("cancelCampaign", async () => { try { await ci.cancelCampaign(BYTES32_ZERO); } catch { /* expected */ } }); + it("setPlatformInfo", async () => { try { await ci.setPlatformInfo(BYTES32_ZERO, ZERO_ADDR); } catch { /* expected */ } }); + it("transferOwnership", async () => { try { await ci.transferOwnership(ZERO_ADDR); } catch { /* expected */ } }); + it("renounceOwnership", async () => { try { await ci.renounceOwnership(); } catch { /* expected */ } }); +}); + +describe("CampaignInfo — simulate (may throw)", () => { + it("simulate.updateDeadline", async () => { try { await ci.simulate.updateDeadline(9999999999n); } catch { /* expected */ } }); + it("simulate.updateGoalAmount", async () => { try { await ci.simulate.updateGoalAmount(1000n); } catch { /* expected */ } }); + it("simulate.updateLaunchTime", async () => { try { await ci.simulate.updateLaunchTime(9999999999n); } catch { /* expected */ } }); + it("simulate.updateSelectedPlatform", async () => { try { await ci.simulate.updateSelectedPlatform(BYTES32_ZERO, true, [], []); } catch { /* expected */ } }); + it("simulate.mintNFTForPledge", async () => { try { await ci.simulate.mintNFTForPledge(ZERO_ADDR, BYTES32_ZERO, ZERO_ADDR, 100n, 0n, 0n); } catch { /* expected */ } }); + it("simulate.pauseCampaign", async () => { try { await ci.simulate.pauseCampaign(BYTES32_ZERO); } catch { /* expected */ } }); + it("simulate.cancelCampaign", async () => { try { await ci.simulate.cancelCampaign(BYTES32_ZERO); } catch { /* expected */ } }); +}); + +describe("CampaignInfo — events", () => { + it("events is an empty object", () => { + expect(ci.events).toEqual({}); + }); +}); diff --git a/packages/contracts/__tests__/integration/client.test.ts b/packages/contracts/__tests__/integration/client.test.ts new file mode 100644 index 00000000..5175789f --- /dev/null +++ b/packages/contracts/__tests__/integration/client.test.ts @@ -0,0 +1,141 @@ +import { createOakContractsClient } from "../../src/client/create"; +import { CHAIN_IDS } from "../../src/constants/chains"; +import { getTestConfig, getTestClient } from "../setup/test-client"; +import { + createJsonRpcProvider, + createWallet, +} from "../../src/lib/viem/provider"; +import { getChainFromId } from "../../src/utils/chain"; + +const cfg = getTestConfig(); + +describe("createOakContractsClient — simple config", () => { + const client = getTestClient(); + + it("resolves chain to Celo Sepolia", () => { + expect(client.config.chain.id).toBe(CHAIN_IDS.CELO_TESTNET_SEPOLIA); + }); + + it("has publicClient and walletClient", () => { + expect(client.publicClient).toBeDefined(); + expect(client.walletClient).toBeDefined(); + }); + + it("defaults timeout to 30000", () => { + expect(client.options.timeout).toBe(30000); + }); +}); + +describe("createOakContractsClient — full config", () => { + const chain = getChainFromId(CHAIN_IDS.CELO_TESTNET_SEPOLIA); + const provider = createJsonRpcProvider(cfg.rpcUrl, chain); + const signer = createWallet(cfg.privateKey, cfg.rpcUrl, chain); + + const client = createOakContractsClient({ + chain, + provider, + signer, + }); + + it("passes through provider and signer", () => { + expect(client.publicClient).toBe(provider); + expect(client.walletClient).toBe(signer); + }); + + it("resolves chain object directly", () => { + expect(client.config.chain).toBe(chain); + }); +}); + +describe("createOakContractsClient — full config with numeric chain", () => { + const chain = getChainFromId(CHAIN_IDS.CELO_TESTNET_SEPOLIA); + const provider = createJsonRpcProvider(cfg.rpcUrl, chain); + const signer = createWallet(cfg.privateKey, cfg.rpcUrl, chain); + + const client = createOakContractsClient({ + chain: CHAIN_IDS.CELO_TESTNET_SEPOLIA, + provider, + signer, + }); + + it("resolves numeric chain to chain object", () => { + expect(client.config.chain.id).toBe(CHAIN_IDS.CELO_TESTNET_SEPOLIA); + }); +}); + +describe("createOakContractsClient — options override", () => { + const client = createOakContractsClient({ + chainId: CHAIN_IDS.CELO_TESTNET_SEPOLIA, + rpcUrl: cfg.rpcUrl, + privateKey: cfg.privateKey, + options: { timeout: 60000 }, + }); + + it("merges custom timeout", () => { + expect(client.options.timeout).toBe(60000); + }); +}); + +describe("entity factory methods", () => { + const client = getTestClient(); + const addr = cfg.addresses; + + it("globalParams returns entity with read/write/simulate/events", () => { + const entity = client.globalParams(addr.globalParams); + expect(typeof entity.getProtocolAdminAddress).toBe("function"); + expect(typeof entity.enlistPlatform).toBe("function"); + expect(entity.simulate).toBeDefined(); + expect(entity.events).toBeDefined(); + }); + + it("campaignInfoFactory returns entity", () => { + const entity = client.campaignInfoFactory(addr.campaignInfoFactory); + expect(typeof entity.identifierToCampaignInfo).toBe("function"); + expect(typeof entity.createCampaign).toBe("function"); + expect(entity.simulate).toBeDefined(); + expect(entity.events).toBeDefined(); + }); + + it("treasuryFactory returns entity", () => { + const entity = client.treasuryFactory(addr.treasuryFactory); + expect(typeof entity.deploy).toBe("function"); + expect(entity.simulate).toBeDefined(); + expect(entity.events).toBeDefined(); + }); + + it("campaignInfo returns entity", () => { + const entity = client.campaignInfo(addr.campaignInfo); + expect(typeof entity.getLaunchTime).toBe("function"); + expect(entity.simulate).toBeDefined(); + expect(entity.events).toBeDefined(); + }); + + it("paymentTreasury returns entity", () => { + const entity = client.paymentTreasury(addr.paymentTreasury); + expect(typeof entity.getPlatformHash).toBe("function"); + expect(entity.simulate).toBeDefined(); + expect(entity.events).toBeDefined(); + }); + + it("allOrNothingTreasury returns entity", () => { + const entity = client.allOrNothingTreasury(addr.allOrNothing); + expect(typeof entity.getRaisedAmount).toBe("function"); + expect(entity.simulate).toBeDefined(); + expect(entity.events).toBeDefined(); + }); + + it("keepWhatsRaisedTreasury returns entity", () => { + const entity = client.keepWhatsRaisedTreasury(addr.keepWhatsRaised); + expect(typeof entity.getRaisedAmount).toBe("function"); + expect(entity.simulate).toBeDefined(); + expect(entity.events).toBeDefined(); + }); + +}); + +describe("waitForReceipt", () => { + it("is a function on the client", () => { + const client = getTestClient(); + expect(typeof client.waitForReceipt).toBe("function"); + }); +}); diff --git a/packages/contracts/__tests__/integration/global-params.test.ts b/packages/contracts/__tests__/integration/global-params.test.ts new file mode 100644 index 00000000..dc350572 --- /dev/null +++ b/packages/contracts/__tests__/integration/global-params.test.ts @@ -0,0 +1,276 @@ +import { getTestClient, getTestConfig } from "../setup/test-client"; +import { BYTES32_ZERO } from "../../src/constants/encoding"; + +const cfg = getTestConfig(); +const client = getTestClient(); +const gp = client.globalParams(cfg.addresses.globalParams); +const ZERO_ADDR = "0x0000000000000000000000000000000000000001" as const; + +describe("GlobalParams — reads", () => { + it("getProtocolAdminAddress returns an address", async () => { + const addr = await gp.getProtocolAdminAddress(); + expect(addr).toMatch(/^0x[0-9a-fA-F]{40}$/); + }); + + it("getProtocolFeePercent returns a bigint", async () => { + const fee = await gp.getProtocolFeePercent(); + expect(typeof fee).toBe("bigint"); + }); + + it("getNumberOfListedPlatforms returns a bigint", async () => { + const count = await gp.getNumberOfListedPlatforms(); + expect(typeof count).toBe("bigint"); + }); + + it("checkIfPlatformIsListed returns a boolean", async () => { + const listed = await gp.checkIfPlatformIsListed(BYTES32_ZERO); + expect(typeof listed).toBe("boolean"); + }); + + it("checkIfPlatformDataKeyValid returns a boolean", async () => { + const valid = await gp.checkIfPlatformDataKeyValid(BYTES32_ZERO); + expect(typeof valid).toBe("boolean"); + }); + + it("getPlatformAdminAddress returns or reverts for unlisted platform", async () => { + try { + const addr = await gp.getPlatformAdminAddress(BYTES32_ZERO); + expect(addr).toMatch(/^0x/); + } catch { + /* reverts for unlisted platform hash */ + } + }); + + it("getPlatformFeePercent returns or reverts for unlisted platform", async () => { + try { + const fee = await gp.getPlatformFeePercent(BYTES32_ZERO); + expect(typeof fee).toBe("bigint"); + } catch { + /* reverts for unlisted platform hash */ + } + }); + + it("getPlatformClaimDelay returns or reverts for unlisted platform", async () => { + try { + const delay = await gp.getPlatformClaimDelay(BYTES32_ZERO); + expect(typeof delay).toBe("bigint"); + } catch { + /* reverts for unlisted platform hash */ + } + }); + + it("getPlatformAdapter returns or reverts for unlisted platform", async () => { + try { + const addr = await gp.getPlatformAdapter(BYTES32_ZERO); + expect(addr).toMatch(/^0x/); + } catch { + /* reverts for unlisted platform hash */ + } + }); + + it("getPlatformDataOwner returns a hex value", async () => { + const owner = await gp.getPlatformDataOwner(BYTES32_ZERO); + expect(owner).toMatch(/^0x/); + }); + + it("getPlatformLineItemType returns a LineItemTypeInfo", async () => { + const info = await gp.getPlatformLineItemType(BYTES32_ZERO, BYTES32_ZERO); + expect(info).toBeDefined(); + }); + + it("getTokensForCurrency returns an array", async () => { + const tokens = await gp.getTokensForCurrency(BYTES32_ZERO); + expect(Array.isArray(tokens)).toBe(true); + }); + + it("getFromRegistry returns a hex value", async () => { + const val = await gp.getFromRegistry(BYTES32_ZERO); + expect(val).toMatch(/^0x/); + }); + + it("owner returns an address", async () => { + const addr = await gp.owner(); + expect(addr).toMatch(/^0x[0-9a-fA-F]{40}$/); + }); +}); + +describe("GlobalParams — writes (may revert)", () => { + it("enlistPlatform", async () => { + try { + await gp.enlistPlatform(BYTES32_ZERO, ZERO_ADDR, 100n, ZERO_ADDR); + } catch { + /* revert expected */ + } + }); + + it("delistPlatform", async () => { + try { + await gp.delistPlatform(BYTES32_ZERO); + } catch { + /* revert expected */ + } + }); + + it("updatePlatformAdminAddress", async () => { + try { + await gp.updatePlatformAdminAddress(BYTES32_ZERO, ZERO_ADDR); + } catch { + /* revert expected */ + } + }); + + it("updatePlatformClaimDelay", async () => { + try { + await gp.updatePlatformClaimDelay(BYTES32_ZERO, 0n); + } catch { + /* revert expected */ + } + }); + + it("updateProtocolAdminAddress", async () => { + try { + await gp.updateProtocolAdminAddress(ZERO_ADDR); + } catch { + /* revert expected */ + } + }); + + it("updateProtocolFeePercent", async () => { + try { + await gp.updateProtocolFeePercent(100n); + } catch { + /* revert expected */ + } + }); + + it("setPlatformAdapter", async () => { + try { + await gp.setPlatformAdapter(BYTES32_ZERO, ZERO_ADDR); + } catch { + /* revert expected */ + } + }); + + it("setPlatformLineItemType", async () => { + try { + await gp.setPlatformLineItemType(BYTES32_ZERO, BYTES32_ZERO, "test", true, true, true, false); + } catch { + /* revert expected */ + } + }); + + it("removePlatformLineItemType", async () => { + try { + await gp.removePlatformLineItemType(BYTES32_ZERO, BYTES32_ZERO); + } catch { + /* revert expected */ + } + }); + + it("addTokenToCurrency", async () => { + try { + await gp.addTokenToCurrency(BYTES32_ZERO, ZERO_ADDR); + } catch { + /* revert expected */ + } + }); + + it("removeTokenFromCurrency", async () => { + try { + await gp.removeTokenFromCurrency(BYTES32_ZERO, ZERO_ADDR); + } catch { + /* revert expected */ + } + }); + + it("addPlatformData", async () => { + try { + await gp.addPlatformData(BYTES32_ZERO, BYTES32_ZERO); + } catch { + /* revert expected */ + } + }); + + it("removePlatformData", async () => { + try { + await gp.removePlatformData(BYTES32_ZERO, BYTES32_ZERO); + } catch { + /* revert expected */ + } + }); + + it("addToRegistry", async () => { + try { + await gp.addToRegistry(BYTES32_ZERO, BYTES32_ZERO); + } catch { + /* revert expected */ + } + }); + + it("transferOwnership", async () => { + try { + await gp.transferOwnership(ZERO_ADDR); + } catch { + /* revert expected */ + } + }); + + it("renounceOwnership", async () => { + try { + await gp.renounceOwnership(); + } catch { + /* revert expected */ + } + }); +}); + +describe("GlobalParams — simulate (may throw)", () => { + it("simulate.enlistPlatform", async () => { + try { await gp.simulate.enlistPlatform(BYTES32_ZERO, ZERO_ADDR, 100n, ZERO_ADDR); } catch { /* expected */ } + }); + it("simulate.delistPlatform", async () => { + try { await gp.simulate.delistPlatform(BYTES32_ZERO); } catch { /* expected */ } + }); + it("simulate.updatePlatformAdminAddress", async () => { + try { await gp.simulate.updatePlatformAdminAddress(BYTES32_ZERO, ZERO_ADDR); } catch { /* expected */ } + }); + it("simulate.updatePlatformClaimDelay", async () => { + try { await gp.simulate.updatePlatformClaimDelay(BYTES32_ZERO, 0n); } catch { /* expected */ } + }); + it("simulate.updateProtocolAdminAddress", async () => { + try { await gp.simulate.updateProtocolAdminAddress(ZERO_ADDR); } catch { /* expected */ } + }); + it("simulate.updateProtocolFeePercent", async () => { + try { await gp.simulate.updateProtocolFeePercent(100n); } catch { /* expected */ } + }); + it("simulate.setPlatformAdapter", async () => { + try { await gp.simulate.setPlatformAdapter(BYTES32_ZERO, ZERO_ADDR); } catch { /* expected */ } + }); + it("simulate.setPlatformLineItemType", async () => { + try { await gp.simulate.setPlatformLineItemType(BYTES32_ZERO, BYTES32_ZERO, "test", true, true, true, false); } catch { /* expected */ } + }); + it("simulate.removePlatformLineItemType", async () => { + try { await gp.simulate.removePlatformLineItemType(BYTES32_ZERO, BYTES32_ZERO); } catch { /* expected */ } + }); + it("simulate.addTokenToCurrency", async () => { + try { await gp.simulate.addTokenToCurrency(BYTES32_ZERO, ZERO_ADDR); } catch { /* expected */ } + }); + it("simulate.removeTokenFromCurrency", async () => { + try { await gp.simulate.removeTokenFromCurrency(BYTES32_ZERO, ZERO_ADDR); } catch { /* expected */ } + }); + it("simulate.addPlatformData", async () => { + try { await gp.simulate.addPlatformData(BYTES32_ZERO, BYTES32_ZERO); } catch { /* expected */ } + }); + it("simulate.removePlatformData", async () => { + try { await gp.simulate.removePlatformData(BYTES32_ZERO, BYTES32_ZERO); } catch { /* expected */ } + }); + it("simulate.addToRegistry", async () => { + try { await gp.simulate.addToRegistry(BYTES32_ZERO, BYTES32_ZERO); } catch { /* expected */ } + }); +}); + +describe("GlobalParams — events", () => { + it("events is an empty object", () => { + expect(gp.events).toEqual({}); + }); +}); diff --git a/packages/contracts/__tests__/integration/keep-whats-raised.test.ts b/packages/contracts/__tests__/integration/keep-whats-raised.test.ts new file mode 100644 index 00000000..2dc596d6 --- /dev/null +++ b/packages/contracts/__tests__/integration/keep-whats-raised.test.ts @@ -0,0 +1,84 @@ +import { getTestClient, getTestConfig } from "../setup/test-client"; +import { BYTES32_ZERO } from "../../src/constants/encoding"; + +const cfg = getTestConfig(); +const client = getTestClient(); +const kwr = client.keepWhatsRaisedTreasury(cfg.addresses.keepWhatsRaised); +const ZERO_ADDR = "0x0000000000000000000000000000000000000001" as const; + +describe("KeepWhatsRaised — reads (may revert on uninitialized implementation)", () => { + it("getRaisedAmount", async () => { try { expect(typeof (await kwr.getRaisedAmount())).toBe("bigint"); } catch { /* implementation revert */ } }); + it("getLifetimeRaisedAmount", async () => { try { expect(typeof (await kwr.getLifetimeRaisedAmount())).toBe("bigint"); } catch { /* implementation revert */ } }); + it("getRefundedAmount", async () => { try { expect(typeof (await kwr.getRefundedAmount())).toBe("bigint"); } catch { /* implementation revert */ } }); + it("getAvailableRaisedAmount", async () => { try { expect(typeof (await kwr.getAvailableRaisedAmount())).toBe("bigint"); } catch { /* implementation revert */ } }); + it("getReward", async () => { try { expect(await kwr.getReward(BYTES32_ZERO)).toBeDefined(); } catch { /* implementation revert */ } }); + it("getPlatformHash", async () => { try { expect(await kwr.getPlatformHash()).toMatch(/^0x/); } catch { /* implementation revert */ } }); + it("getPlatformFeePercent", async () => { try { expect(typeof (await kwr.getPlatformFeePercent())).toBe("bigint"); } catch { /* implementation revert */ } }); + it("getWithdrawalApprovalStatus", async () => { try { expect(typeof (await kwr.getWithdrawalApprovalStatus())).toBe("boolean"); } catch { /* implementation revert */ } }); + it("getLaunchTime", async () => { try { expect(typeof (await kwr.getLaunchTime())).toBe("bigint"); } catch { /* implementation revert */ } }); + it("getDeadline", async () => { try { expect(typeof (await kwr.getDeadline())).toBe("bigint"); } catch { /* implementation revert */ } }); + it("getGoalAmount", async () => { try { expect(typeof (await kwr.getGoalAmount())).toBe("bigint"); } catch { /* implementation revert */ } }); + it("getPaymentGatewayFee", async () => { try { expect(typeof (await kwr.getPaymentGatewayFee(BYTES32_ZERO))).toBe("bigint"); } catch { /* implementation revert */ } }); + it("getFeeValue", async () => { try { expect(typeof (await kwr.getFeeValue(BYTES32_ZERO))).toBe("bigint"); } catch { /* implementation revert */ } }); + it("paused", async () => { try { expect(typeof (await kwr.paused())).toBe("boolean"); } catch { /* implementation revert */ } }); + it("cancelled", async () => { try { expect(typeof (await kwr.cancelled())).toBe("boolean"); } catch { /* implementation revert */ } }); + it("balanceOf", async () => { try { expect(typeof (await kwr.balanceOf(ZERO_ADDR))).toBe("bigint"); } catch { /* implementation revert */ } }); + it("ownerOf (may revert)", async () => { try { await kwr.ownerOf(0n); } catch { /* expected */ } }); + it("tokenURI (may revert)", async () => { try { await kwr.tokenURI(0n); } catch { /* expected */ } }); + it("name", async () => { try { expect(typeof (await kwr.name())).toBe("string"); } catch { /* implementation revert */ } }); + it("symbol", async () => { try { expect(typeof (await kwr.symbol())).toBe("string"); } catch { /* implementation revert */ } }); + it("getApproved (may revert)", async () => { try { await kwr.getApproved(0n); } catch { /* expected */ } }); + it("isApprovedForAll", async () => { try { expect(typeof (await kwr.isApprovedForAll(ZERO_ADDR, ZERO_ADDR))).toBe("boolean"); } catch { /* implementation revert */ } }); + it("supportsInterface", async () => { try { expect(typeof (await kwr.supportsInterface("0x80ac58cd"))).toBe("boolean"); } catch { /* implementation revert */ } }); +}); + +describe("KeepWhatsRaised — writes (may revert)", () => { + it("pauseTreasury", async () => { try { await kwr.pauseTreasury(BYTES32_ZERO); } catch { /* expected */ } }); + it("unpauseTreasury", async () => { try { await kwr.unpauseTreasury(BYTES32_ZERO); } catch { /* expected */ } }); + it("cancelTreasury", async () => { try { await kwr.cancelTreasury(BYTES32_ZERO); } catch { /* expected */ } }); + it("configureTreasury", async () => { + try { + await kwr.configureTreasury( + { minimumWithdrawalForFeeExemption: 0n, withdrawalDelay: 0n, refundDelay: 0n, configLockPeriod: 0n, isColombianCreator: false }, + { launchTime: 9999999999n, deadline: 9999999999n, goalAmount: 1000n, currency: BYTES32_ZERO }, + { flatFeeKey: BYTES32_ZERO, cumulativeFlatFeeKey: BYTES32_ZERO, grossPercentageFeeKeys: [] }, + { flatFeeValue: 0n, cumulativeFlatFeeValue: 0n, grossPercentageFeeValues: [] }, + ); + } catch { /* expected */ } + }); + it("addRewards", async () => { + try { await kwr.addRewards([BYTES32_ZERO], [{ rewardValue: 100n, isRewardTier: false, itemId: [], itemValue: [], itemQuantity: [] }]); } catch { /* expected */ } + }); + it("removeReward", async () => { try { await kwr.removeReward(BYTES32_ZERO); } catch { /* expected */ } }); + it("approveWithdrawal", async () => { try { await kwr.approveWithdrawal(); } catch { /* expected */ } }); + it("setPaymentGatewayFee", async () => { try { await kwr.setPaymentGatewayFee(BYTES32_ZERO, 0n); } catch { /* expected */ } }); + it("setFeeAndPledge", async () => { try { await kwr.setFeeAndPledge(BYTES32_ZERO, ZERO_ADDR, ZERO_ADDR, 100n, 0n, 0n, [BYTES32_ZERO], true); } catch { /* expected */ } }); + it("pledgeForAReward", async () => { try { await kwr.pledgeForAReward(BYTES32_ZERO, ZERO_ADDR, ZERO_ADDR, 0n, [BYTES32_ZERO]); } catch { /* expected */ } }); + it("pledgeWithoutAReward", async () => { try { await kwr.pledgeWithoutAReward(BYTES32_ZERO, ZERO_ADDR, ZERO_ADDR, 100n, 0n); } catch { /* expected */ } }); + it("claimRefund", async () => { try { await kwr.claimRefund(0n); } catch { /* expected */ } }); + it("claimTip", async () => { try { await kwr.claimTip(); } catch { /* expected */ } }); + it("claimFund", async () => { try { await kwr.claimFund(); } catch { /* expected */ } }); + it("disburseFees", async () => { try { await kwr.disburseFees(); } catch { /* expected */ } }); + it("withdraw", async () => { try { await kwr.withdraw(ZERO_ADDR, 0n); } catch { /* expected */ } }); + it("updateDeadline", async () => { try { await kwr.updateDeadline(9999999999n); } catch { /* expected */ } }); + it("updateGoalAmount", async () => { try { await kwr.updateGoalAmount(1000n); } catch { /* expected */ } }); + it("approve", async () => { try { await kwr.approve(ZERO_ADDR, 0n); } catch { /* expected */ } }); + it("setApprovalForAll", async () => { try { await kwr.setApprovalForAll(ZERO_ADDR, true); } catch { /* expected */ } }); + it("safeTransferFrom", async () => { try { await kwr.safeTransferFrom(ZERO_ADDR, ZERO_ADDR, 0n); } catch { /* expected */ } }); + it("transferFrom", async () => { try { await kwr.transferFrom(ZERO_ADDR, ZERO_ADDR, 0n); } catch { /* expected */ } }); +}); + +describe("KeepWhatsRaised — simulate (may throw)", () => { + it("simulate.pledgeForAReward", async () => { try { await kwr.simulate.pledgeForAReward(BYTES32_ZERO, ZERO_ADDR, ZERO_ADDR, 0n, [BYTES32_ZERO]); } catch { /* expected */ } }); + it("simulate.pledgeWithoutAReward", async () => { try { await kwr.simulate.pledgeWithoutAReward(BYTES32_ZERO, ZERO_ADDR, ZERO_ADDR, 100n, 0n); } catch { /* expected */ } }); + it("simulate.claimRefund", async () => { try { await kwr.simulate.claimRefund(0n); } catch { /* expected */ } }); + it("simulate.disburseFees", async () => { try { await kwr.simulate.disburseFees(); } catch { /* expected */ } }); + it("simulate.withdraw", async () => { try { await kwr.simulate.withdraw(ZERO_ADDR, 0n); } catch { /* expected */ } }); + it("simulate.setFeeAndPledge", async () => { try { await kwr.simulate.setFeeAndPledge(BYTES32_ZERO, ZERO_ADDR, ZERO_ADDR, 100n, 0n, 0n, [BYTES32_ZERO], true); } catch { /* expected */ } }); +}); + +describe("KeepWhatsRaised — events", () => { + it("events is an empty object", () => { + expect(kwr.events).toEqual({}); + }); +}); diff --git a/packages/contracts/__tests__/integration/payment-treasury.test.ts b/packages/contracts/__tests__/integration/payment-treasury.test.ts new file mode 100644 index 00000000..86ae5cd8 --- /dev/null +++ b/packages/contracts/__tests__/integration/payment-treasury.test.ts @@ -0,0 +1,61 @@ +import { getTestClient, getTestConfig } from "../setup/test-client"; +import { BYTES32_ZERO } from "../../src/constants/encoding"; + +const cfg = getTestConfig(); +const client = getTestClient(); +const pt = client.paymentTreasury(cfg.addresses.paymentTreasury); +const ZERO_ADDR = "0x0000000000000000000000000000000000000001" as const; + +describe("PaymentTreasury — reads (may revert on uninitialized implementation)", () => { + it("getPlatformHash", async () => { try { expect(await pt.getPlatformHash()).toMatch(/^0x/); } catch { /* implementation revert */ } }); + it("getPlatformFeePercent", async () => { try { expect(typeof (await pt.getPlatformFeePercent())).toBe("bigint"); } catch { /* implementation revert */ } }); + it("getRaisedAmount", async () => { try { expect(typeof (await pt.getRaisedAmount())).toBe("bigint"); } catch { /* implementation revert */ } }); + it("getAvailableRaisedAmount", async () => { try { expect(typeof (await pt.getAvailableRaisedAmount())).toBe("bigint"); } catch { /* implementation revert */ } }); + it("getLifetimeRaisedAmount", async () => { try { expect(typeof (await pt.getLifetimeRaisedAmount())).toBe("bigint"); } catch { /* implementation revert */ } }); + it("getRefundedAmount", async () => { try { expect(typeof (await pt.getRefundedAmount())).toBe("bigint"); } catch { /* implementation revert */ } }); + it("getExpectedAmount", async () => { try { expect(typeof (await pt.getExpectedAmount())).toBe("bigint"); } catch { /* implementation revert */ } }); + it("getPaymentData", async () => { try { expect(await pt.getPaymentData(BYTES32_ZERO)).toBeDefined(); } catch { /* implementation revert */ } }); + it("cancelled", async () => { try { expect(typeof (await pt.cancelled())).toBe("boolean"); } catch { /* implementation revert */ } }); +}); + +describe("PaymentTreasury — writes (may revert)", () => { + it("createPayment", async () => { try { await pt.createPayment(BYTES32_ZERO, BYTES32_ZERO, BYTES32_ZERO, ZERO_ADDR, 100n, 9999999999n, [], []); } catch { /* expected */ } }); + it("createPaymentBatch", async () => { try { await pt.createPaymentBatch([BYTES32_ZERO], [BYTES32_ZERO], [BYTES32_ZERO], [ZERO_ADDR], [100n], [9999999999n], [[]], [[]]); } catch { /* expected */ } }); + it("processCryptoPayment", async () => { try { await pt.processCryptoPayment(BYTES32_ZERO, BYTES32_ZERO, ZERO_ADDR, ZERO_ADDR, 100n, [], []); } catch { /* expected */ } }); + it("cancelPayment", async () => { try { await pt.cancelPayment(BYTES32_ZERO); } catch { /* expected */ } }); + it("confirmPayment", async () => { try { await pt.confirmPayment(BYTES32_ZERO, ZERO_ADDR); } catch { /* expected */ } }); + it("confirmPaymentBatch", async () => { try { await pt.confirmPaymentBatch([BYTES32_ZERO], [ZERO_ADDR]); } catch { /* expected */ } }); + it("disburseFees", async () => { try { await pt.disburseFees(); } catch { /* expected */ } }); + it("withdraw", async () => { try { await pt.withdraw(); } catch { /* expected */ } }); + it("claimRefund", async () => { try { await pt.claimRefund(BYTES32_ZERO, ZERO_ADDR); } catch { /* expected */ } }); + it("claimRefundSelf", async () => { try { await pt.claimRefundSelf(BYTES32_ZERO); } catch { /* expected */ } }); + it("claimExpiredFunds", async () => { try { await pt.claimExpiredFunds(); } catch { /* expected */ } }); + it("claimNonGoalLineItems", async () => { try { await pt.claimNonGoalLineItems(ZERO_ADDR); } catch { /* expected */ } }); + it("pauseTreasury", async () => { try { await pt.pauseTreasury(BYTES32_ZERO); } catch { /* expected */ } }); + it("unpauseTreasury", async () => { try { await pt.unpauseTreasury(BYTES32_ZERO); } catch { /* expected */ } }); + it("cancelTreasury", async () => { try { await pt.cancelTreasury(BYTES32_ZERO); } catch { /* expected */ } }); +}); + +describe("PaymentTreasury — simulate (may throw)", () => { + it("simulate.createPayment", async () => { try { await pt.simulate.createPayment(BYTES32_ZERO, BYTES32_ZERO, BYTES32_ZERO, ZERO_ADDR, 100n, 9999999999n, [], []); } catch { /* expected */ } }); + it("simulate.createPaymentBatch", async () => { try { await pt.simulate.createPaymentBatch([BYTES32_ZERO], [BYTES32_ZERO], [BYTES32_ZERO], [ZERO_ADDR], [100n], [9999999999n], [[]], [[]]); } catch { /* expected */ } }); + it("simulate.processCryptoPayment", async () => { try { await pt.simulate.processCryptoPayment(BYTES32_ZERO, BYTES32_ZERO, ZERO_ADDR, ZERO_ADDR, 100n, [], []); } catch { /* expected */ } }); + it("simulate.cancelPayment", async () => { try { await pt.simulate.cancelPayment(BYTES32_ZERO); } catch { /* expected */ } }); + it("simulate.confirmPayment", async () => { try { await pt.simulate.confirmPayment(BYTES32_ZERO, ZERO_ADDR); } catch { /* expected */ } }); + it("simulate.confirmPaymentBatch", async () => { try { await pt.simulate.confirmPaymentBatch([BYTES32_ZERO], [ZERO_ADDR]); } catch { /* expected */ } }); + it("simulate.disburseFees", async () => { try { await pt.simulate.disburseFees(); } catch { /* expected */ } }); + it("simulate.withdraw", async () => { try { await pt.simulate.withdraw(); } catch { /* expected */ } }); + it("simulate.claimRefund", async () => { try { await pt.simulate.claimRefund(BYTES32_ZERO, ZERO_ADDR); } catch { /* expected */ } }); + it("simulate.claimRefundSelf", async () => { try { await pt.simulate.claimRefundSelf(BYTES32_ZERO); } catch { /* expected */ } }); + it("simulate.claimExpiredFunds", async () => { try { await pt.simulate.claimExpiredFunds(); } catch { /* expected */ } }); + it("simulate.claimNonGoalLineItems", async () => { try { await pt.simulate.claimNonGoalLineItems(ZERO_ADDR); } catch { /* expected */ } }); + it("simulate.pauseTreasury", async () => { try { await pt.simulate.pauseTreasury(BYTES32_ZERO); } catch { /* expected */ } }); + it("simulate.unpauseTreasury", async () => { try { await pt.simulate.unpauseTreasury(BYTES32_ZERO); } catch { /* expected */ } }); + it("simulate.cancelTreasury", async () => { try { await pt.simulate.cancelTreasury(BYTES32_ZERO); } catch { /* expected */ } }); +}); + +describe("PaymentTreasury — events", () => { + it("events is an empty object", () => { + expect(pt.events).toEqual({}); + }); +}); diff --git a/packages/contracts/__tests__/integration/treasury-factory.test.ts b/packages/contracts/__tests__/integration/treasury-factory.test.ts new file mode 100644 index 00000000..21706d6e --- /dev/null +++ b/packages/contracts/__tests__/integration/treasury-factory.test.ts @@ -0,0 +1,116 @@ +import { getTestClient, getTestConfig } from "../setup/test-client"; +import { BYTES32_ZERO } from "../../src/constants/encoding"; + +const cfg = getTestConfig(); +const client = getTestClient(); +const tf = client.treasuryFactory(cfg.addresses.treasuryFactory); + +describe("TreasuryFactory — writes (may revert)", () => { + it("deploy executes", async () => { + try { + await tf.deploy( + BYTES32_ZERO, + "0x0000000000000000000000000000000000000001", + 0n, + ); + } catch { + /* revert expected */ + } + }, 30_000); + + it("registerTreasuryImplementation executes", async () => { + try { + await tf.registerTreasuryImplementation( + BYTES32_ZERO, + 0n, + "0x0000000000000000000000000000000000000001", + ); + } catch { + /* revert expected */ + } + }); + + it("approveTreasuryImplementation executes", async () => { + try { + await tf.approveTreasuryImplementation(BYTES32_ZERO, 0n); + } catch { + /* revert expected */ + } + }); + + it("disapproveTreasuryImplementation executes", async () => { + try { + await tf.disapproveTreasuryImplementation( + "0x0000000000000000000000000000000000000001", + ); + } catch { + /* revert expected */ + } + }); + + it("removeTreasuryImplementation executes", async () => { + try { + await tf.removeTreasuryImplementation(BYTES32_ZERO, 0n); + } catch { + /* revert expected */ + } + }); +}); + +describe("TreasuryFactory — simulate (may throw)", () => { + it("simulate.deploy", async () => { + try { + await tf.simulate.deploy( + BYTES32_ZERO, + "0x0000000000000000000000000000000000000001", + 0n, + ); + } catch { + /* expected */ + } + }); + + it("simulate.registerTreasuryImplementation", async () => { + try { + await tf.simulate.registerTreasuryImplementation( + BYTES32_ZERO, + 0n, + "0x0000000000000000000000000000000000000001", + ); + } catch { + /* expected */ + } + }); + + it("simulate.approveTreasuryImplementation", async () => { + try { + await tf.simulate.approveTreasuryImplementation(BYTES32_ZERO, 0n); + } catch { + /* expected */ + } + }); + + it("simulate.disapproveTreasuryImplementation", async () => { + try { + await tf.simulate.disapproveTreasuryImplementation( + "0x0000000000000000000000000000000000000001", + ); + } catch { + /* expected */ + } + }); + + it("simulate.removeTreasuryImplementation", async () => { + try { + await tf.simulate.removeTreasuryImplementation(BYTES32_ZERO, 0n); + } catch { + /* expected */ + } + }); +}); + +describe("TreasuryFactory — events", () => { + it("events is an empty object", () => { + expect(tf.events).toEqual({}); + }); +}); diff --git a/packages/contracts/__tests__/setup/config.ts b/packages/contracts/__tests__/setup/config.ts new file mode 100644 index 00000000..10d788f7 --- /dev/null +++ b/packages/contracts/__tests__/setup/config.ts @@ -0,0 +1,41 @@ +import type { Address, Hex } from "../../src/lib"; + +function requireEnv(name: string): string { + const value = process.env[name]; + if (!value) { + throw new Error( + `Missing required env var: ${name} — set it in packages/contracts/.env (see .env.example)`, + ); + } + return value; +} + +export interface TestConfig { + rpcUrl: string; + privateKey: Hex; + addresses: { + globalParams: Address; + campaignInfoFactory: Address; + treasuryFactory: Address; + campaignInfo: Address; + paymentTreasury: Address; + allOrNothing: Address; + keepWhatsRaised: Address; + }; +} + +export function loadTestConfig(): TestConfig { + return { + rpcUrl: requireEnv("RPC_URL"), + privateKey: requireEnv("PRIVATE_KEY") as Hex, + addresses: { + globalParams: requireEnv("GLOBAL_PARAMS_ADDRESS") as Address, + campaignInfoFactory: requireEnv("CAMPAIGN_INFO_FACTORY_ADDRESS") as Address, + treasuryFactory: requireEnv("TREASURY_FACTORY_ADDRESS") as Address, + campaignInfo: requireEnv("CAMPAIGN_INFO_ADDRESS") as Address, + paymentTreasury: requireEnv("PAYMENT_TREASURY_ADDRESS") as Address, + allOrNothing: requireEnv("ALL_OR_NOTHING_ADDRESS") as Address, + keepWhatsRaised: requireEnv("KEEP_WHATS_RAISED_ADDRESS") as Address, + }, + }; +} diff --git a/packages/contracts/__tests__/setup/constant.ts b/packages/contracts/__tests__/setup/constant.ts new file mode 100644 index 00000000..1363198e --- /dev/null +++ b/packages/contracts/__tests__/setup/constant.ts @@ -0,0 +1,5 @@ +export const TEST_PRIVATE_KEY = + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" as const; // well-known Hardhat account #0 private key — a standard throwaway key for tests +export const TEST_RPC_URL = "https://rpc.example.com"; // dummy URL +export const TEST_GLOBAL_PARAMS_ADDRESS = + "0x0000000000000000000000000000000000000001" as const; // a valid (checksummed) address diff --git a/packages/contracts/__tests__/setup/test-client.ts b/packages/contracts/__tests__/setup/test-client.ts new file mode 100644 index 00000000..19defd02 --- /dev/null +++ b/packages/contracts/__tests__/setup/test-client.ts @@ -0,0 +1,26 @@ +import { createOakContractsClient } from "../../src/client/create"; +import { CHAIN_IDS } from "../../src/constants/chains"; +import { loadTestConfig, type TestConfig } from "./config"; +import type { OakContractsClient } from "../../src/client/types"; + +let _config: TestConfig | undefined; +let _client: OakContractsClient | undefined; + +export function getTestConfig(): TestConfig { + if (!_config) { + _config = loadTestConfig(); + } + return _config; +} + +export function getTestClient(): OakContractsClient { + if (!_client) { + const cfg = getTestConfig(); + _client = createOakContractsClient({ + chainId: CHAIN_IDS.CELO_TESTNET_SEPOLIA, + rpcUrl: cfg.rpcUrl, + privateKey: cfg.privateKey, + }); + } + return _client; +} diff --git a/packages/contracts/__tests__/unit/client.test.ts b/packages/contracts/__tests__/unit/client.test.ts new file mode 100644 index 00000000..f9d9f245 --- /dev/null +++ b/packages/contracts/__tests__/unit/client.test.ts @@ -0,0 +1,161 @@ +import { createOakContractsClient } from "../../src/client/create"; +import { buildClients } from "../../src/client/resolve"; +import { getChainFromId } from "../../src/utils/chain"; +import { + createJsonRpcProvider, + createWallet, +} from "../../src/lib/viem/provider"; +import { CHAIN_IDS } from "../../src/constants/chains"; +import type { OakContractsClientConfig } from "../../src/client/types"; +import { + TEST_PRIVATE_KEY, + TEST_RPC_URL, + TEST_GLOBAL_PARAMS_ADDRESS, +} from "../setup/constant"; + +const PK = TEST_PRIVATE_KEY; +const RPC = TEST_RPC_URL; +const ADDR = TEST_GLOBAL_PARAMS_ADDRESS; +const chain = getChainFromId(CHAIN_IDS.CELO_TESTNET_SEPOLIA); + +describe("buildClients", () => { + it("builds from simple config", () => { + const { + chain: c, + publicClient, + walletClient, + } = buildClients( + { chainId: CHAIN_IDS.CELO_TESTNET_SEPOLIA, rpcUrl: RPC, privateKey: PK }, + { timeout: 30000 }, + ); + expect(c.id).toBe(CHAIN_IDS.CELO_TESTNET_SEPOLIA); + expect(publicClient).toBeDefined(); + expect(walletClient).toBeDefined(); + }); + + it("builds read-only client (no privateKey)", () => { + const { + chain: c, + publicClient, + walletClient, + } = buildClients( + { + chainId: CHAIN_IDS.CELO_TESTNET_SEPOLIA, + rpcUrl: RPC, + } as OakContractsClientConfig, + { timeout: 30000 }, + ); + expect(c.id).toBe(CHAIN_IDS.CELO_TESTNET_SEPOLIA); + expect(publicClient).toBeDefined(); + expect(walletClient).toBeNull(); + }); + + it("builds from full config with Chain object", () => { + const provider = createJsonRpcProvider(RPC, chain); + const signer = createWallet(PK, RPC, chain); + const { + chain: c, + publicClient, + walletClient, + } = buildClients({ chain, provider, signer }, { timeout: 30000 }); + expect(c).toBe(chain); + expect(publicClient).toBe(provider); + expect(walletClient).toBe(signer); + }); + + it("builds from full config with numeric chain", () => { + const provider = createJsonRpcProvider(RPC, chain); + const signer = createWallet(PK, RPC, chain); + const { chain: c } = buildClients( + { chain: CHAIN_IDS.CELO_TESTNET_SEPOLIA, provider, signer }, + { timeout: 30000 }, + ); + expect(c.id).toBe(CHAIN_IDS.CELO_TESTNET_SEPOLIA); + }); +}); + +describe("createOakContractsClient", () => { + it("creates a client with simple config", () => { + const client = createOakContractsClient({ + chainId: CHAIN_IDS.CELO_TESTNET_SEPOLIA, + rpcUrl: RPC, + privateKey: PK, + }); + expect(client.config.chain.id).toBe(CHAIN_IDS.CELO_TESTNET_SEPOLIA); + expect(client.publicClient).toBeDefined(); + expect(client.walletClient).toBeDefined(); + expect(client.options.timeout).toBe(30000); + }); + + it("merges custom options", () => { + const client = createOakContractsClient({ + chainId: CHAIN_IDS.CELO_TESTNET_SEPOLIA, + rpcUrl: RPC, + privateKey: PK, + options: { timeout: 60000 }, + }); + expect(client.options.timeout).toBe(60000); + }); + + it("entity factories return entities", () => { + const client = createOakContractsClient({ + chainId: CHAIN_IDS.CELO_TESTNET_SEPOLIA, + rpcUrl: RPC, + privateKey: PK, + }); + expect(client.globalParams(ADDR)).toBeDefined(); + expect(client.campaignInfoFactory(ADDR)).toBeDefined(); + expect(client.treasuryFactory(ADDR)).toBeDefined(); + expect(client.campaignInfo(ADDR)).toBeDefined(); + expect(client.paymentTreasury(ADDR)).toBeDefined(); + expect(client.allOrNothingTreasury(ADDR)).toBeDefined(); + expect(client.keepWhatsRaisedTreasury(ADDR)).toBeDefined(); + expect(client.itemRegistry(ADDR)).toBeDefined(); + }); + + it("waitForReceipt is a function", () => { + const client = createOakContractsClient({ + chainId: CHAIN_IDS.CELO_TESTNET_SEPOLIA, + rpcUrl: RPC, + privateKey: PK, + }); + expect(typeof client.waitForReceipt).toBe("function"); + }); + + it("waitForReceipt calls publicClient.waitForTransactionReceipt", async () => { + const client = createOakContractsClient({ + chainId: CHAIN_IDS.CELO_TESTNET_SEPOLIA, + rpcUrl: RPC, + privateKey: PK, + }); + const mockReceipt = { + blockNumber: 123n, + gasUsed: 21000n, + logs: [{ topics: ["0xabc"], data: "0xdef" }], + }; + ( + client.publicClient as unknown as { waitForTransactionReceipt: jest.Mock } + ).waitForTransactionReceipt = jest.fn().mockResolvedValue(mockReceipt); + + const receipt = await client.waitForReceipt("0xdeadbeef"); + expect(receipt.blockNumber).toBe(123n); + expect(receipt.gasUsed).toBe(21000n); + expect(receipt.logs).toHaveLength(1); + }); +}); + +// Import contracts barrel to ensure coverage +import * as contractsIndex from "../../src/contracts/index"; + +describe("contracts barrel export", () => { + it("re-exports all entity factories", () => { + expect(contractsIndex.createGlobalParamsEntity).toBeDefined(); + expect(contractsIndex.createCampaignInfoFactoryEntity).toBeDefined(); + expect(contractsIndex.createTreasuryFactoryEntity).toBeDefined(); + expect(contractsIndex.createCampaignInfoEntity).toBeDefined(); + expect(contractsIndex.createPaymentTreasuryEntity).toBeDefined(); + expect(contractsIndex.createAllOrNothingEntity).toBeDefined(); + expect(contractsIndex.createKeepWhatsRaisedEntity).toBeDefined(); + expect(contractsIndex.createItemRegistryEntity).toBeDefined(); + }); +}); diff --git a/packages/contracts/__tests__/unit/constants.test.ts b/packages/contracts/__tests__/unit/constants.test.ts new file mode 100644 index 00000000..e7cdcf08 --- /dev/null +++ b/packages/contracts/__tests__/unit/constants.test.ts @@ -0,0 +1,62 @@ +import { CHAIN_IDS } from "../../src/constants/chains"; +import { BPS_DENOMINATOR } from "../../src/constants/fees"; +import { BYTES32_ZERO } from "../../src/constants/encoding"; +import { DATA_REGISTRY_KEYS, scopedToPlatform } from "../../src/constants/registry"; + +describe("CHAIN_IDS", () => { + it("has correct values", () => { + expect(CHAIN_IDS.ETHEREUM_MAINNET).toBe(1); + expect(CHAIN_IDS.CELO_MAINNET).toBe(42220); + expect(CHAIN_IDS.ETHEREUM_TESTNET_SEPOLIA).toBe(11155111); + expect(CHAIN_IDS.ETHEREUM_TESTNET_GOERLI).toBe(5); + expect(CHAIN_IDS.CELO_TESTNET_SEPOLIA).toBe(11142220); + }); +}); + +describe("BPS_DENOMINATOR", () => { + it("equals 10_000n", () => { + expect(BPS_DENOMINATOR).toBe(10_000n); + }); +}); + +describe("BYTES32_ZERO", () => { + it("is 66 characters (0x + 64 hex zeros)", () => { + expect(BYTES32_ZERO).toMatch(/^0x0{64}$/); + expect(BYTES32_ZERO.length).toBe(66); + }); +}); + +describe("DATA_REGISTRY_KEYS", () => { + it("has 4 keys, each a valid keccak256 hash", () => { + const keys = Object.keys(DATA_REGISTRY_KEYS); + expect(keys).toEqual([ + "BUFFER_TIME", + "MAX_PAYMENT_EXPIRATION", + "CAMPAIGN_LAUNCH_BUFFER", + "MINIMUM_CAMPAIGN_DURATION", + ]); + for (const key of keys) { + const value = DATA_REGISTRY_KEYS[key as keyof typeof DATA_REGISTRY_KEYS]; + expect(value).toMatch(/^0x[0-9a-f]{64}$/); + } + }); +}); + +describe("scopedToPlatform", () => { + it("returns a deterministic bytes32 hash", () => { + const baseKey = DATA_REGISTRY_KEYS.BUFFER_TIME; + const platformHash = BYTES32_ZERO; + const result = scopedToPlatform(baseKey, platformHash); + expect(result).toMatch(/^0x[0-9a-f]{64}$/); + }); + + it("returns different results for different platform hashes", () => { + const baseKey = DATA_REGISTRY_KEYS.BUFFER_TIME; + const a = scopedToPlatform(baseKey, BYTES32_ZERO); + const b = scopedToPlatform( + baseKey, + "0x0000000000000000000000000000000000000000000000000000000000000001", + ); + expect(a).not.toBe(b); + }); +}); diff --git a/packages/contracts/__tests__/unit/contract-entities.test.ts b/packages/contracts/__tests__/unit/contract-entities.test.ts new file mode 100644 index 00000000..6837e16e --- /dev/null +++ b/packages/contracts/__tests__/unit/contract-entities.test.ts @@ -0,0 +1,497 @@ +/** + * Mock-based unit tests for all 8 contract entity modules. + * These ensure 100% code coverage of reads/writes/simulate/events/index + * without needing a real RPC connection. + */ + +import type { Address, PublicClient, WalletClient, Chain } from "../../src/lib"; + +const ADDR = "0x0000000000000000000000000000000000000001" as Address; +const B32 = ("0x" + "00".repeat(32)) as `0x${string}`; + +function mockPublicClient(): PublicClient { + return { + readContract: jest.fn().mockResolvedValue(0n), + simulateContract: jest.fn().mockResolvedValue({ result: undefined }), + } as unknown as PublicClient; +} + +function mockWalletClient(): WalletClient { + return { + account: { address: ADDR }, + writeContract: jest.fn().mockResolvedValue("0xhash"), + } as unknown as WalletClient; +} + +const chain = { id: 11142220, name: "Celo Sepolia" } as Chain; + +// ============================================================ +// Global Params +// ============================================================ +import { createGlobalParamsEntity } from "../../src/contracts/global-params/index"; + +describe("GlobalParams entity", () => { + const pub = mockPublicClient(); + const wal = mockWalletClient(); + const entity = createGlobalParamsEntity(ADDR, pub, wal, chain); + + describe("reads", () => { + it("getProtocolAdminAddress", async () => { await entity.getProtocolAdminAddress(); expect(pub.readContract).toHaveBeenCalled(); }); + it("getProtocolFeePercent", async () => { await entity.getProtocolFeePercent(); }); + it("getNumberOfListedPlatforms", async () => { await entity.getNumberOfListedPlatforms(); }); + it("checkIfPlatformIsListed", async () => { await entity.checkIfPlatformIsListed(B32); }); + it("checkIfPlatformDataKeyValid", async () => { await entity.checkIfPlatformDataKeyValid(B32); }); + it("getPlatformAdminAddress", async () => { await entity.getPlatformAdminAddress(B32); }); + it("getPlatformFeePercent", async () => { await entity.getPlatformFeePercent(B32); }); + it("getPlatformClaimDelay", async () => { await entity.getPlatformClaimDelay(B32); }); + it("getPlatformAdapter", async () => { await entity.getPlatformAdapter(B32); }); + it("getPlatformDataOwner", async () => { await entity.getPlatformDataOwner(B32); }); + it("getPlatformLineItemType", async () => { await entity.getPlatformLineItemType(B32, B32); }); + it("getTokensForCurrency", async () => { await entity.getTokensForCurrency(B32); }); + it("getFromRegistry", async () => { await entity.getFromRegistry(B32); }); + it("owner", async () => { await entity.owner(); }); + }); + + describe("writes", () => { + it("enlistPlatform", async () => { await entity.enlistPlatform(B32, ADDR, 100n, ADDR); expect(wal.writeContract).toHaveBeenCalled(); }); + it("delistPlatform", async () => { await entity.delistPlatform(B32); }); + it("updatePlatformAdminAddress", async () => { await entity.updatePlatformAdminAddress(B32, ADDR); }); + it("updatePlatformClaimDelay", async () => { await entity.updatePlatformClaimDelay(B32, 0n); }); + it("updateProtocolAdminAddress", async () => { await entity.updateProtocolAdminAddress(ADDR); }); + it("updateProtocolFeePercent", async () => { await entity.updateProtocolFeePercent(100n); }); + it("setPlatformAdapter", async () => { await entity.setPlatformAdapter(B32, ADDR); }); + it("setPlatformLineItemType", async () => { await entity.setPlatformLineItemType(B32, B32, "test", true, true, true, false); }); + it("removePlatformLineItemType", async () => { await entity.removePlatformLineItemType(B32, B32); }); + it("addTokenToCurrency", async () => { await entity.addTokenToCurrency(B32, ADDR); }); + it("removeTokenFromCurrency", async () => { await entity.removeTokenFromCurrency(B32, ADDR); }); + it("addPlatformData", async () => { await entity.addPlatformData(B32, B32); }); + it("removePlatformData", async () => { await entity.removePlatformData(B32, B32); }); + it("addToRegistry", async () => { await entity.addToRegistry(B32, B32); }); + it("transferOwnership", async () => { await entity.transferOwnership(ADDR); }); + it("renounceOwnership", async () => { await entity.renounceOwnership(); }); + }); + + describe("simulate", () => { + it("enlistPlatform", async () => { await entity.simulate.enlistPlatform(B32, ADDR, 100n, ADDR); expect(pub.simulateContract).toHaveBeenCalled(); }); + it("delistPlatform", async () => { await entity.simulate.delistPlatform(B32); }); + it("updatePlatformAdminAddress", async () => { await entity.simulate.updatePlatformAdminAddress(B32, ADDR); }); + it("updatePlatformClaimDelay", async () => { await entity.simulate.updatePlatformClaimDelay(B32, 0n); }); + it("updateProtocolAdminAddress", async () => { await entity.simulate.updateProtocolAdminAddress(ADDR); }); + it("updateProtocolFeePercent", async () => { await entity.simulate.updateProtocolFeePercent(100n); }); + it("setPlatformAdapter", async () => { await entity.simulate.setPlatformAdapter(B32, ADDR); }); + it("setPlatformLineItemType", async () => { await entity.simulate.setPlatformLineItemType(B32, B32, "test", true, true, true, false); }); + it("removePlatformLineItemType", async () => { await entity.simulate.removePlatformLineItemType(B32, B32); }); + it("addTokenToCurrency", async () => { await entity.simulate.addTokenToCurrency(B32, ADDR); }); + it("removeTokenFromCurrency", async () => { await entity.simulate.removeTokenFromCurrency(B32, ADDR); }); + it("addPlatformData", async () => { await entity.simulate.addPlatformData(B32, B32); }); + it("removePlatformData", async () => { await entity.simulate.removePlatformData(B32, B32); }); + it("addToRegistry", async () => { await entity.simulate.addToRegistry(B32, B32); }); + it("transferOwnership", async () => { await entity.simulate.transferOwnership(ADDR); }); + it("renounceOwnership", async () => { await entity.simulate.renounceOwnership(); }); + }); + + it("events is empty", () => { expect(entity.events).toEqual({}); }); +}); + +// ============================================================ +// Campaign Info Factory +// ============================================================ +import { createCampaignInfoFactoryEntity } from "../../src/contracts/campaign-info-factory/index"; + +describe("CampaignInfoFactory entity", () => { + const pub = mockPublicClient(); + const wal = mockWalletClient(); + const entity = createCampaignInfoFactoryEntity(ADDR, pub, wal, chain); + + const params = { + creator: ADDR, + identifierHash: B32, + selectedPlatformHash: [B32], + campaignData: { launchTime: 9999999999n, deadline: 9999999999n, goalAmount: 1000n, currency: B32 }, + nftName: "T", + nftSymbol: "T", + nftImageURI: "u", + contractURI: "c", + }; + + it("identifierToCampaignInfo", async () => { await entity.identifierToCampaignInfo(B32); }); + it("isValidCampaignInfo", async () => { await entity.isValidCampaignInfo(ADDR); }); + it("owner", async () => { await entity.owner(); }); + it("createCampaign", async () => { await entity.createCampaign(params); }); + it("createCampaign with optionalKeys", async () => { await entity.createCampaign({ ...params, platformDataKey: [B32], platformDataValue: [B32] }); }); + it("updateImplementation", async () => { await entity.updateImplementation(ADDR); }); + it("transferOwnership", async () => { await entity.transferOwnership(ADDR); }); + it("renounceOwnership", async () => { await entity.renounceOwnership(); }); + it("simulate.createCampaign", async () => { await entity.simulate.createCampaign(params); }); + it("simulate.updateImplementation", async () => { await entity.simulate.updateImplementation(ADDR); }); + it("simulate.transferOwnership", async () => { await entity.simulate.transferOwnership(ADDR); }); + it("simulate.renounceOwnership", async () => { await entity.simulate.renounceOwnership(); }); + it("events is empty", () => { expect(entity.events).toEqual({}); }); +}); + +// ============================================================ +// Treasury Factory +// ============================================================ +import { createTreasuryFactoryEntity } from "../../src/contracts/treasury-factory/index"; + +describe("TreasuryFactory entity", () => { + const pub = mockPublicClient(); + const wal = mockWalletClient(); + const entity = createTreasuryFactoryEntity(ADDR, pub, wal, chain); + + it("deploy", async () => { await entity.deploy(B32, ADDR, 0n); }); + it("registerTreasuryImplementation", async () => { await entity.registerTreasuryImplementation(B32, 0n, ADDR); }); + it("approveTreasuryImplementation", async () => { await entity.approveTreasuryImplementation(B32, 0n); }); + it("disapproveTreasuryImplementation", async () => { await entity.disapproveTreasuryImplementation(ADDR); }); + it("removeTreasuryImplementation", async () => { await entity.removeTreasuryImplementation(B32, 0n); }); + it("simulate.deploy", async () => { await entity.simulate.deploy(B32, ADDR, 0n); }); + it("simulate.registerTreasuryImplementation", async () => { await entity.simulate.registerTreasuryImplementation(B32, 0n, ADDR); }); + it("simulate.approveTreasuryImplementation", async () => { await entity.simulate.approveTreasuryImplementation(B32, 0n); }); + it("simulate.disapproveTreasuryImplementation", async () => { await entity.simulate.disapproveTreasuryImplementation(ADDR); }); + it("simulate.removeTreasuryImplementation", async () => { await entity.simulate.removeTreasuryImplementation(B32, 0n); }); + it("events is empty", () => { expect(entity.events).toEqual({}); }); +}); + +// ============================================================ +// Campaign Info +// ============================================================ +import { createCampaignInfoEntity } from "../../src/contracts/campaign-info/index"; + +describe("CampaignInfo entity", () => { + const pub = mockPublicClient(); + const wal = mockWalletClient(); + const entity = createCampaignInfoEntity(ADDR, pub, wal, chain); + + describe("reads", () => { + it("getLaunchTime", async () => { await entity.getLaunchTime(); }); + it("getDeadline", async () => { await entity.getDeadline(); }); + it("getGoalAmount", async () => { await entity.getGoalAmount(); }); + it("getCampaignCurrency", async () => { await entity.getCampaignCurrency(); }); + it("getIdentifierHash", async () => { await entity.getIdentifierHash(); }); + it("checkIfPlatformSelected", async () => { await entity.checkIfPlatformSelected(B32); }); + it("checkIfPlatformApproved", async () => { await entity.checkIfPlatformApproved(B32); }); + it("getPlatformAdminAddress", async () => { await entity.getPlatformAdminAddress(B32); }); + it("getPlatformData", async () => { await entity.getPlatformData(B32); }); + it("getPlatformFeePercent", async () => { await entity.getPlatformFeePercent(B32); }); + it("getPlatformClaimDelay", async () => { await entity.getPlatformClaimDelay(B32); }); + it("getProtocolAdminAddress", async () => { await entity.getProtocolAdminAddress(); }); + it("getProtocolFeePercent", async () => { await entity.getProtocolFeePercent(); }); + it("getAcceptedTokens", async () => { await entity.getAcceptedTokens(); }); + it("isTokenAccepted", async () => { await entity.isTokenAccepted(ADDR); }); + it("getTotalRaisedAmount", async () => { await entity.getTotalRaisedAmount(); }); + it("getTotalLifetimeRaisedAmount", async () => { await entity.getTotalLifetimeRaisedAmount(); }); + it("getTotalRefundedAmount", async () => { await entity.getTotalRefundedAmount(); }); + it("getTotalAvailableRaisedAmount", async () => { await entity.getTotalAvailableRaisedAmount(); }); + it("getTotalCancelledAmount", async () => { await entity.getTotalCancelledAmount(); }); + it("getTotalExpectedAmount", async () => { await entity.getTotalExpectedAmount(); }); + it("getDataFromRegistry", async () => { await entity.getDataFromRegistry(B32); }); + it("getBufferTime", async () => { await entity.getBufferTime(); }); + it("getLineItemType", async () => { await entity.getLineItemType(B32, B32); }); + it("getCampaignConfig", async () => { await entity.getCampaignConfig(); }); + it("getApprovedPlatformHashes", async () => { await entity.getApprovedPlatformHashes(); }); + it("isLocked", async () => { await entity.isLocked(); }); + it("cancelled", async () => { await entity.cancelled(); }); + it("owner", async () => { await entity.owner(); }); + it("paused", async () => { await entity.paused(); }); + }); + + describe("writes", () => { + it("updateDeadline", async () => { await entity.updateDeadline(0n); }); + it("updateGoalAmount", async () => { await entity.updateGoalAmount(0n); }); + it("updateLaunchTime", async () => { await entity.updateLaunchTime(0n); }); + it("updateSelectedPlatform", async () => { await entity.updateSelectedPlatform(B32, true, [], []); }); + it("setImageURI", async () => { await entity.setImageURI("uri"); }); + it("updateContractURI", async () => { await entity.updateContractURI("uri"); }); + it("mintNFTForPledge", async () => { await entity.mintNFTForPledge(ADDR, B32, ADDR, 0n, 0n, 0n); }); + it("burn", async () => { await entity.burn(0n); }); + it("pauseCampaign", async () => { await entity.pauseCampaign(B32); }); + it("unpauseCampaign", async () => { await entity.unpauseCampaign(B32); }); + it("cancelCampaign", async () => { await entity.cancelCampaign(B32); }); + it("setPlatformInfo", async () => { await entity.setPlatformInfo(B32, ADDR); }); + it("transferOwnership", async () => { await entity.transferOwnership(ADDR); }); + it("renounceOwnership", async () => { await entity.renounceOwnership(); }); + }); + + describe("simulate", () => { + it("updateDeadline", async () => { await entity.simulate.updateDeadline(0n); }); + it("updateGoalAmount", async () => { await entity.simulate.updateGoalAmount(0n); }); + it("updateLaunchTime", async () => { await entity.simulate.updateLaunchTime(0n); }); + it("updateSelectedPlatform", async () => { await entity.simulate.updateSelectedPlatform(B32, true, [], []); }); + it("setImageURI", async () => { await entity.simulate.setImageURI("uri"); }); + it("updateContractURI", async () => { await entity.simulate.updateContractURI("uri"); }); + it("mintNFTForPledge", async () => { await entity.simulate.mintNFTForPledge(ADDR, B32, ADDR, 0n, 0n, 0n); }); + it("burn", async () => { await entity.simulate.burn(0n); }); + it("pauseCampaign", async () => { await entity.simulate.pauseCampaign(B32); }); + it("unpauseCampaign", async () => { await entity.simulate.unpauseCampaign(B32); }); + it("cancelCampaign", async () => { await entity.simulate.cancelCampaign(B32); }); + it("setPlatformInfo", async () => { await entity.simulate.setPlatformInfo(B32, ADDR); }); + it("transferOwnership", async () => { await entity.simulate.transferOwnership(ADDR); }); + it("renounceOwnership", async () => { await entity.simulate.renounceOwnership(); }); + }); + + it("events is empty", () => { expect(entity.events).toEqual({}); }); +}); + +// ============================================================ +// Payment Treasury +// ============================================================ +import { createPaymentTreasuryEntity } from "../../src/contracts/payment-treasury/index"; + +describe("PaymentTreasury entity", () => { + const pub = mockPublicClient(); + const wal = mockWalletClient(); + const entity = createPaymentTreasuryEntity(ADDR, pub, wal, chain); + + describe("reads", () => { + it("getPlatformHash", async () => { await entity.getPlatformHash(); }); + it("getPlatformFeePercent", async () => { await entity.getPlatformFeePercent(); }); + it("getRaisedAmount", async () => { await entity.getRaisedAmount(); }); + it("getAvailableRaisedAmount", async () => { await entity.getAvailableRaisedAmount(); }); + it("getLifetimeRaisedAmount", async () => { await entity.getLifetimeRaisedAmount(); }); + it("getRefundedAmount", async () => { await entity.getRefundedAmount(); }); + it("getExpectedAmount", async () => { await entity.getExpectedAmount(); }); + it("getPaymentData", async () => { await entity.getPaymentData(B32); }); + it("cancelled", async () => { await entity.cancelled(); }); + }); + + describe("writes", () => { + it("createPayment", async () => { await entity.createPayment(B32, B32, B32, ADDR, 0n, 0n, [], []); }); + it("createPaymentBatch", async () => { await entity.createPaymentBatch([B32], [B32], [B32], [ADDR], [0n], [0n], [[]], [[]]); }); + it("processCryptoPayment", async () => { await entity.processCryptoPayment(B32, B32, ADDR, ADDR, 0n, [], []); }); + it("cancelPayment", async () => { await entity.cancelPayment(B32); }); + it("confirmPayment", async () => { await entity.confirmPayment(B32, ADDR); }); + it("confirmPaymentBatch", async () => { await entity.confirmPaymentBatch([B32], [ADDR]); }); + it("disburseFees", async () => { await entity.disburseFees(); }); + it("withdraw", async () => { await entity.withdraw(); }); + it("claimRefund", async () => { await entity.claimRefund(B32, ADDR); }); + it("claimRefundSelf", async () => { await entity.claimRefundSelf(B32); }); + it("claimExpiredFunds", async () => { await entity.claimExpiredFunds(); }); + it("claimNonGoalLineItems", async () => { await entity.claimNonGoalLineItems(ADDR); }); + it("pauseTreasury", async () => { await entity.pauseTreasury(B32); }); + it("unpauseTreasury", async () => { await entity.unpauseTreasury(B32); }); + it("cancelTreasury", async () => { await entity.cancelTreasury(B32); }); + }); + + describe("simulate", () => { + it("createPayment", async () => { await entity.simulate.createPayment(B32, B32, B32, ADDR, 0n, 0n, [], []); }); + it("createPaymentBatch", async () => { await entity.simulate.createPaymentBatch([B32], [B32], [B32], [ADDR], [0n], [0n], [[]], [[]]); }); + it("processCryptoPayment", async () => { await entity.simulate.processCryptoPayment(B32, B32, ADDR, ADDR, 0n, [], []); }); + it("cancelPayment", async () => { await entity.simulate.cancelPayment(B32); }); + it("confirmPayment", async () => { await entity.simulate.confirmPayment(B32, ADDR); }); + it("confirmPaymentBatch", async () => { await entity.simulate.confirmPaymentBatch([B32], [ADDR]); }); + it("disburseFees", async () => { await entity.simulate.disburseFees(); }); + it("withdraw", async () => { await entity.simulate.withdraw(); }); + it("claimRefund", async () => { await entity.simulate.claimRefund(B32, ADDR); }); + it("claimRefundSelf", async () => { await entity.simulate.claimRefundSelf(B32); }); + it("claimExpiredFunds", async () => { await entity.simulate.claimExpiredFunds(); }); + it("claimNonGoalLineItems", async () => { await entity.simulate.claimNonGoalLineItems(ADDR); }); + it("pauseTreasury", async () => { await entity.simulate.pauseTreasury(B32); }); + it("unpauseTreasury", async () => { await entity.simulate.unpauseTreasury(B32); }); + it("cancelTreasury", async () => { await entity.simulate.cancelTreasury(B32); }); + }); + + it("events is empty", () => { expect(entity.events).toEqual({}); }); +}); + +// ============================================================ +// All Or Nothing +// ============================================================ +import { createAllOrNothingEntity } from "../../src/contracts/all-or-nothing/index"; + +describe("AllOrNothing entity", () => { + const pub = mockPublicClient(); + const wal = mockWalletClient(); + const entity = createAllOrNothingEntity(ADDR, pub, wal, chain); + + describe("reads", () => { + it("getRaisedAmount", async () => { await entity.getRaisedAmount(); }); + it("getLifetimeRaisedAmount", async () => { await entity.getLifetimeRaisedAmount(); }); + it("getRefundedAmount", async () => { await entity.getRefundedAmount(); }); + it("getReward", async () => { await entity.getReward(B32); }); + it("getPlatformHash", async () => { await entity.getPlatformHash(); }); + it("getPlatformFeePercent", async () => { await entity.getPlatformFeePercent(); }); + it("paused", async () => { await entity.paused(); }); + it("cancelled", async () => { await entity.cancelled(); }); + it("balanceOf", async () => { await entity.balanceOf(ADDR); }); + it("ownerOf", async () => { await entity.ownerOf(0n); }); + it("tokenURI", async () => { await entity.tokenURI(0n); }); + it("name", async () => { await entity.name(); }); + it("symbol", async () => { await entity.symbol(); }); + it("getApproved", async () => { await entity.getApproved(0n); }); + it("isApprovedForAll", async () => { await entity.isApprovedForAll(ADDR, ADDR); }); + it("supportsInterface", async () => { await entity.supportsInterface("0x80ac58cd"); }); + }); + + describe("writes", () => { + it("pauseTreasury", async () => { await entity.pauseTreasury(B32); }); + it("unpauseTreasury", async () => { await entity.unpauseTreasury(B32); }); + it("cancelTreasury", async () => { await entity.cancelTreasury(B32); }); + it("addRewards", async () => { await entity.addRewards([B32], [{ rewardValue: 100n, isRewardTier: false, itemId: [], itemValue: [], itemQuantity: [] }]); }); + it("removeReward", async () => { await entity.removeReward(B32); }); + it("pledgeForAReward", async () => { await entity.pledgeForAReward(ADDR, ADDR, 0n, [B32]); }); + it("pledgeWithoutAReward", async () => { await entity.pledgeWithoutAReward(ADDR, ADDR, 100n); }); + it("claimRefund", async () => { await entity.claimRefund(0n); }); + it("disburseFees", async () => { await entity.disburseFees(); }); + it("withdraw", async () => { await entity.withdraw(); }); + it("burn", async () => { await entity.burn(0n); }); + it("approve", async () => { await entity.approve(ADDR, 0n); }); + it("setApprovalForAll", async () => { await entity.setApprovalForAll(ADDR, true); }); + it("safeTransferFrom", async () => { await entity.safeTransferFrom(ADDR, ADDR, 0n); }); + it("transferFrom", async () => { await entity.transferFrom(ADDR, ADDR, 0n); }); + }); + + describe("simulate", () => { + it("pauseTreasury", async () => { await entity.simulate.pauseTreasury(B32); }); + it("unpauseTreasury", async () => { await entity.simulate.unpauseTreasury(B32); }); + it("cancelTreasury", async () => { await entity.simulate.cancelTreasury(B32); }); + it("addRewards", async () => { await entity.simulate.addRewards([B32], [{ rewardValue: 100n, isRewardTier: false, itemId: [], itemValue: [], itemQuantity: [] }]); }); + it("removeReward", async () => { await entity.simulate.removeReward(B32); }); + it("pledgeForAReward", async () => { await entity.simulate.pledgeForAReward(ADDR, ADDR, 0n, [B32]); }); + it("pledgeWithoutAReward", async () => { await entity.simulate.pledgeWithoutAReward(ADDR, ADDR, 100n); }); + it("claimRefund", async () => { await entity.simulate.claimRefund(0n); }); + it("disburseFees", async () => { await entity.simulate.disburseFees(); }); + it("withdraw", async () => { await entity.simulate.withdraw(); }); + it("burn", async () => { await entity.simulate.burn(0n); }); + it("approve", async () => { await entity.simulate.approve(ADDR, 0n); }); + it("setApprovalForAll", async () => { await entity.simulate.setApprovalForAll(ADDR, true); }); + it("safeTransferFrom", async () => { await entity.simulate.safeTransferFrom(ADDR, ADDR, 0n); }); + it("transferFrom", async () => { await entity.simulate.transferFrom(ADDR, ADDR, 0n); }); + }); + + it("events is empty", () => { expect(entity.events).toEqual({}); }); +}); + +// ============================================================ +// Keep Whats Raised +// ============================================================ +import { createKeepWhatsRaisedEntity } from "../../src/contracts/keep-whats-raised/index"; + +describe("KeepWhatsRaised entity", () => { + const pub = mockPublicClient(); + const wal = mockWalletClient(); + const entity = createKeepWhatsRaisedEntity(ADDR, pub, wal, chain); + + describe("reads", () => { + it("getRaisedAmount", async () => { await entity.getRaisedAmount(); }); + it("getLifetimeRaisedAmount", async () => { await entity.getLifetimeRaisedAmount(); }); + it("getRefundedAmount", async () => { await entity.getRefundedAmount(); }); + it("getAvailableRaisedAmount", async () => { await entity.getAvailableRaisedAmount(); }); + it("getReward", async () => { await entity.getReward(B32); }); + it("getPlatformHash", async () => { await entity.getPlatformHash(); }); + it("getPlatformFeePercent", async () => { await entity.getPlatformFeePercent(); }); + it("getWithdrawalApprovalStatus", async () => { await entity.getWithdrawalApprovalStatus(); }); + it("getLaunchTime", async () => { await entity.getLaunchTime(); }); + it("getDeadline", async () => { await entity.getDeadline(); }); + it("getGoalAmount", async () => { await entity.getGoalAmount(); }); + it("getPaymentGatewayFee", async () => { await entity.getPaymentGatewayFee(B32); }); + it("getFeeValue", async () => { await entity.getFeeValue(B32); }); + it("paused", async () => { await entity.paused(); }); + it("cancelled", async () => { await entity.cancelled(); }); + it("balanceOf", async () => { await entity.balanceOf(ADDR); }); + it("ownerOf", async () => { await entity.ownerOf(0n); }); + it("tokenURI", async () => { await entity.tokenURI(0n); }); + it("name", async () => { await entity.name(); }); + it("symbol", async () => { await entity.symbol(); }); + it("getApproved", async () => { await entity.getApproved(0n); }); + it("isApprovedForAll", async () => { await entity.isApprovedForAll(ADDR, ADDR); }); + it("supportsInterface", async () => { await entity.supportsInterface("0x80ac58cd"); }); + }); + + describe("writes", () => { + it("pauseTreasury", async () => { await entity.pauseTreasury(B32); }); + it("unpauseTreasury", async () => { await entity.unpauseTreasury(B32); }); + it("cancelTreasury", async () => { await entity.cancelTreasury(B32); }); + it("configureTreasury", async () => { + await entity.configureTreasury( + { minimumWithdrawalForFeeExemption: 0n, withdrawalDelay: 0n, refundDelay: 0n, configLockPeriod: 0n, isColombianCreator: false }, + { launchTime: 0n, deadline: 0n, goalAmount: 0n, currency: B32 }, + { flatFeeKey: B32, cumulativeFlatFeeKey: B32, grossPercentageFeeKeys: [] }, + { flatFeeValue: 0n, cumulativeFlatFeeValue: 0n, grossPercentageFeeValues: [] }, + ); + }); + it("addRewards", async () => { await entity.addRewards([B32], [{ rewardValue: 0n, isRewardTier: false, itemId: [], itemValue: [], itemQuantity: [] }]); }); + it("removeReward", async () => { await entity.removeReward(B32); }); + it("approveWithdrawal", async () => { await entity.approveWithdrawal(); }); + it("setPaymentGatewayFee", async () => { await entity.setPaymentGatewayFee(B32, 0n); }); + it("setFeeAndPledge", async () => { await entity.setFeeAndPledge(B32, ADDR, ADDR, 0n, 0n, 0n, [B32], true); }); + it("pledgeForAReward", async () => { await entity.pledgeForAReward(B32, ADDR, ADDR, 0n, [B32]); }); + it("pledgeWithoutAReward", async () => { await entity.pledgeWithoutAReward(B32, ADDR, ADDR, 0n, 0n); }); + it("claimRefund", async () => { await entity.claimRefund(0n); }); + it("claimTip", async () => { await entity.claimTip(); }); + it("claimFund", async () => { await entity.claimFund(); }); + it("disburseFees", async () => { await entity.disburseFees(); }); + it("withdraw", async () => { await entity.withdraw(ADDR, 0n); }); + it("updateDeadline", async () => { await entity.updateDeadline(0n); }); + it("updateGoalAmount", async () => { await entity.updateGoalAmount(0n); }); + it("approve", async () => { await entity.approve(ADDR, 0n); }); + it("setApprovalForAll", async () => { await entity.setApprovalForAll(ADDR, true); }); + it("safeTransferFrom", async () => { await entity.safeTransferFrom(ADDR, ADDR, 0n); }); + it("transferFrom", async () => { await entity.transferFrom(ADDR, ADDR, 0n); }); + }); + + describe("simulate", () => { + it("pauseTreasury", async () => { await entity.simulate.pauseTreasury(B32); }); + it("unpauseTreasury", async () => { await entity.simulate.unpauseTreasury(B32); }); + it("cancelTreasury", async () => { await entity.simulate.cancelTreasury(B32); }); + it("configureTreasury", async () => { + await entity.simulate.configureTreasury( + { minimumWithdrawalForFeeExemption: 0n, withdrawalDelay: 0n, refundDelay: 0n, configLockPeriod: 0n, isColombianCreator: false }, + { launchTime: 0n, deadline: 0n, goalAmount: 0n, currency: B32 }, + { flatFeeKey: B32, cumulativeFlatFeeKey: B32, grossPercentageFeeKeys: [] }, + { flatFeeValue: 0n, cumulativeFlatFeeValue: 0n, grossPercentageFeeValues: [] }, + ); + }); + it("addRewards", async () => { await entity.simulate.addRewards([B32], [{ rewardValue: 0n, isRewardTier: false, itemId: [], itemValue: [], itemQuantity: [] }]); }); + it("removeReward", async () => { await entity.simulate.removeReward(B32); }); + it("approveWithdrawal", async () => { await entity.simulate.approveWithdrawal(); }); + it("setPaymentGatewayFee", async () => { await entity.simulate.setPaymentGatewayFee(B32, 0n); }); + it("setFeeAndPledge", async () => { await entity.simulate.setFeeAndPledge(B32, ADDR, ADDR, 0n, 0n, 0n, [B32], true); }); + it("pledgeForAReward", async () => { await entity.simulate.pledgeForAReward(B32, ADDR, ADDR, 0n, [B32]); }); + it("pledgeWithoutAReward", async () => { await entity.simulate.pledgeWithoutAReward(B32, ADDR, ADDR, 0n, 0n); }); + it("claimRefund", async () => { await entity.simulate.claimRefund(0n); }); + it("claimTip", async () => { await entity.simulate.claimTip(); }); + it("claimFund", async () => { await entity.simulate.claimFund(); }); + it("disburseFees", async () => { await entity.simulate.disburseFees(); }); + it("withdraw", async () => { await entity.simulate.withdraw(ADDR, 0n); }); + it("updateDeadline", async () => { await entity.simulate.updateDeadline(0n); }); + it("updateGoalAmount", async () => { await entity.simulate.updateGoalAmount(0n); }); + it("approve", async () => { await entity.simulate.approve(ADDR, 0n); }); + it("setApprovalForAll", async () => { await entity.simulate.setApprovalForAll(ADDR, true); }); + it("safeTransferFrom", async () => { await entity.simulate.safeTransferFrom(ADDR, ADDR, 0n); }); + it("transferFrom", async () => { await entity.simulate.transferFrom(ADDR, ADDR, 0n); }); + }); + + it("events is empty", () => { expect(entity.events).toEqual({}); }); +}); + +// ============================================================ +// Item Registry +// ============================================================ +import { createItemRegistryEntity } from "../../src/contracts/item-registry/index"; + +describe("ItemRegistry entity", () => { + const pub = mockPublicClient(); + const wal = mockWalletClient(); + const entity = createItemRegistryEntity(ADDR, pub, wal, chain); + const item = { actualWeight: 0n, height: 0n, width: 0n, length: 0n, category: B32, declaredCurrency: B32 }; + + it("getItem", async () => { await entity.getItem(ADDR, B32); }); + it("addItem", async () => { await entity.addItem(B32, item); }); + it("addItemsBatch", async () => { await entity.addItemsBatch([B32], [item]); }); + it("simulate.addItem", async () => { await entity.simulate.addItem(B32, item); }); + it("simulate.addItemsBatch", async () => { await entity.simulate.addItemsBatch([B32], [item]); }); + it("events is empty", () => { expect(entity.events).toEqual({}); }); +}); + +// ============================================================ +// Barrel export coverage +// ============================================================ +import * as mainIndex from "../../src/index"; +import * as metricsIndex from "../../src/metrics/index"; +import * as typesIndex from "../../src/types/index"; + +describe("barrel exports", () => { + it("main index re-exports", () => { expect(mainIndex).toBeDefined(); }); + it("metrics index re-exports", () => { expect(metricsIndex).toBeDefined(); }); + it("types index re-exports", () => { expect(typesIndex).toBeDefined(); }); +}); diff --git a/packages/contracts/__tests__/unit/error-parsing.test.ts b/packages/contracts/__tests__/unit/error-parsing.test.ts new file mode 100644 index 00000000..36004319 --- /dev/null +++ b/packages/contracts/__tests__/unit/error-parsing.test.ts @@ -0,0 +1,622 @@ +import { encodeErrorResult } from "viem"; +import { decodeErrorArgs, toSharedContractError, tryDecodeContractError } from "../../src/errors/parse/shared"; +import type { ErrorAbiEntry } from "../../src/errors/parse/shared"; +import { + parseContractError, + getRevertData, + simulateWithErrorDecode, +} from "../../src/errors/parse-contract-error"; +import { parseGlobalParamsError } from "../../src/errors/parse/global-params"; +import { parseCampaignInfoFactoryError } from "../../src/errors/parse/campaign-info-factory"; +import { parseCampaignInfoError } from "../../src/errors/parse/campaign-info"; +import { parseAllOrNothingError } from "../../src/errors/parse/all-or-nothing"; +import { parseKeepWhatsRaisedError } from "../../src/errors/parse/keep-whats-raised"; +import { parseItemRegistryError } from "../../src/errors/parse/item-registry"; +import { parsePaymentTreasuryError } from "../../src/errors/parse/payment-treasury"; +import { parseTreasuryFactoryError } from "../../src/errors/parse/treasury-factory"; + +import { GLOBAL_PARAMS_ABI } from "../../src/contracts/global-params/abi"; +import { CAMPAIGN_INFO_FACTORY_ABI } from "../../src/contracts/campaign-info-factory/abi"; +import { CAMPAIGN_INFO_ABI } from "../../src/contracts/campaign-info/abi"; +import { ALL_OR_NOTHING_ABI } from "../../src/contracts/all-or-nothing/abi"; +import { KEEP_WHATS_RAISED_ABI } from "../../src/contracts/keep-whats-raised/abi"; +import { ITEM_REGISTRY_ABI } from "../../src/contracts/item-registry/abi"; +import { PAYMENT_TREASURY_ABI } from "../../src/contracts/payment-treasury/abi"; +import { TREASURY_FACTORY_ABI } from "../../src/contracts/treasury-factory/abi"; + +function encodeError(abi: readonly unknown[], errorName: string, args?: readonly unknown[]) { + return encodeErrorResult({ + abi: abi as Parameters[0]["abi"], + errorName, + args: args as never, + }); +} + +describe("decodeErrorArgs", () => { + const abi: ErrorAbiEntry[] = [ + { type: "error", name: "TestErr", inputs: [{ name: "val" }] }, + { type: "function", name: "foo" }, + { type: "error", name: "NoInputs" }, + ]; + + it("maps decoded args to a named record", () => { + const result = decodeErrorArgs(abi, "TestErr", [42]); + expect(result).toEqual({ val: 42 }); + }); + + it("returns empty record for unknown error name", () => { + expect(decodeErrorArgs(abi, "Unknown", [1])).toEqual({}); + }); + + it("skips undefined decoded values", () => { + const result = decodeErrorArgs(abi, "TestErr", []); + expect(result).toEqual({}); + }); + + it("handles errors with no inputs", () => { + const result = decodeErrorArgs(abi, "NoInputs", []); + expect(result).toEqual({}); + }); +}); + +describe("toSharedContractError", () => { + it("maps AccessCheckerUnauthorized", () => { + const e = toSharedContractError("AccessCheckerUnauthorized", {}); + expect(e).not.toBeNull(); + expect(e!.name).toBe("AccessCheckerUnauthorized"); + }); + + it("maps AdminAccessCheckerUnauthorized", () => { + const e = toSharedContractError("AdminAccessCheckerUnauthorized", {}); + expect(e!.name).toBe("AdminAccessCheckerUnauthorized"); + }); + + it("maps CurrentTimeIsGreater", () => { + const e = toSharedContractError("CurrentTimeIsGreater", { inputTime: "1", currentTime: "2" }); + expect(e!.name).toBe("CurrentTimeIsGreater"); + }); + + it("maps CurrentTimeIsLess", () => { + const e = toSharedContractError("CurrentTimeIsLess", { inputTime: "2", currentTime: "1" }); + expect(e!.name).toBe("CurrentTimeIsLess"); + }); + + it("maps CurrentTimeIsNotWithinRange", () => { + const e = toSharedContractError("CurrentTimeIsNotWithinRange", { initialTime: "1", finalTime: "2" }); + expect(e!.name).toBe("CurrentTimeIsNotWithinRange"); + }); + + it("maps TreasuryCampaignInfoIsPaused", () => { + const e = toSharedContractError("TreasuryCampaignInfoIsPaused", {}); + expect(e!.name).toBe("TreasuryCampaignInfoIsPaused"); + }); + + it("maps TreasuryFeeNotDisbursed", () => { + const e = toSharedContractError("TreasuryFeeNotDisbursed", {}); + expect(e!.name).toBe("TreasuryFeeNotDisbursed"); + }); + + it("maps TreasuryTransferFailed", () => { + const e = toSharedContractError("TreasuryTransferFailed", {}); + expect(e!.name).toBe("TreasuryTransferFailed"); + }); + + it("returns null for unknown error names", () => { + expect(toSharedContractError("SomethingElse", {})).toBeNull(); + }); +}); + +describe("tryDecodeContractError", () => { + it("returns null for non-decodable data", () => { + const abi: ErrorAbiEntry[] = [{ type: "error", name: "Foo" }]; + const result = tryDecodeContractError(abi, "0xdeadbeef", () => { + throw new Error("should not be called"); + }); + expect(result).toBeNull(); + }); +}); + +describe("getRevertData", () => { + it("extracts data from { data: '0x...' }", () => { + expect(getRevertData({ data: "0xabcd" })).toBe("0xabcd"); + }); + + it("extracts data from nested { data: { data: '0x...' } }", () => { + expect(getRevertData({ data: { data: "0xef01" } })).toBe("0xef01"); + }); + + it("walks the cause chain", () => { + expect(getRevertData({ cause: { data: "0x1234" } })).toBe("0x1234"); + }); + + it("returns null for no data", () => { + expect(getRevertData({})).toBeNull(); + expect(getRevertData(null)).toBeNull(); + expect(getRevertData(undefined)).toBeNull(); + expect(getRevertData("string")).toBeNull(); + }); + + it("extracts data from { raw: '0x...' }", () => { + expect(getRevertData({ raw: "0xabcd" })).toBe("0xabcd"); + }); + + it("returns null for non-hex data string", () => { + expect(getRevertData({ data: "not-hex" })).toBeNull(); + }); + + it("returns null for non-object data that is not hex", () => { + expect(getRevertData({ data: 42 })).toBeNull(); + }); +}); + +describe("simulateWithErrorDecode", () => { + it("does not throw on success", async () => { + await expect(simulateWithErrorDecode(async () => "ok")).resolves.toBeUndefined(); + }); + + it("throws typed error when revert data is parseable", async () => { + const revertData = encodeError(GLOBAL_PARAMS_ABI, "GlobalParamsInvalidInput"); + const op = async () => { + throw { data: revertData }; + }; + await expect(simulateWithErrorDecode(op)).rejects.toMatchObject({ + name: "GlobalParamsInvalidInput", + }); + }); + + it("rethrows original error when not parseable", async () => { + const err = new Error("something else"); + await expect(simulateWithErrorDecode(async () => { throw err; })).rejects.toBe(err); + }); +}); + +describe("parseContractError", () => { + it("returns null for empty string", () => { + expect(parseContractError("")).toBeNull(); + }); + + it("returns null for non-hex string", () => { + expect(parseContractError("not-hex")).toBeNull(); + }); + + it("returns null for hex shorter than 10 chars (no selector)", () => { + expect(parseContractError("0xabcd")).toBeNull(); + }); + + it("returns null for unrecognized selector", () => { + expect(parseContractError("0x12345678")).toBeNull(); + }); + + it("parses a GlobalParams error", () => { + const data = encodeError(GLOBAL_PARAMS_ABI, "GlobalParamsInvalidInput"); + const err = parseContractError(data); + expect(err).not.toBeNull(); + expect(err!.name).toBe("GlobalParamsInvalidInput"); + }); + + it("parses a CampaignInfoFactory error", () => { + const data = encodeError(CAMPAIGN_INFO_FACTORY_ABI, "CampaignInfoFactoryInvalidInput"); + const err = parseContractError(data); + expect(err).not.toBeNull(); + expect(err!.name).toBe("CampaignInfoFactoryInvalidInput"); + }); + + it("parses a CampaignInfo error", () => { + const data = encodeError(CAMPAIGN_INFO_ABI, "CampaignInfoInvalidInput"); + const err = parseContractError(data); + expect(err).not.toBeNull(); + expect(err!.name).toBe("CampaignInfoInvalidInput"); + }); + + it("parses an AllOrNothing error", () => { + const data = encodeError(ALL_OR_NOTHING_ABI, "AllOrNothingInvalidInput"); + const err = parseContractError(data); + expect(err).not.toBeNull(); + expect(err!.name).toBe("AllOrNothingInvalidInput"); + }); + + it("parses a KeepWhatsRaised error", () => { + const data = encodeError(KEEP_WHATS_RAISED_ABI, "KeepWhatsRaisedInvalidInput"); + const err = parseContractError(data); + expect(err).not.toBeNull(); + expect(err!.name).toBe("KeepWhatsRaisedInvalidInput"); + }); + + it("parses an ItemRegistry error", () => { + const data = encodeError(ITEM_REGISTRY_ABI, "ItemRegistryMismatchedArraysLength"); + const err = parseContractError(data); + expect(err).not.toBeNull(); + expect(err!.name).toBe("ItemRegistryMismatchedArraysLength"); + }); + + it("parses a PaymentTreasury error", () => { + const data = encodeError(PAYMENT_TREASURY_ABI, "PaymentTreasuryInvalidInput"); + const err = parseContractError(data); + expect(err).not.toBeNull(); + expect(err!.name).toBe("PaymentTreasuryInvalidInput"); + }); + + it("parses a TreasuryFactory error", () => { + const data = encodeError(TREASURY_FACTORY_ABI, "TreasuryFactoryUnauthorized"); + const err = parseContractError(data); + expect(err).not.toBeNull(); + expect(err!.name).toBe("TreasuryFactoryUnauthorized"); + }); +}); + +describe("parseGlobalParamsError", () => { + function encode(name: string, args?: readonly unknown[]) { + return encodeError(GLOBAL_PARAMS_ABI, name, args); + } + + it.each([ + "GlobalParamsInvalidInput", + "GlobalParamsPlatformDataAlreadySet", + "GlobalParamsPlatformDataNotSet", + "GlobalParamsPlatformDataSlotTaken", + "GlobalParamsUnauthorized", + "GlobalParamsCurrencyTokenLengthMismatch", + ])("parses %s", (name) => { + const err = parseGlobalParamsError(encode(name)); + expect(err!.name).toBe(name); + }); + + it("parses GlobalParamsPlatformAdminNotSet", () => { + const data = encode("GlobalParamsPlatformAdminNotSet", ["0x" + "ab".repeat(32)]); + expect(parseGlobalParamsError(data)!.name).toBe("GlobalParamsPlatformAdminNotSet"); + }); + + it("parses GlobalParamsPlatformAlreadyListed", () => { + const data = encode("GlobalParamsPlatformAlreadyListed", ["0x" + "cd".repeat(32)]); + expect(parseGlobalParamsError(data)!.name).toBe("GlobalParamsPlatformAlreadyListed"); + }); + + it("parses GlobalParamsPlatformFeePercentIsZero", () => { + const data = encode("GlobalParamsPlatformFeePercentIsZero", ["0x" + "ee".repeat(32)]); + expect(parseGlobalParamsError(data)!.name).toBe("GlobalParamsPlatformFeePercentIsZero"); + }); + + it("parses GlobalParamsPlatformNotListed", () => { + const data = encode("GlobalParamsPlatformNotListed", ["0x" + "ff".repeat(32)]); + expect(parseGlobalParamsError(data)!.name).toBe("GlobalParamsPlatformNotListed"); + }); + + it("parses GlobalParamsCurrencyHasNoTokens", () => { + const data = encode("GlobalParamsCurrencyHasNoTokens", ["0x" + "aa".repeat(32)]); + expect(parseGlobalParamsError(data)!.name).toBe("GlobalParamsCurrencyHasNoTokens"); + }); + + it("parses GlobalParamsTokenNotInCurrency", () => { + const data = encode("GlobalParamsTokenNotInCurrency", [ + "0x" + "bb".repeat(32), + "0x0000000000000000000000000000000000000001", + ]); + expect(parseGlobalParamsError(data)!.name).toBe("GlobalParamsTokenNotInCurrency"); + }); + + it("parses GlobalParamsPlatformLineItemTypeNotFound", () => { + const data = encode("GlobalParamsPlatformLineItemTypeNotFound", [ + "0x" + "11".repeat(32), + "0x" + "22".repeat(32), + ]); + expect(parseGlobalParamsError(data)!.name).toBe("GlobalParamsPlatformLineItemTypeNotFound"); + }); + + it("returns null for unrecognized data", () => { + expect(parseGlobalParamsError("0x12345678")).toBeNull(); + }); +}); + +describe("parseCampaignInfoFactoryError", () => { + function encode(name: string, args?: readonly unknown[]) { + return encodeError(CAMPAIGN_INFO_FACTORY_ABI, name, args); + } + + it.each([ + "CampaignInfoFactoryCampaignInitializationFailed", + "CampaignInfoFactoryInvalidInput", + "CampaignInfoInvalidTokenList", + ])("parses %s", (name) => { + expect(parseCampaignInfoFactoryError(encode(name))!.name).toBe(name); + }); + + it("parses CampaignInfoFactoryPlatformNotListed", () => { + const data = encode("CampaignInfoFactoryPlatformNotListed", ["0x" + "ab".repeat(32)]); + expect(parseCampaignInfoFactoryError(data)!.name).toBe("CampaignInfoFactoryPlatformNotListed"); + }); + + it("parses CampaignInfoFactoryCampaignWithSameIdentifierExists", () => { + const data = encode("CampaignInfoFactoryCampaignWithSameIdentifierExists", [ + "0x" + "ab".repeat(32), + "0x0000000000000000000000000000000000000002", + ]); + expect(parseCampaignInfoFactoryError(data)!.name).toBe( + "CampaignInfoFactoryCampaignWithSameIdentifierExists", + ); + }); + + it("returns null for unrecognized data", () => { + expect(parseCampaignInfoFactoryError("0x12345678")).toBeNull(); + }); +}); + +describe("parseCampaignInfoError", () => { + function encode(name: string, args?: readonly unknown[]) { + return encodeError(CAMPAIGN_INFO_ABI, name, args); + } + + it.each([ + "CampaignInfoInvalidInput", + "CampaignInfoUnauthorized", + "CampaignInfoIsLocked", + ])("parses %s", (name) => { + expect(parseCampaignInfoError(encode(name))!.name).toBe(name); + }); + + it("parses CampaignInfoInvalidPlatformUpdate", () => { + const data = encode("CampaignInfoInvalidPlatformUpdate", ["0x" + "ab".repeat(32), true]); + expect(parseCampaignInfoError(data)!.name).toBe("CampaignInfoInvalidPlatformUpdate"); + }); + + it("parses CampaignInfoPlatformNotSelected", () => { + const data = encode("CampaignInfoPlatformNotSelected", ["0x" + "ab".repeat(32)]); + expect(parseCampaignInfoError(data)!.name).toBe("CampaignInfoPlatformNotSelected"); + }); + + it("parses CampaignInfoPlatformAlreadyApproved", () => { + const data = encode("CampaignInfoPlatformAlreadyApproved", ["0x" + "ab".repeat(32)]); + expect(parseCampaignInfoError(data)!.name).toBe("CampaignInfoPlatformAlreadyApproved"); + }); + + it("falls through to shared error for AdminAccessCheckerUnauthorized", () => { + const data = encode("AdminAccessCheckerUnauthorized"); + const err = parseCampaignInfoError(data); + expect(err).not.toBeNull(); + expect(err!.name).toBe("AdminAccessCheckerUnauthorized"); + }); + + it("falls through to shared error for CurrentTimeIsGreater", () => { + const data = encode("CurrentTimeIsGreater", [100n, 200n]); + const err = parseCampaignInfoError(data); + expect(err).not.toBeNull(); + expect(err!.name).toBe("CurrentTimeIsGreater"); + }); + + it("returns null for unrecognized data", () => { + expect(parseCampaignInfoError("0x12345678")).toBeNull(); + }); +}); + +describe("parseAllOrNothingError", () => { + function encode(name: string, args?: readonly unknown[]) { + return encodeError(ALL_OR_NOTHING_ABI, name, args); + } + + it.each([ + "AllOrNothingFeeNotDisbursed", + "AllOrNothingFeeAlreadyDisbursed", + "AllOrNothingInvalidInput", + "AllOrNothingNotSuccessful", + "AllOrNothingRewardExists", + "AllOrNothingTransferFailed", + "AllOrNothingUnAuthorized", + "TreasurySuccessConditionNotFulfilled", + ])("parses %s", (name) => { + expect(parseAllOrNothingError(encode(name))!.name).toBe(name); + }); + + it("parses AllOrNothingNotClaimable", () => { + const data = encode("AllOrNothingNotClaimable", [42n]); + expect(parseAllOrNothingError(data)!.name).toBe("AllOrNothingNotClaimable"); + }); + + it("parses AllOrNothingTokenNotAccepted", () => { + const data = encode("AllOrNothingTokenNotAccepted", ["0x0000000000000000000000000000000000000003"]); + expect(parseAllOrNothingError(data)!.name).toBe("AllOrNothingTokenNotAccepted"); + }); + + it("falls through to shared error for AccessCheckerUnauthorized", () => { + const data = encode("AccessCheckerUnauthorized"); + const err = parseAllOrNothingError(data); + expect(err).not.toBeNull(); + expect(err!.name).toBe("AccessCheckerUnauthorized"); + }); + + it("falls through to shared error for TreasuryCampaignInfoIsPaused", () => { + const data = encode("TreasuryCampaignInfoIsPaused"); + const err = parseAllOrNothingError(data); + expect(err).not.toBeNull(); + expect(err!.name).toBe("TreasuryCampaignInfoIsPaused"); + }); + + it("returns null for unrecognized data", () => { + expect(parseAllOrNothingError("0x12345678")).toBeNull(); + }); +}); + +describe("parseKeepWhatsRaisedError", () => { + function encode(name: string, args?: readonly unknown[]) { + return encodeError(KEEP_WHATS_RAISED_ABI, name, args); + } + + it.each([ + "KeepWhatsRaisedUnAuthorized", + "KeepWhatsRaisedInvalidInput", + "KeepWhatsRaisedRewardExists", + "KeepWhatsRaisedDisabled", + "KeepWhatsRaisedAlreadyEnabled", + "KeepWhatsRaisedAlreadyWithdrawn", + "KeepWhatsRaisedAlreadyClaimed", + "KeepWhatsRaisedNotClaimableAdmin", + "KeepWhatsRaisedConfigLocked", + "KeepWhatsRaisedDisbursementBlocked", + ])("parses %s", (name) => { + expect(parseKeepWhatsRaisedError(encode(name))!.name).toBe(name); + }); + + it("parses KeepWhatsRaisedTokenNotAccepted", () => { + const data = encode("KeepWhatsRaisedTokenNotAccepted", ["0x0000000000000000000000000000000000000004"]); + expect(parseKeepWhatsRaisedError(data)!.name).toBe("KeepWhatsRaisedTokenNotAccepted"); + }); + + it("parses KeepWhatsRaisedInsufficientFundsForWithdrawalAndFee", () => { + const data = encode("KeepWhatsRaisedInsufficientFundsForWithdrawalAndFee", [100n, 200n, 10n]); + expect(parseKeepWhatsRaisedError(data)!.name).toBe("KeepWhatsRaisedInsufficientFundsForWithdrawalAndFee"); + }); + + it("parses KeepWhatsRaisedInsufficientFundsForFee", () => { + const data = encode("KeepWhatsRaisedInsufficientFundsForFee", [50n, 5n]); + expect(parseKeepWhatsRaisedError(data)!.name).toBe("KeepWhatsRaisedInsufficientFundsForFee"); + }); + + it("parses KeepWhatsRaisedNotClaimable", () => { + const data = encode("KeepWhatsRaisedNotClaimable", [7n]); + expect(parseKeepWhatsRaisedError(data)!.name).toBe("KeepWhatsRaisedNotClaimable"); + }); + + it("parses KeepWhatsRaisedPledgeAlreadyProcessed", () => { + const data = encode("KeepWhatsRaisedPledgeAlreadyProcessed", ["0x" + "cc".repeat(32)]); + expect(parseKeepWhatsRaisedError(data)!.name).toBe("KeepWhatsRaisedPledgeAlreadyProcessed"); + }); + + it("falls through to shared error for AccessCheckerUnauthorized", () => { + const data = encode("AccessCheckerUnauthorized"); + const err = parseKeepWhatsRaisedError(data); + expect(err).not.toBeNull(); + expect(err!.name).toBe("AccessCheckerUnauthorized"); + }); + + it("falls through to shared error for TreasuryFeeNotDisbursed", () => { + const data = encode("TreasuryFeeNotDisbursed"); + const err = parseKeepWhatsRaisedError(data); + expect(err).not.toBeNull(); + expect(err!.name).toBe("TreasuryFeeNotDisbursed"); + }); + + it("returns null for unrecognized data", () => { + expect(parseKeepWhatsRaisedError("0x12345678")).toBeNull(); + }); +}); + +describe("parseItemRegistryError", () => { + it("parses ItemRegistryMismatchedArraysLength", () => { + const data = encodeError(ITEM_REGISTRY_ABI, "ItemRegistryMismatchedArraysLength"); + expect(parseItemRegistryError(data)!.name).toBe("ItemRegistryMismatchedArraysLength"); + }); + + it("returns null for unrecognized data", () => { + expect(parseItemRegistryError("0x12345678")).toBeNull(); + }); +}); + +describe("parsePaymentTreasuryError", () => { + function encode(name: string, args?: readonly unknown[]) { + return encodeError(PAYMENT_TREASURY_ABI, name, args); + } + + it.each([ + "PaymentTreasuryUnAuthorized", + "PaymentTreasuryInvalidInput", + "PaymentTreasuryCampaignInfoIsPaused", + "PaymentTreasurySuccessConditionNotFulfilled", + "PaymentTreasuryFeeNotDisbursed", + "PaymentTreasuryAlreadyWithdrawn", + "PaymentTreasuryNoFundsToClaim", + ])("parses %s", (name) => { + expect(parsePaymentTreasuryError(encode(name))!.name).toBe(name); + }); + + it("parses PaymentTreasuryPaymentAlreadyExist", () => { + const data = encode("PaymentTreasuryPaymentAlreadyExist", ["0x" + "ab".repeat(32)]); + expect(parsePaymentTreasuryError(data)!.name).toBe("PaymentTreasuryPaymentAlreadyExist"); + }); + + it("parses PaymentTreasuryPaymentAlreadyConfirmed", () => { + const data = encode("PaymentTreasuryPaymentAlreadyConfirmed", ["0x" + "ab".repeat(32)]); + expect(parsePaymentTreasuryError(data)!.name).toBe("PaymentTreasuryPaymentAlreadyConfirmed"); + }); + + it("parses PaymentTreasuryPaymentAlreadyExpired", () => { + const data = encode("PaymentTreasuryPaymentAlreadyExpired", ["0x" + "ab".repeat(32)]); + expect(parsePaymentTreasuryError(data)!.name).toBe("PaymentTreasuryPaymentAlreadyExpired"); + }); + + it("parses PaymentTreasuryPaymentNotExist", () => { + const data = encode("PaymentTreasuryPaymentNotExist", ["0x" + "ab".repeat(32)]); + expect(parsePaymentTreasuryError(data)!.name).toBe("PaymentTreasuryPaymentNotExist"); + }); + + it("parses PaymentTreasuryTokenNotAccepted", () => { + const data = encode("PaymentTreasuryTokenNotAccepted", ["0x0000000000000000000000000000000000000005"]); + expect(parsePaymentTreasuryError(data)!.name).toBe("PaymentTreasuryTokenNotAccepted"); + }); + + it("parses PaymentTreasuryPaymentNotConfirmed", () => { + const data = encode("PaymentTreasuryPaymentNotConfirmed", ["0x" + "ab".repeat(32)]); + expect(parsePaymentTreasuryError(data)!.name).toBe("PaymentTreasuryPaymentNotConfirmed"); + }); + + it("parses PaymentTreasuryPaymentNotClaimable", () => { + const data = encode("PaymentTreasuryPaymentNotClaimable", ["0x" + "ab".repeat(32)]); + expect(parsePaymentTreasuryError(data)!.name).toBe("PaymentTreasuryPaymentNotClaimable"); + }); + + it("parses PaymentTreasuryCryptoPayment", () => { + const data = encode("PaymentTreasuryCryptoPayment", ["0x" + "ab".repeat(32)]); + expect(parsePaymentTreasuryError(data)!.name).toBe("PaymentTreasuryCryptoPayment"); + }); + + it("parses PaymentTreasuryInsufficientFundsForFee", () => { + const data = encode("PaymentTreasuryInsufficientFundsForFee", [100n, 10n]); + expect(parsePaymentTreasuryError(data)!.name).toBe("PaymentTreasuryInsufficientFundsForFee"); + }); + + it("parses PaymentTreasuryInsufficientBalance", () => { + const data = encode("PaymentTreasuryInsufficientBalance", [500n, 100n]); + expect(parsePaymentTreasuryError(data)!.name).toBe("PaymentTreasuryInsufficientBalance"); + }); + + it("parses PaymentTreasuryExpirationExceedsMax", () => { + const data = encode("PaymentTreasuryExpirationExceedsMax", [999n, 100n]); + expect(parsePaymentTreasuryError(data)!.name).toBe("PaymentTreasuryExpirationExceedsMax"); + }); + + it("parses PaymentTreasuryClaimWindowNotReached", () => { + const data = encode("PaymentTreasuryClaimWindowNotReached", [9999n]); + expect(parsePaymentTreasuryError(data)!.name).toBe("PaymentTreasuryClaimWindowNotReached"); + }); + + it("returns null for unrecognized data", () => { + expect(parsePaymentTreasuryError("0x12345678")).toBeNull(); + }); +}); + +describe("parseTreasuryFactoryError", () => { + function encode(name: string, args?: readonly unknown[]) { + return encodeError(TREASURY_FACTORY_ABI, name, args); + } + + it.each([ + "TreasuryFactoryUnauthorized", + "TreasuryFactoryInvalidKey", + "TreasuryFactoryTreasuryCreationFailed", + "TreasuryFactoryInvalidAddress", + "TreasuryFactoryImplementationNotSet", + "TreasuryFactoryImplementationNotSetOrApproved", + "TreasuryFactoryTreasuryInitializationFailed", + "TreasuryFactorySettingPlatformInfoFailed", + ])("parses %s", (name) => { + expect(parseTreasuryFactoryError(encode(name))!.name).toBe(name); + }); + + it("falls through to shared error for AdminAccessCheckerUnauthorized", () => { + const data = encode("AdminAccessCheckerUnauthorized"); + const err = parseTreasuryFactoryError(data); + expect(err).not.toBeNull(); + expect(err!.name).toBe("AdminAccessCheckerUnauthorized"); + }); + + it("returns null for unrecognized data", () => { + expect(parseTreasuryFactoryError("0x12345678")).toBeNull(); + }); +}); diff --git a/packages/contracts/__tests__/unit/errors.test.ts b/packages/contracts/__tests__/unit/errors.test.ts new file mode 100644 index 00000000..f5ea9fdd --- /dev/null +++ b/packages/contracts/__tests__/unit/errors.test.ts @@ -0,0 +1,486 @@ +import { + GlobalParamsInvalidInputError, + GlobalParamsPlatformAdminNotSetError, + GlobalParamsPlatformAlreadyListedError, + GlobalParamsPlatformDataAlreadySetError, + GlobalParamsPlatformDataNotSetError, + GlobalParamsPlatformDataSlotTakenError, + GlobalParamsPlatformFeePercentIsZeroError, + GlobalParamsPlatformNotListedError, + GlobalParamsUnauthorizedError, + GlobalParamsCurrencyTokenLengthMismatchError, + GlobalParamsCurrencyHasNoTokensError, + GlobalParamsTokenNotInCurrencyError, + GlobalParamsPlatformLineItemTypeNotFoundError, +} from "../../src/errors/contracts/global-params"; + +import { + CampaignInfoFactoryCampaignInitializationFailedError, + CampaignInfoFactoryInvalidInputError, + CampaignInfoFactoryPlatformNotListedError, + CampaignInfoFactoryCampaignWithSameIdentifierExistsError, + CampaignInfoInvalidTokenListError, +} from "../../src/errors/contracts/campaign-info-factory"; + +import { + CampaignInfoInvalidInputError, + CampaignInfoInvalidPlatformUpdateError, + CampaignInfoPlatformNotSelectedError, + CampaignInfoPlatformAlreadyApprovedError, + CampaignInfoUnauthorizedError, + CampaignInfoIsLockedError, +} from "../../src/errors/contracts/campaign-info"; + +import { + AllOrNothingFeeNotDisbursedError, + AllOrNothingFeeAlreadyDisbursedError, + AllOrNothingInvalidInputError, + AllOrNothingNotClaimableError, + AllOrNothingNotSuccessfulError, + AllOrNothingRewardExistsError, + AllOrNothingTransferFailedError, + AllOrNothingUnAuthorizedError, + AllOrNothingTokenNotAcceptedError, + TreasurySuccessConditionNotFulfilledError, +} from "../../src/errors/contracts/all-or-nothing"; + +import { + KeepWhatsRaisedUnAuthorizedError, + KeepWhatsRaisedInvalidInputError, + KeepWhatsRaisedTokenNotAcceptedError, + KeepWhatsRaisedRewardExistsError, + KeepWhatsRaisedDisabledError, + KeepWhatsRaisedAlreadyEnabledError, + KeepWhatsRaisedInsufficientFundsForWithdrawalAndFeeError, + KeepWhatsRaisedInsufficientFundsForFeeError, + KeepWhatsRaisedAlreadyWithdrawnError, + KeepWhatsRaisedAlreadyClaimedError, + KeepWhatsRaisedNotClaimableError, + KeepWhatsRaisedNotClaimableAdminError, + KeepWhatsRaisedConfigLockedError, + KeepWhatsRaisedDisbursementBlockedError, + KeepWhatsRaisedPledgeAlreadyProcessedError, +} from "../../src/errors/contracts/keep-whats-raised"; + +import { ItemRegistryMismatchedArraysLengthError } from "../../src/errors/contracts/item-registry"; + +import { + PaymentTreasuryUnAuthorizedError, + PaymentTreasuryInvalidInputError, + PaymentTreasuryPaymentAlreadyExistError, + PaymentTreasuryPaymentAlreadyConfirmedError, + PaymentTreasuryPaymentAlreadyExpiredError, + PaymentTreasuryPaymentNotExistError, + PaymentTreasuryCampaignInfoIsPausedError, + PaymentTreasuryTokenNotAcceptedError, + PaymentTreasurySuccessConditionNotFulfilledError, + PaymentTreasuryFeeNotDisbursedError, + PaymentTreasuryPaymentNotConfirmedError, + PaymentTreasuryPaymentNotClaimableError, + PaymentTreasuryAlreadyWithdrawnError, + PaymentTreasuryCryptoPaymentError, + PaymentTreasuryInsufficientFundsForFeeError, + PaymentTreasuryInsufficientBalanceError, + PaymentTreasuryExpirationExceedsMaxError, + PaymentTreasuryClaimWindowNotReachedError, + PaymentTreasuryNoFundsToClaimError, +} from "../../src/errors/contracts/payment-treasury"; + +import { + TreasuryFactoryUnauthorizedError, + TreasuryFactoryInvalidKeyError, + TreasuryFactoryTreasuryCreationFailedError, + TreasuryFactoryInvalidAddressError, + TreasuryFactoryImplementationNotSetError, + TreasuryFactoryImplementationNotSetOrApprovedError, + TreasuryFactoryTreasuryInitializationFailedError, + TreasuryFactorySettingPlatformInfoFailedError, +} from "../../src/errors/contracts/treasury-factory"; + +import { + AccessCheckerUnauthorizedError, + AdminAccessCheckerUnauthorizedError, + CurrentTimeIsGreaterError, + CurrentTimeIsLessError, + CurrentTimeIsNotWithinRangeError, + TreasuryCampaignInfoIsPausedError, + TreasuryFeeNotDisbursedError, + TreasuryTransferFailedError, +} from "../../src/errors/contracts/shared"; + +import { getRecoveryHint } from "../../src/errors/recovery"; +import type { ContractErrorBase } from "../../src/errors/base"; + +function assertError( + err: Error & ContractErrorBase, + expectedName: string, +) { + expect(err).toBeInstanceOf(Error); + expect(err.name).toBe(expectedName); + expect(err.message).toBeTruthy(); +} + +describe("Shared errors", () => { + it("AccessCheckerUnauthorizedError", () => { + const e = new AccessCheckerUnauthorizedError(); + assertError(e, "AccessCheckerUnauthorized"); + expect(e.args).toEqual({}); + expect(e.recoveryHint).toBeDefined(); + }); + + it("AdminAccessCheckerUnauthorizedError", () => { + const e = new AdminAccessCheckerUnauthorizedError(); + assertError(e, "AdminAccessCheckerUnauthorized"); + }); + + it("CurrentTimeIsGreaterError", () => { + const e = new CurrentTimeIsGreaterError({ inputTime: "100", currentTime: "200" }); + assertError(e, "CurrentTimeIsGreater"); + expect(e.args.inputTime).toBe("100"); + expect(e.args.currentTime).toBe("200"); + }); + + it("CurrentTimeIsLessError", () => { + const e = new CurrentTimeIsLessError({ inputTime: "200", currentTime: "100" }); + assertError(e, "CurrentTimeIsLess"); + expect(e.args.inputTime).toBe("200"); + }); + + it("CurrentTimeIsNotWithinRangeError", () => { + const e = new CurrentTimeIsNotWithinRangeError({ initialTime: "10", finalTime: "20" }); + assertError(e, "CurrentTimeIsNotWithinRange"); + expect(e.args.initialTime).toBe("10"); + }); + + it("TreasuryCampaignInfoIsPausedError", () => { + const e = new TreasuryCampaignInfoIsPausedError(); + assertError(e, "TreasuryCampaignInfoIsPaused"); + }); + + it("TreasuryFeeNotDisbursedError", () => { + const e = new TreasuryFeeNotDisbursedError(); + assertError(e, "TreasuryFeeNotDisbursed"); + }); + + it("TreasuryTransferFailedError", () => { + const e = new TreasuryTransferFailedError(); + assertError(e, "TreasuryTransferFailed"); + }); +}); + +describe("GlobalParams errors", () => { + const noArgErrors = [ + ["GlobalParamsInvalidInput", GlobalParamsInvalidInputError], + ["GlobalParamsPlatformDataAlreadySet", GlobalParamsPlatformDataAlreadySetError], + ["GlobalParamsPlatformDataNotSet", GlobalParamsPlatformDataNotSetError], + ["GlobalParamsPlatformDataSlotTaken", GlobalParamsPlatformDataSlotTakenError], + ["GlobalParamsUnauthorized", GlobalParamsUnauthorizedError], + ["GlobalParamsCurrencyTokenLengthMismatch", GlobalParamsCurrencyTokenLengthMismatchError], + ] as const; + + it.each(noArgErrors)("%s", (name, Cls) => { + const e = new (Cls as new () => Error & ContractErrorBase)(); + assertError(e, name); + }); + + it("GlobalParamsPlatformAdminNotSetError", () => { + const e = new GlobalParamsPlatformAdminNotSetError({ platformBytes: "0x01" }); + assertError(e, "GlobalParamsPlatformAdminNotSet"); + expect(e.args.platformBytes).toBe("0x01"); + }); + + it("GlobalParamsPlatformAlreadyListedError", () => { + const e = new GlobalParamsPlatformAlreadyListedError({ platformBytes: "0x02" }); + assertError(e, "GlobalParamsPlatformAlreadyListed"); + }); + + it("GlobalParamsPlatformFeePercentIsZeroError", () => { + const e = new GlobalParamsPlatformFeePercentIsZeroError({ platformBytes: "0x03" }); + assertError(e, "GlobalParamsPlatformFeePercentIsZero"); + }); + + it("GlobalParamsPlatformNotListedError", () => { + const e = new GlobalParamsPlatformNotListedError({ platformBytes: "0x04" }); + assertError(e, "GlobalParamsPlatformNotListed"); + }); + + it("GlobalParamsCurrencyHasNoTokensError", () => { + const e = new GlobalParamsCurrencyHasNoTokensError({ currency: "0x05" }); + assertError(e, "GlobalParamsCurrencyHasNoTokens"); + expect(e.args.currency).toBe("0x05"); + }); + + it("GlobalParamsTokenNotInCurrencyError", () => { + const e = new GlobalParamsTokenNotInCurrencyError({ currency: "0x06", token: "0x07" }); + assertError(e, "GlobalParamsTokenNotInCurrency"); + expect(e.args.token).toBe("0x07"); + }); + + it("GlobalParamsPlatformLineItemTypeNotFoundError", () => { + const e = new GlobalParamsPlatformLineItemTypeNotFoundError({ platformHash: "0x08", typeId: "0x09" }); + assertError(e, "GlobalParamsPlatformLineItemTypeNotFound"); + expect(e.args.platformHash).toBe("0x08"); + expect(e.args.typeId).toBe("0x09"); + }); +}); + +describe("CampaignInfoFactory errors", () => { + it("CampaignInfoFactoryCampaignInitializationFailedError", () => { + const e = new CampaignInfoFactoryCampaignInitializationFailedError(); + assertError(e, "CampaignInfoFactoryCampaignInitializationFailed"); + }); + + it("CampaignInfoFactoryInvalidInputError", () => { + const e = new CampaignInfoFactoryInvalidInputError(); + assertError(e, "CampaignInfoFactoryInvalidInput"); + }); + + it("CampaignInfoFactoryPlatformNotListedError", () => { + const e = new CampaignInfoFactoryPlatformNotListedError({ platformHash: "0xaa" }); + assertError(e, "CampaignInfoFactoryPlatformNotListed"); + expect(e.args.platformHash).toBe("0xaa"); + }); + + it("CampaignInfoFactoryCampaignWithSameIdentifierExistsError", () => { + const e = new CampaignInfoFactoryCampaignWithSameIdentifierExistsError({ + identifierHash: "0xbb", + cloneExists: "0xcc", + }); + assertError(e, "CampaignInfoFactoryCampaignWithSameIdentifierExists"); + expect(e.args.identifierHash).toBe("0xbb"); + expect(e.args.cloneExists).toBe("0xcc"); + }); + + it("CampaignInfoInvalidTokenListError", () => { + const e = new CampaignInfoInvalidTokenListError(); + assertError(e, "CampaignInfoInvalidTokenList"); + }); +}); + +describe("CampaignInfo errors", () => { + it("CampaignInfoInvalidInputError", () => { + const e = new CampaignInfoInvalidInputError(); + assertError(e, "CampaignInfoInvalidInput"); + }); + + it("CampaignInfoInvalidPlatformUpdateError", () => { + const e = new CampaignInfoInvalidPlatformUpdateError({ platformBytes: "0x01", selection: true }); + assertError(e, "CampaignInfoInvalidPlatformUpdate"); + expect(e.args.selection).toBe(true); + }); + + it("CampaignInfoPlatformNotSelectedError", () => { + const e = new CampaignInfoPlatformNotSelectedError({ platformBytes: "0x02" }); + assertError(e, "CampaignInfoPlatformNotSelected"); + }); + + it("CampaignInfoPlatformAlreadyApprovedError", () => { + const e = new CampaignInfoPlatformAlreadyApprovedError({ platformHash: "0x03" }); + assertError(e, "CampaignInfoPlatformAlreadyApproved"); + }); + + it("CampaignInfoUnauthorizedError", () => { + const e = new CampaignInfoUnauthorizedError(); + assertError(e, "CampaignInfoUnauthorized"); + }); + + it("CampaignInfoIsLockedError", () => { + const e = new CampaignInfoIsLockedError(); + assertError(e, "CampaignInfoIsLocked"); + }); +}); + +describe("AllOrNothing errors", () => { + const noArgErrors = [ + ["AllOrNothingFeeNotDisbursed", AllOrNothingFeeNotDisbursedError], + ["AllOrNothingFeeAlreadyDisbursed", AllOrNothingFeeAlreadyDisbursedError], + ["AllOrNothingInvalidInput", AllOrNothingInvalidInputError], + ["AllOrNothingNotSuccessful", AllOrNothingNotSuccessfulError], + ["AllOrNothingRewardExists", AllOrNothingRewardExistsError], + ["AllOrNothingTransferFailed", AllOrNothingTransferFailedError], + ["AllOrNothingUnAuthorized", AllOrNothingUnAuthorizedError], + ["TreasurySuccessConditionNotFulfilled", TreasurySuccessConditionNotFulfilledError], + ] as const; + + it.each(noArgErrors)("%s", (name, Cls) => { + const e = new (Cls as new () => Error & ContractErrorBase)(); + assertError(e, name); + }); + + it("AllOrNothingNotClaimableError", () => { + const e = new AllOrNothingNotClaimableError({ tokenId: "42" }); + assertError(e, "AllOrNothingNotClaimable"); + expect(e.args.tokenId).toBe("42"); + }); + + it("AllOrNothingTokenNotAcceptedError", () => { + const e = new AllOrNothingTokenNotAcceptedError({ token: "0xtoken" }); + assertError(e, "AllOrNothingTokenNotAccepted"); + expect(e.args.token).toBe("0xtoken"); + }); +}); + +describe("KeepWhatsRaised errors", () => { + const noArgErrors = [ + ["KeepWhatsRaisedUnAuthorized", KeepWhatsRaisedUnAuthorizedError], + ["KeepWhatsRaisedInvalidInput", KeepWhatsRaisedInvalidInputError], + ["KeepWhatsRaisedRewardExists", KeepWhatsRaisedRewardExistsError], + ["KeepWhatsRaisedDisabled", KeepWhatsRaisedDisabledError], + ["KeepWhatsRaisedAlreadyEnabled", KeepWhatsRaisedAlreadyEnabledError], + ["KeepWhatsRaisedAlreadyWithdrawn", KeepWhatsRaisedAlreadyWithdrawnError], + ["KeepWhatsRaisedAlreadyClaimed", KeepWhatsRaisedAlreadyClaimedError], + ["KeepWhatsRaisedNotClaimableAdmin", KeepWhatsRaisedNotClaimableAdminError], + ["KeepWhatsRaisedConfigLocked", KeepWhatsRaisedConfigLockedError], + ["KeepWhatsRaisedDisbursementBlocked", KeepWhatsRaisedDisbursementBlockedError], + ] as const; + + it.each(noArgErrors)("%s", (name, Cls) => { + const e = new (Cls as new () => Error & ContractErrorBase)(); + assertError(e, name); + }); + + it("KeepWhatsRaisedTokenNotAcceptedError", () => { + const e = new KeepWhatsRaisedTokenNotAcceptedError({ token: "0xt" }); + assertError(e, "KeepWhatsRaisedTokenNotAccepted"); + }); + + it("KeepWhatsRaisedInsufficientFundsForWithdrawalAndFeeError", () => { + const e = new KeepWhatsRaisedInsufficientFundsForWithdrawalAndFeeError({ + availableAmount: "100", + withdrawalAmount: "200", + fee: "10", + }); + assertError(e, "KeepWhatsRaisedInsufficientFundsForWithdrawalAndFee"); + expect(e.args.availableAmount).toBe("100"); + }); + + it("KeepWhatsRaisedInsufficientFundsForFeeError", () => { + const e = new KeepWhatsRaisedInsufficientFundsForFeeError({ withdrawalAmount: "50", fee: "5" }); + assertError(e, "KeepWhatsRaisedInsufficientFundsForFee"); + }); + + it("KeepWhatsRaisedNotClaimableError", () => { + const e = new KeepWhatsRaisedNotClaimableError({ tokenId: "7" }); + assertError(e, "KeepWhatsRaisedNotClaimable"); + }); + + it("KeepWhatsRaisedPledgeAlreadyProcessedError", () => { + const e = new KeepWhatsRaisedPledgeAlreadyProcessedError({ pledgeId: "p1" }); + assertError(e, "KeepWhatsRaisedPledgeAlreadyProcessed"); + }); +}); + +describe("ItemRegistry errors", () => { + it("ItemRegistryMismatchedArraysLengthError", () => { + const e = new ItemRegistryMismatchedArraysLengthError(); + assertError(e, "ItemRegistryMismatchedArraysLength"); + }); +}); + +describe("PaymentTreasury errors", () => { + const noArgErrors = [ + ["PaymentTreasuryUnAuthorized", PaymentTreasuryUnAuthorizedError], + ["PaymentTreasuryInvalidInput", PaymentTreasuryInvalidInputError], + ["PaymentTreasuryCampaignInfoIsPaused", PaymentTreasuryCampaignInfoIsPausedError], + ["PaymentTreasurySuccessConditionNotFulfilled", PaymentTreasurySuccessConditionNotFulfilledError], + ["PaymentTreasuryFeeNotDisbursed", PaymentTreasuryFeeNotDisbursedError], + ["PaymentTreasuryAlreadyWithdrawn", PaymentTreasuryAlreadyWithdrawnError], + ["PaymentTreasuryNoFundsToClaim", PaymentTreasuryNoFundsToClaimError], + ] as const; + + it.each(noArgErrors)("%s", (name, Cls) => { + const e = new (Cls as new () => Error & ContractErrorBase)(); + assertError(e, name); + }); + + it("PaymentTreasuryPaymentAlreadyExistError", () => { + const e = new PaymentTreasuryPaymentAlreadyExistError({ paymentId: "p1" }); + assertError(e, "PaymentTreasuryPaymentAlreadyExist"); + }); + + it("PaymentTreasuryPaymentAlreadyConfirmedError", () => { + const e = new PaymentTreasuryPaymentAlreadyConfirmedError({ paymentId: "p2" }); + assertError(e, "PaymentTreasuryPaymentAlreadyConfirmed"); + }); + + it("PaymentTreasuryPaymentAlreadyExpiredError", () => { + const e = new PaymentTreasuryPaymentAlreadyExpiredError({ paymentId: "p3" }); + assertError(e, "PaymentTreasuryPaymentAlreadyExpired"); + }); + + it("PaymentTreasuryPaymentNotExistError", () => { + const e = new PaymentTreasuryPaymentNotExistError({ paymentId: "p4" }); + assertError(e, "PaymentTreasuryPaymentNotExist"); + }); + + it("PaymentTreasuryTokenNotAcceptedError", () => { + const e = new PaymentTreasuryTokenNotAcceptedError({ token: "0xt" }); + assertError(e, "PaymentTreasuryTokenNotAccepted"); + }); + + it("PaymentTreasuryPaymentNotConfirmedError", () => { + const e = new PaymentTreasuryPaymentNotConfirmedError({ paymentId: "p5" }); + assertError(e, "PaymentTreasuryPaymentNotConfirmed"); + }); + + it("PaymentTreasuryPaymentNotClaimableError", () => { + const e = new PaymentTreasuryPaymentNotClaimableError({ paymentId: "p6" }); + assertError(e, "PaymentTreasuryPaymentNotClaimable"); + }); + + it("PaymentTreasuryCryptoPaymentError", () => { + const e = new PaymentTreasuryCryptoPaymentError({ paymentId: "p7" }); + assertError(e, "PaymentTreasuryCryptoPayment"); + }); + + it("PaymentTreasuryInsufficientFundsForFeeError", () => { + const e = new PaymentTreasuryInsufficientFundsForFeeError({ withdrawalAmount: "100", fee: "10" }); + assertError(e, "PaymentTreasuryInsufficientFundsForFee"); + }); + + it("PaymentTreasuryInsufficientBalanceError", () => { + const e = new PaymentTreasuryInsufficientBalanceError({ required: "500", available: "100" }); + assertError(e, "PaymentTreasuryInsufficientBalance"); + }); + + it("PaymentTreasuryExpirationExceedsMaxError", () => { + const e = new PaymentTreasuryExpirationExceedsMaxError({ expiration: "999", maxExpiration: "100" }); + assertError(e, "PaymentTreasuryExpirationExceedsMax"); + }); + + it("PaymentTreasuryClaimWindowNotReachedError", () => { + const e = new PaymentTreasuryClaimWindowNotReachedError({ claimableAt: "9999" }); + assertError(e, "PaymentTreasuryClaimWindowNotReached"); + }); +}); + +describe("TreasuryFactory errors", () => { + const noArgErrors = [ + ["TreasuryFactoryUnauthorized", TreasuryFactoryUnauthorizedError], + ["TreasuryFactoryInvalidKey", TreasuryFactoryInvalidKeyError], + ["TreasuryFactoryTreasuryCreationFailed", TreasuryFactoryTreasuryCreationFailedError], + ["TreasuryFactoryInvalidAddress", TreasuryFactoryInvalidAddressError], + ["TreasuryFactoryImplementationNotSet", TreasuryFactoryImplementationNotSetError], + ["TreasuryFactoryImplementationNotSetOrApproved", TreasuryFactoryImplementationNotSetOrApprovedError], + ["TreasuryFactoryTreasuryInitializationFailed", TreasuryFactoryTreasuryInitializationFailedError], + ["TreasuryFactorySettingPlatformInfoFailed", TreasuryFactorySettingPlatformInfoFailedError], + ] as const; + + it.each(noArgErrors)("%s", (name, Cls) => { + const e = new (Cls as new () => Error & ContractErrorBase)(); + assertError(e, name); + }); +}); + +describe("getRecoveryHint", () => { + it("returns hint when present", () => { + const err = new GlobalParamsInvalidInputError(); + expect(getRecoveryHint(err)).toBeDefined(); + expect(typeof getRecoveryHint(err)).toBe("string"); + }); + + it("returns undefined when no hint", () => { + const err: ContractErrorBase = { name: "Test", args: {} }; + expect(getRecoveryHint(err)).toBeUndefined(); + }); +}); diff --git a/packages/contracts/__tests__/unit/guard.test.ts b/packages/contracts/__tests__/unit/guard.test.ts new file mode 100644 index 00000000..d3b2adb8 --- /dev/null +++ b/packages/contracts/__tests__/unit/guard.test.ts @@ -0,0 +1,122 @@ +import { isSimpleConfig, isReadOnlySimpleConfig } from "../../src/client/guard"; +import type { OakContractsClientConfig } from "../../src/client/types"; + +describe("isSimpleConfig", () => { + const valid: OakContractsClientConfig = { + chainId: 11142220, + rpcUrl: "https://rpc.example.com", + privateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + }; + + it("returns true for a valid simple config", () => { + expect(isSimpleConfig(valid)).toBe(true); + }); + + it("returns false when chainId is missing", () => { + const { chainId, ...rest } = valid as unknown as Record; + expect(isSimpleConfig(rest as unknown as OakContractsClientConfig)).toBe(false); + }); + + it("returns false when rpcUrl is missing", () => { + const { rpcUrl, ...rest } = valid as unknown as Record; + expect(isSimpleConfig(rest as unknown as OakContractsClientConfig)).toBe(false); + }); + + it("returns false when privateKey is missing", () => { + const { privateKey, ...rest } = valid as unknown as Record; + expect(isSimpleConfig(rest as unknown as OakContractsClientConfig)).toBe(false); + }); + + it("returns false when chainId is not a number", () => { + expect( + isSimpleConfig({ ...valid, chainId: "11142220" } as unknown as OakContractsClientConfig), + ).toBe(false); + }); + + it("returns false when rpcUrl is not a string", () => { + expect( + isSimpleConfig({ ...valid, rpcUrl: 123 } as unknown as OakContractsClientConfig), + ).toBe(false); + }); + + it("returns false when rpcUrl is empty", () => { + expect( + isSimpleConfig({ ...valid, rpcUrl: "" } as unknown as OakContractsClientConfig), + ).toBe(false); + }); + + it("throws when privateKey is not a string", () => { + expect(() => + isSimpleConfig({ ...valid, privateKey: 42 } as unknown as OakContractsClientConfig), + ).toThrow("Invalid privateKey"); + }); + + it("throws when privateKey does not start with 0x", () => { + expect(() => + isSimpleConfig({ + ...valid, + privateKey: "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + } as unknown as OakContractsClientConfig), + ).toThrow("Invalid privateKey"); + }); + + it("returns false for a full config shape", () => { + const full: OakContractsClientConfig = { + chain: 11142220, + provider: {} as never, + signer: {} as never, + }; + expect(isSimpleConfig(full)).toBe(false); + }); +}); + +describe("isReadOnlySimpleConfig", () => { + it("returns true for valid read-only config", () => { + const config = { chainId: 11142220, rpcUrl: "https://rpc.example.com" }; + expect(isReadOnlySimpleConfig(config as OakContractsClientConfig)).toBe(true); + }); + + it("returns false when chainId is missing", () => { + const config = { rpcUrl: "https://rpc.example.com" }; + expect(isReadOnlySimpleConfig(config as unknown as OakContractsClientConfig)).toBe(false); + }); + + it("returns false when privateKey is present", () => { + const config = { + chainId: 11142220, + rpcUrl: "https://rpc.example.com", + privateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + }; + expect(isReadOnlySimpleConfig(config as OakContractsClientConfig)).toBe(false); + }); + + it("returns false when provider/signer are present", () => { + const config = { + chain: 11142220, + provider: {} as never, + signer: {} as never, + }; + expect(isReadOnlySimpleConfig(config as OakContractsClientConfig)).toBe(false); + }); + + it("throws when chainId is not a number", () => { + const config = { chainId: "bad", rpcUrl: "https://rpc.example.com" }; + expect(() => + isReadOnlySimpleConfig(config as unknown as OakContractsClientConfig), + ).toThrow("Invalid chainId"); + }); + + it("throws when rpcUrl is empty", () => { + const config = { chainId: 11142220, rpcUrl: "" }; + expect(() => + isReadOnlySimpleConfig(config as unknown as OakContractsClientConfig), + ).toThrow("Invalid rpcUrl"); + }); + + it("throws when rpcUrl is not a string", () => { + const config = { chainId: 11142220, rpcUrl: 123 }; + expect(() => + isReadOnlySimpleConfig(config as unknown as OakContractsClientConfig), + ).toThrow("Invalid rpcUrl"); + }); +}); diff --git a/packages/contracts/__tests__/unit/metrics.test.ts b/packages/contracts/__tests__/unit/metrics.test.ts new file mode 100644 index 00000000..3f7f97e6 --- /dev/null +++ b/packages/contracts/__tests__/unit/metrics.test.ts @@ -0,0 +1,20 @@ +import { getPlatformStats } from "../../src/metrics/platform"; +import { getCampaignSummary } from "../../src/metrics/campaign"; +import { getTreasuryReport } from "../../src/metrics/treasury"; + +describe("metrics stubs", () => { + it("getPlatformStats returns empty object", async () => { + const result = await getPlatformStats(); + expect(result).toEqual({}); + }); + + it("getCampaignSummary returns empty object", async () => { + const result = await getCampaignSummary("0x1234567890abcdef1234567890abcdef12345678"); + expect(result).toEqual({}); + }); + + it("getTreasuryReport returns empty object", async () => { + const result = await getTreasuryReport("0x1234567890abcdef1234567890abcdef12345678"); + expect(result).toEqual({}); + }); +}); diff --git a/packages/contracts/__tests__/unit/provider.test.ts b/packages/contracts/__tests__/unit/provider.test.ts new file mode 100644 index 00000000..1aa9e8d9 --- /dev/null +++ b/packages/contracts/__tests__/unit/provider.test.ts @@ -0,0 +1,89 @@ +import { + createJsonRpcProvider, + createWallet, + createBrowserProvider, + getSigner, +} from "../../src/lib/viem/provider"; +import { sepolia } from "../../src/lib/viem/index"; +import type { EIP1193Provider } from "viem"; +import { TEST_PRIVATE_KEY, TEST_RPC_URL } from "../setup/constant"; + +describe("createJsonRpcProvider", () => { + it("returns a PublicClient with readContract method", () => { + const provider = createJsonRpcProvider(TEST_RPC_URL, sepolia); + expect(provider).toBeDefined(); + expect(typeof provider.readContract).toBe("function"); + }); + + it("accepts an optional timeout", () => { + const provider = createJsonRpcProvider(TEST_RPC_URL, sepolia, 60000); + expect(provider).toBeDefined(); + }); +}); + +describe("createWallet", () => { + it("returns a Wallet with an account", () => { + const wallet = createWallet(TEST_PRIVATE_KEY, TEST_RPC_URL, sepolia); + expect(wallet.account).toBeDefined(); + expect(wallet.account.address).toMatch(/^0x[0-9a-fA-F]{40}$/); + }); + + it("accepts an optional timeout", () => { + const wallet = createWallet(TEST_PRIVATE_KEY, TEST_RPC_URL, sepolia, 60000); + expect(wallet.account).toBeDefined(); + }); +}); + +describe("createBrowserProvider", () => { + it("returns a PublicClient from an EIP-1193 provider", () => { + const mockEthereum = { + request: jest.fn(), + on: jest.fn(), + removeListener: jest.fn(), + } as unknown as EIP1193Provider; + + const provider = createBrowserProvider(mockEthereum, sepolia); + expect(provider).toBeDefined(); + expect(typeof provider.readContract).toBe("function"); + }); +}); + +describe("getSigner", () => { + it("returns a Wallet when accounts are available", async () => { + const mockEthereum = { + request: jest + .fn() + .mockResolvedValue(["0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"]), + on: jest.fn(), + removeListener: jest.fn(), + } as unknown as EIP1193Provider; + + const signer = await getSigner(mockEthereum, sepolia); + expect(signer).toBeDefined(); + expect(signer.account).toBeDefined(); + }); + + it("throws when no accounts are returned", async () => { + const mockEthereum = { + request: jest.fn().mockResolvedValue([]), + on: jest.fn(), + removeListener: jest.fn(), + } as unknown as EIP1193Provider; + + await expect(getSigner(mockEthereum, sepolia)).rejects.toThrow( + "No accounts found", + ); + }); + + it("throws when accounts is null", async () => { + const mockEthereum = { + request: jest.fn().mockResolvedValue(null), + on: jest.fn(), + removeListener: jest.fn(), + } as unknown as EIP1193Provider; + + await expect(getSigner(mockEthereum, sepolia)).rejects.toThrow( + "No accounts found", + ); + }); +}); diff --git a/packages/contracts/__tests__/unit/scripts.test.ts b/packages/contracts/__tests__/unit/scripts.test.ts new file mode 100644 index 00000000..d8c6f882 --- /dev/null +++ b/packages/contracts/__tests__/unit/scripts.test.ts @@ -0,0 +1,12 @@ +import { checkAbis } from "../../src/scripts/check-abis"; +import { generateAbis } from "../../src/scripts/generate-abis"; + +describe("script stubs", () => { + it("checkAbis throws TODO error", () => { + expect(() => checkAbis()).toThrow("TODO"); + }); + + it("generateAbis throws TODO error", () => { + expect(() => generateAbis()).toThrow("TODO"); + }); +}); diff --git a/packages/contracts/__tests__/unit/utils.test.ts b/packages/contracts/__tests__/unit/utils.test.ts new file mode 100644 index 00000000..97fa47c8 --- /dev/null +++ b/packages/contracts/__tests__/unit/utils.test.ts @@ -0,0 +1,141 @@ +import { requireAccount, requireSigner } from "../../src/utils/account"; +import { getChainFromId } from "../../src/utils/chain"; +import { keccak256, id } from "../../src/utils/hash"; +import { isHex, toHex } from "../../src/utils/hex"; +import { getCurrentTimestamp, addDays } from "../../src/utils/time"; +import type { WalletClient } from "../../src/lib"; + +describe("requireSigner", () => { + it("returns the walletClient when non-null", () => { + const wallet = { account: { address: "0x1234" } } as unknown as WalletClient; + expect(requireSigner(wallet)).toBe(wallet); + }); + + it("throws when walletClient is null", () => { + expect(() => requireSigner(null)).toThrow("No signer configured"); + }); +}); + +describe("requireAccount", () => { + it("returns the account when present", () => { + const account = { address: "0x1234" }; + const walletClient = { account } as unknown as WalletClient; + expect(requireAccount(walletClient)).toBe(account); + }); + + it("throws when account is undefined", () => { + const walletClient = {} as unknown as WalletClient; + expect(() => requireAccount(walletClient)).toThrow( + "WalletClient has no account attached", + ); + }); +}); + +describe("getChainFromId", () => { + it.each([ + [1, "Ethereum"], + [42220, "Celo"], + [11155111, "Sepolia"], + [5, "Goerli"], + [11142220, "Celo Sepolia"], + ])("returns predefined chain for id %d", (chainId, expectedName) => { + const chain = getChainFromId(chainId); + expect(chain.id).toBe(chainId); + expect(chain.name).toContain(expectedName); + }); + + it("returns a fallback chain for unknown ids", () => { + const chain = getChainFromId(999999); + expect(chain.id).toBe(999999); + expect(chain.name).toBe("Chain 999999"); + }); +}); + +describe("keccak256", () => { + it("hashes a plain string via stringToHex", () => { + const hash = keccak256("hello"); + expect(hash).toMatch(/^0x[0-9a-f]{64}$/); + }); + + it("hashes a 0x-prefixed hex string directly", () => { + const hash = keccak256("0xdeadbeef"); + expect(hash).toMatch(/^0x[0-9a-f]{64}$/); + }); + + it("hashes a Uint8Array", () => { + const hash = keccak256(new Uint8Array([1, 2, 3])); + expect(hash).toMatch(/^0x[0-9a-f]{64}$/); + }); +}); + +describe("id", () => { + it("returns a keccak256 hash of a UTF-8 string", () => { + const hash = id("bufferTime"); + expect(hash).toMatch(/^0x[0-9a-f]{64}$/); + }); +}); + +describe("isHex", () => { + it("returns true for valid hex strings", () => { + expect(isHex("0x")).toBe(true); + expect(isHex("0xabcdef0123456789")).toBe(true); + expect(isHex("0xABCDEF")).toBe(true); + }); + + it("returns false for strings without 0x prefix", () => { + expect(isHex("abcdef")).toBe(false); + }); + + it("returns false for strings with invalid hex chars", () => { + expect(isHex("0xZZZZ")).toBe(false); + }); + + it("returns false for empty string", () => { + expect(isHex("")).toBe(false); + }); +}); + +describe("toHex", () => { + it("encodes a number", () => { + expect(toHex(255)).toMatch(/^0x/); + }); + + it("encodes a bigint", () => { + expect(toHex(123n)).toMatch(/^0x/); + }); + + it("encodes a boolean", () => { + expect(toHex(true)).toMatch(/^0x/); + }); + + it("encodes a Uint8Array", () => { + expect(toHex(new Uint8Array([0xab, 0xcd]))).toBe("0xabcd"); + }); + + it("encodes a string", () => { + expect(toHex("hi")).toMatch(/^0x/); + }); + + it("accepts size option", () => { + const hex = toHex(1, { size: 32 }); + expect(hex.length).toBe(66); // 0x + 64 hex chars + }); +}); + +describe("getCurrentTimestamp", () => { + it("returns a bigint close to current time in seconds", () => { + const ts = getCurrentTimestamp(); + const now = BigInt(Math.floor(Date.now() / 1000)); + expect(ts).toBeGreaterThanOrEqual(now - 2n); + expect(ts).toBeLessThanOrEqual(now + 2n); + }); +}); + +describe("addDays", () => { + it("adds correct number of seconds", () => { + const base = 1000000n; + expect(addDays(base, 1)).toBe(base + 86400n); + expect(addDays(base, 7)).toBe(base + 604800n); + expect(addDays(base, 0)).toBe(base); + }); +}); diff --git a/packages/contracts/jest.config.js b/packages/contracts/jest.config.cjs similarity index 51% rename from packages/contracts/jest.config.js rename to packages/contracts/jest.config.cjs index 27aed3fd..5dd2b55a 100644 --- a/packages/contracts/jest.config.js +++ b/packages/contracts/jest.config.cjs @@ -2,9 +2,22 @@ module.exports = { preset: "ts-jest", testEnvironment: "node", testMatch: ["**/__tests__/**/*.test.ts"], + setupFiles: ["dotenv/config"], collectCoverageFrom: [ "src/**/*.{ts,tsx}", "!src/**/*.d.ts", + "!src/scripts/**", + "!src/contracts/*/abi.ts", + "!src/index.ts", + "!src/client/index.ts", + "!src/constants/index.ts", + "!src/contracts/index.ts", + "!src/errors/index.ts", + "!src/lib/index.ts", + "!src/lib/viem/index.ts", + "!src/metrics/index.ts", + "!src/utils/index.ts", + "!src/types/index.ts", ], coverageThreshold: { global: { diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 76a30e0c..40c55bf4 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -5,20 +5,54 @@ "publishConfig": { "access": "public" }, + "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./utils": { + "types": "./dist/utils/index.d.ts", + "import": "./dist/utils/index.js" + }, + "./contracts": { + "types": "./dist/contracts/index.d.ts", + "import": "./dist/contracts/index.js" + }, + "./client": { + "types": "./dist/client/index.d.ts", + "import": "./dist/client/index.js" + }, + "./errors": { + "types": "./dist/errors/index.d.ts", + "import": "./dist/errors/index.js" + }, + "./metrics": { + "types": "./dist/metrics/index.d.ts", + "import": "./dist/metrics/index.js" + } + }, "scripts": { - "build": "tsc", - "prepublishOnly": "npm run build", + "build": "tsup", + "prepublishOnly": "pnpm run build", "test": "jest --coverage", + "test:unit": "jest --testPathPatterns='__tests__/unit' --coverage", + "test:integration": "jest --testPathPatterns='__tests__/integration' --coverage --coverageThreshold='{}'", "test:watch": "jest --watchAll" }, + "dependencies": { + "viem": "^2.23.0" + }, "devDependencies": { "@types/jest": "^30.0.0", "@types/node": "^20.14.11", + "dotenv": "^17.3.1", "jest": "^30.0.5", "ts-jest": "^29.4.6", "ts-node": "^10.9.2", + "tsup": "^8.5.1", "typescript": "^5.5.4" }, "author": "oaknetwork", diff --git a/packages/contracts/src/client/create.ts b/packages/contracts/src/client/create.ts new file mode 100644 index 00000000..1e956d32 --- /dev/null +++ b/packages/contracts/src/client/create.ts @@ -0,0 +1,93 @@ +import type { Address, Hex } from "../lib"; +import type { + OakContractsClient, + OakContractsClientConfig, + PublicOakContractsClientConfig, + TransactionReceipt, + GlobalParamsEntity, + CampaignInfoFactoryEntity, + TreasuryFactoryEntity, + CampaignInfoEntity, + PaymentTreasuryEntity, + AllOrNothingTreasuryEntity, + KeepWhatsRaisedTreasuryEntity, + ItemRegistryEntity, +} from "./types"; +import { DEFAULT_CLIENT_OPTIONS, type OakContractsClientOptions, type EntitySignerOptions } from "./types"; +import { buildClients } from "./resolve"; +import { createGlobalParamsEntity } from "../contracts/global-params"; +import { createCampaignInfoFactoryEntity } from "../contracts/campaign-info-factory"; +import { createTreasuryFactoryEntity } from "../contracts/treasury-factory"; +import { createCampaignInfoEntity } from "../contracts/campaign-info"; +import { createPaymentTreasuryEntity } from "../contracts/payment-treasury"; +import { createAllOrNothingEntity } from "../contracts/all-or-nothing"; +import { createKeepWhatsRaisedEntity } from "../contracts/keep-whats-raised"; +import { createItemRegistryEntity } from "../contracts/item-registry"; + +/** + * Creates a new Oak Contracts SDK client instance. Reads config, calls buildClients + * from resolve, wires entity factories. No transport/chain/contract logic — only composition. + * + * @param config - Simple: `{ chainId, rpcUrl, privateKey }` or full: `{ chain, provider, signer }` + * @returns Configured OakContractsClient + */ +export function createOakContractsClient( + config: OakContractsClientConfig, +): OakContractsClient { + const options: OakContractsClientOptions = { + ...DEFAULT_CLIENT_OPTIONS, + ...config?.options, + }; + + const { chain, publicClient, walletClient } = buildClients(config, options); + const publicConfig: PublicOakContractsClientConfig = { chain }; + + async function waitForReceipt(txHash: Hex): Promise { + const receipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + timeout: options.timeout, + }); + return { + blockNumber: receipt.blockNumber, + gasUsed: receipt.gasUsed, + logs: receipt.logs.map((log) => ({ + topics: log.topics as readonly Hex[], + data: log.data, + })), + }; + } + + return { + config: publicConfig, + options, + publicClient, + walletClient, + waitForReceipt, + + globalParams(address: Address, options?: EntitySignerOptions): GlobalParamsEntity { + return createGlobalParamsEntity(address, publicClient, options?.signer ?? walletClient, chain); + }, + campaignInfoFactory(address: Address, options?: EntitySignerOptions): CampaignInfoFactoryEntity { + return createCampaignInfoFactoryEntity(address, publicClient, options?.signer ?? walletClient, chain); + }, + treasuryFactory(address: Address, options?: EntitySignerOptions): TreasuryFactoryEntity { + return createTreasuryFactoryEntity(address, publicClient, options?.signer ?? walletClient, chain); + }, + campaignInfo(address: Address, options?: EntitySignerOptions): CampaignInfoEntity { + return createCampaignInfoEntity(address, publicClient, options?.signer ?? walletClient, chain); + }, + /** @see {@link OakContractsClient.paymentTreasury} — supports both PaymentTreasury and TimeConstrainedPaymentTreasury. */ + paymentTreasury(address: Address, options?: EntitySignerOptions): PaymentTreasuryEntity { + return createPaymentTreasuryEntity(address, publicClient, options?.signer ?? walletClient, chain); + }, + allOrNothingTreasury(address: Address, options?: EntitySignerOptions): AllOrNothingTreasuryEntity { + return createAllOrNothingEntity(address, publicClient, options?.signer ?? walletClient, chain); + }, + keepWhatsRaisedTreasury(address: Address, options?: EntitySignerOptions): KeepWhatsRaisedTreasuryEntity { + return createKeepWhatsRaisedEntity(address, publicClient, options?.signer ?? walletClient, chain); + }, + itemRegistry(address: Address, options?: EntitySignerOptions): ItemRegistryEntity { + return createItemRegistryEntity(address, publicClient, options?.signer ?? walletClient, chain); + }, + }; +} diff --git a/packages/contracts/src/client/guard.ts b/packages/contracts/src/client/guard.ts new file mode 100644 index 00000000..2213e39a --- /dev/null +++ b/packages/contracts/src/client/guard.ts @@ -0,0 +1,82 @@ +import type { OakContractsClientConfig, SimpleOakContractsClientConfig, SimpleReadOnlyOakContractsClientConfig } from "./types"; + +/** + * Type guard that narrows an {@link OakContractsClientConfig} to {@link SimpleOakContractsClientConfig}. + * Detects simple config by the presence of chainId, rpcUrl, and privateKey fields. + * Throws immediately if the fields are present but privateKey is malformed, so the + * caller never silently falls through to the full-config branch with undefined clients. + * @param config - Client config to test + * @returns True if config is a valid SimpleOakContractsClientConfig; false if it is a full config + * @throws {Error} If simple-config fields are present but privateKey is not a valid 32-byte hex string + */ +export function isSimpleConfig( + config: OakContractsClientConfig, +): config is SimpleOakContractsClientConfig { + const hasSimpleFields = + "chainId" in config && + "rpcUrl" in config && + "privateKey" in config; + + if (!hasSimpleFields) { + return false; + } + + const simple = config as SimpleOakContractsClientConfig; + + if ( + typeof simple.chainId !== "number" || + typeof simple.rpcUrl !== "string" || + simple.rpcUrl.length === 0 + ) { + return false; + } + + if ( + typeof simple.privateKey !== "string" || + !/^0x[0-9a-fA-F]{64}$/.test(simple.privateKey) + ) { + throw new Error( + "Invalid privateKey: must be a 0x-prefixed 32-byte hex string (66 characters).", + ); + } + + return true; +} + +/** + * Type guard that narrows an {@link OakContractsClientConfig} to {@link SimpleReadOnlyOakContractsClientConfig}. + * Matches configs that have chainId + rpcUrl but no privateKey, chain, provider, or signer. + * Throws immediately if the shape matches but the field values are malformed, so callers + * never reach RPC calls with invalid transport/chain inputs. + * @param config - Client config to test + * @returns True if config is a valid read-only simple config (no signer) + * @throws {Error} If read-only simple-config fields are present but chainId is not a number or rpcUrl is empty + */ +export function isReadOnlySimpleConfig( + config: OakContractsClientConfig, +): config is SimpleReadOnlyOakContractsClientConfig { + const c = config as unknown as Record; + const hasReadOnlyFields = + "chainId" in config && + "rpcUrl" in config && + c["privateKey"] == null && + c["chain"] == null && + c["provider"] == null && + c["signer"] == null; + + if (!hasReadOnlyFields) { + return false; + } + + const simple = config as SimpleReadOnlyOakContractsClientConfig; + + if (typeof simple.chainId !== "number") { + throw new Error("Invalid chainId: must be a number."); + } + + if (typeof simple.rpcUrl !== "string" || simple.rpcUrl.length === 0) { + throw new Error("Invalid rpcUrl: must be a non-empty string."); + } + + return true; +} diff --git a/packages/contracts/src/client/index.ts b/packages/contracts/src/client/index.ts new file mode 100644 index 00000000..adbfb461 --- /dev/null +++ b/packages/contracts/src/client/index.ts @@ -0,0 +1,7 @@ +/** + * Public client surface. createOakContractsClient lives in create.ts; resolve, guard, + * transport, and account are used internally by create/resolve. + */ +export { createOakContractsClient } from "./create"; +export { isSimpleConfig } from "./guard"; +export type * from "./types"; diff --git a/packages/contracts/src/client/resolve.ts b/packages/contracts/src/client/resolve.ts new file mode 100644 index 00000000..af91983e --- /dev/null +++ b/packages/contracts/src/client/resolve.ts @@ -0,0 +1,57 @@ +import type { PublicClient, WalletClient } from "../lib"; +import type { Chain } from "../lib"; +import { createJsonRpcProvider, createWallet } from "../lib"; +import { isSimpleConfig, isReadOnlySimpleConfig } from "./guard"; +import { getChainFromId } from "../utils"; +import type { + OakContractsClientConfig, + OakContractsClientOptions, + FullOakContractsClientConfig, +} from "./types"; + +/** + * Resolves a chain identifier (number or Chain object) to a Chain object. + * @param chainIdOrChain - Numeric chain ID or viem Chain + * @returns Viem Chain + */ +function resolveChain(chainIdOrChain: number | Chain): Chain { + if (typeof chainIdOrChain === "number") { + return getChainFromId(chainIdOrChain); + } + return chainIdOrChain; +} + +/** + * Builds viem publicClient and walletClient from the given config. + * Delegates to lib/provider.ts for all viem client construction. + * No transport or account logic of its own. + * + * @param config - Simple or full client config + * @param options - Resolved client options (timeout etc.) + * @returns chain, publicClient, walletClient + */ +export function buildClients( + config: OakContractsClientConfig, + options: OakContractsClientOptions, +): { chain: Chain; publicClient: PublicClient; walletClient: WalletClient | null } { + if (isReadOnlySimpleConfig(config)) { + const chain = getChainFromId(config.chainId); + const publicClient = createJsonRpcProvider(config.rpcUrl, chain, options.timeout); + return { chain, publicClient, walletClient: null }; + } + + if (isSimpleConfig(config)) { + const chain = getChainFromId(config.chainId); + const publicClient = createJsonRpcProvider(config.rpcUrl, chain, options.timeout); + const walletClient = createWallet(config.privateKey, config.rpcUrl, chain, options.timeout); + return { chain, publicClient, walletClient }; + } + + const fullConfig = config as FullOakContractsClientConfig; + const chain = resolveChain(fullConfig.chain); + return { + chain, + publicClient: fullConfig.provider as PublicClient, + walletClient: fullConfig.signer as WalletClient, + }; +} diff --git a/packages/contracts/src/client/types.ts b/packages/contracts/src/client/types.ts new file mode 100644 index 00000000..b818d003 --- /dev/null +++ b/packages/contracts/src/client/types.ts @@ -0,0 +1,165 @@ +import type { Account, Address, Chain, Hex, PublicClient, WalletClient } from "../lib"; +import type { GlobalParamsEntity } from "../contracts/global-params/types"; +import type { CampaignInfoFactoryEntity } from "../contracts/campaign-info-factory/types"; +import type { TreasuryFactoryEntity } from "../contracts/treasury-factory/types"; +import type { CampaignInfoEntity } from "../contracts/campaign-info/types"; +import type { PaymentTreasuryEntity } from "../contracts/payment-treasury/types"; +import type { AllOrNothingTreasuryEntity } from "../contracts/all-or-nothing/types"; +import type { KeepWhatsRaisedTreasuryEntity } from "../contracts/keep-whats-raised/types"; +import type { ItemRegistryEntity } from "../contracts/item-registry/types"; + +/** Chain identifier — numeric chain ID or viem Chain. */ +export type ChainIdentifier = number | Chain; + +/** Provider — type alias for viem PublicClient. */ +export type JsonRpcProvider = PublicClient; + +/** Wallet — WalletClient with a guaranteed attached account. */ +export interface Wallet extends WalletClient { + account: Account; +} + +/** Read-only simple configuration: chainId + RPC URL, no private key. Writes and simulations will throw. */ +export interface SimpleReadOnlyOakContractsClientConfig { + /** Numeric chain ID (e.g. CHAIN_IDS.CELO_TESTNET_SEPOLIA). */ + chainId: number; + /** RPC URL for the chain. */ + rpcUrl: string; + /** Optional client-level overrides. */ + options?: Partial; +} + +/** Simple configuration: chainId + RPC URL + private key. */ +export interface SimpleOakContractsClientConfig { + /** Numeric chain ID (e.g. CHAIN_IDS.CELO_TESTNET_SEPOLIA). */ + chainId: number; + /** RPC URL for the chain. */ + rpcUrl: string; + /** 0x-prefixed private key for the signer account. */ + privateKey: `0x${string}`; + /** Optional client-level overrides. */ + options?: Partial; +} + +/** Full configuration: explicit viem chain, provider, and signer. */ +export interface FullOakContractsClientConfig { + /** Chain identifier (numeric ID or Chain object). */ + chain: ChainIdentifier; + /** PublicClient for on-chain reads. */ + provider: JsonRpcProvider; + /** WalletClient with account for sending transactions. */ + signer: Wallet; + /** Optional client-level overrides. */ + options?: Partial; +} + +/** Optional per-entity signer override passed as the second argument to entity factory methods. */ +export interface EntitySignerOptions { + /** WalletClient to use for writes and simulations on this entity. Overrides the client-level signer. */ + signer: WalletClient; +} + +/** Optional per-call signer override. When provided, overrides the entity-level and client-level signer for a single call. */ +export interface CallSignerOptions { + /** WalletClient to use for this specific call. Overrides the entity-level and client-level signer. */ + signer?: WalletClient; +} + +/** Union of all supported client configurations. */ +export type OakContractsClientConfig = + | SimpleReadOnlyOakContractsClientConfig + | SimpleOakContractsClientConfig + | FullOakContractsClientConfig; + +/** Public client configuration — contains no sensitive data. */ +export interface PublicOakContractsClientConfig { + /** The resolved viem Chain object. */ + chain: Chain; +} + +/** Client-level behavioural options. */ +export interface OakContractsClientOptions { + /** + * Request timeout in milliseconds applied to transport calls and waitForTransactionReceipt. + * @default 30000 + */ + timeout?: number; +} + +/** Default client options applied when none are provided. */ +export const DEFAULT_CLIENT_OPTIONS: OakContractsClientOptions = { + timeout: 30000, +}; + +/** Minimal transaction receipt returned by waitForReceipt. */ +export interface TransactionReceipt { + /** Block in which the transaction was mined. */ + blockNumber: bigint; + /** Total gas used by the transaction. */ + gasUsed: bigint; + /** Raw log entries (topics + data). */ + logs: readonly { topics: readonly Hex[]; data: Hex }[]; +} + +/** Re-exported entity types for SDK consumers. */ +export type { + GlobalParamsEntity, + CampaignInfoFactoryEntity, + TreasuryFactoryEntity, + CampaignInfoEntity, + PaymentTreasuryEntity, + AllOrNothingTreasuryEntity, + KeepWhatsRaisedTreasuryEntity, + ItemRegistryEntity, +}; + + +/** Oak Contracts SDK client; entity factories and receipt helper. */ +export interface OakContractsClient { + /** Public chain configuration (no secrets). */ + readonly config: PublicOakContractsClientConfig; + /** Resolved client options. */ + readonly options: OakContractsClientOptions; + /** Viem PublicClient for reads and receipt polling. */ + readonly publicClient: PublicClient; + /** Viem WalletClient for sending transactions. Null for read-only clients. */ + readonly walletClient: WalletClient | null; + /** + * Waits for a transaction to be mined and returns a minimal receipt. + * @param txHash - Transaction hash to wait for + * @returns TransactionReceipt with blockNumber, gasUsed, and logs + */ + waitForReceipt(txHash: Hex): Promise; + /** Returns a GlobalParams entity for the given contract address. */ + globalParams(address: Address, options?: EntitySignerOptions): GlobalParamsEntity; + /** Returns a CampaignInfoFactory entity for the given contract address. */ + campaignInfoFactory(address: Address, options?: EntitySignerOptions): CampaignInfoFactoryEntity; + /** Returns a TreasuryFactory entity for the given contract address. */ + treasuryFactory(address: Address, options?: EntitySignerOptions): TreasuryFactoryEntity; + /** Returns a CampaignInfo entity for the given contract address. */ + campaignInfo(address: Address, options?: EntitySignerOptions): CampaignInfoEntity; + /** + * Returns a PaymentTreasury entity for the given contract address. + * + * This method is compatible with **both** on-chain treasury variants: + * - **PaymentTreasury** — standard payment treasury with no time restrictions. + * - **TimeConstrainedPaymentTreasury** — payment treasury that enforces launch-time + * and deadline constraints on-chain (e.g. payments can only be created within the + * campaign window, refunds/withdrawals only after launch). + * + * Both contracts share the same ABI and the same SDK interface. Time enforcement + * is handled entirely on-chain, so no client-side configuration is needed — simply + * pass the deployed contract address regardless of which variant was deployed. + * + * @param address - Deployed PaymentTreasury or TimeConstrainedPaymentTreasury contract address + * @param options - Optional per-entity signer override + * @returns PaymentTreasuryEntity with read, write, simulate, and event methods + */ + paymentTreasury(address: Address, options?: EntitySignerOptions): PaymentTreasuryEntity; + /** Returns an AllOrNothing treasury entity for the given contract address. */ + allOrNothingTreasury(address: Address, options?: EntitySignerOptions): AllOrNothingTreasuryEntity; + /** Returns a KeepWhatsRaised treasury entity for the given contract address. */ + keepWhatsRaisedTreasury(address: Address, options?: EntitySignerOptions): KeepWhatsRaisedTreasuryEntity; + /** Returns an ItemRegistry entity for the given contract address. */ + itemRegistry(address: Address, options?: EntitySignerOptions): ItemRegistryEntity; +} diff --git a/packages/contracts/src/constants/chains.ts b/packages/contracts/src/constants/chains.ts new file mode 100644 index 00000000..0ff28338 --- /dev/null +++ b/packages/contracts/src/constants/chains.ts @@ -0,0 +1,11 @@ +/** Chain ID registry mapping human-readable network names to numeric chain IDs. */ +export const CHAIN_IDS = { + ETHEREUM_MAINNET: 1, + CELO_MAINNET: 42220, + ETHEREUM_TESTNET_SEPOLIA: 11155111, + ETHEREUM_TESTNET_GOERLI: 5, + CELO_TESTNET_SEPOLIA: 11142220, +} as const; + +/** Numeric chain ID; one of the values in {@link CHAIN_IDS}. */ +export type ChainId = (typeof CHAIN_IDS)[keyof typeof CHAIN_IDS]; diff --git a/packages/contracts/src/constants/encoding.ts b/packages/contracts/src/constants/encoding.ts new file mode 100644 index 00000000..5f856087 --- /dev/null +++ b/packages/contracts/src/constants/encoding.ts @@ -0,0 +1,4 @@ +import type { Hex } from "../lib"; + +/** Zero bytes32 sentinel value used as a default or unset marker in contract calls. */ +export const BYTES32_ZERO: Hex = `0x${"00".repeat(32)}`; diff --git a/packages/contracts/src/constants/fees.ts b/packages/contracts/src/constants/fees.ts new file mode 100644 index 00000000..34d392ca --- /dev/null +++ b/packages/contracts/src/constants/fees.ts @@ -0,0 +1,2 @@ +/** Basis-points denominator for fee calculations. 100% = 10_000 bps. */ +export const BPS_DENOMINATOR = 10_000n; diff --git a/packages/contracts/src/constants/index.ts b/packages/contracts/src/constants/index.ts new file mode 100644 index 00000000..868b4e91 --- /dev/null +++ b/packages/contracts/src/constants/index.ts @@ -0,0 +1,5 @@ +/** Re-exports chain IDs, fee constants, encoding sentinels, and registry helpers. */ +export { CHAIN_IDS, type ChainId } from "./chains"; +export { BPS_DENOMINATOR } from "./fees"; +export { BYTES32_ZERO } from "./encoding"; +export { DATA_REGISTRY_KEYS, scopedToPlatform, type DataRegistryKeyName } from "./registry"; diff --git a/packages/contracts/src/constants/registry.ts b/packages/contracts/src/constants/registry.ts new file mode 100644 index 00000000..76d65c91 --- /dev/null +++ b/packages/contracts/src/constants/registry.ts @@ -0,0 +1,30 @@ +import { keccak256, toHex, encodeAbiParameters, type Hex } from "../lib"; + +/** Registry keys matching DataRegistryKeys.sol — keccak256 of their string names. */ +export const DATA_REGISTRY_KEYS = { + BUFFER_TIME: keccak256(toHex("bufferTime")), + MAX_PAYMENT_EXPIRATION: keccak256(toHex("maxPaymentExpiration")), + CAMPAIGN_LAUNCH_BUFFER: keccak256(toHex("campaignLaunchBuffer")), + MINIMUM_CAMPAIGN_DURATION: keccak256(toHex("minimumCampaignDuration")), +} as const; + +/** Human-readable name of a data registry key in {@link DATA_REGISTRY_KEYS}. */ +export type DataRegistryKeyName = keyof typeof DATA_REGISTRY_KEYS; + +/** + * Computes a platform-scoped registry key from a base key and platform hash. + * Matches DataRegistryKeys.scopedToPlatform on-chain. + * @param baseKey - Base registry key (bytes32) + * @param platformHash - Platform hash (bytes32) + * @returns Scoped registry key (bytes32) + * + * @example + * ```typescript + * const platformHash = keccak256(toHex("my-platform")); + * const scopedKey = scopedToPlatform(DATA_REGISTRY_KEYS.BUFFER_TIME, platformHash); + * const value = await gp.getFromRegistry(scopedKey); + * ``` + */ +export function scopedToPlatform(baseKey: Hex, platformHash: Hex): Hex { + return keccak256(encodeAbiParameters([{ type: "bytes32" }, { type: "bytes32" }], [baseKey, platformHash])); +} diff --git a/packages/contracts/src/contracts/all-or-nothing/abi.ts b/packages/contracts/src/contracts/all-or-nothing/abi.ts new file mode 100644 index 00000000..5861c7e2 --- /dev/null +++ b/packages/contracts/src/contracts/all-or-nothing/abi.ts @@ -0,0 +1,451 @@ +const REWARD_TIER_COMPONENTS = [ + { internalType: "uint256", name: "rewardValue", type: "uint256" }, + { internalType: "bool", name: "isRewardTier", type: "bool" }, + { internalType: "bytes32[]", name: "itemId", type: "bytes32[]" }, + { internalType: "uint256[]", name: "itemValue", type: "uint256[]" }, + { internalType: "uint256[]", name: "itemQuantity", type: "uint256[]" }, +] as const; + +export const ALL_OR_NOTHING_ABI = [ + { inputs: [], name: "AccessCheckerUnauthorized", type: "error" }, + { inputs: [], name: "AllOrNothingFeeNotDisbursed", type: "error" }, + { inputs: [], name: "AllOrNothingInvalidInput", type: "error" }, + { + inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], + name: "AllOrNothingNotClaimable", + type: "error", + }, + { inputs: [], name: "AllOrNothingNotSuccessful", type: "error" }, + { inputs: [], name: "AllOrNothingRewardExists", type: "error" }, + { inputs: [], name: "AllOrNothingTransferFailed", type: "error" }, + { inputs: [], name: "AllOrNothingUnAuthorized", type: "error" }, + { + inputs: [ + { internalType: "uint256", name: "inputTime", type: "uint256" }, + { internalType: "uint256", name: "currentTime", type: "uint256" }, + ], + name: "CurrentTimeIsGreater", + type: "error", + }, + { + inputs: [ + { internalType: "uint256", name: "inputTime", type: "uint256" }, + { internalType: "uint256", name: "currentTime", type: "uint256" }, + ], + name: "CurrentTimeIsLess", + type: "error", + }, + { + inputs: [ + { internalType: "uint256", name: "initialTime", type: "uint256" }, + { internalType: "uint256", name: "finalTime", type: "uint256" }, + ], + name: "CurrentTimeIsNotWithinRange", + type: "error", + }, + { inputs: [], name: "AllOrNothingFeeAlreadyDisbursed", type: "error" }, + { + inputs: [{ internalType: "address", name: "token", type: "address" }], + name: "AllOrNothingTokenNotAccepted", + type: "error", + }, + { inputs: [], name: "TreasuryCampaignInfoIsPaused", type: "error" }, + { inputs: [], name: "TreasuryFeeNotDisbursed", type: "error" }, + { inputs: [], name: "TreasurySuccessConditionNotFulfilled", type: "error" }, + { inputs: [], name: "TreasuryTransferFailed", type: "error" }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "owner", type: "address" }, + { indexed: true, internalType: "address", name: "approved", type: "address" }, + { indexed: true, internalType: "uint256", name: "tokenId", type: "uint256" }, + ], + name: "Approval", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "owner", type: "address" }, + { indexed: true, internalType: "address", name: "operator", type: "address" }, + { indexed: false, internalType: "bool", name: "approved", type: "bool" }, + ], + name: "ApprovalForAll", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "token", type: "address" }, + { indexed: false, internalType: "uint256", name: "protocolShare", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "platformShare", type: "uint256" }, + ], + name: "FeesDisbursed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: "address", name: "account", type: "address" }, + { indexed: false, internalType: "bytes32", name: "message", type: "bytes32" }, + ], + name: "Paused", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "backer", type: "address" }, + { indexed: true, internalType: "address", name: "pledgeToken", type: "address" }, + { indexed: false, internalType: "bytes32", name: "reward", type: "bytes32" }, + { indexed: false, internalType: "uint256", name: "pledgeAmount", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "shippingFee", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "tokenId", type: "uint256" }, + { indexed: false, internalType: "bytes32[]", name: "rewards", type: "bytes32[]" }, + ], + name: "Receipt", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: "uint256", name: "tokenId", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "refundAmount", type: "uint256" }, + { indexed: false, internalType: "address", name: "claimer", type: "address" }, + ], + name: "RefundClaimed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: "bytes32[]", name: "rewardNames", type: "bytes32[]" }, + { + components: [...REWARD_TIER_COMPONENTS], + indexed: false, + internalType: "struct AllOrNothing.Reward[]", + name: "rewards", + type: "tuple[]", + }, + ], + name: "RewardsAdded", + type: "event", + }, + { + anonymous: false, + inputs: [{ indexed: true, internalType: "bytes32", name: "rewardName", type: "bytes32" }], + name: "RewardRemoved", + type: "event", + }, + { anonymous: false, inputs: [], name: "SuccessConditionNotFulfilled", type: "event" }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "from", type: "address" }, + { indexed: true, internalType: "address", name: "to", type: "address" }, + { indexed: true, internalType: "uint256", name: "tokenId", type: "uint256" }, + ], + name: "Transfer", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: "address", name: "account", type: "address" }, + { indexed: false, internalType: "bytes32", name: "message", type: "bytes32" }, + ], + name: "Unpaused", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "token", type: "address" }, + { indexed: false, internalType: "address", name: "to", type: "address" }, + { indexed: false, internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "WithdrawalSuccessful", + type: "event", + }, + { + inputs: [{ internalType: "bytes32", name: "message", type: "bytes32" }], + name: "pauseTreasury", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "message", type: "bytes32" }], + name: "unpauseTreasury", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "message", type: "bytes32" }], + name: "cancelTreasury", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "cancelled", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32[]", name: "rewardNames", type: "bytes32[]" }, + { + components: [...REWARD_TIER_COMPONENTS], + internalType: "struct AllOrNothing.Reward[]", + name: "rewards", + type: "tuple[]", + }, + ], + name: "addRewards", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "_platformHash", type: "bytes32" }, + { internalType: "address", name: "_infoAddress", type: "address" }, + { internalType: "address", name: "_trustedForwarder", type: "address" }, + ], + name: "initialize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "tokenId", type: "uint256" }, + ], + name: "approve", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "owner", type: "address" }], + name: "balanceOf", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], + name: "burn", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], + name: "claimRefund", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "disburseFees", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], + name: "getApproved", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getLifetimeRaisedAmount", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getRaisedAmount", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "rewardName", type: "bytes32" }], + name: "getReward", + outputs: [ + { + components: REWARD_TIER_COMPONENTS, + internalType: "struct AllOrNothing.Reward", + name: "reward", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getPlatformHash", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getRefundedAmount", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getPlatformFeePercent", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "owner", type: "address" }, + { internalType: "address", name: "operator", type: "address" }, + ], + name: "isApprovedForAll", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "name", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], + name: "ownerOf", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "paused", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "backer", type: "address" }, + { internalType: "address", name: "pledgeToken", type: "address" }, + { internalType: "uint256", name: "shippingFee", type: "uint256" }, + { internalType: "bytes32[]", name: "rewardNames", type: "bytes32[]" }, + ], + name: "pledgeForAReward", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "backer", type: "address" }, + { internalType: "address", name: "pledgeToken", type: "address" }, + { internalType: "uint256", name: "pledgeAmount", type: "uint256" }, + ], + name: "pledgeWithoutAReward", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "rewardName", type: "bytes32" }], + name: "removeReward", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "from", type: "address" }, + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "tokenId", type: "uint256" }, + ], + name: "safeTransferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "from", type: "address" }, + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "tokenId", type: "uint256" }, + { internalType: "bytes", name: "data", type: "bytes" }, + ], + name: "safeTransferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "operator", type: "address" }, + { internalType: "bool", name: "approved", type: "bool" }, + ], + name: "setApprovalForAll", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes4", name: "interfaceId", type: "bytes4" }], + name: "supportsInterface", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "symbol", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], + name: "tokenURI", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "from", type: "address" }, + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "tokenId", type: "uint256" }, + ], + name: "transferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "withdraw", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; diff --git a/packages/contracts/src/contracts/all-or-nothing/events.ts b/packages/contracts/src/contracts/all-or-nothing/events.ts new file mode 100644 index 00000000..1330a58e --- /dev/null +++ b/packages/contracts/src/contracts/all-or-nothing/events.ts @@ -0,0 +1,18 @@ +import type { Address, PublicClient } from "../../lib"; +import type { AllOrNothingEvents } from "./types"; + +// TODO: Add event filter factories (filterPledgeForAReward, filterWithdrawn), log decoder (decodeLog), +// and watcher factories using getLogs / watchEvent. + +/** + * Builds event helpers for an AllOrNothing treasury contract instance. + * @param _address - Deployed AllOrNothing contract address + * @param _publicClient - Viem PublicClient used to call getLogs + * @returns Event helpers bound to the given contract address + */ +export function createAllOrNothingEvents( + _address: Address, + _publicClient: PublicClient, +): AllOrNothingEvents { + return {}; +} diff --git a/packages/contracts/src/contracts/all-or-nothing/index.ts b/packages/contracts/src/contracts/all-or-nothing/index.ts new file mode 100644 index 00000000..6dbfd59b --- /dev/null +++ b/packages/contracts/src/contracts/all-or-nothing/index.ts @@ -0,0 +1,30 @@ +import type { Address, PublicClient, WalletClient, Chain } from "../../lib"; +import { createAllOrNothingReads } from "./reads"; +import { createAllOrNothingWrites } from "./writes"; +import { createAllOrNothingSimulate } from "./simulate"; +import { createAllOrNothingEvents } from "./events"; +import type { AllOrNothingTreasuryEntity } from "./types"; + +/** + * Creates a fully composed AllOrNothing treasury entity combining reads, writes, simulate, and events. + * @param address - Deployed AllOrNothing contract address + * @param publicClient - Viem PublicClient used for reads and simulation + * @param walletClient - Viem WalletClient used for writes; must have an account attached + * @param chain - Chain passed to writeContract and simulateContract + * @returns Composed entity exposing all AllOrNothing methods under a single object + */ +export function createAllOrNothingEntity( + address: Address, + publicClient: PublicClient, + walletClient: WalletClient | null, + chain: Chain, +): AllOrNothingTreasuryEntity { + return { + ...createAllOrNothingReads(address, publicClient), + ...createAllOrNothingWrites(address, walletClient, chain), + simulate: createAllOrNothingSimulate(address, publicClient, walletClient, chain), + events: createAllOrNothingEvents(address, publicClient), + }; +} + +export type { AllOrNothingTreasuryEntity } from "./types"; diff --git a/packages/contracts/src/contracts/all-or-nothing/reads.ts b/packages/contracts/src/contracts/all-or-nothing/reads.ts new file mode 100644 index 00000000..9e7acfa8 --- /dev/null +++ b/packages/contracts/src/contracts/all-or-nothing/reads.ts @@ -0,0 +1,69 @@ +import type { Address, Hex, PublicClient } from "../../lib"; +import { ALL_OR_NOTHING_ABI } from "./abi"; +import type { AllOrNothingReads } from "./types"; +import type { TieredReward } from "../../types/structs"; + +/** + * Builds read methods for an AllOrNothing treasury contract instance. + * @param address - Deployed AllOrNothing contract address + * @param publicClient - Viem PublicClient used to call readContract + * @returns Read methods bound to the given contract address + */ +export function createAllOrNothingReads( + address: Address, + publicClient: PublicClient, +): AllOrNothingReads { + const contract = { address, abi: ALL_OR_NOTHING_ABI } as const; + + return { + async getRaisedAmount() { + return publicClient.readContract({ ...contract, functionName: "getRaisedAmount" }); + }, + async getLifetimeRaisedAmount() { + return publicClient.readContract({ ...contract, functionName: "getLifetimeRaisedAmount" }); + }, + async getRefundedAmount() { + return publicClient.readContract({ ...contract, functionName: "getRefundedAmount" }); + }, + async getReward(rewardName: Hex): Promise { + const result = await publicClient.readContract({ ...contract, functionName: "getReward", args: [rewardName] }); + return result as unknown as TieredReward; + }, + async getPlatformHash() { + return publicClient.readContract({ ...contract, functionName: "getPlatformHash" }); + }, + async getPlatformFeePercent() { + return publicClient.readContract({ ...contract, functionName: "getPlatformFeePercent" }); + }, + async paused() { + return publicClient.readContract({ ...contract, functionName: "paused" }); + }, + async cancelled() { + return publicClient.readContract({ ...contract, functionName: "cancelled" }); + }, + async balanceOf(owner: Address) { + return publicClient.readContract({ ...contract, functionName: "balanceOf", args: [owner] }); + }, + async ownerOf(tokenId: bigint) { + return publicClient.readContract({ ...contract, functionName: "ownerOf", args: [tokenId] }); + }, + async tokenURI(tokenId: bigint) { + return publicClient.readContract({ ...contract, functionName: "tokenURI", args: [tokenId] }); + }, + async name() { + return publicClient.readContract({ ...contract, functionName: "name" }); + }, + async symbol() { + return publicClient.readContract({ ...contract, functionName: "symbol" }); + }, + async getApproved(tokenId: bigint) { + return publicClient.readContract({ ...contract, functionName: "getApproved", args: [tokenId] }); + }, + async isApprovedForAll(owner: Address, operator: Address) { + return publicClient.readContract({ ...contract, functionName: "isApprovedForAll", args: [owner, operator] }); + }, + async supportsInterface(interfaceId: Hex) { + return publicClient.readContract({ ...contract, functionName: "supportsInterface", args: [interfaceId] }); + }, + }; +} diff --git a/packages/contracts/src/contracts/all-or-nothing/simulate.ts b/packages/contracts/src/contracts/all-or-nothing/simulate.ts new file mode 100644 index 00000000..762f1286 --- /dev/null +++ b/packages/contracts/src/contracts/all-or-nothing/simulate.ts @@ -0,0 +1,209 @@ +import type { Address, Hex, PublicClient, WalletClient, Chain } from "../../lib"; +import { ALL_OR_NOTHING_ABI } from "./abi"; +import { requireSigner, requireAccount } from "../../utils/account"; +import { simulateWithErrorDecode } from "../../errors"; +import type { AllOrNothingSimulate } from "./types"; +import type { TieredReward } from "../../types/structs"; +import type { CallSignerOptions } from "../../client/types"; + +/** + * Builds simulate methods for AllOrNothing write calls. + * Each method calls simulateContract against the current chain state and throws a typed + * SDK error on revert, decoded via simulateWithErrorDecode. + * @param address - Deployed AllOrNothing contract address + * @param publicClient - Viem PublicClient used to call simulateContract + * @param walletClient - Viem WalletClient used to resolve the account for simulation + * @param chain - Chain passed to simulateContract + * @returns Simulation methods bound to the given contract address + */ +export function createAllOrNothingSimulate( + address: Address, + publicClient: PublicClient, + walletClient: WalletClient | null, + chain: Chain, +): AllOrNothingSimulate { + const contract = { address, abi: ALL_OR_NOTHING_ABI } as const; + + return { + async pauseTreasury(message: Hex, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "pauseTreasury", + args: [message], + }), + ); + }, + async unpauseTreasury(message: Hex, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "unpauseTreasury", + args: [message], + }), + ); + }, + async cancelTreasury(message: Hex, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "cancelTreasury", + args: [message], + }), + ); + }, + async addRewards(rewardNames: readonly Hex[], rewards: readonly TieredReward[], options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "addRewards", + args: [[...rewardNames], [...rewards]], + }), + ); + }, + async removeReward(rewardName: Hex, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "removeReward", + args: [rewardName], + }), + ); + }, + async pledgeForAReward(backer: Address, pledgeToken: Address, shippingFee: bigint, rewardNames: readonly Hex[], options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "pledgeForAReward", + args: [backer, pledgeToken, shippingFee, [...rewardNames]], + }), + ); + }, + async pledgeWithoutAReward(backer: Address, pledgeToken: Address, pledgeAmount: bigint, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "pledgeWithoutAReward", + args: [backer, pledgeToken, pledgeAmount], + }), + ); + }, + async claimRefund(tokenId: bigint, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "claimRefund", + args: [tokenId], + }), + ); + }, + async disburseFees(options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "disburseFees", + args: [], + }), + ); + }, + async withdraw(options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "withdraw", + args: [], + }), + ); + }, + async burn(tokenId: bigint, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "burn", + args: [tokenId], + }), + ); + }, + async approve(to: Address, tokenId: bigint, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "approve", + args: [to, tokenId], + }), + ); + }, + async setApprovalForAll(operator: Address, approved: boolean, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "setApprovalForAll", + args: [operator, approved], + }), + ); + }, + async safeTransferFrom(from: Address, to: Address, tokenId: bigint, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "safeTransferFrom", + args: [from, to, tokenId], + }), + ); + }, + async transferFrom(from: Address, to: Address, tokenId: bigint, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "transferFrom", + args: [from, to, tokenId], + }), + ); + }, + }; +} diff --git a/packages/contracts/src/contracts/all-or-nothing/types.ts b/packages/contracts/src/contracts/all-or-nothing/types.ts new file mode 100644 index 00000000..52c0ce19 --- /dev/null +++ b/packages/contracts/src/contracts/all-or-nothing/types.ts @@ -0,0 +1,118 @@ +import type { Address, Hex } from "../../lib"; +import type { TieredReward } from "../../types/structs"; +import type { CallSignerOptions } from "../../client/types"; + +/** Read-only methods for an AllOrNothing treasury contract instance. */ +export interface AllOrNothingReads { + /** Returns the current raised amount (total pledges minus refunds). */ + getRaisedAmount(): Promise; + /** Returns the lifetime raised amount including refunded pledges. */ + getLifetimeRaisedAmount(): Promise; + /** Returns the total amount refunded to backers. */ + getRefundedAmount(): Promise; + /** Returns the reward configuration for a given reward name. */ + getReward(rewardName: Hex): Promise; + /** Returns the bytes32 platform hash for this treasury. */ + getPlatformHash(): Promise; + /** Returns the platform fee percent in basis points. */ + getPlatformFeePercent(): Promise; + /** Returns true if the treasury is paused. */ + paused(): Promise; + /** Returns true if the treasury has been cancelled. */ + cancelled(): Promise; + /** Returns the NFT balance for the given owner address. */ + balanceOf(owner: Address): Promise; + /** Returns the owner of a pledge NFT by token ID. */ + ownerOf(tokenId: bigint): Promise
; + /** Returns the token URI for a pledge NFT. */ + tokenURI(tokenId: bigint): Promise; + /** Returns the ERC-721 collection name. */ + name(): Promise; + /** Returns the ERC-721 collection symbol. */ + symbol(): Promise; + /** Returns the approved address for a token ID. */ + getApproved(tokenId: bigint): Promise
; + /** Returns true if operator is approved for all tokens of owner. */ + isApprovedForAll(owner: Address, operator: Address): Promise; + /** Returns true if the contract implements the given ERC-165 interface. */ + supportsInterface(interfaceId: Hex): Promise; +} + +/** Write methods for an AllOrNothing treasury contract instance. */ +export interface AllOrNothingWrites { + /** Pauses the treasury. */ + pauseTreasury(message: Hex, options?: CallSignerOptions): Promise; + /** Unpauses the treasury. */ + unpauseTreasury(message: Hex, options?: CallSignerOptions): Promise; + /** Cancels the treasury permanently. */ + cancelTreasury(message: Hex, options?: CallSignerOptions): Promise; + /** Adds one or more reward tiers. */ + addRewards(rewardNames: readonly Hex[], rewards: readonly TieredReward[], options?: CallSignerOptions): Promise; + /** Removes a reward tier by name. */ + removeReward(rewardName: Hex, options?: CallSignerOptions): Promise; + /** Pledges for a reward; mints a pledge NFT. */ + pledgeForAReward(backer: Address, pledgeToken: Address, shippingFee: bigint, rewardNames: readonly Hex[], options?: CallSignerOptions): Promise; + /** Pledges without selecting a reward tier. */ + pledgeWithoutAReward(backer: Address, pledgeToken: Address, pledgeAmount: bigint, options?: CallSignerOptions): Promise; + /** Claims a refund for a pledge NFT (campaign did not reach goal). */ + claimRefund(tokenId: bigint, options?: CallSignerOptions): Promise; + /** Disburses accumulated fees to protocol and platform. */ + disburseFees(options?: CallSignerOptions): Promise; + /** Withdraws raised funds (campaign succeeded). */ + withdraw(options?: CallSignerOptions): Promise; + /** Burns a pledge NFT. */ + burn(tokenId: bigint, options?: CallSignerOptions): Promise; + /** Approves an address to transfer a specific pledge NFT. */ + approve(to: Address, tokenId: bigint, options?: CallSignerOptions): Promise; + /** Sets or revokes operator approval for all tokens. */ + setApprovalForAll(operator: Address, approved: boolean, options?: CallSignerOptions): Promise; + /** Safely transfers a pledge NFT. */ + safeTransferFrom(from: Address, to: Address, tokenId: bigint, options?: CallSignerOptions): Promise; + /** Transfers a pledge NFT without ERC-721 receiver check. */ + transferFrom(from: Address, to: Address, tokenId: bigint, options?: CallSignerOptions): Promise; +} + +/** Simulate counterparts for AllOrNothing write methods. */ +export interface AllOrNothingSimulate { + /** Simulates pauseTreasury; throws a typed error on revert. */ + pauseTreasury(message: Hex, options?: CallSignerOptions): Promise; + /** Simulates unpauseTreasury; throws a typed error on revert. */ + unpauseTreasury(message: Hex, options?: CallSignerOptions): Promise; + /** Simulates cancelTreasury; throws a typed error on revert. */ + cancelTreasury(message: Hex, options?: CallSignerOptions): Promise; + /** Simulates addRewards; throws a typed error on revert. */ + addRewards(rewardNames: readonly Hex[], rewards: readonly TieredReward[], options?: CallSignerOptions): Promise; + /** Simulates removeReward; throws a typed error on revert. */ + removeReward(rewardName: Hex, options?: CallSignerOptions): Promise; + /** Simulates pledgeForAReward; throws a typed error on revert. */ + pledgeForAReward(backer: Address, pledgeToken: Address, shippingFee: bigint, rewardNames: readonly Hex[], options?: CallSignerOptions): Promise; + /** Simulates pledgeWithoutAReward; throws a typed error on revert. */ + pledgeWithoutAReward(backer: Address, pledgeToken: Address, pledgeAmount: bigint, options?: CallSignerOptions): Promise; + /** Simulates claimRefund; throws a typed error on revert. */ + claimRefund(tokenId: bigint, options?: CallSignerOptions): Promise; + /** Simulates disburseFees; throws a typed error on revert. */ + disburseFees(options?: CallSignerOptions): Promise; + /** Simulates withdraw; throws a typed error on revert. */ + withdraw(options?: CallSignerOptions): Promise; + /** Simulates burn; throws a typed error on revert. */ + burn(tokenId: bigint, options?: CallSignerOptions): Promise; + /** Simulates approve; throws a typed error on revert. */ + approve(to: Address, tokenId: bigint, options?: CallSignerOptions): Promise; + /** Simulates setApprovalForAll; throws a typed error on revert. */ + setApprovalForAll(operator: Address, approved: boolean, options?: CallSignerOptions): Promise; + /** Simulates safeTransferFrom; throws a typed error on revert. */ + safeTransferFrom(from: Address, to: Address, tokenId: bigint, options?: CallSignerOptions): Promise; + /** Simulates transferFrom; throws a typed error on revert. */ + transferFrom(from: Address, to: Address, tokenId: bigint, options?: CallSignerOptions): Promise; +} + +/** Event helpers for an AllOrNothing treasury contract instance. */ +export interface AllOrNothingEvents {} + +/** Full AllOrNothing treasury entity combining reads, writes, simulate, and events. */ +export type AllOrNothingTreasuryEntity = AllOrNothingReads & AllOrNothingWrites & { + /** Simulation counterparts for every write method. */ + simulate: AllOrNothingSimulate; + /** Event helpers for filtering and watching logs. */ + events: AllOrNothingEvents; +}; diff --git a/packages/contracts/src/contracts/all-or-nothing/writes.ts b/packages/contracts/src/contracts/all-or-nothing/writes.ts new file mode 100644 index 00000000..e9467ed5 --- /dev/null +++ b/packages/contracts/src/contracts/all-or-nothing/writes.ts @@ -0,0 +1,84 @@ +import type { Address, Hex, WalletClient, Chain } from "../../lib"; +import { ALL_OR_NOTHING_ABI } from "./abi"; +import { requireSigner, requireAccount } from "../../utils/account"; +import type { AllOrNothingWrites } from "./types"; +import type { TieredReward } from "../../types/structs"; +import type { CallSignerOptions } from "../../client/types"; + +/** + * Builds write methods for an AllOrNothing treasury contract instance. + * @param address - Deployed AllOrNothing contract address + * @param walletClient - Viem WalletClient used to call writeContract; must have an account attached + * @param chain - Chain passed to writeContract for EIP-1559 and replay protection + * @returns Write methods bound to the given contract address + */ +export function createAllOrNothingWrites( + address: Address, + walletClient: WalletClient | null, + chain: Chain, +): AllOrNothingWrites { + const contract = { address, abi: ALL_OR_NOTHING_ABI } as const; + + return { + async pauseTreasury(message: Hex, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "pauseTreasury", args: [message] }); + }, + async unpauseTreasury(message: Hex, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "unpauseTreasury", args: [message] }); + }, + async cancelTreasury(message: Hex, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "cancelTreasury", args: [message] }); + }, + async addRewards(rewardNames: readonly Hex[], rewards: readonly TieredReward[], options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "addRewards", args: [[...rewardNames], [...rewards]] }); + }, + async removeReward(rewardName: Hex, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "removeReward", args: [rewardName] }); + }, + async pledgeForAReward(backer: Address, pledgeToken: Address, shippingFee: bigint, rewardNames: readonly Hex[], options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "pledgeForAReward", args: [backer, pledgeToken, shippingFee, [...rewardNames]] }); + }, + async pledgeWithoutAReward(backer: Address, pledgeToken: Address, pledgeAmount: bigint, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "pledgeWithoutAReward", args: [backer, pledgeToken, pledgeAmount] }); + }, + async claimRefund(tokenId: bigint, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "claimRefund", args: [tokenId] }); + }, + async disburseFees(options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "disburseFees", args: [] }); + }, + async withdraw(options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "withdraw", args: [] }); + }, + async burn(tokenId: bigint, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "burn", args: [tokenId] }); + }, + async approve(to: Address, tokenId: bigint, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "approve", args: [to, tokenId] }); + }, + async setApprovalForAll(operator: Address, approved: boolean, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "setApprovalForAll", args: [operator, approved] }); + }, + async safeTransferFrom(from: Address, to: Address, tokenId: bigint, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "safeTransferFrom", args: [from, to, tokenId] }); + }, + async transferFrom(from: Address, to: Address, tokenId: bigint, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "transferFrom", args: [from, to, tokenId] }); + }, + }; +} diff --git a/packages/contracts/src/contracts/campaign-info-factory/abi.ts b/packages/contracts/src/contracts/campaign-info-factory/abi.ts new file mode 100644 index 00000000..2426c015 --- /dev/null +++ b/packages/contracts/src/contracts/campaign-info-factory/abi.ts @@ -0,0 +1,126 @@ +const CAMPAIGN_DATA_COMPONENTS = [ + { internalType: "uint256", name: "launchTime", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + { internalType: "uint256", name: "goalAmount", type: "uint256" }, + { internalType: "bytes32", name: "currency", type: "bytes32" }, +] as const; + +export const CAMPAIGN_INFO_FACTORY_ABI = [ + { inputs: [], name: "CampaignInfoFactoryInvalidInput", type: "error" }, + { inputs: [], name: "CampaignInfoFactoryCampaignInitializationFailed", type: "error" }, + { + inputs: [{ internalType: "bytes32", name: "platformHash", type: "bytes32" }], + name: "CampaignInfoFactoryPlatformNotListed", + type: "error", + }, + { + inputs: [ + { internalType: "bytes32", name: "identifierHash", type: "bytes32" }, + { internalType: "address", name: "cloneExists", type: "address" }, + ], + name: "CampaignInfoFactoryCampaignWithSameIdentifierExists", + type: "error", + }, + { inputs: [], name: "CampaignInfoInvalidTokenList", type: "error" }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "identifierHash", type: "bytes32" }, + { indexed: true, internalType: "address", name: "campaignInfoAddress", type: "address" }, + ], + name: "CampaignInfoFactoryCampaignCreated", + type: "event", + }, + { + anonymous: false, + inputs: [], + name: "CampaignInfoFactoryCampaignInitialized", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "previousOwner", type: "address" }, + { indexed: true, internalType: "address", name: "newOwner", type: "address" }, + ], + name: "OwnershipTransferred", + type: "event", + }, + { + inputs: [ + { internalType: "address", name: "creator", type: "address" }, + { internalType: "bytes32", name: "identifierHash", type: "bytes32" }, + { internalType: "bytes32[]", name: "selectedPlatformHash", type: "bytes32[]" }, + { internalType: "bytes32[]", name: "platformDataKey", type: "bytes32[]" }, + { internalType: "bytes32[]", name: "platformDataValue", type: "bytes32[]" }, + { + components: [...CAMPAIGN_DATA_COMPONENTS], + internalType: "struct ICampaignData.CampaignData", + name: "campaignData", + type: "tuple", + }, + { internalType: "string", name: "nftName", type: "string" }, + { internalType: "string", name: "nftSymbol", type: "string" }, + { internalType: "string", name: "nftImageURI", type: "string" }, + { internalType: "string", name: "contractURI", type: "string" }, + ], + name: "createCampaign", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "identifierHash", type: "bytes32" }], + name: "identifierToCampaignInfo", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "initialOwner", type: "address" }, + { internalType: "contract IGlobalParams", name: "globalParams", type: "address" }, + { internalType: "address", name: "campaignImplementation", type: "address" }, + { internalType: "address", name: "treasuryFactoryAddress", type: "address" }, + ], + name: "initialize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "campaignInfo", type: "address" }], + name: "isValidCampaignInfo", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "owner", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "renounceOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "newOwner", type: "address" }], + name: "transferOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "newImplementation", type: "address" }], + name: "updateImplementation", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; diff --git a/packages/contracts/src/contracts/campaign-info-factory/events.ts b/packages/contracts/src/contracts/campaign-info-factory/events.ts new file mode 100644 index 00000000..2dbe101a --- /dev/null +++ b/packages/contracts/src/contracts/campaign-info-factory/events.ts @@ -0,0 +1,18 @@ +import type { Address, PublicClient } from "../../lib"; +import type { CampaignInfoFactoryEvents } from "./types"; + +// TODO: Add event filter factories (filterCampaignCreated), log decoder (decodeLog), +// and watcher factories using getLogs / watchEvent. + +/** + * Builds event helpers for a CampaignInfoFactory contract instance. + * @param _address - Deployed CampaignInfoFactory contract address + * @param _publicClient - Viem PublicClient used to call getLogs + * @returns Event helpers bound to the given contract address + */ +export function createCampaignInfoFactoryEvents( + _address: Address, + _publicClient: PublicClient, +): CampaignInfoFactoryEvents { + return {}; +} diff --git a/packages/contracts/src/contracts/campaign-info-factory/index.ts b/packages/contracts/src/contracts/campaign-info-factory/index.ts new file mode 100644 index 00000000..be19f7df --- /dev/null +++ b/packages/contracts/src/contracts/campaign-info-factory/index.ts @@ -0,0 +1,30 @@ +import type { Address, PublicClient, WalletClient, Chain } from "../../lib"; +import { createCampaignInfoFactoryReads } from "./reads"; +import { createCampaignInfoFactoryWrites } from "./writes"; +import { createCampaignInfoFactorySimulate } from "./simulate"; +import { createCampaignInfoFactoryEvents } from "./events"; +import type { CampaignInfoFactoryEntity } from "./types"; + +/** + * Creates a fully composed CampaignInfoFactory entity combining reads, writes, simulate, and events. + * @param address - Deployed CampaignInfoFactory contract address + * @param publicClient - Viem PublicClient used for reads and simulation + * @param walletClient - Viem WalletClient used for writes; must have an account attached + * @param chain - Chain passed to writeContract and simulateContract + * @returns Composed entity exposing all CampaignInfoFactory methods under a single object + */ +export function createCampaignInfoFactoryEntity( + address: Address, + publicClient: PublicClient, + walletClient: WalletClient | null, + chain: Chain, +): CampaignInfoFactoryEntity { + return { + ...createCampaignInfoFactoryReads(address, publicClient), + ...createCampaignInfoFactoryWrites(address, walletClient, chain), + simulate: createCampaignInfoFactorySimulate(address, publicClient, walletClient, chain), + events: createCampaignInfoFactoryEvents(address, publicClient), + }; +} + +export type { CampaignInfoFactoryEntity } from "./types"; diff --git a/packages/contracts/src/contracts/campaign-info-factory/reads.ts b/packages/contracts/src/contracts/campaign-info-factory/reads.ts new file mode 100644 index 00000000..3424e065 --- /dev/null +++ b/packages/contracts/src/contracts/campaign-info-factory/reads.ts @@ -0,0 +1,28 @@ +import type { Address, Hex, PublicClient } from "../../lib"; +import { CAMPAIGN_INFO_FACTORY_ABI } from "./abi"; +import type { CampaignInfoFactoryReads } from "./types"; + +/** + * Builds read methods for a CampaignInfoFactory contract instance. + * @param address - Deployed CampaignInfoFactory contract address + * @param publicClient - Viem PublicClient used to call readContract + * @returns Read methods bound to the given contract address + */ +export function createCampaignInfoFactoryReads( + address: Address, + publicClient: PublicClient, +): CampaignInfoFactoryReads { + const contract = { address, abi: CAMPAIGN_INFO_FACTORY_ABI } as const; + + return { + async identifierToCampaignInfo(identifierHash: Hex): Promise
{ + return publicClient.readContract({ ...contract, functionName: "identifierToCampaignInfo", args: [identifierHash] }); + }, + async isValidCampaignInfo(campaignInfo: Address): Promise { + return publicClient.readContract({ ...contract, functionName: "isValidCampaignInfo", args: [campaignInfo] }); + }, + async owner(): Promise
{ + return publicClient.readContract({ ...contract, functionName: "owner" }); + }, + }; +} diff --git a/packages/contracts/src/contracts/campaign-info-factory/simulate.ts b/packages/contracts/src/contracts/campaign-info-factory/simulate.ts new file mode 100644 index 00000000..81fee8e7 --- /dev/null +++ b/packages/contracts/src/contracts/campaign-info-factory/simulate.ts @@ -0,0 +1,94 @@ +import type { Address, Hex, PublicClient, WalletClient, Chain } from "../../lib"; +import { CAMPAIGN_INFO_FACTORY_ABI } from "./abi"; +import { requireSigner, requireAccount } from "../../utils/account"; +import { simulateWithErrorDecode } from "../../errors"; +import type { CampaignInfoFactorySimulate } from "./types"; +import type { CreateCampaignParams } from "../../types/params"; +import type { CallSignerOptions } from "../../client/types"; + +/** + * Builds simulate methods for CampaignInfoFactory write calls. + * Each method calls simulateContract against the current chain state and throws a typed + * SDK error on revert, decoded via simulateWithErrorDecode. + * @param address - Deployed CampaignInfoFactory contract address + * @param publicClient - Viem PublicClient used to call simulateContract + * @param walletClient - Viem WalletClient used to resolve the account for simulation + * @param chain - Chain passed to simulateContract + * @returns Simulation methods bound to the given contract address + */ +export function createCampaignInfoFactorySimulate( + address: Address, + publicClient: PublicClient, + walletClient: WalletClient | null, + chain: Chain, +): CampaignInfoFactorySimulate { + const contract = { address, abi: CAMPAIGN_INFO_FACTORY_ABI } as const; + + return { + async createCampaign(params: CreateCampaignParams, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "createCampaign", + args: [ + params.creator, + params.identifierHash, + [...params.selectedPlatformHash], + [...(params.platformDataKey ?? [])], + [...(params.platformDataValue ?? [])], + { + launchTime: params.campaignData.launchTime, + deadline: params.campaignData.deadline, + goalAmount: params.campaignData.goalAmount, + currency: params.campaignData.currency, + }, + params.nftName, + params.nftSymbol, + params.nftImageURI, + params.contractURI, + ], + }), + ); + }, + async updateImplementation(newImplementation: Address, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "updateImplementation", + args: [newImplementation], + }), + ); + }, + async transferOwnership(newOwner: Address, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "transferOwnership", + args: [newOwner], + }), + ); + }, + async renounceOwnership(options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "renounceOwnership", + args: [], + }), + ); + }, + }; +} + diff --git a/packages/contracts/src/contracts/campaign-info-factory/types.ts b/packages/contracts/src/contracts/campaign-info-factory/types.ts new file mode 100644 index 00000000..7cfa0774 --- /dev/null +++ b/packages/contracts/src/contracts/campaign-info-factory/types.ts @@ -0,0 +1,52 @@ +import type { Address, Hex } from "../../lib"; +import type { CreateCampaignParams } from "../../types/params"; +import type { CallSignerOptions } from "../../client/types"; + +/** Read-only methods for a CampaignInfoFactory contract instance. */ +export interface CampaignInfoFactoryReads { + /** Returns the CampaignInfo address for a given identifier hash. */ + identifierToCampaignInfo(identifierHash: Hex): Promise
; + /** Returns true if the given address is a valid campaign info clone. */ + isValidCampaignInfo(campaignInfo: Address): Promise; + /** Returns the contract owner address. */ + owner(): Promise
; +} + +/** Write methods for a CampaignInfoFactory contract instance. */ +export interface CampaignInfoFactoryWrites { + /** + * Deploys a new CampaignInfo clone and registers it. + * @param params - Full campaign creation parameters + * @returns Transaction hash + */ + createCampaign(params: CreateCampaignParams, options?: CallSignerOptions): Promise; + /** Updates the CampaignInfo implementation address. */ + updateImplementation(newImplementation: Address, options?: CallSignerOptions): Promise; + /** Transfers contract ownership to a new address. */ + transferOwnership(newOwner: Address, options?: CallSignerOptions): Promise; + /** Renounces contract ownership permanently. */ + renounceOwnership(options?: CallSignerOptions): Promise; +} + +/** Simulate counterparts for CampaignInfoFactory write methods. */ +export interface CampaignInfoFactorySimulate { + /** Simulates createCampaign; throws a typed error on revert. */ + createCampaign(params: CreateCampaignParams, options?: CallSignerOptions): Promise; + /** Simulates updateImplementation; throws a typed error on revert. */ + updateImplementation(newImplementation: Address, options?: CallSignerOptions): Promise; + /** Simulates transferOwnership; throws a typed error on revert. */ + transferOwnership(newOwner: Address, options?: CallSignerOptions): Promise; + /** Simulates renounceOwnership; throws a typed error on revert. */ + renounceOwnership(options?: CallSignerOptions): Promise; +} + +/** Event helpers for a CampaignInfoFactory contract instance. */ +export interface CampaignInfoFactoryEvents {} + +/** Full CampaignInfoFactory entity combining reads, writes, simulate, and events. */ +export type CampaignInfoFactoryEntity = CampaignInfoFactoryReads & CampaignInfoFactoryWrites & { + /** Simulation counterparts for every write method. */ + simulate: CampaignInfoFactorySimulate; + /** Event helpers for filtering and watching logs. */ + events: CampaignInfoFactoryEvents; +}; diff --git a/packages/contracts/src/contracts/campaign-info-factory/writes.ts b/packages/contracts/src/contracts/campaign-info-factory/writes.ts new file mode 100644 index 00000000..de3e9c73 --- /dev/null +++ b/packages/contracts/src/contracts/campaign-info-factory/writes.ts @@ -0,0 +1,62 @@ +import type { Address, Hex, WalletClient, Chain } from "../../lib"; +import { CAMPAIGN_INFO_FACTORY_ABI } from "./abi"; +import { requireSigner, requireAccount } from "../../utils/account"; +import type { CampaignInfoFactoryWrites } from "./types"; +import type { CreateCampaignParams } from "../../types/params"; +import type { CallSignerOptions } from "../../client/types"; + +/** + * Builds write methods for a CampaignInfoFactory contract instance. + * @param address - Deployed CampaignInfoFactory contract address + * @param walletClient - Viem WalletClient used to call writeContract; must have an account attached + * @param chain - Chain passed to writeContract for EIP-1559 and replay protection + * @returns Write methods bound to the given contract address + */ +export function createCampaignInfoFactoryWrites( + address: Address, + walletClient: WalletClient | null, + chain: Chain, +): CampaignInfoFactoryWrites { + const contract = { address, abi: CAMPAIGN_INFO_FACTORY_ABI } as const; + + return { + async createCampaign(params: CreateCampaignParams, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "createCampaign", + args: [ + params.creator, + params.identifierHash, + [...params.selectedPlatformHash], + [...(params.platformDataKey ?? [])], + [...(params.platformDataValue ?? [])], + { + launchTime: params.campaignData.launchTime, + deadline: params.campaignData.deadline, + goalAmount: params.campaignData.goalAmount, + currency: params.campaignData.currency, + }, + params.nftName, + params.nftSymbol, + params.nftImageURI, + params.contractURI, + ], + }); + }, + async updateImplementation(newImplementation: Address, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "updateImplementation", args: [newImplementation] }); + }, + async transferOwnership(newOwner: Address, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "transferOwnership", args: [newOwner] }); + }, + async renounceOwnership(options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "renounceOwnership", args: [] }); + }, + }; +} diff --git a/packages/contracts/src/contracts/campaign-info/abi.ts b/packages/contracts/src/contracts/campaign-info/abi.ts new file mode 100644 index 00000000..c79aa1dd --- /dev/null +++ b/packages/contracts/src/contracts/campaign-info/abi.ts @@ -0,0 +1,486 @@ +const CAMPAIGN_DATA_COMPONENTS = [ + { internalType: "uint256", name: "launchTime", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + { internalType: "uint256", name: "goalAmount", type: "uint256" }, + { internalType: "bytes32", name: "currency", type: "bytes32" }, +] as const; + +export const CAMPAIGN_INFO_ABI = [ + { inputs: [], name: "AdminAccessCheckerUnauthorized", type: "error" }, + { inputs: [], name: "CampaignInfoInvalidInput", type: "error" }, + { + inputs: [ + { internalType: "bytes32", name: "platformBytes", type: "bytes32" }, + { internalType: "bool", name: "selection", type: "bool" }, + ], + name: "CampaignInfoInvalidPlatformUpdate", + type: "error", + }, + { + inputs: [{ internalType: "bytes32", name: "platformBytes", type: "bytes32" }], + name: "CampaignInfoPlatformNotSelected", + type: "error", + }, + { + inputs: [{ internalType: "bytes32", name: "platformHash", type: "bytes32" }], + name: "CampaignInfoPlatformAlreadyApproved", + type: "error", + }, + { inputs: [], name: "CampaignInfoUnauthorized", type: "error" }, + { inputs: [], name: "CampaignInfoIsLocked", type: "error" }, + { + inputs: [ + { internalType: "uint256", name: "inputTime", type: "uint256" }, + { internalType: "uint256", name: "currentTime", type: "uint256" }, + ], + name: "CurrentTimeIsGreater", + type: "error", + }, + { + inputs: [ + { internalType: "uint256", name: "inputTime", type: "uint256" }, + { internalType: "uint256", name: "currentTime", type: "uint256" }, + ], + name: "CurrentTimeIsLess", + type: "error", + }, + { + inputs: [ + { internalType: "uint256", name: "initialTime", type: "uint256" }, + { internalType: "uint256", name: "finalTime", type: "uint256" }, + ], + name: "CurrentTimeIsNotWithinRange", + type: "error", + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: "uint256", name: "newDeadline", type: "uint256" }], + name: "CampaignInfoDeadlineUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: "uint256", name: "newGoalAmount", type: "uint256" }], + name: "CampaignInfoGoalAmountUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: "uint256", name: "newLaunchTime", type: "uint256" }], + name: "CampaignInfoLaunchTimeUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "platformBytes", type: "bytes32" }, + { indexed: true, internalType: "address", name: "platformTreasury", type: "address" }, + ], + name: "CampaignInfoPlatformInfoUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "platformBytes", type: "bytes32" }, + { indexed: false, internalType: "bool", name: "selection", type: "bool" }, + ], + name: "CampaignInfoSelectedPlatformUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "previousOwner", type: "address" }, + { indexed: true, internalType: "address", name: "newOwner", type: "address" }, + ], + name: "OwnershipTransferred", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: "address", name: "account", type: "address" }, + { indexed: false, internalType: "bytes32", name: "message", type: "bytes32" }, + ], + name: "Paused", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: "address", name: "account", type: "address" }, + { indexed: false, internalType: "bytes32", name: "message", type: "bytes32" }, + ], + name: "Unpaused", + type: "event", + }, + { + inputs: [{ internalType: "bytes32", name: "message", type: "bytes32" }], + name: "_cancelCampaign", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "message", type: "bytes32" }], + name: "_pauseCampaign", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "platformBytes", type: "bytes32" }, + { internalType: "address", name: "platformTreasuryAddress", type: "address" }, + ], + name: "_setPlatformInfo", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "message", type: "bytes32" }], + name: "_unpauseCampaign", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "platformBytes", type: "bytes32" }], + name: "checkIfPlatformSelected", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getDeadline", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getGoalAmount", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getIdentifierHash", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getLaunchTime", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "platformBytes", type: "bytes32" }], + name: "getPlatformAdminAddress", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "platformDataKey", type: "bytes32" }], + name: "getPlatformData", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "platformBytes", type: "bytes32" }], + name: "getPlatformFeePercent", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getProtocolAdminAddress", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getProtocolFeePercent", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getCampaignCurrency", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getAcceptedTokens", + outputs: [{ internalType: "address[]", name: "", type: "address[]" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "token", type: "address" }], + name: "isTokenAccepted", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "platformHash", type: "bytes32" }], + name: "getPlatformClaimDelay", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getTotalRaisedAmount", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getTotalLifetimeRaisedAmount", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getTotalRefundedAmount", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getTotalAvailableRaisedAmount", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getTotalCancelledAmount", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getTotalExpectedAmount", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "key", type: "bytes32" }], + name: "getDataFromRegistry", + outputs: [{ internalType: "bytes32", name: "value", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getBufferTime", + outputs: [{ internalType: "uint256", name: "bufferTime", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "platformHash", type: "bytes32" }, + { internalType: "bytes32", name: "typeId", type: "bytes32" }, + ], + name: "getLineItemType", + outputs: [ + { internalType: "bool", name: "exists", type: "bool" }, + { internalType: "string", name: "label", type: "string" }, + { internalType: "bool", name: "countsTowardGoal", type: "bool" }, + { internalType: "bool", name: "applyProtocolFee", type: "bool" }, + { internalType: "bool", name: "canRefund", type: "bool" }, + { internalType: "bool", name: "instantTransfer", type: "bool" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getCampaignConfig", + outputs: [ + { + components: [ + { internalType: "address", name: "treasuryFactory", type: "address" }, + { internalType: "uint256", name: "protocolFeePercent", type: "uint256" }, + { internalType: "bytes32", name: "identifierHash", type: "bytes32" }, + ], + internalType: "struct CampaignInfo.Config", + name: "config", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getApprovedPlatformHashes", + outputs: [{ internalType: "bytes32[]", name: "", type: "bytes32[]" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "isLocked", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "platformHash", type: "bytes32" }], + name: "checkIfPlatformApproved", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "cancelled", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "creator", type: "address" }, + { internalType: "contract IGlobalParams", name: "globalParams", type: "address" }, + { internalType: "bytes32[]", name: "selectedPlatformHash", type: "bytes32[]" }, + { internalType: "bytes32[]", name: "platformDataKey", type: "bytes32[]" }, + { internalType: "bytes32[]", name: "platformDataValue", type: "bytes32[]" }, + { + components: [...CAMPAIGN_DATA_COMPONENTS], + internalType: "struct ICampaignData.CampaignData", + name: "campaignData", + type: "tuple", + }, + { internalType: "address[]", name: "acceptedTokens", type: "address[]" }, + { internalType: "string", name: "nftName", type: "string" }, + { internalType: "string", name: "nftSymbol", type: "string" }, + { internalType: "string", name: "nftImageURI", type: "string" }, + { internalType: "string", name: "nftContractURI", type: "string" }, + ], + name: "initialize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "backer", type: "address" }, + { internalType: "bytes32", name: "reward", type: "bytes32" }, + { internalType: "address", name: "tokenAddress", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "uint256", name: "shippingFee", type: "uint256" }, + { internalType: "uint256", name: "tipAmount", type: "uint256" }, + ], + name: "mintNFTForPledge", + outputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "string", name: "newImageURI", type: "string" }], + name: "setImageURI", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "string", name: "newContractURI", type: "string" }], + name: "updateContractURI", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], + name: "burn", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "owner", + outputs: [{ internalType: "address", name: "account", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "paused", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "renounceOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "newOwner", type: "address" }], + name: "transferOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "deadline", type: "uint256" }], + name: "updateDeadline", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "goalAmount", type: "uint256" }], + name: "updateGoalAmount", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "launchTime", type: "uint256" }], + name: "updateLaunchTime", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "platformHash", type: "bytes32" }, + { internalType: "bool", name: "selection", type: "bool" }, + { internalType: "bytes32[]", name: "platformDataKey", type: "bytes32[]" }, + { internalType: "bytes32[]", name: "platformDataValue", type: "bytes32[]" }, + ], + name: "updateSelectedPlatform", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; diff --git a/packages/contracts/src/contracts/campaign-info/events.ts b/packages/contracts/src/contracts/campaign-info/events.ts new file mode 100644 index 00000000..8ffa2309 --- /dev/null +++ b/packages/contracts/src/contracts/campaign-info/events.ts @@ -0,0 +1,18 @@ +import type { Address, PublicClient } from "../../lib"; +import type { CampaignInfoEvents } from "./types"; + +// TODO: Add event filter factories (filterDeadlineUpdated, filterMintNFTForPledge), log decoder (decodeLog), +// and watcher factories using getLogs / watchEvent. + +/** + * Builds event helpers for a CampaignInfo contract instance. + * @param _address - Deployed CampaignInfo contract address + * @param _publicClient - Viem PublicClient used to call getLogs + * @returns Event helpers bound to the given contract address + */ +export function createCampaignInfoEvents( + _address: Address, + _publicClient: PublicClient, +): CampaignInfoEvents { + return {}; +} diff --git a/packages/contracts/src/contracts/campaign-info/index.ts b/packages/contracts/src/contracts/campaign-info/index.ts new file mode 100644 index 00000000..56980607 --- /dev/null +++ b/packages/contracts/src/contracts/campaign-info/index.ts @@ -0,0 +1,38 @@ +import type { Address, PublicClient, WalletClient, Chain } from "../../lib"; +import { createCampaignInfoReads } from "./reads"; +import { createCampaignInfoWrites } from "./writes"; +import { createCampaignInfoSimulate } from "./simulate"; +import { createCampaignInfoEvents } from "./events"; +import type { CampaignInfoEntity } from "./types"; + +/** + * Creates a fully composed CampaignInfo entity combining reads, writes, simulate, and events. + * @param address - Deployed CampaignInfo contract address + * @param publicClient - Viem PublicClient used for reads and simulation + * @param walletClient - Viem WalletClient used for writes; must have an account attached + * @param chain - Chain passed to writeContract and simulateContract + * @returns Composed entity exposing all CampaignInfo methods under a single object + * + * @example + * ```typescript + * const ci = createCampaignInfoEntity(CAMPAIGN_INFO_ADDRESS, publicClient, walletClient, chain); + * const deadline = await ci.getDeadline(); + * await ci.simulate.updateDeadline(newDeadline); + * await ci.updateDeadline(newDeadline); + * ``` + */ +export function createCampaignInfoEntity( + address: Address, + publicClient: PublicClient, + walletClient: WalletClient | null, + chain: Chain, +): CampaignInfoEntity { + return { + ...createCampaignInfoReads(address, publicClient), + ...createCampaignInfoWrites(address, walletClient, chain), + simulate: createCampaignInfoSimulate(address, publicClient, walletClient, chain), + events: createCampaignInfoEvents(address, publicClient), + }; +} + +export type { CampaignInfoEntity } from "./types"; diff --git a/packages/contracts/src/contracts/campaign-info/reads.ts b/packages/contracts/src/contracts/campaign-info/reads.ts new file mode 100644 index 00000000..6a87f159 --- /dev/null +++ b/packages/contracts/src/contracts/campaign-info/reads.ts @@ -0,0 +1,112 @@ +import type { Address, Hex, PublicClient } from "../../lib"; +import { CAMPAIGN_INFO_ABI } from "./abi"; +import type { CampaignInfoReads } from "./types"; +import type { LineItemTypeInfo, CampaignConfig } from "../../types/structs"; + +/** + * Builds read methods for a CampaignInfo contract instance. + * @param address - Deployed CampaignInfo contract address + * @param publicClient - Viem PublicClient used to call readContract + * @returns Read methods bound to the given contract address + */ +export function createCampaignInfoReads( + address: Address, + publicClient: PublicClient, +): CampaignInfoReads { + const contract = { address, abi: CAMPAIGN_INFO_ABI } as const; + + return { + async getLaunchTime() { + return publicClient.readContract({ ...contract, functionName: "getLaunchTime" }); + }, + async getDeadline() { + return publicClient.readContract({ ...contract, functionName: "getDeadline" }); + }, + async getGoalAmount() { + return publicClient.readContract({ ...contract, functionName: "getGoalAmount" }); + }, + async getCampaignCurrency() { + return publicClient.readContract({ ...contract, functionName: "getCampaignCurrency" }); + }, + async getIdentifierHash() { + return publicClient.readContract({ ...contract, functionName: "getIdentifierHash" }); + }, + async checkIfPlatformSelected(platformBytes: Hex) { + return publicClient.readContract({ ...contract, functionName: "checkIfPlatformSelected", args: [platformBytes] }); + }, + async checkIfPlatformApproved(platformHash: Hex) { + return publicClient.readContract({ ...contract, functionName: "checkIfPlatformApproved", args: [platformHash] }); + }, + async getPlatformAdminAddress(platformBytes: Hex) { + return publicClient.readContract({ ...contract, functionName: "getPlatformAdminAddress", args: [platformBytes] }); + }, + async getPlatformData(platformDataKey: Hex) { + return publicClient.readContract({ ...contract, functionName: "getPlatformData", args: [platformDataKey] }); + }, + async getPlatformFeePercent(platformBytes: Hex) { + return publicClient.readContract({ ...contract, functionName: "getPlatformFeePercent", args: [platformBytes] }); + }, + async getPlatformClaimDelay(platformHash: Hex) { + return publicClient.readContract({ ...contract, functionName: "getPlatformClaimDelay", args: [platformHash] }); + }, + async getProtocolAdminAddress() { + return publicClient.readContract({ ...contract, functionName: "getProtocolAdminAddress" }); + }, + async getProtocolFeePercent() { + return publicClient.readContract({ ...contract, functionName: "getProtocolFeePercent" }); + }, + async getAcceptedTokens() { + return publicClient.readContract({ ...contract, functionName: "getAcceptedTokens" }); + }, + async isTokenAccepted(token: Address) { + return publicClient.readContract({ ...contract, functionName: "isTokenAccepted", args: [token] }); + }, + async getTotalRaisedAmount() { + return publicClient.readContract({ ...contract, functionName: "getTotalRaisedAmount" }); + }, + async getTotalLifetimeRaisedAmount() { + return publicClient.readContract({ ...contract, functionName: "getTotalLifetimeRaisedAmount" }); + }, + async getTotalRefundedAmount() { + return publicClient.readContract({ ...contract, functionName: "getTotalRefundedAmount" }); + }, + async getTotalAvailableRaisedAmount() { + return publicClient.readContract({ ...contract, functionName: "getTotalAvailableRaisedAmount" }); + }, + async getTotalCancelledAmount() { + return publicClient.readContract({ ...contract, functionName: "getTotalCancelledAmount" }); + }, + async getTotalExpectedAmount() { + return publicClient.readContract({ ...contract, functionName: "getTotalExpectedAmount" }); + }, + async getDataFromRegistry(key: Hex) { + return publicClient.readContract({ ...contract, functionName: "getDataFromRegistry", args: [key] }); + }, + async getBufferTime() { + return publicClient.readContract({ ...contract, functionName: "getBufferTime" }); + }, + async getLineItemType(platformHash: Hex, typeId: Hex): Promise { + const result = await publicClient.readContract({ ...contract, functionName: "getLineItemType", args: [platformHash, typeId] }); + return result as unknown as LineItemTypeInfo; + }, + async getCampaignConfig(): Promise { + const result = await publicClient.readContract({ ...contract, functionName: "getCampaignConfig" }); + return result as unknown as CampaignConfig; + }, + async getApprovedPlatformHashes() { + return publicClient.readContract({ ...contract, functionName: "getApprovedPlatformHashes" }); + }, + async isLocked() { + return publicClient.readContract({ ...contract, functionName: "isLocked" }); + }, + async cancelled() { + return publicClient.readContract({ ...contract, functionName: "cancelled" }); + }, + async owner() { + return publicClient.readContract({ ...contract, functionName: "owner" }); + }, + async paused() { + return publicClient.readContract({ ...contract, functionName: "paused" }); + }, + }; +} diff --git a/packages/contracts/src/contracts/campaign-info/simulate.ts b/packages/contracts/src/contracts/campaign-info/simulate.ts new file mode 100644 index 00000000..55ae20f9 --- /dev/null +++ b/packages/contracts/src/contracts/campaign-info/simulate.ts @@ -0,0 +1,197 @@ +import type { Address, Hex, PublicClient, WalletClient, Chain } from "../../lib"; +import { CAMPAIGN_INFO_ABI } from "./abi"; +import { requireSigner, requireAccount } from "../../utils/account"; +import { simulateWithErrorDecode } from "../../errors"; +import type { CampaignInfoSimulate } from "./types"; +import type { CallSignerOptions } from "../../client/types"; + +/** + * Builds simulate methods for CampaignInfo write calls. + * Each method calls simulateContract against the current chain state and throws a typed + * SDK error on revert, decoded via simulateWithErrorDecode. + * @param address - Deployed CampaignInfo contract address + * @param publicClient - Viem PublicClient used to call simulateContract + * @param walletClient - Viem WalletClient used to resolve the account for simulation + * @param chain - Chain passed to simulateContract + * @returns Simulation methods bound to the given contract address + */ +export function createCampaignInfoSimulate( + address: Address, + publicClient: PublicClient, + walletClient: WalletClient | null, + chain: Chain, +): CampaignInfoSimulate { + const contract = { address, abi: CAMPAIGN_INFO_ABI } as const; + + return { + async updateDeadline(deadline: bigint, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "updateDeadline", + args: [deadline], + }), + ); + }, + async updateGoalAmount(goalAmount: bigint, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "updateGoalAmount", + args: [goalAmount], + }), + ); + }, + async updateLaunchTime(launchTime: bigint, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "updateLaunchTime", + args: [launchTime], + }), + ); + }, + async updateSelectedPlatform(platformHash: Hex, selection: boolean, platformDataKey: readonly Hex[], platformDataValue: readonly Hex[], options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "updateSelectedPlatform", + args: [platformHash, selection, [...platformDataKey], [...platformDataValue]], + }), + ); + }, + async setImageURI(newImageURI: string, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "setImageURI", + args: [newImageURI], + }), + ); + }, + async updateContractURI(newContractURI: string, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "updateContractURI", + args: [newContractURI], + }), + ); + }, + async mintNFTForPledge(backer: Address, reward: Hex, tokenAddress: Address, amount: bigint, shippingFee: bigint, tipAmount: bigint, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "mintNFTForPledge", + args: [backer, reward, tokenAddress, amount, shippingFee, tipAmount], + }), + ); + }, + async burn(tokenId: bigint, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "burn", + args: [tokenId], + }), + ); + }, + async pauseCampaign(message: Hex, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "_pauseCampaign", + args: [message], + }), + ); + }, + async unpauseCampaign(message: Hex, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "_unpauseCampaign", + args: [message], + }), + ); + }, + async cancelCampaign(message: Hex, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "_cancelCampaign", + args: [message], + }), + ); + }, + async setPlatformInfo(platformBytes: Hex, platformTreasuryAddress: Address, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "_setPlatformInfo", + args: [platformBytes, platformTreasuryAddress], + }), + ); + }, + async transferOwnership(newOwner: Address, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "transferOwnership", + args: [newOwner], + }), + ); + }, + async renounceOwnership(options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "renounceOwnership", + args: [], + }), + ); + }, + }; +} + diff --git a/packages/contracts/src/contracts/campaign-info/types.ts b/packages/contracts/src/contracts/campaign-info/types.ts new file mode 100644 index 00000000..07e700fe --- /dev/null +++ b/packages/contracts/src/contracts/campaign-info/types.ts @@ -0,0 +1,142 @@ +import type { Address, Hex } from "../../lib"; +import type { LineItemTypeInfo, CampaignConfig } from "../../types/structs"; +import type { CallSignerOptions } from "../../client/types"; + +/** Read-only methods for a CampaignInfo contract instance. */ +export interface CampaignInfoReads { + /** Returns the campaign launch timestamp in seconds. */ + getLaunchTime(): Promise; + /** Returns the campaign deadline timestamp in seconds. */ + getDeadline(): Promise; + /** Returns the campaign funding goal amount. */ + getGoalAmount(): Promise; + /** Returns the bytes32 campaign currency identifier. */ + getCampaignCurrency(): Promise; + /** Returns the bytes32 identifier hash for this campaign. */ + getIdentifierHash(): Promise; + /** Returns true if the platform is selected for this campaign. */ + checkIfPlatformSelected(platformBytes: Hex): Promise; + /** Returns true if the platform is approved for this campaign. */ + checkIfPlatformApproved(platformHash: Hex): Promise; + /** Returns the platform admin address. */ + getPlatformAdminAddress(platformBytes: Hex): Promise
; + /** Returns a platform-scoped data value by key. */ + getPlatformData(platformDataKey: Hex): Promise; + /** Returns the platform fee percent in basis points. */ + getPlatformFeePercent(platformBytes: Hex): Promise; + /** Returns the platform claim delay in seconds. */ + getPlatformClaimDelay(platformHash: Hex): Promise; + /** Returns the protocol admin address. */ + getProtocolAdminAddress(): Promise
; + /** Returns the protocol fee percent in basis points. */ + getProtocolFeePercent(): Promise; + /** Returns the list of accepted token addresses for this campaign. */ + getAcceptedTokens(): Promise; + /** Returns true if the given token is accepted. */ + isTokenAccepted(token: Address): Promise; + /** Returns the cumulative amount raised across all treasuries. */ + getTotalRaisedAmount(): Promise; + /** Returns the lifetime raised amount including refunded funds. */ + getTotalLifetimeRaisedAmount(): Promise; + /** Returns the total amount refunded to backers. */ + getTotalRefundedAmount(): Promise; + /** Returns the available (non-refunded) raised amount. */ + getTotalAvailableRaisedAmount(): Promise; + /** Returns the total cancelled payment amount. */ + getTotalCancelledAmount(): Promise; + /** Returns the total expected payment amount. */ + getTotalExpectedAmount(): Promise; + /** Returns a value from the campaign data registry by key. */ + getDataFromRegistry(key: Hex): Promise; + /** Returns the buffer time in seconds between deadline and claim window. */ + getBufferTime(): Promise; + /** Returns the line item type config for a platform + typeId pair. */ + getLineItemType(platformHash: Hex, typeId: Hex): Promise; + /** Returns the campaign config struct (treasuryFactory, protocolFeePercent, identifierHash). */ + getCampaignConfig(): Promise; + /** Returns the list of approved platform hashes for this campaign. */ + getApprovedPlatformHashes(): Promise; + /** Returns true if the campaign info is locked against modifications. */ + isLocked(): Promise; + /** Returns true if the campaign has been cancelled. */ + cancelled(): Promise; + /** Returns the contract owner address. */ + owner(): Promise
; + /** Returns true if the campaign is paused. */ + paused(): Promise; +} + +/** Write methods for a CampaignInfo contract instance. */ +export interface CampaignInfoWrites { + /** Updates the campaign deadline timestamp. */ + updateDeadline(deadline: bigint, options?: CallSignerOptions): Promise; + /** Updates the campaign funding goal amount. */ + updateGoalAmount(goalAmount: bigint, options?: CallSignerOptions): Promise; + /** Updates the campaign launch timestamp. */ + updateLaunchTime(launchTime: bigint, options?: CallSignerOptions): Promise; + /** Updates whether a platform is selected for this campaign. */ + updateSelectedPlatform(platformHash: Hex, selection: boolean, platformDataKey: readonly Hex[], platformDataValue: readonly Hex[], options?: CallSignerOptions): Promise; + /** Updates the NFT image URI for pledge tokens. */ + setImageURI(newImageURI: string, options?: CallSignerOptions): Promise; + /** Updates the ERC-721 contract metadata URI. */ + updateContractURI(newContractURI: string, options?: CallSignerOptions): Promise; + /** Mints a pledge NFT for a backer; returns the tx hash (tokenId is in receipt events). */ + mintNFTForPledge(backer: Address, reward: Hex, tokenAddress: Address, amount: bigint, shippingFee: bigint, tipAmount: bigint, options?: CallSignerOptions): Promise; + /** Burns a pledge NFT. */ + burn(tokenId: bigint, options?: CallSignerOptions): Promise; + /** Pauses the campaign. */ + pauseCampaign(message: Hex, options?: CallSignerOptions): Promise; + /** Unpauses the campaign. */ + unpauseCampaign(message: Hex, options?: CallSignerOptions): Promise; + /** Cancels the campaign permanently. */ + cancelCampaign(message: Hex, options?: CallSignerOptions): Promise; + /** Sets the platform treasury address for a platform. */ + setPlatformInfo(platformBytes: Hex, platformTreasuryAddress: Address, options?: CallSignerOptions): Promise; + /** Transfers contract ownership to a new address. */ + transferOwnership(newOwner: Address, options?: CallSignerOptions): Promise; + /** Renounces contract ownership permanently. */ + renounceOwnership(options?: CallSignerOptions): Promise; +} + +/** Simulate counterparts for CampaignInfo write methods. */ +export interface CampaignInfoSimulate { + /** Simulates updateDeadline; throws a typed error on revert. */ + updateDeadline(deadline: bigint, options?: CallSignerOptions): Promise; + /** Simulates updateGoalAmount; throws a typed error on revert. */ + updateGoalAmount(goalAmount: bigint, options?: CallSignerOptions): Promise; + /** Simulates updateLaunchTime; throws a typed error on revert. */ + updateLaunchTime(launchTime: bigint, options?: CallSignerOptions): Promise; + /** Simulates updateSelectedPlatform; throws a typed error on revert. */ + updateSelectedPlatform(platformHash: Hex, selection: boolean, platformDataKey: readonly Hex[], platformDataValue: readonly Hex[], options?: CallSignerOptions): Promise; + /** Simulates setImageURI; throws a typed error on revert. */ + setImageURI(newImageURI: string, options?: CallSignerOptions): Promise; + /** Simulates updateContractURI; throws a typed error on revert. */ + updateContractURI(newContractURI: string, options?: CallSignerOptions): Promise; + /** Simulates mintNFTForPledge; throws a typed error on revert. */ + mintNFTForPledge(backer: Address, reward: Hex, tokenAddress: Address, amount: bigint, shippingFee: bigint, tipAmount: bigint, options?: CallSignerOptions): Promise; + /** Simulates burn; throws a typed error on revert. */ + burn(tokenId: bigint, options?: CallSignerOptions): Promise; + /** Simulates pauseCampaign; throws a typed error on revert. */ + pauseCampaign(message: Hex, options?: CallSignerOptions): Promise; + /** Simulates unpauseCampaign; throws a typed error on revert. */ + unpauseCampaign(message: Hex, options?: CallSignerOptions): Promise; + /** Simulates cancelCampaign; throws a typed error on revert. */ + cancelCampaign(message: Hex, options?: CallSignerOptions): Promise; + /** Simulates setPlatformInfo; throws a typed error on revert. */ + setPlatformInfo(platformBytes: Hex, platformTreasuryAddress: Address, options?: CallSignerOptions): Promise; + /** Simulates transferOwnership; throws a typed error on revert. */ + transferOwnership(newOwner: Address, options?: CallSignerOptions): Promise; + /** Simulates renounceOwnership; throws a typed error on revert. */ + renounceOwnership(options?: CallSignerOptions): Promise; +} + +/** Event helpers for a CampaignInfo contract instance. */ +export interface CampaignInfoEvents {} + +/** Full CampaignInfo entity combining reads, writes, simulate, and events. */ +export type CampaignInfoEntity = CampaignInfoReads & CampaignInfoWrites & { + /** Simulation counterparts for every write method. */ + simulate: CampaignInfoSimulate; + /** Event helpers for filtering and watching logs. */ + events: CampaignInfoEvents; +}; diff --git a/packages/contracts/src/contracts/campaign-info/writes.ts b/packages/contracts/src/contracts/campaign-info/writes.ts new file mode 100644 index 00000000..d9161f73 --- /dev/null +++ b/packages/contracts/src/contracts/campaign-info/writes.ts @@ -0,0 +1,79 @@ +import type { Address, Hex, WalletClient, Chain } from "../../lib"; +import { CAMPAIGN_INFO_ABI } from "./abi"; +import { requireSigner, requireAccount } from "../../utils/account"; +import type { CampaignInfoWrites } from "./types"; +import type { CallSignerOptions } from "../../client/types"; + +/** + * Builds write methods for a CampaignInfo contract instance. + * @param address - Deployed CampaignInfo contract address + * @param walletClient - Viem WalletClient used to call writeContract; must have an account attached + * @param chain - Chain passed to writeContract for EIP-1559 and replay protection + * @returns Write methods bound to the given contract address + */ +export function createCampaignInfoWrites( + address: Address, + walletClient: WalletClient | null, + chain: Chain, +): CampaignInfoWrites { + const contract = { address, abi: CAMPAIGN_INFO_ABI } as const; + + return { + async updateDeadline(deadline: bigint, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "updateDeadline", args: [deadline] }); + }, + async updateGoalAmount(goalAmount: bigint, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "updateGoalAmount", args: [goalAmount] }); + }, + async updateLaunchTime(launchTime: bigint, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "updateLaunchTime", args: [launchTime] }); + }, + async updateSelectedPlatform(platformHash: Hex, selection: boolean, platformDataKey: readonly Hex[], platformDataValue: readonly Hex[], options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "updateSelectedPlatform", args: [platformHash, selection, [...platformDataKey], [...platformDataValue]] }); + }, + async setImageURI(newImageURI: string, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "setImageURI", args: [newImageURI] }); + }, + async updateContractURI(newContractURI: string, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "updateContractURI", args: [newContractURI] }); + }, + async mintNFTForPledge(backer: Address, reward: Hex, tokenAddress: Address, amount: bigint, shippingFee: bigint, tipAmount: bigint, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "mintNFTForPledge", args: [backer, reward, tokenAddress, amount, shippingFee, tipAmount] }); + }, + async burn(tokenId: bigint, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "burn", args: [tokenId] }); + }, + async pauseCampaign(message: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "_pauseCampaign", args: [message] }); + }, + async unpauseCampaign(message: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "_unpauseCampaign", args: [message] }); + }, + async cancelCampaign(message: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "_cancelCampaign", args: [message] }); + }, + async setPlatformInfo(platformBytes: Hex, platformTreasuryAddress: Address, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "_setPlatformInfo", args: [platformBytes, platformTreasuryAddress] }); + }, + async transferOwnership(newOwner: Address, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "transferOwnership", args: [newOwner] }); + }, + async renounceOwnership(options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "renounceOwnership", args: [] }); + }, + }; +} diff --git a/packages/contracts/src/contracts/global-params/abi.ts b/packages/contracts/src/contracts/global-params/abi.ts new file mode 100644 index 00000000..6ee2eac1 --- /dev/null +++ b/packages/contracts/src/contracts/global-params/abi.ts @@ -0,0 +1,444 @@ +/** GlobalParams ABI — typed const, co-located with the contract that uses it. */ +export const GLOBAL_PARAMS_ABI = [ + { inputs: [], name: "GlobalParamsInvalidInput", type: "error" }, + { + inputs: [{ internalType: "bytes32", name: "platformBytes", type: "bytes32" }], + name: "GlobalParamsPlatformAdminNotSet", + type: "error", + }, + { + inputs: [{ internalType: "bytes32", name: "platformBytes", type: "bytes32" }], + name: "GlobalParamsPlatformAlreadyListed", + type: "error", + }, + { inputs: [], name: "GlobalParamsPlatformDataAlreadySet", type: "error" }, + { inputs: [], name: "GlobalParamsPlatformDataNotSet", type: "error" }, + { inputs: [], name: "GlobalParamsPlatformDataSlotTaken", type: "error" }, + { + inputs: [{ internalType: "bytes32", name: "platformBytes", type: "bytes32" }], + name: "GlobalParamsPlatformFeePercentIsZero", + type: "error", + }, + { + inputs: [{ internalType: "bytes32", name: "platformBytes", type: "bytes32" }], + name: "GlobalParamsPlatformNotListed", + type: "error", + }, + { inputs: [], name: "GlobalParamsUnauthorized", type: "error" }, + { inputs: [], name: "GlobalParamsCurrencyTokenLengthMismatch", type: "error" }, + { + inputs: [{ internalType: "bytes32", name: "currency", type: "bytes32" }], + name: "GlobalParamsCurrencyHasNoTokens", + type: "error", + }, + { + inputs: [ + { internalType: "bytes32", name: "currency", type: "bytes32" }, + { internalType: "address", name: "token", type: "address" }, + ], + name: "GlobalParamsTokenNotInCurrency", + type: "error", + }, + { + inputs: [ + { internalType: "bytes32", name: "platformHash", type: "bytes32" }, + { internalType: "bytes32", name: "typeId", type: "bytes32" }, + ], + name: "GlobalParamsPlatformLineItemTypeNotFound", + type: "error", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "previousOwner", type: "address" }, + { indexed: true, internalType: "address", name: "newOwner", type: "address" }, + ], + name: "OwnershipTransferred", + type: "event", + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: "address", name: "account", type: "address" }], + name: "Paused", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "platformBytes", type: "bytes32" }, + { indexed: true, internalType: "address", name: "newAdminAddress", type: "address" }, + ], + name: "PlatformAdminAddressUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "platformBytes", type: "bytes32" }, + { indexed: true, internalType: "bytes32", name: "platformDataKey", type: "bytes32" }, + ], + name: "PlatformDataAdded", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "platformBytes", type: "bytes32" }, + { indexed: false, internalType: "bytes32", name: "platformDataKey", type: "bytes32" }, + ], + name: "PlatformDataRemoved", + type: "event", + }, + { + anonymous: false, + inputs: [{ indexed: true, internalType: "bytes32", name: "platformBytes", type: "bytes32" }], + name: "PlatformDelisted", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "platformBytes", type: "bytes32" }, + { indexed: true, internalType: "address", name: "platformAdminAddress", type: "address" }, + { indexed: false, internalType: "uint256", name: "platformFeePercent", type: "uint256" }, + ], + name: "PlatformEnlisted", + type: "event", + }, + { + anonymous: false, + inputs: [{ indexed: true, internalType: "address", name: "newAdminAddress", type: "address" }], + name: "ProtocolAdminAddressUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: "uint256", name: "newFeePercent", type: "uint256" }], + name: "ProtocolFeePercentUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "currency", type: "bytes32" }, + { indexed: true, internalType: "address", name: "token", type: "address" }, + ], + name: "TokenAddedToCurrency", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "currency", type: "bytes32" }, + { indexed: true, internalType: "address", name: "token", type: "address" }, + ], + name: "TokenRemovedFromCurrency", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "platformBytes", type: "bytes32" }, + { indexed: true, internalType: "address", name: "platformAdapter", type: "address" }, + ], + name: "PlatformAdapterSet", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "platformBytes", type: "bytes32" }, + { indexed: false, internalType: "uint256", name: "claimDelay", type: "uint256" }, + ], + name: "PlatformClaimDelayUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: "address", name: "account", type: "address" }], + name: "Unpaused", + type: "event", + }, + { + inputs: [ + { internalType: "bytes32", name: "key", type: "bytes32" }, + { internalType: "bytes32", name: "value", type: "bytes32" }, + ], + name: "addToRegistry", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "platformBytes", type: "bytes32" }, + { internalType: "bytes32", name: "platformDataKey", type: "bytes32" }, + ], + name: "addPlatformData", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "currency", type: "bytes32" }, + { internalType: "address", name: "token", type: "address" }, + ], + name: "addTokenToCurrency", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "platformDataKey", type: "bytes32" }], + name: "checkIfPlatformDataKeyValid", + outputs: [{ internalType: "bool", name: "isValid", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "platformBytes", type: "bytes32" }], + name: "checkIfPlatformIsListed", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "platformBytes", type: "bytes32" }], + name: "delistPlatform", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "platformHash", type: "bytes32" }, + { internalType: "address", name: "platformAdminAddress", type: "address" }, + { internalType: "uint256", name: "platformFeePercent", type: "uint256" }, + { internalType: "address", name: "platformAdapter", type: "address" }, + ], + name: "enlistPlatform", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "key", type: "bytes32" }], + name: "getFromRegistry", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getNumberOfListedPlatforms", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "platformBytes", type: "bytes32" }], + name: "getPlatformAdminAddress", + outputs: [{ internalType: "address", name: "account", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "platformDataKey", type: "bytes32" }], + name: "getPlatformDataOwner", + outputs: [{ internalType: "bytes32", name: "platformBytes", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "platformBytes", type: "bytes32" }], + name: "getPlatformAdapter", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "platformBytes", type: "bytes32" }], + name: "getPlatformClaimDelay", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "platformBytes", type: "bytes32" }], + name: "getPlatformFeePercent", + outputs: [{ internalType: "uint256", name: "platformFeePercent", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "platformHash", type: "bytes32" }, + { internalType: "bytes32", name: "typeId", type: "bytes32" }, + ], + name: "getPlatformLineItemType", + outputs: [ + { internalType: "bool", name: "exists", type: "bool" }, + { internalType: "string", name: "label", type: "string" }, + { internalType: "bool", name: "countsTowardGoal", type: "bool" }, + { internalType: "bool", name: "applyProtocolFee", type: "bool" }, + { internalType: "bool", name: "canRefund", type: "bool" }, + { internalType: "bool", name: "instantTransfer", type: "bool" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "protocolAdminAddress", type: "address" }, + { internalType: "uint256", name: "protocolFeePercent", type: "uint256" }, + { internalType: "bytes32[]", name: "currencies", type: "bytes32[]" }, + { internalType: "address[][]", name: "tokensPerCurrency", type: "address[][]" }, + ], + name: "initialize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "getProtocolAdminAddress", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getProtocolFeePercent", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "currency", type: "bytes32" }], + name: "getTokensForCurrency", + outputs: [{ internalType: "address[]", name: "", type: "address[]" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "owner", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "paused", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "platformBytes", type: "bytes32" }, + { internalType: "bytes32", name: "platformDataKey", type: "bytes32" }, + ], + name: "removePlatformData", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "platformHash", type: "bytes32" }, + { internalType: "bytes32", name: "typeId", type: "bytes32" }, + ], + name: "removePlatformLineItemType", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "currency", type: "bytes32" }, + { internalType: "address", name: "token", type: "address" }, + ], + name: "removeTokenFromCurrency", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "renounceOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "newOwner", type: "address" }], + name: "transferOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "platformBytes", type: "bytes32" }, + { internalType: "address", name: "platformAdapter", type: "address" }, + ], + name: "setPlatformAdapter", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "platformHash", type: "bytes32" }, + { internalType: "bytes32", name: "typeId", type: "bytes32" }, + { internalType: "string", name: "label", type: "string" }, + { internalType: "bool", name: "countsTowardGoal", type: "bool" }, + { internalType: "bool", name: "applyProtocolFee", type: "bool" }, + { internalType: "bool", name: "canRefund", type: "bool" }, + { internalType: "bool", name: "instantTransfer", type: "bool" }, + ], + name: "setPlatformLineItemType", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "platformBytes", type: "bytes32" }, + { internalType: "address", name: "platformAdminAddress", type: "address" }, + ], + name: "updatePlatformAdminAddress", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "platformBytes", type: "bytes32" }, + { internalType: "uint256", name: "claimDelay", type: "uint256" }, + ], + name: "updatePlatformClaimDelay", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "protocolAdminAddress", type: "address" }], + name: "updateProtocolAdminAddress", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "protocolFeePercent", type: "uint256" }], + name: "updateProtocolFeePercent", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; + +/** Inferred TypeScript type of the GlobalParams ABI array. */ +export type GlobalParamsAbi = typeof GLOBAL_PARAMS_ABI; diff --git a/packages/contracts/src/contracts/global-params/events.ts b/packages/contracts/src/contracts/global-params/events.ts new file mode 100644 index 00000000..ac17acc3 --- /dev/null +++ b/packages/contracts/src/contracts/global-params/events.ts @@ -0,0 +1,18 @@ +import type { Address, PublicClient } from "../../lib"; +import type { GlobalParamsEvents } from "./types"; + +// TODO: Add event filter factories (filterPlatformEnlisted, filterPlatformDelisted), log decoder (decodeLog), +// and watcher factories using getLogs / watchEvent. + +/** + * Builds event helpers for a GlobalParams contract instance. + * @param _address - Deployed GlobalParams contract address + * @param _publicClient - Viem PublicClient used to call getLogs + * @returns Event helpers bound to the given contract address + */ +export function createGlobalParamsEvents( + _address: Address, + _publicClient: PublicClient, +): GlobalParamsEvents { + return {}; +} diff --git a/packages/contracts/src/contracts/global-params/index.ts b/packages/contracts/src/contracts/global-params/index.ts new file mode 100644 index 00000000..30f3ff98 --- /dev/null +++ b/packages/contracts/src/contracts/global-params/index.ts @@ -0,0 +1,30 @@ +import type { Address, PublicClient, WalletClient, Chain } from "../../lib"; +import { createGlobalParamsReads } from "./reads"; +import { createGlobalParamsWrites } from "./writes"; +import { createGlobalParamsSimulate } from "./simulate"; +import { createGlobalParamsEvents } from "./events"; +import type { GlobalParamsEntity } from "./types"; + +/** + * Creates a fully composed GlobalParams entity combining reads, writes, simulate, and events. + * @param address - Deployed GlobalParams contract address + * @param publicClient - Viem PublicClient used for reads and simulation + * @param walletClient - Viem WalletClient used for writes; must have an account attached + * @param chain - Chain passed to writeContract and simulateContract + * @returns Composed entity exposing all GlobalParams methods under a single object + */ +export function createGlobalParamsEntity( + address: Address, + publicClient: PublicClient, + walletClient: WalletClient | null, + chain: Chain, +): GlobalParamsEntity { + return { + ...createGlobalParamsReads(address, publicClient), + ...createGlobalParamsWrites(address, walletClient, chain), + simulate: createGlobalParamsSimulate(address, publicClient, walletClient, chain), + events: createGlobalParamsEvents(address, publicClient), + }; +} + +export type { GlobalParamsEntity } from "./types"; diff --git a/packages/contracts/src/contracts/global-params/reads.ts b/packages/contracts/src/contracts/global-params/reads.ts new file mode 100644 index 00000000..ec2fa5ff --- /dev/null +++ b/packages/contracts/src/contracts/global-params/reads.ts @@ -0,0 +1,63 @@ +import type { Address, Hex, PublicClient } from "../../lib"; +import { GLOBAL_PARAMS_ABI } from "./abi"; +import type { GlobalParamsReads } from "./types"; +import type { LineItemTypeInfo } from "../../types/structs"; + +/** + * Builds read methods for a GlobalParams contract instance. + * @param address - Deployed GlobalParams contract address + * @param publicClient - Viem PublicClient used to call readContract + * @returns Read methods bound to the given contract address + */ +export function createGlobalParamsReads( + address: Address, + publicClient: PublicClient, +): GlobalParamsReads { + const contract = { address, abi: GLOBAL_PARAMS_ABI } as const; + + return { + async getProtocolAdminAddress() { + return publicClient.readContract({ ...contract, functionName: "getProtocolAdminAddress" }); + }, + async getProtocolFeePercent() { + return publicClient.readContract({ ...contract, functionName: "getProtocolFeePercent" }); + }, + async getNumberOfListedPlatforms() { + return publicClient.readContract({ ...contract, functionName: "getNumberOfListedPlatforms" }); + }, + async checkIfPlatformIsListed(platformBytes: Hex) { + return publicClient.readContract({ ...contract, functionName: "checkIfPlatformIsListed", args: [platformBytes] }); + }, + async checkIfPlatformDataKeyValid(platformDataKey: Hex) { + return publicClient.readContract({ ...contract, functionName: "checkIfPlatformDataKeyValid", args: [platformDataKey] }); + }, + async getPlatformAdminAddress(platformBytes: Hex) { + return publicClient.readContract({ ...contract, functionName: "getPlatformAdminAddress", args: [platformBytes] }); + }, + async getPlatformFeePercent(platformBytes: Hex) { + return publicClient.readContract({ ...contract, functionName: "getPlatformFeePercent", args: [platformBytes] }); + }, + async getPlatformClaimDelay(platformBytes: Hex) { + return publicClient.readContract({ ...contract, functionName: "getPlatformClaimDelay", args: [platformBytes] }); + }, + async getPlatformAdapter(platformBytes: Hex) { + return publicClient.readContract({ ...contract, functionName: "getPlatformAdapter", args: [platformBytes] }); + }, + async getPlatformDataOwner(platformDataKey: Hex) { + return publicClient.readContract({ ...contract, functionName: "getPlatformDataOwner", args: [platformDataKey] }); + }, + async getPlatformLineItemType(platformHash: Hex, typeId: Hex): Promise { + const result = await publicClient.readContract({ ...contract, functionName: "getPlatformLineItemType", args: [platformHash, typeId] }); + return result as unknown as LineItemTypeInfo; + }, + async getTokensForCurrency(currency: Hex) { + return publicClient.readContract({ ...contract, functionName: "getTokensForCurrency", args: [currency] }); + }, + async getFromRegistry(key: Hex) { + return publicClient.readContract({ ...contract, functionName: "getFromRegistry", args: [key] }); + }, + async owner() { + return publicClient.readContract({ ...contract, functionName: "owner" }); + }, + }; +} diff --git a/packages/contracts/src/contracts/global-params/simulate.ts b/packages/contracts/src/contracts/global-params/simulate.ts new file mode 100644 index 00000000..2fc0e7d3 --- /dev/null +++ b/packages/contracts/src/contracts/global-params/simulate.ts @@ -0,0 +1,220 @@ +import type { Address, Hex, PublicClient, WalletClient, Chain } from "../../lib"; +import { GLOBAL_PARAMS_ABI } from "./abi"; +import { requireSigner, requireAccount } from "../../utils/account"; +import { simulateWithErrorDecode } from "../../errors"; +import type { GlobalParamsSimulate } from "./types"; +import type { CallSignerOptions } from "../../client/types"; + +/** + * Builds simulate methods for GlobalParams write calls. + * Each method calls simulateContract against the current chain state and throws a typed + * SDK error on revert, decoded via simulateWithErrorDecode. + * @param address - Deployed GlobalParams contract address + * @param publicClient - Viem PublicClient used to call simulateContract + * @param walletClient - Viem WalletClient used to resolve the account for simulation + * @param chain - Chain passed to simulateContract + * @returns Simulation methods bound to the given contract address + */ +export function createGlobalParamsSimulate( + address: Address, + publicClient: PublicClient, + walletClient: WalletClient | null, + chain: Chain, +): GlobalParamsSimulate { + const contract = { address, abi: GLOBAL_PARAMS_ABI } as const; + + return { + async enlistPlatform(platformHash: Hex, platformAdminAddress: Address, platformFeePercent: bigint, platformAdapter: Address, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "enlistPlatform", + args: [platformHash, platformAdminAddress, platformFeePercent, platformAdapter], + }), + ); + }, + async delistPlatform(platformBytes: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "delistPlatform", + args: [platformBytes], + }), + ); + }, + async updatePlatformAdminAddress(platformBytes: Hex, platformAdminAddress: Address, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "updatePlatformAdminAddress", + args: [platformBytes, platformAdminAddress], + }), + ); + }, + async updatePlatformClaimDelay(platformBytes: Hex, claimDelay: bigint, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "updatePlatformClaimDelay", + args: [platformBytes, claimDelay], + }), + ); + }, + async updateProtocolAdminAddress(protocolAdminAddress: Address, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "updateProtocolAdminAddress", + args: [protocolAdminAddress], + }), + ); + }, + async updateProtocolFeePercent(protocolFeePercent: bigint, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "updateProtocolFeePercent", + args: [protocolFeePercent], + }), + ); + }, + async setPlatformAdapter(platformBytes: Hex, platformAdapter: Address, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "setPlatformAdapter", + args: [platformBytes, platformAdapter], + }), + ); + }, + async setPlatformLineItemType(platformHash: Hex, typeId: Hex, label: string, countsTowardGoal: boolean, applyProtocolFee: boolean, canRefund: boolean, instantTransfer: boolean, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "setPlatformLineItemType", + args: [platformHash, typeId, label, countsTowardGoal, applyProtocolFee, canRefund, instantTransfer], + }), + ); + }, + async removePlatformLineItemType(platformHash: Hex, typeId: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "removePlatformLineItemType", + args: [platformHash, typeId], + }), + ); + }, + async addTokenToCurrency(currency: Hex, token: Address, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "addTokenToCurrency", + args: [currency, token], + }), + ); + }, + async removeTokenFromCurrency(currency: Hex, token: Address, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "removeTokenFromCurrency", + args: [currency, token], + }), + ); + }, + async addPlatformData(platformBytes: Hex, platformDataKey: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "addPlatformData", + args: [platformBytes, platformDataKey], + }), + ); + }, + async removePlatformData(platformBytes: Hex, platformDataKey: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "removePlatformData", + args: [platformBytes, platformDataKey], + }), + ); + }, + async addToRegistry(key: Hex, value: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "addToRegistry", + args: [key, value], + }), + ); + }, + async transferOwnership(newOwner: Address, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "transferOwnership", + args: [newOwner], + }), + ); + }, + async renounceOwnership(options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "renounceOwnership", + args: [], + }), + ); + }, + }; +} diff --git a/packages/contracts/src/contracts/global-params/types.ts b/packages/contracts/src/contracts/global-params/types.ts new file mode 100644 index 00000000..fe2d7c12 --- /dev/null +++ b/packages/contracts/src/contracts/global-params/types.ts @@ -0,0 +1,118 @@ +import type { Address, Hex } from "../../lib"; +import type { LineItemTypeInfo } from "../../types/structs"; +import type { CallSignerOptions } from "../../client/types"; + +/** Read-only methods for a GlobalParams contract instance. */ +export interface GlobalParamsReads { + /** Returns the protocol admin address. */ + getProtocolAdminAddress(): Promise
; + /** Returns the protocol fee percent (basis points). */ + getProtocolFeePercent(): Promise; + /** Returns the total number of enlisted platforms. */ + getNumberOfListedPlatforms(): Promise; + /** Returns true if the platform identified by platformBytes is enlisted. */ + checkIfPlatformIsListed(platformBytes: Hex): Promise; + /** Returns true if platformDataKey is a valid registered data key. */ + checkIfPlatformDataKeyValid(platformDataKey: Hex): Promise; + /** Returns the admin address for the given platform. */ + getPlatformAdminAddress(platformBytes: Hex): Promise
; + /** Returns the fee percent for the given platform (basis points). */ + getPlatformFeePercent(platformBytes: Hex): Promise; + /** Returns the claim delay for the given platform in seconds. */ + getPlatformClaimDelay(platformBytes: Hex): Promise; + /** Returns the adapter contract address for the given platform. */ + getPlatformAdapter(platformBytes: Hex): Promise
; + /** Returns the platform that owns the given data key. */ + getPlatformDataOwner(platformDataKey: Hex): Promise; + /** Returns the line item type configuration for a platform + typeId pair. */ + getPlatformLineItemType(platformHash: Hex, typeId: Hex): Promise; + /** Returns the list of accepted token addresses for the given currency. */ + getTokensForCurrency(currency: Hex): Promise; + /** Returns a value from the global data registry by key. */ + getFromRegistry(key: Hex): Promise; + /** Returns the contract owner address. */ + owner(): Promise
; +} + +/** Write methods for a GlobalParams contract instance. */ +export interface GlobalParamsWrites { + /** Enlists a new platform with admin address, fee percent, and adapter. */ + enlistPlatform(platformHash: Hex, platformAdminAddress: Address, platformFeePercent: bigint, platformAdapter: Address, options?: CallSignerOptions): Promise; + /** Removes a previously enlisted platform. */ + delistPlatform(platformBytes: Hex, options?: CallSignerOptions): Promise; + /** Updates the admin address for an enlisted platform. */ + updatePlatformAdminAddress(platformBytes: Hex, platformAdminAddress: Address, options?: CallSignerOptions): Promise; + /** Updates the claim delay for an enlisted platform. */ + updatePlatformClaimDelay(platformBytes: Hex, claimDelay: bigint, options?: CallSignerOptions): Promise; + /** Updates the protocol admin address. */ + updateProtocolAdminAddress(protocolAdminAddress: Address, options?: CallSignerOptions): Promise; + /** Updates the protocol fee percent. */ + updateProtocolFeePercent(protocolFeePercent: bigint, options?: CallSignerOptions): Promise; + /** Sets the adapter contract for a platform. */ + setPlatformAdapter(platformBytes: Hex, platformAdapter: Address, options?: CallSignerOptions): Promise; + /** Registers or updates a line item type for a platform. */ + setPlatformLineItemType(platformHash: Hex, typeId: Hex, label: string, countsTowardGoal: boolean, applyProtocolFee: boolean, canRefund: boolean, instantTransfer: boolean, options?: CallSignerOptions): Promise; + /** Removes a line item type for a platform. */ + removePlatformLineItemType(platformHash: Hex, typeId: Hex, options?: CallSignerOptions): Promise; + /** Adds a token to the accepted list for a currency. */ + addTokenToCurrency(currency: Hex, token: Address, options?: CallSignerOptions): Promise; + /** Removes a token from the accepted list for a currency. */ + removeTokenFromCurrency(currency: Hex, token: Address, options?: CallSignerOptions): Promise; + /** Associates a platform data key with a platform. */ + addPlatformData(platformBytes: Hex, platformDataKey: Hex, options?: CallSignerOptions): Promise; + /** Removes a platform data key association. */ + removePlatformData(platformBytes: Hex, platformDataKey: Hex, options?: CallSignerOptions): Promise; + /** Adds a key-value entry to the global data registry. */ + addToRegistry(key: Hex, value: Hex, options?: CallSignerOptions): Promise; + /** Transfers contract ownership to a new address. */ + transferOwnership(newOwner: Address, options?: CallSignerOptions): Promise; + /** Renounces contract ownership permanently. */ + renounceOwnership(options?: CallSignerOptions): Promise; +} + +/** Simulate counterparts for GlobalParams write methods. */ +export interface GlobalParamsSimulate { + /** Simulates enlistPlatform; throws a typed error on revert. */ + enlistPlatform(platformHash: Hex, platformAdminAddress: Address, platformFeePercent: bigint, platformAdapter: Address, options?: CallSignerOptions): Promise; + /** Simulates delistPlatform; throws a typed error on revert. */ + delistPlatform(platformBytes: Hex, options?: CallSignerOptions): Promise; + /** Simulates updatePlatformAdminAddress; throws a typed error on revert. */ + updatePlatformAdminAddress(platformBytes: Hex, platformAdminAddress: Address, options?: CallSignerOptions): Promise; + /** Simulates updatePlatformClaimDelay; throws a typed error on revert. */ + updatePlatformClaimDelay(platformBytes: Hex, claimDelay: bigint, options?: CallSignerOptions): Promise; + /** Simulates updateProtocolAdminAddress; throws a typed error on revert. */ + updateProtocolAdminAddress(protocolAdminAddress: Address, options?: CallSignerOptions): Promise; + /** Simulates updateProtocolFeePercent; throws a typed error on revert. */ + updateProtocolFeePercent(protocolFeePercent: bigint, options?: CallSignerOptions): Promise; + /** Simulates setPlatformAdapter; throws a typed error on revert. */ + setPlatformAdapter(platformBytes: Hex, platformAdapter: Address, options?: CallSignerOptions): Promise; + /** Simulates setPlatformLineItemType; throws a typed error on revert. */ + setPlatformLineItemType(platformHash: Hex, typeId: Hex, label: string, countsTowardGoal: boolean, applyProtocolFee: boolean, canRefund: boolean, instantTransfer: boolean, options?: CallSignerOptions): Promise; + /** Simulates removePlatformLineItemType; throws a typed error on revert. */ + removePlatformLineItemType(platformHash: Hex, typeId: Hex, options?: CallSignerOptions): Promise; + /** Simulates addTokenToCurrency; throws a typed error on revert. */ + addTokenToCurrency(currency: Hex, token: Address, options?: CallSignerOptions): Promise; + /** Simulates removeTokenFromCurrency; throws a typed error on revert. */ + removeTokenFromCurrency(currency: Hex, token: Address, options?: CallSignerOptions): Promise; + /** Simulates addPlatformData; throws a typed error on revert. */ + addPlatformData(platformBytes: Hex, platformDataKey: Hex, options?: CallSignerOptions): Promise; + /** Simulates removePlatformData; throws a typed error on revert. */ + removePlatformData(platformBytes: Hex, platformDataKey: Hex, options?: CallSignerOptions): Promise; + /** Simulates addToRegistry; throws a typed error on revert. */ + addToRegistry(key: Hex, value: Hex, options?: CallSignerOptions): Promise; + /** Simulates transferOwnership; throws a typed error on revert. */ + transferOwnership(newOwner: Address, options?: CallSignerOptions): Promise; + /** Simulates renounceOwnership; throws a typed error on revert. */ + renounceOwnership(options?: CallSignerOptions): Promise; +} + +/** Event helpers for a GlobalParams contract instance. */ +export interface GlobalParamsEvents {} + +/** Full GlobalParams entity combining reads, writes, simulate, and events. */ +export type GlobalParamsEntity = GlobalParamsReads & GlobalParamsWrites & { + /** Simulation counterparts for every write method. */ + simulate: GlobalParamsSimulate; + /** Event helpers for filtering and watching logs. */ + events: GlobalParamsEvents; +}; diff --git a/packages/contracts/src/contracts/global-params/writes.ts b/packages/contracts/src/contracts/global-params/writes.ts new file mode 100644 index 00000000..24f23475 --- /dev/null +++ b/packages/contracts/src/contracts/global-params/writes.ts @@ -0,0 +1,87 @@ +import type { Address, Hex, WalletClient, Chain } from "../../lib"; +import { GLOBAL_PARAMS_ABI } from "./abi"; +import { requireSigner, requireAccount } from "../../utils/account"; +import type { GlobalParamsWrites } from "./types"; +import type { CallSignerOptions } from "../../client/types"; + +/** + * Builds write methods for a GlobalParams contract instance. + * @param address - Deployed GlobalParams contract address + * @param walletClient - Viem WalletClient used to call writeContract; must have an account attached + * @param chain - Chain passed to writeContract for EIP-1559 and replay protection + * @returns Write methods bound to the given contract address + */ +export function createGlobalParamsWrites( + address: Address, + walletClient: WalletClient | null, + chain: Chain, +): GlobalParamsWrites { + const contract = { address, abi: GLOBAL_PARAMS_ABI } as const; + + return { + async enlistPlatform(platformHash: Hex, platformAdminAddress: Address, platformFeePercent: bigint, platformAdapter: Address, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "enlistPlatform", args: [platformHash, platformAdminAddress, platformFeePercent, platformAdapter] }); + }, + async delistPlatform(platformBytes: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "delistPlatform", args: [platformBytes] }); + }, + async updatePlatformAdminAddress(platformBytes: Hex, platformAdminAddress: Address, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "updatePlatformAdminAddress", args: [platformBytes, platformAdminAddress] }); + }, + async updatePlatformClaimDelay(platformBytes: Hex, claimDelay: bigint, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "updatePlatformClaimDelay", args: [platformBytes, claimDelay] }); + }, + async updateProtocolAdminAddress(protocolAdminAddress: Address, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "updateProtocolAdminAddress", args: [protocolAdminAddress] }); + }, + async updateProtocolFeePercent(protocolFeePercent: bigint, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "updateProtocolFeePercent", args: [protocolFeePercent] }); + }, + async setPlatformAdapter(platformBytes: Hex, platformAdapter: Address, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "setPlatformAdapter", args: [platformBytes, platformAdapter] }); + }, + async setPlatformLineItemType(platformHash: Hex, typeId: Hex, label: string, countsTowardGoal: boolean, applyProtocolFee: boolean, canRefund: boolean, instantTransfer: boolean, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "setPlatformLineItemType", args: [platformHash, typeId, label, countsTowardGoal, applyProtocolFee, canRefund, instantTransfer] }); + }, + async removePlatformLineItemType(platformHash: Hex, typeId: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "removePlatformLineItemType", args: [platformHash, typeId] }); + }, + async addTokenToCurrency(currency: Hex, token: Address, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "addTokenToCurrency", args: [currency, token] }); + }, + async removeTokenFromCurrency(currency: Hex, token: Address, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "removeTokenFromCurrency", args: [currency, token] }); + }, + async addPlatformData(platformBytes: Hex, platformDataKey: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "addPlatformData", args: [platformBytes, platformDataKey] }); + }, + async removePlatformData(platformBytes: Hex, platformDataKey: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "removePlatformData", args: [platformBytes, platformDataKey] }); + }, + async addToRegistry(key: Hex, value: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "addToRegistry", args: [key, value] }); + }, + async transferOwnership(newOwner: Address, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "transferOwnership", args: [newOwner] }); + }, + async renounceOwnership(options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "renounceOwnership", args: [] }); + }, + }; +} diff --git a/packages/contracts/src/contracts/index.ts b/packages/contracts/src/contracts/index.ts new file mode 100644 index 00000000..44a1f557 --- /dev/null +++ b/packages/contracts/src/contracts/index.ts @@ -0,0 +1,8 @@ +export { createGlobalParamsEntity } from "./global-params"; +export { createCampaignInfoFactoryEntity } from "./campaign-info-factory"; +export { createTreasuryFactoryEntity } from "./treasury-factory"; +export { createCampaignInfoEntity } from "./campaign-info"; +export { createPaymentTreasuryEntity } from "./payment-treasury"; +export { createAllOrNothingEntity } from "./all-or-nothing"; +export { createKeepWhatsRaisedEntity } from "./keep-whats-raised"; +export { createItemRegistryEntity } from "./item-registry"; diff --git a/packages/contracts/src/contracts/item-registry/abi.ts b/packages/contracts/src/contracts/item-registry/abi.ts new file mode 100644 index 00000000..274f8691 --- /dev/null +++ b/packages/contracts/src/contracts/item-registry/abi.ts @@ -0,0 +1,76 @@ +/** ItemRegistry ABI — typed const, co-located with the contract that uses it. */ +const ITEM_COMPONENTS = [ + { internalType: "uint256", name: "actualWeight", type: "uint256" }, + { internalType: "uint256", name: "height", type: "uint256" }, + { internalType: "uint256", name: "width", type: "uint256" }, + { internalType: "uint256", name: "length", type: "uint256" }, + { internalType: "bytes32", name: "category", type: "bytes32" }, + { internalType: "bytes32", name: "declaredCurrency", type: "bytes32" }, +] as const; + +export const ITEM_REGISTRY_ABI = [ + { inputs: [], name: "ItemRegistryMismatchedArraysLength", type: "error" }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "owner", type: "address" }, + { indexed: true, internalType: "bytes32", name: "itemId", type: "bytes32" }, + { + components: ITEM_COMPONENTS, + indexed: false, + internalType: "struct IItem.Item", + name: "item", + type: "tuple", + }, + ], + name: "ItemAdded", + type: "event", + }, + { + inputs: [ + { internalType: "bytes32", name: "itemId", type: "bytes32" }, + { + components: ITEM_COMPONENTS, + internalType: "struct IItem.Item", + name: "item", + type: "tuple", + }, + ], + name: "addItem", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32[]", name: "itemIds", type: "bytes32[]" }, + { + components: [...ITEM_COMPONENTS], + internalType: "struct IItem.Item[]", + name: "items", + type: "tuple[]", + }, + ], + name: "addItemsBatch", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "owner", type: "address" }, + { internalType: "bytes32", name: "itemId", type: "bytes32" }, + ], + name: "getItem", + outputs: [ + { + components: ITEM_COMPONENTS, + internalType: "struct IItem.Item", + name: "", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, +] as const; diff --git a/packages/contracts/src/contracts/item-registry/events.ts b/packages/contracts/src/contracts/item-registry/events.ts new file mode 100644 index 00000000..644ad3e5 --- /dev/null +++ b/packages/contracts/src/contracts/item-registry/events.ts @@ -0,0 +1,18 @@ +import type { Address, PublicClient } from "../../lib"; +import type { ItemRegistryEvents } from "./types"; + +// TODO: Add event filter factories (filterItemAdded), log decoder (decodeLog), +// and watcher factories using getLogs / watchEvent. + +/** + * Builds event helpers for an ItemRegistry contract instance. + * @param _address - Deployed ItemRegistry contract address + * @param _publicClient - Viem PublicClient used to call getLogs + * @returns Event helpers bound to the given contract address + */ +export function createItemRegistryEvents( + _address: Address, + _publicClient: PublicClient, +): ItemRegistryEvents { + return {}; +} diff --git a/packages/contracts/src/contracts/item-registry/index.ts b/packages/contracts/src/contracts/item-registry/index.ts new file mode 100644 index 00000000..35ad4275 --- /dev/null +++ b/packages/contracts/src/contracts/item-registry/index.ts @@ -0,0 +1,30 @@ +import type { Address, PublicClient, WalletClient, Chain } from "../../lib"; +import { createItemRegistryReads } from "./reads"; +import { createItemRegistryWrites } from "./writes"; +import { createItemRegistrySimulate } from "./simulate"; +import { createItemRegistryEvents } from "./events"; +import type { ItemRegistryEntity } from "./types"; + +/** + * Creates a fully composed ItemRegistry entity combining reads, writes, simulate, and events. + * @param address - Deployed ItemRegistry contract address + * @param publicClient - Viem PublicClient used for reads and simulation + * @param walletClient - Viem WalletClient used for writes; must have an account attached + * @param chain - Chain passed to writeContract and simulateContract + * @returns Composed entity exposing all ItemRegistry methods under a single object + */ +export function createItemRegistryEntity( + address: Address, + publicClient: PublicClient, + walletClient: WalletClient | null, + chain: Chain, +): ItemRegistryEntity { + return { + ...createItemRegistryReads(address, publicClient), + ...createItemRegistryWrites(address, walletClient, chain), + simulate: createItemRegistrySimulate(address, publicClient, walletClient, chain), + events: createItemRegistryEvents(address, publicClient), + }; +} + +export type { ItemRegistryEntity } from "./types"; diff --git a/packages/contracts/src/contracts/item-registry/reads.ts b/packages/contracts/src/contracts/item-registry/reads.ts new file mode 100644 index 00000000..cbbe73c8 --- /dev/null +++ b/packages/contracts/src/contracts/item-registry/reads.ts @@ -0,0 +1,28 @@ +import type { Address, Hex, PublicClient } from "../../lib"; +import { ITEM_REGISTRY_ABI } from "./abi"; +import type { ItemRegistryReads } from "./types"; +import type { Item } from "../../types/structs"; + +/** + * Builds read methods for an ItemRegistry contract instance. + * @param address - Deployed ItemRegistry contract address + * @param publicClient - Viem PublicClient used to call readContract + * @returns Read methods bound to the given contract address + */ +export function createItemRegistryReads( + address: Address, + publicClient: PublicClient, +): ItemRegistryReads { + const contract = { address, abi: ITEM_REGISTRY_ABI } as const; + + return { + async getItem(owner: Address, itemId: Hex): Promise { + const result = await publicClient.readContract({ + ...contract, + functionName: "getItem", + args: [owner, itemId], + }); + return result as unknown as Item; + }, + }; +} diff --git a/packages/contracts/src/contracts/item-registry/simulate.ts b/packages/contracts/src/contracts/item-registry/simulate.ts new file mode 100644 index 00000000..df16d38c --- /dev/null +++ b/packages/contracts/src/contracts/item-registry/simulate.ts @@ -0,0 +1,73 @@ +import type { Address, Hex, PublicClient, WalletClient, Chain } from "../../lib"; +import { ITEM_REGISTRY_ABI } from "./abi"; +import { requireSigner, requireAccount } from "../../utils/account"; +import { simulateWithErrorDecode } from "../../errors"; +import type { ItemRegistrySimulate } from "./types"; +import type { Item } from "../../types/structs"; +import type { CallSignerOptions } from "../../client/types"; + +/** + * Builds simulate methods for ItemRegistry write calls. + * Each method calls simulateContract against the current chain state and throws a typed + * SDK error on revert, decoded via simulateWithErrorDecode. + * @param address - Deployed ItemRegistry contract address + * @param publicClient - Viem PublicClient used to call simulateContract + * @param walletClient - Viem WalletClient used to resolve the account for simulation + * @param chain - Chain passed to simulateContract + * @returns Simulation methods bound to the given contract address + */ +export function createItemRegistrySimulate( + address: Address, + publicClient: PublicClient, + walletClient: WalletClient | null, + chain: Chain, +): ItemRegistrySimulate { + const contract = { address, abi: ITEM_REGISTRY_ABI } as const; + + return { + async addItem(itemId: Hex, item: Item, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "addItem", + args: [ + itemId, + { + actualWeight: item.actualWeight, + height: item.height, + width: item.width, + length: item.length, + category: item.category, + declaredCurrency: item.declaredCurrency, + }, + ], + }), + ); + }, + async addItemsBatch(itemIds: readonly Hex[], items: readonly Item[], options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "addItemsBatch", + args: [ + [...itemIds], + items.map((item) => ({ + actualWeight: item.actualWeight, + height: item.height, + width: item.width, + length: item.length, + category: item.category, + declaredCurrency: item.declaredCurrency, + })), + ], + }), + ); + }, + }; +} diff --git a/packages/contracts/src/contracts/item-registry/types.ts b/packages/contracts/src/contracts/item-registry/types.ts new file mode 100644 index 00000000..0d3a5281 --- /dev/null +++ b/packages/contracts/src/contracts/item-registry/types.ts @@ -0,0 +1,35 @@ +import type { Address, Hex } from "../../lib"; +import type { Item } from "../../types/structs"; +import type { CallSignerOptions } from "../../client/types"; + +/** Read-only methods for ItemRegistry. */ +export interface ItemRegistryReads { + /** Returns the Item struct registered under the given owner address and item ID. */ + getItem(owner: Address, itemId: Hex): Promise; +} + +/** Write methods for ItemRegistry. */ +export interface ItemRegistryWrites { + /** Registers a single item under the caller's address with the given item ID. */ + addItem(itemId: Hex, item: Item, options?: CallSignerOptions): Promise; + /** Registers multiple items in a single transaction; itemIds and items must be the same length. */ + addItemsBatch(itemIds: readonly Hex[], items: readonly Item[], options?: CallSignerOptions): Promise; +} + +/** Simulate counterparts for ItemRegistry write methods. */ +export interface ItemRegistrySimulate { + /** Simulates addItem; throws a typed error on revert. */ + addItem(itemId: Hex, item: Item, options?: CallSignerOptions): Promise; + /** Simulates addItemsBatch; throws a typed error on revert. */ + addItemsBatch(itemIds: readonly Hex[], items: readonly Item[], options?: CallSignerOptions): Promise; +} + +/** Event helpers for ItemRegistry. */ +export interface ItemRegistryEvents {} + +/** Full ItemRegistry entity (reads, writes, simulate, events). */ +export type ItemRegistryEntity = ItemRegistryReads & + ItemRegistryWrites & { + simulate: ItemRegistrySimulate; + events: ItemRegistryEvents; + }; diff --git a/packages/contracts/src/contracts/item-registry/writes.ts b/packages/contracts/src/contracts/item-registry/writes.ts new file mode 100644 index 00000000..65cf8b88 --- /dev/null +++ b/packages/contracts/src/contracts/item-registry/writes.ts @@ -0,0 +1,64 @@ +import type { Address, Hex, WalletClient, Chain } from "../../lib"; +import { ITEM_REGISTRY_ABI } from "./abi"; +import { requireSigner, requireAccount } from "../../utils/account"; +import type { ItemRegistryWrites } from "./types"; +import type { Item } from "../../types/structs"; +import type { CallSignerOptions } from "../../client/types"; + +/** + * Builds write methods for an ItemRegistry contract instance. + * @param address - Deployed ItemRegistry contract address + * @param walletClient - Viem WalletClient used to call writeContract; must have an account attached + * @param chain - Chain passed to writeContract for EIP-1559 and replay protection + * @returns Write methods bound to the given contract address + */ +export function createItemRegistryWrites( + address: Address, + walletClient: WalletClient | null, + chain: Chain, +): ItemRegistryWrites { + const contract = { address, abi: ITEM_REGISTRY_ABI } as const; + + return { + async addItem(itemId: Hex, item: Item, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "addItem", + args: [ + itemId, + { + actualWeight: item.actualWeight, + height: item.height, + width: item.width, + length: item.length, + category: item.category, + declaredCurrency: item.declaredCurrency, + }, + ], + }); + }, + async addItemsBatch(itemIds: readonly Hex[], items: readonly Item[], options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "addItemsBatch", + args: [ + [...itemIds], + items.map((item) => ({ + actualWeight: item.actualWeight, + height: item.height, + width: item.width, + length: item.length, + category: item.category, + declaredCurrency: item.declaredCurrency, + })), + ], + }); + }, + }; +} diff --git a/packages/contracts/src/contracts/keep-whats-raised/abi.ts b/packages/contracts/src/contracts/keep-whats-raised/abi.ts new file mode 100644 index 00000000..99394ef5 --- /dev/null +++ b/packages/contracts/src/contracts/keep-whats-raised/abi.ts @@ -0,0 +1,699 @@ +/** KeepWhatsRaised ABI — typed const, co-located with the contract that uses it. PledgeId-based pledges and withdraw(token, amount). */ +const REWARD_TIER_COMPONENTS = [ + { internalType: "uint256", name: "rewardValue", type: "uint256" }, + { internalType: "bool", name: "isRewardTier", type: "bool" }, + { internalType: "bytes32[]", name: "itemId", type: "bytes32[]" }, + { internalType: "uint256[]", name: "itemValue", type: "uint256[]" }, + { internalType: "uint256[]", name: "itemQuantity", type: "uint256[]" }, +] as const; + +const CONFIG_COMPONENTS = [ + { internalType: "uint256", name: "minimumWithdrawalForFeeExemption", type: "uint256" }, + { internalType: "uint256", name: "withdrawalDelay", type: "uint256" }, + { internalType: "uint256", name: "refundDelay", type: "uint256" }, + { internalType: "uint256", name: "configLockPeriod", type: "uint256" }, + { internalType: "bool", name: "isColombianCreator", type: "bool" }, +] as const; + +const FEE_KEYS_COMPONENTS = [ + { internalType: "bytes32", name: "flatFeeKey", type: "bytes32" }, + { internalType: "bytes32", name: "cumulativeFlatFeeKey", type: "bytes32" }, + { internalType: "bytes32[]", name: "grossPercentageFeeKeys", type: "bytes32[]" }, +] as const; + +const FEE_VALUES_COMPONENTS = [ + { internalType: "uint256", name: "flatFeeValue", type: "uint256" }, + { internalType: "uint256", name: "cumulativeFlatFeeValue", type: "uint256" }, + { internalType: "uint256[]", name: "grossPercentageFeeValues", type: "uint256[]" }, +] as const; + +const CAMPAIGN_DATA_COMPONENTS = [ + { internalType: "uint256", name: "launchTime", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + { internalType: "uint256", name: "goalAmount", type: "uint256" }, + { internalType: "bytes32", name: "currency", type: "bytes32" }, +] as const; + +export const KEEP_WHATS_RAISED_ABI = [ + { inputs: [], name: "AccessCheckerUnauthorized", type: "error" }, + { inputs: [], name: "KeepWhatsRaisedUnAuthorized", type: "error" }, + { inputs: [], name: "KeepWhatsRaisedInvalidInput", type: "error" }, + { + inputs: [{ internalType: "address", name: "token", type: "address" }], + name: "KeepWhatsRaisedTokenNotAccepted", + type: "error", + }, + { inputs: [], name: "KeepWhatsRaisedRewardExists", type: "error" }, + { inputs: [], name: "KeepWhatsRaisedDisabled", type: "error" }, + { inputs: [], name: "KeepWhatsRaisedAlreadyEnabled", type: "error" }, + { + inputs: [ + { internalType: "uint256", name: "availableAmount", type: "uint256" }, + { internalType: "uint256", name: "withdrawalAmount", type: "uint256" }, + { internalType: "uint256", name: "fee", type: "uint256" }, + ], + name: "KeepWhatsRaisedInsufficientFundsForWithdrawalAndFee", + type: "error", + }, + { + inputs: [ + { internalType: "uint256", name: "withdrawalAmount", type: "uint256" }, + { internalType: "uint256", name: "fee", type: "uint256" }, + ], + name: "KeepWhatsRaisedInsufficientFundsForFee", + type: "error", + }, + { inputs: [], name: "KeepWhatsRaisedAlreadyWithdrawn", type: "error" }, + { inputs: [], name: "KeepWhatsRaisedAlreadyClaimed", type: "error" }, + { + inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], + name: "KeepWhatsRaisedNotClaimable", + type: "error", + }, + { inputs: [], name: "KeepWhatsRaisedNotClaimableAdmin", type: "error" }, + { inputs: [], name: "KeepWhatsRaisedConfigLocked", type: "error" }, + { inputs: [], name: "KeepWhatsRaisedDisbursementBlocked", type: "error" }, + { + inputs: [{ internalType: "bytes32", name: "pledgeId", type: "bytes32" }], + name: "KeepWhatsRaisedPledgeAlreadyProcessed", + type: "error", + }, + { inputs: [], name: "TreasuryCampaignInfoIsPaused", type: "error" }, + { inputs: [], name: "TreasuryFeeNotDisbursed", type: "error" }, + { inputs: [], name: "TreasuryTransferFailed", type: "error" }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "owner", type: "address" }, + { indexed: true, internalType: "address", name: "approved", type: "address" }, + { indexed: true, internalType: "uint256", name: "tokenId", type: "uint256" }, + ], + name: "Approval", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "owner", type: "address" }, + { indexed: true, internalType: "address", name: "operator", type: "address" }, + { indexed: false, internalType: "bool", name: "approved", type: "bool" }, + ], + name: "ApprovalForAll", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "token", type: "address" }, + { indexed: false, internalType: "uint256", name: "protocolShare", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "platformShare", type: "uint256" }, + ], + name: "FeesDisbursed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: "address", name: "account", type: "address" }, + { indexed: false, internalType: "bytes32", name: "message", type: "bytes32" }, + ], + name: "Paused", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "backer", type: "address" }, + { indexed: true, internalType: "address", name: "pledgeToken", type: "address" }, + { indexed: false, internalType: "bytes32", name: "reward", type: "bytes32" }, + { indexed: false, internalType: "uint256", name: "pledgeAmount", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "tip", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "tokenId", type: "uint256" }, + { indexed: false, internalType: "bytes32[]", name: "rewards", type: "bytes32[]" }, + ], + name: "Receipt", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: "bytes32[]", name: "rewardNames", type: "bytes32[]" }, + { + components: [...REWARD_TIER_COMPONENTS], + indexed: false, + internalType: "struct IReward.Reward[]", + name: "rewards", + type: "tuple[]", + }, + ], + name: "RewardsAdded", + type: "event", + }, + { + anonymous: false, + inputs: [{ indexed: true, internalType: "bytes32", name: "rewardName", type: "bytes32" }], + name: "RewardRemoved", + type: "event", + }, + { + anonymous: false, + inputs: [], + name: "WithdrawalApproved", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + components: [...CONFIG_COMPONENTS], + indexed: false, + internalType: "struct KeepWhatsRaised.Config", + name: "config", + type: "tuple", + }, + { + components: [...CAMPAIGN_DATA_COMPONENTS], + indexed: false, + internalType: "struct ICampaignData.CampaignData", + name: "campaignData", + type: "tuple", + }, + { + components: [...FEE_KEYS_COMPONENTS], + indexed: false, + internalType: "struct KeepWhatsRaised.FeeKeys", + name: "feeKeys", + type: "tuple", + }, + { + components: [...FEE_VALUES_COMPONENTS], + indexed: false, + internalType: "struct KeepWhatsRaised.FeeValues", + name: "feeValues", + type: "tuple", + }, + ], + name: "TreasuryConfigured", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: "uint256", name: "amount", type: "uint256" }, + { indexed: true, internalType: "address", name: "claimer", type: "address" }, + ], + name: "TipClaimed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: "uint256", name: "amount", type: "uint256" }, + { indexed: true, internalType: "address", name: "claimer", type: "address" }, + ], + name: "FundClaimed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "uint256", name: "tokenId", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "refundAmount", type: "uint256" }, + { indexed: true, internalType: "address", name: "claimer", type: "address" }, + ], + name: "RefundClaimed", + type: "event", + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: "uint256", name: "newDeadline", type: "uint256" }], + name: "KeepWhatsRaisedDeadlineUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: "uint256", name: "newGoalAmount", type: "uint256" }], + name: "KeepWhatsRaisedGoalAmountUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "pledgeId", type: "bytes32" }, + { indexed: false, internalType: "uint256", name: "fee", type: "uint256" }, + ], + name: "KeepWhatsRaisedPaymentGatewayFeeSet", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "to", type: "address" }, + { indexed: false, internalType: "uint256", name: "amount", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "fee", type: "uint256" }, + ], + name: "WithdrawalWithFeeSuccessful", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "from", type: "address" }, + { indexed: true, internalType: "address", name: "to", type: "address" }, + { indexed: true, internalType: "uint256", name: "tokenId", type: "uint256" }, + ], + name: "Transfer", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: "address", name: "account", type: "address" }, + { indexed: false, internalType: "bytes32", name: "message", type: "bytes32" }, + ], + name: "Unpaused", + type: "event", + }, + { + inputs: [ + { internalType: "bytes32", name: "_platformHash", type: "bytes32" }, + { internalType: "address", name: "_infoAddress", type: "address" }, + { internalType: "address", name: "_trustedForwarder", type: "address" }, + ], + name: "initialize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "message", type: "bytes32" }], + name: "pauseTreasury", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "message", type: "bytes32" }], + name: "unpauseTreasury", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "message", type: "bytes32" }], + name: "cancelTreasury", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "cancelled", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32[]", name: "rewardNames", type: "bytes32[]" }, + { + components: [...REWARD_TIER_COMPONENTS], + internalType: "struct KeepWhatsRaised.Reward[]", + name: "rewards", + type: "tuple[]", + }, + ], + name: "addRewards", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "tokenId", type: "uint256" }, + ], + name: "approve", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "owner", type: "address" }], + name: "balanceOf", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], + name: "claimRefund", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "disburseFees", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], + name: "getApproved", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getAvailableRaisedAmount", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getWithdrawalApprovalStatus", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getLaunchTime", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getDeadline", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getGoalAmount", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "pledgeId", type: "bytes32" }], + name: "getPaymentGatewayFee", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "feeKey", type: "bytes32" }], + name: "getFeeValue", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "pledgeId", type: "bytes32" }, + { internalType: "uint256", name: "fee", type: "uint256" }, + ], + name: "setPaymentGatewayFee", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "approveWithdrawal", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [...CONFIG_COMPONENTS], + internalType: "struct KeepWhatsRaised.Config", + name: "config", + type: "tuple", + }, + { + components: [...CAMPAIGN_DATA_COMPONENTS], + internalType: "struct ICampaignData.CampaignData", + name: "campaignData", + type: "tuple", + }, + { + components: [...FEE_KEYS_COMPONENTS], + internalType: "struct KeepWhatsRaised.FeeKeys", + name: "feeKeys", + type: "tuple", + }, + { + components: [...FEE_VALUES_COMPONENTS], + internalType: "struct KeepWhatsRaised.FeeValues", + name: "feeValues", + type: "tuple", + }, + ], + name: "configureTreasury", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "deadline", type: "uint256" }], + name: "updateDeadline", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "goalAmount", type: "uint256" }], + name: "updateGoalAmount", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "pledgeId", type: "bytes32" }, + { internalType: "address", name: "backer", type: "address" }, + { internalType: "address", name: "pledgeToken", type: "address" }, + { internalType: "uint256", name: "pledgeAmount", type: "uint256" }, + { internalType: "uint256", name: "tip", type: "uint256" }, + { internalType: "uint256", name: "fee", type: "uint256" }, + { internalType: "bytes32[]", name: "reward", type: "bytes32[]" }, + { internalType: "bool", name: "isPledgeForAReward", type: "bool" }, + ], + name: "setFeeAndPledge", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "claimTip", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "claimFund", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "getLifetimeRaisedAmount", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getRaisedAmount", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "rewardName", type: "bytes32" }], + name: "getReward", + outputs: [ + { + components: REWARD_TIER_COMPONENTS, + internalType: "struct KeepWhatsRaised.Reward", + name: "reward", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getPlatformHash", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getRefundedAmount", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getPlatformFeePercent", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "owner", type: "address" }, + { internalType: "address", name: "operator", type: "address" }, + ], + name: "isApprovedForAll", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "name", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], + name: "ownerOf", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "paused", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "pledgeId", type: "bytes32" }, + { internalType: "address", name: "backer", type: "address" }, + { internalType: "address", name: "pledgeToken", type: "address" }, + { internalType: "uint256", name: "tip", type: "uint256" }, + { internalType: "bytes32[]", name: "rewardNames", type: "bytes32[]" }, + ], + name: "pledgeForAReward", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "pledgeId", type: "bytes32" }, + { internalType: "address", name: "backer", type: "address" }, + { internalType: "address", name: "pledgeToken", type: "address" }, + { internalType: "uint256", name: "pledgeAmount", type: "uint256" }, + { internalType: "uint256", name: "tip", type: "uint256" }, + ], + name: "pledgeWithoutAReward", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "rewardName", type: "bytes32" }], + name: "removeReward", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "from", type: "address" }, + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "tokenId", type: "uint256" }, + ], + name: "safeTransferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "from", type: "address" }, + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "tokenId", type: "uint256" }, + { internalType: "bytes", name: "data", type: "bytes" }, + ], + name: "safeTransferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "operator", type: "address" }, + { internalType: "bool", name: "approved", type: "bool" }, + ], + name: "setApprovalForAll", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes4", name: "interfaceId", type: "bytes4" }], + name: "supportsInterface", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "symbol", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], + name: "tokenURI", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "from", type: "address" }, + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "tokenId", type: "uint256" }, + ], + name: "transferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "withdraw", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; diff --git a/packages/contracts/src/contracts/keep-whats-raised/events.ts b/packages/contracts/src/contracts/keep-whats-raised/events.ts new file mode 100644 index 00000000..2fdbf2c3 --- /dev/null +++ b/packages/contracts/src/contracts/keep-whats-raised/events.ts @@ -0,0 +1,18 @@ +import type { Address, PublicClient } from "../../lib"; +import type { KeepWhatsRaisedEvents } from "./types"; + +// TODO: Add event filter factories (filterPledgeMade, filterWithdrawn), log decoder (decodeLog), +// and watcher factories using getLogs / watchEvent. + +/** + * Builds event helpers for a KeepWhatsRaised treasury contract instance. + * @param _address - Deployed KeepWhatsRaised contract address + * @param _publicClient - Viem PublicClient used to call getLogs + * @returns Event helpers bound to the given contract address + */ +export function createKeepWhatsRaisedEvents( + _address: Address, + _publicClient: PublicClient, +): KeepWhatsRaisedEvents { + return {}; +} diff --git a/packages/contracts/src/contracts/keep-whats-raised/index.ts b/packages/contracts/src/contracts/keep-whats-raised/index.ts new file mode 100644 index 00000000..f432bc5c --- /dev/null +++ b/packages/contracts/src/contracts/keep-whats-raised/index.ts @@ -0,0 +1,30 @@ +import type { Address, PublicClient, WalletClient, Chain } from "../../lib"; +import { createKeepWhatsRaisedReads } from "./reads"; +import { createKeepWhatsRaisedWrites } from "./writes"; +import { createKeepWhatsRaisedSimulate } from "./simulate"; +import { createKeepWhatsRaisedEvents } from "./events"; +import type { KeepWhatsRaisedTreasuryEntity } from "./types"; + +/** + * Creates a fully composed KeepWhatsRaised treasury entity combining reads, writes, simulate, and events. + * @param address - Deployed KeepWhatsRaised contract address + * @param publicClient - Viem PublicClient used for reads and simulation + * @param walletClient - Viem WalletClient used for writes; must have an account attached + * @param chain - Chain passed to writeContract and simulateContract + * @returns Composed entity exposing all KeepWhatsRaised methods under a single object + */ +export function createKeepWhatsRaisedEntity( + address: Address, + publicClient: PublicClient, + walletClient: WalletClient | null, + chain: Chain, +): KeepWhatsRaisedTreasuryEntity { + return { + ...createKeepWhatsRaisedReads(address, publicClient), + ...createKeepWhatsRaisedWrites(address, walletClient, chain), + simulate: createKeepWhatsRaisedSimulate(address, publicClient, walletClient, chain), + events: createKeepWhatsRaisedEvents(address, publicClient), + }; +} + +export type { KeepWhatsRaisedTreasuryEntity } from "./types"; diff --git a/packages/contracts/src/contracts/keep-whats-raised/reads.ts b/packages/contracts/src/contracts/keep-whats-raised/reads.ts new file mode 100644 index 00000000..34e97bb5 --- /dev/null +++ b/packages/contracts/src/contracts/keep-whats-raised/reads.ts @@ -0,0 +1,126 @@ +import type { Address, Hex, PublicClient } from "../../lib"; +import { KEEP_WHATS_RAISED_ABI } from "./abi"; +import type { KeepWhatsRaisedReads } from "./types"; +import type { TieredReward } from "../../types/structs"; + +/** + * Builds read methods for a KeepWhatsRaised treasury contract instance. + * @param address - Deployed KeepWhatsRaised contract address + * @param publicClient - Viem PublicClient used to call readContract + * @returns Read methods bound to the given contract address + */ +export function createKeepWhatsRaisedReads( + address: Address, + publicClient: PublicClient, +): KeepWhatsRaisedReads { + const contract = { address, abi: KEEP_WHATS_RAISED_ABI } as const; + + return { + async getRaisedAmount() { + return publicClient.readContract({ ...contract, functionName: "getRaisedAmount" }); + }, + async getLifetimeRaisedAmount() { + return publicClient.readContract({ ...contract, functionName: "getLifetimeRaisedAmount" }); + }, + async getRefundedAmount() { + return publicClient.readContract({ ...contract, functionName: "getRefundedAmount" }); + }, + async getAvailableRaisedAmount() { + return publicClient.readContract({ ...contract, functionName: "getAvailableRaisedAmount" }); + }, + async getReward(rewardName: Hex): Promise { + const result = await publicClient.readContract({ + ...contract, + functionName: "getReward", + args: [rewardName], + }); + return result as unknown as TieredReward; + }, + async getPlatformHash() { + return publicClient.readContract({ ...contract, functionName: "getPlatformHash" }); + }, + async getPlatformFeePercent() { + return publicClient.readContract({ ...contract, functionName: "getPlatformFeePercent" }); + }, + async getWithdrawalApprovalStatus() { + return publicClient.readContract({ ...contract, functionName: "getWithdrawalApprovalStatus" }); + }, + async getLaunchTime() { + return publicClient.readContract({ ...contract, functionName: "getLaunchTime" }); + }, + async getDeadline() { + return publicClient.readContract({ ...contract, functionName: "getDeadline" }); + }, + async getGoalAmount() { + return publicClient.readContract({ ...contract, functionName: "getGoalAmount" }); + }, + async getPaymentGatewayFee(pledgeId: Hex) { + return publicClient.readContract({ + ...contract, + functionName: "getPaymentGatewayFee", + args: [pledgeId], + }); + }, + async getFeeValue(feeKey: Hex) { + return publicClient.readContract({ + ...contract, + functionName: "getFeeValue", + args: [feeKey], + }); + }, + async paused() { + return publicClient.readContract({ ...contract, functionName: "paused" }); + }, + async cancelled() { + return publicClient.readContract({ ...contract, functionName: "cancelled" }); + }, + async balanceOf(owner: Address) { + return publicClient.readContract({ + ...contract, + functionName: "balanceOf", + args: [owner], + }); + }, + async ownerOf(tokenId: bigint) { + return publicClient.readContract({ + ...contract, + functionName: "ownerOf", + args: [tokenId], + }); + }, + async tokenURI(tokenId: bigint) { + return publicClient.readContract({ + ...contract, + functionName: "tokenURI", + args: [tokenId], + }); + }, + async name() { + return publicClient.readContract({ ...contract, functionName: "name" }); + }, + async symbol() { + return publicClient.readContract({ ...contract, functionName: "symbol" }); + }, + async getApproved(tokenId: bigint) { + return publicClient.readContract({ + ...contract, + functionName: "getApproved", + args: [tokenId], + }); + }, + async isApprovedForAll(owner: Address, operator: Address) { + return publicClient.readContract({ + ...contract, + functionName: "isApprovedForAll", + args: [owner, operator], + }); + }, + async supportsInterface(interfaceId: Hex) { + return publicClient.readContract({ + ...contract, + functionName: "supportsInterface", + args: [interfaceId], + }); + }, + }; +} diff --git a/packages/contracts/src/contracts/keep-whats-raised/simulate.ts b/packages/contracts/src/contracts/keep-whats-raised/simulate.ts new file mode 100644 index 00000000..47696f77 --- /dev/null +++ b/packages/contracts/src/contracts/keep-whats-raised/simulate.ts @@ -0,0 +1,361 @@ +import type { Address, Hex, PublicClient, WalletClient, Chain } from "../../lib"; +import { KEEP_WHATS_RAISED_ABI } from "./abi"; +import { requireSigner, requireAccount } from "../../utils/account"; +import { simulateWithErrorDecode } from "../../errors"; +import type { KeepWhatsRaisedSimulate } from "./types"; +import type { CallSignerOptions } from "../../client/types"; +import type { TieredReward, CampaignData } from "../../types/structs"; +import type { + KeepWhatsRaisedConfig, + KeepWhatsRaisedFeeKeys, + KeepWhatsRaisedFeeValues, +} from "../../types/params"; + +/** + * Builds simulate methods for KeepWhatsRaised write calls. + * Each method calls simulateContract against the current chain state and throws a typed + * SDK error on revert, decoded via simulateWithErrorDecode. + * @param address - Deployed KeepWhatsRaised contract address + * @param publicClient - Viem PublicClient used to call simulateContract + * @param walletClient - Viem WalletClient used to resolve the account for simulation + * @param chain - Chain passed to simulateContract + * @returns Simulation methods bound to the given contract address + */ +export function createKeepWhatsRaisedSimulate( + address: Address, + publicClient: PublicClient, + walletClient: WalletClient | null, + chain: Chain, +): KeepWhatsRaisedSimulate { + const contract = { address, abi: KEEP_WHATS_RAISED_ABI } as const; + + return { + async pauseTreasury(message: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "pauseTreasury", + args: [message], + }), + ); + }, + async unpauseTreasury(message: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "unpauseTreasury", + args: [message], + }), + ); + }, + async cancelTreasury(message: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "cancelTreasury", + args: [message], + }), + ); + }, + async configureTreasury( + config: KeepWhatsRaisedConfig, + campaignData: CampaignData, + feeKeys: KeepWhatsRaisedFeeKeys, + feeValues: KeepWhatsRaisedFeeValues, + options?: CallSignerOptions, + ) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "configureTreasury", + args: [ + { + minimumWithdrawalForFeeExemption: config.minimumWithdrawalForFeeExemption, + withdrawalDelay: config.withdrawalDelay, + refundDelay: config.refundDelay, + configLockPeriod: config.configLockPeriod, + isColombianCreator: config.isColombianCreator, + }, + { + launchTime: campaignData.launchTime, + deadline: campaignData.deadline, + goalAmount: campaignData.goalAmount, + currency: campaignData.currency, + }, + { + flatFeeKey: feeKeys.flatFeeKey, + cumulativeFlatFeeKey: feeKeys.cumulativeFlatFeeKey, + grossPercentageFeeKeys: [...feeKeys.grossPercentageFeeKeys], + }, + { + flatFeeValue: feeValues.flatFeeValue, + cumulativeFlatFeeValue: feeValues.cumulativeFlatFeeValue, + grossPercentageFeeValues: [...feeValues.grossPercentageFeeValues], + }, + ], + }), + ); + }, + async addRewards(rewardNames: readonly Hex[], rewards: readonly TieredReward[], options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "addRewards", + args: [ + [...rewardNames], + rewards.map((r) => ({ + rewardValue: r.rewardValue, + isRewardTier: r.isRewardTier, + itemId: [...r.itemId], + itemValue: [...r.itemValue], + itemQuantity: [...r.itemQuantity], + })), + ], + }), + ); + }, + async removeReward(rewardName: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "removeReward", + args: [rewardName], + }), + ); + }, + async approveWithdrawal(options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "approveWithdrawal", + args: [], + }), + ); + }, + async setPaymentGatewayFee(pledgeId: Hex, fee: bigint, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "setPaymentGatewayFee", + args: [pledgeId, fee], + }), + ); + }, + async setFeeAndPledge( + pledgeId: Hex, + backer: Address, + pledgeToken: Address, + pledgeAmount: bigint, + tip: bigint, + fee: bigint, + reward: readonly Hex[], + isPledgeForAReward: boolean, + options?: CallSignerOptions, + ) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "setFeeAndPledge", + args: [pledgeId, backer, pledgeToken, pledgeAmount, tip, fee, [...reward], isPledgeForAReward], + }), + ); + }, + async pledgeForAReward( + pledgeId: Hex, + backer: Address, + pledgeToken: Address, + tip: bigint, + rewardNames: readonly Hex[], + options?: CallSignerOptions, + ) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "pledgeForAReward", + args: [pledgeId, backer, pledgeToken, tip, [...rewardNames]], + }), + ); + }, + async pledgeWithoutAReward( + pledgeId: Hex, + backer: Address, + pledgeToken: Address, + pledgeAmount: bigint, + tip: bigint, + options?: CallSignerOptions, + ) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "pledgeWithoutAReward", + args: [pledgeId, backer, pledgeToken, pledgeAmount, tip], + }), + ); + }, + async claimRefund(tokenId: bigint, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "claimRefund", + args: [tokenId], + }), + ); + }, + async claimTip(options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "claimTip", + args: [], + }), + ); + }, + async claimFund(options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "claimFund", + args: [], + }), + ); + }, + async disburseFees(options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "disburseFees", + args: [], + }), + ); + }, + async withdraw(token: Address, amount: bigint, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "withdraw", + args: [token, amount], + }), + ); + }, + async updateDeadline(deadline: bigint, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "updateDeadline", + args: [deadline], + }), + ); + }, + async updateGoalAmount(goalAmount: bigint, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "updateGoalAmount", + args: [goalAmount], + }), + ); + }, + async approve(to: Address, tokenId: bigint, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "approve", + args: [to, tokenId], + }), + ); + }, + async setApprovalForAll(operator: Address, approved: boolean, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "setApprovalForAll", + args: [operator, approved], + }), + ); + }, + async safeTransferFrom(from: Address, to: Address, tokenId: bigint, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "safeTransferFrom", + args: [from, to, tokenId], + }), + ); + }, + async transferFrom(from: Address, to: Address, tokenId: bigint, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "transferFrom", + args: [from, to, tokenId], + }), + ); + }, + }; +} diff --git a/packages/contracts/src/contracts/keep-whats-raised/types.ts b/packages/contracts/src/contracts/keep-whats-raised/types.ts new file mode 100644 index 00000000..9fa47fb5 --- /dev/null +++ b/packages/contracts/src/contracts/keep-whats-raised/types.ts @@ -0,0 +1,220 @@ +import type { Address, Hex } from "../../lib"; +import type { TieredReward, CampaignData } from "../../types/structs"; +import type { KeepWhatsRaisedConfig, KeepWhatsRaisedFeeKeys, KeepWhatsRaisedFeeValues } from "../../types/params"; +import type { CallSignerOptions } from "../../client/types"; + +/** Read-only methods for KeepWhatsRaised treasury. */ +export interface KeepWhatsRaisedReads { + /** Returns the current total amount raised in the treasury (excludes refunds). */ + getRaisedAmount(): Promise; + /** Returns the all-time total raised, including amounts that were later refunded. */ + getLifetimeRaisedAmount(): Promise; + /** Returns the total amount that has been refunded to backers. */ + getRefundedAmount(): Promise; + /** Returns the amount available for withdrawal (raised minus refunded and fees). */ + getAvailableRaisedAmount(): Promise; + /** Returns the TieredReward struct for the given reward name. */ + getReward(rewardName: Hex): Promise; + /** Returns the bytes32 platform hash associated with this treasury. */ + getPlatformHash(): Promise; + /** Returns the platform fee percent in basis points. */ + getPlatformFeePercent(): Promise; + /** Returns true if a withdrawal has been approved by the platform admin. */ + getWithdrawalApprovalStatus(): Promise; + /** Returns the campaign launch time as a Unix timestamp in seconds. */ + getLaunchTime(): Promise; + /** Returns the campaign deadline as a Unix timestamp in seconds. */ + getDeadline(): Promise; + /** Returns the campaign funding goal in currency units. */ + getGoalAmount(): Promise; + /** Returns the payment gateway fee associated with the given pledge ID. */ + getPaymentGatewayFee(pledgeId: Hex): Promise; + /** Returns the fee value stored under the given registry fee key. */ + getFeeValue(feeKey: Hex): Promise; + /** Returns true if the treasury is currently paused. */ + paused(): Promise; + /** Returns true if the treasury has been cancelled. */ + cancelled(): Promise; + /** Returns the number of pledge NFT tokens held by the given owner. */ + balanceOf(owner: Address): Promise; + /** Returns the owner address of the pledge NFT with the given token ID. */ + ownerOf(tokenId: bigint): Promise
; + /** Returns the metadata URI for the pledge NFT with the given token ID. */ + tokenURI(tokenId: bigint): Promise; + /** Returns the ERC-721 collection name. */ + name(): Promise; + /** Returns the ERC-721 collection symbol. */ + symbol(): Promise; + /** Returns the address approved to transfer the given token ID. */ + getApproved(tokenId: bigint): Promise
; + /** Returns true if the operator is approved to manage all tokens of the given owner. */ + isApprovedForAll(owner: Address, operator: Address): Promise; + /** Returns true if the contract implements the given ERC-165 interface ID. */ + supportsInterface(interfaceId: Hex): Promise; +} + +/** Write methods for KeepWhatsRaised treasury. */ +export interface KeepWhatsRaisedWrites { + /** Pauses the treasury, halting pledges and withdrawals; emits a pause message. */ + pauseTreasury(message: Hex, options?: CallSignerOptions): Promise; + /** Unpauses the treasury, resuming normal operation; emits an unpause message. */ + unpauseTreasury(message: Hex, options?: CallSignerOptions): Promise; + /** Cancels the treasury permanently; emits a cancellation message. */ + cancelTreasury(message: Hex, options?: CallSignerOptions): Promise; + /** Configures the treasury with campaign data, fee keys, and fee values. */ + configureTreasury( + config: KeepWhatsRaisedConfig, + campaignData: CampaignData, + feeKeys: KeepWhatsRaisedFeeKeys, + feeValues: KeepWhatsRaisedFeeValues, + options?: CallSignerOptions, + ): Promise; + /** Registers one or more tiered rewards by name. */ + addRewards(rewardNames: readonly Hex[], rewards: readonly TieredReward[], options?: CallSignerOptions): Promise; + /** Removes a previously registered reward by name. */ + removeReward(rewardName: Hex, options?: CallSignerOptions): Promise; + /** Marks the withdrawal as approved by the platform admin. */ + approveWithdrawal(options?: CallSignerOptions): Promise; + /** Sets the payment gateway fee for a specific pledge ID. */ + setPaymentGatewayFee(pledgeId: Hex, fee: bigint, options?: CallSignerOptions): Promise; + /** Records a pledge amount and fee together in a single transaction. */ + setFeeAndPledge( + pledgeId: Hex, + backer: Address, + pledgeToken: Address, + pledgeAmount: bigint, + tip: bigint, + fee: bigint, + reward: readonly Hex[], + isPledgeForAReward: boolean, + options?: CallSignerOptions, + ): Promise; + /** Processes a backer pledge for one or more reward tiers. */ + pledgeForAReward( + pledgeId: Hex, + backer: Address, + pledgeToken: Address, + tip: bigint, + rewardNames: readonly Hex[], + options?: CallSignerOptions, + ): Promise; + /** Processes a backer pledge with no reward tier selected. */ + pledgeWithoutAReward( + pledgeId: Hex, + backer: Address, + pledgeToken: Address, + pledgeAmount: bigint, + tip: bigint, + options?: CallSignerOptions, + ): Promise; + /** Burns the pledge NFT and issues a refund for the given token ID. */ + claimRefund(tokenId: bigint, options?: CallSignerOptions): Promise; + /** Transfers accumulated tips to the platform tip recipient. */ + claimTip(options?: CallSignerOptions): Promise; + /** Transfers raised funds to the campaign creator after successful campaign. */ + claimFund(options?: CallSignerOptions): Promise; + /** Disburses protocol and platform fees to their respective recipients. */ + disburseFees(options?: CallSignerOptions): Promise; + /** Withdraws a specific token amount from the treasury to the caller. */ + withdraw(token: Address, amount: bigint, options?: CallSignerOptions): Promise; + /** Updates the campaign deadline to a new Unix timestamp in seconds. */ + updateDeadline(deadline: bigint, options?: CallSignerOptions): Promise; + /** Updates the campaign funding goal amount. */ + updateGoalAmount(goalAmount: bigint, options?: CallSignerOptions): Promise; + /** Approves an address to transfer a specific pledge NFT token. */ + approve(to: Address, tokenId: bigint, options?: CallSignerOptions): Promise; + /** Grants or revokes operator approval for all tokens owned by the caller. */ + setApprovalForAll(operator: Address, approved: boolean, options?: CallSignerOptions): Promise; + /** Safely transfers a pledge NFT, calling onERC721Received on the recipient. */ + safeTransferFrom(from: Address, to: Address, tokenId: bigint, options?: CallSignerOptions): Promise; + /** Transfers a pledge NFT without the ERC-721 receiver check. */ + transferFrom(from: Address, to: Address, tokenId: bigint, options?: CallSignerOptions): Promise; +} + +/** Simulate counterparts for KeepWhatsRaised write methods. */ +export interface KeepWhatsRaisedSimulate { + /** Simulates pauseTreasury; throws a typed error on revert. */ + pauseTreasury(message: Hex, options?: CallSignerOptions): Promise; + /** Simulates unpauseTreasury; throws a typed error on revert. */ + unpauseTreasury(message: Hex, options?: CallSignerOptions): Promise; + /** Simulates cancelTreasury; throws a typed error on revert. */ + cancelTreasury(message: Hex, options?: CallSignerOptions): Promise; + /** Simulates configureTreasury; throws a typed error on revert. */ + configureTreasury( + config: KeepWhatsRaisedConfig, + campaignData: CampaignData, + feeKeys: KeepWhatsRaisedFeeKeys, + feeValues: KeepWhatsRaisedFeeValues, + options?: CallSignerOptions, + ): Promise; + /** Simulates addRewards; throws a typed error on revert. */ + addRewards(rewardNames: readonly Hex[], rewards: readonly TieredReward[], options?: CallSignerOptions): Promise; + /** Simulates removeReward; throws a typed error on revert. */ + removeReward(rewardName: Hex, options?: CallSignerOptions): Promise; + /** Simulates approveWithdrawal; throws a typed error on revert. */ + approveWithdrawal(options?: CallSignerOptions): Promise; + /** Simulates setPaymentGatewayFee; throws a typed error on revert. */ + setPaymentGatewayFee(pledgeId: Hex, fee: bigint, options?: CallSignerOptions): Promise; + /** Simulates setFeeAndPledge; throws a typed error on revert. */ + setFeeAndPledge( + pledgeId: Hex, + backer: Address, + pledgeToken: Address, + pledgeAmount: bigint, + tip: bigint, + fee: bigint, + reward: readonly Hex[], + isPledgeForAReward: boolean, + options?: CallSignerOptions, + ): Promise; + /** Simulates pledgeForAReward; throws a typed error on revert. */ + pledgeForAReward( + pledgeId: Hex, + backer: Address, + pledgeToken: Address, + tip: bigint, + rewardNames: readonly Hex[], + options?: CallSignerOptions, + ): Promise; + /** Simulates pledgeWithoutAReward; throws a typed error on revert. */ + pledgeWithoutAReward( + pledgeId: Hex, + backer: Address, + pledgeToken: Address, + pledgeAmount: bigint, + tip: bigint, + options?: CallSignerOptions, + ): Promise; + /** Simulates claimRefund; throws a typed error on revert. */ + claimRefund(tokenId: bigint, options?: CallSignerOptions): Promise; + /** Simulates claimTip; throws a typed error on revert. */ + claimTip(options?: CallSignerOptions): Promise; + /** Simulates claimFund; throws a typed error on revert. */ + claimFund(options?: CallSignerOptions): Promise; + /** Simulates disburseFees; throws a typed error on revert. */ + disburseFees(options?: CallSignerOptions): Promise; + /** Simulates withdraw; throws a typed error on revert. */ + withdraw(token: Address, amount: bigint, options?: CallSignerOptions): Promise; + /** Simulates updateDeadline; throws a typed error on revert. */ + updateDeadline(deadline: bigint, options?: CallSignerOptions): Promise; + /** Simulates updateGoalAmount; throws a typed error on revert. */ + updateGoalAmount(goalAmount: bigint, options?: CallSignerOptions): Promise; + /** Simulates approve; throws a typed error on revert. */ + approve(to: Address, tokenId: bigint, options?: CallSignerOptions): Promise; + /** Simulates setApprovalForAll; throws a typed error on revert. */ + setApprovalForAll(operator: Address, approved: boolean, options?: CallSignerOptions): Promise; + /** Simulates safeTransferFrom; throws a typed error on revert. */ + safeTransferFrom(from: Address, to: Address, tokenId: bigint, options?: CallSignerOptions): Promise; + /** Simulates transferFrom; throws a typed error on revert. */ + transferFrom(from: Address, to: Address, tokenId: bigint, options?: CallSignerOptions): Promise; +} + +/** Event helpers for KeepWhatsRaised. */ +export interface KeepWhatsRaisedEvents {} + +/** Full KeepWhatsRaised treasury entity (reads, writes, simulate, events). */ +export type KeepWhatsRaisedTreasuryEntity = KeepWhatsRaisedReads & + KeepWhatsRaisedWrites & { + simulate: KeepWhatsRaisedSimulate; + events: KeepWhatsRaisedEvents; + }; diff --git a/packages/contracts/src/contracts/keep-whats-raised/writes.ts b/packages/contracts/src/contracts/keep-whats-raised/writes.ts new file mode 100644 index 00000000..93615153 --- /dev/null +++ b/packages/contracts/src/contracts/keep-whats-raised/writes.ts @@ -0,0 +1,313 @@ +import type { Address, Hex, WalletClient, Chain } from "../../lib"; +import { KEEP_WHATS_RAISED_ABI } from "./abi"; +import { requireSigner, requireAccount } from "../../utils/account"; +import type { KeepWhatsRaisedWrites } from "./types"; +import type { CallSignerOptions } from "../../client/types"; +import type { TieredReward } from "../../types/structs"; +import type { CampaignData } from "../../types/structs"; +import type { + KeepWhatsRaisedConfig, + KeepWhatsRaisedFeeKeys, + KeepWhatsRaisedFeeValues, +} from "../../types/params"; + +/** + * Builds write methods for a KeepWhatsRaised treasury contract instance. + * @param address - Deployed KeepWhatsRaised contract address + * @param walletClient - Viem WalletClient used to call writeContract; must have an account attached + * @param chain - Chain passed to writeContract for EIP-1559 and replay protection + * @returns Write methods bound to the given contract address + */ +export function createKeepWhatsRaisedWrites( + address: Address, + walletClient: WalletClient | null, + chain: Chain, +): KeepWhatsRaisedWrites { + const contract = { address, abi: KEEP_WHATS_RAISED_ABI } as const; + + return { + async pauseTreasury(message: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "pauseTreasury", + args: [message], + }); + }, + async unpauseTreasury(message: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "unpauseTreasury", + args: [message], + }); + }, + async cancelTreasury(message: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "cancelTreasury", + args: [message], + }); + }, + async configureTreasury( + config: KeepWhatsRaisedConfig, + campaignData: CampaignData, + feeKeys: KeepWhatsRaisedFeeKeys, + feeValues: KeepWhatsRaisedFeeValues, + options?: CallSignerOptions, + ) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "configureTreasury", + args: [ + { + minimumWithdrawalForFeeExemption: config.minimumWithdrawalForFeeExemption, + withdrawalDelay: config.withdrawalDelay, + refundDelay: config.refundDelay, + configLockPeriod: config.configLockPeriod, + isColombianCreator: config.isColombianCreator, + }, + { + launchTime: campaignData.launchTime, + deadline: campaignData.deadline, + goalAmount: campaignData.goalAmount, + currency: campaignData.currency, + }, + { + flatFeeKey: feeKeys.flatFeeKey, + cumulativeFlatFeeKey: feeKeys.cumulativeFlatFeeKey, + grossPercentageFeeKeys: [...feeKeys.grossPercentageFeeKeys], + }, + { + flatFeeValue: feeValues.flatFeeValue, + cumulativeFlatFeeValue: feeValues.cumulativeFlatFeeValue, + grossPercentageFeeValues: [...feeValues.grossPercentageFeeValues], + }, + ], + }); + }, + async addRewards(rewardNames: readonly Hex[], rewards: readonly TieredReward[], options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "addRewards", + args: [ + [...rewardNames], + rewards.map((r) => ({ + rewardValue: r.rewardValue, + isRewardTier: r.isRewardTier, + itemId: [...r.itemId], + itemValue: [...r.itemValue], + itemQuantity: [...r.itemQuantity], + })), + ], + }); + }, + async removeReward(rewardName: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "removeReward", + args: [rewardName], + }); + }, + async approveWithdrawal(options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "approveWithdrawal", + args: [], + }); + }, + async setPaymentGatewayFee(pledgeId: Hex, fee: bigint, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "setPaymentGatewayFee", + args: [pledgeId, fee], + }); + }, + async setFeeAndPledge( + pledgeId: Hex, + backer: Address, + pledgeToken: Address, + pledgeAmount: bigint, + tip: bigint, + fee: bigint, + reward: readonly Hex[], + isPledgeForAReward: boolean, + options?: CallSignerOptions, + ) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "setFeeAndPledge", + args: [pledgeId, backer, pledgeToken, pledgeAmount, tip, fee, [...reward], isPledgeForAReward], + }); + }, + async pledgeForAReward( + pledgeId: Hex, + backer: Address, + pledgeToken: Address, + tip: bigint, + rewardNames: readonly Hex[], + options?: CallSignerOptions, + ) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "pledgeForAReward", + args: [pledgeId, backer, pledgeToken, tip, [...rewardNames]], + }); + }, + async pledgeWithoutAReward( + pledgeId: Hex, + backer: Address, + pledgeToken: Address, + pledgeAmount: bigint, + tip: bigint, + options?: CallSignerOptions, + ) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "pledgeWithoutAReward", + args: [pledgeId, backer, pledgeToken, pledgeAmount, tip], + }); + }, + async claimRefund(tokenId: bigint, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "claimRefund", + args: [tokenId], + }); + }, + async claimTip(options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "claimTip", + args: [], + }); + }, + async claimFund(options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "claimFund", + args: [], + }); + }, + async disburseFees(options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "disburseFees", + args: [], + }); + }, + async withdraw(token: Address, amount: bigint, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "withdraw", + args: [token, amount], + }); + }, + async updateDeadline(deadline: bigint, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "updateDeadline", + args: [deadline], + }); + }, + async updateGoalAmount(goalAmount: bigint, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "updateGoalAmount", + args: [goalAmount], + }); + }, + async approve(to: Address, tokenId: bigint, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "approve", + args: [to, tokenId], + }); + }, + async setApprovalForAll(operator: Address, approved: boolean, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "setApprovalForAll", + args: [operator, approved], + }); + }, + async safeTransferFrom(from: Address, to: Address, tokenId: bigint, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "safeTransferFrom", + args: [from, to, tokenId], + }); + }, + async transferFrom(from: Address, to: Address, tokenId: bigint, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "transferFrom", + args: [from, to, tokenId], + }); + }, + }; +} diff --git a/packages/contracts/src/contracts/payment-treasury/abi.ts b/packages/contracts/src/contracts/payment-treasury/abi.ts new file mode 100644 index 00000000..d127b5b8 --- /dev/null +++ b/packages/contracts/src/contracts/payment-treasury/abi.ts @@ -0,0 +1,462 @@ +/** PaymentTreasury ABI — typed const, co-located with the contract that uses it. Shared interface for PaymentTreasury and TimeConstrainedPaymentTreasury. */ +const PAYMENT_LINE_ITEM_COMPONENTS = [ + { internalType: "bytes32", name: "typeId", type: "bytes32" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "string", name: "label", type: "string" }, + { internalType: "bool", name: "countsTowardGoal", type: "bool" }, + { internalType: "bool", name: "applyProtocolFee", type: "bool" }, + { internalType: "bool", name: "canRefund", type: "bool" }, + { internalType: "bool", name: "instantTransfer", type: "bool" }, +] as const; + +const LINE_ITEM_COMPONENTS = [ + { internalType: "bytes32", name: "typeId", type: "bytes32" }, + { internalType: "uint256", name: "amount", type: "uint256" }, +] as const; + +const EXTERNAL_FEES_COMPONENTS = [ + { internalType: "bytes32", name: "feeType", type: "bytes32" }, + { internalType: "uint256", name: "feeAmount", type: "uint256" }, +] as const; + +export const PAYMENT_TREASURY_ABI = [ + { inputs: [], name: "PaymentTreasuryUnAuthorized", type: "error" }, + { inputs: [], name: "PaymentTreasuryInvalidInput", type: "error" }, + { + inputs: [{ internalType: "bytes32", name: "paymentId", type: "bytes32" }], + name: "PaymentTreasuryPaymentAlreadyExist", + type: "error", + }, + { + inputs: [{ internalType: "bytes32", name: "paymentId", type: "bytes32" }], + name: "PaymentTreasuryPaymentAlreadyConfirmed", + type: "error", + }, + { + inputs: [{ internalType: "bytes32", name: "paymentId", type: "bytes32" }], + name: "PaymentTreasuryPaymentAlreadyExpired", + type: "error", + }, + { + inputs: [{ internalType: "bytes32", name: "paymentId", type: "bytes32" }], + name: "PaymentTreasuryPaymentNotExist", + type: "error", + }, + { inputs: [], name: "PaymentTreasuryCampaignInfoIsPaused", type: "error" }, + { + inputs: [{ internalType: "address", name: "token", type: "address" }], + name: "PaymentTreasuryTokenNotAccepted", + type: "error", + }, + { inputs: [], name: "PaymentTreasurySuccessConditionNotFulfilled", type: "error" }, + { inputs: [], name: "PaymentTreasuryFeeNotDisbursed", type: "error" }, + { + inputs: [{ internalType: "bytes32", name: "paymentId", type: "bytes32" }], + name: "PaymentTreasuryPaymentNotConfirmed", + type: "error", + }, + { + inputs: [{ internalType: "bytes32", name: "paymentId", type: "bytes32" }], + name: "PaymentTreasuryPaymentNotClaimable", + type: "error", + }, + { inputs: [], name: "PaymentTreasuryAlreadyWithdrawn", type: "error" }, + { + inputs: [{ internalType: "bytes32", name: "paymentId", type: "bytes32" }], + name: "PaymentTreasuryCryptoPayment", + type: "error", + }, + { + inputs: [ + { internalType: "uint256", name: "withdrawalAmount", type: "uint256" }, + { internalType: "uint256", name: "fee", type: "uint256" }, + ], + name: "PaymentTreasuryInsufficientFundsForFee", + type: "error", + }, + { + inputs: [ + { internalType: "uint256", name: "required", type: "uint256" }, + { internalType: "uint256", name: "available", type: "uint256" }, + ], + name: "PaymentTreasuryInsufficientBalance", + type: "error", + }, + { + inputs: [ + { internalType: "uint256", name: "expiration", type: "uint256" }, + { internalType: "uint256", name: "maxExpiration", type: "uint256" }, + ], + name: "PaymentTreasuryExpirationExceedsMax", + type: "error", + }, + { + inputs: [{ internalType: "uint256", name: "claimableAt", type: "uint256" }], + name: "PaymentTreasuryClaimWindowNotReached", + type: "error", + }, + { inputs: [], name: "PaymentTreasuryNoFundsToClaim", type: "error" }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: "address", name: "buyerAddress", type: "address" }, + { indexed: true, internalType: "bytes32", name: "paymentId", type: "bytes32" }, + { indexed: false, internalType: "bytes32", name: "buyerId", type: "bytes32" }, + { indexed: true, internalType: "bytes32", name: "itemId", type: "bytes32" }, + { indexed: true, internalType: "address", name: "paymentToken", type: "address" }, + { indexed: false, internalType: "uint256", name: "amount", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "expiration", type: "uint256" }, + { indexed: false, internalType: "bool", name: "isCryptoPayment", type: "bool" }, + ], + name: "PaymentCreated", + type: "event", + }, + { + anonymous: false, + inputs: [{ indexed: true, internalType: "bytes32", name: "paymentId", type: "bytes32" }], + name: "PaymentCancelled", + type: "event", + }, + { + anonymous: false, + inputs: [{ indexed: true, internalType: "bytes32", name: "paymentId", type: "bytes32" }], + name: "PaymentConfirmed", + type: "event", + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: "bytes32[]", name: "paymentIds", type: "bytes32[]" }], + name: "PaymentBatchConfirmed", + type: "event", + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: "bytes32[]", name: "paymentIds", type: "bytes32[]" }], + name: "PaymentBatchCreated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "token", type: "address" }, + { indexed: false, internalType: "uint256", name: "protocolShare", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "platformShare", type: "uint256" }, + ], + name: "FeesDisbursed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "token", type: "address" }, + { indexed: true, internalType: "address", name: "to", type: "address" }, + { indexed: false, internalType: "uint256", name: "amount", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "fee", type: "uint256" }, + ], + name: "WithdrawalWithFeeSuccessful", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "paymentId", type: "bytes32" }, + { indexed: false, internalType: "uint256", name: "refundAmount", type: "uint256" }, + { indexed: true, internalType: "address", name: "claimer", type: "address" }, + ], + name: "RefundClaimed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "token", type: "address" }, + { indexed: false, internalType: "uint256", name: "amount", type: "uint256" }, + { indexed: true, internalType: "address", name: "platformAdmin", type: "address" }, + ], + name: "NonGoalLineItemsClaimed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "token", type: "address" }, + { indexed: false, internalType: "uint256", name: "platformAmount", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "protocolAmount", type: "uint256" }, + ], + name: "ExpiredFundsClaimed", + type: "event", + }, + { + inputs: [ + { internalType: "bytes32", name: "_platformHash", type: "bytes32" }, + { internalType: "address", name: "_infoAddress", type: "address" }, + { internalType: "address", name: "_trustedForwarder", type: "address" }, + ], + name: "initialize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "getplatformHash", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getplatformFeePercent", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getRaisedAmount", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getAvailableRaisedAmount", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "paymentId", type: "bytes32" }], + name: "getPaymentData", + outputs: [ + { + components: [ + { internalType: "address", name: "buyerAddress", type: "address" }, + { internalType: "bytes32", name: "buyerId", type: "bytes32" }, + { internalType: "bytes32", name: "itemId", type: "bytes32" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "uint256", name: "expiration", type: "uint256" }, + { internalType: "bool", name: "isConfirmed", type: "bool" }, + { internalType: "bool", name: "isCryptoPayment", type: "bool" }, + { internalType: "uint256", name: "lineItemCount", type: "uint256" }, + { internalType: "address", name: "paymentToken", type: "address" }, + { + components: [...PAYMENT_LINE_ITEM_COMPONENTS], + internalType: "struct ICampaignPaymentTreasury.PaymentLineItem[]", + name: "lineItems", + type: "tuple[]", + }, + { + components: [...EXTERNAL_FEES_COMPONENTS], + internalType: "struct ICampaignPaymentTreasury.ExternalFees[]", + name: "externalFees", + type: "tuple[]", + }, + ], + internalType: "struct ICampaignPaymentTreasury.PaymentData", + name: "", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getLifetimeRaisedAmount", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getRefundedAmount", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getExpectedAmount", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "cancelled", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "paymentId", type: "bytes32" }, + { internalType: "bytes32", name: "buyerId", type: "bytes32" }, + { internalType: "bytes32", name: "itemId", type: "bytes32" }, + { internalType: "address", name: "paymentToken", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "uint256", name: "expiration", type: "uint256" }, + { + components: [...LINE_ITEM_COMPONENTS], + internalType: "struct ICampaignPaymentTreasury.LineItem[]", + name: "lineItems", + type: "tuple[]", + }, + { + components: [...EXTERNAL_FEES_COMPONENTS], + internalType: "struct ICampaignPaymentTreasury.ExternalFees[]", + name: "externalFees", + type: "tuple[]", + }, + ], + name: "createPayment", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32[]", name: "paymentIds", type: "bytes32[]" }, + { internalType: "bytes32[]", name: "buyerIds", type: "bytes32[]" }, + { internalType: "bytes32[]", name: "itemIds", type: "bytes32[]" }, + { internalType: "address[]", name: "paymentTokens", type: "address[]" }, + { internalType: "uint256[]", name: "amounts", type: "uint256[]" }, + { internalType: "uint256[]", name: "expirations", type: "uint256[]" }, + { + components: [...LINE_ITEM_COMPONENTS], + internalType: "struct ICampaignPaymentTreasury.LineItem[][]", + name: "lineItemsArray", + type: "tuple[][]", + }, + { + components: [...EXTERNAL_FEES_COMPONENTS], + internalType: "struct ICampaignPaymentTreasury.ExternalFees[][]", + name: "externalFeesArray", + type: "tuple[][]", + }, + ], + name: "createPaymentBatch", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "paymentId", type: "bytes32" }, + { internalType: "bytes32", name: "itemId", type: "bytes32" }, + { internalType: "address", name: "buyerAddress", type: "address" }, + { internalType: "address", name: "paymentToken", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { + components: [...LINE_ITEM_COMPONENTS], + internalType: "struct ICampaignPaymentTreasury.LineItem[]", + name: "lineItems", + type: "tuple[]", + }, + { + components: [...EXTERNAL_FEES_COMPONENTS], + internalType: "struct ICampaignPaymentTreasury.ExternalFees[]", + name: "externalFees", + type: "tuple[]", + }, + ], + name: "processCryptoPayment", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "paymentId", type: "bytes32" }], + name: "cancelPayment", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "paymentId", type: "bytes32" }, + { internalType: "address", name: "buyerAddress", type: "address" }, + ], + name: "confirmPayment", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32[]", name: "paymentIds", type: "bytes32[]" }, + { internalType: "address[]", name: "buyerAddresses", type: "address[]" }, + ], + name: "confirmPaymentBatch", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "disburseFees", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "withdraw", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "paymentId", type: "bytes32" }, + { internalType: "address", name: "refundAddress", type: "address" }, + ], + name: "claimRefund", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "paymentId", type: "bytes32" }], + name: "claimRefund", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "claimExpiredFunds", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "token", type: "address" }], + name: "claimNonGoalLineItems", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "message", type: "bytes32" }], + name: "pauseTreasury", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "message", type: "bytes32" }], + name: "unpauseTreasury", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "message", type: "bytes32" }], + name: "cancelTreasury", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; diff --git a/packages/contracts/src/contracts/payment-treasury/events.ts b/packages/contracts/src/contracts/payment-treasury/events.ts new file mode 100644 index 00000000..11d4b98a --- /dev/null +++ b/packages/contracts/src/contracts/payment-treasury/events.ts @@ -0,0 +1,18 @@ +import type { Address, PublicClient } from "../../lib"; +import type { PaymentTreasuryEvents } from "./types"; + +// TODO: Add event filter factories (filterPaymentMade), log decoder (decodeLog), +// and watcher factories using getLogs / watchEvent. + +/** + * Builds event helpers for a PaymentTreasury contract instance. + * @param _address - Deployed PaymentTreasury contract address + * @param _publicClient - Viem PublicClient used to call getLogs + * @returns Event helpers bound to the given contract address + */ +export function createPaymentTreasuryEvents( + _address: Address, + _publicClient: PublicClient, +): PaymentTreasuryEvents { + return {}; +} diff --git a/packages/contracts/src/contracts/payment-treasury/index.ts b/packages/contracts/src/contracts/payment-treasury/index.ts new file mode 100644 index 00000000..cded4992 --- /dev/null +++ b/packages/contracts/src/contracts/payment-treasury/index.ts @@ -0,0 +1,38 @@ +import type { Address, PublicClient, WalletClient, Chain } from "../../lib"; +import { createPaymentTreasuryReads } from "./reads"; +import { createPaymentTreasuryWrites } from "./writes"; +import { createPaymentTreasurySimulate } from "./simulate"; +import { createPaymentTreasuryEvents } from "./events"; +import type { PaymentTreasuryEntity } from "./types"; + +/** + * Creates a fully composed PaymentTreasury entity combining reads, writes, simulate, and events. + * + * Compatible with **both** on-chain treasury variants: + * - **PaymentTreasury** — standard payment treasury with no time restrictions. + * - **TimeConstrainedPaymentTreasury** — payment treasury that enforces launch-time + * and deadline constraints on-chain (payments only within the campaign window, + * refunds/withdrawals only after launch). Time enforcement is handled entirely + * on-chain, so the SDK interface is identical for both. + * + * @param address - Deployed PaymentTreasury or TimeConstrainedPaymentTreasury contract address + * @param publicClient - Viem PublicClient used for reads and simulation + * @param walletClient - Viem WalletClient used for writes; must have an account attached + * @param chain - Chain passed to writeContract and simulateContract + * @returns Composed entity exposing all PaymentTreasury methods under a single object + */ +export function createPaymentTreasuryEntity( + address: Address, + publicClient: PublicClient, + walletClient: WalletClient | null, + chain: Chain, +): PaymentTreasuryEntity { + return { + ...createPaymentTreasuryReads(address, publicClient), + ...createPaymentTreasuryWrites(address, walletClient, chain), + simulate: createPaymentTreasurySimulate(address, publicClient, walletClient, chain), + events: createPaymentTreasuryEvents(address, publicClient), + }; +} + +export type { PaymentTreasuryEntity } from "./types"; diff --git a/packages/contracts/src/contracts/payment-treasury/reads.ts b/packages/contracts/src/contracts/payment-treasury/reads.ts new file mode 100644 index 00000000..72b2e278 --- /dev/null +++ b/packages/contracts/src/contracts/payment-treasury/reads.ts @@ -0,0 +1,52 @@ +import type { Address, Hex, PublicClient } from "../../lib"; +import { PAYMENT_TREASURY_ABI } from "./abi"; +import type { PaymentTreasuryReads } from "./types"; +import type { PaymentData } from "../../types/structs"; + +/** + * Builds read methods for a PaymentTreasury contract instance. + * @param address - Deployed PaymentTreasury contract address + * @param publicClient - Viem PublicClient used to call readContract + * @returns Read methods bound to the given contract address + */ +export function createPaymentTreasuryReads( + address: Address, + publicClient: PublicClient, +): PaymentTreasuryReads { + const contract = { address, abi: PAYMENT_TREASURY_ABI } as const; + + return { + async getPlatformHash() { + return publicClient.readContract({ ...contract, functionName: "getplatformHash" }); + }, + async getPlatformFeePercent() { + return publicClient.readContract({ ...contract, functionName: "getplatformFeePercent" }); + }, + async getRaisedAmount() { + return publicClient.readContract({ ...contract, functionName: "getRaisedAmount" }); + }, + async getAvailableRaisedAmount() { + return publicClient.readContract({ ...contract, functionName: "getAvailableRaisedAmount" }); + }, + async getLifetimeRaisedAmount() { + return publicClient.readContract({ ...contract, functionName: "getLifetimeRaisedAmount" }); + }, + async getRefundedAmount() { + return publicClient.readContract({ ...contract, functionName: "getRefundedAmount" }); + }, + async getExpectedAmount() { + return publicClient.readContract({ ...contract, functionName: "getExpectedAmount" }); + }, + async getPaymentData(paymentId: Hex): Promise { + const result = await publicClient.readContract({ + ...contract, + functionName: "getPaymentData", + args: [paymentId], + }); + return result as unknown as PaymentData; + }, + async cancelled() { + return publicClient.readContract({ ...contract, functionName: "cancelled" }); + }, + }; +} diff --git a/packages/contracts/src/contracts/payment-treasury/simulate.ts b/packages/contracts/src/contracts/payment-treasury/simulate.ts new file mode 100644 index 00000000..85283984 --- /dev/null +++ b/packages/contracts/src/contracts/payment-treasury/simulate.ts @@ -0,0 +1,264 @@ +import type { Address, Hex, PublicClient, WalletClient, Chain } from "../../lib"; +import { PAYMENT_TREASURY_ABI } from "./abi"; +import { requireSigner, requireAccount } from "../../utils/account"; +import { simulateWithErrorDecode } from "../../errors"; +import type { PaymentTreasurySimulate } from "./types"; +import type { LineItem, ExternalFees } from "../../types/structs"; +import type { CallSignerOptions } from "../../client/types"; + +/** + * Builds simulate methods for PaymentTreasury write calls. + * Each method calls simulateContract against the current chain state and throws a typed + * SDK error on revert, decoded via simulateWithErrorDecode. + * @param address - Deployed PaymentTreasury contract address + * @param publicClient - Viem PublicClient used to call simulateContract + * @param walletClient - Viem WalletClient used to resolve the account for simulation + * @param chain - Chain passed to simulateContract + * @returns Simulation methods bound to the given contract address + */ +export function createPaymentTreasurySimulate( + address: Address, + publicClient: PublicClient, + walletClient: WalletClient | null, + chain: Chain, +): PaymentTreasurySimulate { + const contract = { address, abi: PAYMENT_TREASURY_ABI } as const; + + return { + async createPayment( + paymentId: Hex, + buyerId: Hex, + itemId: Hex, + paymentToken: Address, + amount: bigint, + expiration: bigint, + lineItems: readonly LineItem[], + externalFees: readonly ExternalFees[], + options?: CallSignerOptions, + ) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "createPayment", + args: [ + paymentId, + buyerId, + itemId, + paymentToken, + amount, + expiration, + [...lineItems] as { typeId: Hex; amount: bigint }[], + [...externalFees] as { feeType: Hex; feeAmount: bigint }[], + ], + }), + ); + }, + async createPaymentBatch( + paymentIds: readonly Hex[], + buyerIds: readonly Hex[], + itemIds: readonly Hex[], + paymentTokens: readonly Address[], + amounts: readonly bigint[], + expirations: readonly bigint[], + lineItemsArray: readonly (readonly LineItem[])[], + externalFeesArray: readonly (readonly ExternalFees[])[], + options?: CallSignerOptions, + ) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "createPaymentBatch", + args: [ + [...paymentIds], + [...buyerIds], + [...itemIds], + [...paymentTokens], + [...amounts], + [...expirations], + lineItemsArray.map((li) => [...li]) as { typeId: Hex; amount: bigint }[][], + externalFeesArray.map((ef) => [...ef]) as { feeType: Hex; feeAmount: bigint }[][], + ], + }), + ); + }, + async processCryptoPayment( + paymentId: Hex, + itemId: Hex, + buyerAddress: Address, + paymentToken: Address, + amount: bigint, + lineItems: readonly LineItem[], + externalFees: readonly ExternalFees[], + options?: CallSignerOptions, + ) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "processCryptoPayment", + args: [ + paymentId, + itemId, + buyerAddress, + paymentToken, + amount, + [...lineItems] as { typeId: Hex; amount: bigint }[], + [...externalFees] as { feeType: Hex; feeAmount: bigint }[], + ], + }), + ); + }, + async cancelPayment(paymentId: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "cancelPayment", + args: [paymentId], + }), + ); + }, + async confirmPayment(paymentId: Hex, buyerAddress: Address, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "confirmPayment", + args: [paymentId, buyerAddress], + }), + ); + }, + async confirmPaymentBatch(paymentIds: readonly Hex[], buyerAddresses: readonly Address[], options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "confirmPaymentBatch", + args: [[...paymentIds], [...buyerAddresses]], + }), + ); + }, + async disburseFees(options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "disburseFees", + args: [], + }), + ); + }, + async withdraw(options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "withdraw", + args: [], + }), + ); + }, + async claimRefund(paymentId: Hex, refundAddress: Address, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "claimRefund", + args: [paymentId, refundAddress], + }), + ); + }, + async claimRefundSelf(paymentId: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "claimRefund", + args: [paymentId], + }), + ); + }, + async claimExpiredFunds(options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "claimExpiredFunds", + args: [], + }), + ); + }, + async claimNonGoalLineItems(token: Address, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "claimNonGoalLineItems", + args: [token], + }), + ); + }, + async pauseTreasury(message: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "pauseTreasury", + args: [message], + }), + ); + }, + async unpauseTreasury(message: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "unpauseTreasury", + args: [message], + }), + ); + }, + async cancelTreasury(message: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "cancelTreasury", + args: [message], + }), + ); + }, + }; +} diff --git a/packages/contracts/src/contracts/payment-treasury/types.ts b/packages/contracts/src/contracts/payment-treasury/types.ts new file mode 100644 index 00000000..9921a63f --- /dev/null +++ b/packages/contracts/src/contracts/payment-treasury/types.ts @@ -0,0 +1,169 @@ +import type { Address, Hex } from "../../lib"; +import type { PaymentData, LineItem, ExternalFees } from "../../types/structs"; +import type { CallSignerOptions } from "../../client/types"; + +/** Read-only methods for PaymentTreasury. */ +export interface PaymentTreasuryReads { + /** Returns the bytes32 platform hash associated with this treasury. */ + getPlatformHash(): Promise; + /** Returns the platform fee percent in basis points. */ + getPlatformFeePercent(): Promise; + /** Returns the current total amount raised (excludes refunds and non-goal items). */ + getRaisedAmount(): Promise; + /** Returns the amount available for withdrawal after fees and refunds. */ + getAvailableRaisedAmount(): Promise; + /** Returns the all-time total raised, including refunded amounts. */ + getLifetimeRaisedAmount(): Promise; + /** Returns the total amount refunded to buyers. */ + getRefundedAmount(): Promise; + /** Returns the total expected amount from all pending payments. */ + getExpectedAmount(): Promise; + /** Returns the full PaymentData snapshot for the given payment ID. */ + getPaymentData(paymentId: Hex): Promise; + /** Returns true if the treasury has been cancelled. */ + cancelled(): Promise; +} + +/** Write methods for PaymentTreasury. */ +export interface PaymentTreasuryWrites { + /** Creates a new off-chain payment record with line items and external fees. */ + createPayment( + paymentId: Hex, + buyerId: Hex, + itemId: Hex, + paymentToken: Address, + amount: bigint, + expiration: bigint, + lineItems: readonly LineItem[], + externalFees: readonly ExternalFees[], + options?: CallSignerOptions, + ): Promise; + /** Creates multiple payment records in a single transaction. */ + createPaymentBatch( + paymentIds: readonly Hex[], + buyerIds: readonly Hex[], + itemIds: readonly Hex[], + paymentTokens: readonly Address[], + amounts: readonly bigint[], + expirations: readonly bigint[], + lineItemsArray: readonly (readonly LineItem[])[], + externalFeesArray: readonly (readonly ExternalFees[])[], + options?: CallSignerOptions, + ): Promise; + /** Processes a crypto payment, transferring tokens from the buyer on-chain. */ + processCryptoPayment( + paymentId: Hex, + itemId: Hex, + buyerAddress: Address, + paymentToken: Address, + amount: bigint, + lineItems: readonly LineItem[], + externalFees: readonly ExternalFees[], + options?: CallSignerOptions, + ): Promise; + /** Cancels a pending payment and marks it as refundable. */ + cancelPayment(paymentId: Hex, options?: CallSignerOptions): Promise; + /** Confirms a payment, marking funds as settled and triggering instant transfers. */ + confirmPayment(paymentId: Hex, buyerAddress: Address, options?: CallSignerOptions): Promise; + /** Confirms multiple payments in a single transaction. */ + confirmPaymentBatch(paymentIds: readonly Hex[], buyerAddresses: readonly Address[], options?: CallSignerOptions): Promise; + /** Disburses protocol and platform fees to their respective recipients. */ + disburseFees(options?: CallSignerOptions): Promise; + /** Withdraws settled raised funds to the campaign creator. */ + withdraw(options?: CallSignerOptions): Promise; + /** Issues a refund for a cancelled payment to the specified refund address. */ + claimRefund(paymentId: Hex, refundAddress: Address, options?: CallSignerOptions): Promise; + /** Issues a refund for a cancelled payment directly to the caller. */ + claimRefundSelf(paymentId: Hex, options?: CallSignerOptions): Promise; + /** Claims funds from payments that have passed their expiration timestamp. */ + claimExpiredFunds(options?: CallSignerOptions): Promise; + /** Claims line item amounts that do not count toward the funding goal. */ + claimNonGoalLineItems(token: Address, options?: CallSignerOptions): Promise; + /** Pauses the treasury, halting payment processing; emits a pause message. */ + pauseTreasury(message: Hex, options?: CallSignerOptions): Promise; + /** Unpauses the treasury, resuming normal operation; emits an unpause message. */ + unpauseTreasury(message: Hex, options?: CallSignerOptions): Promise; + /** Cancels the treasury permanently; emits a cancellation message. */ + cancelTreasury(message: Hex, options?: CallSignerOptions): Promise; +} + +/** Simulate counterparts for PaymentTreasury write methods. */ +export interface PaymentTreasurySimulate { + /** Simulates createPayment; throws a typed error on revert. */ + createPayment( + paymentId: Hex, + buyerId: Hex, + itemId: Hex, + paymentToken: Address, + amount: bigint, + expiration: bigint, + lineItems: readonly LineItem[], + externalFees: readonly ExternalFees[], + options?: CallSignerOptions, + ): Promise; + /** Simulates createPaymentBatch; throws a typed error on revert. */ + createPaymentBatch( + paymentIds: readonly Hex[], + buyerIds: readonly Hex[], + itemIds: readonly Hex[], + paymentTokens: readonly Address[], + amounts: readonly bigint[], + expirations: readonly bigint[], + lineItemsArray: readonly (readonly LineItem[])[], + externalFeesArray: readonly (readonly ExternalFees[])[], + options?: CallSignerOptions, + ): Promise; + /** Simulates processCryptoPayment; throws a typed error on revert. */ + processCryptoPayment( + paymentId: Hex, + itemId: Hex, + buyerAddress: Address, + paymentToken: Address, + amount: bigint, + lineItems: readonly LineItem[], + externalFees: readonly ExternalFees[], + options?: CallSignerOptions, + ): Promise; + /** Simulates cancelPayment; throws a typed error on revert. */ + cancelPayment(paymentId: Hex, options?: CallSignerOptions): Promise; + /** Simulates confirmPayment; throws a typed error on revert. */ + confirmPayment(paymentId: Hex, buyerAddress: Address, options?: CallSignerOptions): Promise; + /** Simulates confirmPaymentBatch; throws a typed error on revert. */ + confirmPaymentBatch(paymentIds: readonly Hex[], buyerAddresses: readonly Address[], options?: CallSignerOptions): Promise; + /** Simulates disburseFees; throws a typed error on revert. */ + disburseFees(options?: CallSignerOptions): Promise; + /** Simulates withdraw; throws a typed error on revert. */ + withdraw(options?: CallSignerOptions): Promise; + /** Simulates claimRefund; throws a typed error on revert. */ + claimRefund(paymentId: Hex, refundAddress: Address, options?: CallSignerOptions): Promise; + /** Simulates claimRefundSelf; throws a typed error on revert. */ + claimRefundSelf(paymentId: Hex, options?: CallSignerOptions): Promise; + /** Simulates claimExpiredFunds; throws a typed error on revert. */ + claimExpiredFunds(options?: CallSignerOptions): Promise; + /** Simulates claimNonGoalLineItems; throws a typed error on revert. */ + claimNonGoalLineItems(token: Address, options?: CallSignerOptions): Promise; + /** Simulates pauseTreasury; throws a typed error on revert. */ + pauseTreasury(message: Hex, options?: CallSignerOptions): Promise; + /** Simulates unpauseTreasury; throws a typed error on revert. */ + unpauseTreasury(message: Hex, options?: CallSignerOptions): Promise; + /** Simulates cancelTreasury; throws a typed error on revert. */ + cancelTreasury(message: Hex, options?: CallSignerOptions): Promise; +} + +/** Event helpers for PaymentTreasury. */ +export interface PaymentTreasuryEvents {} + +/** + * Full PaymentTreasury entity (reads, writes, simulate, events). + * + * This entity is compatible with both on-chain treasury variants: + * - **PaymentTreasury** — standard payment treasury with no time restrictions. + * - **TimeConstrainedPaymentTreasury** — payment treasury that enforces launch-time + * and deadline constraints on-chain. Time checks are applied transparently by the + * contract; the SDK interface is identical for both variants. + */ +export type PaymentTreasuryEntity = PaymentTreasuryReads & + PaymentTreasuryWrites & { + simulate: PaymentTreasurySimulate; + events: PaymentTreasuryEvents; + }; diff --git a/packages/contracts/src/contracts/payment-treasury/writes.ts b/packages/contracts/src/contracts/payment-treasury/writes.ts new file mode 100644 index 00000000..d822ffc7 --- /dev/null +++ b/packages/contracts/src/contracts/payment-treasury/writes.ts @@ -0,0 +1,229 @@ +import type { Address, Hex, WalletClient, Chain } from "../../lib"; +import { PAYMENT_TREASURY_ABI } from "./abi"; +import { requireSigner, requireAccount } from "../../utils/account"; +import type { PaymentTreasuryWrites } from "./types"; +import type { LineItem, ExternalFees } from "../../types/structs"; +import type { CallSignerOptions } from "../../client/types"; + +/** + * Builds write methods for a PaymentTreasury contract instance. + * @param address - Deployed PaymentTreasury contract address + * @param walletClient - Viem WalletClient used to call writeContract; must have an account attached + * @param chain - Chain passed to writeContract for EIP-1559 and replay protection + * @returns Write methods bound to the given contract address + */ +export function createPaymentTreasuryWrites( + address: Address, + walletClient: WalletClient | null, + chain: Chain, +): PaymentTreasuryWrites { + const contract = { address, abi: PAYMENT_TREASURY_ABI } as const; + + return { + async createPayment( + paymentId: Hex, + buyerId: Hex, + itemId: Hex, + paymentToken: Address, + amount: bigint, + expiration: bigint, + lineItems: readonly LineItem[], + externalFees: readonly ExternalFees[], + options?: CallSignerOptions, + ) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "createPayment", + args: [ + paymentId, + buyerId, + itemId, + paymentToken, + amount, + expiration, + [...lineItems] as { typeId: Hex; amount: bigint }[], + [...externalFees] as { feeType: Hex; feeAmount: bigint }[], + ], + }); + }, + async createPaymentBatch( + paymentIds: readonly Hex[], + buyerIds: readonly Hex[], + itemIds: readonly Hex[], + paymentTokens: readonly Address[], + amounts: readonly bigint[], + expirations: readonly bigint[], + lineItemsArray: readonly (readonly LineItem[])[], + externalFeesArray: readonly (readonly ExternalFees[])[], + options?: CallSignerOptions, + ) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "createPaymentBatch", + args: [ + [...paymentIds], + [...buyerIds], + [...itemIds], + [...paymentTokens], + [...amounts], + [...expirations], + lineItemsArray.map((li) => [...li]) as { typeId: Hex; amount: bigint }[][], + externalFeesArray.map((ef) => [...ef]) as { feeType: Hex; feeAmount: bigint }[][], + ], + }); + }, + async processCryptoPayment( + paymentId: Hex, + itemId: Hex, + buyerAddress: Address, + paymentToken: Address, + amount: bigint, + lineItems: readonly LineItem[], + externalFees: readonly ExternalFees[], + options?: CallSignerOptions, + ) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "processCryptoPayment", + args: [ + paymentId, + itemId, + buyerAddress, + paymentToken, + amount, + [...lineItems] as { typeId: Hex; amount: bigint }[], + [...externalFees] as { feeType: Hex; feeAmount: bigint }[], + ], + }); + }, + async cancelPayment(paymentId: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "cancelPayment", + args: [paymentId], + }); + }, + async confirmPayment(paymentId: Hex, buyerAddress: Address, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "confirmPayment", + args: [paymentId, buyerAddress], + }); + }, + async confirmPaymentBatch(paymentIds: readonly Hex[], buyerAddresses: readonly Address[], options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "confirmPaymentBatch", + args: [[...paymentIds], [...buyerAddresses]], + }); + }, + async disburseFees(options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "disburseFees", + args: [], + }); + }, + async withdraw(options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "withdraw", + args: [], + }); + }, + async claimRefund(paymentId: Hex, refundAddress: Address, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "claimRefund", + args: [paymentId, refundAddress], + }); + }, + async claimRefundSelf(paymentId: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "claimRefund", + args: [paymentId], + }); + }, + async claimExpiredFunds(options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "claimExpiredFunds", + args: [], + }); + }, + async claimNonGoalLineItems(token: Address, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "claimNonGoalLineItems", + args: [token], + }); + }, + async pauseTreasury(message: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "pauseTreasury", + args: [message], + }); + }, + async unpauseTreasury(message: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "unpauseTreasury", + args: [message], + }); + }, + async cancelTreasury(message: Hex, options?: CallSignerOptions) { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ + ...contract, + chain, + account, + functionName: "cancelTreasury", + args: [message], + }); + }, + }; +} diff --git a/packages/contracts/src/contracts/treasury-factory/abi.ts b/packages/contracts/src/contracts/treasury-factory/abi.ts new file mode 100644 index 00000000..1ba9eb8a --- /dev/null +++ b/packages/contracts/src/contracts/treasury-factory/abi.ts @@ -0,0 +1,110 @@ +/** + * TreasuryFactory ABI — from provided ITreasuryFactory + TreasuryFactory.sol. + * UUPS; initialize(globalParams); register/approve/disapprove/remove implementation; deploy returns address. + */ +export const TREASURY_FACTORY_ABI = [ + { inputs: [], name: "AdminAccessCheckerUnauthorized", type: "error" }, + { inputs: [], name: "TreasuryFactoryUnauthorized", type: "error" }, + { inputs: [], name: "TreasuryFactoryInvalidKey", type: "error" }, + { inputs: [], name: "TreasuryFactoryTreasuryCreationFailed", type: "error" }, + { inputs: [], name: "TreasuryFactoryInvalidAddress", type: "error" }, + { inputs: [], name: "TreasuryFactoryImplementationNotSet", type: "error" }, + { inputs: [], name: "TreasuryFactoryImplementationNotSetOrApproved", type: "error" }, + { inputs: [], name: "TreasuryFactoryTreasuryInitializationFailed", type: "error" }, + { inputs: [], name: "TreasuryFactorySettingPlatformInfoFailed", type: "error" }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "platformHash", type: "bytes32" }, + { indexed: true, internalType: "uint256", name: "implementationId", type: "uint256" }, + { indexed: true, internalType: "address", name: "infoAddress", type: "address" }, + { indexed: false, internalType: "address", name: "treasuryAddress", type: "address" }, + ], + name: "TreasuryFactoryTreasuryDeployed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "platformHash", type: "bytes32" }, + { indexed: true, internalType: "uint256", name: "implementationId", type: "uint256" }, + { indexed: true, internalType: "address", name: "implementation", type: "address" }, + ], + name: "TreasuryImplementationRegistered", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "bytes32", name: "platformHash", type: "bytes32" }, + { indexed: true, internalType: "uint256", name: "implementationId", type: "uint256" }, + ], + name: "TreasuryImplementationRemoved", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "implementation", type: "address" }, + { indexed: false, internalType: "bool", name: "isApproved", type: "bool" }, + ], + name: "TreasuryImplementationApproval", + type: "event", + }, + { + inputs: [ + { internalType: "bytes32", name: "platformHash", type: "bytes32" }, + { internalType: "uint256", name: "implementationId", type: "uint256" }, + { internalType: "address", name: "implementation", type: "address" }, + ], + name: "registerTreasuryImplementation", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "platformHash", type: "bytes32" }, + { internalType: "uint256", name: "implementationId", type: "uint256" }, + ], + name: "approveTreasuryImplementation", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "implementation", type: "address" }], + name: "disapproveTreasuryImplementation", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "platformHash", type: "bytes32" }, + { internalType: "uint256", name: "implementationId", type: "uint256" }, + ], + name: "removeTreasuryImplementation", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "platformHash", type: "bytes32" }, + { internalType: "address", name: "infoAddress", type: "address" }, + { internalType: "uint256", name: "implementationId", type: "uint256" }, + ], + name: "deploy", + outputs: [{ internalType: "address", name: "clone", type: "address" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "contract IGlobalParams", name: "globalParams", type: "address" }], + name: "initialize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; diff --git a/packages/contracts/src/contracts/treasury-factory/events.ts b/packages/contracts/src/contracts/treasury-factory/events.ts new file mode 100644 index 00000000..49670f1a --- /dev/null +++ b/packages/contracts/src/contracts/treasury-factory/events.ts @@ -0,0 +1,18 @@ +import type { Address, PublicClient } from "../../lib"; +import type { TreasuryFactoryEvents } from "./types"; + +// TODO: Add event filter factories (filterTreasuryDeployed), log decoder (decodeLog), +// and watcher factories using getLogs / watchEvent. + +/** + * Builds event helpers for a TreasuryFactory contract instance. + * @param _address - Deployed TreasuryFactory contract address + * @param _publicClient - Viem PublicClient used to call getLogs + * @returns Event helpers bound to the given contract address + */ +export function createTreasuryFactoryEvents( + _address: Address, + _publicClient: PublicClient, +): TreasuryFactoryEvents { + return {}; +} diff --git a/packages/contracts/src/contracts/treasury-factory/index.ts b/packages/contracts/src/contracts/treasury-factory/index.ts new file mode 100644 index 00000000..cbee71f2 --- /dev/null +++ b/packages/contracts/src/contracts/treasury-factory/index.ts @@ -0,0 +1,30 @@ +import type { Address, PublicClient, WalletClient, Chain } from "../../lib"; +import { createTreasuryFactoryReads } from "./reads"; +import { createTreasuryFactoryWrites } from "./writes"; +import { createTreasuryFactorySimulate } from "./simulate"; +import { createTreasuryFactoryEvents } from "./events"; +import type { TreasuryFactoryEntity } from "./types"; + +/** + * Creates a fully composed TreasuryFactory entity combining reads, writes, simulate, and events. + * @param address - Deployed TreasuryFactory contract address + * @param publicClient - Viem PublicClient used for reads and simulation + * @param walletClient - Viem WalletClient used for writes; must have an account attached + * @param chain - Chain passed to writeContract and simulateContract + * @returns Composed entity exposing all TreasuryFactory methods under a single object + */ +export function createTreasuryFactoryEntity( + address: Address, + publicClient: PublicClient, + walletClient: WalletClient | null, + chain: Chain, +): TreasuryFactoryEntity { + return { + ...createTreasuryFactoryReads(address, publicClient), + ...createTreasuryFactoryWrites(address, walletClient, chain), + simulate: createTreasuryFactorySimulate(address, publicClient, walletClient, chain), + events: createTreasuryFactoryEvents(address, publicClient), + }; +} + +export type { TreasuryFactoryEntity } from "./types"; diff --git a/packages/contracts/src/contracts/treasury-factory/reads.ts b/packages/contracts/src/contracts/treasury-factory/reads.ts new file mode 100644 index 00000000..38c8df90 --- /dev/null +++ b/packages/contracts/src/contracts/treasury-factory/reads.ts @@ -0,0 +1,16 @@ +import type { Address, PublicClient } from "../../lib"; +import type { TreasuryFactoryReads } from "./types"; + +/** + * Builds read methods for a TreasuryFactory contract instance. + * TreasuryFactory has no public read functions in the ABI; returns an empty object. + * @param _address - Deployed TreasuryFactory contract address + * @param _publicClient - Viem PublicClient (unused; reserved for future reads) + * @returns Read methods bound to the given contract address + */ +export function createTreasuryFactoryReads( + _address: Address, + _publicClient: PublicClient, +): TreasuryFactoryReads { + return {}; +} diff --git a/packages/contracts/src/contracts/treasury-factory/simulate.ts b/packages/contracts/src/contracts/treasury-factory/simulate.ts new file mode 100644 index 00000000..b399cd6e --- /dev/null +++ b/packages/contracts/src/contracts/treasury-factory/simulate.ts @@ -0,0 +1,88 @@ +import type { Address, Hex, PublicClient, WalletClient, Chain } from "../../lib"; +import { TREASURY_FACTORY_ABI } from "./abi"; +import { requireSigner, requireAccount } from "../../utils/account"; +import { simulateWithErrorDecode } from "../../errors"; +import type { TreasuryFactorySimulate } from "./types"; +import type { CallSignerOptions } from "../../client/types"; + +/** + * Builds simulate methods for TreasuryFactory write calls. + * Each method calls simulateContract against the current chain state and throws a typed + * SDK error on revert, decoded via simulateWithErrorDecode. + * @param address - Deployed TreasuryFactory contract address + * @param publicClient - Viem PublicClient used to call simulateContract + * @param walletClient - Viem WalletClient used to resolve the account for simulation + * @param chain - Chain passed to simulateContract + * @returns Simulation methods bound to the given contract address + */ +export function createTreasuryFactorySimulate( + address: Address, + publicClient: PublicClient, + walletClient: WalletClient | null, + chain: Chain, +): TreasuryFactorySimulate { + const contract = { address, abi: TREASURY_FACTORY_ABI } as const; + + return { + async deploy(platformHash: Hex, infoAddress: Address, implementationId: bigint, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "deploy", + args: [platformHash, infoAddress, implementationId], + }), + ); + }, + async registerTreasuryImplementation(platformHash: Hex, implementationId: bigint, implementation: Address, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "registerTreasuryImplementation", + args: [platformHash, implementationId, implementation], + }), + ); + }, + async approveTreasuryImplementation(platformHash: Hex, implementationId: bigint, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "approveTreasuryImplementation", + args: [platformHash, implementationId], + }), + ); + }, + async disapproveTreasuryImplementation(implementation: Address, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "disapproveTreasuryImplementation", + args: [implementation], + }), + ); + }, + async removeTreasuryImplementation(platformHash: Hex, implementationId: bigint, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + await simulateWithErrorDecode(() => + publicClient.simulateContract({ + ...contract, + chain, + account, + functionName: "removeTreasuryImplementation", + args: [platformHash, implementationId], + }), + ); + }, + }; +} diff --git a/packages/contracts/src/contracts/treasury-factory/types.ts b/packages/contracts/src/contracts/treasury-factory/types.ts new file mode 100644 index 00000000..c41679f8 --- /dev/null +++ b/packages/contracts/src/contracts/treasury-factory/types.ts @@ -0,0 +1,45 @@ +import type { Address, Hex } from "../../lib"; +import type { CallSignerOptions } from "../../client/types"; + +/** Read-only methods for TreasuryFactory (none in ABI). */ +export interface TreasuryFactoryReads {} + +/** Write methods for a TreasuryFactory contract instance. */ +export interface TreasuryFactoryWrites { + /** Deploys a new treasury clone for the given platform, info address, and implementation ID. */ + deploy(platformHash: Hex, infoAddress: Address, implementationId: bigint, options?: CallSignerOptions): Promise; + /** Registers a treasury implementation for a platform and implementation ID. */ + registerTreasuryImplementation(platformHash: Hex, implementationId: bigint, implementation: Address, options?: CallSignerOptions): Promise; + /** Approves a registered treasury implementation for use. */ + approveTreasuryImplementation(platformHash: Hex, implementationId: bigint, options?: CallSignerOptions): Promise; + /** Disapproves a treasury implementation by address. */ + disapproveTreasuryImplementation(implementation: Address, options?: CallSignerOptions): Promise; + /** Removes a treasury implementation registration for a platform and implementation ID. */ + removeTreasuryImplementation(platformHash: Hex, implementationId: bigint, options?: CallSignerOptions): Promise; +} + +/** Simulate counterparts for TreasuryFactory write methods. */ +export interface TreasuryFactorySimulate { + /** Simulates deploy; throws a typed error on revert. */ + deploy(platformHash: Hex, infoAddress: Address, implementationId: bigint, options?: CallSignerOptions): Promise; + /** Simulates registerTreasuryImplementation; throws a typed error on revert. */ + registerTreasuryImplementation(platformHash: Hex, implementationId: bigint, implementation: Address, options?: CallSignerOptions): Promise; + /** Simulates approveTreasuryImplementation; throws a typed error on revert. */ + approveTreasuryImplementation(platformHash: Hex, implementationId: bigint, options?: CallSignerOptions): Promise; + /** Simulates disapproveTreasuryImplementation; throws a typed error on revert. */ + disapproveTreasuryImplementation(implementation: Address, options?: CallSignerOptions): Promise; + /** Simulates removeTreasuryImplementation; throws a typed error on revert. */ + removeTreasuryImplementation(platformHash: Hex, implementationId: bigint, options?: CallSignerOptions): Promise; +} + +/** Event helpers for a TreasuryFactory contract instance. */ +export interface TreasuryFactoryEvents {} + +/** Full TreasuryFactory entity combining reads, writes, simulate, and events. */ +export type TreasuryFactoryEntity = TreasuryFactoryReads & + TreasuryFactoryWrites & { + /** Simulation counterparts for every write method. */ + simulate: TreasuryFactorySimulate; + /** Event helpers for filtering and watching logs. */ + events: TreasuryFactoryEvents; +}; diff --git a/packages/contracts/src/contracts/treasury-factory/writes.ts b/packages/contracts/src/contracts/treasury-factory/writes.ts new file mode 100644 index 00000000..9151c7a8 --- /dev/null +++ b/packages/contracts/src/contracts/treasury-factory/writes.ts @@ -0,0 +1,43 @@ +import type { Address, Hex, WalletClient, Chain } from "../../lib"; +import { TREASURY_FACTORY_ABI } from "./abi"; +import { requireSigner, requireAccount } from "../../utils/account"; +import type { TreasuryFactoryWrites } from "./types"; +import type { CallSignerOptions } from "../../client/types"; + +/** + * Builds write methods for a TreasuryFactory contract instance. + * @param address - Deployed TreasuryFactory contract address + * @param walletClient - Viem WalletClient used to call writeContract; must have an account attached + * @param chain - Chain passed to writeContract for EIP-1559 and replay protection + * @returns Write methods bound to the given contract address + */ +export function createTreasuryFactoryWrites( + address: Address, + walletClient: WalletClient | null, + chain: Chain, +): TreasuryFactoryWrites { + const contract = { address, abi: TREASURY_FACTORY_ABI } as const; + + return { + async deploy(platformHash: Hex, infoAddress: Address, implementationId: bigint, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "deploy", args: [platformHash, infoAddress, implementationId] }); + }, + async registerTreasuryImplementation(platformHash: Hex, implementationId: bigint, implementation: Address, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "registerTreasuryImplementation", args: [platformHash, implementationId, implementation] }); + }, + async approveTreasuryImplementation(platformHash: Hex, implementationId: bigint, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "approveTreasuryImplementation", args: [platformHash, implementationId] }); + }, + async disapproveTreasuryImplementation(implementation: Address, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "disapproveTreasuryImplementation", args: [implementation] }); + }, + async removeTreasuryImplementation(platformHash: Hex, implementationId: bigint, options?: CallSignerOptions): Promise { + const signer = requireSigner(options?.signer ?? walletClient); const account = requireAccount(signer); + return signer.writeContract({ ...contract, chain, account, functionName: "removeTreasuryImplementation", args: [platformHash, implementationId] }); + }, + }; +} diff --git a/packages/contracts/src/errors/base.ts b/packages/contracts/src/errors/base.ts new file mode 100644 index 00000000..79c6d78d --- /dev/null +++ b/packages/contracts/src/errors/base.ts @@ -0,0 +1,10 @@ +/** + * Base interface implemented by all typed SDK contract revert errors. + * Every per-contract error class extends Error and implements this interface. + */ +export interface ContractErrorBase { + readonly name: string; + readonly args: Record; + /** Optional human-readable recovery suggestion for developers. */ + readonly recoveryHint?: string; +} diff --git a/packages/contracts/src/errors/contracts/all-or-nothing.ts b/packages/contracts/src/errors/contracts/all-or-nothing.ts new file mode 100644 index 00000000..85e53d64 --- /dev/null +++ b/packages/contracts/src/errors/contracts/all-or-nothing.ts @@ -0,0 +1,152 @@ +import type { ContractErrorBase } from "../base"; + +/** AllOrNothing + TreasurySuccessConditionNotFulfilled error name strings. */ +export const AllOrNothingErrorNames = { + FeeNotDisbursed: "AllOrNothingFeeNotDisbursed", + FeeAlreadyDisbursed: "AllOrNothingFeeAlreadyDisbursed", + InvalidInput: "AllOrNothingInvalidInput", + NotClaimable: "AllOrNothingNotClaimable", + NotSuccessful: "AllOrNothingNotSuccessful", + RewardExists: "AllOrNothingRewardExists", + TransferFailed: "AllOrNothingTransferFailed", + UnAuthorized: "AllOrNothingUnAuthorized", + TokenNotAccepted: "AllOrNothingTokenNotAccepted", + TreasurySuccessConditionNotFulfilled: "TreasurySuccessConditionNotFulfilled", +} as const; + +/** Thrown when a withdrawal is attempted before protocol/platform fees have been disbursed. */ +export class AllOrNothingFeeNotDisbursedError extends Error implements ContractErrorBase { + readonly name = AllOrNothingErrorNames.FeeNotDisbursed; + readonly args: Record = {}; + readonly recoveryHint = "Fees have not been disbursed yet. Call disburseFees() before withdrawing."; + + constructor() { + super(`${AllOrNothingErrorNames.FeeNotDisbursed}()`); + Object.setPrototypeOf(this, AllOrNothingFeeNotDisbursedError.prototype); + } +} + +/** Thrown when disburseFees is called after fees have already been disbursed. */ +export class AllOrNothingFeeAlreadyDisbursedError extends Error implements ContractErrorBase { + readonly name = AllOrNothingErrorNames.FeeAlreadyDisbursed; + readonly args: Record = {}; + readonly recoveryHint = "Fees have already been disbursed for this campaign."; + + constructor() { + super(`${AllOrNothingErrorNames.FeeAlreadyDisbursed}()`); + Object.setPrototypeOf(this, AllOrNothingFeeAlreadyDisbursedError.prototype); + } +} + +/** Thrown when one or more inputs to an AllOrNothing write are invalid. */ +export class AllOrNothingInvalidInputError extends Error implements ContractErrorBase { + readonly name = AllOrNothingErrorNames.InvalidInput; + readonly args: Record = {}; + readonly recoveryHint = "One or more inputs are invalid. Check all provided parameters."; + + constructor() { + super(`${AllOrNothingErrorNames.InvalidInput}()`); + Object.setPrototypeOf(this, AllOrNothingInvalidInputError.prototype); + } +} + +/** Thrown when claimRefund is called for a pledge NFT that is not eligible for a refund. */ +export class AllOrNothingNotClaimableError extends Error implements ContractErrorBase { + readonly name = AllOrNothingErrorNames.NotClaimable; + readonly args: { tokenId: string }; + readonly recoveryHint = + "This pledge NFT is not claimable as a refund. The campaign may have succeeded or the refund window has not opened."; + + constructor(args: { tokenId: string }) { + super(`${AllOrNothingErrorNames.NotClaimable}(tokenId: ${args.tokenId})`); + this.args = args; + Object.setPrototypeOf(this, AllOrNothingNotClaimableError.prototype); + } +} + +/** Thrown when a withdrawal or success-gated operation is attempted on a campaign that did not reach its goal. */ +export class AllOrNothingNotSuccessfulError extends Error implements ContractErrorBase { + readonly name = AllOrNothingErrorNames.NotSuccessful; + readonly args: Record = {}; + readonly recoveryHint = "The campaign has not reached its goal. This operation requires a successful campaign."; + + constructor() { + super(`${AllOrNothingErrorNames.NotSuccessful}()`); + Object.setPrototypeOf(this, AllOrNothingNotSuccessfulError.prototype); + } +} + +/** Thrown when addRewards is called with a reward name that is already registered. */ +export class AllOrNothingRewardExistsError extends Error implements ContractErrorBase { + readonly name = AllOrNothingErrorNames.RewardExists; + readonly args: Record = {}; + readonly recoveryHint = "A reward with this name already exists. Use a different reward name or remove the existing one first."; + + constructor() { + super(`${AllOrNothingErrorNames.RewardExists}()`); + Object.setPrototypeOf(this, AllOrNothingRewardExistsError.prototype); + } +} + +/** Thrown when an ERC-20 token transfer within the AllOrNothing treasury fails. */ +export class AllOrNothingTransferFailedError extends Error implements ContractErrorBase { + readonly name = AllOrNothingErrorNames.TransferFailed; + readonly args: Record = {}; + readonly recoveryHint = "Token transfer failed. Check token balances and allowances."; + + constructor() { + super(`${AllOrNothingErrorNames.TransferFailed}()`); + Object.setPrototypeOf(this, AllOrNothingTransferFailedError.prototype); + } +} + +/** Thrown when the caller is not authorised for the attempted AllOrNothing operation. */ +export class AllOrNothingUnAuthorizedError extends Error implements ContractErrorBase { + readonly name = AllOrNothingErrorNames.UnAuthorized; + readonly args: Record = {}; + readonly recoveryHint = "Caller is not authorized for this operation on the AllOrNothing treasury."; + + constructor() { + super(`${AllOrNothingErrorNames.UnAuthorized}()`); + Object.setPrototypeOf(this, AllOrNothingUnAuthorizedError.prototype); + } +} + +/** Thrown when a pledge token is not on the accepted list for this campaign's currency. */ +export class AllOrNothingTokenNotAcceptedError extends Error implements ContractErrorBase { + readonly name = AllOrNothingErrorNames.TokenNotAccepted; + readonly args: { token: string }; + readonly recoveryHint = "This token is not accepted for pledging. Use an accepted token for this campaign."; + + constructor(args: { token: string }) { + super(`${AllOrNothingErrorNames.TokenNotAccepted}(token: ${args.token})`); + this.args = args; + Object.setPrototypeOf(this, AllOrNothingTokenNotAcceptedError.prototype); + } +} + +/** Thrown when a success-gated operation is attempted before the campaign goal has been met. */ +export class TreasurySuccessConditionNotFulfilledError extends Error implements ContractErrorBase { + readonly name = AllOrNothingErrorNames.TreasurySuccessConditionNotFulfilled; + readonly args: Record = {}; + readonly recoveryHint = + "The campaign success condition has not been fulfilled. The goal amount must be reached before withdrawing."; + + constructor() { + super(`${AllOrNothingErrorNames.TreasurySuccessConditionNotFulfilled}()`); + Object.setPrototypeOf(this, TreasurySuccessConditionNotFulfilledError.prototype); + } +} + +/** Union of all typed errors that can be thrown by AllOrNothing treasury contract calls. */ +export type AllOrNothingError = + | AllOrNothingFeeNotDisbursedError + | AllOrNothingFeeAlreadyDisbursedError + | AllOrNothingInvalidInputError + | AllOrNothingNotClaimableError + | AllOrNothingNotSuccessfulError + | AllOrNothingRewardExistsError + | AllOrNothingTransferFailedError + | AllOrNothingUnAuthorizedError + | AllOrNothingTokenNotAcceptedError + | TreasurySuccessConditionNotFulfilledError; diff --git a/packages/contracts/src/errors/contracts/campaign-info-factory.ts b/packages/contracts/src/errors/contracts/campaign-info-factory.ts new file mode 100644 index 00000000..3dff2027 --- /dev/null +++ b/packages/contracts/src/errors/contracts/campaign-info-factory.ts @@ -0,0 +1,92 @@ +import type { ContractErrorBase } from "../base"; + +/** CampaignInfoFactory + CampaignInfoInvalidTokenList error name strings. */ +export const CampaignInfoFactoryErrorNames = { + CampaignInitializationFailed: "CampaignInfoFactoryCampaignInitializationFailed", + InvalidInput: "CampaignInfoFactoryInvalidInput", + PlatformNotListed: "CampaignInfoFactoryPlatformNotListed", + CampaignWithSameIdentifierExists: "CampaignInfoFactoryCampaignWithSameIdentifierExists", + CampaignInfoInvalidTokenList: "CampaignInfoInvalidTokenList", +} as const; + +/** Thrown when the campaign clone is deployed but fails to initialise with the provided data. */ +export class CampaignInfoFactoryCampaignInitializationFailedError + extends Error + implements ContractErrorBase +{ + readonly name = CampaignInfoFactoryErrorNames.CampaignInitializationFailed; + readonly args: Record = {}; + readonly recoveryHint = + "Campaign initialization failed. Check campaign data, platform listing, and implementation."; + + constructor() { + super("CampaignInfoFactoryCampaignInitializationFailed()"); + Object.setPrototypeOf(this, CampaignInfoFactoryCampaignInitializationFailedError.prototype); + } +} + +/** Thrown when one or more createCampaign inputs are invalid (e.g. zero address, empty arrays). */ +export class CampaignInfoFactoryInvalidInputError extends Error implements ContractErrorBase { + readonly name = CampaignInfoFactoryErrorNames.InvalidInput; + readonly args: Record = {}; + readonly recoveryHint = + "Invalid input for campaign creation. Check creator, platforms, and campaign data."; + + constructor() { + super("CampaignInfoFactoryInvalidInput()"); + Object.setPrototypeOf(this, CampaignInfoFactoryInvalidInputError.prototype); + } +} + +/** Thrown when a selected platform is not enlisted in GlobalParams. */ +export class CampaignInfoFactoryPlatformNotListedError extends Error implements ContractErrorBase { + readonly name = CampaignInfoFactoryErrorNames.PlatformNotListed; + readonly args: { platformHash: string }; + readonly recoveryHint = "The given platform is not listed in GlobalParams. Enlist the platform first."; + + constructor(args: { platformHash: string }) { + super(`CampaignInfoFactoryPlatformNotListed(platformHash: ${args.platformHash})`); + this.args = args; + Object.setPrototypeOf(this, CampaignInfoFactoryPlatformNotListedError.prototype); + } +} + +/** Thrown when createCampaign is called with an identifier hash that is already in use. */ +export class CampaignInfoFactoryCampaignWithSameIdentifierExistsError + extends Error + implements ContractErrorBase +{ + readonly name = CampaignInfoFactoryErrorNames.CampaignWithSameIdentifierExists; + readonly args: { identifierHash: string; cloneExists: string }; + readonly recoveryHint = + "A campaign with this identifier already exists. Use a different identifier or the existing campaign."; + + constructor(args: { identifierHash: string; cloneExists: string }) { + super( + `CampaignInfoFactoryCampaignWithSameIdentifierExists(identifierHash: ${args.identifierHash}, cloneExists: ${args.cloneExists})`, + ); + this.args = args; + Object.setPrototypeOf(this, CampaignInfoFactoryCampaignWithSameIdentifierExistsError.prototype); + } +} + +/** Thrown when the campaign currency's token list does not match the GlobalParams registry. */ +export class CampaignInfoInvalidTokenListError extends Error implements ContractErrorBase { + readonly name = CampaignInfoFactoryErrorNames.CampaignInfoInvalidTokenList; + readonly args: Record = {}; + readonly recoveryHint = + "Campaign currency tokens do not match GlobalParams for the campaign currency. Fix token list."; + + constructor() { + super("CampaignInfoInvalidTokenList()"); + Object.setPrototypeOf(this, CampaignInfoInvalidTokenListError.prototype); + } +} + +/** Union of all typed errors that can be thrown by CampaignInfoFactory contract calls. */ +export type CampaignInfoFactoryError = + | CampaignInfoFactoryCampaignInitializationFailedError + | CampaignInfoFactoryInvalidInputError + | CampaignInfoFactoryPlatformNotListedError + | CampaignInfoFactoryCampaignWithSameIdentifierExistsError + | CampaignInfoInvalidTokenListError; diff --git a/packages/contracts/src/errors/contracts/campaign-info.ts b/packages/contracts/src/errors/contracts/campaign-info.ts new file mode 100644 index 00000000..b8503ea5 --- /dev/null +++ b/packages/contracts/src/errors/contracts/campaign-info.ts @@ -0,0 +1,98 @@ +import type { ContractErrorBase } from "../base"; + +/** CampaignInfo error name strings. */ +export const CampaignInfoErrorNames = { + InvalidInput: "CampaignInfoInvalidInput", + InvalidPlatformUpdate: "CampaignInfoInvalidPlatformUpdate", + PlatformNotSelected: "CampaignInfoPlatformNotSelected", + PlatformAlreadyApproved: "CampaignInfoPlatformAlreadyApproved", + Unauthorized: "CampaignInfoUnauthorized", + IsLocked: "CampaignInfoIsLocked", +} as const; + +/** Thrown when one or more campaign info inputs are invalid. */ +export class CampaignInfoInvalidInputError extends Error implements ContractErrorBase { + readonly name = CampaignInfoErrorNames.InvalidInput; + readonly args: Record = {}; + readonly recoveryHint = "One or more campaign info inputs are invalid. Check all provided parameters."; + + constructor() { + super("CampaignInfoInvalidInput()"); + Object.setPrototypeOf(this, CampaignInfoInvalidInputError.prototype); + } +} + +/** Thrown when a platform selection update is invalid (e.g. already in the requested state). */ +export class CampaignInfoInvalidPlatformUpdateError extends Error implements ContractErrorBase { + readonly name = CampaignInfoErrorNames.InvalidPlatformUpdate; + readonly args: { platformBytes: string; selection: boolean }; + readonly recoveryHint = + "Invalid platform selection update. The platform may already be in the requested state."; + + constructor(args: { platformBytes: string; selection: boolean }) { + super( + `CampaignInfoInvalidPlatformUpdate(platformBytes: ${args.platformBytes}, selection: ${args.selection})`, + ); + this.args = args; + Object.setPrototypeOf(this, CampaignInfoInvalidPlatformUpdateError.prototype); + } +} + +/** Thrown when an operation targets a platform that was not selected for the campaign. */ +export class CampaignInfoPlatformNotSelectedError extends Error implements ContractErrorBase { + readonly name = CampaignInfoErrorNames.PlatformNotSelected; + readonly args: { platformBytes: string }; + readonly recoveryHint = "The platform is not selected for this campaign. Select the platform first."; + + constructor(args: { platformBytes: string }) { + super(`CampaignInfoPlatformNotSelected(platformBytes: ${args.platformBytes})`); + this.args = args; + Object.setPrototypeOf(this, CampaignInfoPlatformNotSelectedError.prototype); + } +} + +/** Thrown when approving a platform that is already approved for the campaign. */ +export class CampaignInfoPlatformAlreadyApprovedError extends Error implements ContractErrorBase { + readonly name = CampaignInfoErrorNames.PlatformAlreadyApproved; + readonly args: { platformHash: string }; + readonly recoveryHint = "The platform is already approved for this campaign. No action required."; + + constructor(args: { platformHash: string }) { + super(`CampaignInfoPlatformAlreadyApproved(platformHash: ${args.platformHash})`); + this.args = args; + Object.setPrototypeOf(this, CampaignInfoPlatformAlreadyApprovedError.prototype); + } +} + +/** Thrown when the caller is not authorised to modify this campaign. */ +export class CampaignInfoUnauthorizedError extends Error implements ContractErrorBase { + readonly name = CampaignInfoErrorNames.Unauthorized; + readonly args: Record = {}; + readonly recoveryHint = "Caller is not authorized for this campaign operation."; + + constructor() { + super("CampaignInfoUnauthorized()"); + Object.setPrototypeOf(this, CampaignInfoUnauthorizedError.prototype); + } +} + +/** Thrown when a modification is attempted on a locked CampaignInfo. */ +export class CampaignInfoIsLockedError extends Error implements ContractErrorBase { + readonly name = CampaignInfoErrorNames.IsLocked; + readonly args: Record = {}; + readonly recoveryHint = "The campaign info is locked and cannot be modified."; + + constructor() { + super("CampaignInfoIsLocked()"); + Object.setPrototypeOf(this, CampaignInfoIsLockedError.prototype); + } +} + +/** Union of all typed errors that can be thrown by CampaignInfo contract calls. */ +export type CampaignInfoError = + | CampaignInfoInvalidInputError + | CampaignInfoInvalidPlatformUpdateError + | CampaignInfoPlatformNotSelectedError + | CampaignInfoPlatformAlreadyApprovedError + | CampaignInfoUnauthorizedError + | CampaignInfoIsLockedError; diff --git a/packages/contracts/src/errors/contracts/global-params.ts b/packages/contracts/src/errors/contracts/global-params.ts new file mode 100644 index 00000000..c70ffc0b --- /dev/null +++ b/packages/contracts/src/errors/contracts/global-params.ts @@ -0,0 +1,205 @@ +import type { ContractErrorBase } from "../base"; + +/** GlobalParams error name strings. */ +export const GlobalParamsErrorNames = { + InvalidInput: "GlobalParamsInvalidInput", + PlatformAdminNotSet: "GlobalParamsPlatformAdminNotSet", + PlatformAlreadyListed: "GlobalParamsPlatformAlreadyListed", + PlatformDataAlreadySet: "GlobalParamsPlatformDataAlreadySet", + PlatformDataNotSet: "GlobalParamsPlatformDataNotSet", + PlatformDataSlotTaken: "GlobalParamsPlatformDataSlotTaken", + PlatformFeePercentIsZero: "GlobalParamsPlatformFeePercentIsZero", + PlatformNotListed: "GlobalParamsPlatformNotListed", + Unauthorized: "GlobalParamsUnauthorized", + CurrencyTokenLengthMismatch: "GlobalParamsCurrencyTokenLengthMismatch", + CurrencyHasNoTokens: "GlobalParamsCurrencyHasNoTokens", + TokenNotInCurrency: "GlobalParamsTokenNotInCurrency", + PlatformLineItemTypeNotFound: "GlobalParamsPlatformLineItemTypeNotFound", +} as const; + +/** Thrown when one or more inputs (address, fee, or bytes) are invalid. */ +export class GlobalParamsInvalidInputError extends Error implements ContractErrorBase { + readonly name = GlobalParamsErrorNames.InvalidInput; + readonly args: Record = {}; + readonly recoveryHint = + "One or more inputs are invalid. Check addresses, fee percent, and platform bytes."; + + constructor() { + super("GlobalParamsInvalidInput()"); + Object.setPrototypeOf(this, GlobalParamsInvalidInputError.prototype); + } +} + +/** Thrown when an operation requires a platform admin that has not been set. */ +export class GlobalParamsPlatformAdminNotSetError extends Error implements ContractErrorBase { + readonly name = GlobalParamsErrorNames.PlatformAdminNotSet; + readonly args: { platformBytes: string }; + readonly recoveryHint = "Set the platform admin address before performing this operation."; + + constructor(args: { platformBytes: string }) { + super(`GlobalParamsPlatformAdminNotSet(platformBytes: ${args.platformBytes})`); + this.args = args; + Object.setPrototypeOf(this, GlobalParamsPlatformAdminNotSetError.prototype); + } +} + +/** Thrown when enlistPlatform is called for a platform that is already listed. */ +export class GlobalParamsPlatformAlreadyListedError extends Error implements ContractErrorBase { + readonly name = GlobalParamsErrorNames.PlatformAlreadyListed; + readonly args: { platformBytes: string }; + readonly recoveryHint = + "Platform is already listed. Use a different platform hash or update the existing platform."; + + constructor(args: { platformBytes: string }) { + super(`GlobalParamsPlatformAlreadyListed(platformBytes: ${args.platformBytes})`); + this.args = args; + Object.setPrototypeOf(this, GlobalParamsPlatformAlreadyListedError.prototype); + } +} + +/** Thrown when a platform data key is added that already exists. */ +export class GlobalParamsPlatformDataAlreadySetError extends Error implements ContractErrorBase { + readonly name = GlobalParamsErrorNames.PlatformDataAlreadySet; + readonly args: Record = {}; + readonly recoveryHint = "Platform data for this key is already set. Use a different key or update."; + + constructor() { + super("GlobalParamsPlatformDataAlreadySet()"); + Object.setPrototypeOf(this, GlobalParamsPlatformDataAlreadySetError.prototype); + } +} + +/** Thrown when an operation references a platform data key that has not been set. */ +export class GlobalParamsPlatformDataNotSetError extends Error implements ContractErrorBase { + readonly name = GlobalParamsErrorNames.PlatformDataNotSet; + readonly args: Record = {}; + readonly recoveryHint = "Platform data is not set. Add platform data first."; + + constructor() { + super("GlobalParamsPlatformDataNotSet()"); + Object.setPrototypeOf(this, GlobalParamsPlatformDataNotSetError.prototype); + } +} + +/** Thrown when a platform data slot is already occupied by another key. */ +export class GlobalParamsPlatformDataSlotTakenError extends Error implements ContractErrorBase { + readonly name = GlobalParamsErrorNames.PlatformDataSlotTaken; + readonly args: Record = {}; + readonly recoveryHint = "This platform data slot is already taken. Use a different key."; + + constructor() { + super("GlobalParamsPlatformDataSlotTaken()"); + Object.setPrototypeOf(this, GlobalParamsPlatformDataSlotTakenError.prototype); + } +} + +/** Thrown when a platform fee percent of zero is provided. */ +export class GlobalParamsPlatformFeePercentIsZeroError extends Error implements ContractErrorBase { + readonly name = GlobalParamsErrorNames.PlatformFeePercentIsZero; + readonly args: { platformBytes: string }; + readonly recoveryHint = "Platform fee percent must be greater than zero."; + + constructor(args: { platformBytes: string }) { + super(`GlobalParamsPlatformFeePercentIsZero(platformBytes: ${args.platformBytes})`); + this.args = args; + Object.setPrototypeOf(this, GlobalParamsPlatformFeePercentIsZeroError.prototype); + } +} + +/** Thrown when an operation targets a platform that is not enlisted. */ +export class GlobalParamsPlatformNotListedError extends Error implements ContractErrorBase { + readonly name = GlobalParamsErrorNames.PlatformNotListed; + readonly args: { platformBytes: string }; + readonly recoveryHint = + "Platform is not enlisted in GlobalParams. Enlist the platform first."; + + constructor(args: { platformBytes: string }) { + super(`GlobalParamsPlatformNotListed(platformBytes: ${args.platformBytes})`); + this.args = args; + Object.setPrototypeOf(this, GlobalParamsPlatformNotListedError.prototype); + } +} + +/** Thrown when the caller is not the protocol admin. */ +export class GlobalParamsUnauthorizedError extends Error implements ContractErrorBase { + readonly name = GlobalParamsErrorNames.Unauthorized; + readonly args: Record = {}; + readonly recoveryHint = "Caller is not authorized for this operation."; + + constructor() { + super("GlobalParamsUnauthorized()"); + Object.setPrototypeOf(this, GlobalParamsUnauthorizedError.prototype); + } +} + +/** Thrown when the currencies and tokensPerCurrency arrays have different lengths during initialisation. */ +export class GlobalParamsCurrencyTokenLengthMismatchError extends Error implements ContractErrorBase { + readonly name = GlobalParamsErrorNames.CurrencyTokenLengthMismatch; + readonly args: Record = {}; + readonly recoveryHint = + "Length of currencies array must match length of tokensPerCurrency. Check initialize or currency config."; + + constructor() { + super("GlobalParamsCurrencyTokenLengthMismatch()"); + Object.setPrototypeOf(this, GlobalParamsCurrencyTokenLengthMismatchError.prototype); + } +} + +/** Thrown when an operation requires accepted tokens for a currency that has none registered. */ +export class GlobalParamsCurrencyHasNoTokensError extends Error implements ContractErrorBase { + readonly name = GlobalParamsErrorNames.CurrencyHasNoTokens; + readonly args: { currency: string }; + readonly recoveryHint = "This currency has no accepted tokens. Add at least one token for the currency."; + + constructor(args: { currency: string }) { + super(`GlobalParamsCurrencyHasNoTokens(currency: ${args.currency})`); + this.args = args; + Object.setPrototypeOf(this, GlobalParamsCurrencyHasNoTokensError.prototype); + } +} + +/** Thrown when a token is not in the accepted list for the given currency. */ +export class GlobalParamsTokenNotInCurrencyError extends Error implements ContractErrorBase { + readonly name = GlobalParamsErrorNames.TokenNotInCurrency; + readonly args: { currency: string; token: string }; + readonly recoveryHint = + "The given token is not in the accepted list for this currency. Use an approved token."; + + constructor(args: { currency: string; token: string }) { + super(`GlobalParamsTokenNotInCurrency(currency: ${args.currency}, token: ${args.token})`); + this.args = args; + Object.setPrototypeOf(this, GlobalParamsTokenNotInCurrencyError.prototype); + } +} + +/** Thrown when a line item type is not registered for the given platform and type ID. */ +export class GlobalParamsPlatformLineItemTypeNotFoundError extends Error implements ContractErrorBase { + readonly name = GlobalParamsErrorNames.PlatformLineItemTypeNotFound; + readonly args: { platformHash: string; typeId: string }; + readonly recoveryHint = + "Platform line item type not found. Register the line item type for this platform first."; + + constructor(args: { platformHash: string; typeId: string }) { + super( + `GlobalParamsPlatformLineItemTypeNotFound(platformHash: ${args.platformHash}, typeId: ${args.typeId})`, + ); + this.args = args; + Object.setPrototypeOf(this, GlobalParamsPlatformLineItemTypeNotFoundError.prototype); + } +} + +/** Union of all typed errors that can be thrown by GlobalParams contract calls. */ +export type GlobalParamsError = + | GlobalParamsInvalidInputError + | GlobalParamsPlatformAdminNotSetError + | GlobalParamsPlatformAlreadyListedError + | GlobalParamsPlatformDataAlreadySetError + | GlobalParamsPlatformDataNotSetError + | GlobalParamsPlatformDataSlotTakenError + | GlobalParamsPlatformFeePercentIsZeroError + | GlobalParamsPlatformNotListedError + | GlobalParamsUnauthorizedError + | GlobalParamsCurrencyTokenLengthMismatchError + | GlobalParamsCurrencyHasNoTokensError + | GlobalParamsTokenNotInCurrencyError + | GlobalParamsPlatformLineItemTypeNotFoundError; diff --git a/packages/contracts/src/errors/contracts/item-registry.ts b/packages/contracts/src/errors/contracts/item-registry.ts new file mode 100644 index 00000000..a66880d9 --- /dev/null +++ b/packages/contracts/src/errors/contracts/item-registry.ts @@ -0,0 +1,22 @@ +import type { ContractErrorBase } from "../base"; + +/** ItemRegistry error name strings. */ +export const ItemRegistryErrorNames = { + MismatchedArraysLength: "ItemRegistryMismatchedArraysLength", +} as const; + +/** Thrown when itemIds and items arrays passed to addItemsBatch have different lengths. */ +export class ItemRegistryMismatchedArraysLengthError extends Error implements ContractErrorBase { + readonly name = ItemRegistryErrorNames.MismatchedArraysLength; + readonly args: Record = {}; + readonly recoveryHint = + "The itemIds and items arrays must have the same length. Ensure both arrays are equal in size."; + + constructor() { + super("ItemRegistryMismatchedArraysLength()"); + Object.setPrototypeOf(this, ItemRegistryMismatchedArraysLengthError.prototype); + } +} + +/** Union of all typed errors that can be thrown by ItemRegistry contract calls. */ +export type ItemRegistryError = ItemRegistryMismatchedArraysLengthError; diff --git a/packages/contracts/src/errors/contracts/keep-whats-raised.ts b/packages/contracts/src/errors/contracts/keep-whats-raised.ts new file mode 100644 index 00000000..445dbbe0 --- /dev/null +++ b/packages/contracts/src/errors/contracts/keep-whats-raised.ts @@ -0,0 +1,239 @@ +import type { ContractErrorBase } from "../base"; + +/** KeepWhatsRaised error name strings. */ +export const KeepWhatsRaisedErrorNames = { + UnAuthorized: "KeepWhatsRaisedUnAuthorized", + InvalidInput: "KeepWhatsRaisedInvalidInput", + TokenNotAccepted: "KeepWhatsRaisedTokenNotAccepted", + RewardExists: "KeepWhatsRaisedRewardExists", + Disabled: "KeepWhatsRaisedDisabled", + AlreadyEnabled: "KeepWhatsRaisedAlreadyEnabled", + InsufficientFundsForWithdrawalAndFee: "KeepWhatsRaisedInsufficientFundsForWithdrawalAndFee", + InsufficientFundsForFee: "KeepWhatsRaisedInsufficientFundsForFee", + AlreadyWithdrawn: "KeepWhatsRaisedAlreadyWithdrawn", + AlreadyClaimed: "KeepWhatsRaisedAlreadyClaimed", + NotClaimable: "KeepWhatsRaisedNotClaimable", + NotClaimableAdmin: "KeepWhatsRaisedNotClaimableAdmin", + ConfigLocked: "KeepWhatsRaisedConfigLocked", + DisbursementBlocked: "KeepWhatsRaisedDisbursementBlocked", + PledgeAlreadyProcessed: "KeepWhatsRaisedPledgeAlreadyProcessed", +} as const; + +/** Thrown when the caller is not authorised for the attempted KeepWhatsRaised operation. */ +export class KeepWhatsRaisedUnAuthorizedError extends Error implements ContractErrorBase { + readonly name = KeepWhatsRaisedErrorNames.UnAuthorized; + readonly args: Record = {}; + readonly recoveryHint = "Caller is not authorized for this operation on the KeepWhatsRaised treasury."; + + constructor() { + super("KeepWhatsRaisedUnAuthorized()"); + Object.setPrototypeOf(this, KeepWhatsRaisedUnAuthorizedError.prototype); + } +} + +/** Thrown when one or more inputs to a KeepWhatsRaised write are invalid. */ +export class KeepWhatsRaisedInvalidInputError extends Error implements ContractErrorBase { + readonly name = KeepWhatsRaisedErrorNames.InvalidInput; + readonly args: Record = {}; + readonly recoveryHint = "One or more inputs are invalid. Check all provided parameters."; + + constructor() { + super("KeepWhatsRaisedInvalidInput()"); + Object.setPrototypeOf(this, KeepWhatsRaisedInvalidInputError.prototype); + } +} + +/** Thrown when a pledge token is not accepted for this campaign's currency. */ +export class KeepWhatsRaisedTokenNotAcceptedError extends Error implements ContractErrorBase { + readonly name = KeepWhatsRaisedErrorNames.TokenNotAccepted; + readonly args: { token: string }; + readonly recoveryHint = "This token is not accepted for pledging. Use an accepted token for this campaign."; + + constructor(args: { token: string }) { + super(`KeepWhatsRaisedTokenNotAccepted(token: ${args.token})`); + this.args = args; + Object.setPrototypeOf(this, KeepWhatsRaisedTokenNotAcceptedError.prototype); + } +} + +/** Thrown when addRewards is called with a reward name that is already registered. */ +export class KeepWhatsRaisedRewardExistsError extends Error implements ContractErrorBase { + readonly name = KeepWhatsRaisedErrorNames.RewardExists; + readonly args: Record = {}; + readonly recoveryHint = "A reward with this name already exists. Use a different reward name or remove the existing one first."; + + constructor() { + super("KeepWhatsRaisedRewardExists()"); + Object.setPrototypeOf(this, KeepWhatsRaisedRewardExistsError.prototype); + } +} + +/** Thrown when an operation is attempted on a treasury that has been disabled. */ +export class KeepWhatsRaisedDisabledError extends Error implements ContractErrorBase { + readonly name = KeepWhatsRaisedErrorNames.Disabled; + readonly args: Record = {}; + readonly recoveryHint = "The KeepWhatsRaised treasury is disabled. Enable it before performing this operation."; + + constructor() { + super("KeepWhatsRaisedDisabled()"); + Object.setPrototypeOf(this, KeepWhatsRaisedDisabledError.prototype); + } +} + +/** Thrown when an enable operation is attempted on a treasury that is already enabled. */ +export class KeepWhatsRaisedAlreadyEnabledError extends Error implements ContractErrorBase { + readonly name = KeepWhatsRaisedErrorNames.AlreadyEnabled; + readonly args: Record = {}; + readonly recoveryHint = "The KeepWhatsRaised treasury is already enabled."; + + constructor() { + super("KeepWhatsRaisedAlreadyEnabled()"); + Object.setPrototypeOf(this, KeepWhatsRaisedAlreadyEnabledError.prototype); + } +} + +/** Thrown when the treasury balance is insufficient to cover both the withdrawal amount and the fee. */ +export class KeepWhatsRaisedInsufficientFundsForWithdrawalAndFeeError + extends Error + implements ContractErrorBase +{ + readonly name = KeepWhatsRaisedErrorNames.InsufficientFundsForWithdrawalAndFee; + readonly args: { availableAmount: string; withdrawalAmount: string; fee: string }; + readonly recoveryHint = + "Insufficient funds to cover both the withdrawal amount and fee. Reduce the withdrawal amount or wait for more pledges."; + + constructor(args: { availableAmount: string; withdrawalAmount: string; fee: string }) { + super( + `KeepWhatsRaisedInsufficientFundsForWithdrawalAndFee(availableAmount: ${args.availableAmount}, withdrawalAmount: ${args.withdrawalAmount}, fee: ${args.fee})`, + ); + this.args = args; + Object.setPrototypeOf( + this, + KeepWhatsRaisedInsufficientFundsForWithdrawalAndFeeError.prototype, + ); + } +} + +/** Thrown when the treasury balance is insufficient to cover the withdrawal fee alone. */ +export class KeepWhatsRaisedInsufficientFundsForFeeError extends Error implements ContractErrorBase { + readonly name = KeepWhatsRaisedErrorNames.InsufficientFundsForFee; + readonly args: { withdrawalAmount: string; fee: string }; + readonly recoveryHint = + "Insufficient funds to cover the withdrawal fee. Ensure the treasury has enough balance to pay the fee."; + + constructor(args: { withdrawalAmount: string; fee: string }) { + super( + `KeepWhatsRaisedInsufficientFundsForFee(withdrawalAmount: ${args.withdrawalAmount}, fee: ${args.fee})`, + ); + this.args = args; + Object.setPrototypeOf(this, KeepWhatsRaisedInsufficientFundsForFeeError.prototype); + } +} + +/** Thrown when a withdrawal is attempted after funds have already been withdrawn. */ +export class KeepWhatsRaisedAlreadyWithdrawnError extends Error implements ContractErrorBase { + readonly name = KeepWhatsRaisedErrorNames.AlreadyWithdrawn; + readonly args: Record = {}; + readonly recoveryHint = "Funds have already been withdrawn from this treasury."; + + constructor() { + super("KeepWhatsRaisedAlreadyWithdrawn()"); + Object.setPrototypeOf(this, KeepWhatsRaisedAlreadyWithdrawnError.prototype); + } +} + +/** Thrown when claimRefund is called for a pledge that has already been claimed or refunded. */ +export class KeepWhatsRaisedAlreadyClaimedError extends Error implements ContractErrorBase { + readonly name = KeepWhatsRaisedErrorNames.AlreadyClaimed; + readonly args: Record = {}; + readonly recoveryHint = "This pledge has already been claimed or refunded."; + + constructor() { + super("KeepWhatsRaisedAlreadyClaimed()"); + Object.setPrototypeOf(this, KeepWhatsRaisedAlreadyClaimedError.prototype); + } +} + +/** Thrown when claimRefund is called for a pledge NFT that does not meet refund conditions. */ +export class KeepWhatsRaisedNotClaimableError extends Error implements ContractErrorBase { + readonly name = KeepWhatsRaisedErrorNames.NotClaimable; + readonly args: { tokenId: string }; + readonly recoveryHint = + "This pledge NFT is not claimable for a refund. The campaign may still be active or refund conditions are not met."; + + constructor(args: { tokenId: string }) { + super(`KeepWhatsRaisedNotClaimable(tokenId: ${args.tokenId})`); + this.args = args; + Object.setPrototypeOf(this, KeepWhatsRaisedNotClaimableError.prototype); + } +} + +/** Thrown when an admin claim is attempted but the required campaign state conditions are not met. */ +export class KeepWhatsRaisedNotClaimableAdminError extends Error implements ContractErrorBase { + readonly name = KeepWhatsRaisedErrorNames.NotClaimableAdmin; + readonly args: Record = {}; + readonly recoveryHint = + "The admin claim conditions are not met. Ensure the campaign is in the correct state before claiming."; + + constructor() { + super("KeepWhatsRaisedNotClaimableAdmin()"); + Object.setPrototypeOf(this, KeepWhatsRaisedNotClaimableAdminError.prototype); + } +} + +/** Thrown when a configuration change is attempted during the config lock period. */ +export class KeepWhatsRaisedConfigLockedError extends Error implements ContractErrorBase { + readonly name = KeepWhatsRaisedErrorNames.ConfigLocked; + readonly args: Record = {}; + readonly recoveryHint = + "The treasury configuration is locked and cannot be modified. Wait for the lock period to expire."; + + constructor() { + super("KeepWhatsRaisedConfigLocked()"); + Object.setPrototypeOf(this, KeepWhatsRaisedConfigLockedError.prototype); + } +} + +/** Thrown when fee disbursement is blocked due to unmet conditions. */ +export class KeepWhatsRaisedDisbursementBlockedError extends Error implements ContractErrorBase { + readonly name = KeepWhatsRaisedErrorNames.DisbursementBlocked; + readonly args: Record = {}; + readonly recoveryHint = + "Fee disbursement is blocked. Ensure all required conditions are met before disbursing fees."; + + constructor() { + super("KeepWhatsRaisedDisbursementBlocked()"); + Object.setPrototypeOf(this, KeepWhatsRaisedDisbursementBlockedError.prototype); + } +} + +/** Thrown when a pledge ID has already been used; each pledge ID can only be processed once. */ +export class KeepWhatsRaisedPledgeAlreadyProcessedError extends Error implements ContractErrorBase { + readonly name = KeepWhatsRaisedErrorNames.PledgeAlreadyProcessed; + readonly args: { pledgeId: string }; + readonly recoveryHint = "This pledge ID has already been processed. Each pledge ID can only be used once."; + + constructor(args: { pledgeId: string }) { + super(`KeepWhatsRaisedPledgeAlreadyProcessed(pledgeId: ${args.pledgeId})`); + this.args = args; + Object.setPrototypeOf(this, KeepWhatsRaisedPledgeAlreadyProcessedError.prototype); + } +} + +/** Union of all typed errors that can be thrown by KeepWhatsRaised treasury contract calls. */ +export type KeepWhatsRaisedError = + | KeepWhatsRaisedUnAuthorizedError + | KeepWhatsRaisedInvalidInputError + | KeepWhatsRaisedTokenNotAcceptedError + | KeepWhatsRaisedRewardExistsError + | KeepWhatsRaisedDisabledError + | KeepWhatsRaisedAlreadyEnabledError + | KeepWhatsRaisedInsufficientFundsForWithdrawalAndFeeError + | KeepWhatsRaisedInsufficientFundsForFeeError + | KeepWhatsRaisedAlreadyWithdrawnError + | KeepWhatsRaisedAlreadyClaimedError + | KeepWhatsRaisedNotClaimableError + | KeepWhatsRaisedNotClaimableAdminError + | KeepWhatsRaisedConfigLockedError + | KeepWhatsRaisedDisbursementBlockedError + | KeepWhatsRaisedPledgeAlreadyProcessedError; diff --git a/packages/contracts/src/errors/contracts/payment-treasury.ts b/packages/contracts/src/errors/contracts/payment-treasury.ts new file mode 100644 index 00000000..a1a0022e --- /dev/null +++ b/packages/contracts/src/errors/contracts/payment-treasury.ts @@ -0,0 +1,302 @@ +import type { ContractErrorBase } from "../base"; + +/** PaymentTreasury error name strings. */ +export const PaymentTreasuryErrorNames = { + UnAuthorized: "PaymentTreasuryUnAuthorized", + InvalidInput: "PaymentTreasuryInvalidInput", + PaymentAlreadyExist: "PaymentTreasuryPaymentAlreadyExist", + PaymentAlreadyConfirmed: "PaymentTreasuryPaymentAlreadyConfirmed", + PaymentAlreadyExpired: "PaymentTreasuryPaymentAlreadyExpired", + PaymentNotExist: "PaymentTreasuryPaymentNotExist", + CampaignInfoIsPaused: "PaymentTreasuryCampaignInfoIsPaused", + TokenNotAccepted: "PaymentTreasuryTokenNotAccepted", + SuccessConditionNotFulfilled: "PaymentTreasurySuccessConditionNotFulfilled", + FeeNotDisbursed: "PaymentTreasuryFeeNotDisbursed", + PaymentNotConfirmed: "PaymentTreasuryPaymentNotConfirmed", + PaymentNotClaimable: "PaymentTreasuryPaymentNotClaimable", + AlreadyWithdrawn: "PaymentTreasuryAlreadyWithdrawn", + CryptoPayment: "PaymentTreasuryCryptoPayment", + InsufficientFundsForFee: "PaymentTreasuryInsufficientFundsForFee", + InsufficientBalance: "PaymentTreasuryInsufficientBalance", + ExpirationExceedsMax: "PaymentTreasuryExpirationExceedsMax", + ClaimWindowNotReached: "PaymentTreasuryClaimWindowNotReached", + NoFundsToClaim: "PaymentTreasuryNoFundsToClaim", +} as const; + +/** Thrown when the caller is not authorised for the attempted PaymentTreasury operation. */ +export class PaymentTreasuryUnAuthorizedError extends Error implements ContractErrorBase { + readonly name = PaymentTreasuryErrorNames.UnAuthorized; + readonly args: Record = {}; + readonly recoveryHint = "Caller is not authorized for this operation on the PaymentTreasury."; + + constructor() { + super("PaymentTreasuryUnAuthorized()"); + Object.setPrototypeOf(this, PaymentTreasuryUnAuthorizedError.prototype); + } +} + +/** Thrown when one or more inputs to a PaymentTreasury write are invalid. */ +export class PaymentTreasuryInvalidInputError extends Error implements ContractErrorBase { + readonly name = PaymentTreasuryErrorNames.InvalidInput; + readonly args: Record = {}; + readonly recoveryHint = "One or more inputs are invalid. Check all provided parameters."; + + constructor() { + super("PaymentTreasuryInvalidInput()"); + Object.setPrototypeOf(this, PaymentTreasuryInvalidInputError.prototype); + } +} + +/** Thrown when createPayment is called with a payment ID that already exists. */ +export class PaymentTreasuryPaymentAlreadyExistError extends Error implements ContractErrorBase { + readonly name = PaymentTreasuryErrorNames.PaymentAlreadyExist; + readonly args: { paymentId: string }; + readonly recoveryHint = "A payment with this ID already exists. Use a unique payment ID."; + + constructor(args: { paymentId: string }) { + super(`PaymentTreasuryPaymentAlreadyExist(paymentId: ${args.paymentId})`); + this.args = args; + Object.setPrototypeOf(this, PaymentTreasuryPaymentAlreadyExistError.prototype); + } +} + +/** Thrown when confirmPayment is called for a payment that has already been confirmed. */ +export class PaymentTreasuryPaymentAlreadyConfirmedError extends Error implements ContractErrorBase { + readonly name = PaymentTreasuryErrorNames.PaymentAlreadyConfirmed; + readonly args: { paymentId: string }; + readonly recoveryHint = "This payment has already been confirmed and cannot be confirmed again."; + + constructor(args: { paymentId: string }) { + super(`PaymentTreasuryPaymentAlreadyConfirmed(paymentId: ${args.paymentId})`); + this.args = args; + Object.setPrototypeOf(this, PaymentTreasuryPaymentAlreadyConfirmedError.prototype); + } +} + +/** Thrown when an operation is attempted on a payment that has passed its expiration timestamp. */ +export class PaymentTreasuryPaymentAlreadyExpiredError extends Error implements ContractErrorBase { + readonly name = PaymentTreasuryErrorNames.PaymentAlreadyExpired; + readonly args: { paymentId: string }; + readonly recoveryHint = "This payment has expired and can no longer be confirmed or modified."; + + constructor(args: { paymentId: string }) { + super(`PaymentTreasuryPaymentAlreadyExpired(paymentId: ${args.paymentId})`); + this.args = args; + Object.setPrototypeOf(this, PaymentTreasuryPaymentAlreadyExpiredError.prototype); + } +} + +/** Thrown when an operation references a payment ID that does not exist. */ +export class PaymentTreasuryPaymentNotExistError extends Error implements ContractErrorBase { + readonly name = PaymentTreasuryErrorNames.PaymentNotExist; + readonly args: { paymentId: string }; + readonly recoveryHint = "No payment found with this ID. Check the payment ID and try again."; + + constructor(args: { paymentId: string }) { + super(`PaymentTreasuryPaymentNotExist(paymentId: ${args.paymentId})`); + this.args = args; + Object.setPrototypeOf(this, PaymentTreasuryPaymentNotExistError.prototype); + } +} + +/** Thrown when a payment operation is attempted while the linked CampaignInfo is paused. */ +export class PaymentTreasuryCampaignInfoIsPausedError extends Error implements ContractErrorBase { + readonly name = PaymentTreasuryErrorNames.CampaignInfoIsPaused; + readonly args: Record = {}; + readonly recoveryHint = "The campaign is paused. Unpause the campaign before performing this operation."; + + constructor() { + super("PaymentTreasuryCampaignInfoIsPaused()"); + Object.setPrototypeOf(this, PaymentTreasuryCampaignInfoIsPausedError.prototype); + } +} + +/** Thrown when a payment token is not on the accepted list for this campaign's currency. */ +export class PaymentTreasuryTokenNotAcceptedError extends Error implements ContractErrorBase { + readonly name = PaymentTreasuryErrorNames.TokenNotAccepted; + readonly args: { token: string }; + readonly recoveryHint = "This token is not accepted for payment. Use an accepted token for this campaign."; + + constructor(args: { token: string }) { + super(`PaymentTreasuryTokenNotAccepted(token: ${args.token})`); + this.args = args; + Object.setPrototypeOf(this, PaymentTreasuryTokenNotAcceptedError.prototype); + } +} + +/** Thrown when withdraw is attempted before the campaign funding goal has been reached. */ +export class PaymentTreasurySuccessConditionNotFulfilledError + extends Error + implements ContractErrorBase +{ + readonly name = PaymentTreasuryErrorNames.SuccessConditionNotFulfilled; + readonly args: Record = {}; + readonly recoveryHint = + "The campaign success condition has not been fulfilled. The goal amount must be reached before withdrawing."; + + constructor() { + super("PaymentTreasurySuccessConditionNotFulfilled()"); + Object.setPrototypeOf(this, PaymentTreasurySuccessConditionNotFulfilledError.prototype); + } +} + +/** Thrown when withdraw is called before disburseFees has been executed. */ +export class PaymentTreasuryFeeNotDisbursedError extends Error implements ContractErrorBase { + readonly name = PaymentTreasuryErrorNames.FeeNotDisbursed; + readonly args: Record = {}; + readonly recoveryHint = "Fees have not been disbursed yet. Call disburseFees() before withdrawing."; + + constructor() { + super("PaymentTreasuryFeeNotDisbursed()"); + Object.setPrototypeOf(this, PaymentTreasuryFeeNotDisbursedError.prototype); + } +} + +/** Thrown when a claim or fund-release operation requires confirmation that has not yet occurred. */ +export class PaymentTreasuryPaymentNotConfirmedError extends Error implements ContractErrorBase { + readonly name = PaymentTreasuryErrorNames.PaymentNotConfirmed; + readonly args: { paymentId: string }; + readonly recoveryHint = "This payment has not been confirmed yet. Confirm the payment before claiming."; + + constructor(args: { paymentId: string }) { + super(`PaymentTreasuryPaymentNotConfirmed(paymentId: ${args.paymentId})`); + this.args = args; + Object.setPrototypeOf(this, PaymentTreasuryPaymentNotConfirmedError.prototype); + } +} + +/** Thrown when claimRefund is called for a payment that does not meet refund eligibility conditions. */ +export class PaymentTreasuryPaymentNotClaimableError extends Error implements ContractErrorBase { + readonly name = PaymentTreasuryErrorNames.PaymentNotClaimable; + readonly args: { paymentId: string }; + readonly recoveryHint = + "This payment is not claimable. It may not be confirmed, may have expired, or the claim window has not been reached."; + + constructor(args: { paymentId: string }) { + super(`PaymentTreasuryPaymentNotClaimable(paymentId: ${args.paymentId})`); + this.args = args; + Object.setPrototypeOf(this, PaymentTreasuryPaymentNotClaimableError.prototype); + } +} + +/** Thrown when withdraw is called after funds have already been withdrawn. */ +export class PaymentTreasuryAlreadyWithdrawnError extends Error implements ContractErrorBase { + readonly name = PaymentTreasuryErrorNames.AlreadyWithdrawn; + readonly args: Record = {}; + readonly recoveryHint = "Funds have already been withdrawn from this treasury."; + + constructor() { + super("PaymentTreasuryAlreadyWithdrawn()"); + Object.setPrototypeOf(this, PaymentTreasuryAlreadyWithdrawnError.prototype); + } +} + +/** Thrown when a non-crypto payment flow is used for a payment that was created as a crypto payment. */ +export class PaymentTreasuryCryptoPaymentError extends Error implements ContractErrorBase { + readonly name = PaymentTreasuryErrorNames.CryptoPayment; + readonly args: { paymentId: string }; + readonly recoveryHint = + "This payment is a crypto payment and cannot be processed through this flow. Use processCryptoPayment() instead."; + + constructor(args: { paymentId: string }) { + super(`PaymentTreasuryCryptoPayment(paymentId: ${args.paymentId})`); + this.args = args; + Object.setPrototypeOf(this, PaymentTreasuryCryptoPaymentError.prototype); + } +} + +/** Thrown when the treasury balance is insufficient to cover the withdrawal fee. */ +export class PaymentTreasuryInsufficientFundsForFeeError extends Error implements ContractErrorBase { + readonly name = PaymentTreasuryErrorNames.InsufficientFundsForFee; + readonly args: { withdrawalAmount: string; fee: string }; + readonly recoveryHint = + "Insufficient funds to cover the withdrawal fee. Ensure the treasury has enough balance to pay the fee."; + + constructor(args: { withdrawalAmount: string; fee: string }) { + super( + `PaymentTreasuryInsufficientFundsForFee(withdrawalAmount: ${args.withdrawalAmount}, fee: ${args.fee})`, + ); + this.args = args; + Object.setPrototypeOf(this, PaymentTreasuryInsufficientFundsForFeeError.prototype); + } +} + +/** Thrown when a requested amount exceeds the available treasury balance. */ +export class PaymentTreasuryInsufficientBalanceError extends Error implements ContractErrorBase { + readonly name = PaymentTreasuryErrorNames.InsufficientBalance; + readonly args: { required: string; available: string }; + readonly recoveryHint = + "Insufficient balance in the treasury. The required amount exceeds the available funds."; + + constructor(args: { required: string; available: string }) { + super( + `PaymentTreasuryInsufficientBalance(required: ${args.required}, available: ${args.available})`, + ); + this.args = args; + Object.setPrototypeOf(this, PaymentTreasuryInsufficientBalanceError.prototype); + } +} + +/** Thrown when a payment expiration timestamp exceeds the maximum allowed duration. */ +export class PaymentTreasuryExpirationExceedsMaxError extends Error implements ContractErrorBase { + readonly name = PaymentTreasuryErrorNames.ExpirationExceedsMax; + readonly args: { expiration: string; maxExpiration: string }; + readonly recoveryHint = + "The payment expiration exceeds the maximum allowed expiration. Use a shorter expiration time."; + + constructor(args: { expiration: string; maxExpiration: string }) { + super( + `PaymentTreasuryExpirationExceedsMax(expiration: ${args.expiration}, maxExpiration: ${args.maxExpiration})`, + ); + this.args = args; + Object.setPrototypeOf(this, PaymentTreasuryExpirationExceedsMaxError.prototype); + } +} + +/** Thrown when a claim is attempted before the claimable timestamp has been reached. */ +export class PaymentTreasuryClaimWindowNotReachedError extends Error implements ContractErrorBase { + readonly name = PaymentTreasuryErrorNames.ClaimWindowNotReached; + readonly args: { claimableAt: string }; + readonly recoveryHint = + "The claim window has not been reached yet. Wait until the claimableAt timestamp before claiming."; + + constructor(args: { claimableAt: string }) { + super(`PaymentTreasuryClaimWindowNotReached(claimableAt: ${args.claimableAt})`); + this.args = args; + Object.setPrototypeOf(this, PaymentTreasuryClaimWindowNotReachedError.prototype); + } +} + +/** Thrown when claimExpiredFunds or a similar operation finds no claimable funds available. */ +export class PaymentTreasuryNoFundsToClaimError extends Error implements ContractErrorBase { + readonly name = PaymentTreasuryErrorNames.NoFundsToClaim; + readonly args: Record = {}; + readonly recoveryHint = "There are no funds available to claim at this time."; + + constructor() { + super("PaymentTreasuryNoFundsToClaim()"); + Object.setPrototypeOf(this, PaymentTreasuryNoFundsToClaimError.prototype); + } +} + +/** Union of all typed errors that can be thrown by PaymentTreasury contract calls. */ +export type PaymentTreasuryError = + | PaymentTreasuryUnAuthorizedError + | PaymentTreasuryInvalidInputError + | PaymentTreasuryPaymentAlreadyExistError + | PaymentTreasuryPaymentAlreadyConfirmedError + | PaymentTreasuryPaymentAlreadyExpiredError + | PaymentTreasuryPaymentNotExistError + | PaymentTreasuryCampaignInfoIsPausedError + | PaymentTreasuryTokenNotAcceptedError + | PaymentTreasurySuccessConditionNotFulfilledError + | PaymentTreasuryFeeNotDisbursedError + | PaymentTreasuryPaymentNotConfirmedError + | PaymentTreasuryPaymentNotClaimableError + | PaymentTreasuryAlreadyWithdrawnError + | PaymentTreasuryCryptoPaymentError + | PaymentTreasuryInsufficientFundsForFeeError + | PaymentTreasuryInsufficientBalanceError + | PaymentTreasuryExpirationExceedsMaxError + | PaymentTreasuryClaimWindowNotReachedError + | PaymentTreasuryNoFundsToClaimError; diff --git a/packages/contracts/src/errors/contracts/shared.ts b/packages/contracts/src/errors/contracts/shared.ts new file mode 100644 index 00000000..063c2545 --- /dev/null +++ b/packages/contracts/src/errors/contracts/shared.ts @@ -0,0 +1,126 @@ +import type { ContractErrorBase } from "../base"; + +/** Shared (cross-contract) error name strings. Use in classes and parse layer. */ +export const SharedErrorNames = { + AccessCheckerUnauthorized: "AccessCheckerUnauthorized", + AdminAccessCheckerUnauthorized: "AdminAccessCheckerUnauthorized", + CurrentTimeIsGreater: "CurrentTimeIsGreater", + CurrentTimeIsLess: "CurrentTimeIsLess", + CurrentTimeIsNotWithinRange: "CurrentTimeIsNotWithinRange", + TreasuryCampaignInfoIsPaused: "TreasuryCampaignInfoIsPaused", + TreasuryFeeNotDisbursed: "TreasuryFeeNotDisbursed", + TreasuryTransferFailed: "TreasuryTransferFailed", +} as const; + +/** Thrown when the caller fails the generic access control check. */ +export class AccessCheckerUnauthorizedError extends Error implements ContractErrorBase { + readonly name = SharedErrorNames.AccessCheckerUnauthorized; + readonly args: Record = {}; + readonly recoveryHint = "Caller is not authorized. Check access control permissions."; + + constructor() { + super(`${SharedErrorNames.AccessCheckerUnauthorized}()`); + Object.setPrototypeOf(this, AccessCheckerUnauthorizedError.prototype); + } +} + +/** Thrown when the caller is not the admin for the accessed resource. */ +export class AdminAccessCheckerUnauthorizedError extends Error implements ContractErrorBase { + readonly name = SharedErrorNames.AdminAccessCheckerUnauthorized; + readonly args: Record = {}; + readonly recoveryHint = "Caller is not an admin. Only admins can perform this operation."; + + constructor() { + super(`${SharedErrorNames.AdminAccessCheckerUnauthorized}()`); + Object.setPrototypeOf(this, AdminAccessCheckerUnauthorizedError.prototype); + } +} + +/** Thrown when the provided timestamp is in the past relative to the current block time. */ +export class CurrentTimeIsGreaterError extends Error implements ContractErrorBase { + readonly name = SharedErrorNames.CurrentTimeIsGreater; + readonly args: { inputTime: string; currentTime: string }; + readonly recoveryHint = "The input time is in the past. Provide a future timestamp."; + + constructor(args: { inputTime: string; currentTime: string }) { + super(`${SharedErrorNames.CurrentTimeIsGreater}(inputTime: ${args.inputTime}, currentTime: ${args.currentTime})`); + this.args = args; + Object.setPrototypeOf(this, CurrentTimeIsGreaterError.prototype); + } +} + +/** Thrown when the current block time has not yet reached the required timestamp. */ +export class CurrentTimeIsLessError extends Error implements ContractErrorBase { + readonly name = SharedErrorNames.CurrentTimeIsLess; + readonly args: { inputTime: string; currentTime: string }; + readonly recoveryHint = "The operation is not yet available. Wait until the specified time has passed."; + + constructor(args: { inputTime: string; currentTime: string }) { + super(`${SharedErrorNames.CurrentTimeIsLess}(inputTime: ${args.inputTime}, currentTime: ${args.currentTime})`); + this.args = args; + Object.setPrototypeOf(this, CurrentTimeIsLessError.prototype); + } +} + +/** Thrown when the current block time falls outside the allowed [initialTime, finalTime] window. */ +export class CurrentTimeIsNotWithinRangeError extends Error implements ContractErrorBase { + readonly name = SharedErrorNames.CurrentTimeIsNotWithinRange; + readonly args: { initialTime: string; finalTime: string }; + readonly recoveryHint = + "Current time is outside the allowed range. The operation is only valid between the initial and final times."; + + constructor(args: { initialTime: string; finalTime: string }) { + super( + `${SharedErrorNames.CurrentTimeIsNotWithinRange}(initialTime: ${args.initialTime}, finalTime: ${args.finalTime})`, + ); + this.args = args; + Object.setPrototypeOf(this, CurrentTimeIsNotWithinRangeError.prototype); + } +} + +/** Thrown when a treasury operation is attempted while the linked CampaignInfo is paused. */ +export class TreasuryCampaignInfoIsPausedError extends Error implements ContractErrorBase { + readonly name = SharedErrorNames.TreasuryCampaignInfoIsPaused; + readonly args: Record = {}; + readonly recoveryHint = "The campaign is paused. Unpause the campaign before performing this operation."; + + constructor() { + super(`${SharedErrorNames.TreasuryCampaignInfoIsPaused}()`); + Object.setPrototypeOf(this, TreasuryCampaignInfoIsPausedError.prototype); + } +} + +/** Thrown when a treasury withdrawal is attempted before fees have been disbursed. */ +export class TreasuryFeeNotDisbursedError extends Error implements ContractErrorBase { + readonly name = SharedErrorNames.TreasuryFeeNotDisbursed; + readonly args: Record = {}; + readonly recoveryHint = "Fees have not been disbursed yet. Call disburseFees() before this operation."; + + constructor() { + super(`${SharedErrorNames.TreasuryFeeNotDisbursed}()`); + Object.setPrototypeOf(this, TreasuryFeeNotDisbursedError.prototype); + } +} + +/** Thrown when an ERC-20 token transfer from the treasury fails. */ +export class TreasuryTransferFailedError extends Error implements ContractErrorBase { + readonly name = SharedErrorNames.TreasuryTransferFailed; + readonly args: Record = {}; + readonly recoveryHint = "Token transfer failed. Check token balances and allowances."; + + constructor() { + super(`${SharedErrorNames.TreasuryTransferFailed}()`); + Object.setPrototypeOf(this, TreasuryTransferFailedError.prototype); + } +} + +/** Union of all typed errors shared across multiple contract types. */ +export type SharedError = + | AccessCheckerUnauthorizedError + | AdminAccessCheckerUnauthorizedError + | CurrentTimeIsGreaterError + | CurrentTimeIsLessError + | CurrentTimeIsNotWithinRangeError + | TreasuryCampaignInfoIsPausedError + | TreasuryFeeNotDisbursedError + | TreasuryTransferFailedError; diff --git a/packages/contracts/src/errors/contracts/treasury-factory.ts b/packages/contracts/src/errors/contracts/treasury-factory.ts new file mode 100644 index 00000000..0bd22e49 --- /dev/null +++ b/packages/contracts/src/errors/contracts/treasury-factory.ts @@ -0,0 +1,133 @@ +import type { ContractErrorBase } from "../base"; + +/** TreasuryFactory error name strings. */ +export const TreasuryFactoryErrorNames = { + Unauthorized: "TreasuryFactoryUnauthorized", + InvalidKey: "TreasuryFactoryInvalidKey", + TreasuryCreationFailed: "TreasuryFactoryTreasuryCreationFailed", + InvalidAddress: "TreasuryFactoryInvalidAddress", + ImplementationNotSet: "TreasuryFactoryImplementationNotSet", + ImplementationNotSetOrApproved: "TreasuryFactoryImplementationNotSetOrApproved", + TreasuryInitializationFailed: "TreasuryFactoryTreasuryInitializationFailed", + SettingPlatformInfoFailed: "TreasuryFactorySettingPlatformInfoFailed", +} as const; + +/** Thrown when the caller is not authorised for the attempted TreasuryFactory operation. */ +export class TreasuryFactoryUnauthorizedError extends Error implements ContractErrorBase { + readonly name = TreasuryFactoryErrorNames.Unauthorized; + readonly args: Record = {}; + readonly recoveryHint = "Caller is not authorized for this operation on the TreasuryFactory."; + + constructor() { + super("TreasuryFactoryUnauthorized()"); + Object.setPrototypeOf(this, TreasuryFactoryUnauthorizedError.prototype); + } +} + +/** Thrown when the platform hash and implementation ID combination does not resolve to a valid key. */ +export class TreasuryFactoryInvalidKeyError extends Error implements ContractErrorBase { + readonly name = TreasuryFactoryErrorNames.InvalidKey; + readonly args: Record = {}; + readonly recoveryHint = "The provided implementation key is invalid. Check the platform hash and implementation ID."; + + constructor() { + super("TreasuryFactoryInvalidKey()"); + Object.setPrototypeOf(this, TreasuryFactoryInvalidKeyError.prototype); + } +} + +/** Thrown when the EIP-1167 minimal proxy clone deployment fails. */ +export class TreasuryFactoryTreasuryCreationFailedError extends Error implements ContractErrorBase { + readonly name = TreasuryFactoryErrorNames.TreasuryCreationFailed; + readonly args: Record = {}; + readonly recoveryHint = "Treasury clone creation failed. Check the implementation address and try again."; + + constructor() { + super("TreasuryFactoryTreasuryCreationFailed()"); + Object.setPrototypeOf(this, TreasuryFactoryTreasuryCreationFailedError.prototype); + } +} + +/** Thrown when one or more provided addresses are the zero address. */ +export class TreasuryFactoryInvalidAddressError extends Error implements ContractErrorBase { + readonly name = TreasuryFactoryErrorNames.InvalidAddress; + readonly args: Record = {}; + readonly recoveryHint = "One or more provided addresses are invalid. Ensure all addresses are non-zero."; + + constructor() { + super("TreasuryFactoryInvalidAddress()"); + Object.setPrototypeOf(this, TreasuryFactoryInvalidAddressError.prototype); + } +} + +/** Thrown when deploy is called but no implementation is registered for the given platform and ID. */ +export class TreasuryFactoryImplementationNotSetError extends Error implements ContractErrorBase { + readonly name = TreasuryFactoryErrorNames.ImplementationNotSet; + readonly args: Record = {}; + readonly recoveryHint = + "No implementation is registered for this platform and implementation ID. Register the implementation first."; + + constructor() { + super("TreasuryFactoryImplementationNotSet()"); + Object.setPrototypeOf(this, TreasuryFactoryImplementationNotSetError.prototype); + } +} + +/** Thrown when deploy is called but the registered implementation has not been approved. */ +export class TreasuryFactoryImplementationNotSetOrApprovedError + extends Error + implements ContractErrorBase +{ + readonly name = TreasuryFactoryErrorNames.ImplementationNotSetOrApproved; + readonly args: Record = {}; + readonly recoveryHint = + "The implementation is not registered or not approved. Register and approve the implementation before deploying."; + + constructor() { + super("TreasuryFactoryImplementationNotSetOrApproved()"); + Object.setPrototypeOf(this, TreasuryFactoryImplementationNotSetOrApprovedError.prototype); + } +} + +/** Thrown when the deployed treasury clone fails its initialise call. */ +export class TreasuryFactoryTreasuryInitializationFailedError + extends Error + implements ContractErrorBase +{ + readonly name = TreasuryFactoryErrorNames.TreasuryInitializationFailed; + readonly args: Record = {}; + readonly recoveryHint = + "Treasury initialization failed after cloning. Check the implementation and initialization parameters."; + + constructor() { + super("TreasuryFactoryTreasuryInitializationFailed()"); + Object.setPrototypeOf(this, TreasuryFactoryTreasuryInitializationFailedError.prototype); + } +} + +/** Thrown when setting platform info on the newly deployed treasury clone fails. */ +export class TreasuryFactorySettingPlatformInfoFailedError + extends Error + implements ContractErrorBase +{ + readonly name = TreasuryFactoryErrorNames.SettingPlatformInfoFailed; + readonly args: Record = {}; + readonly recoveryHint = + "Setting platform info on the deployed treasury failed. Check the treasury implementation and platform data."; + + constructor() { + super("TreasuryFactorySettingPlatformInfoFailed()"); + Object.setPrototypeOf(this, TreasuryFactorySettingPlatformInfoFailedError.prototype); + } +} + +/** Union of all typed errors that can be thrown by TreasuryFactory contract calls. */ +export type TreasuryFactoryError = + | TreasuryFactoryUnauthorizedError + | TreasuryFactoryInvalidKeyError + | TreasuryFactoryTreasuryCreationFailedError + | TreasuryFactoryInvalidAddressError + | TreasuryFactoryImplementationNotSetError + | TreasuryFactoryImplementationNotSetOrApprovedError + | TreasuryFactoryTreasuryInitializationFailedError + | TreasuryFactorySettingPlatformInfoFailedError; diff --git a/packages/contracts/src/errors/index.ts b/packages/contracts/src/errors/index.ts new file mode 100644 index 00000000..440146a4 --- /dev/null +++ b/packages/contracts/src/errors/index.ts @@ -0,0 +1,127 @@ +export type { ContractErrorBase } from "./base"; +export { + parseContractError, + getRevertData, + simulateWithErrorDecode, +} from "./parse-contract-error"; + +export { + GlobalParamsCurrencyHasNoTokensError, + GlobalParamsCurrencyTokenLengthMismatchError, + GlobalParamsInvalidInputError, + GlobalParamsPlatformAdminNotSetError, + GlobalParamsPlatformAlreadyListedError, + GlobalParamsPlatformDataAlreadySetError, + GlobalParamsPlatformDataNotSetError, + GlobalParamsPlatformDataSlotTakenError, + GlobalParamsPlatformFeePercentIsZeroError, + GlobalParamsPlatformLineItemTypeNotFoundError, + GlobalParamsPlatformNotListedError, + GlobalParamsTokenNotInCurrencyError, + GlobalParamsUnauthorizedError, +} from "./contracts/global-params"; +export type { GlobalParamsError } from "./contracts/global-params"; + +export { + CampaignInfoFactoryCampaignInitializationFailedError, + CampaignInfoFactoryCampaignWithSameIdentifierExistsError, + CampaignInfoFactoryInvalidInputError, + CampaignInfoFactoryPlatformNotListedError, + CampaignInfoInvalidTokenListError, +} from "./contracts/campaign-info-factory"; +export type { CampaignInfoFactoryError } from "./contracts/campaign-info-factory"; + +export { + CampaignInfoInvalidInputError, + CampaignInfoInvalidPlatformUpdateError, + CampaignInfoIsLockedError, + CampaignInfoPlatformAlreadyApprovedError, + CampaignInfoPlatformNotSelectedError, + CampaignInfoUnauthorizedError, +} from "./contracts/campaign-info"; +export type { CampaignInfoError } from "./contracts/campaign-info"; + +export { + AllOrNothingFeeAlreadyDisbursedError, + AllOrNothingFeeNotDisbursedError, + AllOrNothingInvalidInputError, + AllOrNothingNotClaimableError, + AllOrNothingNotSuccessfulError, + AllOrNothingRewardExistsError, + AllOrNothingTokenNotAcceptedError, + AllOrNothingTransferFailedError, + AllOrNothingUnAuthorizedError, + TreasurySuccessConditionNotFulfilledError, +} from "./contracts/all-or-nothing"; +export type { AllOrNothingError } from "./contracts/all-or-nothing"; + +export { + KeepWhatsRaisedAlreadyClaimedError, + KeepWhatsRaisedAlreadyEnabledError, + KeepWhatsRaisedAlreadyWithdrawnError, + KeepWhatsRaisedConfigLockedError, + KeepWhatsRaisedDisabledError, + KeepWhatsRaisedDisbursementBlockedError, + KeepWhatsRaisedInsufficientFundsForFeeError, + KeepWhatsRaisedInsufficientFundsForWithdrawalAndFeeError, + KeepWhatsRaisedInvalidInputError, + KeepWhatsRaisedNotClaimableAdminError, + KeepWhatsRaisedNotClaimableError, + KeepWhatsRaisedPledgeAlreadyProcessedError, + KeepWhatsRaisedRewardExistsError, + KeepWhatsRaisedTokenNotAcceptedError, + KeepWhatsRaisedUnAuthorizedError, +} from "./contracts/keep-whats-raised"; +export type { KeepWhatsRaisedError } from "./contracts/keep-whats-raised"; + +export { ItemRegistryMismatchedArraysLengthError } from "./contracts/item-registry"; +export type { ItemRegistryError } from "./contracts/item-registry"; + +export { + PaymentTreasuryAlreadyWithdrawnError, + PaymentTreasuryCampaignInfoIsPausedError, + PaymentTreasuryClaimWindowNotReachedError, + PaymentTreasuryCryptoPaymentError, + PaymentTreasuryExpirationExceedsMaxError, + PaymentTreasuryFeeNotDisbursedError, + PaymentTreasuryInsufficientBalanceError, + PaymentTreasuryInsufficientFundsForFeeError, + PaymentTreasuryInvalidInputError, + PaymentTreasuryNoFundsToClaimError, + PaymentTreasuryPaymentAlreadyConfirmedError, + PaymentTreasuryPaymentAlreadyExpiredError, + PaymentTreasuryPaymentAlreadyExistError, + PaymentTreasuryPaymentNotClaimableError, + PaymentTreasuryPaymentNotConfirmedError, + PaymentTreasuryPaymentNotExistError, + PaymentTreasurySuccessConditionNotFulfilledError, + PaymentTreasuryTokenNotAcceptedError, + PaymentTreasuryUnAuthorizedError, +} from "./contracts/payment-treasury"; +export type { PaymentTreasuryError } from "./contracts/payment-treasury"; + +export { + TreasuryFactoryImplementationNotSetError, + TreasuryFactoryImplementationNotSetOrApprovedError, + TreasuryFactoryInvalidAddressError, + TreasuryFactoryInvalidKeyError, + TreasuryFactorySettingPlatformInfoFailedError, + TreasuryFactoryTreasuryCreationFailedError, + TreasuryFactoryTreasuryInitializationFailedError, + TreasuryFactoryUnauthorizedError, +} from "./contracts/treasury-factory"; +export type { TreasuryFactoryError } from "./contracts/treasury-factory"; + +export { + AccessCheckerUnauthorizedError, + AdminAccessCheckerUnauthorizedError, + CurrentTimeIsGreaterError, + CurrentTimeIsLessError, + CurrentTimeIsNotWithinRangeError, + TreasuryCampaignInfoIsPausedError, + TreasuryFeeNotDisbursedError, + TreasuryTransferFailedError, +} from "./contracts/shared"; +export type { SharedError } from "./contracts/shared"; + +export { getRecoveryHint } from "./recovery"; diff --git a/packages/contracts/src/errors/parse-contract-error.ts b/packages/contracts/src/errors/parse-contract-error.ts new file mode 100644 index 00000000..c0320da8 --- /dev/null +++ b/packages/contracts/src/errors/parse-contract-error.ts @@ -0,0 +1,94 @@ +import type { Hex } from "../lib"; +import { isHex } from "../utils"; +import type { ContractErrorBase } from "./base"; +import { parseGlobalParamsError } from "./parse/global-params"; +import { parseCampaignInfoFactoryError } from "./parse/campaign-info-factory"; +import { parseCampaignInfoError } from "./parse/campaign-info"; +import { parseAllOrNothingError } from "./parse/all-or-nothing"; +import { parseKeepWhatsRaisedError } from "./parse/keep-whats-raised"; +import { parseItemRegistryError } from "./parse/item-registry"; +import { parsePaymentTreasuryError } from "./parse/payment-treasury"; +import { parseTreasuryFactoryError } from "./parse/treasury-factory"; + +/** + * Parses raw revert data from a contract call and returns a typed SDK error if the error + * is recognized. Supports: GlobalParams, CampaignInfoFactory, CampaignInfo, AllOrNothing, + * KeepWhatsRaised, ItemRegistry, PaymentTreasury, TreasuryFactory. + * Returns null if the data is not valid or not from a known contract. + * + * Use this when you have raw revert data (e.g. from a provider or estimateGas) and want + * to get a typed, discriminable error with decoded args and optional recovery hints. + * + * @param revertData - Hex string (0x + selector + encoded args), e.g. from catch (e) \{ e.data \} + * @returns Typed ContractErrorBase instance or null + */ +export function parseContractError(revertData: string): ContractErrorBase | null { + if (!revertData || !isHex(revertData) || revertData.length < 10) { + return null; + } + + const data = revertData as Hex; + + return ( + parseGlobalParamsError(data) ?? + parseCampaignInfoFactoryError(data) ?? + parseCampaignInfoError(data) ?? + parseAllOrNothingError(data) ?? + parseKeepWhatsRaisedError(data) ?? + parseItemRegistryError(data) ?? + parsePaymentTreasuryError(data) ?? + parseTreasuryFactoryError(data) ?? + null + ); +} + +/** + * Extracts raw revert hex data by walking the error cause chain. + * Returns the first `0x`-prefixed hex string found in `error.data` or `error.data.data`, + * or null if none is present. + * + * @param error - Unknown thrown value from a viem contract call + * @returns Hex revert data string or null + */ +export function getRevertData(error: unknown): string | null { + let current: unknown = error; + while (current && typeof current === "object") { + const e = current as Record; + if (typeof e["data"] === "string" && (e["data"] as string).startsWith("0x")) { + return e["data"] as string; + } + if (typeof e["raw"] === "string" && (e["raw"] as string).startsWith("0x")) { + return e["raw"] as string; + } + if ( + e["data"] && + typeof e["data"] === "object" && + typeof (e["data"] as Record)["data"] === "string" + ) { + return (e["data"] as Record)["data"] as string; + } + current = e["cause"]; + } + return null; +} + +/** + * Wraps a simulateContract call, catches reverts, decodes them via parseContractError, + * and re-throws as a typed SDK error. Consumers catch the same error class whether + * they are simulating or transacting. + * + * @param operation - Async function that calls simulateContract + * @throws Typed ContractErrorBase subclass on revert, or the original error if not decodable + */ +export async function simulateWithErrorDecode(operation: () => Promise): Promise { + try { + await operation(); + } catch (error: unknown) { + const revertData = getRevertData(error); + const parsed = parseContractError(revertData ?? ""); + if (parsed) { + throw parsed; + } + throw error; + } +} diff --git a/packages/contracts/src/errors/parse/all-or-nothing.ts b/packages/contracts/src/errors/parse/all-or-nothing.ts new file mode 100644 index 00000000..ade610d2 --- /dev/null +++ b/packages/contracts/src/errors/parse/all-or-nothing.ts @@ -0,0 +1,80 @@ +import type { Hex } from "../../lib"; +import type { ContractErrorBase } from "../base"; +import { ALL_OR_NOTHING_ABI } from "../../contracts/all-or-nothing/abi"; +import { + AllOrNothingErrorNames, + AllOrNothingFeeAlreadyDisbursedError, + AllOrNothingFeeNotDisbursedError, + AllOrNothingInvalidInputError, + AllOrNothingNotClaimableError, + AllOrNothingNotSuccessfulError, + AllOrNothingRewardExistsError, + AllOrNothingTokenNotAcceptedError, + AllOrNothingTransferFailedError, + AllOrNothingUnAuthorizedError, + TreasurySuccessConditionNotFulfilledError, +} from "../contracts/all-or-nothing"; +import type { ErrorAbiEntry } from "./shared"; +import { toSharedContractError, tryDecodeContractError } from "./shared"; + +/** + * Maps a decoded AllOrNothing error name and args to a typed SDK error instance. + * @param name - Decoded error name string + * @param args - Decoded error arguments + * @returns Typed AllOrNothing error, or a shared/generic fallback + */ +function toAllOrNothingError( + name: string, + args: Record, +): ContractErrorBase { + switch (name) { + case AllOrNothingErrorNames.FeeNotDisbursed: + return new AllOrNothingFeeNotDisbursedError(); + case AllOrNothingErrorNames.FeeAlreadyDisbursed: + return new AllOrNothingFeeAlreadyDisbursedError(); + case AllOrNothingErrorNames.InvalidInput: + return new AllOrNothingInvalidInputError(); + case AllOrNothingErrorNames.NotClaimable: + return new AllOrNothingNotClaimableError({ + tokenId: args["tokenId"] as string, + }); + case AllOrNothingErrorNames.NotSuccessful: + return new AllOrNothingNotSuccessfulError(); + case AllOrNothingErrorNames.RewardExists: + return new AllOrNothingRewardExistsError(); + case AllOrNothingErrorNames.TransferFailed: + return new AllOrNothingTransferFailedError(); + case AllOrNothingErrorNames.UnAuthorized: + return new AllOrNothingUnAuthorizedError(); + case AllOrNothingErrorNames.TokenNotAccepted: + return new AllOrNothingTokenNotAcceptedError({ + token: args["token"] as string, + }); + case AllOrNothingErrorNames.TreasurySuccessConditionNotFulfilled: + return new TreasurySuccessConditionNotFulfilledError(); + default: { + const shared = toSharedContractError(name, args); + /* istanbul ignore next -- defensive fallback; all shared errors are recognised */ + if (!shared) { + return new (class extends Error implements ContractErrorBase { + readonly name = name; + readonly args = args; + })(`${name}(${JSON.stringify(args)})`); + } + return shared; + } + } +} + +/** + * Decodes raw revert data from an AllOrNothing treasury contract call into a typed SDK error. + * @param data - 0x-prefixed hex revert data + * @returns Typed AllOrNothing error instance, or null if the selector is not recognised + */ +export function parseAllOrNothingError(data: Hex): ContractErrorBase | null { + return tryDecodeContractError( + ALL_OR_NOTHING_ABI as readonly ErrorAbiEntry[], + data, + toAllOrNothingError, + ); +} diff --git a/packages/contracts/src/errors/parse/campaign-info-factory.ts b/packages/contracts/src/errors/parse/campaign-info-factory.ts new file mode 100644 index 00000000..eb594039 --- /dev/null +++ b/packages/contracts/src/errors/parse/campaign-info-factory.ts @@ -0,0 +1,64 @@ +import type { Hex } from "../../lib"; +import type { ContractErrorBase } from "../base"; +import { CAMPAIGN_INFO_FACTORY_ABI } from "../../contracts/campaign-info-factory/abi"; +import { + CampaignInfoFactoryCampaignInitializationFailedError, + CampaignInfoFactoryCampaignWithSameIdentifierExistsError, + CampaignInfoFactoryErrorNames, + CampaignInfoFactoryInvalidInputError, + CampaignInfoFactoryPlatformNotListedError, + CampaignInfoInvalidTokenListError, +} from "../contracts/campaign-info-factory"; +import type { ErrorAbiEntry } from "./shared"; +import { toSharedContractError, tryDecodeContractError } from "./shared"; + +/** + * Maps a decoded CampaignInfoFactory error name and args to a typed SDK error instance. + * @param name - Decoded error name string + * @param args - Decoded error arguments + * @returns Typed CampaignInfoFactory error, or a shared/generic fallback + */ +function toCampaignInfoFactoryError( + name: string, + args: Record, +): ContractErrorBase { + switch (name) { + case CampaignInfoFactoryErrorNames.CampaignInitializationFailed: + return new CampaignInfoFactoryCampaignInitializationFailedError(); + case CampaignInfoFactoryErrorNames.InvalidInput: + return new CampaignInfoFactoryInvalidInputError(); + case CampaignInfoFactoryErrorNames.PlatformNotListed: + return new CampaignInfoFactoryPlatformNotListedError({ + platformHash: args["platformHash"] as string, + }); + case CampaignInfoFactoryErrorNames.CampaignWithSameIdentifierExists: + return new CampaignInfoFactoryCampaignWithSameIdentifierExistsError({ + identifierHash: args["identifierHash"] as string, + cloneExists: args["cloneExists"] as string, + }); + case CampaignInfoFactoryErrorNames.CampaignInfoInvalidTokenList: + return new CampaignInfoInvalidTokenListError(); + /* istanbul ignore next -- defensive fallback; all ABI errors are handled above */ + default: + return ( + toSharedContractError(name, args) ?? + new (class extends Error implements ContractErrorBase { + readonly name = name; + readonly args = args; + })(`${name}(${JSON.stringify(args)})`) + ); + } +} + +/** + * Decodes raw revert data from a CampaignInfoFactory contract call into a typed SDK error. + * @param data - 0x-prefixed hex revert data + * @returns Typed CampaignInfoFactory error instance, or null if the selector is not recognised + */ +export function parseCampaignInfoFactoryError(data: Hex): ContractErrorBase | null { + return tryDecodeContractError( + CAMPAIGN_INFO_FACTORY_ABI as readonly ErrorAbiEntry[], + data, + toCampaignInfoFactoryError, + ); +} diff --git a/packages/contracts/src/errors/parse/campaign-info.ts b/packages/contracts/src/errors/parse/campaign-info.ts new file mode 100644 index 00000000..bb6426d9 --- /dev/null +++ b/packages/contracts/src/errors/parse/campaign-info.ts @@ -0,0 +1,68 @@ +import type { Hex } from "../../lib"; +import type { ContractErrorBase } from "../base"; +import { CAMPAIGN_INFO_ABI } from "../../contracts/campaign-info/abi"; +import { + CampaignInfoErrorNames, + CampaignInfoInvalidInputError, + CampaignInfoInvalidPlatformUpdateError, + CampaignInfoIsLockedError, + CampaignInfoPlatformAlreadyApprovedError, + CampaignInfoPlatformNotSelectedError, + CampaignInfoUnauthorizedError, +} from "../contracts/campaign-info"; +import type { ErrorAbiEntry } from "./shared"; +import { toSharedContractError, tryDecodeContractError } from "./shared"; + +/** + * Maps a decoded CampaignInfo error name and args to a typed SDK error instance. + * @param name - Decoded error name string + * @param args - Decoded error arguments + * @returns Typed CampaignInfo error, or a shared/generic fallback + */ +function toCampaignInfoError(name: string, args: Record): ContractErrorBase { + switch (name) { + case CampaignInfoErrorNames.InvalidInput: + return new CampaignInfoInvalidInputError(); + case CampaignInfoErrorNames.InvalidPlatformUpdate: + return new CampaignInfoInvalidPlatformUpdateError({ + platformBytes: args["platformBytes"] as string, + selection: args["selection"] as boolean, + }); + case CampaignInfoErrorNames.PlatformNotSelected: + return new CampaignInfoPlatformNotSelectedError({ + platformBytes: args["platformBytes"] as string, + }); + case CampaignInfoErrorNames.PlatformAlreadyApproved: + return new CampaignInfoPlatformAlreadyApprovedError({ + platformHash: args["platformHash"] as string, + }); + case CampaignInfoErrorNames.Unauthorized: + return new CampaignInfoUnauthorizedError(); + case CampaignInfoErrorNames.IsLocked: + return new CampaignInfoIsLockedError(); + default: { + const shared = toSharedContractError(name, args); + /* istanbul ignore next -- defensive fallback; all shared errors are recognised */ + if (!shared) { + return new (class extends Error implements ContractErrorBase { + readonly name = name; + readonly args = args; + })(`${name}(${JSON.stringify(args)})`); + } + return shared; + } + } +} + +/** + * Decodes raw revert data from a CampaignInfo contract call into a typed SDK error. + * @param data - 0x-prefixed hex revert data + * @returns Typed CampaignInfo error instance, or null if the selector is not recognised + */ +export function parseCampaignInfoError(data: Hex): ContractErrorBase | null { + return tryDecodeContractError( + CAMPAIGN_INFO_ABI as readonly ErrorAbiEntry[], + data, + toCampaignInfoError, + ); +} diff --git a/packages/contracts/src/errors/parse/global-params.ts b/packages/contracts/src/errors/parse/global-params.ts new file mode 100644 index 00000000..f07cad8c --- /dev/null +++ b/packages/contracts/src/errors/parse/global-params.ts @@ -0,0 +1,93 @@ +import type { Hex } from "../../lib"; +import type { ContractErrorBase } from "../base"; +import { GLOBAL_PARAMS_ABI } from "../../contracts/global-params/abi"; +import { + GlobalParamsCurrencyHasNoTokensError, + GlobalParamsCurrencyTokenLengthMismatchError, + GlobalParamsErrorNames, + GlobalParamsInvalidInputError, + GlobalParamsPlatformAdminNotSetError, + GlobalParamsPlatformAlreadyListedError, + GlobalParamsPlatformDataAlreadySetError, + GlobalParamsPlatformDataNotSetError, + GlobalParamsPlatformDataSlotTakenError, + GlobalParamsPlatformFeePercentIsZeroError, + GlobalParamsPlatformLineItemTypeNotFoundError, + GlobalParamsPlatformNotListedError, + GlobalParamsTokenNotInCurrencyError, + GlobalParamsUnauthorizedError, +} from "../contracts/global-params"; +import type { ErrorAbiEntry } from "./shared"; +import { tryDecodeContractError } from "./shared"; + +/** + * Maps a decoded GlobalParams error name and args to a typed SDK error instance. + * @param name - Decoded error name string + * @param args - Decoded error arguments + * @returns Typed GlobalParams error, or a shared/generic fallback + */ +function toGlobalParamsError(name: string, args: Record): ContractErrorBase { + switch (name) { + case GlobalParamsErrorNames.InvalidInput: + return new GlobalParamsInvalidInputError(); + case GlobalParamsErrorNames.PlatformAdminNotSet: + return new GlobalParamsPlatformAdminNotSetError({ + platformBytes: args["platformBytes"] as string, + }); + case GlobalParamsErrorNames.PlatformAlreadyListed: + return new GlobalParamsPlatformAlreadyListedError({ + platformBytes: args["platformBytes"] as string, + }); + case GlobalParamsErrorNames.PlatformDataAlreadySet: + return new GlobalParamsPlatformDataAlreadySetError(); + case GlobalParamsErrorNames.PlatformDataNotSet: + return new GlobalParamsPlatformDataNotSetError(); + case GlobalParamsErrorNames.PlatformDataSlotTaken: + return new GlobalParamsPlatformDataSlotTakenError(); + case GlobalParamsErrorNames.PlatformFeePercentIsZero: + return new GlobalParamsPlatformFeePercentIsZeroError({ + platformBytes: args["platformBytes"] as string, + }); + case GlobalParamsErrorNames.PlatformNotListed: + return new GlobalParamsPlatformNotListedError({ + platformBytes: args["platformBytes"] as string, + }); + case GlobalParamsErrorNames.Unauthorized: + return new GlobalParamsUnauthorizedError(); + case GlobalParamsErrorNames.CurrencyTokenLengthMismatch: + return new GlobalParamsCurrencyTokenLengthMismatchError(); + case GlobalParamsErrorNames.CurrencyHasNoTokens: + return new GlobalParamsCurrencyHasNoTokensError({ + currency: args["currency"] as string, + }); + case GlobalParamsErrorNames.TokenNotInCurrency: + return new GlobalParamsTokenNotInCurrencyError({ + currency: args["currency"] as string, + token: args["token"] as string, + }); + case GlobalParamsErrorNames.PlatformLineItemTypeNotFound: + return new GlobalParamsPlatformLineItemTypeNotFoundError({ + platformHash: args["platformHash"] as string, + typeId: args["typeId"] as string, + }); + /* istanbul ignore next -- defensive fallback; all ABI errors are handled above */ + default: + return new (class extends Error implements ContractErrorBase { + readonly name = name; + readonly args = args; + })(`${name}(${JSON.stringify(args)})`); + } +} + +/** + * Decodes raw revert data from a GlobalParams contract call into a typed SDK error. + * @param data - 0x-prefixed hex revert data + * @returns Typed GlobalParams error instance, or null if the selector is not recognised + */ +export function parseGlobalParamsError(data: Hex): ContractErrorBase | null { + return tryDecodeContractError( + GLOBAL_PARAMS_ABI as readonly ErrorAbiEntry[], + data, + toGlobalParamsError, + ); +} diff --git a/packages/contracts/src/errors/parse/item-registry.ts b/packages/contracts/src/errors/parse/item-registry.ts new file mode 100644 index 00000000..90d4fe13 --- /dev/null +++ b/packages/contracts/src/errors/parse/item-registry.ts @@ -0,0 +1,41 @@ +import type { Hex } from "../../lib"; +import type { ContractErrorBase } from "../base"; +import { ITEM_REGISTRY_ABI } from "../../contracts/item-registry/abi"; +import { + ItemRegistryErrorNames, + ItemRegistryMismatchedArraysLengthError, +} from "../contracts/item-registry"; +import type { ErrorAbiEntry } from "./shared"; +import { tryDecodeContractError } from "./shared"; + +/** + * Maps a decoded ItemRegistry error name and args to a typed SDK error instance. + * @param name - Decoded error name string + * @param args - Decoded error arguments + * @returns Typed ItemRegistry error, or a generic fallback + */ +function toItemRegistryError(name: string, args: Record): ContractErrorBase { + switch (name) { + case ItemRegistryErrorNames.MismatchedArraysLength: + return new ItemRegistryMismatchedArraysLengthError(); + /* istanbul ignore next -- defensive fallback; all ABI errors are handled above */ + default: + return new (class extends Error implements ContractErrorBase { + readonly name = name; + readonly args = args; + })(`${name}(${JSON.stringify(args)})`); + } +} + +/** + * Decodes raw revert data from an ItemRegistry contract call into a typed SDK error. + * @param data - 0x-prefixed hex revert data + * @returns Typed ItemRegistry error instance, or null if the selector is not recognised + */ +export function parseItemRegistryError(data: Hex): ContractErrorBase | null { + return tryDecodeContractError( + ITEM_REGISTRY_ABI as readonly ErrorAbiEntry[], + data, + toItemRegistryError, + ); +} diff --git a/packages/contracts/src/errors/parse/keep-whats-raised.ts b/packages/contracts/src/errors/parse/keep-whats-raised.ts new file mode 100644 index 00000000..fedf5b0a --- /dev/null +++ b/packages/contracts/src/errors/parse/keep-whats-raised.ts @@ -0,0 +1,97 @@ +import type { Hex } from "../../lib"; +import type { ContractErrorBase } from "../base"; +import { KEEP_WHATS_RAISED_ABI } from "../../contracts/keep-whats-raised/abi"; +import { + KeepWhatsRaisedAlreadyClaimedError, + KeepWhatsRaisedAlreadyEnabledError, + KeepWhatsRaisedAlreadyWithdrawnError, + KeepWhatsRaisedConfigLockedError, + KeepWhatsRaisedDisabledError, + KeepWhatsRaisedDisbursementBlockedError, + KeepWhatsRaisedErrorNames, + KeepWhatsRaisedInsufficientFundsForFeeError, + KeepWhatsRaisedInsufficientFundsForWithdrawalAndFeeError, + KeepWhatsRaisedInvalidInputError, + KeepWhatsRaisedNotClaimableAdminError, + KeepWhatsRaisedNotClaimableError, + KeepWhatsRaisedPledgeAlreadyProcessedError, + KeepWhatsRaisedRewardExistsError, + KeepWhatsRaisedTokenNotAcceptedError, + KeepWhatsRaisedUnAuthorizedError, +} from "../contracts/keep-whats-raised"; +import type { ErrorAbiEntry } from "./shared"; +import { toSharedContractError, tryDecodeContractError } from "./shared"; + +/** + * Maps a decoded KeepWhatsRaised error name and args to a typed SDK error instance. + * @param name - Decoded error name string + * @param args - Decoded error arguments + * @returns Typed KeepWhatsRaised error, or a shared/generic fallback + */ +function toKeepWhatsRaisedError(name: string, args: Record): ContractErrorBase { + switch (name) { + case KeepWhatsRaisedErrorNames.UnAuthorized: + return new KeepWhatsRaisedUnAuthorizedError(); + case KeepWhatsRaisedErrorNames.InvalidInput: + return new KeepWhatsRaisedInvalidInputError(); + case KeepWhatsRaisedErrorNames.TokenNotAccepted: + return new KeepWhatsRaisedTokenNotAcceptedError({ token: args["token"] as string }); + case KeepWhatsRaisedErrorNames.RewardExists: + return new KeepWhatsRaisedRewardExistsError(); + case KeepWhatsRaisedErrorNames.Disabled: + return new KeepWhatsRaisedDisabledError(); + case KeepWhatsRaisedErrorNames.AlreadyEnabled: + return new KeepWhatsRaisedAlreadyEnabledError(); + case KeepWhatsRaisedErrorNames.InsufficientFundsForWithdrawalAndFee: + return new KeepWhatsRaisedInsufficientFundsForWithdrawalAndFeeError({ + availableAmount: args["availableAmount"] as string, + withdrawalAmount: args["withdrawalAmount"] as string, + fee: args["fee"] as string, + }); + case KeepWhatsRaisedErrorNames.InsufficientFundsForFee: + return new KeepWhatsRaisedInsufficientFundsForFeeError({ + withdrawalAmount: args["withdrawalAmount"] as string, + fee: args["fee"] as string, + }); + case KeepWhatsRaisedErrorNames.AlreadyWithdrawn: + return new KeepWhatsRaisedAlreadyWithdrawnError(); + case KeepWhatsRaisedErrorNames.AlreadyClaimed: + return new KeepWhatsRaisedAlreadyClaimedError(); + case KeepWhatsRaisedErrorNames.NotClaimable: + return new KeepWhatsRaisedNotClaimableError({ tokenId: args["tokenId"] as string }); + case KeepWhatsRaisedErrorNames.NotClaimableAdmin: + return new KeepWhatsRaisedNotClaimableAdminError(); + case KeepWhatsRaisedErrorNames.ConfigLocked: + return new KeepWhatsRaisedConfigLockedError(); + case KeepWhatsRaisedErrorNames.DisbursementBlocked: + return new KeepWhatsRaisedDisbursementBlockedError(); + case KeepWhatsRaisedErrorNames.PledgeAlreadyProcessed: + return new KeepWhatsRaisedPledgeAlreadyProcessedError({ + pledgeId: args["pledgeId"] as string, + }); + default: { + const shared = toSharedContractError(name, args); + /* istanbul ignore next -- defensive fallback; all shared errors are recognised */ + if (!shared) { + return new (class extends Error implements ContractErrorBase { + readonly name = name; + readonly args = args; + })(`${name}(${JSON.stringify(args)})`); + } + return shared; + } + } +} + +/** + * Decodes raw revert data from a KeepWhatsRaised treasury contract call into a typed SDK error. + * @param data - 0x-prefixed hex revert data + * @returns Typed KeepWhatsRaised error instance, or null if the selector is not recognised + */ +export function parseKeepWhatsRaisedError(data: Hex): ContractErrorBase | null { + return tryDecodeContractError( + KEEP_WHATS_RAISED_ABI as readonly ErrorAbiEntry[], + data, + toKeepWhatsRaisedError, + ); +} diff --git a/packages/contracts/src/errors/parse/payment-treasury.ts b/packages/contracts/src/errors/parse/payment-treasury.ts new file mode 100644 index 00000000..137036a6 --- /dev/null +++ b/packages/contracts/src/errors/parse/payment-treasury.ts @@ -0,0 +1,120 @@ +import type { Hex } from "../../lib"; +import type { ContractErrorBase } from "../base"; +import { PAYMENT_TREASURY_ABI } from "../../contracts/payment-treasury/abi"; +import { + PaymentTreasuryAlreadyWithdrawnError, + PaymentTreasuryCampaignInfoIsPausedError, + PaymentTreasuryClaimWindowNotReachedError, + PaymentTreasuryCryptoPaymentError, + PaymentTreasuryErrorNames, + PaymentTreasuryExpirationExceedsMaxError, + PaymentTreasuryFeeNotDisbursedError, + PaymentTreasuryInsufficientBalanceError, + PaymentTreasuryInsufficientFundsForFeeError, + PaymentTreasuryInvalidInputError, + PaymentTreasuryNoFundsToClaimError, + PaymentTreasuryPaymentAlreadyConfirmedError, + PaymentTreasuryPaymentAlreadyExpiredError, + PaymentTreasuryPaymentAlreadyExistError, + PaymentTreasuryPaymentNotClaimableError, + PaymentTreasuryPaymentNotConfirmedError, + PaymentTreasuryPaymentNotExistError, + PaymentTreasurySuccessConditionNotFulfilledError, + PaymentTreasuryTokenNotAcceptedError, + PaymentTreasuryUnAuthorizedError, +} from "../contracts/payment-treasury"; +import type { ErrorAbiEntry } from "./shared"; +import { tryDecodeContractError } from "./shared"; + +/** + * Maps a decoded PaymentTreasury error name and args to a typed SDK error instance. + * @param name - Decoded error name string + * @param args - Decoded error arguments + * @returns Typed PaymentTreasury error, or a shared/generic fallback + */ +function toPaymentTreasuryError(name: string, args: Record): ContractErrorBase { + switch (name) { + case PaymentTreasuryErrorNames.UnAuthorized: + return new PaymentTreasuryUnAuthorizedError(); + case PaymentTreasuryErrorNames.InvalidInput: + return new PaymentTreasuryInvalidInputError(); + case PaymentTreasuryErrorNames.PaymentAlreadyExist: + return new PaymentTreasuryPaymentAlreadyExistError({ + paymentId: args["paymentId"] as string, + }); + case PaymentTreasuryErrorNames.PaymentAlreadyConfirmed: + return new PaymentTreasuryPaymentAlreadyConfirmedError({ + paymentId: args["paymentId"] as string, + }); + case PaymentTreasuryErrorNames.PaymentAlreadyExpired: + return new PaymentTreasuryPaymentAlreadyExpiredError({ + paymentId: args["paymentId"] as string, + }); + case PaymentTreasuryErrorNames.PaymentNotExist: + return new PaymentTreasuryPaymentNotExistError({ + paymentId: args["paymentId"] as string, + }); + case PaymentTreasuryErrorNames.CampaignInfoIsPaused: + return new PaymentTreasuryCampaignInfoIsPausedError(); + case PaymentTreasuryErrorNames.TokenNotAccepted: + return new PaymentTreasuryTokenNotAcceptedError({ token: args["token"] as string }); + case PaymentTreasuryErrorNames.SuccessConditionNotFulfilled: + return new PaymentTreasurySuccessConditionNotFulfilledError(); + case PaymentTreasuryErrorNames.FeeNotDisbursed: + return new PaymentTreasuryFeeNotDisbursedError(); + case PaymentTreasuryErrorNames.PaymentNotConfirmed: + return new PaymentTreasuryPaymentNotConfirmedError({ + paymentId: args["paymentId"] as string, + }); + case PaymentTreasuryErrorNames.PaymentNotClaimable: + return new PaymentTreasuryPaymentNotClaimableError({ + paymentId: args["paymentId"] as string, + }); + case PaymentTreasuryErrorNames.AlreadyWithdrawn: + return new PaymentTreasuryAlreadyWithdrawnError(); + case PaymentTreasuryErrorNames.CryptoPayment: + return new PaymentTreasuryCryptoPaymentError({ + paymentId: args["paymentId"] as string, + }); + case PaymentTreasuryErrorNames.InsufficientFundsForFee: + return new PaymentTreasuryInsufficientFundsForFeeError({ + withdrawalAmount: args["withdrawalAmount"] as string, + fee: args["fee"] as string, + }); + case PaymentTreasuryErrorNames.InsufficientBalance: + return new PaymentTreasuryInsufficientBalanceError({ + required: args["required"] as string, + available: args["available"] as string, + }); + case PaymentTreasuryErrorNames.ExpirationExceedsMax: + return new PaymentTreasuryExpirationExceedsMaxError({ + expiration: args["expiration"] as string, + maxExpiration: args["maxExpiration"] as string, + }); + case PaymentTreasuryErrorNames.ClaimWindowNotReached: + return new PaymentTreasuryClaimWindowNotReachedError({ + claimableAt: args["claimableAt"] as string, + }); + case PaymentTreasuryErrorNames.NoFundsToClaim: + return new PaymentTreasuryNoFundsToClaimError(); + /* istanbul ignore next -- defensive fallback; all ABI errors are handled above */ + default: + return new (class extends Error implements ContractErrorBase { + readonly name = name; + readonly args = args; + })(`${name}(${JSON.stringify(args)})`); + } +} + +/** + * Decodes raw revert data from a PaymentTreasury contract call into a typed SDK error. + * @param data - 0x-prefixed hex revert data + * @returns Typed PaymentTreasury error instance, or null if the selector is not recognised + */ +export function parsePaymentTreasuryError(data: Hex): ContractErrorBase | null { + return tryDecodeContractError( + PAYMENT_TREASURY_ABI as readonly ErrorAbiEntry[], + data, + toPaymentTreasuryError, + ); +} diff --git a/packages/contracts/src/errors/parse/shared.ts b/packages/contracts/src/errors/parse/shared.ts new file mode 100644 index 00000000..6b101a07 --- /dev/null +++ b/packages/contracts/src/errors/parse/shared.ts @@ -0,0 +1,117 @@ +/** + * Shared error-decoding helpers used by all per-contract error parsers. + */ + +import { decodeErrorResult } from "../../lib"; +import type { Hex } from "../../lib"; +import type { ContractErrorBase } from "../base"; +import { + AccessCheckerUnauthorizedError, + AdminAccessCheckerUnauthorizedError, + CurrentTimeIsGreaterError, + CurrentTimeIsLessError, + CurrentTimeIsNotWithinRangeError, + SharedErrorNames, + TreasuryCampaignInfoIsPausedError, + TreasuryFeeNotDisbursedError, + TreasuryTransferFailedError, +} from "../contracts/shared"; + +/** Minimal ABI entry shape used by error decoders. */ +export type ErrorAbiEntry = { + type: string; + name?: string; + inputs?: readonly { name: string }[]; +}; + +/** + * Maps a decoded error args tuple to a named record using the ABI parameter names. + * @param abi - ABI array containing error entries + * @param errorName - Solidity error name to look up in the ABI + * @param decodedArgs - Raw decoded args tuple from decodeErrorResult + * @returns Record mapping each parameter name to its decoded value + */ +export function decodeErrorArgs( + abi: readonly ErrorAbiEntry[], + errorName: string, + decodedArgs: readonly unknown[], +): Record { + const args: Record = {}; + const errorAbi = abi.find((item) => item.type === "error" && item.name === errorName); + if (errorAbi?.inputs) { + errorAbi.inputs.forEach((input, i) => { + if (input.name && decodedArgs[i] !== undefined) { + const value = decodedArgs[i]; + args[input.name] = typeof value === "bigint" ? String(value) : value; + } + }); + } + return args; +} + +/** + * Maps a shared (cross-contract) error name to a typed SDK error instance. + * Called by per-contract parsers when they encounter an error they do not own. + * @param name - Solidity error name from the decoded revert data + * @param args - Named argument record produced by decodeErrorArgs + * @returns Typed shared error instance, or null if the name is not a known shared error + */ +export function toSharedContractError( + name: string, + args: Record, +): ContractErrorBase | null { + switch (name) { + case SharedErrorNames.AccessCheckerUnauthorized: + return new AccessCheckerUnauthorizedError(); + case SharedErrorNames.AdminAccessCheckerUnauthorized: + return new AdminAccessCheckerUnauthorizedError(); + case SharedErrorNames.CurrentTimeIsGreater: + return new CurrentTimeIsGreaterError({ + inputTime: args["inputTime"] as string, + currentTime: args["currentTime"] as string, + }); + case SharedErrorNames.CurrentTimeIsLess: + return new CurrentTimeIsLessError({ + inputTime: args["inputTime"] as string, + currentTime: args["currentTime"] as string, + }); + case SharedErrorNames.CurrentTimeIsNotWithinRange: + return new CurrentTimeIsNotWithinRangeError({ + initialTime: args["initialTime"] as string, + finalTime: args["finalTime"] as string, + }); + case SharedErrorNames.TreasuryCampaignInfoIsPaused: + return new TreasuryCampaignInfoIsPausedError(); + case SharedErrorNames.TreasuryFeeNotDisbursed: + return new TreasuryFeeNotDisbursedError(); + case SharedErrorNames.TreasuryTransferFailed: + return new TreasuryTransferFailedError(); + default: + return null; + } +} + +/** + * Attempts to decode raw revert data using the given ABI, then maps the result to a typed error. + * @param abi - Contract ABI containing error definitions + * @param data - 0x-prefixed hex revert data from a contract call + * @param toError - Per-contract mapper that converts a decoded error name and args to a typed class + * @returns Typed error instance if decoding and mapping succeed, null otherwise + */ +export function tryDecodeContractError( + abi: readonly ErrorAbiEntry[], + data: Hex, + toError: (name: string, args: Record) => ContractErrorBase, +): ContractErrorBase | null { + try { + const decoded = decodeErrorResult({ + abi: abi as Parameters[0]["abi"], + data, + }); + const decodedArgs = (decoded.args ?? []) as readonly unknown[]; + const args = decodeErrorArgs(abi, decoded.errorName, decodedArgs); + return toError(decoded.errorName, args); + } catch { + return null; + } +} diff --git a/packages/contracts/src/errors/parse/treasury-factory.ts b/packages/contracts/src/errors/parse/treasury-factory.ts new file mode 100644 index 00000000..99c47594 --- /dev/null +++ b/packages/contracts/src/errors/parse/treasury-factory.ts @@ -0,0 +1,67 @@ +import type { Hex } from "../../lib"; +import type { ContractErrorBase } from "../base"; +import { TREASURY_FACTORY_ABI } from "../../contracts/treasury-factory/abi"; +import { + TreasuryFactoryErrorNames, + TreasuryFactoryImplementationNotSetError, + TreasuryFactoryImplementationNotSetOrApprovedError, + TreasuryFactoryInvalidAddressError, + TreasuryFactoryInvalidKeyError, + TreasuryFactorySettingPlatformInfoFailedError, + TreasuryFactoryTreasuryCreationFailedError, + TreasuryFactoryTreasuryInitializationFailedError, + TreasuryFactoryUnauthorizedError, +} from "../contracts/treasury-factory"; +import type { ErrorAbiEntry } from "./shared"; +import { toSharedContractError, tryDecodeContractError } from "./shared"; + +/** + * Maps a decoded TreasuryFactory error name and args to a typed SDK error instance. + * @param name - Decoded error name string + * @param args - Decoded error arguments + * @returns Typed TreasuryFactory error, or a shared/generic fallback + */ +function toTreasuryFactoryError(name: string, args: Record): ContractErrorBase { + switch (name) { + case TreasuryFactoryErrorNames.Unauthorized: + return new TreasuryFactoryUnauthorizedError(); + case TreasuryFactoryErrorNames.InvalidKey: + return new TreasuryFactoryInvalidKeyError(); + case TreasuryFactoryErrorNames.TreasuryCreationFailed: + return new TreasuryFactoryTreasuryCreationFailedError(); + case TreasuryFactoryErrorNames.InvalidAddress: + return new TreasuryFactoryInvalidAddressError(); + case TreasuryFactoryErrorNames.ImplementationNotSet: + return new TreasuryFactoryImplementationNotSetError(); + case TreasuryFactoryErrorNames.ImplementationNotSetOrApproved: + return new TreasuryFactoryImplementationNotSetOrApprovedError(); + case TreasuryFactoryErrorNames.TreasuryInitializationFailed: + return new TreasuryFactoryTreasuryInitializationFailedError(); + case TreasuryFactoryErrorNames.SettingPlatformInfoFailed: + return new TreasuryFactorySettingPlatformInfoFailedError(); + default: { + const shared = toSharedContractError(name, args); + /* istanbul ignore next -- defensive fallback; all shared errors are recognised */ + if (!shared) { + return new (class extends Error implements ContractErrorBase { + readonly name = name; + readonly args = args; + })(`${name}(${JSON.stringify(args)})`); + } + return shared; + } + } +} + +/** + * Decodes raw revert data from a TreasuryFactory contract call into a typed SDK error. + * @param data - 0x-prefixed hex revert data + * @returns Typed TreasuryFactory error instance, or null if the selector is not recognised + */ +export function parseTreasuryFactoryError(data: Hex): ContractErrorBase | null { + return tryDecodeContractError( + TREASURY_FACTORY_ABI as readonly ErrorAbiEntry[], + data, + toTreasuryFactoryError, + ); +} diff --git a/packages/contracts/src/errors/recovery.ts b/packages/contracts/src/errors/recovery.ts new file mode 100644 index 00000000..54acacea --- /dev/null +++ b/packages/contracts/src/errors/recovery.ts @@ -0,0 +1,11 @@ +import type { ContractErrorBase } from "./base"; + +/** + * Returns a human-readable recovery suggestion for a typed contract error, if available. + * + * @param error - A typed contract error instance + * @returns Recovery hint string, or undefined if none is available + */ +export function getRecoveryHint(error: ContractErrorBase): string | undefined { + return error.recoveryHint; +} diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index a75b3546..6860caf5 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -1,10 +1,38 @@ -// Contract definitions and types -// Add your contract code here - -/** - * Placeholder function to ensure coverage collection works - * Remove this once you add actual contract code - */ -export function placeholder(): void { - // This ensures coverage is calculated -} +export { createOakContractsClient } from "./client"; + +export * from "./utils"; +export * from "./types"; + +export type { + Account, + Address, + Chain, + Hex, + PublicClient, + WalletClient, +} from "./lib"; +export type { Wallet } from "./lib"; + +export { + createPublicClient, + createWalletClient, + http, + custom, + stringToHex, + toHex, + parseEther, + formatEther, + parseUnits, + isAddress, + getAddress, + mainnet, + sepolia, + goerli, + createJsonRpcProvider, + createBrowserProvider, + getSigner, + createWallet, +} from "./lib"; + +export * from "./constants"; +export * from "./errors"; diff --git a/packages/contracts/src/lib/index.ts b/packages/contracts/src/lib/index.ts new file mode 100644 index 00000000..04118e99 --- /dev/null +++ b/packages/contracts/src/lib/index.ts @@ -0,0 +1 @@ +export * from "./viem"; diff --git a/packages/contracts/src/lib/privy/index.ts b/packages/contracts/src/lib/privy/index.ts new file mode 100644 index 00000000..160cfec3 --- /dev/null +++ b/packages/contracts/src/lib/privy/index.ts @@ -0,0 +1,5 @@ +// TODO: Implement Privy wallet adapter +// +// This module will provide an optional adapter that converts a Privy embedded +// or server wallet into the WalletClient shape accepted by createOakContractsClient. +// \ No newline at end of file diff --git a/packages/contracts/src/lib/viem/index.ts b/packages/contracts/src/lib/viem/index.ts new file mode 100644 index 00000000..ba047d2f --- /dev/null +++ b/packages/contracts/src/lib/viem/index.ts @@ -0,0 +1,46 @@ +// Values +export { + createPublicClient, + createWalletClient, + http, + custom, + keccak256, + toHex, + stringToHex, + encodeAbiParameters, + decodeErrorResult, + parseEther, + formatEther, + parseUnits, + isAddress, + getAddress, + defineChain, +} from "viem"; + +// Types +export type { + Account, + Address, + Chain, + Hex, + PublicClient, + WalletClient, + EIP1193Provider, +} from "viem"; + +// Chain presets +export { mainnet, sepolia, goerli } from "viem/chains"; + +// Accounts +export { privateKeyToAccount } from "viem/accounts"; + +// Provider helpers +export { + createJsonRpcProvider, + createBrowserProvider, + getSigner, + createWallet, +} from "./provider"; + +// SDK-level wallet type (WalletClient with guaranteed account) +export type { Wallet, JsonRpcProvider } from "../../client/types"; diff --git a/packages/contracts/src/lib/viem/provider.ts b/packages/contracts/src/lib/viem/provider.ts new file mode 100644 index 00000000..dda49a1d --- /dev/null +++ b/packages/contracts/src/lib/viem/provider.ts @@ -0,0 +1,137 @@ +import { + createPublicClient, + createWalletClient, + http, + custom, + type Account, + type EIP1193Provider, +} from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import type { Chain } from "viem/chains"; +import type { JsonRpcProvider, Wallet } from "../../client/types"; + +/** + * Creates a JsonRpcProvider (wrapped viem PublicClient) from an RPC URL. + * + * @param rpcUrl - RPC URL string + * @param chain - Chain configuration + * @returns JsonRpcProvider instance + * + * @example + * ```typescript + * import { createJsonRpcProvider, mainnet } from '@oaknetwork/contracts' + * + * const provider = createJsonRpcProvider('https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY', mainnet) + * ``` + */ +export function createJsonRpcProvider( + rpcUrl: string, + chain: Chain, + timeout?: number, +): JsonRpcProvider { + return createPublicClient({ + chain, + transport: http(rpcUrl, { timeout }), + }) as JsonRpcProvider; +} + +/** + * Creates a Wallet (wrapped viem WalletClient) from a private key. + * + * @param privateKey - Private key as hex string + * @param provider - Provider instance (PublicClient) — used to get chain config + * @param rpcUrl - Optional RPC URL. If omitted, uses the provider's transport + * @returns Wallet instance + * + * @example + * ```typescript + * import { createWallet, createJsonRpcProvider, mainnet } from '@oaknetwork/contracts' + * + * const provider = createJsonRpcProvider(rpcUrl, mainnet) + * const signer = createWallet(privateKey, provider, rpcUrl) + * ``` + */ +export function createWallet( + privateKey: `0x${string}`, + rpcUrl: string, + chain: Chain, + timeout?: number, +): Wallet { + const account = privateKeyToAccount(privateKey); + + const walletClient = createWalletClient({ + account, + chain, + transport: http(rpcUrl, { timeout }), + }); + + return { + ...walletClient, + account, + } as Wallet; +} + +/** + * Creates a BrowserProvider (wrapped viem PublicClient) from window.ethereum. + * This is a helper for frontend usage with MetaMask or other injected wallets. + * + * @param ethereum - window.ethereum object (EIP-1193 provider) + * @param chain - Chain configuration + * @returns JsonRpcProvider instance + * + * @example + * ```typescript + * import { createBrowserProvider, mainnet } from '@oaknetwork/contracts' + * + * const provider = createBrowserProvider(window.ethereum, mainnet) + * ``` + */ +export function createBrowserProvider( + ethereum: EIP1193Provider, + chain: Chain, +): JsonRpcProvider { + return createPublicClient({ + chain, + transport: custom(ethereum), + }) as JsonRpcProvider; +} + +/** + * Gets a signer from a browser provider (for use with MetaMask, etc.). + * Requests accounts from the injected wallet and returns a WalletClient. + * + * @param ethereum - window.ethereum object (EIP-1193 provider) + * @param chain - Chain configuration + * @returns Promise resolving to a Wallet instance + * + * @example + * ```typescript + * import { createBrowserProvider, getSigner, mainnet } from '@oaknetwork/contracts' + * + * const provider = createBrowserProvider(window.ethereum, mainnet) + * const signer = await getSigner(window.ethereum, mainnet) + * ``` + */ +export async function getSigner( + ethereum: EIP1193Provider, + chain: Chain, +): Promise { + const accounts = await ethereum.request({ + method: "eth_requestAccounts", + }); + + if (!accounts || accounts.length === 0) { + throw new Error("No accounts found. Please connect your wallet."); + } + + const walletClient = createWalletClient({ + account: accounts[0] as `0x${string}`, + chain, + transport: custom(ethereum), + }); + + return { + ...walletClient, + account: walletClient.account as Account, + } as Wallet; +} diff --git a/packages/contracts/src/metrics/campaign.ts b/packages/contracts/src/metrics/campaign.ts new file mode 100644 index 00000000..04f94a98 --- /dev/null +++ b/packages/contracts/src/metrics/campaign.ts @@ -0,0 +1,18 @@ +/** + * @file metrics/campaign.ts + * TODO: Implement campaign-level aggregation across treasuries. + */ + +import type { CampaignSummary } from "./types"; + +/** + * Aggregates state from all treasuries linked to a campaign. + * @param _campaignInfoAddress - Deployed CampaignInfo contract address + * @returns CampaignSummary — currently a stub returning empty summary + */ +export async function getCampaignSummary( + _campaignInfoAddress: string, +): Promise { + // TODO: implement by reading linked treasury contracts via CampaignInfo + return {}; +} diff --git a/packages/contracts/src/metrics/index.ts b/packages/contracts/src/metrics/index.ts new file mode 100644 index 00000000..97782c34 --- /dev/null +++ b/packages/contracts/src/metrics/index.ts @@ -0,0 +1,10 @@ +/** + * @file metrics/index.ts + * Public surface for the @oaknetwork/contracts/metrics sub-path export. + * TODO: Register in package.json exports as `@oaknetwork/contracts/metrics`. + */ + +export { getPlatformStats } from "./platform"; +export { getCampaignSummary } from "./campaign"; +export { getTreasuryReport } from "./treasury"; +export type { PlatformStats, CampaignSummary, TreasuryReport } from "./types"; diff --git a/packages/contracts/src/metrics/platform.ts b/packages/contracts/src/metrics/platform.ts new file mode 100644 index 00000000..ecc7da61 --- /dev/null +++ b/packages/contracts/src/metrics/platform.ts @@ -0,0 +1,15 @@ +/** + * @file metrics/platform.ts + * TODO: Implement with multicall where supported. + */ + +import type { PlatformStats } from "./types"; + +/** + * Aggregates protocol-level statistics from GlobalParams and all treasury contracts. + * @returns PlatformStats — currently a stub returning empty stats + */ +export async function getPlatformStats(): Promise { + // TODO: implement using multicall across GlobalParams and treasury contracts + return {}; +} diff --git a/packages/contracts/src/metrics/treasury.ts b/packages/contracts/src/metrics/treasury.ts new file mode 100644 index 00000000..1b190798 --- /dev/null +++ b/packages/contracts/src/metrics/treasury.ts @@ -0,0 +1,18 @@ +/** + * @file metrics/treasury.ts + * TODO: Implement per-treasury reporting. + */ + +import type { TreasuryReport } from "./types"; + +/** + * Builds a report for a single treasury contract address. + * @param _treasuryAddress - Deployed treasury contract address + * @returns TreasuryReport — currently a stub returning empty report + */ +export async function getTreasuryReport( + _treasuryAddress: string, +): Promise { + // TODO: implement by reading raised/refunded amounts and fee config + return {}; +} diff --git a/packages/contracts/src/metrics/types.ts b/packages/contracts/src/metrics/types.ts new file mode 100644 index 00000000..891adb0a --- /dev/null +++ b/packages/contracts/src/metrics/types.ts @@ -0,0 +1,33 @@ +/** + * @file metrics/types.ts + * Placeholder types for cross-contract aggregation results. + * TODO: Complete shapes once multicall-based aggregation is implemented. + */ + +/** Aggregated protocol-level statistics across all campaigns. */ +export interface PlatformStats { + /** Total number of enlisted platforms. */ + platformCount?: bigint; + /** Total protocol fees collected across all treasuries. */ + totalProtocolFees?: bigint; +} + +/** Summary of a single campaign's treasury state. */ +export interface CampaignSummary { + /** Total amount raised across all treasury types. */ + totalRaised?: bigint; + /** Total amount refunded. */ + totalRefunded?: bigint; + /** Whether the campaign goal has been reached. */ + goalReached?: boolean; +} + +/** Aggregated report for a single treasury contract. */ +export interface TreasuryReport { + /** Address of the treasury contract. */ + address?: string; + /** Current raised amount held in the treasury. */ + raisedAmount?: bigint; + /** Platform fee percent in basis points. */ + platformFeePercent?: bigint; +} diff --git a/packages/contracts/src/scripts/check-abis.ts b/packages/contracts/src/scripts/check-abis.ts new file mode 100644 index 00000000..e9fd65e1 --- /dev/null +++ b/packages/contracts/src/scripts/check-abis.ts @@ -0,0 +1,20 @@ +/** + * @file scripts/check-abis.ts + * CI check: detect ABI drift between compiled artifacts and SDK source. + * + * TODO: Implement. Compare each contracts/{name}/abi.ts against current + * compiled artifacts; exit with non-zero if any ABI is stale or missing. + * Run in CI on every PR to prevent SDK/contract mismatch. + */ + +/** + * Compares each `contracts/{name}/abi.ts` against current compiled Solidity artifacts. + * Exits with a non-zero code if any ABI is stale or missing. + * Intended to run in CI on every pull request. + * + * @returns true if all ABIs are current, false if any are stale + * @throws {Error} Not yet implemented + */ +export function checkAbis(): boolean { + throw new Error("TODO: check-abis not implemented"); +} diff --git a/packages/contracts/src/scripts/generate-abis.ts b/packages/contracts/src/scripts/generate-abis.ts new file mode 100644 index 00000000..1cc974bd --- /dev/null +++ b/packages/contracts/src/scripts/generate-abis.ts @@ -0,0 +1,22 @@ +/** + * @file scripts/generate-abis.ts + * Extracts ABIs from compiled Solidity artifacts into contracts/{name}/abi.ts. + * + * TODO: Implement. Read Hardhat/Foundry artifacts (e.g. out/ or artifacts/) + * and write each contract's ABI to the corresponding contracts/{name}/abi.ts + * as a typed const array. Run after contract recompile to keep SDK in sync. + * This script is standalone — not imported by SDK source. + */ + +/** + * Reads compiled Solidity artifacts and writes each contract's ABI into + * the corresponding `contracts/{name}/abi.ts` as a typed const array. + * Run after a contract recompile to keep SDK sources in sync. + * + * @returns void + * @throws {Error} Not yet implemented + * @internal + */ +export function generateAbis(): void { + throw new Error("TODO: generate-abis not implemented"); +} diff --git a/packages/contracts/src/types/index.ts b/packages/contracts/src/types/index.ts new file mode 100644 index 00000000..39de5567 --- /dev/null +++ b/packages/contracts/src/types/index.ts @@ -0,0 +1,6 @@ +/** + * Cross-contract type definitions only — no logic, no client dependencies. + * structs.ts holds on-chain struct mirrors; params.ts holds SDK-level input types. + */ +export * from "./structs"; +export * from "./params"; diff --git a/packages/contracts/src/types/params.ts b/packages/contracts/src/types/params.ts new file mode 100644 index 00000000..1c6917d9 --- /dev/null +++ b/packages/contracts/src/types/params.ts @@ -0,0 +1,71 @@ +import type { Address, Hex } from "../lib"; + +/** Shape of campaign data passed to createCampaign (matches ICampaignData.CampaignData). */ +export interface CreateCampaignData { + /** Unix launch timestamp in seconds. */ + launchTime: bigint; + /** Unix deadline timestamp in seconds. */ + deadline: bigint; + /** Funding goal in currency units. */ + goalAmount: bigint; + /** bytes32 currency identifier. */ + currency: Hex; +} + +/** Input parameters for CampaignInfoFactory.createCampaign. */ +export interface CreateCampaignParams { + /** Address of the campaign creator. */ + creator: Address; + /** bytes32 unique campaign identifier hash. */ + identifierHash: Hex; + /** Platform hashes selected for this campaign. */ + selectedPlatformHash: readonly Hex[]; + /** Optional platform-specific data keys (parallel array with platformDataValue). */ + platformDataKey?: readonly Hex[]; + /** Optional platform-specific data values (parallel array with platformDataKey). */ + platformDataValue?: readonly Hex[]; + /** On-chain campaign data struct. */ + campaignData: CreateCampaignData; + /** ERC-721 collection name for pledge NFTs. */ + nftName: string; + /** ERC-721 collection symbol for pledge NFTs. */ + nftSymbol: string; + /** IPFS or HTTPS URI for the NFT image. */ + nftImageURI: string; + /** IPFS or HTTPS URI for the ERC-721 contract metadata. */ + contractURI: string; +} + +/** Config struct for KeepWhatsRaised.configureTreasury. */ +export interface KeepWhatsRaisedConfig { + /** Minimum withdrawal amount exempt from the withdrawal fee. */ + minimumWithdrawalForFeeExemption: bigint; + /** Delay in seconds between approveWithdrawal and actual withdrawal. */ + withdrawalDelay: bigint; + /** Delay in seconds before backers can claim refunds. */ + refundDelay: bigint; + /** Seconds after configuration during which config is locked. */ + configLockPeriod: bigint; + /** True if the creator qualifies for Colombian creator tax treatment. */ + isColombianCreator: boolean; +} + +/** FeeKeys struct for KeepWhatsRaised.configureTreasury. */ +export interface KeepWhatsRaisedFeeKeys { + /** Registry key for the flat withdrawal fee. */ + flatFeeKey: Hex; + /** Registry key for the cumulative flat fee cap. */ + cumulativeFlatFeeKey: Hex; + /** Registry keys for gross percentage fees (one per tier). */ + grossPercentageFeeKeys: readonly Hex[]; +} + +/** FeeValues struct for KeepWhatsRaised.configureTreasury. */ +export interface KeepWhatsRaisedFeeValues { + /** Flat fee amount in token units. */ + flatFeeValue: bigint; + /** Cumulative flat fee cap in token units. */ + cumulativeFlatFeeValue: bigint; + /** Gross percentage fee values (one per key in grossPercentageFeeKeys). */ + grossPercentageFeeValues: readonly bigint[]; +} diff --git a/packages/contracts/src/types/structs.ts b/packages/contracts/src/types/structs.ts new file mode 100644 index 00000000..939bf383 --- /dev/null +++ b/packages/contracts/src/types/structs.ts @@ -0,0 +1,163 @@ +import type { Address, Hex } from "../lib"; + +/** ICampaignData.CampaignData — used by CampaignInfo and CampaignInfoFactory. */ +export interface CampaignData { + /** Unix timestamp (seconds) when the campaign launches. */ + launchTime: bigint; + /** Unix timestamp (seconds) when the campaign ends. */ + deadline: bigint; + /** Minimum funding goal in the campaign currency unit. */ + goalAmount: bigint; + /** bytes32 currency identifier (e.g. keccak256("USD")). */ + currency: Hex; +} + +/** + * Reward struct for AllOrNothing and KeepWhatsRaised treasuries. + * The isRewardTier flag distinguishes tiered rewards from flat rewards. + */ +export interface TieredReward { + /** Minimum pledge value required for this reward tier. */ + rewardValue: bigint; + /** True if this entry is a reward tier rather than a flat reward. */ + isRewardTier: boolean; + /** bytes32 item IDs included in this reward. */ + itemId: readonly Hex[]; + /** Declared item values parallel to itemId. */ + itemValue: readonly bigint[]; + /** Item quantities parallel to itemId. */ + itemQuantity: readonly bigint[]; +} + +/** ICampaignPaymentTreasury.LineItem — typeId + amount pair sent into a payment. */ +export interface LineItem { + /** bytes32 type identifier registered in GlobalParams. */ + typeId: Hex; + /** Token amount for this line item. */ + amount: bigint; +} + +/** IItem.Item — physical-item record used by ItemRegistry. */ +export interface Item { + /** Actual weight in grams. */ + actualWeight: bigint; + /** Height in millimetres. */ + height: bigint; + /** Width in millimetres. */ + width: bigint; + /** Length in millimetres. */ + length: bigint; + /** bytes32 category identifier. */ + category: Hex; + /** bytes32 declared currency identifier. */ + declaredCurrency: Hex; +} + +/** + * ICampaignPaymentTreasury.PaymentLineItem — line item stored with its configuration snapshot. + * Returned by getPaymentData; all uint256 values are bigint; bytes32 are hex strings. + */ +export interface PaymentLineItem { + /** bytes32 type identifier. */ + typeId: Hex; + /** Token amount. */ + amount: bigint; + /** Human-readable label from the line item type config. */ + label: string; + /** True if this amount counts toward the funding goal. */ + countsTowardGoal: boolean; + /** True if the protocol fee is applied to this item. */ + applyProtocolFee: boolean; + /** True if the backer can claim a refund for this item. */ + canRefund: boolean; + /** True if funds are transferred immediately on confirmation. */ + instantTransfer: boolean; +} + +/** ICampaignPaymentTreasury.ExternalFees — informational external fee metadata. */ +export interface ExternalFees { + /** bytes32 fee type identifier. */ + feeType: Hex; + /** Fee amount in token units. */ + feeAmount: bigint; +} + +/** + * ICampaignPaymentTreasury.PaymentData — comprehensive payment snapshot. + * Mirrors the on-chain struct; lineItems and externalFees carry config snapshots. + */ +export interface PaymentData { + /** Buyer's wallet address. */ + buyerAddress: Address; + /** bytes32 off-chain buyer identifier. */ + buyerId: Hex; + /** bytes32 item identifier. */ + itemId: Hex; + /** Total payment amount in token units. */ + amount: bigint; + /** Unix timestamp when the payment expires. */ + expiration: bigint; + /** True once the payment has been confirmed on-chain. */ + isConfirmed: boolean; + /** True if the payment was processed as a crypto payment. */ + isCryptoPayment: boolean; + /** Total number of line items in this payment. */ + lineItemCount: bigint; + /** ERC-20 token address used for payment. */ + paymentToken: Address; + /** Snapshot of all line items with their configuration. */ + lineItems: readonly PaymentLineItem[]; + /** External fee entries associated with this payment. */ + externalFees: readonly ExternalFees[]; +} + +/** + * EIP-2612 permit parameters for off-chain token approvals. + * Reserved for future use when permit-based flows are implemented. + * @internal + */ +export interface PermitParams { + /** Token owner granting the permit. */ + owner: Address; + /** Spender being approved. */ + spender: Address; + /** Amount approved. */ + value: bigint; + /** Permit expiry timestamp. */ + deadline: bigint; + /** Signature v component. */ + v: number; + /** Signature r component. */ + r: Hex; + /** Signature s component. */ + s: Hex; +} + +/** + * Return type for getLineItemType / getPlatformLineItemType. + * Reflects the on-chain struct stored per platform-scoped type ID. + */ +export interface LineItemTypeInfo { + /** True if the line item type is registered. */ + exists: boolean; + /** Human-readable label. */ + label: string; + /** True if amounts of this type count toward the funding goal. */ + countsTowardGoal: boolean; + /** True if the protocol fee applies to this type. */ + applyProtocolFee: boolean; + /** True if backers can claim refunds for this type. */ + canRefund: boolean; + /** True if funds are transferred immediately on confirmation. */ + instantTransfer: boolean; +} + +/** Return type for CampaignInfo.getCampaignConfig. */ +export interface CampaignConfig { + /** Address of the TreasuryFactory used to deploy treasuries. */ + treasuryFactory: Address; + /** Protocol fee percent in basis points. */ + protocolFeePercent: bigint; + /** bytes32 identifier hash for this campaign. */ + identifierHash: Hex; +} diff --git a/packages/contracts/src/utils/account.ts b/packages/contracts/src/utils/account.ts new file mode 100644 index 00000000..c30adeb7 --- /dev/null +++ b/packages/contracts/src/utils/account.ts @@ -0,0 +1,33 @@ +import type { Account, WalletClient } from "../lib"; + +/** + * Narrows a possibly-null WalletClient to a non-null WalletClient, throwing a descriptive + * error if no signer has been configured. Use this in write methods to obtain a typed + * reference before calling writeContract. + * @param walletClient - The wallet client to check, or null for a read-only client + * @returns The non-null WalletClient + * @throws {Error} If walletClient is null (no signer configured) + */ +export function requireSigner(walletClient: WalletClient | null): WalletClient { + if (walletClient === null) { + throw new Error( + "No signer configured. Pass a signer when creating the client " + + "({ privateKey } or { signer }) or supply one per entity: " + + "oak.globalParams(address, { signer: walletClient }).", + ); + } + return walletClient; +} + +/** + * Asserts that a WalletClient has an account attached; throws a descriptive error if not. + * @param walletClient - The wallet client to check + * @returns The attached account + * @throws {Error} If no account is attached to the wallet client + */ +export function requireAccount(walletClient: WalletClient): Account { + if (!walletClient.account) { + throw new Error("WalletClient has no account attached. Provide a signer with an account."); + } + return walletClient.account; +} diff --git a/packages/contracts/src/utils/chain.ts b/packages/contracts/src/utils/chain.ts new file mode 100644 index 00000000..b505b6f1 --- /dev/null +++ b/packages/contracts/src/utils/chain.ts @@ -0,0 +1,44 @@ +import { defineChain } from "../lib"; +import { mainnet, sepolia, goerli } from "../lib"; +import type { Chain } from "../lib"; + +/** Celo Mainnet chain definition. */ +const celoMainnet = defineChain({ + id: 42220, + name: "Celo", + nativeCurrency: { decimals: 18, name: "CELO", symbol: "CELO" }, + rpcUrls: { default: { http: ["https://forno.celo.org"] } }, +}); + +/** Celo Sepolia testnet chain definition. */ +const celoSepolia = defineChain({ + id: 11142220, + name: "Celo Sepolia", + nativeCurrency: { decimals: 18, name: "CELO", symbol: "CELO" }, + rpcUrls: { default: { http: ["https://forno.celo-sepolia.celo-testnet.org"] } }, +}); + +const CHAIN_REGISTRY: Record = { + 1: mainnet, + 42220: celoMainnet, + 11155111: sepolia, + 5: goerli, + 11142220: celoSepolia, +}; + +/** + * Resolves a numeric chain ID to a viem Chain object. + * Falls back to a minimal chain definition for unknown IDs. + * @param chainId - Numeric chain ID + * @returns Viem Chain object + */ +export function getChainFromId(chainId: number): Chain { + const predefinedChain = CHAIN_REGISTRY[chainId]; + if (predefinedChain) return predefinedChain; + return defineChain({ + id: chainId, + name: `Chain ${chainId}`, + nativeCurrency: { decimals: 18, name: "Ether", symbol: "ETH" }, + rpcUrls: { default: { http: [] } }, + }); +} diff --git a/packages/contracts/src/utils/hash.ts b/packages/contracts/src/utils/hash.ts new file mode 100644 index 00000000..f83d3378 --- /dev/null +++ b/packages/contracts/src/utils/hash.ts @@ -0,0 +1,26 @@ +import { keccak256 as viemKeccak256, stringToHex, type Hex } from "../lib"; + +/** + * Hashes a string or Uint8Array using keccak256. + * Accepts 0x-prefixed hex strings directly without conversion. + * @param data - String (converted to hex) or raw Uint8Array + * @returns keccak256 hash as 0x-prefixed hex + */ +export function keccak256(data: string | Uint8Array): `0x${string}` { + if (typeof data === "string") { + return data.startsWith("0x") + ? (viemKeccak256(data as Hex) as `0x${string}`) + : (viemKeccak256(stringToHex(data)) as `0x${string}`); + } + return viemKeccak256(data) as `0x${string}`; +} + +/** + * Produces a bytes32 event topic from a UTF-8 string (keccak256(stringToHex(input))). + * Equivalent to Solidity's keccak256(abi.encodePacked(str)). + * @param input - UTF-8 string to hash + * @returns keccak256 hash as 0x-prefixed hex + */ +export function id(input: string): `0x${string}` { + return viemKeccak256(stringToHex(input)) as `0x${string}`; +} diff --git a/packages/contracts/src/utils/hex.ts b/packages/contracts/src/utils/hex.ts new file mode 100644 index 00000000..abe1affe --- /dev/null +++ b/packages/contracts/src/utils/hex.ts @@ -0,0 +1,25 @@ +import { toHex as viemToHex } from "../lib"; + +/** + * Type guard for 0x-prefixed hex strings. + * @param data - Value to check + * @returns True if the value is a valid hex string + */ +export function isHex(data: string): data is `0x${string}` { + return typeof data === "string" && data.startsWith("0x") && /^0x[0-9a-fA-F]*$/.test(data); +} + +/** + * Encodes a string, number, bigint, boolean, or byte array as a 0x-prefixed hex string. + * Thin re-export of viem's toHex via the lib/ boundary. + * @param value - Value to encode + * @param options - Optional encoding options (e.g. `{ size: 32 }` to pad to 32 bytes) + * @returns 0x-prefixed hex string + */ +export function toHex( + value: string | number | bigint | boolean | Uint8Array, + options?: { size?: number }, +): `0x${string}` { + return viemToHex(value, options); +} + diff --git a/packages/contracts/src/utils/index.ts b/packages/contracts/src/utils/index.ts new file mode 100644 index 00000000..4ff7115b --- /dev/null +++ b/packages/contracts/src/utils/index.ts @@ -0,0 +1,9 @@ +/** + * Pure utility helpers — no client or external library dependencies. + * All external imports are routed through lib/. + */ +export { requireSigner, requireAccount } from "./account"; +export { isHex, toHex } from "./hex"; +export { keccak256, id } from "./hash"; +export { getCurrentTimestamp, addDays } from "./time"; +export { getChainFromId } from "./chain"; diff --git a/packages/contracts/src/utils/time.ts b/packages/contracts/src/utils/time.ts new file mode 100644 index 00000000..4e96f4fe --- /dev/null +++ b/packages/contracts/src/utils/time.ts @@ -0,0 +1,17 @@ +/** + * Returns the current Unix time as a bigint in seconds. + * @returns Current Unix timestamp (seconds) + */ +export function getCurrentTimestamp(): bigint { + return BigInt(Math.floor(Date.now() / 1000)); +} + +/** + * Adds a number of days (expressed as seconds) to a Unix timestamp bigint. + * @param timestamp - Unix timestamp in seconds + * @param days - Number of days to add + * @returns New timestamp as bigint + */ +export function addDays(timestamp: bigint, days: number): bigint { + return timestamp + BigInt(days) * 86400n; +} diff --git a/packages/contracts/tsconfig.json b/packages/contracts/tsconfig.json index 309e9cea..b723b071 100644 --- a/packages/contracts/tsconfig.json +++ b/packages/contracts/tsconfig.json @@ -1,7 +1,8 @@ { "compilerOptions": { - "target": "ES2018", - "module": "commonjs", + "target": "ES2020", + "lib": ["ES2021", "DOM"], + "module": "ES2022", "outDir": "./dist", "rootDir": "./src", "strict": true, @@ -9,7 +10,8 @@ "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "declaration": true, - "moduleResolution": "node" + "moduleResolution": "bundler", + "noEmitOnError": false }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] diff --git a/packages/contracts/tsup.config.ts b/packages/contracts/tsup.config.ts new file mode 100644 index 00000000..a80c02e6 --- /dev/null +++ b/packages/contracts/tsup.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: { + index: "src/index.ts", + "utils/index": "src/utils/index.ts", + "constants/index": "src/constants/index.ts", + "contracts/index": "src/contracts/index.ts", + "client/index": "src/client/index.ts", + "errors/index": "src/errors/index.ts", + "metrics/index": "src/metrics/index.ts", + "lib/privy/index": "src/lib/privy/index.ts", + }, + format: ["esm"], + dts: true, + splitting: false, + clean: true, + sourcemap: false, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94a164c0..9de7e828 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,7 +19,11 @@ importers: specifier: ^30.0.0 version: 30.0.0 - packages/payments: + packages/contracts: + dependencies: + viem: + specifier: ^2.23.0 + version: 2.47.0(typescript@5.9.3) devDependencies: '@types/jest': specifier: ^30.0.0 @@ -28,25 +32,25 @@ importers: specifier: ^20.14.11 version: 20.19.33 dotenv: - specifier: ^17.2.1 + specifier: ^17.3.1 version: 17.3.1 jest: specifier: ^30.0.5 version: 30.2.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)) - nock: - specifier: ^14.0.10 - version: 14.0.11 ts-jest: specifier: ^29.4.6 - version: 29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(jest-util@30.2.0)(jest@30.2.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(esbuild@0.27.3)(jest-util@30.2.0)(jest@30.2.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)))(typescript@5.9.3) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@20.19.33)(typescript@5.9.3) + tsup: + specifier: ^8.5.1 + version: 8.5.1(typescript@5.9.3) typescript: specifier: ^5.5.4 version: 5.9.3 - packages/contracts: + packages/payments: devDependencies: '@types/jest': specifier: ^30.0.0 @@ -54,12 +58,18 @@ importers: '@types/node': specifier: ^20.14.11 version: 20.19.33 + dotenv: + specifier: ^17.2.1 + version: 17.3.1 jest: specifier: ^30.0.5 version: 30.2.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)) + nock: + specifier: ^14.0.10 + version: 14.0.11 ts-jest: specifier: ^29.4.6 - version: 29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(jest-util@30.2.0)(jest@30.2.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(esbuild@0.27.3)(jest-util@30.2.0)(jest@30.2.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)))(typescript@5.9.3) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@20.19.33)(typescript@5.9.3) @@ -69,6 +79,9 @@ importers: packages: + '@adraffy/ens-normalize@1.11.1': + resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -306,6 +319,162 @@ packages: '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@inquirer/external-editor@1.0.3': resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} engines: {node: '>=18'} @@ -441,6 +610,18 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.1': + resolution: {integrity: sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -470,6 +651,140 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + + '@scure/base@1.2.6': + resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} + + '@scure/bip32@1.7.0': + resolution: {integrity: sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==} + + '@scure/bip39@1.6.0': + resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} + '@sinclair/typebox@0.34.48': resolution: {integrity: sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==} @@ -506,6 +821,9 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -631,6 +949,17 @@ packages: cpu: [x64] os: [win32] + abitype@1.2.3: + resolution: {integrity: sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3.22.0 || ^4.0.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + acorn-walk@8.3.5: resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} engines: {node: '>=0.4.0'} @@ -668,6 +997,9 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -746,6 +1078,16 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -772,6 +1114,10 @@ packages: chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -801,6 +1147,17 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -875,6 +1232,11 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -888,6 +1250,9 @@ packages: engines: {node: '>=4'} hasBin: true + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -916,6 +1281,15 @@ packages: fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -924,6 +1298,9 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -1051,6 +1428,11 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isows@1.0.7: + resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==} + peerDependencies: + ws: '*' + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -1202,6 +1584,10 @@ packages: node-notifier: optional: true + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1236,9 +1622,17 @@ packages: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -1255,6 +1649,9 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} @@ -1291,6 +1688,9 @@ packages: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} + mlly@1.8.1: + resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -1298,6 +1698,9 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + napi-postinstall@0.3.4: resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -1327,6 +1730,10 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -1337,6 +1744,14 @@ packages: outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + ox@0.14.0: + resolution: {integrity: sha512-WLOB7IKnmI3Ol6RAqY7CJdZKl8QaI44LN91OGF1061YIeN6bL5IsFcdp7+oQShRyamE/8fW/CBRWhJAOzI35Dw==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + p-filter@2.1.0: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} engines: {node: '>=8'} @@ -1387,6 +1802,9 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1410,6 +1828,27 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} @@ -1439,6 +1878,10 @@ packages: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -1455,6 +1898,11 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -1496,6 +1944,10 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + spawndamnit@3.0.1: resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} @@ -1545,6 +1997,11 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -1565,6 +2022,20 @@ packages: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -1572,6 +2043,13 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-jest@29.4.6: resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} @@ -1616,6 +2094,25 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + type-detect@4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} @@ -1633,6 +2130,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} @@ -1661,6 +2161,14 @@ packages: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} + viem@2.47.0: + resolution: {integrity: sha512-jU5e1E1s5E5M1y+YrELDnNar/34U8NXfVcRfxtVETigs2gS1vvW2ngnBoQUGBwLnNr0kNv+NUu4m10OqHByoFw==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -1684,6 +2192,18 @@ packages: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + 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 + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -1709,6 +2229,8 @@ packages: snapshots: + '@adraffy/ens-normalize@1.11.1': {} + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -2064,6 +2586,84 @@ snapshots: tslib: 2.8.1 optional: true + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + '@inquirer/external-editor@1.0.3(@types/node@20.19.33)': dependencies: chardet: 2.1.1 @@ -2325,6 +2925,14 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@noble/ciphers@1.3.0': {} + + '@noble/curves@1.9.1': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.8.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2351,6 +2959,94 @@ snapshots: '@pkgr/core@0.2.9': {} + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + + '@scure/base@1.2.6': {} + + '@scure/bip32@1.7.0': + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + + '@scure/bip39@1.6.0': + dependencies: + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + '@sinclair/typebox@0.34.48': {} '@sinonjs/commons@3.0.1': @@ -2395,6 +3091,8 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@types/estree@1.0.8': {} + '@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-report@3.0.3': @@ -2485,6 +3183,10 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + abitype@1.2.3(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + acorn-walk@8.3.5: dependencies: acorn: 8.16.0 @@ -2509,6 +3211,8 @@ snapshots: ansi-styles@6.2.3: {} + any-promise@1.3.0: {} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -2610,6 +3314,13 @@ snapshots: buffer-from@1.1.2: {} + bundle-require@5.1.0(esbuild@0.27.3): + dependencies: + esbuild: 0.27.3 + load-tsconfig: 0.2.5 + + cac@6.7.14: {} + callsites@3.1.0: {} camelcase@5.3.1: {} @@ -2627,6 +3338,10 @@ snapshots: chardet@2.1.1: {} + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + ci-info@3.9.0: {} ci-info@4.4.0: {} @@ -2649,6 +3364,12 @@ snapshots: color-name@1.1.4: {} + commander@4.1.1: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + convert-source-map@2.0.0: {} create-require@1.1.1: {} @@ -2698,12 +3419,43 @@ snapshots: dependencies: is-arrayish: 0.2.1 + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + escalade@3.2.0: {} escape-string-regexp@2.0.0: {} esprima@4.0.1: {} + eventemitter3@5.0.1: {} + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -2747,6 +3499,10 @@ snapshots: dependencies: bser: 2.1.1 + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -2756,6 +3512,12 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.1 + rollup: 4.59.0 + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -2864,6 +3626,10 @@ snapshots: isexe@2.0.0: {} + isows@1.0.7(ws@8.18.3): + dependencies: + ws: 8.18.3 + istanbul-lib-coverage@3.2.2: {} istanbul-lib-instrument@6.0.3: @@ -3213,6 +3979,8 @@ snapshots: - supports-color - ts-node + joycon@3.1.1: {} + js-tokens@4.0.0: {} js-yaml@3.14.2: @@ -3238,8 +4006,12 @@ snapshots: leven@3.1.0: {} + lilconfig@3.1.3: {} + lines-and-columns@1.2.4: {} + load-tsconfig@0.2.5: {} + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -3254,6 +4026,10 @@ snapshots: dependencies: yallist: 3.1.1 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + make-dir@4.0.0: dependencies: semver: 7.7.4 @@ -3283,10 +4059,23 @@ snapshots: minipass@7.1.3: {} + mlly@1.8.1: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + mri@1.2.0: {} ms@2.1.3: {} + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} @@ -3309,6 +4098,8 @@ snapshots: dependencies: path-key: 3.1.1 + object-assign@4.1.1: {} + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 @@ -3317,6 +4108,21 @@ snapshots: outvariant@1.4.3: {} + ox@0.14.0(typescript@5.9.3): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@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(typescript@5.9.3) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - zod + p-filter@2.1.0: dependencies: p-map: 2.1.0 @@ -3361,6 +4167,8 @@ snapshots: path-type@4.0.0: {} + pathe@2.0.3: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -3375,6 +4183,16 @@ snapshots: dependencies: find-up: 4.1.0 + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.1 + pathe: 2.0.3 + + postcss-load-config@6.0.1: + dependencies: + lilconfig: 3.1.3 + prettier@2.8.8: {} pretty-format@30.2.0: @@ -3400,6 +4218,8 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 + readdirp@4.1.2: {} + require-directory@2.1.1: {} resolve-cwd@3.0.0: @@ -3410,6 +4230,37 @@ snapshots: reusify@1.1.0: {} + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -3439,6 +4290,8 @@ snapshots: source-map@0.6.1: {} + source-map@0.7.6: {} + spawndamnit@3.0.1: dependencies: cross-spawn: 7.0.6 @@ -3485,6 +4338,16 @@ snapshots: strip-json-comments@3.1.1: {} + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -3505,13 +4368,32 @@ snapshots: glob: 10.5.0 minimatch: 10.2.2 + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinyexec@0.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + tmpl@1.0.5: {} to-regex-range@5.0.1: dependencies: is-number: 7.0.0 - ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(jest-util@30.2.0)(jest@30.2.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)))(typescript@5.9.3): + tree-kill@1.2.2: {} + + ts-interface-checker@0.1.13: {} + + ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.29.0))(esbuild@0.27.3)(jest-util@30.2.0)(jest@30.2.0(@types/node@20.19.33)(ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 @@ -3529,6 +4411,7 @@ snapshots: '@jest/transform': 30.2.0 '@jest/types': 30.2.0 babel-jest: 30.2.0(@babel/core@7.29.0) + esbuild: 0.27.3 jest-util: 30.2.0 ts-node@10.9.2(@types/node@20.19.33)(typescript@5.9.3): @@ -3552,6 +4435,33 @@ snapshots: tslib@2.8.1: optional: true + tsup@8.5.1(typescript@5.9.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.3) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.3 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1 + resolve-from: 5.0.0 + rollup: 4.59.0 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + type-detect@4.0.8: {} type-fest@0.21.3: {} @@ -3560,6 +4470,8 @@ snapshots: typescript@5.9.3: {} + ufo@1.6.3: {} + uglify-js@3.19.3: optional: true @@ -3605,6 +4517,23 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 + viem@2.47.0(typescript@5.9.3): + 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(typescript@5.9.3) + isows: 1.0.7(ws@8.18.3) + ox: 0.14.0(typescript@5.9.3) + ws: 8.18.3 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + walker@1.0.8: dependencies: makeerror: 1.0.12 @@ -3632,6 +4561,8 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 4.1.0 + ws@8.18.3: {} + y18n@5.0.8: {} yallist@3.1.1: {}