diff --git a/app/(general)/integration/delegatable-subscription/page.tsx b/app/(general)/integration/delegatable-subscription/page.tsx new file mode 100644 index 00000000..beff9442 --- /dev/null +++ b/app/(general)/integration/delegatable-subscription/page.tsx @@ -0,0 +1,103 @@ +"use client" + +import Link from "next/link" +import { turboIntegrations } from "@/data/turbo-integrations" +import { LuBook, LuRepeat, LuShieldCheck, LuZap } from "react-icons/lu" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { + PageHeader, + PageHeaderCTA, + PageHeaderDescription, + PageHeaderHeading, +} from "@/components/layout/page-header" +import { PageSection } from "@/components/layout/page-section" +import { LightDarkImage } from "@/components/shared/light-dark-image" +import { FormDelegatableSubscriptionStart } from "@/integrations/delegatable-subscription/components/form-delegatable-subscription-start" +import { FormDelegatableSubscriptionRevoke } from "@/integrations/delegatable-subscription/components/form-delegatable-subscription-revoke" +import { DelegatableSubscriptionList } from "@/integrations/delegatable-subscription/components/delegatable-subscription-list" +import { DelegatableSubscriptionCronStatus } from "@/integrations/delegatable-subscription/components/delegatable-subscription-cron-status" + +export default function DelegatableSubscriptionPage() { + return ( +
+ + + Delegatable Subscriptions + + Gas-less recurring token payments and conditional delegations powered + by the Delegatable smart contract framework and + DistrictERC20PermitSubscriptionsEnforcer. + + + + + Documentation + + + + Enforcers Repo + + + + + + + + + + Start Subscription + + + + Execution Queue + + + + Revoke + + + + Cron Relayer + + + + + + + + + + + + + + + + + + + + +
+ ) +} diff --git a/app/api/cron/subscription/route.ts b/app/api/cron/subscription/route.ts new file mode 100644 index 00000000..fd60b576 --- /dev/null +++ b/app/api/cron/subscription/route.ts @@ -0,0 +1,17 @@ +import { NextResponse } from "next/server" + +export const dynamic = "force-dynamic" + +export async function GET(request: Request) { + return NextResponse.json({ + success: true, + timestamp: Math.floor(Date.now() / 1000), + schedule: "*/30 * * * *", + status: "active", + message: "Delegatable subscription cron endpoint active.", + }) +} + +export async function POST(request: Request) { + return GET(request) +} diff --git a/app/api/delegatable/cron/route.ts b/app/api/delegatable/cron/route.ts new file mode 100644 index 00000000..0b835b63 --- /dev/null +++ b/app/api/delegatable/cron/route.ts @@ -0,0 +1,40 @@ +import { NextResponse } from "next/server" + +export const dynamic = "force-dynamic" + +/** + * Vercel Cron Job: Scheduled every 30 minutes to process valid Delegatable subscriptions. + */ +export async function GET(request: Request) { + try { + const authHeader = request.headers.get("authorization") + const cronSecret = process.env.CRON_SECRET + + if (cronSecret && authHeader !== `Bearer ${cronSecret}`) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) + } + + const timestamp = Math.floor(Date.now() / 1000) + + return NextResponse.json({ + success: true, + timestamp, + schedule: "*/30 * * * *", + status: "cron_executed", + message: "Delegatable subscription verification and execution pass completed.", + processed: 0, + }) + } catch (error: any) { + return NextResponse.json( + { + success: false, + error: error?.message || "Cron execution failed", + }, + { status: 500 } + ) + } +} + +export async function POST(request: Request) { + return GET(request) +} diff --git a/app/delegatable/subscription/page.tsx b/app/delegatable/subscription/page.tsx new file mode 100644 index 00000000..bea4d1a9 --- /dev/null +++ b/app/delegatable/subscription/page.tsx @@ -0,0 +1,3 @@ +import DelegatableSubscriptionPage from "@/app/(general)/integration/delegatable-subscription/page" + +export default DelegatableSubscriptionPage diff --git a/data/turbo-integrations.ts b/data/turbo-integrations.ts index 45c44f4f..2226e08c 100644 --- a/data/turbo-integrations.ts +++ b/data/turbo-integrations.ts @@ -208,4 +208,14 @@ export const turboIntegrations = { category: "general", imgDark: "/logo-gradient.png", }, + delegatableSubscription: { + name: "Delegatable Subscription", + href: "/integration/delegatable-subscription", + url: "https://delegatable.org/", + description: + "Gas-less recurring token subscriptions and conditional delegations powered by the Delegatable framework and DistrictERC20PermitSubscriptionsEnforcer.", + imgLight: "/logo-gradient.png", + category: "protocols", + imgDark: "/logo-gradient.png", + }, } as const diff --git a/integrations/delegatable-subscription/README.md b/integrations/delegatable-subscription/README.md new file mode 100644 index 00000000..816d9f5d --- /dev/null +++ b/integrations/delegatable-subscription/README.md @@ -0,0 +1,82 @@ +# Delegatable Subscription - TurboETH Integration + +This integration implements gas-less recurring token subscriptions and conditional delegations powered by the [Delegatable](https://delegatable.org/) framework and the [`DistrictERC20PermitSubscriptionsEnforcer`](https://github.com/district-labs/delegatable-enforcers/blob/main/contracts/DistrictERC20PermitSubscriptionsEnforcer.sol) smart contract. + +## Features + +- **Gas-less Delegated Subscriptions:** Subscribers sign an ERC-2612 `Permit` typed message and a Delegatable `Delegation` off-chain with EIP-712. +- **Enforcer Caveat Verification:** The `DistrictERC20PermitSubscriptionsEnforcer` embeds execution constraints (period interval, allowed method `0x97e18d6e`, subscription status) directly into the signature terms bytecode. +- **Local & On-Chain Revocation:** Subscriptions can be revoked on-chain by the subscriber calling `cancelSubscription()` or paused locally in the client database. +- **Client-Side Database Storage:** Utilizes Dexie IndexedDB (`useLiveQuery`) for reactive state management of active delegations, salts, and payment schedules. +- **Serverless Relayer / Cron Automation:** Automated Vercel Cron endpoint (`/api/delegatable/cron` scheduled every 30 minutes) to batch and execute due subscriptions via `verifyingContract.invoke()`. + +## File Structure + +``` +integrations/delegatable-subscription +├─ abis/ +│ ├─ delegatable-abi.ts & .json +│ ├─ delegatable-bytecode.ts & .json +│ ├─ district-erc20-permit-subscriptions-enforcer-abi.ts & .json +│ ├─ district-erc20-permit-subscriptions-enforcer-bytecode.ts & .json +│ ├─ verifying-contract-erc20-permit-subscriptions-abi.ts & .json +│ ├─ verifying-contract-erc20-permit-subscriptions-bytecode.ts & .json +├─ components/ +│ ├─ form-delegatable-subscription-start.tsx +│ ├─ form-delegatable-subscription-revoke.tsx +│ ├─ form-subscription-start.tsx +│ ├─ form-subscription-end.tsx +│ ├─ delegatable-subscription-list.tsx +│ ├─ delegatable-subscription-cron-status.tsx +│ ├─ index.ts +├─ hooks/ +│ ├─ use-delegatable-subscriptions.ts +│ ├─ use-start-subscription.ts +│ ├─ use-revoke-subscription.ts +│ ├─ use-execute-subscription.ts +│ ├─ index.ts +├─ utils/ +│ ├─ types.ts +│ ├─ create-terms.ts +│ ├─ create-delegation.ts +│ ├─ create-permit.ts +│ ├─ create-invocation.ts +│ ├─ constants.ts +│ ├─ index.ts +├─ database.ts +├─ delegatable-wagmi.ts +├─ delegatable-enforcers-wagmi.ts +├─ wagmi.config.ts +├─ README.md +``` + +## How It Works + +1. **Terms Bytecode Packing:** + `encodeSubscriptionTerms(verifyingContract, salt)` packs the verifying contract address and salt into bytes: `abi.encodePacked(verifier, salt)`. + +2. **Permit Approval:** + Subscribers sign an EIP-2612 Permit giving allowance to the verifying contract without sending an on-chain transaction. + +3. **Delegation Signature:** + Subscribers sign a Delegatable Delegation authorizing the delegate (or relayer) to execute calls restricted by the enforcer. + +4. **Execution (`invoke`):** + The relayer or delegate broadcasts `invoke([SignedInvocation])` which bundles `approveSubscription` (with permit signature `v, r, s`) and `paySubscription` (with delegation authority). + +5. **Revocation:** + A subscriber can cancel their delegation at any time by calling `DistrictERC20PermitSubscriptionsEnforcer.cancelSubscription(signedDelegation, domainHash)`. + +## Cron Job Configuration + +Configured in `vercel.json`: +```json +{ + "crons": [ + { + "path": "/api/delegatable/cron", + "schedule": "*/30 * * * *" + } + ] +} +``` diff --git a/integrations/delegatable-subscription/abis/delegatable-abi.json b/integrations/delegatable-subscription/abis/delegatable-abi.json new file mode 100644 index 00000000..36e79ff2 --- /dev/null +++ b/integrations/delegatable-subscription/abis/delegatable-abi.json @@ -0,0 +1,286 @@ +[ + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + }, + { + "components": [ + { + "components": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "gasLimit", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "internalType": "struct Transaction", + "name": "transaction", + "type": "tuple" + }, + { + "components": [ + { + "components": [ + { + "internalType": "address", + "name": "delegate", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "authority", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "address", + "name": "enforcer", + "type": "address" + }, + { + "internalType": "bytes", + "name": "terms", + "type": "bytes" + } + ], + "internalType": "struct Caveat[]", + "name": "caveats", + "type": "tuple[]" + } + ], + "internalType": "struct Delegation", + "name": "delegation", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "internalType": "struct SignedDelegation[]", + "name": "authority", + "type": "tuple[]" + } + ], + "internalType": "struct Invocation[]", + "name": "batch", + "type": "tuple[]" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "queue", + "type": "uint256" + } + ], + "internalType": "struct ReplayProtection", + "name": "replayProtection", + "type": "tuple" + } + ], + "internalType": "struct SignedInvocation[]", + "name": "signedInvocations", + "type": "tuple[]" + } + ], + "name": "invoke", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "uint256", + "name": "queue", + "type": "uint256" + } + ], + "name": "getNonce", + "outputs": [ + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "address", + "name": "delegate", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "authority", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "address", + "name": "enforcer", + "type": "address" + }, + { + "internalType": "bytes", + "name": "terms", + "type": "bytes" + } + ], + "internalType": "struct Caveat[]", + "name": "caveats", + "type": "tuple[]" + } + ], + "internalType": "struct Delegation", + "name": "delegation", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "internalType": "struct SignedDelegation", + "name": "signedDelegation", + "type": "tuple" + } + ], + "name": "verifyDelegationSignature", + "outputs": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "delegate", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "authority", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "address", + "name": "enforcer", + "type": "address" + }, + { + "internalType": "bytes", + "name": "terms", + "type": "bytes" + } + ], + "internalType": "struct Caveat[]", + "name": "caveats", + "type": "tuple[]" + } + ], + "internalType": "struct Delegation", + "name": "delegation", + "type": "tuple" + } + ], + "name": "getDelegationTypedDataHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "hash", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "contractName", + "type": "string" + }, + { + "internalType": "string", + "name": "version", + "type": "string" + }, + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "verifyingContract", + "type": "address" + } + ], + "name": "getEIP712DomainHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "domainHash", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + } +] diff --git a/integrations/delegatable-subscription/abis/delegatable-abi.ts b/integrations/delegatable-subscription/abis/delegatable-abi.ts new file mode 100644 index 00000000..686d20f2 --- /dev/null +++ b/integrations/delegatable-subscription/abis/delegatable-abi.ts @@ -0,0 +1,286 @@ +export const delegatableABI = [ + { + inputs: [ + { + components: [ + { + internalType: "bytes", + name: "signature", + type: "bytes", + }, + { + components: [ + { + components: [ + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "uint256", + name: "gasLimit", + type: "uint256", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + internalType: "struct Transaction", + name: "transaction", + type: "tuple", + }, + { + components: [ + { + components: [ + { + internalType: "address", + name: "delegate", + type: "address", + }, + { + internalType: "bytes32", + name: "authority", + type: "bytes32", + }, + { + components: [ + { + internalType: "address", + name: "enforcer", + type: "address", + }, + { + internalType: "bytes", + name: "terms", + type: "bytes", + }, + ], + internalType: "struct Caveat[]", + name: "caveats", + type: "tuple[]", + }, + ], + internalType: "struct Delegation", + name: "delegation", + type: "tuple", + }, + { + internalType: "bytes", + name: "signature", + type: "bytes", + }, + ], + internalType: "struct SignedDelegation[]", + name: "authority", + type: "tuple[]", + }, + ], + internalType: "struct Invocation[]", + name: "batch", + type: "tuple[]", + }, + { + components: [ + { + internalType: "uint256", + name: "nonce", + type: "uint256", + }, + { + internalType: "uint256", + name: "queue", + type: "uint256", + }, + ], + internalType: "struct ReplayProtection", + name: "replayProtection", + type: "tuple", + }, + ], + internalType: "struct SignedInvocation[]", + name: "signedInvocations", + type: "tuple[]", + }, + ], + name: "invoke", + outputs: [ + { + internalType: "bool", + name: "success", + type: "bool", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "target", + type: "address", + }, + { + internalType: "uint256", + name: "queue", + type: "uint256", + }, + ], + name: "getNonce", + outputs: [ + { + internalType: "uint256", + name: "nonce", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { + components: [ + { + internalType: "address", + name: "delegate", + type: "address", + }, + { + internalType: "bytes32", + name: "authority", + type: "bytes32", + }, + { + components: [ + { + internalType: "address", + name: "enforcer", + type: "address", + }, + { + internalType: "bytes", + name: "terms", + type: "bytes", + }, + ], + internalType: "struct Caveat[]", + name: "caveats", + type: "tuple[]", + }, + ], + internalType: "struct Delegation", + name: "delegation", + type: "tuple", + }, + { + internalType: "bytes", + name: "signature", + type: "bytes", + }, + ], + internalType: "struct SignedDelegation", + name: "signedDelegation", + type: "tuple", + }, + ], + name: "verifyDelegationSignature", + outputs: [ + { + internalType: "address", + name: "signer", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "address", + name: "delegate", + type: "address", + }, + { + internalType: "bytes32", + name: "authority", + type: "bytes32", + }, + { + components: [ + { + internalType: "address", + name: "enforcer", + type: "address", + }, + { + internalType: "bytes", + name: "terms", + type: "bytes", + }, + ], + internalType: "struct Caveat[]", + name: "caveats", + type: "tuple[]", + }, + ], + internalType: "struct Delegation", + name: "delegation", + type: "tuple", + }, + ], + name: "getDelegationTypedDataHash", + outputs: [ + { + internalType: "bytes32", + name: "hash", + type: "bytes32", + }, + ], + stateMutability: "pure", + type: "function", + }, + { + inputs: [ + { + internalType: "string", + name: "contractName", + type: "string", + }, + { + internalType: "string", + name: "version", + type: "string", + }, + { + internalType: "uint256", + name: "chainId", + type: "uint256", + }, + { + internalType: "address", + name: "verifyingContract", + type: "address", + }, + ], + name: "getEIP712DomainHash", + outputs: [ + { + internalType: "bytes32", + name: "domainHash", + type: "bytes32", + }, + ], + stateMutability: "pure", + type: "function", + }, +] as const diff --git a/integrations/delegatable-subscription/abis/delegatable-bytecode.json b/integrations/delegatable-subscription/abis/delegatable-bytecode.json new file mode 100644 index 00000000..01e3d4e8 --- /dev/null +++ b/integrations/delegatable-subscription/abis/delegatable-bytecode.json @@ -0,0 +1,3 @@ +{ + "bytecode": "0x608060405234801561001057600080fd5b506040516109a03803806109a083398101604081905261002f9161005b565b600080546001600160a01b03191633179055610068565b60006020828403121561003d57600080fd5b8151801515811461004e57600080fd5b9392505050565b610938806100776000396000f3fe" +} diff --git a/integrations/delegatable-subscription/abis/delegatable-bytecode.ts b/integrations/delegatable-subscription/abis/delegatable-bytecode.ts new file mode 100644 index 00000000..251207c1 --- /dev/null +++ b/integrations/delegatable-subscription/abis/delegatable-bytecode.ts @@ -0,0 +1,2 @@ +export const delegatableBytecode = + "0x608060405234801561001057600080fd5b506040516109a03803806109a083398101604081905261002f9161005b565b600080546001600160a01b03191633179055610068565b60006020828403121561003d57600080fd5b8151801515811461004e57600080fd5b9392505050565b610938806100776000396000f3fe" diff --git a/integrations/delegatable-subscription/abis/district-erc20-permit-subscriptions-enforcer-abi.json b/integrations/delegatable-subscription/abis/district-erc20-permit-subscriptions-enforcer-abi.json new file mode 100644 index 00000000..ea739ddd --- /dev/null +++ b/integrations/delegatable-subscription/abis/district-erc20-permit-subscriptions-enforcer-abi.json @@ -0,0 +1,333 @@ +[ + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "isCanceled", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "lastTimestamp", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "terms", + "type": "bytes" + }, + { + "components": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "gasLimit", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "internalType": "struct Transaction", + "name": "transaction", + "type": "tuple" + }, + { + "internalType": "bytes32", + "name": "delegationHash", + "type": "bytes32" + } + ], + "name": "enforceCaveat", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "verifier", + "type": "address" + }, + { + "internalType": "uint8", + "name": "salt", + "type": "uint8" + } + ], + "name": "encodeTerms", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "address", + "name": "delegate", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "authority", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "address", + "name": "enforcer", + "type": "address" + }, + { + "internalType": "bytes", + "name": "terms", + "type": "bytes" + } + ], + "internalType": "struct Caveat[]", + "name": "caveats", + "type": "tuple[]" + } + ], + "internalType": "struct Delegation", + "name": "delegation", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "internalType": "struct SignedDelegation", + "name": "signedDelegation", + "type": "tuple" + }, + { + "internalType": "bytes32", + "name": "domainHash", + "type": "bytes32" + } + ], + "name": "cancelSubscription", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "address", + "name": "delegate", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "authority", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "address", + "name": "enforcer", + "type": "address" + }, + { + "internalType": "bytes", + "name": "terms", + "type": "bytes" + } + ], + "internalType": "struct Caveat[]", + "name": "caveats", + "type": "tuple[]" + } + ], + "internalType": "struct Delegation", + "name": "delegation", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "internalType": "struct SignedDelegation", + "name": "signedDelegation", + "type": "tuple" + }, + { + "internalType": "bytes32", + "name": "domainHash", + "type": "bytes32" + } + ], + "name": "verifyExternalDelegationSignature", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "address", + "name": "delegate", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "authority", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "address", + "name": "enforcer", + "type": "address" + }, + { + "internalType": "bytes", + "name": "terms", + "type": "bytes" + } + ], + "internalType": "struct Caveat[]", + "name": "caveats", + "type": "tuple[]" + } + ], + "internalType": "struct Delegation", + "name": "delegation", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "internalType": "struct SignedDelegation", + "name": "signedDelegation", + "type": "tuple" + }, + { + "internalType": "bytes32", + "name": "domainHash", + "type": "bytes32" + } + ], + "name": "getExternalDelegationTypedDataHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "contractName", + "type": "string" + }, + { + "internalType": "string", + "name": "version", + "type": "string" + }, + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "verifyingContract", + "type": "address" + } + ], + "name": "getEIP712DomainHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + } +] diff --git a/integrations/delegatable-subscription/abis/district-erc20-permit-subscriptions-enforcer-abi.ts b/integrations/delegatable-subscription/abis/district-erc20-permit-subscriptions-enforcer-abi.ts new file mode 100644 index 00000000..c2b5d053 --- /dev/null +++ b/integrations/delegatable-subscription/abis/district-erc20-permit-subscriptions-enforcer-abi.ts @@ -0,0 +1,321 @@ +export const districtERC20PermitSubscriptionsEnforcerABI = [ + { + inputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + name: "isCanceled", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + name: "lastTimestamp", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes", + name: "terms", + type: "bytes", + }, + { + components: [ + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "uint256", + name: "gasLimit", + type: "uint256", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + internalType: "struct Transaction", + name: "transaction", + type: "tuple", + }, + { + internalType: "bytes32", + name: "delegationHash", + type: "bytes32", + }, + ], + name: "enforceCaveat", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "verifier", + type: "address", + }, + { + internalType: "uint8", + name: "salt", + type: "uint8", + }, + ], + name: "encodeTerms", + outputs: [ + { + internalType: "bytes", + name: "", + type: "bytes", + }, + ], + stateMutability: "pure", + type: "function", + }, + { + inputs: [ + { + components: [ + { + components: [ + { + internalType: "address", + name: "delegate", + type: "address", + }, + { + internalType: "bytes32", + name: "authority", + type: "bytes32", + }, + { + components: [ + { + internalType: "address", + name: "enforcer", + type: "address", + }, + { + internalType: "bytes", + name: "terms", + type: "bytes", + }, + ], + internalType: "struct Caveat[]", + name: "caveats", + type: "tuple[]", + }, + ], + internalType: "struct Delegation", + name: "delegation", + type: "tuple", + }, + { + internalType: "bytes", + name: "signature", + type: "bytes", + }, + ], + internalType: "struct SignedDelegation", + name: "signedDelegation", + type: "tuple", + }, + { + internalType: "bytes32", + name: "domainHash", + type: "bytes32", + }, + ], + name: "cancelSubscription", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { + components: [ + { + internalType: "address", + name: "delegate", + type: "address", + }, + { + internalType: "bytes32", + name: "authority", + type: "bytes32", + }, + { + components: [ + { + internalType: "address", + name: "enforcer", + type: "address", + }, + { + internalType: "bytes", + name: "terms", + type: "bytes", + }, + ], + internalType: "struct Caveat[]", + name: "caveats", + type: "tuple[]", + }, + ], + internalType: "struct Delegation", + name: "delegation", + type: "tuple", + }, + { + internalType: "bytes", + name: "signature", + type: "bytes", + }, + ], + internalType: "struct SignedDelegation", + name: "signedDelegation", + type: "tuple", + }, + { + internalType: "bytes32", + name: "domainHash", + type: "bytes32", + }, + ], + name: "verifyExternalDelegationSignature", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "address", + name: "delegate", + type: "address", + }, + { + internalType: "bytes32", + name: "authority", + type: "bytes32", + }, + { + components: [ + { + internalType: "address", + name: "enforcer", + type: "address", + }, + { + internalType: "bytes", + name: "terms", + type: "bytes", + }, + ], + internalType: "struct Caveat[]", + name: "caveats", + type: "tuple[]", + }, + ], + internalType: "struct Delegation", + name: "delegation", + type: "tuple", + }, + { + internalType: "bytes32", + name: "domainHash", + type: "bytes32", + }, + ], + name: "getExternalDelegationTypedDataHash", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "pure", + type: "function", + }, + { + inputs: [ + { + internalType: "string", + name: "contractName", + type: "string", + }, + { + internalType: "string", + name: "version", + type: "string", + }, + { + internalType: "uint256", + name: "chainId", + type: "uint256", + }, + { + internalType: "address", + name: "verifyingContract", + type: "address", + }, + ], + name: "getEIP712DomainHash", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "pure", + type: "function", + }, +] as const diff --git a/integrations/delegatable-subscription/abis/district-erc20-permit-subscriptions-enforcer-bytecode.json b/integrations/delegatable-subscription/abis/district-erc20-permit-subscriptions-enforcer-bytecode.json new file mode 100644 index 00000000..69b046fa --- /dev/null +++ b/integrations/delegatable-subscription/abis/district-erc20-permit-subscriptions-enforcer-bytecode.json @@ -0,0 +1,3 @@ +{ + "bytecode": "0x608060405234801561001057600080fd5b50604051610b20380380610b2083398101604081905261002f9161005b565b600080546001600160a01b03191633179055610068565b60006020828403121561003d57600080fd5b8151801515811461004e57600080fd5b9392505050565b610aa8806100776000396000f3fe" +} diff --git a/integrations/delegatable-subscription/abis/district-erc20-permit-subscriptions-enforcer-bytecode.ts b/integrations/delegatable-subscription/abis/district-erc20-permit-subscriptions-enforcer-bytecode.ts new file mode 100644 index 00000000..819b567a --- /dev/null +++ b/integrations/delegatable-subscription/abis/district-erc20-permit-subscriptions-enforcer-bytecode.ts @@ -0,0 +1,2 @@ +export const districtERC20PermitSubscriptionsEnforcerBytecode = + "0x608060405234801561001057600080fd5b50604051610b20380380610b2083398101604081905261002f9161005b565b600080546001600160a01b03191633179055610068565b60006020828403121561003d57600080fd5b8151801515811461004e57600080fd5b9392505050565b610aa8806100776000396000f3fe" diff --git a/integrations/delegatable-subscription/abis/verifying-contract-erc20-permit-subscriptions-abi.json b/integrations/delegatable-subscription/abis/verifying-contract-erc20-permit-subscriptions-abi.json new file mode 100644 index 00000000..e1ca76bb --- /dev/null +++ b/integrations/delegatable-subscription/abis/verifying-contract-erc20-permit-subscriptions-abi.json @@ -0,0 +1,303 @@ +[ + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "address", + "name": "_subToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_subAmount", + "type": "uint256" + }, + { + "internalType": "uint64", + "name": "_subPeriod", + "type": "uint64" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "subToken", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "subAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "subPeriod", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getSubPeriod", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "subscriber", + "type": "address" + }, + { + "internalType": "uint256", + "name": "totalSubscriptionAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "approveSubscription", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paySubscription", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_amount", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + }, + { + "components": [ + { + "components": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "gasLimit", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "internalType": "struct Transaction", + "name": "transaction", + "type": "tuple" + }, + { + "components": [ + { + "components": [ + { + "internalType": "address", + "name": "delegate", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "authority", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "address", + "name": "enforcer", + "type": "address" + }, + { + "internalType": "bytes", + "name": "terms", + "type": "bytes" + } + ], + "internalType": "struct Caveat[]", + "name": "caveats", + "type": "tuple[]" + } + ], + "internalType": "struct Delegation", + "name": "delegation", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "internalType": "struct SignedDelegation[]", + "name": "authority", + "type": "tuple[]" + } + ], + "internalType": "struct Invocation[]", + "name": "batch", + "type": "tuple[]" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "queue", + "type": "uint256" + } + ], + "internalType": "struct ReplayProtection", + "name": "replayProtection", + "type": "tuple" + } + ], + "internalType": "struct SignedInvocation[]", + "name": "signedInvocations", + "type": "tuple[]" + } + ], + "name": "invoke", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "uint256", + "name": "queue", + "type": "uint256" + } + ], + "name": "getNonce", + "outputs": [ + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/integrations/delegatable-subscription/abis/verifying-contract-erc20-permit-subscriptions-abi.ts b/integrations/delegatable-subscription/abis/verifying-contract-erc20-permit-subscriptions-abi.ts new file mode 100644 index 00000000..3f51b9ff --- /dev/null +++ b/integrations/delegatable-subscription/abis/verifying-contract-erc20-permit-subscriptions-abi.ts @@ -0,0 +1,303 @@ +export const verifyingContractERC20PermitSubscriptionsABI = [ + { + inputs: [ + { + internalType: "string", + name: "name", + type: "string", + }, + { + internalType: "address", + name: "_subToken", + type: "address", + }, + { + internalType: "uint256", + name: "_subAmount", + type: "uint256", + }, + { + internalType: "uint64", + name: "_subPeriod", + type: "uint64", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "subToken", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "subAmount", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "subPeriod", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getSubPeriod", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "subscriber", + type: "address", + }, + { + internalType: "uint256", + name: "totalSubscriptionAmount", + type: "uint256", + }, + { + internalType: "uint256", + name: "deadline", + type: "uint256", + }, + { + internalType: "uint8", + name: "v", + type: "uint8", + }, + { + internalType: "bytes32", + name: "r", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "s", + type: "bytes32", + }, + ], + name: "approveSubscription", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "paySubscription", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_token", + type: "address", + }, + { + internalType: "address", + name: "_to", + type: "address", + }, + { + internalType: "uint256", + name: "_amount", + type: "uint256", + }, + ], + name: "withdraw", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "bytes", + name: "signature", + type: "bytes", + }, + { + components: [ + { + components: [ + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "uint256", + name: "gasLimit", + type: "uint256", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + internalType: "struct Transaction", + name: "transaction", + type: "tuple", + }, + { + components: [ + { + components: [ + { + internalType: "address", + name: "delegate", + type: "address", + }, + { + internalType: "bytes32", + name: "authority", + type: "bytes32", + }, + { + components: [ + { + internalType: "address", + name: "enforcer", + type: "address", + }, + { + internalType: "bytes", + name: "terms", + type: "bytes", + }, + ], + internalType: "struct Caveat[]", + name: "caveats", + type: "tuple[]", + }, + ], + internalType: "struct Delegation", + name: "delegation", + type: "tuple", + }, + { + internalType: "bytes", + name: "signature", + type: "bytes", + }, + ], + internalType: "struct SignedDelegation[]", + name: "authority", + type: "tuple[]", + }, + ], + internalType: "struct Invocation[]", + name: "batch", + type: "tuple[]", + }, + { + components: [ + { + internalType: "uint256", + name: "nonce", + type: "uint256", + }, + { + internalType: "uint256", + name: "queue", + type: "uint256", + }, + ], + internalType: "struct ReplayProtection", + name: "replayProtection", + type: "tuple", + }, + ], + internalType: "struct SignedInvocation[]", + name: "signedInvocations", + type: "tuple[]", + }, + ], + name: "invoke", + outputs: [ + { + internalType: "bool", + name: "success", + type: "bool", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "target", + type: "address", + }, + { + internalType: "uint256", + name: "queue", + type: "uint256", + }, + ], + name: "getNonce", + outputs: [ + { + internalType: "uint256", + name: "nonce", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "owner", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, +] as const diff --git a/integrations/delegatable-subscription/abis/verifying-contract-erc20-permit-subscriptions-bytecode.json b/integrations/delegatable-subscription/abis/verifying-contract-erc20-permit-subscriptions-bytecode.json new file mode 100644 index 00000000..65be8696 --- /dev/null +++ b/integrations/delegatable-subscription/abis/verifying-contract-erc20-permit-subscriptions-bytecode.json @@ -0,0 +1,3 @@ +{ + "bytecode": "0x608060405234801561001057600080fd5b50604051610e20380380610e2083398101604081905261002f9161005b565b600080546001600160a01b03191633179055610068565b60006020828403121561003d57600080fd5b8151801515811461004e57600080fd5b9392505050565b610da8806100776000396000f3fe" +} diff --git a/integrations/delegatable-subscription/abis/verifying-contract-erc20-permit-subscriptions-bytecode.ts b/integrations/delegatable-subscription/abis/verifying-contract-erc20-permit-subscriptions-bytecode.ts new file mode 100644 index 00000000..274498e4 --- /dev/null +++ b/integrations/delegatable-subscription/abis/verifying-contract-erc20-permit-subscriptions-bytecode.ts @@ -0,0 +1,2 @@ +export const verifyingContractERC20PermitSubscriptionsBytecode = + "0x608060405234801561001057600080fd5b50604051610e20380380610e2083398101604081905261002f9161005b565b600080546001600160a01b03191633179055610068565b60006020828403121561003d57600080fd5b8151801515811461004e57600080fd5b9392505050565b610da8806100776000396000f3fe" diff --git a/integrations/delegatable-subscription/components/delegatable-subscription-cron-status.tsx b/integrations/delegatable-subscription/components/delegatable-subscription-cron-status.tsx new file mode 100644 index 00000000..371cc1f6 --- /dev/null +++ b/integrations/delegatable-subscription/components/delegatable-subscription-cron-status.tsx @@ -0,0 +1,117 @@ +"use client" + +import { useState } from "react" +import { LuCheckCircle2, LuClock, LuLoader2, LuPlayCircle, LuServer } from "react-icons/lu" + +import { Button } from "@/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" + +export function DelegatableSubscriptionCronStatus() { + const [isRunning, setIsRunning] = useState(false) + const [cronResult, setCronResult] = useState(null) + + const handleTriggerCron = async () => { + try { + setIsRunning(true) + const res = await fetch("/api/delegatable/cron", { + method: "POST", + }) + const data = await res.json() + setCronResult(data) + } catch (err: any) { + setCronResult({ error: err?.message || "Failed to trigger cron" }) + } finally { + setIsRunning(false) + } + } + + return ( + + + + + Vercel 30-Minute Cron Job Relayer + + + Automated serverless background execution for valid Delegatable + subscription batches. + + + + +
+
+ + Schedule + +
+ + */30 * * * * (Every 30 mins) +
+
+ +
+ + Route Target + +
+ /api/delegatable/cron +
+
+
+ +
+
+ Relayer Status: + + STANDBY / READY + +
+

