Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions app/(general)/integration/delegatable-subscription/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="container relative mt-20">
<PageHeader className="pb-8">
<LightDarkImage
LightImage={turboIntegrations.delegatableSubscription.imgDark}
DarkImage={turboIntegrations.delegatableSubscription.imgLight}
alt="Delegatable Logo"
width={100}
height={100}
className="h-20 w-20 rounded-2xl"
/>
<PageHeaderHeading>Delegatable Subscriptions</PageHeaderHeading>
<PageHeaderDescription>
Gas-less recurring token payments and conditional delegations powered
by the Delegatable smart contract framework and
DistrictERC20PermitSubscriptionsEnforcer.
</PageHeaderDescription>
<PageHeaderCTA>
<Link
href={turboIntegrations.delegatableSubscription.url}
target="_blank"
rel="noreferrer noopener"
className={cn(buttonVariants({ variant: "outline" }))}
>
<LuBook className="mr-2 h-4 w-4" />
Documentation
</Link>
<Link
href="https://github.com/district-labs/delegatable-enforcers"
target="_blank"
rel="noreferrer noopener"
className={cn(buttonVariants({ variant: "outline" }))}
>
<LuShieldCheck className="mr-2 h-4 w-4" />
Enforcers Repo
</Link>
</PageHeaderCTA>
</PageHeader>

<PageSection>
<Tabs defaultValue="start" className="w-full max-w-4xl space-y-6">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="start" className="flex items-center gap-2">
<LuZap className="h-4 w-4" />
<span className="hidden sm:inline">Start</span> Subscription
</TabsTrigger>
<TabsTrigger value="queue" className="flex items-center gap-2">
<LuRepeat className="h-4 w-4" />
<span className="hidden sm:inline">Execution</span> Queue
</TabsTrigger>
<TabsTrigger value="revoke" className="flex items-center gap-2">
<LuShieldCheck className="h-4 w-4" />
Revoke
</TabsTrigger>
<TabsTrigger value="cron" className="flex items-center gap-2">
<LuRepeat className="h-4 w-4" />
Cron Relayer
</TabsTrigger>
</TabsList>

<TabsContent value="start" className="space-y-4">
<FormDelegatableSubscriptionStart />
</TabsContent>

<TabsContent value="queue" className="space-y-4">
<DelegatableSubscriptionList />
</TabsContent>

<TabsContent value="revoke" className="space-y-4">
<FormDelegatableSubscriptionRevoke />
</TabsContent>

<TabsContent value="cron" className="space-y-4">
<DelegatableSubscriptionCronStatus />
</TabsContent>
</Tabs>
</PageSection>
</div>
)
}
17 changes: 17 additions & 0 deletions app/api/cron/subscription/route.ts
Original file line number Diff line number Diff line change
@@ -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)
}
40 changes: 40 additions & 0 deletions app/api/delegatable/cron/route.ts
Original file line number Diff line number Diff line change
@@ -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)
}
3 changes: 3 additions & 0 deletions app/delegatable/subscription/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import DelegatableSubscriptionPage from "@/app/(general)/integration/delegatable-subscription/page"

export default DelegatableSubscriptionPage
10 changes: 10 additions & 0 deletions data/turbo-integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
82 changes: 82 additions & 0 deletions integrations/delegatable-subscription/README.md
Original file line number Diff line number Diff line change
@@ -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 * * * *"
}
]
}
```
Loading