Skip to content
Merged
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
5 changes: 4 additions & 1 deletion backend/scripts/009_create_event_tables.sql
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ CREATE INDEX idx_renewal_approvals_sub_id ON renewal_approvals(blockchain_sub_id
-- Add blockchain_sub_id to subscriptions if not exists
ALTER TABLE subscriptions
ADD COLUMN IF NOT EXISTS blockchain_sub_id BIGINT,
ADD COLUMN IF NOT EXISTS failure_count INTEGER DEFAULT 0;
ADD COLUMN IF NOT EXISTS failure_count INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS executor_address VARCHAR(56),
ADD COLUMN IF NOT EXISTS billing_start_timestamp TIMESTAMP WITH TIME ZONE,
ADD COLUMN IF NOT EXISTS billing_end_timestamp TIMESTAMP WITH TIME ZONE;

CREATE INDEX IF NOT EXISTS idx_subscriptions_blockchain_sub_id ON subscriptions(blockchain_sub_id);
12 changes: 1 addition & 11 deletions backend/src/routes/subscriptions.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,3 @@
<<<<<<< HEAD
import { Router, Response } from 'express';
import { subscriptionService } from '../services/subscription-service';
import { giftCardService } from '../services/gift-card-service';
import { idempotencyService } from '../services/idempotency';
import { authenticate, AuthenticatedRequest } from '../middleware/auth';
import { validateSubscriptionOwnership, validateBulkSubscriptionOwnership } from '../middleware/ownership';
import logger from '../config/logger';
=======
import { Router, Response } from "express";
import { subscriptionService } from "../services/subscription-service";
import { idempotencyService } from "../services/idempotency";
Expand All @@ -16,7 +7,6 @@ import {
validateBulkSubscriptionOwnership,
} from "../middleware/ownership";
import logger from "../config/logger";
>>>>>>> main

const router = Router();

Expand Down Expand Up @@ -195,7 +185,7 @@ router.patch("/:id", validateSubscriptionOwnership, async (req: AuthenticatedReq

const result = await subscriptionService.updateSubscription(
req.user!.id,
req.params.id,
Array.isArray(req.params.id) ? req.params.id[0] : req.params.id,
req.body,
expectedVersion ? parseInt(expectedVersion) : undefined,
);
Expand Down
1 change: 1 addition & 0 deletions backend/src/services/subscription-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export class SubscriptionService {
async createSubscription(
userId: string,
input: SubscriptionCreateInput,
idempotencyKey?: string
): Promise<SubscriptionSyncResult> {
return await DatabaseTransaction.execute(async (client) => {
try {
Expand Down
3 changes: 2 additions & 1 deletion backend/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"include": [ "src/**/*"],
"exclude": [
"node_modules",
"dist"
"dist",
"src/services/blockchain-service-impl-example.ts"
]
}
49 changes: 49 additions & 0 deletions contracts/DELEGATED_EXECUTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Delegated Execution Feature

## Overview
Users can now assign another address to execute renewals without transferring ownership.

## Contract Changes

### New Storage
- `ExecutorKey` - Storage key for executor addresses per subscription

### New Functions
- `set_executor(sub_id, executor)` - Assign executor (owner only)
- `remove_executor(sub_id)` - Remove executor (owner only)
- `get_executor(sub_id)` - Query current executor

### Updated Functions
- `renew()` - Now accepts `caller` parameter and verifies caller is owner OR executor

### New Events
- `ExecutorAssigned { sub_id, executor }` - Emitted when executor is assigned
- `ExecutorRemoved { sub_id }` - Emitted when executor is removed

## Backend Changes

### Database
- Added `executor_address` column to `subscriptions` table

### Event Listener
- Handles `ExecutorAssigned` events → Updates `executor_address` in DB
- Handles `ExecutorRemoved` events → Clears `executor_address` in DB

## Usage Example

```rust
// Owner assigns executor
contract.set_executor(env, sub_id, executor_address);

// Executor can now call renew
contract.renew(env, executor_address, sub_id, approval_id, amount, max_retries, cooldown, true);

// Owner can remove executor
contract.remove_executor(env, sub_id);
```

## Security
- Only owner can assign/remove executors
- Executor cannot transfer ownership
- Executor can only execute renewals (with valid approvals)
- Owner retains full control
52 changes: 52 additions & 0 deletions contracts/RENEWAL_WINDOW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Renewal Time Window Feature

## Overview
Restricts renewal execution to a defined time window to prevent renewals from executing too early or too late relative to the billing schedule.

## Contract Changes

### New Storage
- `WindowKey` - Storage key for renewal windows per subscription
- `RenewalWindow` - Struct containing `billing_start` and `billing_end` timestamps

### New Functions
- `set_window(sub_id, billing_start, billing_end)` - Set renewal window (owner only)
- `get_window(sub_id)` - Query current renewal window

### Updated Functions
- `renew()` - Now validates current timestamp is within the renewal window before executing

### New Events
- `WindowUpdated { sub_id, billing_start, billing_end }` - Emitted when window is set/updated

### Validation
- Reverts with "Outside renewal window" if current timestamp is before `billing_start` or after `billing_end`
- Reverts with "Invalid window: start must be before end" if window is misconfigured

## Backend Changes

### Database
- Added `billing_start_timestamp` column to `subscriptions` table
- Added `billing_end_timestamp` column to `subscriptions` table

### Event Listener
- Handles `WindowUpdated` events → Updates billing timestamps in DB

## Usage Example

```rust
// Owner sets renewal window (Unix timestamps)
let start = 1704067200; // Jan 1, 2024 00:00:00 UTC
let end = 1704153600; // Jan 2, 2024 00:00:00 UTC
contract.set_window(env, sub_id, start, end);

// Renewal only succeeds within window
contract.renew(env, caller, sub_id, approval_id, amount, max_retries, cooldown, true);
// ✅ Success if current time is between start and end
// ❌ Reverts if outside window
```

## Security
- Only owner can set/update renewal window
- Window validation happens before approval consumption
- Prevents premature or delayed renewals
Loading