Skip to content

Commit 0bd56c6

Browse files
committed
KEEP-1138 Transfer funds implemented
1 parent 00a3a84 commit 0bd56c6

File tree

8 files changed

+411
-1034
lines changed

8 files changed

+411
-1034
lines changed

PARA_INTEGRATION.md

Lines changed: 66 additions & 1016 deletions
Large diffs are not rendered by default.

PARA_WORKFLOW_STEP.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Para Wallet - Send Sepolia ETH Workflow Step
2+
3+
## Goal
4+
5+
Create a workflow step that allows users to send ETH on the Sepolia testnet using their Para wallet.
6+
7+
## File locations
8+
9+
- Plugin definition: plugins/[name]/index.ts
10+
- Step implementation: plugins/[name]/steps/[step-name].ts
11+
- Step registry: lib/step-registry.ts (auto-generated)
12+
- Workflow executor: lib/workflow-executor.workflow.ts
13+
- Step handler utilities: lib/steps/step-handler.ts
14+
15+
## For this step
16+
17+
You'll need to:
18+
19+
- Create plugins/para/index.ts — register the plugin
20+
- Create plugins/para/steps/send-eth.ts — step implementation
21+
- Access userId — look up from executionId via workflowExecutions table
22+
- Use wallet helpers — initializeParaSigner(userId, rpcUrl) from lib/para/wallet-helpers.ts
23+
- Define config fields — amount and recipientAddress in plugin registration
24+
- The PARA integration already exists (wallet creation, encryption, helpers), so you're adding a step that uses it.
25+
26+
## Current Status
27+
28+
- ✅ Para wallet integration is complete (wallets auto-created on user signup)
29+
- ✅ Wallet addresses display in user dropdown menu
30+
31+
## What Works
32+
33+
1. Users automatically get a Para wallet when they sign up
34+
2. Wallet keyshares are encrypted and stored securely in the database
35+
36+
## What Doesn't Work
37+
38+
Nothing about the step is yet implemented
39+
40+
## The Issue
41+
42+
## Next Steps
43+
44+
## Key Files

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ Visit [http://localhost:3000](http://localhost:3000) to get started.
9191
- **Stripe**: Create Customer, Get Customer, Create Invoice
9292
- **Superagent**: Guard, Redact
9393
- **v0**: Create Chat, Send Message
94+
- **Web3**: Transfer Funds
9495
<!-- PLUGINS:END -->
9596

9697
## Code Generation

plugins/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
* To remove an integration:
1313
* 1. Delete the plugin directory
1414
* 2. Run: pnpm discover-plugins (or it runs automatically on build)
15+
*
16+
* Discovered plugins: ai-gateway, blob, fal, firecrawl, github, linear, perplexity, resend, slack, stripe, superagent, v0, web3
1517
*/
1618

1719
import "./ai-gateway";
@@ -26,6 +28,7 @@ import "./slack";
2628
import "./stripe";
2729
import "./superagent";
2830
import "./v0";
31+
import "./web3";
2932

3033
export type {
3134
ActionConfigField,

plugins/web3/icon.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export function Web3Icon({ className }: { className?: string }) {
2+
return (
3+
<svg
4+
aria-label="Web3"
5+
className={className}
6+
fill="currentColor"
7+
viewBox="0 0 24 24"
8+
xmlns="http://www.w3.org/2000/svg"
9+
>
10+
<title>Web3</title>
11+
{/* Blockchain/connected blocks icon */}
12+
<path d="M12 2L2 7v10l10 5 10-5V7L12 2zm0 2.18l8 4v8.64l-8 4-8-4V8.18l8-4z" />
13+
<path d="M12 6L6 9v6l6 3 6-3V9l-6-3zm0 2.18l3.82 1.91v3.82L12 15.82l-3.82-1.91V10.09L12 8.18z" />
14+
</svg>
15+
);
16+
}

plugins/web3/index.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type { IntegrationPlugin } from "../registry";
2+
import { registerIntegration } from "../registry";
3+
import { Web3Icon } from "./icon";
4+
5+
const web3Plugin: IntegrationPlugin = {
6+
type: "web3",
7+
label: "Web3",
8+
description: "Interact with blockchain networks - transfer funds, read/write smart contracts, and more",
9+
10+
icon: Web3Icon,
11+
12+
// Minimal form field - Web3 uses PARA wallet (auto-created for users)
13+
// This field is informational only and not used
14+
formFields: [
15+
{
16+
id: "info",
17+
label: "Note",
18+
type: "text",
19+
placeholder: "Web3 uses your PARA wallet (auto-created on signup)",
20+
configKey: "info",
21+
helpText: "No configuration needed. Your PARA wallet is automatically available.",
22+
},
23+
],
24+
25+
// No test function needed - no credentials to test
26+
// testConfig is optional, so we omit it
27+
28+
actions: [
29+
{
30+
slug: "transfer-funds",
31+
label: "Transfer Funds",
32+
description: "Transfer ETH from your wallet to a recipient address",
33+
category: "Web3",
34+
stepFunction: "transferFundsStep",
35+
stepImportPath: "transfer-funds",
36+
configFields: [
37+
{
38+
key: "amount",
39+
label: "Amount (ETH)",
40+
type: "template-input",
41+
placeholder: "0.1 or {{NodeName.amount}}",
42+
example: "0.1",
43+
required: true,
44+
},
45+
{
46+
key: "recipientAddress",
47+
label: "Recipient Address",
48+
type: "template-input",
49+
placeholder: "0x... or {{NodeName.address}}",
50+
example: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
51+
required: true,
52+
},
53+
],
54+
},
55+
],
56+
};
57+
58+
// Auto-register on import
59+
registerIntegration(web3Plugin);
60+
61+
export default web3Plugin;
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import "server-only";
2+
3+
import { db } from "@/lib/db";
4+
import { workflowExecutions } from "@/lib/db/schema";
5+
import { eq } from "drizzle-orm";
6+
import { ethers } from "ethers";
7+
import { initializeParaSigner } from "@/lib/para/wallet-helpers";
8+
import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
9+
import { getErrorMessage } from "@/lib/utils";
10+
11+
type TransferFundsResult =
12+
| { success: true; transactionHash: string }
13+
| { success: false; error: string };
14+
15+
export type TransferFundsCoreInput = {
16+
amount: string;
17+
recipientAddress: string;
18+
};
19+
20+
export type TransferFundsInput = StepInput & TransferFundsCoreInput;
21+
22+
/**
23+
* Get userId from executionId by querying the workflowExecutions table
24+
*/
25+
async function getUserIdFromExecution(
26+
executionId: string | undefined
27+
): Promise<string> {
28+
if (!executionId) {
29+
throw new Error("Execution ID is required to get user ID");
30+
}
31+
32+
const execution = await db
33+
.select({ userId: workflowExecutions.userId })
34+
.from(workflowExecutions)
35+
.where(eq(workflowExecutions.id, executionId))
36+
.limit(1);
37+
38+
if (execution.length === 0) {
39+
throw new Error(`Execution not found: ${executionId}`);
40+
}
41+
42+
return execution[0].userId;
43+
}
44+
45+
/**
46+
* Core transfer logic
47+
*/
48+
async function stepHandler(
49+
input: TransferFundsInput
50+
): Promise<TransferFundsResult> {
51+
console.log("[Transfer Funds] Starting step with input:", {
52+
amount: input.amount,
53+
recipientAddress: input.recipientAddress,
54+
hasContext: !!input._context,
55+
executionId: input._context?.executionId,
56+
});
57+
58+
const { amount, recipientAddress, _context } = input;
59+
60+
// Validate recipient address
61+
if (!ethers.isAddress(recipientAddress)) {
62+
console.error("[Transfer Funds] Invalid recipient address:", recipientAddress);
63+
return {
64+
success: false,
65+
error: `Invalid recipient address: ${recipientAddress}`,
66+
};
67+
}
68+
69+
// Validate amount
70+
if (!amount || amount.trim() === "") {
71+
console.error("[Transfer Funds] Amount is missing");
72+
return {
73+
success: false,
74+
error: "Amount is required",
75+
};
76+
}
77+
78+
let amountInWei: bigint;
79+
try {
80+
amountInWei = ethers.parseEther(amount);
81+
console.log("[Transfer Funds] Amount parsed:", { amount, amountInWei: amountInWei.toString() });
82+
} catch (error) {
83+
console.error("[Transfer Funds] Failed to parse amount:", error);
84+
return {
85+
success: false,
86+
error: `Invalid amount format: ${getErrorMessage(error)}`,
87+
};
88+
}
89+
90+
// Get userId from executionId (passed via _context)
91+
if (!_context?.executionId) {
92+
console.error("[Transfer Funds] No execution ID in context");
93+
return {
94+
success: false,
95+
error: "Execution ID is required to identify the user",
96+
};
97+
}
98+
99+
let userId: string;
100+
try {
101+
console.log("[Transfer Funds] Looking up user from execution:", _context.executionId);
102+
userId = await getUserIdFromExecution(_context.executionId);
103+
console.log("[Transfer Funds] Found userId:", userId);
104+
} catch (error) {
105+
console.error("[Transfer Funds] Failed to get user ID:", error);
106+
return {
107+
success: false,
108+
error: `Failed to get user ID: ${getErrorMessage(error)}`,
109+
};
110+
}
111+
112+
// Sepolia testnet RPC URL
113+
// TODO: Make this configurable in the future
114+
const SEPOLIA_RPC_URL = "https://chain.techops.services/eth-sepolia";
115+
116+
let signer;
117+
try {
118+
console.log("[Transfer Funds] Initializing Para signer for user:", userId);
119+
signer = await initializeParaSigner(userId, SEPOLIA_RPC_URL);
120+
const signerAddress = await signer.getAddress();
121+
console.log("[Transfer Funds] Signer initialized successfully:", signerAddress);
122+
} catch (error) {
123+
console.error("[Transfer Funds] Failed to initialize wallet:", error);
124+
return {
125+
success: false,
126+
error: `Failed to initialize wallet: ${getErrorMessage(error)}`,
127+
};
128+
}
129+
130+
// Send transaction
131+
try {
132+
console.log("[Transfer Funds] Sending transaction:", {
133+
to: recipientAddress,
134+
value: amountInWei.toString(),
135+
});
136+
137+
const tx = await signer.sendTransaction({
138+
to: recipientAddress,
139+
value: amountInWei,
140+
});
141+
142+
console.log("[Transfer Funds] Transaction sent, hash:", tx.hash);
143+
console.log("[Transfer Funds] Waiting for confirmation...");
144+
145+
// Wait for transaction to be mined
146+
const receipt = await tx.wait();
147+
148+
if (!receipt) {
149+
console.error("[Transfer Funds] No receipt received");
150+
return {
151+
success: false,
152+
error: "Transaction sent but receipt not available",
153+
};
154+
}
155+
156+
console.log("[Transfer Funds] Transaction confirmed:", {
157+
hash: receipt.hash,
158+
status: receipt.status,
159+
blockNumber: receipt.blockNumber,
160+
});
161+
162+
return {
163+
success: true,
164+
transactionHash: receipt.hash,
165+
};
166+
} catch (error) {
167+
console.error("[Transfer Funds] Transaction failed:", error);
168+
return {
169+
success: false,
170+
error: `Transaction failed: ${getErrorMessage(error)}`,
171+
};
172+
}
173+
}
174+
175+
/**
176+
* Transfer Funds Step
177+
* Transfers ETH from the user's wallet to a recipient address
178+
*/
179+
export async function transferFundsStep(
180+
input: TransferFundsInput
181+
): Promise<TransferFundsResult> {
182+
"use step";
183+
184+
return withStepLogging(input, () => stepHandler(input));
185+
}
186+
187+
export const _integrationType = "web3";

0 commit comments

Comments
 (0)