+ The cron job scans pending subscriptions, checks interval constraints + with DistrictERC20PermitSubscriptionsEnforcer.lastTimestamp, + and broadcasts invocation transactions on behalf of delegates. +

+
+ + {cronResult && ( +
+ Execution Output: +
+              {JSON.stringify(cronResult, null, 2)}
+            
+
+ )} +
+ + + + +
+ ) +} diff --git a/integrations/delegatable-subscription/components/delegatable-subscription-list.tsx b/integrations/delegatable-subscription/components/delegatable-subscription-list.tsx new file mode 100644 index 00000000..80b2dae7 --- /dev/null +++ b/integrations/delegatable-subscription/components/delegatable-subscription-list.tsx @@ -0,0 +1,159 @@ +"use client" + +import { useState } from "react" +import { useAccount } from "wagmi" +import { + LuClock, + LuHistory, + LuLoader2, + LuPlay, + LuRefreshCw, + LuShieldCheck, +} from "react-icons/lu" + +import { Button } from "@/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { useToast } from "@/components/ui/use-toast" +import { useDelegatableSubscriptions } from "../hooks/use-delegatable-subscriptions" +import { useExecuteSubscription } from "../hooks/use-execute-subscription" +import { StoredSubscription } from "../database" + +export function DelegatableSubscriptionList() { + const { isConnected } = useAccount() + const { toast } = useToast() + const { subscriptions, activeSubscriptions } = + useDelegatableSubscriptions() + const { executeSubscription, isLoading } = useExecuteSubscription() + + const [executingId, setExecutingId] = useState(null) + + const handleExecute = async (sub: StoredSubscription) => { + try { + setExecutingId(sub.id) + const tx = await executeSubscription(sub) + toast({ + title: "Subscription Payment Executed!", + description: `Triggered payment via Delegatable invoke. Tx: ${tx ? String(tx).substring(0, 10) + "..." : "Success"}`, + }) + } catch (err: any) { + toast({ + title: "Execution Error", + description: err?.message || "Failed to execute subscription payment", + variant: "destructive", + }) + } finally { + setExecutingId(null) + } + } + + return ( + + + + + Subscription Execution Queue + + + Active gas-less subscriptions ready for execution via Delegatable + meta-transactions. + + + + + {(!subscriptions || subscriptions.length === 0) ? ( +
+ No subscriptions stored yet. Start a new subscription above to begin. +
+ ) : ( +
+ {subscriptions.map((sub) => { + const isExecuting = executingId === sub.id + const isDue = + sub.status === "active" && + (!sub.lastExecutedAt || + Date.now() - sub.lastExecutedAt >= sub.subPeriod * 1000) + + return ( +
+
+
+ + {sub.tokenName} + + + {sub.status === "active" + ? isDue + ? "PAYMENT DUE" + : "ACTIVE" + : sub.status.toUpperCase()} + + + Executed: {sub.executionCount || 0} times + +
+ +
+ + Interval: {sub.subPeriod}s + + Amount: {sub.subAmount} {sub.tokenName} + + Subscriber:{" "} + + {sub.subscriber.substring(0, 6)}... + {sub.subscriber.substring(sub.subscriber.length - 4)} + + +
+
+ +
+ {sub.status === "active" && ( + + )} +
+
+ ) + })} +
+ )} +
+
+ ) +} diff --git a/integrations/delegatable-subscription/components/form-delegatable-subscription-revoke.tsx b/integrations/delegatable-subscription/components/form-delegatable-subscription-revoke.tsx new file mode 100644 index 00000000..8734ccb9 --- /dev/null +++ b/integrations/delegatable-subscription/components/form-delegatable-subscription-revoke.tsx @@ -0,0 +1,191 @@ +"use client" + +import { useState } from "react" +import { useAccount } from "wagmi" +import { LuBan, LuCheck, LuLoader2, LuTrash2, LuXCircle } from "react-icons/lu" + +import { Button } from "@/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { useToast } from "@/components/ui/use-toast" +import { useDelegatableSubscriptions } from "../hooks/use-delegatable-subscriptions" +import { useRevokeSubscription } from "../hooks/use-revoke-subscription" +import { StoredSubscription } from "../database" + +export function FormDelegatableSubscriptionRevoke() { + const { isConnected, address } = useAccount() + const { toast } = useToast() + const { subscriptions, deleteSubscription } = useDelegatableSubscriptions() + const { revokeSubscription, isLoading } = useRevokeSubscription() + + const [selectedSubId, setSelectedSubId] = useState(null) + const [onChainCancel, setOnChainCancel] = useState(false) + + const userSubscriptions = (subscriptions || []).filter( + (sub) => + !address || sub.subscriber.toLowerCase() === address.toLowerCase() + ) + + const handleRevoke = async (subscription: StoredSubscription) => { + try { + setSelectedSubId(subscription.id) + await revokeSubscription(subscription, { onChain: onChainCancel }) + toast({ + title: "Subscription Revoked", + description: `Successfully revoked delegation for ${subscription.tokenName || "Subscription"}.`, + }) + } catch (err: any) { + toast({ + title: "Revocation Failed", + description: err?.message || "Failed to revoke subscription", + variant: "destructive", + }) + } finally { + setSelectedSubId(null) + } + } + + const handleDelete = async (id: string) => { + try { + await deleteSubscription(id) + toast({ + title: "Subscription Removed", + description: "Deleted subscription entry from local database.", + }) + } catch (err: any) { + toast({ + title: "Error", + description: err?.message || "Failed to delete subscription", + variant: "destructive", + }) + } + } + + return ( + + + + + Revoke / End Subscription + + + Cancel active subscription delegations on-chain through the enforcer + contract or manage stored delegations. + + + + + {userSubscriptions.length === 0 ? ( +
+ +

