Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
271 changes: 152 additions & 119 deletions godot/autoloads/WebBridge.gd
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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, {})
12 changes: 10 additions & 2 deletions godot/web/stellar_bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
}
}
Expand All @@ -119,4 +127,4 @@
};

global.HumanVsBotsBridge = bridge;
})(globalThis);
})(globalThis);
Loading