Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
c7a38f8
feat: make proposal nonce submission idempotent
yvesfracari Aug 26, 2026
8f7b4a0
feat(proposals): persist supersession metadata
yvesfracari Aug 26, 2026
0bbbc57
feat(subsolver): introduce reusable provider boundary
yvesfracari Aug 26, 2026
6eedd5d
fix(db): register proposal supersession migration
yvesfracari Aug 26, 2026
2f5b2c6
fix(db): register proposal supersession migration
yvesfracari Aug 26, 2026
7dc6892
chore: lock subsolver core workspace
yvesfracari Aug 26, 2026
d9394d5
test(proposals): use unique nonces in shared fixtures
yvesfracari Aug 26, 2026
ad5ed26
test(proposals): use unique nonces in shared fixtures
yvesfracari Aug 26, 2026
fdd8c9d
test(solve): make encoded route nonce explicit
yvesfracari Aug 26, 2026
7675c9a
test(solve): make encoded route nonce explicit
yvesfracari Aug 26, 2026
06f1167
style: format solve route fixture
yvesfracari Aug 26, 2026
e261bce
style: format solve route fixture
yvesfracari Aug 26, 2026
50f3045
feat(proposals): supersede older live proposals on activation
yvesfracari Aug 26, 2026
2bf03e3
docs(api): describe superseded proposals
yvesfracari Aug 26, 2026
03b88e0
Merge branch 'pedro/cow-1274-create-fynd-subsolver' into pedro/cow-12…
yvesfracari Aug 27, 2026
a99daa2
fix(proposals): keep superseded non-terminal for expiry
yvesfracari Aug 27, 2026
7b14a3d
feat(subsolver): add Fynd quote provider
yvesfracari Aug 27, 2026
407c3a1
fix(proposals): keep superseded non-terminal for expiry
yvesfracari Aug 27, 2026
fe7f858
feat(dev): add subsolver compose profiles
yvesfracari Aug 27, 2026
4bcd958
feat(subsolver): submit Fynd routes through BYOS
yvesfracari Aug 27, 2026
268604a
feat(subsolver): gate Fynd quotes on sidecar readiness
yvesfracari Aug 27, 2026
072bbe3
feat(subsolver): add shared Fynd polling pipeline
yvesfracari Aug 27, 2026
d54fd6d
feat(proposals): complete supersession lifecycle
yvesfracari Aug 27, 2026
b5d26f4
Merge branch 'pedro/cow-1274-create-fynd-subsolver' into pedro/cow-12…
yvesfracari Aug 27, 2026
45a5a54
feat(subsolver): complete shared client pipeline
yvesfracari Aug 27, 2026
b4fd32a
test(proposals): cover supersession edge cases
yvesfracari Aug 27, 2026
f6b9f03
Merge branch 'pedro/cow-1274-create-fynd-subsolver' into pedro/cow-12…
yvesfracari Aug 27, 2026
7b5b294
style(subsolver): satisfy shared pipeline lint
yvesfracari Aug 27, 2026
56c2f56
fix(subsolver): mount compose config
yvesfracari Aug 27, 2026
51406df
refactor(subsolver): separate Fynd app
yvesfracari Aug 27, 2026
44cdbf3
Rename baseline subsolver and fix lint
yvesfracari Aug 28, 2026
6915cd3
fix(subsolver): harden Fynd polling
yvesfracari Aug 31, 2026
70d6fe8
Merge remote-tracking branch 'origin/main' into pedro/cow-1274-fynd-s…
yvesfracari Aug 31, 2026
d5ac8dc
feat(subsolver): add BSC config with PancakeSwap V2 addresses
jean-neiverth Sep 1, 2026
83ec4b7
fix(subsolver): fix BSC config fields and bake into Docker image
jean-neiverth Sep 1, 2026
b6aa20e
Merge pull request #78 from bleu/jean/bsc-subsolver-config
jean-neiverth Sep 1, 2026
a5b285c
Merge main into pedro/cow-1274-fynd-subsolver
jean-neiverth Sep 9, 2026
89be371
fix: subsolver core signature
jean-neiverth Sep 9, 2026
ef88d95
fix: docker compose build
jean-neiverth Sep 10, 2026
f349e1d
fix: lint
jean-neiverth Sep 10, 2026
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
14 changes: 14 additions & 0 deletions .env.fynd-subsolver.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
ORDERBOOK_URL=http://host.docker.internal:8080
BYOS_URL=http://host.docker.internal:9585
RPC_URL=
SUBSOLVER_PRIVATE_KEY=
CONFIG_PATH=config.toml
SUBSOLVER_PROVIDER=fynd
FYND_URL=http://fynd:3000
FYND_SLIPPAGE_BPS=50
FYND_QUOTE_CONCURRENCY=32
FYND_MAX_QUOTE_AGE_SECONDS=10
GPV2_SETTLEMENT=
BYOS_REQUESTS_PER_MINUTE=100
BYOS_READ_RESERVE=10
PROPOSAL_REFRESH_INTERVAL_SECONDS=15
8 changes: 8 additions & 0 deletions .env.subsolver.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
ORDERBOOK_URL=http://host.docker.internal:8080
BYOS_URL=http://host.docker.internal:9585
RPC_URL=
SUBSOLVER_PRIVATE_KEY=
CONFIG_PATH=config.toml
BYOS_REQUESTS_PER_MINUTE=100
BYOS_READ_RESERVE=10
PROPOSAL_REFRESH_INTERVAL_SECONDS=15
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ node_modules/
.env
.env.*
!.env.example
!.env.*.example

