Skip to content

Commit

Permalink
feat(wallet-backend): introduce web-monetization payment pointers (#896)
Browse files Browse the repository at this point in the history
* fix(wallet-backend): env parsing

* feat(wallet-backend) add caching

* fix(wallet-backend): docker-compose dev variable names

* feat(wallet-backend): introduce web monetized payment pointers module

* feat(wallet-backend): update balance now only does balance
add register key and revoke key to wmPaymentPointerService
move wm module out of paymentPointers directory

* feat(wallet-backend): WM payment pointers:
use revoke and register key in paymentPointerService to also apply changes to WMPaymentPointers
getAccounts for user now also returns wmPaymentPointers
getRafikiAsset can now receive an assetScale filter param along with the assetCode one

* chore(wallet-backend): remove comment

* fix(wallet-backend): wm get shouldn't throw

* chore(wallet-backend): wmPp get method refactor

* move wmPaymentPointerModel into paymentPointerModule

* move wmPaymentPointers to PaymentPointers

* add REDIS_URL to prod env.example

* cache service now has a generic type across the cache

* rename CacheService to Cache

* list now returns 2 arrays of wm or not wm paymentPointers

* update frontend to match list response

* remove leftover wmPaymentPointers relation on getAccounts

* format

* add concat on paymentPointers response to transactions list as well

* lint

* fix frontend error

* fix frontend error

* add redis_url default value in env

* accountId is now optional on Payment pointer  getByID

* update balance no longer needs accoutId

* format & errors

* resolve PR comments

* remove wmPaymentPointers from the account page payment pointers list

* format
  • Loading branch information
beniaminmunteanu authored and raducristianpopa committed Oct 24, 2023
1 parent 75a01f3 commit 4c06043
Show file tree
Hide file tree
Showing 24 changed files with 415 additions and 182 deletions.
5 changes: 3 additions & 2 deletions docker/dev/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,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
Expand Down
1 change: 1 addition & 0 deletions docker/prod/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
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<void> }
*/
exports.down = function (knex) {
return knex.schema.alterTable('paymentPointers', (table) => {
table.dropColumn('isWM')
table.dropColumn('assetCode')
table.dropColumn('assetScale')
table.dropColumn('balance')
})
}
5 changes: 3 additions & 2 deletions packages/wallet/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@
"graphql-request": "^6.1.0",
"hash-wasm": "^4.10.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"
Expand All @@ -40,9 +41,9 @@
"@types/express": "^4.17.20",
"@types/jest": "^29.5.6",
"@types/node": "^18.14.0",
"@types/socket.io": "^3.0.2",
"@types/uuid": "^9.0.6",
"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",
Expand Down
2 changes: 2 additions & 0 deletions packages/wallet/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@ 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'

export interface Bindings {
env: Env
logger: Logger
knex: Knex
redisClient: RedisClient
rapydClient: RapydClient
rafikiClient: RafikiClient
rafikiService: RafikiService
Expand Down
48 changes: 48 additions & 0 deletions packages/wallet/backend/src/cache/redis-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Redis } from 'ioredis'

export type EntryOptions = {
expiry?: number
}

export interface IRedisClient {
set<T>(
key: string,
value: T | string,
options?: EntryOptions
): Promise<string>
get<T>(key: string): Promise<T | null>
delete(key: string): Promise<number>
}

export class RedisClient implements IRedisClient {
constructor(private redis: Redis) {}

async set<T>(
key: string,
value: T | string,
options?: EntryOptions
): Promise<string> {
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<T>(key: string): Promise<T | null> {
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<number> {
return await this.redis.del(key)
}
}
37 changes: 37 additions & 0 deletions packages/wallet/backend/src/cache/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { EntryOptions, IRedisClient } from './redis-client'

export interface ICacheService<T> {
set(key: string, value: T | string): Promise<string>
get(key: string): Promise<T | null>
delete(key: string): Promise<number>
}

export class Cache<T> implements ICacheService<T> {
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<string> {
const namespacedKey = this.namespace + key
return await this.cache.set<T>(namespacedKey, value, options)
}

async get(key: string): Promise<T | null> {
const namespacedKey = this.namespace + key
return await this.cache.get<T>(namespacedKey)
}

async delete(key: string): Promise<number> {
const namespacedKey = this.namespace + key
return await this.cache.delete(namespacedKey)
}
}
9 changes: 5 additions & 4 deletions packages/wallet/backend/src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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<typeof envSchema>
Expand Down
34 changes: 24 additions & 10 deletions packages/wallet/backend/src/createContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ 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'

export const createContainer = (config: Env): Container<Bindings> => {
const container = new Container<Bindings>()
Expand Down Expand Up @@ -179,6 +183,26 @@ export const createContainer = (config: Env): Container<Bindings> => {
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<PaymentPointer>(
await container.resolve('redisClient'),
'WMPaymentPointers'
)
})
)

container.singleton(
'rapydController',
async () =>
Expand All @@ -192,16 +216,6 @@ export const createContainer = (config: Env): Container<Bindings> => {
})
)

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 () =>
Expand Down
28 changes: 16 additions & 12 deletions packages/wallet/backend/src/paymentPointer/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -11,7 +15,7 @@ import {

interface IPaymentPointerController {
create: ControllerFunction<PaymentPointer>
list: ControllerFunction<PaymentPointer[]>
list: ControllerFunction<PaymentPointerList>
getById: ControllerFunction<PaymentPointer>
softDelete: ControllerFunction
}
Expand All @@ -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 })
Expand All @@ -57,7 +62,7 @@ export class PaymentPointerController implements IPaymentPointerController {

list = async (
req: Request,
res: CustomResponse<PaymentPointer[]>,
res: CustomResponse<PaymentPointerList>,
next: NextFunction
) => {
const userId = req.session.user.id
Expand All @@ -68,7 +73,6 @@ export class PaymentPointerController implements IPaymentPointerController {
userId,
accountId
)

res
.status(200)
.json({ success: true, message: 'SUCCESS', data: paymentPointers })
Expand Down Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 4c06043

Please sign in to comment.