Skip to content
Open
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
3 changes: 1 addition & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ jobs:
- name: Run Backend Tests
run: |
ls -la src/generated/prisma
npm install @vitest/[email protected] --no-save
npx vitest run --coverage --reporter=basic
working-directory: backend
env:
Expand Down Expand Up @@ -143,7 +142,7 @@ jobs:
run: |
docker compose up -d postgres
sleep 10
docker compose run --rm -e DATABASE_URL=postgresql://flowfi:flowfi_dev_password@postgres:5432/flowfi backend npx -y prisma db push --accept-data-loss
docker compose run --rm -e DATABASE_URL=postgresql://flowfi:flowfi_dev_password@postgres:5432/flowfi backend npx -y prisma db push --accept-data-loss --schema=/app/prisma/schema.prisma
docker compose up -d backend
sleep 15
curl --fail http://localhost:3001/health || (docker compose logs backend && exit 1)
Expand Down
13 changes: 1 addition & 12 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,25 +1,16 @@
FROM node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 AS builder

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY tsconfig.json ./
COPY src ./src
COPY prisma ./prisma

RUN npm run build

FROM node:20-alpine@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293 AS runner

WORKDIR /app

ENV NODE_ENV=production

COPY package*.json ./
RUN npm install --omit=dev

COPY --from=builder /app/dist ./dist
COPY --from=builder /app/src/generated ./dist/generated
COPY --from=builder /app/prisma ./prisma
Expand All @@ -28,7 +19,5 @@ COPY --from=builder /app/prisma ./prisma
# check runs `prisma db push` inside this image, so the config must be present
# too (dotenv is a runtime dependency, so the config loads).
COPY prisma.config.ts ./

EXPOSE 3001

CMD ["npm", "start"]
CMD ["npm", "start"]
25 changes: 25 additions & 0 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,31 @@ app.use((req: Request, res: Response, next: NextFunction) => {
return next(); // Not versioned, continue to deprecated handlers
});

// Deprecated route handlers (return 410 Gone)
app.post('/streams', (req: Request, res: Response) => {
res.status(410).json({
error: 'Deprecated endpoint',
message: 'This endpoint has been deprecated. Please use /v1/streams instead.',
deprecated: true,
migration: {
old: '/streams',
new: '/v1/streams'
}
});
});

app.post('/events', (req: Request, res: Response) => {
res.status(410).json({
error: 'Deprecated endpoint',
message: 'This endpoint has been deprecated. Please use /v1/events instead.',
deprecated: true,
migration: {
old: '/events',
new: '/v1/events'
}
});
});

// Health check routes
app.use('/health', healthRoutes);

Expand Down
2 changes: 1 addition & 1 deletion backend/src/workers/soroban-event-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -975,4 +975,4 @@ export class SorobanEventWorker {
}
}

export const sorobanEventWorker = new SorobanEventWorker();
export const sorobanEventWorker = new SorobanEventWorker();
14 changes: 9 additions & 5 deletions backend/tests/deprecated.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,26 @@ import app from '../src/app.js';
// This file tests deprecated endpoints WITHOUT any Prisma mocking
// so the real route handlers respond directly without interference.

