#![allow(unreachable_pub)]
#![allow(dead_code)]
#![allow(clippy::disallowed_types)]
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
use axum::body::Body;
use axum::http::{Method, Request, StatusCode, header};
use cratefield_core::{Clock, RandomBytes, RandomError};
use cratefield_module_wallets::{ContractSignatureVerifier, ContractVerifierError, Wallets};
use cratefield_testing::{FakeAuth, ManualClock, TestHarness};
use serde_json::{Value, json};
use time::{Duration, OffsetDateTime};
pub const DOMAIN: &str = "wallet.test";
pub const STATEMENT: &str = "Link this wallet to your account.";
pub const BASE_EPOCH: i64 = 1_800_000_000;
pub const ALICE: &str = "alice";
pub const BOB: &str = "bob";
#[derive(Clone)]
pub struct SeqRandom(Arc<AtomicUsize>);
impl SeqRandom {
#[must_use]
pub fn new() -> Self {
Self(Arc::new(AtomicUsize::new(1)))
}
}
impl Default for SeqRandom {
fn default() -> Self {
Self::new()
}
}
impl RandomBytes for SeqRandom {
fn fill(&self, dest: &mut [u8]) -> Result<(), RandomError> {
let base = self.0.fetch_add(1, Ordering::SeqCst);
for (index, byte) in dest.iter_mut().enumerate() {
*byte = u8::try_from((base.wrapping_mul(31) + index * 7 + 13) % 256)
.expect("modulo 256 fits a byte");
}
Ok(())
}
}
pub type TestClock = ManualClock;
#[must_use]
pub fn test_clock() -> TestClock {
ManualClock::new(OffsetDateTime::from_unix_timestamp(BASE_EPOCH).expect("in range"))
}
pub fn advance(clock: &TestClock, secs: u64) {
clock.advance(std::time::Duration::from_secs(secs));
}
#[must_use]
pub fn now(clock: &TestClock) -> OffsetDateTime {
clock.now()
}
#[must_use]
pub fn stamp(at: OffsetDateTime) -> String {
use time::format_description::well_known::Rfc3339;
at.replace_nanosecond(0)
.expect("truncation stays in range")
.format(&Rfc3339)
.unwrap_or_default()
}
pub struct EvmWallet {
key: k256::ecdsa::SigningKey,
address: String,
}
impl EvmWallet {
#[must_use]
pub fn from_seed(seed: u8) -> Self {
let key = k256::ecdsa::SigningKey::from_bytes(&[seed; 32].into())
.expect("32 bytes is a valid scalar");
let address = eth_address(&key);
Self { key, address }
}
#[must_use]
pub fn address(&self) -> &str {
&self.address
}
#[must_use]
pub fn sign_personal(&self, message: &str) -> String {
let (sig, recovery) = self.key.sign_prehash_recoverable(&eip191_hash(message));
let mut bytes = sig.to_bytes().to_vec();
bytes.push(u8::from(recovery) + 27);
hex::encode(bytes)
}
}
#[must_use]
pub fn eth_address(key: &k256::ecdsa::SigningKey) -> String {
let verifying = k256::ecdsa::VerifyingKey::from(key);
let point = verifying.to_sec1_point(false);
let hash = keccak256(&point.as_bytes()[1..]);
format!("0x{}", hex::encode(&hash[12..]))
}
#[must_use]
pub fn eip191_hash(message: &str) -> [u8; 32] {
let mut data = format!("\x19Ethereum Signed Message:\n{}", message.len()).into_bytes();
data.extend_from_slice(message.as_bytes());
keccak256(&data)
}
#[must_use]
pub fn raw_keccak(message: &str) -> [u8; 32] {
keccak256(message.as_bytes())
}
fn keccak256(data: &[u8]) -> [u8; 32] {
use sha3::{Digest, Keccak256};
Keccak256::digest(data).into()
}
pub struct SolanaWallet {
key: ed25519_dalek::SigningKey,
address: String,
}
impl SolanaWallet {
#[must_use]
pub fn from_seed(seed: u8) -> Self {
let key = ed25519_dalek::SigningKey::from_bytes(&[seed; 32]);
let address = bs58::encode(key.verifying_key().to_bytes()).into_string();
Self { key, address }
}
#[must_use]
pub fn address(&self) -> &str {
&self.address
}
#[must_use]
pub fn sign(&self, message: &str) -> String {
use ed25519_dalek::Signer as _;
bs58::encode(self.key.sign(message.as_bytes()).to_bytes()).into_string()
}
}
#[derive(Debug, Clone)]
pub struct VerifierCall {
pub chain_id: String,
pub address: String,
pub message_hash: [u8; 32],
pub signature: Vec<u8>,
}
#[derive(Clone)]
pub struct RecordingVerifier {
address: String,
calls: Arc<Mutex<Vec<VerifierCall>>>,
}
impl RecordingVerifier {
#[must_use]
pub fn new(address: impl Into<String>) -> Self {
Self {
address: address.into(),
calls: Arc::new(Mutex::new(Vec::new())),
}
}
#[must_use]
pub fn calls(&self) -> Vec<VerifierCall> {
self.calls
.lock()
.expect("the recorder is not poisoned")
.clone()
}
#[must_use]
pub fn only_call(&self) -> VerifierCall {
let calls = self.calls();
assert_eq!(
calls.len(),
1,
"expected exactly one verifier call: {calls:?}"
);
calls.into_iter().next().expect("checked length")
}
}
#[async_trait::async_trait]
impl ContractSignatureVerifier for RecordingVerifier {
async fn is_valid_signature(
&self,
chain_id: &str,
address: &str,
message_hash: [u8; 32],
signature: &[u8],
) -> Result<bool, ContractVerifierError> {
self.calls
.lock()
.expect("the recorder is not poisoned")
.push(VerifierCall {
chain_id: chain_id.to_owned(),
address: address.to_owned(),
message_hash,
signature: signature.to_vec(),
});
Ok(address == self.address)
}
}
struct VerifierRef(Arc<dyn ContractSignatureVerifier>);
#[async_trait::async_trait]
impl ContractSignatureVerifier for VerifierRef {
async fn is_valid_signature(
&self,
chain_id: &str,
address: &str,
message_hash: [u8; 32],
signature: &[u8],
) -> Result<bool, ContractVerifierError> {
self.0
.is_valid_signature(chain_id, address, message_hash, signature)
.await
}
}
#[derive(Debug, Clone)]
pub struct Fields {
pub domain: String,
pub uri: String,
pub version: String,
pub chain_id: String,
pub nonce: String,
pub statement: Option<String>,
pub issued_at: String,
pub expiration_time: Option<String>,
pub not_before: Option<String>,
}
impl Fields {
#[must_use]
pub fn from_nonce(body: &Value, chain_id: &str) -> Self {
Self {
domain: body["domain"].as_str().expect("domain").to_owned(),
uri: body["uri"].as_str().expect("uri").to_owned(),
version: body["version"].as_str().expect("version").to_owned(),
chain_id: chain_id.to_owned(),
nonce: body["nonce"].as_str().expect("nonce").to_owned(),
statement: body["statement"].as_str().map(str::to_owned),
issued_at: body["issued_at"].as_str().expect("issued_at").to_owned(),
expiration_time: body["expiration_time"].as_str().map(str::to_owned),
not_before: None,
}
}
#[must_use]
pub fn siwe(&self, address: &str) -> String {
let mut message = format!(
"{} wants you to sign in with your Ethereum account:\n{address}\n\n",
self.domain
);
if let Some(statement) = &self.statement {
message.push_str(statement);
message.push('\n');
}
message.push_str(&self.tail());
message
}
#[must_use]
pub fn siws(&self, address: &str) -> String {
let mut message = format!(
"{} wants you to sign in with your Solana account:\n{address}\n\n",
self.domain
);
if let Some(statement) = &self.statement {
message.push_str(statement);
message.push('\n');
}
message.push_str(&self.tail());
message
}
fn tail(&self) -> String {
let mut out = format!(
"URI: {}\nVersion: {}\nChain ID: {}\nNonce: {}\nIssued At: {}",
self.uri, self.version, self.chain_id, self.nonce, self.issued_at
);
if let Some(expiry) = &self.expiration_time {
out.push_str("\nExpiration Time: ");
out.push_str(expiry);
}
if let Some(not_before) = &self.not_before {
out.push_str("\nNot Before: ");
out.push_str(not_before);
}
out
}
#[must_use]
pub fn with_domain(mut self, domain: &str) -> Self {
domain.clone_into(&mut self.domain);
self
}
#[must_use]
pub fn with_chain_id(mut self, chain_id: &str) -> Self {
chain_id.clone_into(&mut self.chain_id);
self
}
#[must_use]
pub fn expired_at(mut self, at: OffsetDateTime) -> Self {
self.expiration_time = Some(stamp(at));
self
}
#[must_use]
pub fn valid_from(mut self, at: OffsetDateTime) -> Self {
self.not_before = Some(stamp(at));
self
}
}
#[derive(Default, Clone)]
pub struct Spec {
pub domain: String,
pub chain_id: String,
pub nonce_ttl: Option<Duration>,
pub contract_verifier: Option<Arc<dyn ContractSignatureVerifier>>,
pub anonymous: bool,
}
impl Spec {
#[must_use]
pub fn new() -> Self {
Self {
domain: DOMAIN.to_owned(),
chain_id: "1".to_owned(),
nonce_ttl: Some(Duration::seconds(600)),
contract_verifier: None,
anonymous: false,
}
}
}
#[must_use]
pub fn kit(spec: &Spec, clock: &TestClock) -> TestHarness {
let mut builder = Wallets::builder()
.domain(&spec.domain)
.statement(STATEMENT)
.chain_id(&spec.chain_id)
.random(SeqRandom::new());
if let Some(ttl) = spec.nonce_ttl {
builder = builder.nonce_ttl(ttl);
}
if let Some(verifier) = &spec.contract_verifier {
builder = builder.contract_verifier(VerifierRef(Arc::clone(verifier)));
}
let clock = clock.clone();
let anonymous = spec.anonymous;
TestHarness::with_ports(vec![Box::new(builder.build())], move |ports| {
ports.auth = Some(Arc::new(if anonymous {
FakeAuth::new(cratefield_testing::AuthMode::Anonymous)
} else {
FakeAuth::subjects()
}));
ports.clock = Some(Arc::new(clock.clone()) as Arc<dyn Clock>);
})
}
#[must_use]
pub fn fixture() -> (TestHarness, TestClock) {
let clock = test_clock();
(kit(&Spec::new(), &clock), clock)
}
pub struct Res {
pub status: StatusCode,
pub body: Vec<u8>,
}
impl Res {
#[must_use]
pub fn json(&self) -> Value {
serde_json::from_slice(&self.body).unwrap_or_else(|err| {
panic!(
"body is not JSON ({err}): {}",
String::from_utf8_lossy(&self.body)
)
})
}
#[must_use]
pub fn problem(&self) -> String {
self.json()["type"]
.as_str()
.unwrap_or_else(|| panic!("no problem type: {}", self.text()))
.to_owned()
}
#[must_use]
pub fn text(&self) -> String {
String::from_utf8_lossy(&self.body).into_owned()
}
}
pub async fn send_as(
kit: &TestHarness,
subject: &str,
method: Method,
path: &str,
body: Option<&Value>,
) -> Res {
let mut builder = Request::builder()
.method(method)
.uri(path)
.header(header::AUTHORIZATION, format!("Bearer {subject}"));
let body = match body {
Some(body) => {
builder = builder.header(header::CONTENT_TYPE, "application/json");
Body::from(body.to_string())
}
None => Body::empty(),
};
let request = builder.body(body).expect("request builds");
dispatch(kit, request).await
}
pub async fn send_anonymous(
kit: &TestHarness,
method: Method,
path: &str,
body: Option<&Value>,
) -> Res {
let mut builder = Request::builder().method(method).uri(path);
let body = match body {
Some(body) => {
builder = builder.header(header::CONTENT_TYPE, "application/json");
Body::from(body.to_string())
}
None => Body::empty(),
};
let request = builder.body(body).expect("request builds");
dispatch(kit, request).await
}
async fn dispatch(kit: &TestHarness, request: Request<Body>) -> Res {
use tower::ServiceExt;
let response = kit
.router
.clone()
.oneshot(request)
.await
.expect("router answers");
let (parts, body) = response.into_parts();
let bytes = axum::body::to_bytes(body, 1024 * 1024)
.await
.expect("body reads");
Res {
status: parts.status,
body: bytes.to_vec(),
}
}
pub async fn nonce(kit: &TestHarness, subject: &str, chain: &str) -> Fields {
let response = send_as(
kit,
subject,
Method::POST,
"/v1/wallets/nonce",
Some(&json!({ "chain": chain })),
)
.await;
assert_eq!(
response.status,
StatusCode::OK,
"nonce: {}",
response.text()
);
let body = response.json();
Fields::from_nonce(&body, body["chain_id"].as_str().expect("chain_id"))
}
#[must_use]
pub fn verify_body(chain: &str, message: &str, signature: &str) -> Value {
json!({ "chain": chain, "message": message, "signature": signature })
}
pub async fn verify(kit: &TestHarness, subject: &str, body: &Value) -> Res {
send_as(kit, subject, Method::POST, "/v1/wallets/verify", Some(body)).await
}
pub async fn verify_concurrently(
kit: &TestHarness,
subject: &str,
body: &Value,
n: usize,
) -> Vec<Res> {
let calls = (0..n).map(|_| verify(kit, subject, body));
futures_util::future::join_all(calls).await
}
pub async fn list(kit: &TestHarness, subject: &str) -> Res {
send_as(kit, subject, Method::GET, "/v1/wallets", None).await
}
pub async fn unlink(kit: &TestHarness, subject: &str, id: &str) -> Res {
send_as(
kit,
subject,
Method::DELETE,
&format!("/v1/wallets/{id}"),
None,
)
.await
}
#[must_use]
pub fn links(body: &Value) -> Vec<(String, String, String)> {
body.as_array()
.expect("the list is an array")
.iter()
.map(|row| {
(
row["id"].as_str().expect("id").to_owned(),
row["chain"].as_str().expect("chain").to_owned(),
row["address"].as_str().expect("address").to_owned(),
)
})
.collect()
}