Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create wallet API #6

Merged
merged 5 commits into from
Nov 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
"cors": "^2.8.5",
"dotenv": "^16.0.1",
"envalid": "^7.3.1",
"ethers": "^6.12.0",
"express": "^4.18.1",
"helmet": "^5.1.1",
"hpp": "^0.2.3",
Expand Down Expand Up @@ -69,7 +68,7 @@
"eslint": "^8.20.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"ethers": "^6.11.1",
"ethers": "^6.13.4",
"husky": "^8.0.1",
"jest": "^28.1.1",
"latlon-geohash": "^2.0.0",
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { config } from 'dotenv';
import { mnemonic } from '@vechain/sdk-core';
import { Mnemonic } from '@vechain/sdk-core';
// config({ path: `.env.${process.env.NODE_ENV || 'development'}.local` });
config({ path: `.env.development.local` }); // Tests were failing to run with the above line

Expand All @@ -12,4 +12,4 @@ export const { NETWORK_URL, NETWORK_TYPE } = process.env;
export const { RECAPTCHA_SECRET_KEY } = process.env;
export const { REWARD_AMOUNT } = process.env;

export const ADMIN_PRIVATE_KEY = mnemonic.derivePrivateKey(ADMIN_MNEMONIC.split(' '));
export const ADMIN_PRIVATE_KEY = Mnemonic.toPrivateKey(ADMIN_MNEMONIC.split(' '));
4 changes: 2 additions & 2 deletions apps/backend/src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ export class UserController {
bio: userBio,
});

await user.save();
user = await user.save();

res.json({
msg: `User created successfully`,
user: { username: user.username, email: user.email, bio: user.bio, createdAt: user.createdAt },
user: { _id: user._id, username: user.username, email: user.email, bio: user.bio, createdAt: user.createdAt },
});
});
} catch (error) {
Expand Down
120 changes: 120 additions & 0 deletions apps/backend/src/controllers/wallet.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { NextFunction, Request, Response } from 'express';
import { thor } from '@/utils/thor';
import { Wallet } from '@/models/Wallet';
import createBlockchainWallet from '@/utils/createBlockchainWallet';

export class WalletController {
// @route POST /wallet
// @desc Create a new wallet
public createWallet = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
try {
const { userId } = req.body;

// Validate input
if (!userId) {
res.status(400).json({ message: 'User ID required in request body.' });
return;
}

// Check if there is an existing wallet for the user
let wallet = await Wallet.findOne().where('user').equals(userId);

// Perform step to handle existing user wallet
if (wallet) {
res.status(400).json({ msg: 'User has an existing wallet.' });
return;
}

// Create a new wallet for the user
const { walletAddress } = createBlockchainWallet();

// Create a new wallet for the user
wallet = new Wallet({
balance: 0,
address: walletAddress,
user: userId,
nftList: [],
});

await wallet.save();

res.status(200).json({
msg: `Wallet created successfully`,
wallet: wallet,
});
} catch (error) {
next(error);
}
};

// @route GET /wallet
// @desc Retrieve information on a single wallet
public getWallet = async (req: Request, res: Response) => {
try {
const { userId } = req.params;

// Check if there is an existing wallet for the user
const wallet = await Wallet.findOne().where('user').equals(userId);

if (!wallet) {
return res.status(404).json({ message: 'No wallet found for this user' });
}

res.status(200).json(wallet);
} catch (error) {
res.status(500).json({ message: 'Error retrieving wallet', error });
}
};

// @route PATCH /wallet/:userId
// @desc Update wallet information
public updateWallet = async (req: Request, res: Response, next: NextFunction) => {
try {
const { userId } = req.params;
const { nftList } = req.body || [];

// Check if there is an existing wallet for the user
let wallet = await Wallet.findOne().where('user').equals(userId);

if (!wallet) {
return res.status(404).json({ message: 'No wallet found for this user' });
}

// Get account details for the user's wallet from VeChain
const accountDetails = await thor.accounts.getAccount(wallet.address);

// Store updated balance after converting wei to VET
const updatedBalance = parseInt(accountDetails.balance, 16) / Math.pow(10, 18);

wallet = await Wallet.findByIdAndUpdate(wallet._id, { $set: { nftList, balance: updatedBalance } }, { new: true });

res.status(200).json({
msg: 'Wallet details have been updated!',
wallet: wallet,
});
} catch (error) {
next(error);
}
};

// @route DELETE /wallet/:userId
// @desc Delete user's wallet
public deleteWallet = async (req: Request, res: Response, next: NextFunction) => {
try {
const { userId } = req.params;

// Check if there is an existing wallet for the user
const wallet = await Wallet.findOne().where('user').equals(userId);

if (!wallet) {
return res.status(404).json({ message: 'No wallet found for this user' });
}

await Wallet.findByIdAndDelete(wallet._id);

res.status(200).json({ msg: 'Wallet has been deleted' });
} catch (error) {
next(error);
}
};
}
7 changes: 7 additions & 0 deletions apps/backend/src/dtos/wallet.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { IsNotEmpty, IsString } from 'class-validator';

export class WalletDto {
@IsString()
@IsNotEmpty()
public userId: string;
}
29 changes: 29 additions & 0 deletions apps/backend/src/models/Wallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import mongoose from 'mongoose';
const Schema = mongoose.Schema;

const WalletSchema = new Schema({
balance: {
type: Number,
default: 0,
},
address: {
type: String,
required: true,
unique: true,
},
user: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true,
},
nftList: {
type: [String],
default: [],
},
createdAt: {
type: Date,
default: Date.now,
},
});