# Local agent state
.claude/
Expand Down
8 changes: 8 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
COPY apps/byos/package.json apps/byos/
COPY apps/subsolver/package.json apps/subsolver/
COPY packages/common/package.json packages/common/
COPY packages/subsolver-core/package.json packages/subsolver-core/

RUN pnpm install --frozen-lockfile

Expand All @@ -32,6 +33,13 @@ COPY --from=build /app/apps/byos/node_modules apps/byos/node_modules/
COPY --from=build /app/packages/common/dist packages/common/dist/
COPY --from=build /app/packages/common/package.json packages/common/
COPY --from=build /app/packages/common/node_modules packages/common/node_modules/
# Configurable subsolver executable and reusable provider package.
COPY --from=build /app/apps/subsolver/dist apps/subsolver/dist/
COPY --from=build /app/apps/subsolver/package.json apps/subsolver/
COPY --from=build /app/apps/subsolver/node_modules apps/subsolver/node_modules/
COPY --from=build /app/packages/subsolver-core/dist packages/subsolver-core/dist/
COPY --from=build /app/packages/subsolver-core/package.json packages/subsolver-core/
COPY --from=build /app/packages/subsolver-core/node_modules packages/subsolver-core/node_modules/

EXPOSE 9585 9586
CMD ["node", "apps/byos/dist/index.js"]
16 changes: 16 additions & 0 deletions apps/byos/drizzle/0004_nonce_uniqueness_and_supersession.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
ALTER TABLE "proposals" ADD COLUMN "superseded_by_proposal_id" bigint;--> statement-breakpoint
ALTER TABLE "proposals" ADD CONSTRAINT "proposals_superseded_by_proposal_id_proposals_id_fk" FOREIGN KEY ("superseded_by_proposal_id") REFERENCES "public"."proposals"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
WITH newest_active AS (
SELECT DISTINCT ON (sub_solver, order_uid) id, sub_solver, order_uid
FROM "proposals"
WHERE status = 'active'
ORDER BY sub_solver, order_uid, id DESC
)
UPDATE "proposals" AS old
SET status = 'superseded', superseded_by_proposal_id = newest_active.id
FROM newest_active
WHERE old.sub_solver = newest_active.sub_solver
AND old.order_uid = newest_active.order_uid
AND old.id < newest_active.id
AND old.status IN ('submitted', 'active');--> statement-breakpoint
CREATE UNIQUE INDEX "proposals_sub_solver_nonce_key" ON "proposals" USING btree ("sub_solver","nonce");
2 changes: 2 additions & 0 deletions apps/byos/drizzle/0005_supersession_reference_retention.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE "proposals" DROP CONSTRAINT "proposals_superseded_by_proposal_id_proposals_id_fk";--> statement-breakpoint
ALTER TABLE "proposals" ADD CONSTRAINT "proposals_superseded_by_proposal_id_proposals_id_fk" FOREIGN KEY ("superseded_by_proposal_id") REFERENCES "public"."proposals"("id") ON DELETE set null ON UPDATE no action;
14 changes: 14 additions & 0 deletions apps/byos/drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@
"when": 1786896000000,
"tag": "0003_buffer_entries",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1787780800000,
"tag": "0004_nonce_uniqueness_and_supersession",
"breakpoints": true
},
{
"idx": 5,
"version": "7",
"when": 1787940000000,
"tag": "0005_supersession_reference_retention",
"breakpoints": true
}
]
}
11 changes: 11 additions & 0 deletions apps/byos/openapi.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
openapi: 3.0.3

