Skip to content

Commit f810bed

Browse files
authored
Merge pull request #216 from shaaibu7/feat/savings-goals
Feat/savings goals
2 parents ce9d138 + 00ed6e3 commit f810bed

File tree

14 files changed

+2029
-0
lines changed

14 files changed

+2029
-0
lines changed

.env.local.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Stellar Network Configuration
2+
NEXT_PUBLIC_STELLAR_NETWORK=testnet
3+
NEXT_PUBLIC_STELLAR_RPC_URL=https://soroban-testnet.stellar.org
4+
5+
# Soroban Contract Addresses
6+
NEXT_PUBLIC_SAVINGS_GOALS_CONTRACT_ID=your_contract_id_here

app/api/goals/[id]/add/route.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// POST /api/goals/[id]/add - Add funds to a savings goal
2+
3+
import { NextRequest, NextResponse } from 'next/server';
4+
import { buildAddToGoalTx } from '@/lib/contracts/savings-goals';
5+
import { getSessionFromRequest, getPublicKeyFromSession } from '@/lib/auth/session';
6+
import {
7+
createValidationError,
8+
createAuthenticationError,
9+
handleUnexpectedError
10+
} from '@/lib/errors/api-errors';
11+
import {
12+
validateAmount,
13+
validateGoalId
14+
} from '@/lib/validation/savings-goals';
15+
import { ApiSuccessResponse } from '@/lib/types/savings-goals';
16+
17+
export async function POST(
18+
request: NextRequest,
19+
{ params }: { params: { id: string } }
20+
) {
21+
try {
22+
// Authenticate user
23+
const session = getSessionFromRequest(request);
24+
if (!session) {
25+
return createAuthenticationError('Authentication required', 'Please provide a valid session');
26+
}
27+
28+
let publicKey: string;
29+
try {
30+
publicKey = getPublicKeyFromSession(session);
31+
} catch (error) {
32+
return createAuthenticationError('Invalid session', 'Session does not contain a valid public key');
33+
}
34+
35+
// Validate goal ID from URL params
36+
const goalId = params.id;
37+
const goalIdValidation = validateGoalId(goalId);
38+
if (!goalIdValidation.isValid) {
39+
return createValidationError('Invalid goal ID', goalIdValidation.error);
40+
}
41+
42+
// Parse request body
43+
let body;
44+
try {
45+
body = await request.json();
46+
} catch (error) {
47+
return createValidationError('Invalid request body', 'Request body must be valid JSON');
48+
}
49+
50+
const { amount } = body;
51+
52+
// Validate amount
53+
if (amount === undefined || amount === null) {
54+
return createValidationError('Missing required field', 'Amount is required');
55+
}
56+
const amountValidation = validateAmount(amount);
57+
if (!amountValidation.isValid) {
58+
return createValidationError('Invalid amount', amountValidation.error);
59+
}
60+
61+
// Build transaction
62+
const result = await buildAddToGoalTx(publicKey, goalId, amount);
63+
64+
// Return success response
65+
const response: ApiSuccessResponse = {
66+
xdr: result.xdr
67+
};
68+
69+
return NextResponse.json(response, { status: 200 });
70+
71+
} catch (error) {
72+
return handleUnexpectedError(error);
73+
}
74+
}