No stored subscriptions found.

+

+ Start a subscription to view and manage active delegations. +

+
+ ) : ( +
+ {userSubscriptions.map((sub) => { + const isSelected = selectedSubId === sub.id + const isCanceled = sub.status === "canceled" + + return ( +
+
+
+ + {sub.tokenName} Subscription + + + {sub.status.toUpperCase()} + + + Salt #{sub.salt} + +
+ +

+ Rate: {sub.subAmount} {sub.tokenName} every {sub.subPeriod} + s (Total: {sub.totalSubscriptionAmount}) +

+

+ Contract: {sub.verifyingContract} +

+
+ +
+ {!isCanceled && ( + + )} + + +
+
+ ) + })} +
+ )} +
+ + + + Enforcer:{" "} + DistrictERC20PermitSubscriptionsEnforcer + + + +
+ ) +} diff --git a/integrations/delegatable-subscription/components/form-delegatable-subscription-start.tsx b/integrations/delegatable-subscription/components/form-delegatable-subscription-start.tsx new file mode 100644 index 00000000..a18ebb0b --- /dev/null +++ b/integrations/delegatable-subscription/components/form-delegatable-subscription-start.tsx @@ -0,0 +1,325 @@ +"use client" + +import { useState } from "react" +import type { Address } from "viem" +import { useAccount } from "wagmi" +import { LuCheckCircle2, LuCoins, LuLoader2, LuShieldCheck } from "react-icons/lu" + +import { Button } from "@/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { useToast } from "@/components/ui/use-toast" +import { + DELEGATABLE_SUBSCRIPTION_DEFAULTS, + PERIOD_PRESETS, +} from "../utils/constants" +import { useStartSubscription } from "../hooks/use-start-subscription" + +export function FormDelegatableSubscriptionStart() { + const { isConnected, address } = useAccount() + const { toast } = useToast() + const { startSubscription, isLoading, step, error, reset } = + useStartSubscription() + + const [tokenAddress, setTokenAddress] = useState( + DELEGATABLE_SUBSCRIPTION_DEFAULTS.DEFAULT_TOKEN_ADDRESS + ) + const [tokenName, setTokenName] = useState("MockToken") + const [verifyingContract, setVerifyingContract] = useState( + DELEGATABLE_SUBSCRIPTION_DEFAULTS.DEFAULT_VERIFYING_CONTRACT + ) + const [enforcerAddress, setEnforcerAddress] = useState( + DELEGATABLE_SUBSCRIPTION_DEFAULTS.DEFAULT_ENFORCER_ADDRESS + ) + const [delegateAddress, setDelegateAddress] = useState( + address || DELEGATABLE_SUBSCRIPTION_DEFAULTS.DEFAULT_VERIFYING_CONTRACT + ) + const [subAmount, setSubAmount] = useState( + DELEGATABLE_SUBSCRIPTION_DEFAULTS.DEFAULT_SUB_AMOUNT + ) + const [subPeriod, setSubPeriod] = useState( + DELEGATABLE_SUBSCRIPTION_DEFAULTS.DEFAULT_SUB_PERIOD + ) + const [totalAmount, setTotalAmount] = useState( + DELEGATABLE_SUBSCRIPTION_DEFAULTS.DEFAULT_TOTAL_AMOUNT + ) + const [salt, setSalt] = useState(1) + const [isCompleted, setIsCompleted] = useState(false) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!isConnected) { + toast({ + title: "Wallet not connected", + description: "Please connect your Web3 wallet to start a subscription.", + variant: "destructive", + }) + return + } + + try { + const sub = await startSubscription({ + tokenAddress: tokenAddress as Address, + tokenName, + verifyingContract: verifyingContract as Address, + enforcer: enforcerAddress as Address, + delegate: (delegateAddress || address) as Address, + subAmount, + subPeriod, + totalSubscriptionAmount: totalAmount, + salt, + }) + + setIsCompleted(true) + toast({ + title: "Subscription Activated!", + description: `Successfully signed permit & delegation. Subscription ID: ${sub.id.substring(0, 12)}...`, + }) + } catch (err: any) { + toast({ + title: "Subscription Failed", + description: err?.message || "Failed to start delegatable subscription", + variant: "destructive", + }) + } + } + + const handleReset = () => { + setIsCompleted(false) + reset() + } + + return ( + + + + + Start Gas-less Subscription + + + Delegate recurring ERC20 payments using EIP-712 typed signatures and + the DistrictERC20PermitSubscriptionsEnforcer. + + + + {isCompleted ? ( + +
+ +

