Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 11 additions & 1 deletion src/runtime/http/express-router-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { DatabaseAdapter, WebhookProcessor } from '@/runtime/interfaces.ts'
import type { PluginRouteContext, RouteDefinition } from '@/types/foundation.ts';
import type { AnchorPluginHooks, DepositRequestBody } from '@/types/plugin.ts';
import { IdempotencyUtils } from '@/utils/idempotency.ts';
import Big from 'big.js';
import {
Account,
Keypair,
Expand Down Expand Up @@ -995,7 +996,13 @@ async function handleDepositInteractive(
typeof parsedBody.body.asset_code === 'string' ? parsedBody.body.asset_code : '';
const amountRaw = parsedBody.body.amount;
const amount =
typeof amountRaw === 'number' || typeof amountRaw === 'string' ? `${amountRaw}` : '';
typeof amountRaw === 'number'
? Number.isFinite(amountRaw)
? new Big(amountRaw).toFixed()
: `${amountRaw}`
: typeof amountRaw === 'string'
? amountRaw
: '';

if (!assetCode || !amount) {
sendJson(res, 400, {
Expand Down Expand Up @@ -1155,6 +1162,7 @@ async function handleDepositInteractive(
account: auth.account,
interactive_url: buildInteractiveUrl(serverConfig.interactiveDomain, transactionId),
created_at: createdAt,
updated_at: createdAt,
};

try {
Expand Down Expand Up @@ -1207,6 +1215,7 @@ async function handleDepositInteractive(
account: created.account,
interactive_url: buildInteractiveUrl(serverConfig.interactiveDomain, created.id),
created_at: created.createdAt,
updated_at: created.updatedAt,
});
}

Expand Down Expand Up @@ -1367,6 +1376,7 @@ async function handleWebhook(
error: 'webhook_error',
message: 'Webhook processing failed',
event_id: eventId,
provider,
});
}
}
Expand Down
1 change: 1 addition & 0 deletions src/runtime/webhooks/default-webhook-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export class DefaultWebhookProcessor implements WebhookProcessor {
{
receivedAt: insertion.record.createdAt,
signature: input.signature,
rawBody: input.rawBody,
},
),
),
Expand Down
1 change: 1 addition & 0 deletions src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,7 @@ export interface AnchorKitConfig {
context: {
receivedAt: string;
signature?: string;
rawBody: string | Buffer | Uint8Array;
},
) => Promise<void> | void;
};
Expand Down
16 changes: 14 additions & 2 deletions tests/mvp-express.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ describe('MVP Express-mounted integration', () => {
const dbPath = dbUrl.startsWith('file:') ? dbUrl.slice('file:'.length) : dbUrl;

let webhookCallbackCount = 0;
let lastWebhookRawBody: string | Buffer | Uint8Array | undefined;
let anchor: AnchorInstance;
let invoke: (options: TestRequestOptions) => Promise<TestResponse>;
let accessToken = '';
Expand Down Expand Up @@ -230,8 +231,9 @@ describe('MVP Express-mounted integration', () => {
},
},
webhooks: {
onEvent: async () => {
onEvent: async (_event, callbackContext) => {
webhookCallbackCount += 1;
lastWebhookRawBody = callbackContext.rawBody;
},
},
});
Expand Down Expand Up @@ -1545,12 +1547,14 @@ describe('MVP Express-mounted integration', () => {
authorization: `Bearer ${accessToken}`,
'idempotency-key': 'deposit-numeric-amount',
},
body: { asset_code: 'USDC', amount: 50 },
rawBody: '{"asset_code":"USDC","amount":5e1}',
});

expect(response.status).toBe(201);
expect(response.body.id).toBeTruthy();
expect(response.body.status).toBe('pending_user_transfer_start');
expect(response.body.amount).toBe('50');
expect(response.body.updated_at).toBe(response.body.created_at);
});

it('6) authorized deposit interactive creates persistent transaction', async () => {
Expand Down Expand Up @@ -1592,6 +1596,7 @@ describe('MVP Express-mounted integration', () => {

expect(createResponse.status).toBe(201);
expect(createResponse.body.amount).toBe(submittedAmount);
expect(createResponse.body.updated_at).toBe(createResponse.body.created_at);

const persistedTransaction = await (
anchor as unknown as {
Expand All @@ -1616,6 +1621,7 @@ describe('MVP Express-mounted integration', () => {

expect(lookupResponse.status).toBe(200);
expect(lookupResponse.body.amount).toBe(submittedAmount);
expect(lookupResponse.body.updated_at).toBe(createResponse.body.updated_at);
});

it('6b) deposit with SAME idempotency-key but DIFFERENT body is rejected', async () => {
Expand Down Expand Up @@ -2160,6 +2166,7 @@ describe('MVP Express-mounted integration', () => {
error: 'webhook_error',
event_id: 'evt_misconfigured',
message: 'Webhook processing failed',
provider: 'generic',
});

const signature = createHmac('sha256', 'any-secret')
Expand All @@ -2182,6 +2189,7 @@ describe('MVP Express-mounted integration', () => {
error: 'webhook_error',
event_id: 'evt_misconfigured',
message: 'Webhook processing failed',
provider: 'generic',
});
expect(misconfiguredWebhookCallbackCount).toBe(0);

Expand Down Expand Up @@ -2352,6 +2360,7 @@ describe('MVP Express-mounted integration', () => {
expect(response.body.received).toBe(true);
expect(response.body.duplicate).toBe(false);
expect(response.body.event_id).toBe('evt_buffer');
expect(lastWebhookRawBody).toEqual(Buffer.from(payloadText));
});

it('8h) webhook route treats whitespace-only ids as missing and generates an event id', async () => {
Expand Down Expand Up @@ -2594,6 +2603,7 @@ describe('MVP Express-mounted integration', () => {
expect(response.status).toBe(400);
expect(response.body.error).toBe('webhook_error');
expect(response.body.event_id).toBe('evt_err_1');
expect(response.body.provider).toBe('generic');

await customAnchor.shutdown();
const customDbPath = customDbUrl.startsWith('file:')
Expand Down Expand Up @@ -3111,6 +3121,7 @@ describe('MVP Express-mounted integration', () => {

expect(firstResponse.status).toBe(201);
expect(firstResponse.body.account).toBe(clientKeypair.publicKey());
expect(firstResponse.body.updated_at).toBe(firstResponse.body.created_at);
const firstTxId = firstResponse.body.id;

const secondResponse = await invoke({
Expand All @@ -3126,6 +3137,7 @@ describe('MVP Express-mounted integration', () => {

expect(secondResponse.status).toBe(201);
expect(secondResponse.body.id).toBe(firstTxId);
expect(secondResponse.body.updated_at).toBe(firstResponse.body.updated_at);
});

it('12a) an in-flight idempotency reservation returns a retry response', async () => {
Expand Down
Loading