v0.3.10-pre.005-fix.002
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 497
|
||||
# version: 498
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-raw-transaction-lib", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.10-pre.5.fix.1"
|
||||
version = "0.3.10-pre.5.fix.2"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-job-backfill-lib/tests/hardening.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Adversarial, security, visibility and external-boundary hardening canaries for `pre.010`.
|
||||
|
||||
@@ -275,7 +275,7 @@ fn pre_010_manifest_dependency_surface_is_exact_and_backend_neutral() {
|
||||
"tokio",
|
||||
])
|
||||
);
|
||||
assert_eq!(dev, std::collections::BTreeSet::from(["tokio"]));
|
||||
assert_eq!(dev, std::collections::BTreeSet::from(["tokio", "tokio-tungstenite"]));
|
||||
assert!(manifest.contains("ksp-store-lib = { path = \"../ksp-store-lib\", default-features = false }"));
|
||||
assert!(manifest.contains("tokio = { workspace = true, features = [\"macros\", \"sync\"] }"));
|
||||
assert!(!manifest.contains("ksp-store-postgres-lib"));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-job-backfill-lib/tests/ws_raw_parity.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Cross-layer parity canaries qualifying standard `blockSubscribe` and Helius `transactionSubscribe` for RAW v1 ingestion.
|
||||
|
||||
@@ -179,7 +179,7 @@ fn raw_from_block_transaction(
|
||||
},
|
||||
_ => return std::option::Option::None,
|
||||
};
|
||||
let material = ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64_with_embedded_signature(
|
||||
let material = match ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64_with_embedded_signature(
|
||||
network,
|
||||
slot,
|
||||
block_time,
|
||||
@@ -187,8 +187,10 @@ fn raw_from_block_transaction(
|
||||
map_meta(transaction.meta()),
|
||||
map_version(transaction.version()),
|
||||
ksp_raw_transaction_lib::RawTransactionWireField::Value(transaction_index),
|
||||
)
|
||||
.ok()?;
|
||||
) {
|
||||
Ok(material) => material,
|
||||
Err(_) => return std::option::Option::None,
|
||||
};
|
||||
return ksp_raw_transaction_lib::canonicalize_raw_transaction(material).ok();
|
||||
}
|
||||
|
||||
@@ -202,7 +204,7 @@ fn raw_from_confirmed_transaction(
|
||||
},
|
||||
_ => return std::option::Option::None,
|
||||
};
|
||||
let material = ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64_with_embedded_signature(
|
||||
let material = match ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64_with_embedded_signature(
|
||||
network,
|
||||
transaction.slot(),
|
||||
transaction.block_time(),
|
||||
@@ -214,8 +216,10 @@ fn raw_from_confirmed_transaction(
|
||||
ksp_onchain_transport_lib::SolanaWireField::Null => ksp_raw_transaction_lib::RawTransactionWireField::Null,
|
||||
ksp_onchain_transport_lib::SolanaWireField::Value(value) => ksp_raw_transaction_lib::RawTransactionWireField::Value(*value),
|
||||
},
|
||||
)
|
||||
.ok()?;
|
||||
) {
|
||||
Ok(material) => material,
|
||||
Err(_) => return std::option::Option::None,
|
||||
};
|
||||
return ksp_raw_transaction_lib::canonicalize_raw_transaction(material).ok();
|
||||
}
|
||||
|
||||
@@ -223,19 +227,50 @@ fn raw_from_helius_full(
|
||||
network: ksp_store_lib::RawNetworkId,
|
||||
notification: &ksp_onchain_transport_lib::HeliusFullTransactionNotification,
|
||||
) -> std::option::Option<ksp_store_lib::RawTransaction> {
|
||||
let object = notification.transaction().as_object()?;
|
||||
let encoded = object.get("transaction")?.as_array()?;
|
||||
if encoded.len() != 2 || encoded.get(1)?.as_str()? != "base64" {
|
||||
let object = match notification.transaction().as_object() {
|
||||
std::option::Option::Some(object) => object,
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
};
|
||||
let encoded = match object.get("transaction") {
|
||||
std::option::Option::Some(value) => match value.as_array() {
|
||||
std::option::Option::Some(encoded) => encoded,
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
},
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
};
|
||||
if encoded.len() != 2 {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
let data = encoded.first()?.as_str()?;
|
||||
let encoding = match encoded.get(1) {
|
||||
std::option::Option::Some(value) => match value.as_str() {
|
||||
std::option::Option::Some(encoding) => encoding,
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
},
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
};
|
||||
if encoding != "base64" {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
let data = match encoded.first() {
|
||||
std::option::Option::Some(value) => match value.as_str() {
|
||||
std::option::Option::Some(data) => data,
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
},
|
||||
std::option::Option::None => return std::option::Option::None,
|
||||
};
|
||||
let meta = match object.get("meta") {
|
||||
std::option::Option::None => ksp_raw_transaction_lib::RawTransactionWireField::Omitted,
|
||||
std::option::Option::Some(serde_json::Value::Null) => ksp_raw_transaction_lib::RawTransactionWireField::Null,
|
||||
std::option::Option::Some(value) => ksp_raw_transaction_lib::RawTransactionWireField::Value(value.clone()),
|
||||
};
|
||||
let signature = ksp_raw_transaction_lib::parse_raw_transaction_signature(notification.signature()).ok()?;
|
||||
let transaction_index = u32::try_from(notification.transaction_index()).ok()?;
|
||||
let signature = match ksp_raw_transaction_lib::parse_raw_transaction_signature(notification.signature()) {
|
||||
Ok(signature) => signature,
|
||||
Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let transaction_index = match u32::try_from(notification.transaction_index()) {
|
||||
Ok(transaction_index) => transaction_index,
|
||||
Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let material = ksp_raw_transaction_lib::RawTransactionMaterial::binary_base64(
|
||||
network,
|
||||
signature,
|
||||
@@ -276,14 +311,25 @@ async fn pre_005_standard_block_subscribe_full_base64_matches_http_get_block_raw
|
||||
let address = listener.local_addr().expect("WS parity listener address must resolve");
|
||||
let ws_block = serde_json::from_str::<serde_json::Value>(HTTP_BODY).expect("HTTP parity JSON must parse")["result"].clone();
|
||||
let server = tokio::spawn(async move {
|
||||
let (stream, _) = listener.accept().await.map_err(|_| return "WS parity server accept failed".to_owned())?;
|
||||
let mut websocket = tokio_tungstenite::accept_async(stream).await.map_err(|_| return "WS parity handshake failed".to_owned())?;
|
||||
let subscribe = read_ws_request(&mut websocket).await?;
|
||||
let (stream, _) = match listener.accept().await {
|
||||
Ok(accepted) => accepted,
|
||||
Err(_) => return Err("WS parity server accept failed".to_owned()),
|
||||
};
|
||||
let mut websocket = match tokio_tungstenite::accept_async(stream).await {
|
||||
Ok(websocket) => websocket,
|
||||
Err(_) => return Err("WS parity handshake failed".to_owned()),
|
||||
};
|
||||
let subscribe = match read_ws_request(&mut websocket).await {
|
||||
Ok(subscribe) => subscribe,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
if subscribe.get("method") != std::option::Option::Some(&serde_json::json!("blockSubscribe")) {
|
||||
return Err("WS parity server received unexpected subscribe method".to_owned());
|
||||
}
|
||||
send_ws_result(&mut websocket, &subscribe, serde_json::json!(501)).await?;
|
||||
send_ws_json(
|
||||
if let Err(error) = send_ws_result(&mut websocket, &subscribe, serde_json::json!(501)).await {
|
||||
return Err(error);
|
||||
}
|
||||
if let Err(error) = send_ws_json(
|
||||
&mut websocket,
|
||||
serde_json::json!({
|
||||
"jsonrpc":"2.0",
|
||||
@@ -291,12 +337,20 @@ async fn pre_005_standard_block_subscribe_full_base64_matches_http_get_block_raw
|
||||
"params":{"subscription":501,"result":{"context":{"slot":FIXTURE_SLOT},"value":{"slot":FIXTURE_SLOT,"block":ws_block,"err":null}}}
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
let unsubscribe = read_ws_request(&mut websocket).await?;
|
||||
.await
|
||||
{
|
||||
return Err(error);
|
||||
}
|
||||
let unsubscribe = match read_ws_request(&mut websocket).await {
|
||||
Ok(unsubscribe) => unsubscribe,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
if unsubscribe.get("method") != std::option::Option::Some(&serde_json::json!("blockUnsubscribe")) {
|
||||
return Err("WS parity server received unexpected unsubscribe method".to_owned());
|
||||
}
|
||||
send_ws_result(&mut websocket, &unsubscribe, serde_json::json!(true)).await?;
|
||||
if let Err(error) = send_ws_result(&mut websocket, &unsubscribe, serde_json::json!(true)).await {
|
||||
return Err(error);
|
||||
}
|
||||
wait_for_ws_close(&mut websocket).await;
|
||||
return Ok::<(), std::string::String>(());
|
||||
});
|
||||
@@ -364,14 +418,25 @@ async fn pre_005_helius_full_base64_without_block_time_and_version_requires_http
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("Helius parity listener must bind");
|
||||
let address = listener.local_addr().expect("Helius parity listener address must resolve");
|
||||
let server = tokio::spawn(async move {
|
||||
let (stream, _) = listener.accept().await.map_err(|_| return "Helius parity server accept failed".to_owned())?;
|
||||
let mut websocket = tokio_tungstenite::accept_async(stream).await.map_err(|_| return "Helius parity handshake failed".to_owned())?;
|
||||
let subscribe = read_ws_request(&mut websocket).await?;
|
||||
let (stream, _) = match listener.accept().await {
|
||||
Ok(accepted) => accepted,
|
||||
Err(_) => return Err("Helius parity server accept failed".to_owned()),
|
||||
};
|
||||
let mut websocket = match tokio_tungstenite::accept_async(stream).await {
|
||||
Ok(websocket) => websocket,
|
||||
Err(_) => return Err("Helius parity handshake failed".to_owned()),
|
||||
};
|
||||
let subscribe = match read_ws_request(&mut websocket).await {
|
||||
Ok(subscribe) => subscribe,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
if subscribe.get("method") != std::option::Option::Some(&serde_json::json!("transactionSubscribe")) {
|
||||
return Err("Helius parity server received unexpected subscribe method".to_owned());
|
||||
}
|
||||
send_ws_result(&mut websocket, &subscribe, serde_json::json!(601)).await?;
|
||||
send_ws_json(
|
||||
if let Err(error) = send_ws_result(&mut websocket, &subscribe, serde_json::json!(601)).await {
|
||||
return Err(error);
|
||||
}
|
||||
if let Err(error) = send_ws_json(
|
||||
&mut websocket,
|
||||
serde_json::json!({
|
||||
"jsonrpc":"2.0",
|
||||
@@ -387,12 +452,20 @@ async fn pre_005_helius_full_base64_without_block_time_and_version_requires_http
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
let unsubscribe = read_ws_request(&mut websocket).await?;
|
||||
.await
|
||||
{
|
||||
return Err(error);
|
||||
}
|
||||
let unsubscribe = match read_ws_request(&mut websocket).await {
|
||||
Ok(unsubscribe) => unsubscribe,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
if unsubscribe.get("method") != std::option::Option::Some(&serde_json::json!("transactionUnsubscribe")) {
|
||||
return Err("Helius parity server received unexpected unsubscribe method".to_owned());
|
||||
}
|
||||
send_ws_result(&mut websocket, &unsubscribe, serde_json::json!(true)).await?;
|
||||
if let Err(error) = send_ws_result(&mut websocket, &unsubscribe, serde_json::json!(true)).await {
|
||||
return Err(error);
|
||||
}
|
||||
wait_for_ws_close(&mut websocket).await;
|
||||
return Ok::<(), std::string::String>(());
|
||||
});
|
||||
|
||||
101
deltas/0.3.10/pre.005-fix.002.md
Normal file
101
deltas/0.3.10/pre.005-fix.002.md
Normal file
@@ -0,0 +1,101 @@
|
||||
<!-- file: deltas/0.3.10/pre.005-fix.002.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.10-pre.005-fix.002` — conformité Clippy du canari WS + inventaire dev exact
|
||||
|
||||
## Base requise
|
||||
|
||||
```text
|
||||
0.3.10-pre.005-fix.001
|
||||
```
|
||||
|
||||
## Motif du fix
|
||||
|
||||
Le second rejeu opérateur confirme `cargo check --workspace`, la common RAW et la suite Transport complète, mais révèle deux défauts test-only :
|
||||
|
||||
```text
|
||||
25 clippy::question_mark_used dans crates/ksp-job-backfill-lib/tests/ws_raw_parity.rs
|
||||
1 canari hardening encore calibré sur dev-dependencies = {tokio}
|
||||
```
|
||||
|
||||
Le manifest `pre.005` contient légitimement `tokio-tungstenite` en dev-only pour les serveurs WebSocket déterministes ; `tests/dependency_boundary.rs` verrouille déjà qu’il ne migre pas vers les dépendances de production.
|
||||
|
||||
## Corrections
|
||||
|
||||
`ws_raw_parity.rs` remplace tous les `?` par du contrôle de flux explicite :
|
||||
|
||||
```text
|
||||
Option -> match + retour None
|
||||
Result -> match + retour Err/None
|
||||
appel async Result -> if let Err(error) + retour Err(error)
|
||||
```
|
||||
|
||||
Aucun `#[allow(...)]`, aucun changement de golden et aucune modification du scénario positif `blockSubscribe` ou négatif Helius ne sont introduits.
|
||||
|
||||
`hardening.rs` attend désormais exactement :
|
||||
|
||||
```text
|
||||
dev-dependencies = {tokio, tokio-tungstenite}
|
||||
```
|
||||
|
||||
L’inventaire des dépendances normales reste inchangé.
|
||||
|
||||
## Invariants inchangés
|
||||
|
||||
```text
|
||||
blockSubscribe standard confirmed/full/base64 legacy-v0 -> golden RAW-direct positif
|
||||
Helius transactionSubscribe full/base64 -> signal + hydration HTTP
|
||||
RAW v1 bytes/hash contracts
|
||||
TR-C2 Worker-owned
|
||||
tokio-tungstenite strictement dev-only
|
||||
aucun code de production modifié
|
||||
aucune dépendance ni feature modifiée par ce fix
|
||||
```
|
||||
|
||||
## Version technique
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.10-pre.5.fix.2
|
||||
Cargo.toml header = 498
|
||||
workspace members = 20
|
||||
```
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-job-backfill-lib/tests/hardening.rs
|
||||
crates/ksp-job-backfill-lib/tests/ws_raw_parity.rs
|
||||
docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md
|
||||
docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md
|
||||
```
|
||||
|
||||
## Fichier ajouté
|
||||
|
||||
```text
|
||||
deltas/0.3.10/pre.005-fix.002.md
|
||||
```
|
||||
|
||||
## Audits d’assemblage
|
||||
|
||||
```text
|
||||
General Rust rule audit: clean
|
||||
Rust export completeness audit: 0 candidate(s)
|
||||
KSP workspace Rust rule audit: clean
|
||||
Markdown table audit: clean (339 table(s), 761 file(s))
|
||||
```
|
||||
|
||||
## Gate opérateur à rejouer
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||
cargo test -p ksp-raw-transaction-lib
|
||||
cargo test -p ksp-onchain-transport-lib
|
||||
cargo test -p ksp-job-backfill-lib
|
||||
```
|
||||
|
||||
Les arbres Cargo n’ont pas besoin d’être rejoués pour ce fix : aucun manifest de crate, aucune dépendance et aucune feature ne change.
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/031-V0_3_10_RAW_TRANSACTION_INGEST_PLAN.md -->
|
||||
<!-- version: 8 -->
|
||||
<!-- version: 9 -->
|
||||
|
||||
# Plan v0.3.10 — RAW Transaction commune + Worker d’ingestion multi-source
|
||||
|
||||
@@ -1076,6 +1076,8 @@ Helius `transactionSubscribe` full/base64 conserve la signature, le slot, le tra
|
||||
|
||||
Le premier rejeu opérateur de `pre.005` le 2026-09-06 valide les audits statiques, `cargo check --workspace`, la common RAW, les 387 tests Transport et les arbres normal/dev/features, mais `cargo clippy --workspace --all-targets --all-features -- -D warnings` ainsi que `cargo test -p ksp-job-backfill-lib` s’arrêtent sur une unique erreur de compilation du nouveau canari : `RawTransaction::block_time().unix_millis()` retourne `u64`, tandis que `FIXTURE_BLOCK_TIME * 1_000` était inféré en `i64`. `pre.005-fix.001` corrige uniquement cette constante de comparaison en millisecondes `u64`; aucun code de production, golden, contrat RAW/WS ni dépendance n’est modifié.
|
||||
|
||||
Le second rejeu opérateur de `pre.005-fix.001` révèle ensuite deux gardes de test supplémentaires : Clippy strict refuse 25 usages de l’opérateur `?` dans les helpers/canaris de `ws_raw_parity.rs`, et le canari historique `pre_010_manifest_dependency_surface_is_exact_and_backend_neutral` attend encore uniquement `tokio` dans les dev-dependencies alors que `pre.005` a ajouté `tokio-tungstenite` en dev-only. `pre.005-fix.002` remplace les `?` par des `match`/`if let Err` explicites et recalibre uniquement le set dev attendu vers `tokio` + `tokio-tungstenite`. Aucun code de production, dépendance, feature, golden ou décision de qualification WS/Helius n’est modifié.
|
||||
|
||||
### `pre.006` — parité Yellowstone transactions/blocks
|
||||
|
||||
Fermer les adapters structurés Yellowstone transaction/block, Transaction V1, meta et cross-source parity. Aucune persistance directe si le gate byte-identical échoue.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/validation/027-V0_3_10_RAW_TRANSACTION_INGEST.md -->
|
||||
<!-- version: 8 -->
|
||||
<!-- version: 9 -->
|
||||
|
||||
# Validation v0.3.10 — RAW Transaction commune + Worker d’ingestion
|
||||
|
||||
@@ -889,3 +889,56 @@ Rust export completeness audit: 0 candidate(s)
|
||||
KSP workspace Rust rule audit: clean
|
||||
Markdown table audit: clean (339 table(s), 760 file(s))
|
||||
```
|
||||
|
||||
### 13.9 Second rejeu opérateur de `pre.005-fix.001` et fix Clippy/hardening
|
||||
|
||||
Le rejeu communiqué le 2026-09-06 confirme d’abord :
|
||||
|
||||
```text
|
||||
cargo fmt --all : exécuté
|
||||
General Rust rule audit: clean
|
||||
Rust export completeness audit: 0 candidate(s)
|
||||
KSP workspace Rust rule audit: clean
|
||||
Markdown table audit: clean (339 table(s), 760 file(s))
|
||||
cargo check --workspace : PASS
|
||||
cargo test -p ksp-raw-transaction-lib : PASS, 12 unit + 9 intégration, 0 échec
|
||||
cargo test -p ksp-onchain-transport-lib : PASS, 387 unit + 51 public API + 43 release completeness + 4 doctests, 0 échec ; smokes live ignorés comme prévu
|
||||
```
|
||||
|
||||
Deux défauts restent strictement confinés aux nouveaux canaris de `pre.005` :
|
||||
|
||||
```text
|
||||
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||
-> FAIL : 25 clippy::question_mark_used dans tests/ws_raw_parity.rs
|
||||
|
||||
cargo test -p ksp-job-backfill-lib
|
||||
-> 51 unit PASS
|
||||
-> dependency_boundary 3/3 PASS
|
||||
-> hardening 8/9 PASS
|
||||
-> FAIL : inventaire dev attendu {tokio} alors que le manifest réel contient {tokio, tokio-tungstenite}
|
||||
```
|
||||
|
||||
`pre.005-fix.002` corrige uniquement ces gardes :
|
||||
|
||||
```text
|
||||
ws_raw_parity.rs
|
||||
? sur Option/Result -> match explicite ou if let Err(...)
|
||||
aucun #[allow(...)]
|
||||
aucun changement de scénario/golden
|
||||
|
||||
hardening.rs
|
||||
dev attendu -> {tokio, tokio-tungstenite}
|
||||
inventaire normal inchangé
|
||||
```
|
||||
|
||||
Le fix ne modifie ni `crates/ksp-job-backfill-lib/Cargo.toml`, ni aucune dépendance/feature, ni le code de production. Les arbres Cargo déjà validés pour `pre.005` restent donc applicables et n’ont pas besoin d’être rejoués pour ce fix.
|
||||
|
||||
L’assemblage local de `pre.005-fix.002` obtient :
|
||||
|
||||
```text
|
||||
General Rust rule audit: clean
|
||||
Rust export completeness audit: 0 candidate(s)
|
||||
KSP workspace Rust rule audit: clean
|
||||
Markdown table audit: clean (339 table(s), 761 file(s))
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user