Skip to content

Commit 00a3a84

Browse files
committed
Pre-gen wallet for user when they create account
1 parent fca834c commit 00a3a84

File tree

10 files changed

+2100
-3
lines changed

10 files changed

+2100
-3
lines changed

PARA_INTEGRATION.md

Lines changed: 1040 additions & 0 deletions
Large diffs are not rendered by default.

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/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="text-muted-foreground font-mono text-xs leading-none">
140+
{walletAddress.slice(0, 6)}...{walletAddress.slice(-4)}
141+
</p>
142+
)}
131143
</div>
132144
</DropdownMenuLabel>
133145
<DropdownMenuSeparator />

lib/auth.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import { betterAuth } from "better-auth";
22
import { drizzleAdapter } from "better-auth/adapters/drizzle";
33
import { anonymous, genericOAuth } from "better-auth/plugins";
44
import { eq } from "drizzle-orm";
5+
import { Para as ParaServer, Environment } from "@getpara/server-sdk";
56
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,72 @@ 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+
// Store encrypted wallet in database
212+
await db.insert(paraWallets).values({
213+
userId: user.id,
214+
email: user.email,
215+
walletId: wallet.id, // v2 API uses wallet.id instead of wallet.walletId
216+
walletAddress: wallet.address,
217+
userShare: encryptUserShare(userShare), // Encrypted!
218+
});
219+
220+
console.log(
221+
`[Para] ✓ Wallet created successfully: ${wallet.address}`
222+
);
223+
} catch (error) {
224+
console.error(`[Para] Failed to create wallet:`, error);
225+
// Don't throw - let signup succeed even if wallet creation fails
226+
}
227+
},
228+
},
229+
},
230+
},
160231
});

lib/db/schema.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,22 @@ export const workflowExecutionLogs = pgTable("workflow_execution_logs", {
139139
timestamp: timestamp("timestamp").notNull().defaultNow(),
140140
});
141141

