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) => (