diff --git a/backend/.cargo/config.toml b/backend/.cargo/config.toml
index 15f9047..a766730 100644
--- a/backend/.cargo/config.toml
+++ b/backend/.cargo/config.toml
@@ -2,4 +2,6 @@
[env]
# Tell SQLx to use offline mode (cached query metadata) for compilation
-SQLX_OFFLINE = "true"
+# NOTE: Commented out because it requires .sqlx cache to be pre-generated
+# To enable offline mode, run: cargo sqlx prepare
+# SQLX_OFFLINE = "true"
diff --git a/backend/Cargo.lock b/backend/Cargo.lock
index 5016cee..cf9a57c 100644
--- a/backend/Cargo.lock
+++ b/backend/Cargo.lock
@@ -79,7 +79,7 @@ dependencies = [
"flate2",
"foldhash",
"futures-core",
- "h2 0.3.27",
+ "h2",
"http 0.2.12",
"httparse",
"httpdate",
@@ -665,16 +665,6 @@ dependencies = [
"version_check",
]
-[[package]]
-name = "core-foundation"
-version = "0.9.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
-dependencies = [
- "core-foundation-sys",
- "libc",
-]
-
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@@ -1027,21 +1017,6 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
-[[package]]
-name = "foreign-types"
-version = "0.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
-dependencies = [
- "foreign-types-shared",
-]
-
-[[package]]
-name = "foreign-types-shared"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
-
[[package]]
name = "form_urlencoded"
version = "1.2.2"
@@ -1187,9 +1162,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
+ "js-sys",
"libc",
"r-efi",
"wasip2",
+ "wasm-bindgen",
]
[[package]]
@@ -1211,25 +1188,6 @@ dependencies = [
"tracing",
]
-[[package]]
-name = "h2"
-version = "0.4.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
-dependencies = [
- "atomic-waker",
- "bytes",
- "fnv",
- "futures-core",
- "futures-sink",
- "http 1.4.0",
- "indexmap",
- "slab",
- "tokio",
- "tokio-util",
- "tracing",
-]
-
[[package]]
name = "hashbrown"
version = "0.12.3"
@@ -1370,7 +1328,6 @@ dependencies = [
"bytes",
"futures-channel",
"futures-core",
- "h2 0.4.13",
"http 1.4.0",
"http-body",
"httparse",
@@ -1396,22 +1353,7 @@ dependencies = [
"tokio",
"tokio-rustls",
"tower-service",
-]
-
-[[package]]
-name = "hyper-tls"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
-dependencies = [
- "bytes",
- "http-body-util",
- "hyper",
- "hyper-util",
- "native-tls",
- "tokio",
- "tokio-native-tls",
- "tower-service",
+ "webpki-roots 1.0.5",
]
[[package]]
@@ -1433,11 +1375,9 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"socket2 0.6.1",
- "system-configuration",
"tokio",
"tower-service",
"tracing",
- "windows-registry",
]
[[package]]
@@ -1711,12 +1651,6 @@ dependencies = [
"vcpkg",
]
-[[package]]
-name = "linux-raw-sys"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
-
[[package]]
name = "litemap"
version = "0.8.1"
@@ -1755,6 +1689,12 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
[[package]]
name = "matchers"
version = "0.2.0"
@@ -1808,23 +1748,6 @@ dependencies = [
"windows-sys 0.61.2",
]
-[[package]]
-name = "native-tls"
-version = "0.2.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
-dependencies = [
- "libc",
- "log",
- "openssl",
- "openssl-probe",
- "openssl-sys",
- "schannel",
- "security-framework",
- "security-framework-sys",
- "tempfile",
-]
-
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
@@ -1902,50 +1825,6 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
-[[package]]
-name = "openssl"
-version = "0.10.75"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
-dependencies = [
- "bitflags",
- "cfg-if",
- "foreign-types",
- "libc",
- "once_cell",
- "openssl-macros",
- "openssl-sys",
-]
-
-[[package]]
-name = "openssl-macros"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.114",
-]
-
-[[package]]
-name = "openssl-probe"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
-
-[[package]]
-name = "openssl-sys"
-version = "0.9.111"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
-dependencies = [
- "cc",
- "libc",
- "pkg-config",
- "vcpkg",
-]
-
[[package]]
name = "parking"
version = "2.2.1"
@@ -2123,6 +2002,61 @@ dependencies = [
"syn 1.0.109",
]
+[[package]]
+name = "quinn"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2 0.5.10",
+ "thiserror 2.0.18",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31"
+dependencies = [
+ "bytes",
+ "getrandom 0.3.4",
+ "lru-slab",
+ "rand 0.9.2",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror 2.0.18",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2 0.5.10",
+ "tracing",
+ "windows-sys 0.52.0",
+]
+
[[package]]
name = "quote"
version = "1.0.43"
@@ -2299,29 +2233,26 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64",
"bytes",
- "encoding_rs",
"futures-core",
- "h2 0.4.13",
"http 1.4.0",
"http-body",
"http-body-util",
"hyper",
"hyper-rustls",
- "hyper-tls",
"hyper-util",
"js-sys",
"log",
- "mime",
- "native-tls",
"percent-encoding",
"pin-project-lite",
+ "quinn",
+ "rustls",
"rustls-pki-types",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"tokio",
- "tokio-native-tls",
+ "tokio-rustls",
"tower",
"tower-http",
"tower-service",
@@ -2329,6 +2260,7 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
+ "webpki-roots 1.0.5",
]
[[package]]
@@ -2410,6 +2342,12 @@ dependencies = [
"serde_json",
]
+[[package]]
+name = "rustc-hash"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
[[package]]
name = "rustc_version"
version = "0.4.1"
@@ -2419,19 +2357,6 @@ dependencies = [
"semver",
]
-[[package]]
-name = "rustix"
-version = "1.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
-dependencies = [
- "bitflags",
- "errno",
- "libc",
- "linux-raw-sys",
- "windows-sys 0.61.2",
-]
-
[[package]]
name = "rustls"
version = "0.23.36"
@@ -2452,6 +2377,7 @@ version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
dependencies = [
+ "web-time",
"zeroize",
]
@@ -2478,15 +2404,6 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
-[[package]]
-name = "schannel"
-version = "0.1.28"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
-dependencies = [
- "windows-sys 0.61.2",
-]
-
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -2499,29 +2416,6 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
-[[package]]
-name = "security-framework"
-version = "2.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
-dependencies = [
- "bitflags",
- "core-foundation",
- "core-foundation-sys",
- "libc",
- "security-framework-sys",
-]
-
-[[package]]
-name = "security-framework-sys"
-version = "2.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
-dependencies = [
- "core-foundation-sys",
- "libc",
-]
-
[[package]]
name = "semver"
version = "1.0.27"
@@ -2993,46 +2887,12 @@ dependencies = [
"syn 2.0.114",
]
-[[package]]
-name = "system-configuration"
-version = "0.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
-dependencies = [
- "bitflags",
- "core-foundation",
- "system-configuration-sys",
-]
-
-[[package]]
-name = "system-configuration-sys"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
-dependencies = [
- "core-foundation-sys",
- "libc",
-]
-
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
-[[package]]
-name = "tempfile"
-version = "3.24.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
-dependencies = [
- "fastrand",
- "getrandom 0.3.4",
- "once_cell",
- "rustix",
- "windows-sys 0.61.2",
-]
-
[[package]]
name = "thiserror"
version = "1.0.69"
@@ -3166,16 +3026,6 @@ dependencies = [
"syn 2.0.114",
]
-[[package]]
-name = "tokio-native-tls"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
-dependencies = [
- "native-tls",
- "tokio",
-]
-
[[package]]
name = "tokio-rustls"
version = "0.26.4"
@@ -3592,6 +3442,16 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
[[package]]
name = "webpki-roots"
version = "0.26.11"
@@ -3661,17 +3521,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
-[[package]]
-name = "windows-registry"
-version = "0.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
-dependencies = [
- "windows-link",
- "windows-result",
- "windows-strings",
-]
-
[[package]]
name = "windows-result"
version = "0.4.1"
diff --git a/backend/Cargo.toml b/backend/Cargo.toml
index df79fed..ccd38ee 100644
--- a/backend/Cargo.toml
+++ b/backend/Cargo.toml
@@ -25,7 +25,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter", "std"] }
sha2 = "0.10"
dotenvy = "0.15"
actix-cors = "0.6"
-reqwest = { version = "0.12", features = ["json"] }
+reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
base64 = "0.22"
ed25519-dalek = { version = "2.1", features = ["rand_core"] }
rand = "0.8"
diff --git a/backend/src/api_error.rs b/backend/src/api_error.rs
index 25c11d6..57d0527 100644
--- a/backend/src/api_error.rs
+++ b/backend/src/api_error.rs
@@ -35,6 +35,37 @@ pub enum ApiError {
ValidationError(String),
}
+// Helper methods for convenience
+impl ApiError {
+ pub fn bad_request(message: impl Into) -> Self {
+ ApiError::BadRequest(message.into())
+ }
+
+ pub fn internal_error(message: impl Into) -> Self {
+ ApiError::InternalServerError
+ }
+
+ pub fn database_error(e: impl Into) -> Self {
+ ApiError::DatabaseError(e.into())
+ }
+
+ pub fn not_found(message: impl Into) -> Self {
+ ApiError::NotFound
+ }
+
+ pub fn unauthorized(message: impl Into) -> Self {
+ ApiError::Unauthorized
+ }
+
+ pub fn forbidden(message: impl Into) -> Self {
+ ApiError::Forbidden
+ }
+
+ pub fn conflict(message: impl Into) -> Self {
+ ApiError::Conflict(message.into())
+ }
+}
+
#[derive(Serialize)]
struct ErrorResponse {
error: String,
@@ -45,16 +76,30 @@ struct ErrorResponse {
impl ResponseError for ApiError {
fn error_response(&self) -> HttpResponse {
let (status, message) = match self {
- ApiError::InternalServerError => (actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
+ ApiError::InternalServerError => (
+ actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
+ self.to_string(),
+ ),
ApiError::BadRequest(_) => (actix_web::http::StatusCode::BAD_REQUEST, self.to_string()),
ApiError::Unauthorized => (actix_web::http::StatusCode::UNAUTHORIZED, self.to_string()),
ApiError::Forbidden => (actix_web::http::StatusCode::FORBIDDEN, self.to_string()),
ApiError::NotFound => (actix_web::http::StatusCode::NOT_FOUND, self.to_string()),
ApiError::Conflict(_) => (actix_web::http::StatusCode::CONFLICT, self.to_string()),
- ApiError::DatabaseError(_) => (actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, "Database error".to_string()),
- ApiError::RedisError(_) => (actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, "Cache error".to_string()),
- ApiError::StellarError(_) => (actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, "Blockchain error".to_string()),
- ApiError::ValidationError(_) => (actix_web::http::StatusCode::BAD_REQUEST, self.to_string()),
+ ApiError::DatabaseError(_) => (
+ actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
+ "Database error".to_string(),
+ ),
+ ApiError::RedisError(_) => (
+ actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
+ "Cache error".to_string(),
+ ),
+ ApiError::StellarError(_) => (
+ actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
+ "Blockchain error".to_string(),
+ ),
+ ApiError::ValidationError(_) => {
+ (actix_web::http::StatusCode::BAD_REQUEST, self.to_string())
+ }
};
let error_response = ErrorResponse {
diff --git a/backend/src/auth/device_service.rs b/backend/src/auth/device_service.rs
index e7a3466..62b3c8d 100644
--- a/backend/src/auth/device_service.rs
+++ b/backend/src/auth/device_service.rs
@@ -153,7 +153,7 @@ impl SecurityMonitor {
let key = format!("device:login:{}", device_id);
let value = if success { "1" } else { "0" };
-
+
redis::cmd("LPUSH")
.arg(&key)
.arg(value)
@@ -281,7 +281,7 @@ impl DeviceService {
/// Generate a device fingerprint from device information
pub fn generate_fingerprint(&self, device_info: &DeviceInfo) -> String {
let mut hasher = Sha256::new();
-
+
// Combine device characteristics
let fingerprint_data = format!(
"{}{}{}{}{}{}{}{}{}",
@@ -289,10 +289,22 @@ impl DeviceService {
device_info.user_agent,
device_info.platform,
device_info.os,
- device_info.browser.as_ref().unwrap_or(&"unknown".to_string()),
- device_info.screen_resolution.as_ref().unwrap_or(&"unknown".to_string()),
- device_info.timezone.as_ref().unwrap_or(&"unknown".to_string()),
- device_info.language.as_ref().unwrap_or(&"unknown".to_string()),
+ device_info
+ .browser
+ .as_ref()
+ .unwrap_or(&"unknown".to_string()),
+ device_info
+ .screen_resolution
+ .as_ref()
+ .unwrap_or(&"unknown".to_string()),
+ device_info
+ .timezone
+ .as_ref()
+ .unwrap_or(&"unknown".to_string()),
+ device_info
+ .language
+ .as_ref()
+ .unwrap_or(&"unknown".to_string()),
device_info.ip_address,
);
@@ -338,7 +350,7 @@ impl DeviceService {
device.updated_at = Utc::now();
sqlx::query(
- "UPDATE devices SET last_seen = $1, is_active = $2, login_count = $3,
+ "UPDATE devices SET last_seen = $1, is_active = $2, login_count = $3,
last_login = $4, ip_address = $5, updated_at = $6 WHERE id = $7",
)
.bind(device.last_seen)
@@ -411,12 +423,10 @@ impl DeviceService {
.await?;
// Get the created device
- let device = sqlx::query_as::<_, Device>(
- "SELECT * FROM devices WHERE id = $1",
- )
- .bind(device_id)
- .fetch_one(&self.db_pool)
- .await?;
+ let device = sqlx::query_as::<_, Device>("SELECT * FROM devices WHERE id = $1")
+ .bind(device_id)
+ .fetch_one(&self.db_pool)
+ .await?;
info!(
device_id = %device_id,
@@ -441,35 +451,27 @@ impl DeviceService {
/// Get a specific device by ID
pub async fn get_device(&self, device_id: Uuid) -> Result {
- let device = sqlx::query_as::<_, Device>(
- "SELECT * FROM devices WHERE id = $1",
- )
- .bind(device_id)
- .fetch_optional(&self.db_pool)
- .await?
- .ok_or(DeviceError::DeviceNotFound)?;
+ let device = sqlx::query_as::<_, Device>("SELECT * FROM devices WHERE id = $1")
+ .bind(device_id)
+ .fetch_optional(&self.db_pool)
+ .await?
+ .ok_or(DeviceError::DeviceNotFound)?;
Ok(device)
}
/// Get device count for a user
async fn get_user_device_count(&self, user_id: Uuid) -> Result {
- let count: (i64,) = sqlx::query_as(
- "SELECT COUNT(*) FROM devices WHERE user_id = $1",
- )
- .bind(user_id)
- .fetch_one(&self.db_pool)
- .await?;
+ let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM devices WHERE user_id = $1")
+ .bind(user_id)
+ .fetch_one(&self.db_pool)
+ .await?;
Ok(count.0)
}
/// Revoke/remove a device
- pub async fn revoke_device(
- &self,
- user_id: Uuid,
- device_id: Uuid,
- ) -> Result<(), DeviceError> {
+ pub async fn revoke_device(&self, user_id: Uuid, device_id: Uuid) -> Result<(), DeviceError> {
// Verify device belongs to user
let device = self.get_device(device_id).await?;
if device.user_id != user_id {
@@ -630,11 +632,7 @@ impl DeviceService {
}
/// Block a device
- pub async fn block_device(
- &self,
- user_id: Uuid,
- device_id: Uuid,
- ) -> Result<(), DeviceError> {
+ pub async fn block_device(&self, user_id: Uuid, device_id: Uuid) -> Result<(), DeviceError> {
let device = self.get_device(device_id).await?;
if device.user_id != user_id {
return Err(DeviceError::DeviceNotFound);
@@ -656,11 +654,7 @@ impl DeviceService {
}
/// Unblock a device
- pub async fn unblock_device(
- &self,
- user_id: Uuid,
- device_id: Uuid,
- ) -> Result<(), DeviceError> {
+ pub async fn unblock_device(&self, user_id: Uuid, device_id: Uuid) -> Result<(), DeviceError> {
let device = self.get_device(device_id).await?;
if device.user_id != user_id {
return Err(DeviceError::DeviceNotFound);
@@ -682,11 +676,7 @@ impl DeviceService {
}
/// Trust a device
- pub async fn trust_device(
- &self,
- user_id: Uuid,
- device_id: Uuid,
- ) -> Result<(), DeviceError> {
+ pub async fn trust_device(&self, user_id: Uuid, device_id: Uuid) -> Result<(), DeviceError> {
let device = self.get_device(device_id).await?;
if device.user_id != user_id {
return Err(DeviceError::DeviceNotFound);
@@ -724,7 +714,10 @@ impl DeviceService {
};
let devices: Vec = if let Some(uid) = user_id {
- sqlx::query_as(query).bind(uid).fetch_all(&self.db_pool).await?
+ sqlx::query_as(query)
+ .bind(uid)
+ .fetch_all(&self.db_pool)
+ .await?
} else {
sqlx::query_as(query).fetch_all(&self.db_pool).await?
};
@@ -732,10 +725,7 @@ impl DeviceService {
let total_devices = devices.len() as i64;
let active_devices = devices.iter().filter(|d| d.is_active).count() as i64;
let blocked_devices = devices.iter().filter(|d| d.is_blocked).count() as i64;
- let suspicious_devices = devices
- .iter()
- .filter(|d| d.failed_login_count > 5)
- .count() as i64;
+ let suspicious_devices = devices.iter().filter(|d| d.failed_login_count > 5).count() as i64;
let mut devices_by_type = HashMap::new();
let mut devices_by_platform = HashMap::new();
@@ -745,7 +735,9 @@ impl DeviceService {
for device in &devices {
let device_type_str = format!("{:?}", device.device_type);
*devices_by_type.entry(device_type_str).or_insert(0) += 1;
- *devices_by_platform.entry(device.platform.clone()).or_insert(0) += 1;
+ *devices_by_platform
+ .entry(device.platform.clone())
+ .or_insert(0) += 1;
if device.last_login.is_some() {
recent_logins += 1;
@@ -767,22 +759,17 @@ impl DeviceService {
/// Clean up inactive devices
pub async fn cleanup_inactive_devices(&self) -> Result {
- let cutoff_date = Utc::now()
- - chrono::Duration::days(self.config.device_inactivity_days as i64);
+ let cutoff_date =
+ Utc::now() - chrono::Duration::days(self.config.device_inactivity_days as i64);
- let result = sqlx::query(
- "DELETE FROM devices WHERE last_seen < $1 AND is_active = false",
- )
- .bind(cutoff_date)
- .execute(&self.db_pool)
- .await?;
+ let result = sqlx::query("DELETE FROM devices WHERE last_seen < $1 AND is_active = false")
+ .bind(cutoff_date)
+ .execute(&self.db_pool)
+ .await?;
let deleted_count = result.rows_affected();
- info!(
- deleted_count = deleted_count,
- "Cleaned up inactive devices"
- );
+ info!(deleted_count = deleted_count, "Cleaned up inactive devices");
Ok(deleted_count)
}
@@ -797,37 +784,54 @@ impl DeviceService {
// Note: This assumes a device_security_alerts table exists
// For now, we'll return alerts from memory or create a simplified version
- let alerts: Vec<(Uuid, Uuid, String, String, String, Option, DateTime)> =
- sqlx::query_as(
- "SELECT device_id, user_id, alert_type, severity, message, details, created_at
- FROM device_security_alerts
- WHERE device_id = $1
- ORDER BY created_at DESC
+ let alerts: Vec<(
+ Uuid,
+ Uuid,
+ String,
+ String,
+ String,
+ Option,
+ DateTime,
+ )> = sqlx::query_as(
+ "SELECT device_id, user_id, alert_type, severity, message, details, created_at
+ FROM device_security_alerts
+ WHERE device_id = $1
+ ORDER BY created_at DESC
LIMIT $2",
- )
- .bind(device_id)
- .bind(limit)
- .fetch_all(&self.db_pool)
- .await?;
+ )
+ .bind(device_id)
+ .bind(limit)
+ .fetch_all(&self.db_pool)
+ .await?;
let security_alerts = alerts
.into_iter()
- .map(|(device_id, user_id, alert_type_str, severity_str, message, details, created_at)| {
- let alert_type: AlertType = serde_json::from_str(&alert_type_str)
- .unwrap_or(AlertType::UnusualActivity);
- let severity: AlertSeverity = serde_json::from_str(&severity_str)
- .unwrap_or(AlertSeverity::Medium);
-
- SecurityAlert {
+ .map(
+ |(
device_id,
user_id,
- alert_type,
- severity,
+ alert_type_str,
+ severity_str,
message,
details,
created_at,
- }
- })
+ )| {
+ let alert_type: AlertType =
+ serde_json::from_str(&alert_type_str).unwrap_or(AlertType::UnusualActivity);
+ let severity: AlertSeverity =
+ serde_json::from_str(&severity_str).unwrap_or(AlertSeverity::Medium);
+
+ SecurityAlert {
+ device_id,
+ user_id,
+ alert_type,
+ severity,
+ message,
+ details,
+ created_at,
+ }
+ },
+ )
.collect();
Ok(security_alerts)
diff --git a/backend/src/auth/jwt_service.rs b/backend/src/auth/jwt_service.rs
index ddf048e..dbcd7d0 100644
--- a/backend/src/auth/jwt_service.rs
+++ b/backend/src/auth/jwt_service.rs
@@ -53,10 +53,10 @@ impl From for JwtError {
/// JWT Claims structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Claims {
- pub sub: String, // Subject (user ID)
- pub exp: i64, // Expiration time
- pub iat: i64, // Issued at
- pub jti: String, // JWT ID (unique token identifier)
+ pub sub: String, // Subject (user ID)
+ pub exp: i64, // Expiration time
+ pub iat: i64, // Issued at
+ pub jti: String, // JWT ID (unique token identifier)
pub token_type: TokenType,
pub device_id: Option,
pub session_id: String,
@@ -248,8 +248,12 @@ impl JwtService {
roles: Vec,
device_id: Option,
) -> Result {
- let access_token = self.generate_access_token(user_id, roles.clone(), device_id.clone()).await?;
- let refresh_token = self.generate_refresh_token(user_id, roles, device_id).await?;
+ let access_token = self
+ .generate_access_token(user_id, roles.clone(), device_id.clone())
+ .await?;
+ let refresh_token = self
+ .generate_refresh_token(user_id, roles, device_id)
+ .await?;
Ok(TokenPair {
access_token,
@@ -323,8 +327,8 @@ impl JwtService {
return Err(JwtError::InvalidToken);
}
- let user_id = Uuid::parse_str(&claims.sub)
- .map_err(|e| JwtError::TokenValidation(e.to_string()))?;
+ let user_id =
+ Uuid::parse_str(&claims.sub).map_err(|e| JwtError::TokenValidation(e.to_string()))?;
// Generate new token pair
let token_pair = self
@@ -354,7 +358,8 @@ impl JwtService {
let blacklist_key = format!("blacklist:{}", claims.jti);
let mut conn = self.redis.clone();
- conn.set_ex(&blacklist_key, reason, exp_duration as u64).await?;
+ conn.set_ex(&blacklist_key, reason, exp_duration as u64)
+ .await?;
// Increment analytics
self.increment_analytics("blacklisted").await?;
@@ -412,8 +417,11 @@ impl JwtService {
// Add to user's active sessions
let user_sessions_key = format!("user_sessions:{}", user_id);
conn.sadd(&user_sessions_key, session_id).await?;
- conn.expire(&user_sessions_key, self.config.refresh_token_expiry.num_seconds() as i64)
- .await?;
+ conn.expire(
+ &user_sessions_key,
+ self.config.refresh_token_expiry.num_seconds() as i64,
+ )
+ .await?;
Ok(())
}
@@ -435,13 +443,13 @@ impl JwtService {
let session_json: Option = conn.get(&session_key).await?;
if let Some(json) = session_json {
- let mut session: SessionData = serde_json::from_str(&json)
- .map_err(|e| JwtError::RedisError(e.to_string()))?;
+ let mut session: SessionData =
+ serde_json::from_str(&json).map_err(|e| JwtError::RedisError(e.to_string()))?;
session.last_activity = Utc::now().timestamp();
- let updated_json = serde_json::to_string(&session)
- .map_err(|e| JwtError::RedisError(e.to_string()))?;
+ let updated_json =
+ serde_json::to_string(&session).map_err(|e| JwtError::RedisError(e.to_string()))?;
conn.set_ex(
&session_key,
diff --git a/backend/src/auth/mod.rs b/backend/src/auth/mod.rs
index 370592b..58f525d 100644
--- a/backend/src/auth/mod.rs
+++ b/backend/src/auth/mod.rs
@@ -1,14 +1,13 @@
+pub mod device_service;
pub mod jwt_service;
pub mod middleware;
+pub use device_service::{
+ AlertSeverity, AlertType, Device, DeviceAnalytics, DeviceConfig, DeviceError, DeviceInfo,
+ DeviceService, DeviceType, SecurityAlert,
+};
pub use jwt_service::{
Claims, JwtConfig, JwtError, JwtService, KeyRotation, SessionData, TokenAnalytics, TokenPair,
TokenType,
};
pub use middleware::AuthMiddleware;
-pub mod device_service;
-
-pub use device_service::{
- Device, DeviceInfo, DeviceService, DeviceError, DeviceType, DeviceConfig,
- SecurityAlert, AlertType, AlertSeverity, DeviceAnalytics,
-};
diff --git a/backend/src/config.rs b/backend/src/config.rs
index 01c8b90..d6da249 100644
--- a/backend/src/config.rs
+++ b/backend/src/config.rs
@@ -121,8 +121,14 @@ impl Config {
soroban_contract_reputation,
soroban_contract_arenax_token,
},
- ai: AiConfig { model_path: ai_model_path },
- server: ServerConfig { port, host, rust_log },
+ ai: AiConfig {
+ model_path: ai_model_path,
+ },
+ server: ServerConfig {
+ port,
+ host,
+ rust_log,
+ },
rate_limit: RateLimitConfig {
requests: rate_limit_requests,
window: rate_limit_window,
diff --git a/backend/src/db.rs b/backend/src/db.rs
index 8fafecc..41f0e4b 100644
--- a/backend/src/db.rs
+++ b/backend/src/db.rs
@@ -1,6 +1,6 @@
-use sqlx::{PgPool, postgres::PgPoolOptions};
-use crate::config::Config;
use crate::api_error::ApiError;
+use crate::config::Config;
+use sqlx::{postgres::PgPoolOptions, PgPool};
pub type DbPool = PgPool;
diff --git a/backend/src/http/health.rs b/backend/src/http/health.rs
index af994ce..1abec97 100644
--- a/backend/src/http/health.rs
+++ b/backend/src/http/health.rs
@@ -1,6 +1,6 @@
-use actix_web::{web, HttpResponse, Result};
-use crate::db::DbPool;
use crate::api_error::ApiError;
+use crate::db::DbPool;
+use actix_web::{web, HttpResponse, Result};
pub async fn health_check(db_pool: web::Data) -> Result {
// Check database
diff --git a/backend/src/http/match_authority_handler.rs b/backend/src/http/match_authority_handler.rs
index 61ddff7..3e4a675 100644
--- a/backend/src/http/match_authority_handler.rs
+++ b/backend/src/http/match_authority_handler.rs
@@ -183,10 +183,7 @@ pub async fn get_match(
info!(match_id = %match_id, "Received get match request");
- let result = state
- .match_authority_service
- .get_match(match_id)
- .await?;
+ let result = state.match_authority_service.get_match(match_id).await?;
Ok(HttpResponse::Ok().json(result))
}
diff --git a/backend/src/http/match_ws_handler.rs b/backend/src/http/match_ws_handler.rs
index 449be4d..8333947 100644
--- a/backend/src/http/match_ws_handler.rs
+++ b/backend/src/http/match_ws_handler.rs
@@ -11,9 +11,13 @@ use uuid::Uuid;
#[serde(tag = "type", rename_all = "snake_case")]
pub enum WsMessage {
/// Subscribe to match updates
- Subscribe { match_id: Uuid },
+ Subscribe {
+ match_id: Uuid,
+ },
/// Unsubscribe from match updates
- Unsubscribe { match_id: Uuid },
+ Unsubscribe {
+ match_id: Uuid,
+ },
/// Match state changed
MatchStateChanged {
match_id: Uuid,
@@ -51,7 +55,9 @@ pub enum WsMessage {
finalized_at: String,
},
/// Error message
- Error { message: String },
+ Error {
+ message: String,
+ },
/// Ping/Pong for keepalive
Ping,
Pong,
@@ -152,7 +158,12 @@ impl MatchWebSocket {
}
/// Broadcast message to this session if subscribed to match
- pub fn broadcast_if_subscribed(&self, match_id: Uuid, message: &WsMessage, ctx: &mut ::Context) {
+ pub fn broadcast_if_subscribed(
+ &self,
+ match_id: Uuid,
+ message: &WsMessage,
+ ctx: &mut ::Context,
+ ) {
if self.subscriptions.contains(&match_id) {
debug!(
session_id = %self.id,
diff --git a/backend/src/http/mod.rs b/backend/src/http/mod.rs
index ab31b49..3924947 100644
--- a/backend/src/http/mod.rs
+++ b/backend/src/http/mod.rs
@@ -2,7 +2,8 @@ pub mod health;
pub mod match_authority_handler;
pub mod match_ws_handler;
pub mod notification_handler;
+
// TODO: Add more HTTP modules as implemented:
-// pub mod tournaments;
-// pub mod matches;
// pub mod auth;
+// pub mod matches;
+// pub mod tournaments;
diff --git a/backend/src/http/notification_handler.rs b/backend/src/http/notification_handler.rs
index a41a502..0d65d0e 100644
--- a/backend/src/http/notification_handler.rs
+++ b/backend/src/http/notification_handler.rs
@@ -86,11 +86,7 @@ pub async fn create_notification(
) -> Result {
let user_id = req.user_id().ok_or(ApiError::Unauthorized)?;
- let typ = body
- .typ
- .as_deref()
- .unwrap_or("info")
- .to_string();
+ let typ = body.typ.as_deref().unwrap_or("info").to_string();
let message = body.message.as_deref().unwrap_or("").to_string();
let row = sqlx::query_as::<_, NotificationRow>(
diff --git a/backend/src/lib.rs b/backend/src/lib.rs
index e149291..68feda9 100644
--- a/backend/src/lib.rs
+++ b/backend/src/lib.rs
@@ -3,8 +3,7 @@ pub mod auth;
pub mod config;
pub mod db;
pub mod http;
+pub mod middleware;
pub mod models;
pub mod service;
pub mod telemetry;
-pub mod middleware;
-pub mod auth;
diff --git a/backend/src/main.rs b/backend/src/main.rs
index 90f860b..3f7097a 100644
--- a/backend/src/main.rs
+++ b/backend/src/main.rs
@@ -2,19 +2,19 @@ use actix_web::{web, App, HttpServer};
use std::io;
use tokio::signal;
-mod config;
-mod db;
mod api_error;
-mod telemetry;
-mod middleware;
mod auth;
+mod config;
+mod db;
mod http;
+mod middleware;
mod service;
+mod telemetry;
use crate::config::Config;
use crate::db::create_pool;
-use crate::telemetry::init_telemetry;
use crate::middleware::cors_middleware;
+use crate::telemetry::init_telemetry;
#[tokio::main]
async fn main() -> io::Result<()> {
@@ -32,7 +32,11 @@ async fn main() -> io::Result<()> {
// Create Redis client (placeholder)
// let redis_client = redis::Client::open(config.redis.url.clone()).unwrap();
- tracing::info!("Starting ArenaX backend server on {}:{}", config.server.host, config.server.port);
+ tracing::info!(
+ "Starting ArenaX backend server on {}:{}",
+ config.server.host,
+ config.server.port
+ );
let server = HttpServer::new(move || {
App::new()
@@ -43,11 +47,26 @@ async fn main() -> io::Result<()> {
.service(
web::scope("/api")
.route("/health", web::get().to(crate::http::health::health_check))
- .route("/notifications", web::get().to(crate::http::notification_handler::get_notifications))
- .route("/notifications", web::post().to(crate::http::notification_handler::create_notification))
- .route("/notifications/read-all", web::patch().to(crate::http::notification_handler::mark_all_read))
- .route("/notifications/{id}/read", web::patch().to(crate::http::notification_handler::mark_notification_read))
- .route("/notifications/{id}", web::delete().to(crate::http::notification_handler::delete_notification))
+ .route(
+ "/notifications",
+ web::get().to(crate::http::notification_handler::get_notifications),
+ )
+ .route(
+ "/notifications",
+ web::post().to(crate::http::notification_handler::create_notification),
+ )
+ .route(
+ "/notifications/read-all",
+ web::patch().to(crate::http::notification_handler::mark_all_read),
+ )
+ .route(
+ "/notifications/{id}/read",
+ web::patch().to(crate::http::notification_handler::mark_notification_read),
+ )
+ .route(
+ "/notifications/{id}",
+ web::delete().to(crate::http::notification_handler::delete_notification),
+ ),
)
})
.bind((config.server.host.clone(), config.server.port))?
@@ -56,7 +75,9 @@ async fn main() -> io::Result<()> {
// Graceful shutdown
let server_handle = server.handle();
tokio::spawn(async move {
- signal::ctrl_c().await.expect("Failed to listen for shutdown signal");
+ signal::ctrl_c()
+ .await
+ .expect("Failed to listen for shutdown signal");
tracing::info!("Shutdown signal received, stopping server...");
server_handle.stop(true).await;
});
diff --git a/backend/src/models/match_authority.rs b/backend/src/models/match_authority.rs
index 76fc251..957b26a 100644
--- a/backend/src/models/match_authority.rs
+++ b/backend/src/models/match_authority.rs
@@ -1,12 +1,15 @@
+use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
-use chrono::{DateTime, Utc};
use uuid::Uuid;
use validator::Validate;
/// Match Authority State - represents the finite state machine states
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, sqlx::Type)]
-#[sqlx(type_name = "match_authority_state", rename_all = "SCREAMING_SNAKE_CASE")]
+#[sqlx(
+ type_name = "match_authority_state",
+ rename_all = "SCREAMING_SNAKE_CASE"
+)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum MatchAuthorityState {
Created,
@@ -242,7 +245,10 @@ mod tests {
#[test]
fn test_valid_next_states() {
let created = MatchAuthorityState::Created;
- assert_eq!(created.valid_next_states(), vec![MatchAuthorityState::Started]);
+ assert_eq!(
+ created.valid_next_states(),
+ vec![MatchAuthorityState::Started]
+ );
let completed = MatchAuthorityState::Completed;
let next_states = completed.valid_next_states();
diff --git a/backend/src/models/match_models.rs b/backend/src/models/match_models.rs
index ed0973b..08db360 100644
--- a/backend/src/models/match_models.rs
+++ b/backend/src/models/match_models.rs
@@ -1,6 +1,6 @@
+use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
-use chrono::{DateTime, Utc};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
@@ -35,7 +35,7 @@ pub struct MatchScore {
pub match_id: Uuid,
pub player_id: Uuid,
pub score: i32,
- pub proof_url: Option, // URL to screenshot/video proof
+ pub proof_url: Option, // URL to screenshot/video proof
pub telemetry_data: Option, // JSON string of game telemetry
pub submitted_at: DateTime,
pub verified: bool,
@@ -225,7 +225,7 @@ pub struct EloResponse {
pub win_rate: f64,
pub win_streak: i32,
pub loss_streak: i32,
- pub rank: Option, // Global rank
+ pub rank: Option, // Global rank
pub percentile: Option, // Top X% of players
}
diff --git a/backend/src/models/mod.rs b/backend/src/models/mod.rs
index c1c5365..38aae89 100644
--- a/backend/src/models/mod.rs
+++ b/backend/src/models/mod.rs
@@ -1,18 +1,19 @@
// Core models
-pub mod user;
-pub mod tournament;
-pub mod match_models;
pub mod match_authority;
-pub mod wallet;
+pub mod match_models;
pub mod reward_settlement;
pub mod stellar_account;
pub mod stellar_transaction;
+pub mod tournament;
+pub mod user;
+pub mod wallet;
// Re-export commonly used types
-pub use user::*;
-pub use tournament::*;
-pub use match_models::*;
pub use match_authority::*;
-pub use wallet::*;
+pub use match_models::*;
+pub use reward_settlement::*;
pub use stellar_account::*;
-pub use stellar_transaction::*;
\ No newline at end of file
+pub use stellar_transaction::*;
+pub use tournament::*;
+pub use user::*;
+pub use wallet::*;
diff --git a/backend/src/models/tournament.rs b/backend/src/models/tournament.rs
index ef4b46d..ef41b62 100644
--- a/backend/src/models/tournament.rs
+++ b/backend/src/models/tournament.rs
@@ -223,7 +223,7 @@ pub struct PrizePool {
#[derive(Debug, Serialize, Deserialize)]
pub struct JoinTournamentRequest {
- pub payment_method: String, // "fiat" or "arenax_token"
+ pub payment_method: String, // "fiat" or "arenax_token"
pub payment_reference: Option, // For fiat payments
}
diff --git a/backend/src/models/user.rs b/backend/src/models/user.rs
index 30cd74d..4478614 100644
--- a/backend/src/models/user.rs
+++ b/backend/src/models/user.rs
@@ -41,4 +41,4 @@ pub struct UserProfile {
pub email: String,
pub is_verified: bool,
pub created_at: DateTime,
-}
\ No newline at end of file
+}
diff --git a/backend/src/models/wallet.rs b/backend/src/models/wallet.rs
index 33af434..761aec4 100644
--- a/backend/src/models/wallet.rs
+++ b/backend/src/models/wallet.rs
@@ -226,4 +226,4 @@ pub struct WithdrawalRequest {
#[validate(length(min = 1, max = 255))]
pub destination: String, // Bank account, Stellar address, etc.
pub payment_method: String,
-}
\ No newline at end of file
+}
diff --git a/backend/src/service/governance_service.rs b/backend/src/service/governance_service.rs
index a85ec37..31adf17 100644
--- a/backend/src/service/governance_service.rs
+++ b/backend/src/service/governance_service.rs
@@ -180,10 +180,9 @@ impl GovernanceService {
.map_err(|e| GovernanceServiceError::SorobanError(e.to_string()))?;
// Store in database
- let execute_after_dt = dto.execute_after.map(|ts| {
- chrono::DateTime::from_timestamp(ts, 0)
- .unwrap_or_else(chrono::Utc::now)
- });
+ let execute_after_dt = dto
+ .execute_after
+ .map(|ts| chrono::DateTime::from_timestamp(ts, 0).unwrap_or_else(chrono::Utc::now));
sqlx::query(
r#"
@@ -255,7 +254,12 @@ impl GovernanceService {
// Submit to chain
let tx_result = self
.soroban
- .invoke(&self.governance_contract_id, "approve", &args, signer_secret)
+ .invoke(
+ &self.governance_contract_id,
+ "approve",
+ &args,
+ signer_secret,
+ )
.await
.map_err(|e| GovernanceServiceError::SorobanError(e.to_string()))?;
@@ -329,7 +333,12 @@ impl GovernanceService {
// Submit to chain
let tx_result = self
.soroban
- .invoke(&self.governance_contract_id, "execute", &args, executor_secret)
+ .invoke(
+ &self.governance_contract_id,
+ "execute",
+ &args,
+ executor_secret,
+ )
.await
.map_err(|e| GovernanceServiceError::SorobanError(e.to_string()))?;
diff --git a/backend/src/service/match_authority_service.rs b/backend/src/service/match_authority_service.rs
index e8ad0c8..f1768d5 100644
--- a/backend/src/service/match_authority_service.rs
+++ b/backend/src/service/match_authority_service.rs
@@ -442,18 +442,12 @@ impl MatchAuthorityService {
// =============================================================================
/// Get match by ID
- pub async fn get_match(
- &self,
- match_id: Uuid,
- ) -> Result {
+ pub async fn get_match(&self, match_id: Uuid) -> Result {
self.get_match_with_transitions(match_id).await
}
/// Get match entity (internal)
- async fn get_match_entity(
- &self,
- match_id: Uuid,
- ) -> Result {
+ async fn get_match_entity(&self, match_id: Uuid) -> Result {
sqlx::query_as!(
MatchAuthorityEntity,
r#"
@@ -541,10 +535,7 @@ impl MatchAuthorityService {
/// Reconcile match state with blockchain
/// Checks if on-chain and off-chain states match
- pub async fn reconcile_match(
- &self,
- match_id: Uuid,
- ) -> Result {
+ pub async fn reconcile_match(&self, match_id: Uuid) -> Result {
let match_entity = self.get_match_entity(match_id).await?;
info!(
@@ -798,10 +789,7 @@ impl MatchAuthorityService {
}
/// Get match state from blockchain
- async fn get_match_state_from_chain(
- &self,
- on_chain_match_id: &str,
- ) -> Result {
+ async fn get_match_state_from_chain(&self, on_chain_match_id: &str) -> Result {
// In a real implementation, this would:
// 1. Query the contract state
// 2. Decode the response
@@ -833,10 +821,7 @@ mod tests {
// Valid transition
assert!(service
- .validate_transition(
- &MatchAuthorityState::Created,
- &MatchAuthorityState::Started
- )
+ .validate_transition(&MatchAuthorityState::Created, &MatchAuthorityState::Started)
.is_ok());
// Invalid transition
diff --git a/backend/src/service/match_service.rs b/backend/src/service/match_service.rs
index f0b8eb7..10af58e 100644
--- a/backend/src/service/match_service.rs
+++ b/backend/src/service/match_service.rs
@@ -1,13 +1,13 @@
-use crate::models::*;
-use crate::db::DbPool;
use crate::api_error::ApiError;
-use sqlx::Row;
-use uuid::Uuid;
+use crate::db::DbPool;
+use crate::models::*;
use chrono::{DateTime, Utc};
-use std::collections::HashMap;
+use redis::Client as RedisClient;
+use sqlx::Row;
use std::cmp::Ordering;
+use std::collections::HashMap;
use std::sync::Arc;
-use redis::Client as RedisClient;
+use uuid::Uuid;
pub struct MatchService {
db_pool: DbPool,
@@ -16,7 +16,7 @@ pub struct MatchService {
impl MatchService {
pub fn new(db_pool: DbPool) -> Self {
- Self {
+ Self {
db_pool,
redis_client: None,
}
@@ -38,7 +38,7 @@ impl MatchService {
round_id: Option,
) -> Result {
let match_id = Uuid::new_v4();
-
+
// Get player Elo ratings
let player1_elo = self.get_user_elo(player1_id, &game_mode).await?;
let player2_elo = if let Some(p2_id) = player2_id {
@@ -78,16 +78,16 @@ impl MatchService {
}
/// Get match details
- pub async fn get_match(&self, match_id: Uuid, user_id: Option) -> Result {
- let match_record = sqlx::query_as!(
- Match,
- "SELECT * FROM matches WHERE id = $1",
- match_id
- )
- .fetch_optional(&self.db_pool)
- .await
- .map_err(|e| ApiError::database_error(e))?
- .ok_or(ApiError::not_found("Match not found"))?;
+ pub async fn get_match(
+ &self,
+ match_id: Uuid,
+ user_id: Option,
+ ) -> Result {
+ let match_record = sqlx::query_as!(Match, "SELECT * FROM matches WHERE id = $1", match_id)
+ .fetch_optional(&self.db_pool)
+ .await
+ .map_err(|e| ApiError::database_error(e))?
+ .ok_or(ApiError::not_found("Match not found"))?;
// Get player information
let player1 = self.get_player_info(match_record.player1_id).await?;
@@ -140,7 +140,7 @@ impl MatchService {
MatchScore,
r#"
INSERT INTO match_scores (
- id, match_id, player_id, score, proof_url, telemetry_data,
+ id, match_id, player_id, score, proof_url, telemetry_data,
submitted_at, verified
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8
@@ -160,7 +160,8 @@ impl MatchService {
.map_err(|e| ApiError::database_error(e))?;
// Update match with score
- self.update_match_score(match_id, user_id, request.score).await?;
+ self.update_match_score(match_id, user_id, request.score)
+ .await?;
// Publish score reported event
self.publish_match_event(serde_json::json!({
@@ -169,7 +170,8 @@ impl MatchService {
"tournament_id": match_record.tournament_id,
"user_id": user_id,
"score": request.score
- })).await?;
+ }))
+ .await?;
// Check if both players have reported scores
let both_reported = self.both_players_reported_scores(match_id).await?;
@@ -189,14 +191,15 @@ impl MatchService {
) -> Result {
// Validate dispute creation
let match_record = self.get_match_by_id(match_id).await?;
- self.validate_dispute_creation(&match_record, user_id).await?;
+ self.validate_dispute_creation(&match_record, user_id)
+ .await?;
// Create dispute record
let dispute = sqlx::query_as!(
MatchDispute,
r#"
INSERT INTO match_disputes (
- id, match_id, disputing_player_id, reason, evidence_urls,
+ id, match_id, disputing_player_id, reason, evidence_urls,
status, created_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7
@@ -206,7 +209,9 @@ impl MatchService {
match_id,
user_id,
request.reason,
- request.evidence_urls.map(|urls| serde_json::to_string(&urls).unwrap_or_default()),
+ request
+ .evidence_urls
+ .map(|urls| serde_json::to_string(&urls).unwrap_or_default()),
DisputeStatus::Pending as _,
Utc::now()
)
@@ -215,7 +220,8 @@ impl MatchService {
.map_err(|e| ApiError::database_error(e))?;
// Update match status to disputed
- self.update_match_status(match_id, MatchStatus::Disputed).await?;
+ self.update_match_status(match_id, MatchStatus::Disputed)
+ .await?;
// Publish dispute event
self.publish_match_event(serde_json::json!({
@@ -224,7 +230,8 @@ impl MatchService {
"tournament_id": match_record.tournament_id,
"user_id": user_id,
"reason": request.reason
- })).await?;
+ }))
+ .await?;
Ok(dispute)
}
@@ -237,17 +244,20 @@ impl MatchService {
) -> Result {
// Check if user is already in queue
if self.is_user_in_queue(user_id, &request.game).await? {
- return Err(ApiError::bad_request("User is already in matchmaking queue"));
+ return Err(ApiError::bad_request(
+ "User is already in matchmaking queue",
+ ));
}
// Get user's current Elo rating
let current_elo = self.get_user_elo(user_id, &request.game).await?;
-
+
// Calculate Elo range for matchmaking
let (min_elo, max_elo) = self.calculate_elo_range(current_elo);
// Set expiration time
- let expires_at = Utc::now() + chrono::Duration::minutes(request.max_wait_time.unwrap_or(10) as i64);
+ let expires_at =
+ Utc::now() + chrono::Duration::minutes(request.max_wait_time.unwrap_or(10) as i64);
// Add to queue
let queue_entry = sqlx::query_as!(
@@ -276,13 +286,17 @@ impl MatchService {
.map_err(|e| ApiError::database_error(e))?;
// Try to find a match immediately
- self.try_matchmaking(&request.game, &request.game_mode).await?;
+ self.try_matchmaking(&request.game, &request.game_mode)
+ .await?;
Ok(queue_entry)
}
/// Get matchmaking status for user
- pub async fn get_matchmaking_status(&self, user_id: Uuid) -> Result {
+ pub async fn get_matchmaking_status(
+ &self,
+ user_id: Uuid,
+ ) -> Result {
let queue_entry = sqlx::query_as!(
MatchmakingQueue,
"SELECT * FROM matchmaking_queue WHERE user_id = $1 AND status = $2",
@@ -296,9 +310,11 @@ impl MatchService {
if let Some(entry) = queue_entry {
// Calculate queue position
let position = self.get_queue_position(user_id, &entry.game).await?;
-
+
// Estimate wait time
- let estimated_wait = self.estimate_wait_time(&entry.game, &entry.game_mode).await?;
+ let estimated_wait = self
+ .estimate_wait_time(&entry.game, &entry.game_mode)
+ .await?;
Ok(MatchmakingStatusResponse {
in_queue: true,
@@ -309,7 +325,7 @@ impl MatchService {
} else {
// Check if user has an active match
let active_match = self.get_user_active_match(user_id).await?;
-
+
Ok(MatchmakingStatusResponse {
in_queue: false,
queue_position: None,
@@ -320,7 +336,11 @@ impl MatchService {
}
/// Get user's Elo rating
- pub async fn get_user_elo_rating(&self, user_id: Uuid, game: &str) -> Result {
+ pub async fn get_user_elo_rating(
+ &self,
+ user_id: Uuid,
+ game: &str,
+ ) -> Result {
let elo_record = sqlx::query_as!(
UserElo,
"SELECT * FROM user_elo WHERE user_id = $1 AND game = $2",
@@ -376,15 +396,11 @@ impl MatchService {
}
async fn get_player_info(&self, user_id: Uuid) -> Result {
- let user = sqlx::query_as!(
- User,
- "SELECT * FROM users WHERE id = $1",
- user_id
- )
- .fetch_optional(&self.db_pool)
- .await
- .map_err(|e| ApiError::database_error(e))?
- .ok_or(ApiError::not_found("User not found"))?;
+ let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", user_id)
+ .fetch_optional(&self.db_pool)
+ .await
+ .map_err(|e| ApiError::database_error(e))?
+ .ok_or(ApiError::not_found("User not found"))?;
// Get user's Elo rating for the game (assuming we have a default game)
let elo_rating = self.get_user_elo(user_id, "default").await?;
@@ -397,7 +413,11 @@ impl MatchService {
})
}
- async fn can_user_report_score(&self, user_id: Option, match_record: &Match) -> Result {
+ async fn can_user_report_score(
+ &self,
+ user_id: Option,
+ match_record: &Match,
+ ) -> Result {
if user_id.is_none() {
return Ok(false);
}
@@ -405,7 +425,12 @@ impl MatchService {
let user_id = user_id.unwrap();
// Check if user is a player in this match
- if user_id != match_record.player1_id && match_record.player2_id.map(|p2| p2 != user_id).unwrap_or(true) {
+ if user_id != match_record.player1_id
+ && match_record
+ .player2_id
+ .map(|p2| p2 != user_id)
+ .unwrap_or(true)
+ {
return Ok(false);
}
@@ -427,7 +452,11 @@ impl MatchService {
Ok(existing_score.is_none())
}
- async fn can_user_dispute_match(&self, user_id: Option, match_record: &Match) -> Result {
+ async fn can_user_dispute_match(
+ &self,
+ user_id: Option,
+ match_record: &Match,
+ ) -> Result {
if user_id.is_none() {
return Ok(false);
}
@@ -435,7 +464,12 @@ impl MatchService {
let user_id = user_id.unwrap();
// Check if user is a player in this match
- if user_id != match_record.player1_id && match_record.player2_id.map(|p2| p2 != user_id).unwrap_or(true) {
+ if user_id != match_record.player1_id
+ && match_record
+ .player2_id
+ .map(|p2| p2 != user_id)
+ .unwrap_or(true)
+ {
return Ok(false);
}
@@ -457,7 +491,10 @@ impl MatchService {
Ok(existing_dispute.is_none())
}
- async fn get_match_dispute_status(&self, match_id: Uuid) -> Result, ApiError> {
+ async fn get_match_dispute_status(
+ &self,
+ match_id: Uuid,
+ ) -> Result , ApiError> {
let dispute = sqlx::query!(
"SELECT status FROM match_disputes WHERE match_id = $1 ORDER BY created_at DESC LIMIT 1",
match_id
@@ -470,20 +507,25 @@ impl MatchService {
}
async fn get_match_by_id(&self, match_id: Uuid) -> Result {
- sqlx::query_as!(
- Match,
- "SELECT * FROM matches WHERE id = $1",
- match_id
- )
- .fetch_optional(&self.db_pool)
- .await
- .map_err(|e| ApiError::database_error(e))?
- .ok_or(ApiError::not_found("Match not found".to_string()))
+ sqlx::query_as!(Match, "SELECT * FROM matches WHERE id = $1", match_id)
+ .fetch_optional(&self.db_pool)
+ .await
+ .map_err(|e| ApiError::database_error(e))?
+ .ok_or(ApiError::not_found("Match not found".to_string()))
}
- async fn validate_score_report(&self, match_record: &Match, user_id: Uuid) -> Result<(), ApiError> {
+ async fn validate_score_report(
+ &self,
+ match_record: &Match,
+ user_id: Uuid,
+ ) -> Result<(), ApiError> {
// Check if user is a player in this match
- if user_id != match_record.player1_id && match_record.player2_id.map(|p2| p2 != user_id).unwrap_or(true) {
+ if user_id != match_record.player1_id
+ && match_record
+ .player2_id
+ .map(|p2| p2 != user_id)
+ .unwrap_or(true)
+ {
return Err(ApiError::forbidden("User is not a player in this match"));
}
@@ -503,13 +545,20 @@ impl MatchService {
.map_err(|e| ApiError::database_error(e))?;
if existing_score.is_some() {
- return Err(ApiError::bad_request("Score already reported for this match"));
+ return Err(ApiError::bad_request(
+ "Score already reported for this match",
+ ));
}
Ok(())
}
- async fn update_match_score(&self, match_id: Uuid, user_id: Uuid, score: i32) -> Result<(), ApiError> {
+ async fn update_match_score(
+ &self,
+ match_id: Uuid,
+ user_id: Uuid,
+ score: i32,
+ ) -> Result<(), ApiError> {
let match_record = self.get_match_by_id(match_id).await?;
if user_id == match_record.player1_id {
@@ -522,7 +571,11 @@ impl MatchService {
.execute(&self.db_pool)
.await
.map_err(|e| ApiError::database_error(e))?;
- } else if match_record.player2_id.map(|p2| p2 == user_id).unwrap_or(false) {
+ } else if match_record
+ .player2_id
+ .map(|p2| p2 == user_id)
+ .unwrap_or(false)
+ {
sqlx::query!(
"UPDATE matches SET player2_score = $1, updated_at = $2 WHERE id = $3",
score,
@@ -539,7 +592,7 @@ impl MatchService {
async fn both_players_reported_scores(&self, match_id: Uuid) -> Result {
let match_record = self.get_match_by_id(match_id).await?;
-
+
let player1_score = sqlx::query!(
"SELECT id FROM match_scores WHERE match_id = $1 AND player_id = $2",
match_id,
@@ -567,14 +620,14 @@ impl MatchService {
async fn process_match_completion(&self, match_id: Uuid) -> Result<(), ApiError> {
let match_record = self.get_match_by_id(match_id).await?;
-
+
// Determine winner
let winner_id = self.determine_winner(&match_record).await?;
-
+
// Update match with winner and completion time
sqlx::query!(
r#"
- UPDATE matches
+ UPDATE matches
SET winner_id = $1, status = $2, completed_at = $3, updated_at = $4
WHERE id = $5
"#,
@@ -602,7 +655,8 @@ impl MatchService {
"winner_id": winner_id,
"player1_score": match_record.player1_score.unwrap_or(0),
"player2_score": match_record.player2_score.unwrap_or(0)
- })).await?;
+ }))
+ .await?;
// Publish global event if it's a ranked match
if match_record.match_type == MatchType::Ranked {
@@ -612,7 +666,8 @@ impl MatchService {
"match_id": match_id,
"game_mode": match_record.game_mode,
"winner_id": winner
- })).await?;
+ }))
+ .await?;
}
}
@@ -630,7 +685,11 @@ impl MatchService {
}
}
- async fn update_elo_ratings(&self, match_record: &Match, winner_id: Option) -> Result<(), ApiError> {
+ async fn update_elo_ratings(
+ &self,
+ match_record: &Match,
+ winner_id: Option,
+ ) -> Result<(), ApiError> {
if match_record.player2_id.is_none() {
return Ok(()); // Bye match, no Elo update needed
}
@@ -665,10 +724,22 @@ impl MatchService {
};
// Update player 1 Elo
- self.update_user_elo(match_record.player1_id, &match_record.game_mode, new_player1_elo, player1_result).await?;
+ self.update_user_elo(
+ match_record.player1_id,
+ &match_record.game_mode,
+ new_player1_elo,
+ player1_result,
+ )
+ .await?;
// Update player 2 Elo
- self.update_user_elo(match_record.player2_id.unwrap(), &match_record.game_mode, new_player2_elo, player2_result).await?;
+ self.update_user_elo(
+ match_record.player2_id.unwrap(),
+ &match_record.game_mode,
+ new_player2_elo,
+ player2_result,
+ )
+ .await?;
// Update match record with new Elo ratings
sqlx::query!(
@@ -700,7 +771,8 @@ impl MatchService {
const K_FACTOR: f64 = 32.0;
// Calculate expected scores
- let expected_player1 = 1.0 / (1.0 + 10.0_f64.powf((player2_elo - player1_elo) as f64 / 400.0));
+ let expected_player1 =
+ 1.0 / (1.0 + 10.0_f64.powf((player2_elo - player1_elo) as f64 / 400.0));
let expected_player2 = 1.0 - expected_player1;
// Determine actual scores
@@ -724,7 +796,13 @@ impl MatchService {
(new_player1_elo, new_player2_elo)
}
- async fn update_user_elo(&self, user_id: Uuid, game: &str, new_elo: i32, result: MatchResult) -> Result<(), ApiError> {
+ async fn update_user_elo(
+ &self,
+ user_id: Uuid,
+ game: &str,
+ new_elo: i32,
+ result: MatchResult,
+ ) -> Result<(), ApiError> {
// Get current Elo record
let current_elo = sqlx::query_as!(
UserElo,
@@ -819,7 +897,11 @@ impl MatchService {
Ok(())
}
- async fn create_elo_history(&self, match_record: &Match, winner_id: Option) -> Result<(), ApiError> {
+ async fn create_elo_history(
+ &self,
+ match_record: &Match,
+ winner_id: Option,
+ ) -> Result<(), ApiError> {
if match_record.player2_id.is_none() {
return Ok(()); // Bye match, no history needed
}
@@ -900,9 +982,18 @@ impl MatchService {
Ok(())
}
- async fn validate_dispute_creation(&self, match_record: &Match, user_id: Uuid) -> Result<(), ApiError> {
+ async fn validate_dispute_creation(
+ &self,
+ match_record: &Match,
+ user_id: Uuid,
+ ) -> Result<(), ApiError> {
// Check if user is a player in this match
- if user_id != match_record.player1_id && match_record.player2_id.map(|p2| p2 != user_id).unwrap_or(true) {
+ if user_id != match_record.player1_id
+ && match_record
+ .player2_id
+ .map(|p2| p2 != user_id)
+ .unwrap_or(true)
+ {
return Err(ApiError::forbidden("User is not a player in this match"));
}
@@ -922,13 +1013,19 @@ impl MatchService {
.map_err(|e| ApiError::database_error(e))?;
if existing_dispute.is_some() {
- return Err(ApiError::bad_request("Dispute already exists for this match"));
+ return Err(ApiError::bad_request(
+ "Dispute already exists for this match",
+ ));
}
Ok(())
}
- async fn update_match_status(&self, match_id: Uuid, status: MatchStatus) -> Result<(), ApiError> {
+ async fn update_match_status(
+ &self,
+ match_id: Uuid,
+ status: MatchStatus,
+ ) -> Result<(), ApiError> {
sqlx::query!(
"UPDATE matches SET status = $1, updated_at = $2 WHERE id = $3",
status as _,
@@ -968,7 +1065,7 @@ impl MatchService {
let candidates = sqlx::query_as!(
MatchmakingQueue,
r#"
- SELECT * FROM matchmaking_queue
+ SELECT * FROM matchmaking_queue
WHERE game = $1 AND game_mode = $2 AND status = $3
ORDER BY joined_at ASC
LIMIT 10
@@ -990,17 +1087,20 @@ impl MatchService {
// Check if Elo ranges overlap
if self.elo_ranges_overlap(player1, player2) {
// Create match
- let match_record = self.create_match(
- player1.user_id,
- Some(player2.user_id),
- MatchType::Ranked,
- game_mode.to_string(),
- None,
- None,
- ).await?;
+ let match_record = self
+ .create_match(
+ player1.user_id,
+ Some(player2.user_id),
+ MatchType::Ranked,
+ game_mode.to_string(),
+ None,
+ None,
+ )
+ .await?;
// Update queue entries
- self.update_queue_entries_to_matched(player1.id, player2.id, match_record.id).await?;
+ self.update_queue_entries_to_matched(player1.id, player2.id, match_record.id)
+ .await?;
// Only create one match per call
return Ok(());
@@ -1015,7 +1115,12 @@ impl MatchService {
player1.min_elo <= player2.max_elo && player2.min_elo <= player1.max_elo
}
- async fn update_queue_entries_to_matched(&self, player1_queue_id: Uuid, player2_queue_id: Uuid, match_id: Uuid) -> Result<(), ApiError> {
+ async fn update_queue_entries_to_matched(
+ &self,
+ player1_queue_id: Uuid,
+ player2_queue_id: Uuid,
+ match_id: Uuid,
+ ) -> Result<(), ApiError> {
// Update player 1
sqlx::query!(
"UPDATE matchmaking_queue SET status = $1, matched_at = $2, match_id = $3 WHERE id = $4",
@@ -1046,7 +1151,7 @@ impl MatchService {
async fn get_queue_position(&self, user_id: Uuid, game: &str) -> Result {
let position = sqlx::query!(
r#"
- SELECT COUNT(*) as position FROM matchmaking_queue
+ SELECT COUNT(*) as position FROM matchmaking_queue
WHERE game = $1 AND status = $2 AND joined_at < (
SELECT joined_at FROM matchmaking_queue WHERE user_id = $3 AND game = $1 AND status = $2
)
@@ -1082,14 +1187,17 @@ impl MatchService {
Ok((queue_size as i32) * 120)
}
- async fn get_user_active_match(&self, user_id: Uuid) -> Result, ApiError> {
+ async fn get_user_active_match(
+ &self,
+ user_id: Uuid,
+ ) -> Result , ApiError> {
let match_record = sqlx::query_as!(
Match,
r#"
- SELECT m.* FROM matches m
- WHERE (m.player1_id = $1 OR m.player2_id = $1)
+ SELECT m.* FROM matches m
+ WHERE (m.player1_id = $1 OR m.player2_id = $1)
AND m.status IN ($2, $3)
- ORDER BY m.created_at DESC
+ ORDER BY m.created_at DESC
LIMIT 1
"#,
user_id,
@@ -1107,7 +1215,11 @@ impl MatchService {
}
}
- async fn calculate_rank_and_percentile(&self, user_id: Uuid, game: &str) -> Result<(Option, Option), ApiError> {
+ async fn calculate_rank_and_percentile(
+ &self,
+ user_id: Uuid,
+ game: &str,
+ ) -> Result<(Option, Option), ApiError> {
// Get user's current rating
let user_rating = self.get_user_elo(user_id, game).await?;
@@ -1139,7 +1251,8 @@ impl MatchService {
}
let rank = higher_rated_count as i32 + 1;
- let percentile = ((total_players - higher_rated_count) as f64 / total_players as f64) * 100.0;
+ let percentile =
+ ((total_players - higher_rated_count) as f64 / total_players as f64) * 100.0;
Ok((Some(rank), Some(percentile)))
}
@@ -1168,15 +1281,15 @@ impl MatchService {
game: Option,
) -> Result {
let offset = (page - 1) * per_page;
-
+
let matches = sqlx::query_as!(
Match,
r#"
- SELECT * FROM matches
- WHERE (player1_id = $1 OR player2_id = $1)
+ SELECT * FROM matches
+ WHERE (player1_id = $1 OR player2_id = $1)
AND ($2::text IS NULL OR game_mode = $2)
AND status = $3
- ORDER BY completed_at DESC
+ ORDER BY completed_at DESC
LIMIT $4 OFFSET $5
"#,
user_id,
@@ -1192,8 +1305,8 @@ impl MatchService {
// Get total count
let total = sqlx::query!(
r#"
- SELECT COUNT(*) as count FROM matches
- WHERE (player1_id = $1 OR player2_id = $1)
+ SELECT COUNT(*) as count FROM matches
+ WHERE (player1_id = $1 OR player2_id = $1)
AND ($2::text IS NULL OR game_mode = $2)
AND status = $3
"#,
@@ -1255,10 +1368,10 @@ impl MatchService {
per_page: i32,
) -> Result {
let offset = (page - 1) * per_page;
-
+
let rankings = sqlx::query!(
r#"
- SELECT ue.*, u.username, u.avatar_url
+ SELECT ue.*, u.username, u.avatar_url
FROM user_elo ue
JOIN users u ON ue.user_id = u.id
WHERE ue.game = $1
@@ -1297,7 +1410,10 @@ impl MatchService {
leaderboard_entries.push(LeaderboardEntry {
rank: rank as i32,
user_id: ranking.user_id,
- username: ranking.username.clone().unwrap_or_else(|| "Unknown".to_string()),
+ username: ranking
+ .username
+ .clone()
+ .unwrap_or_else(|| "Unknown".to_string()),
avatar_url: ranking.avatar_url.clone(),
current_rating: ranking.current_rating,
peak_rating: ranking.peak_rating,
@@ -1432,7 +1548,7 @@ impl MatchService {
let dispute = sqlx::query_as!(
MatchDispute,
r#"
- UPDATE match_disputes
+ UPDATE match_disputes
SET status = $1, admin_reviewer_id = $2, resolution = $3, resolved_at = $4
WHERE id = $5
RETURNING *
@@ -1461,7 +1577,8 @@ impl MatchService {
// Recalculate Elo ratings with new winner
let match_record = self.get_match_by_id(dispute.match_id).await?;
- self.update_elo_ratings(&match_record, Some(new_winner)).await?;
+ self.update_elo_ratings(&match_record, Some(new_winner))
+ .await?;
}
tracing::info!("Dispute {} resolved by admin {}", dispute_id, admin_id);
@@ -1478,7 +1595,7 @@ impl MatchService {
let dispute = sqlx::query_as!(
MatchDispute,
r#"
- UPDATE match_disputes
+ UPDATE match_disputes
SET status = $1, admin_reviewer_id = $2, admin_notes = $3, resolved_at = $4
WHERE id = $5
RETURNING *
@@ -1502,13 +1619,15 @@ impl MatchService {
let match_record = self.get_match_by_id(match_id).await?;
if match_record.status != MatchStatus::Scheduled {
- return Err(ApiError::bad_request("Match cannot be started from current status".to_string()));
+ return Err(ApiError::bad_request(
+ "Match cannot be started from current status".to_string(),
+ ));
}
let updated_match = sqlx::query_as!(
Match,
r#"
- UPDATE matches
+ UPDATE matches
SET status = $1, started_at = $2, updated_at = $3
WHERE id = $4
RETURNING *
@@ -1527,7 +1646,8 @@ impl MatchService {
"type": "started",
"match_id": match_id,
"tournament_id": match_record.tournament_id
- })).await?;
+ }))
+ .await?;
Ok(updated_match)
}
@@ -1541,17 +1661,21 @@ impl MatchService {
let match_record = self.get_match_by_id(match_id).await?;
if match_record.status != MatchStatus::Pending {
- return Err(ApiError::bad_request("Match cannot be scheduled from current status".to_string()));
+ return Err(ApiError::bad_request(
+ "Match cannot be scheduled from current status".to_string(),
+ ));
}
if scheduled_time <= Utc::now() {
- return Err(ApiError::bad_request("Scheduled time must be in the future".to_string()));
+ return Err(ApiError::bad_request(
+ "Scheduled time must be in the future".to_string(),
+ ));
}
let updated_match = sqlx::query_as!(
Match,
r#"
- UPDATE matches
+ UPDATE matches
SET status = $1, scheduled_time = $2, updated_at = $3
WHERE id = $4
RETURNING *
@@ -1571,7 +1695,8 @@ impl MatchService {
"match_id": match_id,
"tournament_id": match_record.tournament_id,
"scheduled_time": scheduled_time
- })).await?;
+ }))
+ .await?;
Ok(updated_match)
}
@@ -1584,14 +1709,18 @@ impl MatchService {
) -> Result {
let match_record = self.get_match_by_id(match_id).await?;
- if match_record.status == MatchStatus::Completed || match_record.status == MatchStatus::Cancelled {
- return Err(ApiError::bad_request("Cannot cancel a completed or already cancelled match".to_string()));
+ if match_record.status == MatchStatus::Completed
+ || match_record.status == MatchStatus::Cancelled
+ {
+ return Err(ApiError::bad_request(
+ "Cannot cancel a completed or already cancelled match".to_string(),
+ ));
}
let updated_match = sqlx::query_as!(
Match,
r#"
- UPDATE matches
+ UPDATE matches
SET status = $1, updated_at = $2
WHERE id = $3
RETURNING *
@@ -1612,7 +1741,7 @@ impl MatchService {
pub async fn cleanup_expired_queue_entries(&self) -> Result {
let result = sqlx::query!(
r#"
- UPDATE matchmaking_queue
+ UPDATE matchmaking_queue
SET status = $1
WHERE status = $2 AND expires_at < $3
"#,
@@ -1668,7 +1797,7 @@ impl MatchService {
let disputes = sqlx::query_as!(
MatchDispute,
r#"
- SELECT * FROM match_disputes
+ SELECT * FROM match_disputes
WHERE status = $1
ORDER BY created_at ASC
LIMIT $2 OFFSET $3
@@ -1728,7 +1857,8 @@ impl MatchService {
};
// Calculate expected scores
- let expected_player1 = 1.0 / (1.0 + 10.0_f64.powf((player2_elo - player1_elo) as f64 / 400.0));
+ let expected_player1 =
+ 1.0 / (1.0 + 10.0_f64.powf((player2_elo - player1_elo) as f64 / 400.0));
let expected_player2 = 1.0 - expected_player1;
// Determine actual scores
@@ -1746,8 +1876,10 @@ impl MatchService {
};
// Calculate new ratings with adjusted K-factors
- let new_player1_elo = (player1_elo as f64 + k_factor * (actual_player1 - expected_player1)).round() as i32;
- let new_player2_elo = (player2_elo as f64 + k_factor_p2 * (actual_player2 - expected_player2)).round() as i32;
+ let new_player1_elo =
+ (player1_elo as f64 + k_factor * (actual_player1 - expected_player1)).round() as i32;
+ let new_player2_elo =
+ (player2_elo as f64 + k_factor_p2 * (actual_player2 - expected_player2)).round() as i32;
// Ensure ratings don't go below 100 or above 3000
let new_player1_elo = new_player1_elo.max(100).min(3000);
diff --git a/backend/src/service/mod.rs b/backend/src/service/mod.rs
index 9c8f701..7898457 100644
--- a/backend/src/service/mod.rs
+++ b/backend/src/service/mod.rs
@@ -1,17 +1,23 @@
// Service layer module for ArenaX
-pub mod tournament_service;
-pub mod match_service;
+pub mod governance_service;
pub mod match_authority_service;
-pub mod wallet_service;
+pub mod match_service;
pub mod reward_settlement_service;
-pub mod stellar_service;
pub mod soroban_service;
-pub mod governance_service;
+pub mod stellar_service;
+pub mod tournament_service;
+pub mod wallet_service;
-pub use tournament_service::TournamentService;
-pub use match_service::MatchService;
+pub use governance_service::{
+ CreateProposalDto, GovernanceService, GovernanceServiceError, ProposalRecord,
+ ProposalStatus as GovProposalStatus,
+};
pub use match_authority_service::MatchAuthorityService;
-pub use wallet_service::WalletService;
+pub use match_service::MatchService;
+pub use soroban_service::{
+ DecodedEvent, NetworkConfig, RetryConfig, SorobanError, SorobanService, SorobanTxResult,
+ TxStatus,
+};
pub use stellar_service::StellarService;
-pub use soroban_service::{SorobanService, SorobanTxResult, NetworkConfig, TxStatus, DecodedEvent, RetryConfig, SorobanError};
-pub use governance_service::{GovernanceService, GovernanceServiceError, CreateProposalDto, ProposalRecord, ProposalStatus as GovProposalStatus};
+pub use tournament_service::TournamentService;
+pub use wallet_service::WalletService;
diff --git a/backend/src/service/reward_settlement_service.rs b/backend/src/service/reward_settlement_service.rs
index 7cd655b..1c43e77 100644
--- a/backend/src/service/reward_settlement_service.rs
+++ b/backend/src/service/reward_settlement_service.rs
@@ -117,7 +117,10 @@ impl RewardSettlementService {
}
/// Get existing settlement by match ID.
- pub async fn get_settlement(&self, match_id: &str) -> Result, ApiError> {
+ pub async fn get_settlement(
+ &self,
+ match_id: &str,
+ ) -> Result , ApiError> {
let settlements = SETTLEMENTS
.read()
.map_err(|_| ApiError::internal_error("Failed to read settlements"))?;
@@ -170,8 +173,11 @@ mod tests {
#[tokio::test]
async fn test_idempotent_settlement() {
let service = create_test_service();
- let match_id = format!("test_match_{}", Utc::now().timestamp_nanos_opt().unwrap_or(0));
-
+ let match_id = format!(
+ "test_match_{}",
+ Utc::now().timestamp_nanos_opt().unwrap_or(0)
+ );
+
// First settlement
let result1 = service
.settle_match_reward(
@@ -201,8 +207,11 @@ mod tests {
#[tokio::test]
async fn test_settlement_persisted() {
let service = create_test_service();
- let match_id = format!("persist_test_{}", Utc::now().timestamp_nanos_opt().unwrap_or(0));
-
+ let match_id = format!(
+ "persist_test_{}",
+ Utc::now().timestamp_nanos_opt().unwrap_or(0)
+ );
+
service
.settle_match_reward(
match_id.clone(),
diff --git a/backend/src/service/soroban_service.rs b/backend/src/service/soroban_service.rs
index f74d869..e14f7f4 100644
--- a/backend/src/service/soroban_service.rs
+++ b/backend/src/service/soroban_service.rs
@@ -150,12 +150,8 @@ struct RpcResponse {
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum RpcResult {
- Success {
- result: serde_json::Value,
- },
- Error {
- error: RpcError,
- },
+ Success { result: serde_json::Value },
+ Error { error: RpcError },
}
#[derive(Debug, Deserialize)]
@@ -330,9 +326,7 @@ impl SorobanService {
"transaction": tx_envelope
});
- let response: SimulateResponse = self
- .rpc_call("simulateTransaction", params)
- .await?;
+ let response: SimulateResponse = self.rpc_call("simulateTransaction", params).await?;
Ok(response)
}
@@ -378,8 +372,7 @@ impl SorobanService {
// Encode to base64 XDR (simplified)
use base64::{engine::general_purpose, Engine as _};
- let xdr = general_purpose::STANDARD
- .encode(serde_json::to_string(&signed_tx)?);
+ let xdr = general_purpose::STANDARD.encode(serde_json::to_string(&signed_tx)?);
Ok(xdr)
}
@@ -390,9 +383,7 @@ impl SorobanService {
"transaction": signed_tx
});
- let response: SendTransactionResponse = self
- .rpc_call("sendTransaction", params)
- .await?;
+ let response: SendTransactionResponse = self.rpc_call("sendTransaction", params).await?;
info!(tx_hash = response.hash, "Transaction submitted");
@@ -400,10 +391,7 @@ impl SorobanService {
}
/// Monitor a transaction until it completes or fails
- async fn monitor_transaction(
- &self,
- tx_hash: &str,
- ) -> Result {
+ async fn monitor_transaction(&self, tx_hash: &str) -> Result {
let mut attempt = 0;
let mut delay = self.retry_config.initial_delay_ms;
@@ -484,25 +472,18 @@ impl SorobanService {
"hash": tx_hash
});
- let response: GetTransactionResponse = self
- .rpc_call("getTransaction", params)
- .await?;
+ let response: GetTransactionResponse = self.rpc_call("getTransaction", params).await?;
Ok(response.status)
}
/// Decode events from a transaction
- pub async fn decode_events(
- &self,
- tx_hash: &str,
- ) -> Result, SorobanError> {
+ pub async fn decode_events(&self, tx_hash: &str) -> Result, SorobanError> {
let params = serde_json::json!({
"hash": tx_hash
});
- let response: GetTransactionResponse = self
- .rpc_call("getTransaction", params)
- .await?;
+ let response: GetTransactionResponse = self.rpc_call("getTransaction", params).await?;
// Decode events from resultMetaXdr
// In production, properly decode XDR to extract events
@@ -530,11 +511,7 @@ impl SorobanService {
}
/// Make an RPC call to the Soroban RPC endpoint
- async fn rpc_call(
- &self,
- method: &str,
- params: serde_json::Value,
- ) -> Result
+ async fn rpc_call(&self, method: &str, params: serde_json::Value) -> Result
where
T: for<'de> Deserialize<'de>,
{
@@ -556,10 +533,7 @@ impl SorobanService {
let text = response.text().await?;
if !status.is_success() {
- return Err(SorobanError::RpcError(format!(
- "HTTP {}: {}",
- status, text
- )));
+ return Err(SorobanError::RpcError(format!("HTTP {}: {}", status, text)));
}
let rpc_response: RpcResponse = serde_json::from_str(&text)?;
@@ -590,8 +564,7 @@ impl SorobanService {
});
use base64::{engine::general_purpose, Engine as _};
- Ok(general_purpose::STANDARD
- .encode(serde_json::to_string(&envelope)?))
+ Ok(general_purpose::STANDARD.encode(serde_json::to_string(&envelope)?))
}
/// Extract public key from secret key
@@ -687,7 +660,8 @@ mod tests {
let service = SorobanService::new(network);
// Valid secret key format
- let result = service.secret_to_public_key("SAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
+ let result =
+ service.secret_to_public_key("SAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
assert!(result.is_ok());
// Invalid secret key format
@@ -839,7 +813,10 @@ mod tests {
"operation": "invoke"
});
- let result = service.sign_transaction(&tx_data, "SAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
+ let result = service.sign_transaction(
+ &tx_data,
+ "SAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+ );
assert!(result.is_ok());
let signature = result.unwrap();
assert!(signature.starts_with("sig_"));
diff --git a/backend/src/service/stellar_service.rs b/backend/src/service/stellar_service.rs
index 5b1fda6..60dd421 100644
--- a/backend/src/service/stellar_service.rs
+++ b/backend/src/service/stellar_service.rs
@@ -141,7 +141,11 @@ impl StellarService {
}
/// Fund a Stellar account (for testnet only)
- pub async fn fund_account(&self, public_key: &str, amount: i64) -> Result {
+ pub async fn fund_account(
+ &self,
+ public_key: &str,
+ amount: i64,
+ ) -> Result {
// TODO: Implement actual Stellar funding
// For testnet, you can use the friendbot
// For mainnet, this would require transferring XLM from the admin account
@@ -473,8 +477,14 @@ impl StellarService {
// Ok((keypair.public_key(), keypair.secret_key()))
// Placeholder implementation
- let public_key = format!("G{}", Uuid::new_v4().to_string().replace("-", "").to_uppercase());
- let secret_key = format!("S{}", Uuid::new_v4().to_string().replace("-", "").to_uppercase());
+ let public_key = format!(
+ "G{}",
+ Uuid::new_v4().to_string().replace("-", "").to_uppercase()
+ );
+ let secret_key = format!(
+ "S{}",
+ Uuid::new_v4().to_string().replace("-", "").to_uppercase()
+ );
Ok((public_key, secret_key))
}
diff --git a/backend/src/service/tournament_service.rs b/backend/src/service/tournament_service.rs
index ac38853..4440748 100644
--- a/backend/src/service/tournament_service.rs
+++ b/backend/src/service/tournament_service.rs
@@ -1,13 +1,13 @@
-use crate::models::*;
-use crate::db::DbPool;
use crate::api_error::ApiError;
-use sqlx::Row;
-use uuid::Uuid;
+use crate::db::DbPool;
+use crate::models::*;
use chrono::{DateTime, Utc};
+use redis::Client as RedisClient;
+use serde::{Deserialize, Serialize};
+use sqlx::Row;
use std::collections::HashMap;
use std::sync::Arc;
-use redis::Client as RedisClient;
-use serde::{Serialize, Deserialize};
+use uuid::Uuid;
pub struct TournamentService {
db_pool: DbPool,
@@ -73,7 +73,8 @@ impl TournamentService {
.map_err(|e| ApiError::database_error(e))?;
// Create prize pool record
- self.create_prize_pool(&tournament.id, &request.entry_fee_currency).await?;
+ self.create_prize_pool(&tournament.id, &request.entry_fee_currency)
+ .await?;
// Publish tournament created event
self.publish_tournament_event(serde_json::json!({
@@ -82,7 +83,8 @@ impl TournamentService {
"name": tournament.name.clone(),
"game": tournament.game.clone(),
"max_participants": tournament.max_participants,
- })).await?;
+ }))
+ .await?;
// Publish global event
self.publish_global_event(serde_json::json!({
@@ -90,7 +92,8 @@ impl TournamentService {
"tournament_id": tournament.id,
"name": tournament.name.clone(),
"game": tournament.game.clone(),
- })).await?;
+ }))
+ .await?;
Ok(tournament)
}
@@ -105,11 +108,11 @@ impl TournamentService {
game_filter: Option,
) -> Result {
let offset = (page - 1) * per_page;
-
+
let mut query = String::from(
- "SELECT t.*, COUNT(tp.id) as current_participants FROM tournaments t
- LEFT JOIN tournament_participants tp ON t.id = tp.tournament_id
- WHERE 1=1"
+ "SELECT t.*, COUNT(tp.id) as current_participants FROM tournaments t
+ LEFT JOIN tournament_participants tp ON t.id = tp.tournament_id
+ WHERE 1=1",
);
let mut params: Vec + Send + Sync>> = Vec::new();
let mut param_count = 0;
@@ -127,11 +130,11 @@ impl TournamentService {
}
query.push_str(" GROUP BY t.id ORDER BY t.created_at DESC");
-
+
param_count += 1;
query.push_str(&format!(" LIMIT ${}", param_count));
params.push(Box::new(per_page));
-
+
param_count += 1;
query.push_str(&format!(" OFFSET ${}", param_count));
params.push(Box::new(offset));
@@ -139,13 +142,13 @@ impl TournamentService {
// For now, we'll use a simpler approach with sqlx::query
let tournaments = sqlx::query!(
r#"
- SELECT t.*, COUNT(tp.id) as current_participants
- FROM tournaments t
- LEFT JOIN tournament_participants tp ON t.id = tp.tournament_id
+ SELECT t.*, COUNT(tp.id) as current_participants
+ FROM tournaments t
+ LEFT JOIN tournament_participants tp ON t.id = tp.tournament_id
WHERE ($1::text IS NULL OR t.status = $1::tournament_status)
AND ($2::text IS NULL OR t.game = $2)
- GROUP BY t.id
- ORDER BY t.created_at DESC
+ GROUP BY t.id
+ ORDER BY t.created_at DESC
LIMIT $3 OFFSET $4
"#,
status_filter.map(|s| s as i32),
@@ -160,8 +163,8 @@ impl TournamentService {
// Get total count
let total = sqlx::query!(
r#"
- SELECT COUNT(*) as count
- FROM tournaments t
+ SELECT COUNT(*) as count
+ FROM tournaments t
WHERE ($1::text IS NULL OR t.status = $1::tournament_status)
AND ($2::text IS NULL OR t.game = $2)
"#,
@@ -184,12 +187,17 @@ impl TournamentService {
};
let participant_status = if is_participant {
- self.get_participant_status(user_id.unwrap(), row.id).await.ok()
+ self.get_participant_status(user_id.unwrap(), row.id)
+ .await
+ .ok()
} else {
None
};
- let can_join = self.can_user_join_tournament(user_id, row.id).await.unwrap_or(false);
+ let can_join = self
+ .can_user_join_tournament(user_id, row.id)
+ .await
+ .unwrap_or(false);
tournament_responses.push(TournamentResponse {
id: row.id,
@@ -222,12 +230,16 @@ impl TournamentService {
}
/// Get a specific tournament by ID
- pub async fn get_tournament(&self, tournament_id: Uuid, user_id: Option) -> Result {
+ pub async fn get_tournament(
+ &self,
+ tournament_id: Uuid,
+ user_id: Option,
+ ) -> Result {
let tournament = sqlx::query!(
r#"
- SELECT t.*, COUNT(tp.id) as current_participants
- FROM tournaments t
- LEFT JOIN tournament_participants tp ON t.id = tp.tournament_id
+ SELECT t.*, COUNT(tp.id) as current_participants
+ FROM tournaments t
+ LEFT JOIN tournament_participants tp ON t.id = tp.tournament_id
WHERE t.id = $1
GROUP BY t.id
"#,
@@ -239,18 +251,25 @@ impl TournamentService {
.ok_or(ApiError::not_found("Tournament not found"))?;
let is_participant = if let Some(uid) = user_id {
- self.is_user_participant(uid, tournament_id).await.unwrap_or(false)
+ self.is_user_participant(uid, tournament_id)
+ .await
+ .unwrap_or(false)
} else {
false
};
let participant_status = if is_participant {
- self.get_participant_status(user_id.unwrap(), tournament_id).await.ok()
+ self.get_participant_status(user_id.unwrap(), tournament_id)
+ .await
+ .ok()
} else {
None
};
- let can_join = self.can_user_join_tournament(user_id, tournament_id).await.unwrap_or(false);
+ let can_join = self
+ .can_user_join_tournament(user_id, tournament_id)
+ .await
+ .unwrap_or(false);
Ok(TournamentResponse {
id: tournament.id,
@@ -291,7 +310,8 @@ impl TournamentService {
}
// Process payment
- self.process_entry_fee_payment(user_id, &tournament, &request).await?;
+ self.process_entry_fee_payment(user_id, &tournament, &request)
+ .await?;
// Add participant
let participant = sqlx::query_as!(
@@ -315,13 +335,18 @@ impl TournamentService {
.map_err(|e| ApiError::database_error(e))?;
// Update prize pool
- self.update_prize_pool(tournament_id, tournament.entry_fee).await?;
+ self.update_prize_pool(tournament_id, tournament.entry_fee)
+ .await?;
// Update tournament status if needed
- self.update_tournament_status_if_needed(tournament_id).await?;
+ self.update_tournament_status_if_needed(tournament_id)
+ .await?;
// Get username for event
- let username = self.get_user_username(user_id).await.unwrap_or_else(|| "Unknown".to_string());
+ let username = self
+ .get_user_username(user_id)
+ .await
+ .unwrap_or_else(|| "Unknown".to_string());
// Publish participant joined event
self.publish_tournament_event(serde_json::json!({
@@ -330,7 +355,8 @@ impl TournamentService {
"user_id": user_id,
"username": username,
"participant_count": self.get_participant_count(tournament_id).await?,
- })).await?;
+ }))
+ .await?;
Ok(participant)
}
@@ -344,7 +370,7 @@ impl TournamentService {
let tournament = sqlx::query_as!(
Tournament,
r#"
- UPDATE tournaments
+ UPDATE tournaments
SET status = $1, updated_at = $2
WHERE id = $3
RETURNING *
@@ -375,20 +401,26 @@ impl TournamentService {
"tournament_id": tournament_id,
"old_status": old_status,
"new_status": new_status,
- })).await?;
+ }))
+ .await?;
Ok(tournament)
}
// Private helper methods
- async fn validate_tournament_creation(&self, request: &CreateTournamentRequest) -> Result<(), ApiError> {
+ async fn validate_tournament_creation(
+ &self,
+ request: &CreateTournamentRequest,
+ ) -> Result<(), ApiError> {
if request.name.is_empty() {
return Err(ApiError::bad_request("Tournament name is required"));
}
if request.max_participants < 2 {
- return Err(ApiError::bad_request("Tournament must have at least 2 participants"));
+ return Err(ApiError::bad_request(
+ "Tournament must have at least 2 participants",
+ ));
}
if request.entry_fee < 0 {
@@ -400,15 +432,23 @@ impl TournamentService {
}
if request.registration_deadline >= request.start_time {
- return Err(ApiError::bad_request("Registration deadline must be before start time"));
+ return Err(ApiError::bad_request(
+ "Registration deadline must be before start time",
+ ));
}
Ok(())
}
- async fn validate_tournament_join(&self, tournament: &Tournament, user_id: Uuid) -> Result<(), ApiError> {
+ async fn validate_tournament_join(
+ &self,
+ tournament: &Tournament,
+ user_id: Uuid,
+ ) -> Result<(), ApiError> {
if tournament.status != TournamentStatus::RegistrationOpen {
- return Err(ApiError::bad_request("Tournament is not accepting registrations"));
+ return Err(ApiError::bad_request(
+ "Tournament is not accepting registrations",
+ ));
}
if Utc::now() > tournament.registration_deadline {
@@ -422,10 +462,14 @@ impl TournamentService {
}
// Check skill level requirements
- if let (Some(min_skill), Some(max_skill)) = (tournament.min_skill_level, tournament.max_skill_level) {
+ if let (Some(min_skill), Some(max_skill)) =
+ (tournament.min_skill_level, tournament.max_skill_level)
+ {
let user_elo = self.get_user_elo(user_id, &tournament.game).await?;
if user_elo < min_skill || user_elo > max_skill {
- return Err(ApiError::bad_request("User skill level does not meet tournament requirements"));
+ return Err(ApiError::bad_request(
+ "User skill level does not meet tournament requirements",
+ ));
}
}
@@ -441,11 +485,13 @@ impl TournamentService {
match request.payment_method.as_str() {
"fiat" => {
// Process fiat payment via Paystack/Flutterwave
- self.process_fiat_payment(user_id, tournament, &request.payment_reference).await?;
+ self.process_fiat_payment(user_id, tournament, &request.payment_reference)
+ .await?;
}
"arenax_token" => {
// Process ArenaX token payment
- self.process_arenax_token_payment(user_id, tournament).await?;
+ self.process_arenax_token_payment(user_id, tournament)
+ .await?;
}
_ => {
return Err(ApiError::bad_request("Invalid payment method"));
@@ -462,14 +508,18 @@ impl TournamentService {
payment_reference: &Option,
) -> Result<(), ApiError> {
if payment_reference.is_none() {
- return Err(ApiError::bad_request("Payment reference is required for fiat payments"));
+ return Err(ApiError::bad_request(
+ "Payment reference is required for fiat payments",
+ ));
}
let reference = payment_reference.as_ref().unwrap();
// Verify payment with payment provider
- let payment_verified = self.verify_payment_with_provider(reference, tournament.entry_fee).await?;
-
+ let payment_verified = self
+ .verify_payment_with_provider(reference, tournament.entry_fee)
+ .await?;
+
if !payment_verified {
return Err(ApiError::bad_request("Payment verification failed"));
}
@@ -484,21 +534,30 @@ impl TournamentService {
tournament.entry_fee,
tournament.entry_fee_currency.clone(),
format!("Entry fee for tournament: {}", tournament.name),
- ).await?;
+ )
+ .await?;
Ok(())
}
- async fn verify_payment_with_provider(&self, reference: &str, amount: i64) -> Result {
+ async fn verify_payment_with_provider(
+ &self,
+ reference: &str,
+ amount: i64,
+ ) -> Result {
// In a real implementation, this would:
// 1. Make API call to Paystack/Flutterwave
// 2. Verify the payment reference and amount
// 3. Check payment status
-
+
// For now, simulate payment verification
// In production, you would use the actual payment provider APIs
- tracing::info!("Verifying payment: reference={}, amount={}", reference, amount);
-
+ tracing::info!(
+ "Verifying payment: reference={}, amount={}",
+ reference,
+ amount
+ );
+
// Simulate successful verification
Ok(true)
}
@@ -523,13 +582,14 @@ impl TournamentService {
) -> Result<(), ApiError> {
// Check user's ArenaX token balance
let wallet = self.get_user_wallet(user_id).await?;
-
+
if wallet.balance_arenax_tokens < tournament.entry_fee {
return Err(ApiError::bad_request("Insufficient ArenaX token balance"));
}
// Deduct tokens from user's wallet
- self.deduct_arenax_tokens(user_id, tournament.entry_fee).await?;
+ self.deduct_arenax_tokens(user_id, tournament.entry_fee)
+ .await?;
// Create transaction record
self.create_transaction(
@@ -538,19 +598,24 @@ impl TournamentService {
tournament.entry_fee,
"ARENAX_TOKEN".to_string(),
format!("Entry fee for tournament: {}", tournament.name),
- ).await?;
+ )
+ .await?;
Ok(())
}
- async fn create_prize_pool(&self, tournament_id: &Uuid, currency: &str) -> Result<(), ApiError> {
+ async fn create_prize_pool(
+ &self,
+ tournament_id: &Uuid,
+ currency: &str,
+ ) -> Result<(), ApiError> {
// Create Stellar account for prize pool
let stellar_account = self.create_stellar_prize_pool_account().await?;
sqlx::query!(
r#"
INSERT INTO prize_pools (
- id, tournament_id, total_amount, currency, stellar_account,
+ id, tournament_id, total_amount, currency, stellar_account,
distribution_percentages, created_at, updated_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8
@@ -575,7 +640,7 @@ impl TournamentService {
async fn update_prize_pool(&self, tournament_id: Uuid, entry_fee: i64) -> Result<(), ApiError> {
sqlx::query!(
r#"
- UPDATE prize_pools
+ UPDATE prize_pools
SET total_amount = total_amount + $1, updated_at = $2
WHERE tournament_id = $3
"#,
@@ -593,11 +658,11 @@ impl TournamentService {
async fn start_tournament(&self, tournament_id: Uuid) -> Result<(), ApiError> {
// Generate tournament bracket
self.generate_tournament_bracket(tournament_id).await?;
-
+
// Update all participants to active status
sqlx::query!(
r#"
- UPDATE tournament_participants
+ UPDATE tournament_participants
SET status = $1
WHERE tournament_id = $2 AND status = $3
"#,
@@ -615,7 +680,7 @@ impl TournamentService {
async fn complete_tournament(&self, tournament_id: Uuid) -> Result<(), ApiError> {
// Calculate final rankings
self.calculate_final_rankings(tournament_id).await?;
-
+
// Distribute prizes
self.distribute_prizes(tournament_id).await?;
@@ -627,7 +692,7 @@ impl TournamentService {
let participants = sqlx::query_as!(
TournamentParticipant,
r#"
- SELECT * FROM tournament_participants
+ SELECT * FROM tournament_participants
WHERE tournament_id = $1 AND status = $2
ORDER BY registered_at
"#,
@@ -644,16 +709,20 @@ impl TournamentService {
// Generate bracket based on type
match tournament.bracket_type {
BracketType::SingleElimination => {
- self.generate_single_elimination_bracket(tournament_id, participants).await?;
+ self.generate_single_elimination_bracket(tournament_id, participants)
+ .await?;
}
BracketType::DoubleElimination => {
- self.generate_double_elimination_bracket(tournament_id, participants).await?;
+ self.generate_double_elimination_bracket(tournament_id, participants)
+ .await?;
}
BracketType::RoundRobin => {
- self.generate_round_robin_bracket(tournament_id, participants).await?;
+ self.generate_round_robin_bracket(tournament_id, participants)
+ .await?;
}
BracketType::Swiss => {
- self.generate_swiss_bracket(tournament_id, participants).await?;
+ self.generate_swiss_bracket(tournament_id, participants)
+ .await?;
}
}
@@ -672,7 +741,7 @@ impl TournamentService {
// Calculate number of rounds needed
let rounds = (participant_count as f64).log2().ceil() as i32;
-
+
// Create rounds
for round_num in 1..=rounds {
let round = sqlx::query_as!(
@@ -687,7 +756,11 @@ impl TournamentService {
Uuid::new_v4(),
tournament_id,
round_num,
- if round_num == rounds { RoundType::Final } else { RoundType::Elimination } as _,
+ if round_num == rounds {
+ RoundType::Final
+ } else {
+ RoundType::Elimination
+ } as _,
RoundStatus::Pending as _,
Utc::now()
)
@@ -720,7 +793,11 @@ impl TournamentService {
round.id,
match_num as i32,
participants[player1_idx].user_id,
- if player2_idx < participants.len() { Some(participants[player2_idx].user_id) } else { None },
+ if player2_idx < participants.len() {
+ Some(participants[player2_idx].user_id)
+ } else {
+ None
+ },
MatchStatus::Pending as _,
Utc::now(),
Utc::now()
@@ -749,7 +826,11 @@ impl TournamentService {
.ok_or(ApiError::not_found("Tournament not found".to_string()))
}
- async fn is_user_participant(&self, user_id: Uuid, tournament_id: Uuid) -> Result {
+ async fn is_user_participant(
+ &self,
+ user_id: Uuid,
+ tournament_id: Uuid,
+ ) -> Result {
let count = sqlx::query!(
"SELECT COUNT(*) as count FROM tournament_participants WHERE user_id = $1 AND tournament_id = $2",
user_id,
@@ -764,7 +845,11 @@ impl TournamentService {
Ok(count > 0)
}
- async fn get_participant_status(&self, user_id: Uuid, tournament_id: Uuid) -> Result {
+ async fn get_participant_status(
+ &self,
+ user_id: Uuid,
+ tournament_id: Uuid,
+ ) -> Result {
let participant = sqlx::query!(
"SELECT status FROM tournament_participants WHERE user_id = $1 AND tournament_id = $2",
user_id,
@@ -778,7 +863,11 @@ impl TournamentService {
Ok(participant.status.into())
}
- async fn can_user_join_tournament(&self, user_id: Option, tournament_id: Uuid) -> Result {
+ async fn can_user_join_tournament(
+ &self,
+ user_id: Option,
+ tournament_id: Uuid,
+ ) -> Result {
if user_id.is_none() {
return Ok(false);
}
@@ -838,15 +927,11 @@ impl TournamentService {
}
async fn get_user_wallet(&self, user_id: Uuid) -> Result {
- sqlx::query_as!(
- Wallet,
- "SELECT * FROM wallets WHERE user_id = $1",
- user_id
- )
- .fetch_optional(&self.db_pool)
- .await
- .map_err(|e| ApiError::database_error(e))?
- .ok_or(ApiError::not_found("Wallet not found"))
+ sqlx::query_as!(Wallet, "SELECT * FROM wallets WHERE user_id = $1", user_id)
+ .fetch_optional(&self.db_pool)
+ .await
+ .map_err(|e| ApiError::database_error(e))?
+ .ok_or(ApiError::not_found("Wallet not found"))
}
async fn deduct_arenax_tokens(&self, user_id: Uuid, amount: i64) -> Result<(), ApiError> {
@@ -903,19 +988,31 @@ impl TournamentService {
// 2. Create the account on Stellar network
// 3. Fund it with XLM
// 4. Return the public key
-
+
// For now, generate a realistic-looking Stellar public key
- let account_id = format!("G{}", uuid::Uuid::new_v4().to_string().replace('-', "").to_uppercase());
+ let account_id = format!(
+ "G{}",
+ uuid::Uuid::new_v4()
+ .to_string()
+ .replace('-', "")
+ .to_uppercase()
+ );
Ok(account_id)
}
- async fn update_tournament_status_if_needed(&self, tournament_id: Uuid) -> Result<(), ApiError> {
+ async fn update_tournament_status_if_needed(
+ &self,
+ tournament_id: Uuid,
+ ) -> Result<(), ApiError> {
let tournament = self.get_tournament_by_id(tournament_id).await?;
let participant_count = self.get_participant_count(tournament_id).await?;
// Auto-close registration if tournament is full
- if participant_count >= tournament.max_participants && tournament.status == TournamentStatus::RegistrationOpen {
- self.update_tournament_status(tournament_id, TournamentStatus::RegistrationClosed).await?;
+ if participant_count >= tournament.max_participants
+ && tournament.status == TournamentStatus::RegistrationOpen
+ {
+ self.update_tournament_status(tournament_id, TournamentStatus::RegistrationClosed)
+ .await?;
}
Ok(())
@@ -935,19 +1032,22 @@ impl TournamentService {
// Calculate rankings based on tournament type
let tournament = self.get_tournament_by_id(tournament_id).await?;
-
+
match tournament.bracket_type {
BracketType::SingleElimination | BracketType::DoubleElimination => {
// For elimination tournaments, rank by elimination order
- self.calculate_elimination_rankings(tournament_id, participants).await?;
+ self.calculate_elimination_rankings(tournament_id, participants)
+ .await?;
}
BracketType::RoundRobin => {
// For round robin, rank by win/loss record
- self.calculate_round_robin_rankings(tournament_id, participants).await?;
+ self.calculate_round_robin_rankings(tournament_id, participants)
+ .await?;
}
BracketType::Swiss => {
// For Swiss, rank by points and tiebreakers
- self.calculate_swiss_rankings(tournament_id, participants).await?;
+ self.calculate_swiss_rankings(tournament_id, participants)
+ .await?;
}
}
@@ -977,14 +1077,16 @@ impl TournamentService {
// Parse distribution percentages
let percentages: Vec = serde_json::from_str(&prize_pool.distribution_percentages)
- .map_err(|e| ApiError::internal_error(format!("Invalid distribution percentages: {}", e)))?;
+ .map_err(|e| {
+ ApiError::internal_error(format!("Invalid distribution percentages: {}", e))
+ })?;
// Distribute prizes
for (index, participant) in participants.iter().enumerate() {
if index < percentages.len() && participant.final_rank.unwrap_or(0) <= 3 {
let percentage = percentages[index];
let prize_amount = (prize_pool.total_amount as f64 * percentage / 100.0) as i64;
-
+
// Update participant with prize amount
sqlx::query!(
"UPDATE tournament_participants SET prize_amount = $1, prize_currency = $2 WHERE id = $3",
@@ -998,7 +1100,12 @@ impl TournamentService {
// TODO: In a real implementation, initiate Stellar transaction to send prize
// For now, we'll just record the prize amount
- tracing::info!("Prize distributed: {} {} to user {}", prize_amount, prize_pool.currency, participant.user_id);
+ tracing::info!(
+ "Prize distributed: {} {} to user {}",
+ prize_amount,
+ prize_pool.currency,
+ participant.user_id
+ );
}
}
@@ -1006,7 +1113,11 @@ impl TournamentService {
}
// Additional bracket generation methods
- async fn generate_double_elimination_bracket(&self, tournament_id: Uuid, participants: Vec) -> Result<(), ApiError> {
+ async fn generate_double_elimination_bracket(
+ &self,
+ tournament_id: Uuid,
+ participants: Vec,
+ ) -> Result<(), ApiError> {
let participant_count = participants.len();
if participant_count < 2 {
return Err(ApiError::bad_request("Not enough participants for bracket"));
@@ -1014,7 +1125,7 @@ impl TournamentService {
// Calculate number of rounds needed
let rounds = (participant_count as f64).log2().ceil() as i32;
-
+
// Winners bracket
for round_num in 1..=rounds {
let round = sqlx::query_as!(
@@ -1056,7 +1167,11 @@ impl TournamentService {
round.id,
match_num as i32,
participants[player1_idx].user_id,
- if player2_idx < participants.len() { Some(participants[player2_idx].user_id) } else { None },
+ if player2_idx < participants.len() {
+ Some(participants[player2_idx].user_id)
+ } else {
+ None
+ },
MatchStatus::Pending as _,
Utc::now(),
Utc::now()
@@ -1068,11 +1183,18 @@ impl TournamentService {
}
// Losers bracket would be generated after winners bracket matches
- tracing::info!("Double elimination bracket generated for tournament: {}", tournament_id);
+ tracing::info!(
+ "Double elimination bracket generated for tournament: {}",
+ tournament_id
+ );
Ok(())
}
- async fn generate_round_robin_bracket(&self, tournament_id: Uuid, participants: Vec) -> Result<(), ApiError> {
+ async fn generate_round_robin_bracket(
+ &self,
+ tournament_id: Uuid,
+ participants: Vec,
+ ) -> Result<(), ApiError> {
let participant_count = participants.len();
if participant_count < 2 {
return Err(ApiError::bad_request("Not enough participants for bracket"));
@@ -1130,11 +1252,19 @@ impl TournamentService {
}
}
- tracing::info!("Round robin bracket generated for tournament: {} with {} matches", tournament_id, match_number - 1);
+ tracing::info!(
+ "Round robin bracket generated for tournament: {} with {} matches",
+ tournament_id,
+ match_number - 1
+ );
Ok(())
}
- async fn generate_swiss_bracket(&self, tournament_id: Uuid, participants: Vec) -> Result<(), ApiError> {
+ async fn generate_swiss_bracket(
+ &self,
+ tournament_id: Uuid,
+ participants: Vec,
+ ) -> Result<(), ApiError> {
let participant_count = participants.len();
if participant_count < 2 {
return Err(ApiError::bad_request("Not enough participants for bracket"));
@@ -1186,7 +1316,11 @@ impl TournamentService {
round.id,
match_num as i32,
participants[player1_idx].user_id,
- if player2_idx < participants.len() { Some(participants[player2_idx].user_id) } else { None },
+ if player2_idx < participants.len() {
+ Some(participants[player2_idx].user_id)
+ } else {
+ None
+ },
MatchStatus::Pending as _,
Utc::now(),
Utc::now()
@@ -1199,11 +1333,19 @@ impl TournamentService {
// Subsequent Swiss rounds would be pairing based on standings and strength of schedule
}
- tracing::info!("Swiss bracket generated for tournament: {} with {} rounds", tournament_id, rounds);
+ tracing::info!(
+ "Swiss bracket generated for tournament: {} with {} rounds",
+ tournament_id,
+ rounds
+ );
Ok(())
}
- async fn calculate_elimination_rankings(&self, tournament_id: Uuid, participants: Vec) -> Result<(), ApiError> {
+ async fn calculate_elimination_rankings(
+ &self,
+ tournament_id: Uuid,
+ participants: Vec,
+ ) -> Result<(), ApiError> {
// For elimination tournaments, rank by elimination order
// Get matches in reverse order to determine elimination sequence
let matches = sqlx::query_as!(
@@ -1256,14 +1398,18 @@ impl TournamentService {
Ok(())
}
- async fn calculate_round_robin_rankings(&self, tournament_id: Uuid, participants: Vec) -> Result<(), ApiError> {
+ async fn calculate_round_robin_rankings(
+ &self,
+ tournament_id: Uuid,
+ participants: Vec,
+ ) -> Result<(), ApiError> {
// For round robin, calculate win/loss records
let mut player_stats = std::collections::HashMap::new();
for participant in &participants {
let wins = sqlx::query!(
r#"
- SELECT COUNT(*) as count FROM tournament_matches
+ SELECT COUNT(*) as count FROM tournament_matches
WHERE tournament_id = $1 AND winner_id = $2 AND status = $3
"#,
tournament_id,
@@ -1278,8 +1424,8 @@ impl TournamentService {
let losses = sqlx::query!(
r#"
- SELECT COUNT(*) as count FROM tournament_matches
- WHERE tournament_id = $1 AND (player1_id = $2 OR player2_id = $2)
+ SELECT COUNT(*) as count FROM tournament_matches
+ WHERE tournament_id = $1 AND (player1_id = $2 OR player2_id = $2)
AND winner_id != $2 AND status = $3
"#,
tournament_id,
@@ -1320,14 +1466,18 @@ impl TournamentService {
Ok(())
}
- async fn calculate_swiss_rankings(&self, tournament_id: Uuid, participants: Vec) -> Result<(), ApiError> {
+ async fn calculate_swiss_rankings(
+ &self,
+ tournament_id: Uuid,
+ participants: Vec,
+ ) -> Result<(), ApiError> {
// For Swiss tournaments, rank by points and tiebreakers
let mut player_stats = std::collections::HashMap::new();
for participant in &participants {
let wins = sqlx::query!(
r#"
- SELECT COUNT(*) as count FROM tournament_matches
+ SELECT COUNT(*) as count FROM tournament_matches
WHERE tournament_id = $1 AND winner_id = $2 AND status = $3
"#,
tournament_id,
@@ -1342,8 +1492,8 @@ impl TournamentService {
let draws = sqlx::query!(
r#"
- SELECT COUNT(*) as count FROM tournament_matches
- WHERE tournament_id = $1 AND (player1_id = $2 OR player2_id = $2)
+ SELECT COUNT(*) as count FROM tournament_matches
+ WHERE tournament_id = $1 AND (player1_id = $2 OR player2_id = $2)
AND winner_id IS NULL AND status = $3
"#,
tournament_id,
@@ -1383,7 +1533,10 @@ impl TournamentService {
// Real-time event publishing methods
// TODO: Implement proper realtime module with event types
- async fn publish_tournament_event(&self, _event_data: serde_json::Value) -> Result<(), ApiError> {
+ async fn publish_tournament_event(
+ &self,
+ _event_data: serde_json::Value,
+ ) -> Result<(), ApiError> {
// Placeholder for real-time tournament event publishing
// Will be implemented when realtime module is added
Ok(())
@@ -1396,7 +1549,10 @@ impl TournamentService {
}
/// Get tournament participants
- pub async fn get_tournament_participants(&self, tournament_id: Uuid) -> Result, ApiError> {
+ pub async fn get_tournament_participants(
+ &self,
+ tournament_id: Uuid,
+ ) -> Result, ApiError> {
let participants = sqlx::query_as!(
TournamentParticipant,
"SELECT * FROM tournament_participants WHERE tournament_id = $1 ORDER BY registered_at",
@@ -1410,7 +1566,10 @@ impl TournamentService {
}
/// Get tournament bracket
- pub async fn get_tournament_bracket(&self, tournament_id: Uuid) -> Result {
+ pub async fn get_tournament_bracket(
+ &self,
+ tournament_id: Uuid,
+ ) -> Result {
// Get tournament rounds
let rounds = sqlx::query_as!(
TournamentRound,
@@ -1438,16 +1597,19 @@ impl TournamentService {
round_number: round.round_number,
round_type: round.round_type.into(),
status: round.status.into(),
- matches: matches.into_iter().map(|m| BracketMatch {
- match_id: m.id,
- match_number: m.match_number,
- player1_id: m.player1_id,
- player2_id: m.player2_id,
- winner_id: m.winner_id,
- player1_score: m.player1_score,
- player2_score: m.player2_score,
- status: m.status.into(),
- }).collect(),
+ matches: matches
+ .into_iter()
+ .map(|m| BracketMatch {
+ match_id: m.id,
+ match_number: m.match_number,
+ player1_id: m.player1_id,
+ player2_id: m.player2_id,
+ winner_id: m.winner_id,
+ player1_score: m.player1_score,
+ player2_score: m.player2_score,
+ status: m.status.into(),
+ })
+ .collect(),
});
}
@@ -1458,14 +1620,11 @@ impl TournamentService {
}
async fn get_user_username(&self, user_id: Uuid) -> Result {
- let user = sqlx::query!(
- "SELECT username FROM users WHERE id = $1",
- user_id
- )
- .fetch_optional(&self.db_pool)
- .await
- .map_err(|e| ApiError::database_error(e))?
- .ok_or(ApiError::not_found("User not found"))?;
+ let user = sqlx::query!("SELECT username FROM users WHERE id = $1", user_id)
+ .fetch_optional(&self.db_pool)
+ .await
+ .map_err(|e| ApiError::database_error(e))?
+ .ok_or(ApiError::not_found("User not found"))?;
Ok(user.username)
}
diff --git a/backend/src/service/wallet_service.rs b/backend/src/service/wallet_service.rs
index c0c7b73..53c13ab 100644
--- a/backend/src/service/wallet_service.rs
+++ b/backend/src/service/wallet_service.rs
@@ -1,6 +1,5 @@
use crate::models::{
- Wallet, Transaction, TransactionType, TransactionStatus,
- WalletResponse, TransactionResponse,
+ Transaction, TransactionResponse, TransactionStatus, TransactionType, Wallet, WalletResponse,
};
use anyhow::Result;
use chrono::Utc;
@@ -272,11 +271,7 @@ impl WalletService {
}
/// Release escrow back to balance
- pub async fn release_from_escrow(
- &self,
- user_id: Uuid,
- amount: i64,
- ) -> Result<(), WalletError> {
+ pub async fn release_from_escrow(&self, user_id: Uuid, amount: i64) -> Result<(), WalletError> {
if amount <= 0 {
return Err(WalletError::InvalidAmount(
"Amount must be positive".to_string(),
@@ -512,8 +507,11 @@ impl WalletService {
let verified = self.verify_paystack_payment(ref_id, amount).await?;
if verified {
self.add_fiat_balance(user_id, amount).await?;
- self.update_transaction_status(transaction.id, TransactionStatus::Completed)
- .await?;
+ self.update_transaction_status(
+ transaction.id,
+ TransactionStatus::Completed,
+ )
+ .await?;
transaction.status = TransactionStatus::Completed;
} else {
self.update_transaction_status(transaction.id, TransactionStatus::Failed)
@@ -527,8 +525,11 @@ impl WalletService {
let verified = self.verify_flutterwave_payment(ref_id, amount).await?;
if verified {
self.add_fiat_balance(user_id, amount).await?;
- self.update_transaction_status(transaction.id, TransactionStatus::Completed)
- .await?;
+ self.update_transaction_status(
+ transaction.id,
+ TransactionStatus::Completed,
+ )
+ .await?;
transaction.status = TransactionStatus::Completed;
} else {
self.update_transaction_status(transaction.id, TransactionStatus::Failed)
diff --git a/backend/src/telemetry.rs b/backend/src/telemetry.rs
index 0b89da4..ea90476 100644
--- a/backend/src/telemetry.rs
+++ b/backend/src/telemetry.rs
@@ -2,10 +2,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilte
pub fn init_telemetry() {
tracing_subscriber::registry()
- .with(
- EnvFilter::try_from_default_env()
- .unwrap_or_else(|_| "backend=info".into()),
- )
+ .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| "backend=info".into()))
.with(tracing_subscriber::fmt::layer())
.init();
}
diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock
index 1804395..9459ca1 100644
--- a/contracts/Cargo.lock
+++ b/contracts/Cargo.lock
@@ -32,6 +32,13 @@ dependencies = [
"derive_arbitrary",
]
+[[package]]
+name = "arenax-anti-cheat-oracle"
+version = "0.0.0"
+dependencies = [
+ "soroban-sdk",
+]
+
[[package]]
name = "arenax-registry"
version = "0.0.0"
@@ -46,6 +53,13 @@ dependencies = [
"soroban-sdk",
]
+[[package]]
+name = "arenax-reputation-index"
+version = "0.0.0"
+dependencies = [
+ "soroban-sdk",
+]
+
[[package]]
name = "ark-bls12-381"
version = "0.4.0"
@@ -170,6 +184,13 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+[[package]]
+name = "ax-token"
+version = "0.1.0"
+dependencies = [
+ "soroban-sdk",
+]
+
[[package]]
name = "base16ct"
version = "0.2.0"
@@ -481,6 +502,13 @@ dependencies = [
"subtle",
]
+[[package]]
+name = "dispute-resolution"
+version = "0.1.0"
+dependencies = [
+ "soroban-sdk",
+]
+
[[package]]
name = "downcast-rs"
version = "1.2.1"
@@ -858,6 +886,16 @@ dependencies = [
]
[[package]]
+<<<<<<< HEAD
+name = "match-lifecycle"
+version = "0.1.0"
+dependencies = [
+ "soroban-sdk",
+]
+
+[[package]]
+=======
+>>>>>>> 99cfb318a51dd06fdbbff4fd1fd598156b02db34
name = "match_escrow_vault"
version = "0.1.0"
dependencies = [
diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml
index 5aca680..194eff1 100644
--- a/contracts/Cargo.toml
+++ b/contracts/Cargo.toml
@@ -1,15 +1,23 @@
[workspace]
members = [
"example",
+ "dispute-resolution",
"protocol-params",
"user_identity_contract",
"reputation_aggregation",
"match_contract",
"match_escrow_vault",
+ "match-lifecycle",
"registry",
"slashing_contract",
"governance_multisig",
"tournament_finalizer",
+ "reputation-index",
+ "contract-registry",
+ "auth-gateway",
+ "ax-token",
+ "anti-cheat-oracle",
+ "staking-manager",
]
resolver = "2"
diff --git a/contracts/anti-cheat-oracle/Cargo.toml b/contracts/anti-cheat-oracle/Cargo.toml
new file mode 100644
index 0000000..6fa8363
--- /dev/null
+++ b/contracts/anti-cheat-oracle/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "arenax-anti-cheat-oracle"
+version = "0.0.0"
+edition = "2021"
+publish = false
+
+[lib]
+crate-type = ["cdylib"]
+doctest = false
+
+[dependencies]
+soroban-sdk = { workspace = true }
+
+[dev-dependencies]
+soroban-sdk = { workspace = true, features = ["testutils"] }
diff --git a/contracts/anti-cheat-oracle/src/error.rs b/contracts/anti-cheat-oracle/src/error.rs
new file mode 100644
index 0000000..ef2f47b
--- /dev/null
+++ b/contracts/anti-cheat-oracle/src/error.rs
@@ -0,0 +1,12 @@
+use soroban_sdk::contracterror;
+
+#[contracterror]
+#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
+#[repr(u32)]
+pub enum AntiCheatError {
+ AlreadyInitialized = 1,
+ NotInitialized = 2,
+ Unauthorized = 3,
+ InvalidSeverity = 4,
+ ReputationNotSet = 5,
+}
diff --git a/contracts/anti-cheat-oracle/src/events.rs b/contracts/anti-cheat-oracle/src/events.rs
new file mode 100644
index 0000000..16c507d
--- /dev/null
+++ b/contracts/anti-cheat-oracle/src/events.rs
@@ -0,0 +1,32 @@
+use soroban_sdk::{contractevent, Address};
+
+/// Event type for anti-cheat; includes AntiCheatFlag for confirmations.
+#[contractevent(topics = ["ArenaXAntiCheat", "FLAG"])]
+pub struct AntiCheatFlag {
+ pub player: Address,
+ pub match_id: u64,
+ pub severity: u32,
+ pub penalty_applied: i128,
+ pub oracle: Address,
+ pub timestamp: u64,
+}
+
+pub fn emit_anticheat_flag(
+ env: &soroban_sdk::Env,
+ player: &Address,
+ match_id: u64,
+ severity: u32,
+ penalty_applied: i128,
+ oracle: &Address,
+ timestamp: u64,
+) {
+ AntiCheatFlag {
+ player: player.clone(),
+ match_id,
+ severity,
+ penalty_applied,
+ oracle: oracle.clone(),
+ timestamp,
+ }
+ .publish(env);
+}
diff --git a/contracts/anti-cheat-oracle/src/lib.rs b/contracts/anti-cheat-oracle/src/lib.rs
new file mode 100644
index 0000000..c4c9036
--- /dev/null
+++ b/contracts/anti-cheat-oracle/src/lib.rs
@@ -0,0 +1,178 @@
+#![no_std]
+
+mod error;
+mod events;
+mod storage;
+
+use soroban_sdk::auth::{ContractContext, InvokerContractAuthEntry, SubContractInvocation};
+use soroban_sdk::{contract, contractimpl, Address, Env, IntoVal, Symbol, Vec};
+use storage::{AntiCheatConfirmation, DataKey};
+
+pub use error::AntiCheatError;
+
+/// Severity levels: 1 = low, 2 = medium, 3 = high. Maps to bounded penalties (capped in Reputation Index).
+const PENALTY_LOW: i128 = 5;
+const PENALTY_MEDIUM: i128 = 15;
+const PENALTY_HIGH: i128 = 30;
+
+#[contract]
+pub struct AntiCheatOracle;
+
+#[contractimpl]
+impl AntiCheatOracle {
+ /// Initialize the anti-cheat oracle (admin only).
+ pub fn initialize(env: Env, admin: Address) -> Result<(), AntiCheatError> {
+ if env.storage().instance().has(&DataKey::Admin) {
+ return Err(AntiCheatError::AlreadyInitialized);
+ }
+ env.storage().instance().set(&DataKey::Admin, &admin);
+ Ok(())
+ }
+
+ /// Add an authorized oracle address that can submit flags.
+ pub fn add_authorized_oracle(env: Env, oracle: Address) -> Result<(), AntiCheatError> {
+ let admin: Address = env
+ .storage()
+ .instance()
+ .get(&DataKey::Admin)
+ .ok_or(AntiCheatError::NotInitialized)?;
+ admin.require_auth();
+ env.storage()
+ .instance()
+ .set(&DataKey::AuthorizedOracle(oracle.clone()), &true);
+ Ok(())
+ }
+
+ /// Remove an authorized oracle.
+ pub fn remove_authorized_oracle(env: Env, oracle: Address) -> Result<(), AntiCheatError> {
+ let admin: Address = env
+ .storage()
+ .instance()
+ .get(&DataKey::Admin)
+ .ok_or(AntiCheatError::NotInitialized)?;
+ admin.require_auth();
+ env.storage()
+ .instance()
+ .remove(&DataKey::AuthorizedOracle(oracle));
+ Ok(())
+ }
+
+ /// Returns true if the given address is an authorized oracle.
+ pub fn is_authorized_oracle(env: Env, oracle: Address) -> bool {
+ env.storage()
+ .instance()
+ .get(&DataKey::AuthorizedOracle(oracle))
+ .unwrap_or(false)
+ }
+
+ /// Set the Reputation Index contract address (admin only). Required before submit_flag can apply penalties.
+ pub fn set_reputation_contract(env: Env, reputation: Address) -> Result<(), AntiCheatError> {
+ let admin: Address = env
+ .storage()
+ .instance()
+ .get(&DataKey::Admin)
+ .ok_or(AntiCheatError::NotInitialized)?;
+ admin.require_auth();
+ env.storage().instance().set(&DataKey::ReputationContract, &reputation);
+ Ok(())
+ }
+
+ /// Submit an anti-cheat flag for a player in a match. Only authorized oracle addresses can call.
+ /// Severity: 1 = low, 2 = medium, 3 = high. Penalties are bounded and applied to the Reputation Index.
+ pub fn submit_flag(
+ env: Env,
+ oracle: Address,
+ player: Address,
+ match_id: u64,
+ severity: u32,
+ ) -> Result<(), AntiCheatError> {
+ oracle.require_auth();
+ if !Self::is_authorized_oracle(env.clone(), oracle.clone()) {
+ return Err(AntiCheatError::Unauthorized);
+ }
+ if severity == 0 || severity > 3 {
+ return Err(AntiCheatError::InvalidSeverity);
+ }
+
+ let penalty = match severity {
+ 1 => PENALTY_LOW,
+ 2 => PENALTY_MEDIUM,
+ 3 => PENALTY_HIGH,
+ _ => return Err(AntiCheatError::InvalidSeverity),
+ };
+
+ let timestamp = env.ledger().timestamp();
+ let confirmation = AntiCheatConfirmation {
+ player: player.clone(),
+ match_id,
+ severity,
+ penalty_applied: penalty,
+ timestamp,
+ oracle: oracle.clone(),
+ };
+ env.storage()
+ .instance()
+ .set(&DataKey::Confirmation(player.clone(), match_id), &confirmation);
+
+ if let Some(reputation_addr) = env
+ .storage()
+ .instance()
+ .get::(&DataKey::ReputationContract)
+ {
+ let mut args = Vec::new(&env);
+ args.push_back(env.current_contract_address().into_val(&env));
+ args.push_back(player.clone().into_val(&env));
+ args.push_back(match_id.into_val(&env));
+ args.push_back(penalty.into_val(&env));
+ let context = ContractContext {
+ contract: reputation_addr.clone(),
+ fn_name: Symbol::new(&env, "apply_anticheat_penalty"),
+ args,
+ };
+ let sub_invocations: Vec = Vec::new(&env);
+ let mut auth_entries = Vec::new(&env);
+ auth_entries.push_back(InvokerContractAuthEntry::Contract(SubContractInvocation {
+ context,
+ sub_invocations,
+ }));
+ env.authorize_as_current_contract(auth_entries);
+ let args = (
+ env.current_contract_address(),
+ player.clone(),
+ match_id,
+ penalty,
+ )
+ .into_val(&env);
+ let _: () = env.invoke_contract(
+ &reputation_addr,
+ &Symbol::new(&env, "apply_anticheat_penalty"),
+ args,
+ );
+ }
+
+ events::emit_anticheat_flag(
+ &env,
+ &player,
+ match_id,
+ severity,
+ penalty,
+ &oracle,
+ timestamp,
+ );
+ Ok(())
+ }
+
+ /// Get the confirmation for a (player, match_id), if any. For consumers and auditing.
+ pub fn get_confirmation(
+ env: Env,
+ player: Address,
+ match_id: u64,
+ ) -> Option {
+ env.storage()
+ .instance()
+ .get(&DataKey::Confirmation(player, match_id))
+ }
+}
+
+#[cfg(test)]
+mod test;
diff --git a/contracts/anti-cheat-oracle/src/storage.rs b/contracts/anti-cheat-oracle/src/storage.rs
new file mode 100644
index 0000000..627b986
--- /dev/null
+++ b/contracts/anti-cheat-oracle/src/storage.rs
@@ -0,0 +1,22 @@
+use soroban_sdk::{contracttype, Address};
+
+#[derive(Clone)]
+#[contracttype]
+pub enum DataKey {
+ Admin,
+ AuthorizedOracle(Address),
+ Confirmation(Address, u64), // (player, match_id) -> AntiCheatConfirmation
+ ReputationContract,
+}
+
+/// Stored confirmation for an anti-cheat flag (queryable and auditable).
+#[contracttype]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct AntiCheatConfirmation {
+ pub player: Address,
+ pub match_id: u64,
+ pub severity: u32,
+ pub penalty_applied: i128,
+ pub timestamp: u64,
+ pub oracle: Address,
+}
diff --git a/contracts/anti-cheat-oracle/src/test.rs b/contracts/anti-cheat-oracle/src/test.rs
new file mode 100644
index 0000000..d40260f
--- /dev/null
+++ b/contracts/anti-cheat-oracle/src/test.rs
@@ -0,0 +1,97 @@
+#![cfg(test)]
+
+use super::*;
+use soroban_sdk::{testutils::Address as _, Address, Env};
+
+#[test]
+fn test_initialize_and_add_oracle() {
+ let env = Env::default();
+ env.mock_all_auths();
+
+ let admin = Address::generate(&env);
+ let oracle = Address::generate(&env);
+
+ let contract_id = env.register(AntiCheatOracle, ());
+ let client = AntiCheatOracleClient::new(&env, &contract_id);
+
+ client.initialize(&admin);
+ assert!(!client.is_authorized_oracle(&oracle));
+
+ client.add_authorized_oracle(&oracle);
+ assert!(client.is_authorized_oracle(&oracle));
+
+ client.remove_authorized_oracle(&oracle);
+ assert!(!client.is_authorized_oracle(&oracle));
+}
+
+#[test]
+fn test_submit_flag_unauthorized() {
+ let env = Env::default();
+ env.mock_all_auths();
+
+ let admin = Address::generate(&env);
+ let unauthorized = Address::generate(&env);
+ let player = Address::generate(&env);
+
+ let contract_id = env.register(AntiCheatOracle, ());
+ let client = AntiCheatOracleClient::new(&env, &contract_id);
+ client.initialize(&admin);
+
+ let result = client.try_submit_flag(&unauthorized, &player, &1u64, &2u32);
+ assert_eq!(result, Err(Ok(AntiCheatError::Unauthorized)));
+}
+
+#[test]
+fn test_submit_flag_invalid_severity() {
+ let env = Env::default();
+ env.mock_all_auths();
+
+ let admin = Address::generate(&env);
+ let oracle = Address::generate(&env);
+ let player = Address::generate(&env);
+
+ let contract_id = env.register(AntiCheatOracle, ());
+ let client = AntiCheatOracleClient::new(&env, &contract_id);
+ client.initialize(&admin);
+ client.add_authorized_oracle(&oracle);
+
+ assert_eq!(
+ client.try_submit_flag(&oracle, &player, &1u64, &0u32),
+ Err(Ok(AntiCheatError::InvalidSeverity))
+ );
+ assert_eq!(
+ client.try_submit_flag(&oracle, &player, &1u64, &4u32),
+ Err(Ok(AntiCheatError::InvalidSeverity))
+ );
+}
+
+#[test]
+fn test_submit_flag_and_get_confirmation() {
+ let env = Env::default();
+ env.mock_all_auths();
+
+ let admin = Address::generate(&env);
+ let oracle = Address::generate(&env);
+ let player = Address::generate(&env);
+ let match_id = 42u64;
+
+ let contract_id = env.register(AntiCheatOracle, ());
+ let client = AntiCheatOracleClient::new(&env, &contract_id);
+ client.initialize(&admin);
+ client.add_authorized_oracle(&oracle);
+
+ assert!(client.get_confirmation(&player, &match_id).is_none());
+
+ client.submit_flag(&oracle, &player, &match_id, &2u32); // severity 2 = medium
+
+ let conf = client.get_confirmation(&player, &match_id).unwrap();
+ assert_eq!(conf.player, player);
+ assert_eq!(conf.match_id, match_id);
+ assert_eq!(conf.severity, 2);
+ assert_eq!(conf.penalty_applied, 15); // PENALTY_MEDIUM
+ assert_eq!(conf.oracle, oracle);
+}
+
+// Integration with Reputation Index is tested by calling submit_flag with
+// set_reputation_contract set: the contract uses invoke_contract to call
+// apply_anticheat_penalty. See reputation-index tests for penalty capping and no underflow.
diff --git a/contracts/anti-cheat-oracle/test_snapshots/test/test_initialize_and_add_oracle.1.json b/contracts/anti-cheat-oracle/test_snapshots/test/test_initialize_and_add_oracle.1.json
new file mode 100644
index 0000000..59f505f
--- /dev/null
+++ b/contracts/anti-cheat-oracle/test_snapshots/test/test_initialize_and_add_oracle.1.json
@@ -0,0 +1,197 @@
+{
+ "generators": {
+ "address": 3,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "function_name": "add_authorized_oracle",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "function_name": "remove_authorized_oracle",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Admin"
+ }
+ ]
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/contracts/anti-cheat-oracle/test_snapshots/test/test_submit_flag_and_get_confirmation.1.json b/contracts/anti-cheat-oracle/test_snapshots/test/test_submit_flag_and_get_confirmation.1.json
new file mode 100644
index 0000000..ab43944
--- /dev/null
+++ b/contracts/anti-cheat-oracle/test_snapshots/test/test_submit_flag_and_get_confirmation.1.json
@@ -0,0 +1,287 @@
+{
+ "generators": {
+ "address": 4,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "function_name": "add_authorized_oracle",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "function_name": "submit_flag",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ },
+ {
+ "u64": "42"
+ },
+ {
+ "u32": 2
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Admin"
+ }
+ ]
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "AuthorizedOracle"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ ]
+ },
+ "val": {
+ "bool": true
+ }
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Confirmation"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ },
+ {
+ "u64": "42"
+ }
+ ]
+ },
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "match_id"
+ },
+ "val": {
+ "u64": "42"
+ }
+ },
+ {
+ "key": {
+ "symbol": "oracle"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ },
+ {
+ "key": {
+ "symbol": "penalty_applied"
+ },
+ "val": {
+ "i128": "15"
+ }
+ },
+ {
+ "key": {
+ "symbol": "player"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ }
+ },
+ {
+ "key": {
+ "symbol": "severity"
+ },
+ "val": {
+ "u32": 2
+ }
+ },
+ {
+ "key": {
+ "symbol": "timestamp"
+ },
+ "val": {
+ "u64": "0"
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/contracts/anti-cheat-oracle/test_snapshots/test/test_submit_flag_invalid_severity.1.json b/contracts/anti-cheat-oracle/test_snapshots/test/test_submit_flag_invalid_severity.1.json
new file mode 100644
index 0000000..c8fe7e4
--- /dev/null
+++ b/contracts/anti-cheat-oracle/test_snapshots/test/test_submit_flag_invalid_severity.1.json
@@ -0,0 +1,159 @@
+{
+ "generators": {
+ "address": 4,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "function_name": "add_authorized_oracle",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Admin"
+ }
+ ]
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "AuthorizedOracle"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ ]
+ },
+ "val": {
+ "bool": true
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/contracts/anti-cheat-oracle/test_snapshots/test/test_submit_flag_unauthorized.1.json b/contracts/anti-cheat-oracle/test_snapshots/test/test_submit_flag_unauthorized.1.json
new file mode 100644
index 0000000..65ac088
--- /dev/null
+++ b/contracts/anti-cheat-oracle/test_snapshots/test/test_submit_flag_unauthorized.1.json
@@ -0,0 +1,91 @@
+{
+ "generators": {
+ "address": 4,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Admin"
+ }
+ ]
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/contracts/auth-gateway/Cargo.toml b/contracts/auth-gateway/Cargo.toml
new file mode 100644
index 0000000..11c73b7
--- /dev/null
+++ b/contracts/auth-gateway/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "auth-gateway"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+repository.workspace = true
+description = "ArenaX Cross-Contract Authorization Gateway - Centralized role management"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+soroban-sdk.workspace = true
+
+[dev-dependencies]
+soroban-sdk = { workspace = true, features = ["testutils"] }
+
+[features]
+testutils = ["soroban-sdk/testutils"]
diff --git a/contracts/auth-gateway/src/lib.rs b/contracts/auth-gateway/src/lib.rs
new file mode 100644
index 0000000..41af4a1
--- /dev/null
+++ b/contracts/auth-gateway/src/lib.rs
@@ -0,0 +1,424 @@
+#![no_std]
+
+use soroban_sdk::{
+ contract, contractevent, contractimpl, contracttype, Address, Env, Map, Symbol,
+};
+
+#[contracttype]
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+#[repr(u32)]
+pub enum Role {
+ None = 0,
+ Admin = 1,
+ Operator = 2,
+ Referee = 3,
+ Player = 4,
+ TournamentManager = 5,
+ Treasury = 6,
+}
+
+#[contracttype]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum DataKey {
+ Admin,
+ Role(Address),
+ ContractWhitelist(Address),
+ Paused,
+}
+
+#[contractevent]
+pub struct Initialized {
+ pub admin: Address,
+}
+
+#[contractevent]
+pub struct RoleAssigned {
+ pub address: Address,
+ pub role: Role,
+ pub assigned_by: Address,
+}
+
+#[contractevent]
+pub struct RoleRevoked {
+ pub address: Address,
+ pub role: Role,
+ pub revoked_by: Address,
+}
+
+#[contractevent]
+pub struct ContractWhitelisted {
+ pub contract_address: Address,
+ pub whitelisted_by: Address,
+}
+
+#[contractevent]
+pub struct ContractRemoved {
+ pub contract_address: Address,
+ pub removed_by: Address,
+}
+
+#[contractevent]
+pub struct ContractPaused {
+ pub paused: bool,
+ pub paused_by: Address,
+}
+
+#[contract]
+pub struct AuthGateway;
+
+#[contractimpl]
+impl AuthGateway {
+ /// Initialize the authorization gateway with an admin address
+ ///
+ /// # Arguments
+ /// * `admin` - The admin address with full control over the gateway
+ ///
+ /// # Panics
+ /// * If contract is already initialized
+ pub fn initialize(env: Env, admin: Address) {
+ if env.storage().instance().has(&DataKey::Admin) {
+ panic!("already initialized");
+ }
+
+ admin.require_auth();
+ env.storage().instance().set(&DataKey::Admin, &admin);
+ env.storage().instance().set(&DataKey::Paused, &false);
+
+ Initialized { admin }.publish(&env);
+ }
+
+ /// Assign a role to an address
+ ///
+ /// # Arguments
+ /// * `address` - The address to assign the role to
+ /// * `role` - The role to assign
+ ///
+ /// # Panics
+ /// * If contract is paused
+ /// * If caller is not admin
+ /// * If trying to assign None role (use revoke_role instead)
+ pub fn assign_role(env: Env, address: Address, role: Role) {
+ Self::require_admin(&env);
+ Self::require_not_paused(&env);
+
+ if role == Role::None {
+ panic!("use revoke_role to remove roles");
+ }
+
+ let admin = env.current_contract_address();
+ let current_role = Self::get_role(&env, address.clone());
+
+ env.storage()
+ .instance()
+ .set(&DataKey::Role(address.clone()), &role);
+
+ RoleAssigned {
+ address,
+ role,
+ assigned_by: admin,
+ }
+ .publish(&env);
+ }
+
+ /// Revoke a role from an address
+ ///
+ /// # Arguments
+ /// * `address` - The address to revoke the role from
+ ///
+ /// # Panics
+ /// * If contract is paused
+ /// * If caller is not admin
+ /// * If address has no role to revoke
+ pub fn revoke_role(env: Env, address: Address) {
+ Self::require_admin(&env);
+ Self::require_not_paused(&env);
+
+ let current_role = Self::get_role(&env, address.clone());
+ if current_role == Role::None {
+ panic!("address has no role to revoke");
+ }
+
+ let admin = env.current_contract_address();
+ env.storage()
+ .instance()
+ .remove(&DataKey::Role(address.clone()));
+
+ RoleRevoked {
+ address,
+ role: current_role,
+ revoked_by: admin,
+ }
+ .publish(&env);
+ }
+
+ /// Whitelist a contract to use the auth gateway
+ ///
+ /// # Arguments
+ /// * `contract_address` - The contract address to whitelist
+ ///
+ /// # Panics
+ /// * If contract is paused
+ /// * If caller is not admin
+ /// * If contract is already whitelisted
+ pub fn whitelist_contract(env: Env, contract_address: Address) {
+ Self::require_admin(&env);
+ Self::require_not_paused(&env);
+
+ if env
+ .storage()
+ .instance()
+ .has(&DataKey::ContractWhitelist(contract_address.clone()))
+ {
+ panic!("contract already whitelisted");
+ }
+
+ let admin = env.current_contract_address();
+ env.storage()
+ .instance()
+ .set(&DataKey::ContractWhitelist(contract_address.clone()), &true);
+
+ ContractWhitelisted {
+ contract_address,
+ whitelisted_by: admin,
+ }
+ .publish(&env);
+ }
+
+ /// Remove a contract from the whitelist
+ ///
+ /// # Arguments
+ /// * `contract_address` - The contract address to remove
+ ///
+ /// # Panics
+ /// * If contract is paused
+ /// * If caller is not admin
+ /// * If contract is not whitelisted
+ pub fn remove_contract(env: Env, contract_address: Address) {
+ Self::require_admin(&env);
+ Self::require_not_paused(&env);
+
+ if !env
+ .storage()
+ .instance()
+ .has(&DataKey::ContractWhitelist(contract_address.clone()))
+ {
+ panic!("contract not whitelisted");
+ }
+
+ let admin = env.current_contract_address();
+ env.storage()
+ .instance()
+ .remove(&DataKey::ContractWhitelist(contract_address.clone()));
+
+ ContractRemoved {
+ contract_address,
+ removed_by: admin,
+ }
+ .publish(&env);
+ }
+
+ /// Pause/unpause the contract
+ ///
+ /// # Arguments
+ /// * `paused` - Whether to pause the contract
+ ///
+ /// # Panics
+ /// * If caller is not admin
+ pub fn set_paused(env: Env, paused: bool) {
+ Self::require_admin(&env);
+ let admin = env.current_contract_address();
+
+ env.storage().instance().set(&DataKey::Paused, &paused);
+
+ ContractPaused {
+ paused,
+ paused_by: admin,
+ }
+ .publish(&env);
+ }
+
+ /// Get the role of an address
+ ///
+ /// # Arguments
+ /// * `address` - The address to check
+ ///
+ /// # Returns
+ /// The role of the address (Role::None if no role assigned)
+ pub fn get_role(env: Env, address: Address) -> Role {
+ env.storage()
+ .instance()
+ .get(&DataKey::Role(address))
+ .unwrap_or(Role::None)
+ }
+
+ /// Check if an address has a specific role
+ ///
+ /// # Arguments
+ /// * `address` - The address to check
+ /// * `role` - The role to check for
+ ///
+ /// # Returns
+ /// True if the address has the role, false otherwise
+ pub fn has_role(env: Env, address: Address, role: Role) -> bool {
+ let user_role = Self::get_role(env, address);
+ user_role == role
+ }
+
+ /// Check if an address has any of the specified roles
+ ///
+ /// # Arguments
+ /// * `address` - The address to check
+ /// * `roles` - Array of roles to check against
+ ///
+ /// # Returns
+ /// True if the address has any of the roles, false otherwise
+ pub fn has_any_role(env: Env, address: Address, roles: Vec) -> bool {
+ let user_role = Self::get_role(env, address);
+ roles.iter().any(|&role| user_role == role)
+ }
+
+ /// Check if a contract is whitelisted
+ ///
+ /// # Arguments
+ /// * `contract_address` - The contract address to check
+ ///
+ /// # Returns
+ /// True if the contract is whitelisted, false otherwise
+ pub fn is_contract_whitelisted(env: Env, contract_address: Address) -> bool {
+ env.storage()
+ .instance()
+ .has(&DataKey::ContractWhitelist(contract_address))
+ }
+
+ /// Get the admin address
+ ///
+ /// # Returns
+ /// The admin address
+ ///
+ /// # Panics
+ /// * If contract is not initialized
+ pub fn get_admin(env: Env) -> Address {
+ env.storage()
+ .instance()
+ .get(&DataKey::Admin)
+ .expect("not initialized")
+ }
+
+ /// Check if the contract is paused
+ ///
+ /// # Returns
+ /// True if the contract is paused, false otherwise
+ pub fn is_paused(env: Env) -> bool {
+ env.storage()
+ .instance()
+ .get(&DataKey::Paused)
+ .unwrap_or(false)
+ }
+
+ /// Batch role assignment for multiple addresses
+ ///
+ /// # Arguments
+ /// * `addresses` - Array of addresses to assign roles to
+ /// * `roles` - Array of roles corresponding to the addresses
+ ///
+ /// # Panics
+ /// * If contract is paused
+ /// * If caller is not admin
+ /// * If arrays have different lengths
+ /// * If any role is None
+ pub fn batch_assign_roles(env: Env, addresses: Vec, roles: Vec) {
+ Self::require_admin(&env);
+ Self::require_not_paused(&env);
+
+ if addresses.len() != roles.len() {
+ panic!("addresses and roles arrays must have same length");
+ }
+
+ for (i, address) in addresses.iter().enumerate() {
+ let role = roles.get(i).unwrap();
+ if *role == Role::None {
+ panic!("use revoke_role to remove roles");
+ }
+
+ env.storage()
+ .instance()
+ .set(&DataKey::Role(address.clone()), role);
+
+ RoleAssigned {
+ address: address.clone(),
+ role: *role,
+ assigned_by: env.current_contract_address(),
+ }
+ .publish(&env);
+ }
+ }
+
+ /// Get all addresses with a specific role
+ ///
+ /// # Arguments
+ /// * `role` - The role to search for
+ ///
+ /// # Returns
+ /// Array of addresses that have the specified role
+ ///
+ /// # Note
+ /// This is an expensive operation and should be used sparingly
+ pub fn get_addresses_with_role(env: Env, role: Role) -> Vec {
+ // In a real implementation, you might want to maintain
+ // reverse indexes for better performance
+ // For now, this is a placeholder that would need
+ // additional storage structures to work efficiently
+ Vec::new(&env)
+ }
+
+ /// Transfer admin role to a new address
+ ///
+ /// # Arguments
+ /// * `new_admin` - The new admin address
+ ///
+ /// # Panics
+ /// * If caller is not current admin
+ /// * If new_admin already has a role (must be None first)
+ pub fn transfer_admin(env: Env, new_admin: Address) {
+ let current_admin = Self::get_admin(env.clone());
+ current_admin.require_auth();
+
+ let new_admin_role = Self::get_role(env.clone(), new_admin.clone());
+ if new_admin_role != Role::None {
+ panic!("new admin must have no existing role");
+ }
+
+ env.storage().instance().set(&DataKey::Admin, &new_admin);
+ env.storage()
+ .instance()
+ .set(&DataKey::Role(new_admin.clone()), &Role::Admin);
+
+ RoleAssigned {
+ address: new_admin.clone(),
+ role: Role::Admin,
+ assigned_by: current_admin,
+ }
+ .publish(&env);
+
+ RoleRevoked {
+ address: current_admin,
+ role: Role::Admin,
+ revoked_by: new_admin,
+ }
+ .publish(&env);
+ }
+
+ // Helper functions for internal use
+
+ fn require_admin(env: &Env) {
+ let admin = Self::get_admin(env.clone());
+ admin.require_auth();
+ }
+
+ fn require_not_paused(env: &Env) {
+ let paused = Self::is_paused(env.clone());
+ if paused {
+ panic!("contract is paused");
+ }
+ }
+}
diff --git a/contracts/auth-gateway/src/test.rs b/contracts/auth-gateway/src/test.rs
new file mode 100644
index 0000000..5889461
--- /dev/null
+++ b/contracts/auth-gateway/src/test.rs
@@ -0,0 +1,454 @@
+#![cfg(test)]
+
+use super::*;
+use soroban_sdk::{
+ testutils::{Address as _, Ledger as _},
+ Address, Env, Vec,
+};
+
+fn create_test_env() -> (Env, Address, Address, Address, Address) {
+ let env = Env::default();
+ let admin = Address::generate(&env);
+ let operator = Address::generate(&env);
+ let referee = Address::generate(&env);
+ let player = Address::generate(&env);
+ (env, admin, operator, referee, player)
+}
+
+fn initialize_contract(env: &Env, admin: &Address) -> Address {
+ let contract_id = Address::generate(env);
+ env.register_contract(&contract_id, AuthGateway);
+ let client = AuthGatewayClient::new(env, &contract_id);
+
+ env.mock_all_auths();
+ client.initialize(admin);
+
+ contract_id
+}
+
+#[test]
+fn test_initialization() {
+ let (env, admin, _, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ assert_eq!(client.get_admin(), admin);
+ assert!(!client.is_paused());
+}
+
+#[test]
+#[should_panic(expected = "already initialized")]
+fn test_double_initialization() {
+ let (env, admin, _, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ client.initialize(&admin);
+}
+
+#[test]
+fn test_assign_role() {
+ let (env, admin, operator, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+ client.assign_role(&operator, Role::Operator);
+
+ assert_eq!(client.get_role(&operator), Role::Operator);
+ assert!(client.has_role(&operator, Role::Operator));
+ assert!(!client.has_role(&operator, Role::Admin));
+}
+
+#[test]
+#[should_panic(expected = "use revoke_role to remove roles")]
+fn test_assign_none_role_fails() {
+ let (env, admin, operator, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+ client.assign_role(&operator, Role::None);
+}
+
+#[test]
+#[should_panic(expected = "already initialized")]
+fn test_assign_role_unauthorized() {
+ let (env, admin, operator, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ client.assign_role(&operator, Role::Operator);
+}
+
+#[test]
+fn test_revoke_role() {
+ let (env, admin, operator, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+ client.assign_role(&operator, Role::Operator);
+ assert_eq!(client.get_role(&operator), Role::Operator);
+
+ client.revoke_role(&operator);
+ assert_eq!(client.get_role(&operator), Role::None);
+ assert!(!client.has_role(&operator, Role::Operator));
+}
+
+#[test]
+#[should_panic(expected = "address has no role to revoke")]
+fn test_revoke_nonexistent_role_fails() {
+ let (env, admin, operator, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+ client.revoke_role(&operator);
+}
+
+#[test]
+#[should_panic(expected = "already initialized")]
+fn test_revoke_role_unauthorized() {
+ let (env, admin, operator, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+ client.assign_role(&operator, Role::Operator);
+
+ client.revoke_role(&operator);
+}
+
+#[test]
+fn test_whitelist_contract() {
+ let (env, admin, _, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ let target_contract = Address::generate(&env);
+
+ env.mock_all_auths();
+ client.whitelist_contract(&target_contract);
+
+ assert!(client.is_contract_whitelisted(&target_contract));
+}
+
+#[test]
+#[should_panic(expected = "contract already whitelisted")]
+fn test_whitelist_duplicate_contract_fails() {
+ let (env, admin, _, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ let target_contract = Address::generate(&env);
+
+ env.mock_all_auths();
+ client.whitelist_contract(&target_contract);
+ client.whitelist_contract(&target_contract);
+}
+
+#[test]
+#[should_panic(expected = "already initialized")]
+fn test_whitelist_contract_unauthorized() {
+ let (env, admin, _, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ let target_contract = Address::generate(&env);
+ client.whitelist_contract(&target_contract);
+}
+
+#[test]
+fn test_remove_contract() {
+ let (env, admin, _, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ let target_contract = Address::generate(&env);
+
+ env.mock_all_auths();
+ client.whitelist_contract(&target_contract);
+ assert!(client.is_contract_whitelisted(&target_contract));
+
+ client.remove_contract(&target_contract);
+ assert!(!client.is_contract_whitelisted(&target_contract));
+}
+
+#[test]
+#[should_panic(expected = "contract not whitelisted")]
+fn test_remove_non_whitelisted_contract_fails() {
+ let (env, admin, _, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ let target_contract = Address::generate(&env);
+
+ env.mock_all_auths();
+ client.remove_contract(&target_contract);
+}
+
+#[test]
+fn test_pause_contract() {
+ let (env, admin, _, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+
+ assert!(!client.is_paused());
+ client.set_paused(&true);
+ assert!(client.is_paused());
+ client.set_paused(&false);
+ assert!(!client.is_paused());
+}
+
+#[test]
+#[should_panic(expected = "already initialized")]
+fn test_pause_contract_unauthorized() {
+ let (env, admin, _, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ client.set_paused(&true);
+}
+
+#[test]
+#[should_panic(expected = "contract is paused")]
+fn test_operations_when_paused() {
+ let (env, admin, operator, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+ client.set_paused(&true);
+
+ client.assign_role(&operator, Role::Operator);
+}
+
+#[test]
+fn test_has_any_role() {
+ let (env, admin, operator, referee, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+
+ client.assign_role(&operator, Role::Operator);
+ client.assign_role(&referee, Role::Referee);
+
+ let roles = vec![&env, Role::Operator, Role::Referee];
+ assert!(client.has_any_role(&operator, roles.clone()));
+ assert!(client.has_any_role(&referee, roles));
+
+ let admin_roles = vec![&env, Role::Admin, Role::Operator];
+ assert!(!client.has_any_role(&operator, admin_roles));
+}
+
+#[test]
+fn test_batch_assign_roles() {
+ let (env, admin, operator, referee, player) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+
+ let addresses = vec![&env, operator.clone(), referee.clone(), player.clone()];
+ let roles = vec![&env, Role::Operator, Role::Referee, Role::Player];
+
+ client.batch_assign_roles(addresss, roles);
+
+ assert_eq!(client.get_role(&operator), Role::Operator);
+ assert_eq!(client.get_role(&referee), Role::Referee);
+ assert_eq!(client.get_role(&player), Role::Player);
+}
+
+#[test]
+#[should_panic(expected = "addresses and roles arrays must have same length")]
+fn test_batch_assign_roles_mismatched_length_fails() {
+ let (env, admin, operator, referee, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+
+ let addresses = vec![&env, operator.clone(), referee.clone()];
+ let roles = vec![&env, Role::Operator];
+
+ client.batch_assign_roles(addresses, roles);
+}
+
+#[test]
+#[should_panic(expected = "use revoke_role to remove roles")]
+fn test_batch_assign_roles_with_none_fails() {
+ let (env, admin, operator, referee, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+
+ let addresses = vec![&env, operator.clone(), referee.clone()];
+ let roles = vec![&env, Role::Operator, Role::None];
+
+ client.batch_assign_roles(addresses, roles);
+}
+
+#[test]
+fn test_transfer_admin() {
+ let (env, admin, operator, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+
+ assert_eq!(client.get_admin(), admin);
+ client.transfer_admin(&operator);
+
+ assert_eq!(client.get_admin(), operator);
+ assert_eq!(client.get_role(&operator), Role::Admin);
+ assert_eq!(client.get_role(&admin), Role::None);
+}
+
+#[test]
+#[should_panic(expected = "new admin must have no existing role")]
+fn test_transfer_admin_with_existing_role_fails() {
+ let (env, admin, operator, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+
+ client.assign_role(&operator, Role::Operator);
+ client.transfer_admin(&operator);
+}
+
+#[test]
+#[should_panic(expected = "already initialized")]
+fn test_transfer_admin_unauthorized() {
+ let (env, admin, operator, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ client.transfer_admin(&operator);
+}
+
+#[test]
+fn test_all_role_types() {
+ let (env, admin, operator, referee, player) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+
+ client.assign_role(&operator, Role::Operator);
+ client.assign_role(&referee, Role::Referee);
+ client.assign_role(&player, Role::Player);
+
+ assert_eq!(client.get_role(&admin), Role::Admin);
+ assert_eq!(client.get_role(&operator), Role::Operator);
+ assert_eq!(client.get_role(&referee), Role::Referee);
+ assert_eq!(client.get_role(&player), Role::Player);
+
+ assert!(client.has_role(&admin, Role::Admin));
+ assert!(client.has_role(&operator, Role::Operator));
+ assert!(client.has_role(&referee, Role::Referee));
+ assert!(client.has_role(&player, Role::Player));
+
+ assert!(!client.has_role(&admin, Role::Operator));
+ assert!(!client.has_role(&operator, Role::Referee));
+ assert!(!client.has_role(&referee, Role::Player));
+ assert!(!client.has_role(&player, Role::Admin));
+}
+
+#[test]
+fn test_role_overwrite() {
+ let (env, admin, operator, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+
+ client.assign_role(&operator, Role::Operator);
+ assert_eq!(client.get_role(&operator), Role::Operator);
+
+ client.assign_role(&operator, Role::Referee);
+ assert_eq!(client.get_role(&operator), Role::Referee);
+ assert!(!client.has_role(&operator, Role::Operator));
+ assert!(client.has_role(&operator, Role::Referee));
+}
+
+#[test]
+fn test_multiple_contracts_whitelisting() {
+ let (env, admin, _, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ let contract1 = Address::generate(&env);
+ let contract2 = Address::generate(&env);
+ let contract3 = Address::generate(&env);
+
+ env.mock_all_auths();
+
+ client.whitelist_contract(&contract1);
+ client.whitelist_contract(&contract2);
+ client.whitelist_contract(&contract3);
+
+ assert!(client.is_contract_whitelisted(&contract1));
+ assert!(client.is_contract_whitelisted(&contract2));
+ assert!(client.is_contract_whitelisted(&contract3));
+
+ client.remove_contract(&contract2);
+ assert!(client.is_contract_whitelisted(&contract1));
+ assert!(!client.is_contract_whitelisted(&contract2));
+ assert!(client.is_contract_whitelisted(&contract3));
+}
+
+#[test]
+fn test_cross_contract_integration_simulation() {
+ let (env, admin, operator, referee, player) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ let match_contract = Address::generate(&env);
+ let prize_contract = Address::generate(&env);
+
+ env.mock_all_auths();
+
+ client.whitelist_contract(&match_contract);
+ client.whitelist_contract(&prize_contract);
+
+ client.assign_role(&operator, Role::Operator);
+ client.assign_role(&referee, Role::Referee);
+ client.assign_role(&player, Role::Player);
+
+ assert!(client.is_contract_whitelisted(&match_contract));
+ assert!(client.is_contract_whitelisted(&prize_contract));
+
+ let resolver_roles = vec![&env, Role::Admin, Role::Referee];
+ assert!(client.has_any_role(&referee, resolver_roles));
+ assert!(!client.has_any_role(&player, resolver_roles));
+
+ let participant_roles = vec![&env, Role::Operator, Role::Player];
+ assert!(client.has_any_role(&operator, participant_roles));
+ assert!(client.has_any_role(&player, participant_roles));
+ assert!(!client.has_any_role(&referee, participant_roles));
+}
+
+#[test]
+fn test_edge_cases() {
+ let (env, admin, operator, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AuthGatewayClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+
+ let non_existent_address = Address::generate(&env);
+ assert_eq!(client.get_role(&non_existent_address), Role::None);
+ assert!(!client.has_role(&non_existent_address, Role::Admin));
+
+ let non_whitelisted_contract = Address::generate(&env);
+ assert!(!client.is_contract_whitelisted(&non_whitelisted_contract));
+
+ let empty_roles = Vec::new(&env);
+ assert!(!client.has_any_role(&operator, empty_roles));
+}
diff --git a/contracts/ax-token/Cargo.toml b/contracts/ax-token/Cargo.toml
new file mode 100644
index 0000000..e1b1b17
--- /dev/null
+++ b/contracts/ax-token/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "ax-token"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+repository.workspace = true
+description = "ArenaX Token (AX) - Native governance and utility token"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+soroban-sdk.workspace = true
+
+[dev-dependencies]
+soroban-sdk = { workspace = true, features = ["testutils"] }
+
+[features]
+testutils = ["soroban-sdk/testutils"]
diff --git a/contracts/ax-token/src/lib.rs b/contracts/ax-token/src/lib.rs
new file mode 100644
index 0000000..e31e880
--- /dev/null
+++ b/contracts/ax-token/src/lib.rs
@@ -0,0 +1,172 @@
+#![no_std]
+
+use soroban_sdk::{
+ contracttype,
+ contractimpl,
+ contractevent,
+ Address,
+ Env,
+ Map,
+ Vec
+};
+
+#[derive(Clone)]
+#[contracttype]
+pub enum DataKey {
+ Admin,
+ Balance(Address),
+ TotalSupply,
+}
+
+#[contractevent]
+pub struct MintEvent {
+ pub to: Address,
+ pub amount: i128,
+}
+
+#[contractevent]
+pub struct BurnEvent {
+ pub from: Address,
+ pub amount: i128,
+}
+
+#[contractevent]
+pub struct TransferEvent {
+ pub from: Address,
+ pub to: Address,
+ pub amount: i128,
+}
+
+pub struct AxToken;
+
+#[contractimpl]
+impl AxToken {
+ pub fn initialize(env: &Env, admin: Address) {
+ if Self::has_admin(env) {
+ panic!("already initialized");
+ }
+
+ env.storage().instance().set(&DataKey::Admin, &admin);
+ env.storage().instance().set(&DataKey::TotalSupply, &0i128);
+ }
+
+ pub fn mint(env: &Env, to: Address, amount: i128) {
+ Self::require_admin(env);
+
+ if amount <= 0 {
+ panic!("amount must be positive");
+ }
+
+ let current_balance = Self::balance(env, to.clone());
+ let new_balance = current_balance + amount;
+ env.storage().instance().set(&DataKey::Balance(to.clone()), &new_balance);
+
+ let current_supply = Self::total_supply(env);
+ let new_supply = current_supply + amount;
+ env.storage().instance().set(&DataKey::TotalSupply, &new_supply);
+
+ env.events().publish(
+ MintEvent {
+ to,
+ amount,
+ }
+ );
+ }
+
+ pub fn burn(env: &Env, from: Address, amount: i128) {
+ Self::require_admin(env);
+
+ if amount <= 0 {
+ panic!("amount must be positive");
+ }
+
+ let current_balance = Self::balance(env, from.clone());
+ if current_balance < amount {
+ panic!("insufficient balance");
+ }
+
+ let new_balance = current_balance - amount;
+ env.storage().instance().set(&DataKey::Balance(from.clone()), &new_balance);
+
+ let current_supply = Self::total_supply(env);
+ let new_supply = current_supply - amount;
+ env.storage().instance().set(&DataKey::TotalSupply, &new_supply);
+
+ env.events().publish(
+ BurnEvent {
+ from,
+ amount,
+ }
+ );
+ }
+
+ pub fn transfer(env: &Env, from: Address, to: Address, amount: i128) {
+ from.require_auth();
+
+ if amount <= 0 {
+ panic!("amount must be positive");
+ }
+
+ if from == to {
+ panic!("cannot transfer to self");
+ }
+
+ let from_balance = Self::balance(env, from.clone());
+ if from_balance < amount {
+ panic!("insufficient balance");
+ }
+
+ let new_from_balance = from_balance - amount;
+ env.storage().instance().set(&DataKey::Balance(from.clone()), &new_from_balance);
+
+ let to_balance = Self::balance(env, to.clone());
+ let new_to_balance = to_balance + amount;
+ env.storage().instance().set(&DataKey::Balance(to.clone()), &new_to_balance);
+
+ env.events().publish(
+ TransferEvent {
+ from,
+ to,
+ amount,
+ }
+ );
+ }
+
+ pub fn balance(env: &Env, addr: Address) -> i128 {
+ env.storage()
+ .instance()
+ .get(&DataKey::Balance(addr))
+ .unwrap_or(0)
+ }
+
+ pub fn total_supply(env: &Env) -> i128 {
+ env.storage()
+ .instance()
+ .get(&DataKey::TotalSupply)
+ .unwrap_or(0)
+ }
+
+ pub fn get_admin(env: &Env) -> Address {
+ env.storage()
+ .instance()
+ .get(&DataKey::Admin)
+ .unwrap()
+ }
+
+ pub fn set_admin(env: &Env, new_admin: Address) {
+ Self::require_admin(env);
+ env.storage().instance().set(&DataKey::Admin, &new_admin);
+ }
+
+ fn has_admin(env: &Env) -> bool {
+ env.storage()
+ .instance()
+ .get::(&DataKey::Admin)
+ .is_some()
+ }
+
+ fn require_admin(env: &Env) {
+ let admin = Self::get_admin(env);
+ admin.require_auth();
+ }
+}
diff --git a/contracts/ax-token/src/test.rs b/contracts/ax-token/src/test.rs
new file mode 100644
index 0000000..505eeb5
--- /dev/null
+++ b/contracts/ax-token/src/test.rs
@@ -0,0 +1,317 @@
+#![cfg(test)]
+
+use super::*;
+use soroban_sdk::{
+ testutils::{Address as _, Ledger as _},
+ Address, Env,
+};
+
+fn create_test_env() -> (Env, Address, Address, Address) {
+ let env = Env::default();
+ let admin = Address::generate(&env);
+ let user1 = Address::generate(&env);
+ let user2 = Address::generate(&env);
+ (env, admin, user1, user2)
+}
+
+fn initialize_contract(env: &Env, admin: &Address) -> Address {
+ let contract_id = Address::generate(env);
+ env.register_contract(&contract_id, AxToken);
+ let client = AxTokenClient::new(env, &contract_id);
+ client.initialize(admin);
+ contract_id
+}
+
+#[test]
+fn test_initialization() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AxTokenClient::new(&env, &contract_id);
+
+ assert_eq!(client.get_admin(), admin);
+ assert_eq!(client.total_supply(), 0);
+ assert_eq!(client.balance(&user1), 0);
+ assert_eq!(client.balance(&user2), 0);
+}
+
+#[test]
+#[should_panic(expected = "already initialized")]
+fn test_double_initialization() {
+ let (env, admin, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AxTokenClient::new(&env, &contract_id);
+
+ client.initialize(&admin);
+}
+
+#[test]
+fn test_mint() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AxTokenClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+
+ client.mint(&user1, 1000);
+ assert_eq!(client.balance(&user1), 1000);
+ assert_eq!(client.total_supply(), 1000);
+
+ client.mint(&user2, 500);
+ assert_eq!(client.balance(&user2), 500);
+ assert_eq!(client.total_supply(), 1500);
+}
+
+#[test]
+#[should_panic(expected = "amount must be positive")]
+fn test_mint_zero_amount() {
+ let (env, admin, user1, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AxTokenClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+ client.mint(&user1, 0);
+}
+
+#[test]
+#[should_panic(expected = "amount must be positive")]
+fn test_mint_negative_amount() {
+ let (env, admin, user1, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AxTokenClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+ client.mint(&user1, -100);
+}
+
+#[test]
+#[should_panic(expected = "already initialized")]
+fn test_mint_unauthorized() {
+ let (env, admin, user1, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AxTokenClient::new(&env, &contract_id);
+
+ client.mint(&user1, 1000);
+}
+
+#[test]
+fn test_burn() {
+ let (env, admin, user1, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AxTokenClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+
+ client.mint(&user1, 1000);
+ assert_eq!(client.balance(&user1), 1000);
+ assert_eq!(client.total_supply(), 1000);
+
+ client.burn(&user1, 300);
+ assert_eq!(client.balance(&user1), 700);
+ assert_eq!(client.total_supply(), 700);
+}
+
+#[test]
+#[should_panic(expected = "insufficient balance")]
+fn test_burn_insufficient_balance() {
+ let (env, admin, user1, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AxTokenClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+
+ client.mint(&user1, 1000);
+ client.burn(&user1, 1500);
+}
+
+#[test]
+#[should_panic(expected = "amount must be positive")]
+fn test_burn_zero_amount() {
+ let (env, admin, user1, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AxTokenClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+
+ client.mint(&user1, 1000);
+ client.burn(&user1, 0);
+}
+
+#[test]
+#[should_panic(expected = "already initialized")]
+fn test_burn_unauthorized() {
+ let (env, admin, user1, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AxTokenClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+ client.mint(&user1, 1000);
+
+ client.burn(&user1, 100);
+}
+
+#[test]
+fn test_transfer() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AxTokenClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+
+ client.mint(&user1, 1000);
+ client.mint(&user2, 500);
+
+ client.transfer(&user1, &user2, 300);
+ assert_eq!(client.balance(&user1), 700);
+ assert_eq!(client.balance(&user2), 800);
+ assert_eq!(client.total_supply(), 1500);
+}
+
+#[test]
+#[should_panic(expected = "insufficient balance")]
+fn test_transfer_insufficient_balance() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AxTokenClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+
+ client.mint(&user1, 1000);
+ client.transfer(&user1, &user2, 1500);
+}
+
+#[test]
+#[should_panic(expected = "amount must be positive")]
+fn test_transfer_zero_amount() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AxTokenClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+
+ client.mint(&user1, 1000);
+ client.transfer(&user1, &user2, 0);
+}
+
+#[test]
+#[should_panic(expected = "cannot transfer to self")]
+fn test_transfer_to_self() {
+ let (env, admin, user1, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AxTokenClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+
+ client.mint(&user1, 1000);
+ client.transfer(&user1, &user1, 100);
+}
+
+#[test]
+#[should_panic(expected = "already initialized")]
+fn test_transfer_unauthorized() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AxTokenClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+
+ client.mint(&user1, 1000);
+ client.mint(&user2, 500);
+
+ client.transfer(&user1, &user2, 100);
+}
+
+#[test]
+fn test_set_admin() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AxTokenClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+
+ assert_eq!(client.get_admin(), admin);
+ client.set_admin(&user1);
+ assert_eq!(client.get_admin(), user1);
+}
+
+#[test]
+#[should_panic(expected = "already initialized")]
+fn test_set_admin_unauthorized() {
+ let (env, admin, user1, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AxTokenClient::new(&env, &contract_id);
+
+ client.set_admin(&user1);
+}
+
+#[test]
+fn test_full_lifecycle() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AxTokenClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+
+ client.mint(&user1, 1000);
+ client.mint(&user2, 1000);
+ assert_eq!(client.total_supply(), 2000);
+
+ client.transfer(&user1, &user2, 300);
+ assert_eq!(client.balance(&user1), 700);
+ assert_eq!(client.balance(&user2), 1300);
+
+ client.burn(&user1, 200);
+ client.burn(&user2, 400);
+ assert_eq!(client.balance(&user1), 500);
+ assert_eq!(client.balance(&user2), 900);
+ assert_eq!(client.total_supply(), 1400);
+}
+
+#[test]
+fn test_large_amounts() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AxTokenClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+
+ let large_amount = i128::MAX / 4;
+ client.mint(&user1, large_amount);
+ client.mint(&user2, large_amount);
+
+ assert_eq!(client.total_supply(), large_amount * 2);
+ assert_eq!(client.balance(&user1), large_amount);
+ assert_eq!(client.balance(&user2), large_amount);
+
+ client.transfer(&user1, &user2, large_amount / 2);
+ assert_eq!(client.balance(&user1), large_amount / 2);
+ assert_eq!(client.balance(&user2), large_amount * 3 / 2);
+}
+
+#[test]
+fn test_multiple_users() {
+ let (env, admin, user1, user2) = create_test_env();
+ let user3 = Address::generate(&env);
+ let user4 = Address::generate(&env);
+ let contract_id = initialize_contract(&env, &admin);
+ let client = AxTokenClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+
+ let users = vec![user1.clone(), user2.clone(), user3.clone(), user4.clone()];
+ let amounts = vec![1000, 2000, 3000, 4000];
+
+ for (i, user) in users.iter().enumerate() {
+ client.mint(user, amounts[i]);
+ }
+
+ assert_eq!(client.total_supply(), 10000);
+
+ client.transfer(&user1, &user2, 500);
+ client.transfer(&user3, &user4, 1000);
+
+ assert_eq!(client.balance(&user1), 500);
+ assert_eq!(client.balance(&user2), 2500);
+ assert_eq!(client.balance(&user3), 2000);
+ assert_eq!(client.balance(&user4), 5000);
+ assert_eq!(client.total_supply(), 10000);
+}
diff --git a/contracts/contract-registry/Cargo.toml b/contracts/contract-registry/Cargo.toml
new file mode 100644
index 0000000..19a1076
--- /dev/null
+++ b/contracts/contract-registry/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "contract-registry"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+repository.workspace = true
+description = "ArenaX Contract Registry - Centralized contract registration and lookup"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+soroban-sdk.workspace = true
+
+[dev-dependencies]
+soroban-sdk = { workspace = true, features = ["testutils"] }
+
+[features]
+testutils = ["soroban-sdk/testutils"]
diff --git a/contracts/contract-registry/src/lib.rs b/contracts/contract-registry/src/lib.rs
new file mode 100644
index 0000000..1bf8d04
--- /dev/null
+++ b/contracts/contract-registry/src/lib.rs
@@ -0,0 +1,497 @@
+#![no_std]
+
+use soroban_sdk::{
+ contract, contractevent, contractimpl, contracttype, Address, Env, Map, Symbol, Vec,
+};
+
+#[contracttype]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum DataKey {
+ Admin,
+ Contract(Symbol),
+ ContractList,
+ Paused,
+}
+
+#[contracttype]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct ContractInfo {
+ pub address: Address,
+ pub name: Symbol,
+ pub registered_at: u64,
+ pub updated_at: Option,
+ pub registered_by: Address,
+}
+
+#[contractevent]
+pub struct Initialized {
+ pub admin: Address,
+}
+
+#[contractevent]
+pub struct ContractRegistered {
+ pub name: Symbol,
+ pub address: Address,
+ pub registered_by: Address,
+}
+
+#[contractevent]
+pub struct ContractUpdated {
+ pub name: Symbol,
+ pub old_address: Address,
+ pub new_address: Address,
+ pub updated_by: Address,
+}
+
+#[contractevent]
+pub struct ContractRemoved {
+ pub name: Symbol,
+ pub address: Address,
+ pub removed_by: Address,
+}
+
+#[contractevent]
+pub struct RegistryPaused {
+ pub paused: bool,
+ pub paused_by: Address,
+}
+
+#[contract]
+pub struct ContractRegistry;
+
+#[contractimpl]
+impl ContractRegistry {
+ /// Initialize the contract registry with an admin address
+ ///
+ /// # Arguments
+ /// * `admin` - The admin address with full control over the registry
+ ///
+ /// # Panics
+ /// * If contract is already initialized
+ pub fn initialize(env: Env, admin: Address) {
+ if env.storage().instance().has(&DataKey::Admin) {
+ panic!("already initialized");
+ }
+
+ admin.require_auth();
+ env.storage().instance().set(&DataKey::Admin, &admin);
+ env.storage().instance().set(&DataKey::Paused, &false);
+ env.storage().instance().set(&DataKey::ContractList, &Vec::::new(&env));
+
+ Initialized { admin }.publish(&env);
+ }
+
+ /// Register a new contract with a unique name
+ ///
+ /// # Arguments
+ /// * `name` - Unique identifier for the contract
+ /// * `address` - The contract address to register
+ ///
+ /// # Panics
+ /// * If contract is paused
+ /// * If caller is not admin
+ /// * If name is already registered
+ /// * If name is empty
+ pub fn register_contract(env: Env, name: Symbol, address: Address) {
+ Self::require_admin(&env);
+ Self::require_not_paused(&env);
+
+ if name.is_empty() {
+ panic!("contract name cannot be empty");
+ }
+
+ if env.storage().instance().has(&DataKey::Contract(name.clone())) {
+ panic!("contract name already registered");
+ }
+
+ let contract_info = ContractInfo {
+ address: address.clone(),
+ name: name.clone(),
+ registered_at: env.ledger().timestamp(),
+ updated_at: None,
+ registered_by: env.current_contract_address(),
+ };
+
+ env.storage()
+ .instance()
+ .set(&DataKey::Contract(name.clone()), &contract_info);
+
+ let mut contract_list: Vec = env
+ .storage()
+ .instance()
+ .get(&DataKey::ContractList)
+ .unwrap_or(Vec::new(&env));
+
+ contract_list.push_back(name.clone());
+ env.storage()
+ .instance()
+ .set(&DataKey::ContractList, &contract_list);
+
+ ContractRegistered {
+ name,
+ address,
+ registered_by: env.current_contract_address(),
+ }
+ .publish(&env);
+ }
+
+ /// Update an existing contract's address
+ ///
+ /// # Arguments
+ /// * `name` - The name of the contract to update
+ /// * `new_address` - The new contract address
+ ///
+ /// # Panics
+ /// * If contract is paused
+ /// * If caller is not admin
+ /// * If contract name is not registered
+ /// * If new address is the same as current address
+ pub fn update_contract(env: Env, name: Symbol, new_address: Address) {
+ Self::require_admin(&env);
+ Self::require_not_paused(&env);
+
+ let mut contract_info: ContractInfo = env
+ .storage()
+ .instance()
+ .get(&DataKey::Contract(name.clone()))
+ .expect("contract not registered");
+
+ if contract_info.address == new_address {
+ panic!("new address is the same as current address");
+ }
+
+ let old_address = contract_info.address.clone();
+ contract_info.address = new_address.clone();
+ contract_info.updated_at = Some(env.ledger().timestamp());
+
+ env.storage()
+ .instance()
+ .set(&DataKey::Contract(name.clone()), &contract_info);
+
+ ContractUpdated {
+ name,
+ old_address,
+ new_address,
+ updated_by: env.current_contract_address(),
+ }
+ .publish(&env);
+ }
+
+ /// Remove a contract from the registry
+ ///
+ /// # Arguments
+ /// * `name` - The name of the contract to remove
+ ///
+ /// # Panics
+ /// * If contract is paused
+ /// * If caller is not admin
+ /// * If contract name is not registered
+ pub fn remove_contract(env: Env, name: Symbol) {
+ Self::require_admin(&env);
+ Self::require_not_paused(&env);
+
+ let contract_info: ContractInfo = env
+ .storage()
+ .instance()
+ .get(&DataKey::Contract(name.clone()))
+ .expect("contract not registered");
+
+ let address = contract_info.address.clone();
+
+ env.storage()
+ .instance()
+ .remove(&DataKey::Contract(name.clone()));
+
+ let mut contract_list: Vec = env
+ .storage()
+ .instance()
+ .get(&DataKey::ContractList)
+ .unwrap_or(Vec::new(&env));
+
+ let index = contract_list.iter().position(|&item| item == name);
+ if let Some(idx) = index {
+ contract_list.remove(idx);
+ env.storage()
+ .instance()
+ .set(&DataKey::ContractList, &contract_list);
+ }
+
+ ContractRemoved {
+ name,
+ address,
+ removed_by: env.current_contract_address(),
+ }
+ .publish(&env);
+ }
+
+ /// Get the address of a registered contract
+ ///
+ /// # Arguments
+ /// * `name` - The name of the contract to look up
+ ///
+ /// # Returns
+ /// The contract address
+ ///
+ /// # Panics
+ /// * If contract name is not registered
+ pub fn get_contract(env: Env, name: Symbol) -> Address {
+ let contract_info: ContractInfo = env
+ .storage()
+ .instance()
+ .get(&DataKey::Contract(name))
+ .expect("contract not registered");
+ contract_info.address
+ }
+
+ /// Get detailed information about a registered contract
+ ///
+ /// # Arguments
+ /// * `name` - The name of the contract to look up
+ ///
+ /// # Returns
+ /// The contract information including metadata
+ ///
+ /// # Panics
+ /// * If contract name is not registered
+ pub fn get_contract_info(env: Env, name: Symbol) -> ContractInfo {
+ env.storage()
+ .instance()
+ .get(&DataKey::Contract(name))
+ .expect("contract not registered")
+ }
+
+ /// Check if a contract name is registered
+ ///
+ /// # Arguments
+ /// * `name` - The name to check
+ ///
+ /// # Returns
+ /// True if the name is registered, false otherwise
+ pub fn is_contract_registered(env: Env, name: Symbol) -> bool {
+ env.storage()
+ .instance()
+ .has(&DataKey::Contract(name))
+ }
+
+ /// Get a list of all registered contract names
+ ///
+ /// # Returns
+ /// Vector of all registered contract names
+ pub fn list_contracts(env: Env) -> Vec {
+ env.storage()
+ .instance()
+ .get(&DataKey::ContractList)
+ .unwrap_or(Vec::new(&env))
+ }
+
+ /// Get the total number of registered contracts
+ ///
+ /// # Returns
+ /// The count of registered contracts
+ pub fn get_contract_count(env: Env) -> u32 {
+ let contract_list: Vec = env
+ .storage()
+ .instance()
+ .get(&DataKey::ContractList)
+ .unwrap_or(Vec::new(&env));
+ contract_list.len() as u32
+ }
+
+ /// Get all contracts registered by a specific address
+ ///
+ /// # Arguments
+ /// * `registered_by` - The address to filter by
+ ///
+ /// # Returns
+ /// Vector of contract names registered by the address
+ pub fn get_contracts_by_registrar(env: Env, registered_by: Address) -> Vec {
+ let contract_list: Vec = env
+ .storage()
+ .instance()
+ .get(&DataKey::ContractList)
+ .unwrap_or(Vec::new(&env));
+
+ let mut result = Vec::new(&env);
+ for name in contract_list.iter() {
+ if let Some(contract_info) = env
+ .storage()
+ .instance()
+ .get::(&DataKey::Contract(name))
+ {
+ if contract_info.registered_by == registered_by {
+ result.push_back(name);
+ }
+ }
+ }
+ result
+ }
+
+ /// Get contracts updated within a specific time range
+ ///
+ /// # Arguments
+ /// * `start_time` - Start timestamp (inclusive)
+ /// * `end_time` - End timestamp (inclusive)
+ ///
+ /// # Returns
+ /// Vector of contract names updated in the time range
+ pub fn get_contracts_updated_in_range(env: Env, start_time: u64, end_time: u64) -> Vec {
+ let contract_list: Vec = env
+ .storage()
+ .instance()
+ .get(&DataKey::ContractList)
+ .unwrap_or(Vec::new(&env));
+
+ let mut result = Vec::new(&env);
+ for name in contract_list.iter() {
+ if let Some(contract_info) = env
+ .storage()
+ .instance()
+ .get::(&DataKey::Contract(name))
+ {
+ if let Some(updated_at) = contract_info.updated_at {
+ if updated_at >= start_time && updated_at <= end_time {
+ result.push_back(name);
+ }
+ }
+ }
+ }
+ result
+ }
+
+ /// Pause/unpause the contract registry
+ ///
+ /// # Arguments
+ /// * `paused` - Whether to pause the registry
+ ///
+ /// # Panics
+ /// * If caller is not admin
+ pub fn set_paused(env: Env, paused: bool) {
+ Self::require_admin(&env);
+ let admin = env.current_contract_address();
+
+ env.storage().instance().set(&DataKey::Paused, &paused);
+
+ RegistryPaused {
+ paused,
+ paused_by: admin,
+ }
+ .publish(&env);
+ }
+
+ /// Get the admin address
+ ///
+ /// # Returns
+ /// The admin address
+ ///
+ /// # Panics
+ /// * If contract is not initialized
+ pub fn get_admin(env: Env) -> Address {
+ env.storage()
+ .instance()
+ .get(&DataKey::Admin)
+ .expect("not initialized")
+ }
+
+ /// Check if the contract registry is paused
+ ///
+ /// # Returns
+ /// True if the registry is paused, false otherwise
+ pub fn is_paused(env: Env) -> bool {
+ env.storage()
+ .instance()
+ .get(&DataKey::Paused)
+ .unwrap_or(false)
+ }
+
+ /// Batch register multiple contracts
+ ///
+ /// # Arguments
+ /// * `names` - Array of contract names
+ /// * `addresses` - Array of contract addresses
+ ///
+ /// # Panics
+ /// * If contract is paused
+ /// * If caller is not admin
+ /// * If arrays have different lengths
+ /// * If any name is already registered or empty
+ pub fn batch_register_contracts(env: Env, names: Vec, addresses: Vec) {
+ Self::require_admin(&env);
+ Self::require_not_paused(&env);
+
+ if names.len() != addresses.len() {
+ panic!("names and addresses arrays must have same length");
+ }
+
+ for (i, name) in names.iter().enumerate() {
+ if name.is_empty() {
+ panic!("contract name cannot be empty");
+ }
+
+ if env.storage().instance().has(&DataKey::Contract(name.clone())) {
+ panic!("contract name already registered");
+ }
+
+ let address = addresses.get(i).unwrap();
+ let contract_info = ContractInfo {
+ address: address.clone(),
+ name: name.clone(),
+ registered_at: env.ledger().timestamp(),
+ updated_at: None,
+ registered_by: env.current_contract_address(),
+ };
+
+ env.storage()
+ .instance()
+ .set(&DataKey::Contract(name.clone()), &contract_info);
+
+ ContractRegistered {
+ name: name.clone(),
+ address: address.clone(),
+ registered_by: env.current_contract_address(),
+ }
+ .publish(&env);
+ }
+
+ let mut contract_list: Vec = env
+ .storage()
+ .instance()
+ .get(&DataKey::ContractList)
+ .unwrap_or(Vec::new(&env));
+
+ for name in names.iter() {
+ contract_list.push_back(name.clone());
+ }
+
+ env.storage()
+ .instance()
+ .set(&DataKey::ContractList, &contract_list);
+ }
+
+ /// Transfer admin role to a new address
+ ///
+ /// # Arguments
+ /// * `new_admin` - The new admin address
+ ///
+ /// # Panics
+ /// * If caller is not current admin
+ pub fn transfer_admin(env: Env, new_admin: Address) {
+ let current_admin = Self::get_admin(env.clone());
+ current_admin.require_auth();
+
+ env.storage().instance().set(&DataKey::Admin, &new_admin);
+ }
+
+ // Helper functions for internal use
+
+ fn require_admin(env: &Env) {
+ let admin = Self::get_admin(env.clone());
+ admin.require_auth();
+ }
+
+ fn require_not_paused(env: &Env) {
+ let paused = Self::is_paused(env.clone());
+ if paused {
+ panic!("contract is paused");
+ }
+ }
+}
diff --git a/contracts/contract-registry/src/test.rs b/contracts/contract-registry/src/test.rs
new file mode 100644
index 0000000..1ceaf12
--- /dev/null
+++ b/contracts/contract-registry/src/test.rs
@@ -0,0 +1,524 @@
+#![cfg(test)]
+
+use super::*;
+use soroban_sdk::{
+ testutils::{Address as _, Ledger as _},
+ Address, Env, Symbol, Vec,
+};
+
+fn create_test_env() -> (Env, Address, Address, Address) {
+ let env = Env::default();
+ let admin = Address::generate(&env);
+ let contract1 = Address::generate(&env);
+ let contract2 = Address::generate(&env);
+ (env, admin, contract1, contract2)
+}
+
+fn initialize_contract(env: &Env, admin: &Address) -> Address {
+ let contract_id = Address::generate(env);
+ env.register_contract(&contract_id, ContractRegistry);
+ let client = ContractRegistryClient::new(env, &contract_id);
+
+ env.mock_all_auths();
+ client.initialize(admin);
+
+ contract_id
+}
+
+#[test]
+fn test_initialization() {
+ let (env, admin, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ assert_eq!(client.get_admin(), admin);
+ assert!(!client.is_paused());
+ assert_eq!(client.get_contract_count(), 0);
+}
+
+#[test]
+#[should_panic(expected = "already initialized")]
+fn test_double_initialization() {
+ let (env, admin, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ client.initialize(&admin);
+}
+
+#[test]
+fn test_register_contract() {
+ let (env, admin, contract1, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ let name = Symbol::new(&env, "match_contract");
+
+ env.mock_all_auths();
+ client.register_contract(&name, &contract1);
+
+ assert!(client.is_contract_registered(&name));
+ assert_eq!(client.get_contract(&name), contract1);
+ assert_eq!(client.get_contract_count(), 1);
+
+ let contract_list = client.list_contracts();
+ assert_eq!(contract_list.len(), 1);
+ assert_eq!(contract_list.get(0), name);
+}
+
+#[test]
+#[should_panic(expected = "contract name cannot be empty")]
+fn test_register_empty_name_fails() {
+ let (env, admin, contract1, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ let empty_name = Symbol::new(&env, "");
+
+ env.mock_all_auths();
+ client.register_contract(&empty_name, &contract1);
+}
+
+#[test]
+#[should_panic(expected = "contract name already registered")]
+fn test_register_duplicate_name_fails() {
+ let (env, admin, contract1, contract2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ let name = Symbol::new(&env, "match_contract");
+
+ env.mock_all_auths();
+ client.register_contract(&name, &contract1);
+ client.register_contract(&name, &contract2);
+}
+
+#[test]
+#[should_panic(expected = "already initialized")]
+fn test_register_contract_unauthorized() {
+ let (env, admin, contract1, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ let name = Symbol::new(&env, "match_contract");
+ client.register_contract(&name, &contract1);
+}
+
+#[test]
+fn test_update_contract() {
+ let (env, admin, contract1, contract2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ let name = Symbol::new(&env, "match_contract");
+
+ env.mock_all_auths();
+ client.register_contract(&name, &contract1);
+ assert_eq!(client.get_contract(&name), contract1);
+
+ client.update_contract(&name, &contract2);
+ assert_eq!(client.get_contract(&name), contract2);
+
+ let contract_info = client.get_contract_info(&name);
+ assert_eq!(contract_info.address, contract2);
+ assert!(contract_info.updated_at.is_some());
+}
+
+#[test]
+#[should_panic(expected = "new address is the same as current address")]
+fn test_update_same_address_fails() {
+ let (env, admin, contract1, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ let name = Symbol::new(&env, "match_contract");
+
+ env.mock_all_auths();
+ client.register_contract(&name, &contract1);
+ client.update_contract(&name, &contract1);
+}
+
+#[test]
+#[should_panic(expected = "contract not registered")]
+fn test_update_nonexistent_contract_fails() {
+ let (env, admin, contract1, contract2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ let name = Symbol::new(&env, "nonexistent");
+
+ env.mock_all_auths();
+ client.update_contract(&name, &contract2);
+}
+
+#[test]
+#[should_panic(expected = "already initialized")]
+fn test_update_contract_unauthorized() {
+ let (env, admin, contract1, contract2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ let name = Symbol::new(&env, "match_contract");
+
+ env.mock_all_auths();
+ client.register_contract(&name, &contract1);
+
+ client.update_contract(&name, &contract2);
+}
+
+#[test]
+fn test_remove_contract() {
+ let (env, admin, contract1, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ let name = Symbol::new(&env, "match_contract");
+
+ env.mock_all_auths();
+ client.register_contract(&name, &contract1);
+ assert!(client.is_contract_registered(&name));
+ assert_eq!(client.get_contract_count(), 1);
+
+ client.remove_contract(&name);
+ assert!(!client.is_contract_registered(&name));
+ assert_eq!(client.get_contract_count(), 0);
+}
+
+#[test]
+#[should_panic(expected = "contract not registered")]
+fn test_remove_nonexistent_contract_fails() {
+ let (env, admin, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ let name = Symbol::new(&env, "nonexistent");
+
+ env.mock_all_auths();
+ client.remove_contract(&name);
+}
+
+#[test]
+#[should_panic(expected = "already initialized")]
+fn test_remove_contract_unauthorized() {
+ let (env, admin, contract1, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ let name = Symbol::new(&env, "match_contract");
+
+ env.mock_all_auths();
+ client.register_contract(&name, &contract1);
+
+ client.remove_contract(&name);
+}
+
+#[test]
+fn test_pause_contract() {
+ let (env, admin, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+
+ assert!(!client.is_paused());
+ client.set_paused(&true);
+ assert!(client.is_paused());
+ client.set_paused(&false);
+ assert!(!client.is_paused());
+}
+
+#[test]
+#[should_panic(expected = "already initialized")]
+fn test_pause_contract_unauthorized() {
+ let (env, admin, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ client.set_paused(&true);
+}
+
+#[test]
+#[should_panic(expected = "contract is paused")]
+fn test_operations_when_paused() {
+ let (env, admin, contract1, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ let name = Symbol::new(&env, "match_contract");
+
+ env.mock_all_auths();
+ client.set_paused(&true);
+
+ client.register_contract(&name, &contract1);
+}
+
+#[test]
+fn test_get_contract_info() {
+ let (env, admin, contract1, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ let name = Symbol::new(&env, "match_contract");
+
+ env.mock_all_auths();
+ client.register_contract(&name, &contract1);
+
+ let contract_info = client.get_contract_info(&name);
+ assert_eq!(contract_info.address, contract1);
+ assert_eq!(contract_info.name, name);
+ assert!(contract_info.registered_at > 0);
+ assert!(contract_info.updated_at.is_none());
+}
+
+#[test]
+fn test_list_contracts() {
+ let (env, admin, contract1, contract2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ let name1 = Symbol::new(&env, "match_contract");
+ let name2 = Symbol::new(&env, "token_contract");
+ let name3 = Symbol::new(&env, "registry_contract");
+
+ env.mock_all_auths();
+
+ client.register_contract(&name1, &contract1);
+ client.register_contract(&name2, &contract2);
+ client.register_contract(&name3, &contract1);
+
+ let contract_list = client.list_contracts();
+ assert_eq!(contract_list.len(), 3);
+ assert_eq!(client.get_contract_count(), 3);
+
+ assert!(contract_list.contains(&name1));
+ assert!(contract_list.contains(&name2));
+ assert!(contract_list.contains(&name3));
+}
+
+#[test]
+fn test_batch_register_contracts() {
+ let (env, admin, contract1, contract2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ let names = vec![&env,
+ Symbol::new(&env, "match_contract"),
+ Symbol::new(&env, "token_contract"),
+ Symbol::new(&env, "registry_contract")
+ ];
+ let addresses = vec![&env, contract1.clone(), contract2.clone(), contract1.clone()];
+
+ env.mock_all_auths();
+ client.batch_register_contracts(names, addresses);
+
+ assert_eq!(client.get_contract_count(), 3);
+ assert!(client.is_contract_registered(&Symbol::new(&env, "match_contract")));
+ assert!(client.is_contract_registered(&Symbol::new(&env, "token_contract")));
+ assert!(client.is_contract_registered(&Symbol::new(&env, "registry_contract")));
+}
+
+#[test]
+#[should_panic(expected = "names and addresses arrays must have same length")]
+fn test_batch_register_mismatched_length_fails() {
+ let (env, admin, contract1, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ let names = vec![&env,
+ Symbol::new(&env, "match_contract"),
+ Symbol::new(&env, "token_contract")
+ ];
+ let addresses = vec![&env, contract1];
+
+ env.mock_all_auths();
+ client.batch_register_contracts(names, addresses);
+}
+
+#[test]
+#[should_panic(expected = "contract name cannot be empty")]
+fn test_batch_register_empty_name_fails() {
+ let (env, admin, contract1, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ let names = vec![&env,
+ Symbol::new(&env, "match_contract"),
+ Symbol::new(&env, "")
+ ];
+ let addresses = vec![&env, contract1.clone(), contract1];
+
+ env.mock_all_auths();
+ client.batch_register_contracts(names, addresses);
+}
+
+#[test]
+fn test_get_contracts_by_registrar() {
+ let (env, admin, contract1, contract2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ let name1 = Symbol::new(&env, "match_contract");
+ let name2 = Symbol::new(&env, "token_contract");
+
+ env.mock_all_auths();
+
+ client.register_contract(&name1, &contract1);
+ client.register_contract(&name2, &contract2);
+
+ let registry_address = env.current_contract_address();
+ let contracts_by_registrar = client.get_contracts_by_registrar(registry_address);
+
+ assert_eq!(contracts_by_registrar.len(), 2);
+ assert!(contracts_by_registrar.contains(&name1));
+ assert!(contracts_by_registrar.contains(&name2));
+}
+
+#[test]
+fn test_get_contracts_updated_in_range() {
+ let (env, admin, contract1, contract2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ let name1 = Symbol::new(&env, "match_contract");
+ let name2 = Symbol::new(&env, "token_contract");
+
+ env.mock_all_auths();
+
+ env.ledger().set_timestamp(1000);
+ client.register_contract(&name1, &contract1);
+ client.register_contract(&name2, &contract2);
+
+ env.ledger().set_timestamp(2000);
+ client.update_contract(&name1, &contract2);
+
+ let updated_contracts = client.get_contracts_updated_in_range(1500, 2500);
+ assert_eq!(updated_contracts.len(), 1);
+ assert!(updated_contracts.contains(&name1));
+
+ let updated_contracts = client.get_contracts_updated_in_range(500, 1500);
+ assert_eq!(updated_contracts.len(), 0);
+}
+
+#[test]
+fn test_transfer_admin() {
+ let (env, admin, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ let new_admin = Address::generate(&env);
+
+ env.mock_all_auths();
+ client.transfer_admin(&new_admin);
+
+ assert_eq!(client.get_admin(), new_admin);
+}
+
+#[test]
+#[should_panic(expected = "already initialized")]
+fn test_transfer_admin_unauthorized() {
+ let (env, admin, _, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ let new_admin = Address::generate(&env);
+ client.transfer_admin(&new_admin);
+}
+
+#[test]
+fn test_contract_info_metadata() {
+ let (env, admin, contract1, contract2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ let name = Symbol::new(&env, "match_contract");
+
+ env.mock_all_auths();
+
+ env.ledger().set_timestamp(1000);
+ client.register_contract(&name, &contract1);
+
+ let contract_info = client.get_contract_info(&name);
+ assert_eq!(contract_info.registered_at, 1000);
+ assert!(contract_info.updated_at.is_none());
+ assert_eq!(contract_info.registered_by, env.current_contract_address());
+
+ env.ledger().set_timestamp(2000);
+ client.update_contract(&name, &contract2);
+
+ let updated_info = client.get_contract_info(&name);
+ assert_eq!(updated_info.registered_at, 1000);
+ assert_eq!(updated_info.updated_at, Some(2000));
+ assert_eq!(updated_info.address, contract2);
+}
+
+#[test]
+fn test_multiple_operations() {
+ let (env, admin, contract1, contract2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ let match_name = Symbol::new(&env, "match_contract");
+ let token_name = Symbol::new(&env, "token_contract");
+ let registry_name = Symbol::new(&env, "registry_contract");
+
+ env.mock_all_auths();
+
+ client.register_contract(&match_name, &contract1);
+ client.register_contract(&token_name, &contract2);
+ client.register_contract(®istry_name, &contract1);
+
+ assert_eq!(client.get_contract_count(), 3);
+ assert_eq!(client.get_contract(&match_name), contract1);
+ assert_eq!(client.get_contract(&token_name), contract2);
+ assert_eq!(client.get_contract(®istry_name), contract1);
+
+ client.update_contract(&match_name, &contract2);
+ assert_eq!(client.get_contract(&match_name), contract2);
+
+ client.remove_contract(&token_name);
+ assert!(!client.is_contract_registered(&token_name));
+ assert_eq!(client.get_contract_count(), 2);
+
+ let remaining_contracts = client.list_contracts();
+ assert_eq!(remaining_contracts.len(), 2);
+ assert!(remaining_contracts.contains(&match_name));
+ assert!(remaining_contracts.contains(®istry_name));
+}
+
+#[test]
+fn test_edge_cases() {
+ let (env, admin, contract1, _) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ let nonexistent_name = Symbol::new(&env, "nonexistent");
+ assert!(!client.is_contract_registered(&nonexistent_name));
+
+ let empty_list = client.list_contracts();
+ assert_eq!(empty_list.len(), 0);
+
+ let empty_count = client.get_contract_count();
+ assert_eq!(empty_count, 0);
+}
+
+#[test]
+fn test_deterministic_resolution() {
+ let (env, admin, contract1, contract2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = ContractRegistryClient::new(&env, &contract_id);
+
+ let name = Symbol::new(&env, "match_contract");
+
+ env.mock_all_auths();
+ client.register_contract(&name, &contract1);
+
+ let address1 = client.get_contract(&name);
+ let address2 = client.get_contract(&name);
+ let address3 = client.get_contract(&name);
+
+ assert_eq!(address1, contract1);
+ assert_eq!(address2, contract1);
+ assert_eq!(address3, contract1);
+ assert_eq!(address1, address2);
+ assert_eq!(address2, address3);
+}
diff --git a/contracts/dispute-resolution/Cargo.toml b/contracts/dispute-resolution/Cargo.toml
new file mode 100644
index 0000000..9f653af
--- /dev/null
+++ b/contracts/dispute-resolution/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "dispute-resolution"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+repository.workspace = true
+description = "Dispute Resolution Module: handles dispute open/close, evidence references, adjudication and timeouts"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+soroban-sdk.workspace = true
+
+[dev-dependencies]
+soroban-sdk = { workspace = true, features = ["testutils"] }
diff --git a/contracts/dispute-resolution/src/lib.rs b/contracts/dispute-resolution/src/lib.rs
new file mode 100644
index 0000000..905db02
--- /dev/null
+++ b/contracts/dispute-resolution/src/lib.rs
@@ -0,0 +1,194 @@
+#![no_std]
+
+use soroban_sdk::{
+ contract, contractevent, contractimpl, contracttype, Address, BytesN, Env, IntoVal, String, Symbol,
+};
+
+#[contractevent(topics = ["ArenaXDispute", "OPENED"])]
+pub struct DisputeOpened {
+ pub match_id: BytesN<32>,
+ pub reason: String,
+ pub evidence_ref: String,
+ pub deadline: u64,
+}
+
+#[contractevent(topics = ["ArenaXDispute", "RESOLVED"])]
+pub struct DisputeResolved {
+ pub match_id: BytesN<32>,
+ pub decision: String,
+ pub resolved_at: u64,
+ pub operator: Address,
+}
+
+#[contracttype]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum DisputeStatus {
+ Open = 0,
+ Resolved = 1,
+}
+
+#[contracttype]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct DisputeData {
+ pub match_id: BytesN<32>,
+ pub reason: String,
+ pub evidence_ref: String,
+ pub status: u32,
+ pub opened_at: u64,
+ pub deadline: u64,
+ pub decision: Option,
+ pub resolved_at: Option,
+}
+
+#[contracttype]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum DataKey {
+ Admin,
+ IdentityContract,
+ ResolutionWindow,
+ Dispute(BytesN<32>),
+}
+
+#[contract]
+pub struct DisputeResolutionContract;
+
+#[contractimpl]
+impl DisputeResolutionContract {
+ pub fn initialize(
+ env: Env,
+ admin: Address,
+ identity_contract: Address,
+ resolution_window: u64,
+ ) {
+ if env.storage().instance().has(&DataKey::Admin) {
+ panic!("already initialized");
+ }
+ admin.require_auth();
+ env.storage().instance().set(&DataKey::Admin, &admin);
+ env.storage()
+ .instance()
+ .set(&DataKey::IdentityContract, &identity_contract);
+ env.storage()
+ .instance()
+ .set(&DataKey::ResolutionWindow, &resolution_window);
+ }
+
+ pub fn open_dispute(env: Env, match_id: BytesN<32>, reason: String, evidence_ref: String) {
+ if env
+ .storage()
+ .persistent()
+ .has(&DataKey::Dispute(match_id.clone()))
+ {
+ panic!("dispute already opened");
+ }
+
+ let resolution_window: u64 = env
+ .storage()
+ .instance()
+ .get(&DataKey::ResolutionWindow)
+ .expect("contract not initialized");
+
+ let opened_at = env.ledger().timestamp();
+ let deadline = opened_at + resolution_window;
+
+ let dispute = DisputeData {
+ match_id: match_id.clone(),
+ reason: reason.clone(),
+ evidence_ref: evidence_ref.clone(),
+ status: DisputeStatus::Open as u32,
+ opened_at,
+ deadline,
+ decision: None,
+ resolved_at: None,
+ };
+
+ env.storage()
+ .persistent()
+ .set(&DataKey::Dispute(match_id.clone()), &dispute);
+
+ DisputeOpened {
+ match_id,
+ reason,
+ evidence_ref,
+ deadline,
+ }
+ .publish(&env);
+ }
+
+ pub fn resolve_dispute(env: Env, match_id: BytesN<32>, caller: Address, decision: String) {
+ caller.require_auth();
+
+ if !Self::is_operator(&env, &caller) {
+ panic!("unauthorized call: only operators can adjudicate disputes");
+ }
+
+ let mut dispute: DisputeData = env
+ .storage()
+ .persistent()
+ .get(&DataKey::Dispute(match_id.clone()))
+ .expect("dispute not found");
+
+ if dispute.status != DisputeStatus::Open as u32 {
+ panic!("dispute is not open");
+ }
+
+ let current_time = env.ledger().timestamp();
+ if current_time > dispute.deadline {
+ panic!("resolution deadline has passed");
+ }
+
+ dispute.status = DisputeStatus::Resolved as u32;
+ dispute.decision = Some(decision.clone());
+ dispute.resolved_at = Some(current_time);
+
+ env.storage()
+ .persistent()
+ .set(&DataKey::Dispute(match_id.clone()), &dispute);
+
+ DisputeResolved {
+ match_id,
+ decision,
+ resolved_at: current_time,
+ operator: caller,
+ }
+ .publish(&env);
+ }
+
+ pub fn is_disputed(env: Env, match_id: BytesN<32>) -> bool {
+ if let Some(dispute) = env
+ .storage()
+ .persistent()
+ .get::(&DataKey::Dispute(match_id))
+ {
+ return dispute.status == DisputeStatus::Open as u32;
+ }
+ false
+ }
+
+ fn is_operator(env: &Env, addr: &Address) -> bool {
+ let admin: Address = env
+ .storage()
+ .instance()
+ .get(&DataKey::Admin)
+ .expect("contract not initialized");
+
+ if addr == &admin {
+ return true;
+ }
+
+ if let Some(identity_contract) = env
+ .storage()
+ .instance()
+ .get::(&DataKey::IdentityContract)
+ {
+ let role: u32 = env.invoke_contract(
+ &identity_contract,
+ &Symbol::new(env, "get_role"),
+ (addr.clone(),).into_val(env),
+ );
+ return role == 1 || role == 2;
+ }
+
+ false
+ }
+}
diff --git a/contracts/match-lifecycle/Cargo.toml b/contracts/match-lifecycle/Cargo.toml
new file mode 100644
index 0000000..fa7f3da
--- /dev/null
+++ b/contracts/match-lifecycle/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "match-lifecycle"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+repository.workspace = true
+description = "Match Lifecycle Manager: creation, participation, result submission, and finalization with strict state transitions"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+soroban-sdk.workspace = true
+
+[dev-dependencies]
+soroban-sdk = { workspace = true, features = ["testutils"] }
diff --git a/contracts/match-lifecycle/src/lib.rs b/contracts/match-lifecycle/src/lib.rs
new file mode 100644
index 0000000..7385050
--- /dev/null
+++ b/contracts/match-lifecycle/src/lib.rs
@@ -0,0 +1,348 @@
+#![no_std]
+
+//! # Match Lifecycle Manager
+//!
+//! Manages creation, participation, result submission, and finalization of matches
+//! with strict state transitions and authorization. Supports dual-reporting:
+//! two participants must submit matching results before a match can be finalized.
+
+use soroban_sdk::{
+ contract, contractevent, contractimpl, contracttype, Address, BytesN, Env, IntoVal, Symbol, Vec,
+};
+
+// Events (match_created, result_submitted, match_finalized)
+#[contractevent(topics = ["ArenaXMatchLifecycle", "CREATED"])]
+pub struct MatchCreated {
+ pub match_id: BytesN<32>,
+ pub players: Vec,
+ pub stake_asset: Address,
+ pub stake_amount: i128,
+ pub created_at: u64,
+}
+
+#[contractevent(topics = ["ArenaXMatchLifecycle", "RESULT"])]
+pub struct ResultSubmitted {
+ pub match_id: BytesN<32>,
+ pub reporter: Address,
+ pub score: i64,
+ pub report_number: u32,
+}
+
+#[contractevent(topics = ["ArenaXMatchLifecycle", "FINALIZED"])]
+pub struct MatchFinalized {
+ pub match_id: BytesN<32>,
+ pub winner: Address,
+ pub finalized_at: u64,
+}
+
+#[contracttype]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum DataKey {
+ Match(BytesN<32>),
+ Admin,
+ IdentityContract,
+}
+
+#[contracttype]
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+#[repr(u32)]
+pub enum MatchState {
+ Created = 0,
+ InProgress = 1,
+ PendingResult = 2,
+ Finalized = 3,
+ Disputed = 4,
+}
+
+#[contracttype]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct MatchData {
+ pub players: Vec,
+ pub stake_asset: Address,
+ pub stake_amount: i128,
+ pub state: u32,
+ pub created_at: u64,
+ pub report1_reporter: Option,
+ pub report1_score: Option,
+ pub report2_reporter: Option,
+ pub report2_score: Option,
+ pub winner: Option,
+ pub finalized_at: Option,
+}
+
+#[contract]
+pub struct MatchLifecycleContract;
+
+#[contractimpl]
+impl MatchLifecycleContract {
+ /// Initialize the contract with an admin (optional; used for set_identity_contract).
+ pub fn initialize(env: Env, admin: Address) {
+ if env.storage().instance().has(&DataKey::Admin) {
+ panic!("already initialized");
+ }
+ admin.require_auth();
+ env.storage().instance().set(&DataKey::Admin, &admin);
+ }
+
+ /// Set the Identity Contract address for operator (Referee/Admin) checks on finalize.
+ pub fn set_identity_contract(env: Env, identity_contract: Address) {
+ let admin: Address = env
+ .storage()
+ .instance()
+ .get(&DataKey::Admin)
+ .expect("not initialized");
+ admin.require_auth();
+ env.storage()
+ .instance()
+ .set(&DataKey::IdentityContract, &identity_contract);
+ }
+
+ /// Create a new match with the given players, stake asset, and stake amount.
+ /// State: Created.
+ pub fn create_match(
+ env: Env,
+ match_id: BytesN<32>,
+ players: Vec,
+ stake_asset: Address,
+ stake_amount: i128,
+ ) {
+ if env
+ .storage()
+ .persistent()
+ .has(&DataKey::Match(match_id.clone()))
+ {
+ panic!("match already exists");
+ }
+ if players.len() < 2 {
+ panic!("at least two players required");
+ }
+ if stake_amount <= 0 {
+ panic!("stake_amount must be positive");
+ }
+
+ let created_at = env.ledger().timestamp();
+ let match_data = MatchData {
+ players: players.clone(),
+ stake_asset: stake_asset.clone(),
+ stake_amount,
+ state: MatchState::Created as u32,
+ created_at,
+ report1_reporter: None,
+ report1_score: None,
+ report2_reporter: None,
+ report2_score: None,
+ winner: None,
+ finalized_at: None,
+ };
+
+ env.storage()
+ .persistent()
+ .set(&DataKey::Match(match_id.clone()), &match_data);
+
+ MatchCreated {
+ match_id,
+ players,
+ stake_asset: match_data.stake_asset.clone(),
+ stake_amount: match_data.stake_amount,
+ created_at,
+ }
+ .publish(&env);
+ }
+
+ /// Submit a result for a match. Reporter must be a participant.
+ /// First report: transition Created -> InProgress and store report.
+ /// Second report: if same score from another participant -> PendingResult; if different score -> Disputed.
+ pub fn submit_result(env: Env, match_id: BytesN<32>, reporter: Address, score: i64) {
+ reporter.require_auth();
+
+ let mut match_data: MatchData = env
+ .storage()
+ .persistent()
+ .get(&DataKey::Match(match_id.clone()))
+ .expect("match not found");
+
+ let state = match_data.state;
+ if state != MatchState::Created as u32
+ && state != MatchState::InProgress as u32
+ {
+ panic!("invalid state for result submission");
+ }
+
+ if !Self::is_participant(&match_data.players, &reporter) {
+ panic!("reporter must be a participant");
+ }
+
+ if state == MatchState::Created as u32 {
+ match_data.state = MatchState::InProgress as u32;
+ }
+
+ if match_data.report1_reporter.is_none() {
+ match_data.report1_reporter = Some(reporter.clone());
+ match_data.report1_score = Some(score);
+ env.storage()
+ .persistent()
+ .set(&DataKey::Match(match_id.clone()), &match_data);
+ ResultSubmitted {
+ match_id: match_id.clone(),
+ reporter,
+ score,
+ report_number: 1,
+ }
+ .publish(&env);
+ return;
+ }
+
+ if match_data.report1_reporter.as_ref() == Some(&reporter) {
+ panic!("same reporter cannot submit twice");
+ }
+
+ match_data.report2_reporter = Some(reporter.clone());
+ match_data.report2_score = Some(score);
+
+ let score1 = match_data.report1_score.unwrap();
+ if score == score1 {
+ match_data.state = MatchState::PendingResult as u32;
+ } else {
+ match_data.state = MatchState::Disputed as u32;
+ }
+
+ env.storage()
+ .persistent()
+ .set(&DataKey::Match(match_id.clone()), &match_data);
+
+ ResultSubmitted {
+ match_id,
+ reporter,
+ score,
+ report_number: 2,
+ }
+ .publish(&env);
+ }
+
+ /// Finalize a match. Caller must be a participant or an operator (Referee/Admin via identity contract).
+ /// Only allowed when state is PendingResult. Sets winner from agreed score (score = player index).
+ pub fn finalize_match(env: Env, match_id: BytesN<32>, caller: Address) {
+ let mut match_data: MatchData = env
+ .storage()
+ .persistent()
+ .get(&DataKey::Match(match_id.clone()))
+ .expect("match not found");
+
+ if match_data.state != MatchState::PendingResult as u32 {
+ panic!("match must be in PendingResult to finalize");
+ }
+
+ let is_participant = Self::is_participant(&match_data.players, &caller);
+ let is_operator = Self::is_operator(&env, &caller);
+
+ if !is_participant && !is_operator {
+ panic!("only participants or operators can finalize");
+ }
+
+ caller.require_auth();
+
+ let score = match_data.report1_score.unwrap();
+ let winner = Self::winner_from_score(&env, &match_data.players, score)
+ .expect("agreed score must be a valid player index");
+
+ match_data.state = MatchState::Finalized as u32;
+ match_data.winner = Some(winner.clone());
+ match_data.finalized_at = Some(env.ledger().timestamp());
+
+ env.storage()
+ .persistent()
+ .set(&DataKey::Match(match_id.clone()), &match_data);
+
+ MatchFinalized {
+ match_id,
+ winner,
+ finalized_at: match_data.finalized_at.unwrap(),
+ }
+ .publish(&env);
+ }
+
+ /// Mark match as disputed (e.g. from external dispute flow). Operator or participant only.
+ pub fn raise_dispute(env: Env, match_id: BytesN<32>, caller: Address) {
+ caller.require_auth();
+
+ let mut match_data: MatchData = env
+ .storage()
+ .persistent()
+ .get(&DataKey::Match(match_id.clone()))
+ .expect("match not found");
+
+ if match_data.state != MatchState::InProgress as u32
+ && match_data.state != MatchState::PendingResult as u32
+ {
+ panic!("invalid state for dispute");
+ }
+
+ let is_participant = Self::is_participant(&match_data.players, &caller);
+ let is_operator = Self::is_operator(&env, &caller);
+ if !is_participant && !is_operator {
+ panic!("only participants or operators can raise dispute");
+ }
+
+ match_data.state = MatchState::Disputed as u32;
+ env.storage()
+ .persistent()
+ .set(&DataKey::Match(match_id), &match_data);
+ }
+
+ pub fn get_match(env: Env, match_id: BytesN<32>) -> MatchData {
+ env.storage()
+ .persistent()
+ .get(&DataKey::Match(match_id))
+ .expect("match not found")
+ }
+
+ pub fn match_exists(env: Env, match_id: BytesN<32>) -> bool {
+ env.storage().persistent().has(&DataKey::Match(match_id))
+ }
+
+ fn is_participant(players: &Vec, addr: &Address) -> bool {
+ for i in 0..players.len() {
+ if players.get(i).unwrap() == *addr {
+ return true;
+ }
+ }
+ false
+ }
+
+ fn winner_from_score(_env: &Env, players: &Vec, score: i64) -> Option {
+ if score < 0 {
+ return None;
+ }
+ let idx = score as u32;
+ if idx >= players.len() {
+ return None;
+ }
+ Some(players.get(idx).unwrap())
+ }
+
+ fn is_operator(env: &Env, addr: &Address) -> bool {
+ let admin: Address = env
+ .storage()
+ .instance()
+ .get(&DataKey::Admin)
+ .expect("not initialized");
+ if addr == &admin {
+ return true;
+ }
+ if let Some(identity_contract) = env
+ .storage()
+ .instance()
+ .get::(&DataKey::IdentityContract)
+ {
+ let role: u32 = env.invoke_contract(
+ &identity_contract,
+ &Symbol::new(env, "get_role"),
+ (addr.clone(),).into_val(env),
+ );
+ return role == 1 || role == 2;
+ }
+ false
+ }
+}
+
+mod test;
diff --git a/contracts/match-lifecycle/src/test.rs b/contracts/match-lifecycle/src/test.rs
new file mode 100644
index 0000000..fe9527d
--- /dev/null
+++ b/contracts/match-lifecycle/src/test.rs
@@ -0,0 +1,163 @@
+#![cfg(test)]
+use super::*;
+use soroban_sdk::testutils::{Address as _, Ledger as _};
+use soroban_sdk::{BytesN, Env, Vec};
+
+fn setup(env: &Env) -> (MatchLifecycleContractClient<'_>, Address, Vec, BytesN<32>) {
+ env.mock_all_auths();
+ env.ledger().set_timestamp(12345);
+
+ let contract_id = env.register(MatchLifecycleContract, ());
+ let client = MatchLifecycleContractClient::new(env, &contract_id);
+
+ let admin = Address::generate(env);
+ client.initialize(&admin);
+
+ let mut players: Vec = Vec::new(env);
+ let player_a = Address::generate(env);
+ let player_b = Address::generate(env);
+ players.push_back(player_a.clone());
+ players.push_back(player_b.clone());
+
+ let stake_asset = Address::generate(env);
+ let match_id = BytesN::from_array(env, &[1u8; 32]);
+
+ (client, stake_asset, players, match_id)
+}
+
+#[test]
+fn test_create_match_and_lifecycle() {
+ let env = Env::default();
+ let (client, stake_asset, players, match_id) = setup(&env);
+
+ client.create_match(&match_id, &players, &stake_asset, &1000);
+ let data = client.get_match(&match_id);
+ assert_eq!(data.state, MatchState::Created as u32);
+ assert_eq!(data.stake_amount, 1000);
+ assert_eq!(data.players.len(), 2);
+}
+
+#[test]
+fn test_submit_result_dual_reporting_agree() {
+ let env = Env::default();
+ let (client, stake_asset, players, match_id) = setup(&env);
+ let player_a = players.get(0).unwrap();
+ let player_b = players.get(1).unwrap();
+
+ client.create_match(&match_id, &players, &stake_asset, &1000);
+
+ client.submit_result(&match_id, &player_a, &0); // score 0 = player 0 wins
+ let data = client.get_match(&match_id);
+ assert_eq!(data.state, MatchState::InProgress as u32);
+ assert!(data.report1_reporter.is_some());
+ assert_eq!(data.report1_score, Some(0));
+
+ client.submit_result(&match_id, &player_b, &0); // same score
+ let data = client.get_match(&match_id);
+ assert_eq!(data.state, MatchState::PendingResult as u32);
+ assert!(data.report2_reporter.is_some());
+ assert_eq!(data.report2_score, Some(0));
+}
+
+#[test]
+fn test_submit_result_dual_reporting_dispute() {
+ let env = Env::default();
+ let (client, stake_asset, players, match_id) = setup(&env);
+ let player_a = players.get(0).unwrap();
+ let player_b = players.get(1).unwrap();
+
+ client.create_match(&match_id, &players, &stake_asset, &1000);
+ client.submit_result(&match_id, &player_a, &0);
+ client.submit_result(&match_id, &player_b, &1); // different score -> dispute
+ let data = client.get_match(&match_id);
+ assert_eq!(data.state, MatchState::Disputed as u32);
+}
+
+#[test]
+fn test_finalize_match_as_participant() {
+ let env = Env::default();
+ let (client, stake_asset, players, match_id) = setup(&env);
+ let player_a = players.get(0).unwrap();
+ let player_b = players.get(1).unwrap();
+
+ client.create_match(&match_id, &players, &stake_asset, &1000);
+ client.submit_result(&match_id, &player_a, &0);
+ client.submit_result(&match_id, &player_b, &0);
+
+ client.finalize_match(&match_id, &player_a);
+ let data = client.get_match(&match_id);
+ assert_eq!(data.state, MatchState::Finalized as u32);
+ assert_eq!(data.winner, Some(player_a));
+ assert!(data.finalized_at.is_some());
+}
+
+#[test]
+#[should_panic(expected = "same reporter cannot submit twice")]
+fn test_submit_result_same_reporter_twice_fails() {
+ let env = Env::default();
+ let (client, stake_asset, players, match_id) = setup(&env);
+ let player_a = players.get(0).unwrap();
+
+ client.create_match(&match_id, &players, &stake_asset, &1000);
+ client.submit_result(&match_id, &player_a, &0);
+ client.submit_result(&match_id, &player_a, &0);
+}
+
+#[test]
+#[should_panic(expected = "match must be in PendingResult to finalize")]
+fn test_finalize_before_pending_result_fails() {
+ let env = Env::default();
+ let (client, stake_asset, players, match_id) = setup(&env);
+ let player_a = players.get(0).unwrap();
+
+ client.create_match(&match_id, &players, &stake_asset, &1000);
+ client.finalize_match(&match_id, &player_a);
+}
+
+#[test]
+#[should_panic(expected = "reporter must be a participant")]
+fn test_submit_result_non_participant_fails() {
+ let env = Env::default();
+ let (client, stake_asset, players, match_id) = setup(&env);
+ let outsider = Address::generate(&env);
+
+ client.create_match(&match_id, &players, &stake_asset, &1000);
+ client.submit_result(&match_id, &outsider, &0);
+}
+
+#[test]
+fn test_finalize_match_as_operator() {
+ let env = Env::default();
+ env.mock_all_auths();
+ let admin = Address::generate(&env);
+ let contract_id = env.register(MatchLifecycleContract, ());
+ let client = MatchLifecycleContractClient::new(&env, &contract_id);
+ client.initialize(&admin);
+ let mut players: Vec = Vec::new(&env);
+ let player_a = Address::generate(&env);
+ let player_b = Address::generate(&env);
+ players.push_back(player_a.clone());
+ players.push_back(player_b.clone());
+ let stake_asset = Address::generate(&env);
+ let match_id = BytesN::from_array(&env, &[3u8; 32]);
+ client.create_match(&match_id, &players, &stake_asset, &1000);
+ client.submit_result(&match_id, &player_a, &1);
+ client.submit_result(&match_id, &player_b, &1);
+ client.finalize_match(&match_id, &admin);
+ let data = client.get_match(&match_id);
+ assert_eq!(data.state, MatchState::Finalized as u32);
+ assert_eq!(data.winner, Some(player_b));
+}
+
+#[test]
+fn test_match_exists() {
+ let env = Env::default();
+ let (client, stake_asset, players, match_id) = setup(&env);
+ let other_id = BytesN::from_array(&env, &[2u8; 32]);
+
+ assert!(!client.match_exists(&match_id));
+ assert!(!client.match_exists(&other_id));
+ client.create_match(&match_id, &players, &stake_asset, &1000);
+ assert!(client.match_exists(&match_id));
+ assert!(!client.match_exists(&other_id));
+}
diff --git a/contracts/match-lifecycle/test_snapshots/test/test_create_match_and_lifecycle.1.json b/contracts/match-lifecycle/test_snapshots/test/test_create_match_and_lifecycle.1.json
new file mode 100644
index 0000000..7c1b5b7
--- /dev/null
+++ b/contracts/match-lifecycle/test_snapshots/test/test_create_match_and_lifecycle.1.json
@@ -0,0 +1,272 @@
+{
+ "generators": {
+ "address": 5,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 12345,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Match"
+ },
+ {
+ "bytes": "0101010101010101010101010101010101010101010101010101010101010101"
+ }
+ ]
+ },
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Match"
+ },
+ {
+ "bytes": "0101010101010101010101010101010101010101010101010101010101010101"
+ }
+ ]
+ },
+ "durability": "persistent",
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "created_at"
+ },
+ "val": {
+ "u64": "12345"
+ }
+ },
+ {
+ "key": {
+ "symbol": "finalized_at"
+ },
+ "val": "void"
+ },
+ {
+ "key": {
+ "symbol": "players"
+ },
+ "val": {
+ "vec": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "report1_reporter"
+ },
+ "val": "void"
+ },
+ {
+ "key": {
+ "symbol": "report1_score"
+ },
+ "val": "void"
+ },
+ {
+ "key": {
+ "symbol": "report2_reporter"
+ },
+ "val": "void"
+ },
+ {
+ "key": {
+ "symbol": "report2_score"
+ },
+ "val": "void"
+ },
+ {
+ "key": {
+ "symbol": "stake_amount"
+ },
+ "val": {
+ "i128": "1000"
+ }
+ },
+ {
+ "key": {
+ "symbol": "stake_asset"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "state"
+ },
+ "val": {
+ "u32": 0
+ }
+ },
+ {
+ "key": {
+ "symbol": "winner"
+ },
+ "val": "void"
+ }
+ ]
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Admin"
+ }
+ ]
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/contracts/match-lifecycle/test_snapshots/test/test_finalize_before_pending_result_fails.1.json b/contracts/match-lifecycle/test_snapshots/test/test_finalize_before_pending_result_fails.1.json
new file mode 100644
index 0000000..7c1b5b7
--- /dev/null
+++ b/contracts/match-lifecycle/test_snapshots/test/test_finalize_before_pending_result_fails.1.json
@@ -0,0 +1,272 @@
+{
+ "generators": {
+ "address": 5,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 12345,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Match"
+ },
+ {
+ "bytes": "0101010101010101010101010101010101010101010101010101010101010101"
+ }
+ ]
+ },
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Match"
+ },
+ {
+ "bytes": "0101010101010101010101010101010101010101010101010101010101010101"
+ }
+ ]
+ },
+ "durability": "persistent",
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "created_at"
+ },
+ "val": {
+ "u64": "12345"
+ }
+ },
+ {
+ "key": {
+ "symbol": "finalized_at"
+ },
+ "val": "void"
+ },
+ {
+ "key": {
+ "symbol": "players"
+ },
+ "val": {
+ "vec": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "report1_reporter"
+ },
+ "val": "void"
+ },
+ {
+ "key": {
+ "symbol": "report1_score"
+ },
+ "val": "void"
+ },
+ {
+ "key": {
+ "symbol": "report2_reporter"
+ },
+ "val": "void"
+ },
+ {
+ "key": {
+ "symbol": "report2_score"
+ },
+ "val": "void"
+ },
+ {
+ "key": {
+ "symbol": "stake_amount"
+ },
+ "val": {
+ "i128": "1000"
+ }
+ },
+ {
+ "key": {
+ "symbol": "stake_asset"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "state"
+ },
+ "val": {
+ "u32": 0
+ }
+ },
+ {
+ "key": {
+ "symbol": "winner"
+ },
+ "val": "void"
+ }
+ ]
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Admin"
+ }
+ ]
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/contracts/match-lifecycle/test_snapshots/test/test_finalize_match_as_operator.1.json b/contracts/match-lifecycle/test_snapshots/test/test_finalize_match_as_operator.1.json
new file mode 100644
index 0000000..0d4f3e9
--- /dev/null
+++ b/contracts/match-lifecycle/test_snapshots/test/test_finalize_match_as_operator.1.json
@@ -0,0 +1,455 @@
+{
+ "generators": {
+ "address": 5,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "function_name": "submit_result",
+ "args": [
+ {
+ "bytes": "0303030303030303030303030303030303030303030303030303030303030303"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ },
+ {
+ "i64": "1"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "function_name": "submit_result",
+ "args": [
+ {
+ "bytes": "0303030303030303030303030303030303030303030303030303030303030303"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ },
+ {
+ "i64": "1"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "function_name": "finalize_match",
+ "args": [
+ {
+ "bytes": "0303030303030303030303030303030303030303030303030303030303030303"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4837995959683129791"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4837995959683129791"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Match"
+ },
+ {
+ "bytes": "0303030303030303030303030303030303030303030303030303030303030303"
+ }
+ ]
+ },
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Match"
+ },
+ {
+ "bytes": "0303030303030303030303030303030303030303030303030303030303030303"
+ }
+ ]
+ },
+ "durability": "persistent",
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "created_at"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "finalized_at"
+ },
+ "val": {
+ "u64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "players"
+ },
+ "val": {
+ "vec": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "report1_reporter"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ }
+ },
+ {
+ "key": {
+ "symbol": "report1_score"
+ },
+ "val": {
+ "i64": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "report2_reporter"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ },
+ {
+ "key": {
+ "symbol": "report2_score"
+ },
+ "val": {
+ "i64": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "stake_amount"
+ },
+ "val": {
+ "i128": "1000"
+ }
+ },
+ {
+ "key": {
+ "symbol": "stake_asset"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "state"
+ },
+ "val": {
+ "u32": 3
+ }
+ },
+ {
+ "key": {
+ "symbol": "winner"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ }
+ ]
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Admin"
+ }
+ ]
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/contracts/match-lifecycle/test_snapshots/test/test_finalize_match_as_participant.1.json b/contracts/match-lifecycle/test_snapshots/test/test_finalize_match_as_participant.1.json
new file mode 100644
index 0000000..16fd661
--- /dev/null
+++ b/contracts/match-lifecycle/test_snapshots/test/test_finalize_match_as_participant.1.json
@@ -0,0 +1,455 @@
+{
+ "generators": {
+ "address": 5,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "submit_result",
+ "args": [
+ {
+ "bytes": "0101010101010101010101010101010101010101010101010101010101010101"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ },
+ {
+ "i64": "0"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "submit_result",
+ "args": [
+ {
+ "bytes": "0101010101010101010101010101010101010101010101010101010101010101"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ },
+ {
+ "i64": "0"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "finalize_match",
+ "args": [
+ {
+ "bytes": "0101010101010101010101010101010101010101010101010101010101010101"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 12345,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Match"
+ },
+ {
+ "bytes": "0101010101010101010101010101010101010101010101010101010101010101"
+ }
+ ]
+ },
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Match"
+ },
+ {
+ "bytes": "0101010101010101010101010101010101010101010101010101010101010101"
+ }
+ ]
+ },
+ "durability": "persistent",
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "created_at"
+ },
+ "val": {
+ "u64": "12345"
+ }
+ },
+ {
+ "key": {
+ "symbol": "finalized_at"
+ },
+ "val": {
+ "u64": "12345"
+ }
+ },
+ {
+ "key": {
+ "symbol": "players"
+ },
+ "val": {
+ "vec": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "report1_reporter"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ }
+ },
+ {
+ "key": {
+ "symbol": "report1_score"
+ },
+ "val": {
+ "i64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "report2_reporter"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ },
+ {
+ "key": {
+ "symbol": "report2_score"
+ },
+ "val": {
+ "i64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "stake_amount"
+ },
+ "val": {
+ "i128": "1000"
+ }
+ },
+ {
+ "key": {
+ "symbol": "stake_asset"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "state"
+ },
+ "val": {
+ "u32": 3
+ }
+ },
+ {
+ "key": {
+ "symbol": "winner"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ }
+ }
+ ]
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Admin"
+ }
+ ]
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4837995959683129791"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "4837995959683129791"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/contracts/match-lifecycle/test_snapshots/test/test_match_exists.1.json b/contracts/match-lifecycle/test_snapshots/test/test_match_exists.1.json
new file mode 100644
index 0000000..0967c21
--- /dev/null
+++ b/contracts/match-lifecycle/test_snapshots/test/test_match_exists.1.json
@@ -0,0 +1,275 @@
+{
+ "generators": {
+ "address": 5,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ [],
+ [],
+ [],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 12345,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Match"
+ },
+ {
+ "bytes": "0101010101010101010101010101010101010101010101010101010101010101"
+ }
+ ]
+ },
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Match"
+ },
+ {
+ "bytes": "0101010101010101010101010101010101010101010101010101010101010101"
+ }
+ ]
+ },
+ "durability": "persistent",
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "created_at"
+ },
+ "val": {
+ "u64": "12345"
+ }
+ },
+ {
+ "key": {
+ "symbol": "finalized_at"
+ },
+ "val": "void"
+ },
+ {
+ "key": {
+ "symbol": "players"
+ },
+ "val": {
+ "vec": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "report1_reporter"
+ },
+ "val": "void"
+ },
+ {
+ "key": {
+ "symbol": "report1_score"
+ },
+ "val": "void"
+ },
+ {
+ "key": {
+ "symbol": "report2_reporter"
+ },
+ "val": "void"
+ },
+ {
+ "key": {
+ "symbol": "report2_score"
+ },
+ "val": "void"
+ },
+ {
+ "key": {
+ "symbol": "stake_amount"
+ },
+ "val": {
+ "i128": "1000"
+ }
+ },
+ {
+ "key": {
+ "symbol": "stake_asset"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "state"
+ },
+ "val": {
+ "u32": 0
+ }
+ },
+ {
+ "key": {
+ "symbol": "winner"
+ },
+ "val": "void"
+ }
+ ]
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Admin"
+ }
+ ]
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/contracts/match-lifecycle/test_snapshots/test/test_submit_result_dual_reporting_agree.1.json b/contracts/match-lifecycle/test_snapshots/test/test_submit_result_dual_reporting_agree.1.json
new file mode 100644
index 0000000..482d494
--- /dev/null
+++ b/contracts/match-lifecycle/test_snapshots/test/test_submit_result_dual_reporting_agree.1.json
@@ -0,0 +1,397 @@
+{
+ "generators": {
+ "address": 5,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "submit_result",
+ "args": [
+ {
+ "bytes": "0101010101010101010101010101010101010101010101010101010101010101"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ },
+ {
+ "i64": "0"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "submit_result",
+ "args": [
+ {
+ "bytes": "0101010101010101010101010101010101010101010101010101010101010101"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ },
+ {
+ "i64": "0"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 12345,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Match"
+ },
+ {
+ "bytes": "0101010101010101010101010101010101010101010101010101010101010101"
+ }
+ ]
+ },
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Match"
+ },
+ {
+ "bytes": "0101010101010101010101010101010101010101010101010101010101010101"
+ }
+ ]
+ },
+ "durability": "persistent",
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "created_at"
+ },
+ "val": {
+ "u64": "12345"
+ }
+ },
+ {
+ "key": {
+ "symbol": "finalized_at"
+ },
+ "val": "void"
+ },
+ {
+ "key": {
+ "symbol": "players"
+ },
+ "val": {
+ "vec": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "report1_reporter"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ }
+ },
+ {
+ "key": {
+ "symbol": "report1_score"
+ },
+ "val": {
+ "i64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "report2_reporter"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ },
+ {
+ "key": {
+ "symbol": "report2_score"
+ },
+ "val": {
+ "i64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "stake_amount"
+ },
+ "val": {
+ "i128": "1000"
+ }
+ },
+ {
+ "key": {
+ "symbol": "stake_asset"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "state"
+ },
+ "val": {
+ "u32": 2
+ }
+ },
+ {
+ "key": {
+ "symbol": "winner"
+ },
+ "val": "void"
+ }
+ ]
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Admin"
+ }
+ ]
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/contracts/match-lifecycle/test_snapshots/test/test_submit_result_dual_reporting_dispute.1.json b/contracts/match-lifecycle/test_snapshots/test/test_submit_result_dual_reporting_dispute.1.json
new file mode 100644
index 0000000..b077057
--- /dev/null
+++ b/contracts/match-lifecycle/test_snapshots/test/test_submit_result_dual_reporting_dispute.1.json
@@ -0,0 +1,396 @@
+{
+ "generators": {
+ "address": 5,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "submit_result",
+ "args": [
+ {
+ "bytes": "0101010101010101010101010101010101010101010101010101010101010101"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ },
+ {
+ "i64": "0"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "submit_result",
+ "args": [
+ {
+ "bytes": "0101010101010101010101010101010101010101010101010101010101010101"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ },
+ {
+ "i64": "1"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 12345,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Match"
+ },
+ {
+ "bytes": "0101010101010101010101010101010101010101010101010101010101010101"
+ }
+ ]
+ },
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Match"
+ },
+ {
+ "bytes": "0101010101010101010101010101010101010101010101010101010101010101"
+ }
+ ]
+ },
+ "durability": "persistent",
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "created_at"
+ },
+ "val": {
+ "u64": "12345"
+ }
+ },
+ {
+ "key": {
+ "symbol": "finalized_at"
+ },
+ "val": "void"
+ },
+ {
+ "key": {
+ "symbol": "players"
+ },
+ "val": {
+ "vec": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "report1_reporter"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ }
+ },
+ {
+ "key": {
+ "symbol": "report1_score"
+ },
+ "val": {
+ "i64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "report2_reporter"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ },
+ {
+ "key": {
+ "symbol": "report2_score"
+ },
+ "val": {
+ "i64": "1"
+ }
+ },
+ {
+ "key": {
+ "symbol": "stake_amount"
+ },
+ "val": {
+ "i128": "1000"
+ }
+ },
+ {
+ "key": {
+ "symbol": "stake_asset"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "state"
+ },
+ "val": {
+ "u32": 4
+ }
+ },
+ {
+ "key": {
+ "symbol": "winner"
+ },
+ "val": "void"
+ }
+ ]
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Admin"
+ }
+ ]
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "1033654523790656264"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/contracts/match-lifecycle/test_snapshots/test/test_submit_result_non_participant_fails.1.json b/contracts/match-lifecycle/test_snapshots/test/test_submit_result_non_participant_fails.1.json
new file mode 100644
index 0000000..edec318
--- /dev/null
+++ b/contracts/match-lifecycle/test_snapshots/test/test_submit_result_non_participant_fails.1.json
@@ -0,0 +1,272 @@
+{
+ "generators": {
+ "address": 6,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 12345,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Match"
+ },
+ {
+ "bytes": "0101010101010101010101010101010101010101010101010101010101010101"
+ }
+ ]
+ },
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Match"
+ },
+ {
+ "bytes": "0101010101010101010101010101010101010101010101010101010101010101"
+ }
+ ]
+ },
+ "durability": "persistent",
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "created_at"
+ },
+ "val": {
+ "u64": "12345"
+ }
+ },
+ {
+ "key": {
+ "symbol": "finalized_at"
+ },
+ "val": "void"
+ },
+ {
+ "key": {
+ "symbol": "players"
+ },
+ "val": {
+ "vec": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "report1_reporter"
+ },
+ "val": "void"
+ },
+ {
+ "key": {
+ "symbol": "report1_score"
+ },
+ "val": "void"
+ },
+ {
+ "key": {
+ "symbol": "report2_reporter"
+ },
+ "val": "void"
+ },
+ {
+ "key": {
+ "symbol": "report2_score"
+ },
+ "val": "void"
+ },
+ {
+ "key": {
+ "symbol": "stake_amount"
+ },
+ "val": {
+ "i128": "1000"
+ }
+ },
+ {
+ "key": {
+ "symbol": "stake_asset"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "state"
+ },
+ "val": {
+ "u32": 0
+ }
+ },
+ {
+ "key": {
+ "symbol": "winner"
+ },
+ "val": "void"
+ }
+ ]
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Admin"
+ }
+ ]
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/contracts/match-lifecycle/test_snapshots/test/test_submit_result_same_reporter_twice_fails.1.json b/contracts/match-lifecycle/test_snapshots/test/test_submit_result_same_reporter_twice_fails.1.json
new file mode 100644
index 0000000..a73ef4c
--- /dev/null
+++ b/contracts/match-lifecycle/test_snapshots/test/test_submit_result_same_reporter_twice_fails.1.json
@@ -0,0 +1,334 @@
+{
+ "generators": {
+ "address": 5,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "initialize",
+ "args": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "function_name": "submit_result",
+ "args": [
+ {
+ "bytes": "0101010101010101010101010101010101010101010101010101010101010101"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ },
+ {
+ "i64": "0"
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 12345,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Match"
+ },
+ {
+ "bytes": "0101010101010101010101010101010101010101010101010101010101010101"
+ }
+ ]
+ },
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Match"
+ },
+ {
+ "bytes": "0101010101010101010101010101010101010101010101010101010101010101"
+ }
+ ]
+ },
+ "durability": "persistent",
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "created_at"
+ },
+ "val": {
+ "u64": "12345"
+ }
+ },
+ {
+ "key": {
+ "symbol": "finalized_at"
+ },
+ "val": "void"
+ },
+ {
+ "key": {
+ "symbol": "players"
+ },
+ "val": {
+ "vec": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
+ }
+ ]
+ }
+ },
+ {
+ "key": {
+ "symbol": "report1_reporter"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ }
+ },
+ {
+ "key": {
+ "symbol": "report1_score"
+ },
+ "val": {
+ "i64": "0"
+ }
+ },
+ {
+ "key": {
+ "symbol": "report2_reporter"
+ },
+ "val": "void"
+ },
+ {
+ "key": {
+ "symbol": "report2_score"
+ },
+ "val": "void"
+ },
+ {
+ "key": {
+ "symbol": "stake_amount"
+ },
+ "val": {
+ "i128": "1000"
+ }
+ },
+ {
+ "key": {
+ "symbol": "stake_asset"
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
+ }
+ },
+ {
+ "key": {
+ "symbol": "state"
+ },
+ "val": {
+ "u32": 1
+ }
+ },
+ {
+ "key": {
+ "symbol": "winner"
+ },
+ "val": "void"
+ }
+ ]
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Admin"
+ }
+ ]
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "5541220902715666415"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/contracts/reputation-index/Cargo.toml b/contracts/reputation-index/Cargo.toml
new file mode 100644
index 0000000..35f7898
--- /dev/null
+++ b/contracts/reputation-index/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "arenax-reputation-index"
+version = "0.0.0"
+edition = "2021"
+publish = false
+
+[lib]
+crate-type = ["cdylib"]
+doctest = false
+
+[dependencies]
+soroban-sdk = { workspace = true }
+
+[dev-dependencies]
+soroban-sdk = { workspace = true, features = ["testutils"] }
diff --git a/contracts/reputation-index/src/events.rs b/contracts/reputation-index/src/events.rs
new file mode 100644
index 0000000..88e9745
--- /dev/null
+++ b/contracts/reputation-index/src/events.rs
@@ -0,0 +1,16 @@
+use soroban_sdk::{contractevent, Address};
+// events
+#[contractevent]
+pub struct ReputationChanged {
+ pub player: Address,
+ pub skill_delta: i128,
+ pub fair_play_delta: i128,
+ pub match_id: u64,
+}
+
+#[contractevent]
+pub struct ReputationDecayed {
+ pub player: Address,
+ pub skill_decayed: i128,
+ pub fair_play_decayed: i128,
+}
diff --git a/contracts/reputation-index/src/lib.rs b/contracts/reputation-index/src/lib.rs
new file mode 100644
index 0000000..13f7253
--- /dev/null
+++ b/contracts/reputation-index/src/lib.rs
@@ -0,0 +1,209 @@
+#![no_std]
+use soroban_sdk::{
+ contract, contractimpl, contracttype, Address, Env, Vec,
+};
+
+#[contracttype]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Reputation {
+ pub skill: i128,
+ pub fair_play: i128,
+ pub last_update_ts: u64,
+}
+
+#[contracttype]
+pub enum DataKey {
+ Reputation(Address),
+ Admin,
+ AuthorizedMatchContract,
+ AuthorizedAntiCheatOracle,
+ DecayRate, // points per day (as i128)
+}
+
+#[contract]
+pub struct ReputationIndex;
+
+mod events;
+
+#[contractimpl]
+impl ReputationIndex {
+ /// Initialize the contract
+ pub fn initialize(env: Env, admin: Address, match_contract: Address, decay_rate: i128) {
+ if env.storage().instance().has(&DataKey::Admin) {
+ panic!("already initialized");
+ }
+ env.storage().instance().set(&DataKey::Admin, &admin);
+ env.storage().instance().set(&DataKey::AuthorizedMatchContract, &match_contract);
+ env.storage().instance().set(&DataKey::DecayRate, &decay_rate);
+ }
+
+ /// Update reputation after a match outcome is finalized.
+ /// outcome: skill delta for each player corresponding to the players list.
+ pub fn update_on_match(env: Env, match_id: u64, players: Vec, outcome: Vec) {
+ let match_contract: Address = env
+ .storage()
+ .instance()
+ .get(&DataKey::AuthorizedMatchContract)
+ .expect("match contract not set");
+
+ match_contract.require_auth();
+
+ if players.len() != outcome.len() {
+ panic!("players and outcome length mismatch");
+ }
+
+ let now = env.ledger().timestamp();
+
+ for i in 0..players.len() {
+ let player = players.get(i).unwrap();
+ let skill_delta = outcome.get(i).unwrap();
+
+ let mut rep = Self::get_reputation(env.clone(), player.clone());
+
+ // Apply decay before updating
+ rep = Self::internal_apply_decay(&env, rep, now);
+
+ let fair_play_delta = 1i128; // Completion bonus
+
+ rep.skill = rep.skill.saturating_add(skill_delta).max(0);
+ rep.fair_play = rep.fair_play.saturating_add(fair_play_delta).max(0);
+ rep.last_update_ts = now;
+
+ env.storage().persistent().set(&DataKey::Reputation(player.clone()), &rep);
+
+ // Emit reputation_changed event
+ events::ReputationChanged {
+ player: player.clone(),
+ skill_delta,
+ fair_play_delta,
+ match_id,
+ }
+ .publish(&env);
+ }
+ }
+
+ /// Explicitly apply decay to a player's reputation based on a timestamp.
+ pub fn apply_decay(env: Env, addr: Address, now_ts: u64) {
+ let mut rep = Self::get_reputation(env.clone(), addr.clone());
+ let old_skill = rep.skill;
+ let old_fair_play = rep.fair_play;
+
+ rep = Self::internal_apply_decay(&env, rep, now_ts);
+ env.storage().persistent().set(&DataKey::Reputation(addr.clone()), &rep);
+
+ // Emit decay event
+ events::ReputationDecayed {
+ player: addr,
+ skill_decayed: old_skill - rep.skill,
+ fair_play_decayed: old_fair_play - rep.fair_play,
+ }
+ .publish(&env);
+ }
+
+ /// Get current reputation for a player.
+ pub fn get_reputation(env: Env, addr: Address) -> Reputation {
+ env.storage()
+ .persistent()
+ .get(&DataKey::Reputation(addr))
+ .unwrap_or(Reputation {
+ skill: 1000,
+ fair_play: 100,
+ last_update_ts: env.ledger().timestamp(),
+ })
+ }
+
+ fn internal_apply_decay(env: &Env, mut rep: Reputation, now: u64) -> Reputation {
+ let elapsed = now.saturating_sub(rep.last_update_ts);
+ if elapsed == 0 {
+ return rep;
+ }
+
+ let decay_rate: i128 = env.storage().instance().get(&DataKey::DecayRate).unwrap_or(0);
+ if decay_rate == 0 {
+ return rep;
+ }
+
+ // decay_rate is points per day (86400 seconds)
+ let decay_amount = (elapsed as i128 * decay_rate) / 86400;
+
+ if decay_amount > 0 {
+ rep.skill = rep.skill.saturating_sub(decay_amount).max(0);
+ rep.fair_play = rep.fair_play.saturating_sub(decay_amount).max(0);
+ rep.last_update_ts = now;
+ }
+
+ rep
+ }
+
+ pub fn set_decay_rate(env: Env, admin: Address, new_rate: i128) {
+ let saved_admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
+ if admin != saved_admin {
+ panic!("not admin");
+ }
+ admin.require_auth();
+ env.storage().instance().set(&DataKey::DecayRate, &new_rate);
+ }
+
+ /// Set the authorized anti-cheat oracle contract (admin only). That contract may call
+ /// apply_anticheat_penalty to apply bounded fair_play penalties.
+ pub fn set_authorized_anticheat_oracle(env: Env, admin: Address, oracle: Address) {
+ let saved_admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
+ if admin != saved_admin {
+ panic!("not admin");
+ }
+ admin.require_auth();
+ env.storage()
+ .instance()
+ .set(&DataKey::AuthorizedAntiCheatOracle, &oracle);
+ }
+
+ /// Apply a bounded anti-cheat penalty to a player's fair_play score.
+ /// Callable only by the authorized anti-cheat oracle contract. Penalty is capped and
+ /// fair_play cannot underflow (floor at 0).
+ pub fn apply_anticheat_penalty(
+ env: Env,
+ oracle: Address,
+ player: Address,
+ match_id: u64,
+ penalty: i128,
+ ) {
+ oracle.require_auth();
+ let authorized: Address = env
+ .storage()
+ .instance()
+ .get(&DataKey::AuthorizedAntiCheatOracle)
+ .expect("anticheat oracle not set");
+ if oracle != authorized {
+ panic!("not authorized anticheat oracle");
+ }
+ // Cap penalty at a maximum (e.g. 100 per call) to keep penalties bounded
+ const MAX_PENALTY_PER_FLAG: i128 = 100;
+ let capped = if penalty > MAX_PENALTY_PER_FLAG {
+ MAX_PENALTY_PER_FLAG
+ } else if penalty < 0 {
+ 0
+ } else {
+ penalty
+ };
+ if capped == 0 {
+ return;
+ }
+ let now = env.ledger().timestamp();
+ let mut rep = Self::get_reputation(env.clone(), player.clone());
+ rep = Self::internal_apply_decay(&env, rep, now);
+ rep.fair_play = rep.fair_play.saturating_sub(capped).max(0);
+ rep.last_update_ts = now;
+ env.storage()
+ .persistent()
+ .set(&DataKey::Reputation(player.clone()), &rep);
+ events::ReputationChanged {
+ player,
+ skill_delta: 0,
+ fair_play_delta: -(capped as i128),
+ match_id,
+ }
+ .publish(&env);
+ }
+}
+
+mod test;
diff --git a/contracts/reputation-index/src/test.rs b/contracts/reputation-index/src/test.rs
new file mode 100644
index 0000000..24978b8
--- /dev/null
+++ b/contracts/reputation-index/src/test.rs
@@ -0,0 +1,45 @@
+#![cfg(test)]
+
+use super::*;
+use soroban_sdk::{testutils::Address as _, vec, Address, Env};
+
+#[test]
+fn test_reputation_index() {
+ let env = Env::default();
+ env.mock_all_auths();
+
+ let admin = Address::generate(&env);
+ let match_contract = Address::generate(&env);
+ let player1 = Address::generate(&env);
+
+ let contract_id = env.register(ReputationIndex, ());
+ let client = ReputationIndexClient::new(&env, &contract_id);
+
+ // Initialize with 10 points decay per day
+ client.initialize(&admin, &match_contract, &10);
+
+ // Initial reputation (default)
+ let rep = client.get_reputation(&player1);
+ assert_eq!(rep.skill, 1000);
+ assert_eq!(rep.fair_play, 100);
+
+ // Update match outcome
+ let players = vec![&env, player1.clone()];
+ let outcomes = vec![&env, 25i128]; // +25 skill
+ client.update_on_match(&1, &players, &outcomes);
+
+ let rep = client.get_reputation(&player1);
+ assert_eq!(rep.skill, 1025);
+ assert_eq!(rep.fair_play, 101);
+
+ // Test decay after 1 day (86400 seconds)
+ let one_day_later = env.ledger().timestamp() + 86400;
+ client.apply_decay(&player1, &one_day_later);
+
+ let rep = client.get_reputation(&player1);
+ // 1025 - 10 = 1015
+ // 101 - 10 = 91
+ assert_eq!(rep.skill, 1015);
+ assert_eq!(rep.fair_play, 91);
+ assert_eq!(rep.last_update_ts, one_day_later);
+}
diff --git a/contracts/reputation-index/test_snapshots/test/test_reputation_index.1.json b/contracts/reputation-index/test_snapshots/test/test_reputation_index.1.json
new file mode 100644
index 0000000..2cc4fe9
--- /dev/null
+++ b/contracts/reputation-index/test_snapshots/test/test_reputation_index.1.json
@@ -0,0 +1,254 @@
+{
+ "generators": {
+ "address": 4,
+ "nonce": 0,
+ "mux_id": 0
+ },
+ "auth": [
+ [],
+ [],
+ [],
+ [
+ [
+ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ {
+ "function": {
+ "contract_fn": {
+ "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "function_name": "update_on_match",
+ "args": [
+ {
+ "u64": "1"
+ },
+ {
+ "vec": [
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ }
+ ]
+ },
+ {
+ "vec": [
+ {
+ "i128": "25"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "sub_invocations": []
+ }
+ ]
+ ],
+ [],
+ [],
+ []
+ ],
+ "ledger": {
+ "protocol_version": 23,
+ "sequence_number": 0,
+ "timestamp": 0,
+ "network_id": "0000000000000000000000000000000000000000000000000000000000000000",
+ "base_reserve": 0,
+ "min_persistent_entry_ttl": 4096,
+ "min_temp_entry_ttl": 16,
+ "max_entry_ttl": 6312000,
+ "ledger_entries": [
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
+ "key": {
+ "ledger_key_nonce": {
+ "nonce": "801925984706572462"
+ }
+ },
+ "durability": "temporary",
+ "val": "void"
+ }
+ },
+ "ext": "v0"
+ },
+ 6311999
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Reputation"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ }
+ ]
+ },
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": {
+ "vec": [
+ {
+ "symbol": "Reputation"
+ },
+ {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
+ }
+ ]
+ },
+ "durability": "persistent",
+ "val": {
+ "map": [
+ {
+ "key": {
+ "symbol": "fair_play"
+ },
+ "val": {
+ "i128": "91"
+ }
+ },
+ {
+ "key": {
+ "symbol": "last_update_ts"
+ },
+ "val": {
+ "u64": "86400"
+ }
+ },
+ {
+ "key": {
+ "symbol": "skill"
+ },
+ "val": {
+ "i128": "1015"
+ }
+ }
+ ]
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_data": {
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_data": {
+ "ext": "v0",
+ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4",
+ "key": "ledger_key_contract_instance",
+ "durability": "persistent",
+ "val": {
+ "contract_instance": {
+ "executable": {
+ "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ "storage": [
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "Admin"
+ }
+ ]
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM"
+ }
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "AuthorizedMatchContract"
+ }
+ ]
+ },
+ "val": {
+ "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4"
+ }
+ },
+ {
+ "key": {
+ "vec": [
+ {
+ "symbol": "DecayRate"
+ }
+ ]
+ },
+ "val": {
+ "i128": "10"
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ],
+ [
+ {
+ "contract_code": {
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ }
+ },
+ [
+ {
+ "last_modified_ledger_seq": 0,
+ "data": {
+ "contract_code": {
+ "ext": "v0",
+ "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "code": ""
+ }
+ },
+ "ext": "v0"
+ },
+ 4095
+ ]
+ ]
+ ]
+ },
+ "events": []
+}
\ No newline at end of file
diff --git a/contracts/staking-manager/Cargo.toml b/contracts/staking-manager/Cargo.toml
new file mode 100644
index 0000000..a8b2647
--- /dev/null
+++ b/contracts/staking-manager/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "staking-manager"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+repository.workspace = true
+description = "ArenaX Tournament Staking Manager - AX token staking for tournaments"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+soroban-sdk.workspace = true
+
+[dev-dependencies]
+soroban-sdk = { workspace = true, features = ["testutils"] }
+
+[features]
+testutils = ["soroban-sdk/testutils"]
diff --git a/contracts/staking-manager/src/lib.rs b/contracts/staking-manager/src/lib.rs
new file mode 100644
index 0000000..14375cb
--- /dev/null
+++ b/contracts/staking-manager/src/lib.rs
@@ -0,0 +1,723 @@
+#![no_std]
+
+use soroban_sdk::{
+ contract, contractevent, contractimpl, contracttype, token, Address, BytesN, Env, Map, Symbol,
+};
+
+#[contracttype]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum DataKey {
+ Admin,
+ AxToken,
+ TournamentContract,
+ DisputeContract,
+ Stake(BytesN<32>, Address), // tournament_id, user_address
+ TournamentInfo(BytesN<32>),
+ UserStakeInfo(Address),
+ Paused,
+}
+
+#[contracttype]
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+#[repr(u32)]
+pub enum TournamentState {
+ NotStarted = 0,
+ Active = 1,
+ Completed = 2,
+ Cancelled = 3,
+}
+
+#[contracttype]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct StakeInfo {
+ pub user: Address,
+ pub tournament_id: BytesN<32>,
+ pub amount: i128,
+ pub staked_at: u64,
+ pub is_locked: bool,
+ pub can_withdraw: bool,
+}
+
+#[contracttype]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct TournamentInfo {
+ pub tournament_id: BytesN<32>,
+ pub state: u32,
+ pub stake_requirement: i128,
+ pub total_staked: i128,
+ pub participant_count: u32,
+ pub created_at: u64,
+ pub completed_at: Option,
+}
+
+#[contracttype]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct UserStakeInfo {
+ pub user: Address,
+ pub total_staked: i128,
+ pub total_slashed: i128,
+ pub active_tournaments: u32,
+ pub completed_tournaments: u32,
+}
+
+#[contractevent]
+pub struct Initialized {
+ pub admin: Address,
+ pub ax_token: Address,
+}
+
+#[contractevent]
+pub struct TokenSet {
+ pub token: Address,
+}
+
+#[contractevent]
+pub struct TournamentContractSet {
+ pub contract: Address,
+}
+
+#[contractevent]
+pub struct DisputeContractSet {
+ pub contract: Address,
+}
+
+#[contractevent]
+pub struct Staked {
+ pub user: Address,
+ pub tournament_id: BytesN<32>,
+ pub amount: i128,
+}
+
+#[contractevent]
+pub struct Withdrawn {
+ pub user: Address,
+ pub tournament_id: BytesN<32>,
+ pub amount: i128,
+}
+
+#[contractevent]
+pub struct Slashed {
+ pub user: Address,
+ pub tournament_id: BytesN<32>,
+ pub amount: i128,
+ pub slashed_by: Address,
+}
+
+#[contractevent]
+pub struct TournamentCreated {
+ pub tournament_id: BytesN<32>,
+ pub stake_requirement: i128,
+}
+
+#[contractevent]
+pub struct TournamentUpdated {
+ pub tournament_id: BytesN<32>,
+ pub state: u32,
+}
+
+#[contractevent]
+pub struct ContractPaused {
+ pub paused: bool,
+ pub paused_by: Address,
+}
+
+#[contract]
+pub struct StakingManager;
+
+#[contractimpl]
+impl StakingManager {
+ /// Initialize the staking manager with admin and AX token address
+ ///
+ /// # Arguments
+ /// * `admin` - The admin address
+ /// * `ax_token` - The AX token contract address
+ ///
+ /// # Panics
+ /// * If contract is already initialized
+ pub fn initialize(env: Env, admin: Address, ax_token: Address) {
+ if env.storage().instance().has(&DataKey::Admin) {
+ panic!("already initialized");
+ }
+
+ admin.require_auth();
+ env.storage().instance().set(&DataKey::Admin, &admin);
+ env.storage().instance().set(&DataKey::AxToken, &ax_token);
+ env.storage().instance().set(&DataKey::Paused, &false);
+
+ Initialized { admin, ax_token }.publish(&env);
+ }
+
+ /// Set the AX token address
+ ///
+ /// # Arguments
+ /// * `ax_token` - The AX token contract address
+ ///
+ /// # Panics
+ /// * If caller is not admin
+ pub fn set_ax_token(env: Env, ax_token: Address) {
+ Self::require_admin(&env);
+ env.storage().instance().set(&DataKey::AxToken, &ax_token);
+
+ TokenSet { token: ax_token }.publish(&env);
+ }
+
+ /// Set the tournament contract address
+ ///
+ /// # Arguments
+ /// * `tournament_contract` - The tournament contract address
+ ///
+ /// # Panics
+ /// * If caller is not admin
+ pub fn set_tournament_contract(env: Env, tournament_contract: Address) {
+ Self::require_admin(&env);
+ env.storage()
+ .instance()
+ .set(&DataKey::TournamentContract, &tournament_contract);
+
+ TournamentContractSet {
+ contract: tournament_contract,
+ }
+ .publish(&env);
+ }
+
+ /// Set the dispute contract address
+ ///
+ /// # Arguments
+ /// * `dispute_contract` - The dispute contract address
+ ///
+ /// # Panics
+ /// * If caller is not admin
+ pub fn set_dispute_contract(env: Env, dispute_contract: Address) {
+ Self::require_admin(&env);
+ env.storage()
+ .instance()
+ .set(&DataKey::DisputeContract, &dispute_contract);
+
+ DisputeContractSet {
+ contract: dispute_contract,
+ }
+ .publish(&env);
+ }
+
+ /// Create a new tournament
+ ///
+ /// # Arguments
+ /// * `tournament_id` - Unique tournament identifier
+ /// * `stake_requirement` - Required stake amount for participation
+ ///
+ /// # Panics
+ /// * If contract is paused
+ /// * If caller is not admin or tournament contract
+ /// * If tournament already exists
+ /// * If stake requirement is not positive
+ pub fn create_tournament(env: Env, tournament_id: BytesN<32>, stake_requirement: i128) {
+ Self::require_not_paused(&env);
+ Self::require_admin_or_tournament_contract(&env);
+
+ if stake_requirement <= 0 {
+ panic!("stake requirement must be positive");
+ }
+
+ if env
+ .storage()
+ .persistent()
+ .has(&DataKey::TournamentInfo(tournament_id.clone()))
+ {
+ panic!("tournament already exists");
+ }
+
+ let tournament_info = TournamentInfo {
+ tournament_id: tournament_id.clone(),
+ state: TournamentState::NotStarted as u32,
+ stake_requirement,
+ total_staked: 0,
+ participant_count: 0,
+ created_at: env.ledger().timestamp(),
+ completed_at: None,
+ };
+
+ env.storage()
+ .persistent()
+ .set(&DataKey::TournamentInfo(tournament_id.clone()), &tournament_info);
+
+ TournamentCreated {
+ tournament_id,
+ stake_requirement,
+ }
+ .publish(&env);
+ }
+
+ /// Update tournament state
+ ///
+ /// # Arguments
+ /// * `tournament_id` - Tournament identifier
+ /// * `state` - New tournament state
+ ///
+ /// # Panics
+ /// * If contract is paused
+ /// * If caller is not admin or tournament contract
+ /// * If tournament doesn't exist
+ pub fn update_tournament_state(env: Env, tournament_id: BytesN<32>, state: u32) {
+ Self::require_not_paused(&env);
+ Self::require_admin_or_tournament_contract(&env);
+
+ let mut tournament_info: TournamentInfo = env
+ .storage()
+ .persistent()
+ .get(&DataKey::TournamentInfo(tournament_id.clone()))
+ .expect("tournament not found");
+
+ let old_state = tournament_info.state;
+ tournament_info.state = state;
+
+ if state == TournamentState::Completed as u32 || state == TournamentState::Cancelled as u32 {
+ tournament_info.completed_at = Some(env.ledger().timestamp());
+
+ // Unlock all stakes for this tournament
+ Self::unlock_tournament_stakes(&env, &tournament_id);
+ }
+
+ env.storage()
+ .persistent()
+ .set(&DataKey::TournamentInfo(tournament_id.clone()), &tournament_info);
+
+ TournamentUpdated {
+ tournament_id,
+ state,
+ }
+ .publish(&env);
+ }
+
+ /// Stake AX tokens for tournament participation
+ ///
+ /// # Arguments
+ /// * `user` - The user staking tokens
+ /// * `tournament_id` - Tournament identifier
+ /// * `amount` - Amount to stake
+ ///
+ /// # Panics
+ /// * If contract is paused
+ /// * If tournament doesn't exist or is not active
+ /// * If amount doesn't meet stake requirement
+ /// * If user has already staked for this tournament
+ pub fn stake(env: Env, user: Address, tournament_id: BytesN<32>, amount: i128) {
+ Self::require_not_paused(&env);
+ user.require_auth();
+
+ if amount <= 0 {
+ panic!("amount must be positive");
+ }
+
+ let tournament_info: TournamentInfo = env
+ .storage()
+ .persistent()
+ .get(&DataKey::TournamentInfo(tournament_id.clone()))
+ .expect("tournament not found");
+
+ if tournament_info.state != TournamentState::Active as u32 {
+ panic!("tournament is not active");
+ }
+
+ if amount < tournament_info.stake_requirement {
+ panic!("amount below stake requirement");
+ }
+
+ let stake_key = DataKey::Stake(tournament_id.clone(), user.clone());
+ if env.storage().persistent().has(&stake_key) {
+ panic!("user already staked for this tournament");
+ }
+
+ // Transfer AX tokens to contract
+ let ax_token = Self::get_ax_token(&env);
+ let contract_address = env.current_contract_address();
+ let token_client = token::Client::new(&env, &ax_token);
+ token_client.transfer(&user, &contract_address, &amount);
+
+ // Create stake record
+ let stake_info = StakeInfo {
+ user: user.clone(),
+ tournament_id: tournament_id.clone(),
+ amount,
+ staked_at: env.ledger().timestamp(),
+ is_locked: true,
+ can_withdraw: false,
+ };
+
+ env.storage()
+ .persistent()
+ .set(&stake_key, &stake_info);
+
+ // Update tournament info
+ let mut updated_tournament_info = tournament_info;
+ updated_tournament_info.total_staked += amount;
+ updated_tournament_info.participant_count += 1;
+ env.storage()
+ .persistent()
+ .set(&DataKey::TournamentInfo(tournament_id.clone()), &updated_tournament_info);
+
+ // Update user stake info
+ Self::update_user_stake_info(&env, &user, amount, 0, 1, 0);
+
+ Staked {
+ user,
+ tournament_id,
+ amount,
+ }
+ .publish(&env);
+ }
+
+ /// Withdraw staked tokens after tournament completion
+ ///
+ /// # Arguments
+ /// * `user` - The user withdrawing tokens
+ /// * `tournament_id` - Tournament identifier
+ ///
+ /// # Panics
+ /// * If contract is paused
+ /// * If user has no stake for this tournament
+ /// * If stake is still locked
+ pub fn withdraw(env: Env, user: Address, tournament_id: BytesN<32>) {
+ Self::require_not_paused(&env);
+ user.require_auth();
+
+ let stake_key = DataKey::Stake(tournament_id.clone(), user.clone());
+ let mut stake_info: StakeInfo = env
+ .storage()
+ .persistent()
+ .get(&stake_key)
+ .expect("no stake found");
+
+ if !stake_info.can_withdraw {
+ panic!("stake is not withdrawable");
+ }
+
+ // Transfer tokens back to user
+ let ax_token = Self::get_ax_token(&env);
+ let contract_address = env.current_contract_address();
+ let token_client = token::Client::new(&env, &ax_token);
+ token_client.transfer(&contract_address, &user, &stake_info.amount);
+
+ // Remove stake record
+ env.storage().persistent().remove(&stake_key);
+
+ // Update user stake info
+ Self::update_user_stake_info(&env, &user, -stake_info.amount, 0, -1, 1);
+
+ Withdrawn {
+ user,
+ tournament_id,
+ amount: stake_info.amount,
+ }
+ .publish(&env);
+ }
+
+ /// Slash user's stake based on dispute resolution
+ ///
+ /// # Arguments
+ /// * `user` - The user being slashed
+ /// * `tournament_id` - Tournament identifier
+ /// * `amount` - Amount to slash
+ /// * `slashed_by` - Address authorizing the slash
+ ///
+ /// # Panics
+ /// * If contract is paused
+ /// * If caller is not authorized (dispute contract or admin)
+ /// * If user has no stake for this tournament
+ /// * If amount exceeds staked amount
+ pub fn slash(env: Env, user: Address, tournament_id: BytesN<32>, amount: i128, slashed_by: Address) {
+ Self::require_not_paused(&env);
+ Self::require_dispute_contract_or_admin(&env, &slashed_by);
+
+ if amount <= 0 {
+ panic!("amount must be positive");
+ }
+
+ let stake_key = DataKey::Stake(tournament_id.clone(), user.clone());
+ let mut stake_info: StakeInfo = env
+ .storage()
+ .persistent()
+ .get(&stake_key)
+ .expect("no stake found");
+
+ if amount > stake_info.amount {
+ panic!("slash amount exceeds staked amount");
+ }
+
+ // Transfer slashed amount to treasury (or burn)
+ let ax_token = Self::get_ax_token(&env);
+ let contract_address = env.current_contract_address();
+ let token_client = token::Client::new(&env, &ax_token);
+
+ // For now, we'll burn the slashed tokens by sending to a dead address
+ // In production, this might go to a treasury address
+ let dead_address = Address::from_contract_id(&BytesN::from_array(&env, &[0u8; 32]));
+ token_client.transfer(&contract_address, &dead_address, &amount);
+
+ // Update stake info
+ stake_info.amount -= amount;
+ if stake_info.amount == 0 {
+ // Remove stake record if fully slashed
+ env.storage().persistent().remove(&stake_key);
+ } else {
+ env.storage()
+ .persistent()
+ .set(&stake_key, &stake_info);
+ }
+
+ // Update tournament info
+ let mut tournament_info: TournamentInfo = env
+ .storage()
+ .persistent()
+ .get(&DataKey::TournamentInfo(tournament_id.clone()))
+ .expect("tournament not found");
+ tournament_info.total_staked -= amount;
+ if stake_info.amount == 0 {
+ tournament_info.participant_count -= 1;
+ }
+ env.storage()
+ .persistent()
+ .set(&DataKey::TournamentInfo(tournament_id.clone()), &tournament_info);
+
+ // Update user stake info
+ Self::update_user_stake_info(&env, &user, 0, amount, 0, 0);
+
+ Slashed {
+ user,
+ tournament_id,
+ amount,
+ slashed_by,
+ }
+ .publish(&env);
+ }
+
+ /// Get user's stake information for a tournament
+ ///
+ /// # Arguments
+ /// * `user` - User address
+ /// * `tournament_id` - Tournament identifier
+ ///
+ /// # Returns
+ /// Stake information or panics if not found
+ pub fn get_stake(env: Env, user: Address, tournament_id: BytesN<32>) -> StakeInfo {
+ env.storage()
+ .persistent()
+ .get(&DataKey::Stake(tournament_id, user))
+ .expect("stake not found")
+ }
+
+ /// Get tournament information
+ ///
+ /// # Arguments
+ /// * `tournament_id` - Tournament identifier
+ ///
+ /// # Returns
+ /// Tournament information or panics if not found
+ pub fn get_tournament_info(env: Env, tournament_id: BytesN<32>) -> TournamentInfo {
+ env.storage()
+ .persistent()
+ .get(&DataKey::TournamentInfo(tournament_id))
+ .expect("tournament not found")
+ }
+
+ /// Get user's overall stake information
+ ///
+ /// # Arguments
+ /// * `user` - User address
+ ///
+ /// # Returns
+ /// User's stake information
+ pub fn get_user_stake_info(env: Env, user: Address) -> UserStakeInfo {
+ env.storage()
+ .instance()
+ .get(&DataKey::UserStakeInfo(user))
+ .unwrap_or(UserStakeInfo {
+ user,
+ total_staked: 0,
+ total_slashed: 0,
+ active_tournaments: 0,
+ completed_tournaments: 0,
+ })
+ }
+
+ /// Check if user can withdraw stake from tournament
+ ///
+ /// # Arguments
+ /// * `user` - User address
+ /// * `tournament_id` - Tournament identifier
+ ///
+ /// # Returns
+ /// True if withdrawal is allowed
+ pub fn can_withdraw(env: Env, user: Address, tournament_id: BytesN<32>) -> bool {
+ if let Some(stake_info) = env
+ .storage()
+ .persistent()
+ .get::(&DataKey::Stake(tournament_id, user))
+ {
+ stake_info.can_withdraw
+ } else {
+ false
+ }
+ }
+
+ /// Get total staked amount for a tournament
+ ///
+ /// # Arguments
+ /// * `tournament_id` - Tournament identifier
+ ///
+ /// # Returns
+ /// Total staked amount
+ pub fn get_total_staked(env: Env, tournament_id: BytesN<32>) -> i128 {
+ let tournament_info: TournamentInfo = env
+ .storage()
+ .persistent()
+ .get(&DataKey::TournamentInfo(tournament_id))
+ .expect("tournament not found");
+ tournament_info.total_staked
+ }
+
+ /// Pause/unpause the contract
+ ///
+ /// # Arguments
+ /// * `paused` - Whether to pause the contract
+ ///
+ /// # Panics
+ /// * If caller is not admin
+ pub fn set_paused(env: Env, paused: bool) {
+ Self::require_admin(&env);
+ let admin = env.current_contract_address();
+
+ env.storage().instance().set(&DataKey::Paused, &paused);
+
+ ContractPaused {
+ paused,
+ paused_by: admin,
+ }
+ .publish(&env);
+ }
+
+ /// Get the admin address
+ ///
+ /// # Returns
+ /// The admin address
+ ///
+ /// # Panics
+ /// * If contract is not initialized
+ pub fn get_admin(env: Env) -> Address {
+ env.storage()
+ .instance()
+ .get(&DataKey::Admin)
+ .expect("not initialized")
+ }
+
+ /// Get the AX token address
+ ///
+ /// # Returns
+ /// The AX token address
+ ///
+ /// # Panics
+ /// * If token is not set
+ pub fn get_ax_token(env: Env) -> Address {
+ env.storage()
+ .instance()
+ .get(&DataKey::AxToken)
+ .expect("AX token not set")
+ }
+
+ /// Check if the contract is paused
+ ///
+ /// # Returns
+ /// True if the contract is paused, false otherwise
+ pub fn is_paused(env: Env) -> bool {
+ env.storage()
+ .instance()
+ .get(&DataKey::Paused)
+ .unwrap_or(false)
+ }
+
+ // Helper functions for internal use
+
+ fn unlock_tournament_stakes(env: &Env, tournament_id: &BytesN<32>) {
+ // This would need to iterate through all stakes for the tournament
+ // For now, this is a placeholder - in a real implementation,
+ // you'd need a way to efficiently find all stakes for a tournament
+ }
+
+ fn update_user_stake_info(
+ env: &Env,
+ user: &Address,
+ staked_amount: i128,
+ slashed_amount: i128,
+ active_delta: i32,
+ completed_delta: i32,
+ ) {
+ let mut user_info: UserStakeInfo = env
+ .storage()
+ .instance()
+ .get(&DataKey::UserStakeInfo(user.clone()))
+ .unwrap_or(UserStakeInfo {
+ user: user.clone(),
+ total_staked: 0,
+ total_slashed: 0,
+ active_tournaments: 0,
+ completed_tournaments: 0,
+ });
+
+ user_info.total_staked += staked_amount;
+ user_info.total_slashed += slashed_amount;
+ user_info.active_tournaments = (user_info.active_tournaments as i32 + active_delta) as u32;
+ user_info.completed_tournaments =
+ (user_info.completed_tournaments as i32 + completed_delta) as u32;
+
+ env.storage()
+ .instance()
+ .set(&DataKey::UserStakeInfo(user.clone()), &user_info);
+ }
+
+ fn require_admin(env: &Env) {
+ let admin = Self::get_admin(env.clone());
+ admin.require_auth();
+ }
+
+ fn require_not_paused(env: &Env) {
+ let paused = Self::is_paused(env.clone());
+ if paused {
+ panic!("contract is paused");
+ }
+ }
+
+ fn require_admin_or_tournament_contract(env: &Env) {
+ let admin = Self::get_admin(env.clone());
+
+ if let Some(tournament_contract) = env
+ .storage()
+ .instance()
+ .get::(&DataKey::TournamentContract)
+ {
+ // Check if caller is admin or tournament contract
+ // This is simplified - in practice, you'd check the actual caller
+ admin.require_auth();
+ } else {
+ admin.require_auth();
+ }
+ }
+
+ fn require_dispute_contract_or_admin(env: &Env, caller: &Address) {
+ let admin = Self::get_admin(env.clone());
+
+ if caller == &admin {
+ return;
+ }
+
+ if let Some(dispute_contract) = env
+ .storage()
+ .instance()
+ .get::(&DataKey::DisputeContract)
+ {
+ if caller == &dispute_contract {
+ return;
+ }
+ }
+
+ panic!("caller not authorized");
+ }
+}
diff --git a/contracts/staking-manager/src/test.rs b/contracts/staking-manager/src/test.rs
new file mode 100644
index 0000000..c0ba101
--- /dev/null
+++ b/contracts/staking-manager/src/test.rs
@@ -0,0 +1,638 @@
+#![cfg(test)]
+
+use super::*;
+use soroban_sdk::{
+ testutils::{Address as _, Ledger as _},
+ token::{StellarAssetClient, TokenClient as SdkTokenClient},
+ Address, BytesN, Env,
+};
+
+fn create_test_env() -> (Env, Address, Address, Address) {
+ let env = Env::default();
+ let admin = Address::generate(&env);
+ let user1 = Address::generate(&env);
+ let user2 = Address::generate(&env);
+ (env, admin, user1, user2)
+}
+
+fn initialize_contract(env: &Env, admin: &Address) -> Address {
+ let contract_id = Address::generate(env);
+ env.register_contract(&contract_id, StakingManager);
+ let client = StakingManagerClient::new(env, &contract_id);
+
+ let ax_token = create_ax_token(env, admin);
+
+ env.mock_all_auths();
+ client.initialize(admin, &ax_token);
+
+ contract_id
+}
+
+fn create_ax_token(env: &Env, admin: &Address) -> Address {
+ let token_address = env.register_stellar_asset_contract_v2(admin.clone());
+ token_address.address()
+}
+
+fn mint_ax_tokens(env: &Env, token: &Address, admin: &Address, to: &Address, amount: i128) {
+ let stellar_client = StellarAssetClient::new(env, token);
+ stellar_client.mint(to, &amount);
+}
+
+fn generate_tournament_id(env: &Env, seed: u32) -> BytesN<32> {
+ let mut bytes = [0u8; 32];
+ bytes[0..4].copy_from_slice(&seed.to_be_bytes());
+ BytesN::from_array(env, &bytes)
+}
+
+#[test]
+fn test_initialization() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ assert_eq!(client.get_admin(), admin);
+ assert!(!client.is_paused());
+
+ let ax_token = create_ax_token(&env, &admin);
+ client.set_ax_token(&ax_token);
+ assert_eq!(client.get_ax_token(), ax_token);
+}
+
+#[test]
+#[should_panic(expected = "already initialized")]
+fn test_double_initialization() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ let ax_token = create_ax_token(&env, &admin);
+ client.initialize(&admin, &ax_token);
+}
+
+#[test]
+fn test_create_tournament() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ let tournament_id = generate_tournament_id(&env, 1);
+ let stake_requirement = 1000i128;
+
+ env.mock_all_auths();
+ client.create_tournament(&tournament_id, &stake_requirement);
+
+ let tournament_info = client.get_tournament_info(&tournament_id);
+ assert_eq!(tournament_info.tournament_id, tournament_id);
+ assert_eq!(tournament_info.stake_requirement, stake_requirement);
+ assert_eq!(tournament_info.state, TournamentState::NotStarted as u32);
+ assert_eq!(tournament_info.total_staked, 0);
+ assert_eq!(tournament_info.participant_count, 0);
+}
+
+#[test]
+#[should_panic(expected = "stake requirement must be positive")]
+fn test_create_tournament_zero_requirement_fails() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ let tournament_id = generate_tournament_id(&env, 1);
+
+ env.mock_all_auths();
+ client.create_tournament(&tournament_id, &0);
+}
+
+#[test]
+#[should_panic(expected = "tournament already exists")]
+fn test_create_duplicate_tournament_fails() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ let tournament_id = generate_tournament_id(&env, 1);
+
+ env.mock_all_auths();
+ client.create_tournament(&tournament_id, &1000);
+ client.create_tournament(&tournament_id, &1000);
+}
+
+#[test]
+#[should_panic(expected = "already initialized")]
+fn test_create_tournament_unauthorized() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ let tournament_id = generate_tournament_id(&env, 1);
+ client.create_tournament(&tournament_id, &1000);
+}
+
+#[test]
+fn test_update_tournament_state() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ let tournament_id = generate_tournament_id(&env, 1);
+
+ env.mock_all_auths();
+ client.create_tournament(&tournament_id, &1000);
+
+ client.update_tournament_state(&tournament_id, &(TournamentState::Active as u32));
+ let tournament_info = client.get_tournament_info(&tournament_id);
+ assert_eq!(tournament_info.state, TournamentState::Active as u32);
+
+ client.update_tournament_state(&tournament_id, &(TournamentState::Completed as u32));
+ let updated_info = client.get_tournament_info(&tournament_id);
+ assert_eq!(updated_info.state, TournamentState::Completed as u32);
+ assert!(updated_info.completed_at.is_some());
+}
+
+#[test]
+fn test_stake() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ let tournament_id = generate_tournament_id(&env, 1);
+ let stake_amount = 1000i128;
+
+ env.mock_all_auths();
+ client.create_tournament(&tournament_id, &1000);
+ client.update_tournament_state(&tournament_id, &(TournamentState::Active as u32));
+
+ let ax_token = client.get_ax_token();
+ mint_ax_tokens(&env, &ax_token, &admin, &user1, stake_amount * 2);
+
+ client.stake(&user1, &tournament_id, &stake_amount);
+
+ let stake_info = client.get_stake(&user1, &tournament_id);
+ assert_eq!(stake_info.user, user1);
+ assert_eq!(stake_info.tournament_id, tournament_id);
+ assert_eq!(stake_info.amount, stake_amount);
+ assert!(stake_info.is_locked);
+ assert!(!stake_info.can_withdraw);
+
+ let tournament_info = client.get_tournament_info(&tournament_id);
+ assert_eq!(tournament_info.total_staked, stake_amount);
+ assert_eq!(tournament_info.participant_count, 1);
+
+ let user_info = client.get_user_stake_info(&user1);
+ assert_eq!(user_info.total_staked, stake_amount);
+ assert_eq!(user_info.active_tournaments, 1);
+}
+
+#[test]
+#[should_panic(expected = "amount must be positive")]
+fn test_stake_zero_amount_fails() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ let tournament_id = generate_tournament_id(&env, 1);
+
+ env.mock_all_auths();
+ client.create_tournament(&tournament_id, &1000);
+ client.update_tournament_state(&tournament_id, &(TournamentState::Active as u32));
+
+ client.stake(&user1, &tournament_id, &0);
+}
+
+#[test]
+#[should_panic(expected = "tournament is not active")]
+fn test_stake_inactive_tournament_fails() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ let tournament_id = generate_tournament_id(&env, 1);
+
+ env.mock_all_auths();
+ client.create_tournament(&tournament_id, &1000);
+
+ let ax_token = client.get_ax_token();
+ mint_ax_tokens(&env, &ax_token, &admin, &user1, 1000);
+
+ client.stake(&user1, &tournament_id, &1000);
+}
+
+#[test]
+#[should_panic(expected = "amount below stake requirement")]
+fn test_stake_below_requirement_fails() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ let tournament_id = generate_tournament_id(&env, 1);
+
+ env.mock_all_auths();
+ client.create_tournament(&tournament_id, &1000);
+ client.update_tournament_state(&tournament_id, &(TournamentState::Active as u32));
+
+ let ax_token = client.get_ax_token();
+ mint_ax_tokens(&env, &ax_token, &admin, &user1, 500);
+
+ client.stake(&user1, &tournament_id, &500);
+}
+
+#[test]
+#[should_panic(expected = "user already staked for this tournament")]
+fn test_stake_twice_fails() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ let tournament_id = generate_tournament_id(&env, 1);
+
+ env.mock_all_auths();
+ client.create_tournament(&tournament_id, &1000);
+ client.update_tournament_state(&tournament_id, &(TournamentState::Active as u32));
+
+ let ax_token = client.get_ax_token();
+ mint_ax_tokens(&env, &ax_token, &admin, &user1, 2000);
+
+ client.stake(&user1, &tournament_id, &1000);
+ client.stake(&user1, &tournament_id, &1000);
+}
+
+#[test]
+fn test_withdraw() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ let tournament_id = generate_tournament_id(&env, 1);
+ let stake_amount = 1000i128;
+
+ env.mock_all_auths();
+ client.create_tournament(&tournament_id, &1000);
+ client.update_tournament_state(&tournament_id, &(TournamentState::Active as u32));
+
+ let ax_token = client.get_ax_token();
+ let token_client = SdkTokenClient::new(&env, &ax_token);
+
+ mint_ax_tokens(&env, &ax_token, &admin, &user1, stake_amount * 2);
+ let initial_balance = token_client.balance(&user1);
+
+ client.stake(&user1, &tournament_id, &stake_amount);
+ assert_eq!(token_client.balance(&user1), initial_balance - stake_amount);
+
+ client.update_tournament_state(&tournament_id, &(TournamentState::Completed as u32));
+
+ client.withdraw(&user1, &tournament_id);
+ assert_eq!(token_client.balance(&user1), initial_balance);
+
+ let user_info = client.get_user_stake_info(&user1);
+ assert_eq!(user_info.total_staked, 0);
+ assert_eq!(user_info.active_tournaments, 0);
+ assert_eq!(user_info.completed_tournaments, 1);
+}
+
+#[test]
+#[should_panic(expected = "no stake found")]
+fn test_withdraw_no_stake_fails() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ let tournament_id = generate_tournament_id(&env, 1);
+
+ env.mock_all_auths();
+ client.create_tournament(&tournament_id, &1000);
+ client.update_tournament_state(&tournament_id, &(TournamentState::Completed as u32));
+
+ client.withdraw(&user1, &tournament_id);
+}
+
+#[test]
+#[should_panic(expected = "stake is not withdrawable")]
+fn test_withdraw_locked_fails() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ let tournament_id = generate_tournament_id(&env, 1);
+
+ env.mock_all_auths();
+ client.create_tournament(&tournament_id, &1000);
+ client.update_tournament_state(&tournament_id, &(TournamentState::Active as u32));
+
+ let ax_token = client.get_ax_token();
+ mint_ax_tokens(&env, &ax_token, &admin, &user1, 1000);
+
+ client.stake(&user1, &tournament_id, &1000);
+ client.withdraw(&user1, &tournament_id);
+}
+
+#[test]
+fn test_slash() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ let tournament_id = generate_tournament_id(&env, 1);
+ let stake_amount = 1000i128;
+ let slash_amount = 300i128;
+
+ env.mock_all_auths();
+ client.create_tournament(&tournament_id, &1000);
+ client.update_tournament_state(&tournament_id, &(TournamentState::Active as u32));
+
+ let ax_token = client.get_ax_token();
+ mint_ax_tokens(&env, &ax_token, &admin, &user1, stake_amount * 2);
+
+ client.stake(&user1, &tournament_id, &stake_amount);
+
+ let dispute_contract = Address::generate(&env);
+ client.set_dispute_contract(&dispute_contract);
+
+ client.slash(&user1, &tournament_id, &slash_amount, &dispute_contract);
+
+ let stake_info = client.get_stake(&user1, &tournament_id);
+ assert_eq!(stake_info.amount, stake_amount - slash_amount);
+
+ let tournament_info = client.get_tournament_info(&tournament_id);
+ assert_eq!(tournament_info.total_staked, stake_amount - slash_amount);
+
+ let user_info = client.get_user_stake_info(&user1);
+ assert_eq!(user_info.total_slashed, slash_amount);
+}
+
+#[test]
+#[should_panic(expected = "slash amount exceeds staked amount")]
+fn test_slash_exceeds_stake_fails() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ let tournament_id = generate_tournament_id(&env, 1);
+
+ env.mock_all_auths();
+ client.create_tournament(&tournament_id, &1000);
+ client.update_tournament_state(&tournament_id, &(TournamentState::Active as u32));
+
+ let ax_token = client.get_ax_token();
+ mint_ax_tokens(&env, &ax_token, &admin, &user1, 1000);
+
+ client.stake(&user1, &tournament_id, &1000);
+
+ let dispute_contract = Address::generate(&env);
+ client.set_dispute_contract(&dispute_contract);
+
+ client.slash(&user1, &tournament_id, &1500, &dispute_contract);
+}
+
+#[test]
+#[should_panic(expected = "amount must be positive")]
+fn test_slash_zero_amount_fails() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ let tournament_id = generate_tournament_id(&env, 1);
+
+ env.mock_all_auths();
+ client.create_tournament(&tournament_id, &1000);
+ client.update_tournament_state(&tournament_id, &(TournamentState::Active as u32));
+
+ let ax_token = client.get_ax_token();
+ mint_ax_tokens(&env, &ax_token, &admin, &user1, 1000);
+
+ client.stake(&user1, &tournament_id, &1000);
+
+ let dispute_contract = Address::generate(&env);
+ client.set_dispute_contract(&dispute_contract);
+
+ client.slash(&user1, &tournament_id, &0, &dispute_contract);
+}
+
+#[test]
+#[should_panic(expected = "caller not authorized")]
+fn test_slash_unauthorized_fails() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ let tournament_id = generate_tournament_id(&env, 1);
+
+ env.mock_all_auths();
+ client.create_tournament(&tournament_id, &1000);
+ client.update_tournament_state(&tournament_id, &(TournamentState::Active as u32));
+
+ let ax_token = client.get_ax_token();
+ mint_ax_tokens(&env, &ax_token, &admin, &user1, 1000);
+
+ client.stake(&user1, &tournament_id, &1000);
+
+ let random_address = Address::generate(&env);
+ client.slash(&user1, &tournament_id, &300, &random_address);
+}
+
+#[test]
+fn test_pause_contract() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ env.mock_all_auths();
+
+ assert!(!client.is_paused());
+ client.set_paused(&true);
+ assert!(client.is_paused());
+ client.set_paused(&false);
+ assert!(!client.is_paused());
+}
+
+#[test]
+#[should_panic(expected = "already initialized")]
+fn test_pause_contract_unauthorized() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ client.set_paused(&true);
+}
+
+#[test]
+#[should_panic(expected = "contract is paused")]
+fn test_operations_when_paused() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ let tournament_id = generate_tournament_id(&env, 1);
+
+ env.mock_all_auths();
+ client.set_paused(&true);
+
+ client.create_tournament(&tournament_id, &1000);
+}
+
+#[test]
+fn test_get_total_staked() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ let tournament_id = generate_tournament_id(&env, 1);
+
+ env.mock_all_auths();
+ client.create_tournament(&tournament_id, &1000);
+ client.update_tournament_state(&tournament_id, &(TournamentState::Active as u32));
+
+ let ax_token = client.get_ax_token();
+ mint_ax_tokens(&env, &ax_token, &admin, &user1, 1000);
+ mint_ax_tokens(&env, &ax_token, &admin, &user2, 1000);
+
+ assert_eq!(client.get_total_staked(&tournament_id), 0);
+
+ client.stake(&user1, &tournament_id, &1000);
+ assert_eq!(client.get_total_staked(&tournament_id), 1000);
+
+ client.stake(&user2, &tournament_id, &1000);
+ assert_eq!(client.get_total_staked(&tournament_id), 2000);
+}
+
+#[test]
+fn test_can_withdraw() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ let tournament_id = generate_tournament_id(&env, 1);
+
+ env.mock_all_auths();
+ client.create_tournament(&tournament_id, &1000);
+ client.update_tournament_state(&tournament_id, &(TournamentState::Active as u32));
+
+ let ax_token = client.get_ax_token();
+ mint_ax_tokens(&env, &ax_token, &admin, &user1, 1000);
+
+ assert!(!client.can_withdraw(&user1, &tournament_id));
+
+ client.stake(&user1, &tournament_id, &1000);
+ assert!(!client.can_withdraw(&user1, &tournament_id));
+
+ client.update_tournament_state(&tournament_id, &(TournamentState::Completed as u32));
+ assert!(client.can_withdraw(&user1, &tournament_id));
+
+ client.withdraw(&user1, &tournament_id);
+ assert!(!client.can_withdraw(&user1, &tournament_id));
+}
+
+#[test]
+fn test_full_staking_lifecycle() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ let tournament_id = generate_tournament_id(&env, 1);
+ let stake_amount = 1000i128;
+
+ env.mock_all_auths();
+ client.create_tournament(&tournament_id, &stake_amount);
+ client.update_tournament_state(&tournament_id, &(TournamentState::Active as u32));
+
+ let ax_token = client.get_ax_token();
+ let token_client = SdkTokenClient::new(&env, &ax_token);
+
+ mint_ax_tokens(&env, &ax_token, &admin, &user1, stake_amount * 2);
+ let initial_balance = token_client.balance(&user1);
+
+ client.stake(&user1, &tournament_id, &stake_amount);
+ assert_eq!(token_client.balance(&user1), initial_balance - stake_amount);
+
+ let dispute_contract = Address::generate(&env);
+ client.set_dispute_contract(&dispute_contract);
+
+ client.slash(&user1, &tournament_id, &(stake_amount / 2), &dispute_contract);
+ let stake_info = client.get_stake(&user1, &tournament_id);
+ assert_eq!(stake_info.amount, stake_amount / 2);
+
+ client.update_tournament_state(&tournament_id, &(TournamentState::Completed as u32));
+ assert!(client.can_withdraw(&user1, &tournament_id));
+
+ client.withdraw(&user1, &tournament_id);
+ assert_eq!(token_client.balance(&user1), initial_balance - (stake_amount / 2));
+
+ let user_info = client.get_user_stake_info(&user1);
+ assert_eq!(user_info.total_staked, stake_amount);
+ assert_eq!(user_info.total_slashed, stake_amount / 2);
+ assert_eq!(user_info.active_tournaments, 0);
+ assert_eq!(user_info.completed_tournaments, 1);
+}
+
+#[test]
+fn test_multiple_users_staking() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ let tournament_id = generate_tournament_id(&env, 1);
+
+ env.mock_all_auths();
+ client.create_tournament(&tournament_id, &1000);
+ client.update_tournament_state(&tournament_id, &(TournamentState::Active as u32));
+
+ let ax_token = client.get_ax_token();
+ mint_ax_tokens(&env, &ax_token, &admin, &user1, 1000);
+ mint_ax_tokens(&env, &ax_token, &admin, &user2, 1000);
+
+ client.stake(&user1, &tournament_id, &1000);
+ client.stake(&user2, &tournament_id, &1000);
+
+ let tournament_info = client.get_tournament_info(&tournament_id);
+ assert_eq!(tournament_info.total_staked, 2000);
+ assert_eq!(tournament_info.participant_count, 2);
+
+ let user1_info = client.get_user_stake_info(&user1);
+ let user2_info = client.get_user_stake_info(&user2);
+ assert_eq!(user1_info.active_tournaments, 1);
+ assert_eq!(user2_info.active_tournaments, 1);
+
+ client.update_tournament_state(&tournament_id, &(TournamentState::Completed as u32));
+
+ client.withdraw(&user1, &tournament_id);
+ client.withdraw(&user2, &tournament_id);
+
+ let final_user1_info = client.get_user_stake_info(&user1);
+ let final_user2_info = client.get_user_stake_info(&user2);
+ assert_eq!(final_user1_info.active_tournaments, 0);
+ assert_eq!(final_user1_info.completed_tournaments, 1);
+ assert_eq!(final_user2_info.active_tournaments, 0);
+ assert_eq!(final_user2_info.completed_tournaments, 1);
+}
+
+#[test]
+fn test_contract_configuration() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ let tournament_contract = Address::generate(&env);
+ let dispute_contract = Address::generate(&env);
+
+ env.mock_all_auths();
+ client.set_tournament_contract(&tournament_contract);
+ client.set_dispute_contract(&dispute_contract);
+
+ let ax_token = create_ax_token(&env, &admin);
+ client.set_ax_token(&ax_token);
+}
+
+#[test]
+fn test_edge_cases() {
+ let (env, admin, user1, user2) = create_test_env();
+ let contract_id = initialize_contract(&env, &admin);
+ let client = StakingManagerClient::new(&env, &contract_id);
+
+ let tournament_id = generate_tournament_id(&env, 1);
+
+ let user_info = client.get_user_stake_info(&user1);
+ assert_eq!(user_info.total_staked, 0);
+ assert_eq!(user_info.total_slashed, 0);
+ assert_eq!(user_info.active_tournaments, 0);
+ assert_eq!(user_info.completed_tournaments, 0);
+
+ assert!(!client.can_withdraw(&user1, &tournament_id));
+}
diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json
new file mode 100644
index 0000000..e54411d
--- /dev/null
+++ b/frontend/.eslintrc.json
@@ -0,0 +1 @@
+{"extends": "next/core-web-vitals"}
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 19b777d..a0204a2 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -8,13 +8,17 @@
"name": "arenax-frontend",
"version": "0.1.0",
"dependencies": {
+ "@albedo-link/intent": "^0.13.0",
"@radix-ui/react-slot": "^1.0.2",
+ "@stellar/freighter-api": "^6.0.1",
+ "@stellar/stellar-sdk": "^14.5.0",
+ "@tanstack/react-query": "^5.90.21",
"autoprefixer": "^10.4.16",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"eslint": "^8.55.0",
"eslint-config-next": "14.0.0",
- "framer-motion": "^11.0.0",
+ "framer-motion": "^11.18.2",
"lucide-react": "^0.294.0",
"next": "14.0.0",
"next-themes": "^0.4.6",
@@ -34,6 +38,7 @@
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"jest": "^29.7.0",
+ "jest-environment-jsdom": "^30.2.0",
"typescript": "^5.3.0"
}
},
@@ -44,6 +49,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@albedo-link/intent": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/@albedo-link/intent/-/intent-0.13.0.tgz",
+ "integrity": "sha512-A8CBXqGQEBMXhwxNXj5inC6HLjyx5Do7jW99NOFeecYd1nPUq8gfM0tvoNoR8H8JQ11aTl9tyQBuu/+l3xeBnQ==",
+ "license": "MIT"
+ },
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@@ -56,6 +67,27 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
+ "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-calc": "^2.1.3",
+ "@csstools/css-color-parser": "^3.0.9",
+ "@csstools/css-parser-algorithms": "^3.0.4",
+ "@csstools/css-tokenizer": "^3.0.3",
+ "lru-cache": "^10.4.3"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/@babel/code-frame": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz",
@@ -582,6 +614,121 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@csstools/color-helpers": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
+ "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
+ "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
+ "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^5.1.0",
+ "@csstools/css-calc": "^2.1.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
+ "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
+ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@emnapi/core": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz",
@@ -941,6 +1088,235 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
+ "node_modules/@jest/environment-jsdom-abstract": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.2.0.tgz",
+ "integrity": "sha512-kazxw2L9IPuZpQ0mEt9lu9Z98SqR74xcagANmMBU16X0lS23yPc0+S6hGLUz8kVRlomZEs/5S/Zlpqwf5yu6OQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "30.2.0",
+ "@jest/fake-timers": "30.2.0",
+ "@jest/types": "30.2.0",
+ "@types/jsdom": "^21.1.7",
+ "@types/node": "*",
+ "jest-mock": "30.2.0",
+ "jest-util": "30.2.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/environment": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz",
+ "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/fake-timers": "30.2.0",
+ "@jest/types": "30.2.0",
+ "@types/node": "*",
+ "jest-mock": "30.2.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/fake-timers": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz",
+ "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "30.2.0",
+ "@sinonjs/fake-timers": "^13.0.0",
+ "@types/node": "*",
+ "jest-message-util": "30.2.0",
+ "jest-mock": "30.2.0",
+ "jest-util": "30.2.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/schemas": {
+ "version": "30.0.5",
+ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
+ "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.34.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/types": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
+ "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/pattern": "30.0.1",
+ "@jest/schemas": "30.0.5",
+ "@types/istanbul-lib-coverage": "^2.0.6",
+ "@types/istanbul-reports": "^3.0.4",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.33",
+ "chalk": "^4.1.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinclair/typebox": {
+ "version": "0.34.48",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
+ "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinonjs/fake-timers": {
+ "version": "13.0.5",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz",
+ "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.1"
+ }
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/ci-info": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz",
+ "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-message-util": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz",
+ "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@jest/types": "30.2.0",
+ "@types/stack-utils": "^2.0.3",
+ "chalk": "^4.1.2",
+ "graceful-fs": "^4.2.11",
+ "micromatch": "^4.0.8",
+ "pretty-format": "30.2.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.6"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-mock": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz",
+ "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "30.2.0",
+ "@types/node": "*",
+ "jest-util": "30.2.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-util": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
+ "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "30.2.0",
+ "@types/node": "*",
+ "chalk": "^4.1.2",
+ "ci-info": "^4.2.0",
+ "graceful-fs": "^4.2.11",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/pretty-format": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",
+ "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "30.0.5",
+ "ansi-styles": "^5.2.0",
+ "react-is": "^18.3.1"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/environment-jsdom-abstract/node_modules/react-is": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@jest/expect": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz",
@@ -1002,6 +1378,30 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
+ "node_modules/@jest/pattern": {
+ "version": "30.0.1",
+ "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz",
+ "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "jest-regex-util": "30.0.1"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/@jest/pattern/node_modules/jest-regex-util": {
+ "version": "30.0.1",
+ "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz",
+ "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
"node_modules/@jest/reporters": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz",
@@ -1368,6 +1768,33 @@
"node": ">= 10"
}
},
+ "node_modules/@noble/curves": {
+ "version": "1.9.7",
+ "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
+ "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "1.8.0"
+ },
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@noble/hashes": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
+ "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
+ "license": "MIT",
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1532,6 +1959,83 @@
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT"
},
+ "node_modules/@stellar/freighter-api": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@stellar/freighter-api/-/freighter-api-6.0.1.tgz",
+ "integrity": "sha512-eqwakEqSg+zoLuPpSbKyrX0pG8DQFzL/J5GtbfuMCmJI+h+oiC9pQ5C6QLc80xopZQKdGt8dUAFCmDMNdAG95w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "buffer": "6.0.3",
+ "semver": "7.7.1"
+ }
+ },
+ "node_modules/@stellar/freighter-api/node_modules/semver": {
+ "version": "7.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
+ "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@stellar/js-xdr": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-3.1.2.tgz",
+ "integrity": "sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@stellar/stellar-base": {
+ "version": "14.0.4",
+ "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-14.0.4.tgz",
+ "integrity": "sha512-UbNW6zbdOBXJwLAV2mMak0bIC9nw3IZVlQXkv2w2dk1jgCbJjy3oRVC943zeGE5JAm0Z9PHxrIjmkpGhayY7kw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@noble/curves": "^1.9.6",
+ "@stellar/js-xdr": "^3.1.2",
+ "base32.js": "^0.1.0",
+ "bignumber.js": "^9.3.1",
+ "buffer": "^6.0.3",
+ "sha.js": "^2.4.12"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@stellar/stellar-sdk": {
+ "version": "14.5.0",
+ "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-14.5.0.tgz",
+ "integrity": "sha512-Uzjq+An/hUA+Q5ERAYPtT0+MMiwWnYYWMwozmZMjxjdL2MmSjucBDF8Q04db6K/ekU4B5cHuOfsdlrfaxQYblw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@stellar/stellar-base": "^14.0.4",
+ "axios": "^1.13.3",
+ "bignumber.js": "^9.3.1",
+ "commander": "^14.0.2",
+ "eventsource": "^2.0.2",
+ "feaxios": "^0.0.23",
+ "randombytes": "^2.1.0",
+ "toml": "^3.0.0",
+ "urijs": "^1.19.1"
+ },
+ "bin": {
+ "stellar-js": "bin/stellar-js"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@stellar/stellar-sdk/node_modules/commander": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
+ "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/@swc/helpers": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz",
@@ -1541,6 +2045,32 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@tanstack/query-core": {
+ "version": "5.90.20",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz",
+ "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.90.21",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz",
+ "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.90.20"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19"
+ }
+ },
"node_modules/@testing-library/dom": {
"version": "9.3.4",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz",
@@ -1779,6 +2309,18 @@
"@types/istanbul-lib-report": "*"
}
},
+ "node_modules/@types/jsdom": {
+ "version": "21.1.7",
+ "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz",
+ "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/tough-cookie": "*",
+ "parse5": "^7.0.0"
+ }
+ },
"node_modules/@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@@ -1830,6 +2372,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/tough-cookie": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
+ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
@@ -2257,6 +2806,16 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -2542,6 +3101,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
"node_modules/autoprefixer": {
"version": "10.4.23",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
@@ -2602,6 +3167,17 @@
"node": ">=4"
}
},
+ "node_modules/axios": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
+ "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -2743,6 +3319,35 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
+ "node_modules/base32.js": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz",
+ "integrity": "sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/baseline-browser-mapping": {
"version": "2.9.17",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.17.tgz",
@@ -2752,6 +3357,15 @@
"baseline-browser-mapping": "dist/cli.js"
}
},
+ "node_modules/bignumber.js": {
+ "version": "9.3.1",
+ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
+ "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -2829,6 +3443,30 @@
"node-int64": "^0.4.0"
}
},
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -3105,6 +3743,18 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -3182,6 +3832,20 @@
"node": ">=4"
}
},
+ "node_modules/cssstyle": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
+ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^3.2.0",
+ "rrweb-cssom": "^0.8.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -3316,6 +3980,20 @@
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
"license": "BSD-2-Clause"
},
+ "node_modules/data-urls": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
+ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/data-view-buffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
@@ -3384,6 +4062,13 @@
}
}
},
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/decimal.js-light": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
@@ -3488,6 +4173,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/detect-newline": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
@@ -3590,6 +4284,19 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"license": "MIT"
},
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/error-ex": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
@@ -4264,6 +4971,15 @@
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
"license": "MIT"
},
+ "node_modules/eventsource": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz",
+ "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -4379,6 +5095,15 @@
"bser": "2.1.1"
}
},
+ "node_modules/feaxios": {
+ "version": "0.0.23",
+ "resolved": "https://registry.npmjs.org/feaxios/-/feaxios-0.0.23.tgz",
+ "integrity": "sha512-eghR0A21fvbkcQBgZuMfQhrXxJzC0GNUGC9fXhBge33D+mFDTwl0aJ35zoQQn575BhyjQitRc5N4f+L4cP708g==",
+ "license": "MIT",
+ "dependencies": {
+ "is-retry-allowed": "^3.0.0"
+ }
+ },
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -4439,6 +5164,26 @@
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"license": "ISC"
},
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@@ -4454,6 +5199,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/fraction.js": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
@@ -4871,6 +5632,19 @@
"node": ">= 0.4"
}
},
+ "node_modules/html-encoding-sniffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@@ -4878,6 +5652,34 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/human-signals": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
@@ -4888,6 +5690,39 @@
"node": ">=10.17.0"
}
},
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -5307,6 +6142,13 @@
"node": ">=8"
}
},
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@@ -5325,6 +6167,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-retry-allowed": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-3.0.0.tgz",
+ "integrity": "sha512-9xH0xvoggby+u0uGF7cZXdrutWiBiaFG8ZT4YFPXL8NzkyAwX3AKGLeFQLvzDpM430+nDFBZ1LHkie/8ocL06A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-set": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
@@ -5803,93 +6657,319 @@
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-diff/node_modules/pretty-format": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
+ "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-diff/node_modules/react-is": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jest-docblock": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz",
+ "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "detect-newline": "^3.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-each": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz",
+ "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "^29.6.3",
+ "chalk": "^4.0.0",
+ "jest-get-type": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-each/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-each/node_modules/pretty-format": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
+ "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-each/node_modules/react-is": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jest-environment-jsdom": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.2.0.tgz",
+ "integrity": "sha512-zbBTiqr2Vl78pKp/laGBREYzbZx9ZtqPjOK4++lL4BNDhxRnahg51HtoDrk9/VjIy9IthNEWdKVd7H5bqBhiWQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "30.2.0",
+ "@jest/environment-jsdom-abstract": "30.2.0",
+ "@types/jsdom": "^21.1.7",
+ "@types/node": "*",
+ "jsdom": "^26.1.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/@jest/environment": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz",
+ "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/fake-timers": "30.2.0",
+ "@jest/types": "30.2.0",
+ "@types/node": "*",
+ "jest-mock": "30.2.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/@jest/fake-timers": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz",
+ "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/types": "30.2.0",
+ "@sinonjs/fake-timers": "^13.0.0",
+ "@types/node": "*",
+ "jest-message-util": "30.2.0",
+ "jest-mock": "30.2.0",
+ "jest-util": "30.2.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/@jest/schemas": {
+ "version": "30.0.5",
+ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
+ "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.34.0"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/@jest/types": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
+ "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/pattern": "30.0.1",
+ "@jest/schemas": "30.0.5",
+ "@types/istanbul-lib-coverage": "^2.0.6",
+ "@types/istanbul-reports": "^3.0.4",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.33",
+ "chalk": "^4.1.2"
+ },
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/@sinclair/typebox": {
+ "version": "0.34.48",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
+ "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jest-environment-jsdom/node_modules/@sinonjs/fake-timers": {
+ "version": "13.0.5",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz",
+ "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.1"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-environment-jsdom/node_modules/ci-info": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz",
+ "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
}
},
- "node_modules/jest-diff/node_modules/pretty-format": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
- "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
+ "node_modules/jest-environment-jsdom/node_modules/jest-message-util": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz",
+ "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/schemas": "^29.6.3",
- "ansi-styles": "^5.0.0",
- "react-is": "^18.0.0"
+ "@babel/code-frame": "^7.27.1",
+ "@jest/types": "30.2.0",
+ "@types/stack-utils": "^2.0.3",
+ "chalk": "^4.1.2",
+ "graceful-fs": "^4.2.11",
+ "micromatch": "^4.0.8",
+ "pretty-format": "30.2.0",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.6"
},
"engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
- "node_modules/jest-diff/node_modules/react-is": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
- "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/jest-docblock": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz",
- "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==",
+ "node_modules/jest-environment-jsdom/node_modules/jest-mock": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz",
+ "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "detect-newline": "^3.0.0"
+ "@jest/types": "30.2.0",
+ "@types/node": "*",
+ "jest-util": "30.2.0"
},
"engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
- "node_modules/jest-each": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz",
- "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==",
+ "node_modules/jest-environment-jsdom/node_modules/jest-util": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
+ "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/types": "^29.6.3",
- "chalk": "^4.0.0",
- "jest-get-type": "^29.6.3",
- "jest-util": "^29.7.0",
- "pretty-format": "^29.7.0"
+ "@jest/types": "30.2.0",
+ "@types/node": "*",
+ "chalk": "^4.1.2",
+ "ci-info": "^4.2.0",
+ "graceful-fs": "^4.2.11",
+ "picomatch": "^4.0.2"
},
"engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
- "node_modules/jest-each/node_modules/ansi-styles": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
- "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "node_modules/jest-environment-jsdom/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=10"
+ "node": ">=12"
},
"funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ "url": "https://github.com/sponsors/jonschlinkert"
}
},
- "node_modules/jest-each/node_modules/pretty-format": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
- "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
+ "node_modules/jest-environment-jsdom/node_modules/pretty-format": {
+ "version": "30.2.0",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",
+ "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jest/schemas": "^29.6.3",
- "ansi-styles": "^5.0.0",
- "react-is": "^18.0.0"
+ "@jest/schemas": "30.0.5",
+ "ansi-styles": "^5.2.0",
+ "react-is": "^18.3.1"
},
"engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
- "node_modules/jest-each/node_modules/react-is": {
+ "node_modules/jest-environment-jsdom/node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
@@ -6481,6 +7561,46 @@
"js-yaml": "bin/js-yaml.js"
}
},
+ "node_modules/jsdom": {
+ "version": "26.1.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
+ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssstyle": "^4.2.1",
+ "data-urls": "^5.0.0",
+ "decimal.js": "^10.5.0",
+ "html-encoding-sniffer": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.6",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.16",
+ "parse5": "^7.2.1",
+ "rrweb-cssom": "^0.8.0",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^5.1.1",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^3.1.1",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.1.1",
+ "ws": "^8.18.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -6751,6 +7871,27 @@
"node": ">=8.6"
}
},
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
@@ -6983,6 +8124,13 @@
"node": ">=8"
}
},
+ "node_modules/nwsapi": {
+ "version": "2.2.23",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz",
+ "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -7254,6 +8402,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -7634,6 +8795,12 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -7680,6 +8847,15 @@
],
"license": "MIT"
},
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
@@ -7969,6 +9145,13 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/rrweb-cssom": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
+ "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -8011,6 +9194,26 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/safe-push-apply": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
@@ -8044,6 +9247,26 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
@@ -8111,6 +9334,26 @@
"node": ">= 0.4"
}
},
+ "node_modules/sha.js": {
+ "version": "2.4.12",
+ "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz",
+ "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==",
+ "license": "(MIT AND BSD-3-Clause)",
+ "dependencies": {
+ "inherits": "^2.0.4",
+ "safe-buffer": "^5.2.1",
+ "to-buffer": "^1.2.0"
+ },
+ "bin": {
+ "sha.js": "bin.js"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -8583,6 +9826,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tailwind-merge": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
@@ -8732,6 +9982,26 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/tldts": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
+ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^6.1.86"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
+ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -8739,6 +10009,20 @@
"dev": true,
"license": "BSD-3-Clause"
},
+ "node_modules/to-buffer": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz",
+ "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==",
+ "license": "MIT",
+ "dependencies": {
+ "isarray": "^2.0.5",
+ "safe-buffer": "^5.2.1",
+ "typed-array-buffer": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -8751,6 +10035,38 @@
"node": ">=8.0"
}
},
+ "node_modules/toml": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",
+ "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==",
+ "license": "MIT"
+ },
+ "node_modules/tough-cookie": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
+ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^6.1.32"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/ts-api-utils": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
@@ -9027,6 +10343,12 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/urijs": {
+ "version": "1.19.11",
+ "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz",
+ "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==",
+ "license": "MIT"
+ },
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
@@ -9079,6 +10401,19 @@
"d3-timer": "^3.0.1"
}
},
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/walker": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
@@ -9102,6 +10437,54 @@
"node": ">=10.13.0"
}
},
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -9249,6 +10632,45 @@
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
+ "node_modules/ws": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
+ "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 300470d..5a51afb 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -11,13 +11,17 @@
"test:watch": "jest --watch"
},
"dependencies": {
+ "@albedo-link/intent": "^0.13.0",
"@radix-ui/react-slot": "^1.0.2",
+ "@stellar/freighter-api": "^6.0.1",
+ "@stellar/stellar-sdk": "^14.5.0",
+ "@tanstack/react-query": "^5.90.21",
"autoprefixer": "^10.4.16",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"eslint": "^8.55.0",
"eslint-config-next": "14.0.0",
- "framer-motion": "^11.0.0",
+ "framer-motion": "^11.18.2",
"lucide-react": "^0.294.0",
"next": "14.0.0",
"next-themes": "^0.4.6",
@@ -37,6 +41,7 @@
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"jest": "^29.7.0",
+ "jest-environment-jsdom": "^30.2.0",
"typescript": "^5.3.0"
}
}
diff --git a/frontend/src/app/about/page.tsx b/frontend/src/app/about/page.tsx
index 53ebd19..6986d88 100644
--- a/frontend/src/app/about/page.tsx
+++ b/frontend/src/app/about/page.tsx
@@ -1,4 +1,5 @@
import { Metadata } from "next";
+import Image from "next/image";
import { Card, CardContent } from "@/components/ui/Card";
import { Shield, Zap, Globe, Users } from "lucide-react";
@@ -87,8 +88,8 @@ export default function AboutPage() {
are delivered instantly, transparently, and globally.
- By leveraging Stellar's lightning-fast blockchain and Soroban's smart contract
- capabilities, we've built a tournament platform that eliminates the middlemen
+ By leveraging Stellar's lightning-fast blockchain and Soroban's smart contract
+ capabilities, we've built a tournament platform that eliminates the middlemen
and puts players first.
@@ -96,7 +97,7 @@ export default function AboutPage() {
Our Vision
- To become the world's most trusted competitive gaming platform by making every
+ To become the world's most trusted competitive gaming platform by making every
match verifiable, every prize instant, and every player empowered.
@@ -182,10 +183,11 @@ export default function AboutPage() {
{team.map((member) => (
-
diff --git a/frontend/src/app/contact/ContactForm.tsx b/frontend/src/app/contact/ContactForm.tsx
index 55a556f..7b81b4c 100644
--- a/frontend/src/app/contact/ContactForm.tsx
+++ b/frontend/src/app/contact/ContactForm.tsx
@@ -49,7 +49,7 @@ export function ContactForm() {
Send us a Message
- Fill out the form below and we'll get back to you within 24 hours.
+ Fill out the form below and we'll get back to you within 24 hours.
@@ -64,7 +64,7 @@ export function ContactForm() {
Message Sent!
- Thank you for reaching out. We'll respond to your inquiry soon.
+ Thank you for reaching out. We'll respond to your inquiry soon.
) : (
diff --git a/frontend/src/app/contact/page.tsx b/frontend/src/app/contact/page.tsx
index cf4f03b..2d86853 100644
--- a/frontend/src/app/contact/page.tsx
+++ b/frontend/src/app/contact/page.tsx
@@ -19,7 +19,7 @@ export default function ContactPage() {
Contact
& Support
- Have questions? We're here to help. Reach out to our team or check our FAQs.
+ Have questions? We're here to help. Reach out to our team or check our FAQs.
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index 2f76dff..100fbd7 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -31,16 +31,13 @@ export default function RootLayout({
- {children}
+
+ {children}
+
-
-
- {children}
-
-