From 15ac91c42f19d84a7d304fe25cedc791e60a25f2 Mon Sep 17 00:00:00 2001 From: daddy Date: Tue, 15 Sep 2026 20:48:10 +0000 Subject: [PATCH] feat(godot): add EconomyApiClient with wallet, marketplace, and payout helpers --- scripts/api_client.gd | 5 +- scripts/economy_api_client.gd | 142 +++++++++++++++++++++++++++ scripts/smoke_test_economy_client.gd | 44 +++++++++ 3 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 scripts/economy_api_client.gd create mode 100644 scripts/smoke_test_economy_client.gd diff --git a/scripts/api_client.gd b/scripts/api_client.gd index 3550fed..d5961a7 100644 --- a/scripts/api_client.gd +++ b/scripts/api_client.gd @@ -138,7 +138,8 @@ func request( path: String, body: String = "", content_type: String = "application/json", - require_auth: bool = false + require_auth: bool = false, + custom_headers: PackedStringArray = PackedStringArray() ) -> Dictionary: var http := HTTPRequest.new() http.timeout = timeout_seconds @@ -152,6 +153,8 @@ func request( headers.append("Content-Type: " + content_type) if require_auth and has_token(): headers.append("Authorization: " + bearer_token) + for ch in custom_headers: + headers.append(ch) var url := _base_url + path var err := http.request(url, headers, method, body) diff --git a/scripts/economy_api_client.gd b/scripts/economy_api_client.gd new file mode 100644 index 0000000..e3fc708 --- /dev/null +++ b/scripts/economy_api_client.gd @@ -0,0 +1,142 @@ +extends RefCounted +class_name EconomyApiClient +## Helper class for AGP Economy REST endpoints (Wallet, Payments, Marketplace, Payouts). +## +## docs/planning/economy.md US-6 (task cc09c1428892). + +static func get_api_client(node: Node) -> Node: + if node.has_node("/root/ApiClient"): + return node.get_node("/root/ApiClient") + return null + +static func generate_idempotency_key() -> String: + return "%d-%d" % [Time.get_unix_time_from_system(), randi() % 1000000] + +# ── Wallet & Currency ── + +static func get_wallet(api: Node) -> Dictionary: + return await api.request(HTTPClient.METHOD_GET, "/api/wallet", "", "application/json", true) + +static func get_wallet_transactions(api: Node, limit: int = 50, offset: int = 0) -> Dictionary: + return await api.request(HTTPClient.METHOD_GET, "/api/wallet/transactions?limit=%d&offset=%d" % [limit, offset], "", "application/json", true) + +# ── Premium Purchases ── + +static func purchase_credits(api: Node, credits: int, idempotency_key: String = "") -> Dictionary: + var custom_headers := PackedStringArray() + var key := idempotency_key if idempotency_key != "" else generate_idempotency_key() + custom_headers.append("Idempotency-Key: " + key) + + var sess_res: Dictionary = await api.request( + HTTPClient.METHOD_POST, + "/api/payments/purchase", + JSON.stringify({"credits": credits}), + "application/json", + true, + custom_headers + ) + if not sess_res.ok: + return sess_res + + var sess_id: String = str(sess_res.data.get("session_id", "")) + if sess_id == "": + return {"ok": false, "status": 0, "data": null, "detail": "Invalid session returned"} + + return await api.request( + HTTPClient.METHOD_POST, + "/api/payments/confirm", + JSON.stringify({"session_id": sess_id}), + "application/json", + true + ) + +# ── Catalog Items & Listings ── + +static func get_catalog_items(api: Node, mine: bool = false, item_type: String = "", limit: int = 50, offset: int = 0) -> Dictionary: + var q := "/api/marketplace/items?limit=%d&offset=%d" % [limit, offset] + if mine: + q += "&mine=true" + if item_type != "": + q += "&item_type=" + item_type.uri_encode() + return await api.request(HTTPClient.METHOD_GET, q, "", "application/json", true) + +static func create_catalog_item(api: Node, name: String, item_type: String, description: String = "", metadata_json: Dictionary = {}) -> Dictionary: + var payload := { + "name": name, + "item_type": item_type, + "description": description, + "metadata_json": metadata_json + } + return await api.request(HTTPClient.METHOD_POST, "/api/marketplace/items", JSON.stringify(payload), "application/json", true) + +static func create_listing(api: Node, catalog_item_id: int, price: int) -> Dictionary: + var payload := { + "catalog_item_id": catalog_item_id, + "price": price + } + return await api.request(HTTPClient.METHOD_POST, "/api/marketplace/listings", JSON.stringify(payload), "application/json", true) + +static func search_listings(api: Node, query: String = "", item_type: String = "", min_price: int = -1, max_price: int = -1, limit: int = 50, offset: int = 0) -> Dictionary: + var q := "/api/marketplace/listings?limit=%d&offset=%d" % [limit, offset] + if query != "": + q += "&q=" + query.uri_encode() + if item_type != "": + q += "&item_type=" + item_type.uri_encode() + if min_price >= 0: + q += "&min_price=%d" % min_price + if max_price >= 0: + q += "&max_price=%d" % max_price + return await api.request(HTTPClient.METHOD_GET, q, "", "application/json", false) + +static func get_listing(api: Node, listing_id: int) -> Dictionary: + return await api.request(HTTPClient.METHOD_GET, "/api/marketplace/listings/%d" % listing_id, "", "application/json", false) + +static func update_listing(api: Node, listing_id: int, price: int = -1, status: String = "") -> Dictionary: + var payload := {} + if price > 0: + payload["price"] = price + if status != "": + payload["status"] = status + return await api.request(HTTPClient.METHOD_PATCH, "/api/marketplace/listings/%d" % listing_id, JSON.stringify(payload), "application/json", true) + +static func delist_listing(api: Node, listing_id: int) -> Dictionary: + return await api.request(HTTPClient.METHOD_DELETE, "/api/marketplace/listings/%d" % listing_id, "", "application/json", true) + +# ── Purchases & Escrow ── + +static func purchase_listing(api: Node, listing_id: int, idempotency_key: String = "") -> Dictionary: + var custom_headers := PackedStringArray() + var key := idempotency_key if idempotency_key != "" else generate_idempotency_key() + custom_headers.append("Idempotency-Key: " + key) + + return await api.request( + HTTPClient.METHOD_POST, + "/api/marketplace/listings/%d/purchase" % listing_id, + "", + "application/json", + true, + custom_headers + ) + +static func get_purchases(api: Node, role: String = "all", limit: int = 50, offset: int = 0) -> Dictionary: + return await api.request( + HTTPClient.METHOD_GET, + "/api/marketplace/purchases?role=%s&limit=%d&offset=%d" % [role.uri_encode(), limit, offset], + "", + "application/json", + true + ) + +# ── Creator Balances & Payouts ── + +static func get_creator_balance(api: Node) -> Dictionary: + return await api.request(HTTPClient.METHOD_GET, "/api/marketplace/creator/balance", "", "application/json", true) + +static func request_payout(api: Node, amount: int, external_ref: String = "") -> Dictionary: + var payload := {"amount": amount} + if external_ref != "": + payload["external_ref"] = external_ref + return await api.request(HTTPClient.METHOD_POST, "/api/payouts", JSON.stringify(payload), "application/json", true) + +static func get_payouts(api: Node, limit: int = 50, offset: int = 0) -> Dictionary: + return await api.request(HTTPClient.METHOD_GET, "/api/payouts?limit=%d&offset=%d" % [limit, offset], "", "application/json", true) diff --git a/scripts/smoke_test_economy_client.gd b/scripts/smoke_test_economy_client.gd new file mode 100644 index 0000000..a13c29e --- /dev/null +++ b/scripts/smoke_test_economy_client.gd @@ -0,0 +1,44 @@ +extends SceneTree +## Headless smoke test for EconomyApiClient. + +func _init() -> void: + print("[SMOKE] Starting EconomyApiClient smoke test...") + + var api_script = load("res://scripts/api_client.gd") + if not api_script: + printerr("[FAIL] Could not load api_client.gd") + quit(1) + return + + var api: Node = api_script.new() + root.add_child(api) + + # Verify idempotency key generation + var key1: String = EconomyApiClient.generate_idempotency_key() + var key2: String = EconomyApiClient.generate_idempotency_key() + assert(key1 != "", "Key should not be empty") + assert(key1 != key2, "Keys should be unique") + print("[PASS] Idempotency key generation: ", key1) + + # Verify method existence on EconomyApiClient + var client = EconomyApiClient.new() + assert(client.has_method("get_wallet"), "Missing get_wallet") + assert(client.has_method("get_wallet_transactions"), "Missing get_wallet_transactions") + assert(client.has_method("purchase_credits"), "Missing purchase_credits") + assert(client.has_method("get_catalog_items"), "Missing get_catalog_items") + assert(client.has_method("create_catalog_item"), "Missing create_catalog_item") + assert(client.has_method("create_listing"), "Missing create_listing") + assert(client.has_method("search_listings"), "Missing search_listings") + assert(client.has_method("get_listing"), "Missing get_listing") + assert(client.has_method("update_listing"), "Missing update_listing") + assert(client.has_method("delist_listing"), "Missing delist_listing") + assert(client.has_method("purchase_listing"), "Missing purchase_listing") + assert(client.has_method("get_purchases"), "Missing get_purchases") + assert(client.has_method("get_creator_balance"), "Missing get_creator_balance") + assert(client.has_method("request_payout"), "Missing request_payout") + assert(client.has_method("get_payouts"), "Missing get_payouts") + print("[PASS] All EconomyApiClient methods verified") + + api.queue_free() + print("[SMOKE] EconomyApiClient smoke test PASSED") + quit(0)