app/api/goals/[id]/lock/route.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// POST /api/goals/[id]/lock - Lock a savings goal
2+
3+
import { NextRequest, NextResponse } from 'next/server';
4+
import { buildLockGoalTx } from '@/lib/contracts/savings-goals';
5+
import { getSessionFromRequest, getPublicKeyFromSession } from '@/lib/auth/session';
6+
import {
7+
createValidationError,
8+
createAuthenticationError,
9+
handleUnexpectedError
10+
} from '@/lib/errors/api-errors';
11+
import { validateGoalId } from '@/lib/validation/savings-goals';
12+
import { ApiSuccessResponse } from '@/lib/types/savings-goals';
13+
14+
export async function POST(
15+
request: NextRequest,
16+
{ params }: { params: { id: string } }
17+
) {
18+
try {
19+
// Authenticate user
20+
const session = getSessionFromRequest(request);
21+
if (!session) {
22+
return createAuthenticationError('Authentication required', 'Please provide a valid session');
23+
}
24+
25+
let publicKey: string;
26+
try {
27+
publicKey = getPublicKeyFromSession(session);
28+
} catch (error) {
29+
return createAuthenticationError('Invalid session', 'Session does not contain a valid public key');
30+
}
31+
32+
// Validate goal ID from URL params
33+
const goalId = params.id;
34+
const goalIdValidation = validateGoalId(goalId);
35+
if (!goalIdValidation.isValid) {
36+
return createValidationError('Invalid goal ID', goalIdValidation.error);
37+
}
38+
39+
// Build transaction
40+
const result = await buildLockGoalTx(publicKey, goalId);
41+
42+
// Return success response
43+
const response: ApiSuccessResponse = {
44+
xdr: result.xdr
45+
};
46+
47+
return NextResponse.json(response, { status: 200 });
48+
49+
} catch (error) {
50+
return handleUnexpectedError(error);
51+
}
52+
}

