From 3ddf04c3a2f0b6b4dcddcc172a64f3fedf2ecfb9 Mon Sep 17 00:00:00 2001 From: Peolite001 Date: Wed, 1 Jul 2026 01:52:31 +0100 Subject: [PATCH] =?UTF-8?q?feat(#36):=20Godot=20HTML5=20export=20+=20JavaS?= =?UTF-8?q?criptBridge=20=E2=86=92=20Stellar/ZK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Wire WebBridge callbacks to EventBus (wallet_connected, tx_confirmed, proof_generated, web3_error) - Add custom HTML5 export template that loads stellar_bridge.js + stellar_game_service.js before Godot runtime - Add StellarGameService.js stub for wallet connection, commit/reveal TX, and ZK proof generation - Update Makefile to copy all web assets to dist/ on export - Add get_wallet_address(), commit_action(hash), reveal_action(key), export_proof(state) wrappers --- Makefile | 4 +- godot/autoloads/WebBridge.gd | 271 +++++++++++++++++------------- godot/web/stellar_bridge.js | 12 +- godot/web/stellar_game_service.js | 79 +++++++++ godot/web_template/index.html | 91 ++++++++++ 5 files changed, 335 insertions(+), 122 deletions(-) create mode 100644 godot/web/stellar_game_service.js create mode 100644 godot/web_template/index.html diff --git a/Makefile b/Makefile index 81a9a42..2665ab2 100644 --- a/Makefile +++ b/Makefile @@ -19,4 +19,6 @@ godot-export: DIST_PATH="$$(pwd)/dist/index.html"; \ echo "Exporting Godot Web build with $$GODOT_BIN to $$DIST_PATH"; \ "$$GODOT_BIN" --headless --path godot --export-release Web "$$DIST_PATH"; \ - cp godot/web/stellar_bridge.js "$$(dirname "$$DIST_PATH")/stellar_bridge.js" + cp godot/web/stellar_bridge.js "$$(dirname "$$DIST_PATH")/stellar_bridge.js"; \ + cp godot/web/stellar_game_service.js "$$(dirname "$$DIST_PATH")/stellar_game_service.js"; \ + cp godot/web_template/index.html "$$(dirname "$$DIST_PATH")/index.html" \ No newline at end of file diff --git a/godot/autoloads/WebBridge.gd b/godot/autoloads/WebBridge.gd index 5826805..0812d4a 100644 --- a/godot/autoloads/WebBridge.gd +++ b/godot/autoloads/WebBridge.gd @@ -1,5 +1,14 @@ extends Node +## WebBridge — Godot ↔ Browser JavaScript Bridge +## +## Autoload that exchanges asynchronous Stellar/ZK requests with browser +## JavaScript via Godot's JavaScriptBridge. Active only in Web exports. +## Native/editor calls emit bridge_error instead of touching JS. +## +## All responses are routed through EventBus so UI and game logic can +## subscribe to a single signal bus. + signal wallet_connected(address: String) signal wallet_connection_failed(error: String) signal address_received(address: String) @@ -16,153 +25,177 @@ var _bridge = null var _callback_interface = null var _callback_references: Array = [] - func _ready() -> void: - if not OS.has_feature("web"): - return - - JavaScriptBridge.eval( - "globalThis.%s = globalThis.%s || {};" % [CALLBACK_NAMESPACE, CALLBACK_NAMESPACE] - ) - _callback_interface = JavaScriptBridge.get_interface(CALLBACK_NAMESPACE) - _bridge = JavaScriptBridge.get_interface(BRIDGE_INTERFACE) - - if _callback_interface == null: - return - - _register_callback("walletConnected", _on_wallet_connected) - _register_callback("walletConnectionFailed", _on_wallet_connection_failed) - _register_callback("addressReceived", _on_address_received) - _register_callback("commitSubmitted", _on_commit_submitted) - _register_callback("revealSubmitted", _on_reveal_submitted) - _register_callback("proofGenerated", _on_proof_generated) - _register_callback("proofExported", _on_proof_exported) - _register_callback("bridgeError", _on_bridge_error) - + if not OS.has_feature("web"): + return + + # Create the global callback namespace + JavaScriptBridge.eval( + "globalThis.%s = globalThis.%s || {};" % [CALLBACK_NAMESPACE, CALLBACK_NAMESPACE] + ) + _callback_interface = JavaScriptBridge.get_interface(CALLBACK_NAMESPACE) + _bridge = JavaScriptBridge.get_interface(BRIDGE_INTERFACE) + + if _callback_interface == null: + return + + _register_callback("walletConnected", _on_wallet_connected) + _register_callback("walletConnectionFailed", _on_wallet_connection_failed) + _register_callback("addressReceived", _on_address_received) + _register_callback("commitSubmitted", _on_commit_submitted) + _register_callback("revealSubmitted", _on_reveal_submitted) + _register_callback("proofGenerated", _on_proof_generated) + _register_callback("proofExported", _on_proof_exported) + _register_callback("bridgeError", _on_bridge_error) func is_available() -> bool: - return OS.has_feature("web") and _bridge != null and _callback_interface != null + return OS.has_feature("web") and _bridge != null and _callback_interface != null +## ─── Public API ──────────────────────────────────────────────────────────── +## Connect the user's Stellar wallet. +## Emits EventBus.wallet_connected(address) on success. +## Emits EventBus.web3_error(action, message, {}) on failure. func connect_wallet() -> void: - if _ensure_available("connect_wallet"): - _bridge.connectWallet() - - -func get_address() -> void: - if _ensure_available("get_address"): - _bridge.getAddress() - - -func commit_action(payload: Dictionary) -> void: - if _ensure_available("commit_action"): - _bridge.commitAction(JSON.stringify(payload)) - - -func reveal_action(payload: Dictionary) -> void: - if _ensure_available("reveal_action"): - _bridge.revealAction(JSON.stringify(payload)) - - -func generate_proof(payload: Dictionary) -> void: - if _ensure_available("generate_proof"): - _bridge.generateProof(JSON.stringify(payload)) - - -func export_proof(payload: Dictionary) -> void: - if _ensure_available("export_proof"): - _bridge.exportProof(JSON.stringify(payload)) - + if _ensure_available("connect_wallet"): + _bridge.connectWallet() + +## Get the currently connected wallet address. +## Emits EventBus.wallet_connected(address, "") on success. +func get_wallet_address() -> void: + if _ensure_available("get_wallet_address"): + _bridge.getAddress() + +## Commit a hidden action (commit-reveal pattern). +## @param hash: hex commitment hash +## Emits EventBus.tx_confirmed(tx_hash, receipt) on success. +## Emits EventBus.web3_error("commit_action", message, {}) on failure. +func commit_action(hash: String) -> void: + if _ensure_available("commit_action"): + _bridge.commitAction(JSON.stringify({"hash": hash})) + +## Reveal a previously committed action. +## @param key: reveal key / preimage +## Emits EventBus.tx_confirmed(tx_hash, receipt) on success. +## Emits EventBus.web3_error("reveal_action", message, {}) on failure. +func reveal_action(key: String) -> void: + if _ensure_available("reveal_action"): + _bridge.revealAction(JSON.stringify({"key": key})) + +## Generate a ZK proof from game state. +## @param state: Dictionary with proof inputs +## Emits EventBus.proof_generated(proof_id, proof_dict) on success. +## Emits EventBus.web3_error("generate_proof", message, {}) on failure. +func export_proof(state: Dictionary) -> void: + if _ensure_available("export_proof"): + _bridge.generateProof(JSON.stringify(state)) + +## ─── Internal helpers ────────────────────────────────────────────────────── func _register_callback(callback_name: String, callable: Callable) -> void: - var callback = JavaScriptBridge.create_callback(callable) - _callback_references.append(callback) - _callback_interface.set(callback_name, callback) - + var callback = JavaScriptBridge.create_callback(callable) + _callback_references.append(callback) + _callback_interface.set(callback_name, callback) func _ensure_available(action: String) -> bool: - if not OS.has_feature("web"): - _report_error(action, "WebBridge is only available in Godot Web exports.") - return false - if _callback_interface == null: - _report_error(action, "Could not register the browser callback interface.") - return false - if _bridge == null: - _report_error( - action, - "window.%s is unavailable; load stellar_bridge.js before Godot starts." % BRIDGE_INTERFACE - ) - return false - return true - + if not OS.has_feature("web"): + _report_error(action, "WebBridge is only available in Godot Web exports.") + return false + if _callback_interface == null: + _report_error(action, "Could not register the browser callback interface.") + return false + if _bridge == null: + _report_error( + action, + "window.%s is unavailable; load stellar_bridge.js before Godot starts." % BRIDGE_INTERFACE + ) + return false + return true + +## ─── Callback handlers ───────────────────────────────────────────────────── func _on_wallet_connected(arguments: Array) -> void: - var address := _read_string_argument("connect_wallet", arguments) - if not address.is_empty(): - wallet_connected.emit(address) - + var address := _read_string_argument("connect_wallet", arguments) + if not address.is_empty(): + wallet_connected.emit(address) + EventBus.emit_wallet_connected(address, &"stellar") func _on_wallet_connection_failed(arguments: Array) -> void: - var error := _read_string_argument("connect_wallet", arguments) - if not error.is_empty(): - wallet_connection_failed.emit(error) - + var error := _read_string_argument("connect_wallet", arguments) + if not error.is_empty(): + wallet_connection_failed.emit(error) + EventBus.emit_web3_error(&"connect_wallet", error, {}) func _on_address_received(arguments: Array) -> void: - var address := _read_string_argument("get_address", arguments) - if not address.is_empty(): - address_received.emit(address) - + var address := _read_string_argument("get_wallet_address", arguments) + if not address.is_empty(): + address_received.emit(address) + EventBus.emit_wallet_connected(address, &"stellar") func _on_commit_submitted(arguments: Array) -> void: - _emit_json_result("commit_action", commit_submitted, arguments) - + var result = _parse_json_result("commit_action", arguments) + if result != null: + commit_submitted.emit(result) + var tx_hash = result.get("txHash", "") + if not tx_hash.is_empty(): + EventBus.emit_tx_confirmed(tx_hash, result) func _on_reveal_submitted(arguments: Array) -> void: - _emit_json_result("reveal_action", reveal_submitted, arguments) - + var result = _parse_json_result("reveal_action", arguments) + if result != null: + reveal_submitted.emit(result) + var tx_hash = result.get("txHash", "") + if not tx_hash.is_empty(): + EventBus.emit_tx_confirmed(tx_hash, result) func _on_proof_generated(arguments: Array) -> void: - _emit_json_result("generate_proof", proof_generated, arguments) - + var result = _parse_json_result("generate_proof", arguments) + if result != null: + proof_generated.emit(result) + var proof_id = result.get("proofId", "proof_" + str(Time.get_unix_time_from_system())) + EventBus.emit_proof_generated(StringName(proof_id), result) func _on_proof_exported(arguments: Array) -> void: - _emit_json_result("export_proof", proof_exported, arguments) - + var result = _parse_json_result("export_proof", arguments) + if result != null: + proof_exported.emit(result) func _on_bridge_error(arguments: Array) -> void: - if arguments.size() < 2: - _report_error("unknown", "Browser bridge returned an incomplete error callback.") - return - bridge_error.emit(str(arguments[0]), str(arguments[1])) + if arguments.size() < 2: + _report_error("unknown", "Browser bridge returned an incomplete error callback.") + return + var action := str(arguments[0]) + var error := str(arguments[1]) + bridge_error.emit(action, error) + EventBus.emit_web3_error(StringName(action), error, {}) +## ─── Argument parsing ──────────────────────────────────────────────────────── func _read_string_argument(action: String, arguments: Array) -> String: - if arguments.is_empty(): - _report_error(action, "Browser bridge callback did not include a value.") - return "" - var value := str(arguments[0]) - if value.is_empty(): - _report_error(action, "Browser bridge callback returned an empty value.") - return value - - -func _emit_json_result(action: String, target_signal: Signal, arguments: Array) -> void: - if arguments.is_empty() or typeof(arguments[0]) != TYPE_STRING: - _report_error(action, "Browser bridge callback must include a JSON string.") - return - - var json := JSON.new() - var parse_error := json.parse(arguments[0]) - if parse_error != OK: - _report_error( - action, - "Browser bridge returned invalid JSON: %s" % json.get_error_message() - ) - return - target_signal.emit(json.data) - + if arguments.is_empty(): + _report_error(action, "Browser bridge callback did not include a value.") + return "" + var value := str(arguments[0]) + if value.is_empty(): + _report_error(action, "Browser bridge callback returned an empty value.") + return value + +func _parse_json_result(action: String, arguments: Array) -> Variant: + if arguments.is_empty() or typeof(arguments[0]) != TYPE_STRING: + _report_error(action, "Browser bridge callback must include a JSON string.") + return null + + var json := JSON.new() + var parse_error := json.parse(arguments[0]) + if parse_error != OK: + _report_error( + action, + "Browser bridge returned invalid JSON: %s" % json.get_error_message() + ) + return null + return json.data func _report_error(action: String, error: String) -> void: - push_warning("%s: %s" % [action, error]) - bridge_error.emit(action, error) + push_warning("%s: %s" % [action, error]) + bridge_error.emit(action, error) + EventBus.emit_web3_error(StringName(action), error, {}) \ No newline at end of file diff --git a/godot/web/stellar_bridge.js b/godot/web/stellar_bridge.js index 8f7b218..1f522c8 100644 --- a/godot/web/stellar_bridge.js +++ b/godot/web/stellar_bridge.js @@ -7,6 +7,14 @@ * `window.HumanVsBotsBridge.configure(handlers)`. Every handler may return a * value or a Promise. Godot sends action payloads as JSON strings and this * adapter sends result payloads back as JSON strings. + * + * Expected handlers: + * connectWallet() -> string | { address: string } + * getAddress() -> string | { address: string } + * commitAction(payload) -> { txHash: string, ... } + * revealAction(payload) -> { txHash: string, ... } + * generateProof(payload) -> { proof: string, publicInputs: [], proofId: string } + * exportProof(payload) -> { filename: string, ... } */ (function installHumanVsBotsBridge(global) { "use strict"; @@ -99,7 +107,7 @@ notify("addressReceived", address); return result; } catch (error) { - notify("bridgeError", "get_address", errorMessage(error)); + notify("bridgeError", "get_wallet_address", errorMessage(error)); return null; } } @@ -119,4 +127,4 @@ }; global.HumanVsBotsBridge = bridge; -})(globalThis); +})(globalThis); \ No newline at end of file diff --git a/godot/web/stellar_game_service.js b/godot/web/stellar_game_service.js new file mode 100644 index 0000000..2df587f --- /dev/null +++ b/godot/web/stellar_game_service.js @@ -0,0 +1,79 @@ +/** + * StellarGameService — Web3 integration layer for Human vs Bots + * + * Handles wallet connection, transaction submission, and ZK proof + * generation via the Stellar blockchain. + * + * This is a stub interface. Replace with your actual Freighter/Albedo + * wallet integration and ZK circuit prover. + */ +class StellarGameService { + constructor() { + this._address = null; + this._provider = null; + } + + async connectWallet() { + // TODO: Integrate with Freighter, Albedo, or xBull + // Example with Freighter: + // if (!window.freighterApi) throw new Error("Freighter not installed"); + // await window.freighterApi.connect(); + // this._address = await window.freighterApi.getPublicKey(); + // this._provider = "freighter"; + // return { address: this._address, provider: this._provider }; + + // Stub: simulate connection + this._address = "G" + "A".repeat(55); + this._provider = "stub"; + return { address: this._address, provider: this._provider }; + } + + async getAddress() { + return this._address; + } + + async commitAction(hash) { + // TODO: Submit commitment transaction to Stellar contract + // const tx = await buildCommitTx(this._address, hash); + // const result = await submitTx(tx); + // return { hash: result.hash, ledger: result.ledger }; + + // Stub + return { hash: "tx_" + Math.random().toString(36).slice(2), ledger: 12345678 }; + } + + async revealAction(key) { + // TODO: Submit reveal transaction to Stellar contract + // const tx = await buildRevealTx(this._address, key); + // const result = await submitTx(tx); + // return { hash: result.hash, ledger: result.ledger }; + + // Stub + return { hash: "tx_" + Math.random().toString(36).slice(2), ledger: 12345679 }; + } + + async generateProof(state) { + // TODO: Call ZK prover (WASM or JS) with game state + // const proof = await generateZkProof(state); + // return { id: proof.id, proof: proof.data, publicInputs: proof.inputs }; + + // Stub + return { + id: "proof_" + Date.now(), + proof: "0x" + "00".repeat(256), + publicInputs: [state.turn || 0, state.seed || 0], + }; + } + + async exportProof(payload) { + // TODO: Serialize and download proof + const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); + const url = URL.createObjectURL(blob); + return { filename: "proof.json", url }; + } +} + +// Expose globally so the HTML template can instantiate it +if (typeof window !== "undefined") { + window.StellarGameService = StellarGameService; +} \ No newline at end of file diff --git a/godot/web_template/index.html b/godot/web_template/index.html new file mode 100644 index 0000000..f354251 --- /dev/null +++ b/godot/web_template/index.html @@ -0,0 +1,91 @@ + + + + + + Human vs Bots — Web3 Arena + + + +
Loading Human vs Bots...
+
+ + + + + + + + + + + + \ No newline at end of file