Skip to content

Commit dce94fe

Browse files
Merge pull request #5 from techops-services/KEEP-1136-remove-unecessary-actions
Keep 1136 Initial Web3 integration
2 parents e96d4da + ebd79eb commit dce94fe

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+1516
-5539
lines changed

PARA_INTEGRATION.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Para Wallet Integration
2+
3+
## Overview
4+
5+
This integration automatically creates and manages Para wallets for users, enabling server-side blockchain operations within workflow steps.
6+
7+
## What This Does
8+
9+
1. **Automatic Wallet Creation**: When a user signs up, a Para wallet is automatically created for them
10+
2. **Secure Storage**: Wallet keyshares are encrypted with AES-256-GCM and stored securely in the database
11+
3. **Server-Side Access**: Workflow steps can use the user's wallet to perform blockchain operations (sending transactions, signing messages, interacting with smart contracts, etc.)
12+
4. **User Visibility**: Users can see their wallet address displayed in the user dropdown menu
13+
14+
## How It Works
15+
16+
### Wallet Creation Flow
17+
1. **User signs up** → Better Auth triggers a database hook
18+
- File: `lib/auth.ts`
19+
- Hook: `databaseHooks.user.create.after`
20+
21+
2. **Database hook calls Para SDK** to create a pregenerated wallet
22+
- `const wallet = await paraClient.createPregenWallet({ type: "EVM", pregenId: { email } })`
23+
- Returns: `{ id, address, ... }`
24+
25+
3. **Wallet keyshare is encrypted** and stored in database
26+
- File: `lib/encryption.ts`
27+
- Function: `encryptUserShare(userShare)`
28+
- Stored in: `para_wallets` table
29+
30+
4. **Wallet address is displayed** to the user
31+
- File: `components/workflows/user-menu.tsx`
32+
- Shown in user dropdown menu
33+
34+
### Workflow Step Usage
35+
When a workflow step needs to use the wallet:
36+
1. Fetch encrypted keyshare from database (using authenticated userId)
37+
2. Decrypt the keyshare
38+
3. Initialize Para signer for blockchain operations
39+
4. Perform the operation (send transaction, sign message, call smart contract, etc.)
40+
41+
## Key Components
42+
43+
### Database
44+
- **Table**: `para_wallets`
45+
- **Fields**: userId (unique), email, walletId, walletAddress, encrypted userShare, createdAt
46+
- **Security**: One wallet per user, foreign key to users table, cascading delete
47+
48+
### Encryption
49+
- **Algorithm**: AES-256-GCM (authenticated encryption)
50+
- **Key Storage**: Environment variable `WALLET_ENCRYPTION_KEY`
51+
- **Format**: `iv:authTag:encryptedData` (prevents tampering)
52+
53+
### Helper Functions
54+
- `getUserWallet(userId)` - Retrieve wallet from database
55+
- `initializeParaSigner(userId, rpcUrl)` - Get ready-to-use signer for transactions
56+
- `getUserWalletAddress(userId)` - Get wallet address for display
57+
- `userHasWallet(userId)` - Check if user has wallet
58+
59+
## What You Can Build
60+
61+
Workflow steps that use the user's wallet can:
62+
- Send ETH or ERC-20 tokens
63+
- Interact with smart contracts (DeFi, NFTs, DAOs)
64+
- Sign messages or data
65+
- Check balances and token ownership
66+
- Execute any blockchain operation the user authorizes
67+
68+
## Key Files
69+
70+
- `lib/auth.ts` - Better Auth configuration with wallet creation hook
71+
- `lib/db/schema.ts` - Database schema for `para_wallets` table
72+
- `lib/encryption.ts` - AES-256-GCM encryption/decryption utilities
73+
- `lib/para/wallet-helpers.ts` - Helper functions for wallet operations
74+
- `app/api/user/route.ts` - API endpoint that includes wallet address
75+
- `components/workflows/user-menu.tsx` - UI component displaying wallet address
76+
77+
## Environment Variables Required
78+
79+
```env
80+
PARA_API_KEY=your-para-api-key
81+
PARA_ENVIRONMENT=beta # or 'prod'
82+
WALLET_ENCRYPTION_KEY=64-character-hex-string
83+
```
84+
85+
## Security Notes
86+
87+
- All Para SDK operations happen server-side only
88+
- userShare never transmitted to browser
89+
- Encryption key stored only in environment variables
90+
- Each user can only access their own wallet (enforced by userId authentication)

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 & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,18 +79,10 @@ Visit [http://localhost:3000](http://localhost:3000) to get started.
7979
### Action Nodes
8080

8181
<!-- PLUGINS:START - Do not remove. Auto-generated by discover-plugins -->
82-
- **AI Gateway**: Generate Text, Generate Image
83-
- **Blob**: Put Blob, List Blobs
84-
- **fal.ai**: Generate Image, Generate Video, Upscale Image, Remove Background, Image to Image
85-
- **Firecrawl**: Scrape URL, Search Web
86-
- **GitHub**: Create Issue, List Issues, Get Issue, Update Issue
87-
- **Linear**: Create Ticket, Find Issues
88-
- **Perplexity**: Search Web, Ask Question, Research Topic
8982
- **Resend**: Send Email
9083
- **Slack**: Send Slack Message
91-
- **Stripe**: Create Customer, Get Customer, Create Invoice
92-
- **Superagent**: Guard, Redact
9384
- **v0**: Create Chat, Send Message
85+
- **Web3**: Transfer Funds
9486
<!-- PLUGINS:END -->
9587

9688
## Code Generation

app/api/user/route.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { NextResponse } from "next/server";
33
import { auth } from "@/lib/auth";
44
import { db } from "@/lib/db";
55
import { accounts, users } from "@/lib/db/schema";
6+
import { getUserWallet } from "@/lib/para/wallet-helpers";
67

78
export async function GET(request: Request) {
89
try {
@@ -37,9 +38,20 @@ export async function GET(request: Request) {
3738
},
3839
});
3940

41+
// Get the user's wallet address (if exists)
42+
let walletAddress: string | null = null;
43+
try {
44+
const wallet = await getUserWallet(session.user.id);
45+
walletAddress = wallet.walletAddress;
46+
} catch (_error) {
47+
// User doesn't have a wallet yet, that's ok
48+
walletAddress = null;
49+
}
50+
4051
return NextResponse.json({
4152
...userData,
4253
providerId: userAccount?.providerId ?? null,
54+
walletAddress,
4355
});
4456
} catch (error) {
4557
console.error("Failed to get user:", error);

app/api/user/wallet/route.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { NextResponse } from "next/server";
2+
import { auth } from "@/lib/auth";
3+
import { getUserWallet, userHasWallet } from "@/lib/para/wallet-helpers";
4+
5+
export async function GET(request: Request) {
6+
try {
7+
const session = await auth.api.getSession({
8+
headers: request.headers,
9+
});
10+
11+
if (!session?.user) {
12+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
13+
}
14+
15+
const hasWallet = await userHasWallet(session.user.id);
16+
17+
if (!hasWallet) {
18+
return NextResponse.json({
19+
hasWallet: false,
20+
message: "No Para wallet found for this user",
21+
});
22+
}
23+
24+
const wallet = await getUserWallet(session.user.id);
25+
26+
return NextResponse.json({
27+
hasWallet: true,
28+
walletAddress: wallet.walletAddress,
29+
walletId: wallet.walletId,
30+
email: wallet.email,
31+
createdAt: wallet.createdAt,
32+
});
33+
} catch (error) {
34+
console.error("Failed to get wallet:", error);
35+
return NextResponse.json(
36+
{
37+
error: error instanceof Error ? error.message : "Failed to get wallet",
38+
},
39+
{ status: 500 }
40+
);
41+
}
42+
}

components/settings/integration-form-dialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ export function IntegrationFormDialog({
226226
<div className="flex items-center gap-2">
227227
<IntegrationIcon
228228
className="size-4"
229-
integration={type === "ai-gateway" ? "vercel" : type}
229+
integration={type}
230230
/>
231231
{getLabel(type)}
232232
</div>

components/settings/integrations-manager.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,7 @@ export function IntegrationsManager({
130130
<div className="flex items-center gap-3">
131131
<IntegrationIcon
132132
className="size-8"
133-
integration={
134-
integration.type === "ai-gateway"
135-
? "vercel"
136-
: integration.type
137-
}
133+
integration={integration.type}
138134
/>
139135
<div>
140136
<p className="font-medium text-sm">{integration.name}</p>

components/workflows/user-menu.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,21 @@ export const UserMenu = () => {
3333
const [settingsOpen, setSettingsOpen] = useState(false);
3434
const [integrationsOpen, setIntegrationsOpen] = useState(false);
3535
const [providerId, setProviderId] = useState<string | null>(null);
36+
const [walletAddress, setWalletAddress] = useState<string | null>(null);
3637

37-
// Fetch provider info when session is available
38+
// Fetch provider info and wallet when session is available
3839
useEffect(() => {
3940
if (session?.user && !session.user.name?.startsWith("Anonymous")) {
4041
api.user
4142
.get()
42-
.then((user) => setProviderId(user.providerId))
43-
.catch(() => setProviderId(null));
43+
.then((user) => {
44+
setProviderId(user.providerId);
45+
setWalletAddress(user.walletAddress || null);
46+
})
47+
.catch(() => {
48+
setProviderId(null);
49+
setWalletAddress(null);
50+
});
4451
}
4552
}, [session?.user]);
4653

@@ -128,6 +135,11 @@ export const UserMenu = () => {
128135
<p className="text-muted-foreground text-xs leading-none">
129136
{session?.user?.email}
130137
</p>
138+
{walletAddress && (
139+
<p className="font-mono text-muted-foreground text-xs leading-none">
140+
{walletAddress.slice(0, 6)}...{walletAddress.slice(-4)}
141+
</p>
142+
)}
131143
</div>
132144
</DropdownMenuLabel>
133145
<DropdownMenuSeparator />

lib/api-client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ export const userApi = {
377377
image: string | null;
378378
isAnonymous: boolean | null;
379379
providerId: string | null;
380+
walletAddress: string | null;
380381
}>("/api/user"),
381382

382383
update: (data: { name?: string; email?: string }) =>

lib/auth.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Environment, Para as ParaServer } from "@getpara/server-sdk";
12
import { betterAuth } from "better-auth";
23
import { drizzleAdapter } from "better-auth/adapters/drizzle";
34
import { anonymous, genericOAuth } from "better-auth/plugins";
@@ -6,6 +7,7 @@ import { db } from "./db";
67
import {
78
accounts,
89
integrations,
10+
paraWallets,
911
sessions,
1012
users,
1113
verifications,
@@ -14,6 +16,7 @@ import {
1416
workflowExecutionsRelations,
1517
workflows,
1618
} from "./db/schema";
19+
import { encryptUserShare } from "./encryption";
1720

1821
// Construct schema object for drizzle adapter
1922
const schema = {
@@ -157,4 +160,80 @@ export const auth = betterAuth({
157160
},
158161
},
159162
plugins,
163+
164+
// Database hooks for automatic Para wallet creation
165+
databaseHooks: {
166+
user: {
167+
create: {
168+
after: async (user) => {
169+
// Skip wallet creation if no email
170+
if (!user.email) {
171+
console.log("[Para] Skipping wallet creation - no email");
172+
return;
173+
}
174+
175+
console.log(`[Para] Creating wallet for user: ${user.email}`);
176+
177+
try {
178+
const PARA_API_KEY = process.env.PARA_API_KEY;
179+
const PARA_ENV = process.env.PARA_ENVIRONMENT || "beta";
180+
181+
if (!PARA_API_KEY) {
182+
console.warn("[Para] PARA_API_KEY not configured");
183+
return;
184+
}
185+
186+
// Initialize Para SDK
187+
const paraClient = new ParaServer(
188+
PARA_ENV === "prod" ? Environment.PROD : Environment.BETA,
189+
PARA_API_KEY
190+
);
191+
192+
// Check if wallet already exists
193+
const hasWallet = await paraClient.hasPregenWallet({
194+
pregenId: { email: user.email },
195+
});
196+
197+
if (hasWallet) {
198+
console.log(`[Para] Wallet already exists for ${user.email}`);
199+
return;
200+
}
201+
202+
// Create pregenerated wallet
203+
const wallet = await paraClient.createPregenWallet({
204+
type: "EVM",
205+
pregenId: { email: user.email },
206+
});
207+
208+
// Get user's cryptographic share
209+
const userShare = await paraClient.getUserShare();
210+
211+
if (!userShare) {
212+
throw new Error("Failed to get user share from Para");
213+
}
214+
215+
if (!(wallet.id && wallet.address)) {
216+
throw new Error("Invalid wallet data from Para");
217+
}
218+
219+
// Store encrypted wallet in database
220+
await db.insert(paraWallets).values({
221+
userId: user.id,
222+
email: user.email,
223+
walletId: wallet.id, // v2 API uses wallet.id instead of wallet.walletId
224+
walletAddress: wallet.address,
225+
userShare: encryptUserShare(userShare), // Encrypted!
226+
});
227+
228+
console.log(
229+
`[Para] ✓ Wallet created successfully: ${wallet.address}`
230+
);
231+
} catch (error) {
232+
console.error("[Para] Failed to create wallet:", error);
233+
// Don't throw - let signup succeed even if wallet creation fails
234+
}
235+
},
236+
},
237+
},
238+
},
160239
});

0 commit comments

Comments
 (0)