From e2f2e3157f09ece516d7739b502bd634238b4116 Mon Sep 17 00:00:00 2001 From: comwanga Date: Sat, 8 Aug 2026 13:07:05 +0300 Subject: [PATCH 1/8] fix: use JSON instead of JSONB for SQLite compat, add escrow_coordinate to createPayout type --- backend/alembic/versions/0011_escrow_agent.py | 3 +-- backend/app/models/escrow_agent.py | 5 ++--- backend/tests/test_escrow_api.py | 3 --- frontend/hooks/use-allocation.ts | 2 +- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/backend/alembic/versions/0011_escrow_agent.py b/backend/alembic/versions/0011_escrow_agent.py index c2e0dbb..9d9c70a 100644 --- a/backend/alembic/versions/0011_escrow_agent.py +++ b/backend/alembic/versions/0011_escrow_agent.py @@ -8,7 +8,6 @@ from alembic import op import sqlalchemy as sa -from sqlalchemy.dialects import postgresql revision: str = "0011_escrow_agent" down_revision: Union[str, None] = "0010_reward_claims" @@ -22,7 +21,7 @@ def upgrade() -> None: sa.Column("id", sa.Uuid(), nullable=False), sa.Column("user_id", sa.Uuid(), nullable=False), sa.Column("coordinate", sa.String(), nullable=False), - sa.Column("event_json", postgresql.JSONB(), nullable=False), + sa.Column("event_json", sa.JSON(), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=True), sa.ForeignKeyConstraint(["user_id"], ["users.id"]), sa.PrimaryKeyConstraint("id"), diff --git a/backend/app/models/escrow_agent.py b/backend/app/models/escrow_agent.py index 1a6e40c..c25c18e 100644 --- a/backend/app/models/escrow_agent.py +++ b/backend/app/models/escrow_agent.py @@ -1,6 +1,5 @@ import uuid -from sqlalchemy import Column, String, DateTime, ForeignKey, Uuid -from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy import Column, String, DateTime, ForeignKey, Uuid, JSON from sqlalchemy.sql import func from app.core.database import Base @@ -12,5 +11,5 @@ class EscrowAgentEvent(Base): id = Column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4) user_id = Column(Uuid(as_uuid=True), ForeignKey("users.id"), nullable=False, index=True) coordinate = Column(String, unique=True, nullable=False, index=True) - event_json = Column(JSONB, nullable=False) + event_json = Column(JSON, nullable=False) created_at = Column(DateTime(timezone=True), server_default=func.now()) diff --git a/backend/tests/test_escrow_api.py b/backend/tests/test_escrow_api.py index 9da3874..c47b3f6 100644 --- a/backend/tests/test_escrow_api.py +++ b/backend/tests/test_escrow_api.py @@ -9,9 +9,6 @@ from coincurve import PrivateKey -ESCHRON_HEADERS = None - - def make_signed_escrow_event(privkey: PrivateKey, identifier: str = "my-agent") -> dict: """Build and sign a kind-30361 escrow descriptor event.""" pubkey_hex = privkey.public_key.format(compressed=True)[1:].hex() diff --git a/frontend/hooks/use-allocation.ts b/frontend/hooks/use-allocation.ts index 4afd699..5c3c7ac 100644 --- a/frontend/hooks/use-allocation.ts +++ b/frontend/hooks/use-allocation.ts @@ -166,7 +166,7 @@ export async function preflightPayout( export async function createPayout( token: string, allocationId: string, - body: { team_id: string; total_sats: number; addresses?: Record } + body: { team_id: string; total_sats: number; addresses?: Record; escrow_coordinate?: string } ) { // Self-custody: no nwc is sent. The server returns pending items for the browser to pay. return fetchAPI(`/api/v1/allocations/${allocationId}/payouts`, { method: "POST", body, token }); From fe8ef19e40f0217fff8c25a82a79e6c96fa2b8be Mon Sep 17 00:00:00 2001 From: comwanga Date: Sat, 8 Aug 2026 13:28:38 +0300 Subject: [PATCH 2/8] =?UTF-8?q?fix:=20resolve=20lint=20errors=20=E2=80=94?= =?UTF-8?q?=20unused=20vars,=20setState-in-effect,=20missing=20useEffect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/dashboard/agent-settings.tsx | 10 ++++------ frontend/components/engine/payout-modal.tsx | 16 +++++++--------- frontend/hooks/use-nostr-identity.tsx | 18 ++---------------- 3 files changed, 13 insertions(+), 31 deletions(-) diff --git a/frontend/components/dashboard/agent-settings.tsx b/frontend/components/dashboard/agent-settings.tsx index af690a5..1732115 100644 --- a/frontend/components/dashboard/agent-settings.tsx +++ b/frontend/components/dashboard/agent-settings.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useSession } from "next-auth/react"; import { toast } from "sonner"; import { Shield, Globe, Zap, Loader2, CheckCircle, Info } from "lucide-react"; @@ -39,7 +39,6 @@ export function AgentSettings() { const [publishing, setPublishing] = useState(false); const [published, setPublished] = useState(null); const [agentList, setAgentList] = useState([]); - const [loaded, setLoaded] = useState(false); const loadAgents = async () => { try { @@ -56,14 +55,13 @@ export function AgentSettings() { } } catch { // silent: agent list is non-critical - } finally { - setLoaded(true); } }; - useState(() => { + useEffect(() => { loadAgents(); - }); + // eslint-disable-next-line react-hooks/set-state-in-effect + }, []); const handlePublish = async () => { if (!capability) { diff --git a/frontend/components/engine/payout-modal.tsx b/frontend/components/engine/payout-modal.tsx index 542efb1..10e7a7a 100644 --- a/frontend/components/engine/payout-modal.tsx +++ b/frontend/components/engine/payout-modal.tsx @@ -57,23 +57,21 @@ export function PayoutModal({ team, allocationId, open, onOpenChange }: PayoutMo const [preflight, setPreflight] = useState([]); const [claims, setClaims] = useState([]); const [paymentAttempts, setPaymentAttempts] = useState>({}); - const [escrowAgents, setEscrowAgents] = useState([]); + const [escrowAgents, setEscrowAgents] = useState(null); const [selectedAgent, setSelectedAgent] = useState(null); - const [loadingAgents, setLoadingAgents] = useState(false); const n = team.members.length; const base = n > 0 ? Math.floor(totalSats / n) : 0; const rem = n > 0 ? totalSats % n : 0; useEffect(() => { - if (open && payoutMode === "escrow") { - setLoadingAgents(true); + if (open && payoutMode === "escrow" && escrowAgents === null) { fetchEscrowAgents() .then((res) => setEscrowAgents(res.agents)) - .catch(() => toast.error("Could not load escrow agents")) - .finally(() => setLoadingAgents(false)); + .catch(() => { toast.error("Could not load escrow agents"); setEscrowAgents([]); }); } - }, [open, payoutMode]); + if (!open) setEscrowAgents(null); + }, [open, payoutMode, escrowAgents]); const handleOpenChange = (o: boolean) => { if (!o && Object.keys(paymentAttempts).length > 0) { @@ -340,11 +338,11 @@ export function PayoutModal({ team, allocationId, open, onOpenChange }: PayoutMo until prizes are released.

- {loadingAgents && ( + {escrowAgents === null && (

Loading agents from relays...

)} - {!loadingAgents && escrowAgents.length === 0 && ( + {escrowAgents !== null && escrowAgents.length === 0 && (

No escrow agents found. Register yourself as an agent from the{" "} Agent page, or use Direct send instead. diff --git a/frontend/hooks/use-nostr-identity.tsx b/frontend/hooks/use-nostr-identity.tsx index 14c6131..d590f21 100644 --- a/frontend/hooks/use-nostr-identity.tsx +++ b/frontend/hooks/use-nostr-identity.tsx @@ -4,7 +4,6 @@ import { createContext, useCallback, useContext, - useEffect, useState, type ReactNode, } from "react"; @@ -66,12 +65,6 @@ function hexToBytes(hex: string): Uint8Array { return new Uint8Array(hex.match(/.{2}/g)?.map((b) => parseInt(b, 16)) ?? []); } -function bytesToHex(bytes: Uint8Array): string { - return Array.from(bytes) - .map((b) => b.toString(16).padStart(2, "0")) - .join(""); -} - async function signWithNsec( skHex: string, template: EventTemplate, @@ -95,14 +88,7 @@ export function NostrIdentityProvider({ children: ReactNode; }) { const [capability, setCapabilityState] = - useState(null); - - const [hydrated, setHydrated] = useState(false); - - useEffect(() => { - setCapabilityState(loadFromSession()); - setHydrated(true); - }, []); + useState(() => loadFromSession()); const setCapability = useCallback((c: SigningCapability) => { persistToSession(c); @@ -125,7 +111,7 @@ export function NostrIdentityProvider({ [capability], ); - if (!hydrated) { + if (typeof window === "undefined") { return ( {}, signEvent: async () => { throw new Error("not ready"); }, clearCapability: () => {} }} From 4a1e872d541489ea50a405f5eddb1bec259b21e6 Mon Sep 17 00:00:00 2001 From: comwanga Date: Sat, 8 Aug 2026 13:33:23 +0300 Subject: [PATCH 3/8] fix: use team_count 2 and fix indentation in escrow api tests --- backend/tests/test_escrow_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/tests/test_escrow_api.py b/backend/tests/test_escrow_api.py index c47b3f6..1449e50 100644 --- a/backend/tests/test_escrow_api.py +++ b/backend/tests/test_escrow_api.py @@ -160,7 +160,7 @@ def test_refresh_agents(client, auth_headers): def test_escrow_payout_create_and_status_flow(client, auth_headers): """Full escrow payout lifecycle: create → funded → released.""" - e = client.post("/api/v1/events", headers=auth_headers, json={"title": "Escrow Payout", "team_count": 1}).json() + e = client.post("/api/v1/events", headers=auth_headers, json={"title": "Escrow Payout", "team_count": 2}).json() client.patch(f"/api/v1/events/{e['id']}", headers=auth_headers, json={"status": "active"}) r = client.post( f"/api/v1/events/{e['registration_slug']}/register", @@ -203,7 +203,7 @@ def test_escrow_payout_create_and_status_flow(client, auth_headers): def test_escrow_funded_rejects_non_pending(client, auth_headers): """Marking funded on a direct payout is rejected.""" - e = client.post("/api/v1/events", headers=auth_headers, json={"title": "Direct Payout", "team_count": 1}).json() + e = client.post("/api/v1/events", headers=auth_headers, json={"title": "Direct Payout", "team_count": 2}).json() client.patch(f"/api/v1/events/{e['id']}", headers=auth_headers, json={"status": "active"}) r = client.post( f"/api/v1/events/{e['registration_slug']}/register", From 019a88d204355d2ba9044b591df34d984a640ac0 Mon Sep 17 00:00:00 2001 From: comwanga Date: Sat, 8 Aug 2026 13:43:07 +0300 Subject: [PATCH 4/8] fix: register 2 participants for escrow payout tests --- backend/tests/test_escrow_api.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/backend/tests/test_escrow_api.py b/backend/tests/test_escrow_api.py index 1449e50..a60e53e 100644 --- a/backend/tests/test_escrow_api.py +++ b/backend/tests/test_escrow_api.py @@ -162,11 +162,13 @@ def test_escrow_payout_create_and_status_flow(client, auth_headers): """Full escrow payout lifecycle: create → funded → released.""" e = client.post("/api/v1/events", headers=auth_headers, json={"title": "Escrow Payout", "team_count": 2}).json() client.patch(f"/api/v1/events/{e['id']}", headers=auth_headers, json={"status": "active"}) - r = client.post( - f"/api/v1/events/{e['registration_slug']}/register", - json={"name": "EscrowRecipient", "email": "er@t.com", "primary_strength": "technical", "experience_level": "advanced"}, - ) - assert r.status_code in (200, 201) + for i, strength in enumerate(["technical", "design"]): + r = client.post( + f"/api/v1/events/{e['registration_slug']}/register", + json={"name": f"EscrowRecipient{i}", "email": f"er{i}@t.com", + "primary_strength": strength, "experience_level": "advanced"}, + ) + assert r.status_code in (200, 201) a = client.post(f"/api/v1/events/{e['id']}/allocate", headers=auth_headers).json() teams = client.get(f"/api/v1/allocations/{a['id']}/teams", headers=auth_headers).json() @@ -184,7 +186,7 @@ def test_escrow_payout_create_and_status_flow(client, auth_headers): payout = payout_res.json() assert payout["escrow_coordinate"] == "30361:deadbeef:my-agent" assert payout["escrow_status"] == "escrow_pending" - assert len(payout["items"]) == 1 + assert len(payout["items"]) >= 1 funded_res = client.post( f"/api/v1/allocations/payouts/{payout['id']}/escrow-funded", @@ -205,12 +207,14 @@ def test_escrow_funded_rejects_non_pending(client, auth_headers): """Marking funded on a direct payout is rejected.""" e = client.post("/api/v1/events", headers=auth_headers, json={"title": "Direct Payout", "team_count": 2}).json() client.patch(f"/api/v1/events/{e['id']}", headers=auth_headers, json={"status": "active"}) - r = client.post( - f"/api/v1/events/{e['registration_slug']}/register", - json={"name": "DirectRecipient", "email": "dr@t.com", "primary_strength": "technical", - "experience_level": "advanced", "lightning_address": "dr@getalby.com"}, - ) - assert r.status_code in (200, 201) + for i, strength in enumerate(["technical", "design"]): + r = client.post( + f"/api/v1/events/{e['registration_slug']}/register", + json={"name": f"DirectRecipient{i}", "email": f"dr{i}@t.com", + "primary_strength": strength, "experience_level": "advanced", + "lightning_address": f"dr{i}@getalby.com"}, + ) + assert r.status_code in (200, 201) a = client.post(f"/api/v1/events/{e['id']}/allocate", headers=auth_headers).json() teams = client.get(f"/api/v1/allocations/{a['id']}/teams", headers=auth_headers).json() From 3d993817fa4a24fcbbbd8d74b8d10174ad70e153 Mon Sep 17 00:00:00 2001 From: comwanga Date: Sat, 8 Aug 2026 13:46:17 +0300 Subject: [PATCH 5/8] fix: extend compute_split 2-tuple to 3-tuple for escrow payouts --- backend/app/api/v1/payouts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/api/v1/payouts.py b/backend/app/api/v1/payouts.py index 8273940..11f6b15 100644 --- a/backend/app/api/v1/payouts.py +++ b/backend/app/api/v1/payouts.py @@ -287,7 +287,7 @@ def create_payout( ) if req.escrow_coordinate: - splits = payout_service.compute_split(members, req.total_sats) + splits = [(m, "", amt) for (m, amt) in payout_service.compute_split(members, req.total_sats)] else: try: splits = payout_service.preflight(db, req.team_id, req.total_sats, req.addresses) From 8889b0bc3ccee15bc3ad24dd40ca4e92793ad1a3 Mon Sep 17 00:00:00 2001 From: comwanga Date: Sat, 8 Aug 2026 13:48:19 +0300 Subject: [PATCH 6/8] fix: add null guard for escrowAgents map --- frontend/components/engine/payout-modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/engine/payout-modal.tsx b/frontend/components/engine/payout-modal.tsx index 10e7a7a..189f867 100644 --- a/frontend/components/engine/payout-modal.tsx +++ b/frontend/components/engine/payout-modal.tsx @@ -350,7 +350,7 @@ export function PayoutModal({ team, allocationId, open, onOpenChange }: PayoutMo )}

- {escrowAgents.map((agent) => ( + {escrowAgents?.map((agent) => ( diff --git a/frontend/components/engine/payout-modal.tsx b/frontend/components/engine/payout-modal.tsx index 189f867..98205a0 100644 --- a/frontend/components/engine/payout-modal.tsx +++ b/frontend/components/engine/payout-modal.tsx @@ -70,7 +70,6 @@ export function PayoutModal({ team, allocationId, open, onOpenChange }: PayoutMo .then((res) => setEscrowAgents(res.agents)) .catch(() => { toast.error("Could not load escrow agents"); setEscrowAgents([]); }); } - if (!open) setEscrowAgents(null); }, [open, payoutMode, escrowAgents]); const handleOpenChange = (o: boolean) => { @@ -82,6 +81,7 @@ export function PayoutModal({ team, allocationId, open, onOpenChange }: PayoutMo setNwc(""); setPreflight([]); setClaims([]); + setEscrowAgents(null); } onOpenChange(o); }; From cefa5e29a3d170624adbb474d7b471f60e20a225 Mon Sep 17 00:00:00 2001 From: comwanga Date: Sat, 8 Aug 2026 14:09:11 +0300 Subject: [PATCH 8/8] fix: pass escrow_coordinate and escrow_status through _payout_out --- backend/app/api/v1/payouts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/app/api/v1/payouts.py b/backend/app/api/v1/payouts.py index 11f6b15..498c6f7 100644 --- a/backend/app/api/v1/payouts.py +++ b/backend/app/api/v1/payouts.py @@ -38,6 +38,7 @@ def _payout_out(db: Session, payout: Payout) -> PayoutOut: return PayoutOut( id=payout.id, event_id=payout.event_id, allocation_id=payout.allocation_id, team_label=payout.team_label, total_sats=payout.total_sats, status=payout.status, + escrow_coordinate=payout.escrow_coordinate, escrow_status=payout.escrow_status, items=items, )