Skip to content

Commit

Permalink
Create wallet API (#6)
Browse files Browse the repository at this point in the history
* created simple endpoint which interacts with the vechain testnet

* able to create and retrieve a wallet

* fixed updateWallet function and created deleteWallet endpoint

* created test suite for wallet endpoints

* created documentation for wallet api endpoints
  • Loading branch information
KShervington authored Nov 24, 2024
1 parent 26abac8 commit ff9f3de
Show file tree
Hide file tree
Showing 13 changed files with 1,045 additions and 119 deletions.
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

0 comments on commit ff9f3de

Please sign in to comment.