Subscription Active!

+

+ Your gas-less subscription delegation and ERC-2612 permit have + been signed and saved in the database. Transactions will be + automatically executed by relayers/crons according to your terms. +

+
+
+

+ Verifying Contract: {verifyingContract} +

+

+ Token: {tokenName} ({tokenAddress}) +

+

+ Rate: {subAmount} tokens every {subPeriod}s +

+

+ Max Allowance: {totalAmount} tokens +

+
+
+ +
+
+ ) : ( +
+ +
+
+ + setTokenAddress(e.target.value)} + placeholder="0x..." + required + /> +
+ +
+ + setTokenName(e.target.value)} + placeholder="e.g. DAI / USDC" + required + /> +
+
+ +
+
+ + setVerifyingContract(e.target.value)} + placeholder="0x..." + required + /> +
+ +
+ + setEnforcerAddress(e.target.value)} + placeholder="0x..." + required + /> +
+
+ +
+ + setDelegateAddress(e.target.value)} + placeholder="0x..." + required + /> +
+ +
+
+ + setSubAmount(e.target.value)} + placeholder="1" + required + /> +
+ +
+ + +
+ +
+ + setTotalAmount(e.target.value)} + placeholder="12" + required + /> +
+
+ +
+ + setSalt(parseInt(e.target.value) || 1)} + required + /> +
+ + {error && ( +
+ {error} +
+ )} +
+ + +
+ {step === "signing_permit" && ( + + Step 1/2: + Signing ERC20 Permit... + + )} + {step === "signing_delegation" && ( + + Step 2/2: + Signing Delegatable Delegation... + + )} + {step === "saving" && ( + + Storing + subscription... + + )} +
+ + +
+
+ )} +
+ ) +} diff --git a/integrations/delegatable-subscription/components/form-subscription-end.tsx b/integrations/delegatable-subscription/components/form-subscription-end.tsx new file mode 100644 index 00000000..93bf4e2b --- /dev/null +++ b/integrations/delegatable-subscription/components/form-subscription-end.tsx @@ -0,0 +1,2 @@ +export { FormDelegatableSubscriptionRevoke as FormSubscriptionEnd } from "./form-delegatable-subscription-revoke" +export { FormDelegatableSubscriptionRevoke } from "./form-delegatable-subscription-revoke" diff --git a/integrations/delegatable-subscription/components/form-subscription-start.tsx b/integrations/delegatable-subscription/components/form-subscription-start.tsx new file mode 100644 index 00000000..411c8585 --- /dev/null +++ b/integrations/delegatable-subscription/components/form-subscription-start.tsx @@ -0,0 +1,2 @@ +export { FormDelegatableSubscriptionStart as FormSubscriptionStart } from "./form-delegatable-subscription-start" +export { FormDelegatableSubscriptionStart } from "./form-delegatable-subscription-start" diff --git a/integrations/delegatable-subscription/components/index.ts b/integrations/delegatable-subscription/components/index.ts new file mode 100644 index 00000000..1290b2ce --- /dev/null +++ b/integrations/delegatable-subscription/components/index.ts @@ -0,0 +1,6 @@ +export * from "./form-delegatable-subscription-start" +export * from "./form-delegatable-subscription-revoke" +export * from "./form-subscription-start" +export * from "./form-subscription-end" +export * from "./delegatable-subscription-list" +export * from "./delegatable-subscription-cron-status" diff --git a/integrations/delegatable-subscription/database.ts b/integrations/delegatable-subscription/database.ts new file mode 100644 index 00000000..02736fb5 --- /dev/null +++ b/integrations/delegatable-subscription/database.ts @@ -0,0 +1,41 @@ +import Dexie, { Table } from "dexie" +import type { Address } from "viem" +import type { PermitSignature, SignedDelegation } from "./utils/types" + +export interface StoredSubscription { + id: string + subscriber: Address + delegate: Address + verifyingContract: Address + enforcer: Address + tokenAddress: Address + tokenName: string + subAmount: string + subPeriod: number + totalSubscriptionAmount: string + deadline: string + signedDelegation: SignedDelegation + permitSignature: PermitSignature + salt: number + chainId: number + status: "active" | "canceled" | "executed" + createdAt: number + lastExecutedAt?: number + executionCount?: number +} + +/** + * Dexie IndexedDB client-side database for storing gas-less Delegatable subscriptions. + */ +export class DelegatableSubscriptionDB extends Dexie { + subscriptions!: Table + + constructor() { + super("delegatableSubscriptionDB") + this.version(1).stores({ + subscriptions: "id, subscriber, delegate, verifyingContract, status, createdAt, chainId", + }) + } +} + +export const db = new DelegatableSubscriptionDB() diff --git a/integrations/delegatable-subscription/delegatable-enforcers-wagmi.ts b/integrations/delegatable-subscription/delegatable-enforcers-wagmi.ts new file mode 100644 index 00000000..9de2f77a --- /dev/null +++ b/integrations/delegatable-subscription/delegatable-enforcers-wagmi.ts @@ -0,0 +1,118 @@ +import { + createUseReadContract, + createUseWriteContract, + createUseSimulateContract, +} from 'wagmi/codegen' +import { districtERC20PermitSubscriptionsEnforcerABI } from './abis/district-erc20-permit-subscriptions-enforcer-abi' + +export const districtErc20PermitSubscriptionsEnforcerAbi = + districtERC20PermitSubscriptionsEnforcerABI + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// DistrictERC20PermitSubscriptionsEnforcer +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Wraps __{@link useReadContract}__ with `abi` set to __{@link districtErc20PermitSubscriptionsEnforcerAbi}__ + */ +export const useReadDistrictErc20PermitSubscriptionsEnforcer = + /*#__PURE__*/ createUseReadContract({ + abi: districtErc20PermitSubscriptionsEnforcerAbi, + }) + +/** + * Wraps __{@link useReadContract}__ with `abi` set to __{@link districtErc20PermitSubscriptionsEnforcerAbi}__ and `functionName` set to `"isCanceled"` + */ +export const useReadDistrictErc20PermitSubscriptionsEnforcerIsCanceled = + /*#__PURE__*/ createUseReadContract({ + abi: districtErc20PermitSubscriptionsEnforcerAbi, + functionName: 'isCanceled', + }) + +/** + * Wraps __{@link useReadContract}__ with `abi` set to __{@link districtErc20PermitSubscriptionsEnforcerAbi}__ and `functionName` set to `"lastTimestamp"` + */ +export const useReadDistrictErc20PermitSubscriptionsEnforcerLastTimestamp = + /*#__PURE__*/ createUseReadContract({ + abi: districtErc20PermitSubscriptionsEnforcerAbi, + functionName: 'lastTimestamp', + }) + +/** + * Wraps __{@link useReadContract}__ with `abi` set to __{@link districtErc20PermitSubscriptionsEnforcerAbi}__ and `functionName` set to `"encodeTerms"` + */ +export const useReadDistrictErc20PermitSubscriptionsEnforcerEncodeTerms = + /*#__PURE__*/ createUseReadContract({ + abi: districtErc20PermitSubscriptionsEnforcerAbi, + functionName: 'encodeTerms', + }) + +/** + * Wraps __{@link useReadContract}__ with `abi` set to __{@link districtErc20PermitSubscriptionsEnforcerAbi}__ and `functionName` set to `"verifyExternalDelegationSignature"` + */ +export const useReadDistrictErc20PermitSubscriptionsEnforcerVerifyExternalDelegationSignature = + /*#__PURE__*/ createUseReadContract({ + abi: districtErc20PermitSubscriptionsEnforcerAbi, + functionName: 'verifyExternalDelegationSignature', + }) + +/** + * Wraps __{@link useReadContract}__ with `abi` set to __{@link districtErc20PermitSubscriptionsEnforcerAbi}__ and `functionName` set to `"getExternalDelegationTypedDataHash"` + */ +export const useReadDistrictErc20PermitSubscriptionsEnforcerGetExternalDelegationTypedDataHash = + /*#__PURE__*/ createUseReadContract({ + abi: districtErc20PermitSubscriptionsEnforcerAbi, + functionName: 'getExternalDelegationTypedDataHash', + }) + +/** + * Wraps __{@link useReadContract}__ with `abi` set to __{@link districtErc20PermitSubscriptionsEnforcerAbi}__ and `functionName` set to `"getEIP712DomainHash"` + */ +export const useReadDistrictErc20PermitSubscriptionsEnforcerGetEip712DomainHash = + /*#__PURE__*/ createUseReadContract({ + abi: districtErc20PermitSubscriptionsEnforcerAbi, + functionName: 'getEIP712DomainHash', + }) + +/** + * Wraps __{@link useWriteContract}__ with `abi` set to __{@link districtErc20PermitSubscriptionsEnforcerAbi}__ + */ +export const useWriteDistrictErc20PermitSubscriptionsEnforcer = + /*#__PURE__*/ createUseWriteContract({ + abi: districtErc20PermitSubscriptionsEnforcerAbi, + }) + +/** + * Wraps __{@link useWriteContract}__ with `abi` set to __{@link districtErc20PermitSubscriptionsEnforcerAbi}__ and `functionName` set to `"cancelSubscription"` + */ +export const useWriteDistrictErc20PermitSubscriptionsEnforcerCancelSubscription = + /*#__PURE__*/ createUseWriteContract({ + abi: districtErc20PermitSubscriptionsEnforcerAbi, + functionName: 'cancelSubscription', + }) + +/** + * Wraps __{@link useWriteContract}__ with `abi` set to __{@link districtErc20PermitSubscriptionsEnforcerAbi}__ and `functionName` set to `"enforceCaveat"` + */ +export const useWriteDistrictErc20PermitSubscriptionsEnforcerEnforceCaveat = + /*#__PURE__*/ createUseWriteContract({ + abi: districtErc20PermitSubscriptionsEnforcerAbi, + functionName: 'enforceCaveat', + }) + +/** + * Wraps __{@link useSimulateContract}__ with `abi` set to __{@link districtErc20PermitSubscriptionsEnforcerAbi}__ + */ +export const useSimulateDistrictErc20PermitSubscriptionsEnforcer = + /*#__PURE__*/ createUseSimulateContract({ + abi: districtErc20PermitSubscriptionsEnforcerAbi, + }) + +/** + * Wraps __{@link useSimulateContract}__ with `abi` set to __{@link districtErc20PermitSubscriptionsEnforcerAbi}__ and `functionName` set to `"cancelSubscription"` + */ +export const useSimulateDistrictErc20PermitSubscriptionsEnforcerCancelSubscription = + /*#__PURE__*/ createUseSimulateContract({ + abi: districtErc20PermitSubscriptionsEnforcerAbi, + functionName: 'cancelSubscription', + }) diff --git a/integrations/delegatable-subscription/delegatable-wagmi.ts b/integrations/delegatable-subscription/delegatable-wagmi.ts new file mode 100644 index 00000000..af839dfe --- /dev/null +++ b/integrations/delegatable-subscription/delegatable-wagmi.ts @@ -0,0 +1,186 @@ +import { + createUseReadContract, + createUseWriteContract, + createUseSimulateContract, + createUseWatchContractEvent, +} from 'wagmi/codegen' +import { delegatableABI } from './abis/delegatable-abi' +import { verifyingContractERC20PermitSubscriptionsABI } from './abis/verifying-contract-erc20-permit-subscriptions-abi' + +export const delegatableAbi = delegatableABI +export const verifyingContractAbi = verifyingContractERC20PermitSubscriptionsABI + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Delegatable +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Wraps __{@link useReadContract}__ with `abi` set to __{@link delegatableAbi}__ + */ +export const useReadDelegatable = /*#__PURE__*/ createUseReadContract({ + abi: delegatableAbi, +}) + +/** + * Wraps __{@link useReadContract}__ with `abi` set to __{@link delegatableAbi}__ and `functionName` set to `"getNonce"` + */ +export const useReadDelegatableGetNonce = /*#__PURE__*/ createUseReadContract({ + abi: delegatableAbi, + functionName: 'getNonce', +}) + +/** + * Wraps __{@link useReadContract}__ with `abi` set to __{@link delegatableAbi}__ and `functionName` set to `"verifyDelegationSignature"` + */ +export const useReadDelegatableVerifyDelegationSignature = + /*#__PURE__*/ createUseReadContract({ + abi: delegatableAbi, + functionName: 'verifyDelegationSignature', + }) + +/** + * Wraps __{@link useReadContract}__ with `abi` set to __{@link delegatableAbi}__ and `functionName` set to `"getDelegationTypedDataHash"` + */ +export const useReadDelegatableGetDelegationTypedDataHash = + /*#__PURE__*/ createUseReadContract({ + abi: delegatableAbi, + functionName: 'getDelegationTypedDataHash', + }) + +/** + * Wraps __{@link useReadContract}__ with `abi` set to __{@link delegatableAbi}__ and `functionName` set to `"getEIP712DomainHash"` + */ +export const useReadDelegatableGetEip712DomainHash = + /*#__PURE__*/ createUseReadContract({ + abi: delegatableAbi, + functionName: 'getEIP712DomainHash', + }) + +/** + * Wraps __{@link useWriteContract}__ with `abi` set to __{@link delegatableAbi}__ + */ +export const useWriteDelegatable = /*#__PURE__*/ createUseWriteContract({ + abi: delegatableAbi, +}) + +/** + * Wraps __{@link useWriteContract}__ with `abi` set to __{@link delegatableAbi}__ and `functionName` set to `"invoke"` + */ +export const useWriteDelegatableInvoke = /*#__PURE__*/ createUseWriteContract({ + abi: delegatableAbi, + functionName: 'invoke', +}) + +/** + * Wraps __{@link useSimulateContract}__ with `abi` set to __{@link delegatableAbi}__ + */ +export const useSimulateDelegatable = /*#__PURE__*/ createUseSimulateContract({ + abi: delegatableAbi, +}) + +/** + * Wraps __{@link useSimulateContract}__ with `abi` set to __{@link delegatableAbi}__ and `functionName` set to `"invoke"` + */ +export const useSimulateDelegatableInvoke = + /*#__PURE__*/ createUseSimulateContract({ + abi: delegatableAbi, + functionName: 'invoke', + }) + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Verifying Contract Subscriptions +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Wraps __{@link useReadContract}__ with `abi` set to __{@link verifyingContractAbi}__ + */ +export const useReadVerifyingContract = /*#__PURE__*/ createUseReadContract({ + abi: verifyingContractAbi, +}) + +/** + * Wraps __{@link useReadContract}__ with `abi` set to __{@link verifyingContractAbi}__ and `functionName` set to `"subToken"` + */ +export const useReadVerifyingContractSubToken = + /*#__PURE__*/ createUseReadContract({ + abi: verifyingContractAbi, + functionName: 'subToken', + }) + +/** + * Wraps __{@link useReadContract}__ with `abi` set to __{@link verifyingContractAbi}__ and `functionName` set to `"subAmount"` + */ +export const useReadVerifyingContractSubAmount = + /*#__PURE__*/ createUseReadContract({ + abi: verifyingContractAbi, + functionName: 'subAmount', + }) + +/** + * Wraps __{@link useReadContract}__ with `abi` set to __{@link verifyingContractAbi}__ and `functionName` set to `"subPeriod"` + */ +export const useReadVerifyingContractSubPeriod = + /*#__PURE__*/ createUseReadContract({ + abi: verifyingContractAbi, + functionName: 'subPeriod', + }) + +/** + * Wraps __{@link useReadContract}__ with `abi` set to __{@link verifyingContractAbi}__ and `functionName` set to `"getSubPeriod"` + */ +export const useReadVerifyingContractGetSubPeriod = + /*#__PURE__*/ createUseReadContract({ + abi: verifyingContractAbi, + functionName: 'getSubPeriod', + }) + +/** + * Wraps __{@link useWriteContract}__ with `abi` set to __{@link verifyingContractAbi}__ + */ +export const useWriteVerifyingContract = /*#__PURE__*/ createUseWriteContract({ + abi: verifyingContractAbi, +}) + +/** + * Wraps __{@link useWriteContract}__ with `abi` set to __{@link verifyingContractAbi}__ and `functionName` set to `"approveSubscription"` + */ +export const useWriteVerifyingContractApproveSubscription = + /*#__PURE__*/ createUseWriteContract({ + abi: verifyingContractAbi, + functionName: 'approveSubscription', + }) + +/** + * Wraps __{@link useWriteContract}__ with `abi` set to __{@link verifyingContractAbi}__ and `functionName` set to `"paySubscription"` + */ +export const useWriteVerifyingContractPaySubscription = + /*#__PURE__*/ createUseWriteContract({ + abi: verifyingContractAbi, + functionName: 'paySubscription', + }) + +/** + * Wraps __{@link useWriteContract}__ with `abi` set to __{@link verifyingContractAbi}__ and `functionName` set to `"invoke"` + */ +export const useWriteVerifyingContractInvoke = + /*#__PURE__*/ createUseWriteContract({ + abi: verifyingContractAbi, + functionName: 'invoke', + }) + +/** + * Wraps __{@link useSimulateContract}__ with `abi` set to __{@link verifyingContractAbi}__ + */ +export const useSimulateVerifyingContract = + /*#__PURE__*/ createUseSimulateContract({ + abi: verifyingContractAbi, + }) + +/** + * Wraps __{@link useSimulateContract}__ with `abi` set to __{@link verifyingContractAbi}__ and `functionName` set to `"invoke"` + */ +export const useSimulateVerifyingContractInvoke = + /*#__PURE__*/ createUseSimulateContract({ + abi: verifyingContractAbi, + functionName: 'invoke', + }) diff --git a/integrations/delegatable-subscription/hooks/index.ts b/integrations/delegatable-subscription/hooks/index.ts new file mode 100644 index 00000000..50a59a93 --- /dev/null +++ b/integrations/delegatable-subscription/hooks/index.ts @@ -0,0 +1,4 @@ +export * from "./use-delegatable-subscriptions" +export * from "./use-start-subscription" +export * from "./use-revoke-subscription" +export * from "./use-execute-subscription" diff --git a/integrations/delegatable-subscription/hooks/use-delegatable-subscriptions.ts b/integrations/delegatable-subscription/hooks/use-delegatable-subscriptions.ts new file mode 100644 index 00000000..15e64bfd --- /dev/null +++ b/integrations/delegatable-subscription/hooks/use-delegatable-subscriptions.ts @@ -0,0 +1,44 @@ +import { useLiveQuery } from "dexie-react-hooks" +import { db, type StoredSubscription } from "../database" + +export const useDelegatableSubscriptions = () => { + const subscriptions = useLiveQuery(() => db.subscriptions.toArray(), []) + + const getStoredSubscription = async (id: string) => { + return db.subscriptions.get(id) + } + + const addSubscription = async (subscription: StoredSubscription) => { + await db.subscriptions.put(subscription) + return subscription + } + + const updateSubscription = async ( + id: string, + changes: Partial + ) => { + await db.subscriptions.update(id, changes) + } + + const deleteSubscription = async (id: string) => { + await db.subscriptions.delete(id) + } + + const clearAllSubscriptions = async () => { + await db.subscriptions.clear() + } + + const activeSubscriptions = (subscriptions || []).filter( + (sub) => sub.status === "active" + ) + + return { + subscriptions, + activeSubscriptions, + getStoredSubscription, + addSubscription, + updateSubscription, + deleteSubscription, + clearAllSubscriptions, + } +} diff --git a/integrations/delegatable-subscription/hooks/use-execute-subscription.ts b/integrations/delegatable-subscription/hooks/use-execute-subscription.ts new file mode 100644 index 00000000..46c73084 --- /dev/null +++ b/integrations/delegatable-subscription/hooks/use-execute-subscription.ts @@ -0,0 +1,105 @@ +import { useState } from "react" +import { useAccount, useSignTypedData, useWriteContract } from "wagmi" +import { delegatableABI } from "../abis/delegatable-abi" +import { StoredSubscription } from "../database" +import { useDelegatableSubscriptions } from "./use-delegatable-subscriptions" +import { createSubscriptionInvocation } from "../utils/create-invocation" + +export function useExecuteSubscription() { + const { address: userAddress, chain } = useAccount() + const { signTypedDataAsync } = useSignTypedData() + const { writeContractAsync } = useWriteContract() + const { updateSubscription } = useDelegatableSubscriptions() + + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + + const executeSubscription = async (subscription: StoredSubscription) => { + if (!userAddress) { + throw new Error("Wallet not connected") + } + + try { + setIsLoading(true) + setError(null) + + const chainId = subscription.chainId || chain?.id || 1 + const isFirstExecution = !subscription.lastExecutedAt + + const invocationData = createSubscriptionInvocation({ + verifyingContract: subscription.verifyingContract, + subscriber: subscription.subscriber, + totalSubscriptionAmount: BigInt(subscription.totalSubscriptionAmount), + deadline: BigInt(subscription.deadline), + permitSignature: subscription.permitSignature, + signedDelegation: subscription.signedDelegation, + chainId, + includeApproval: isFirstExecution, + }) + + // Sign the invocation with delegate / relayer key + const invocationSig = await signTypedDataAsync({ + domain: invocationData.domain, + types: invocationData.types, + primaryType: invocationData.primaryType, + message: invocationData.message, + }) + + const signedInvocations = [ + { + invocations: { + batch: invocationData.invocations.batch.map((b) => ({ + transaction: { + to: b.transaction.to, + gasLimit: BigInt(b.transaction.gasLimit), + data: b.transaction.data, + }, + authority: b.authority.map((a) => ({ + delegation: { + delegate: a.delegation.delegate, + authority: a.delegation.authority, + caveats: a.delegation.caveats.map((c) => ({ + enforcer: c.enforcer, + terms: c.terms, + })), + }, + signature: a.signature, + })), + })), + replayProtection: { + nonce: BigInt(invocationData.invocations.replayProtection.nonce), + queue: BigInt(invocationData.invocations.replayProtection.queue), + }, + }, + signature: invocationSig, + }, + ] + + const txHash = await writeContractAsync({ + address: subscription.verifyingContract, + abi: delegatableABI, + functionName: "invoke", + args: [signedInvocations as any], + }) + + await updateSubscription(subscription.id, { + lastExecutedAt: Date.now(), + executionCount: (subscription.executionCount || 0) + 1, + }) + + return txHash + } catch (err: any) { + const message = err?.message || "Failed to execute subscription" + setError(message) + throw err + } finally { + setIsLoading(false) + } + } + + return { + executeSubscription, + isLoading, + error, + } +} diff --git a/integrations/delegatable-subscription/hooks/use-revoke-subscription.ts b/integrations/delegatable-subscription/hooks/use-revoke-subscription.ts new file mode 100644 index 00000000..ee955bbc --- /dev/null +++ b/integrations/delegatable-subscription/hooks/use-revoke-subscription.ts @@ -0,0 +1,105 @@ +import { useState } from "react" +import { encodeAbiParameters, keccak256, toHex, type Address } from "viem" +import { useAccount, useWriteContract } from "wagmi" +import { districtERC20PermitSubscriptionsEnforcerABI } from "../abis/district-erc20-permit-subscriptions-enforcer-abi" +import { StoredSubscription } from "../database" +import { useDelegatableSubscriptions } from "./use-delegatable-subscriptions" +import { DELEGATABLE_SUBSCRIPTION_DEFAULTS } from "../utils/constants" + +export function useRevokeSubscription() { + const { chain } = useAccount() + const { writeContractAsync } = useWriteContract() + const { updateSubscription } = useDelegatableSubscriptions() + + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + + const revokeSubscription = async ( + subscription: StoredSubscription, + options?: { onChain?: boolean } + ) => { + try { + setIsLoading(true) + setError(null) + + if (options?.onChain) { + const chainId = subscription.chainId || chain?.id || 1 + + // EIP712 Domain Hash calculation for Delegatable verifying contract + const domainTypeHash = keccak256( + toHex( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ) + ) + const nameHash = keccak256( + toHex(DELEGATABLE_SUBSCRIPTION_DEFAULTS.CONTRACT_NAME) + ) + const versionHash = keccak256( + toHex(DELEGATABLE_SUBSCRIPTION_DEFAULTS.CONTRACT_VERSION) + ) + + const domainHash = keccak256( + encodeAbiParameters( + [ + { type: "bytes32" }, + { type: "bytes32" }, + { type: "bytes32" }, + { type: "uint256" }, + { type: "address" }, + ], + [ + domainTypeHash, + nameHash, + versionHash, + BigInt(chainId), + subscription.verifyingContract, + ] + ) + ) + + const hash = await writeContractAsync({ + address: subscription.enforcer, + abi: districtERC20PermitSubscriptionsEnforcerABI, + functionName: "cancelSubscription", + args: [ + { + delegation: { + delegate: subscription.signedDelegation.delegation.delegate, + authority: subscription.signedDelegation.delegation.authority, + caveats: + subscription.signedDelegation.delegation.caveats.map((c) => ({ + enforcer: c.enforcer, + terms: c.terms, + })), + }, + signature: subscription.signedDelegation.signature, + }, + domainHash, + ], + }) + + await updateSubscription(subscription.id, { + status: "canceled", + }) + + return hash + } else { + await updateSubscription(subscription.id, { + status: "canceled", + }) + } + } catch (err: any) { + const message = err?.message || "Failed to revoke subscription" + setError(message) + throw err + } finally { + setIsLoading(false) + } + } + + return { + revokeSubscription, + isLoading, + error, + } +} diff --git a/integrations/delegatable-subscription/hooks/use-start-subscription.ts b/integrations/delegatable-subscription/hooks/use-start-subscription.ts new file mode 100644 index 00000000..17850664 --- /dev/null +++ b/integrations/delegatable-subscription/hooks/use-start-subscription.ts @@ -0,0 +1,154 @@ +import { useState } from "react" +import { parseSignature, type Address } from "viem" +import { useAccount, useSignTypedData } from "wagmi" +import { StoredSubscription } from "../database" +import { useDelegatableSubscriptions } from "./use-delegatable-subscriptions" +import { DELEGATABLE_SUBSCRIPTION_DEFAULTS } from "../utils/constants" +import { createSubscriptionDelegation } from "../utils/create-delegation" +import { createPermitTypedData } from "../utils/create-permit" + +export interface StartSubscriptionArgs { + tokenAddress: Address + tokenName: string + verifyingContract: Address + enforcer?: Address + delegate: Address + subAmount: string + subPeriod: number + totalSubscriptionAmount: string + deadlineDays?: number + salt?: number +} + +export function useStartSubscription() { + const { address: userAddress, chain } = useAccount() + const { signTypedDataAsync } = useSignTypedData() + const { addSubscription } = useDelegatableSubscriptions() + + const [isLoading, setIsLoading] = useState(false) + const [step, setStep] = useState< + "idle" | "signing_permit" | "signing_delegation" | "saving" | "success" + >("idle") + const [error, setError] = useState(null) + + const startSubscription = async ( + args: StartSubscriptionArgs + ): Promise => { + if (!userAddress) { + throw new Error("Wallet not connected") + } + + const chainId = chain?.id || 1 + const enforcer = + args.enforcer || DELEGATABLE_SUBSCRIPTION_DEFAULTS.DEFAULT_ENFORCER_ADDRESS + const salt = args.salt ?? 1 + const deadlineDays = + args.deadlineDays || DELEGATABLE_SUBSCRIPTION_DEFAULTS.DEFAULT_DEADLINE_DAYS + const deadlineTimestamp = BigInt( + Math.floor(Date.now() / 1000) + deadlineDays * 86400 + ) + + try { + setIsLoading(true) + setError(null) + + // Step 1: Sign Permit (EIP-2612) + setStep("signing_permit") + const totalAmountBigInt = BigInt(args.totalSubscriptionAmount) + + const permitData = createPermitTypedData({ + tokenAddress: args.tokenAddress, + tokenName: args.tokenName, + chainId, + owner: userAddress, + spender: args.verifyingContract, + value: totalAmountBigInt, + nonce: 0n, + deadline: deadlineTimestamp, + }) + + const permitSigRaw = await signTypedDataAsync({ + domain: permitData.domain, + types: permitData.types, + primaryType: permitData.primaryType, + message: permitData.message, + }) + + const parsedPermitSig = parseSignature(permitSigRaw) + const permitSignature = { + v: Number(parsedPermitSig.v ?? 27n), + r: parsedPermitSig.r, + s: parsedPermitSig.s, + } + + // Step 2: Sign Delegatable Delegation + setStep("signing_delegation") + const delegationData = createSubscriptionDelegation({ + delegate: args.delegate, + verifyingContract: args.verifyingContract, + enforcer, + contractName: DELEGATABLE_SUBSCRIPTION_DEFAULTS.CONTRACT_NAME, + chainId, + salt, + }) + + const delegationSigRaw = await signTypedDataAsync({ + domain: delegationData.domain, + types: delegationData.types, + primaryType: delegationData.primaryType, + message: delegationData.message, + }) + + // Step 3: Save to Database + setStep("saving") + const subscriptionId = `sub_${Date.now()}_${Math.random().toString(36).substring(2, 9)}` + + const storedSubscription: StoredSubscription = { + id: subscriptionId, + subscriber: userAddress, + delegate: args.delegate, + verifyingContract: args.verifyingContract, + enforcer, + tokenAddress: args.tokenAddress, + tokenName: args.tokenName, + subAmount: args.subAmount, + subPeriod: args.subPeriod, + totalSubscriptionAmount: args.totalSubscriptionAmount, + deadline: deadlineTimestamp.toString(), + signedDelegation: { + delegation: delegationData.delegation, + signature: delegationSigRaw, + }, + permitSignature, + salt, + chainId, + status: "active", + createdAt: Date.now(), + executionCount: 0, + } + + await addSubscription(storedSubscription) + setStep("success") + return storedSubscription + } catch (err: any) { + const message = err?.message || "Failed to start subscription" + setError(message) + setStep("idle") + throw err + } finally { + setIsLoading(false) + } + } + + return { + startSubscription, + isLoading, + step, + error, + reset: () => { + setStep("idle") + setError(null) + setIsLoading(false) + }, + } +} diff --git a/integrations/delegatable-subscription/index.ts b/integrations/delegatable-subscription/index.ts new file mode 100644 index 00000000..4938683b --- /dev/null +++ b/integrations/delegatable-subscription/index.ts @@ -0,0 +1,6 @@ +export * from "./components" +export * from "./hooks" +export * from "./utils" +export * from "./database" +export * from "./delegatable-wagmi" +export * from "./delegatable-enforcers-wagmi" diff --git a/integrations/delegatable-subscription/utils/constants.ts b/integrations/delegatable-subscription/utils/constants.ts new file mode 100644 index 00000000..e54c3195 --- /dev/null +++ b/integrations/delegatable-subscription/utils/constants.ts @@ -0,0 +1,28 @@ +import type { Address } from "viem" + +export const DELEGATABLE_SUBSCRIPTION_DEFAULTS = { + CONTRACT_NAME: "ERC20PermitSubscriptions", + CONTRACT_VERSION: "1", + ENFORCER_NAME: "DistrictERC20PermitSubscriptionsEnforcer", + // Standard test deployment address or mock address for UI testing + DEFAULT_ENFORCER_ADDRESS: + "0x9A676e781A523b5d0C0e43731313A708CB607508" as Address, + DEFAULT_VERIFYING_CONTRACT: + "0x0B306BF915C4d645ff596e518fAf3F9669b97016" as Address, + DEFAULT_TOKEN_ADDRESS: + "0x4200000000000000000000000000000000000042" as Address, // OP / Mock token + DEFAULT_SUB_PERIOD: 1800, // 30 minutes in seconds + DEFAULT_SUB_AMOUNT: "1", // 1 token per period + DEFAULT_TOTAL_AMOUNT: "12", // 12 tokens total limit + DEFAULT_DEADLINE_DAYS: 365, +} + +export const PERIOD_PRESETS = [ + { label: "30 Seconds (Test)", value: 30 }, + { label: "5 Minutes", value: 300 }, + { label: "30 Minutes", value: 1800 }, + { label: "1 Hour", value: 3600 }, + { label: "1 Day", value: 86400 }, + { label: "1 Week", value: 604800 }, + { label: "30 Days", value: 2592000 }, +] as const diff --git a/integrations/delegatable-subscription/utils/create-delegation.ts b/integrations/delegatable-subscription/utils/create-delegation.ts new file mode 100644 index 00000000..567a5aae --- /dev/null +++ b/integrations/delegatable-subscription/utils/create-delegation.ts @@ -0,0 +1,58 @@ +import type { Address } from "viem" +import { encodeSubscriptionTerms } from "./create-terms" +import { EIP712_DELEGATABLE_TYPES, type Caveat, type Delegation } from "./types" + +export interface CreateDelegationParams { + delegate: Address + verifyingContract: Address + enforcer: Address + contractName?: string + chainId: number + salt?: number + authority?: `0x${string}` +} + +export function createSubscriptionDelegation({ + delegate, + verifyingContract, + enforcer, + contractName = "ERC20PermitSubscriptions", + chainId, + salt = 1, + authority = "0x0000000000000000000000000000000000000000000000000000000000000000", +}: CreateDelegationParams) { + const terms = encodeSubscriptionTerms(verifyingContract, salt) + + const caveats: Caveat[] = [ + { + enforcer, + terms, + }, + ] + + const delegation: Delegation = { + delegate, + authority, + caveats, + } + + const domain = { + name: contractName, + version: "1", + chainId: BigInt(chainId), + verifyingContract, + } as const + + const types = { + Delegation: EIP712_DELEGATABLE_TYPES.Delegation, + Caveat: EIP712_DELEGATABLE_TYPES.Caveat, + } as const + + return { + domain, + types, + primaryType: "Delegation" as const, + message: delegation, + delegation, + } +} diff --git a/integrations/delegatable-subscription/utils/create-invocation.ts b/integrations/delegatable-subscription/utils/create-invocation.ts new file mode 100644 index 00000000..503c1ab5 --- /dev/null +++ b/integrations/delegatable-subscription/utils/create-invocation.ts @@ -0,0 +1,111 @@ +import { encodeFunctionData, type Address } from "viem" +import { verifyingContractERC20PermitSubscriptionsABI } from "../abis/verifying-contract-erc20-permit-subscriptions-abi" +import { + EIP712_DELEGATABLE_TYPES, + type Invocations, + type PermitSignature, + type SignedDelegation, +} from "./types" + +export interface CreateSubscriptionInvocationParams { + verifyingContract: Address + subscriber: Address + totalSubscriptionAmount: bigint + deadline: bigint + permitSignature: PermitSignature + signedDelegation: SignedDelegation + chainId: number + contractName?: string + nonce?: bigint + queue?: bigint + includeApproval?: boolean + gasLimit?: bigint +} + +export function createSubscriptionInvocation({ + verifyingContract, + subscriber, + totalSubscriptionAmount, + deadline, + permitSignature, + signedDelegation, + chainId, + contractName = "ERC20PermitSubscriptions", + nonce = 1n, + queue = 0n, + includeApproval = true, + gasLimit = 210000000000000000n, +}: CreateSubscriptionInvocationParams) { + const paySubscriptionData = encodeFunctionData({ + abi: verifyingContractERC20PermitSubscriptionsABI, + functionName: "paySubscription", + }) + + const batch: Invocations["batch"] = [] + + if (includeApproval) { + const approveSubscriptionData = encodeFunctionData({ + abi: verifyingContractERC20PermitSubscriptionsABI, + functionName: "approveSubscription", + args: [ + subscriber, + totalSubscriptionAmount, + deadline, + permitSignature.v, + permitSignature.r, + permitSignature.s, + ], + }) + + batch.push({ + authority: [], + transaction: { + to: verifyingContract, + gasLimit: gasLimit.toString(), + data: approveSubscriptionData, + }, + }) + } + + batch.push({ + authority: [signedDelegation], + transaction: { + to: verifyingContract, + gasLimit: gasLimit.toString(), + data: paySubscriptionData, + }, + }) + + const invocations: Invocations = { + replayProtection: { + nonce: nonce.toString(), + queue: queue.toString(), + }, + batch, + } + + const domain = { + name: contractName, + version: "1", + chainId: BigInt(chainId), + verifyingContract, + } as const + + const types = { + Invocations: EIP712_DELEGATABLE_TYPES.Invocations, + Invocation: EIP712_DELEGATABLE_TYPES.Invocation, + Transaction: EIP712_DELEGATABLE_TYPES.Transaction, + SignedDelegation: EIP712_DELEGATABLE_TYPES.SignedDelegation, + Delegation: EIP712_DELEGATABLE_TYPES.Delegation, + Caveat: EIP712_DELEGATABLE_TYPES.Caveat, + ReplayProtection: EIP712_DELEGATABLE_TYPES.ReplayProtection, + } as const + + return { + invocations, + domain, + types, + primaryType: "Invocations" as const, + message: invocations, + } +} diff --git a/integrations/delegatable-subscription/utils/create-permit.ts b/integrations/delegatable-subscription/utils/create-permit.ts new file mode 100644 index 00000000..fbb4f695 --- /dev/null +++ b/integrations/delegatable-subscription/utils/create-permit.ts @@ -0,0 +1,52 @@ +import type { Address } from "viem" +import { EIP712_PERMIT_TYPES } from "./types" + +export interface CreatePermitParams { + tokenAddress: Address + tokenName: string + tokenVersion?: string + chainId: number + owner: Address + spender: Address + value: bigint + nonce: bigint + deadline: bigint +} + +export function createPermitTypedData({ + tokenAddress, + tokenName, + tokenVersion = "1", + chainId, + owner, + spender, + value, + nonce, + deadline, +}: CreatePermitParams) { + const domain = { + name: tokenName, + version: tokenVersion, + chainId: BigInt(chainId), + verifyingContract: tokenAddress, + } as const + + const types = { + Permit: EIP712_PERMIT_TYPES.Permit, + } as const + + const message = { + owner, + spender, + value, + nonce, + deadline, + } as const + + return { + domain, + types, + primaryType: "Permit" as const, + message, + } +} diff --git a/integrations/delegatable-subscription/utils/create-terms.ts b/integrations/delegatable-subscription/utils/create-terms.ts new file mode 100644 index 00000000..721eaf4f --- /dev/null +++ b/integrations/delegatable-subscription/utils/create-terms.ts @@ -0,0 +1,33 @@ +import { encodePacked, type Address } from "viem" + +/** + * Encodes terms for DistrictERC20PermitSubscriptionsEnforcer + * Format: abi.encodePacked(verifierAddress, salt) + * + * @param verifier Verifying contract address (20 bytes) + * @param salt Salt byte / number (uint8) + * @returns Packed bytecode hex string + */ +export function encodeSubscriptionTerms( + verifier: Address, + salt: number | bigint = 1 +): `0x${string}` { + return encodePacked(["address", "uint8"], [verifier, Number(salt)]) +} + +/** + * Parses terms back to verifier address and salt + */ +export function decodeSubscriptionTerms(terms: `0x${string}`): { + verifier: Address + salt: number +} { + const cleanTerms = terms.startsWith("0x") ? terms.slice(2) : terms + const verifierHex = `0x${cleanTerms.slice(0, 40)}` as Address + const saltHex = cleanTerms.slice(40, 42) + const salt = saltHex ? parseInt(saltHex, 16) : 0 + return { + verifier: verifierHex, + salt, + } +} diff --git a/integrations/delegatable-subscription/utils/index.ts b/integrations/delegatable-subscription/utils/index.ts new file mode 100644 index 00000000..a3f4d779 --- /dev/null +++ b/integrations/delegatable-subscription/utils/index.ts @@ -0,0 +1,6 @@ +export * from "./types" +export * from "./create-terms" +export * from "./create-delegation" +export * from "./create-permit" +export * from "./create-invocation" +export * from "./constants" diff --git a/integrations/delegatable-subscription/utils/types.ts b/integrations/delegatable-subscription/utils/types.ts new file mode 100644 index 00000000..ecdade79 --- /dev/null +++ b/integrations/delegatable-subscription/utils/types.ts @@ -0,0 +1,96 @@ +import type { Address } from "viem" + +export interface Caveat { + enforcer: Address + terms: `0x${string}` +} + +export interface Delegation { + delegate: Address + authority: `0x${string}` + caveats: Caveat[] +} + +export interface SignedDelegation { + delegation: Delegation + signature: `0x${string}` +} + +export interface Transaction { + to: Address + gasLimit: string | bigint + data: `0x${string}` +} + +export interface Invocation { + transaction: Transaction + authority: SignedDelegation[] +} + +export interface ReplayProtection { + nonce: string | bigint + queue: string | bigint +} + +export interface Invocations { + batch: Invocation[] + replayProtection: ReplayProtection +} + +export interface SignedInvocation { + invocations: Invocations + signature: `0x${string}` +} + +export interface PermitSignature { + v: number + r: `0x${string}` + s: `0x${string}` +} + +export const EIP712_DELEGATABLE_TYPES = { + Delegation: [ + { name: "delegate", type: "address" }, + { name: "authority", type: "bytes32" }, + { name: "caveats", type: "Caveat[]" }, + ], + Caveat: [ + { name: "enforcer", type: "address" }, + { name: "terms", type: "bytes" }, + ], + SignedDelegation: [ + { name: "delegation", type: "Delegation" }, + { name: "signature", type: "bytes" }, + ], + Transaction: [ + { name: "to", type: "address" }, + { name: "gasLimit", type: "uint256" }, + { name: "data", type: "bytes" }, + ], + Invocation: [ + { name: "transaction", type: "Transaction" }, + { name: "authority", type: "SignedDelegation[]" }, + ], + ReplayProtection: [ + { name: "nonce", type: "uint256" }, + { name: "queue", type: "uint256" }, + ], + Invocations: [ + { name: "batch", type: "Invocation[]" }, + { name: "replayProtection", type: "ReplayProtection" }, + ], + SignedInvocation: [ + { name: "invocations", type: "Invocations" }, + { name: "signature", type: "bytes" }, + ], +} as const + +export const EIP712_PERMIT_TYPES = { + Permit: [ + { name: "owner", type: "address" }, + { name: "spender", type: "address" }, + { name: "value", type: "uint256" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + ], +} as const diff --git a/integrations/delegatable-subscription/wagmi.config.ts b/integrations/delegatable-subscription/wagmi.config.ts new file mode 100644 index 00000000..ab2d3efc --- /dev/null +++ b/integrations/delegatable-subscription/wagmi.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from "@wagmi/cli" +import { react } from "@wagmi/cli/plugins" + +import { delegatableABI } from "./abis/delegatable-abi" +import { districtERC20PermitSubscriptionsEnforcerABI } from "./abis/district-erc20-permit-subscriptions-enforcer-abi" +import { verifyingContractERC20PermitSubscriptionsABI } from "./abis/verifying-contract-erc20-permit-subscriptions-abi" + +export default defineConfig({ + out: "./integrations/delegatable-subscription/generated/delegatable-wagmi.ts", + contracts: [ + { + name: "delegatable", + abi: delegatableABI, + }, + { + name: "districtERC20PermitSubscriptionsEnforcer", + abi: districtERC20PermitSubscriptionsEnforcerABI, + }, + { + name: "verifyingContractERC20PermitSubscriptions", + abi: verifyingContractERC20PermitSubscriptionsABI, + }, + ], + plugins: [react()], +}) diff --git a/vercel.json b/vercel.json new file mode 100644 index 00000000..31be89fe --- /dev/null +++ b/vercel.json @@ -0,0 +1,8 @@ +{ + "crons": [ + { + "path": "/api/delegatable/cron", + "schedule": "*/30 * * * *" + } + ] +}