Skip to content

Commit

Permalink
fix: chunk the code into multiple parts (#44)
Browse files Browse the repository at this point in the history
* fix: chunk the code into multiple parts

* fix: permission
  • Loading branch information
sirily11 authored May 22, 2023
1 parent 8e77de9 commit 3ea82f9
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 26 deletions.
28 changes: 28 additions & 0 deletions src/token/dto/get-token-history.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { z } from 'zod';

export const GetTokenHistorySchema = z.object({
_id: z.string(),
transactions: z.array(
z.object({
_id: z.string(),
value: z.string(),
timestamp: z.string(),
type: z.string(),
txHash: z.string().optional(),
Video: z
.object({
thumbnail: z.string(),
_id: z.string(),
title: z.string(),
})
.optional(),
}),
),
});

export type GetTokenHistoryDto = z.infer<typeof GetTokenHistorySchema>;
export const GetTokenHistoryCountSchema = z
.object({
total: z.number().int().positive(),
})
.array();
4 changes: 3 additions & 1 deletion src/token/dto/smart-contract.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { z } from 'zod';

export const SmartContract = z.object({
export const SmartContractSchema = z.object({
balanceOf: z.function(z.tuple([z.string()]), z.any().promise()),
// function reward(address _to, uint256 _amount) public onlyOwner
reward: z.function(z.tuple([z.string(), z.number()]), z.any()),
Expand All @@ -12,3 +12,5 @@ export const SmartContract = z.object({
z.boolean().promise(),
),
});

export type SmartContract = z.infer<typeof SmartContractSchema>;
8 changes: 2 additions & 6 deletions src/token/token.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,7 @@ export class TokenController {
@Param('page') page: string | undefined,
) {
const { page: pageInt, limit: limitInt } = getPageAndLimit(page, per);
return await this.tokenService.getTransactionHistory(
userId,
limitInt,
pageInt,
);
return await this.tokenService.getTokenHistory(userId, limitInt, pageInt);
}

@UseGuards(JwtAuthGuard)
Expand All @@ -51,7 +47,7 @@ export class TokenController {
) {
const { page: pageInt, limit: limitInt } = getPageAndLimit(page, per);

return await this.tokenService.getTransactionHistory(
return await this.tokenService.getTokenHistory(
user.user.userId,
limitInt,
pageInt,
Expand Down
4 changes: 2 additions & 2 deletions src/token/token.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ describe('TokenService', () => {
});

it('Should be able to list my transaction when history is empty', async () => {
const transactions = await service.getTransactionHistory(user.id, 1, 1);
const transactions = await service.getTokenHistory(user.id, 1, 1);
expect(transactions.items).toHaveLength(0);
expect(transactions.metadata.totalPages).toBe(0);
expect(transactions.metadata.total).toBe(0);
Expand Down Expand Up @@ -123,7 +123,7 @@ describe('TokenService', () => {
await service.rewardToken(user.id, '100', video.id, tx as any);
});

const transactions = await service.getTransactionHistory(user.id, 1, 1);
const transactions = await service.getTokenHistory(user.id, 1, 1);
expect(transactions.items).toHaveLength(1);
expect(transactions.metadata.totalPages).toBe(1);
expect(transactions.metadata.total).toBe(1);
Expand Down
113 changes: 96 additions & 17 deletions src/token/token.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { Injectable } from '@nestjs/common';
import { PrismaClient, TokenHistoryType } from '@prisma/client';
import { PrismaClient, TokenHistoryType, User, Wallet } from '@prisma/client';
import { PrismaService } from '../prisma.service';
import * as abi from './abi.json';
import { ethers } from 'ethers';
import { SmartContract } from './dto/smart-contract';
import { SmartContract, SmartContractSchema } from './dto/smart-contract';
import { getPaginationMetaData } from '../common/pagination';
import { objectIdToId } from '../common/objectIdToId';
import { StorageService } from '../storage/storage.service';
import {
GetTokenHistoryCountSchema,
GetTokenHistoryDto,
GetTokenHistorySchema,
} from './dto/get-token-history.dto';

@Injectable()
export class TokenService {
Expand All @@ -15,27 +20,34 @@ export class TokenService {
private readonly storage: StorageService,
) {}

private async getContract(pk?: string) {
/**
* Get contract object using solidity abi.
* @param pk Private key of the user. If not specified, it will use the private key specified in the environment
*/
protected async getContract(pk?: string) {
// Connect to the RPC url specified in the environment, or localhost if not specified
const provider = new ethers.providers.JsonRpcProvider(process.env.RPC_URL!);
// Connect to the blockchain with the private key specified in the environment
const signer = new ethers.Wallet(pk ?? process.env.PRIVATE_KEY!, provider);
// Connect to the contract with the contract address specified in the environment
const contract = new ethers.Contract(
process.env.CONTRACT_ADDRESS!,
abi.abi,
signer,
);
return SmartContract.parse(contract);
// Return the contract object
return SmartContractSchema.parse(contract);
}

/**
* Reward user with token
*/
async rewardToken(
public async rewardToken(
user: string,
value: string,
videoId: string,
tx: PrismaClient,
) {
const contract = await this.getContract();
const userToBeRewarded = await tx.user.findUnique({
where: {
id: user,
Expand All @@ -45,21 +57,52 @@ export class TokenService {
},
});
// call smart contract to reward user
const transaction = await contract.reward(
const transaction = await this.rewardUser(
userToBeRewarded.Wallet.address,
parseFloat(value),
value,
);
// await transaction.wait();

// create token history
await this.createTokenHistory(user, transaction.hash, value, videoId, tx);
}

/**
* Reward user with token.
* This function will call smart contract to reward user.
* @param address User's wallet address
* @param value Amount of token to be rewarded
* @returns
*/
protected async rewardUser(address: string, value: string) {
const contract = await this.getContract();
const transaction = await contract.reward(address, parseFloat(value));
return transaction;
}

/**
* Create token history in database
* @param user User's id
* @param txHash Transaction hash. This hash is returned from smart contract
* @param value Amount of token
* @param videoId Video's id
* @param tx PrismaClient
*/
protected async createTokenHistory(
user: string,
txHash: string,
value: string,
videoId: string,
tx: PrismaClient,
) {
await tx.tokenHistory.create({
data: {
user: {
connect: {
id: user,
},
},
txHash: transaction.hash,
txHash: txHash,
value: value,
timestamp: new Date().toISOString(),
type: TokenHistoryType.REWARD,
Expand All @@ -72,7 +115,13 @@ export class TokenService {
});
}

async getTransactionHistory(user: string, per: number, page: number) {
/**
* Get token history of a user
* @param user User's id
* @param per Number of items per page
* @param page Page number
*/
public async getTokenHistory(user: string, per: number, page: number) {
const pipeline = [
{
$match: {
Expand Down Expand Up @@ -134,8 +183,12 @@ export class TokenService {
tokenHistoryPromise,
count,
]);
const itemsPromise = (tokenHistory as any)

const verifiedTotalResult = GetTokenHistoryCountSchema.parse(totalResult);

const itemsPromise = (tokenHistory as unknown as any[])
.map((item) => objectIdToId(item))
.map<GetTokenHistoryDto>((item) => GetTokenHistorySchema.parse(item))
.map(async (item) => ({
...item,
transactions: await Promise.all(
Expand All @@ -147,9 +200,9 @@ export class TokenService {
...tx.Video,
thumbnail: (
await this.storage.generatePreSignedUrlForThumbnail({
...tx.video,
...tx.Video,
id: tx.Video._id,
})
} as any)
).previewUrl,
}
: undefined,
Expand All @@ -162,12 +215,17 @@ export class TokenService {
metadata: getPaginationMetaData(
page,
per,
(totalResult[0] as any)?.total ?? 0,
verifiedTotalResult[0]?.total ?? 0,
),
};
}

async getTotalToken(user: string): Promise<string> {
/**
* Get total token of a user
* @param user User's id
* @returns
*/
public async getTotalToken(user: string): Promise<string> {
const userObj = await this.prisma.user.findUnique({
where: {
id: user,
Expand All @@ -181,7 +239,10 @@ export class TokenService {
return ethers.utils.formatUnits(totalToken, 'wei');
}

async useToken(
/**
* Spend token to purchase video
*/
public async useToken(
fromUser: string,
toUser: string,
value: string,
Expand All @@ -207,6 +268,24 @@ export class TokenService {

const contract = await this.getContract(spender.Wallet.privateKey);

await this.purchase(contract, spender, receiver, value, tx);
}

/**
* Purchase video using [spender]'s token to [receiver]
* @param contract Smart contract object
* @param spender User who will spend token
* @param receiver User who will receive token
* @param value Amount of token to be spent
* @param tx PrismaClient
*/
protected async purchase(
contract: SmartContract,
spender: User & { Wallet: Wallet },
receiver: User & { Wallet: Wallet },
value: string,
tx: PrismaClient,
) {
const canPurchase = await contract.canPurchase(
spender.Wallet.address,
receiver.Wallet.address,
Expand All @@ -226,7 +305,7 @@ export class TokenService {
data: {
user: {
connect: {
id: fromUser,
id: spender.id,
},
},
value: `-${value}`,
Expand Down

1 comment on commit 3ea82f9

@vercel
Copy link

@vercel vercel bot commented on 3ea82f9 May 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.