diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58f713cf6..222ba6c03 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: ./.github/workflows/setup - - run: pnpm wallet:backend test + - run: pnpm wallet:backend test --detectOpenHandles --forceExit test-boutique-backend: name: BOUTIQUE - Test backend diff --git a/docker/dev/docker-compose.yml b/docker/dev/docker-compose.yml index f432cb025..0866e9c61 100644 --- a/docker/dev/docker-compose.yml +++ b/docker/dev/docker-compose.yml @@ -25,6 +25,7 @@ services: depends_on: - postgres - rafiki-backend + - redis environment: NODE_ENV: development PORT: 3003 @@ -45,10 +46,11 @@ services: FROM_EMAIL: ${FROM_EMAIL} SEND_EMAIL: ${SEND_EMAIL} RATE_API_KEY: ${RATE_API_KEY} - BASE_ASSET_SCALE: 2, - MAX_ASSET_SCALE: 9, + BASE_ASSET_SCALE: 2 + MAX_ASSET_SCALE: 9 WM_THRESHOLD: 100000000 DEBT_THRESHOLD: 5 + REDIS_URL: ${REDIS_URL} restart: always networks: - testnet diff --git a/docker/prod/.env.example b/docker/prod/.env.example index ea9371843..623135285 100644 --- a/docker/prod/.env.example +++ b/docker/prod/.env.example @@ -15,6 +15,7 @@ WALLET_FRONTEND_AUTH_HOST= # WALLET BACKEND WALLET_BACKEND_PORT= WALLET_BACKEND_DATABASE_URL= +WALLET_BACKEND_REDIS_URL= WALLET_BACKEND_COOKIE_NAME= WALLET_BACKEND_COOKIE_PASSWORD= WALLET_BACKEND_COOKIE_TTL= diff --git a/packages/wallet/backend/jest.setup.js b/packages/wallet/backend/jest.setup.js index 32fa2cbe2..ee7c0bf24 100644 --- a/packages/wallet/backend/jest.setup.js +++ b/packages/wallet/backend/jest.setup.js @@ -4,6 +4,7 @@ const { randomBytes } = require('crypto') const POSTGRES_PASSWORD = 'password' const POSTGRES_DB = randomBytes(16).toString('hex') const POSTGRES_PORT = 5432 +const REDIS_PORT = 6379 module.exports = async () => { const container = await new GenericContainer('postgres:15') @@ -14,9 +15,16 @@ module.exports = async () => { .withExposedPorts(POSTGRES_PORT) .start() + const redisContainer = await new GenericContainer('redis:7') + .withExposedPorts(REDIS_PORT) + .start() + + process.env.REDIS_URL = `redis://redis:${REDIS_PORT}/0` + process.env.DATABASE_URL = `postgresql://postgres:${POSTGRES_PASSWORD}@localhost:${container.getMappedPort( POSTGRES_PORT )}/${POSTGRES_DB}` - global.__POSTGRES_CONTAINER__ = container + global.__TESTING_POSTGRES_CONTAINER__ = container + global.__TESTING_REDIS_CONTAINER__ = redisContainer } diff --git a/packages/wallet/backend/jest.teardown.js b/packages/wallet/backend/jest.teardown.js index 77def90f1..442839ffe 100644 --- a/packages/wallet/backend/jest.teardown.js +++ b/packages/wallet/backend/jest.teardown.js @@ -1,5 +1,6 @@ module.exports = async () => { if (global.__TESTING_POSTGRES_CONTAINER__) { await global.__TESTING_POSTGRES_CONTAINER__.stop() + await global.__TESTING_REDIS_CONTAINER__.stop() } } diff --git a/packages/wallet/backend/migrations/20231013113754_add_wm_payment_pointers_table.js b/packages/wallet/backend/migrations/20231013113754_add_wm_payment_pointers_table.js deleted file mode 100644 index b9a070365..000000000 --- a/packages/wallet/backend/migrations/20231013113754_add_wm_payment_pointers_table.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @param { import("knex").Knex } knex - * @returns { Promise } - */ -exports.up = function (knex) { - return knex.schema.createTable('wmPaymentPointers', (table) => { - table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')) - table.string('url').notNullable() - table.string('publicName').notNullable() - - table.uuid('accountId').notNullable() - table.foreign('accountId').references('accounts.id').onDelete('CASCADE') - table.boolean('active').notNullable().defaultTo(true) - table.string('assetCode', 3).notNullable() - table.smallint('assetScale').notNullable() - table.decimal('balance', 10, 2).notNullable().defaultTo(0.0) - table.jsonb('keyIds') - - table.timestamp('createdAt').notNullable() - table.timestamp('updatedAt').notNullable() - }) -} - -/** - * @param { import("knex").Knex } knex - * @returns { Promise } - */ -exports.down = function (knex) { - return knex.schema.dropTableIfExists('wmPaymentPointers') -} diff --git a/packages/wallet/backend/migrations/20231013114620_add_wm_transactions_table.js b/packages/wallet/backend/migrations/20231013114620_add_wm_transactions_table.js index 1569fd62b..8f6dbbc69 100644 --- a/packages/wallet/backend/migrations/20231013114620_add_wm_transactions_table.js +++ b/packages/wallet/backend/migrations/20231013114620_add_wm_transactions_table.js @@ -7,8 +7,8 @@ exports.up = function (knex) { table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')) table.uuid('paymentId').notNullable() - table.uuid('wmPaymentPointerId').notNullable() - table.foreign('wmPaymentPointerId').references('wmPaymentPointers.id') + table.uuid('paymentPointerId').notNullable() + table.foreign('paymentPointerId').references('paymentPointers.id') table.bigint('value').notNullable() diff --git a/packages/wallet/backend/migrations/20231018084125_update_payment_pointers_table.js b/packages/wallet/backend/migrations/20231018084125_update_payment_pointers_table.js new file mode 100644 index 000000000..85f8a8270 --- /dev/null +++ b/packages/wallet/backend/migrations/20231018084125_update_payment_pointers_table.js @@ -0,0 +1,25 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex.schema.alterTable('paymentPointers', (table) => { + table.boolean('isWM').defaultTo(false) + table.string('assetCode', 3) + table.smallint('assetScale') + table.decimal('balance', 10, 2).defaultTo(0) + }) +} + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex.schema.alterTable('paymentPointers', (table) => { + table.dropColumn('isWM') + table.dropColumn('assetCode') + table.dropColumn('assetScale') + table.dropColumn('balance') + }) +} diff --git a/packages/wallet/backend/package.json b/packages/wallet/backend/package.json index aade0e31d..9c148851c 100644 --- a/packages/wallet/backend/package.json +++ b/packages/wallet/backend/package.json @@ -19,13 +19,14 @@ "graphql-request": "^6.1.0", "hash-wasm": "^4.9.0", "helmet": "^7.0.0", + "ioredis": "^5.3.2", "iron-session": "^6.3.1", "knex": "^3.0.1", "node-cache": "^5.1.2", "objection": "^3.1.2", "pg": "^8.11.3", - "socket.io": "^4.7.2", "randexp": "^0.5.3", + "socket.io": "^4.7.2", "uuid": "^9.0.1", "winston": "^3.11.0", "zod": "^3.22.4" @@ -40,9 +41,9 @@ "@types/express": "^4.17.19", "@types/jest": "^29.5.5", "@types/node": "^18.14.0", + "@types/socket.io": "^3.0.2", "@types/uuid": "^9.0.5", "jest": "^29.7.0", - "@types/socket.io": "^3.0.2", "node-mocks-http": "^1.13.0", "testcontainers": "^10.2.1", "ts-jest": "^29.1.1", diff --git a/packages/wallet/backend/src/app.ts b/packages/wallet/backend/src/app.ts index ca21e5432..5f62ad82c 100644 --- a/packages/wallet/backend/src/app.ts +++ b/packages/wallet/backend/src/app.ts @@ -41,11 +41,14 @@ import { UserController } from './user/controller' import type { UserService } from './user/service' import { SocketService } from './socket/service' import { GrantService } from '@/grant/service' +import { RedisClient } from './cache/redis-client' +import { WMTransactionService } from '@/webMonetization/transaction/service' export interface Bindings { env: Env logger: Logger knex: Knex + redisClient: RedisClient rapydClient: RapydClient rafikiClient: RafikiClient rafikiService: RafikiService @@ -76,6 +79,7 @@ export interface Bindings { grantService: GrantService emailService: EmailService socketService: SocketService + wmTransactionService: WMTransactionService } export class App { diff --git a/packages/wallet/backend/src/cache/redis-client.ts b/packages/wallet/backend/src/cache/redis-client.ts new file mode 100644 index 000000000..9d3dd0cc0 --- /dev/null +++ b/packages/wallet/backend/src/cache/redis-client.ts @@ -0,0 +1,48 @@ +import { Redis } from 'ioredis' + +export type EntryOptions = { + expiry?: number +} + +export interface IRedisClient { + set( + key: string, + value: T | string, + options?: EntryOptions + ): Promise + get(key: string): Promise + delete(key: string): Promise +} + +export class RedisClient implements IRedisClient { + constructor(private redis: Redis) {} + + async set( + key: string, + value: T | string, + options?: EntryOptions + ): Promise { + const serializedValue = + typeof value === 'string' ? value : JSON.stringify(value) + + if (options?.expiry) { + return await this.redis.set(key, serializedValue, 'EX', options.expiry) + } + + return await this.redis.set(key, serializedValue) + } + + async get(key: string): Promise { + const serializedValue = await this.redis.get(key) + if (serializedValue) { + return typeof serializedValue === 'string' + ? (JSON.parse(serializedValue) as T) + : serializedValue + } + return null + } + + async delete(key: string): Promise { + return await this.redis.del(key) + } +} diff --git a/packages/wallet/backend/src/cache/service.ts b/packages/wallet/backend/src/cache/service.ts new file mode 100644 index 000000000..42f31586d --- /dev/null +++ b/packages/wallet/backend/src/cache/service.ts @@ -0,0 +1,37 @@ +import { EntryOptions, IRedisClient } from './redis-client' + +export interface ICacheService { + set(key: string, value: T | string): Promise + get(key: string): Promise + delete(key: string): Promise +} + +export class Cache implements ICacheService { + private namespace: string = 'Testnet:' + + constructor( + private cache: IRedisClient, + namespace: string + ) { + this.namespace = this.namespace + namespace + ':' + } + + async set( + key: string, + value: T | string, + options?: EntryOptions + ): Promise { + const namespacedKey = this.namespace + key + return await this.cache.set(namespacedKey, value, options) + } + + async get(key: string): Promise { + const namespacedKey = this.namespace + key + return await this.cache.get(namespacedKey) + } + + async delete(key: string): Promise { + const namespacedKey = this.namespace + key + return await this.cache.delete(namespacedKey) + } +} diff --git a/packages/wallet/backend/src/config/env.ts b/packages/wallet/backend/src/config/env.ts index 4b847d816..ebc4c8a44 100644 --- a/packages/wallet/backend/src/config/env.ts +++ b/packages/wallet/backend/src/config/env.ts @@ -6,6 +6,7 @@ const envSchema = z.object({ DATABASE_URL: z .string() .default('postgres://postgres:password@localhost:5433/wallet_backend'), + REDIS_URL: z.string().default('redis://redis:6379/0'), COOKIE_NAME: z.string().default('testnet.cookie'), COOKIE_PASSWORD: z .string() @@ -31,10 +32,10 @@ const envSchema = z.object({ .enum(['true', 'false']) .default('false') .transform((value) => value === 'true'), - BASE_ASSET_SCALE: z.number().nonnegative().default(2), - MAX_ASSET_SCALE: z.number().nonnegative().default(9), - WM_THRESHOLD: z.bigint().nonnegative().default(100_000_000n), // $0.1 in asset scale 9 - DEBT_THRESHOLD: z.number().multipleOf(0.01).nonnegative().default(5.0) // $5.00 + BASE_ASSET_SCALE: z.coerce.number().nonnegative().default(2), + MAX_ASSET_SCALE: z.coerce.number().nonnegative().default(9), + WM_THRESHOLD: z.coerce.bigint().nonnegative().default(100_000_000n), // $0.1 in asset scale 9 + DEBT_THRESHOLD: z.coerce.number().multipleOf(0.01).nonnegative().default(5.0) // $5.00 }) export type Env = z.infer diff --git a/packages/wallet/backend/src/createContainer.ts b/packages/wallet/backend/src/createContainer.ts index 53c74dfee..49abfd279 100644 --- a/packages/wallet/backend/src/createContainer.ts +++ b/packages/wallet/backend/src/createContainer.ts @@ -34,6 +34,11 @@ import knex from 'knex' import { SocketService } from './socket/service' import { GrantService } from './grant/service' import { RatesService } from './rates/service' +import { Cache } from './cache/service' +import { RedisClient } from './cache/redis-client' +import { Redis } from 'ioredis' +import { PaymentPointer } from './paymentPointer/model' +import { WMTransactionService } from '@/webMonetization/transaction/service' export const createContainer = (config: Env): Container => { const container = new Container() @@ -179,6 +184,26 @@ export const createContainer = (config: Env): Container => { async () => new RatesService({ env: await container.resolve('env') }) ) + container.singleton('redisClient', async () => { + const env = await container.resolve('env') + const redis = new Redis(env.REDIS_URL) + return new RedisClient(redis) + }) + + container.singleton( + 'paymentPointerService', + async () => + new PaymentPointerService({ + env: await container.resolve('env'), + rafikiClient: await container.resolve('rafikiClient'), + accountService: await container.resolve('accountService'), + cache: new Cache( + await container.resolve('redisClient'), + 'WMPaymentPointers' + ) + }) + ) + container.singleton( 'rapydController', async () => @@ -192,16 +217,6 @@ export const createContainer = (config: Env): Container => { }) ) - container.singleton( - 'paymentPointerService', - async () => - new PaymentPointerService({ - env: await container.resolve('env'), - rafikiClient: await container.resolve('rafikiClient'), - accountService: await container.resolve('accountService') - }) - ) - container.singleton( 'paymentPointerController', async () => @@ -270,6 +285,15 @@ export const createContainer = (config: Env): Container => { }) ) + container.singleton( + 'wmTransactionService', + async () => + new WMTransactionService({ + logger: await container.resolve('logger'), + paymentPointerService: await container.resolve('paymentPointerService') + }) + ) + container.singleton('rafikiService', async () => { const rapydClient = await container.resolve('rapydClient') const env = await container.resolve('env') @@ -279,6 +303,10 @@ export const createContainer = (config: Env): Container => { const socketService = await container.resolve('socketService') const userService = await container.resolve('userService') const ratesService = await container.resolve('ratesService') + const wmTransactionService = await container.resolve('wmTransactionService') + const paymentPointerService = await container.resolve( + 'paymentPointerService' + ) return new RafikiService({ rafikiClient, @@ -288,7 +316,9 @@ export const createContainer = (config: Env): Container => { logger, transactionService, socketService, - userService + userService, + wmTransactionService, + paymentPointerService }) }) diff --git a/packages/wallet/backend/src/paymentPointer/controller.ts b/packages/wallet/backend/src/paymentPointer/controller.ts index 6408c92e3..091ed1be1 100644 --- a/packages/wallet/backend/src/paymentPointer/controller.ts +++ b/packages/wallet/backend/src/paymentPointer/controller.ts @@ -2,7 +2,11 @@ import { PaymentPointer } from '@/paymentPointer/model' import { validate } from '@/shared/validate' import type { NextFunction, Request } from 'express' import type { Logger } from 'winston' -import { ExternalPaymentPointer, PaymentPointerService } from './service' +import { + ExternalPaymentPointer, + PaymentPointerList, + PaymentPointerService +} from './service' import { externalPaymentPointerSchema, paymentPointerSchema, @@ -11,7 +15,7 @@ import { interface IPaymentPointerController { create: ControllerFunction - list: ControllerFunction + list: ControllerFunction getById: ControllerFunction softDelete: ControllerFunction } @@ -38,15 +42,16 @@ export class PaymentPointerController implements IPaymentPointerController { const userId = req.session.user.id const { accountId } = req.params const { - body: { paymentPointerName, publicName } + body: { paymentPointerName, publicName, isWM } } = await validate(paymentPointerSchema, req) - const paymentPointer = await this.deps.paymentPointerService.create( + const paymentPointer = await this.deps.paymentPointerService.create({ userId, accountId, paymentPointerName, - publicName - ) + publicName, + isWM + }) res .status(200) .json({ success: true, message: 'SUCCESS', data: paymentPointer }) @@ -57,7 +62,7 @@ export class PaymentPointerController implements IPaymentPointerController { list = async ( req: Request, - res: CustomResponse, + res: CustomResponse, next: NextFunction ) => { const userId = req.session.user.id @@ -68,7 +73,6 @@ export class PaymentPointerController implements IPaymentPointerController { userId, accountId ) - res .status(200) .json({ success: true, message: 'SUCCESS', data: paymentPointers }) @@ -123,14 +127,14 @@ export class PaymentPointerController implements IPaymentPointerController { next: NextFunction ) => { const userId = req.session.user.id - const { accountId, id } = req.params + const { accountId, id: paymentPointerId } = req.params try { - const paymentPointer = await this.deps.paymentPointerService.getById( + const paymentPointer = await this.deps.paymentPointerService.getById({ userId, accountId, - id - ) + paymentPointerId + }) res .status(200) diff --git a/packages/wallet/backend/src/paymentPointer/model.ts b/packages/wallet/backend/src/paymentPointer/model.ts index cd01728ef..9b1906423 100644 --- a/packages/wallet/backend/src/paymentPointer/model.ts +++ b/packages/wallet/backend/src/paymentPointer/model.ts @@ -10,19 +10,21 @@ interface PaymentPointerKey { createdOn: Date } -export class PaymentPointerBaseModel extends BaseModel { +export class PaymentPointer extends BaseModel { + static tableName = 'paymentPointers' + publicName!: string readonly id!: string readonly url!: string readonly accountId!: string + isWM!: boolean + assetCode!: string | null + assetScale!: number | null + balance!: number active!: boolean account!: Account transactions!: Array keyIds!: PaymentPointerKey | null -} - -export class PaymentPointer extends PaymentPointerBaseModel { - static tableName = 'paymentPointers' static relationMappings = () => ({ account: { diff --git a/packages/wallet/backend/src/paymentPointer/service.ts b/packages/wallet/backend/src/paymentPointer/service.ts index feaaebc62..a374840a8 100644 --- a/packages/wallet/backend/src/paymentPointer/service.ts +++ b/packages/wallet/backend/src/paymentPointer/service.ts @@ -7,9 +7,10 @@ import { generateJwk } from '@/utils/jwk' import axios from 'axios' import { generateKeyPairSync, getRandomValues } from 'crypto' import { v4 as uuid } from 'uuid' +import { Cache } from '../cache/service' import { PaymentPointer } from './model' -interface UpdatePaymentPointerArgs { +export interface UpdatePaymentPointerArgs { userId: string accountId: string paymentPointerId: string @@ -24,20 +25,34 @@ export interface ExternalPaymentPointer { authServer: string } +export interface CreatePaymentPointerArgs { + userId: string + accountId: string + paymentPointerName: string + publicName: string + isWM: boolean +} + +export type UpdatePaymentPointerBalanceArgs = { + paymentPointerId: string + balance: number +} + +export type GetPaymentPointerArgs = { + paymentPointerId: string + accountId?: string + userId?: string +} + +export type PaymentPointerList = { + wmPaymentPointers: PaymentPointer[] + paymentPointers: PaymentPointer[] +} interface IPaymentPointerService { - create: ( - userId: string, - accountId: string, - paymentPointerName: string, - publicName: string - ) => Promise + create: (params: CreatePaymentPointerArgs) => Promise update: (args: UpdatePaymentPointerArgs) => Promise - list: (userId: string, accountId: string) => Promise - getById: ( - userId: string, - accountId: string, - id: string - ) => Promise + list: (userId: string, accountId: string) => Promise + getById: (args: GetPaymentPointerArgs) => Promise softDelete: (userId: string, id: string) => Promise } @@ -45,6 +60,7 @@ interface PaymentPointerServiceDependencies { accountService: AccountService rafikiClient: RafikiClient env: Env + cache: Cache } export const createPaymentPointerIfFalsy = async ({ @@ -64,12 +80,13 @@ export const createPaymentPointerIfFalsy = async ({ return paymentPointer } - const newPaymentPointer = await paymentPointerService.create( + const newPaymentPointer = await paymentPointerService.create({ userId, accountId, - getRandomValues(new Uint32Array(1))[0].toString(16), - publicName - ) + paymentPointerName: getRandomValues(new Uint32Array(1))[0].toString(16), + publicName, + isWM: false + }) return newPaymentPointer } @@ -77,32 +94,30 @@ export const createPaymentPointerIfFalsy = async ({ export class PaymentPointerService implements IPaymentPointerService { constructor(private deps: PaymentPointerServiceDependencies) {} - async create( - userId: string, - accountId: string, - paymentPointerName: string, - publicName: string - ): Promise { + async create(args: CreatePaymentPointerArgs): Promise { const account = await this.deps.accountService.findAccountById( - accountId, - userId + args.accountId, + args.userId ) - const url = `${this.deps.env.OPEN_PAYMENTS_HOST}/${paymentPointerName}` + const url = `${this.deps.env.OPEN_PAYMENTS_HOST}/${args.paymentPointerName}` let paymentPointer = await PaymentPointer.query().findOne({ url }) if (paymentPointer) { - if (paymentPointer.accountId != accountId || account.userId !== userId) { + if ( + paymentPointer.accountId != args.accountId || + account.userId !== args.userId + ) { throw new Conflict( 'This payment pointer already exists. Please choose another name.' ) } else if ( - paymentPointer.accountId === accountId && - account.userId === userId + paymentPointer.accountId === args.accountId && + account.userId === args.userId ) { paymentPointer = await PaymentPointer.query().patchAndFetchById( paymentPointer.id, { - publicName, + publicName: args.publicName, active: true } ) @@ -110,61 +125,132 @@ export class PaymentPointerService implements IPaymentPointerService { } else { const rafikiPaymentPointer = await this.deps.rafikiClient.createRafikiPaymentPointer( - publicName, + args.publicName, account.assetId, url ) + let assetScale = null + let assetCode = null + if (args.isWM) { + //* Web monetization feature requires an asset with scale MAX_ASSET_SCALE to exist. It's default assetCode is USD for now + const webMonetizationAsset = + await this.deps.rafikiClient.getRafikiAsset( + 'USD', + this.deps.env.MAX_ASSET_SCALE + ) + + if (!webMonetizationAsset) { + throw new NotFound('Web monetization asset not found.') + } + + assetScale = webMonetizationAsset.scale + assetCode = webMonetizationAsset.code + } + paymentPointer = await PaymentPointer.query().insert({ url: rafikiPaymentPointer.url, - publicName, - accountId, - id: rafikiPaymentPointer.id + publicName: args.publicName, + accountId: args.accountId, + id: rafikiPaymentPointer.id, + assetCode, + assetScale, + balance: 0, + isWM: args.isWM }) + + args.isWM && + (await this.deps.cache.set(paymentPointer.id, paymentPointer, { + expiry: 60 + })) } return paymentPointer } - async list(userId: string, accountId: string): Promise { - // Validate that account id belongs to current user + async list(userId: string, accountId: string): Promise { const account = await this.deps.accountService.findAccountById( accountId, userId ) - return PaymentPointer.query() + const paymentPointersResult = await PaymentPointer.query() .where('accountId', account.id) .where('active', true) + + const result = paymentPointersResult.reduce( + (acc, pp) => { + if (pp.isWM) { + acc.wmPaymentPointers.push(pp) + } else { + acc.paymentPointers.push(pp) + } + return acc + }, + { + wmPaymentPointers: [] as PaymentPointer[], + paymentPointers: [] as PaymentPointer[] + } + ) + + return result } async listAll(userId: string): Promise { - return PaymentPointer.query().joinRelated('account').where({ - 'account.userId': userId, - 'paymentPointers.active': true - }) + return PaymentPointer.query() + .where({ isWM: false, active: true }) + .joinRelated('account') + .where({ + 'account.userId': userId + }) } - async getById( - userId: string, - accountId: string, - id: string - ): Promise { - // Validate that account id belongs to current user - await this.deps.accountService.findAccountById(accountId, userId) + async getById(args: GetPaymentPointerArgs): Promise { + //* Cache only contains PaymentPointers with isWM = true + const cacheHit = await this.deps.cache.get(args.paymentPointerId) + if (cacheHit) { + //* TODO: reset ttl + return cacheHit + } - const paymentPointer = await PaymentPointer.query() - .findById(id) - .where('accountId', accountId) + if (args.userId && args.accountId) { + await this.deps.accountService.findAccountById( + args.accountId, + args.userId + ) + } + + const query = PaymentPointer.query() + .findById(args.paymentPointerId) .where('active', true) + if (args.accountId) { + query.where('accountId', args.accountId) + } + const paymentPointer = await query if (!paymentPointer) { throw new NotFound() } + if (paymentPointer.isWM) { + await this.deps.cache.set(paymentPointer.id, paymentPointer, { + expiry: 60 + }) + } + return paymentPointer } + async updateBalance(args: UpdatePaymentPointerBalanceArgs): Promise { + const { paymentPointerId, balance } = args + + const paymentPointer = await this.getById({ paymentPointerId }) + if (!paymentPointer) { + throw new NotFound(`Web monetization payment pointer does not exist.`) + } + await paymentPointer.$query().patch({ balance }) + } + async listIdentifiersByUserId(userId: string): Promise { const accounts = await Account.query() .where('userId', userId) @@ -214,12 +300,11 @@ export class PaymentPointerService implements IPaymentPointerService { accountId: string, paymentPointerId: string ): Promise<{ privateKey: string; publicKey: string; keyId: string }> { - const paymentPointer = await this.getById( + const paymentPointer = await this.getById({ userId, accountId, paymentPointerId - ) - + }) const { privateKey, publicKey } = generateKeyPairSync('ed25519') const publicKeyPEM = publicKey .export({ type: 'spki', format: 'pem' }) @@ -254,17 +339,18 @@ export class PaymentPointerService implements IPaymentPointerService { accountId: string, paymentPointerId: string ): Promise { - const paymentPointer = await this.getById( + const paymentPointer = await this.getById({ userId, accountId, paymentPointerId - ) + }) if (!paymentPointer.keyIds) { return } const trx = await PaymentPointer.startTransaction() + try { await Promise.all([ paymentPointer.$query(trx).patch({ keyIds: null }), @@ -280,11 +366,11 @@ export class PaymentPointerService implements IPaymentPointerService { async update(args: UpdatePaymentPointerArgs): Promise { const { userId, accountId, paymentPointerId, publicName } = args - const paymentPointer = await this.getById( + const paymentPointer = await this.getById({ userId, accountId, paymentPointerId - ) + }) const trx = await PaymentPointer.startTransaction() @@ -319,6 +405,13 @@ export class PaymentPointerService implements IPaymentPointerService { } async findByIdWithoutValidation(id: string) { + //* Cache only contains PaymentPointers with isWM = true + const cacheHit = await this.deps.cache.get(id) + if (cacheHit) { + //* TODO: reset ttl + return cacheHit + } + const paymentPointer = await PaymentPointer.query() .findById(id) .where('active', true) @@ -327,6 +420,12 @@ export class PaymentPointerService implements IPaymentPointerService { throw new NotFound() } + if (paymentPointer.isWM) { + await this.deps.cache.set(paymentPointer.id, paymentPointer, { + expiry: 60 + }) + } + return paymentPointer } } diff --git a/packages/wallet/backend/src/paymentPointer/validation.ts b/packages/wallet/backend/src/paymentPointer/validation.ts index 953a5c109..bc70faf07 100644 --- a/packages/wallet/backend/src/paymentPointer/validation.ts +++ b/packages/wallet/backend/src/paymentPointer/validation.ts @@ -44,7 +44,8 @@ export const paymentPointerSchema = z.object({ publicName: z .string() .trim() - .min(3, { message: 'Public name must be at least 3 characters long' }) + .min(3, { message: 'Public name must be at least 3 characters long' }), + isWM: z.boolean() }) }) diff --git a/packages/wallet/backend/src/rafiki/rafiki-client.ts b/packages/wallet/backend/src/rafiki/rafiki-client.ts index bc41f9379..a97fad656 100644 --- a/packages/wallet/backend/src/rafiki/rafiki-client.ts +++ b/packages/wallet/backend/src/rafiki/rafiki-client.ts @@ -373,10 +373,15 @@ export class RafikiClient implements IRafikiClient { return getQuote.quote } - public async getRafikiAsset(assetCode: string) { - const assetInRafiki = (await this.listAssets({ first: 100 })).find( - (asset) => asset.code === assetCode - ) + public async getRafikiAsset(assetCode: string, assetScale?: number) { + const assets = await this.listAssets({ first: 100 }) + + const assetInRafiki = assets.find((asset) => { + if (!assetScale) { + return asset.code === assetCode + } + return asset.code === assetCode && asset.scale === assetScale + }) return assetInRafiki } diff --git a/packages/wallet/backend/src/rafiki/service.ts b/packages/wallet/backend/src/rafiki/service.ts index 15ad72108..640d4b3f2 100644 --- a/packages/wallet/backend/src/rafiki/service.ts +++ b/packages/wallet/backend/src/rafiki/service.ts @@ -4,10 +4,14 @@ import { PaymentPointer } from '@/paymentPointer/model' import { RapydClient } from '@/rapyd/rapyd-client' import { TransactionService } from '@/transaction/service' import { Logger } from 'winston' -import { RatesService } from '../rates/service' +import { RatesService } from '@/rates/service' import { RafikiClient } from './rafiki-client' import { UserService } from '@/user/service' import { SocketService } from '@/socket/service' +import { PaymentPointerService } from '@/paymentPointer/service' +import { WMTransactionService } from '@/webMonetization/transaction/service' +import { Account } from '@/account/model' +import { WMTransaction } from '@/webMonetization/transaction/model' export enum EventType { IncomingPaymentCreated = 'incoming_payment.created', @@ -79,6 +83,8 @@ interface RafikiServiceDependencies { logger: Logger rafikiClient: RafikiClient transactionService: TransactionService + paymentPointerService: PaymentPointerService + wmTransactionService: WMTransactionService } export class RafikiService implements IRafikiService { @@ -108,9 +114,7 @@ export class RafikiService implements IRafikiService { await this.handleIncomingPaymentCompleted(wh) break case EventType.IncomingPaymentCreated: - await this.deps.transactionService.createIncomingTransaction( - wh.data.incomingPayment - ) + await this.handleIncomingPaymentCreated(wh) break case EventType.IncomingPaymentExpired: await this.handleIncomingPaymentExpired(wh) @@ -123,33 +127,14 @@ export class RafikiService implements IRafikiService { } } - private async getRapydWalletIdFromWebHook(wh: WebHook): Promise { - let ppId = '' - if ( - [ - EventType.IncomingPaymentCompleted, - EventType.IncomingPaymentExpired - ].includes(wh.type) - ) { - ppId = wh.data.incomingPayment.paymentPointerId as string - } - if ( - [ - EventType.OutgoingPaymentCreated, - EventType.OutgoingPaymentCompleted - ].includes(wh.type) - ) { - ppId = wh.data.payment.paymentPointerId as string - } - - const pp = await PaymentPointer.query() - .findById(ppId) - .withGraphFetched('account.user') - if (!pp) { - throw new BadRequest('Invalid payment pointer') - } + private async getRapydWalletId( + paymentPointer: PaymentPointer + ): Promise { + const account = await Account.query() + .findById(paymentPointer.accountId) + .withGraphFetched('user') - const user = pp.account.user + const user = account?.user if (!user || !user.rapydWalletId) { throw new BadRequest('No user associated to the provided payment pointer') } @@ -197,10 +182,22 @@ export class RafikiService implements IRafikiService { } private async handleIncomingPaymentCompleted(wh: WebHook) { - const receiverWalletId = await this.getRapydWalletIdFromWebHook(wh) - + const paymentPointer = await this.getPaymentPointer(wh) const amount = this.getAmountFromWebHook(wh) + if (paymentPointer.isWM) { + await this.deps.rafikiClient.withdrawLiqudity(wh.id) + + await this.deps.wmTransactionService.updateTransaction( + { paymentId: wh.data.incomingPayment.id }, + { status: 'COMPLETED', value: amount.value } + ) + + return + } + + const receiverWalletId = await this.getRapydWalletId(paymentPointer) + if (!this.validateAmount(amount, wh.type)) { //* Only in case the expired incoming payment has no money received will it be set as expired. //* Otherwise, it will complete, even if not all the money is yet sent. @@ -253,16 +250,46 @@ export class RafikiService implements IRafikiService { ) } + private async handleIncomingPaymentCreated(wh: WebHook) { + const paymentPointer = await this.getPaymentPointer(wh) + + if (paymentPointer.isWM) { + await this.deps.wmTransactionService.createIncomingTransaction( + wh.data.incomingPayment + ) + + return + } + + await this.deps.transactionService.createIncomingTransaction( + wh.data.incomingPayment, + paymentPointer + ) + } + private async handleOutgoingPaymentCreated(wh: WebHook) { - const rapydWalletId = await this.getRapydWalletIdFromWebHook(wh) + const paymentPointer = await this.getPaymentPointer(wh) const amount = this.getAmountFromWebHook(wh) + if (paymentPointer.isWM) { + await this.deps.rafikiClient.depositLiquidity(wh.id) + + await this.deps.wmTransactionService.createOutgoingTransaction( + wh.data.payment + ) + + return + } + + const rapydWalletId = await this.getRapydWalletId(paymentPointer) + if (!this.validateAmount(amount, wh.type)) { return } await this.deps.transactionService.createOutgoingTransaction( - wh.data.payment + wh.data.payment, + paymentPointer ) const holdResult = await this.deps.rapydClient.holdLiquidity({ amount: this.amountToNumber(amount), @@ -287,9 +314,22 @@ export class RafikiService implements IRafikiService { } private async handleOutgoingPaymentCompleted(wh: WebHook) { - const source_ewallet = await this.getRapydWalletIdFromWebHook(wh) + const paymentPointer = await this.getPaymentPointer(wh) const debitAmount = this.getAmountFromWebHook(wh) + if (paymentPointer.isWM) { + await this.deps.rafikiClient.withdrawLiqudity(wh.id) + + await this.deps.wmTransactionService.updateTransaction( + { paymentId: wh.data.payment.id }, + { status: 'COMPLETED', value: debitAmount.value } + ) + + return + } + + const source_ewallet = await this.getRapydWalletId(paymentPointer) + if (!this.validateAmount(debitAmount, wh.type)) { return } @@ -331,14 +371,33 @@ export class RafikiService implements IRafikiService { } private async handleOutgoingPaymentFailed(wh: WebHook) { - const source_ewallet = await this.getRapydWalletIdFromWebHook(wh) - + const paymentPointer = await this.getPaymentPointer(wh) const debitAmount = this.getAmountFromWebHook(wh) if (!this.validateAmount(debitAmount, wh.type)) { return } + const sentAmount = this.parseAmount( + wh.data.payment.sentAmount as AmountJSON + ) + + if (paymentPointer.isWM) { + await this.deps.rafikiClient.withdrawLiqudity(wh.id) + + const update: Partial = sentAmount.value + ? { status: 'COMPLETED', value: sentAmount.value } + : { status: 'FAILED', value: 0n } + await this.deps.wmTransactionService.updateTransaction( + { paymentId: wh.data.payment.id }, + update + ) + + return + } + + const source_ewallet = await this.getRapydWalletId(paymentPointer) + const releaseResult = await this.deps.rapydClient.releaseLiquidity({ amount: this.amountToNumber(debitAmount), currency: debitAmount.assetCode, @@ -360,9 +419,6 @@ export class RafikiService implements IRafikiService { { status: 'FAILED', value: 0n } ) - const sentAmount = this.parseAmount( - wh.data.payment.sentAmount as AmountJSON - ) if (!sentAmount.value) { return } @@ -407,4 +463,11 @@ export class RafikiService implements IRafikiService { return false } + + async getPaymentPointer(wh: WebHook) { + const ppId: string = + wh.data.incomingPayment?.paymentPointerId || + wh.data.payment?.paymentPointerId + return await this.deps.paymentPointerService.findByIdWithoutValidation(ppId) + } } diff --git a/packages/wallet/backend/src/rapyd/controller.ts b/packages/wallet/backend/src/rapyd/controller.ts index 37abc54bf..0c0ac720d 100644 --- a/packages/wallet/backend/src/rapyd/controller.ts +++ b/packages/wallet/backend/src/rapyd/controller.ts @@ -1,14 +1,14 @@ import { AccountService } from '@/account/service' import { PaymentPointerService } from '@/paymentPointer/service' import { validate } from '@/shared/validate' +import { SocketService } from '@/socket/service' import { User } from '@/user/model' +import { UserService } from '@/user/service' import { getRandomValues } from 'crypto' -import { SocketService } from '@/socket/service' import { NextFunction, Request } from 'express' import { Logger } from 'winston' import { Options, RapydService } from './service' import { kycSchema, profileSchema, walletSchema } from './validation' -import { UserService } from '@/user/service' interface IRapydController { getCountryNames: ControllerFunction @@ -96,12 +96,13 @@ export class RapydController implements IRapydController { getRandomValues(typedArray) const paymentPointerName = typedArray[0].toString(16) - await this.deps.paymentPointerService.create( - id, - defaultAccount.id, + await this.deps.paymentPointerService.create({ + accountId: defaultAccount.id, paymentPointerName, - 'Default Payment Pointer' - ) + publicName: 'Default Payment Pointer', + userId: id, + isWM: false + }) } res.status(200).json({ diff --git a/packages/wallet/backend/src/transaction/model.ts b/packages/wallet/backend/src/transaction/model.ts index 54d6e7c27..6fd197d1a 100644 --- a/packages/wallet/backend/src/transaction/model.ts +++ b/packages/wallet/backend/src/transaction/model.ts @@ -3,6 +3,7 @@ import { BaseModel } from '@/shared/model' import { PaymentPointer } from '@/paymentPointer/model' import { Account } from '@/account/model' +export type TransactionType = 'INCOMING' | 'OUTGOING' export type TransactionExtended = Transaction & { paymentPointerUrl: PaymentPointer['url'] accountName: Account['name'] @@ -11,7 +12,7 @@ export type TransactionExtended = Transaction & { export class TransactionBaseModel extends BaseModel { paymentId!: string value!: bigint | null - type!: 'INCOMING' | 'OUTGOING' + type!: TransactionType status!: 'PENDING' | 'COMPLETED' | 'EXPIRED' | 'FAILED' expiresAt!: Date | null } diff --git a/packages/wallet/backend/src/transaction/service.ts b/packages/wallet/backend/src/transaction/service.ts index 6581284f6..237f6ef3b 100644 --- a/packages/wallet/backend/src/transaction/service.ts +++ b/packages/wallet/backend/src/transaction/service.ts @@ -10,6 +10,7 @@ import { OutgoingPayment } from '@/rafiki/backend/generated/graphql' import { PaymentPointerService } from '@/paymentPointer/service' +import { PaymentPointer } from '@/paymentPointer/model' type ListAllTransactionsInput = { userId: string @@ -128,12 +129,10 @@ export class TransactionService implements ITransactionService { }) } - async createIncomingTransaction(params: IncomingPayment) { - const paymentPointer = - await this.deps.paymentPointerService.findByIdWithoutValidation( - params.paymentPointerId - ) - + async createIncomingTransaction( + params: IncomingPayment, + paymentPointer: PaymentPointer + ) { const amount = params.incomingAmount || params.receivedAmount return Transaction.query().insert({ paymentPointerId: params.paymentPointerId, @@ -148,12 +147,10 @@ export class TransactionService implements ITransactionService { }) } - async createOutgoingTransaction(params: OutgoingPayment) { - const paymentPointer = - await this.deps.paymentPointerService.findByIdWithoutValidation( - params.paymentPointerId - ) - + async createOutgoingTransaction( + params: OutgoingPayment, + paymentPointer: PaymentPointer + ) { const amount = params.debitAmount return Transaction.query().insert({ paymentPointerId: params.paymentPointerId, diff --git a/packages/wallet/backend/src/webMonetization/paymentPointer/model.ts b/packages/wallet/backend/src/webMonetization/paymentPointer/model.ts deleted file mode 100644 index 1b605fbb9..000000000 --- a/packages/wallet/backend/src/webMonetization/paymentPointer/model.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Account } from '@/account/model' -import { PaymentPointerBaseModel } from '@/paymentPointer/model' -import { Model } from 'objection' -import { WMTransaction } from '../transaction/model' - -export class WMPaymentPointer extends PaymentPointerBaseModel { - static tableName = 'wmPaymentPointers' - - public balance!: bigint - - static relationMappings = () => ({ - account: { - relation: Model.BelongsToOneRelation, - modelClass: Account, - join: { - from: 'wmPaymentPointers.accountId', - to: 'accounts.id' - } - }, - transactions: { - relation: Model.HasManyRelation, - modelClass: WMTransaction, - join: { - from: 'wmPaymentPointers.id', - to: 'wmTransactions.wmPaymentPointerId' - } - } - }) -} diff --git a/packages/wallet/backend/src/webMonetization/transaction/model.ts b/packages/wallet/backend/src/webMonetization/transaction/model.ts index e3f7d590f..4197b5323 100644 --- a/packages/wallet/backend/src/webMonetization/transaction/model.ts +++ b/packages/wallet/backend/src/webMonetization/transaction/model.ts @@ -1,20 +1,20 @@ import { TransactionBaseModel } from '@/transaction/model' -import { WMPaymentPointer } from '../paymentPointer/model' import { Model } from 'objection' +import { PaymentPointer } from '@/paymentPointer/model' export class WMTransaction extends TransactionBaseModel { static tableName = 'wmTransactions' - wmPaymentPointerId!: string - wmPaymentPointer!: WMPaymentPointer + paymentPointerId!: string + paymentPointer!: PaymentPointer static relationMappings = () => ({ wmPaymentPointer: { relation: Model.BelongsToOneRelation, - modelClass: WMPaymentPointer, + modelClass: PaymentPointer, join: { - from: 'wmTransactions.wmPaymentPointerId', - to: 'wmPaymentPointers.id' + from: 'wmTransactions.paymentPointerId', + to: 'paymentPointers.id' } } }) diff --git a/packages/wallet/backend/src/webMonetization/transaction/service.ts b/packages/wallet/backend/src/webMonetization/transaction/service.ts new file mode 100644 index 000000000..f72a042c6 --- /dev/null +++ b/packages/wallet/backend/src/webMonetization/transaction/service.ts @@ -0,0 +1,76 @@ +import { WMTransaction } from './model' +import { PartialModelObject } from 'objection' +import { Logger } from 'winston' +import { + IncomingPayment, + OutgoingPayment +} from '@/rafiki/backend/generated/graphql' +import { PaymentPointerService } from '@/paymentPointer/service' +import { TransactionType } from '@/transaction/model' + +export interface IWMTransactionService {} + +interface WMTransactionServiceDependencies { + paymentPointerService: PaymentPointerService + logger: Logger +} + +export class WMTransactionService implements IWMTransactionService { + constructor(private deps: WMTransactionServiceDependencies) {} + + async updateTransaction( + where: PartialModelObject, + update: PartialModelObject + ): Promise { + try { + this.deps.logger.info( + `Updating transaction with: ${JSON.stringify(update)}` + ) + await WMTransaction.query().where(where).update(update) + } catch (e) { + this.deps.logger.error(`Update transaction error:`, e) + } + } + async createIncomingTransaction(params: IncomingPayment) { + const amount = params.incomingAmount || params.receivedAmount + return WMTransaction.query().insert({ + paymentPointerId: params.paymentPointerId, + paymentId: params.id, + expiresAt: params.expiresAt ? new Date(params.expiresAt) : undefined, + value: amount.value, + type: 'INCOMING', + status: 'PENDING' + }) + } + + async createOutgoingTransaction(params: OutgoingPayment) { + const amount = params.debitAmount + return WMTransaction.query().insert({ + paymentPointerId: params.paymentPointerId, + paymentId: params.id, + value: amount.value, + type: 'OUTGOING', + status: 'PENDING' + }) + } + + async deleteByTransactionIds(ids: string[]) { + return WMTransaction.query().del().whereIn('id', ids) + } + + async deleteByPaymentPointer( + paymentPointerId: string, + status: TransactionType + ) { + return WMTransaction.query().del().where({ paymentPointerId, status }) + } + + async sumByPaymentPointerId( + paymentPointerId: string, + status: TransactionType + ) { + return WMTransaction.query() + .sum('value') + .where({ paymentPointerId, status }) + } +} diff --git a/packages/wallet/backend/tests/account/controller.test.ts b/packages/wallet/backend/tests/account/controller.test.ts index 8497f71e9..006af2f7e 100644 --- a/packages/wallet/backend/tests/account/controller.test.ts +++ b/packages/wallet/backend/tests/account/controller.test.ts @@ -133,8 +133,8 @@ describe('Asset Controller', (): void => { }) afterAll(async (): Promise => { - appContainer.stop() - knex.destroy() + await appContainer.stop() + await knex.destroy() }) afterEach(async (): Promise => { diff --git a/packages/wallet/backend/tests/app.test.ts b/packages/wallet/backend/tests/app.test.ts index 7378c776f..cba774f1f 100644 --- a/packages/wallet/backend/tests/app.test.ts +++ b/packages/wallet/backend/tests/app.test.ts @@ -18,8 +18,8 @@ describe('Application', (): void => { }) afterAll(async (): Promise => { - appContainer.stop() - knex.destroy() + await appContainer.stop() + await knex.destroy() }) it('should return status 404 if the route does not exist', async (): Promise => { diff --git a/packages/wallet/backend/tests/asset/controller.test.ts b/packages/wallet/backend/tests/asset/controller.test.ts index 706d72141..eda4b941a 100644 --- a/packages/wallet/backend/tests/asset/controller.test.ts +++ b/packages/wallet/backend/tests/asset/controller.test.ts @@ -59,8 +59,8 @@ describe('Asset Controller', (): void => { }) afterAll(async (): Promise => { - appContainer.stop() - knex.destroy() + await appContainer.stop() + await knex.destroy() }) afterEach(async (): Promise => { diff --git a/packages/wallet/backend/tests/auth/controller.test.ts b/packages/wallet/backend/tests/auth/controller.test.ts index 04cf7a954..9a010af7a 100644 --- a/packages/wallet/backend/tests/auth/controller.test.ts +++ b/packages/wallet/backend/tests/auth/controller.test.ts @@ -48,8 +48,8 @@ describe('Authentication Controller', (): void => { }) afterAll(async (): Promise => { - appContainer.stop() - knex.destroy() + await appContainer.stop() + await knex.destroy() }) afterEach(async (): Promise => { diff --git a/packages/wallet/backend/tests/auth/service.test.ts b/packages/wallet/backend/tests/auth/service.test.ts index e33f4b97b..0b2acdfdb 100644 --- a/packages/wallet/backend/tests/auth/service.test.ts +++ b/packages/wallet/backend/tests/auth/service.test.ts @@ -23,8 +23,8 @@ describe('Authentication Service', (): void => { }) afterAll(async (): Promise => { - appContainer.stop() - knex.destroy() + await appContainer.stop() + await knex.destroy() }) afterEach(async (): Promise => { diff --git a/packages/wallet/backend/tests/quote/controller.test.ts b/packages/wallet/backend/tests/quote/controller.test.ts index 3c5efdb23..b79ddabf4 100644 --- a/packages/wallet/backend/tests/quote/controller.test.ts +++ b/packages/wallet/backend/tests/quote/controller.test.ts @@ -95,8 +95,8 @@ describe('Quote Controller', () => { }) afterAll(async (): Promise => { - appContainer.stop() - knex.destroy() + await appContainer.stop() + await knex.destroy() }) afterEach(async (): Promise => { diff --git a/packages/wallet/backend/tests/quote/service.test.ts b/packages/wallet/backend/tests/quote/service.test.ts index 33715d6a1..e2843c3f2 100644 --- a/packages/wallet/backend/tests/quote/service.test.ts +++ b/packages/wallet/backend/tests/quote/service.test.ts @@ -132,8 +132,8 @@ describe('Quote Service', () => { }) afterAll(async (): Promise => { - appContainer.stop() - knex.destroy() + await appContainer.stop() + await knex.destroy() }) afterEach(async (): Promise => { diff --git a/packages/wallet/backend/tests/rapyd/controller.test.ts b/packages/wallet/backend/tests/rapyd/controller.test.ts index 09552d920..6f4d3ff15 100644 --- a/packages/wallet/backend/tests/rapyd/controller.test.ts +++ b/packages/wallet/backend/tests/rapyd/controller.test.ts @@ -114,8 +114,8 @@ describe('Rapyd Controller', () => { }) afterAll(async (): Promise => { - appContainer.stop() - knex.destroy() + await appContainer.stop() + await knex.destroy() }) afterEach(async (): Promise => { diff --git a/packages/wallet/backend/tests/rapyd/service.test.ts b/packages/wallet/backend/tests/rapyd/service.test.ts index c441764d2..e3c07adcc 100644 --- a/packages/wallet/backend/tests/rapyd/service.test.ts +++ b/packages/wallet/backend/tests/rapyd/service.test.ts @@ -52,8 +52,8 @@ describe('Rapyd Service', () => { }) afterAll(async (): Promise => { - appContainer.stop() - knex.destroy() + await appContainer.stop() + await knex.destroy() }) afterEach(async (): Promise => { diff --git a/packages/wallet/backend/tests/socket/service.test.ts b/packages/wallet/backend/tests/socket/service.test.ts index b33793372..f45ef0c6e 100644 --- a/packages/wallet/backend/tests/socket/service.test.ts +++ b/packages/wallet/backend/tests/socket/service.test.ts @@ -87,8 +87,8 @@ describe('Socket Service', () => { }) afterAll(async (): Promise => { - appContainer.stop() - knex.destroy() + await appContainer.stop() + await knex.destroy() }) afterEach(async (): Promise => { diff --git a/packages/wallet/backend/tests/transaction/service.test.ts b/packages/wallet/backend/tests/transaction/service.test.ts index 94300c8c1..e7651562d 100644 --- a/packages/wallet/backend/tests/transaction/service.test.ts +++ b/packages/wallet/backend/tests/transaction/service.test.ts @@ -68,8 +68,8 @@ describe('Transaction Controller', (): void => { }) afterAll(async (): Promise => { - appContainer.stop() - knex.destroy() + await appContainer.stop() + await knex.destroy() }) afterEach(async (): Promise => { diff --git a/packages/wallet/backend/tests/user/controller.test.ts b/packages/wallet/backend/tests/user/controller.test.ts index 4e5c78857..5c2456bdb 100644 --- a/packages/wallet/backend/tests/user/controller.test.ts +++ b/packages/wallet/backend/tests/user/controller.test.ts @@ -60,8 +60,8 @@ describe('User Controller', (): void => { }) afterAll(async (): Promise => { - appContainer.stop() - knex.destroy() + await appContainer.stop() + await knex.destroy() }) afterEach(async (): Promise => { diff --git a/packages/wallet/backend/tests/user/service.test.ts b/packages/wallet/backend/tests/user/service.test.ts index 46ed7ff90..3510cf2ba 100644 --- a/packages/wallet/backend/tests/user/service.test.ts +++ b/packages/wallet/backend/tests/user/service.test.ts @@ -33,8 +33,8 @@ describe('User Service', (): void => { }) afterAll(async (): Promise => { - appContainer.stop() - knex.destroy() + await appContainer.stop() + await knex.destroy() }) afterEach(async (): Promise => { diff --git a/packages/wallet/frontend/src/lib/api/paymentPointer.ts b/packages/wallet/frontend/src/lib/api/paymentPointer.ts index 51920ed41..821f6e11d 100644 --- a/packages/wallet/frontend/src/lib/api/paymentPointer.ts +++ b/packages/wallet/frontend/src/lib/api/paymentPointer.ts @@ -40,6 +40,11 @@ export type PaymentPointer = { keyIds: PaymentPointerKey | null } +export type ListPaymentPointersResult = { + wmPaymentPointers: Array + paymentPointers: Array +} + type PaymentPointerKeyDetails = { privateKey: string publicKey: string @@ -61,7 +66,7 @@ type GetPaymentPointerArgs = { accountId: string; paymentPointerId: string } type GetPaymentPointerResult = SuccessResponse type GetPaymentPointerResponse = GetPaymentPointerResult | ErrorResponse -type ListPaymentPointerResult = SuccessResponse +type ListPaymentPointerResult = SuccessResponse type ListPaymentPointerResponse = ListPaymentPointerResult | ErrorResponse type CreatePaymentPointerArgs = z.infer diff --git a/packages/wallet/frontend/src/pages/account/[accountId].tsx b/packages/wallet/frontend/src/pages/account/[accountId].tsx index cec600c67..b3cce7bf6 100644 --- a/packages/wallet/frontend/src/pages/account/[accountId].tsx +++ b/packages/wallet/frontend/src/pages/account/[accountId].tsx @@ -202,10 +202,12 @@ export const getServerSideProps: GetServerSideProps<{ } } - const paymentPointers = paymentPointersResponse.data.map((pp) => ({ - ...pp, - url: pp.url.replace('https://', '$') - })) + const paymentPointers = paymentPointersResponse.data.paymentPointers + .concat(paymentPointersResponse.data.wmPaymentPointers) + .map((pp) => ({ + ...pp, + url: pp.url.replace('https://', '$') + })) return { props: { diff --git a/packages/wallet/frontend/src/pages/transactions.tsx b/packages/wallet/frontend/src/pages/transactions.tsx index 38d2b6cb6..51294470c 100644 --- a/packages/wallet/frontend/src/pages/transactions.tsx +++ b/packages/wallet/frontend/src/pages/transactions.tsx @@ -326,13 +326,15 @@ export const getServerSideProps: GetServerSideProps< }) ) - paymentPointersResponse.data.map((pp) => - paymentPointers.push({ - label: pp.url, - value: pp.id, - accountId: pp.accountId - }) - ) + paymentPointersResponse.data.paymentPointers + .concat(paymentPointersResponse.data.wmPaymentPointers) + .map((pp) => + paymentPointers.push({ + label: pp.url, + value: pp.id, + accountId: pp.accountId + }) + ) return { props: { diff --git a/packages/wallet/frontend/src/pages/transfer/request.tsx b/packages/wallet/frontend/src/pages/transfer/request.tsx index cca8357ab..2e7f804f3 100644 --- a/packages/wallet/frontend/src/pages/transfer/request.tsx +++ b/packages/wallet/frontend/src/pages/transfer/request.tsx @@ -86,16 +86,16 @@ const RequestPage: NextPageWithLayout = ({ accounts }) => { return } - const paymentPointers = paymentPointersResponse.data.map( - (paymentPointer) => ({ + const paymentPointers = paymentPointersResponse.data.paymentPointers + .concat(paymentPointersResponse.data.wmPaymentPointers) + .map((paymentPointer) => ({ label: `${paymentPointer.publicName} (${paymentPointer.url.replace( 'https://', '$' )})`, value: paymentPointer.id, url: paymentPointer.url - }) - ) + })) setPaymentPointers(paymentPointers) } diff --git a/packages/wallet/frontend/src/pages/transfer/send.tsx b/packages/wallet/frontend/src/pages/transfer/send.tsx index 4ecd1dc8d..5b68ad5e0 100644 --- a/packages/wallet/frontend/src/pages/transfer/send.tsx +++ b/packages/wallet/frontend/src/pages/transfer/send.tsx @@ -106,15 +106,15 @@ const SendPage: NextPageWithLayout = ({ accounts }) => { return } - const paymentPointers = paymentPointersResponse.data.map( - (paymentPointer) => ({ + const paymentPointers = paymentPointersResponse.data.paymentPointers + .concat(paymentPointersResponse.data.wmPaymentPointers) + .map((paymentPointer) => ({ label: `${paymentPointer.publicName} (${paymentPointer.url.replace( 'https://', '$' )})`, value: paymentPointer.id - }) - ) + })) setPaymentPointers(paymentPointers) if (selectedAccount) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3df5def8..ed9b88c66 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -262,6 +262,9 @@ importers: helmet: specifier: ^7.0.0 version: 7.0.0 + ioredis: + specifier: ^5.3.2 + version: 5.3.2 iron-session: specifier: ^6.3.1 version: 6.3.1(express@4.18.2) @@ -2173,6 +2176,10 @@ packages: - supports-color dev: false + /@ioredis/commands@1.2.0: + resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} + dev: false + /@istanbuljs/load-nyc-config@1.1.0: resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} engines: {node: '>=8'} @@ -4754,6 +4761,11 @@ packages: engines: {node: '>=6'} dev: false + /cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + dev: false + /co-body@6.1.0: resolution: {integrity: sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==} dependencies: @@ -5159,6 +5171,11 @@ packages: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} dev: false + /denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + dev: false + /depd@1.1.2: resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} engines: {node: '>= 0.6'} @@ -6465,6 +6482,23 @@ packages: dependencies: loose-envify: 1.4.0 + /ioredis@5.3.2: + resolution: {integrity: sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==} + engines: {node: '>=12.22.0'} + dependencies: + '@ioredis/commands': 1.2.0 + cluster-key-slot: 1.1.2 + debug: 4.3.4 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + dev: false + /ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -7554,7 +7588,6 @@ packages: /lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} - dev: true /lodash.difference@4.5.0: resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} @@ -7564,6 +7597,10 @@ packages: resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} dev: true + /lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + dev: false + /lodash.isplainobject@4.0.6: resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} dev: true @@ -9025,6 +9062,18 @@ packages: resolve: 1.22.6 dev: false + /redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + dev: false + + /redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + dependencies: + redis-errors: 1.2.0 + dev: false + /reflect.getprototypeof@1.0.4: resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==} engines: {node: '>= 0.4'} @@ -9545,6 +9594,10 @@ packages: escape-string-regexp: 2.0.0 dev: true + /standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + dev: false + /statuses@1.5.0: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} engines: {node: '>= 0.6'}