Skip to content

Commit 926fdcd

Browse files
committed
chore(dca-backend): bump uniswap ability to version 8.0.0
1 parent cb5e095 commit 926fdcd

File tree

7 files changed

+204
-302
lines changed

7 files changed

+204
-302
lines changed

README.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ This project demonstrates how to schedule and execute recurring DCA (Dollar-Cost
99
- Node ^22.16.0
1010
- pnpm ^10.7.0
1111
- Docker or a local MongoDB instance
12-
- A Vincent App with ERC20 approval and Uniswap swap abilities
12+
- A Vincent App with Uniswap swap ability
1313

1414
## Monorepo Structure
1515

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

2726
## Packages
@@ -45,11 +44,10 @@ To run this code and sign on behalf of your delegators, create your own Vincent
4544

4645
1. Go to the [Vincent Dashboard](https://dashboard.heyvincent.ai/) and log in as a builder.
4746
2. Create a new app similar to [wBTC DCA](https://dashboard.heyvincent.ai/user/appId/9796398001/connect).
48-
3. Add the ERC20 Approval ability.
49-
4. Add the Uniswap Swap ability.
50-
5. Publish the app.
51-
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.
52-
7. Once deployed, you'll need to update the `App User URL` and `Redirect URIs` to the URL deployed from Railway.
47+
3. Add the Uniswap Swap ability.
48+
4. Publish the app.
49+
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.
50+
6. Once deployed, you'll need to update the `App User URL` and `Redirect URIs` to the URL deployed from Railway.
5351

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

packages/dca-backend/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,8 @@
4949
"@lit-protocol/contracts-sdk": "^7.3.0",
5050
"@lit-protocol/lit-node-client": "^7.3.0",
5151
"@lit-protocol/types": "^7.3.0",
52-
"@lit-protocol/vincent-ability-erc20-approval": "3.1.0",
53-
"@lit-protocol/vincent-ability-uniswap-swap": "5.0.0",
54-
"@lit-protocol/vincent-app-sdk": "2.2.1",
52+
"@lit-protocol/vincent-ability-uniswap-swap": "8.0.0",
53+
"@lit-protocol/vincent-app-sdk": "2.2.2",
5554
"@lit-protocol/vincent-contracts-sdk": "^1.0.1",
5655
"@lit-protocol/vincent-scaffold-sdk": "1.1.9-mma",
5756
"@noble/secp256k1": "^2.2.3",

packages/dca-backend/src/lib/agenda/jobs/executeDCASwap/executeDCASwap.ts

Lines changed: 70 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import consola from 'consola';
44
import { ethers } from 'ethers';
55

66
import { IRelayPKP } from '@lit-protocol/types';
7+
import { AbilityAction } from '@lit-protocol/vincent-ability-uniswap-swap';
78

89
import { type AppData, assertPermittedVersion } from '../jobVersion';
910
import {
@@ -15,11 +16,7 @@ import {
1516
getUserPermittedVersion,
1617
handleOperationExecution,
1718
} from './utils';
18-
import {
19-
getErc20ApprovalToolClient,
20-
getSignedUniswapQuote,
21-
getUniswapToolClient,
22-
} from './vincentAbilities';
19+
import { getSignedUniswapQuote, getUniswapAbilityClient } from './vincentAbilities';
2320
import { env } from '../../../env';
2421
import { normalizeError } from '../../../error';
2522
import { PurchasedCoin } from '../../../mongo/models/PurchasedCoin';
@@ -36,69 +33,22 @@ export type JobParams = {
3633

3734
const { BASE_RPC_URL, VINCENT_APP_ID } = env;
3835

39-
const BASE_CHAIN_ID = 8453;
4036
const BASE_USDC_ADDRESS = '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913';
4137
const BASE_WBTC_ADDRESS = '0x0555E30da8f98308EdB960aa94C0Db47230d2B9c';
42-
const BASE_UNISWAP_V3_ROUTER = '0x2626664c2603336E57B271c5C0b26F421741e481';
4338

4439
const baseProvider = new ethers.providers.StaticJsonRpcProvider(BASE_RPC_URL);
4540
const usdcContract = getERC20Contract(BASE_USDC_ADDRESS, baseProvider);
4641

47-
async function addUsdcApproval({
48-
ethAddress,
49-
usdcAmount,
50-
}: {
51-
ethAddress: `0x${string}`;
52-
usdcAmount: ethers.BigNumber;
53-
}): Promise<`0x${string}` | undefined> {
54-
const erc20ApprovalToolClient = getErc20ApprovalToolClient();
55-
const approvalParams = {
56-
alchemyGasSponsor,
57-
alchemyGasSponsorApiKey,
58-
alchemyGasSponsorPolicyId,
59-
chainId: BASE_CHAIN_ID,
60-
rpcUrl: BASE_RPC_URL,
61-
spenderAddress: BASE_UNISWAP_V3_ROUTER,
62-
tokenAddress: BASE_USDC_ADDRESS,
63-
tokenAmount: usdcAmount.mul(5).toString(), // Approve 5x the amount to spend so we don't wait for approval tx's every time we run
64-
};
65-
const approvalContext = {
66-
delegatorPkpEthAddress: ethAddress,
67-
};
68-
69-
// Running precheck to prevent sending approval tx if not needed or will fail
70-
const approvalPrecheckResult = await erc20ApprovalToolClient.precheck(
71-
approvalParams,
72-
approvalContext
73-
);
74-
if (!approvalPrecheckResult.success) {
75-
throw new Error(`ERC20 approval tool precheck failed: ${approvalPrecheckResult}`);
76-
} else if (approvalPrecheckResult.result.alreadyApproved) {
77-
// No need to send tx, allowance is already at that amount
78-
return undefined;
79-
}
80-
81-
// Sending approval tx
82-
const approvalExecutionResult = await erc20ApprovalToolClient.execute(
83-
approvalParams,
84-
approvalContext
85-
);
86-
consola.trace('ERC20 Approval Vincent Tool Response:', approvalExecutionResult);
87-
if (!approvalExecutionResult.success) {
88-
throw new Error(`ERC20 approval tool execution failed: ${approvalExecutionResult}`);
89-
}
90-
91-
return approvalExecutionResult.result.approvalTxHash as `0x${string}`;
92-
}
93-
9442
async function handleSwapExecution({
9543
delegatorAddress,
44+
pkpPublicKey,
9645
tokenInAddress,
9746
tokenInAmount,
9847
tokenInDecimals,
9948
tokenOutAddress,
10049
}: {
10150
delegatorAddress: `0x${string}`;
51+
pkpPublicKey: `0x${string}`;
10252
tokenInAddress: `0x${string}`;
10353
tokenInAmount: ethers.BigNumber;
10454
tokenInDecimals: number;
@@ -112,27 +62,75 @@ async function handleSwapExecution({
11262
tokenInAmount: ethers.utils.formatUnits(tokenInAmount, tokenInDecimals),
11363
});
11464

115-
const uniswapToolClient = getUniswapToolClient();
116-
const swapParams = {
65+
const uniswapToolClient = getUniswapAbilityClient();
66+
const swapContext = {
67+
delegatorPkpEthAddress: delegatorAddress,
68+
};
69+
70+
const approveParams = {
71+
alchemyGasSponsor,
72+
alchemyGasSponsorApiKey,
73+
alchemyGasSponsorPolicyId,
11774
signedUniswapQuote,
75+
action: AbilityAction.Approve as 'approve',
11876
rpcUrlForUniswap: BASE_RPC_URL,
11977
};
120-
const swapContext = {
121-
delegatorPkpEthAddress: delegatorAddress,
78+
79+
const approvePrecheckResult = await uniswapToolClient.precheck(approveParams, swapContext);
80+
consola.trace('Uniswap Approve Precheck Response:', approvePrecheckResult);
81+
if (!approvePrecheckResult.success) {
82+
throw new Error(`Uniswap approve precheck failed: ${approvePrecheckResult.result?.reason}`);
83+
}
84+
85+
const approveExecutionResult = await uniswapToolClient.execute(approveParams, swapContext);
86+
consola.trace('Uniswap Approve Vincent Tool Response:', approveExecutionResult);
87+
if (approveExecutionResult.success === false) {
88+
throw new Error(`Uniswap tool approval failed: ${approveExecutionResult.runtimeError}`);
89+
}
90+
91+
const approveResult = approveExecutionResult.result!;
92+
const approveOperationHash = (approveResult.approvalTxUserOperationHash ||
93+
approveResult.approvalTxHash) as `0x${string}` | undefined;
94+
95+
if (approveOperationHash) {
96+
consola.debug('Waiting for approval transaction to be mined...');
97+
await handleOperationExecution({
98+
pkpPublicKey,
99+
isSponsored: alchemyGasSponsor,
100+
operationHash: approveOperationHash,
101+
provider: baseProvider,
102+
});
103+
consola.debug('Approval transaction mined successfully');
104+
} else {
105+
consola.debug('Approval already sufficient, no transaction needed');
106+
}
107+
108+
const swapParams = {
109+
alchemyGasSponsor,
110+
alchemyGasSponsorApiKey,
111+
alchemyGasSponsorPolicyId,
112+
signedUniswapQuote,
113+
action: AbilityAction.Swap as 'swap',
114+
rpcUrlForUniswap: BASE_RPC_URL,
122115
};
123116

124117
const swapPrecheckResult = await uniswapToolClient.precheck(swapParams, swapContext);
118+
consola.trace('Uniswap Swap Precheck Response:', swapPrecheckResult);
125119
if (!swapPrecheckResult.success) {
126-
throw new Error(`Uniswap tool precheck failed: ${swapPrecheckResult}`);
120+
throw new Error(`Uniswap swap precheck failed: ${swapPrecheckResult.result?.reason}`);
127121
}
128122

129123
const swapExecutionResult = await uniswapToolClient.execute(swapParams, swapContext);
130124
consola.trace('Uniswap Swap Vincent Tool Response:', swapExecutionResult);
131-
if (!swapExecutionResult.success) {
132-
throw new Error(`Uniswap tool execution failed: ${swapExecutionResult}`);
125+
if (swapExecutionResult.success === false) {
126+
throw new Error(`Uniswap tool execution failed: ${swapExecutionResult.runtimeError}`);
133127
}
134128

135-
return swapExecutionResult.result.swapTxHash as `0x${string}`;
129+
const result = swapExecutionResult.result!;
130+
const operationHash = (result.swapTxUserOperationHash ||
131+
result.swapTxHash) as `0x${string}`;
132+
133+
return operationHash;
136134
}
137135

138136
export async function executeDCASwap(job: JobType, sentryScope: Sentry.Scope): Promise<void> {
@@ -200,32 +198,22 @@ export async function executeDCASwap(job: JobType, sentryScope: Sentry.Scope): P
200198
usdcBalance: ethers.utils.formatUnits(usdcBalance, 6),
201199
});
202200

203-
const approvalHash = await addUsdcApproval({
204-
ethAddress: ethAddress as `0x${string}`,
205-
usdcAmount: _purchaseAmount,
206-
});
207-
sentryScope.addBreadcrumb({
208-
data: {
209-
approvalHash,
210-
},
211-
});
212-
213-
if (approvalHash) {
214-
await handleOperationExecution({
215-
isSponsored: alchemyGasSponsor,
216-
operationHash: approvalHash,
217-
pkpPublicKey: publicKey,
218-
provider: baseProvider,
219-
});
220-
}
221-
222-
const swapHash = await handleSwapExecution({
201+
const swapOperationHash = await handleSwapExecution({
223202
delegatorAddress: ethAddress as `0x${string}`,
203+
pkpPublicKey: publicKey as `0x${string}`,
224204
tokenInAddress: BASE_USDC_ADDRESS,
225205
tokenInAmount: _purchaseAmount,
226206
tokenInDecimals: 6,
227207
tokenOutAddress: BASE_WBTC_ADDRESS,
228208
});
209+
210+
const { txHash: swapHash } = await handleOperationExecution({
211+
isSponsored: alchemyGasSponsor,
212+
operationHash: swapOperationHash,
213+
pkpPublicKey: publicKey,
214+
provider: baseProvider,
215+
});
216+
229217
sentryScope.addBreadcrumb({
230218
data: {
231219
swapHash,

packages/dca-backend/src/lib/agenda/jobs/executeDCASwap/utils/handle-operation-execution.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export async function handleOperationExecution({
2323
let useropHash;
2424
if (!isSponsored) {
2525
txHash = operationHash;
26+
await waitForTransaction({ confirmations, provider, transactionHash: txHash });
2627
} else {
2728
useropHash = operationHash;
2829
txHash = await waitForUserOperation({

packages/dca-backend/src/lib/agenda/jobs/executeDCASwap/vincentAbilities.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { LitNodeClient } from '@lit-protocol/lit-node-client';
2-
import { bundledVincentAbility as erc20ApprovalBundledVincentAbility } from '@lit-protocol/vincent-ability-erc20-approval';
32
import {
43
bundledVincentAbility as uniswapSwapBundledVincentAbility,
54
getSignedUniswapQuote as getSignedUniswapQuoteAction,
@@ -29,14 +28,7 @@ export async function getSignedUniswapQuote(
2928
});
3029
}
3130

32-
export function getErc20ApprovalToolClient() {
33-
return getVincentAbilityClient({
34-
bundledVincentAbility: erc20ApprovalBundledVincentAbility,
35-
ethersSigner: delegateeSigner,
36-
});
37-
}
38-
39-
export function getUniswapToolClient() {
31+
export function getUniswapAbilityClient() {
4032
return getVincentAbilityClient({
4133
bundledVincentAbility: uniswapSwapBundledVincentAbility,
4234
ethersSigner: delegateeSigner,

packages/dca-frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"@lit-protocol/constants": "^7.3.0",
2727
"@lit-protocol/lit-node-client": "^7.3.0",
2828
"@lit-protocol/types": "^7.3.0",
29-
"@lit-protocol/vincent-app-sdk": "2.0.2-mma",
29+
"@lit-protocol/vincent-app-sdk": "2.2.2",
3030
"@radix-ui/react-checkbox": "^1.1.4",
3131
"@radix-ui/react-dialog": "^1.1.6",
3232
"@radix-ui/react-dropdown-menu": "^2.1.6",

0 commit comments

Comments
 (0)