Skip to content
Open
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
23 changes: 16 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This project demonstrates how to schedule and execute recurring DCA (Dollar-Cost
- Node ^22.16.0
- pnpm ^10.7.0
- Docker or a local MongoDB instance
- A Vincent App with ERC20 approval and Uniswap swap abilities
- A Vincent App with Uniswap swap ability

## Monorepo Structure

Expand All @@ -21,7 +21,6 @@ This codebase is composed of three main parts:
- Express.js API server used by the frontend
- Agenda-based job scheduler that runs DCA jobs
- Integration with a Vincent App to execute swaps on behalf of users
- Vincent ERC20 Approval ability: authorizes Uniswap to spend user tokens
- Vincent Uniswap Swap ability: executes the actual token swaps

## Packages
Expand All @@ -45,14 +44,24 @@ To run this code and sign on behalf of your delegators, create your own Vincent

1. Go to the [Vincent Dashboard](https://dashboard.heyvincent.ai/) and log in as a builder.
2. Create a new app similar to [wBTC DCA](https://dashboard.heyvincent.ai/user/appId/9796398001/connect).
3. Add the ERC20 Approval ability.
4. Add the Uniswap Swap ability.
5. Publish the app.
6. Once users can connect to it, configure the backend with your App ID and the delegatee private key via environment variables. You can use the Deploy on Railway button below to deploy the entire app.
7. Once deployed, you'll need to update the `App User URL` and `Redirect URIs` to the URL deployed from Railway.
3. Add the Uniswap Swap ability.
4. Publish the app.
5. Once users can connect to it, configure the backend with your App ID and the delegatee private key via environment variables. You can use the Deploy on Railway button below to deploy the entire app.
6. Once deployed, you'll need to update the `App User URL` and `Redirect URIs` to the URL deployed from Railway.

[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/UY2g5I?referralCode=iNEMKY&utm_medium=integration&utm_source=template&utm_campaign=generic)

## Handling Multiple Vincent App Versions

This repository is deployed as the [Vincent wBTC DCA App](https://dashboard.heyvincent.ai/explorer/appId/9796398001). The production app is a good example of how you might handle multiple versions of the same app, where some versions may rely on different abilities, or on different versions of the same ability.

Note that the default configuration of this repository will use the latest version of the Uniswap Swap ability, but you can see an example of how to use different versions of the same ability in the [dca-backend](./packages/dca-backend/src/lib/agenda/jobs/executeDCASwap) package.

If you want to modify the app version specific logic for your own app and enable it, you will need to:

1. Modify the `getJobAPIVersionFromVincentAppVersion()` function in the [jobVersion.ts](./packages/dca-backend/src/lib/agenda/jobs/executeDCASwap/jobVersion.ts) file appropriately for your app.
2. Set `ENABLE_APP_VERSIONING` env var to `true` to enable the per-app version logic.

## Quick Start

Install dependencies and build the packages (works for both local and production setups):
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"devDependencies": {
"@dotenvx/dotenvx": "^0.26.0",
"@tsconfig/node20": "^20.1.5",
"@types/node": "^20.11.30",
"@types/node": "^22.16.0",
"@types/verror": "^1.10.10",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
Expand Down
3 changes: 2 additions & 1 deletion packages/dca-backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ SENTRY_DSN=
SENTRY_PROJECT=
SENTRY_ORG=
VINCENT_APP_ID=0
VINCENT_DELEGATEE_PRIVATE_KEY=
VINCENT_DELEGATEE_PRIVATE_KEY=
ENABLE_APP_VERSIONING=false
9 changes: 5 additions & 4 deletions packages/dca-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,12 @@
"@lit-protocol/contracts-sdk": "^7.3.0",
"@lit-protocol/lit-node-client": "^7.3.0",
"@lit-protocol/types": "^7.3.0",
"@lit-protocol/vincent-ability-erc20-approval": "3.1.0",
"@lit-protocol/vincent-ability-uniswap-swap": "5.0.0",
"@lit-protocol/vincent-app-sdk": "2.2.1",
"@lit-protocol/vincent-contracts-sdk": "^1.0.1",
"@lit-protocol/vincent-app-sdk": "2.2.3",
"@lit-protocol/vincent-contracts-sdk": "^2.0.0",
"@lit-protocol/vincent-scaffold-sdk": "1.1.9-mma",
"@lit-protocol/vincent-ability-erc20-approval": "3.1.0",
"@lit-protocol/vincent-ability-uniswap-swap-v5": "npm:@lit-protocol/vincent-ability-uniswap-swap@5.0.0",
"@lit-protocol/vincent-ability-uniswap-swap-v8": "npm:@lit-protocol/vincent-ability-uniswap-swap@8.0.0",
"@noble/secp256k1": "^2.2.3",
"@sentry/cli": "^2.52.0",
"@sentry/node": "^10.5.0",
Expand Down
25 changes: 11 additions & 14 deletions packages/dca-backend/src/lib/agenda/jobs/dcaSwapJobManager.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import consola from 'consola';
import { Types } from 'mongoose';

import * as executeDCASwapJobDef from './executeDCASwap';
import { getAgenda } from '../agendaClient';
import { jobName } from './executeDCASwap';
import { JobParams, JobType } from './executeDCASwap/types';

interface FindSpecificScheduledJobParams {
ethAddress: string;
Expand All @@ -18,26 +19,22 @@ export async function listJobsByEthAddress({ ethAddress }: { ethAddress: string

return (await agendaClient.jobs({
'data.pkpInfo.ethAddress': ethAddress,
})) as executeDCASwapJobDef.JobType[];
})) as JobType[];
}

export async function findJob(
params: FindSpecificScheduledJobParams
): Promise<executeDCASwapJobDef.JobType>;
export async function findJob(
params: FindSpecificScheduledJobParams
): Promise<executeDCASwapJobDef.JobType | undefined>;
export async function findJob(params: FindSpecificScheduledJobParams): Promise<JobType>;
export async function findJob(params: FindSpecificScheduledJobParams): Promise<JobType | undefined>;
export async function findJob({
ethAddress,
mustExist,
scheduleId,
}: FindSpecificScheduledJobParams): Promise<executeDCASwapJobDef.JobType | undefined> {
}: FindSpecificScheduledJobParams): Promise<JobType | undefined> {
const agendaClient = getAgenda();

const jobs = (await agendaClient.jobs({
_id: new Types.ObjectId(scheduleId),
'data.pkpInfo.ethAddress': ethAddress,
})) as executeDCASwapJobDef.JobType[];
})) as JobType[];

logger.log(`Found ${jobs.length} jobs with ID ${scheduleId}`);
if (mustExist && !jobs.length) {
Expand All @@ -51,7 +48,7 @@ export async function editJob({
data,
scheduleId,
}: {
data: Omit<executeDCASwapJobDef.JobParams, 'updatedAt'>;
data: Omit<JobParams, 'updatedAt'>;
scheduleId: string;
}) {
const {
Expand All @@ -70,7 +67,7 @@ export async function editJob({

job.attrs.data = { ...data, updatedAt: new Date() };

return (await job.save()) as unknown as executeDCASwapJobDef.JobType;
return (await job.save()) as unknown as JobType;
}

export async function disableJob({
Expand Down Expand Up @@ -113,7 +110,7 @@ export async function cancelJob({
}

export async function createJob(
data: Omit<executeDCASwapJobDef.JobParams, 'updatedAt'>,
data: Omit<JobParams, 'updatedAt'>,
options: {
interval?: string;
schedule?: string;
Expand All @@ -122,7 +119,7 @@ export async function createJob(
const agenda = getAgenda();

// Create a new job instance
const job = agenda.create<executeDCASwapJobDef.JobParams>(executeDCASwapJobDef.jobName, {
const job = agenda.create<JobParams>(jobName, {
...data,
updatedAt: new Date(),
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const BASE_CHAIN_ID = 8453;
export const BASE_USDC_ADDRESS = '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913';
export const BASE_WBTC_ADDRESS = '0x0555E30da8f98308EdB960aa94C0Db47230d2B9c';
export const BASE_UNISWAP_V3_ROUTER = '0x2626664c2603336E57B271c5C0b26F421741e481';
Loading