export const Wallet = mongoose.model('Wallet', WalletSchema);
22 changes: 22 additions & 0 deletions apps/backend/src/routes/wallet.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Router } from 'express';
import { Routes } from '@interfaces/routes.interface';
import { WalletController } from '@/controllers/wallet.controller';
import { ValidationMiddleware } from '@/middlewares/validation.middleware';
import { WalletDto } from '@/dtos/wallet.dto';

export class WalletRoute implements Routes {
public router = Router();
public wallet = new WalletController();

constructor() {
this.initializeRoutes();
}

// New routes will be declared here
private initializeRoutes() {
this.router.get('/wallet/:userId', this.wallet.getWallet);
this.router.post('/wallet', ValidationMiddleware(WalletDto), this.wallet.createWallet);
this.router.patch('/wallet/:userId', this.wallet.updateWallet);
this.router.delete('/wallet/:userId', this.wallet.deleteWallet);
}
}
3 changes: 2 additions & 1 deletion apps/backend/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { initializeOpenAI } from './utils/initializeOpenAI';
import { SubmissionRoute } from './routes/submission.route';
import { UserRoute } from './routes/users.route';
import { ProductRoute } from './routes/products.route';
import { WalletRoute } from './routes/wallet.route';

ValidateEnv();

export const openAIHelper = initializeOpenAI();

const app = new App([new SubmissionRoute(), new UserRoute(), new ProductRoute()]);
const app = new App([new SubmissionRoute(), new UserRoute(), new ProductRoute(), new WalletRoute()]);

app.listen();
76 changes: 75 additions & 1 deletion apps/backend/src/tests/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import mongoose from 'mongoose';
import { App } from '@/app';
import { UserRoute } from '@/routes/users.route';
import { ProductRoute } from '@/routes/products.route';
import { WalletRoute } from '@/routes/wallet.route';

// Mock data for testing
const mockUserData = {
Expand All @@ -15,7 +16,7 @@ const mockUserData = {

let mockUserId = '';

const app = new App([new UserRoute(), new ProductRoute()]).getServer();
const app = new App([new UserRoute(), new ProductRoute(), new WalletRoute()]).getServer();

// Test suite for User API endpoints
describe('User API Endpoints', () => {
Expand Down Expand Up @@ -170,3 +171,76 @@ describe('Product API Endpoints', () => {
}
});
});

// Test suite for wallet API endpoints
describe('Wallet API Endpoints', () => {
let testUserId: string;

beforeAll(async () => {
// Connect to the in-memory database if needed, or use a testing database
await mongoose.connect(process.env.MONGO_URI);
console.log('Connected to DB to perform Wallet tests');

// Create a new user for testing
const userResponse = await request(app).post('/users').send({ username: 'Test User', email: 'testuser@example.com', password: 'password123' });
testUserId = userResponse.body.user._id;
});

afterAll(async () => {
// Delete the test user
await request(app).delete(`/users/${testUserId}`);

// Disconnect from the database after all tests are complete
await mongoose.connection.close();
console.log('Disconnected from DB after Wallet tests');
});

// Test POST /wallet - Create a new wallet
it('should create a new wallet', async () => {
const response = await request(app).post('/wallet').send({ userId: testUserId });

expect(response.statusCode).toBe(200);
expect(response.body).toHaveProperty('msg', 'Wallet created successfully');
expect(response.body.wallet).toHaveProperty('user', testUserId);
});

// Test GET /wallet/:userId - Retrieve wallet information
it('should retrieve wallet information', async () => {
const response = await request(app).get(`/wallet/${testUserId}`);

if (response.statusCode === 200) {
expect(response.body).toHaveProperty('user', testUserId);
} else {
expect(response.statusCode).toBe(404);
expect(response.body).toHaveProperty('message', 'No wallet found for this user');
}
});

// Test PATCH /wallet/:userId - Update wallet information
it('should update wallet information', async () => {
const nftList = ['nft1', 'nft2', 'nft3'];

const response = await request(app).patch(`/wallet/${testUserId}`).send({ nftList });

if (response.statusCode === 200) {
expect(response.body).toHaveProperty('msg', 'Wallet details have been updated!');
expect(response.body.wallet).toHaveProperty('user', testUserId);
expect(response.body.wallet).toHaveProperty('nftList', nftList);
} else {
expect(response.statusCode).toBe(404);
expect(response.body).toHaveProperty('message', 'No wallet found for this user');
}
});

// Test DELETE /wallet/:userId - Delete wallet
it('should delete a wallet', async () => {
const response = await request(app).delete(`/wallet/${testUserId}`);

if (response.statusCode === 200) {
expect(response.body).toHaveProperty('msg', 'Wallet has been deleted');
} else {
expect(response.statusCode).toBe(404);
expect(response.body).toHaveProperty('message', 'No wallet found for this user');
}
});
});
19 changes: 19 additions & 0 deletions apps/backend/src/utils/createBlockchainWallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Wallet } from 'ethers';

interface WalletDetails {
walletAddress: string;
privateKey: string;
publicKey: string;
}

export default function createBlockchainWallet(): WalletDetails {
// Generate a random wallet
const wallet = Wallet.createRandom();

// Extract details
const privateKey = wallet.privateKey; // Use this securely
const publicKey = wallet.publicKey;
const walletAddress = wallet.address;

return { walletAddress, privateKey, publicKey };
}
Loading
Loading