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)}
+
+
+ )}
+
+
+
+
+ {isRunning ? (
+ <>
+
+ Running Cron...
+ >
+ ) : (
+ <>
+
+ Test Trigger Cron
+ >
+ )}
+
+
+
+ )
+}
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" && (
+ handleExecute(sub)}
+ >
+ {isExecuting && isLoading ? (
+ <>
+
+ Executing...
+ >
+ ) : (
+ <>
+
+ Execute Payment
+ >
+ )}
+
+ )}
+
+
+ )
+ })}
+
+ )}
+
+
+ )
+}
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 && (
+ handleRevoke(sub)}
+ >
+ {isSelected && isLoading ? (
+ <>
+
+ Revoking...
+ >
+ ) : (
+ <>
+
+ Revoke Delegation
+ >
+ )}
+
+ )}
+
+ handleDelete(sub.id)}
+ title="Remove from DB"
+ >
+
+
+
+
+ )
+ })}
+
+ )}
+
+
+
+
+ Enforcer:{" "}
+ DistrictERC20PermitSubscriptionsEnforcer
+
+
+ setOnChainCancel(e.target.checked)}
+ className="rounded border-gray-300"
+ />
+ Submit on-chain cancel tx
+
+
+
+ )
+}
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
+
+
+
+
+ Create Another 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 * * * *"
+ }
+ ]
+}