info:

Check warning on line 3 in apps/byos/openapi.yml

View workflow job for this annotation

GitHub Actions / openapi

info-contact Info object must have "contact" object.
title: BYOS Proposal API
description: |
The public HTTP API by which sub-solvers submit signed settlement
Expand Down Expand Up @@ -77,7 +77,7 @@

paths:
/healthz:
get:

Check warning on line 80 in apps/byos/openapi.yml

View workflow job for this annotation

GitHub Actions / openapi

operation-tags Operation must have non-empty "tags" array.
operationId: healthz
summary: Health check.
description: >-
Expand All @@ -90,7 +90,7 @@
description: Service is up.

/proposals:
post:

Check warning on line 93 in apps/byos/openapi.yml

View workflow job for this annotation

GitHub Actions / openapi

operation-tags Operation must have non-empty "tags" array.
operationId: createProposal
summary: Submit a signed settlement proposal.
description: |
Expand Down Expand Up @@ -143,7 +143,7 @@
/proposal/{id}:
parameters:
- $ref: "#/components/parameters/ProposalIdPath"
get:

Check warning on line 146 in apps/byos/openapi.yml

View workflow job for this annotation

GitHub Actions / openapi

operation-tags Operation must have non-empty "tags" array.
operationId: getProposal
summary: Get one of the caller's proposals.
description: |
Expand Down Expand Up @@ -183,7 +183,7 @@
"503":
$ref: "#/components/responses/ServiceUnavailable"

delete:

Check warning on line 186 in apps/byos/openapi.yml

View workflow job for this annotation

GitHub Actions / openapi

operation-tags Operation must have non-empty "tags" array.
operationId: cancelProposal
summary: Cancel a live proposal.
description: |
Expand Down Expand Up @@ -225,7 +225,7 @@
$ref: "#/components/responses/ServiceUnavailable"

/proposals/by-sub-solver:
get:

Check warning on line 228 in apps/byos/openapi.yml

View workflow job for this annotation

GitHub Actions / openapi

operation-tags Operation must have non-empty "tags" array.
operationId: listProposalsBySubSolver
summary: List the caller's own proposals.
description: |
Expand Down Expand Up @@ -253,7 +253,7 @@
$ref: "#/components/responses/ServiceUnavailable"

/proposals/{orderUid}:
get:

Check warning on line 256 in apps/byos/openapi.yml

View workflow job for this annotation

GitHub Actions / openapi

operation-tags Operation must have non-empty "tags" array.
operationId: listProposalsByOrder
summary: List the caller's proposals on one order.
description: |
Expand Down Expand Up @@ -418,6 +418,9 @@
- `penalized` — that debit landed; see `penaltyTxHash`.
- `simFailed` — the settlement simulation reverted.
- `cancelled` — cancelled by a signed `DELETE`.
- `superseded` — a newer validated proposal from the same sub-solver
replaced this route for the order. It may still receive a delayed
settlement outcome and expires at `validUntil`.

