morphit/apps/indexer/test/integration/order-handler.test.ts

207 lines
5.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Integration test — order handler fee verification against real
* Postgres. Unit tests mocked the query; this test runs the
* actual SQL and verifies the Sybil-count predicate behaves
* correctly (24h cutoff, live OR recent).
*/
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import orderHandler from '../../src/indexer/handlers/order';
import {
INTEGRATION_ENABLED,
setupWithMigrations,
truncateAll,
type IntegrationFixture
} from './harness';
import { makeCtx } from '../testutils/context';
import type { ChainOperation } from '../../src/blurt/client';
/** Build a matching fee-transfer sibling op for the order. */
function feeTransferOp(
sender: string,
amountBlurt: number,
permlink: string,
recipient = 'morphit-fees'
): ChainOperation {
return [
'transfer',
{
from: sender,
to: recipient,
amount: `${amountBlurt.toFixed(3)} BLURT`,
memo: `morphit-fee:${permlink}`
}
] as const;
}
function orderPayload(permlink: string) {
return {
permlink,
side: 'sell',
asset: 'BTC',
fiat_currency: 'USD',
amount_min: 100,
amount_max: 1000,
price_model: { kind: 'spread', percent: 0 },
payment_methods: ['cash']
};
}
async function insertLiveOrder(
fx: IntegrationFixture,
account: string,
permlink: string,
createdAt: Date
): Promise<void> {
await fx.db.query(
`INSERT INTO orders (
account, permlink, side, asset, fiat_currency,
price_model, payment_methods, status, fee_status,
created_at, updated_at
) VALUES ($1, $2, 'sell', 'BTC', 'USD',
'{}'::jsonb, ARRAY['cash'], 'live', 'verified',
$3, $3)`,
[account, permlink, createdAt]
);
}
describe.skipIf(!INTEGRATION_ENABLED)('order handler fee verification — integration', () => {
let fx: IntegrationFixture;
beforeAll(async () => {
fx = await setupWithMigrations();
});
afterAll(async () => {
if (fx) await fx.teardown();
});
beforeEach(async () => {
await truncateAll(fx);
});
it('first order: nth=1, fee_status=verified at 62.500 BLURT', async () => {
const blockTime = new Date('2026-04-19T12:00:00Z');
const payload = orderPayload('sell-btc-usd-aaa');
const result = await fx.db.withTx(async (client) => {
return orderHandler(
makeCtx({
signer: 'alice',
blockTime,
payload,
siblingOps: [feeTransferOp('alice', 62.5, payload.permlink)]
}),
client
);
});
expect(result).toEqual({ ok: true });
const row = await fx.db.query<{ fee_status: string }>(
`SELECT fee_status FROM orders WHERE permlink = $1`,
[payload.permlink]
);
expect(row.rows[0]!.fee_status).toBe('verified');
});
it("counts only the user's own orders toward Sybil tier", async () => {
// Seed: alice has 3 existing live orders in the last hour,
// bob has 10 live orders. Alice posts her 4th; she should
// be at tier 4 (1.25×), not affected by bob's volume.
const now = new Date('2026-04-19T12:00:00Z');
for (let i = 0; i < 3; i++) {
await insertLiveOrder(fx, 'alice', `alice-${i}`, new Date(now.getTime() - (i + 1) * 60_000));
}
for (let i = 0; i < 10; i++) {
await insertLiveOrder(fx, 'bob', `bob-${i}`, new Date(now.getTime() - (i + 1) * 60_000));
}
const payload = orderPayload('alice-new');
// Tier 4 fee: 62.5 × 1.25 = 78.125 BLURT.
const result = await fx.db.withTx(async (client) => {
return orderHandler(
makeCtx({
signer: 'alice',
blockTime: now,
payload,
siblingOps: [feeTransferOp('alice', 78.125, payload.permlink)]
}),
client
);
});
expect(result).toEqual({ ok: true });
const row = await fx.db.query<{ fee_status: string }>(
`SELECT fee_status FROM orders WHERE permlink = $1`,
[payload.permlink]
);
expect(row.rows[0]!.fee_status).toBe('verified');
});
it('excludes orders older than 24h from Sybil count', async () => {
// Seed: alice has 10 orders, all OLDER than 24h and
// status='cancelled' (so they don't satisfy the 'live OR
// recent' predicate). Alice posts a new one; she should
// be at tier 1, paying 62.5 BLURT.
const now = new Date('2026-04-19T12:00:00Z');
const old = new Date(now.getTime() - 48 * 3600 * 1000);
for (let i = 0; i < 10; i++) {
await fx.db.query(
`INSERT INTO orders (
account, permlink, side, asset, fiat_currency,
price_model, payment_methods, status, fee_status,
created_at, updated_at
) VALUES ($1, $2, 'sell', 'BTC', 'USD',
'{}'::jsonb, ARRAY['cash'], 'cancelled', 'verified',
$3, $3)`,
['alice', `alice-old-${i}`, old]
);
}
const payload = orderPayload('alice-new');
const result = await fx.db.withTx(async (client) => {
return orderHandler(
makeCtx({
signer: 'alice',
blockTime: now,
payload,
siblingOps: [feeTransferOp('alice', 62.5, payload.permlink)]
}),
client
);
});
expect(result).toEqual({ ok: true });
const row = await fx.db.query<{ fee_status: string }>(
`SELECT fee_status FROM orders WHERE permlink = $1`,
[payload.permlink]
);
// 62.5 BLURT was the CORRECT tier-1 fee given the old
// orders don't count. If the 24h cutoff predicate were
// broken, Alice would be computed as tier 11 and this
// would land as 'underpaid'.
expect(row.rows[0]!.fee_status).toBe('verified');
});
it('missing fee transfer: fee_status=missing', async () => {
const payload = orderPayload('sell-btc-usd-none');
const result = await fx.db.withTx(async (client) => {
return orderHandler(
makeCtx({
signer: 'alice',
payload,
siblingOps: [] // no transfer at all
}),
client
);
});
expect(result).toEqual({ ok: true });
const row = await fx.db.query<{ fee_status: string }>(
`SELECT fee_status FROM orders WHERE permlink = $1`,
[payload.permlink]
);
expect(row.rows[0]!.fee_status).toBe('missing');
});
});