app/api/goals/[id]/unlock/route.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// POST /api/goals/[id]/unlock - Unlock a savings goal
2+
3+
import { NextRequest, NextResponse } from 'next/server';
4+
import { buildUnlockGoalTx } from '@/lib/contracts/savings-goals';
5+
import { getSessionFromRequest, getPublicKeyFromSession } from '@/lib/auth/session';
6+
import {
7+
createValidationError,
8+
createAuthenticationError,
9+
handleUnexpectedError
10+
} from '@/lib/errors/api-errors';
11+
import { validateGoalId } from '@/lib/validation/savings-goals';
12+
import { ApiSuccessResponse } from '@/lib/types/savings-goals';
13+
14+
export async function POST(
15+
request: NextRequest,
16+
{ params }: { params: { id: string } }
17+
) {
18+
try {
19+
// Authenticate user
20+
const session = getSessionFromRequest(request);
21+
if (!session) {
22+
return createAuthenticationError('Authentication required', 'Please provide a valid session');
23+
}
24+
25+
let publicKey: string;
26+
try {
27+
publicKey = getPublicKeyFromSession(session);
28+
} catch (error) {
29+
return createAuthenticationError('Invalid session', 'Session does not contain a valid public key');
30+
}
31+
32+
// Validate goal ID from URL params
33+
const goalId = params.id;
34+
const goalIdValidation = validateGoalId(goalId);
35+
if (!goalIdValidation.isValid) {
36+
return createValidationError('Invalid goal ID', goalIdValidation.error);
37+
}
38+
39+
// Build transaction
40+
const result = await buildUnlockGoalTx(publicKey, goalId);
41+
42+
// Return success response
43+
const response: ApiSuccessResponse = {
44+
xdr: result.xdr
45+
};
46+
47+
return NextResponse.json(response, { status: 200 });
48+
49+
} catch (error) {
50+
return handleUnexpectedError(error);
51+
}
52+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// POST /api/goals/[id]/withdraw - Withdraw funds from a savings goal
2+
3+
import { NextRequest, NextResponse } from 'next/server';
4+
import { buildWithdrawFromGoalTx } from '@/lib/contracts/savings-goals';
5+
import { getSessionFromRequest, getPublicKeyFromSession } from '@/lib/auth/session';
6+
import {
7+
createValidationError,
8+
createAuthenticationError,
9+
handleUnexpectedError
10+
} from '@/lib/errors/api-errors';
11+
import {
12+
validateAmount,
13+
validateGoalId
14+
} from '@/lib/validation/savings-goals';
15+
import { ApiSuccessResponse } from '@/lib/types/savings-goals';
16+
17+
export async function POST(
18+
request: NextRequest,
19+
{ params }: { params: { id: string } }
20+
) {
21+
try {
22+
// Authenticate user
23+
const session = getSessionFromRequest(request);
24+
if (!session) {
25+
return createAuthenticationError('Authentication required', 'Please provide a valid session');
26+
}
27+
28+
let publicKey: string;
29+
try {
30+
publicKey = getPublicKeyFromSession(session);
31+
} catch (error) {
32+
return createAuthenticationError('Invalid session', 'Session does not contain a valid public key');
33+
}
34+
35+
// Validate goal ID from URL params
36+
const goalId = params.id;
37+
const goalIdValidation = validateGoalId(goalId);
38+
if (!goalIdValidation.isValid) {
39+
return createValidationError('Invalid goal ID', goalIdValidation.error);
40+
}
41+
42+
// Parse request body
43+
let body;
44+
try {
45+
body = await request.json();
46+
} catch (error) {
47+
return createValidationError('Invalid request body', 'Request body must be valid JSON');
48+
}
49+
50+
const { amount } = body;
51+
52+
// Validate amount
53+
if (amount === undefined || amount === null) {
54+
return createValidationError('Missing required field', 'Amount is required');
55+
}
56+
const amountValidation = validateAmount(amount);
57+
if (!amountValidation.isValid) {
58+
return createValidationError('Invalid amount', amountValidation.error);
59+
}
60+
61+
// Build transaction
62+
const result = await buildWithdrawFromGoalTx(publicKey, goalId, amount);
63+
64+
// Return success response
65+
const response: ApiSuccessResponse = {
66+
xdr: result.xdr
67+
};
68+
69+
return NextResponse.json(response, { status: 200 });
70+
71+
} catch (error) {
72+
return handleUnexpectedError(error);
73+
}
74+
}

app/api/goals/route.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// POST /api/goals - Create a new savings goal
2+
3+
import { NextRequest, NextResponse } from 'next/server';
4+
import { buildCreateGoalTx } from '@/lib/contracts/savings-goals';
5+
import { getSessionFromRequest, getPublicKeyFromSession } from '@/lib/auth/session';
6+
import {
7+
createValidationError,
8+
createAuthenticationError,
9+
handleUnexpectedError
10+
} from '@/lib/errors/api-errors';
11+
import {
12+
validateAmount,
13+
validateFutureDate,
14+
validateGoalName
15+
} from '@/lib/validation/savings-goals';
16+
import { ApiSuccessResponse } from '@/lib/types/savings-goals';
17+
18+
export async function POST(request: NextRequest) {
19+
try {
20+
// Authenticate user
21+
const session = getSessionFromRequest(request);
22+
if (!session) {
23+
return createAuthenticationError('Authentication required', 'Please provide a valid session');
24+
}
25+
26+
let publicKey: string;
27+
try {
28+
publicKey = getPublicKeyFromSession(session);
29+
} catch (error) {
30+
return createAuthenticationError('Invalid session', 'Session does not contain a valid public key');
31+
}
32+
33+
// Parse request body
34+
let body;
35+
try {
36+
body = await request.json();
37+
} catch (error) {
38+
return createValidationError('Invalid request body', 'Request body must be valid JSON');
39+
}
40+
41+
const { name, targetAmount, targetDate } = body;
42+
43+
// Validate name
44+
if (!name) {
45+
return createValidationError('Missing required field', 'Goal name is required');
46+
}
47+
const nameValidation = validateGoalName(name);
48+
if (!nameValidation.isValid) {
49+
return createValidationError('Invalid goal name', nameValidation.error);
50+
}
51+
52+
// Validate target amount
53+
if (targetAmount === undefined || targetAmount === null) {
54+
return createValidationError('Missing required field', 'Target amount is required');
55+
}
56+
const amountValidation = validateAmount(targetAmount);
57+
if (!amountValidation.isValid) {
58+
return createValidationError('Invalid target amount', amountValidation.error);
59+
}
60+
61+
// Validate target date
62+
if (!targetDate) {
63+
return createValidationError('Missing required field', 'Target date is required');
64+
}
65+
const dateValidation = validateFutureDate(targetDate);
66+
if (!dateValidation.isValid) {
67+
return createValidationError('Invalid target date', dateValidation.error);
68+
}
69+
70+
// Build transaction
71+
const result = await buildCreateGoalTx(publicKey, name, targetAmount, targetDate);
72+
73+
// Return success response
74+
const response: ApiSuccessResponse = {
75+
xdr: result.xdr
76+
};
77+
78+
return NextResponse.json(response, { status: 200 });
79+
80+
} catch (error) {
81+
return handleUnexpectedError(error);
82+
}
83+
}

0 commit comments

Comments
 (0)