Skip to content

Commit 5ab4553

Browse files
hardening and cleanup
1 parent 147166e commit 5ab4553

23 files changed

Lines changed: 1714 additions & 552 deletions

__init__.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@
99
from .views import market_town_generic_router
1010
from .views_api import market_town_api_router
1111

12-
market_town_ext: APIRouter = APIRouter(
13-
prefix="/market_town", tags=["Market Town"]
14-
)
12+
market_town_ext: APIRouter = APIRouter(prefix="/market_town", tags=["Market Town"])
1513
market_town_ext.include_router(market_town_generic_router)
1614
market_town_ext.include_router(market_town_api_router)
1715

@@ -35,12 +33,8 @@ def market_town_stop():
3533

3634

3735
def market_town_start():
38-
scheduled_tasks.append(
39-
create_permanent_unique_task("ext_market_town_paid_invoices", wait_for_paid_invoices)
40-
)
41-
scheduled_tasks.append(
42-
create_permanent_unique_task("ext_market_town_scheduler", run_scheduler_loop)
43-
)
36+
scheduled_tasks.append(create_permanent_unique_task("ext_market_town_paid_invoices", wait_for_paid_invoices))
37+
scheduled_tasks.append(create_permanent_unique_task("ext_market_town_scheduler", run_scheduler_loop))
4438

4539