describe('Removed unversioned routes', () => {
it('POST /streams returns 404 Not Found', async () => {
describe('Deprecated route responses', () => {
it('POST /streams returns 410 Gone', async () => {
const response = await request(app)
.post('/streams')
.send({})
.set('Accept', 'application/json');

expect(response.status).toBe(404);
expect(response.status).toBe(410);
expect(response.body.deprecated).toBe(true);
expect(response.body.migration).toMatchObject({ old: '/streams', new: '/v1/streams' });
});

it('POST /events returns 404 Not Found', async () => {
it('POST /events returns 410 Gone', async () => {
const response = await request(app)
.post('/events')
.send({})
.set('Accept', 'application/json');

expect(response.status).toBe(404);
expect(response.status).toBe(410);
expect(response.body.deprecated).toBe(true);
expect(response.body.migration).toMatchObject({ old: '/events', new: '/v1/events' });
});
});
14 changes: 14 additions & 0 deletions contracts/stream_contract/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,20 @@ fn test_update_fee_config_rejects_invalid_fee_rate() {
assert_eq!(result, Err(Ok(StreamError::InvalidFeeRate)));
}

#[test]
fn test_update_fee_config_rejects_not_initialized() {
let env = Env::default();
env.mock_all_auths();
let client = create_contract(&env);

let admin = Address::generate(&env);
let treasury = Address::generate(&env);

// Call update_fee_config without calling initialize first
let result = client.try_update_fee_config(&admin, &treasury, &100);
assert_eq!(result, Err(Ok(StreamError::NotInitialized)));
}

#[test]
fn test_initialize_emits_event() {
let env = Env::default();
Expand Down
21 changes: 11 additions & 10 deletions frontend/src/components/dashboard/dashboard-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -882,7 +882,8 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) {
<p>Save recurring stream settings once, apply instantly, then override before submitting.</p>

<div className="stream-template-editor">
<input value={templateNameInput} onChange={(e) => setTemplateNameInput(e.target.value)} placeholder="e.g. Monthly Contributor Payroll" aria-label="Template name" />
<label htmlFor="template-name-input" className="visually-hidden">Template name</label>
<input id="template-name-input" value={templateNameInput} onChange={(e) => setTemplateNameInput(e.target.value)} placeholder="e.g. Monthly Contributor Payroll" />
<div className="stream-template-editor__actions">
<button type="button" className="secondary-button" disabled={!isTemplateNameValid} onClick={handleSaveTemplate}>{saveTemplateButtonLabel}</button>
{editingTemplateId ? <button type="button" className="secondary-button" onClick={handleClearTemplateEditor}>Stop Editing</button> : null}
Expand Down Expand Up @@ -916,28 +917,28 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) {
<h4>Stream Configuration</h4>
<p>{requiredFieldsCompleted} / 5 required fields completed</p>
</div>
<label className="stream-form__template-select">
<label htmlFor="template-select" className="stream-form__template-select">
Load template
<select value={selectedTemplateId ?? ""} onChange={(e) => { const id = e.target.value; if (!id) { setSelectedTemplateId(null); return; } handleApplyTemplate(id); }}>
<select id="template-select" value={selectedTemplateId ?? ""} onChange={(e) => { const id = e.target.value; if (!id) { setSelectedTemplateId(null); return; } handleApplyTemplate(id); }}>
<option value="">Select saved template</option>
{templates.map((t) => <option key={t.id} value={t.id}>{t.name}</option>)}
</select>
</label>
</div>

<label>Recipient Address<input required type="text" value={streamForm.recipient} onChange={(e) => updateStreamForm("recipient", e.target.value)} placeholder="G..." /></label>
<label htmlFor="stream-recipient">Recipient Address<input id="stream-recipient" required type="text" value={streamForm.recipient} onChange={(e) => updateStreamForm("recipient", e.target.value)} placeholder="G..." /></label>
<div className="stream-form__row">
<label>Token<input required type="text" value={streamForm.token} onChange={(e) => updateStreamForm("token", e.target.value.toUpperCase())} placeholder="USDC" /></label>
<label>Total Amount<input required type="number" min="0" step="0.0000001" value={streamForm.totalAmount} onChange={(e) => updateStreamForm("totalAmount", e.target.value)} placeholder="100" /></label>
<label htmlFor="stream-token">Token<input id="stream-token" required type="text" value={streamForm.token} onChange={(e) => updateStreamForm("token", e.target.value.toUpperCase())} placeholder="USDC" /></label>
<label htmlFor="stream-total-amount">Total Amount<input id="stream-total-amount" required type="number" min="0" step="0.0000001" value={streamForm.totalAmount} onChange={(e) => updateStreamForm("totalAmount", e.target.value)} placeholder="100" /></label>
</div>
<div className="stream-form__row">
<label>Starts At<input required type="datetime-local" value={streamForm.startsAt} onChange={(e) => updateStreamForm("startsAt", e.target.value)} /></label>
<label>Ends At<input required type="datetime-local" value={streamForm.endsAt} onChange={(e) => updateStreamForm("endsAt", e.target.value)} /></label>
<label htmlFor="stream-starts-at">Starts At<input id="stream-starts-at" required type="datetime-local" value={streamForm.startsAt} onChange={(e) => updateStreamForm("startsAt", e.target.value)} /></label>
<label htmlFor="stream-ends-at">Ends At<input id="stream-ends-at" required type="datetime-local" value={streamForm.endsAt} onChange={(e) => updateStreamForm("endsAt", e.target.value)} /></label>
</div>
<div className="stream-form__row">
<label>Cadence (seconds)<input type="number" min="1" step="1" value={streamForm.cadenceSeconds} onChange={(e) => updateStreamForm("cadenceSeconds", e.target.value)} /></label>
<label htmlFor="stream-cadence">Cadence (seconds)<input id="stream-cadence" type="number" min="1" step="1" value={streamForm.cadenceSeconds} onChange={(e) => updateStreamForm("cadenceSeconds", e.target.value)} /></label>
</div>
<label>Note<textarea value={streamForm.note} onChange={(e) => updateStreamForm("note", e.target.value)} placeholder="Optional internal note for this stream configuration." /></label>
<label htmlFor="stream-note">Note<textarea id="stream-note" value={streamForm.note} onChange={(e) => updateStreamForm("note", e.target.value)} placeholder="Optional internal note for this stream configuration." /></label>

<div className="stream-form__actions">
<button type="submit" className="wallet-button" disabled={isFormSubmitting}>{isFormSubmitting ? "Submitting..." : "Create Stream"}</button>
Expand Down
Loading