diff --git a/idea/explorer/src/errors/index.ts b/idea/explorer/src/errors/index.ts index 5e2662b7dd..afb96d1dfd 100644 --- a/idea/explorer/src/errors/index.ts +++ b/idea/explorer/src/errors/index.ts @@ -2,5 +2,6 @@ export * from './code'; export * from './program'; export * from './message'; export * from './event'; +export * from './voucher'; export * from './jsonrpc'; export * from './base'; diff --git a/idea/explorer/src/errors/voucher.ts b/idea/explorer/src/errors/voucher.ts new file mode 100644 index 0000000000..7762de2b74 --- /dev/null +++ b/idea/explorer/src/errors/voucher.ts @@ -0,0 +1,6 @@ +import { JsonRpcError } from '../types'; + +export class VoucherNotFound implements JsonRpcError { + code = -32404; + message = 'Voucher not found'; +} diff --git a/idea/explorer/src/jsonrpc.ts b/idea/explorer/src/jsonrpc.ts index af58b7b16c..0c6cd5d38f 100644 --- a/idea/explorer/src/jsonrpc.ts +++ b/idea/explorer/src/jsonrpc.ts @@ -10,6 +10,8 @@ import { ParamGetMsgsToProgram, ParamGetProgram, ParamGetPrograms, + ParamGetVoucher, + ParamGetVouchers, ParamMsgFromProgram, ParamMsgToProgram, ParamSetProgramMeta, @@ -17,6 +19,7 @@ import { import { Cache } from './middlewares/caching'; import { redisConnect } from './middlewares/redis'; import { Retry } from './middlewares/retry'; +import { VoucherNotFound } from './errors'; export class JsonRpcServer extends JsonRpc(JsonRpcBase) { private _app: Express; @@ -30,6 +33,52 @@ export class JsonRpcServer extends JsonRpc(JsonRpcBase) { const result = await this.handleRequest(req.body); res.json(result); }); + + this._app.get('/api/voucher/:id', async (req, res) => { + const { genesis } = req.query; + if (!genesis) { + res.status(400).json({ error: 'Genesis not found in the request' }); + return; + } + + const voucherService = this._services.get(genesis.toString())?.voucher; + if (!voucherService) { + res.status(400).json({ error: 'Network is not supported' }); + return; + } + + try { + const voucher = await voucherService.getVoucher({ id: req.params.id, genesis: genesis.toString() }); + res.json(voucher); + } catch (error) { + if (error instanceof VoucherNotFound) { + res.json(null); + return; + } + res.status(500).json({ error: 'Internal server error' }); + } + }); + + this._app.post('/api/vouchers', async (req, res) => { + const { genesis } = req.query; + if (!genesis) { + res.status(400).json({ error: 'Genesis not found in the request' }); + return; + } + + const voucherService = this._services.get(genesis.toString())?.voucher; + if (!voucherService) { + res.status(400).json({ error: 'Network is not supported' }); + return; + } + + try { + const vouchers = await voucherService.getVouchers(req.body); + res.json(vouchers); + } catch (error) { + res.status(500).json({ error: 'Internal server error' }); + } + }); } public async run() { @@ -110,4 +159,16 @@ export class JsonRpcServer extends JsonRpc(JsonRpcBase) { async eventData(params: ParamGetEvent) { return this._services.get(params.genesis).event.getEvent(params); } + + @JsonRpcMethod('voucher.all') + @Cache(15) + async voucherAll(params: ParamGetVouchers) { + return this._services.get(params.genesis).voucher.getVouchers(params); + } + + @JsonRpcMethod('voucher.data') + @Cache(15) + async voucherData(params: ParamGetVoucher) { + return this._services.get(params.genesis).voucher.getVoucher(params); + } } diff --git a/idea/explorer/src/services/all-in-one.ts b/idea/explorer/src/services/all-in-one.ts index 7586cc5da6..903a9515ed 100644 --- a/idea/explorer/src/services/all-in-one.ts +++ b/idea/explorer/src/services/all-in-one.ts @@ -3,17 +3,20 @@ import { CodeService } from './code'; import { EventService } from './event'; import { MessageService } from './message'; import { ProgramService } from './program'; +import { VoucherService } from './voucher'; export class AllInOneService { public code: CodeService; public program: ProgramService; public message: MessageService; public event: EventService; + public voucher: VoucherService; constructor(dataSource: DataSource) { this.code = new CodeService(dataSource); this.program = new ProgramService(dataSource); this.message = new MessageService(dataSource); this.event = new EventService(dataSource); + this.voucher = new VoucherService(dataSource); } } diff --git a/idea/explorer/src/services/voucher.ts b/idea/explorer/src/services/voucher.ts new file mode 100644 index 0000000000..5c20ad27ee --- /dev/null +++ b/idea/explorer/src/services/voucher.ts @@ -0,0 +1,93 @@ +import { DataSource, Repository } from 'typeorm'; +import { Voucher } from 'indexer-db'; +import { Pagination } from '../decorators'; +import { ParamGetVoucher, ParamGetVouchers, ResManyResult } from '../types'; +import { VoucherNotFound } from '../errors'; +import { RequiredParams } from '../decorators/required'; + +export class VoucherService { + private _repo: Repository; + + constructor(dataSource: DataSource) { + this._repo = dataSource.getRepository(Voucher); + } + + @RequiredParams(['id']) + async getVoucher({ id }: ParamGetVoucher) { + const v = await this._repo.findOne({ where: { id } }); + + if (!v) { + throw new VoucherNotFound(); + } + + return v; + } + + @Pagination() + public async getVouchers({ + id, + owner, + spender, + declined, + codeUploading, + programs, + limit, + offset, + expired, + }: ParamGetVouchers): Promise> { + const qb = this._repo.createQueryBuilder('v'); + + if (id) { + if (id.length === 66) { + qb.andWhere('v.id = :id', { id }); + } else { + qb.andWhere('v.id LIKE :id', { id: `%${id}%` }); + } + } + + if (declined !== undefined) { + qb.andWhere('v.isDeclined = :declined', { declined }); + } + + if (codeUploading !== undefined) { + qb.andWhere('v.codeUploading = :codeUploading', { codeUploading }); + } + + if (programs) { + let where = '('; + let params: Record = {}; + + for (let i = 0; i < programs.length; i++) { + where += `${i > 0 ? ' OR ' : ''}v.programs::jsonb ? :p${i}`; + params[`p${i}`] = programs[i]; + } + qb.andWhere(where + ')', params); + console.log(qb.getQuery()); + } + + if (expired !== undefined) { + const now = new Date(); + if (expired) { + qb.andWhere('v.expiryAt < :now', { now }); + } else { + qb.andWhere('v.expiryAt >= :now', { now }); + } + } + + if (owner && spender) { + qb.andWhere('(v.owner = :owner OR v.spender = :spender)', { owner, spender }); + } else if (owner) { + qb.andWhere('v.owner = :owner', { owner }); + } else if (spender) { + qb.andWhere('v.spender = :spender', { spender }); + } + + qb.limit(limit || 20); + qb.offset(offset || 0); + qb.orderBy('v.issuedAt', 'DESC'); + + const [result, count] = await qb.getManyAndCount(); + + return { result, count }; + } +} diff --git a/idea/explorer/src/types/requests/index.ts b/idea/explorer/src/types/requests/index.ts index b6e6e7cafc..abc7d0074a 100644 --- a/idea/explorer/src/types/requests/index.ts +++ b/idea/explorer/src/types/requests/index.ts @@ -2,3 +2,4 @@ export * from './event'; export * from './message'; export * from './program'; export * from './code'; +export * from './voucher'; diff --git a/idea/explorer/src/types/requests/voucher.ts b/idea/explorer/src/types/requests/voucher.ts new file mode 100644 index 0000000000..ac693b8b8c --- /dev/null +++ b/idea/explorer/src/types/requests/voucher.ts @@ -0,0 +1,20 @@ +import { IsString, Contains } from 'class-validator'; +import { ParamPagination } from './common'; + +export class ParamGetVoucher extends ParamPagination { + @IsString() + @Contains('0x') + readonly id: string; +} + +export class ParamGetVouchers extends ParamPagination { + @IsString() + @Contains('0x') + readonly id?: string; + readonly owner?: string; + readonly spender?: string; + readonly programs?: string[]; + readonly codeUploading?: boolean; + readonly declined?: boolean; + readonly expired?: boolean; +}