Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
3f8304f
tests(tenants): when tenant is created, default settings should be set
golobitch Feb 1, 2025
f045e16
feat(graphql): tenant settings
golobitch Feb 10, 2025
5dbad86
fix(tenantSettings): address PR comments
golobitch Feb 17, 2025
c81e36e
fix(tenantSettings): address PR comments
golobitch Feb 17, 2025
cfb41c5
feat(backend): tenanted webhooks
njlie Feb 21, 2025
912bc59
fix: rebase
njlie Feb 21, 2025
7d6f53b
chore: fix some rebasing issues
njlie Feb 24, 2025
d212132
feat: add tenant id to tests; temporary operator tenant id in peer we…
njlie Feb 26, 2025
8907eb5
fix: build errors
njlie Feb 26, 2025
3eacf38
feat: remove temporary tenantId code from before tenanted peers
njlie Apr 1, 2025
7a649bb
fix: build errors
njlie Apr 1, 2025
bfb9619
tests(tenants): when tenant is created, default settings should be set
golobitch Feb 1, 2025
6a16fa2
fix(tenantSettings): address PR comments
golobitch Feb 17, 2025
b642b1d
feat(backend): tenanted webhooks
njlie Feb 21, 2025
980a881
chore: fix some rebasing issues
njlie Feb 24, 2025
505ca58
feat(backend): also publish webhooks to operators if primary recipien…
njlie Apr 2, 2025
1b5c5ba
fix: rebase issues
njlie Apr 8, 2025
4bff86c
fix: tests
njlie Apr 8, 2025
ad1256f
feat: add webhook model
njlie Apr 16, 2025
ee4bbcc
refactor: move webhook & webhookevent models
njlie Apr 16, 2025
f63fb51
feat: include tenant id in webhook gql response
njlie Apr 17, 2025
659895b
fix: generated files
njlie Apr 17, 2025
3324c3b
fix: rebase errors
njlie Apr 24, 2025
284c049
feat: review comments
njlie Apr 29, 2025
406d636
feat: remove getWebhook, rename processWebhookEvent to processWebhook
njlie Apr 30, 2025
a09ef79
fix: remove operatorSettings from sendWebhook
njlie May 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions localenv/mock-account-servicing-entity/generated/graphql.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema
.createTable('webhooks', function (table) {
table.uuid('id').notNullable().primary()
table
.uuid('eventId')
.notNullable()
.references('webhookEvents.id')
.onDelete('CASCADE')
.index()
table
.uuid('recipientTenantId')
.notNullable()
.references('tenants.id')
.onDelete('CASCADE')
.index()

table.integer('attempts').notNullable().defaultTo(0)
table.integer('statusCode').nullable()

table.timestamp('processAt').nullable().defaultTo(knex.fn.now())

table.timestamp('createdAt').defaultTo(knex.fn.now())
table.timestamp('updatedAt').defaultTo(knex.fn.now())

table.index('processAt')
})
.then(() => {
return knex.raw(
`INSERT INTO "webhooks" (id, "eventId", "recipientTenantId", attempts, "statusCode", "processAt") select gen_random_uuid(), id as "eventId", "tenantId" as "recipientTenantId", attempts, "statusCode", "processAt" from "webhookEvents"`
)
})
.then(() => {
return knex.schema.alterTable('webhookEvents', (table) => {
table.dropColumn('attempts')
table.dropColumn('statusCode')
table.dropColumn('processAt')
})
})
}

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema
.alterTable('webhookEvents', function (table) {
table.integer('attempts').notNullable().defaultTo(0)
table.integer('statusCode').nullable()
table.timestamp('processAt').nullable().defaultTo(knex.fn.now())
table.index('processAt')
})
.then(() => {
return knex.raw(
`UPDATE "webhookEvents" SET "attempts" = (SELECT "attempts" from "webhooks" where "recipientTenantId" = "webhookEvents"."tenantId" AND "eventId" = "webhookEvents"."id"), "statusCode" = (SELECT "statusCode" from "webhooks" where "recipientTenantId" = "webhookEvents"."tenantId" AND "eventId" = "webhookEvents"."id"), "processAt" = (SELECT "processAt" from "webhooks" where "recipientTenantId" = "webhookEvents"."tenantId" AND "eventId" = "webhookEvents"."id")`
)
})
.then(() => {
return knex.schema.dropTableIfExists('webhooks')
})
}
1 change: 1 addition & 0 deletions packages/backend/src/accounting/psql/balance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe('Balances', (): void => {
deps = initIocContainer({ ...Config, useTigerBeetle: false })
appContainer = await createTestApp(deps)
serviceDeps = {
config: await deps.use('config'),
logger: await deps.use('logger'),
knex: await deps.use('knex'),
telemetry: await deps.use('telemetry')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ describe('Ledger Account', (): void => {
deps = initIocContainer({ ...Config, useTigerBeetle: false })
appContainer = await createTestApp(deps)
serviceDeps = {
config: await deps.use('config'),
logger: await deps.use('logger'),
knex: await deps.use('knex'),
telemetry: await deps.use('telemetry')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ describe('Ledger Transfer', (): void => {
deps = initIocContainer({ ...Config, useTigerBeetle: false })
appContainer = await createTestApp(deps)
serviceDeps = {
config: await deps.use('config'),
logger: await deps.use('logger'),
knex: await deps.use('knex'),
telemetry: await deps.use('telemetry')
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/accounting/psql/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ import {
} from './ledger-transfer'
import { LedgerTransfer, LedgerTransferType } from './ledger-transfer/model'
import { TelemetryService } from '../../telemetry/service'
import { IAppConfig } from '../../config/app'

export interface ServiceDependencies extends BaseService {
knex: TransactionOrKnex
config: IAppConfig
telemetry: TelemetryService
withdrawalThrottleDelay?: number
}
Expand Down
28 changes: 20 additions & 8 deletions packages/backend/src/accounting/service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { TransactionOrKnex } from 'objection'
import { BaseService } from '../shared/baseService'
import { TransferError, isTransferError } from './errors'
import { IAppConfig } from '../config/app'

export enum LiquidityAccountType {
ASSET = 'ASSET',
Expand All @@ -27,14 +28,20 @@ export interface LiquidityAccountAsset {
code?: string
scale?: number
ledger: number
onDebit?: (options: OnDebitOptions) => Promise<LiquidityAccount>
onDebit?: (
options: OnDebitOptions,
config: IAppConfig
) => Promise<LiquidityAccount>
}

export interface LiquidityAccount {
id: string
asset: LiquidityAccountAsset
onCredit?: (options: OnCreditOptions) => Promise<LiquidityAccount>
onDebit?: (options: OnDebitOptions) => Promise<LiquidityAccount>
onDebit?: (
options: OnDebitOptions,
config: IAppConfig
) => Promise<LiquidityAccount>
}

export interface OnCreditOptions {
Expand Down Expand Up @@ -133,6 +140,7 @@ export interface TransferToCreate {
}

export interface BaseAccountingServiceDependencies extends BaseService {
config: IAppConfig
withdrawalThrottleDelay?: number
}

Expand Down Expand Up @@ -195,15 +203,19 @@ export async function createAccountToAccountTransfer(
}

const onDebit = async (
account: LiquidityAccount | LiquidityAccount['asset']
account: LiquidityAccount | LiquidityAccount['asset'],
config: IAppConfig
) => {
if (account.onDebit) {
const balance = await getAccountBalance(account.id)
if (balance === undefined) throw new Error('undefined account balance')

await account.onDebit({
balance
})
await account.onDebit(
{
balance
},
config
)
}
}

Expand All @@ -213,8 +225,8 @@ export async function createAccountToAccountTransfer(
if (error) return error

await Promise.all([
onDebit(sourceAccount),
onDebit(destinationAccount.asset)
onDebit(sourceAccount, deps.config),
onDebit(destinationAccount.asset, deps.config)
])

if (destinationAccount.onCredit) {
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/accounting/tigerbeetle/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
} from './transfers'
import { toTigerBeetleId } from './utils'
import { TelemetryService } from '../../telemetry/service'
import { IAppConfig } from '../../config/app'

export enum TigerBeetleAccountCode {
LIQUIDITY_WEB_MONETIZATION = 1,
Expand Down Expand Up @@ -68,6 +69,7 @@ export const convertToTigerBeetleTransferCode: {
}

export interface ServiceDependencies extends BaseService {
config: IAppConfig
tigerBeetle: Client
telemetry: TelemetryService
withdrawalThrottleDelay?: number
Expand Down
81 changes: 73 additions & 8 deletions packages/backend/src/asset/model.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Knex } from 'knex'

import { AssetService } from './service'
import { Config } from '../config/app'
import { Config, IAppConfig } from '../config/app'
import { createTestApp, TestContainer } from '../tests/app'
import { IocContract } from '@adonisjs/fold'
import { initIocContainer } from '../'
Expand All @@ -10,6 +10,7 @@ import { randomAsset } from '../tests/asset'
import { truncateTables } from '../tests/tableManager'
import { Asset, AssetEvent, AssetEventError, AssetEventType } from './model'
import { isAssetError } from './errors'
import { createTenant } from '../tests/tenant'

describe('Models', (): void => {
let deps: IocContract<AppServices>
Expand All @@ -35,7 +36,9 @@ describe('Models', (): void => {
describe('Asset Model', (): void => {
describe('onDebit', (): void => {
let asset: Asset
let config: IAppConfig
beforeEach(async (): Promise<void> => {
config = await deps.use('config')
const options = {
...randomAsset(),
tenantId: Config.operatorTenantId,
Expand All @@ -54,13 +57,13 @@ describe('Models', (): void => {
`(
'creates webhook event if balance=$balance <= liquidityThreshold',
async ({ balance }): Promise<void> => {
await asset.onDebit({ balance })
await asset.onDebit({ balance }, config)
const event = (
await AssetEvent.query(knex).where(
'type',
AssetEventType.LiquidityLow
)
await AssetEvent.query(knex)
.where('type', AssetEventType.LiquidityLow)
.withGraphFetched('webhooks')
)[0]
expect(event.webhooks).toHaveLength(1)
expect(event).toMatchObject({
type: AssetEventType.LiquidityLow,
data: {
Expand All @@ -72,16 +75,78 @@ describe('Models', (): void => {
},
liquidityThreshold: asset.liquidityThreshold?.toString(),
balance: balance.toString()
}
},
webhooks: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
eventId: event.id,
recipientTenantId: Config.operatorTenantId,
processAt: expect.any(Date),
attempts: 0
})
])
})
}
)
test('does not create webhook event if balance > liquidityThreshold', async (): Promise<void> => {
await asset.onDebit({ balance: BigInt(110) })
await asset.onDebit({ balance: BigInt(110) }, config)
await expect(
AssetEvent.query(knex).where('type', AssetEventType.LiquidityLow)
).resolves.toEqual([])
})
test('creates corresponding webhook for operator if asset belongs to tenant', async (): Promise<void> => {
const tenant = await createTenant(deps)
const tenantAssetOptions = {
...randomAsset(),
tenantId: tenant.id,
liquidityThreshold: BigInt(100)
}

let tenantAsset: Asset
const assetOrError = await assetService.create(tenantAssetOptions)
if (!isAssetError(assetOrError)) {
tenantAsset = assetOrError
} else {
throw assetOrError
}

await tenantAsset.onDebit({ balance: BigInt(50) }, config)
const event = (
await AssetEvent.query(knex)
.where('type', AssetEventType.LiquidityLow)
.withGraphFetched('webhooks')
)[0]
expect(event.webhooks).toHaveLength(2)
expect(event).toMatchObject({
type: AssetEventType.LiquidityLow,
data: {
id: tenantAsset.id,
asset: {
id: tenantAsset.id,
code: tenantAsset.code,
scale: tenantAsset.scale
},
liquidityThreshold: tenantAsset.liquidityThreshold?.toString(),
balance: '50'
},
webhooks: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
eventId: event.id,
recipientTenantId: Config.operatorTenantId,
processAt: expect.any(Date),
attempts: 0
}),
expect.objectContaining({
id: expect.any(String),
eventId: event.id,
recipientTenantId: tenant.id,
processAt: expect.any(Date),
attempts: 0
})
])
})
})
})
})

Expand Down
17 changes: 13 additions & 4 deletions packages/backend/src/asset/model.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { QueryContext } from 'objection'
import { LiquidityAccount, OnDebitOptions } from '../accounting/service'
import { BaseModel } from '../shared/baseModel'
import { WebhookEvent } from '../webhook/model'
import { WebhookEvent } from '../webhook/event/model'
import { IAppConfig } from '../config/app'

export class Asset extends BaseModel implements LiquidityAccount {
public static get tableName(): string {
Expand Down Expand Up @@ -29,10 +30,17 @@ export class Asset extends BaseModel implements LiquidityAccount {
}
}

public async onDebit({ balance }: OnDebitOptions): Promise<Asset> {
public async onDebit(
{ balance }: OnDebitOptions,
config: IAppConfig
): Promise<Asset> {
if (this.liquidityThreshold !== null) {
if (balance <= this.liquidityThreshold) {
await AssetEvent.query().insert({
const webhooks = [{ recipientTenantId: this.tenantId }]
if (this.tenantId !== config.operatorTenantId) {
webhooks.push({ recipientTenantId: config.operatorTenantId })
}
await AssetEvent.query().insertGraph({
assetId: this.id,
type: AssetEventType.LiquidityLow,
data: {
Expand All @@ -45,7 +53,8 @@ export class Asset extends BaseModel implements LiquidityAccount {
liquidityThreshold: this.liquidityThreshold,
balance
},
tenantId: this.tenantId
tenantId: this.tenantId,
webhooks
})
}
}
Expand Down
Loading
Loading