Skip to content

Commit a39d97c

Browse files
committed
Merge staging into chore/KEEP-1151
Resolved conflicts: - drizzle/meta/0002_snapshot.json: kept staging's para_wallets table - drizzle/meta/_journal.json: kept staging's migration history - docs/web3/*: kept our documentation files
2 parents 8014aa1 + abff2d6 commit a39d97c

File tree

12 files changed

+1194
-127
lines changed

12 files changed

+1194
-127
lines changed
Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,19 @@ on:
55
# - main
66
workflow_dispatch:
77

8-
name: deploy-vw
8+
name: deploy-keeperhub
99
jobs:
10-
build-and-deploy-vw:
10+
build-and-deploy-keeperhub:
1111
environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'staging' }}
1212
runs-on: ubuntu-latest
1313
env:
1414
HELM_FILE: deploy/${{ github.ref == 'refs/heads/main' && 'prod' || 'staging' }}/values.yaml
1515
REGION: ${{ github.ref == 'refs/heads/main' && 'us-east-1' || 'us-east-2' }}
1616
CLUSTER_NAME: ${{ github.ref == 'refs/heads/main' && 'maker-prod' || 'maker-staging' }}
1717
ENVIRONMENT_TAG: ${{ github.ref == 'refs/heads/main' && 'prod' || 'staging' }}
18-
NAMESPACE: vw
19-
SERVICE_NAME: vw
20-
AWS_ECR_NAME: keeper-app-vw-${{ github.ref == 'refs/heads/main' && 'prod' || 'staging' }}
18+
NAMESPACE: keeperhub
19+
SERVICE_NAME: keeperhub
20+
AWS_ECR_NAME: keeperhub-${{ github.ref == 'refs/heads/main' && 'prod' || 'staging' }}
2121
steps:
2222
- name: Checkout
2323
uses: actions/checkout@v4
@@ -42,8 +42,8 @@ jobs:
4242
run: |
4343
echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
4444
45-
- name: Build, tag, and push image to ECR
46-
id: build-image
45+
- name: Build, tag, and push app image to ECR
46+
id: build-app-image
4747
if: ${{ !contains(github.event.head_commit.message , '[skip build]') }}
4848
env:
4949
SHA_TAG: ${{ steps.vars.outputs.sha_short }}
@@ -61,11 +61,36 @@ jobs:
6161
6262
docker push $ECR_REGISTRY/$AWS_ECR_NAME --all-tags
6363
64+
- name: Build, tag, and push migrator image to ECR
65+
id: build-migrator
66+
if: ${{ !contains(github.event.head_commit.message , '[skip build]') }}
67+
env:
68+
SHA_TAG: ${{ steps.vars.outputs.sha_short }}
69+
LATEST_TAG: latest
70+
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
71+
run: |
72+
# Build image for database migrations
73+
docker build --target migrator \
74+
-t $ECR_REGISTRY/$AWS_ECR_NAME:migrator \
75+
-f ./Dockerfile \
76+
.
77+
78+
docker push $ECR_REGISTRY/$AWS_ECR_NAME:migrator
79+
80+
- name: Replace variables in the Helm values file
81+
id: replace-vars
82+
if: ${{ !contains(github.event.head_commit.message , '[skip deploy]') }}
83+
env:
84+
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
85+
IMAGE_TAG: ${{ steps.vars.outputs.sha_short }}
86+
run: |
87+
sed -i 's|${ECR_REGISTRY}|'"$ECR_REGISTRY"'|g' $HELM_FILE
88+
sed -i 's|${IMAGE_TAG}|'"$IMAGE_TAG"'|g' $HELM_FILE
89+
6490
- name: Deploying Service to Kubernetes with Helm
6591
id: deploy
6692
uses: bitovi/github-actions-deploy-eks-helm@v1.2.10
6793
with:
68-
values: image.repository=${{ steps.login-ecr.outputs.registry }}/${{ env.AWS_ECR_NAME }},image.tag=${{ steps.vars.outputs.sha_short }}
6994
cluster-name: ${{ env.CLUSTER_NAME }}
7095
config-files: ${{ env.HELM_FILE }}
7196
chart-path: techops-services/common

Dockerfile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,22 @@ RUN touch README.md || true
2929
# Build the application
3030
RUN pnpm build
3131

32+
# Stage 2.5: Migration stage (optional - for running migrations)
33+
FROM node:25-alpine AS migrator
34+
WORKDIR /app
35+
RUN npm install -g pnpm
36+
37+
# Copy dependencies and migration files
38+
COPY --from=deps /app/node_modules ./node_modules
39+
COPY --from=builder /app/drizzle ./drizzle
40+
COPY --from=builder /app/drizzle.config.ts ./drizzle.config.ts
41+
COPY --from=builder /app/lib ./lib
42+
COPY --from=builder /app/package.json ./package.json
43+
44+
# This stage can be used to run migrations
45+
# Run with: docker build --target migrator -t myapp-migrator .
46+
# Then: docker run --env DATABASE_URL=xxx myapp-migrator pnpm db:push
47+
3248
# Stage 3: Runner
3349
FROM node:25-alpine AS runner
3450
WORKDIR /app

app/api/user/wallet/route.ts

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,164 @@
1+
import { Environment, Para as ParaServer } from "@getpara/server-sdk";
2+
import { and, eq } from "drizzle-orm";
13
import { NextResponse } from "next/server";
24
import { auth } from "@/lib/auth";
5+
import { db } from "@/lib/db";
6+
import { createIntegration } from "@/lib/db/integrations";
7+
import { integrations, paraWallets } from "@/lib/db/schema";
8+
import { encryptUserShare } from "@/lib/encryption";
39
import { getUserWallet, userHasWallet } from "@/lib/para/wallet-helpers";
410

11+
const PARA_API_KEY = process.env.PARA_API_KEY || "";
12+
const PARA_ENV = process.env.PARA_ENVIRONMENT || "beta";
13+
14+
// Helper: Validate user authentication and email
15+
async function validateUser(request: Request) {
16+
const session = await auth.api.getSession({
17+
headers: request.headers,
18+
});
19+
20+
if (!session?.user) {
21+
return { error: "Unauthorized", status: 401 };
22+
}
23+
24+
const user = session.user;
25+
26+
if (!user.email) {
27+
return { error: "Email required to create wallet", status: 400 };
28+
}
29+
30+
// Check if user is anonymous
31+
if (
32+
user.email.includes("@http://") ||
33+
user.email.includes("@https://") ||
34+
user.email.startsWith("temp-")
35+
) {
36+
return {
37+
error:
38+
"Anonymous users cannot create wallets. Please sign in with a real account.",
39+
status: 400,
40+
};
41+
}
42+
43+
return { user };
44+
}
45+
46+
// Helper: Check if wallet or integration already exists
47+
async function checkExistingWallet(userId: string) {
48+
const hasWallet = await userHasWallet(userId);
49+
if (hasWallet) {
50+
return { error: "Wallet already exists for this user", status: 400 };
51+
}
52+
53+
const existingIntegration = await db
54+
.select()
55+
.from(integrations)
56+
.where(and(eq(integrations.userId, userId), eq(integrations.type, "web3")))
57+
.limit(1);
58+
59+
if (existingIntegration.length > 0) {
60+
return {
61+
error: "Web3 integration already exists for this user",
62+
status: 400,
63+
};
64+
}
65+
66+
return { valid: true };
67+
}
68+
69+
// Helper: Create wallet via Para SDK
70+
async function createParaWallet(email: string) {
71+
if (!PARA_API_KEY) {
72+
console.error("[Para] PARA_API_KEY not configured");
73+
throw new Error("Para API key not configured");
74+
}
75+
76+
const environment = PARA_ENV === "prod" ? Environment.PROD : Environment.BETA;
77+
console.log(
78+
`[Para] Initializing SDK with environment: ${PARA_ENV} (${environment})`
79+
);
80+
console.log(`[Para] API key: ${PARA_API_KEY.slice(0, 8)}...`);
81+
82+
const paraClient = new ParaServer(environment, PARA_API_KEY);
83+
84+
console.log(`[Para] Creating wallet for email: ${email}`);
85+
86+
const wallet = await paraClient.createPregenWallet({
87+
type: "EVM",
88+
pregenId: { email },
89+
});
90+
91+
const userShare = await paraClient.getUserShare();
92+
93+
if (!userShare) {
94+
throw new Error("Failed to get user share from Para");
95+
}
96+
97+
if (!(wallet.id && wallet.address)) {
98+
throw new Error("Invalid wallet data from Para");
99+
}
100+
101+
return { wallet, userShare };
102+
}
103+
104+
// Helper: Get user-friendly error response for wallet creation failures
105+
function getErrorResponse(error: unknown) {
106+
console.error("[Para] Wallet creation failed:", error);
107+
108+
let errorMessage = "Failed to create wallet";
109+
let statusCode = 500;
110+
111+
if (error instanceof Error) {
112+
const message = error.message.toLowerCase();
113+
114+
if (message.includes("already exists")) {
115+
errorMessage = "A wallet already exists for this email address";
116+
statusCode = 409;
117+
} else if (message.includes("invalid email")) {
118+
errorMessage = "Invalid email format";
119+
statusCode = 400;
120+
} else if (message.includes("forbidden") || message.includes("403")) {
121+
errorMessage = "API key authentication failed. Please contact support.";
122+
statusCode = 403;
123+
} else {
124+
errorMessage = error.message;
125+
}
126+
}
127+
128+
return NextResponse.json({ error: errorMessage }, { status: statusCode });
129+
}
130+
131+
// Helper: Store wallet in database and create integration
132+
async function storeWalletAndIntegration(options: {
133+
userId: string;
134+
email: string;
135+
walletId: string;
136+
walletAddress: string;
137+
userShare: string;
138+
}) {
139+
const { userId, email, walletId, walletAddress, userShare } = options;
140+
141+
// Store wallet in para_wallets table
142+
await db.insert(paraWallets).values({
143+
userId,
144+
email,
145+
walletId,
146+
walletAddress,
147+
userShare: encryptUserShare(userShare),
148+
});
149+
150+
console.log(`[Para] ✓ Wallet created: ${walletAddress}`);
151+
152+
// Create Web3 integration record with truncated address as name
153+
const truncatedAddress = `${walletAddress.slice(0, 6)}...${walletAddress.slice(-4)}`;
154+
155+
await createIntegration(userId, truncatedAddress, "web3", {});
156+
157+
console.log(`[Para] ✓ Web3 integration created: ${truncatedAddress}`);
158+
159+
return { walletAddress, walletId, truncatedAddress };
160+
}
161+
5162
export async function GET(request: Request) {
6163
try {
7164
const session = await auth.api.getSession({
@@ -40,3 +197,109 @@ export async function GET(request: Request) {
40197
);
41198
}
42199
}
200+
201+
export async function POST(request: Request) {
202+
try {
203+
// 1. Validate user
204+
const userValidation = await validateUser(request);
205+
if ("error" in userValidation) {
206+
return NextResponse.json(
207+
{ error: userValidation.error },
208+
{ status: userValidation.status }
209+
);
210+
}
211+
const { user } = userValidation;
212+
213+
// 2. Check if wallet/integration already exists
214+
const existingCheck = await checkExistingWallet(user.id);
215+
if ("error" in existingCheck) {
216+
return NextResponse.json(
217+
{ error: existingCheck.error },
218+
{ status: existingCheck.status }
219+
);
220+
}
221+
222+
// 3. Create wallet via Para SDK
223+
const { wallet, userShare } = await createParaWallet(user.email);
224+
225+
// wallet.id and wallet.address are validated in createParaWallet
226+
const walletId = wallet.id as string;
227+
const walletAddress = wallet.address as string;
228+
229+
// 4. Store wallet and create integration
230+
await storeWalletAndIntegration({
231+
userId: user.id,
232+
email: user.email,
233+
walletId,
234+
walletAddress,
235+
userShare,
236+
});
237+
238+
// 5. Return success
239+
return NextResponse.json({
240+
success: true,
241+
wallet: {
242+
address: walletAddress,
243+
walletId,
244+
email: user.email,
245+
},
246+
});
247+
} catch (error) {
248+
return getErrorResponse(error);
249+
}
250+
}
251+
252+
export async function DELETE(request: Request) {
253+
try {
254+
// 1. Authenticate user
255+
const session = await auth.api.getSession({
256+
headers: request.headers,
257+
});
258+
259+
if (!session?.user) {
260+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
261+
}
262+
263+
const user = session.user;
264+
265+
// 2. Delete wallet data
266+
const deletedWallet = await db
267+
.delete(paraWallets)
268+
.where(eq(paraWallets.userId, user.id))
269+
.returning();
270+
271+
if (deletedWallet.length === 0) {
272+
return NextResponse.json(
273+
{ error: "No wallet found to delete" },
274+
{ status: 404 }
275+
);
276+
}
277+
278+
console.log(
279+
`[Para] Wallet deleted for user ${user.id}: ${deletedWallet[0].walletAddress}`
280+
);
281+
282+
// 3. Delete associated Web3 integration record
283+
await db
284+
.delete(integrations)
285+
.where(
286+
and(eq(integrations.userId, user.id), eq(integrations.type, "web3"))
287+
);
288+
289+
console.log(`[Para] Web3 integration deleted for user ${user.id}`);
290+
291+
return NextResponse.json({
292+
success: true,
293+
message: "Wallet deleted successfully",
294+
});
295+
} catch (error) {
296+
console.error("[Para] Wallet deletion failed:", error);
297+
return NextResponse.json(
298+
{
299+
error: "Failed to delete wallet",
300+
details: error instanceof Error ? error.message : "Unknown error",
301+
},
302+
{ status: 500 }
303+
);
304+
}
305+
}

0 commit comments

Comments
 (0)