All states except `submitted`, `active`, `executing`, and
`settleFailed` are terminal.
Expand All @@ -433,6 +436,7 @@
- penalized
- simFailed
- cancelled
- superseded
RejectionReason:
description: |
Why the background validator rejected a proposal:
Expand Down Expand Up @@ -593,17 +597,24 @@
required:
- id
- subSolver
- orderUid
- validUntil
- status
properties:
id:
$ref: "#/components/schemas/ProposalId"
subSolver:
$ref: "#/components/schemas/Address"
orderUid:
$ref: "#/components/schemas/OrderUid"
validUntil:
$ref: "#/components/schemas/UnixTimestamp"
status:
$ref: "#/components/schemas/ProposalStatus"
supersededByProposalId:
description: Present when this proposal was replaced by a newer active proposal.
allOf:
- $ref: "#/components/schemas/ProposalId"
ListProposalsResponse:
type: object
required:
Expand Down
3 changes: 3 additions & 0 deletions apps/byos/src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
primaryKey,
text,
timestamp,
uniqueIndex,
} from "drizzle-orm/pg-core";

// --- proposals ---
Expand Down Expand Up @@ -37,13 +38,15 @@ export const proposals = pgTable(
settlementTxHash: text("settlement_tx_hash"),
penaltyTxHash: text("penalty_tx_hash"),
pendingCancellation: boolean("pending_cancellation").notNull().default(false),
supersededByProposalId: bigint("superseded_by_proposal_id", { mode: "number" }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
statusChangedAt: timestamp("status_changed_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => [
index("proposals_order_uid_status_idx").on(table.orderUid, table.status),
index("proposals_sub_solver_status_idx").on(table.subSolver, table.status),
index("proposals_status_status_changed_at_idx").on(table.status, table.statusChangedAt),
uniqueIndex("proposals_sub_solver_nonce_key").on(table.subSolver, table.nonce),
],
);

Expand Down
11 changes: 11 additions & 0 deletions apps/byos/src/domain/audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ import type { Proposal } from "./proposal.js";

export type AuditKind =
| { type: "received"; proposal: Proposal }
| {
type: "nonceConflict";
proposalId: number;
subSolver: Address;
orderUid: string;
incomingFingerprint: string;
}
| { type: "cancelled"; proposalId: number; subSolver: Address; orderUid: string }
// Emitted when a sub-solver requests cancellation while the proposal is in
// an active auction settlement ("executing"). The proposal stays executing
Expand Down Expand Up @@ -63,6 +70,8 @@ export function eventType(kind: AuditKind): string {
switch (kind.type) {
case "received":
return "received";
case "nonceConflict":
return "nonce_conflict";
case "cancelled":
return "cancelled";
case "cancellationDeferred":
Expand Down Expand Up @@ -115,6 +124,8 @@ export function auditPayload(kind: AuditKind): Record<string, unknown> {
nonce: kind.proposal.nonce.toString(),
signature: kind.proposal.signature,
};
case "nonceConflict":
return { incomingFingerprint: kind.incomingFingerprint };
case "cancelled":
return {};
case "cancellationDeferred":
Expand Down
2 changes: 2 additions & 0 deletions apps/byos/src/domain/proposal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export interface Proposal {
settlementTxHash: Hex | null;
penaltyTxHash: Hex | null;
pendingCancellation: boolean;
/** Set when a newer proposal for this sub-solver/order supersedes this one. */
supersededByProposalId?: number | null;
}

/** What the driver reported about a settlement. */
Expand Down
25 changes: 25 additions & 0 deletions apps/byos/src/infra/__tests__/audit.db.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,29 @@ describe("audit persistence", () => {

expect(rows.length).toBeGreaterThanOrEqual(1);
});

it("persists a nonce conflict against the original proposal", async () => {
const event: AuditEvent = {
occurredAt: new Date(),
kind: {
type: "nonceConflict",
proposalId: 4,
subSolver: "0xe05fcc23807536bee418f142d19fa0d21bb0cff7" as Address,
orderUid: `0x${"ab".repeat(56)}`,
incomingFingerprint: "signed-payload-fingerprint",
},
};

await insertAuditEvent(ctx.db, event);

const [row] = await ctx.db
.select()
.from(auditEvents)
.where(eq(auditEvents.eventType, "nonce_conflict"));
expect(row).toMatchObject({ proposalId: 4, eventType: "nonce_conflict" });
if (!row) throw new Error("nonce conflict audit event missing");
expect((row.payload as Record<string, unknown>).incomingFingerprint).toBe(
"signed-payload-fingerprint",
);
});
});
Loading
Loading