142+
// Para Wallets table to store user wallet information
143+
export const paraWallets = pgTable("para_wallets", {
144+
id: text("id")
145+
.primaryKey()
146+
.$defaultFn(() => generateId()),
147+
userId: text("user_id")
148+
.notNull()
149+
.unique() // One wallet per user
150+
.references(() => users.id, { onDelete: "cascade" }),
151+
email: text("email").notNull(),
152+
walletId: text("wallet_id").notNull(), // Para wallet ID
153+
walletAddress: text("wallet_address").notNull(), // EVM address (0x...)
154+
userShare: text("user_share").notNull(), // Encrypted keyshare for signing
155+
createdAt: timestamp("created_at").notNull().defaultNow(),
156+
});
157+
142158
// Relations
143159
export const workflowExecutionsRelations = relations(
144160
workflowExecutions,
@@ -160,3 +176,5 @@ export type WorkflowExecution = typeof workflowExecutions.$inferSelect;
160176
export type NewWorkflowExecution = typeof workflowExecutions.$inferInsert;
161177
export type WorkflowExecutionLog = typeof workflowExecutionLogs.$inferSelect;
162178
export type NewWorkflowExecutionLog = typeof workflowExecutionLogs.$inferInsert;
179+
export type ParaWallet = typeof paraWallets.$inferSelect;
180+
export type NewParaWallet = typeof paraWallets.$inferInsert;

lib/encryption.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import crypto from "crypto";
2+
3+
const ENCRYPTION_KEY = process.env.WALLET_ENCRYPTION_KEY!;
4+
const ALGORITHM = "aes-256-gcm";
5+
6+
if (!ENCRYPTION_KEY || ENCRYPTION_KEY.length !== 64) {
7+
throw new Error(
8+
"WALLET_ENCRYPTION_KEY must be a 32-byte hex string (64 characters)"
9+
);
10+
}
11+
12+
/**
13+
* Encrypt sensitive userShare before storing in database
14+
* Uses AES-256-GCM for authenticated encryption
15+
*
16+
* @param userShare - The plaintext userShare from Para SDK
17+
* @returns Encrypted string in format: iv:authTag:encryptedData
18+
*/
19+
export function encryptUserShare(userShare: string): string {
20+
const iv = crypto.randomBytes(16);
21+
const cipher = crypto.createCipheriv(
22+
ALGORITHM,
23+
Buffer.from(ENCRYPTION_KEY, "hex"),
24+
iv
25+
);
26+
27+
let encrypted = cipher.update(userShare, "utf8", "hex");
28+
encrypted += cipher.final("hex");
29+
30+
const authTag = cipher.getAuthTag();
31+
32+
// Format: iv:authTag:encryptedData
33+
return (
34+
iv.toString("hex") + ":" + authTag.toString("hex") + ":" + encrypted
35+
);
36+
}
37+
38+
/**
39+
* Decrypt userShare when needed for signing transactions
40+
*
41+
* @param encryptedData - Encrypted string from database
42+
* @returns Decrypted userShare for Para SDK
43+
*/
44+
export function decryptUserShare(encryptedData: string): string {
45+
const parts = encryptedData.split(":");
46+
if (parts.length !== 3) {
47+
throw new Error("Invalid encrypted data format");
48+
}
49+
50+
const iv = Buffer.from(parts[0], "hex");
51+
const authTag = Buffer.from(parts[1], "hex");
52+
const encrypted = parts[2];
53+
54+
const decipher = crypto.createDecipheriv(
55+
ALGORITHM,
56+
Buffer.from(ENCRYPTION_KEY, "hex"),
57+
iv
58+
);
59+
decipher.setAuthTag(authTag);
60+
61+
let decrypted = decipher.update(encrypted, "hex", "utf8");
62+
decrypted += decipher.final("utf8");
63+
64+
return decrypted;
65+
}

lib/para/wallet-helpers.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import "server-only";
2+
import { db } from "@/lib/db";
3+
import { paraWallets } from "@/lib/db/schema";
4+
import { eq } from "drizzle-orm";
5+
import { Para as ParaServer, Environment } from "@getpara/server-sdk";
6+
import { ParaEthersSigner } from "@getpara/ethers-v6-integration";
7+
import { ethers } from "ethers";
8+
import { decryptUserShare } from "@/lib/encryption";
9+
10+
/**
11+
* Get user's Para wallet from database
12+
* @throws Error if wallet not found
13+
*/
14+
export async function getUserWallet(userId: string) {
15+
const wallet = await db
16+
.select()
17+
.from(paraWallets)
18+
.where(eq(paraWallets.userId, userId))
19+
.limit(1);
20+
21+
if (wallet.length === 0) {
22+
throw new Error("No Para wallet found for user");
23+
}
24+
25+
return wallet[0];
26+
}
27+
28+
/**
29+
* Initialize Para signer for user
30+
* This signer can sign transactions using the user's Para wallet
31+
*
32+
* @param userId - User ID from session
33+
* @param rpcUrl - Blockchain RPC URL (e.g., Ethereum mainnet, Polygon, etc.)
34+
* @returns Para Ethers signer ready to sign transactions
35+
*/
36+
export async function initializeParaSigner(
37+
userId: string,
38+
rpcUrl: string
39+
): Promise<ParaEthersSigner> {
40+
const PARA_API_KEY = process.env.PARA_API_KEY;
41+
const PARA_ENV = process.env.PARA_ENVIRONMENT || "beta";
42+
43+
if (!PARA_API_KEY) {
44+
throw new Error("PARA_API_KEY not configured");
45+
}
46+
47+
// Get user's wallet from database
48+
const wallet = await getUserWallet(userId);
49+
50+
// Initialize Para client
51+
const paraClient = new ParaServer(
52+
PARA_ENV === "prod" ? Environment.PROD : Environment.BETA,
53+
PARA_API_KEY
54+
);
55+
56+
// Decrypt and set user's keyshare
57+
const decryptedShare = decryptUserShare(wallet.userShare);
58+
await paraClient.setUserShare(decryptedShare);
59+
60+
// Create blockchain provider and signer
61+
const provider = new ethers.JsonRpcProvider(rpcUrl);
62+
const signer = new ParaEthersSigner(paraClient, provider);
63+
64+
return signer;
65+
}
66+
67+
/**
68+
* Get user's wallet address
69+
* Useful for displaying wallet address in UI
70+
*/
71+
export async function getUserWalletAddress(userId: string): Promise<string> {
72+
const wallet = await getUserWallet(userId);
73+
return wallet.walletAddress;
74+
}
75+
76+
/**
77+
* Check if user has a Para wallet
78+
*/
79+
export async function userHasWallet(userId: string): Promise<boolean> {
80+
const wallet = await db
81+
.select()
82+
.from(paraWallets)
83+
.where(eq(paraWallets.userId, userId))
84+
.limit(1);
85+
86+
return wallet.length > 0;
87+
}

0 commit comments

Comments
 (0)