4640
__all__ = [

crud.py

Lines changed: 182 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,10 @@ async def create_agent(
260260
return agent
261261

262262

263+
async def delete_agent(agent_id: str) -> None:
264+
await db.execute("DELETE FROM market_town.agents WHERE id = :agent_id", {"agent_id": agent_id})
265+
266+
263267
async def get_agent(agent_id: str) -> Agent | None:
264268
return await db.fetchone(
265269
"SELECT * FROM market_town.agents WHERE id = :id",
@@ -318,6 +322,13 @@ async def create_business(
318322
return business
319323

320324

325+
async def delete_business(business_id: str) -> None:
326+
await db.execute(
327+
"DELETE FROM market_town.businesses WHERE id = :business_id",
328+
{"business_id": business_id},
329+
)
330+
331+
321332
async def get_business(business_id: str) -> Business | None:
322333
return await db.fetchone(
323334
"SELECT * FROM market_town.businesses WHERE id = :id",
@@ -381,6 +392,33 @@ async def get_epoch(world_id: str, epoch_number: int) -> Epoch | None:
381392
)
382393

383394

395+
async def claim_epoch_for_resolution(world_id: str, epoch_number: int, stale_before: datetime) -> bool:
396+
result = await db.execute(
397+
f"""
398+
UPDATE market_town.epochs
399+
SET status = 'resolving',
400+
updated_at = {db.timestamp_placeholder("now")}
401+
WHERE world_id = :world_id
402+
AND epoch_number = :epoch_number
403+
AND resolved_at IS NULL
404+
AND (
405+
status = 'open'
406+
OR (
407+
status = 'resolving'
408+
AND updated_at <= {db.timestamp_placeholder("stale_before")}
409+
)
410+
)
411+
""",
412+
{
413+
"world_id": world_id,
414+
"epoch_number": epoch_number,
415+
"now": utc_now().timestamp(),
416+
"stale_before": stale_before.timestamp(),
417+
},
418+
)
419+
return bool(result.rowcount)
420+
421+
384422
async def get_latest_epoch(world_id: str) -> Epoch | None:
385423
return await db.fetchone(
386424
"""
@@ -511,6 +549,26 @@ async def create_snapshot(snapshot: BusinessEpochSnapshot) -> BusinessEpochSnaps
511549
return snapshot
512550

513551

552+
async def get_snapshot_for_business_epoch(
553+
world_id: str, epoch_number: int, business_id: str
554+
) -> BusinessEpochSnapshot | None:
555+
return await db.fetchone(
556+
"""
557+
SELECT * FROM market_town.business_epoch_snapshots
558+
WHERE world_id = :world_id
559+
AND epoch_number = :epoch_number
560+
AND business_id = :business_id
561+
LIMIT 1
562+
""",
563+
{
564+
"world_id": world_id,
565+
"epoch_number": epoch_number,
566+
"business_id": business_id,
567+
},
568+
BusinessEpochSnapshot,
569+
)
570+
571+
514572
async def list_snapshots_for_business(business_id: str, limit: int = 10) -> list[BusinessEpochSnapshot]:
515573
return await db.fetchall(
516574
f"""
@@ -528,6 +586,18 @@ async def create_season_result(season_result: SeasonResult) -> SeasonResult:
528586
return season_result
529587

530588

589+
async def get_season_result(world_id: str, season_number: int) -> SeasonResult | None:
590+
return await db.fetchone(
591+
"""
592+
SELECT * FROM market_town.season_results
593+
WHERE world_id = :world_id AND season_number = :season_number
594+
LIMIT 1
595+
""",
596+
{"world_id": world_id, "season_number": season_number},
597+
SeasonResult,
598+
)
599+
600+
531601
async def update_season_result(season_result: SeasonResult) -> SeasonResult:
532602
updated = season_result.copy(update={"updated_at": utc_now()})
533603
await db.update("market_town.season_results", updated)
@@ -553,11 +623,7 @@ async def list_paid_payment_requests_for_season(
553623
*,
554624
include_before_start: bool = False,
555625
) -> list[PaymentRequestRecord]:
556-
lower_clause = (
557-
""
558-
if include_before_start
559-
else f"AND paid_at >= {db.timestamp_placeholder('season_started_at')}"
560-
)
626+
lower_clause = "" if include_before_start else f"AND paid_at >= {db.timestamp_placeholder('season_started_at')}"
561627
return await db.fetchall(
562628
f"""
563629
SELECT * FROM market_town.payment_requests
@@ -602,6 +668,35 @@ async def get_payment_request_by_hash(payment_hash: str) -> PaymentRequestRecord
602668
)
603669

604670

671+
async def claim_payment_request_for_settlement(
672+
payment_hash: str, stale_before: datetime
673+
) -> PaymentRequestRecord | None:
674+
result = await db.execute(
675+
f"""
676+
UPDATE market_town.payment_requests
677+
SET status = 'settling',
678+
updated_at = {db.timestamp_placeholder("now")}
679+
WHERE payment_hash = :payment_hash
680+
AND (
681+
status = 'pending'
682+
OR status = 'expired'
683+
OR (
684+
status = 'settling'
685+
AND updated_at <= {db.timestamp_placeholder("stale_before")}
686+
)
687+
)
688+
""",
689+
{
690+
"payment_hash": payment_hash,
691+
"now": utc_now().timestamp(),
692+
"stale_before": stale_before.timestamp(),
693+
},
694+
)
695+
if not result.rowcount:
696+
return None
697+
return await get_payment_request_by_hash(payment_hash)
698+
699+
605700
async def get_payment_request_by_claim_token(claim_token: str) -> PaymentRequestRecord | None:
606701
return await db.fetchone(
607702
"""
@@ -614,6 +709,41 @@ async def get_payment_request_by_claim_token(claim_token: str) -> PaymentRequest
614709
)
615710

616711

712+
async def claim_payment_request_credentials_reveal(
713+
claim_token: str,
714+
) -> PaymentRequestRecord | None:
715+
result = await db.execute(
716+
f"""
717+
UPDATE market_town.payment_requests
718+
SET credentials_revealed = TRUE,
719+
issued_api_key = NULL,
720+
updated_at = {db.timestamp_placeholder("now")}
721+
WHERE claim_token = :claim_token
722+
AND status = 'paid'
723+
AND credentials_revealed = FALSE
724+
AND agent_id IS NOT NULL
725+
AND business_id IS NOT NULL
726+
""",
727+
{"claim_token": claim_token, "now": utc_now().timestamp()},
728+
)
729+
if not result.rowcount:
730+
return None
731+
return await get_payment_request_by_claim_token(claim_token)
732+
733+
734+
async def reset_payment_request_credentials_reveal(claim_token: str) -> None:
735+
await db.execute(
736+
f"""
737+
UPDATE market_town.payment_requests
738+
SET credentials_revealed = FALSE,
739+
updated_at = {db.timestamp_placeholder("now")}
740+
WHERE claim_token = :claim_token
741+
AND credentials_revealed = TRUE
742+
""",
743+
{"claim_token": claim_token, "now": utc_now().timestamp()},
744+
)
745+
746+
617747
async def list_pending_payment_requests(world_id: str) -> list[PaymentRequestRecord]:
618748
return await db.fetchall(
619749
"""
@@ -626,6 +756,53 @@ async def list_pending_payment_requests(world_id: str) -> list[PaymentRequestRec
626756
)
627757

628758

759+
async def list_active_pending_payment_requests(world_id: str, before_time: datetime) -> list[PaymentRequestRecord]:
760+
now = utc_now()
761+
return await db.fetchall(
762+
f"""
763+
SELECT * FROM market_town.payment_requests
764+
WHERE world_id = :world_id
765+
AND status = 'pending'
766+
AND created_at > {db.timestamp_placeholder("before_time")}
767+
AND (
768+
reservation_expires_at IS NULL
769+
OR reservation_expires_at > {db.timestamp_placeholder("now")}
770+
)
771+
ORDER BY created_at DESC
772+
""",
773+
{
774+
"world_id": world_id,
775+
"before_time": before_time.timestamp(),
776+
"now": now.timestamp(),
777+
},
778+
PaymentRequestRecord,
779+
)
780+
781+
782+
async def expire_pending_payment_requests(world_id: str, before_time: datetime) -> None:
783+
await db.execute(
784+
f"""
785+
UPDATE market_town.payment_requests
786+
SET status = 'expired',
787+
updated_at = {db.timestamp_placeholder("now")}
788+
WHERE world_id = :world_id
789+
AND status = 'pending'
790+
AND (
791+
created_at <= {db.timestamp_placeholder("before_time")}
792+
OR (
793+
reservation_expires_at IS NOT NULL
794+
AND reservation_expires_at <= {db.timestamp_placeholder("now")}
795+
)
796+
)
797+
""",
798+
{
799+
"world_id": world_id,
800+
"now": utc_now().timestamp(),
801+
"before_time": before_time.timestamp(),
802+
},
803+
)
804+
805+
629806
async def update_payment_request(payment_request: PaymentRequestRecord) -> PaymentRequestRecord:
630807
updated = payment_request.copy(update={"updated_at": utc_now()})
631808
await db.update("market_town.payment_requests", updated)

market-town-player/docs/storage-and-secrets.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,14 @@ Logs should be JSON Lines files.
8080
`logs/decisions.jsonl` may contain:
8181

8282
```json
83-
{"epoch":1,"price_sat":220,"restock_units":40,"maintenance_budget_sat":6,"quality_budget_sat":5,"reason":"initial conservative policy"}
83+
{
84+
"epoch": 1,
85+
"price_sat": 220,
86+
"restock_units": 40,
87+
"maintenance_budget_sat": 6,
88+
"quality_budget_sat": 5,
89+
"reason": "initial conservative policy"
90+
}
8491
```
8592

8693
`logs/errors.jsonl` may contain non-secret error details.

migrations.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ async def m001_initial(db):
2626
last_digest_text TEXT,
2727
started_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
2828
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
29-
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
29+
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
30+
UNIQUE (user_id)
3031
);
3132
"""
3233
)
@@ -45,7 +46,8 @@ async def m001_initial(db):
4546
slot_limit INTEGER NOT NULL DEFAULT 10,
4647
config_text TEXT,
4748
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
48-
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
49+
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
50+
UNIQUE (world_id, district_key)
4951
);
5052
"""
5153
)
@@ -64,7 +66,8 @@ async def m001_initial(db):
6466
base_capacity_units INTEGER NOT NULL DEFAULT 20,
6567
config_text TEXT,
6668
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
67-
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
69+
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
70+
UNIQUE (world_id, type_key)
6871
);
6972
"""
7073
)
@@ -81,7 +84,8 @@ async def m001_initial(db):
8184
last_claimed_at TIMESTAMP,
8285
last_opened_at TIMESTAMP,
8386
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
84-
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
87+
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
88+
UNIQUE (world_id, api_key_hash)
8589
);
8690
"""
8791
)
@@ -126,7 +130,8 @@ async def m001_initial(db):
126130
event_summary_text TEXT,
127131
digest_text TEXT,
128132
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
129-
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
133+
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
134+
UNIQUE (world_id, epoch_number)
130135
);
131136
"""
132137
)
@@ -166,7 +171,8 @@ async def m001_initial(db):
166171
reliability_after REAL NOT NULL DEFAULT 0,
167172
quality_before REAL NOT NULL DEFAULT 0,
168173
quality_after REAL NOT NULL DEFAULT 0,
169-
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
174+
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
175+
UNIQUE (world_id, epoch_number, business_id)
170176
);
171177
"""
172178
)
@@ -183,7 +189,8 @@ async def m001_initial(db):
183189
payout_status TEXT NOT NULL DEFAULT 'pending',
184190
payout_summary_text TEXT,
185191
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
186-
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
192+
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
193+
UNIQUE (world_id, season_number)
187194
);
188195
"""
189196
)
@@ -211,7 +218,9 @@ async def m001_initial(db):
211218
credentials_revealed BOOLEAN NOT NULL DEFAULT FALSE,
212219
paid_at TIMESTAMP,
213220
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
214-
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
221+
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
222+
UNIQUE (payment_hash),
223+
UNIQUE (claim_token)
215224
);
216225
"""
217226
)
@@ -228,3 +237,7 @@ async def m001_initial(db):
228237
);
229238
"""
230239
)
240+
241+
242+
async def m002_add_payment_request_reservation_expiry(db):
243+
await db.execute("ALTER TABLE market_town.payment_requests ADD COLUMN reservation_expires_at TIMESTAMP;")

0 commit comments

Comments
 (0)